Skip to content

Commit

Permalink
Add v3_routeConfig future flag
Browse files Browse the repository at this point in the history
  • Loading branch information
markdalgleish authored Oct 29, 2024
1 parent 122b7b0 commit 72511f7
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 37 deletions.
26 changes: 18 additions & 8 deletions docs/start/future-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [`<Link discover>`][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.
Expand Down Expand Up @@ -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
Expand All @@ -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
10 changes: 8 additions & 2 deletions integration/helpers/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }" : ""
})]
}
`;
},
Expand Down
16 changes: 12 additions & 4 deletions integration/vite-fs-routes-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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`
Expand Down
76 changes: 56 additions & 20 deletions integration/vite-route-config-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,35 @@ 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": `
import { vitePlugin as remix } from "@remix-run/dev";
export default {
plugins: [remix({
future: { v3_routeConfig: true },
routes: () => {},
})]
}
Expand All @@ -66,6 +88,7 @@ test.describe("route config", () => {
export default {
${await viteConfig.server({ port })}
plugins: [remix({
future: { v3_routeConfig: true },
routes: () => {},
})]
}
Expand All @@ -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 });
Expand All @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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";
`,
Expand Down Expand Up @@ -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";
Expand All @@ -256,11 +300,6 @@ test.describe("route config", () => {
return <div data-test-route>Test route 2</div>
}
`,
"app/routes/_index.tsx": `
export default function FsRoute() {
return <div data-test-route>FS route</div>
}
`,
});
let { cwd, port } = await viteDev(files);

Expand All @@ -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
Expand All @@ -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";
Expand Down
1 change: 1 addition & 0 deletions packages/remix-dev/__tests__/readConfig-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe("readConfig", () => {
"v3_fetcherPersist": false,
"v3_lazyRouteDiscovery": false,
"v3_relativeSplatPath": false,
"v3_routeConfig": false,
"v3_singleFetch": false,
"v3_throwAbortReason": false,
},
Expand Down
22 changes: 19 additions & 3 deletions packages/remix-dev/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -49,6 +50,7 @@ interface FutureConfig {
v3_throwAbortReason: boolean;
v3_singleFetch: boolean;
v3_lazyRouteDiscovery: boolean;
v3_routeConfig: boolean;
unstable_optimizeDeps: boolean;
}

Expand Down Expand Up @@ -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, {
Expand All @@ -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)
Expand Down Expand Up @@ -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,
};

Expand Down

0 comments on commit 72511f7

Please sign in to comment.