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 +78 -0
- package/dist/index.cjs +110 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.mjs +104 -0
- package/package.json +39 -0
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;
|
package/dist/index.d.cts
ADDED
package/dist/index.d.mts
ADDED
package/dist/index.d.ts
ADDED
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
|
+
}
|