orpc-file-based-router 0.0.1

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 ADDED
@@ -0,0 +1,78 @@
1
+ # orpc-file-based-router
2
+
3
+ A plugin for [oRPC](https://orpc.unnoq.com) that automatically creates an oRPC router configuration based on your file structure.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install orpc-file-based-router
9
+ # or
10
+ yarn add orpc-file-based-router
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ // generator.ts
17
+ import { generateRouter } from 'orpc-file-based-router'
18
+
19
+ const routesDir = './routes'
20
+ const outputFile = './router.ts'
21
+
22
+ generateRouter(routesDir, outputFile)
23
+ .then(() => console.log('Router generated successfully'))
24
+ .catch(console.error)
25
+ ```
26
+
27
+ ### File Structure Example
28
+
29
+ ```
30
+ src/routes
31
+ ├── auth
32
+ │ ├── me.ts (exports: me)
33
+ │ ├── signin.ts (exports: signin)
34
+ │ └── signup.ts (exports: signup)
35
+
36
+ ├── planets
37
+ │ ├── {id}
38
+ │ │ ├── find.ts (exports: findPlanet)
39
+ │ │ └── update.ts (exports: updatePlanet)
40
+ │ │
41
+ │ ├── create.ts (exports: createPlanet)
42
+ │ ├── index.ts (exports: indexRoute)
43
+ │ └── list.ts (exports: listPlanets)
44
+
45
+ └── sse.ts (exports: sse)
46
+ ```
47
+ ### Run `tsx src/generator.ts `
48
+
49
+ ### Generated result
50
+
51
+ ```typescript
52
+ import { me } from './routes/auth/me'
53
+ import { signin } from './routes/auth/signin'
54
+ import { signup } from './routes/auth/signup'
55
+ import { createPlanet } from './routes/planets/create'
56
+ import { indexRoute } from './routes/planets'
57
+ import { listPlanets } from './routes/planets/list'
58
+ import { findPlanet } from './routes/planets/{id}/find'
59
+ import { updatePlanet } from './routes/planets/{id}/update'
60
+ import { sse } from './routes/sse'
61
+
62
+ export const router = {
63
+ auth: {
64
+ me: me.route({ path: '/auth/me' }),
65
+ signin: signin.route({ path: '/auth/signin' }),
66
+ signup: signup.route({ path: '/auth/signup' }),
67
+ },
68
+ planets: {
69
+ create: createPlanet.route({ path: '/planets/create' }),
70
+ indexRoute: indexRoute.route({ path: '/planets' }),
71
+ list: listPlanets.route({ path: '/planets/list' }),
72
+ find: findPlanet.route({ path: '/planets/{id}/find' }),
73
+ update: updatePlanet.route({ path: '/planets/{id}/update' }),
74
+ },
75
+ sse: sse.route({ path: '/sse' }),
76
+ }
77
+ ```
78
+
package/dist/index.cjs ADDED
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ const node_fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
7
+
8
+ const path__default = /*#__PURE__*/_interopDefaultCompat(path);
9
+
10
+ function walkTree(directory, tree = []) {
11
+ const results = [];
12
+ for (const fileName of node_fs.readdirSync(directory)) {
13
+ const filePath = path.join(directory, fileName);
14
+ const fileStats = node_fs.statSync(filePath);
15
+ if (fileStats.isDirectory()) {
16
+ results.push(...walkTree(filePath, [...tree, fileName]));
17
+ } else {
18
+ results.push({
19
+ name: fileName,
20
+ path: directory,
21
+ rel: mergePaths(...tree, fileName)
22
+ });
23
+ }
24
+ }
25
+ return results;
26
+ }
27
+ function mergePaths(...paths) {
28
+ return `/${paths.map((path2) => path2.replace(/^\/|\/$/g, "")).filter((path2) => path2 !== "").join("/")}`;
29
+ }
30
+ async function generateRouter(routesDir, outputFile) {
31
+ const files = walkTree(
32
+ routesDir
33
+ );
34
+ const exports = await generateRoutes(files);
35
+ const importPaths = exports.map((x) => path.relative(path.dirname(outputFile), routesDir).concat(x.routePath));
36
+ const router = buildRouter(exports.map((x) => {
37
+ return {
38
+ exports: Object.keys(x.exports),
39
+ routePath: x.routePath
40
+ };
41
+ }));
42
+ let routerContent = `// This file is auto-generated
43
+
44
+ `;
45
+ routerContent += importPaths.map((x, i) => `import { ${Object.keys(exports[i].exports).join(", ")} } from "./${x}"`).join("\n");
46
+ routerContent += "\n\nexport const router = ";
47
+ routerContent += JSON.stringify(router, null, 2).replace(/"/g, "");
48
+ node_fs.writeFileSync(path.join(outputFile), routerContent);
49
+ }
50
+ function buildRoutePath(parsedFile) {
51
+ const directory = parsedFile.dir === parsedFile.root ? "" : parsedFile.dir;
52
+ const name = parsedFile.name.startsWith("index") ? parsedFile.name.replace("index", "") : `/${parsedFile.name}`;
53
+ return directory + name;
54
+ }
55
+ function buildRouter(inputs) {
56
+ const router = {};
57
+ for (const input of inputs) {
58
+ const segments = input.routePath.replace(/\{\w+\}/g, "").replace(/\/\//g, "/").split("/").filter(Boolean);
59
+ let current = router;
60
+ for (let i = 0; i < segments.length; i++) {
61
+ const segment = segments[i];
62
+ if (i === segments.length - 1) {
63
+ current[segment] = current[segment] || {};
64
+ for (const exp of input.exports) {
65
+ current[segment][exp] = `${exp}.route({ path: '${input.routePath}' })`;
66
+ }
67
+ } else {
68
+ current[segment] = current[segment] || {};
69
+ current = current[segment];
70
+ }
71
+ }
72
+ }
73
+ return simplifyRouter(router);
74
+ }
75
+ function simplifyRouter(router) {
76
+ const simplifiedRouter = {};
77
+ for (const key in router) {
78
+ if (typeof router[key] === "string") {
79
+ simplifiedRouter[key] = router[key];
80
+ } else if (typeof router[key] === "object" && isSingleLeaf(router[key])) {
81
+ const childKey = Object.keys(router[key])[0];
82
+ simplifiedRouter[key] = router[key][childKey];
83
+ } else {
84
+ simplifiedRouter[key] = simplifyRouter(router[key]);
85
+ }
86
+ }
87
+ return simplifiedRouter;
88
+ }
89
+ function isSingleLeaf(obj) {
90
+ const keys = Object.keys(obj);
91
+ return keys.length === 1 && typeof obj[keys[0]] === "string";
92
+ }
93
+ const isCjs = () => typeof module !== "undefined" && !!module?.exports;
94
+ const IS_ESM = !isCjs();
95
+ const MODULE_IMPORT_PREFIX = IS_ESM ? "file://" : "";
96
+ async function generateRoutes(files) {
97
+ const routes = [];
98
+ for (const file of files) {
99
+ const parsedFile = path__default.parse(file.rel);
100
+ const routePath = buildRoutePath(parsedFile);
101
+ const exports = await import(MODULE_IMPORT_PREFIX + path__default.join(file.path, file.name));
102
+ routes.push({
103
+ exports,
104
+ routePath
105
+ });
106
+ }
107
+ return routes;
108
+ }
109
+
110
+ exports.generateRouter = generateRouter;
@@ -0,0 +1,3 @@
1
+ declare function generateRouter(routesDir: string, outputFile: string): Promise<void>;
2
+
3
+ export { generateRouter };
@@ -0,0 +1,3 @@
1
+ declare function generateRouter(routesDir: string, outputFile: string): Promise<void>;
2
+
3
+ export { generateRouter };
@@ -0,0 +1,3 @@
1
+ declare function generateRouter(routesDir: string, outputFile: string): Promise<void>;
2
+
3
+ export { generateRouter };
package/dist/index.mjs ADDED
@@ -0,0 +1,104 @@
1
+ import { writeFileSync, readdirSync, statSync } from 'node:fs';
2
+ import path, { relative, dirname, join } from 'node:path';
3
+
4
+ function walkTree(directory, tree = []) {
5
+ const results = [];
6
+ for (const fileName of readdirSync(directory)) {
7
+ const filePath = join(directory, fileName);
8
+ const fileStats = statSync(filePath);
9
+ if (fileStats.isDirectory()) {
10
+ results.push(...walkTree(filePath, [...tree, fileName]));
11
+ } else {
12
+ results.push({
13
+ name: fileName,
14
+ path: directory,
15
+ rel: mergePaths(...tree, fileName)
16
+ });
17
+ }
18
+ }
19
+ return results;
20
+ }
21
+ function mergePaths(...paths) {
22
+ return `/${paths.map((path2) => path2.replace(/^\/|\/$/g, "")).filter((path2) => path2 !== "").join("/")}`;
23
+ }
24
+ async function generateRouter(routesDir, outputFile) {
25
+ const files = walkTree(
26
+ routesDir
27
+ );
28
+ const exports = await generateRoutes(files);
29
+ const importPaths = exports.map((x) => relative(dirname(outputFile), routesDir).concat(x.routePath));
30
+ const router = buildRouter(exports.map((x) => {
31
+ return {
32
+ exports: Object.keys(x.exports),
33
+ routePath: x.routePath
34
+ };
35
+ }));
36
+ let routerContent = `// This file is auto-generated
37
+
38
+ `;
39
+ routerContent += importPaths.map((x, i) => `import { ${Object.keys(exports[i].exports).join(", ")} } from "./${x}"`).join("\n");
40
+ routerContent += "\n\nexport const router = ";
41
+ routerContent += JSON.stringify(router, null, 2).replace(/"/g, "");
42
+ writeFileSync(join(outputFile), routerContent);
43
+ }
44
+ function buildRoutePath(parsedFile) {
45
+ const directory = parsedFile.dir === parsedFile.root ? "" : parsedFile.dir;
46
+ const name = parsedFile.name.startsWith("index") ? parsedFile.name.replace("index", "") : `/${parsedFile.name}`;
47
+ return directory + name;
48
+ }
49
+ function buildRouter(inputs) {
50
+ const router = {};
51
+ for (const input of inputs) {
52
+ const segments = input.routePath.replace(/\{\w+\}/g, "").replace(/\/\//g, "/").split("/").filter(Boolean);
53
+ let current = router;
54
+ for (let i = 0; i < segments.length; i++) {
55
+ const segment = segments[i];
56
+ if (i === segments.length - 1) {
57
+ current[segment] = current[segment] || {};
58
+ for (const exp of input.exports) {
59
+ current[segment][exp] = `${exp}.route({ path: '${input.routePath}' })`;
60
+ }
61
+ } else {
62
+ current[segment] = current[segment] || {};
63
+ current = current[segment];
64
+ }
65
+ }
66
+ }
67
+ return simplifyRouter(router);
68
+ }
69
+ function simplifyRouter(router) {
70
+ const simplifiedRouter = {};
71
+ for (const key in router) {
72
+ if (typeof router[key] === "string") {
73
+ simplifiedRouter[key] = router[key];
74
+ } else if (typeof router[key] === "object" && isSingleLeaf(router[key])) {
75
+ const childKey = Object.keys(router[key])[0];
76
+ simplifiedRouter[key] = router[key][childKey];
77
+ } else {
78
+ simplifiedRouter[key] = simplifyRouter(router[key]);
79
+ }
80
+ }
81
+ return simplifiedRouter;
82
+ }
83
+ function isSingleLeaf(obj) {
84
+ const keys = Object.keys(obj);
85
+ return keys.length === 1 && typeof obj[keys[0]] === "string";
86
+ }
87
+ const isCjs = () => typeof module !== "undefined" && !!module?.exports;
88
+ const IS_ESM = !isCjs();
89
+ const MODULE_IMPORT_PREFIX = IS_ESM ? "file://" : "";
90
+ async function generateRoutes(files) {
91
+ const routes = [];
92
+ for (const file of files) {
93
+ const parsedFile = path.parse(file.rel);
94
+ const routePath = buildRoutePath(parsedFile);
95
+ const exports = await import(MODULE_IMPORT_PREFIX + path.join(file.path, file.name));
96
+ routes.push({
97
+ exports,
98
+ routePath
99
+ });
100
+ }
101
+ return routes;
102
+ }
103
+
104
+ export { generateRouter };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "orpc-file-based-router",
3
+ "version": "0.0.1",
4
+ "description": "File-based router plugin for oRPC - automatically generate oRPC router from your file structure",
5
+ "author": "zeeeeby",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/zeeeeby/orpc-file-based-router",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/zeeeeby/orpc-file-based-router.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/zeeeeby/orpc-file-based-router/issues"
14
+ },
15
+ "keywords": [
16
+ "orpc",
17
+ "router",
18
+ "file-based-routing",
19
+ "plugin"
20
+ ],
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.mjs",
25
+ "require": "./dist/index.cjs"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "scripts": {
32
+ "build": "npm run type-check && unbuild",
33
+ "type-check": "tsc --noEmit"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.14.0",
37
+ "unbuild": "^3.5.0"
38
+ }
39
+ }