├── .gitignore
├── examples
├── json-routes
│ ├── .gitignore
│ ├── app
│ │ ├── routes
│ │ │ ├── home.tsx
│ │ │ ├── child.tsx
│ │ │ ├── layout.tsx
│ │ │ └── parent.tsx
│ │ └── root.tsx
│ ├── remix.env.d.ts
│ ├── public
│ │ └── favicon.png
│ ├── README.md
│ ├── tsconfig.json
│ ├── package.json
│ └── remix.config.js
└── jsx-routes
│ ├── .gitignore
│ ├── app
│ ├── routes
│ │ ├── home.tsx
│ │ ├── child.tsx
│ │ ├── layout.tsx
│ │ └── parent.tsx
│ └── root.tsx
│ ├── remix.env.d.ts
│ ├── public
│ └── favicon.png
│ ├── routes.jsx
│ ├── README.md
│ ├── remix.config.js
│ ├── routes.js
│ ├── tsconfig.json
│ └── package.json
├── package.json
├── index.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/examples/json-routes/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | /public/build
6 | .env
7 |
--------------------------------------------------------------------------------
/examples/jsx-routes/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | /public/build
6 | .env
7 |
--------------------------------------------------------------------------------
/examples/json-routes/app/routes/home.tsx:
--------------------------------------------------------------------------------
1 | export default function Home() {
2 | return
Home Route
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/jsx-routes/app/routes/home.tsx:
--------------------------------------------------------------------------------
1 | export default function Home() {
2 | return Home Route
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/json-routes/app/routes/child.tsx:
--------------------------------------------------------------------------------
1 | export default function Child() {
2 | return Child Route
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/jsx-routes/app/routes/child.tsx:
--------------------------------------------------------------------------------
1 | export default function Child() {
2 | return Child Route
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/json-routes/remix.env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/examples/jsx-routes/remix.env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/examples/json-routes/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brophdawg11/remix-json-routes/HEAD/examples/json-routes/public/favicon.png
--------------------------------------------------------------------------------
/examples/jsx-routes/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brophdawg11/remix-json-routes/HEAD/examples/jsx-routes/public/favicon.png
--------------------------------------------------------------------------------
/examples/json-routes/app/routes/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "@remix-run/react";
2 |
3 | export default function Layout() {
4 | return (
5 | <>
6 | Layout Route
7 |
8 | >
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/examples/json-routes/app/routes/parent.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "@remix-run/react";
2 |
3 | export default function Layout() {
4 | return (
5 | <>
6 | Parent Route
7 |
8 | >
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/examples/jsx-routes/app/routes/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "@remix-run/react";
2 |
3 | export default function Layout() {
4 | return (
5 | <>
6 | Layout Route
7 |
8 | >
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/examples/jsx-routes/app/routes/parent.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "@remix-run/react";
2 |
3 | export default function Layout() {
4 | return (
5 | <>
6 | Parent Route
7 |
8 | >
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/examples/json-routes/README.md:
--------------------------------------------------------------------------------
1 | # Remix JSON Routes Ecample
2 |
3 | This is an example Remix app using `jsonRoutes` from `remix-json-routes`. It defines the route hierarchy in a JSON format in `remix.config.js`.
4 |
5 | You can run locally by running the following from this directory:
6 |
7 | ```sh
8 | npm ci && npm run dev
9 | ```
10 |
--------------------------------------------------------------------------------
/examples/jsx-routes/routes.jsx:
--------------------------------------------------------------------------------
1 | const React = require("react");
2 | const { Route } = require("remix-json-routes");
3 |
4 | module.exports = (
5 |
6 |
7 |
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/examples/jsx-routes/README.md:
--------------------------------------------------------------------------------
1 | # Remix JSON Routes Ecample
2 |
3 | This is an example Remix app using `jsxRoutes` from `remix-json-routes`. It defines a JSX route hierarchy in a `routes.jsx` file which it transpiles to `routes.js` in a `predev`/`prebuild` npm script. Then remix config can import the generated `routes.js` file and pass it to `jsxRoutes`.
4 |
5 | You can run locally by running the following from this directory:
6 |
7 | ```sh
8 | npm ci && npm run dev
9 | ```
10 |
--------------------------------------------------------------------------------
/examples/jsx-routes/remix.config.js:
--------------------------------------------------------------------------------
1 | const { jsxRoutes } = require("remix-json-routes");
2 | const routes = require("./routes");
3 |
4 | /** @type {import('@remix-run/dev').AppConfig} */
5 | module.exports = {
6 | // Ignore everything routes, we'll define them all via routes()
7 | ignoredRouteFiles: ["**/*"],
8 | serverModuleFormat: "cjs",
9 | future: {
10 | v2_dev: true,
11 | v2_errorBoundary: true,
12 | v2_headers: true,
13 | v2_meta: true,
14 | v2_normalizeFormMethod: true,
15 | v2_routeConvention: true,
16 | },
17 | routes: (defineRoute) => jsxRoutes(defineRoute, routes),
18 | };
19 |
--------------------------------------------------------------------------------
/examples/jsx-routes/routes.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var import_jsx_runtime = require("react/jsx-runtime");
3 | const React = require("react");
4 | const { Route } = require("remix-json-routes");
5 | module.exports = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Route, { path: "/", file: "routes/layout.tsx", children: [
6 | /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, { index: true, file: "routes/home.tsx" }),
7 | /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, { path: "parent", file: "routes/parent.tsx", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, { path: "child", file: "routes/child.tsx" }) })
8 | ] });
9 |
--------------------------------------------------------------------------------
/examples/json-routes/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "lib": ["DOM", "DOM.Iterable", "ES2019"],
5 | "isolatedModules": true,
6 | "esModuleInterop": true,
7 | "jsx": "react-jsx",
8 | "moduleResolution": "node",
9 | "resolveJsonModule": true,
10 | "target": "ES2019",
11 | "strict": true,
12 | "allowJs": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "baseUrl": ".",
15 | "paths": {
16 | "~/*": ["./app/*"]
17 | },
18 |
19 | // Remix takes care of building everything in `remix build`.
20 | "noEmit": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/jsx-routes/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "lib": ["DOM", "DOM.Iterable", "ES2019"],
5 | "isolatedModules": true,
6 | "esModuleInterop": true,
7 | "jsx": "react-jsx",
8 | "moduleResolution": "node",
9 | "resolveJsonModule": true,
10 | "target": "ES2019",
11 | "strict": true,
12 | "allowJs": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "baseUrl": ".",
15 | "paths": {
16 | "~/*": ["./app/*"]
17 | },
18 |
19 | // Remix takes care of building everything in `remix build`.
20 | "noEmit": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/json-routes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "sideEffects": false,
4 | "scripts": {
5 | "build": "remix build",
6 | "dev": "remix dev",
7 | "start": "remix-serve build",
8 | "typecheck": "tsc"
9 | },
10 | "dependencies": {
11 | "@remix-run/css-bundle": "^1.19.3",
12 | "@remix-run/node": "^1.19.3",
13 | "@remix-run/react": "^1.19.3",
14 | "@remix-run/serve": "^1.19.3",
15 | "isbot": "^3.6.8",
16 | "react": "^18.2.0",
17 | "react-dom": "^18.2.0",
18 | "remix-json-routes": "^0.1.1"
19 | },
20 | "devDependencies": {
21 | "@remix-run/dev": "^1.19.3",
22 | "@types/react": "^18.0.35",
23 | "@types/react-dom": "^18.0.11",
24 | "typescript": "^5.0.4"
25 | },
26 | "engines": {
27 | "node": ">=14.0.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "remix-json-routes",
3 | "version": "0.1.1",
4 | "description": "A Remix package to allow custom route definition via JSON or React elements",
5 | "sideEffects": false,
6 | "main": "index.js",
7 | "author": "matt@brophy.org",
8 | "license": "ISC",
9 | "homepage": "https://github.com/brophdawg11/remix-json-routes#readme",
10 | "repository": {
11 | "type": "git",
12 | "url": "git+ssh://git@github.com/brophdawg11/remix-json-routes.git"
13 | },
14 | "bugs": {
15 | "url": "https://github.com/brophdawg11/remix-json-routes/issues"
16 | },
17 | "keywords": [
18 | "remix",
19 | "routes",
20 | "json",
21 | "jsx"
22 | ],
23 | "scripts": {},
24 | "devDependencies": {
25 | "react": "^18.2.0"
26 | },
27 | "peerDependencies": {
28 | "react": ">=16.8"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/examples/json-routes/app/root.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Link,
3 | Links,
4 | LiveReload,
5 | Meta,
6 | Outlet,
7 | Scripts,
8 | ScrollRestoration,
9 | } from "@remix-run/react";
10 |
11 | export default function App() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Remix JSON Routes Example Application
23 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/examples/jsx-routes/app/root.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Link,
3 | Links,
4 | LiveReload,
5 | Meta,
6 | Outlet,
7 | Scripts,
8 | ScrollRestoration,
9 | } from "@remix-run/react";
10 |
11 | export default function App() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Remix JSX Routes Example Application
23 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/examples/jsx-routes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "sideEffects": false,
4 | "scripts": {
5 | "jsxroutes": "esbuild routes.jsx --format=cjs --outfile=routes.js",
6 | "prebuild": "npm run jsxroutes",
7 | "predev": "npm run jsxroutes",
8 | "build": "remix build",
9 | "dev": "remix dev",
10 | "start": "remix-serve build",
11 | "typecheck": "tsc"
12 | },
13 | "dependencies": {
14 | "@remix-run/css-bundle": "^1.19.3",
15 | "@remix-run/node": "^1.19.3",
16 | "@remix-run/react": "^1.19.3",
17 | "@remix-run/serve": "^1.19.3",
18 | "esbuild": "^0.17.6",
19 | "isbot": "^3.6.8",
20 | "react": "^18.2.0",
21 | "react-dom": "^18.2.0",
22 | "remix-json-routes": "^0.1.1"
23 | },
24 | "devDependencies": {
25 | "@remix-run/dev": "^1.19.3",
26 | "@types/react": "^18.0.35",
27 | "@types/react-dom": "^18.0.11",
28 | "typescript": "^5.0.4"
29 | },
30 | "engines": {
31 | "node": ">=14.0.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/examples/json-routes/remix.config.js:
--------------------------------------------------------------------------------
1 | const { jsonRoutes } = require("remix-json-routes");
2 |
3 | /** @type {import('@remix-run/dev').AppConfig} */
4 | module.exports = {
5 | // Ignore everything routes, we'll define them all via routes()
6 | ignoredRouteFiles: ["**/*"],
7 | serverModuleFormat: "cjs",
8 | future: {
9 | v2_dev: true,
10 | v2_errorBoundary: true,
11 | v2_headers: true,
12 | v2_meta: true,
13 | v2_normalizeFormMethod: true,
14 | v2_routeConvention: true,
15 | },
16 | routes(defineRoutes) {
17 | return jsonRoutes(defineRoutes, [
18 | {
19 | path: "/",
20 | file: "routes/layout.tsx",
21 | children: [
22 | {
23 | index: true,
24 | file: "routes/home.tsx",
25 | },
26 | {
27 | path: "parent",
28 | file: "routes/parent.tsx",
29 | children: [
30 | {
31 | path: "child",
32 | file: "routes/child.tsx",
33 | },
34 | ],
35 | },
36 | ],
37 | },
38 | ]);
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const React = require("react");
2 |
3 | function jsxRoutes(defineRoutes, routes) {
4 | return jsonRoutes(defineRoutes, createRoutesFromElements(routes));
5 | }
6 |
7 | function jsonRoutes(defineRoutes, routes) {
8 | return defineRoutes((route) => routes.forEach((r) => defineRoute(route, r)));
9 | }
10 |
11 | function defineRoute(routeFn, jsonRoute) {
12 | let path = jsonRoute.path;
13 | let file = jsonRoute.file;
14 | let children = jsonRoute.children;
15 | let opts = { ...jsonRoute };
16 | delete opts.path;
17 | delete opts.file;
18 | if (children) {
19 | routeFn(path, file, opts, () => {
20 | children.forEach((c) => defineRoute(routeFn, c));
21 | });
22 | } else {
23 | routeFn(path, file, opts);
24 | }
25 | }
26 |
27 | function Route() {
28 | throw new Error(" is for defining routes, not for rendering");
29 | }
30 |
31 | function createRoutesFromElements(children, parentPath = []) {
32 | let routes = [];
33 |
34 | React.Children.forEach(children, (element, index) => {
35 | let treePath = [...parentPath, index];
36 |
37 | if (element.type === React.Fragment) {
38 | routes.push(...createRoutesFromElements(element.props.children, treePath));
39 | return;
40 | }
41 |
42 | if (element.type !== Route) {
43 | let type =
44 | typeof element.type === "string" ? element.type : element.type.name;
45 | throw new Error(
46 | `Only elements are supported, found type: ${type}`
47 | );
48 | }
49 |
50 | if (element.props.index && element.props.children) {
51 | throw new Error("An index route cannot have child routes.");
52 | }
53 |
54 | let route = {
55 | id: element.props.id || treePath.join("-"),
56 | ...element.props,
57 | };
58 |
59 | if (element.props.children) {
60 | route.children = createRoutesFromElements(
61 | element.props.children,
62 | treePath
63 | );
64 | }
65 |
66 | routes.push(route);
67 | });
68 |
69 | return routes;
70 | }
71 |
72 | module.exports = {
73 | jsonRoutes,
74 | jsxRoutes,
75 | Route,
76 | };
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Remix JSON Routes
2 |
3 | `remix-json-routes` is a package to allow you to define your Remix routes from a custom JSON structure, instead of (or in addition to ) the built in file-based routing convention.
4 |
5 | ## Installation
6 |
7 | ```sh
8 | npm install remix-json-routes
9 | ```
10 |
11 | ## Using JSON Routes
12 |
13 | [Check out the example app in examples/](examples/remix-json-routes)
14 |
15 | You leverage this package via the `routes` function in `remix.config.js`. The second argument to `jsonRoutes` is an array of routes similar to what you would pass to [`createBrowserRouter`](https://reactrouter.com/en/main/routers/create-browser-router) in React Router, where you define the route path information (`path`, `index`, `children`), but then instead of specifying an `element`/`action`/`loader`/etc., you specify the `file` path pointing to a Remix route file which exports those aspects.
16 |
17 | ```js
18 | const { jsonRoutes } = require("remix-json-routes");
19 |
20 | /** @type {import('@remix-run/dev').AppConfig} */
21 | module.exports = {
22 | // Note this ignores everything in routes/ giving you complete control over
23 | // your routes. If you want to define routes in addition to what's in routes/,
24 | // change this to "ignoredRouteFiles": ["**/.*"].
25 | ignoredRouteFiles: ["**/*"],
26 | routes(defineRoutes) {
27 | return jsonRoutes(defineRoutes, [
28 | {
29 | path: "/",
30 | file: "routes/layout.tsx",
31 | children: [
32 | {
33 | index: true,
34 | file: "routes/some/path/to/home.tsx",
35 | },
36 | {
37 | path: "about",
38 | file: "routes/some/path/to/about.tsx",
39 | },
40 | ],
41 | },
42 | ]);
43 | },
44 | };
45 | ```
46 |
47 | ## Using JSX Routes
48 |
49 | [Check out the example app in examples/](examples/remix-jsx-routes)
50 |
51 | `remix.config.js` does not support JSX out of the box, but with a small prebuild step you can also define your routes with JSX. The easiest way to do this is to put your JSX route definitions in a `route.jsx` file that is transpiled to a `routes.js` file as a prebuild step which you can then `require` from `remix.config.js`.
52 |
53 | **Create your routes.jsx file**
54 |
55 | This file should export your JSX tree using the `Route` component from `remix-json-routes`:
56 |
57 | ```jsx
58 | const React = require("react");
59 | const { Route } = require("remix-json-routes");
60 |
61 | module.exports = (
62 |
63 |
64 |
65 |
66 | );
67 | ```
68 |
69 | **Create a prebuild step to build `routes.js`**
70 |
71 | Add a `jsxroutes` script to `package.json` that will transpile `routes.jsx` to `routes.js`, then add `prebuild`/`predev` scripts so we always build a fresh version of `routes.js` before `npm run build`/`npm run dev`:
72 |
73 | ```json
74 | {
75 | "scripts": {
76 | "jsxroutes": "esbuild routes.jsx --format=cjs --outfile=routes.js",
77 | "prebuild": "npm run jsxroutes",
78 | "predev": "npm run jsxroutes",
79 | "build": "remix build",
80 | "dev": "remix dev",
81 | "...": "..."
82 | }
83 | }
84 | ```
85 |
86 | > **Note**
87 | > You will probably want to add `routes.js` to your `.gitignore` file as well.
88 |
89 | **Edit your remix.config.js to use `jsxRoutes`**
90 |
91 | ```js
92 | // remix.config.js
93 | const { jsxRoutes } = require("remix-json-routes");
94 | const routes = require("./routes");
95 |
96 | /** @type {import('@remix-run/dev').AppConfig} */
97 | module.exports = {
98 | ignoredRouteFiles: ["**/*"],
99 | routes(defineRoute) {
100 | return jsxRoutes(defineRoute, routes);
101 | },
102 | };
103 | ```
104 |
--------------------------------------------------------------------------------