diff --git a/integration/helpers/node-template/package.json b/integration/helpers/node-template/package.json
index 1d26acc8cfb..2f477337507 100644
--- a/integration/helpers/node-template/package.json
+++ b/integration/helpers/node-template/package.json
@@ -23,8 +23,9 @@
},
"devDependencies": {
"@remix-run/dev": "workspace:*",
- "@remix-run/route-config": "workspace:*",
"@remix-run/fs-routes": "workspace:*",
+ "@remix-run/route-config": "workspace:*",
+ "@remix-run/routes-option-adapter": "workspace:*",
"@vanilla-extract/css": "^1.10.0",
"@vanilla-extract/vite-plugin": "^3.9.2",
"@types/react": "^18.2.20",
diff --git a/integration/vite-fs-routes-test.ts b/integration/vite-fs-routes-test.ts
index e3709d99967..5e9f9d44545 100644
--- a/integration/vite-fs-routes-test.ts
+++ b/integration/vite-fs-routes-test.ts
@@ -29,11 +29,22 @@ test.describe("fs-routes", () => {
"app/routes.ts": js`
import { type RouteConfig } from "@remix-run/route-config";
import { flatRoutes } from "@remix-run/fs-routes";
-
- export const routes: RouteConfig = flatRoutes({
- rootDirectory: "fs-routes",
- ignoredRouteFiles: ["**/ignored-route.*"],
- });
+ import { routesOptionAdapter } from "@remix-run/routes-option-adapter";
+
+ export const routes: RouteConfig = [
+ ...await flatRoutes({
+ rootDirectory: "fs-routes",
+ ignoredRouteFiles: ["**/ignored-route.*"],
+ }),
+
+ // Ensure back compat layer works
+ ...await routesOptionAdapter(async (defineRoutes) => {
+ // Ensure async routes work
+ return defineRoutes((route) => {
+ route("/routes/option/adapter/route", "routes-option-adapter-route.tsx")
+ });
+ })
+ ];
`,
"app/root.tsx": js`
import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
@@ -81,6 +92,12 @@ test.describe("fs-routes", () => {
}
`,
+ "app/routes-option-adapter-route.tsx": js`
+ export default function () {
+ return
Routes Option Adapter Route
;
+ }
+ `,
+
"app/fs-routes/.dotfile": `
DOTFILE SHOULD BE IGNORED
`,
@@ -176,6 +193,17 @@ test.describe("fs-routes", () => {
`);
});
+ test("renders matching routes (routes option adapter)", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/routes/option/adapter/route");
+ expect(await app.getHtml("#content")).toBe(`
+
Root
+ Routes Option Adapter Route
+`);
+ });
+
test("renders matching routes (route with escaped leading dot)", async ({
page,
}) => {
diff --git a/packages/remix-fs-routes/__tests__/routeManifestToRouteConfig-test.ts b/packages/remix-dev/__tests__/routeManifestToRouteConfig-test.ts
similarity index 80%
rename from packages/remix-fs-routes/__tests__/routeManifestToRouteConfig-test.ts
rename to packages/remix-dev/__tests__/routeManifestToRouteConfig-test.ts
index d4e9be71597..a236c022ada 100644
--- a/packages/remix-fs-routes/__tests__/routeManifestToRouteConfig-test.ts
+++ b/packages/remix-dev/__tests__/routeManifestToRouteConfig-test.ts
@@ -1,6 +1,4 @@
-import { route } from "@remix-run/route-config";
-
-import { routeManifestToRouteConfig } from "../manifest";
+import { type RouteConfig, routeManifestToRouteConfig } from "../config/routes";
const clean = (obj: any) => cleanUndefined(cleanIds(obj));
@@ -43,14 +41,27 @@ describe("routeManifestToRouteConfig", () => {
caseSensitive: true,
},
});
- let routeConfig = [
- route("/", "routes/home.js"),
- route("inbox", "routes/inbox.js", [
- route("/", "routes/inbox/index.js", { index: true }),
- route(":messageId", "routes/inbox/$messageId.js", {
- caseSensitive: true,
- }),
- ]),
+ let routeConfig: RouteConfig = [
+ {
+ path: "/",
+ file: "routes/home.js",
+ },
+ {
+ path: "inbox",
+ file: "routes/inbox.js",
+ children: [
+ {
+ path: "/",
+ file: "routes/inbox/index.js",
+ index: true,
+ },
+ {
+ path: ":messageId",
+ file: "routes/inbox/$messageId.js",
+ caseSensitive: true,
+ },
+ ],
+ },
];
expect(clean(routeManifestConfig)).toEqual(clean(routeConfig));
diff --git a/packages/remix-dev/config/routes.ts b/packages/remix-dev/config/routes.ts
index fd4dd22cbbf..3e2b919517c 100644
--- a/packages/remix-dev/config/routes.ts
+++ b/packages/remix-dev/config/routes.ts
@@ -57,6 +57,45 @@ export interface RouteManifest {
[routeId: string]: RouteManifestEntry;
}
+export function routeManifestToRouteConfig(
+ routeManifest: RouteManifest,
+ rootId = "root"
+): RouteConfigEntry[] {
+ let routeConfigById: {
+ [id: string]: Omit &
+ Required>;
+ } = {};
+
+ for (let id in routeManifest) {
+ let route = routeManifest[id];
+ routeConfigById[id] = {
+ id: route.id,
+ file: route.file,
+ path: route.path,
+ index: route.index,
+ caseSensitive: route.caseSensitive,
+ };
+ }
+
+ let routeConfig: RouteConfigEntry[] = [];
+
+ for (let id in routeConfigById) {
+ let route = routeConfigById[id];
+ let parentId = routeManifest[route.id].parentId;
+ if (parentId === rootId) {
+ routeConfig.push(route);
+ } else {
+ let parentRoute = parentId && routeConfigById[parentId];
+ if (parentRoute) {
+ parentRoute.children = parentRoute.children || [];
+ parentRoute.children.push(route);
+ }
+ }
+ }
+
+ return routeConfig;
+}
+
/**
* Configuration for an individual route, for use within `routes.ts`. As a
* convenience, route config entries can be created with the {@link route},
diff --git a/packages/remix-dev/index.ts b/packages/remix-dev/index.ts
index 38581e081f7..baabf348966 100644
--- a/packages/remix-dev/index.ts
+++ b/packages/remix-dev/index.ts
@@ -6,10 +6,17 @@ export * as cli from "./cli/index";
export type { Manifest as AssetsManifest } from "./manifest";
export type {
+ DefineRoutesFunction as UNSAFE_DefineRoutesFunction,
+ RouteManifest as UNSAFE_RouteManifest,
+ RouteManifestEntry as UNSAFE_RouteManifestEntry,
RouteConfig as UNSAFE_RouteConfig,
RouteConfigEntry as UNSAFE_RouteConfigEntry,
} from "./config/routes";
-export { getRouteConfigAppDirectory as UNSAFE_getRouteConfigAppDirectory } from "./config/routes";
+export {
+ defineRoutes as UNSAFE_defineRoutes,
+ routeManifestToRouteConfig as UNSAFE_routeManifestToRouteConfig,
+ getRouteConfigAppDirectory as UNSAFE_getRouteConfigAppDirectory,
+} from "./config/routes";
export { getDependenciesToBundle } from "./dependencies";
export type {
BuildManifest,
diff --git a/packages/remix-fs-routes/flatRoutes.ts b/packages/remix-fs-routes/flatRoutes.ts
index 4195f735a4c..9d48763b9dc 100644
--- a/packages/remix-fs-routes/flatRoutes.ts
+++ b/packages/remix-fs-routes/flatRoutes.ts
@@ -1,8 +1,11 @@
import fs from "node:fs";
import path from "node:path";
import { makeRe } from "minimatch";
+import type {
+ UNSAFE_RouteManifest as RouteManifest,
+ UNSAFE_RouteManifestEntry as RouteManifestEntry,
+} from "@remix-run/dev";
-import type { RouteManifest, RouteManifestEntry } from "./manifest";
import { normalizeSlashes } from "./normalizeSlashes";
export const routeModuleExts = [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"];
diff --git a/packages/remix-fs-routes/index.ts b/packages/remix-fs-routes/index.ts
index a0ccc39fefd..faf5ea44c63 100644
--- a/packages/remix-fs-routes/index.ts
+++ b/packages/remix-fs-routes/index.ts
@@ -1,11 +1,11 @@
import fs from "node:fs";
import path from "node:path";
+import { UNSAFE_routeManifestToRouteConfig as routeManifestToRouteConfig } from "@remix-run/dev";
import {
type RouteConfigEntry,
getAppDirectory,
} from "@remix-run/route-config";
-import { routeManifestToRouteConfig } from "./manifest";
import { flatRoutes as flatRoutesImpl } from "./flatRoutes";
import { normalizeSlashes } from "./normalizeSlashes";
diff --git a/packages/remix-fs-routes/manifest.ts b/packages/remix-fs-routes/manifest.ts
deleted file mode 100644
index 3b9ea7ae1d8..00000000000
--- a/packages/remix-fs-routes/manifest.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import type { RouteConfigEntry } from "@remix-run/route-config";
-
-export interface RouteManifestEntry {
- path?: string;
- index?: boolean;
- caseSensitive?: boolean;
- id: string;
- parentId?: string;
- file: string;
-}
-
-export interface RouteManifest {
- [routeId: string]: RouteManifestEntry;
-}
-
-export function routeManifestToRouteConfig(
- routeManifest: RouteManifest,
- rootId = "root"
-): RouteConfigEntry[] {
- let routeConfigById: {
- [id: string]: Omit &
- Required>;
- } = {};
-
- for (let id in routeManifest) {
- let route = routeManifest[id];
- routeConfigById[id] = {
- id: route.id,
- file: route.file,
- path: route.path,
- index: route.index,
- caseSensitive: route.caseSensitive,
- };
- }
-
- let routeConfig: RouteConfigEntry[] = [];
-
- for (let id in routeConfigById) {
- let route = routeConfigById[id];
- let parentId = routeManifest[route.id].parentId;
- if (parentId === rootId) {
- routeConfig.push(route);
- } else {
- let parentRoute = parentId && routeConfigById[parentId];
- if (parentRoute) {
- parentRoute.children = parentRoute.children || [];
- parentRoute.children.push(route);
- }
- }
- }
-
- return routeConfig;
-}
diff --git a/packages/remix-fs-routes/package.json b/packages/remix-fs-routes/package.json
index 5f803ade36b..75b82093267 100644
--- a/packages/remix-fs-routes/package.json
+++ b/packages/remix-fs-routes/package.json
@@ -1,7 +1,7 @@
{
"name": "@remix-run/fs-routes",
"version": "2.13.1",
- "description": "Config-based file system routing conventions for Remix",
+ "description": "Config-based file system routing conventions, for use within routes.ts",
"bugs": {
"url": "https://github.com/remix-run/remix/issues"
},
@@ -27,10 +27,12 @@
"minimatch": "^9.0.0"
},
"devDependencies": {
+ "@remix-run/dev": "workspace:*",
"@remix-run/route-config": "workspace:*",
"typescript": "^5.1.6"
},
"peerDependencies": {
+ "@remix-run/dev": "workspace:*",
"@remix-run/route-config": "workspace:*",
"typescript": "^5.1.0"
},
diff --git a/packages/remix-route-config/package.json b/packages/remix-route-config/package.json
index 574c8c042c0..701968b67e9 100644
--- a/packages/remix-route-config/package.json
+++ b/packages/remix-route-config/package.json
@@ -1,14 +1,14 @@
{
"name": "@remix-run/route-config",
"version": "2.13.1",
- "description": "Config-based routing for Remix",
+ "description": "Config-based routing utilities, for use within routes.ts",
"bugs": {
"url": "https://github.com/remix-run/remix/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/remix-run/remix",
- "directory": "packages/remix-fs-routes"
+ "directory": "packages/remix-route-config"
},
"license": "MIT",
"main": "dist/index.js",
diff --git a/packages/remix-routes-option-adapter/README.md b/packages/remix-routes-option-adapter/README.md
new file mode 100644
index 00000000000..40685a7476f
--- /dev/null
+++ b/packages/remix-routes-option-adapter/README.md
@@ -0,0 +1,13 @@
+# Welcome to Remix!
+
+[Remix](https://remix.run) is a web framework that helps you build better websites with React.
+
+To get started, open a new shell and run:
+
+```sh
+npx create-remix@latest
+```
+
+Then follow the prompts you see in your terminal.
+
+For more information about Remix, [visit remix.run](https://remix.run)!
diff --git a/packages/remix-routes-option-adapter/index.ts b/packages/remix-routes-option-adapter/index.ts
new file mode 100644
index 00000000000..bc16aa18fa8
--- /dev/null
+++ b/packages/remix-routes-option-adapter/index.ts
@@ -0,0 +1,24 @@
+import {
+ type UNSAFE_DefineRoutesFunction as DefineRoutesFunction,
+ UNSAFE_defineRoutes as defineRoutes,
+ UNSAFE_routeManifestToRouteConfig as routeManifestToRouteConfig,
+} from "@remix-run/dev";
+import { type RouteConfigEntry } from "@remix-run/route-config";
+
+export type { DefineRoutesFunction };
+
+/**
+ * Adapter for [Remix's `routes` config
+ * option](https://remix.run/docs/en/v2/file-conventions/vite-config#routes),
+ * for use within `routes.ts`.
+ */
+export async function routesOptionAdapter(
+ routes: (
+ defineRoutes: DefineRoutesFunction
+ ) =>
+ | ReturnType
+ | Promise>
+): Promise {
+ let routeManifest = await routes(defineRoutes);
+ return routeManifestToRouteConfig(routeManifest);
+}
diff --git a/packages/remix-routes-option-adapter/package.json b/packages/remix-routes-option-adapter/package.json
new file mode 100644
index 00000000000..529b43c5531
--- /dev/null
+++ b/packages/remix-routes-option-adapter/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@remix-run/routes-option-adapter",
+ "version": "2.13.1",
+ "description": "Adapter for Remix's \"routes\" config option, for use within routes.ts",
+ "bugs": {
+ "url": "https://github.com/remix-run/remix/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/remix-run/remix",
+ "directory": "packages/remix-routes-option-adapter"
+ },
+ "license": "MIT",
+ "main": "dist/index.js",
+ "typings": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "tsc": "tsc"
+ },
+ "dependencies": {
+ "minimatch": "^9.0.0"
+ },
+ "devDependencies": {
+ "@remix-run/dev": "workspace:*",
+ "@remix-run/route-config": "workspace:*",
+ "typescript": "^5.1.6"
+ },
+ "peerDependencies": {
+ "@remix-run/dev": "workspace:*",
+ "@remix-run/route-config": "workspace:*",
+ "typescript": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "files": [
+ "dist/",
+ "CHANGELOG.md",
+ "LICENSE.md",
+ "README.md"
+ ]
+}
diff --git a/packages/remix-routes-option-adapter/rollup.config.js b/packages/remix-routes-option-adapter/rollup.config.js
new file mode 100644
index 00000000000..59e7189afe3
--- /dev/null
+++ b/packages/remix-routes-option-adapter/rollup.config.js
@@ -0,0 +1,45 @@
+const path = require("node:path");
+const babel = require("@rollup/plugin-babel").default;
+const nodeResolve = require("@rollup/plugin-node-resolve").default;
+const copy = require("rollup-plugin-copy");
+
+const {
+ copyToPlaygrounds,
+ createBanner,
+ getOutputDir,
+ isBareModuleId,
+} = require("../../rollup.utils");
+const { name: packageName, version } = require("./package.json");
+
+/** @returns {import("rollup").RollupOptions[]} */
+module.exports = function rollup() {
+ let sourceDir = "packages/remix-routes-option-adapter";
+ let outputDir = getOutputDir(packageName);
+ let outputDist = path.join(outputDir, "dist");
+
+ return [
+ {
+ external: (id) => isBareModuleId(id),
+ input: `${sourceDir}/index.ts`,
+ output: {
+ banner: createBanner(packageName, version),
+ dir: outputDist,
+ format: "cjs",
+ preserveModules: true,
+ exports: "auto",
+ },
+ plugins: [
+ babel({
+ babelHelpers: "bundled",
+ exclude: /node_modules/,
+ extensions: [".ts"],
+ }),
+ nodeResolve({ extensions: [".ts"] }),
+ copy({
+ targets: [{ src: "LICENSE.md", dest: sourceDir }],
+ }),
+ copyToPlaygrounds(),
+ ],
+ },
+ ];
+};
diff --git a/packages/remix-routes-option-adapter/tsconfig.json b/packages/remix-routes-option-adapter/tsconfig.json
new file mode 100644
index 00000000000..897cf7dd585
--- /dev/null
+++ b/packages/remix-routes-option-adapter/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "include": ["**/*.ts"],
+ "exclude": ["dist", "__tests__", "node_modules"],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "target": "ES2022",
+ "module": "ES2022",
+ "skipLibCheck": true,
+
+ "moduleResolution": "Bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "jsx": "react",
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "rootDir": ".",
+ "outDir": "../../build/node_modules/@remix-run/routes-option-adapter/dist"
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 502368f25bd..3ee9624be80 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -526,6 +526,9 @@ importers:
'@remix-run/route-config':
specifier: workspace:*
version: link:../../../packages/remix-route-config
+ '@remix-run/routes-option-adapter':
+ specifier: workspace:*
+ version: link:../../../packages/remix-routes-option-adapter
'@types/react':
specifier: ^18.2.20
version: 18.2.20
@@ -1201,6 +1204,9 @@ importers:
specifier: ^9.0.0
version: 9.0.3
devDependencies:
+ '@remix-run/dev':
+ specifier: workspace:*
+ version: link:../remix-dev
'@remix-run/route-config':
specifier: workspace:*
version: link:../remix-route-config
@@ -1307,6 +1313,22 @@ importers:
specifier: 5.1.8
version: 5.1.8(@types/node@18.17.1)
+ packages/remix-routes-option-adapter:
+ dependencies:
+ minimatch:
+ specifier: ^9.0.0
+ version: 9.0.3
+ devDependencies:
+ '@remix-run/dev':
+ specifier: workspace:*
+ version: link:../remix-dev
+ '@remix-run/route-config':
+ specifier: workspace:*
+ version: link:../remix-route-config
+ typescript:
+ specifier: ^5.1.6
+ version: 5.1.6
+
packages/remix-serve:
dependencies:
'@remix-run/express':
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 8671741fd4c..e8c91c3e82c 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -20,6 +20,7 @@ packages:
- "packages/remix-node"
- "packages/remix-react"
- "packages/remix-route-config"
+ - "packages/remix-routes-option-adapter"
- "packages/remix-serve"
- "packages/remix-server-runtime"
- "packages/remix-testing"
diff --git a/scripts/publish.js b/scripts/publish.js
index 5300a854af8..37fa8f7f37b 100644
--- a/scripts/publish.js
+++ b/scripts/publish.js
@@ -64,6 +64,7 @@ async function run() {
"css-bundle",
"testing",
"route-config",
+ "routes-option-adapter",
]) {
publish(path.join(buildDir, "@remix-run", name), tag);
}