diff --git a/.changeset/swift-bears-carry.md b/.changeset/swift-bears-carry.md new file mode 100644 index 00000000..e8f58b6c --- /dev/null +++ b/.changeset/swift-bears-carry.md @@ -0,0 +1,5 @@ +--- +"@dmno/react-router-integration": patch +--- + +react-router 7 integration - initial version diff --git a/packages/integrations/react-router/.eslintrc.cjs b/packages/integrations/react-router/.eslintrc.cjs new file mode 100644 index 00000000..8f2a36a2 --- /dev/null +++ b/packages/integrations/react-router/.eslintrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + extends: ["@dmno/eslint-config/base"], + ignorePatterns: ["tsup.config.ts"], + rules: { + }, +}; diff --git a/packages/integrations/react-router/README.md b/packages/integrations/react-router/README.md new file mode 100644 index 00000000..94e6ef1f --- /dev/null +++ b/packages/integrations/react-router/README.md @@ -0,0 +1,48 @@ +Check out the [docs](https://dmno.dev/docs/integrations/remix/) for more information on how to use [DMNO](https://dmno.dev) + [Remix](https://remix.run/). + +If you have any questions, please reach out to us on [Discord](https://chat.dmno.dev). + +---- + +# @dmno/remix-integration [![npm](https://img.shields.io/npm/v/@dmno/remix-integration)](https://www.npmjs.com/package/@dmno/remix-integration) + +Provides tooling to integrate dmno into your Remix dev/build workflow + +### Installation + +```bash +# let dmno init automatically add the integration +npx dmno init +``` + +```bash +# or do it manually +npm add @dmno/remix-integration +``` + +### Example Usage + +Import and initialize our remix integration and add to your `vite.config.ts` file. +You must add both the Vite plugin and the Remix preset. + +```typescript +import { dmnoRemixVitePlugin, dmnoRemixPreset } from "@dmno/remix-integration"; +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + dmnoRemixVitePlugin(), + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + presets: [dmnoRemixPreset() as any], + }), + tsconfigPaths(), + ], +}); +``` diff --git a/packages/integrations/react-router/dmno.meta.json b/packages/integrations/react-router/dmno.meta.json new file mode 100644 index 00000000..2082b63c --- /dev/null +++ b/packages/integrations/react-router/dmno.meta.json @@ -0,0 +1,22 @@ +{ + "integrationForPackage": "react-router", + "publicPrefix": "VITE_", + "installationCodemods": [ + { + "glob": "vite.config.*", + "imports": [ { + "moduleName": "@dmno/react-router-integration", + "importVars": [ "dmnoReactRouterVitePlugin" ], + } ], + "updates": [ + { + "symbol": "EXPORT", + "path": [ "plugins" ], + "action": { + "arrayContains": "dmnoReactRouterVitePlugin()" + } + } + ] + } + ], +} diff --git a/packages/integrations/react-router/package.json b/packages/integrations/react-router/package.json new file mode 100644 index 00000000..d6e61ddf --- /dev/null +++ b/packages/integrations/react-router/package.json @@ -0,0 +1,69 @@ +{ + "name": "@dmno/react-router-integration", + "version": "0.0.0", + "description": "tools for integrating dmno into react router 7+ (formerly Remix)", + "author": "dmno-dev", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/dmno-dev/dmno.git", + "directory": "packages/integrations/react-router" + }, + "bugs": "https://github.com/dmno-dev/dmno/issues", + "homepage": "https://dmno.dev/docs/integrations/react-router", + "keywords": [ + "dmno", + "react-router", + "config", + "env vars", + "environment variables", + "secrets", + "integration", + "react-router-preset", + "dmno-integration" + ], + "type": "module", + "exports": { + ".": { + "ts-src": "./src/index.ts", + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./meta": { + "default": "./dmno.meta.json" + } + }, + "files": [ + "/dist", + "/dmno.meta.json" + ], + "scripts": { + "build": "tsup", + "build:ifnodist": "[ -d \"./dist\" ] && echo 'dist exists' || pnpm build", + "dev": "pnpm run build --watch", + "lint": "eslint src --ext .ts,.cjs", + "lint:fix": "pnpm run lint --fix" + }, + "devDependencies": { + "@dmno/eslint-config": "workspace:*", + "@dmno/ts-lib": "workspace:*", + "@dmno/tsconfig": "workspace:*", + "@types/async": "^3.2.24", + "@types/debug": "catalog:", + "@types/lodash-es": "catalog:", + "@types/node": "catalog:", + "dmno": "workspace:*", + "tsup": "catalog:", + "typescript": "catalog:", + "vite": "^6.2.6" + }, + "peerDependencies": { + "react-router": "^7", + "dmno": "^0", + "vite": ">=6" + }, + "dependencies": { + "debug": "catalog:", + "lodash-es": "catalog:" + } +} diff --git a/packages/integrations/react-router/src/index.ts b/packages/integrations/react-router/src/index.ts new file mode 100644 index 00000000..123fe957 --- /dev/null +++ b/packages/integrations/react-router/src/index.ts @@ -0,0 +1,330 @@ +import { dirname, relative } from 'path'; +import { fileURLToPath } from 'url'; + +import * as _ from 'lodash-es'; +import Debug from 'debug'; +import { checkServiceIsValid, DmnoServer, injectDmnoGlobals } from 'dmno'; +import { Plugin } from 'vite'; + +const debug = Debug('dmno:react-router-integration'); +const __dirname = dirname(fileURLToPath(import.meta.url)); + +let firstLoad = !(process as any).dmnoServer; + +debug('dmno react-router+vite plugin loaded. first load = ', firstLoad); + +let isDevMode: boolean; +let dmnoHasTriggeredReload = false; +let configItemKeysAccessed: Record = {}; +let dmnoConfigValid = true; +let dmnoServer: DmnoServer; +let dmnoInjectionResult: ReturnType; +let enableDynamicPublicClientLoading = false; + +async function reloadDmnoConfig() { + (process as any).dmnoServer ||= new DmnoServer({ watch: true }); + dmnoServer = (process as any).dmnoServer; + const resolvedService = await dmnoServer.getCurrentPackageConfig(); + const injectedConfig = resolvedService.injectedDmnoEnv; + dmnoConfigValid = resolvedService.serviceDetails.isValid; + configItemKeysAccessed = {}; + + // shows nicely formatted errors in the terminal + checkServiceIsValid(resolvedService.serviceDetails); + + dmnoInjectionResult = injectDmnoGlobals({ + injectedConfig, + trackingObject: configItemKeysAccessed, + }); +} + +// we run this right away so the globals get injected into the vite.config file +await reloadDmnoConfig(); + +// const DYNAMIC_CONFIG_VIRTUAL_MODULE_ID = 'virtual:dmno-public-dynamic-config-api-route'; + + +let buildDir: string; + +type DmnoPluginOptions = { + injectResolvedConfigAtBuildTime: boolean, +}; + +export function dmnoReactRouterVitePlugin(dmnoOptions?: DmnoPluginOptions): Plugin { + const dmnoServer: DmnoServer = (process as any).dmnoServer; + + // detect if we need to build the resolved config into the output + // which is needed when running on external platforms where we dont have ability to use `dmno run` + let injectResolvedConfigAtBuildTime = ( + process.env.__VERCEL_BUILD_RUNNING // build running via `vercel` cli + || process.env.NETLIFY // build running remotely on netlify + || (process.env.NETLIFY_LOCAL && !process.env.NETLIFY_DEV) // build running locally via `netlify` cli + || process.env.CF_PAGES // maybe add additional check for /functions folder? + || dmnoOptions?.injectResolvedConfigAtBuildTime // explicit opt-in + ); + + let buildingForCloudflare = false; + + return { + name: 'dmno-react-router-vite-plugin', + enforce: 'pre', // not positive this matters + + // this function will get called on each restart! + async config(config, env) { + // check if the vite cloudflare plugin is registered to detect if we are building for cloudflare + buildingForCloudflare = !!config.plugins?.find((p) => { + return (Array.isArray(p) ? p : [p]).find((p2) => { + return p2 && (p2 as any).name?.startsWith('vite-plugin-cloudflare:') + }); + }); + if (buildingForCloudflare) injectResolvedConfigAtBuildTime = true; + + // console.log('vite config hook', config, env); + // RR loads 2 vite servers, one for frontend, one for back + // this feels hacky to identify it but seems to be working ok + const isBackendViteServer = ( + // during dev mode, the "mode" is only actually set to "development" on the backend s + config.mode === 'development' + // during build, we can use the "build.ssr" flag + || config.build?.ssr + ); + debug('detected vite backend build =', isBackendViteServer); + + isDevMode = env.command === 'serve'; + buildDir = `${config.root || process.cwd()}/${config.build?.outDir}`; + + // this handles the case where astro's vite server reloaded but this file did not get reloaded + // we need to reload if we just found out we are in dev mode - so it will use the config client + if (dmnoHasTriggeredReload) { + await reloadDmnoConfig(); + dmnoHasTriggeredReload = false; + } + + // inject rollup rewrites via config.define + // we have to filter out existing DMNO entries, or else we'd have config items stay + // even after removed from our config schema + const existingDefineWithoutDmnoEntries = Object.fromEntries( + Object.entries(config.define || {}) + .filter((k) => !k[0].match(/DMNO_(PUBLIC_)?CONFIG\./)), + ); + config.define = { + ...existingDefineWithoutDmnoEntries, + // always inject static DMNO_PUBLIC_CONFIG + ...dmnoInjectionResult.staticReplacements.dmnoPublicConfig, + // only inject static DMNO_CONFIG on the backend build + ...isBackendViteServer && dmnoInjectionResult.staticReplacements.dmnoConfig, + }; + + if (!dmnoConfigValid) { + if (isDevMode) { + // adjust vite's setting so it doesnt bury the error messages + config.clearScreen = false; + } else { + dmnoServer.shutdown(); + console.log('💥 DMNO config validation failed 💥'); + // throwing an error spits out a big useless stack trace... so better to just exit? + process.exit(1); + } + } + + // TODO: should also check if we are building in totally static mode + enableDynamicPublicClientLoading = dmnoInjectionResult.publicDynamicKeys.length > 0; + + return config; + }, + async configureServer(server) { + // console.log('configure server!', server.config); + + // there are 2 vite servers running, we need to trigger the reload on the non "spa" one + if (firstLoad && server.config.command === 'serve' && server.config.appType !== 'spa') { + firstLoad = false; + dmnoServer.enableWatchMode(() => { + debug('dmno config client received reload event - restarting vite server'); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + server.restart(); + dmnoHasTriggeredReload = true; + }); + } + + + // this scans server-responses in dev mode only! + server.middlewares.use((req, res, next) => { + // custom handle request... + // console.log('vite server middleware!', req.url, res); + + // TODO: skip handling for more data (images, etc) + if (req.url?.endsWith('.ico')) return next(); + + // console.log('vite dev server middleware for url: ', req.url); + + const oWrite = res.write; + res.write = function (...args) { + const rawChunk = args[0]; + const decoder = new TextDecoder(); + const chunkStr = decoder.decode(rawChunk); + // console.log(chunkStr); + (globalThis as any)._dmnoLeakScan(chunkStr, { method: 'react router vite dev server middleware (ServerResponse.write)', file: req.url }); + // @ts-ignore + return oWrite.apply(this, args); + }; + + const oEnd = res.end; + // @ts-ignore + res.end = function (...args) { + let chunkStr = args[0]; + if ( + chunkStr + && typeof chunkStr === 'string' + // vite client env includes all static rewrites during dev mode + // TODO: maybe can exclude them somehow? + && !req.url?.endsWith('/vite/dist/client/env.mjs') + ) { + (globalThis as any)._dmnoLeakScan(chunkStr, { method: 'react router vite dev server middleware (ServerResponse.end)', file: req.url }); + } + // @ts-ignore + return oEnd.apply(this, args); + }; + + return next(); + }); + + if (!dmnoConfigValid) { + // triggers the built-in vite error overlay + server.middlewares.use((req, res, next) => { + server.hot.send({ + type: 'error', + err: { + plugin: 'DMNO', + message: 'Your config is currently invalid - check your terminal for more details', + stack: '', + }, + }); + return next(); + }); + } + }, + + renderChunk(code, chunk, options, meta) { + if (dmnoInjectionResult.serviceSettings.preventClientLeaks) { + // scan all "client" chunks for secrets + if (options.dir?.endsWith('/build/client')) { + // TODO: add more metadata + (globalThis as any)._dmnoLeakScan(code, { + method: 'react router vite client chunk scan', + file: chunk.fileName, + }); + } + } + }, + + // leak detection in _built_ files + transform(src, id) { + // inject server-side code here - either into the user-provided `entry.server.tsx` or a default file like `entry.server.node.tsx` + if (id.match(/\/entry\.server(\.[a-z]+)?\.[jt]sx/)) { + // note - can also patch '\0virtual:react-router/server-build' + return [ + // 'console.log(\'>>> injected server dmno code <<<\');', + + `import { injectDmnoGlobals } from "dmno/injector-standalone${buildingForCloudflare ? '/edge' : ''}";`, + + // call the globals injector + // and inject the resolved config values if we are building for netlify/vercel/etc + 'injectDmnoGlobals({', + injectResolvedConfigAtBuildTime ? `injectedConfig: ${JSON.stringify(dmnoInjectionResult.injectedDmnoEnv)},` : '', + '});', + + src, + ].join('\n'); + } + + if ( + this.environment.name === 'client' && ( + // `/entry.client.tsx` would seem like the appropriate place, + // but it is not actually loaded first, so we dont get DMNO_CONFIG available in calls at the top level of routes + // and is also not effective in both dev and built code + + // this works for "built" code + id.endsWith('/node_modules/react/jsx-runtime.js') + + // this works during local dev + || id.endsWith('/node_modules/vite/dist/client/env.mjs') + ) + ) { + return [ + // original source - must be first, because in the vite env case, it has already injected the static config from the define plugin + src, + + // 'console.log(\'>>> injected CLIENT dmno code <<<\');', + + // client side DMNO_PUBLIC_CONFIG proxy object + // TODO: ideally we can throw a better error if we know its a dynamic item and we aren't loading dynamic stuff + ` + window._DMNO_PUBLIC_STATIC_CONFIG = window.DMNO_PUBLIC_CONFIG || {}; + window.DMNO_PUBLIC_CONFIG = new Proxy({}, { + get(o, key) { + if (key in window._DMNO_PUBLIC_STATIC_CONFIG) { + return window._DMNO_PUBLIC_STATIC_CONFIG[key]; + } + `, + + // if dynamic public config is enabled, we'll fetch it on-demand + // this is fine because we only hit this block if the rewrite failed + // (or wasnt found in the static vars during dev) + enableDynamicPublicClientLoading ? ` + if (!window._DMNO_PUBLIC_DYNAMIC_CONFIG) { + const request = new XMLHttpRequest(); + request.open("GET", "/_dmno-public-dynamic-config", false); // false means sync/blocking! + request.send(null); + + if (request.status !== 200) { + throw new Error('Failed to load public dynamic DMNO config'); + } + window._DMNO_PUBLIC_DYNAMIC_CONFIG = JSON.parse(request.responseText); + + console.log('loaded public dynamic config', window._DMNO_PUBLIC_DYNAMIC_CONFIG); + } + + if (key in window._DMNO_PUBLIC_DYNAMIC_CONFIG) { + return window._DMNO_PUBLIC_DYNAMIC_CONFIG[key]; + } + ` : ` + if (${JSON.stringify(dmnoInjectionResult.publicDynamicKeys)}.includes(key)) { + throw new Error(\`❌ Unable to access dynamic config item \\\`\${key}\\\` in "static" output mode\`); + } + `, // TODO: tailor message above to react router's static mode + + // in dev mode, we'll give a more detailed error message, letting the user know if they tried to access a sensitive or non-existant item + isDevMode ? ` + if (${JSON.stringify(dmnoInjectionResult.sensitiveKeys)}.includes(key)) { + throw new Error(\`❌ \\\`DMNO_PUBLIC_CONFIG.\${key}\\\` not found - it is sensitive and must be accessed via DMNO_CONFIG on the server only\`); + } else { + throw new Error(\`❌ \\\`DMNO_PUBLIC_CONFIG.\${key}\\\` not found - it does not exist in your config schema\`); + } + ` : ` + throw new Error(\`❌ \\\`DMNO_PUBLIC_CONFIG.\${key}\\\` not found - it may be sensitive or it may not exist at all\`); + `, + ` + } + }); + `, + + // DMNO_CONFIG proxy object just to give a helpful error message + // TODO: we could make this a warning instead? because it does get replaced during the build and doesn't actually harm anything + ` + window.DMNO_CONFIG = new Proxy({}, { + get(o, key) { + throw new Error(\`❌ You cannot access DMNO_CONFIG on the client, try DMNO_PUBLIC_CONFIG.\${key} instead \`); + } + }); + `, + ].join('\n'); + } + + return src; + }, + + // handleHotUpdate({ file, server }) { + // console.log('hot update', file); + // }, + } +} diff --git a/packages/integrations/react-router/tsconfig.json b/packages/integrations/react-router/tsconfig.json new file mode 100644 index 00000000..55ad3015 --- /dev/null +++ b/packages/integrations/react-router/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@dmno/tsconfig/tsconfig.node.json", + "compilerOptions": { + "rootDir": ".", + "module": "ESNext", + "outDir": "dist", + "lib": [ + "ESNext" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts" + ] +} diff --git a/packages/integrations/react-router/tsup.config.ts b/packages/integrations/react-router/tsup.config.ts new file mode 100644 index 00000000..96138d30 --- /dev/null +++ b/packages/integrations/react-router/tsup.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: [ // Entry point(s) + 'src/index.ts', // main lib, users will import from here + // 'src/public-dynamic-config-api-route.ts', + ], + + external: [ + "dmno", "astro", + ], + noExternal: ['@dmno/ts-lib', '@dmno/encryption-lib'], + + + dts: true, // Generate .d.ts files + // minify: true, // Minify output + sourcemap: true, // Generate sourcemaps + treeshake: true, // Remove unused code + + clean: true, // Clean output directory before building + outDir: "dist", // Output directory + + format: ['esm'], // Output format(s) + + splitting: true, // split output into chunks - MUST BE ON! or we get issues with multiple copies of classes and instanceof + keepNames: true, // stops build from prefixing our class names with `_` in some cases +}); diff --git a/packages/integrations/remix/README.md b/packages/integrations/remix/README.md index 94e6ef1f..98d9154b 100644 --- a/packages/integrations/remix/README.md +++ b/packages/integrations/remix/README.md @@ -1,12 +1,12 @@ -Check out the [docs](https://dmno.dev/docs/integrations/remix/) for more information on how to use [DMNO](https://dmno.dev) + [Remix](https://remix.run/). +Check out the [docs](https://dmno.dev/docs/integrations/react-router/) for more information on how to use [DMNO](https://dmno.dev) + [React Router](https://reactrouter.com/). If you have any questions, please reach out to us on [Discord](https://chat.dmno.dev). ---- -# @dmno/remix-integration [![npm](https://img.shields.io/npm/v/@dmno/remix-integration)](https://www.npmjs.com/package/@dmno/remix-integration) +# @dmno/react-router-integration [![npm](https://img.shields.io/npm/v/@dmno/react-router-integration)](https://www.npmjs.com/package/@dmno/react-router-integration) -Provides tooling to integrate dmno into your Remix dev/build workflow +Provides tooling to integrate dmno into your React Router dev/build workflow ### Installation @@ -17,32 +17,28 @@ npx dmno init ```bash # or do it manually -npm add @dmno/remix-integration +npm add @dmno/react-router-integration ``` ### Example Usage -Import and initialize our remix integration and add to your `vite.config.ts` file. -You must add both the Vite plugin and the Remix preset. +Import and initialize our react-router integration and add to your `vite.config.ts` file. ```typescript -import { dmnoRemixVitePlugin, dmnoRemixPreset } from "@dmno/remix-integration"; -import { vitePlugin as remix } from "@remix-run/dev"; +import { dmnoReactRouterVitePlugin } from "@dmno/react-router-integration"; + import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ plugins: [ - dmnoRemixVitePlugin(), - remix({ - future: { - v3_fetcherPersist: true, - v3_relativeSplatPath: true, - v3_throwAbortReason: true, - }, - presets: [dmnoRemixPreset() as any], - }), + dmnoReactRouterVitePlugin(), // <- add this + tailwindcss(), + reactRouter(), tsconfigPaths(), ], }); ``` + + +Depending on your setup, you may also need to explicitly include `dmno-env.d.ts` in your `tsconfig.json` file. diff --git a/packages/integrations/remix/src/index.ts b/packages/integrations/remix/src/index.ts index 60d8ab92..cec1990f 100644 --- a/packages/integrations/remix/src/index.ts +++ b/packages/integrations/remix/src/index.ts @@ -214,7 +214,6 @@ export function dmnoRemixVitePlugin(dmnoOptions?: DmnoPluginOptions) { // leak detection in _built_ files transform(src, id) { - console.log(`transform - ${id}`); // inject server-side code here - either into the user-provided `entry.server.tsx` or a default file like `entry.server.node.tsx` if (id.match(/\/entry\.server(\.[a-z]+)?\.[jt]sx/)) { // note - can also patch '\0virtual:remix/server-build' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52844377..62c9d59a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -702,6 +702,52 @@ importers: specifier: 'catalog:' version: 5.7.2 + packages/integrations/react-router: + dependencies: + debug: + specifier: 'catalog:' + version: 4.4.0 + lodash-es: + specifier: 'catalog:' + version: 4.17.21 + react-router: + specifier: ^7 + version: 7.5.0(react-dom@18.3.0(react@18.3.0))(react@18.3.0) + devDependencies: + '@dmno/eslint-config': + specifier: workspace:* + version: link:../../eslint-config + '@dmno/ts-lib': + specifier: workspace:* + version: link:../../ts-lib + '@dmno/tsconfig': + specifier: workspace:* + version: link:../../tsconfig + '@types/async': + specifier: ^3.2.24 + version: 3.2.24 + '@types/debug': + specifier: 'catalog:' + version: 4.1.12 + '@types/lodash-es': + specifier: 'catalog:' + version: 4.17.12 + '@types/node': + specifier: 'catalog:' + version: 20.14.12 + dmno: + specifier: workspace:* + version: link:../../core + tsup: + specifier: 'catalog:' + version: 8.2.4(jiti@1.21.0)(postcss@8.5.3)(tsx@4.19.2)(typescript@5.7.2)(yaml@2.5.1) + typescript: + specifier: 'catalog:' + version: 5.7.2 + vite: + specifier: ^6.2.6 + version: 6.2.6(@types/node@20.14.12)(jiti@1.21.0)(less@4.2.0)(tsx@4.19.2)(yaml@2.5.1) + packages/integrations/remix: dependencies: debug: @@ -8204,6 +8250,16 @@ packages: peerDependencies: react: '>=16.8' + react-router@7.5.0: + resolution: {integrity: sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@18.3.0: resolution: {integrity: sha512-RPutkJftSAldDibyrjuku7q11d3oy6wKOyPe5K1HA/HwwrXcEqBdHsLypkC2FFYjP7bPUa6gbzSBhw4sY2JcDg==} engines: {node: '>=0.10.0'} @@ -9148,6 +9204,9 @@ packages: turbo-stream@2.2.0: resolution: {integrity: sha512-FKFg7A0To1VU4CH9YmSMON5QphK0BXjSoiC7D9yMh+mEEbXLUP9qJ4hEt1qcjKtzncs1OpcnjZO8NgrlVbZH+g==} + turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + turbo-windows-64@2.5.0: resolution: {integrity: sha512-9BCo8oQ7BO7J0K913Czbc3tw8QwLqn2nTe4E47k6aVYkM12ASTScweXPTuaPFP5iYXAT6z5Dsniw704Ixa5eGg==} cpu: [x64] @@ -9732,6 +9791,46 @@ packages: yaml: optional: true + vite@6.2.6: + resolution: {integrity: sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitefu@1.0.2: resolution: {integrity: sha512-0/iAvbXyM3RiPPJ4lyD4w6Mjgtf4ejTK6TPvTNG3H32PLwuT0N/ZjJLiXug7ETE/LWtTeHw9WRv7uX/tIKYyKg==} peerDependencies: @@ -18923,6 +19022,16 @@ snapshots: '@remix-run/router': 1.18.0 react: 18.3.0 + react-router@7.5.0(react-dom@18.3.0(react@18.3.0))(react@18.3.0): + dependencies: + '@types/cookie': 0.6.0 + cookie: 1.0.2 + react: 18.3.0 + set-cookie-parser: 2.6.0 + turbo-stream: 2.4.0 + optionalDependencies: + react-dom: 18.3.0(react@18.3.0) + react@18.3.0: dependencies: loose-envify: 1.4.0 @@ -20186,6 +20295,8 @@ snapshots: turbo-stream@2.2.0: {} + turbo-stream@2.4.0: {} + turbo-windows-64@2.5.0: optional: true @@ -20849,6 +20960,19 @@ snapshots: tsx: 4.19.2 yaml: 2.5.1 + vite@6.2.6(@types/node@20.14.12)(jiti@1.21.0)(less@4.2.0)(tsx@4.19.2)(yaml@2.5.1): + dependencies: + esbuild: 0.25.0 + postcss: 8.5.3 + rollup: 4.34.8 + optionalDependencies: + '@types/node': 20.14.12 + fsevents: 2.3.3 + jiti: 1.21.0 + less: 4.2.0 + tsx: 4.19.2 + yaml: 2.5.1 + vitefu@1.0.2(vite@5.4.11(@types/node@20.14.12)(less@4.2.0)): optionalDependencies: vite: 5.4.11(@types/node@20.14.12)(less@4.2.0)