orpc-file-based-router 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,98 +43,40 @@ src/routes
43
43
  └── sse.ts
44
44
  ```
45
45
 
46
- 3. Each file should export an oRPC function
46
+ 3. Each file should export an oRPC function (non-oRPC exports will be ignored)
47
47
 
48
- 4. Simply replace router in your handlers with the result of the `createRouter`
49
- function:
48
+ 4. Generate your router before starting the server using the `generateRouter` function:
50
49
 
51
50
  ```typescript
52
- import { RPCHandler } from "@orpc/server/node";
53
- import { createRouter } from "orpc-file-based-router";
54
-
55
- const routesDir = new URL("./routes", import.meta.url).pathname;
56
- const router = await createRouter(routesDir);
57
-
58
- const handler = new RPCHandler(router);
59
-
60
- ```
61
- **Note:** If your environment doesn't support top-level await, just use `lazy` for example in expressjs it could be:
62
- ```typescript
63
- import { RPCHandler } from "@orpc/server/node";
64
- import { lazy, createRouter } from "orpc-file-based-router";
51
+ // router-gen.ts
52
+ import { generateRouter } from "orpc-file-based-router";
65
53
 
66
54
  const routesDir = new URL("./routes", import.meta.url).pathname;
55
+ const outputFile = new URL("./router.ts", import.meta.url).pathname;
67
56
 
68
- const router = lazy(() => createRouter(routesDir))
69
-
70
- app.use('/rpc{/*path}', async (req, res, next) => {
71
-
72
- const handler = new RPCHandler(await router());
73
-
74
- const { matched } = await handler.handle(req, res, {
75
- prefix: '/rpc',
76
- })
77
-
78
- if (matched) {
79
- return
80
- }
81
-
82
- next()
83
- })
57
+ generateRouter(routesDir, outputFile);
84
58
  ```
85
59
 
86
- ## 🔒 Type-Safe Client Configuration (Optional)
87
-
88
- For users of the [oRPC client](https://orpc.unnoq.com/docs/client/client-side), we provide automatic configuration generation for enhanced type safety and improved developer experience.
89
-
90
- 1. You can add this code either directly in your server project (e.g., in server.ts or main.ts) or put it into a separate script (e.g., router-gen.ts).
60
+ 5. Import and use the generated router in your server:
91
61
 
92
62
  ```typescript
93
- import { generateRouter } from "orpc-file-based-router";
63
+ import { RPCHandler } from "@orpc/server/node";
64
+ import { router } from "./router.js"; // Import the generated router
94
65
 
95
- const routesDir = new URL("./routes", import.meta.url).pathname;
96
- const outputFile = new URL("./router.ts", import.meta.url).pathname;
97
- generateRouter(routesDir, outputFile);
66
+ const handler = new RPCHandler(router);
98
67
  ```
99
68
 
100
- 2. Generated router is ready to use in client:
69
+ ## 🔒 Enhanced Type-Safe Client Configuration
101
70
 
102
- > ⚠️ If you don't want plugin to generate openapi `route({})` suffix, just set parameter `includeRoute` to `false`
71
+ After generating your router (as shown in step 4 above), you can use it in your client:
103
72
 
104
73
  ```typescript
105
- // router.ts
106
- import { me } from "./routes/auth/me";
107
- import { signin } from "./routes/auth/signin";
108
- import { signup } from "./routes/auth/signup";
109
- import { createPlanet } from "./routes/planets/create";
110
- import { indexRoute } from "./routes/planets";
111
- import { listPlanets } from "./routes/planets/list";
112
- import { findPlanet } from "./routes/planets/{id}/find";
113
- import { updatePlanet } from "./routes/planets/{id}/update";
114
- import { sse } from "./routes/sse";
115
-
116
- export const router = {
117
- auth: {
118
- me: me.route({ path: "/auth/me" }),
119
- signin: signin.route({ path: "/auth/signin" }),
120
- signup: signup.route({ path: "/auth/signup" }),
121
- },
122
- planets: {
123
- create: createPlanet.route({ path: "/planets/create" }),
124
- indexRoute: indexRoute.route({ path: "/planets" }),
125
- list: listPlanets.route({ path: "/planets/list" }),
126
- find: findPlanet.route({ path: "/planets/{id}/find" }),
127
- update: updatePlanet.route({ path: "/planets/{id}/update" }),
128
- },
129
- sse: sse.route({ path: "/sse" }),
130
- };
131
-
132
-
133
74
  // lib/orpc.ts
