From fff1a299041280a79780fd74fda217a5adef819e Mon Sep 17 00:00:00 2001 From: JoseDFlorez <63378897+JoseDFN@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:55:31 -0500 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20feat:=20migrate=20the=20webpage?= =?UTF-8?q?=20to=20nextjs=20static=20exports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the Vite entry point with Next.js App Router pages for the home page and seven documentation topics. Preserve the shared visual design, saved preferences, evidence traces, motion and standalone reports while supporting direct URLs and legacy bookmarks. Add a loopback static preview, repository base-path handling, retained Next.js licensing and byte-checked publication tooling. Normalize the Windows segment export filenames without changing payload bytes, rejecting collisions before any move. Update screenshot tools and cover exported HTML, navigation, runtime requests, preview containment and publication boundaries. Validate with 144 passing Node tests, 73 browser tests at the root and 73 at the repository prefix, unchanged deterministic examples, frozen installation, a clean dependency audit and isolated package installation smoke checks. --- .gitignore | 2 + playwright.config.mjs | 6 +- pnpm-lock.yaml | 771 ++++++++++++--------- scripts/capture-site-screenshots.mjs | 56 +- scripts/check-site-publication.mjs | 92 ++- scripts/package-site.mjs | 2 +- site/.gitignore | 2 + site/THIRD_PARTY_NOTICES.md | 4 +- site/index.html | 39 -- site/licenses/next-LICENSE.md | 21 + site/next-env.d.ts | 7 + site/next.config.mjs | 19 + site/package.json | 11 +- site/scripts/capture-docs.mjs | 21 +- site/scripts/finalize-export.mjs | 25 + site/scripts/normalize-segments.mjs | 64 ++ site/scripts/preview.mjs | 110 +++ site/src/App.tsx | 500 +++++-------- site/src/DocumentationPage.tsx | 19 + site/src/Home.tsx | 248 +++++++ site/src/app/docs/[slug]/page.tsx | 23 + site/src/app/docs/page.tsx | 5 + site/src/app/layout.tsx | 40 ++ site/src/app/not-found.tsx | 15 + site/src/app/page.tsx | 4 + site/src/components/Documentation.tsx | 48 +- site/src/components/Examples.tsx | 3 +- site/src/components/react-bits/DotGrid.tsx | 5 +- site/src/main.tsx | 13 - site/src/paths.ts | 3 + site/src/preferences-context.tsx | 16 + site/src/useReducedMotion.ts | 5 +- site/src/vite-env.d.ts | 1 - site/tsconfig.json | 18 +- site/vite.config.ts | 18 - tests/browser/next-static.spec.mjs | 91 +++ tests/browser/site-docs.spec.mjs | 56 +- tests/browser/site-regressions.spec.mjs | 8 +- tests/node/site-publication.test.mjs | 130 +++- tests/node/static-preview.test.mjs | 80 +++ tests/node/static-segments.test.mjs | 76 ++ 41 files changed, 1806 insertions(+), 871 deletions(-) delete mode 100644 site/index.html create mode 100644 site/licenses/next-LICENSE.md create mode 100644 site/next-env.d.ts create mode 100644 site/next.config.mjs create mode 100644 site/scripts/finalize-export.mjs create mode 100644 site/scripts/normalize-segments.mjs create mode 100644 site/scripts/preview.mjs create mode 100644 site/src/DocumentationPage.tsx create mode 100644 site/src/Home.tsx create mode 100644 site/src/app/docs/[slug]/page.tsx create mode 100644 site/src/app/docs/page.tsx create mode 100644 site/src/app/layout.tsx create mode 100644 site/src/app/not-found.tsx create mode 100644 site/src/app/page.tsx delete mode 100644 site/src/main.tsx create mode 100644 site/src/paths.ts create mode 100644 site/src/preferences-context.tsx delete mode 100644 site/src/vite-env.d.ts delete mode 100644 site/vite.config.ts create mode 100644 tests/browser/next-static.spec.mjs create mode 100644 tests/node/static-preview.test.mjs create mode 100644 tests/node/static-segments.test.mjs diff --git a/.gitignore b/.gitignore index a456557..c712f96 100644 --- a/.gitignore +++ b/.gitignore @@ -32,5 +32,7 @@ coverage/ test-results/ playwright-report/ site/dist/ +.next/ +site/out/ site/public/demos/ site/public/demo-manifest.json diff --git a/playwright.config.mjs b/playwright.config.mjs index 12533ae..f13edfa 100644 --- a/playwright.config.mjs +++ b/playwright.config.mjs @@ -8,14 +8,14 @@ export default defineConfig({ use: { headless: true, acceptDownloads: true, - baseURL: "http://127.0.0.1:4173", + baseURL: process.env.DBDEP_SITE_URL ?? "http://127.0.0.1:4173", }, reporter: [["list"], ["json", { outputFile: "out/browser-results.json" }]], webServer: { command: "pnpm --filter database-dependency-migration-site preview --port 4173 --strictPort", - url: "http://127.0.0.1:4173", - reuseExistingServer: !process.env.CI, + url: process.env.DBDEP_SITE_URL ?? "http://127.0.0.1:4173", + reuseExistingServer: false, timeout: 30000, }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c41968..359780b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -212,6 +212,9 @@ importers: motion: specifier: 14.0.0 version: 14.0.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + next: + specifier: 16.4.0 + version: 16.4.0(@playwright/test@1.63.0)(@types/node@24.19.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) react: specifier: 19.3.0 version: 19.3.0 @@ -219,6 +222,9 @@ importers: specifier: 19.3.0 version: 19.3.0(react@19.3.0) devDependencies: + '@types/node': + specifier: 24.19.1 + version: 24.19.1 '@types/react': specifier: 19.3.0 version: 19.3.0 @@ -231,127 +237,248 @@ importers: typescript: specifier: 7.0.2 version: 7.0.2 - vite: - specifier: 8.3.4 - version: 8.3.4 packages: + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@fontsource-variable/archivo@5.3.0': resolution: {integrity: sha512-HogK8FJelrD1o7TlZlkIVtHgc20bO5PZRWE7mUeUTdMN055alznQV6/00J00IBeu8FQAH4s3zW9UJNvKExXf+g==} '@fontsource/ibm-plex-mono@5.3.0': resolution: {integrity: sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==} - '@oxc-project/types@0.153.0': - resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==} - - '@pgsql/types@18.0.0': - resolution: {integrity: sha512-jtAY4JU7jdVYZ/Vv3zlZqVP+FRWZokyCLKPPCnAApiaJAElpyyiMC3HRiquBu29kSrVs+rkqZD50SUIb/zyEGw==} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} - '@playwright/test@1.63.0': - resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} - engines: {node: '>=20'} - hasBin: true + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] - '@rolldown/binding-android-arm-eabi@1.2.13': - resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] - '@rolldown/binding-android-arm64@1.2.13': - resolution: {integrity: sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] - '@rolldown/binding-darwin-arm64@1.2.13': - resolution: {integrity: sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==} - engines: {node: ^20.19.0 || >=22.12.0} + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.13': - resolution: {integrity: sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==} - engines: {node: ^20.19.0 || >=22.12.0} + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.13': - resolution: {integrity: sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + libc: [glibc] - '@rolldown/binding-linux-arm-gnueabihf@1.2.13': - resolution: {integrity: sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==} - engines: {node: ^20.19.0 || >=22.12.0} + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} cpu: [arm] os: [linux] + libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.2.13': - resolution: {integrity: sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.13': - resolution: {integrity: sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==} - engines: {node: ^20.19.0 || >=22.12.0} + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.13': - resolution: {integrity: sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==} - engines: {node: ^20.19.0 || >=22.12.0} + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.13': - resolution: {integrity: sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==} - engines: {node: ^20.19.0 || >=22.12.0} + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.13': - resolution: {integrity: sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.13': - resolution: {integrity: sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==} - engines: {node: ^20.19.0 || >=22.12.0} + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.13': - resolution: {integrity: sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==} - engines: {node: ^20.19.0 || >=22.12.0} + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} cpu: [arm64] - os: [openharmony] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@next/env@16.4.0': + resolution: {integrity: sha512-pyLlT5oafGlX8X7xjgGNXz5dNIDSxREm+qD6i9VLSZiZFopA/XWl8NM/KPmLx/8EG/++c49iid3gNWSQW1+2dQ==} + + '@next/swc-darwin-arm64@16.4.0': + resolution: {integrity: sha512-yyZ+9MgViAKLLb5l1gPBGjRwnq3teNLb5Zx6JEw8LzNSTjlsnp+baUHFxTgl7PpjCmKf48VQfyipG1eBflb48Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.4.0': + resolution: {integrity: sha512-RePEEejEj1Auf5s9qEcG0JbcDP3ceNUS/ofNX8AhxzgIjTVlZIHMatlp16YplcSMpsVo/ZMQHJTjbMYJznjGUg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.4.0': + resolution: {integrity: sha512-HhsyCqd+NpmvDDVY/LqkAN24f+QVvXh3ie+PfcwKinWL2Mu826JymQxJXGjAIfmJ4VlHjp4Pe/nZtXtUoEMAqw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.4.0': + resolution: {integrity: sha512-sN9Pf+Zh+RpZAOmbR7d8KAA8T61CVhawtbEJ2Bk6QrRzx6Z/6haaF4UiwBzytM96GRRRWZh9/WALIEO3npMamA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.4.0': + resolution: {integrity: sha512-D5M5xLWQVd1CvR+L7BickM1weRfmM41x3L1NWcnKY/oyJJqr1qwaCgmu3dMAhLBOMdZwSuDCRV6DJRRpiUkbDQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] - '@rolldown/binding-win32-arm64-msvc@1.2.13': - resolution: {integrity: sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@next/swc-linux-x64-musl@16.4.0': + resolution: {integrity: sha512-8d5WLS6a9YalqpOgXdlbvonFu+sIgjA+CO6tGzetrpPVcUL56foa7B7664h41rAlbLi5fJwK4vWji3zf4QDy+w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.4.0': + resolution: {integrity: sha512-sQuOIiW3snb48UwrBcDMvFA/u03qtw2LeuSIxFIQ9AJuW9UG/4Ddsfo+eV73PnZwBhyncMtsKVh7PrzMol2/fA==} + engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.13': - resolution: {integrity: sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==} - engines: {node: ^20.19.0 || >=22.12.0} + '@next/swc-win32-x64-msvc@16.4.0': + resolution: {integrity: sha512-6/33lCuxpdHUy27RegyGM/5uTpF7YZd3UR4D0gpkOA/tWY5YR7msbQjqPgCZmSmxrrTLGCXYA4A8vT2H5DPGTQ==} + engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@pgsql/types@18.0.0': + resolution: {integrity: sha512-jtAY4JU7jdVYZ/Vv3zlZqVP+FRWZokyCLKPPCnAApiaJAElpyyiMC3HRiquBu29kSrVs+rkqZD50SUIb/zyEGw==} + + '@playwright/test@1.63.0': + resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} + engines: {node: '>=20'} + hasBin: true + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@types/node@24.19.1': + resolution: {integrity: sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==} '@types/react-dom@19.3.0': resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} @@ -492,6 +619,17 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + baseline-browser-mapping@2.11.28: + resolution: {integrity: sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==} + engines: {node: '>=6.0.0'} + hasBin: true + + caniuse-lite@1.0.30001815: + resolution: {integrity: sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + commander@15.0.0: resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} engines: {node: '>=22.12.0'} @@ -509,15 +647,6 @@ packages: fast-uri@3.1.8: resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fflate@0.8.3: resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} @@ -532,11 +661,6 @@ packages: react-dom: optional: true - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - gsap@3.15.0: resolution: {integrity: sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==} @@ -560,80 +684,6 @@ packages: libpg-query@18.1.5: resolution: {integrity: sha512-xsksgH106LyVUylIp2bJz1MPFmVVIGKbB8belGWmh+J8/i38w8eM1twDIBnd97tzeNQFctH9MWKMthl22+fWcQ==} - lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.33.0: - resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} - engines: {node: '>= 12.0.0'} - motion-dom@14.0.0: resolution: {integrity: sha512-aU3ApXo1yTpeUi3JU+jbDk2TELjxPE3MbBMUP7nZHlARb6RweJK+ojmLjpuB0YsY6zhbBl1YWGo8LMsI1/Llqw==} @@ -656,6 +706,27 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + next@16.4.0: + resolution: {integrity: sha512-N2C+9q89Z/oLrsepSRS4jRMtwmUZAoZuAHU0e9T6+sAB5DdhADmrnOW+zjYijlrYFmVfmQhGVGy9eMFoB7UgKg==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + pg-cloudflare@1.4.1: resolution: {integrity: sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==} @@ -693,10 +764,6 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@4.0.7: - resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} - engines: {node: '>=12'} - playwright-core@1.63.0: resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} engines: {node: '>=20'} @@ -707,8 +774,8 @@ packages: engines: {node: '>=20'} hasBin: true - postcss@8.5.29: - resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==} + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -745,14 +812,23 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - rolldown@1.2.13: - resolution: {integrity: sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - scheduler@0.28.0: resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + source-map-js@1.2.2: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} @@ -761,9 +837,18 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -773,48 +858,8 @@ packages: engines: {node: '>=16.20.0'} hasBin: true - vite@8.3.4: - resolution: {integrity: sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.7.1 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} @@ -822,64 +867,161 @@ packages: snapshots: + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@fontsource-variable/archivo@5.3.0': {} '@fontsource/ibm-plex-mono@5.3.0': {} - '@oxc-project/types@0.153.0': {} + '@img/colour@1.1.0': + optional: true - '@pgsql/types@18.0.0': {} + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true - '@playwright/test@1.63.0': + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': dependencies: - playwright: 1.63.0 + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true - '@rolldown/binding-android-arm-eabi@1.2.13': + '@img/sharp-libvips-linux-arm@1.3.4': optional: true - '@rolldown/binding-android-arm64@1.2.13': + '@img/sharp-libvips-linux-ppc64@1.3.4': optional: true - '@rolldown/binding-darwin-arm64@1.2.13': + '@img/sharp-libvips-linux-riscv64@1.3.4': optional: true - '@rolldown/binding-darwin-x64@1.2.13': + '@img/sharp-libvips-linux-s390x@1.3.4': optional: true - '@rolldown/binding-freebsd-x64@1.2.13': + '@img/sharp-libvips-linux-x64@1.3.4': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.13': + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.13': + '@img/sharp-libvips-linuxmusl-x64@1.3.4': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.13': + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.13': + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.13': + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 optional: true - '@rolldown/binding-linux-x64-gnu@1.2.13': + '@img/sharp-wasm32@0.35.5': + dependencies: + '@emnapi/runtime': 1.11.3 optional: true - '@rolldown/binding-linux-x64-musl@1.2.13': + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 optional: true - '@rolldown/binding-openharmony-arm64@1.2.13': + '@img/sharp-win32-arm64@0.35.5': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.13': + '@img/sharp-win32-ia32@0.35.5': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.13': + '@img/sharp-win32-x64@0.35.5': optional: true - '@rolldown/pluginutils@1.0.1': {} + '@next/env@16.4.0': {} + + '@next/swc-darwin-arm64@16.4.0': + optional: true + + '@next/swc-darwin-x64@16.4.0': + optional: true + + '@next/swc-linux-arm64-gnu@16.4.0': + optional: true + + '@next/swc-linux-arm64-musl@16.4.0': + optional: true + + '@next/swc-linux-x64-gnu@16.4.0': + optional: true + + '@next/swc-linux-x64-musl@16.4.0': + optional: true + + '@next/swc-win32-arm64-msvc@16.4.0': + optional: true + + '@next/swc-win32-x64-msvc@16.4.0': + optional: true + + '@pgsql/types@18.0.0': {} + + '@playwright/test@1.63.0': + dependencies: + playwright: 1.63.0 + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@types/node@24.19.1': + dependencies: + undici-types: 7.24.6 '@types/react-dom@19.3.0(@types/react@19.3.0)': dependencies: @@ -960,20 +1102,23 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + baseline-browser-mapping@2.11.28: {} + + caniuse-lite@1.0.30001815: {} + + client-only@0.0.1: {} + commander@15.0.0: {} csstype@3.2.3: {} - detect-libc@2.1.2: {} + detect-libc@2.1.2: + optional: true fast-deep-equal@3.1.3: {} fast-uri@3.1.8: {} - fdir@6.5.0(picomatch@4.0.7): - optionalDependencies: - picomatch: 4.0.7 - fflate@0.8.3: {} framer-motion@14.0.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0): @@ -985,9 +1130,6 @@ snapshots: react: 19.3.0 react-dom: 19.3.0(react@19.3.0) - fsevents@2.3.3: - optional: true - gsap@3.15.0: {} json-schema-traverse@1.0.0: {} @@ -1000,55 +1142,6 @@ snapshots: dependencies: '@pgsql/types': 18.0.0 - lightningcss-android-arm64@1.33.0: - optional: true - - lightningcss-darwin-arm64@1.33.0: - optional: true - - lightningcss-darwin-x64@1.33.0: - optional: true - - lightningcss-freebsd-x64@1.33.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.33.0: - optional: true - - lightningcss-linux-arm64-gnu@1.33.0: - optional: true - - lightningcss-linux-arm64-musl@1.33.0: - optional: true - - lightningcss-linux-x64-gnu@1.33.0: - optional: true - - lightningcss-linux-x64-musl@1.33.0: - optional: true - - lightningcss-win32-arm64-msvc@1.33.0: - optional: true - - lightningcss-win32-x64-msvc@1.33.0: - optional: true - - lightningcss@1.33.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.33.0 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-freebsd-x64: 1.33.0 - lightningcss-linux-arm-gnueabihf: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-arm64-msvc: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 - motion-dom@14.0.0: dependencies: motion-utils: 14.0.0 @@ -1065,6 +1158,32 @@ snapshots: nanoid@3.3.20: {} + next@16.4.0(@playwright/test@1.63.0)(@types/node@24.19.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + '@next/env': 16.4.0 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.28 + caniuse-lite: 1.0.30001815 + postcss: 8.5.23 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + styled-jsx: 5.1.6(react@19.3.0) + optionalDependencies: + '@next/swc-darwin-arm64': 16.4.0 + '@next/swc-darwin-x64': 16.4.0 + '@next/swc-linux-arm64-gnu': 16.4.0 + '@next/swc-linux-arm64-musl': 16.4.0 + '@next/swc-linux-x64-gnu': 16.4.0 + '@next/swc-linux-x64-musl': 16.4.0 + '@next/swc-win32-arm64-msvc': 16.4.0 + '@next/swc-win32-x64-msvc': 16.4.0 + '@playwright/test': 1.63.0 + sharp: 0.35.5(@types/node@24.19.1) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + pg-cloudflare@1.4.1: optional: true @@ -1102,15 +1221,13 @@ snapshots: picocolors@1.1.1: {} - picomatch@4.0.7: {} - playwright-core@1.63.0: {} playwright@1.63.0: dependencies: playwright-core: 1.63.0 - postcss@8.5.29: + postcss@8.5.23: dependencies: nanoid: 3.3.20 picocolors: 1.1.1 @@ -1137,37 +1254,53 @@ snapshots: require-from-string@2.0.2: {} - rolldown@1.2.13: + scheduler@0.28.0: {} + + semver@7.8.5: + optional: true + + sharp@0.35.5(@types/node@24.19.1): dependencies: - '@oxc-project/types': 0.153.0 - '@rolldown/pluginutils': 1.0.1 + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 optionalDependencies: - '@rolldown/binding-android-arm-eabi': 1.2.13 - '@rolldown/binding-android-arm64': 1.2.13 - '@rolldown/binding-darwin-arm64': 1.2.13 - '@rolldown/binding-darwin-x64': 1.2.13 - '@rolldown/binding-freebsd-x64': 1.2.13 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.13 - '@rolldown/binding-linux-arm64-gnu': 1.2.13 - '@rolldown/binding-linux-arm64-musl': 1.2.13 - '@rolldown/binding-linux-ppc64-gnu': 1.2.13 - '@rolldown/binding-linux-s390x-gnu': 1.2.13 - '@rolldown/binding-linux-x64-gnu': 1.2.13 - '@rolldown/binding-linux-x64-musl': 1.2.13 - '@rolldown/binding-openharmony-arm64': 1.2.13 - '@rolldown/binding-win32-arm64-msvc': 1.2.13 - '@rolldown/binding-win32-x64-msvc': 1.2.13 - - scheduler@0.28.0: {} + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 24.19.1 + optional: true source-map-js@1.2.2: {} split2@4.2.0: {} - tinyglobby@0.2.17: + styled-jsx@5.1.6(react@19.3.0): dependencies: - fdir: 6.5.0(picomatch@4.0.7) - picomatch: 4.0.7 + client-only: 0.0.1 + react: 19.3.0 tslib@2.8.1: {} @@ -1194,14 +1327,6 @@ snapshots: '@typescript/typescript-win32-arm64': 7.0.2 '@typescript/typescript-win32-x64': 7.0.2 - vite@8.3.4: - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.7 - postcss: 8.5.29 - rolldown: 1.2.13 - tinyglobby: 0.2.17 - optionalDependencies: - fsevents: 2.3.3 + undici-types@7.24.6: {} xtend@4.0.2: {} diff --git a/scripts/capture-site-screenshots.mjs b/scripts/capture-site-screenshots.mjs index ad74d38..4b2d5c0 100644 --- a/scripts/capture-site-screenshots.mjs +++ b/scripts/capture-site-screenshots.mjs @@ -1,47 +1,22 @@ #!/usr/bin/env node -import { createServer } from "node:http"; import { readFileSync, existsSync, mkdirSync } from "node:fs"; -import { join, resolve, relative, extname, sep, isAbsolute } from "node:path"; +import { join, resolve } from "node:path"; import { chromium } from "@playwright/test"; import { ROOT, save } from "./tooling.mjs"; -const built = join(ROOT, "site/dist"), - destination = join(ROOT, "site/screenshots"); +import { createPreviewServer } from "../site/scripts/preview.mjs"; +const built = join(ROOT, "site/out"), + destination = process.env.DBDEP_CAPTURE_DIR + ? resolve(process.env.DBDEP_CAPTURE_DIR) + : join(ROOT, "site/screenshots"); if (!existsSync(join(built, "index.html"))) throw new Error("Build the website before capturing it"); mkdirSync(destination, { recursive: true }); -const server = createServer((request, response) => { - const path = resolve( - built, - "." + - decodeURIComponent(new URL(request.url, "http://localhost").pathname), - ), - rel = relative(built, path); - if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) { - response.writeHead(403); - response.end(); - return; - } - const target = - existsSync(path) && extname(path) ? path : join(built, "index.html"); - const mime = - { - ".html": "text/html", - ".js": "text/javascript", - ".css": "text/css", - ".json": "application/json", - ".woff2": "font/woff2", - ".png": "image/png", - }[extname(target)] ?? "text/plain"; - try { - response.writeHead(200, { "Content-Type": mime }); - response.end(readFileSync(target)); - } catch { - response.writeHead(404); - response.end(); - } -}); +const basePath = JSON.parse( + readFileSync(join(built, "site-config.json"), "utf8"), +).base_path; +const server = createPreviewServer({ built, basePath }); await new Promise((ok) => server.listen(0, "127.0.0.1", ok)); -const url = `http://127.0.0.1:${server.address().port}`, +const url = `http://127.0.0.1:${server.address().port}${basePath}`, browser = await chromium.launch(), captured = []; try { @@ -64,6 +39,12 @@ try { ); await page.goto(url); await page.locator("h1").waitFor(); + await page.waitForFunction( + ({ locale, theme }) => + document.documentElement.lang === locale && + document.documentElement.dataset.theme === theme, + { locale, theme }, + ); await page.evaluate(() => document.fonts.ready); if ( await page.evaluate( @@ -82,7 +63,7 @@ try { }); captured.push(filename); } - await page.goto(url + "/#/docs"); + await page.goto(url + "/docs/"); await page.locator("h1").waitFor(); await page.evaluate(() => document.fonts.ready); const filename = `${label}-docs.png`; @@ -95,6 +76,7 @@ try { } save(join(ROOT, "out/site-captures.json"), { captured, + base_path: basePath, reduced_motion: true, self_hosted_fonts: true, }); diff --git a/scripts/check-site-publication.mjs b/scripts/check-site-publication.mjs index 5e25f3e..07e6a22 100644 --- a/scripts/check-site-publication.mjs +++ b/scripts/check-site-publication.mjs @@ -1,13 +1,13 @@ #!/usr/bin/env node /** Reject unintended public files and stale curated reports before Pages packaging. */ import { readdirSync, readFileSync, lstatSync } from "node:fs"; -import { join, resolve } from "node:path"; +import { join, resolve, relative } from "node:path"; import { fileURLToPath } from "node:url"; import { ROOT, files, sha256, save, verify_built_docs } from "./tooling.mjs"; export function check_site_publication( root = ROOT, - built = join(root, "site/dist"), + built = join(root, "site/out"), ) { verify_built_docs(root, built); const copies = new Map([ @@ -33,6 +33,79 @@ export function check_site_publication( `licenses/${path.slice(join(root, "site/licenses").length + 1).replaceAll("\\", "/")}`, path, ); + const compiled = join(root, "site/.next"); + const config = JSON.parse( + readFileSync(join(built, "site-config.json"), "utf8"), + ); + const manifest = JSON.parse( + readFileSync(join(compiled, "routes-manifest.json"), "utf8"), + ); + if ( + config.format !== "dbdep-static-site/1" || + config.next !== "16.4.0" || + config.base_path !== manifest.basePath || + typeof config.base_path !== "string" || + (config.base_path && + !/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(config.base_path)) || + Object.keys(config).sort().join(",") !== "base_path,format,next" + ) + throw Error( + "Static export base path or metadata differs from the reviewed build", + ); + copies.set("site-config.json", null); + // Compare exported route bodies with the compiler's private artifacts. Only + // deliberately published routes can contribute HTML and RSC payloads. + const topics = [ + "introduction", + "installation", + "quickstart", + "good-requests", + "command-reference", + "confidence", + "safety-limits", + ]; + const stems = [ + "index", + "docs", + "_not-found", + ...topics.map((topic) => `docs/${topic}`), + ]; + for (const stem of stems) { + const prefix = stem === "index" ? "" : `${stem}/`; + copies.set( + `${prefix}index.html`, + join(compiled, "server/app", `${stem}.html`), + ); + copies.set( + `${prefix}index.txt`, + join(compiled, "server/app", `${stem}.rsc`), + ); + const segments = join(compiled, "server/app", `${stem}.segments`); + for (const path of files(segments)) { + const name = relative(segments, path).replaceAll("\\", "/"); + if (!/^(?:[A-Za-z0-9_.$-]+\/)*[A-Za-z0-9_.$-]+\.segment\.rsc$/.test(name)) + throw Error("Unexpected compiled route segment"); + copies.set( + `${prefix}__next.${name.replace(/\.segment\.rsc$/, ".txt").replaceAll("/", ".")}`, + path, + ); + } + } + for (const name of ["404.html", "404/index.html"]) + copies.set(name, join(compiled, "server/app/_not-found.html")); + const staticRoot = join(compiled, "static"); + for (const path of files(staticRoot)) { + const name = relative(staticRoot, path).replaceAll("\\", "/"); + if ( + !/^(?:chunks\/(?:[A-Za-z0-9_.-]+\/)*[A-Za-z0-9_.-]+\.(?:js|css)|media\/[A-Za-z0-9_.-]+\.woff2?|[A-Za-z0-9_-]+\/_(?:ssgManifest|clientMiddlewareManifest|buildManifest)\.js)$/.test( + name, + ) + ) + throw Error( + "Unexpected compiled public asset; source maps are not published", + ); + copies.set(`_next/static/${name}`, path); + } const observed = []; function visit(directory, prefix = "") { for (const entry of readdirSync(directory, { withFileTypes: true })) { @@ -46,16 +119,12 @@ export function check_site_publication( } if (!entry.isFile()) throw Error("Public build contains a non-regular file"); - if ( - name !== "index.html" && - !copies.has(name) && - !/^assets\/[A-Za-z0-9_.-]+\.(?:js|css|woff2?)$/.test(name) - ) + if (!copies.has(name)) throw Error( "Unexpected public file; inspect the local build before publishing", ); const bytes = readFileSync(path); - if (copies.has(name) && !bytes.equals(readFileSync(copies.get(name)))) + if (copies.get(name) && !bytes.equals(readFileSync(copies.get(name)))) throw Error("Public source copy differs; rebuild before publishing"); observed.push({ path: name, sha256: sha256(bytes) }); } @@ -64,12 +133,17 @@ export function check_site_publication( for (const name of ["index.html", ...copies.keys()]) if (!observed.some((file) => file.path === name)) throw Error("Expected public file is missing"); - if (!observed.some((file) => /^assets\/.*\.js$/.test(file.path))) + if ( + !observed.some((file) => /^_next\/static\/chunks\/.*\.js$/.test(file.path)) + ) throw Error("Built application JavaScript is missing"); return { valid: true, public_demo_files: 12, documentation_source_matches: 2, + static_content_routes: 9, + base_path: config.base_path, + compiled_export_bytes_match: true, private_workspace_directories_published: false, files: observed.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0, diff --git a/scripts/package-site.mjs b/scripts/package-site.mjs index 01395af..e920ecc 100644 --- a/scripts/package-site.mjs +++ b/scripts/package-site.mjs @@ -11,7 +11,7 @@ import { verify_built_docs, } from "./tooling.mjs"; const site = join(ROOT, "site"), - built = join(site, "dist"); + built = join(site, "out"); if (!existsSync(join(built, "index.html"))) throw new Error("Build the webpage with pnpm build before packaging"); verify_built_docs(ROOT, built); diff --git a/site/.gitignore b/site/.gitignore index f3cd1cb..386fdec 100644 --- a/site/.gitignore +++ b/site/.gitignore @@ -1,5 +1,7 @@ node_modules/ dist/ +out/ +.next/ public/demos/ public/docs/ src/data/demos.json diff --git a/site/THIRD_PARTY_NOTICES.md b/site/THIRD_PARTY_NOTICES.md index e2776f4..c4b408c 100644 --- a/site/THIRD_PARTY_NOTICES.md +++ b/site/THIRD_PARTY_NOTICES.md @@ -18,6 +18,8 @@ Local adaptations use accessible text semantics, disable movement for reduced mo ## Runtime libraries and fonts +Next.js 16.4.0 is MIT licensed. Its complete [license](licenses/next-LICENSE.md) is retained with this static export. Build-time prerendering does not add a hosted server to this website. + | Dependency | Pinned version | License / retained notice | | ---------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | React, React DOM | 19.3.0 | MIT, [React license](licenses/react-LICENSE), [React DOM license](licenses/react-dom-LICENSE) | @@ -27,4 +29,4 @@ Local adaptations use accessible text semantics, disable movement for reduced mo | Archivo variable font | @fontsource-variable/archivo 5.3.0 | SIL Open Font License 1.1, [license](licenses/archivo-LICENSE) | | IBM Plex Mono font | @fontsource/ibm-plex-mono 5.3.0 | SIL Open Font License 1.1, [license](licenses/ibm-plex-mono-LICENSE) | -TypeScript and Vite are build dependencies and keep their upstream licenses in their npm packages. Exact dependency integrity and transitive versions are recorded in `package-lock.json`. Fonts and runtime assets are bundled locally, with no runtime CDN or external font request. +TypeScript is a build dependency and retains its upstream license in its npm package. Exact dependency integrity and transitive versions are recorded in `pnpm-lock.yaml`. Fonts and runtime assets are bundled locally, with no runtime CDN or external font request. diff --git a/site/index.html b/site/index.html deleted file mode 100644 index 9343af8..0000000 --- a/site/index.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - Database dependency migration · Whetstone - - - -
- - - - diff --git a/site/licenses/next-LICENSE.md b/site/licenses/next-LICENSE.md new file mode 100644 index 0000000..5948ee9 --- /dev/null +++ b/site/licenses/next-LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 Vercel, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/site/next-env.d.ts b/site/next-env.d.ts new file mode 100644 index 0000000..ce4e94a --- /dev/null +++ b/site/next-env.d.ts @@ -0,0 +1,7 @@ +/// +/// +import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/site/next.config.mjs b/site/next.config.mjs new file mode 100644 index 0000000..7f3b281 --- /dev/null +++ b/site/next.config.mjs @@ -0,0 +1,19 @@ +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; +if (basePath && !/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(basePath)) + throw new Error( + "NEXT_PUBLIC_BASE_PATH must be an absolute path without a trailing slash", + ); + +/** @type {import('next').NextConfig} */ +export default { + output: "export", + trailingSlash: true, + basePath, + env: { NEXT_PUBLIC_BASE_PATH: basePath }, + poweredByHeader: false, + productionBrowserSourceMaps: false, + turbopack: { root: resolve(fileURLToPath(new URL("..", import.meta.url))) }, +}; diff --git a/site/package.json b/site/package.json index 38a7414..2b6e5ca 100644 --- a/site/package.json +++ b/site/package.json @@ -5,9 +5,9 @@ "type": "module", "scripts": { "prepare:demos": "node scripts/prepare-demos.mjs", - "dev": "pnpm run prepare:demos && vite --host 127.0.0.1", - "build": "pnpm run prepare:demos && tsc --noEmit && vite build", - "preview": "vite preview --host 127.0.0.1" + "dev": "pnpm run prepare:demos && next dev --hostname 127.0.0.1", + "build": "pnpm run prepare:demos && next build && node scripts/finalize-export.mjs", + "preview": "node scripts/preview.mjs" }, "dependencies": { "@fontsource-variable/archivo": "5.3.0", @@ -15,14 +15,15 @@ "gsap": "3.15.0", "lenis": "1.3.26", "motion": "14.0.0", + "next": "16.4.0", "react": "19.3.0", "react-dom": "19.3.0" }, "devDependencies": { + "@types/node": "24.19.1", "@types/react": "19.3.0", "@types/react-dom": "19.3.0", "prettier": "3.9.9", - "typescript": "7.0.2", - "vite": "8.3.4" + "typescript": "7.0.2" } } diff --git a/site/scripts/capture-docs.mjs b/site/scripts/capture-docs.mjs index 655c016..0871f48 100644 --- a/site/scripts/capture-docs.mjs +++ b/site/scripts/capture-docs.mjs @@ -1,9 +1,12 @@ import { chromium } from "@playwright/test"; import { mkdir } from "node:fs/promises"; import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; const url = process.env.DBDEP_SITE_URL ?? "http://127.0.0.1:4173"; -const directory = new URL("../screenshots/", import.meta.url); +const directory = process.env.DBDEP_CAPTURE_DIR + ? resolve(process.env.DBDEP_CAPTURE_DIR) + : fileURLToPath(new URL("../screenshots/", import.meta.url)); await mkdir(directory, { recursive: true }); const browser = await chromium.launch(); try { @@ -14,7 +17,7 @@ try { height: 1000, locale: "en", theme: "light", - path: "#/docs", + path: "docs/", }, { name: "docs-desktop-dark", @@ -22,7 +25,7 @@ try { height: 1000, locale: "en", theme: "dark", - path: "#/docs", + path: "docs/", }, { name: "docs-installation", @@ -30,7 +33,7 @@ try { height: 1000, locale: "en", theme: "light", - path: "#/docs/installation", + path: "docs/installation/", }, { name: "docs-mobile-es-dark", @@ -38,7 +41,7 @@ try { height: 844, locale: "es", theme: "dark", - path: "#/docs", + path: "docs/", }, ]) { const context = await browser.newContext({ @@ -52,8 +55,14 @@ try { }, capture); await page.goto(`${url}/${capture.path}`); await page.locator("#docs-heading").waitFor(); + await page.waitForFunction( + ({ locale, theme }) => + document.documentElement.lang === locale && + document.documentElement.dataset.theme === theme, + capture, + ); await page.evaluate(() => document.fonts.ready); - const output = fileURLToPath(new URL(`${capture.name}.png`, directory)); + const output = resolve(directory, `${capture.name}.png`); await page.screenshot({ path: output, fullPage: true }); console.log(output); await context.close(); diff --git a/site/scripts/finalize-export.mjs b/site/scripts/finalize-export.mjs new file mode 100644 index 0000000..59061b0 --- /dev/null +++ b/site/scripts/finalize-export.mjs @@ -0,0 +1,25 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { normalizeStaticSegments } from "./normalize-segments.mjs"; +const site = new URL("../", import.meta.url); +const normalized = normalizeStaticSegments( + fileURLToPath(new URL("out/", site)), +); +const manifest = JSON.parse( + readFileSync(new URL(".next/routes-manifest.json", site), "utf8"), +); +writeFileSync( + new URL("out/site-config.json", site), + JSON.stringify( + { + format: "dbdep-static-site/1", + base_path: manifest.basePath, + next: "16.4.0", + }, + null, + 2, + ) + "\n", +); +console.log( + `Finalized static export at ${fileURLToPath(new URL("out/", site))} with base path ${manifest.basePath || "/"}; normalized ${normalized} Windows segment paths`, +); diff --git a/site/scripts/normalize-segments.mjs b/site/scripts/normalize-segments.mjs new file mode 100644 index 0000000..53e3d6c --- /dev/null +++ b/site/scripts/normalize-segments.mjs @@ -0,0 +1,64 @@ +import { + readdirSync, + realpathSync, + existsSync, + renameSync, + rmdirSync, +} from "node:fs"; +import { resolve, relative, sep, isAbsolute, dirname } from "node:path"; + +// Next.js 16.4 writes nested segment filenames on Windows while its router +// requests dot-separated names. Linux already emits the requested filenames. +// https://github.com/vercel/next.js/issues/92339 +export function normalizeStaticSegments(built) { + const root = realpathSync(built), + moves = []; + function visit(directory) { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const path = resolve(directory, entry.name); + if (entry.isSymbolicLink()) + throw Error("Static export contains a symbolic link"); + if (entry.isDirectory()) { + visit(path); + continue; + } + if (!entry.isFile()) + throw Error("Static export contains a non-regular file"); + const parts = relative(root, path).split(sep), + index = parts.findIndex((p) => p.startsWith("__next.")); + if (index < 0 || index === parts.length - 1) continue; + const tail = parts.slice(index); + if ( + !tail.every((p) => /^[A-Za-z0-9_.$-]+$/.test(p)) || + !tail.at(-1).endsWith(".txt") + ) + throw Error("Unexpected nested static segment path"); + const target = resolve(root, ...parts.slice(0, index), tail.join(".")); + const rel = relative(root, target); + if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) + throw Error("Static segment path escapes export"); + moves.push({ path, target }); + } + } + visit(root); + const targets = new Set(); + for (const { target } of moves) { + if (existsSync(target) || targets.has(target)) + throw Error("Static segment filename collision"); + targets.add(target); + } + for (const { path, target } of moves) renameSync(path, target); + const directories = new Set(moves.map(({ path }) => dirname(path))); + for (const directory of directories) { + if (!existsSync(directory)) continue; + let current = directory; + while (current !== root && readdirSync(current).length === 0) { + const rel = relative(root, current); + if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) + throw Error("Directory escapes export"); + rmdirSync(current); + current = dirname(current); + } + } + return moves.length; +} diff --git a/site/scripts/preview.mjs b/site/scripts/preview.mjs new file mode 100644 index 0000000..f35d496 --- /dev/null +++ b/site/scripts/preview.mjs @@ -0,0 +1,110 @@ +#!/usr/bin/env node +import { createServer } from "node:http"; +import { readFileSync, realpathSync, statSync } from "node:fs"; +import { resolve, relative, isAbsolute, sep, extname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseArgs } from "node:util"; + +const site = fileURLToPath(new URL("..", import.meta.url)); +export function createPreviewServer({ + built = resolve(site, "out"), + basePath, +} = {}) { + const root = realpathSync(built); + if (basePath === undefined) + basePath = JSON.parse( + readFileSync(resolve(root, "site-config.json"), "utf8"), + ).base_path; + if ( + typeof basePath !== "string" || + (basePath && !/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(basePath)) + ) + throw Error("Invalid static export base path"); + const types = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".txt": "text/plain; charset=utf-8", + ".md": "text/plain; charset=utf-8", + ".svg": "image/svg+xml", + ".woff": "font/woff", + ".woff2": "font/woff2", + }; + return createServer((request, response) => { + const send = (status, file) => { + let bytes; + try { + bytes = readFileSync(file); + } catch { + bytes = Buffer.from("Not found"); + } + response.writeHead(status, { + "Content-Type": types[extname(file)] ?? "text/plain; charset=utf-8", + "Content-Length": bytes.length, + "X-Content-Type-Options": "nosniff", + }); + response.end(request.method === "HEAD" ? undefined : bytes); + }; + if (!["GET", "HEAD"].includes(request.method)) { + response.writeHead(405, { Allow: "GET, HEAD" }); + response.end(); + return; + } + try { + const pathname = decodeURIComponent( + new URL(request.url, "http://127.0.0.1").pathname, + ); + if (pathname.includes("\\") || pathname.includes("\0")) + throw Error("Invalid path"); + if (basePath && pathname === basePath) { + response.writeHead(308, { Location: `${basePath}/` }); + response.end(); + return; + } + if (basePath && !pathname.startsWith(`${basePath}/`)) + throw Error("Outside export base path"); + let target = resolve(root, `.${pathname.slice(basePath.length)}`); + if (statSync(target).isDirectory()) + target = resolve(target, "index.html"); + const actual = realpathSync(target), + rel = relative(root, actual); + if ( + !rel || + rel === ".." || + rel.startsWith(`..${sep}`) || + isAbsolute(rel) || + !statSync(actual).isFile() + ) + throw Error("Outside static export"); + send(200, actual); + } catch { + send(404, resolve(root, "404.html")); + } + }); +} + +if ( + process.argv[1] && + resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + const { values } = parseArgs({ + options: { + port: { type: "string", default: "4173" }, + strictPort: { type: "boolean" }, + }, + }); + const port = Number(values.port); + if (!Number.isInteger(port) || port < 1 || port > 65535) + throw Error("Invalid preview port"); + const server = createPreviewServer(); + const basePath = JSON.parse( + readFileSync(resolve(site, "out/site-config.json"), "utf8"), + ).base_path; + server.listen(port, "127.0.0.1", () => + console.log(`Static Next.js export: http://127.0.0.1:${port}${basePath}/`), + ); + const stop = () => server.close(() => process.exit(0)); + process.on("SIGINT", stop); + process.on("SIGTERM", stop); +} diff --git a/site/src/App.tsx b/site/src/App.tsx index 0dc6e52..b98f841 100644 --- a/site/src/App.tsx +++ b/site/src/App.tsx @@ -1,3 +1,4 @@ +"use client"; import { useEffect, useLayoutEffect, @@ -6,6 +7,8 @@ import { type MouseEvent, type ReactNode, } from "react"; +import Link from "next/link"; +import { usePathname, useRouter } from "next/navigation"; import { copies, type Copy, type Locale } from "./i18n"; import { Arrow, Mark } from "./components/Icons"; import { @@ -13,94 +16,79 @@ import { persistPreference, type Theme, } from "./components/Preferences"; -import { ImpactStudy } from "./components/ImpactStudy"; -import { AnalysisFlow } from "./components/AnalysisFlow"; -import { Examples, repository } from "./components/Examples"; -import Magnet from "./components/react-bits/Magnet"; +import { SitePreferences } from "./preferences-context"; import { useReducedMotion } from "./useReducedMotion"; import { usePageMotion } from "./usePageMotion"; +import { assetPath } from "./paths"; import { version } from "../package.json"; -import { CopyButton } from "./components/CopyButton"; -import { Documentation } from "./components/Documentation"; -import { docsCopies, topicOrder, type TopicSlug } from "./docs"; -const installCommand = "pnpm install --frozen-lockfile"; -const analyzeCommand = [ - "pnpm dbdep inspect --ddl examples/ecommerce/schema.sql --repo examples/ecommerce/app --out out/schema.dbdep.json", - "pnpm dbdep validate out/schema.dbdep.json --strict --json", - "pnpm dbdep render out/schema.dbdep.json --object public.customers.id --out out/dependencies.html", -].join("\n\n"); - -function MagneticAction({ children }: { children: ReactNode }) { - const reduced = useReducedMotion(); - const [finePointer, setFinePointer] = useState(false); - useEffect(() => { - const query = window.matchMedia("(hover: hover) and (pointer: fine)"); - const update = () => setFinePointer(query.matches); - update(); - query.addEventListener("change", update); - return () => query.removeEventListener("change", update); - }, []); - return ( - - {children} - - ); -} +const repository = + "https://github.com/whetstone-dev/Database-Dependency-Migration-Visualizer"; function Brand({ c }: { c: Copy }) { return ( - + whetstone/ dbdep - + ); } -export default function App() { +export default function App({ children }: { children: ReactNode }) { const root = useRef(null); - const [hash, setHash] = useState(() => window.location.hash); - const navigateFocus = useRef(false); - const isDocs = hash === "#/docs" || hash.startsWith("#/docs/"); - const slug = isDocs ? hash.slice("#/docs".length).replace(/^\//, "") : ""; + const pathname = usePathname(); + const navigationFocus = useRef(false); + const router = useRouter(); + const isDocs = pathname === "/docs" || pathname.startsWith("/docs/"); const reduced = useReducedMotion(); - const [locale, setLocale] = useState(() => - document.documentElement.lang === "es" ? "es" : "en", - ); - const [theme, setTheme] = useState(() => - document.documentElement.dataset.theme === "dark" ? "dark" : "light", - ); + const [locale, setLocale] = useState("en"); + const [theme, setTheme] = useState("light"); + const [ready, setReady] = useState(false); const c = copies[locale]; usePageMotion(root, reduced || isDocs, locale); + useLayoutEffect(() => { + setLocale(document.documentElement.lang === "es" ? "es" : "en"); + setTheme( + document.documentElement.dataset.theme === "dark" ? "dark" : "light", + ); + setReady(true); + }, []); useEffect(() => { - const changeRoute = () => { - navigateFocus.current = true; - setHash(window.location.hash); + const legacyRoute = () => { + const match = /^#\/docs(?:\/([a-z-]+))?\/?$/.exec(window.location.hash); + if (match) { + navigationFocus.current = true; + router.replace(`/docs/${match[1] ? `${match[1]}/` : ""}`, { + scroll: false, + }); + } }; - window.addEventListener("hashchange", changeRoute); - return () => window.removeEventListener("hashchange", changeRoute); - }, []); - useLayoutEffect(() => { - if (!navigateFocus.current) return; - navigateFocus.current = false; - const target = isDocs + legacyRoute(); + window.addEventListener("hashchange", legacyRoute); + const historyFocus = () => { + navigationFocus.current = true; + }; + window.addEventListener("popstate", historyFocus); + return () => { + window.removeEventListener("hashchange", legacyRoute); + window.removeEventListener("popstate", historyFocus); + }; + }, [router]); + const completeNavigation = (documentation: boolean) => { + const fragment = window.location.hash.slice(1); + const target = documentation ? document.getElementById("docs-heading") - : (document.getElementById(hash.slice(1)) ?? - document.getElementById("main")); - if (isDocs || hash === "#" || hash === "") - window.scrollTo({ top: 0, behavior: "instant" }); - else target?.scrollIntoView({ behavior: "instant" }); - target?.focus({ preventScroll: true }); - }, [hash, isDocs]); + : document.getElementById(fragment || "main"); + if ((!navigationFocus.current && !fragment) || !target) return; + navigationFocus.current = false; + if (fragment && !documentation) + target.scrollIntoView({ behavior: "instant" }); + else window.scrollTo({ top: 0, behavior: "instant" }); + target.focus({ preventScroll: true }); + }; const handleRoute = (event: MouseEvent) => { if ( event.defaultPrevented || @@ -121,41 +109,41 @@ export default function App() { const main = document.getElementById("main"); main?.scrollIntoView({ behavior: "instant" }); main?.focus({ preventScroll: true }); - return; - } - if ( - !href || - (!href.startsWith("#/docs") && - !(isDocs && href.startsWith("#") && href !== "#main")) - ) - return; - event.preventDefault(); - if (href === hash) { - document.getElementById("docs-heading")?.focus({ preventScroll: true }); - return; + } else if (href && !href.startsWith("#")) { + const destination = new URL(href, window.location.href); + if ( + destination.origin === window.location.origin && + destination.pathname !== window.location.pathname + ) + navigationFocus.current = true; + if ( + destination.origin === window.location.origin && + destination.pathname === window.location.pathname && + !destination.hash + ) { + event.preventDefault(); + window.scrollTo({ top: 0, behavior: "instant" }); + document + .getElementById(isDocs ? "docs-heading" : "main") + ?.focus({ preventScroll: true }); + } } - history.pushState(null, "", href); - navigateFocus.current = true; - setHash(href); }; useEffect(() => { + if (!ready) return; document.documentElement.lang = locale; - const topic = topicOrder.includes(slug as TopicSlug) - ? docsCopies[locale].topics[slug as TopicSlug] - : null; - document.title = isDocs - ? `${topic?.title ?? docsCopies[locale].title} · dbdep` - : c.title; + if (!isDocs) document.title = c.title; document .querySelector('meta[name="description"]') ?.setAttribute("content", c.description); - }, [locale, c, isDocs, slug]); + }, [locale, c, isDocs, ready]); useEffect(() => { + if (!ready) return; document.documentElement.dataset.theme = theme; document .querySelector('meta[name="theme-color"]') ?.setAttribute("content", theme === "dark" ? "#10141c" : "#f8fafc"); - }, [theme]); + }, [theme, ready]); useEffect(() => { const pointer = () => { document.documentElement.dataset.input = "pointer"; @@ -172,266 +160,88 @@ export default function App() { document.removeEventListener("wheel", pointer); }; }, []); + const sectionLink = (id: string, label: string) => + isDocs ? ( + + {label} + + ) : ( + {label} + ); return ( -
- - {c.skip} - - +
+ + + ); } diff --git a/site/src/DocumentationPage.tsx b/site/src/DocumentationPage.tsx new file mode 100644 index 0000000..a61b4ef --- /dev/null +++ b/site/src/DocumentationPage.tsx @@ -0,0 +1,19 @@ +"use client"; +import { useEffect, useLayoutEffect } from "react"; +import { Documentation } from "./components/Documentation"; +import { docsCopies, topicOrder, type TopicSlug } from "./docs"; +import { useSitePreferences } from "./preferences-context"; + +export default function DocumentationPage({ slug }: { slug: string }) { + const { c, locale, completeNavigation } = useSitePreferences(); + useLayoutEffect(() => { + completeNavigation(true); + }, [slug]); + useEffect(() => { + const topic = topicOrder.includes(slug as TopicSlug) + ? docsCopies[locale].topics[slug as TopicSlug] + : null; + document.title = `${topic?.title ?? docsCopies[locale].title} · dbdep`; + }, [slug, locale]); + return ; +} diff --git a/site/src/Home.tsx b/site/src/Home.tsx new file mode 100644 index 0000000..35398b6 --- /dev/null +++ b/site/src/Home.tsx @@ -0,0 +1,248 @@ +"use client"; +import { useEffect, useLayoutEffect, useState, type ReactNode } from "react"; +import { Arrow } from "./components/Icons"; +import { ImpactStudy } from "./components/ImpactStudy"; +import { AnalysisFlow } from "./components/AnalysisFlow"; +import { Examples, repository } from "./components/Examples"; +import Magnet from "./components/react-bits/Magnet"; +import { CopyButton } from "./components/CopyButton"; +import { useReducedMotion } from "./useReducedMotion"; +import { useSitePreferences } from "./preferences-context"; +import Link from "next/link"; + +const installCommand = "pnpm install --frozen-lockfile"; +const analyzeCommand = [ + "pnpm dbdep inspect --ddl examples/ecommerce/schema.sql --repo examples/ecommerce/app --out out/schema.dbdep.json", + "pnpm dbdep validate out/schema.dbdep.json --strict --json", + "pnpm dbdep render out/schema.dbdep.json --object public.customers.id --out out/dependencies.html", +].join("\n\n"); + +function MagneticAction({ children }: { children: ReactNode }) { + const reduced = useReducedMotion(); + const [finePointer, setFinePointer] = useState(false); + useEffect(() => { + const query = window.matchMedia("(hover: hover) and (pointer: fine)"); + const update = () => setFinePointer(query.matches); + update(); + query.addEventListener("change", update); + return () => query.removeEventListener("change", update); + }, []); + return ( + + {children} + + ); +} + +export default function Home() { + const { c, theme, completeNavigation } = useSitePreferences(); + useLayoutEffect(() => { + completeNavigation(false); + }, []); + const reduced = useReducedMotion(); + return ( +
+
+
+ + + {c.hero.eyebrow} + +

+ {c.hero.lead} +
+ {c.hero.accent} +
+ {c.hero.end} +

+

{c.hero.body}

+ +

{c.hero.foot}

+
+ +
+
+ {c.scope.map((item, index) => ( + + + {item} + + ))} +
+
+
+
+ {c.workflow.eyebrow} +

+ {c.workflow.lead} +
+ {c.workflow.accent} +

+
+

{c.workflow.body}

+
+ + + {c.workflow.skill} + + +
+ +
+
+ {c.install.eyebrow} +

+ {c.install.lead} +
+ {c.install.accent} +

+

{c.install.body}

+
    + {c.install.steps.map((step, index) => ( +
  1. + 0{index + 1} +
    + {step.title} +

    + {step.body} + {index === 0 && ( + <> + {" "} + + GitHub + + + + )} +

    +
    +
  2. + ))} +
+
+ + {c.install.folders} + + +

Codex

+ ~/.agents/skills/database-dependency-migration +

Claude Code

+ ~/.claude/skills/database-dependency-migration +

{c.install.folderNote}

+
+
+
+
+ + {c.install.root} +
+
+
+ {c.install.install} / NODE 22.18+ + +
+
+              
+                $ 
+                {installCommand}
+              
+            
+
+
+
+ {c.install.commands} + +
+
+              {analyzeCommand}
+            
+
+
+ + {c.install.output} / out/dependencies.html +
+
+
+
+
+ {c.limits.eyebrow} +

+ {c.limits.lead} +
+ {c.limits.accent} +

+ + {c.limits.docs} + + +
+
+ {c.limits.entries.map((entry, index) => ( +
+ 0{index + 1} +
+

{entry.title}

+

{entry.body}

+
+
+ ))} +
+
+
+ ); +} diff --git a/site/src/app/docs/[slug]/page.tsx b/site/src/app/docs/[slug]/page.tsx new file mode 100644 index 0000000..1d1c81a --- /dev/null +++ b/site/src/app/docs/[slug]/page.tsx @@ -0,0 +1,23 @@ +import DocumentationPage from "../../../DocumentationPage"; +import { docsCopies, topicOrder, type TopicSlug } from "../../../docs"; + +export const dynamicParams = false; +export function generateStaticParams() { + return topicOrder.map((slug) => ({ slug })); +} +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: TopicSlug }>; +}) { + const { slug } = await params; + return { title: `${docsCopies.en.topics[slug].title} · dbdep` }; +} +export default async function TopicPage({ + params, +}: { + params: Promise<{ slug: TopicSlug }>; +}) { + const { slug } = await params; + return ; +} diff --git a/site/src/app/docs/page.tsx b/site/src/app/docs/page.tsx new file mode 100644 index 0000000..ca80164 --- /dev/null +++ b/site/src/app/docs/page.tsx @@ -0,0 +1,5 @@ +import DocumentationPage from "../../DocumentationPage"; +export const metadata = { title: "Documentation · dbdep" }; +export default function DocsPage() { + return ; +} diff --git a/site/src/app/layout.tsx b/site/src/app/layout.tsx new file mode 100644 index 0000000..1ba1bed --- /dev/null +++ b/site/src/app/layout.tsx @@ -0,0 +1,40 @@ +import type { Metadata, Viewport } from "next"; +import type { ReactNode } from "react"; +import "@fontsource-variable/archivo"; +import "@fontsource/ibm-plex-mono/400.css"; +import "lenis/dist/lenis.css"; +import "../styles.css"; +import App from "../App"; +import { assetPath } from "../paths"; + +export const metadata: Metadata = { + title: "Database dependency migration · Whetstone", + description: + "Source-backed PostgreSQL dependency graphs, blast-radius analysis, and review-only migration plans for AI coding agents.", + icons: { icon: assetPath("favicon.svg") }, +}; +export const viewport: Viewport = { themeColor: "#f8fafc" }; + +const bootstrap = `(() => { + let theme, locale; + try { + theme = localStorage.getItem("dbdep-theme"); + locale = localStorage.getItem("dbdep-locale"); + } catch {} + const dark = theme === "dark" || (theme !== "light" && matchMedia("(prefers-color-scheme: dark)").matches); + document.documentElement.dataset.theme = dark ? "dark" : "light"; + document.documentElement.lang = locale === "es" ? "es" : "en"; +})();`; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + ', + join(root, "site/.next/routes-manifest.json"), + JSON.stringify({ basePath: "" }), + ); + put( + join(built, "site-config.json"), + JSON.stringify({ + format: "dbdep-static-site/1", + base_path: "", + next: "16.4.0", + }), ); - put(join(built, "assets/index-123abc.js"), "export {};"); for (const slug of ["ecommerce", "analytics", "high-traffic"]) for (const name of [ "report.html", @@ -72,3 +126,69 @@ test("Pages publication rejects a replaced demo even if its filename is allowed" /source copy differs/i, ); })); + +test("Pages publication rejects a changed Next.js bundle", () => + fixture(({ root, built, put }) => { + put( + join(built, "_next/static/chunks/runtime-123abc.js"), + "Unreviewed replacement", + ); + assert.throws( + () => publication.check_site_publication(root, built), + /source copy differs/i, + ); + })); + +test("Pages publication rejects private files disguised as route data", () => + fixture(({ root, built, put }) => { + put( + join(built, "docs/installation/private-catalog.txt"), + "Synthetic private data", + ); + assert.throws( + () => publication.check_site_publication(root, built), + /unexpected public file/i, + ); + })); + +test("Pages publication rejects mismatched base paths", () => + fixture(({ root, built, put }) => { + put( + join(built, "site-config.json"), + JSON.stringify({ + format: "dbdep-static-site/1", + base_path: "/wrong", + next: "16.4.0", + }), + ); + assert.throws( + () => publication.check_site_publication(root, built), + /base path/i, + ); + })); + +test("Pages publication rejects a source map in the runtime directory", () => + fixture(({ root, built, put }) => { + put( + join(built, "_next/static/chunks/runtime-123abc.js.map"), + "Source locations", + ); + assert.throws( + () => publication.check_site_publication(root, built), + /unexpected public file/i, + ); + })); + +test("Pages publication rejects a directory link outside the export", () => + fixture(({ root, built, put }) => { + put(join(root, "private/index.html"), "Synthetic private content"); + symlinkSync( + join(root, "private"), + join(built, "linked"), + process.platform === "win32" ? "junction" : "dir", + ); + assert.throws( + () => publication.check_site_publication(root, built), + /symbolic link/i, + ); + })); diff --git a/tests/node/static-preview.test.mjs b/tests/node/static-preview.test.mjs new file mode 100644 index 0000000..6bf33e6 --- /dev/null +++ b/tests/node/static-preview.test.mjs @@ -0,0 +1,80 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createPreviewServer } from "../../site/scripts/preview.mjs"; +import { checked_remove } from "../../scripts/tooling.mjs"; + +async function preview(work, basePath = "/repo") { + const parent = tmpdir(), + root = mkdtempSync(join(parent, "dbdep-static-preview-")), + built = join(root, "public"); + mkdirSync(join(built, "docs/installation"), { recursive: true }); + writeFileSync(join(built, "index.html"), "Home"); + writeFileSync(join(built, "docs/installation/index.html"), "Installation"); + writeFileSync(join(built, "404.html"), "Missing page"); + const server = createPreviewServer({ built, basePath }); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const url = `http://127.0.0.1:${server.address().port}`; + try { + await work({ root, built, url }); + } finally { + await new Promise((resolve) => server.close(resolve)); + checked_remove(root, parent); + } +} + +test("static preview serves directory indexes and HEAD under the configured prefix", async () => + preview(async ({ url }) => { + const page = await fetch(`${url}/repo/docs/installation/`); + assert.equal(page.status, 200); + assert.equal(await page.text(), "Installation"); + assert.match(page.headers.get("content-type"), /^text\/html/); + const head = await fetch(`${url}/repo/docs/installation/`, { + method: "HEAD", + }); + assert.equal(head.status, 200); + assert.equal(await head.text(), ""); + assert.equal( + head.headers.get("content-length"), + String(Buffer.byteLength("Installation")), + ); + })); + +test("static preview returns real errors instead of the home page", async () => + preview(async ({ url }) => { + for (const path of [ + "/repo/docs/private/", + "/docs/installation/", + "/repo%2f..%2fsecret.txt", + "/repo/%5c..%5csecret.txt", + ]) { + const response = await fetch(url + path); + assert.equal(response.status, 404); + assert.equal(await response.text(), "Missing page"); + } + const response = await fetch(`${url}/repo/`, { + method: "POST", + body: "Ignored", + }); + assert.equal(response.status, 405); + })); + +test("static preview cannot follow a directory link outside the exported tree", async () => + preview(async ({ root, built, url }) => { + const privateDir = join(root, "private"); + mkdirSync(privateDir); + writeFileSync(join(privateDir, "index.html"), "Synthetic private content"); + symlinkSync( + privateDir, + join(built, "linked"), + process.platform === "win32" ? "junction" : "dir", + ); + const response = await fetch(`${url}/repo/linked/`); + assert.equal(response.status, 404); + assert.equal(await response.text(), "Missing page"); + })); diff --git a/tests/node/static-segments.test.mjs b/tests/node/static-segments.test.mjs new file mode 100644 index 0000000..411a521 --- /dev/null +++ b/tests/node/static-segments.test.mjs @@ -0,0 +1,76 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + mkdtempSync, + mkdirSync, + writeFileSync, + readFileSync, + existsSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { checked_remove } from "../../scripts/tooling.mjs"; +import { normalizeStaticSegments } from "../../site/scripts/normalize-segments.mjs"; + +function fixture(work) { + const base = tmpdir(), + root = mkdtempSync(join(base, "dbdep-segments-")); + const put = (name, bytes) => { + const path = join(root, name); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, bytes); + return path; + }; + try { + work({ root, put }); + } finally { + checked_remove(root, base); + } +} +test("Windows segment paths become the exact names requested by the router", () => + fixture(({ root, put }) => { + const source = put( + "docs/introduction/__next.docs/$d$slug/__PAGE__.txt", + Buffer.from([0, 255, 42]), + ); + const target = join( + root, + "docs/introduction/__next.docs.$d$slug.__PAGE__.txt", + ); + put("docs/introduction/__next._tree.txt", "Tree"); + assert.equal(normalizeStaticSegments(root), 1); + assert.equal(existsSync(source), false); + assert.deepEqual(readFileSync(target), Buffer.from([0, 255, 42])); + assert.equal(normalizeStaticSegments(root), 0); + })); +test("segment normalization rejects a collision before changing any source", () => + fixture(({ root, put }) => { + const source = put("docs/__next.docs/__PAGE__.txt", "Original"); + const target = put("docs/__next.docs.__PAGE__.txt", "Conflicting"); + assert.throws(() => normalizeStaticSegments(root), /collision/i); + assert.equal(readFileSync(source, "utf8"), "Original"); + assert.equal(readFileSync(target, "utf8"), "Conflicting"); + })); +test("Linux segment paths and unrelated publication inputs remain unchanged", () => + fixture(({ root, put }) => { + const flat = put("docs/__next.docs.__PAGE__.txt", "Segment"); + const report = put("demos/ecommerce/report.html", "Curated report"); + assert.equal(normalizeStaticSegments(root), 0); + assert.equal(readFileSync(flat, "utf8"), "Segment"); + assert.equal(readFileSync(report, "utf8"), "Curated report"); + })); + +test("segment cleanup handles a parent already removed with its child", () => + fixture(({ root, put }) => { + put("docs/__next.docs/nested/__PAGE__.txt", "Child"); + put("docs/__next.docs/__PAGE__.txt", "Parent"); + assert.equal(normalizeStaticSegments(root), 2); + assert.equal( + readFileSync(join(root, "docs/__next.docs.nested.__PAGE__.txt"), "utf8"), + "Child", + ); + assert.equal( + readFileSync(join(root, "docs/__next.docs.__PAGE__.txt"), "utf8"), + "Parent", + ); + })); From af20e3883dbfc0c85ad12bdc6d7a0057ae0fdff2 Mon Sep 17 00:00:00 2001 From: JoseDFlorez <63378897+JoseDFN@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:55:31 -0500 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=91=B7=20ci:=20validate=20nextjs=20pa?= =?UTF-8?q?ges=20paths=20and=20stop=20dependency=20update=20prs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build and validate both the root export and GitHub Pages repository prefix in the existing CI workflow. Configure Pages before building, pass its base path to Next.js and upload only the validated site/out directory. Preserve manual deployment, SHA-pinned Actions and the PostgreSQL service matrix. Remove Dependabot's version-update configuration at the maintainer's request. Automatic security-fix PRs are already disabled remotely; retain frozen dependency installation and vulnerability auditing. Keep these changes in the same PR as the webpage migration and preserve required maintainer approval. --- .github/dependabot.yml | 14 -------------- .github/workflows/ci.yml | 9 +++++++++ .github/workflows/pages.yml | 9 +++++++-- 3 files changed, 16 insertions(+), 16 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 4e816e4..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: 2 -updates: - - package-ecosystem: npm - directories: - - / - - /site - schedule: - interval: weekly - open-pull-requests-limit: 5 - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly - open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c811784..ccbd001 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,15 @@ jobs: - run: pnpm package:skill - run: pnpm package:site - run: pnpm smoke:artifacts + - name: build the GitHub Pages repository prefix + run: pnpm build + env: + NEXT_PUBLIC_BASE_PATH: /${{ github.event.repository.name }} + - run: pnpm check:site + - name: verify the repository prefix export + run: pnpm test:browser + env: + DBDEP_SITE_URL: http://127.0.0.1:4173/${{ github.event.repository.name }} catalog: name: catalog (${{ matrix.postgres }}) runs-on: ubuntu-latest diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 08fef6d..69ee05f 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -26,13 +26,18 @@ jobs: - run: pnpm exec playwright install --with-deps chromium - run: pnpm test - run: pnpm verify:examples + - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + id: pages - run: pnpm build + env: + NEXT_PUBLIC_BASE_PATH: ${{ steps.pages.outputs.base_path }} - run: pnpm check:site - run: pnpm test:browser - - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + env: + DBDEP_SITE_URL: http://127.0.0.1:4173${{ steps.pages.outputs.base_path }} - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: - path: site/dist + path: site/out include-hidden-files: true deploy: needs: build From f413dbb57068d5c1e662cb6e6db8c3697f0038c6 Mon Sep 17 00:00:00 2001 From: JoseDFlorez <63378897+JoseDFN@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:55:32 -0500 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20docs:=20explain=20nextjs=20e?= =?UTF-8?q?xports=20and=20migration=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update setup, contributor instructions and bilingual website documentation for Next.js, direct documentation routes and static GitHub Pages deployment. Explain build-time base paths, the Windows filename correction, manual dependency upgrades and the distinction between JavaScript interactions and exported English content. Record local build, audit, browser, deterministic artifact, packaging and visual review results with their limits. Preserve immutable release assets and historical evaluation evidence; this work does not publish a release or deploy Pages. --- CONTRIBUTING.md | 4 +- README.md | 8 ++-- docs/github-pages.md | 29 ++++++++++--- docs/nextjs-validation.md | 43 +++++++++++++++++++ .../plans/2026-10-09-nextjs-migration.md | 34 +++++++++++++++ .../specs/2026-10-09-nextjs-design.md | 13 ++++++ site/README.md | 14 +++--- site/src/docs.ts | 4 +- 8 files changed, 131 insertions(+), 18 deletions(-) create mode 100644 docs/nextjs-validation.md create mode 100644 docs/superpowers/plans/2026-10-09-nextjs-migration.md create mode 100644 docs/superpowers/specs/2026-10-09-nextjs-design.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a2f3596..f4bf852 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ pnpm test:browser pnpm audit --audit-level=low ``` -Build before browser tests; Playwright serves `site/dist` through the local preview. The Node tests run offline and skip live cases without a disposable fixture DSN. Run `pnpm test:live` when PostgreSQL tools are available. This creates, stops and removes its own loopback cluster; supply `--pg-bin ` through `pnpm test:live --pg-bin ` if needed. CI has separate disposable PostgreSQL services. +Build before browser tests; Playwright serves the Next.js static export in `site/out` through the local preview. CI also rebuilds and tests under the repository's Pages path. See [the Pages guide](docs/github-pages.md) to reproduce that check locally. The Node tests run offline and skip live cases without a disposable fixture DSN. Run `pnpm test:live` when PostgreSQL tools are available. This creates, stops and removes its own loopback cluster; supply `--pg-bin ` through `pnpm test:live --pg-bin ` if needed. CI has separate disposable PostgreSQL services. Use `pnpm exec prettier --check` with the files you changed. Describe checks actually performed, skipped prerequisites, and unresolved limits. Do not treat local receipts or configured workflows as proof that remote CI passed. @@ -116,3 +116,5 @@ Follow [SemVer](https://semver.org/) and [docs/releases.md](docs/releases.md). U Complete relevant checks and artifact/hash comparisons on final `main` before creating an annotated `v` tag. Never move or replace an existing release tag. Local commit/tag creation, remote push, GitHub release assets, npm publication and Pages deployment are distinct actions; perform publication only when authorized. The [Pages workflow](.github/workflows/pages.yml) is manual-only. Review the curated public contents before running it, and follow [Pages setup](docs/github-pages.md). CI action references are pinned to commit SHAs; retain least-privilege permissions when changing workflows. + +Dependency upgrades are maintainer-initiated pull requests. This repository has no Dependabot update configuration or automatic security-fix PRs. Keep the frozen lockfile, install-script restrictions and vulnerability audit. Review proposed upgrades and combine related changes into one pull request. diff --git a/README.md b/README.md index b776cfa..fd22906 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A PostgreSQL dependency and migration-review skill for AI coding agents, with an independent Node.js toolkit. Inspect a schema, trace downstream consumers, and review a proposed change using evidence from SQL files or supplied catalog snapshots. The toolkit never applies migrations. -The versioned `*.dbdep.json` model is the source of truth for validation, findings, interactive HTML, Markdown, Mermaid and DOT. Its canonical contract is **1.0.0**. The current hardening work targets local **v0.3.2**; remote installation receives these changes after they are pushed. Local checks do not establish a published release, deployed website, or passing remote CI. +The versioned `*.dbdep.json` model is the source of truth for validation, findings, interactive HTML, Markdown, Mermaid and DOT. Its canonical contract is **1.0.0**. The current package version is **v0.3.2**. Installing from the repository uses its current `main` source; release tags remain immutable. Local checks do not establish a deployed website or passing remote CI. ## What it does @@ -141,14 +141,14 @@ Screenshots illustrate presentation. Tests and source evidence establish graph b ## Website and GitHub Pages -The separate [React/Vite website](site/README.md) has bilingual documentation at `/#/docs`, source-backed examples, and links to full standalone reports. It has no upload flow or database connection. +The separate [Next.js website](site/README.md) exports the home page and bilingual documentation at `/docs/` as static HTML. It retains source-backed examples and links to full standalone reports. It has no upload flow or database connection. ```sh pnpm build pnpm preview ``` -Open the localhost URL printed by Vite. GitHub Pages deployment is a manual workflow after review and publication; it publishes `site/dist`. Curated examples and copied documentation become public. Local `out/`, `tmp/` and evaluation directories are not deployed. Follow the [Pages setup and deployment guide](docs/github-pages.md). No live deployment is claimed here. +Open the localhost URL printed by the preview. GitHub Pages deployment is a manual workflow after review and publication; it publishes `site/out`. The build uses the configured Pages base path, including a repository subdirectory. Existing `/#/docs` bookmarks redirect after JavaScript loads. Curated examples and copied documentation become public. Local `out/`, `tmp/` and evaluation directories are not deployed. Follow the [Pages setup and deployment guide](docs/github-pages.md). No live deployment is claimed here. ## Repository layout @@ -162,7 +162,7 @@ Open the localhost URL printed by Vite. GitHub Pages deployment is a manual work | `assets/viewer/` | Offline report viewer and embedded licensed fonts | | `examples/` | Curated inputs, authentic sanitized capture and generated reports | | `tests/` | Node, browser and isolated live regressions | -| `site/` | Separate React/Vite website, documentation and license notices | +| `site/` | Next.js static website, documentation and license notices | | `evals/`, `docs/` | Frozen evaluation evidence, validation, security audit and maintenance guides | ## Known limitations diff --git a/docs/github-pages.md b/docs/github-pages.md index ba7103b..0b983aa 100644 --- a/docs/github-pages.md +++ b/docs/github-pages.md @@ -1,6 +1,6 @@ # GitHub Pages -The website is a static React/Vite application. GitHub Pages publishes the built `site/dist` directory through the repository's [manual Pages workflow](../.github/workflows/pages.yml). Local builds and release archives do not deploy it. No public deployment is claimed for the pending v0.3.2 work. +The website uses Next.js App Router with a static export. GitHub Pages publishes the built `site/out` directory through the repository's [manual Pages workflow](../.github/workflows/pages.yml). Local builds and release archives do not deploy it. No public deployment is claimed here. ## Repository setup @@ -26,7 +26,7 @@ pnpm check:site pnpm preview ``` -Open the local Vite URL, the documentation at `/#/docs`, and all three standalone example reports. Check asset loading, English/Spanish controls, keyboard navigation and report JSON/SVG exports. Browser verification needs Chromium: +Open the local preview URL, the documentation at `/docs/`, and all three standalone example reports. Check asset loading, English/Spanish controls, keyboard navigation and report JSON/SVG exports. Browser verification needs Chromium: ```sh pnpm exec playwright install chromium @@ -37,18 +37,35 @@ pnpm test:browser ## Repository-path routing -`site/vite.config.ts` uses `base: "./"`, so generated asset links resolve relative to the repository's Pages directory. Application documentation uses hash routes, including `/#/docs`, which support direct links and reloads without server-side route rewrites. Keep both behaviors when changing routing or asset URLs. [Vite's deployment guide](https://vite.dev/guide/static-deploy.html#github-pages) explains the relationship between the asset base and hosting path. +`site/next.config.mjs` uses `output: "export"` and `trailingSlash: true`. Each documentation route has an `index.html`, so direct links and reloads need no server rewrite. The workflow runs `configure-pages` before building and passes its `base_path` to `NEXT_PUBLIC_BASE_PATH`. Next.js prefixes internal links and runtime bundles; the site's asset utility prefixes report and license URLs. The base path is fixed at build time. See Next.js's [static export guide](https://nextjs.org/docs/app/guides/static-exports) and [basePath reference](https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath). For a project site, a documentation URL has this shape: ```text -https://.github.io//#/docs +https://.github.io//docs/ ``` -Use the deployment environment's actual URL; a configured workflow or local preview does not establish that URL is live. +Existing `/#/docs` bookmarks redirect to the new routes after JavaScript loads. English content is present in the exported HTML; translation, saved preferences and interactive controls require JavaScript. A custom domain or account site can use an empty base path. + +To reproduce the project path on PowerShell: + +```powershell +$env:NEXT_PUBLIC_BASE_PATH = '/Database-Dependency-Migration-Visualizer' +pnpm build +Remove-Item Env:NEXT_PUBLIC_BASE_PATH +pnpm check:site +$env:DBDEP_SITE_URL = 'http://127.0.0.1:4173/Database-Dependency-Migration-Visualizer' +pnpm test:browser +Remove-Item Env:DBDEP_SITE_URL +pnpm preview +``` + +On macOS/Linux, use `NEXT_PUBLIC_BASE_PATH=/Database-Dependency-Migration-Visualizer pnpm build` and `DBDEP_SITE_URL=http://127.0.0.1:4173/Database-Dependency-Migration-Visualizer pnpm test:browser`. + +The preview reads the built base path, serves directory indexes and returns a real HTTP 404 for missing files. It binds only to loopback and rejects paths or symlinks outside the export. Use the deployment environment's actual URL after publishing; a local preview does not establish that URL is live. Check direct topic reloads, assets, report exports and the deployed 404 page. ## Public contents and subsequent deployments -Only `site/dist` is uploaded. The curated examples, their architecture identifiers, generated reports, copied README/SKILL and license notices become publicly accessible. Review these deliberate publication inputs before deployment. Local `out/`, `tmp/`, raw evaluations and the repository's `evals/` directory are not deployment artifacts. The site has no upload endpoint, credentials or live database connection. +Only `site/out` is uploaded. The curated examples, their architecture identifiers, generated reports, copied README/SKILL and license notices become publicly accessible. The publication validator compares exported pages, route payloads and runtime bundles with their compiled sources and checks public inputs byte-for-byte. It rejects unexpected files, source maps and symlinks. Review these deliberate publication inputs before deployment. Private `site/.next`, root `out/`, `tmp/`, raw evaluations and the repository's `evals/` directory are not deployment artifacts. The site has no upload endpoint, credentials or live database connection. For an update, review and publish the intended source, rerun the checks, then manually run the workflow for that reviewed `main` revision. Keep release tags immutable. See the [security policy](../SECURITY.md), [audit](security-audit.md), and [release procedure](releases.md). diff --git a/docs/nextjs-validation.md b/docs/nextjs-validation.md new file mode 100644 index 0000000..07282cf --- /dev/null +++ b/docs/nextjs-validation.md @@ -0,0 +1,43 @@ +# Next.js migration validation + +Validated locally on 2026-10-09 with Windows, Node.js 24.15.0, pnpm 12.10.1, Next.js 16.4.0 and Playwright 1.63.0. This records the webpage migration on `feat/nextjs-static-site`. It does not replace frozen creator evaluations or earlier release receipts. + +## Completed changes + +The App Router exports the home page, documentation overview and seven topic pages as HTML. Direct topic links and reloads work without a server rewrite. English documentation is readable with JavaScript disabled. Old `#/docs` bookmarks redirect to the new routes when JavaScript runs. + +The shared shell retains the existing styles, fonts, ENG/ESP and theme preferences, motion, keyboard behavior and evidence traces. Standalone reports remain generated from the toolkit and open separately. Browser APIs run after mounting; the theme bootstrap restores preferences before paint. Navigation focuses the destination after its content mounts. + +Pages builds with its configured base path and uploads only `site/out`. CI validates both a root build and the repository path. The loopback preview serves actual files and HTTP 404 responses, rejects writes and prevents reads through paths or symlinks outside the export. Publication validation rejects unexpected public files, source maps and symlinks, and compares copied inputs and compiled runtime files byte-for-byte. + +The finalizer corrects Next.js's [Windows segment filename issue](https://github.com/vercel/next.js/issues/92339). It converts nested filenames to the names requested by the router, preserves bytes, rejects collisions before moving files and leaves Linux filenames unchanged. An explicit browser regression navigates all seven topics and fails on HTTP errors, console errors or hydration errors. + +The PR removes `.github/dependabot.yml`. Automatic security-fix PRs were already disabled in repository settings. Dependency auditing and the frozen lockfile remain active. Previous bot commits are preserved. All changes are submitted together in one PR. + +## Verification + +| Check | Result | +| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Frozen installation with scripts disabled | Passed; lockfile supply-chain policy accepted | +| Node test suite | 144 passed, 2 live cases skipped without a fixture DSN | +| Curated artifact regeneration | All 18 outputs matched byte-for-byte | +| Dependency audit | No known advisories reported across 131 dependencies | +| Root production build and publication validation | Passed; 9 content routes, 12 demo files and both copied source documents verified | +| Root browser suite | 73 passed | +| Repository-prefix production build and publication validation | Passed at `/Database-Dependency-Migration-Visualizer` | +| Repository-prefix browser suite | 73 passed | +| Desktop/mobile visual review | Light/dark and English/Spanish captures retained the existing layout; no horizontal overflow in captures | +| Distribution smoke in an isolated snapshot | npm package and unpacked skill installed outside the checkout; doctor and all three strict demo validations passed | +| Website archive | Publication inputs and retained licenses verified before packaging | + +Local logs, publication inventories, audit results and captures are under the ignored `out/nextjs-*` paths. Package checks used an isolated snapshot rather than replacing existing `dist/v0.3.2` release artifacts. The normalizer has regressions for byte preservation, idempotence, collisions, Linux output and directory cleanup. Publication tests cover tampered bundles, unexpected payloads, mismatched base paths, source maps and directory links. + +## Limits and publication + +Remote CI must pass for the final PR revision, including the existing PostgreSQL 14-18 matrix. Local skipped live cases are not proof of that matrix. Main still requires an approval from `JoseDFlorez` or `Juanfrxz`, with a different maintainer approving the author's latest push. + +No Pages deployment, new release tag or release asset publication is part of this migration. Verify the deployed URL, direct topic reloads and 404 behavior after manually running Pages on reviewed `main`. Removing the version-update configuration takes effect when this PR merges. + +English is the exported default. Spanish translation, saved preferences, bookmark redirects and interactive controls require JavaScript. A base-path change requires rebuilding. The filename correction depends on the pinned Next.js export layout and must be reviewed when upgrading Next.js. + +Next.js generates runtime build identifiers, so website exports are not claimed to be byte-identical across independent builds. Toolkit/model/report reproducibility remains covered by deterministic checks. Packaging verification proves the contents of the tested snapshot and preserves earlier release evidence; it does not create a new release. diff --git a/docs/superpowers/plans/2026-10-09-nextjs-migration.md b/docs/superpowers/plans/2026-10-09-nextjs-migration.md new file mode 100644 index 0000000..5eb0f6e --- /dev/null +++ b/docs/superpowers/plans/2026-10-09-nextjs-migration.md @@ -0,0 +1,34 @@ +# Next.js migration implementation plan + +> Execute inline in the existing session. Keep all work on `feat/nextjs-static-site` and submit one PR. + +**Goal:** Port the webpage to a validated Next.js static export for GitHub Pages while disabling automated dependency PRs. + +**Architecture:** App Router renders home and bounded documentation routes. A shared client shell owns preferences and navigation focus. Reports remain curated static assets. Publication and preview tools operate only on `site/out`. + +**Tech stack:** Next.js 16.4.0, React 19.3, TypeScript, pnpm 12.10.1, existing GSAP/Lenis/Motion components and Playwright 1.63. + +## Implementation checkpoints + +- [x] Add browser regressions for exported documentation with JavaScript disabled, direct reloads, old hash bookmarks and real unknown-route 404s. Run against the current Vite export and observe the missing behavior. +- [x] Replace Vite configuration and dependencies with `next.config.mjs`, App Router entries, an explicit base-path utility, and SSR-safe preferences/motion. Split `site/src/App.tsx` into the shared shell and `Home.tsx`; reuse existing documentation and visual components. +- [x] Implement `site/scripts/preview.mjs` for actual static files and safe base-path routing. Update Playwright and screenshot entry points to use it. +- [x] Adapt `scripts/check-site-publication.mjs`, `scripts/package-site.mjs`, and publication tests to Next.js output. Retain byte equality and reject unexpected files and symlinks. Test the real build inventory before allowing runtime file patterns. +- [x] Remove Dependabot configuration, add Next.js output ignores and license notices, and update Pages/CI and contributor instructions. Keep one branch and one PR. +- [x] Run frozen installation and dependency audit, Node tests, deterministic examples, root production build/publication validation, and the full browser suite. Rebuild with the repository base path and verify the same interactions on a static prefix preview. +- [x] Inspect desktop/mobile/light/dark/Spanish captures, package the changed website in an isolated temporary artifact directory, verify notices and copied source bytes, and record results and remaining limits. +- [ ] Commit focused changes using the Gitmoji convention, push this branch and create one PR with the final scope and actual validation results. Leave main approval protections active. + +Useful verification commands from the repository root: + +```sh +pnpm install --frozen-lockfile --ignore-scripts +pnpm audit --audit-level=low +pnpm test +pnpm verify:examples +pnpm build +pnpm check:site +pnpm test:browser +``` + +The repository-prefix build passes `NEXT_PUBLIC_BASE_PATH=/Database-Dependency-Migration-Visualizer` to the build. Browser checks then use `DBDEP_SITE_URL=http://127.0.0.1:4173/Database-Dependency-Migration-Visualizer`. The Pages workflow uses the configure-pages base path rather than requiring a hosted Node server. diff --git a/docs/superpowers/specs/2026-10-09-nextjs-design.md b/docs/superpowers/specs/2026-10-09-nextjs-design.md new file mode 100644 index 0000000..04c4950 --- /dev/null +++ b/docs/superpowers/specs/2026-10-09-nextjs-design.md @@ -0,0 +1,13 @@ +# Next.js webpage migration + +The maintainer requested a Next.js port of the existing webpage, disabled automated Dependabot PRs, and required one PR for this work. Preserve the current interface and its evidence claims. GitHub Pages remains the deployment target. + +Use Next.js 16.4.0 App Router with `output: "export"` and directory-style routes. Generate the home page, the documentation overview, and every existing documentation topic at build time. Retain compatibility redirects for old `#/docs` bookmarks in the client. An explicitly configured build-time base path supports the GitHub repository URL; development and ordinary local exports use the domain root. + +Retain the current styles, licensed fonts, animations, pointer traces, accessibility behavior, and ENG/ESP and theme preferences. Keep browser APIs in effects or guarded client code, with deterministic server markup and a pre-paint theme bootstrap. Separate the shared client shell from home and documentation entry points. Serve generated report HTML as independent files with their existing controls and canonical JSON. + +Export to `site/out`. Preview the actual static export with a localhost-only file server that supports the configured base path, directory indexes, HTTP 404 responses and safe path containment. Update the publication validator to recognize bounded Next.js route/runtime files and preserve byte comparisons for copied reports, documents and notices. Continue rejecting unexpected private files, source maps and symlinks. + +Keep SHA-pinned Actions, manual deployment, maintainer approval and existing database tests. The Pages workflow obtains its base path before building. Validate root and repository-prefix exports in automation. Remove `.github/dependabot.yml`; automatic security-update PRs are already disabled remotely. Retain vulnerability visibility and the dependency audit. Do not rewrite previous bot commits or immutable release evidence. + +Completion requires a working export, browser checks for direct documentation navigation/reload and legacy bookmarks, persistence and reduced motion, publication rejection tests, a dependency audit, unchanged curated examples, retained licenses, and one reviewable PR. Publishing the page or creating a release is outside this change. diff --git a/site/README.md b/site/README.md index 8fc24a8..3f94ef1 100644 --- a/site/README.md +++ b/site/README.md @@ -1,6 +1,6 @@ # Database dependency migration website -A separate React/Vite website for the Agent Skill and Node.js toolkit. It explains PostgreSQL dependency analysis, provides English/Spanish documentation at `/#/docs`, and opens three generated standalone HTML reports. The pending local release is v0.3.2. Hosting uses the [manual GitHub Pages workflow](../.github/workflows/pages.yml); deployment is separate from local validation and is not claimed here. +A separate Next.js 16.4 App Router website for the Agent Skill and Node.js toolkit. It explains PostgreSQL dependency analysis, exports English documentation as HTML at `/docs/`, supports English/Spanish controls, and opens three generated standalone HTML reports. Hosting uses the [manual GitHub Pages workflow](../.github/workflows/pages.yml); deployment is separate from local validation and is not claimed here. ## Development and build @@ -18,7 +18,7 @@ pnpm build pnpm preview ``` -Open the localhost address printed by Vite. `prepare:demos` runs before development and builds. It copies report HTML, model JSON, review JSON and Markdown from `examples/rendered/`, copies README/SKILL documentation, derives the visible graph/counts from the models, and retains complete website license notices. It rejects missing selected dependency paths. Generated copies, build output and dependencies are ignored. +Open the localhost address printed by Next.js in development or the static preview in production. `prepare:demos` runs before development and builds. It copies report HTML, model JSON, review JSON and Markdown from `examples/rendered/`, copies README/SKILL documentation, derives the visible graph/counts from the models, and retains complete website license notices. It rejects missing selected dependency paths. Generated copies, build output and dependencies are ignored. When the engine changes, run `pnpm demo` and `pnpm verify:examples` before rebuilding. Do not edit generated reports or model data by hand. The CLI also supports `node scripts/dbdep.mjs `. @@ -36,7 +36,11 @@ The site has no backend, upload endpoint, credentials, database discovery or mig Follow [the Pages guide](../docs/github-pages.md). Select **GitHub Actions** as the Pages source, then manually run `pages.yml` for reviewed, published `main` source. Pushes and pull requests do not deploy automatically. -The workflow uploads only `site/dist`. Curated reports/models and copied documentation/licenses are public; local `out/`, `tmp/` and `evals/` directories are excluded from the deployment. Relative asset paths use Vite's `base: "./"`; hash routes support direct documentation links and reloads under a repository subdirectory. Inspect the actual deployment URL after the workflow succeeds. +The workflow uploads only `site/out`. Curated reports/models and copied documentation/licenses are public; local `out/`, `tmp/` and `evals/` directories are excluded from the deployment. Next.js uses `output: "export"`, directory indexes and a build-time `NEXT_PUBLIC_BASE_PATH` supplied by Pages configuration. No Node server is deployed. Inspect the actual deployment URL after the workflow succeeds. + +All seven documentation topics have direct `/docs//` routes that survive reloads and contain English content with JavaScript disabled. Existing `/#/docs` bookmarks redirect after JavaScript loads. Theme, Spanish translation and interactive controls require JavaScript. Unknown routes return 404 in the preview; actual GitHub Pages behavior must be checked after deployment. Next.js runtime build IDs can differ between builds; deterministic toolkit/example checks remain separate from website bundle generation. + +The post-build finalizer normalizes Windows segment filenames to the dot-separated names requested by Next.js's router. Linux already emits those names. This addresses the [upstream Windows export issue](https://github.com/vercel/next.js/issues/92339), preserves payload bytes and rejects filename collisions. Publication validation compares normalized payloads with the compiler's original artifacts. ## Verification and notices @@ -48,8 +52,8 @@ pnpm check:site pnpm test:browser ``` -The browser configuration serves the built site at `http://127.0.0.1:4173`. Tests cover documentation routes, keyboard/mobile navigation, theme/language persistence, blocked storage, source-backed traces, actual report exports, motion preferences, retained licenses and browser errors. CLI/parser checks and visual inspection remain separate. +The browser configuration serves the built site at `http://127.0.0.1:4173`. For a prefixed build, set `DBDEP_SITE_URL` to the full local base URL. Tests cover exported documentation without JavaScript, direct routes and old bookmarks, keyboard/mobile navigation, theme/language persistence, blocked storage, source-backed traces, actual report exports, motion preferences, retained licenses and browser errors. Publication checks compare pages, route payloads and runtime files with their compiled sources and reject unexpected files, source maps and symlinks. CLI/parser checks and visual inspection remain separate. -Reviewed captures are in [screenshots/](screenshots/). `node scripts/capture-site-screenshots.mjs` captures the built site; with a preview running, `node site/scripts/capture-docs.mjs` captures documentation pages. `DBDEP_SITE_URL` can select another local preview address. Static captures use reduced motion. +Reviewed captures are in [screenshots/](screenshots/). `node scripts/capture-site-screenshots.mjs` captures the built site; with a preview running, `node site/scripts/capture-docs.mjs` captures documentation pages. `DBDEP_SITE_URL` can select another local preview address. Set `DBDEP_CAPTURE_DIR` to save new review captures separately. Static captures use reduced motion. React Bits adaptations retain pinned source hashes and MIT + Commons Clause notices. GSAP retains its Standard License. All retained notices are copied into the build and linked from the footer. Read [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) before redistribution. The explanatory flow was independently implemented after inspecting [network-engineering's flow](https://github.com/whetstone-dev/network-engineering/blob/main/web/src/components/Flow.tsx); no PostgreSQL engine code was imported from that project. diff --git a/site/src/docs.ts b/site/src/docs.ts index 0b23f21..f65b10b 100644 --- a/site/src/docs.ts +++ b/site/src/docs.ts @@ -175,7 +175,7 @@ const en: DocsCopy = { }, ], paragraphs: [ - "Open the localhost address printed by Vite. The website explains the toolkit and opens the shipped reports. It has no database connection or upload flow.", + "Open the localhost address printed by Next.js. The website explains the toolkit and opens the shipped reports. It has no database connection or upload flow.", ], }, ], @@ -636,7 +636,7 @@ const es: DocsCopy = { }, ], paragraphs: [ - "Abre la dirección local que imprime Vite. La web explica el toolkit y abre los informes incluidos. No tiene conexión a bases de datos ni carga de archivos.", + "Abre la dirección local que imprime Next.js. La web explica el toolkit y abre los informes incluidos. No tiene conexión a bases de datos ni carga de archivos.", ], }, ],