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); }