Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

routes.ts: Add v3_routeConfig future flag #10171

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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