├── .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 | --------------------------------------------------------------------------------