75
+ import { router } from "../router.js"; // Use the same generated router
134
76
  const client: RouterClient<typeof router> = createORPCClient(link)
135
-
136
77
  ```
137
78
 
79
+
138
80
  ## 🛠 Configuration Options
139
81
 
140
82
  When using `generateRouter`, you can provide additional options to customize the output:
@@ -142,9 +84,20 @@ When using `generateRouter`, you can provide additional options to customize the
142
84
  | Field | Type | Required | Default Value | Description |
143
85
  |-------------------|----------|--------------|-----------------------|------------------------------------------------------------------------------------------------------------------------------|
144
86
  | `importExtension` | string | false | `""`(No extension) | File extension to append to import statements in the generated router. Useful when your build setup requires specific extensions. <br>Example: `.js` → `import { me } from "./routes/auth/me.js"` |
145
- | `includeRoute` | boolean | false | `true` | When set to true, each route will be wrapped with openapi `.route({ path: '...' })` call |
87
+ | `additionalMethods` | string[] | false | `[]` | Additional HTTP methods to recognize from export names. |
88
+
146
89
 
90
+ ## Examples
91
+ ### HTTP Method Matching
92
+ If you export functions named e.g. `get`, `post`, `put`, `patch`, `delete/del` etc. from a route file, those will get matched their corresponding http method automatically.
147
93
 
94
+ ```typescript
95
+ // ./routes/planets.ts
96
+
97
+ export const get = orpc.handler(async ({ input, context }) => {})
98
+
99
+ export const post = orpc.handler(async ({ input, context }) => {})
100
+ ```
148
101
 
149
102
  ## 📄 License
150
103
 
package/dist/index.cjs CHANGED
@@ -7,6 +7,25 @@ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'defau
7
7
 
8
8
  const path__default = /*#__PURE__*/_interopDefaultCompat(path);
9
9
 
10
+ const METHOD_EXPORTS = [
11
+ "GET",
12
+ "POST",
13
+ "PUT",
14
+ "PATCH",
15
+ "DELETE",
16
+ "HEAD"
17
+ ];
18
+
19
+ const getMethodKey = (method) => {
20
+ let methodKey = method.toUpperCase();
21
+ if (methodKey === "DEL") return "DELETE";
22
+ return methodKey;
23
+ };
24
+ const isMethodExport = (exportName, extraMethods) => {
25
+ const methods = [...METHOD_EXPORTS, ...extraMethods].map(getMethodKey);
26
+ return methods.includes(getMethodKey(exportName));
27
+ };
28
+
10
29
  function walkTree(directory, tree = []) {
11
30
  const results = [];
12
31
  for (const fileName of node_fs.readdirSync(directory)) {
@@ -27,15 +46,6 @@ function walkTree(directory, tree = []) {
27
46
  function mergePaths(...paths) {
28
47
  return `/${paths.map((path2) => path2.replace(/^\/|\/$/g, "")).filter((path2) => path2 !== "").join("/")}`;
29
48
  }
30
- async function createRouter(routesDir) {
31
- const files = walkTree(
32
- routesDir
33
- );
34
- const exports = await generateRoutes(files);
35
- return buildRouter(exports, (r, e) => {
36
- return r.exports[e].route({ path: `${r.path}` });
37
- });
38
- }
39
49
  const lazy = (create) => {
40
50
  let cache = null;
41
51
  const fn = () => {
@@ -49,6 +59,10 @@ const lazy = (create) => {
49
59
  };
50
60
  return fn;
51
61
  };
62
+ async function createRouter(routesDir) {
63
+ const msg = "createRouter is deprecated. Please use generateRouter to generate router file at build time. See https://github.com/zeeeeby/orpc-file-based-router?tab=readme-ov-file#quickstart for more details.";
64
+ throw new Error(msg);
65
+ }
52
66
  async function generateRouter(routesDir, outputFile, options) {
53
67
  const files = walkTree(
54
68
  routesDir
@@ -56,20 +70,25 @@ async function generateRouter(routesDir, outputFile, options) {
56
70
  const exports = await generateRoutes(files);
57
71
  const importPaths = exports.map((x) => path.relative(path.dirname(outputFile), routesDir).concat(x.path));
58
72
  const content = buildRouter(exports, (r, e) => {
59
- if (options?.includeRoute ?? true) {
60
- return `${e}.route({ path: '${r.path.replace(/\/{0,1}index$/, "")}' })`;
61
- }
62
- return e;
73
+ const alias = routePathToAlias(r.path, e);
74
+ const orpcMeta = isORPCProcedure(r.exports[e]) ? r.exports[e]["~orpc"] : void 0;
75
+ const method = isMethodExport(e, options?.additionalMethods || []) ? getMethodKey(e) : orpcMeta?.route?.method || "POST";
76
+ const config = { path: r.path.replace(/\/{0,1}index$/, ""), method };
77
+ return `${alias}.route({ path: '${config.path}', method: '${config.method}' })`;
63
78
  });
64
79
  let routerContent = `// This file is auto-generated
