From d6015a205cdcafd43b69019cfe968dff02a0b975 Mon Sep 17 00:00:00 2001 From: Alexander Karan Date: Thu, 26 Mar 2026 20:57:15 +0800 Subject: [PATCH 1/6] SPA Test --- .github/frameworks.json | 14 +- .github/workflows/generate-stats.yml | 3 + .github/workflows/measure-framework.yml | 54 + Dockerfile | 7 + packages/app-astro/astro.config.mjs | 2 + packages/app-astro/package.json | 8 +- packages/app-astro/pnpm-lock.yaml | 435 ++++ packages/app-astro/serve.mjs | 53 + packages/app-astro/spa-stats.json | 9 + packages/app-astro/src/components/SpaPage.tsx | 35 + .../app-astro/src/components/SpaResult.tsx | 33 + packages/app-astro/src/pages/spa.astro | 16 + packages/app-astro/src/pages/spa/result.astro | 16 + packages/app-next-js/app/spa/page.tsx | 21 + packages/app-next-js/app/spa/result/page.tsx | 35 + packages/app-next-js/package.json | 3 +- packages/app-next-js/spa-stats.json | 9 + packages/app-nuxt/app/pages/spa/index.vue | 9 + packages/app-nuxt/app/pages/spa/result.vue | 21 + packages/app-nuxt/package.json | 1 + packages/app-nuxt/serve.mjs | 36 + packages/app-nuxt/spa-stats.json | 9 + packages/app-react-router/app/routes.ts | 8 +- .../app/routes/spa.result.tsx | 27 + packages/app-react-router/app/routes/spa.tsx | 11 + packages/app-react-router/package.json | 4 +- packages/app-react-router/pnpm-lock.yaml | 707 ++++++- packages/app-react-router/spa-stats.json | 9 + packages/app-solid-start/package.json | 3 +- packages/app-solid-start/serve.mjs | 36 + packages/app-solid-start/spa-stats.json | 9 + .../app-solid-start/src/routes/spa/index.tsx | 13 + .../app-solid-start/src/routes/spa/result.tsx | 31 + packages/app-sveltekit/package.json | 1 + packages/app-sveltekit/spa-stats.json | 9 + .../app-sveltekit/src/routes/spa/+page.svelte | 7 + .../app-sveltekit/src/routes/spa/+page.ts | 1 + .../src/routes/spa/result/+page.svelte | 19 + .../src/routes/spa/result/+page.ts | 1 + .../app-tanstack-start-react/package.json | 3 +- packages/app-tanstack-start-react/serve.mjs | 37 + .../app-tanstack-start-react/spa-stats.json | 9 + .../src/routeTree.gen.ts | 42 +- .../src/routes/spa.tsx | 26 + .../src/routes/spa_.result.tsx | 38 + packages/docs/src/components/SPACharts.astro | 15 + .../docs/src/components/SPAFCPChart.astro | 10 + .../docs/src/components/SPAINPChart.astro | 10 + packages/docs/src/content.config.ts | 5 + .../docs/src/content/runtime/app-astro.json | 6 +- .../docs/src/content/runtime/app-next-js.json | 6 +- .../docs/src/content/runtime/app-nuxt.json | 6 +- .../src/content/runtime/app-react-router.json | 6 +- .../src/content/runtime/app-solid-start.json | 6 +- .../src/content/runtime/app-sveltekit.json | 6 +- .../runtime/app-tanstack-start-react.json | 6 +- packages/docs/src/lib/collections.ts | 12 + packages/docs/src/pages/index.astro | 3 + packages/stats-generator/package.json | 4 +- packages/stats-generator/src/collect-stats.ts | 13 + .../stats-generator/src/run-spa-benchmark.ts | 72 + packages/stats-generator/src/save-ci-stats.ts | 22 + packages/stats-generator/src/spa/index.ts | 123 ++ .../stats-generator/src/spa/run-benchmark.ts | 128 ++ packages/stats-generator/src/spa/types.ts | 25 + .../src/ssr/handlers/nextjs.ts | 15 +- packages/stats-generator/src/types.ts | 6 + pnpm-lock.yaml | 1747 +++++++++++++++-- 68 files changed, 3932 insertions(+), 200 deletions(-) create mode 100644 packages/app-astro/serve.mjs create mode 100644 packages/app-astro/spa-stats.json create mode 100644 packages/app-astro/src/components/SpaPage.tsx create mode 100644 packages/app-astro/src/components/SpaResult.tsx create mode 100644 packages/app-astro/src/pages/spa.astro create mode 100644 packages/app-astro/src/pages/spa/result.astro create mode 100644 packages/app-next-js/app/spa/page.tsx create mode 100644 packages/app-next-js/app/spa/result/page.tsx create mode 100644 packages/app-next-js/spa-stats.json create mode 100644 packages/app-nuxt/app/pages/spa/index.vue create mode 100644 packages/app-nuxt/app/pages/spa/result.vue create mode 100644 packages/app-nuxt/serve.mjs create mode 100644 packages/app-nuxt/spa-stats.json create mode 100644 packages/app-react-router/app/routes/spa.result.tsx create mode 100644 packages/app-react-router/app/routes/spa.tsx create mode 100644 packages/app-react-router/spa-stats.json create mode 100644 packages/app-solid-start/serve.mjs create mode 100644 packages/app-solid-start/spa-stats.json create mode 100644 packages/app-solid-start/src/routes/spa/index.tsx create mode 100644 packages/app-solid-start/src/routes/spa/result.tsx create mode 100644 packages/app-sveltekit/spa-stats.json create mode 100644 packages/app-sveltekit/src/routes/spa/+page.svelte create mode 100644 packages/app-sveltekit/src/routes/spa/+page.ts create mode 100644 packages/app-sveltekit/src/routes/spa/result/+page.svelte create mode 100644 packages/app-sveltekit/src/routes/spa/result/+page.ts create mode 100644 packages/app-tanstack-start-react/serve.mjs create mode 100644 packages/app-tanstack-start-react/spa-stats.json create mode 100644 packages/app-tanstack-start-react/src/routes/spa.tsx create mode 100644 packages/app-tanstack-start-react/src/routes/spa_.result.tsx create mode 100644 packages/docs/src/components/SPACharts.astro create mode 100644 packages/docs/src/components/SPAFCPChart.astro create mode 100644 packages/docs/src/components/SPAINPChart.astro create mode 100644 packages/stats-generator/src/run-spa-benchmark.ts create mode 100644 packages/stats-generator/src/spa/index.ts create mode 100644 packages/stats-generator/src/spa/run-benchmark.ts create mode 100644 packages/stats-generator/src/spa/types.ts diff --git a/.github/frameworks.json b/.github/frameworks.json index adb013f..7875700 100644 --- a/.github/frameworks.json +++ b/.github/frameworks.json @@ -30,7 +30,7 @@ "package": "app-astro", "buildScript": "build", "buildOutputDir": "dist", - "measurements": [{ "type": "ssr" }] + "measurements": [{ "type": "ssr" }, { "type": "spa" }] } }, { @@ -74,7 +74,7 @@ "package": "app-next-js", "buildScript": "build", "buildOutputDir": ".next", - "measurements": [{ "type": "ssr" }] + "measurements": [{ "type": "ssr" }, { "type": "spa" }] } }, { @@ -96,7 +96,7 @@ "package": "app-nuxt", "buildScript": "build", "buildOutputDir": ".output", - "measurements": [{ "type": "ssr" }] + "measurements": [{ "type": "ssr" }, { "type": "spa" }] } }, { @@ -118,7 +118,7 @@ "package": "app-react-router", "buildScript": "build", "buildOutputDir": "build", - "measurements": [{ "type": "ssr" }] + "measurements": [{ "type": "ssr" }, { "type": "spa" }] } }, { @@ -140,7 +140,7 @@ "package": "app-solid-start", "buildScript": "build", "buildOutputDir": ".output", - "measurements": [{ "type": "ssr" }] + "measurements": [{ "type": "ssr" }, { "type": "spa" }] } }, { @@ -162,7 +162,7 @@ "package": "app-sveltekit", "buildScript": "build", "buildOutputDir": "build", - "measurements": [{ "type": "ssr" }] + "measurements": [{ "type": "ssr" }, { "type": "spa" }] } }, { @@ -184,7 +184,7 @@ "package": "app-tanstack-start-react", "buildScript": "build", "buildOutputDir": ".output", - "measurements": [{ "type": "ssr" }] + "measurements": [{ "type": "ssr" }, { "type": "spa" }] } } ] diff --git a/.github/workflows/generate-stats.yml b/.github/workflows/generate-stats.yml index de63ed0..553b5ed 100644 --- a/.github/workflows/generate-stats.yml +++ b/.github/workflows/generate-stats.yml @@ -26,6 +26,7 @@ jobs: build-matrix: ${{ steps.set-matrix.outputs.build }} ssr-matrix: ${{ steps.set-matrix.outputs.ssr }} deps-matrix: ${{ steps.set-matrix.outputs.deps }} + spa-matrix: ${{ steps.set-matrix.outputs.spa }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -38,6 +39,7 @@ jobs: echo "build=$(echo "$FRAMEWORKS" | jq -c '[.[] | select(.starter) | select(.starter.measurements | map(.type) | contains(["build"])) | {name, displayName, package: .starter.package, buildScript: .starter.buildScript, buildOutputDir: .starter.buildOutputDir, measurements: .starter.measurements}]')" >> $GITHUB_OUTPUT echo "ssr=$(echo "$FRAMEWORKS" | jq -c '[.[] | select(.app) | select(.app.measurements | map(.type) | contains(["ssr"])) | {name, displayName, package: .app.package, buildScript: .app.buildScript, buildOutputDir: .app.buildOutputDir, measurements: .app.measurements}]')" >> $GITHUB_OUTPUT echo "deps=$(echo "$FRAMEWORKS" | jq -c '[.[] | select(.starter) | select(.starter.measurements | map(.type) | contains(["dependencies"])) | {name, displayName, package: .starter.package}]')" >> $GITHUB_OUTPUT + echo "spa=$(echo "$FRAMEWORKS" | jq -c '[.[] | select(.app) | select(.app.measurements | map(.type) | contains(["spa"])) | {name, displayName, package: .app.package, buildScript: .app.buildScript, buildOutputDir: .app.buildOutputDir, measurements: .app.measurements}]')" >> $GITHUB_OUTPUT measure: needs: setup @@ -47,6 +49,7 @@ jobs: build-matrix: ${{ needs.setup.outputs.build-matrix }} ssr-matrix: ${{ needs.setup.outputs.ssr-matrix }} deps-matrix: ${{ needs.setup.outputs.deps-matrix }} + spa-matrix: ${{ needs.setup.outputs.spa-matrix }} generate-stats: needs: [setup, measure] diff --git a/.github/workflows/measure-framework.yml b/.github/workflows/measure-framework.yml index 484d7f6..23ee7dd 100644 --- a/.github/workflows/measure-framework.yml +++ b/.github/workflows/measure-framework.yml @@ -20,6 +20,11 @@ on: type: string required: false default: '[]' + spa-matrix: + description: 'JSON array of frameworks to measure SPA paint and interaction performance' + type: string + required: false + default: '[]' jobs: measure-install: @@ -176,3 +181,52 @@ jobs: path: packages/${{ matrix.framework.package }}/e18e-stats.json retention-days: 1 if-no-files-found: error + + measure-spa: + if: inputs.spa-matrix != '[]' + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.49.0-noble + options: --ipc=host + strategy: + fail-fast: false + matrix: + framework: ${{ fromJson(inputs.spa-matrix) }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'pnpm' + + - name: Install workspace dependencies + run: pnpm install --frozen-lockfile + + - name: Install package dependencies + working-directory: ./packages/${{ matrix.framework.package }} + run: pnpm install --frozen-lockfile --ignore-workspace + + - name: Build app + working-directory: ./packages/${{ matrix.framework.package }} + run: pnpm build + + - name: Run SPA benchmark + run: pnpm --filter @framework-tracker/stats-generator run:spa ${{ matrix.framework.package }} + env: + PLAYWRIGHT_BROWSERS_PATH: /ms-playwright + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' + RUNNER_LABEL: ubuntu-latest + + - name: Upload SPA stats + uses: actions/upload-artifact@v4 + with: + name: spa-stats-${{ matrix.framework.name }} + path: packages/${{ matrix.framework.package }}/spa-stats.json + retention-days: 1 + if-no-files-found: error diff --git a/Dockerfile b/Dockerfile index 31b59fc..53b6824 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,3 +21,10 @@ CMD [ "node", "src/lcp/index.ts" ] # LCP Stats FROM cwv-stats-base AS cwv-stats-lcp CMD [ "node", "src/lcp/index.ts" ] + +FROM mcr.microsoft.com/playwright:v1.49.0-noble AS spa-benchmark +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN npm install -g pnpm +ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 diff --git a/packages/app-astro/astro.config.mjs b/packages/app-astro/astro.config.mjs index 5fb5828..cf4375d 100644 --- a/packages/app-astro/astro.config.mjs +++ b/packages/app-astro/astro.config.mjs @@ -1,9 +1,11 @@ import { defineConfig } from 'astro/config' import node from '@astrojs/node' +import react from '@astrojs/react' export default defineConfig({ output: 'server', adapter: node({ mode: 'middleware', }), + integrations: [react()], }) diff --git a/packages/app-astro/package.json b/packages/app-astro/package.json index 476a58c..a4d65c0 100644 --- a/packages/app-astro/package.json +++ b/packages/app-astro/package.json @@ -10,11 +10,15 @@ "astro": "astro", "lint": "eslint .", "lint:fix": "eslint . --fix", - "type-check": "astro check" + "type-check": "astro check", + "serve": "node serve.mjs" }, "dependencies": { "@astrojs/node": "9.5.2", - "astro": "5.16.15" + "@astrojs/react": "4.3.0", + "astro": "5.16.15", + "react": "19.1.0", + "react-dom": "19.1.0" }, "devDependencies": { "@astrojs/check": "^0.9.6", diff --git a/packages/app-astro/pnpm-lock.yaml b/packages/app-astro/pnpm-lock.yaml index cdc22a2..f406bc7 100644 --- a/packages/app-astro/pnpm-lock.yaml +++ b/packages/app-astro/pnpm-lock.yaml @@ -11,9 +11,18 @@ importers: '@astrojs/node': specifier: 9.5.2 version: 9.5.2(astro@5.16.15(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2)) + '@astrojs/react': + specifier: 4.3.0 + version: 4.3.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yaml@2.8.2) astro: specifier: 5.16.15 version: 5.16.15(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2) + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) devDependencies: '@astrojs/check': specifier: ^0.9.6 @@ -60,6 +69,15 @@ packages: resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + '@astrojs/react@4.3.0': + resolution: {integrity: sha512-N02aj52Iezn69qHyx5+XvPqgsPMEnel9mI5JMbGiRMTzzLMuNaxRVoQTaq2024Dpr7BLsxCjqMkNvelqMDhaHA==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + peerDependencies: + '@types/react': ^17.0.50 || ^18.0.21 || ^19.0.0 + '@types/react-dom': ^17.0.17 || ^18.0.6 || ^19.0.0 + react: ^17.0.2 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.2 || ^18.0.0 || ^19.0.0 + '@astrojs/telemetry@3.3.0': resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} @@ -67,6 +85,44 @@ packages: '@astrojs/yaml2ts@0.2.2': resolution: {integrity: sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ==} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -75,11 +131,39 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.0': resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -405,12 +489,28 @@ packages: cpu: [x64] os: [win32] + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -566,6 +666,18 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -584,12 +696,26 @@ packages: '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@volar/kit@2.4.28': resolution: {integrity: sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg==} peerDependencies: @@ -680,6 +806,11 @@ packages: base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + baseline-browser-mapping@2.10.10: + resolution: {integrity: sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==} + engines: {node: '>=6.0.0'} + hasBin: true + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -687,10 +818,18 @@ packages: resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} engines: {node: '>=18'} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + camelcase@8.0.0: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} + caniuse-lite@1.0.30001780: + resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -748,6 +887,9 @@ packages: common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} @@ -782,6 +924,9 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -849,6 +994,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.321: + resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==} + emmet@2.4.11: resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} @@ -940,6 +1088,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1028,13 +1180,26 @@ packages: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonc-parser@2.3.1: resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} @@ -1059,6 +1224,9 @@ packages: resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1233,6 +1401,9 @@ packages: node-mock-http@1.0.4: resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -1321,6 +1492,19 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -1401,6 +1585,13 @@ packages: resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} engines: {node: '>=11.0.0'} + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -1618,6 +1809,12 @@ packages: uploadthing: optional: true + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -1793,6 +1990,9 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml-language-server@1.19.2: resolution: {integrity: sha512-9F3myNmJzUN/679jycdMxqtydPSDRAarSj3wPiF7pchEPnO9Dg07Oc+gIYLqXR4L+g+FSEVXXv2+mr54StLFOg==} hasBin: true @@ -1925,6 +2125,29 @@ snapshots: dependencies: prismjs: 1.30.0 + '@astrojs/react@4.3.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yaml@2.8.2)': + dependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(yaml@2.8.2)) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + ultrahtml: 1.6.0 + vite: 6.4.1(yaml@2.8.2) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + '@astrojs/telemetry@3.3.0': dependencies: ci-info: 4.4.0 @@ -1941,14 +2164,113 @@ snapshots: dependencies: yaml: 2.8.2 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@babel/parser@7.29.0': dependencies: '@babel/types': 7.29.0 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -2161,10 +2483,29 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@oslojs/encoding@1.1.0': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/pluginutils@5.3.0(rollup@4.57.1)': dependencies: '@types/estree': 1.0.8 @@ -2281,6 +2622,27 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -2301,10 +2663,30 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + '@types/unist@3.0.3': {} '@ungap/structured-clone@1.3.0': {} + '@vitejs/plugin-react@4.7.0(vite@6.4.1(yaml@2.8.2))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.1(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + '@volar/kit@2.4.28(typescript@5.9.3)': dependencies: '@volar/language-service': 2.4.28 @@ -2501,6 +2883,8 @@ snapshots: base-64@1.0.0: {} + baseline-browser-mapping@2.10.10: {} + boolbase@1.0.0: {} boxen@8.0.1: @@ -2514,8 +2898,18 @@ snapshots: widest-line: 5.0.0 wrap-ansi: 9.0.2 + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.10 + caniuse-lite: 1.0.30001780 + electron-to-chromium: 1.5.321 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + camelcase@8.0.0: {} + caniuse-lite@1.0.30001780: {} + ccount@2.0.1: {} chalk@5.6.2: {} @@ -2558,6 +2952,8 @@ snapshots: common-ancestor-path@1.0.1: {} + convert-source-map@2.0.0: {} + cookie-es@1.2.2: {} cookie@1.1.1: {} @@ -2592,6 +2988,8 @@ snapshots: dependencies: css-tree: 2.2.1 + csstype@3.2.3: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -2647,6 +3045,8 @@ snapshots: ee-first@1.1.1: {} + electron-to-chromium@1.5.321: {} + emmet@2.4.11: dependencies: '@emmetio/abbreviation': 2.3.3 @@ -2734,6 +3134,8 @@ snapshots: fsevents@2.3.3: optional: true + gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} get-east-asian-width@1.4.0: {} @@ -2873,12 +3275,18 @@ snapshots: dependencies: is-inside-container: 1.0.0 + js-tokens@4.0.0: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 + jsesc@3.1.0: {} + json-schema-traverse@1.0.0: {} + json5@2.2.3: {} + jsonc-parser@2.3.1: {} jsonc-parser@3.3.1: {} @@ -2893,6 +3301,10 @@ snapshots: lru-cache@11.2.6: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3244,6 +3656,8 @@ snapshots: node-mock-http@1.0.4: {} + node-releases@2.0.36: {} + normalize-path@3.0.0: {} nth-check@2.1.1: @@ -3327,6 +3741,15 @@ snapshots: range-parser@1.2.1: {} + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + + react-refresh@0.17.0: {} + + react@19.1.0: {} + readdirp@4.1.2: {} readdirp@5.0.0: {} @@ -3473,6 +3896,10 @@ snapshots: sax@1.4.4: {} + scheduler@0.26.0: {} + + semver@6.3.1: {} + semver@7.7.4: {} send@1.2.1: @@ -3690,6 +4117,12 @@ snapshots: ofetch: 1.5.1 ufo: 1.6.3 + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -3842,6 +4275,8 @@ snapshots: y18n@5.0.8: {} + yallist@3.1.1: {} + yaml-language-server@1.19.2: dependencies: '@vscode/l10n': 0.0.18 diff --git a/packages/app-astro/serve.mjs b/packages/app-astro/serve.mjs new file mode 100644 index 0000000..6c83d6d --- /dev/null +++ b/packages/app-astro/serve.mjs @@ -0,0 +1,53 @@ +/** + * Production server for SPA benchmarking. + * Serves static assets from dist/client/ and passes all other + * requests through the Astro middleware handler. + */ +import { createServer } from 'node:http' +import { createReadStream, existsSync, statSync } from 'node:fs' +import { join, extname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const PORT = parseInt(process.env.PORT ?? '4321', 10) +const clientDir = join(__dirname, 'dist', 'client') + +const { handler } = await import('./dist/server/entry.mjs') + +const MIME_TYPES = { + '.html': 'text/html; charset=utf-8', + '.js': 'application/javascript', + '.mjs': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.woff': 'font/woff', + '.woff2': 'font/woff2', +} + +createServer((req, res) => { + const urlPath = (req.url ?? '/').split('?')[0] + const filePath = join(clientDir, urlPath) + + try { + if (existsSync(filePath) && statSync(filePath).isFile()) { + const contentType = + MIME_TYPES[extname(filePath)] ?? 'application/octet-stream' + res.setHeader('Content-Type', contentType) + createReadStream(filePath).pipe(res) + return + } + } catch { + // not a static file, fall through + } + + handler(req, res, () => { + res.writeHead(404) + res.end('Not Found') + }) +}).listen(PORT, () => { + console.log(`Ready at http://localhost:${PORT}`) +}) diff --git a/packages/app-astro/spa-stats.json b/packages/app-astro/spa-stats.json new file mode 100644 index 0000000..49e4b8b --- /dev/null +++ b/packages/app-astro/spa-stats.json @@ -0,0 +1,9 @@ +{ + "timingMeasuredAt": "2026-03-22T07:44:06.598Z", + "runner": "local", + "frameworkVersion": "5.16.15", + "spaFirstPaintMs": 36.8, + "spaFCPMs": 36.8, + "spaINPMs": 98.72, + "spaRuns": 5 +} diff --git a/packages/app-astro/src/components/SpaPage.tsx b/packages/app-astro/src/components/SpaPage.tsx new file mode 100644 index 0000000..7c845ed --- /dev/null +++ b/packages/app-astro/src/components/SpaPage.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' + +type Entry = { id: string; name: string } + +function generateData(): Entry[] { + return Array.from({ length: 1000 }, () => ({ + id: crypto.randomUUID(), + name: crypto.randomUUID(), + })) +} + +export default function SpaPage() { + const [entries, setEntries] = useState(null) + + if (entries !== null) { + return ( + + + {entries.map((entry) => ( + + + + + ))} + +
{entry.id}{entry.name}
+ ) + } + + return ( + + ) +} diff --git a/packages/app-astro/src/components/SpaResult.tsx b/packages/app-astro/src/components/SpaResult.tsx new file mode 100644 index 0000000..c6f212f --- /dev/null +++ b/packages/app-astro/src/components/SpaResult.tsx @@ -0,0 +1,33 @@ +import { useState, useEffect } from 'react' + +type Entry = { id: string; name: string } + +function generateData(): Entry[] { + return Array.from({ length: 1000 }, () => ({ + id: crypto.randomUUID(), + name: crypto.randomUUID(), + })) +} + +export default function SpaResult() { + const [entries, setEntries] = useState(null) + + useEffect(() => { + setEntries(generateData()) + }, []) + + if (entries === null) return null + + return ( + + + {entries.map((entry) => ( + + + + + ))} + +
{entry.id}{entry.name}
+ ) +} diff --git a/packages/app-astro/src/pages/spa.astro b/packages/app-astro/src/pages/spa.astro new file mode 100644 index 0000000..b1673fc --- /dev/null +++ b/packages/app-astro/src/pages/spa.astro @@ -0,0 +1,16 @@ +--- +import { ViewTransitions } from 'astro:transitions' +import SpaPage from '../components/SpaPage' +--- + + + + + + Astro SPA Benchmark + + + + + + diff --git a/packages/app-astro/src/pages/spa/result.astro b/packages/app-astro/src/pages/spa/result.astro new file mode 100644 index 0000000..d1b86e2 --- /dev/null +++ b/packages/app-astro/src/pages/spa/result.astro @@ -0,0 +1,16 @@ +--- +import { ViewTransitions } from 'astro:transitions' +import SpaResult from '../../components/SpaResult' +--- + + + + + + Astro SPA Benchmark + + + + + + diff --git a/packages/app-next-js/app/spa/page.tsx b/packages/app-next-js/app/spa/page.tsx new file mode 100644 index 0000000..e20eb9e --- /dev/null +++ b/packages/app-next-js/app/spa/page.tsx @@ -0,0 +1,21 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' + +export default function SpaPage() { + const [mounted, setMounted] = useState(false) + const router = useRouter() + + // 'use client' components are still SSR'd in Next.js — useEffect ensures the + // button only renders client-side so FCP measures CSR, not server-rendered HTML + useEffect(() => setMounted(true), []) + + if (!mounted) return null + + return ( + + ) +} diff --git a/packages/app-next-js/app/spa/result/page.tsx b/packages/app-next-js/app/spa/result/page.tsx new file mode 100644 index 0000000..ad30f62 --- /dev/null +++ b/packages/app-next-js/app/spa/result/page.tsx @@ -0,0 +1,35 @@ +'use client' + +import { useState, useEffect } from 'react' + +type Entry = { id: string; name: string } + +function generateData(): Entry[] { + return Array.from({ length: 1000 }, () => ({ + id: crypto.randomUUID(), + name: crypto.randomUUID(), + })) +} + +export default function SpaResultPage() { + const [entries, setEntries] = useState(null) + + useEffect(() => { + setEntries(generateData()) + }, []) + + if (entries === null) return null + + return ( + + + {entries.map((entry) => ( + + + + + ))} + +
{entry.id}{entry.name}
+ ) +} diff --git a/packages/app-next-js/package.json b/packages/app-next-js/package.json index 104c7e1..732aaf0 100644 --- a/packages/app-next-js/package.json +++ b/packages/app-next-js/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start" + "start": "next start", + "serve": "next start" }, "dependencies": { "next": "16.1.1", diff --git a/packages/app-next-js/spa-stats.json b/packages/app-next-js/spa-stats.json new file mode 100644 index 0000000..10e7c41 --- /dev/null +++ b/packages/app-next-js/spa-stats.json @@ -0,0 +1,9 @@ +{ + "timingMeasuredAt": "2026-03-22T07:44:20.140Z", + "runner": "local", + "frameworkVersion": "16.1.1", + "spaFirstPaintMs": 48, + "spaFCPMs": 48, + "spaINPMs": 75.48, + "spaRuns": 5 +} diff --git a/packages/app-nuxt/app/pages/spa/index.vue b/packages/app-nuxt/app/pages/spa/index.vue new file mode 100644 index 0000000..8da81ed --- /dev/null +++ b/packages/app-nuxt/app/pages/spa/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/packages/app-nuxt/app/pages/spa/result.vue b/packages/app-nuxt/app/pages/spa/result.vue new file mode 100644 index 0000000..79b9de7 --- /dev/null +++ b/packages/app-nuxt/app/pages/spa/result.vue @@ -0,0 +1,21 @@ + + + diff --git a/packages/app-nuxt/package.json b/packages/app-nuxt/package.json index 2eca93c..6f5fabc 100644 --- a/packages/app-nuxt/package.json +++ b/packages/app-nuxt/package.json @@ -7,6 +7,7 @@ "dev": "nuxt dev", "generate": "nuxt generate", "preview": "nuxt preview", + "serve": "node serve.mjs", "postinstall": "nuxt prepare", "lint": "eslint .", "lint:fix": "eslint . --fix", diff --git a/packages/app-nuxt/serve.mjs b/packages/app-nuxt/serve.mjs new file mode 100644 index 0000000..d48c32b --- /dev/null +++ b/packages/app-nuxt/serve.mjs @@ -0,0 +1,36 @@ +import { createServer } from 'node:http' +import { createReadStream, existsSync, statSync } from 'node:fs' +import { join, extname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { listener } from './.output/server/index.mjs' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const PORT = parseInt(process.env.PORT ?? '3000', 10) +const publicDir = join(__dirname, '.output', 'public') + +const MIME = { + '.js': 'application/javascript', + '.css': 'text/css', + '.html': 'text/html', + '.json': 'application/json', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.woff2': 'font/woff2', +} + +createServer((req, res) => { + const url = new URL(req.url, `http://localhost:${PORT}`) + const filePath = join(publicDir, url.pathname) + + if (existsSync(filePath) && statSync(filePath).isFile()) { + const ext = extname(filePath) + res.setHeader('Content-Type', MIME[ext] ?? 'application/octet-stream') + createReadStream(filePath).pipe(res) + return + } + + listener(req, res) +}).listen(PORT, () => { + console.log(`Ready at http://localhost:${PORT}`) +}) diff --git a/packages/app-nuxt/spa-stats.json b/packages/app-nuxt/spa-stats.json new file mode 100644 index 0000000..5e559d9 --- /dev/null +++ b/packages/app-nuxt/spa-stats.json @@ -0,0 +1,9 @@ +{ + "timingMeasuredAt": "2026-03-22T07:44:24.631Z", + "runner": "local", + "frameworkVersion": "4.2.2", + "spaFirstPaintMs": 32.8, + "spaFCPMs": 32.8, + "spaINPMs": 69.98, + "spaRuns": 5 +} diff --git a/packages/app-react-router/app/routes.ts b/packages/app-react-router/app/routes.ts index f8effb3..987cbce 100644 --- a/packages/app-react-router/app/routes.ts +++ b/packages/app-react-router/app/routes.ts @@ -1,3 +1,7 @@ -import { type RouteConfig, index } from '@react-router/dev/routes' +import { type RouteConfig, index, route } from '@react-router/dev/routes' -export default [index('routes/home.tsx')] satisfies RouteConfig +export default [ + index('routes/home.tsx'), + route('/spa', 'routes/spa.tsx'), + route('/spa/result', 'routes/spa.result.tsx'), +] satisfies RouteConfig diff --git a/packages/app-react-router/app/routes/spa.result.tsx b/packages/app-react-router/app/routes/spa.result.tsx new file mode 100644 index 0000000..9379e24 --- /dev/null +++ b/packages/app-react-router/app/routes/spa.result.tsx @@ -0,0 +1,27 @@ +import { useState } from 'react' + +type Entry = { id: string; name: string } + +function generateData(): Entry[] { + return Array.from({ length: 1000 }, () => ({ + id: crypto.randomUUID(), + name: crypto.randomUUID(), + })) +} + +export default function SpaResultPage() { + const [entries] = useState(generateData) + + return ( + + + {entries.map((entry) => ( + + + + + ))} + +
{entry.id}{entry.name}
+ ) +} diff --git a/packages/app-react-router/app/routes/spa.tsx b/packages/app-react-router/app/routes/spa.tsx new file mode 100644 index 0000000..5e4e3ec --- /dev/null +++ b/packages/app-react-router/app/routes/spa.tsx @@ -0,0 +1,11 @@ +import { useNavigate } from 'react-router' + +export default function SpaPage() { + const navigate = useNavigate() + + return ( + + ) +} diff --git a/packages/app-react-router/package.json b/packages/app-react-router/package.json index 6392b43..8b28398 100644 --- a/packages/app-react-router/package.json +++ b/packages/app-react-router/package.json @@ -5,10 +5,12 @@ "scripts": { "dev": "react-router dev", "build": "react-router build", - "start": "react-router-serve ./build/server/index.js" + "start": "react-router-serve ./build/server/index.js", + "serve": "react-router-serve ./build/server/index.js" }, "dependencies": { "@react-router/node": "7.10.1", + "@react-router/serve": "7.10.1", "react": "19.2.3", "react-dom": "19.2.3", "react-router": "7.10.1", diff --git a/packages/app-react-router/pnpm-lock.yaml b/packages/app-react-router/pnpm-lock.yaml index e78c4a0..a02dedb 100644 --- a/packages/app-react-router/pnpm-lock.yaml +++ b/packages/app-react-router/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@react-router/node': specifier: 7.10.1 version: 7.10.1(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@react-router/serve': + specifier: 7.10.1 + version: 7.10.1(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) isbot: specifier: '5' version: 5.1.35 @@ -26,7 +29,7 @@ importers: devDependencies: '@react-router/dev': specifier: 7.10.1 - version: 7.10.1(@types/node@22.19.11)(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@22.19.11)) + version: 7.10.1(@react-router/serve@7.10.1(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)))(@types/node@22.19.11)(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@22.19.11)) '@types/node': specifier: '22' version: 22.19.11 @@ -367,6 +370,17 @@ packages: wrangler: optional: true + '@react-router/express@7.10.1': + resolution: {integrity: sha512-O7xjg6wWHfrsnPyVWgQG+tCamIE09SqLqtHwa1tAFzKPjcDpCw4S4+/OkJvNXLtBL60H3VhZ1r2OQgXBgGOMpw==} + engines: {node: '>=20.0.0'} + peerDependencies: + express: ^4.17.1 || ^5 + react-router: 7.10.1 + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + '@react-router/node@7.10.1': resolution: {integrity: sha512-RLmjlR1zQu+ve8ibI0lu91pJrXGcmfkvsrQl7z/eTc5V5FZgl0OvQVWL5JDWBlBZyzdLMQQekUOX5WcPhCP1FQ==} engines: {node: '>=20.0.0'} @@ -377,6 +391,13 @@ packages: typescript: optional: true + '@react-router/serve@7.10.1': + resolution: {integrity: sha512-qYco7sFpbRgoKJKsCgJmFBQwaLVsLv255K8vbPodnXe13YBEzV/ugIqRCYVz2hghvlPiEKgaHh2On0s/5npn6w==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + react-router: 7.10.1 + '@remix-run/node-fetch-server@0.9.0': resolution: {integrity: sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA==} @@ -519,9 +540,16 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + babel-dead-code-elimination@1.0.12: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} @@ -529,15 +557,38 @@ packages: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001769: resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} @@ -545,12 +596,35 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -558,6 +632,14 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -575,12 +657,43 @@ packages: babel-plugin-macros: optional: true + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.286: resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} @@ -590,10 +703,21 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + exit-hook@2.2.1: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -606,15 +730,69 @@ packages: picomatch: optional: true + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + isbot@5.1.35: resolution: {integrity: sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg==} engines: {node: '>=18'} @@ -638,6 +816,45 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + morgan@1.10.1: + resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} + engines: {node: '>= 0.8.0'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -646,13 +863,44 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + p-map@7.0.4: resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} engines: {node: '>=18'} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -678,6 +926,22 @@ packages: engines: {node: '>=14'} hasBin: true + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.14.2: + resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + react-dom@19.2.3: resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: @@ -710,6 +974,15 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -722,26 +995,80 @@ packages: engines: {node: '>=10'} hasBin: true + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + valibot@1.2.0: resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} peerDependencies: @@ -750,6 +1077,10 @@ packages: typescript: optional: true + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1085,7 +1416,7 @@ snapshots: '@mjackson/node-fetch-server@0.2.0': {} - '@react-router/dev@7.10.1(@types/node@22.19.11)(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@22.19.11))': + '@react-router/dev@7.10.1(@react-router/serve@7.10.1(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)))(@types/node@22.19.11)(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@22.19.11))': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -1117,6 +1448,8 @@ snapshots: valibot: 1.2.0 vite: 7.3.0(@types/node@22.19.11) vite-node: 3.2.4(@types/node@22.19.11) + optionalDependencies: + '@react-router/serve': 7.10.1(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -1132,11 +1465,32 @@ snapshots: - tsx - yaml + '@react-router/express@7.10.1(express@4.22.1)(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + dependencies: + '@react-router/node': 7.10.1(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + express: 4.22.1 + react-router: 7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@react-router/node@7.10.1(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': dependencies: '@mjackson/node-fetch-server': 0.2.0 react-router: 7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@react-router/serve@7.10.1(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + dependencies: + '@mjackson/node-fetch-server': 0.2.0 + '@react-router/express': 7.10.1(express@4.22.1)(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@react-router/node': 7.10.1(react-router@7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + compression: 1.8.1 + express: 4.22.1 + get-port: 5.1.1 + morgan: 1.10.1 + react-router: 7.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + source-map-support: 0.5.21 + transitivePeerDependencies: + - supports-color + - typescript + '@remix-run/node-fetch-server@0.9.0': {} '@rollup/rollup-android-arm-eabi@4.57.1': @@ -1228,8 +1582,15 @@ snapshots: dependencies: csstype: 3.2.3 + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + arg@5.0.2: {} + array-flatten@1.1.1: {} + babel-dead-code-elimination@1.0.12: dependencies: '@babel/core': 7.29.0 @@ -1241,6 +1602,27 @@ snapshots: baseline-browser-mapping@2.9.19: {} + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + + body-parser@1.20.4: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.2 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.19 @@ -1249,32 +1631,98 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer-from@1.1.2: {} + + bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + caniuse-lite@1.0.30001769: {} chokidar@4.0.3: dependencies: readdirp: 4.1.2 + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + confbox@0.2.4: {} + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.0.7: {} + + cookie@0.7.2: {} + cookie@1.1.1: {} csstype@3.2.3: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.4.3: dependencies: ms: 2.1.3 dedent@1.7.1: {} + depd@2.0.0: {} + + destroy@1.2.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + electron-to-chromium@1.5.286: {} + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -1306,19 +1754,121 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + + etag@1.8.1: {} + exit-hook@2.2.1: {} + express@4.22.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.14.2 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.8: {} fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-port@5.1.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + isbot@5.1.35: {} js-tokens@4.0.0: {} @@ -1333,14 +1883,64 @@ snapshots: dependencies: yallist: 3.1.1 + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + morgan@1.10.1: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.1.0 + transitivePeerDependencies: + - supports-color + + ms@2.0.0: {} + ms@2.1.3: {} nanoid@3.3.11: {} + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + node-releases@2.0.27: {} + object-inspect@1.13.4: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + p-map@7.0.4: {} + parseurl@1.3.3: {} + + path-to-regexp@0.1.12: {} + pathe@1.1.2: {} pathe@2.0.3: {} @@ -1363,6 +1963,24 @@ snapshots: prettier@3.8.1: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.14.2: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + react-dom@19.2.3(react@19.2.3): dependencies: react: 19.2.3 @@ -1413,31 +2031,116 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + scheduler@0.27.0: {} semver@6.3.1: {} semver@7.7.4: {} + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + set-cookie-parser@2.7.2: {} + setprototypeof@1.2.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + statuses@2.0.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + toidentifier@1.0.1: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + undici-types@6.21.0: {} + unpipe@1.0.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 + utils-merge@1.0.1: {} + valibot@1.2.0: {} + vary@1.1.2: {} + vite-node@3.2.4(@types/node@22.19.11): dependencies: cac: 6.7.14 diff --git a/packages/app-react-router/spa-stats.json b/packages/app-react-router/spa-stats.json new file mode 100644 index 0000000..77c9487 --- /dev/null +++ b/packages/app-react-router/spa-stats.json @@ -0,0 +1,9 @@ +{ + "timingMeasuredAt": "2026-03-22T07:44:28.966Z", + "runner": "local", + "frameworkVersion": "7.10.1", + "spaFirstPaintMs": 35.2, + "spaFCPMs": 35.2, + "spaINPMs": 59.78, + "spaRuns": 5 +} diff --git a/packages/app-solid-start/package.json b/packages/app-solid-start/package.json index e52543d..c2e6102 100644 --- a/packages/app-solid-start/package.json +++ b/packages/app-solid-start/package.json @@ -5,7 +5,8 @@ "scripts": { "dev": "vinxi dev", "build": "vinxi build", - "start": "vinxi start" + "start": "vinxi start", + "serve": "node serve.mjs" }, "dependencies": { "@solidjs/meta": "0.29.4", diff --git a/packages/app-solid-start/serve.mjs b/packages/app-solid-start/serve.mjs new file mode 100644 index 0000000..d48c32b --- /dev/null +++ b/packages/app-solid-start/serve.mjs @@ -0,0 +1,36 @@ +import { createServer } from 'node:http' +import { createReadStream, existsSync, statSync } from 'node:fs' +import { join, extname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { listener } from './.output/server/index.mjs' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const PORT = parseInt(process.env.PORT ?? '3000', 10) +const publicDir = join(__dirname, '.output', 'public') + +const MIME = { + '.js': 'application/javascript', + '.css': 'text/css', + '.html': 'text/html', + '.json': 'application/json', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.woff2': 'font/woff2', +} + +createServer((req, res) => { + const url = new URL(req.url, `http://localhost:${PORT}`) + const filePath = join(publicDir, url.pathname) + + if (existsSync(filePath) && statSync(filePath).isFile()) { + const ext = extname(filePath) + res.setHeader('Content-Type', MIME[ext] ?? 'application/octet-stream') + createReadStream(filePath).pipe(res) + return + } + + listener(req, res) +}).listen(PORT, () => { + console.log(`Ready at http://localhost:${PORT}`) +}) diff --git a/packages/app-solid-start/spa-stats.json b/packages/app-solid-start/spa-stats.json new file mode 100644 index 0000000..23f883c --- /dev/null +++ b/packages/app-solid-start/spa-stats.json @@ -0,0 +1,9 @@ +{ + "timingMeasuredAt": "2026-03-22T07:44:42.535Z", + "runner": "local", + "frameworkVersion": "1.2.1", + "spaFirstPaintMs": 22.4, + "spaFCPMs": 22.4, + "spaINPMs": 63.09, + "spaRuns": 5 +} diff --git a/packages/app-solid-start/src/routes/spa/index.tsx b/packages/app-solid-start/src/routes/spa/index.tsx new file mode 100644 index 0000000..fd8552d --- /dev/null +++ b/packages/app-solid-start/src/routes/spa/index.tsx @@ -0,0 +1,13 @@ +export const ssr = false + +import { useNavigate } from '@solidjs/router' + +export default function SpaPage() { + const navigate = useNavigate() + + return ( + + ) +} diff --git a/packages/app-solid-start/src/routes/spa/result.tsx b/packages/app-solid-start/src/routes/spa/result.tsx new file mode 100644 index 0000000..f38c0e1 --- /dev/null +++ b/packages/app-solid-start/src/routes/spa/result.tsx @@ -0,0 +1,31 @@ +export const ssr = false + +import { For } from 'solid-js' + +type Entry = { id: string; name: string } + +function generateData(): Entry[] { + return Array.from({ length: 1000 }, () => ({ + id: crypto.randomUUID(), + name: crypto.randomUUID(), + })) +} + +export default function SpaResultPage() { + const entries = generateData() + + return ( + + + + {(entry) => ( + + + + + )} + + +
{entry.id}{entry.name}
+ ) +} diff --git a/packages/app-sveltekit/package.json b/packages/app-sveltekit/package.json index 12b6135..b064bc0 100644 --- a/packages/app-sveltekit/package.json +++ b/packages/app-sveltekit/package.json @@ -7,6 +7,7 @@ "dev": "vite dev", "build": "vite build", "preview": "vite preview", + "serve": "node build/index.js", "prepare": "svelte-kit sync || echo ''", "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "lint": "eslint .", diff --git a/packages/app-sveltekit/spa-stats.json b/packages/app-sveltekit/spa-stats.json new file mode 100644 index 0000000..7d0b8f6 --- /dev/null +++ b/packages/app-sveltekit/spa-stats.json @@ -0,0 +1,9 @@ +{ + "timingMeasuredAt": "2026-03-22T07:44:37.749Z", + "runner": "local", + "frameworkVersion": "2.49.4", + "spaFirstPaintMs": 26.4, + "spaFCPMs": 26.4, + "spaINPMs": 64.89, + "spaRuns": 5 +} diff --git a/packages/app-sveltekit/src/routes/spa/+page.svelte b/packages/app-sveltekit/src/routes/spa/+page.svelte new file mode 100644 index 0000000..14e90c1 --- /dev/null +++ b/packages/app-sveltekit/src/routes/spa/+page.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/app-sveltekit/src/routes/spa/+page.ts b/packages/app-sveltekit/src/routes/spa/+page.ts new file mode 100644 index 0000000..62ad4e4 --- /dev/null +++ b/packages/app-sveltekit/src/routes/spa/+page.ts @@ -0,0 +1 @@ +export const ssr = false diff --git a/packages/app-sveltekit/src/routes/spa/result/+page.svelte b/packages/app-sveltekit/src/routes/spa/result/+page.svelte new file mode 100644 index 0000000..f8e775c --- /dev/null +++ b/packages/app-sveltekit/src/routes/spa/result/+page.svelte @@ -0,0 +1,19 @@ + + + + + {#each entries as entry (entry.id)} + + + + + {/each} + +
{entry.id}{entry.name}
diff --git a/packages/app-sveltekit/src/routes/spa/result/+page.ts b/packages/app-sveltekit/src/routes/spa/result/+page.ts new file mode 100644 index 0000000..62ad4e4 --- /dev/null +++ b/packages/app-sveltekit/src/routes/spa/result/+page.ts @@ -0,0 +1 @@ +export const ssr = false diff --git a/packages/app-tanstack-start-react/package.json b/packages/app-tanstack-start-react/package.json index 6c1a50c..b651644 100644 --- a/packages/app-tanstack-start-react/package.json +++ b/packages/app-tanstack-start-react/package.json @@ -5,7 +5,8 @@ "scripts": { "dev": "vite dev", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "serve": "node serve.mjs" }, "dependencies": { "@tanstack/react-router": "1.144.0", diff --git a/packages/app-tanstack-start-react/serve.mjs b/packages/app-tanstack-start-react/serve.mjs new file mode 100644 index 0000000..2b5c4f6 --- /dev/null +++ b/packages/app-tanstack-start-react/serve.mjs @@ -0,0 +1,37 @@ +import { createServer } from 'node:http' +import { createReadStream, existsSync, statSync } from 'node:fs' +import { join, extname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const PORT = parseInt(process.env.PORT ?? '3000', 10) +const publicDir = join(__dirname, '.output', 'public') + +const MIME = { + '.js': 'application/javascript', + '.css': 'text/css', + '.html': 'text/html', + '.json': 'application/json', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.woff2': 'font/woff2', +} + +const { middleware } = await import('./.output/server/index.mjs') + +createServer((req, res) => { + const url = new URL(req.url, `http://localhost:${PORT}`) + const filePath = join(publicDir, url.pathname) + + if (existsSync(filePath) && statSync(filePath).isFile()) { + const ext = extname(filePath) + res.setHeader('Content-Type', MIME[ext] ?? 'application/octet-stream') + createReadStream(filePath).pipe(res) + return + } + + middleware(req, res) +}).listen(PORT, () => { + console.log(`Ready at http://localhost:${PORT}`) +}) diff --git a/packages/app-tanstack-start-react/spa-stats.json b/packages/app-tanstack-start-react/spa-stats.json new file mode 100644 index 0000000..a827759 --- /dev/null +++ b/packages/app-tanstack-start-react/spa-stats.json @@ -0,0 +1,9 @@ +{ + "timingMeasuredAt": "2026-03-22T07:44:46.850Z", + "runner": "local", + "frameworkVersion": "1.145.3", + "spaFirstPaintMs": 40.8, + "spaFCPMs": 40.8, + "spaINPMs": 59.27, + "spaRuns": 5 +} diff --git a/packages/app-tanstack-start-react/src/routeTree.gen.ts b/packages/app-tanstack-start-react/src/routeTree.gen.ts index dceedff..db59a78 100644 --- a/packages/app-tanstack-start-react/src/routeTree.gen.ts +++ b/packages/app-tanstack-start-react/src/routeTree.gen.ts @@ -9,38 +9,65 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as SpaRouteImport } from './routes/spa' import { Route as IndexRouteImport } from './routes/index' +import { Route as SpaResultRouteImport } from './routes/spa_.result' +const SpaRoute = SpaRouteImport.update({ + id: '/spa', + path: '/spa', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) +const SpaResultRoute = SpaResultRouteImport.update({ + id: '/spa_/result', + path: '/spa/result', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/spa': typeof SpaRoute + '/spa/result': typeof SpaResultRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/spa': typeof SpaRoute + '/spa/result': typeof SpaResultRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/spa': typeof SpaRoute + '/spa_/result': typeof SpaResultRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' + fullPaths: '/' | '/spa' | '/spa/result' fileRoutesByTo: FileRoutesByTo - to: '/' - id: '__root__' | '/' + to: '/' | '/spa' | '/spa/result' + id: '__root__' | '/' | '/spa' | '/spa_/result' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + SpaRoute: typeof SpaRoute + SpaResultRoute: typeof SpaResultRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/spa': { + id: '/spa' + path: '/spa' + fullPath: '/spa' + preLoaderRoute: typeof SpaRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -48,11 +75,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/spa_/result': { + id: '/spa_/result' + path: '/spa/result' + fullPath: '/spa/result' + preLoaderRoute: typeof SpaResultRouteImport + parentRoute: typeof rootRouteImport + } } } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + SpaRoute: SpaRoute, + SpaResultRoute: SpaResultRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/packages/app-tanstack-start-react/src/routes/spa.tsx b/packages/app-tanstack-start-react/src/routes/spa.tsx new file mode 100644 index 0000000..f65b826 --- /dev/null +++ b/packages/app-tanstack-start-react/src/routes/spa.tsx @@ -0,0 +1,26 @@ +import { createFileRoute, useRouter } from '@tanstack/react-router' +import { useState, useEffect } from 'react' + +export const Route = createFileRoute('/spa')({ + component: SpaPage, +}) + +function SpaPage() { + const [mounted, setMounted] = useState(false) + const router = useRouter() + + // TanStack Start SSRs routes by default — useEffect ensures the button only + // renders client-side so FCP measures CSR, not server-rendered HTML + useEffect(() => setMounted(true), []) + + if (!mounted) return null + + return ( + + ) +} diff --git a/packages/app-tanstack-start-react/src/routes/spa_.result.tsx b/packages/app-tanstack-start-react/src/routes/spa_.result.tsx new file mode 100644 index 0000000..876ffd9 --- /dev/null +++ b/packages/app-tanstack-start-react/src/routes/spa_.result.tsx @@ -0,0 +1,38 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useState, useEffect } from 'react' + +export const Route = createFileRoute('/spa_/result')({ + component: SpaResultPage, +}) + +type Entry = { id: string; name: string } + +function generateData(): Entry[] { + return Array.from({ length: 1000 }, () => ({ + id: crypto.randomUUID(), + name: crypto.randomUUID(), + })) +} + +function SpaResultPage() { + const [entries, setEntries] = useState(null) + + useEffect(() => { + setEntries(generateData()) + }, []) + + if (entries === null) return null + + return ( + + + {entries.map((entry) => ( + + + + + ))} + +
{entry.id}{entry.name}
+ ) +} diff --git a/packages/docs/src/components/SPACharts.astro b/packages/docs/src/components/SPACharts.astro new file mode 100644 index 0000000..0823a4c --- /dev/null +++ b/packages/docs/src/components/SPACharts.astro @@ -0,0 +1,15 @@ +--- +import ChartTabs from './ChartTabs.astro' +import SPAFCPChart from './SPAFCPChart.astro' +import SPAINPChart from './SPAINPChart.astro' +--- + + + + + diff --git a/packages/docs/src/components/SPAFCPChart.astro b/packages/docs/src/components/SPAFCPChart.astro new file mode 100644 index 0000000..2f0612e --- /dev/null +++ b/packages/docs/src/components/SPAFCPChart.astro @@ -0,0 +1,10 @@ +--- +import { chartSPAFCPData } from '../lib/collections' +import ComparisonBarChart from './ComparisonBarChart.astro' +--- + + diff --git a/packages/docs/src/components/SPAINPChart.astro b/packages/docs/src/components/SPAINPChart.astro new file mode 100644 index 0000000..6062297 --- /dev/null +++ b/packages/docs/src/components/SPAINPChart.astro @@ -0,0 +1,10 @@ +--- +import { chartSPAINPData } from '../lib/collections' +import ComparisonBarChart from './ComparisonBarChart.astro' +--- + + diff --git a/packages/docs/src/content.config.ts b/packages/docs/src/content.config.ts index 62ee0dc..214242d 100644 --- a/packages/docs/src/content.config.ts +++ b/packages/docs/src/content.config.ts @@ -53,6 +53,11 @@ const runtimeCollection = defineCollection({ ssrSamples: z.number(), ssrBodySizeKb: z.number(), ssrDuplicationFactor: z.number(), + // SPA paint + interaction metrics + spaFirstPaintMs: z.number().optional(), + spaFCPMs: z.number().optional(), + spaINPMs: z.number().optional(), + spaRuns: z.number().optional(), }), }) diff --git a/packages/docs/src/content/runtime/app-astro.json b/packages/docs/src/content/runtime/app-astro.json index 6a5a30e..d2c3aed 100644 --- a/packages/docs/src/content/runtime/app-astro.json +++ b/packages/docs/src/content/runtime/app-astro.json @@ -11,5 +11,9 @@ "timingMeasuredAt": "2026-03-08T00:31:57.171Z", "runner": "ubuntu-latest", "frameworkVersion": "5.16.15", - "order": 1 + "order": 1, + "spaFirstPaintMs": 36.8, + "spaFCPMs": 36.8, + "spaINPMs": 98.72, + "spaRuns": 5 } diff --git a/packages/docs/src/content/runtime/app-next-js.json b/packages/docs/src/content/runtime/app-next-js.json index f96f907..72d9474 100644 --- a/packages/docs/src/content/runtime/app-next-js.json +++ b/packages/docs/src/content/runtime/app-next-js.json @@ -11,5 +11,9 @@ "ssrSamples": 1293, "ssrBodySizeKb": 198.59, "ssrDuplicationFactor": 2, - "order": 3 + "order": 3, + "spaFirstPaintMs": 48, + "spaFCPMs": 48, + "spaINPMs": 75.48, + "spaRuns": 5 } diff --git a/packages/docs/src/content/runtime/app-nuxt.json b/packages/docs/src/content/runtime/app-nuxt.json index 1907d19..d130700 100644 --- a/packages/docs/src/content/runtime/app-nuxt.json +++ b/packages/docs/src/content/runtime/app-nuxt.json @@ -11,5 +11,9 @@ "timingMeasuredAt": "2026-03-08T00:31:57.171Z", "runner": "ubuntu-latest", "frameworkVersion": "4.2.2", - "order": 4 + "order": 4, + "spaFirstPaintMs": 32.8, + "spaFCPMs": 32.8, + "spaINPMs": 69.98, + "spaRuns": 5 } diff --git a/packages/docs/src/content/runtime/app-react-router.json b/packages/docs/src/content/runtime/app-react-router.json index fb7daa3..4deadba 100644 --- a/packages/docs/src/content/runtime/app-react-router.json +++ b/packages/docs/src/content/runtime/app-react-router.json @@ -11,5 +11,9 @@ "ssrSamples": 644, "ssrBodySizeKb": 211.14, "ssrDuplicationFactor": 2, - "order": 5 + "order": 5, + "spaFirstPaintMs": 35.2, + "spaFCPMs": 35.2, + "spaINPMs": 59.78, + "spaRuns": 5 } diff --git a/packages/docs/src/content/runtime/app-solid-start.json b/packages/docs/src/content/runtime/app-solid-start.json index d36dd03..eeaa4f2 100644 --- a/packages/docs/src/content/runtime/app-solid-start.json +++ b/packages/docs/src/content/runtime/app-solid-start.json @@ -11,5 +11,9 @@ "ssrSamples": 2340, "ssrBodySizeKb": 225.49, "ssrDuplicationFactor": 2, - "order": 6 + "order": 6, + "spaFirstPaintMs": 22.4, + "spaFCPMs": 22.4, + "spaINPMs": 63.09, + "spaRuns": 5 } diff --git a/packages/docs/src/content/runtime/app-sveltekit.json b/packages/docs/src/content/runtime/app-sveltekit.json index efed6d4..bd164f4 100644 --- a/packages/docs/src/content/runtime/app-sveltekit.json +++ b/packages/docs/src/content/runtime/app-sveltekit.json @@ -11,5 +11,9 @@ "ssrSamples": 2592, "ssrBodySizeKb": 183.55, "ssrDuplicationFactor": 2, - "order": 7 + "order": 7, + "spaFirstPaintMs": 26.4, + "spaFCPMs": 26.4, + "spaINPMs": 64.89, + "spaRuns": 5 } diff --git a/packages/docs/src/content/runtime/app-tanstack-start-react.json b/packages/docs/src/content/runtime/app-tanstack-start-react.json index 7697b82..a5ac7f3 100644 --- a/packages/docs/src/content/runtime/app-tanstack-start-react.json +++ b/packages/docs/src/content/runtime/app-tanstack-start-react.json @@ -11,5 +11,9 @@ "ssrSamples": 1854, "ssrBodySizeKb": 193.53, "ssrDuplicationFactor": 2, - "order": 8 + "order": 8, + "spaFirstPaintMs": 40.8, + "spaFCPMs": 40.8, + "spaINPMs": 59.27, + "spaRuns": 5 } diff --git a/packages/docs/src/lib/collections.ts b/packages/docs/src/lib/collections.ts index e869fce..c1c010c 100644 --- a/packages/docs/src/lib/collections.ts +++ b/packages/docs/src/lib/collections.ts @@ -48,4 +48,16 @@ export const chartDuplicateDependencyData = starterStats focused: f.isFocused, })) +export const chartSPAFCPData = runtimeEntries + .map((entry) => entry.data) + .sort((a, b) => a.order - b.order) + .filter((f) => f?.name != null && Number.isFinite(f.spaFCPMs)) + .map((f) => ({ name: f.name, value: f.spaFCPMs!, focused: f.isFocused })) + +export const chartSPAINPData = runtimeEntries + .map((entry) => entry.data) + .sort((a, b) => a.order - b.order) + .filter((f) => f?.name != null && Number.isFinite(f.spaINPMs)) + .map((f) => ({ name: f.name, value: f.spaINPMs!, focused: f.isFocused })) + export { ssrStats, depsStats, buildInstallData } diff --git a/packages/docs/src/pages/index.astro b/packages/docs/src/pages/index.astro index f732401..e5412a5 100644 --- a/packages/docs/src/pages/index.astro +++ b/packages/docs/src/pages/index.astro @@ -6,6 +6,7 @@ import Description from '../components/Description.astro' import DetailsLink from '../components/DetailsLink.astro' import FocusedToggle from '../components/FocusedToggle.astro' import PageHeader from '../components/PageHeader.astro' +import SPACharts from '../components/SPACharts.astro' import SSRCharts from '../components/SSRCharts.astro' import SSRStatsTable from '../components/SSRStatsTable.astro' import Layout from '../layouts/Layout.astro' @@ -53,6 +54,8 @@ import Layout from '../layouts/Layout.astro' +

SPA Performance

+