diff --git a/.gitignore b/.gitignore index f6affa9c..50b5aa76 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ build/ scripts/dist/ scratch/ .ts-node-build/ +.react-router/ benches/ # Generated diagnostics and test artifacts diff --git a/jest.config.cjs b/jest.config.cjs index 9585a3b7..93549220 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -8,8 +8,8 @@ module.exports = { ], extensionsToTreatAsEsm: [".ts", ".tsx"], moduleNameMapper: { + "^react-router$": "/test/ts/__mocks__/react-router.cjs", "^(\\.{1,2}/.*)\\.js$": "$1", - "^react-router-dom$": "/node_modules/react-router-dom/dist/index.js", "^@n-apt/consts$": "/src/ts/consts", "^@n-apt/md-preview/utils/hmr$": "/test/ts/__mocks__/mdPreviewHmrMock.cjs", "^@n-apt/md-preview/(.*)$": "/src/md-preview/$1", diff --git a/package-lock.json b/package-lock.json index cb024293..4904c2a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "@radix-ui/react-toggle": "1.1.14", "@radix-ui/react-toggle-group": "1.1.15", "@radix-ui/react-tooltip": "1.2.12", + "@react-router/node": "8.3.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", "@react-three/postprocessing": "^3.0.4", @@ -69,6 +70,7 @@ "ink": "7.1.0", "ink-spinner": "^5.0.0", "input-otp": "1.4.2", + "isbot": "^5", "katex": "0.17.0", "leaflet": "^1.9.4", "leva": "^0.10.1", @@ -79,9 +81,9 @@ "motion": "12.42.2", "n_apt_canvas": "file:./packages/n_apt_canvas", "next-themes": "0.4.6", - "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^19.2.7", "react-day-picker": "10.0.1", - "react-dom": "^19.2.4", + "react-dom": "^19.2.7", "react-flow": "^1.0.3", "react-helmet-async": "^3.0.0", "react-katex": "^3.1.0", @@ -90,7 +92,7 @@ "react-redux": "9.3.0", "react-resizable-panels": "4.12.1", "react-responsive-masonry": "2.7.2", - "react-router-dom": "7.18.1", + "react-router": "8.3.0", "recharts": "3.9.2", "redis": "6.1.0", "rehype-highlight": "^7.0.2", @@ -108,6 +110,7 @@ "devDependencies": { "@ladle/react": "^5.1.1", "@playwright/test": "1.61.1", + "@react-router/dev": "8.3.0", "@tailwindcss/postcss": "4.3.2", "@tailwindcss/vite": "4.3.2", "@testing-library/dom": "^10.4.1", @@ -139,8 +142,8 @@ "better-sqlite3": "^12.4.1", "concurrently": "10.0.3", "express": "5.2.1", - "express-rate-limit": "^8.5.2", "express-fileupload": "^1.5.2", + "express-rate-limit": "^8.5.2", "extract-zip": "^2.0.1", "fkill-cli": "^9.0.0", "fs-extra": "^11.2.0", @@ -163,6 +166,9 @@ "vitest": "4.1.10", "wgsl-test": "0.2.30" }, + "engines": { + "node": ">=22.22.0" + }, "peerDependencies": { "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -304,6 +310,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", @@ -321,6 +340,28 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", @@ -330,6 +371,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", @@ -361,6 +416,19 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", @@ -371,6 +439,38 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -667,6 +767,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", @@ -699,6 +816,46 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", @@ -6805,6 +6962,147 @@ "react-dom": "^19.0.0" } }, + "node_modules/@react-router/dev": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-8.3.0.tgz", + "integrity": "sha512-XR+N2fEFOPjczYo2efc3/AOtosbSICCroLF/IxnZ6ErGBeBGRG6SqAso0SYoff0e18OA05qOyhJHFhXKMGIPRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/preset-typescript": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@react-router/node": "8.3.0", + "@remix-run/node-fetch-server": "^0.13.3", + "babel-dead-code-elimination": "^1.0.12", + "chokidar": "^5.0.0", + "dedent": "^1.7.2", + "es-module-lexer": "^2.1.0", + "exit-hook": "5.1.0", + "isbot": "^5.1.40", + "jsesc": "3.1.0", + "lodash": "^4.18.1", + "p-map": "^7.0.4", + "pathe": "^2.0.3", + "picocolors": "^1.1.1", + "pkg-types": "^2.3.1", + "prettier": "^3.8.3", + "react-refresh": "^0.18.0", + "semver": "^7.8.1", + "tinyglobby": "^0.2.16", + "valibot": "^1.4.1" + }, + "bin": { + "react-router": "bin.cjs" + }, + "engines": { + "node": ">=22.22.0" + }, + "peerDependencies": { + "@react-router/serve": "^8.3.0", + "@vitejs/plugin-rsc": "~0.5.26", + "react-router": "^8.3.0", + "react-server-dom-webpack": "^19.2.7", + "typescript": "^5.1.0 || ^6.0.0 || ^7.0.0", + "vite": "^7.0.0 || ^8.0.0", + "wrangler": "^4.0.0" + }, + "peerDependenciesMeta": { + "@react-router/serve": { + "optional": true + }, + "@vitejs/plugin-rsc": { + "optional": true + }, + "react-server-dom-webpack": { + "optional": true + }, + "typescript": { + "optional": true + }, + "wrangler": { + "optional": true + } + } + }, + "node_modules/@react-router/dev/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@react-router/dev/node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@react-router/dev/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@react-router/dev/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-router/node": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@react-router/node/-/node-8.3.0.tgz", + "integrity": "sha512-qw5ibcolE1OcwngiEw6t7LR11Hi6yWEDvj6cfvaYROX+w18JPxc0YSmsdn3Wlc9TOX+Qo8FVcxbXRdc9vojn2w==", + "license": "MIT", + "dependencies": { + "@remix-run/node-fetch-server": "^0.13.3" + }, + "engines": { + "node": ">=22.22.0" + }, + "peerDependencies": { + "react-router": "8.3.0", + "typescript": "^5.1.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@react-three/drei": { "version": "10.7.7", "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz", @@ -7034,6 +7332,12 @@ } } }, + "node_modules/@remix-run/node-fetch-server": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@remix-run/node-fetch-server/-/node-fetch-server-0.13.3.tgz", + "integrity": "sha512-UfjOXed/DQteaM5VyTfqTeGpHwyL2J5aoRGY6cydip4tt1ehNNeSwuXCC7AEGE0RWBs/7bgKxYkL/B/+UDe4AA==", + "license": "MIT" + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", @@ -8322,6 +8626,72 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", @@ -10771,6 +11141,19 @@ } } }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.12.tgz", + "integrity": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, "node_modules/babel-jest": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", @@ -11983,6 +12366,13 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -12032,6 +12422,12 @@ "node": ">= 0.6" } }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -13327,6 +13723,19 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exit-hook": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-5.1.0.tgz", + "integrity": "sha512-INjr2xyxHo7bhAqf5ong++GZPPnpcuBcaXUKt03yf7Fie9yWD7FapL4teOU0+awQazGs5ucBh7xWs/AD+6nhog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/exit-x": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", @@ -13436,6 +13845,7 @@ "version": "8.5.2", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dev": true, "license": "MIT", "dependencies": { "ip-address": "^10.2.0" @@ -13450,6 +13860,13 @@ "express": ">= 4.11" } }, + "node_modules/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "dev": true, + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -15779,6 +16196,15 @@ "dev": true, "license": "MIT" }, + "node_modules/isbot": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.2.1.tgz", + "integrity": "sha512-dJ+LpKyClQZ7NG+j3OensC/mAZkGpukE9YUrgPYvAZj2doVL0edfDgywTUh5CXa0o+nW9a1V9e5+CJTX8+SxRw==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -21001,6 +21427,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz", + "integrity": "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -21300,6 +21739,18 @@ "node": ">=8" } }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/platform": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", @@ -21441,6 +21892,22 @@ "node": ">=10" } }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -22127,20 +22594,19 @@ } }, "node_modules/react-router": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", - "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.3.0.tgz", + "integrity": "sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==", "license": "MIT", "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" + "cookie-es": "^3.1.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.22.0" }, "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" + "react": ">=19.2.7", + "react-dom": ">=19.2.7" }, "peerDependenciesMeta": { "react-dom": { @@ -22148,41 +22614,6 @@ } } }, - "node_modules/react-router-dom": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", - "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", - "license": "MIT", - "dependencies": { - "react-router": "7.18.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/react-router/node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/react-router/node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -25205,6 +25636,21 @@ "integrity": "sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A==", "license": "MIT" }, + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 79904887..854706ae 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,9 @@ "sigint" ], "author": "Ceane Lamerez", + "engines": { + "node": ">=22.22.0" + }, "scripts": { "setup": "node scripts/setup/create_env.js && cargo fetch", "decrypt-modules": "npx tsx scripts/encrypted-modules/crypt.ts decrypt", @@ -54,12 +57,14 @@ "build:markdown": "vite build --config vite.markdown.config.ts", "preview:markdown": "vite preview --config vite.markdown.config.ts --port 4174", "dev": "npm --silent run decrypt-modules-if-needed && npm --silent run build_orchestrator", + "dev:router": "NAPT_REACT_ROUTER=1 react-router dev", "build_orchestrator": "node --import tsx scripts/build/build-orchestrator.tsx", "legal-app": "node scripts/legal-app-server/index.mjs", "cli": "tsx scripts/cli/index.ts", "build:rust": "cargo build --bin n-apt-backend", "build:rust:release": "cargo build --release --bin n-apt-backend", "build:frontend": "vite build", + "build:router": "NAPT_REACT_ROUTER=1 react-router build", "build:wasm": "./scripts/build/build_wasm.sh", "build": "npm run build:rust && npm run build:wasm && npm run build:frontend", "test": "npm run test:shader:fixtures && jest test/ts && npm run test:shader", @@ -84,6 +89,7 @@ "format:rust:fast": "./scripts/rust/rustfmt-fast.sh", "format:rust:check": "cargo fmt --all -- --check", "typecheck": "tsc --noEmit --ignoreDeprecations 6.0", + "typegen": "NAPT_REACT_ROUTER=1 react-router typegen", "format:check": "oxfmt --check 'src/ts/**/*.{ts,tsx,js,jsx,cjs,mjs}' 'test/**/*.{ts,tsx,js,jsx,cjs,mjs}'", "towers:load:redis": "node scripts/redis/load_towers_to_redis.cjs", "towers:load:all": "node scripts/redis/load_all_towers_to_redis.cjs", @@ -175,9 +181,9 @@ "motion": "12.42.2", "n_apt_canvas": "file:./packages/n_apt_canvas", "next-themes": "0.4.6", - "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^19.2.7", "react-day-picker": "10.0.1", - "react-dom": "^19.2.4", + "react-dom": "^19.2.7", "react-flow": "^1.0.3", "react-helmet-async": "^3.0.0", "react-katex": "^3.1.0", @@ -186,7 +192,8 @@ "react-redux": "9.3.0", "react-resizable-panels": "4.12.1", "react-responsive-masonry": "2.7.2", - "react-router-dom": "7.18.1", + "react-router": "8.3.0", + "@react-router/node": "8.3.0", "recharts": "3.9.2", "redis": "6.1.0", "rehype-highlight": "^7.0.2", @@ -199,10 +206,12 @@ "unist-util-visit": "^5.1.0", "vaul": "1.1.2", "zod": "^4.3.6", - "zustand": "^5.0.11" + "zustand": "^5.0.11", + "isbot": "^5" }, "devDependencies": { "@ladle/react": "^5.1.1", + "@react-router/dev": "8.3.0", "@playwright/test": "1.61.1", "@tailwindcss/postcss": "4.3.2", "@tailwindcss/vite": "4.3.2", diff --git a/react-router.config.ts b/react-router.config.ts new file mode 100644 index 00000000..1f4fecc9 --- /dev/null +++ b/react-router.config.ts @@ -0,0 +1,15 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + appDirectory: "src/ts", + buildDirectory: "build/react-router", + ssr: false, + routeDiscovery: { + // React Router 8.3.0 rejects lazy discovery in SPA mode (ssr:false). + // Keep the SPA build safe while route-module code splitting is adopted; + // switch to "lazy" with an SSR-capable runtime in a later deployment + // milestone. + mode: "initial", + manifestPath: "/__manifest", + }, +} satisfies Config; diff --git a/src/ts/App.tsx b/src/ts/App.tsx index a632ec15..4b73f7c4 100644 --- a/src/ts/App.tsx +++ b/src/ts/App.tsx @@ -1,38 +1,14 @@ -import React, { lazy, Suspense } from "react"; -import { BrowserRouter as Router } from "react-router-dom"; +import React from "react"; import { Helmet } from "react-helmet-async"; import { AuthProvider } from "@n-apt/hooks/useAuthentication"; import { AuthenticationRoute as AuthRoute } from "@n-apt/routes/AuthenticationRoute"; import ReduxThemeProvider from "@n-apt/components/ReduxThemeProvider"; import { PostAuthLandingRedirect } from "@n-apt/components/PostAuthLandingRedirect"; import { useRustRebuildStatus } from "@n-apt/hooks/useRustRebuildStatus"; +import { AuthenticatedAppShell } from "@n-apt/AuthenticatedAppShell"; -// The full routed app (all providers + sidebars + the websocket streaming -// pipeline) is heavy. Lazy-load it inside the auth gate so the auth screen -// paints without downloading/parsing ReactFlow, the 3D/Model3D stack, or the -// spectrum streaming middleware graph. -const LazyAppShell = lazy(() => - import("@n-apt/AppShell").then((m) => ({ default: m.AppShell })), -); - -const AppLoadingFallback = () => ( -
- Loading… -
-); - -// Main App component with BrowserRouter wrapper +// Main App component. The Framework Mode root owns routing in production; +// Main.tsx supplies a BrowserRouter for the legacy Vite entry and stories. export const App: React.FC = () => { useRustRebuildStatus(); return ( @@ -61,25 +37,18 @@ export const App: React.FC = () => { /> - - - - {/* Wraps the auth-gated content so it observes the full auth - lifecycle and can block the app from painting while a - post-login redirect is pending. */} - - - {/* The heavy app (websocket streaming pipeline + all routed - providers/sidebars) only mounts after authentication - succeeds, so the auth screen paints first. */} - }> - - - - - - - + + + {/* Wraps the auth-gated content so it observes the full auth + lifecycle and can block the app from painting while a + post-login redirect is pending. */} + + + + + + + ); }; diff --git a/src/ts/AuthenticatedAppShell.tsx b/src/ts/AuthenticatedAppShell.tsx new file mode 100644 index 00000000..65f32725 --- /dev/null +++ b/src/ts/AuthenticatedAppShell.tsx @@ -0,0 +1,52 @@ +import React, { lazy, Suspense } from "react"; +import { useLocation } from "react-router"; + +export type AuthenticatedShellKind = "onboarding" | "application"; + +/** + * Keep authenticated onboarding separate from the full SDR application shell. + * `/get-started` is protected, but it must not initialize the live streaming + * provider tree just to render the lightweight starting-point page. + */ +export const getAuthenticatedShellKind = ( + pathname: string, +): AuthenticatedShellKind => + pathname === "/get-started" ? "onboarding" : "application"; + +const LazyAppShell = lazy(() => + import("@n-apt/AppShell").then((module) => ({ default: module.AppShell })), +); + +const LazyGetStartedRoute = lazy(() => + import("@n-apt/routes/GetStartedRoute").then((module) => ({ + default: module.default, + })), +); + +const ShellLoadingFallback: React.FC = () => ( +
+ Loading… +
+); + +export const AuthenticatedAppShell: React.FC = () => { + const { pathname } = useLocation(); + const shellKind = getAuthenticatedShellKind(pathname); + + return ( + }> + {shellKind === "onboarding" ? : } + + ); +}; diff --git a/src/ts/Main.tsx b/src/ts/Main.tsx index 30697712..e544d8e0 100644 --- a/src/ts/Main.tsx +++ b/src/ts/Main.tsx @@ -1,5 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router"; import { App } from "@n-apt/App"; import ReduxProvider from "@n-apt/components/ReduxProvider"; import { HelmetProvider } from "react-helmet-async"; @@ -13,7 +14,9 @@ root.render( - + + + , diff --git a/src/ts/agents/AgentIntegrationProvider.tsx b/src/ts/agents/AgentIntegrationProvider.tsx index 24f2efab..87115fdf 100644 --- a/src/ts/agents/AgentIntegrationProvider.tsx +++ b/src/ts/agents/AgentIntegrationProvider.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { useLocation } from "react-router-dom"; +import { useLocation } from "react-router"; import { useWebMCP, initializeWebMCP, diff --git a/src/ts/components/MainLayout.tsx b/src/ts/components/MainLayout.tsx index 1b807b70..57da80f4 100644 --- a/src/ts/components/MainLayout.tsx +++ b/src/ts/components/MainLayout.tsx @@ -5,7 +5,7 @@ import { NAPTSidebarHeader } from "@n-apt/components/sidebar/NAPTSidebarHeader"; import { SidebarRoutesNav } from "@n-apt/components/sidebar/SidebarRoutesNav"; import { CollapsedToggleButton } from "@n-apt/components/sidebar/SidebarToggle"; import { ContentArea } from "@n-apt/components/Layout"; -import { useLocation } from "react-router-dom"; +import { useLocation } from "react-router"; import { useSidebarNavigationScroll } from "@n-apt/hooks/useSidebarNavigationScroll"; const NavigationContainer = memo(styled.nav` diff --git a/src/ts/components/PostAuthLandingRedirect.tsx b/src/ts/components/PostAuthLandingRedirect.tsx index ee2da061..760ed6ed 100644 --- a/src/ts/components/PostAuthLandingRedirect.tsx +++ b/src/ts/components/PostAuthLandingRedirect.tsx @@ -1,5 +1,5 @@ import React, { useLayoutEffect, useRef, useState } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router"; import { useAuthentication } from "@n-apt/hooks/useAuthentication"; import { getPostAuthLandingPath } from "@n-apt/utils/bypassStartPage"; diff --git a/src/ts/components/ladle/LadleAppShell.tsx b/src/ts/components/ladle/LadleAppShell.tsx index 596080e7..bce5f832 100644 --- a/src/ts/components/ladle/LadleAppShell.tsx +++ b/src/ts/components/ladle/LadleAppShell.tsx @@ -2,7 +2,7 @@ import React from "react"; import { MainLayout } from "@n-apt/components/MainLayout"; import { useLadleContext, useLink } from "@ladle/react"; -import { useLocation } from "react-router-dom"; +import { useLocation } from "react-router"; interface LadleAppShellProps { children: React.ReactNode; diff --git a/src/ts/components/layout/AppPublicPageLayout.tsx b/src/ts/components/layout/AppPublicPageLayout.tsx index 01e4c588..43079239 100644 --- a/src/ts/components/layout/AppPublicPageLayout.tsx +++ b/src/ts/components/layout/AppPublicPageLayout.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Link } from "react-router-dom"; +import { Link } from "react-router"; import styled from "styled-components"; import { Logo } from "@n-apt/components/ui/Logo"; import { AppBackButton } from "@n-apt/components/ui/AppBackButton"; diff --git a/src/ts/components/sidebar/LearnSignalsSidebar.tsx b/src/ts/components/sidebar/LearnSignalsSidebar.tsx index 61d3d85c..34bc942d 100644 --- a/src/ts/components/sidebar/LearnSignalsSidebar.tsx +++ b/src/ts/components/sidebar/LearnSignalsSidebar.tsx @@ -1,6 +1,6 @@ import React from "react"; import styled from "styled-components"; -import { useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router"; import { useLearnSignals, SignalSection, diff --git a/src/ts/components/sidebar/SpectrumSidebar.tsx b/src/ts/components/sidebar/SpectrumSidebar.tsx index 1268376e..7395884c 100644 --- a/src/ts/components/sidebar/SpectrumSidebar.tsx +++ b/src/ts/components/sidebar/SpectrumSidebar.tsx @@ -8,7 +8,7 @@ import React, { } from "react"; import styled from "styled-components"; import { SatelliteDish, Trash2, Unplug, ChevronDown } from "lucide-react"; -import { useLocation } from "react-router-dom"; +import { useLocation } from "react-router"; import { useAppSelector, useAppDispatch, diff --git a/src/ts/components/ui/AppBackButton.tsx b/src/ts/components/ui/AppBackButton.tsx index 619d2ef5..0fd766a5 100644 --- a/src/ts/components/ui/AppBackButton.tsx +++ b/src/ts/components/ui/AppBackButton.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Link } from "react-router-dom"; +import { Link } from "react-router"; import styled from "styled-components"; import { DoorOpen } from "lucide-react"; import { useAuthentication } from "@n-apt/hooks/useAuthentication"; diff --git a/src/ts/components/ui/LinkCardGrid.tsx b/src/ts/components/ui/LinkCardGrid.tsx index 4e581bf6..3d954078 100644 --- a/src/ts/components/ui/LinkCardGrid.tsx +++ b/src/ts/components/ui/LinkCardGrid.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Link } from "react-router-dom"; +import { Link } from "react-router"; import styled from "styled-components"; import type { LucideIcon } from "lucide-react"; diff --git a/src/ts/contexts/LearnSignalsContext.tsx b/src/ts/contexts/LearnSignalsContext.tsx index 1eb319a4..7a3dc400 100644 --- a/src/ts/contexts/LearnSignalsContext.tsx +++ b/src/ts/contexts/LearnSignalsContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; +import { useParams } from "react-router"; export type SignalSection = | "Radio Waves" diff --git a/src/ts/framework/AuthenticatedRouteLayout.tsx b/src/ts/framework/AuthenticatedRouteLayout.tsx new file mode 100644 index 00000000..2c8c4615 --- /dev/null +++ b/src/ts/framework/AuthenticatedRouteLayout.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { Outlet } from "react-router"; + +export default function AuthenticatedRouteLayout() { + return ; +} + +export { RouteErrorBoundary as ErrorBoundary } from "@n-apt/framework/RouteErrorBoundary"; diff --git a/src/ts/framework/FullApplicationRoute.tsx b/src/ts/framework/FullApplicationRoute.tsx new file mode 100644 index 00000000..d5d46cb0 --- /dev/null +++ b/src/ts/framework/FullApplicationRoute.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { AuthenticatedAppShell } from "@n-apt/AuthenticatedAppShell"; +import { RouteErrorBoundary } from "@n-apt/framework/RouteErrorBoundary"; + +export default function FullApplicationRoute() { + // Keep the authenticated route module itself lightweight. The auth boundary + // must be able to render the login UI on a direct /visualizer (or /) request + // without evaluating the Spectrum/WebSocket application graph first. + return ; +} + +export const ErrorBoundary = RouteErrorBoundary; diff --git a/src/ts/framework/GetStartedRoute.tsx b/src/ts/framework/GetStartedRoute.tsx new file mode 100644 index 00000000..f58ea67a --- /dev/null +++ b/src/ts/framework/GetStartedRoute.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import GetStartedRoute from "@n-apt/routes/GetStartedRoute"; + +export default function FrameworkGetStartedRoute() { + return ; +} + +export { RouteErrorBoundary as ErrorBoundary } from "@n-apt/framework/RouteErrorBoundary"; diff --git a/src/ts/framework/LearnSignalsRoute.tsx b/src/ts/framework/LearnSignalsRoute.tsx new file mode 100644 index 00000000..eed1c271 --- /dev/null +++ b/src/ts/framework/LearnSignalsRoute.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { LearnSignalsProvider } from "@n-apt/contexts/LearnSignalsContext"; +import { LearnSignalsRoute as LearnSignalsPage } from "@n-apt/routes/LearnSignalsRoute"; + +export default function FrameworkLearnSignalsRoute() { + return ( + + + + ); +} + +export { RouteErrorBoundary as ErrorBoundary } from "@n-apt/framework/RouteErrorBoundary"; diff --git a/src/ts/framework/LegacyRedirectRoute.tsx b/src/ts/framework/LegacyRedirectRoute.tsx new file mode 100644 index 00000000..0e2b3b7b --- /dev/null +++ b/src/ts/framework/LegacyRedirectRoute.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { Navigate, useLocation } from "react-router"; + +const REDIRECTS: Record = { + "/faq": "/learn-signals", + "/faq/iq-captures": "/learn-signals/iq-captures", + "/iq-captures": "/learn-signals/iq-captures", + "/faq/fft-ifft": "/learn-signals/fft-ifft", + "/fft-ifft": "/learn-signals/fft-ifft", +}; + +export default function LegacyRedirectRoute() { + const { pathname } = useLocation(); + return ; +} + +export { RouteErrorBoundary as ErrorBoundary } from "@n-apt/framework/RouteErrorBoundary"; diff --git a/src/ts/framework/LegalDocumentRoute.tsx b/src/ts/framework/LegalDocumentRoute.tsx new file mode 100644 index 00000000..23ef501d --- /dev/null +++ b/src/ts/framework/LegalDocumentRoute.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { LegalDocumentRoute as LegalDocumentPage } from "@n-apt/routes/LegalDocumentRoute"; + +export default function FrameworkLegalDocumentRoute() { + return ; +} + +export { RouteErrorBoundary as ErrorBoundary } from "@n-apt/framework/RouteErrorBoundary"; diff --git a/src/ts/framework/NotFoundRoute.tsx b/src/ts/framework/NotFoundRoute.tsx new file mode 100644 index 00000000..3f69eca7 --- /dev/null +++ b/src/ts/framework/NotFoundRoute.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +export default function NotFoundRoute() { + return ( +
+ Page not found. +
+ ); +} + +export { RouteErrorBoundary as ErrorBoundary } from "@n-apt/framework/RouteErrorBoundary"; diff --git a/src/ts/framework/PublicRouteLayout.tsx b/src/ts/framework/PublicRouteLayout.tsx new file mode 100644 index 00000000..57d6a23e --- /dev/null +++ b/src/ts/framework/PublicRouteLayout.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { Outlet } from "react-router"; + +export default function PublicRouteLayout() { + return ; +} + +export { RouteErrorBoundary as ErrorBoundary } from "@n-apt/framework/RouteErrorBoundary"; diff --git a/src/ts/framework/QuestionnaireRoute.tsx b/src/ts/framework/QuestionnaireRoute.tsx new file mode 100644 index 00000000..9b943f2e --- /dev/null +++ b/src/ts/framework/QuestionnaireRoute.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { NsaProgramToolsShell } from "@n-apt/legal-app/NsaProgramToolsShell"; +import QuestionnaireRoute from "@n-apt/legal-app/routes/QuestionnaireRoute"; + +export default function FrameworkQuestionnaireRoute() { + return ( + + + + ); +} + +export { RouteErrorBoundary as ErrorBoundary } from "@n-apt/framework/RouteErrorBoundary"; diff --git a/src/ts/framework/RouteErrorBoundary.tsx b/src/ts/framework/RouteErrorBoundary.tsx new file mode 100644 index 00000000..b5d10e3b --- /dev/null +++ b/src/ts/framework/RouteErrorBoundary.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { isRouteErrorResponse, useRouteError } from "react-router"; + +export function RouteErrorBoundary() { + const error = useRouteError(); + const message = isRouteErrorResponse(error) + ? `${error.status} ${error.statusText}` + : "The route could not be loaded."; + + return ( +
+

Unable to load this page

+

{message} Please refresh and try again.

+
+ ); +} diff --git a/src/ts/framework/XArchiveFormatterRoute.tsx b/src/ts/framework/XArchiveFormatterRoute.tsx new file mode 100644 index 00000000..cf0bcb42 --- /dev/null +++ b/src/ts/framework/XArchiveFormatterRoute.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { NsaProgramToolsShell } from "@n-apt/legal-app/NsaProgramToolsShell"; +import TranscriptFixerRoute from "@n-apt/legal-app/routes/TranscriptFixerRoute"; + +export default function FrameworkXArchiveFormatterRoute() { + return ( + + + + ); +} + +export { RouteErrorBoundary as ErrorBoundary } from "@n-apt/framework/RouteErrorBoundary"; diff --git a/src/ts/hooks/useAuthentication.ts b/src/ts/hooks/useAuthentication.ts index 3a8e0066..4d28f239 100644 --- a/src/ts/hooks/useAuthentication.ts +++ b/src/ts/hooks/useAuthentication.ts @@ -56,6 +56,10 @@ type AuthAction = | { type: "REGISTER_SUCCESS"; hasPasskeys: boolean }; const getInitialHasPasskeys = () => { + if (typeof localStorage === "undefined") { + return false; + } + try { return localStorage.getItem("n_apt_has_passkeys") === "true"; } catch { diff --git a/src/ts/hooks/useSidebarNavigationScroll.ts b/src/ts/hooks/useSidebarNavigationScroll.ts index adf767d0..c72d4602 100644 --- a/src/ts/hooks/useSidebarNavigationScroll.ts +++ b/src/ts/hooks/useSidebarNavigationScroll.ts @@ -1,6 +1,6 @@ import { useCallback, useLayoutEffect, useRef } from "react"; import type React from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router"; let hasInitializedSidebarScroll = false; let lastSidebarScrollTop = 0; diff --git a/src/ts/hooks/useSpectrumStore.tsx b/src/ts/hooks/useSpectrumStore.tsx index 2cc544b2..84f1d0c6 100644 --- a/src/ts/hooks/useSpectrumStore.tsx +++ b/src/ts/hooks/useSpectrumStore.tsx @@ -25,7 +25,7 @@ import type { import { useAuthentication } from "@n-apt/hooks/useAuthentication"; import { isMockDevice, isMockTxSource } from "@n-apt/utils/deviceCapabilities"; import { buildWsUrl } from "@n-apt/services/auth"; -import { useLocation } from "react-router-dom"; +import { useLocation } from "react-router"; import { useAppDispatch, useAppSelector } from "@n-apt/redux/store"; import { selectActiveSourceDerivedState, diff --git a/src/ts/legal-app/NsaProgramToolsShell.tsx b/src/ts/legal-app/NsaProgramToolsShell.tsx index 6d6d36da..404c00fe 100644 --- a/src/ts/legal-app/NsaProgramToolsShell.tsx +++ b/src/ts/legal-app/NsaProgramToolsShell.tsx @@ -1,6 +1,6 @@ import React from "react"; import styled from "styled-components"; -import { Link, useLocation } from "react-router-dom"; +import { Link, useLocation } from "react-router"; import "./nsa-program-tools.css"; const Shell = styled.div` diff --git a/src/ts/redux/middleware/localStorageMiddleware.ts b/src/ts/redux/middleware/localStorageMiddleware.ts index 5d6a7cd7..fa28e5bf 100644 --- a/src/ts/redux/middleware/localStorageMiddleware.ts +++ b/src/ts/redux/middleware/localStorageMiddleware.ts @@ -11,9 +11,20 @@ const STORAGE_KEYS = { } as const; // Safe localStorage operations +const getAvailableLocalStorage = (): Storage | null => { + try { + return typeof localStorage === "undefined" ? null : localStorage; + } catch { + return null; + } +}; + const safeSetItem = (key: string, value: string): boolean => { + const storage = getAvailableLocalStorage(); + if (!storage) return false; + try { - localStorage.setItem(key, value); + storage.setItem(key, value); return true; } catch (error) { console.warn(`Failed to save to localStorage (${key}):`, error); @@ -22,8 +33,11 @@ const safeSetItem = (key: string, value: string): boolean => { }; const safeGetItem = (key: string): string | null => { + const storage = getAvailableLocalStorage(); + if (!storage) return null; + try { - return localStorage.getItem(key); + return storage.getItem(key); } catch (error) { console.warn(`Failed to read from localStorage (${key}):`, error); return null; @@ -31,8 +45,11 @@ const safeGetItem = (key: string): string | null => { }; const safeRemoveItem = (key: string): boolean => { + const storage = getAvailableLocalStorage(); + if (!storage) return false; + try { - localStorage.removeItem(key); + storage.removeItem(key); return true; } catch (error) { console.warn(`Failed to remove from localStorage (${key}):`, error); diff --git a/src/ts/root.tsx b/src/ts/root.tsx new file mode 100644 index 00000000..a80272e4 --- /dev/null +++ b/src/ts/root.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; +import { HelmetProvider } from "react-helmet-async"; +import ReduxProvider from "@n-apt/components/ReduxProvider"; +import ReduxThemeProvider from "@n-apt/components/ReduxThemeProvider"; +import { AuthProvider } from "@n-apt/hooks/useAuthentication"; +import { AuthenticationRoute as AuthRoute } from "@n-apt/routes/AuthenticationRoute"; +import { PostAuthLandingRedirect } from "@n-apt/components/PostAuthLandingRedirect"; +import "./fonts.css"; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + N-APT + + + + +
{children}
+ + + + + ); +} + +export function HydrateFallback() { + return ( +
+ Loading N-APT… +
+ ); +} + +export default function Root() { + return ( + + + + + + + + + + + + + + ); +} diff --git a/src/ts/routes.ts b/src/ts/routes.ts new file mode 100644 index 00000000..75fcc0d7 --- /dev/null +++ b/src/ts/routes.ts @@ -0,0 +1,83 @@ +import { + index, + layout, + type RouteConfig, + route, +} from "@react-router/dev/routes"; + +// Incremental adoption boundary: the Framework router owns the document, +// discovery manifest, and history while the existing route tree continues to +// own feature behavior. Individual routes can move out of this bridge without +// changing authentication or streaming ownership. +export default [ + layout("./framework/PublicRouteLayout.tsx", [ + route("learn-signals", "./framework/LearnSignalsRoute.tsx", { + id: "learn-signals", + }), + route("learn-signals/:sectionSlug", "./framework/LearnSignalsRoute.tsx", { + id: "learn-signals-section", + }), + route("terms", "./framework/LegalDocumentRoute.tsx", { id: "terms" }), + route("privacy", "./framework/LegalDocumentRoute.tsx", { id: "privacy" }), + route("license", "./framework/LegalDocumentRoute.tsx", { id: "license" }), + route("responsible-use", "./framework/LegalDocumentRoute.tsx", { + id: "responsible-use", + }), + route("faq", "./framework/LegacyRedirectRoute.tsx", { id: "faq" }), + route("faq/iq-captures", "./framework/LegacyRedirectRoute.tsx", { + id: "faq-iq-captures", + }), + route("iq-captures", "./framework/LegacyRedirectRoute.tsx", { + id: "iq-captures", + }), + route("faq/fft-ifft", "./framework/LegacyRedirectRoute.tsx", { + id: "faq-fft-ifft", + }), + route("fft-ifft", "./framework/LegacyRedirectRoute.tsx", { + id: "fft-ifft", + }), + ]), + layout("./framework/AuthenticatedRouteLayout.tsx", [ + route("auth", "./framework/FullApplicationRoute.tsx", { id: "auth" }), + route("get-started", "./framework/GetStartedRoute.tsx"), + index("./framework/FullApplicationRoute.tsx", { id: "index" }), + route("visualizer", "./framework/FullApplicationRoute.tsx", { + id: "visualizer", + }), + route("demodulate", "./framework/FullApplicationRoute.tsx", { + id: "demodulate", + }), + route("demod", "./framework/FullApplicationRoute.tsx", { id: "demod" }), + route("settings", "./framework/FullApplicationRoute.tsx", { + id: "settings", + }), + route("draw-signal", "./framework/FullApplicationRoute.tsx", { + id: "draw-signal", + }), + route("3d-model", "./framework/FullApplicationRoute.tsx", { + id: "3d-model", + }), + route("3d-model-gallery", "./framework/FullApplicationRoute.tsx", { + id: "3d-model-gallery", + }), + route("map-endpoints", "./framework/FullApplicationRoute.tsx", { + id: "map-endpoints", + }), + route("diagnostics/anti-aliasing", "./framework/FullApplicationRoute.tsx", { + id: "anti-aliasing", + }), + route("pretext-demo", "./framework/FullApplicationRoute.tsx", { + id: "pretext-demo", + }), + route("vfo-grid-demo", "./framework/FullApplicationRoute.tsx", { + id: "vfo-grid-demo", + }), + route("transformers", "./framework/FullApplicationRoute.tsx", { + id: "transformers", + }), + route("game", "./framework/FullApplicationRoute.tsx", { id: "game" }), + route("questionnaire", "./framework/QuestionnaireRoute.tsx"), + route("x-archive-formatter", "./framework/XArchiveFormatterRoute.tsx"), + ]), + route("*", "./framework/NotFoundRoute.tsx"), +] satisfies RouteConfig; diff --git a/src/ts/routes/AuthenticationRoute.tsx b/src/ts/routes/AuthenticationRoute.tsx index 769b9b3a..91c52a8d 100644 --- a/src/ts/routes/AuthenticationRoute.tsx +++ b/src/ts/routes/AuthenticationRoute.tsx @@ -4,7 +4,7 @@ import styled, { ThemeProvider, ThemeContext, } from "styled-components"; -import { Link, useLocation } from "react-router-dom"; +import { Link, useLocation } from "react-router"; import { Button } from "@n-apt/components/ui/Button"; import { FileSignal, Lock, Radio, ThumbsUp, TriangleAlert } from "lucide-react"; import { Tooltip } from "@n-apt/components/ui/Tooltip"; @@ -1032,8 +1032,7 @@ export const AuthenticationRoute: React.FC = ({ location.pathname === "/privacy" || location.pathname === "/license" || location.pathname === "/responsible-use" || - location.pathname.startsWith("/learn-signals") || - location.pathname === "/get-started"; + location.pathname.startsWith("/learn-signals"); if (isPublicRoute) { return <>{children}; diff --git a/src/ts/routes/GetStartedRoute.tsx b/src/ts/routes/GetStartedRoute.tsx index c703aba3..f4852daa 100644 --- a/src/ts/routes/GetStartedRoute.tsx +++ b/src/ts/routes/GetStartedRoute.tsx @@ -1,6 +1,6 @@ import React, { useRef, useState } from "react"; import styled from "styled-components"; -import { Link, useNavigate } from "react-router-dom"; +import { Link, useNavigate } from "react-router"; import { Box, FileSignal, diff --git a/src/ts/routes/IQCapturesRoute.tsx b/src/ts/routes/IQCapturesRoute.tsx index 2d0f3350..753e5751 100644 --- a/src/ts/routes/IQCapturesRoute.tsx +++ b/src/ts/routes/IQCapturesRoute.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Link } from "react-router-dom"; +import { Link } from "react-router"; import styled from "styled-components"; import IQCaptureCanvasGraphic from "@n-apt/components/canvas/IQCaptureCanvasGraphic"; diff --git a/src/ts/routes/LearnSignalsRoute.tsx b/src/ts/routes/LearnSignalsRoute.tsx index 58f8d1cb..53afbfc3 100644 --- a/src/ts/routes/LearnSignalsRoute.tsx +++ b/src/ts/routes/LearnSignalsRoute.tsx @@ -1,6 +1,6 @@ import React from "react"; import styled from "styled-components"; -import { useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router"; import { useLearnSignals, SignalSection, diff --git a/src/ts/routes/LegalDocumentRoute.tsx b/src/ts/routes/LegalDocumentRoute.tsx index 1622352e..93495d1e 100644 --- a/src/ts/routes/LegalDocumentRoute.tsx +++ b/src/ts/routes/LegalDocumentRoute.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useState } from "react"; -import { useLocation } from "react-router-dom"; +import { useLocation } from "react-router"; import styled from "styled-components"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; diff --git a/src/ts/routes/RouteScopedProviders.tsx b/src/ts/routes/RouteScopedProviders.tsx new file mode 100644 index 00000000..ca960c15 --- /dev/null +++ b/src/ts/routes/RouteScopedProviders.tsx @@ -0,0 +1,78 @@ +import React, { lazy, Suspense } from "react"; +import { useLocation } from "react-router"; + +export type RouteProviderGroup = + | "none" + | "demod" + | "model" + | "map" + | "draw-signal"; + +export const getRouteProviderGroup = (pathname: string): RouteProviderGroup => { + if (pathname === "/demod" || pathname === "/demodulate") return "demod"; + if (pathname === "/3d-model" || pathname === "/3d-model-gallery") { + return "model"; + } + if (pathname === "/map-endpoints") return "map"; + if (pathname === "/draw-signal") return "draw-signal"; + return "none"; +}; + +const LazyDemodRouteProviders = lazy(() => + import("@n-apt/routes/route-providers/DemodRouteProviders").then( + (module) => ({ + default: module.DemodRouteProviders, + }), + ), +); +const LazyModelRouteProviders = lazy(() => + import("@n-apt/routes/route-providers/ModelRouteProviders").then( + (module) => ({ + default: module.ModelRouteProviders, + }), + ), +); +const LazyMapRouteProviders = lazy(() => + import("@n-apt/routes/route-providers/MapRouteProviders").then((module) => ({ + default: module.MapRouteProviders, + })), +); +const LazyDrawSignalRouteProviders = lazy(() => + import("@n-apt/routes/route-providers/DrawSignalRouteProviders").then( + (module) => ({ default: module.DrawSignalRouteProviders }), + ), +); + +export const LazyDemodSidebarAdapter = lazy(() => + import("@n-apt/routes/route-providers/DemodRouteProviders").then( + (module) => ({ + default: module.DemodSidebarAdapter, + }), + ), +); + +const RouteProviderLoading: React.FC = () =>