65
80
 
66
81
  `;
67
82
  const extension = options?.importExtension || "";
68
- routerContent += importPaths.map((x, i) => `import { ${Object.keys(exports[i].exports).join(", ")} } from "./${x}${extension}"`).join("\n");
83
+ routerContent += importPaths.map((x, i) => `import { ${Object.keys(exports[i].exports).map((y) => `${y} as ${routePathToAlias(exports[i].path, y)}`).join(", ")} } from "./${x}${extension}"`).join("\n");
69
84
  routerContent += "\n\nexport const router = ";
70
85
  routerContent += JSON.stringify(content, null, 2).replace(/"/g, "").replace(/(\s*)([a-zA-Z0-9]+-[a-zA-Z0-9-]+):/g, '$1"$2":');
71
86
  node_fs.writeFileSync(path.join(outputFile), routerContent);
72
87
  }
88
+ function routePathToAlias(routePath, methodName) {
89
+ const cleanPath = routePath.replace(/\{(\w+)\}/g, "$1").replace(/-/g, "_");
90
+ return cleanPath.split("/").filter(Boolean).join("_") + "__" + methodName;
91
+ }
73
92
  function buildRoutePath(parsedFile) {
74
93
  const directory = parsedFile.dir === parsedFile.root ? "" : parsedFile.dir;
75
94
  const name = `/${parsedFile.name}`;
@@ -128,13 +147,24 @@ async function generateRoutes(files) {
128
147
  const parsedFile = path__default.parse(file.rel);
129
148
  const routePath = buildRoutePath(parsedFile);
130
149
  const exports = await import(MODULE_IMPORT_PREFIX + path__default.join(file.path, file.name));
131
- routes.push({
132
- exports,
133
- path: routePath
134
- });
150
+ const cleanedExports = removeNonOrpcExports(exports);
151
+ if (Object.keys(cleanedExports).length > 0)
152
+ routes.push({
153
+ exports: cleanedExports,
154
+ path: routePath
155
+ });
135
156
  }
136
157
  return routes;
137
158
  }
159
+ const removeNonOrpcExports = (exports) => {
160
+ const cleanedExports = {};
161
+ for (const key in exports) {
162
+ if (isORPCProcedure(exports[key])) {
163
+ cleanedExports[key] = exports[key];
164
+ }
165
+ }
166
+ return cleanedExports;
167
+ };
138
168
 
139
169
  exports.createRouter = createRouter;
140
170
  exports.generateRouter = generateRouter;
package/dist/index.d.cts CHANGED
@@ -1,8 +1,11 @@
1
- declare function createRouter(routesDir: string): Promise<Router>;
2
1
  declare const lazy: <T>(create: () => Promise<T> | T) => {
3
2
  (): Promise<T>;
4
3
  reset(): void;
5
4
  };
5
+ /**
6
+ * @deprecated use generateRouter instead. See https://github.com/zeeeeby/orpc-file-based-router?tab=readme-ov-file#quickstart for more details.
7
+ */
8
+ declare function createRouter(routesDir: string): Promise<void>;
6
9
  type GeneratorOptions = {
7
10
  /**
8
11
  * File extension to append to import statements in the generated router.
@@ -12,13 +15,10 @@ type GeneratorOptions = {
12
15
  */
13
16
  importExtension?: string;
14
17
  /**
15
- * Include openapi route paths in the generated router.
16
- * When set to true, each route will have its openapi path
17
- * @default true
18
+ * Additional HTTP methods to recognize from export names.
18
19
  */
19
- includeRoute?: boolean;
20
+ additionalMethods?: string[];
20
21
  };
21
22
  declare function generateRouter(routesDir: string, outputFile: string, options?: GeneratorOptions): Promise<void>;
22
- type Router = Record<string, any>;
23
23
 
24
24
  export { createRouter, generateRouter, lazy };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,11 @@
1
- declare function createRouter(routesDir: string): Promise<Router>;
2
1
  declare const lazy: <T>(create: () => Promise<T> | T) => {
3
2
  (): Promise<T>;
4
3
  reset(): void;
5
4
  };
5
+ /**
6
+ * @deprecated use generateRouter instead. See https://github.com/zeeeeby/orpc-file-based-router?tab=readme-ov-file#quickstart for more details.
7
+ */
8
+ declare function createRouter(routesDir: string): Promise<void>;
6
9
  type GeneratorOptions = {
7
10
  /**
8
11
  * File extension to append to import statements in the generated router.
@@ -12,13 +15,10 @@ type GeneratorOptions = {
12
15
  */
13
16
  importExtension?: string;
14
17
  /**
15
- * Include openapi route paths in the generated router.
16
- * When set to true, each route will have its openapi path
17
- * @default true
18
+ * Additional HTTP methods to recognize from export names.
18
19
  */
19
- includeRoute?: boolean;
20
+ additionalMethods?: string[];
20
21
  };
21
22
  declare function generateRouter(routesDir: string, outputFile: string, options?: GeneratorOptions): Promise<void>;
22
- type Router = Record<string, any>;
23
23
 
24
24
  export { createRouter, generateRouter, lazy };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,11 @@
1
- declare function createRouter(routesDir: string): Promise<Router>;
2
1
  declare const lazy: <T>(create: () => Promise<T> | T) => {
3
2
  (): Promise<T>;
4
3
  reset(): void;
5
4
  };
5
+ /**
6
+ * @deprecated use generateRouter instead. See https://github.com/zeeeeby/orpc-file-based-router?tab=readme-ov-file#quickstart for more details.
7
+ */
8
+ declare function createRouter(routesDir: string): Promise<void>;
6
9
  type GeneratorOptions = {
7
10
  /**
8
11
  * File extension to append to import statements in the generated router.
@@ -12,13 +15,10 @@ type GeneratorOptions = {
12
15
  */
13
16
  importExtension?: string;
14
17
  /**
15
- * Include openapi route paths in the generated router.
16
- * When set to true, each route will have its openapi path
17
- * @default true
18
+ * Additional HTTP methods to recognize from export names.
18
19
  */
19
- includeRoute?: boolean;
20
+ additionalMethods?: string[];
20
21
  };
21
22
  declare function generateRouter(routesDir: string, outputFile: string, options?: GeneratorOptions): Promise<void>;
22
- type Router = Record<string, any>;
23
23
 
24
24
  export { createRouter, generateRouter, lazy };
package/dist/index.mjs CHANGED
@@ -1,6 +1,25 @@
1
1
  import { writeFileSync, readdirSync, statSync } from 'node:fs';
2
2
  import path, { relative, dirname, join } from 'node:path';
3
3
 
4
+ const METHOD_EXPORTS = [
5
+ "GET",
6
+ "POST",
7
+ "PUT",
8
+ "PATCH",
9
+ "DELETE",
10
+ "HEAD"
11
+ ];
12
+
13
+ const getMethodKey = (method) => {
14
+ let methodKey = method.toUpperCase();
15
+ if (methodKey === "DEL") return "DELETE";
16
+ return methodKey;
17
+ };
18
+ const isMethodExport = (exportName, extraMethods) => {
19
+ const methods = [...METHOD_EXPORTS, ...extraMethods].map(getMethodKey);
20
+ return methods.includes(getMethodKey(exportName));
21
+ };
22
+
4
23
  function walkTree(directory, tree = []) {
5
24
  const results = [];
6
25
  for (const fileName of readdirSync(directory)) {
@@ -21,15 +40,6 @@ function walkTree(directory, tree = []) {
21
40
  function mergePaths(...paths) {
22
41
  return `/${paths.map((path2) => path2.replace(/^\/|\/$/g, "")).filter((path2) => path2 !== "").join("/")}`;
23
42
  }
24
- async function createRouter(routesDir) {
25
- const files = walkTree(
26
- routesDir
27
- );
28
- const exports = await generateRoutes(files);
29
- return buildRouter(exports, (r, e) => {
30
- return r.exports[e].route({ path: `${r.path}` });
31
- });
32
- }
33
43
  const lazy = (create) => {
34
44
  let cache = null;
35
45
  const fn = () => {
@@ -43,6 +53,10 @@ const lazy = (create) => {
43
53
  };
44
54
  return fn;
45
55
  };
56
+ async function createRouter(routesDir) {
57
+ const msg = "createRouter is deprecated. Please use generateRouter to generate router file at build time. See https://github.com/zeeeeby/orpc-file-based-router?tab=readme-ov-file#quickstart for more details.";
58
+ throw new Error(msg);
59
+ }
46
60
  async function generateRouter(routesDir, outputFile, options) {
47
61
  const files = walkTree(
48
62
  routesDir
@@ -50,20 +64,25 @@ async function generateRouter(routesDir, outputFile, options) {
50
64
  const exports = await generateRoutes(files);
51
65
  const importPaths = exports.map((x) => relative(dirname(outputFile), routesDir).concat(x.path));
52
66
  const content = buildRouter(exports, (r, e) => {
53
- if (options?.includeRoute ?? true) {
54
- return `${e}.route({ path: '${r.path.replace(/\/{0,1}index$/, "")}' })`;
55
- }
56
- return e;
67
+ const alias = routePathToAlias(r.path, e);
68
+ const orpcMeta = isORPCProcedure(r.exports[e]) ? r.exports[e]["~orpc"] : void 0;
69
+ const method = isMethodExport(e, options?.additionalMethods || []) ? getMethodKey(e) : orpcMeta?.route?.method || "POST";
70
+ const config = { path: r.path.replace(/\/{0,1}index$/, ""), method };
71
+ return `${alias}.route({ path: '${config.path}', method: '${config.method}' })`;
57
72
  });
58
73
  let routerContent = `// This file is auto-generated
