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 2883b43..1a00a72 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -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 69b8d0f..033bce8 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:
@@ -187,3 +192,54 @@ 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
+ env:
+ BUILD_MODE: spa
+
+ - 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/package.json b/package.json
index bc02d5d..ec0e8c9 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"type-check:all": "pnpm type-check && for pkg in packages/starter-* packages/app-*; do (cd \"$pkg\" && pnpm run --if-present type-check); done",
"install:all": "for pkg in packages/starter-* packages/app-*; do (cd \"$pkg\" && pnpm install --ignore-workspace); done",
"install:all:frozen": "for pkg in packages/starter-* packages/app-*; do (cd \"$pkg\" && pnpm install --frozen-lockfile --ignore-workspace); done",
+ "build:apps": "for pkg in packages/app-*; do echo \"Building $pkg...\" && (cd \"$pkg\" && pnpm build) || exit 1; done",
"check:all": "pnpm install:all && pnpm format:check && pnpm lint:all && pnpm type-check:all",
"check:all:ci": "pnpm install:all:frozen && pnpm format:check && pnpm lint:all && pnpm type-check:all"
},
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..855ff95
--- /dev/null
+++ b/packages/app-astro/serve.mjs
@@ -0,0 +1,47 @@
+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 = {
+ '.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[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..aa16957
--- /dev/null
+++ b/packages/app-astro/spa-stats.json
@@ -0,0 +1,9 @@
+{
+ "timingMeasuredAt": "2026-03-28T13:17:16.865Z",
+ "runner": "local",
+ "frameworkVersion": "5.16.15",
+ "spaFirstPaintMs": 159.4,
+ "spaFCPMs": 159.12,
+ "spaINPMs": 24.1,
+ "spaRuns": 5
+}
diff --git a/packages/app-astro/src/components/SpaDetail.tsx b/packages/app-astro/src/components/SpaDetail.tsx
new file mode 100644
index 0000000..af1b9b1
--- /dev/null
+++ b/packages/app-astro/src/components/SpaDetail.tsx
@@ -0,0 +1,7 @@
+interface Props {
+ id: string | null
+}
+
+export default function SpaDetail({ id }: Props) {
+ return
{id}
+}
diff --git a/packages/app-astro/src/components/SpaPage.tsx b/packages/app-astro/src/components/SpaPage.tsx
new file mode 100644
index 0000000..544ec92
--- /dev/null
+++ b/packages/app-astro/src/components/SpaPage.tsx
@@ -0,0 +1,30 @@
+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] = useState(generateData)
+
+ return (
+
+
+ {entries.map((entry) => (
+
+ {entry.id}
+ {entry.name}
+
+ View →
+
+
+ ))}
+
+
+ )
+}
diff --git a/packages/app-astro/src/pages/spa.astro b/packages/app-astro/src/pages/spa.astro
new file mode 100644
index 0000000..7c975b9
--- /dev/null
+++ b/packages/app-astro/src/pages/spa.astro
@@ -0,0 +1,16 @@
+---
+import { ClientRouter } from 'astro:transitions'
+import SpaPage from '../components/SpaPage'
+---
+
+
+
+
+
+ Astro SPA Benchmark
+
+
+
+
+
+
diff --git a/packages/app-astro/src/pages/spa/detail.astro b/packages/app-astro/src/pages/spa/detail.astro
new file mode 100644
index 0000000..0fa2828
--- /dev/null
+++ b/packages/app-astro/src/pages/spa/detail.astro
@@ -0,0 +1,16 @@
+---
+import SpaDetail from '../../components/SpaDetail'
+
+const id = Astro.url.searchParams.get('id')
+---
+
+
+
+
+
+ Astro SPA Detail
+
+
+
+
+
diff --git a/packages/app-mastro/generated/styles.css b/packages/app-mastro/generated/styles.css
new file mode 100644
index 0000000..efa12c4
--- /dev/null
+++ b/packages/app-mastro/generated/styles.css
@@ -0,0 +1,26 @@
+html {
+ font-family: sans-serif;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ word-break: break-word;
+ text-wrap-style: pretty;
+}
+
+p {
+ hyphens: auto;
+ word-break: break-word;
+}
+
+img {
+ max-width: 100%;
+}
+
+@view-transition {
+ navigation: auto;
+}
diff --git a/packages/app-next-js/app/spa/SpaTable.tsx b/packages/app-next-js/app/spa/SpaTable.tsx
new file mode 100644
index 0000000..32274db
--- /dev/null
+++ b/packages/app-next-js/app/spa/SpaTable.tsx
@@ -0,0 +1,33 @@
+'use client'
+
+import { useState } from 'react'
+import Link from 'next/link'
+
+type Entry = { id: string; name: string }
+
+function generateData(): Entry[] {
+ return Array.from({ length: 1000 }, () => ({
+ id: crypto.randomUUID(),
+ name: crypto.randomUUID(),
+ }))
+}
+
+export default function SpaTable() {
+ const [entries] = useState(generateData)
+
+ return (
+
+
+ {entries.map((entry) => (
+
+ {entry.id}
+ {entry.name}
+
+ View →
+
+
+ ))}
+
+
+ )
+}
diff --git a/packages/app-next-js/app/spa/detail/page.tsx b/packages/app-next-js/app/spa/detail/page.tsx
new file mode 100644
index 0000000..4ce252a
--- /dev/null
+++ b/packages/app-next-js/app/spa/detail/page.tsx
@@ -0,0 +1,19 @@
+'use client'
+
+import { Suspense } from 'react'
+import { useSearchParams } from 'next/navigation'
+
+function DetailContent() {
+ const searchParams = useSearchParams()
+ const id = searchParams.get('id')
+
+ return {id}
+}
+
+export default function SpaDetailPage() {
+ return (
+
+
+
+ )
+}
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..3d9b5e6
--- /dev/null
+++ b/packages/app-next-js/app/spa/page.tsx
@@ -0,0 +1,9 @@
+'use client'
+
+import dynamic from 'next/dynamic'
+
+const SpaTable = dynamic(() => import('./SpaTable'), { ssr: false })
+
+export default function SpaPage() {
+ return
+}
diff --git a/packages/app-next-js/package.json b/packages/app-next-js/package.json
index 104c7e1..dde2a0f 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": "node serve.mjs"
},
"dependencies": {
"next": "16.1.1",
diff --git a/packages/app-next-js/serve.mjs b/packages/app-next-js/serve.mjs
new file mode 100644
index 0000000..d0ceb94
--- /dev/null
+++ b/packages/app-next-js/serve.mjs
@@ -0,0 +1,29 @@
+import { join } from 'node:path'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+import { createServer } from 'node:http'
+
+const __dirname = fileURLToPath(new URL('.', import.meta.url))
+const PORT = parseInt(process.env.PORT ?? '3000', 10)
+
+const nextEntryPath = join(
+ __dirname,
+ 'node_modules',
+ 'next',
+ 'dist',
+ 'server',
+ 'next.js',
+)
+const { default: next } = await import(pathToFileURL(nextEntryPath).href)
+
+const app = next({
+ dev: false,
+ hostname: 'localhost',
+ port: PORT,
+ dir: __dirname,
+})
+await app.prepare()
+const handler = app.getRequestHandler()
+
+createServer((req, res) => handler(req, res)).listen(PORT, () => {
+ console.log(`Ready at http://localhost:${PORT}`)
+})
diff --git a/packages/app-next-js/spa-stats.json b/packages/app-next-js/spa-stats.json
new file mode 100644
index 0000000..f0e5fa2
--- /dev/null
+++ b/packages/app-next-js/spa-stats.json
@@ -0,0 +1,9 @@
+{
+ "timingMeasuredAt": "2026-03-28T12:38:19.490Z",
+ "runner": "local",
+ "frameworkVersion": "16.1.1",
+ "spaFirstPaintMs": 373.6,
+ "spaFCPMs": 373.62,
+ "spaINPMs": 21.83,
+ "spaRuns": 5
+}
diff --git a/packages/app-nuxt/app/pages/spa/detail.vue b/packages/app-nuxt/app/pages/spa/detail.vue
new file mode 100644
index 0000000..0db002c
--- /dev/null
+++ b/packages/app-nuxt/app/pages/spa/detail.vue
@@ -0,0 +1,10 @@
+
+
+
+ {{ id }}
+
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..54882d0
--- /dev/null
+++ b/packages/app-nuxt/app/pages/spa/index.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ {{ entry.id }}
+ {{ entry.name }}
+
+ View →
+
+
+
+
+
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..fbd78bd
--- /dev/null
+++ b/packages/app-nuxt/serve.mjs
@@ -0,0 +1,38 @@
+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 = {
+ '.html': 'text/html; charset=utf-8',
+ '.js': 'application/javascript',
+ '.mjs': 'application/javascript',
+ '.css': 'text/css',
+ '.json': 'application/json',
+ '.png': 'image/png',
+ '.svg': 'image/svg+xml',
+ '.ico': 'image/x-icon',
+ '.woff': 'font/woff',
+ '.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..3bb3fa8
--- /dev/null
+++ b/packages/app-nuxt/spa-stats.json
@@ -0,0 +1,9 @@
+{
+ "timingMeasuredAt": "2026-03-28T13:21:47.278Z",
+ "runner": "local",
+ "frameworkVersion": "4.2.2",
+ "spaFirstPaintMs": 88.6,
+ "spaFCPMs": 88.54,
+ "spaINPMs": 22.16,
+ "spaRuns": 5
+}
diff --git a/packages/app-react-router/app/routes.ts b/packages/app-react-router/app/routes.ts
index f8effb3..92b333b 100644
--- a/packages/app-react-router/app/routes.ts
+++ b/packages/app-react-router/app/routes.ts
@@ -1,3 +1,9 @@
-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
+const isSpa = process.env.BUILD_MODE === 'spa'
+
+export default [
+ ...(isSpa ? [] : [index('routes/home.tsx')]),
+ route('/spa', 'routes/spa.tsx'),
+ route('/spa/detail', 'routes/spa.detail.tsx'),
+] satisfies RouteConfig
diff --git a/packages/app-react-router/app/routes/spa.detail.tsx b/packages/app-react-router/app/routes/spa.detail.tsx
new file mode 100644
index 0000000..79ed492
--- /dev/null
+++ b/packages/app-react-router/app/routes/spa.detail.tsx
@@ -0,0 +1,8 @@
+import { useSearchParams } from 'react-router'
+
+export default function SpaDetailPage() {
+ const [searchParams] = useSearchParams()
+ const id = searchParams.get('id')
+
+ return {id}
+}
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..1a85bfd
--- /dev/null
+++ b/packages/app-react-router/app/routes/spa.tsx
@@ -0,0 +1,31 @@
+import { useState } from 'react'
+import { Link } from 'react-router'
+
+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] = useState(generateData)
+
+ return (
+
+
+ {entries.map((entry) => (
+
+ {entry.id}
+ {entry.name}
+
+ View →
+
+
+ ))}
+
+
+ )
+}
diff --git a/packages/app-react-router/package.json b/packages/app-react-router/package.json
index 6392b43..0635eb9 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": "node serve.mjs"
},
"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/react-router.config.ts b/packages/app-react-router/react-router.config.ts
index 92ad46c..f9beb38 100644
--- a/packages/app-react-router/react-router.config.ts
+++ b/packages/app-react-router/react-router.config.ts
@@ -1,5 +1,5 @@
import type { Config } from '@react-router/dev/config'
export default {
- ssr: true,
+ ssr: process.env.BUILD_MODE !== 'spa',
} satisfies Config
diff --git a/packages/app-react-router/serve.mjs b/packages/app-react-router/serve.mjs
new file mode 100644
index 0000000..0647700
--- /dev/null
+++ b/packages/app-react-router/serve.mjs
@@ -0,0 +1,42 @@
+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 clientDir = join(__dirname, 'build', 'client')
+
+const MIME = {
+ '.html': 'text/html; charset=utf-8',
+ '.js': 'application/javascript',
+ '.mjs': 'application/javascript',
+ '.css': 'text/css',
+ '.json': 'application/json',
+ '.png': 'image/png',
+ '.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 ext = extname(filePath)
+ res.setHeader('Content-Type', MIME[ext] ?? 'application/octet-stream')
+ createReadStream(filePath).pipe(res)
+ return
+ }
+ } catch {
+ // not a static file, fall through to SPA index
+ }
+
+ res.setHeader('Content-Type', 'text/html; charset=utf-8')
+ createReadStream(join(clientDir, 'index.html')).pipe(res)
+}).listen(PORT, () => {
+ console.log(`Ready at http://localhost:${PORT}`)
+})
diff --git a/packages/app-react-router/spa-stats.json b/packages/app-react-router/spa-stats.json
new file mode 100644
index 0000000..fa07ca3
--- /dev/null
+++ b/packages/app-react-router/spa-stats.json
@@ -0,0 +1,9 @@
+{
+ "timingMeasuredAt": "2026-03-28T12:17:21.756Z",
+ "runner": "local",
+ "frameworkVersion": "7.10.1",
+ "spaFirstPaintMs": 105,
+ "spaFCPMs": 104.89,
+ "spaINPMs": 21.51,
+ "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..fbd78bd
--- /dev/null
+++ b/packages/app-solid-start/serve.mjs
@@ -0,0 +1,38 @@
+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 = {
+ '.html': 'text/html; charset=utf-8',
+ '.js': 'application/javascript',
+ '.mjs': 'application/javascript',
+ '.css': 'text/css',
+ '.json': 'application/json',
+ '.png': 'image/png',
+ '.svg': 'image/svg+xml',
+ '.ico': 'image/x-icon',
+ '.woff': 'font/woff',
+ '.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..8480855
--- /dev/null
+++ b/packages/app-solid-start/spa-stats.json
@@ -0,0 +1,9 @@
+{
+ "timingMeasuredAt": "2026-03-28T12:51:22.387Z",
+ "runner": "local",
+ "frameworkVersion": "1.2.1",
+ "spaFirstPaintMs": 83.6,
+ "spaFCPMs": 83.8,
+ "spaINPMs": 23.56,
+ "spaRuns": 5
+}
diff --git a/packages/app-solid-start/src/routes/spa/detail.tsx b/packages/app-solid-start/src/routes/spa/detail.tsx
new file mode 100644
index 0000000..32453f9
--- /dev/null
+++ b/packages/app-solid-start/src/routes/spa/detail.tsx
@@ -0,0 +1,9 @@
+export const ssr = false
+
+import { useSearchParams } from '@solidjs/router'
+
+export default function SpaDetailPage() {
+ const [searchParams] = useSearchParams()
+
+ return {searchParams.id}
+}
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..1544000
--- /dev/null
+++ b/packages/app-solid-start/src/routes/spa/index.tsx
@@ -0,0 +1,35 @@
+export const ssr = false
+
+import { For } from 'solid-js'
+import { A } from '@solidjs/router'
+
+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 = generateData()
+
+ return (
+
+
+
+ {(entry) => (
+
+ {entry.id}
+ {entry.name}
+
+ View →
+
+
+ )}
+
+
+
+ )
+}
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..da14418
--- /dev/null
+++ b/packages/app-sveltekit/spa-stats.json
@@ -0,0 +1,9 @@
+{
+ "timingMeasuredAt": "2026-03-28T13:23:54.323Z",
+ "runner": "local",
+ "frameworkVersion": "2.49.4",
+ "spaFirstPaintMs": 93.4,
+ "spaFCPMs": 93.44,
+ "spaINPMs": 23.01,
+ "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..f5a4f1a
--- /dev/null
+++ b/packages/app-sveltekit/src/routes/spa/+page.svelte
@@ -0,0 +1,20 @@
+
+
+
+
+ {#each entries as entry (entry.id)}
+
+ {entry.id}
+ {entry.name}
+ View →
+
+ {/each}
+
+
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/detail/+page.svelte b/packages/app-sveltekit/src/routes/spa/detail/+page.svelte
new file mode 100644
index 0000000..eeaaa63
--- /dev/null
+++ b/packages/app-sveltekit/src/routes/spa/detail/+page.svelte
@@ -0,0 +1,7 @@
+
+
+{id}
diff --git a/packages/app-sveltekit/src/routes/spa/detail/+page.ts b/packages/app-sveltekit/src/routes/spa/detail/+page.ts
new file mode 100644
index 0000000..62ad4e4
--- /dev/null
+++ b/packages/app-sveltekit/src/routes/spa/detail/+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..5b7a6a8
--- /dev/null
+++ b/packages/app-tanstack-start-react/serve.mjs
@@ -0,0 +1,56 @@
+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 spaClientDir = join(__dirname, 'dist', 'client')
+const ssrPublicDir = join(__dirname, '.output', 'public')
+
+// Auto-detect: SPA build outputs _shell.html to dist/client; SSR build uses .output
+const isSpa = existsSync(join(spaClientDir, '_shell.html'))
+const publicDir = isSpa ? spaClientDir : ssrPublicDir
+
+const MIME = {
+ '.html': 'text/html; charset=utf-8',
+ '.js': 'application/javascript',
+ '.mjs': 'application/javascript',
+ '.css': 'text/css',
+ '.json': 'application/json',
+ '.png': 'image/png',
+ '.svg': 'image/svg+xml',
+ '.ico': 'image/x-icon',
+ '.woff': 'font/woff',
+ '.woff2': 'font/woff2',
+}
+
+let middleware
+if (!isSpa) {
+ const mod = await import('./.output/server/index.mjs')
+ middleware = mod.middleware
+}
+
+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
+ }
+
+ if (isSpa) {
+ const shell = join(publicDir, '_shell.html')
+ res.setHeader('Content-Type', 'text/html; charset=utf-8')
+ createReadStream(shell).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..4f18cb5
--- /dev/null
+++ b/packages/app-tanstack-start-react/spa-stats.json
@@ -0,0 +1,9 @@
+{
+ "timingMeasuredAt": "2026-03-28T13:19:31.478Z",
+ "runner": "local",
+ "frameworkVersion": "1.145.3",
+ "spaFirstPaintMs": 678.2,
+ "spaFCPMs": 678.19,
+ "spaINPMs": 101.06,
+ "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..46adef8 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 SpaDetailRouteImport } from './routes/spa_.detail'
+const SpaRoute = SpaRouteImport.update({
+ id: '/spa',
+ path: '/spa',
+ getParentRoute: () => rootRouteImport,
+} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const SpaDetailRoute = SpaDetailRouteImport.update({
+ id: '/spa_/detail',
+ path: '/spa/detail',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
+ '/spa': typeof SpaRoute
+ '/spa/detail': typeof SpaDetailRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
+ '/spa': typeof SpaRoute
+ '/spa/detail': typeof SpaDetailRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
+ '/spa': typeof SpaRoute
+ '/spa_/detail': typeof SpaDetailRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
- fullPaths: '/'
+ fullPaths: '/' | '/spa' | '/spa/detail'
fileRoutesByTo: FileRoutesByTo
- to: '/'
- id: '__root__' | '/'
+ to: '/' | '/spa' | '/spa/detail'
+ id: '__root__' | '/' | '/spa' | '/spa_/detail'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
+ SpaRoute: typeof SpaRoute
+ SpaDetailRoute: typeof SpaDetailRoute
}
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_/detail': {
+ id: '/spa_/detail'
+ path: '/spa/detail'
+ fullPath: '/spa/detail'
+ preLoaderRoute: typeof SpaDetailRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
+ SpaRoute: SpaRoute,
+ SpaDetailRoute: SpaDetailRoute,
}
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..bc6295c
--- /dev/null
+++ b/packages/app-tanstack-start-react/src/routes/spa.tsx
@@ -0,0 +1,37 @@
+import { createFileRoute, Link } from '@tanstack/react-router'
+import { useState } from 'react'
+
+export const Route = createFileRoute('/spa')({
+ component: SpaPage,
+})
+
+type Entry = { id: string; name: string }
+
+function generateData(): Entry[] {
+ return Array.from({ length: 1000 }, () => ({
+ id: crypto.randomUUID(),
+ name: crypto.randomUUID(),
+ }))
+}
+
+function SpaPage() {
+ const [entries] = useState(generateData)
+
+ return (
+
+
+ {entries.map((entry) => (
+
+ {entry.id}
+ {entry.name}
+
+
+ View →
+
+
+
+ ))}
+
+
+ )
+}
diff --git a/packages/app-tanstack-start-react/src/routes/spa_.detail.tsx b/packages/app-tanstack-start-react/src/routes/spa_.detail.tsx
new file mode 100644
index 0000000..1cb4153
--- /dev/null
+++ b/packages/app-tanstack-start-react/src/routes/spa_.detail.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/spa_/detail')({
+ validateSearch: (search) => ({
+ id: typeof search.id === 'string' ? search.id : undefined,
+ }),
+ component: SpaDetailPage,
+})
+
+function SpaDetailPage() {
+ const { id } = Route.useSearch()
+
+ return {id}
+}
diff --git a/packages/app-tanstack-start-react/vite.config.ts b/packages/app-tanstack-start-react/vite.config.ts
index b5f9f74..f99d807 100644
--- a/packages/app-tanstack-start-react/vite.config.ts
+++ b/packages/app-tanstack-start-react/vite.config.ts
@@ -3,6 +3,12 @@ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import { nitro } from 'nitro/vite'
+const isSpa = process.env.BUILD_MODE === 'spa'
+
export default defineConfig({
- plugins: [nitro({ preset: 'node-middleware' }), tanstackStart(), viteReact()],
+ plugins: [
+ ...(isSpa ? [] : [nitro({ preset: 'node-middleware' })]),
+ tanstackStart({ spa: { enabled: isSpa } }),
+ viteReact(),
+ ],
})
diff --git a/packages/docs/src/components/SPACharts.astro b/packages/docs/src/components/SPACharts.astro
new file mode 100644
index 0000000..b3172d3
--- /dev/null
+++ b/packages/docs/src/components/SPACharts.astro
@@ -0,0 +1,18 @@
+---
+import ChartTabs from './ChartTabs.astro'
+import SPAFPChart from './SPAFPChart.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/SPAFPChart.astro b/packages/docs/src/components/SPAFPChart.astro
new file mode 100644
index 0000000..95f3d93
--- /dev/null
+++ b/packages/docs/src/components/SPAFPChart.astro
@@ -0,0 +1,10 @@
+---
+import { chartSPAFPData } 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 1152392..ec52200 100644
--- a/packages/docs/src/content.config.ts
+++ b/packages/docs/src/content.config.ts
@@ -56,6 +56,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 566250e..7f512f9 100644
--- a/packages/docs/src/lib/collections.ts
+++ b/packages/docs/src/lib/collections.ts
@@ -50,6 +50,28 @@ export const chartDuplicateDependencyData = starterStats
focused: f.isFocused,
}))
+export const chartSPAFPData = runtimeEntries
+ .map((entry) => entry.data)
+ .sort((a, b) => a.order - b.order)
+ .filter((f) => f?.name != null && Number.isFinite(f.spaFirstPaintMs))
+ .map((f) => ({
+ name: f.name,
+ value: f.spaFirstPaintMs!,
+ 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 const coreJsTableData = starterStats.map((f) => {
const hasCorejs = (f.vendoredCoreJsUnnecessaryModules?.length ?? 0) > 0
return {
diff --git a/packages/docs/src/pages/index.astro b/packages/docs/src/pages/index.astro
index c0bee79..1ebd62e 100644
--- a/packages/docs/src/pages/index.astro
+++ b/packages/docs/src/pages/index.astro
@@ -7,6 +7,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'
@@ -60,6 +61,17 @@ import Layout from '../layouts/Layout.astro'
+ SPA Performance
+
+
+ Next.js, TanStack Start, and React Router default to SSR with no per-route
+ opt-out. Next.js wraps the SPA table in a dynamic import with ssr: false to prevent build-time prerendering. TanStack Start uses its built-in spa mode.
+ React Router disables SSR entirely via ssr: false in its config.
+ All other frameworks (Nuxt, SvelteKit, SolidStart, Astro) disable SSR per-route
+ without a separate build.
+