diff --git a/docs/start/future-flags.md b/docs/start/future-flags.md index 5e18137c71d..4a6894239f8 100644 --- a/docs/start/future-flags.md +++ b/docs/start/future-flags.md @@ -472,20 +472,26 @@ You shouldn't need to make any changes to your application code for this feature You may find some usage for the new [``][discover-prop] API if you wish to disable eager route discovery on certain links. -## unstable_optimizeDeps - -Opt into automatic [dependency optimization][dependency-optimization] during development. This flag will remain in an "unstable" state until React Router v7 so you do not need to adopt this in your Remix v2 app prior to upgrading to React Router v7. +## v3_routeConfig -## routes.ts +Config-based routing is the new default in React Router v7, configured via the `routes.ts` file in the app directory. Support for `routes.ts` and its related APIs in Remix are designed as a migration path to help minimize the number of changes required when moving your Remix project over to React Router v7. While some new packages have been introduced within the `@remix-run` scope, these new packages only exist to keep the code in `routes.ts` as similar as possible to the equivalent code for React Router v7. -Config-based routing is the new default in React Router v7. Support for `routes.ts` and its related APIs in Remix are designed as a migration path to help minimize the number of changes required when moving your Remix project over to React Router v7. Since React Router v7 is not yet stable, these APIs are also considered unstable. - -While not a future flag, the presence of an `app/routes.ts` file when using the Remix Vite plugin will disable Remix's built-in file system routing and opt your project into React Router v7's config-based routing. To opt back in to file system routing, this can be explicitly configured within `routes.ts` as we'll cover below. +When the `v3_routeConfig` future flag is enabled, Remix's built-in file system routing will be disabled and your project will opted into React Router v7's config-based routing. To opt back in to file system routing, this can be explicitly configured within `routes.ts` as we'll cover below. **Update your code** To migrate Remix's file system routing and route config to the equivalent setup in React Router v7, you can follow these steps: +👉 **Enable the Flag** + +```ts filename=vite.config.ts +remix({ + future: { + v3_routeConfig: true, + }, +}); +``` + 👉 **Install `@remix-run/route-config`** This package matches the API of React Router v7's `@react-router/dev/routes`, making the React Router v7 migration as easy as possible. @@ -613,6 +619,10 @@ export const routes: RouteConfig = [ ]; ``` +## unstable_optimizeDeps + +Opt into automatic [dependency optimization][dependency-optimization] during development. This flag will remain in an "unstable" state until React Router v7 so you do not need to adopt this in your Remix v2 app prior to upgrading to React Router v7. + [development-strategy]: ../guides/api-development-strategy [fetcherpersist-rfc]: https://github.com/remix-run/remix/discussions/7698 [relativesplatpath-changelog]: https://github.com/remix-run/remix/blob/main/CHANGELOG.md#futurev3_relativesplatpath @@ -632,5 +642,5 @@ export const routes: RouteConfig = [ [vite-url-imports]: https://vitejs.dev/guide/assets.html#explicit-url-imports [mdx]: https://mdxjs.com [mdx-rollup-plugin]: https://mdxjs.com/packages/rollup -[dependency-optimization]: ../guides/dependency-optimization [remix-flat-routes]: https://github.com/kiliman/remix-flat-routes +[dependency-optimization]: ../guides/dependency-optimization diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts index 65c805288ff..cf6cc609dbf 100644 --- a/integration/helpers/vite.ts +++ b/integration/helpers/vite.ts @@ -32,13 +32,19 @@ export const viteConfig = { `; return text; }, - basic: async (args: { port: number; fsAllow?: string[] }) => { + basic: async (args: { + port: number; + fsAllow?: string[]; + routeConfig?: boolean; + }) => { return dedent` import { vitePlugin as remix } from "@remix-run/dev"; export default { ${await viteConfig.server(args)} - plugins: [remix()] + plugins: [remix(${ + args.routeConfig ? "{ future: { v3_routeConfig: true } }" : "" + })] } `; }, diff --git a/integration/vite-fs-routes-test.ts b/integration/vite-fs-routes-test.ts index 1bd9d06cfb3..09c6755681c 100644 --- a/integration/vite-fs-routes-test.ts +++ b/integration/vite-fs-routes-test.ts @@ -23,7 +23,9 @@ test.describe("fs-routes", () => { import { vitePlugin as remix } from "@remix-run/dev"; export default defineConfig({ - plugins: [remix()], + plugins: [remix({ + future: { v3_routeConfig: true }, + })], }); `, "app/routes.ts": js` @@ -254,7 +256,9 @@ test.describe("emits warnings for route conflicts", async () => { import { vitePlugin as remix } from "@remix-run/dev"; export default defineConfig({ - plugins: [remix()], + plugins: [remix({ + future: { v3_routeConfig: true }, + })], }); `, "app/routes.ts": js` @@ -326,7 +330,9 @@ test.describe("", () => { import { vitePlugin as remix } from "@remix-run/dev"; export default defineConfig({ - plugins: [remix()], + plugins: [remix({ + future: { v3_routeConfig: true }, + })], }); `, "app/routes.ts": js` @@ -373,7 +379,9 @@ test.describe("pathless routes and route collisions", () => { import { vitePlugin as remix } from "@remix-run/dev"; export default defineConfig({ - plugins: [remix()], + plugins: [remix({ + future: { v3_routeConfig: true }, + })], }); `, "app/routes.ts": js` diff --git a/integration/vite-route-config-test.ts b/integration/vite-route-config-test.ts index d1ec6c3d5bd..28c96e9f34b 100644 --- a/integration/vite-route-config-test.ts +++ b/integration/vite-route-config-test.ts @@ -36,6 +36,27 @@ async function reloadPage({ } test.describe("route config", () => { + test("fails the build if route config is missing", async () => { + let cwd = await createProject({ + "vite.config.js": ` + import { vitePlugin as remix } from "@remix-run/dev"; + + export default { + plugins: [remix({ + future: { v3_routeConfig: true }, + })] + } + `, + }); + // Ensure file is missing in case it's ever added to test fixture + await fs.rm(path.join(cwd, "app/routes.ts"), { force: true }); + let buildResult = viteBuild({ cwd }); + expect(buildResult.status).toBe(1); + expect(buildResult.stderr.toString()).toContain( + 'Route config file not found at "app/routes.ts"' + ); + }); + test("fails the build if routes option is used", async () => { let cwd = await createProject({ "vite.config.js": ` @@ -43,6 +64,7 @@ test.describe("route config", () => { export default { plugins: [remix({ + future: { v3_routeConfig: true }, routes: () => {}, })] } @@ -66,6 +88,7 @@ test.describe("route config", () => { export default { ${await viteConfig.server({ port })} plugins: [remix({ + future: { v3_routeConfig: true }, routes: () => {}, })] } @@ -85,6 +108,15 @@ test.describe("route config", () => { test("fails the build if route config is invalid", async () => { let cwd = await createProject({ + "vite.config.js": ` + import { vitePlugin as remix } from "@remix-run/dev"; + + export default { + plugins: [remix({ + future: { v3_routeConfig: true }, + })] + } + `, "app/routes.ts": `export default INVALID(`, }); let buildResult = viteBuild({ cwd }); @@ -98,7 +130,10 @@ test.describe("route config", () => { viteDev, }) => { let files: Files = async ({ port }) => ({ - "vite.config.js": await viteConfig.basic({ port }), + "vite.config.js": await viteConfig.basic({ + routeConfig: true, + port, + }), "app/routes.ts": `export default INVALID(`, }); let devError: Error | undefined; @@ -119,7 +154,10 @@ test.describe("route config", () => { viteDev, }) => { let files: Files = async ({ port }) => ({ - "vite.config.js": await viteConfig.basic({ port }), + "vite.config.js": await viteConfig.basic({ + routeConfig: true, + port, + }), "app/routes.ts": js` import { type RouteConfig, index } from "@remix-run/route-config"; @@ -177,7 +215,10 @@ test.describe("route config", () => { viteDev, }) => { let files: Files = async ({ port }) => ({ - "vite.config.js": await viteConfig.basic({ port }), + "vite.config.js": await viteConfig.basic({ + routeConfig: true, + port, + }), "app/routes.ts": js` export { routes } from "./actual-routes"; `, @@ -238,7 +279,10 @@ test.describe("route config", () => { viteDev, }) => { let files: Files = async ({ port }) => ({ - "vite.config.js": await viteConfig.basic({ port }), + "vite.config.js": await viteConfig.basic({ + routeConfig: true, + port, + }), "app/routes.ts": js` import { type RouteConfig, index } from "@remix-run/route-config"; @@ -256,11 +300,6 @@ test.describe("route config", () => { return
Test route 2
} `, - "app/routes/_index.tsx": ` - export default function FsRoute() { - return
FS route
- } - `, }); let { cwd, port } = await viteDev(files); @@ -277,18 +316,12 @@ test.describe("route config", () => { path.join(cwd, INVALID_FILENAME) ); - await expect(async () => { - // Reload to pick up classic FS routes - page = await reloadPage({ browserName, page, context }); - await expect(page.locator("[data-test-route]")).toHaveText("FS route"); - }).toPass(); - - // Ensure dev server falls back to FS routes + HMR - await edit("app/routes/_index.tsx", (contents) => - contents.replace("FS route", "FS route updated") + // Ensure dev server is still running with old config + HMR + await edit("app/test-route-1.tsx", (contents) => + contents.replace("Test route 1", "Test route 1 updated") ); await expect(page.locator("[data-test-route]")).toHaveText( - "FS route updated" + "Test route 1 updated" ); // Add new route @@ -313,7 +346,10 @@ test.describe("route config", () => { test("supports absolute route file paths", async ({ page, viteDev }) => { let files: Files = async ({ port }) => ({ - "vite.config.js": await viteConfig.basic({ port }), + "vite.config.js": await viteConfig.basic({ + routeConfig: true, + port, + }), "app/routes.ts": js` import path from "node:path"; import { type RouteConfig, index } from "@remix-run/route-config"; diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index f528c16f15e..f24e37ecf5e 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -40,6 +40,7 @@ describe("readConfig", () => { "v3_fetcherPersist": false, "v3_lazyRouteDiscovery": false, "v3_relativeSplatPath": false, + "v3_routeConfig": false, "v3_singleFetch": false, "v3_throwAbortReason": false, }, diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 6e8f8f9da5e..2c0d9a65d57 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -22,6 +22,7 @@ import { serverBuildVirtualModule } from "./compiler/server/virtualModules"; import { flatRoutes } from "./config/flat-routes"; import { detectPackageManager } from "./cli/detectPackageManager"; import { logger } from "./tux"; +import invariant from "./invariant"; export interface RemixMdxConfig { rehypePlugins?: any[]; @@ -49,6 +50,7 @@ interface FutureConfig { v3_throwAbortReason: boolean; v3_singleFetch: boolean; v3_lazyRouteDiscovery: boolean; + v3_routeConfig: boolean; unstable_optimizeDeps: boolean; } @@ -577,9 +579,12 @@ export async function resolveConfig( root: { path: "", id: "root", file: rootRouteFile }, }; - setRouteConfigAppDirectory(appDirectory); - let routeConfigFile = findEntry(appDirectory, "routes"); - if (routesViteNodeContext && vite && routeConfigFile) { + if (appConfig.future?.v3_routeConfig) { + invariant(routesViteNodeContext); + invariant(vite); + + let routeConfigFile = findEntry(appDirectory, "routes"); + class FriendlyError extends Error {} let logger = vite.createLogger(viteUserConfig?.logLevel, { @@ -593,6 +598,16 @@ export async function resolveConfig( ); } + if (!routeConfigFile) { + let routeConfigDisplayPath = vite.normalizePath( + path.relative(rootDirectory, path.join(appDirectory, "routes.ts")) + ); + throw new FriendlyError( + `Route config file not found at "${routeConfigDisplayPath}".` + ); + } + + setRouteConfigAppDirectory(appDirectory); let routeConfigExport: RouteConfig = ( await routesViteNodeContext.runner.executeFile( path.join(appDirectory, routeConfigFile) @@ -706,6 +721,7 @@ export async function resolveConfig( v3_throwAbortReason: appConfig.future?.v3_throwAbortReason === true, v3_singleFetch: appConfig.future?.v3_singleFetch === true, v3_lazyRouteDiscovery: appConfig.future?.v3_lazyRouteDiscovery === true, + v3_routeConfig: appConfig.future?.v3_routeConfig === true, unstable_optimizeDeps: appConfig.future?.unstable_optimizeDeps === true, };