59
74
 
60
75
  `;
61
76
  const extension = options?.importExtension || "";
62
- routerContent += importPaths.map((x, i) => `import { ${Object.keys(exports[i].exports).join(", ")} } from "./${x}${extension}"`).join("\n");
77
+ routerContent += importPaths.map((x, i) => `import { ${Object.keys(exports[i].exports).map((y) => `${y} as ${routePathToAlias(exports[i].path, y)}`).join(", ")} } from "./${x}${extension}"`).join("\n");
63
78
  routerContent += "\n\nexport const router = ";
64
79
  routerContent += JSON.stringify(content, null, 2).replace(/"/g, "").replace(/(\s*)([a-zA-Z0-9]+-[a-zA-Z0-9-]+):/g, '$1"$2":');
65
80
  writeFileSync(join(outputFile), routerContent);
66
81
  }
82
+ function routePathToAlias(routePath, methodName) {
83
+ const cleanPath = routePath.replace(/\{(\w+)\}/g, "$1").replace(/-/g, "_");
84
+ return cleanPath.split("/").filter(Boolean).join("_") + "__" + methodName;
85
+ }
67
86
  function buildRoutePath(parsedFile) {
68
87
  const directory = parsedFile.dir === parsedFile.root ? "" : parsedFile.dir;
69
88
  const name = `/${parsedFile.name}`;
@@ -122,12 +141,23 @@ async function generateRoutes(files) {
122
141
  const parsedFile = path.parse(file.rel);
123
142
  const routePath = buildRoutePath(parsedFile);
124
143
  const exports = await import(MODULE_IMPORT_PREFIX + path.join(file.path, file.name));
125
- routes.push({
126
- exports,
127
- path: routePath
128
- });
144
+ const cleanedExports = removeNonOrpcExports(exports);
145
+ if (Object.keys(cleanedExports).length > 0)
146
+ routes.push({
147
+ exports: cleanedExports,
148
+ path: routePath
149
+ });
129
150
  }
130
151
  return routes;
131
152
  }
153
+ const removeNonOrpcExports = (exports) => {
154
+ const cleanedExports = {};
155
+ for (const key in exports) {
156
+ if (isORPCProcedure(exports[key])) {
157
+ cleanedExports[key] = exports[key];
158
+ }
159
+ }
160
+ return cleanedExports;
161
+ };
132
162
 
133
163
  export { createRouter, generateRouter, lazy };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orpc-file-based-router",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "File-based router plugin for oRPC - automatically generate oRPC router from your file structure",
5
5
  "author": "zeeeeby",
6
6
  "license": "MIT",