Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'markdalgleish/routes-ts' into markdalgleish/routes-ts-fs
Browse files Browse the repository at this point in the history
markdalgleish committed Oct 16, 2024
2 parents 68455c9 + c743f72 commit 9e146e8
Showing 20 changed files with 532 additions and 329 deletions.
1 change: 1 addition & 0 deletions integration/helpers/vite-template/package.json
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@
"devDependencies": {
"@remix-run/dev": "workspace:*",
"@remix-run/eslint-config": "workspace:*",
"@remix-run/route-config": "workspace:*",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"eslint": "^8.38.0",
32 changes: 10 additions & 22 deletions integration/vite-route-config-test.ts
Original file line number Diff line number Diff line change
@@ -47,7 +47,7 @@ test.describe("route config", () => {
})]
}
`,
"app/routes.ts": `export default INVALID(`,
"app/routes.ts": `export const routes = [];`,
});
let buildResult = viteBuild({ cwd });
expect(buildResult.status).toBe(1);
@@ -70,7 +70,7 @@ test.describe("route config", () => {
})]
}
`,
"app/routes.ts": `export default INVALID(`,
"app/routes.ts": `export const routes = [];`,
});
let devError: Error | undefined;
try {
@@ -121,13 +121,10 @@ test.describe("route config", () => {
let files: Files = async ({ port }) => ({
"vite.config.js": await viteConfig.basic({ port }),
"app/routes.ts": js`
import { type RouteConfig } from "@remix-run/dev/routes";
import { type RouteConfig, index } from "@remix-run/route-config";
export const routes: RouteConfig = [
{
file: "test-route-1.tsx",
index: true,
},
index("test-route-1.tsx"),
];
`,
"app/test-route-1.tsx": `
@@ -185,13 +182,10 @@ test.describe("route config", () => {
export { routes } from "./actual-routes";
`,
"app/actual-routes.ts": js`
import { type RouteConfig } from "@remix-run/dev/routes";
import { type RouteConfig, index } from "@remix-run/route-config";
export const routes: RouteConfig = [
{
file: "test-route-1.tsx",
index: true,
},
index("test-route-1.tsx"),
];
`,
"app/test-route-1.tsx": `
@@ -246,13 +240,10 @@ test.describe("route config", () => {
let files: Files = async ({ port }) => ({
"vite.config.js": await viteConfig.basic({ port }),
"app/routes.ts": js`
import { type RouteConfig } from "@remix-run/dev/routes";
import { type RouteConfig, index } from "@remix-run/route-config";
export const routes: RouteConfig = [
{
file: "test-route-1.tsx",
index: true,
},
index("test-route-1.tsx"),
];
`,
"app/test-route-1.tsx": `
@@ -325,13 +316,10 @@ test.describe("route config", () => {
"vite.config.js": await viteConfig.basic({ port }),
"app/routes.ts": js`
import path from "node:path";
import { type RouteConfig } from "@remix-run/dev/routes";
import { type RouteConfig, index } from "@remix-run/route-config";
export const routes: RouteConfig = [
{
file: path.resolve(import.meta.dirname, "test-route.tsx"),
index: true,
},
index(path.resolve(import.meta.dirname, "test-route.tsx")),
];
`,
"app/test-route.tsx": `
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ module.exports = {
"packages/remix-fs-routes",
"packages/remix-node",
"packages/remix-react",
"packages/remix-route-config",
"packages/remix-serve",
"packages/remix-server-runtime",
"packages/remix-testing",
141 changes: 141 additions & 0 deletions packages/remix-dev/__tests__/validateRouteConfig-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { validateRouteConfig } from "../config/routes";

describe("validateRouteConfig", () => {
it("validates a route config", () => {
expect(
validateRouteConfig({
routeConfigFile: "routes.ts",
routeConfig: [
{
path: "parent",
file: "parent.tsx",
children: [
{
path: "child",
file: "child.tsx",
},
],
},
],
}).valid
).toBe(true);
});

it("is invalid when not an array", () => {
let result = validateRouteConfig({
routeConfigFile: "routes.ts",
routeConfig: { path: "path", file: "file.tsx" },
});

expect(result.valid).toBe(false);
expect(!result.valid && result.message).toMatchInlineSnapshot(
`"Route config in "routes.ts" must be an array."`
);
});

it("is invalid when route is a promise", () => {
let result = validateRouteConfig({
routeConfigFile: "routes.ts",
routeConfig: [
{
path: "parent",
file: "parent.tsx",
children: [Promise.resolve({})],
},
],
});

expect(result.valid).toBe(false);
expect(!result.valid && result.message).toMatchInlineSnapshot(`
"Route config in "routes.ts" is invalid.
Path: routes.0.children.0
Invalid type: Expected object but received a promise. Did you forget to await?"
`);
});

it("is invalid when file is missing", () => {
let result = validateRouteConfig({
routeConfigFile: "routes.ts",
routeConfig: [
{
path: "parent",
file: "parent.tsx",
children: [
{
id: "child",
},
],
},
],
});

expect(result.valid).toBe(false);
expect(!result.valid && result.message).toMatchInlineSnapshot(`
"Route config in "routes.ts" is invalid.
Path: routes.0.children.0.file
Invalid type: Expected string but received undefined"
`);
});

it("is invalid when property is wrong type", () => {
let result = validateRouteConfig({
routeConfigFile: "routes.ts",
routeConfig: [
{
path: "parent",
file: "parent.tsx",
children: [
{
file: 123,
},
],
},
],
});

expect(result.valid).toBe(false);
expect(!result.valid && result.message).toMatchInlineSnapshot(`
"Route config in "routes.ts" is invalid.
Path: routes.0.children.0.file
Invalid type: Expected string but received 123"
`);
});

it("shows multiple error messages", () => {
let result = validateRouteConfig({
routeConfigFile: "routes.ts",
routeConfig: [
{
path: "parent",
file: "parent.tsx",
children: [
{
id: "child",
},
{
file: 123,
},
Promise.resolve(),
],
},
],
});

expect(result.valid).toBe(false);
expect(!result.valid && result.message).toMatchInlineSnapshot(`
"Route config in "routes.ts" is invalid.
Path: routes.0.children.0.file
Invalid type: Expected string but received undefined
Path: routes.0.children.1.file
Invalid type: Expected string but received 123
Path: routes.0.children.2
Invalid type: Expected object but received a promise. Did you forget to await?"
`);
});
});
4 changes: 2 additions & 2 deletions packages/remix-dev/config.ts
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import {
type RouteManifest,
type RouteConfig,
type DefineRoutesFunction,
setAppDirectory,
setRouteConfigAppDirectory,
validateRouteConfig,
configRoutesToRouteManifest,
defineRoutes,
@@ -577,7 +577,7 @@ export async function resolveConfig(
root: { path: "", id: "root", file: rootRouteFile },
};

setAppDirectory(appDirectory);
setRouteConfigAppDirectory(appDirectory);
let routeConfigFile = findEntry(appDirectory, "routes");
if (routesViteNodeContext && vite && routeConfigFile) {
class FriendlyError extends Error {}
192 changes: 7 additions & 185 deletions packages/remix-dev/config/routes.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { resolve, win32 } from "node:path";
import { win32 } from "node:path";
import * as v from "valibot";
import pick from "lodash/pick";

import invariant from "../invariant";

let appDirectory: string;
let routeConfigAppDirectory: string;

export function setAppDirectory(directory: string) {
appDirectory = directory;
export function setRouteConfigAppDirectory(directory: string) {
routeConfigAppDirectory = directory;
}

/**
* Provides the absolute path to the app directory, for use within `routes.ts`.
* This is designed to support resolving file system routes.
*/
export function getAppDirectory() {
invariant(appDirectory);
return appDirectory;
export function getRouteConfigAppDirectory() {
invariant(routeConfigAppDirectory);
return routeConfigAppDirectory;
}

export interface RouteManifestEntry {
@@ -171,176 +170,6 @@ export function validateRouteConfig({
return { valid: true };
}

const createConfigRouteOptionKeys = [
"id",
"index",
"caseSensitive",
] as const satisfies ReadonlyArray<keyof RouteConfigEntry>;
type CreateRouteOptions = Pick<
RouteConfigEntry,
typeof createConfigRouteOptionKeys[number]
>;
/**
* Helper function for creating a route config entry, for use within
* `routes.ts`.
*/
function route(
path: string | null | undefined,
file: string,
children?: RouteConfigEntry[]
): RouteConfigEntry;
function route(
path: string | null | undefined,
file: string,
options: CreateRouteOptions,
children?: RouteConfigEntry[]
): RouteConfigEntry;
function route(
path: string | null | undefined,
file: string,
optionsOrChildren: CreateRouteOptions | RouteConfigEntry[] | undefined,
children?: RouteConfigEntry[]
): RouteConfigEntry {
let options: CreateRouteOptions = {};

if (Array.isArray(optionsOrChildren) || !optionsOrChildren) {
children = optionsOrChildren;
} else {
options = optionsOrChildren;
}

return {
file,
children,
path: path ?? undefined,
...pick(options, createConfigRouteOptionKeys),
};
}

const createIndexOptionKeys = ["id"] as const satisfies ReadonlyArray<
keyof RouteConfigEntry
>;
type CreateIndexOptions = Pick<
RouteConfigEntry,
typeof createIndexOptionKeys[number]
>;
/**
* Helper function for creating a route config entry for an index route, for use
* within `routes.ts`.
*/
function index(file: string, options?: CreateIndexOptions): RouteConfigEntry {
return {
file,
index: true,
...pick(options, createIndexOptionKeys),
};
}

const createLayoutOptionKeys = ["id"] as const satisfies ReadonlyArray<
keyof RouteConfigEntry
>;
type CreateLayoutOptions = Pick<
RouteConfigEntry,
typeof createLayoutOptionKeys[number]
>;
/**
* Helper function for creating a route config entry for a layout route, for use
* within `routes.ts`.
*/
function layout(file: string, children?: RouteConfigEntry[]): RouteConfigEntry;
function layout(
file: string,
options: CreateLayoutOptions,
children?: RouteConfigEntry[]
): RouteConfigEntry;
function layout(
file: string,
optionsOrChildren: CreateLayoutOptions | RouteConfigEntry[] | undefined,
children?: RouteConfigEntry[]
): RouteConfigEntry {
let options: CreateLayoutOptions = {};

if (Array.isArray(optionsOrChildren) || !optionsOrChildren) {
children = optionsOrChildren;
} else {
options = optionsOrChildren;
}

return {
file,
children,
...pick(options, createLayoutOptionKeys),
};
}

/**
* Helper function for adding a path prefix to a set of routes without needing
* to introduce a parent route file, for use within `routes.ts`.
*/
function prefix(
prefixPath: string,
routes: RouteConfigEntry[]
): RouteConfigEntry[] {
return routes.map((route) => {
if (route.index || typeof route.path === "string") {
return {
...route,
path: route.path ? joinRoutePaths(prefixPath, route.path) : prefixPath,
children: route.children,
};
} else if (route.children) {
return {
...route,
children: prefix(prefixPath, route.children),
};
}
return route;
});
}

const helpers = { route, index, layout, prefix };
export { route, index, layout, prefix };
/**
* Creates a set of route config helpers that resolve file paths relative to the
* given directory, for use within `routes.ts`. This is designed to support
* splitting route config into multiple files within different directories.
*/
export function relative(directory: string): typeof helpers {
return {
/**
* Helper function for creating a route config entry, for use within
* `routes.ts`. Note that this helper has been scoped, meaning that file
* path will be resolved relative to the directory provided to the
* `relative` call that created this helper.
*/
route: (path, file, ...rest) => {
return route(path, resolve(directory, file), ...(rest as any));
},
/**
* Helper function for creating a route config entry for an index route, for
* use within `routes.ts`. Note that this helper has been scoped, meaning
* that file path will be resolved relative to the directory provided to the
* `relative` call that created this helper.
*/
index: (file, ...rest) => {
return index(resolve(directory, file), ...(rest as any));
},
/**
* Helper function for creating a route config entry for a layout route, for
* use within `routes.ts`. Note that this helper has been scoped, meaning
* that file path will be resolved relative to the directory provided to the
* `relative` call that created this helper.
*/
layout: (file, ...rest) => {
return layout(resolve(directory, file), ...(rest as any));
},

// Passthrough of helper functions that don't need relative scoping so that
// a complete API is still provided.
prefix,
};
}

export function configRoutesToRouteManifest(
routes: RouteConfigEntry[],
rootId = "root"
@@ -379,13 +208,6 @@ export function configRoutesToRouteManifest(
return routeManifest;
}

function joinRoutePaths(path1: string, path2: string): string {
return [
path1.replace(/\/+$/, ""), // Remove trailing slashes
path2.replace(/^\/+/, ""), // Remove leading slashes
].join("/");
}

export interface DefineRouteOptions {
/**
* Should be `true` if the route `path` is case-sensitive. Defaults to
5 changes: 5 additions & 0 deletions packages/remix-dev/index.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,11 @@ export type { AppConfig, RemixConfig as ResolvedRemixConfig } from "./config";
export * as cli from "./cli/index";

export type { Manifest as AssetsManifest } from "./manifest";
export type {
RouteConfig as UNSAFE_RouteConfig,
RouteConfigEntry as UNSAFE_RouteConfigEntry,
} from "./config/routes";
export { getRouteConfigAppDirectory as UNSAFE_getRouteConfigAppDirectory } from "./config/routes";
export { getDependenciesToBundle } from "./dependencies";
export type {
BuildManifest,
1 change: 0 additions & 1 deletion packages/remix-dev/rollup.config.js
Original file line number Diff line number Diff line change
@@ -25,7 +25,6 @@ module.exports = function rollup() {
},
input: [
`${sourceDir}/index.ts`,
`${sourceDir}/routes.ts`,
// Since we're using a dynamic require for the Vite plugin, we
// need to tell Rollup it's an entry point
`${sourceDir}/vite/plugin.ts`,
10 changes: 0 additions & 10 deletions packages/remix-dev/routes.ts

This file was deleted.

13 changes: 13 additions & 0 deletions packages/remix-route-config/README.md
Original file line number Diff line number Diff line change
@@ -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)!
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import path from "node:path";
import { normalizePath } from "vite";

import {
validateRouteConfig,
route,
layout,
index,
prefix,
relative,
} from "../config/routes";
import { route, layout, index, prefix, relative } from "../routes";

function cleanPathsForSnapshot(obj: any): any {
return JSON.parse(
@@ -22,107 +15,6 @@ function cleanPathsForSnapshot(obj: any): any {
}

describe("route config", () => {
describe("validateRouteConfig", () => {
it("validates a route config", () => {
expect(
validateRouteConfig({
routeConfigFile: "routes.ts",
routeConfig: prefix("prefix", [
route("parent", "parent.tsx", [route("child", "child.tsx")]),
]),
}).valid
).toBe(true);
});

it("is invalid when not an array", () => {
let result = validateRouteConfig({
routeConfigFile: "routes.ts",
routeConfig: route("path", "file.tsx"),
});

expect(result.valid).toBe(false);
expect(!result.valid && result.message).toMatchInlineSnapshot(
`"Route config in "routes.ts" must be an array."`
);
});

it("is invalid when route is a promise", () => {
let result = validateRouteConfig({
routeConfigFile: "routes.ts",
/* @ts-expect-error */
routeConfig: [route("parent", "parent.tsx", [Promise.resolve({})])],
});

expect(result.valid).toBe(false);
expect(!result.valid && result.message).toMatchInlineSnapshot(`
"Route config in "routes.ts" is invalid.
Path: routes.0.children.0
Invalid type: Expected object but received a promise. Did you forget to await?"
`);
});

it("is invalid when file is missing", () => {
let result = validateRouteConfig({
routeConfigFile: "routes.ts",
/* @ts-expect-error */
routeConfig: [route("parent", "parent.tsx", [{ id: "child" }])],
});

expect(result.valid).toBe(false);
expect(!result.valid && result.message).toMatchInlineSnapshot(`
"Route config in "routes.ts" is invalid.
Path: routes.0.children.0.file
Invalid type: Expected string but received undefined"
`);
});

it("is invalid when property is wrong type", () => {
let result = validateRouteConfig({
routeConfigFile: "routes.ts",
/* @ts-expect-error */
routeConfig: [route("parent", "parent.tsx", [{ file: 123 }])],
});

expect(result.valid).toBe(false);
expect(!result.valid && result.message).toMatchInlineSnapshot(`
"Route config in "routes.ts" is invalid.
Path: routes.0.children.0.file
Invalid type: Expected string but received 123"
`);
});

it("shows multiple error messages", () => {
let result = validateRouteConfig({
routeConfigFile: "routes.ts",
routeConfig: [
/* @ts-expect-error */
route("parent", "parent.tsx", [
{ id: "child" },
{ file: 123 },
Promise.resolve(),
]),
],
});

expect(result.valid).toBe(false);
expect(!result.valid && result.message).toMatchInlineSnapshot(`
"Route config in "routes.ts" is invalid.
Path: routes.0.children.0.file
Invalid type: Expected string but received undefined
Path: routes.0.children.1.file
Invalid type: Expected string but received 123
Path: routes.0.children.2
Invalid type: Expected object but received a promise. Did you forget to await?"
`);
});
});

describe("route helpers", () => {
describe("route", () => {
it("supports basic routes", () => {
13 changes: 13 additions & 0 deletions packages/remix-route-config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type {
UNSAFE_RouteConfig as RouteConfig,
UNSAFE_RouteConfigEntry as RouteConfigEntry,
} from "@remix-run/dev";

export {
route,
index,
layout,
prefix,
relative,
getAppDirectory,
} from "./routes";
6 changes: 6 additions & 0 deletions packages/remix-route-config/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('jest').Config} */
module.exports = {
...require("../../jest/jest.config.shared"),
displayName: "route-config",
setupFiles: [],
};
53 changes: 53 additions & 0 deletions packages/remix-route-config/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@remix-run/route-config",
"version": "2.13.1",
"description": "Config-based routing for Remix",
"bugs": {
"url": "https://github.com/remix-run/remix/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/remix-run/remix",
"directory": "packages/remix-fs-routes"
},
"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": {
"lodash": "^4.17.21"
},
"devDependencies": {
"@remix-run/dev": "workspace:*",
"@types/lodash": "^4.14.182",
"typescript": "^5.1.6",
"vite": "5.1.8"
},
"peerDependencies": {
"@remix-run/dev": "workspace:^",
"typescript": "^5.1.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
"engines": {
"node": ">=18.0.0"
},
"files": [
"dist/",
"CHANGELOG.md",
"LICENSE.md",
"README.md"
]
}
45 changes: 45 additions & 0 deletions packages/remix-route-config/rollup.config.js
Original file line number Diff line number Diff line change
@@ -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-route-config";
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(),
],
},
];
};
191 changes: 191 additions & 0 deletions packages/remix-route-config/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { resolve } from "node:path";
import pick from "lodash/pick";
import {
type UNSAFE_RouteConfigEntry as RouteConfigEntry,
UNSAFE_getRouteConfigAppDirectory as getRouteConfigAppDirectory,
} from "@remix-run/dev";

/**
* Provides the absolute path to the app directory, for use within `routes.ts`.
* This is designed to support resolving file system routes.
*/
export function getAppDirectory() {
return getRouteConfigAppDirectory();
}

const createConfigRouteOptionKeys = [
"id",
"index",
"caseSensitive",
] as const satisfies ReadonlyArray<keyof RouteConfigEntry>;
type CreateRouteOptions = Pick<
RouteConfigEntry,
typeof createConfigRouteOptionKeys[number]
>;
/**
* Helper function for creating a route config entry, for use within
* `routes.ts`.
*/
function route(
path: string | null | undefined,
file: string,
children?: RouteConfigEntry[]
): RouteConfigEntry;
function route(
path: string | null | undefined,
file: string,
options: CreateRouteOptions,
children?: RouteConfigEntry[]
): RouteConfigEntry;
function route(
path: string | null | undefined,
file: string,
optionsOrChildren: CreateRouteOptions | RouteConfigEntry[] | undefined,
children?: RouteConfigEntry[]
): RouteConfigEntry {
let options: CreateRouteOptions = {};

if (Array.isArray(optionsOrChildren) || !optionsOrChildren) {
children = optionsOrChildren;
} else {
options = optionsOrChildren;
}

return {
file,
children,
path: path ?? undefined,
...pick(options, createConfigRouteOptionKeys),
};
}

const createIndexOptionKeys = ["id"] as const satisfies ReadonlyArray<
keyof RouteConfigEntry
>;
type CreateIndexOptions = Pick<
RouteConfigEntry,
typeof createIndexOptionKeys[number]
>;
/**
* Helper function for creating a route config entry for an index route, for use
* within `routes.ts`.
*/
function index(file: string, options?: CreateIndexOptions): RouteConfigEntry {
return {
file,
index: true,
...pick(options, createIndexOptionKeys),
};
}

const createLayoutOptionKeys = ["id"] as const satisfies ReadonlyArray<
keyof RouteConfigEntry
>;
type CreateLayoutOptions = Pick<
RouteConfigEntry,
typeof createLayoutOptionKeys[number]
>;
/**
* Helper function for creating a route config entry for a layout route, for use
* within `routes.ts`.
*/
function layout(file: string, children?: RouteConfigEntry[]): RouteConfigEntry;
function layout(
file: string,
options: CreateLayoutOptions,
children?: RouteConfigEntry[]
): RouteConfigEntry;
function layout(
file: string,
optionsOrChildren: CreateLayoutOptions | RouteConfigEntry[] | undefined,
children?: RouteConfigEntry[]
): RouteConfigEntry {
let options: CreateLayoutOptions = {};

if (Array.isArray(optionsOrChildren) || !optionsOrChildren) {
children = optionsOrChildren;
} else {
options = optionsOrChildren;
}

return {
file,
children,
...pick(options, createLayoutOptionKeys),
};
}

/**
* Helper function for adding a path prefix to a set of routes without needing
* to introduce a parent route file, for use within `routes.ts`.
*/
function prefix(
prefixPath: string,
routes: RouteConfigEntry[]
): RouteConfigEntry[] {
return routes.map((route) => {
if (route.index || typeof route.path === "string") {
return {
...route,
path: route.path ? joinRoutePaths(prefixPath, route.path) : prefixPath,
children: route.children,
};
} else if (route.children) {
return {
...route,
children: prefix(prefixPath, route.children),
};
}
return route;
});
}

const helpers = { route, index, layout, prefix };
export { route, index, layout, prefix };
/**
* Creates a set of route config helpers that resolve file paths relative to the
* given directory, for use within `routes.ts`. This is designed to support
* splitting route config into multiple files within different directories.
*/
export function relative(directory: string): typeof helpers {
return {
/**
* Helper function for creating a route config entry, for use within
* `routes.ts`. Note that this helper has been scoped, meaning that file
* path will be resolved relative to the directory provided to the
* `relative` call that created this helper.
*/
route: (path, file, ...rest) => {
return route(path, resolve(directory, file), ...(rest as any));
},
/**
* Helper function for creating a route config entry for an index route, for
* use within `routes.ts`. Note that this helper has been scoped, meaning
* that file path will be resolved relative to the directory provided to the
* `relative` call that created this helper.
*/
index: (file, ...rest) => {
return index(resolve(directory, file), ...(rest as any));
},
/**
* Helper function for creating a route config entry for a layout route, for
* use within `routes.ts`. Note that this helper has been scoped, meaning
* that file path will be resolved relative to the directory provided to the
* `relative` call that created this helper.
*/
layout: (file, ...rest) => {
return layout(resolve(directory, file), ...(rest as any));
},

// Passthrough of helper functions that don't need relative scoping so that
// a complete API is still provided.
prefix,
};
}

function joinRoutePaths(path1: string, path2: string): string {
return [
path1.replace(/\/+$/, ""), // Remove trailing slashes
path2.replace(/^\/+/, ""), // Remove leading slashes
].join("/");
}
19 changes: 19 additions & 0 deletions packages/remix-route-config/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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": "./dist"
}
}
22 changes: 22 additions & 0 deletions pnpm-lock.yaml
1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ packages:
- "packages/remix-fs-routes"
- "packages/remix-node"
- "packages/remix-react"
- "packages/remix-route-config"
- "packages/remix-serve"
- "packages/remix-server-runtime"
- "packages/remix-testing"
1 change: 1 addition & 0 deletions scripts/publish.js
Original file line number Diff line number Diff line change
@@ -63,6 +63,7 @@ async function run() {
"fs-routes",
"css-bundle",
"testing",
"route-config",
]) {
publish(path.join(buildDir, "@remix-run", name), tag);
}

0 comments on commit 9e146e8

Please sign in to comment.