@potentiajs/core 0.1.0-preview.0
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/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/bin/potentia.js +14 -0
- package/examples/file-routing-basic/README.md +156 -0
- package/examples/file-routing-basic/app.js +16 -0
- package/examples/file-routing-basic/generate.js +21 -0
- package/examples/file-routing-basic/routes/health.js +3 -0
- package/examples/file-routing-basic/routes/index.js +3 -0
- package/examples/file-routing-basic/routes/users/[id].js +6 -0
- package/examples/file-routing-basic/routes/users/_routes.js +15 -0
- package/examples/form-rendering-basic/README.md +43 -0
- package/examples/form-rendering-basic/index.js +120 -0
- package/examples/full-flow-basic/README.md +141 -0
- package/examples/full-flow-basic/app.js +16 -0
- package/examples/full-flow-basic/form.js +205 -0
- package/examples/full-flow-basic/generate.js +21 -0
- package/examples/full-flow-basic/routes/index.js +5 -0
- package/examples/full-flow-basic/routes/users/[id].js +5 -0
- package/examples/full-flow-basic/routes/users/_routes.js +8 -0
- package/examples/full-flow-basic/routes/users/index.js +5 -0
- package/examples/full-flow-basic/routes/users/new.js +5 -0
- package/package.json +90 -0
- package/src/cli.js +407 -0
- package/src/dev/file-routing/diagnostics.js +24 -0
- package/src/dev/file-routing/generator.js +126 -0
- package/src/dev/file-routing/index.js +5 -0
- package/src/dev/file-routing/path-mapping.js +125 -0
- package/src/dev/file-routing/scanner.js +154 -0
- package/src/dev/file-routing/writer.js +100 -0
- package/src/file-routing.d.ts +36 -0
- package/src/file-routing.js +1 -0
- package/src/forms.d.ts +9 -0
- package/src/forms.js +334 -0
- package/src/index.d.ts +185 -0
- package/src/index.js +15 -0
- package/src/index.mjs +25 -0
- package/src/kernel/action-projection.js +35 -0
- package/src/kernel/action.js +172 -0
- package/src/kernel/app.js +144 -0
- package/src/kernel/context.js +37 -0
- package/src/kernel/contract-projection.js +129 -0
- package/src/kernel/contract.js +135 -0
- package/src/kernel/diagnostics.js +147 -0
- package/src/kernel/effect.js +119 -0
- package/src/kernel/error.js +93 -0
- package/src/kernel/form-projection.js +251 -0
- package/src/kernel/form-state.js +173 -0
- package/src/kernel/plugin.js +46 -0
- package/src/kernel/response.js +187 -0
- package/src/kernel/result.js +45 -0
- package/src/kernel/route-collection.js +148 -0
- package/src/kernel/route-id.js +11 -0
- package/src/kernel/route-manifest.js +129 -0
- package/src/kernel/route-projection.js +139 -0
- package/src/kernel/route.js +156 -0
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@potentiajs/core",
|
|
3
|
+
"version": "0.1.0-preview.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Experimental Bun-first JavaScript framework kernel for contract-driven routing, actions, and form metadata.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.js",
|
|
8
|
+
"module": "./src/index.js",
|
|
9
|
+
"types": "./src/index.d.ts",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public",
|
|
12
|
+
"tag": "preview"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"potentia": "./bin/potentia.js"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./src/index.d.ts",
|
|
20
|
+
"import": "./src/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./file-routing": {
|
|
23
|
+
"types": "./src/file-routing.d.ts",
|
|
24
|
+
"import": "./src/file-routing.js"
|
|
25
|
+
},
|
|
26
|
+
"./forms": {
|
|
27
|
+
"types": "./src/forms.d.ts",
|
|
28
|
+
"import": "./src/forms.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"bin/",
|
|
33
|
+
"src/",
|
|
34
|
+
"examples/file-routing-basic/",
|
|
35
|
+
"examples/full-flow-basic/",
|
|
36
|
+
"examples/form-rendering-basic/",
|
|
37
|
+
"README.md",
|
|
38
|
+
"CHANGELOG.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "bun test",
|
|
43
|
+
"check": "bun test",
|
|
44
|
+
"pack:dry": "npm pack --dry-run --json",
|
|
45
|
+
"check:preview": "bun run test && bun run check && npm pack --dry-run --json",
|
|
46
|
+
"check:release": "bun run check:preview",
|
|
47
|
+
"check:file-routing": "bun test tests/file-routing-*.test.js",
|
|
48
|
+
"generate:file-routes": "bun scripts/generate-file-routes.js"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"actions",
|
|
52
|
+
"bun",
|
|
53
|
+
"contracts",
|
|
54
|
+
"effects",
|
|
55
|
+
"form-projection",
|
|
56
|
+
"form-state",
|
|
57
|
+
"forms",
|
|
58
|
+
"framework",
|
|
59
|
+
"full-stack",
|
|
60
|
+
"functional-core",
|
|
61
|
+
"javascript",
|
|
62
|
+
"metadata",
|
|
63
|
+
"plain-javascript",
|
|
64
|
+
"router",
|
|
65
|
+
"routing",
|
|
66
|
+
"runtime-contracts",
|
|
67
|
+
"server-actions",
|
|
68
|
+
"server-framework",
|
|
69
|
+
"sigiljs",
|
|
70
|
+
"validation",
|
|
71
|
+
"web-framework"
|
|
72
|
+
],
|
|
73
|
+
"author": "Daniel Weipert",
|
|
74
|
+
"license": "MIT",
|
|
75
|
+
"repository": {
|
|
76
|
+
"type": "git",
|
|
77
|
+
"url": "git+https://github.com/antistructured/potentiajs.git"
|
|
78
|
+
},
|
|
79
|
+
"bugs": {
|
|
80
|
+
"url": "https://github.com/antistructured/potentiajs/issues"
|
|
81
|
+
},
|
|
82
|
+
"homepage": "https://github.com/antistructured/potentiajs#readme",
|
|
83
|
+
"engines": {
|
|
84
|
+
"bun": ">=1.3.0"
|
|
85
|
+
},
|
|
86
|
+
"dependencies": {
|
|
87
|
+
"@weipertda/sigiljs": "0.18.0"
|
|
88
|
+
},
|
|
89
|
+
"devDependencies": {}
|
|
90
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import { generateFileRoutes } from './file-routing.js';
|
|
4
|
+
import { generateFileRouteSource } from './dev/file-routing/writer.js';
|
|
5
|
+
|
|
6
|
+
export const USAGE = 'Usage: potentia routes <generate|check> [--root <dir>] [--out <file>] [--package <specifier>] [--cwd <dir>] [--json]';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_OPTIONS = {
|
|
9
|
+
root: 'routes',
|
|
10
|
+
out: '.potentia/routes.generated.js',
|
|
11
|
+
packageName: '@potentiajs/core',
|
|
12
|
+
cwd: null,
|
|
13
|
+
json: false
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const FLAG_MAP = new Map([
|
|
17
|
+
['--root', 'root'],
|
|
18
|
+
['--out', 'out'],
|
|
19
|
+
['--package', 'packageName'],
|
|
20
|
+
['--cwd', 'cwd']
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export function parseArgs(args = []) {
|
|
24
|
+
if (!Array.isArray(args)) {
|
|
25
|
+
return usageError('arguments must be an array', { json: false, command: null });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const json = args.includes('--json');
|
|
29
|
+
const [namespace, command, ...rest] = args;
|
|
30
|
+
const resolvedCommand = namespace === 'routes' && ['generate', 'check'].includes(command) ? `routes ${command}` : null;
|
|
31
|
+
|
|
32
|
+
if (namespace !== 'routes' || !['generate', 'check'].includes(command)) {
|
|
33
|
+
return usageError('Expected `potentia routes <generate|check>`', { json, command: resolvedCommand });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const options = { ...DEFAULT_OPTIONS, json };
|
|
37
|
+
|
|
38
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
39
|
+
const arg = rest[index];
|
|
40
|
+
|
|
41
|
+
if (arg === '--json') {
|
|
42
|
+
options.json = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const optionName = FLAG_MAP.get(arg);
|
|
47
|
+
|
|
48
|
+
if (!optionName) {
|
|
49
|
+
return usageError(arg?.startsWith('--') ? `unknown option: ${arg}` : `unexpected argument: ${arg}`, {
|
|
50
|
+
json,
|
|
51
|
+
command: resolvedCommand
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const value = rest[index + 1];
|
|
56
|
+
if (!value || value.startsWith('--')) {
|
|
57
|
+
return usageError(`${arg} requires a value`, { json, command: resolvedCommand });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
options[optionName] = value;
|
|
61
|
+
index += 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
command: resolvedCommand,
|
|
67
|
+
options: {
|
|
68
|
+
root: options.root,
|
|
69
|
+
out: options.out,
|
|
70
|
+
packageName: options.packageName,
|
|
71
|
+
cwd: options.cwd,
|
|
72
|
+
json: options.json
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function main(args = [], io = {}) {
|
|
78
|
+
const stdout = io.stdout || process.stdout;
|
|
79
|
+
const stderr = io.stderr || process.stderr;
|
|
80
|
+
const baseCwd = io.cwd || process.cwd();
|
|
81
|
+
const parsed = parseArgs(args);
|
|
82
|
+
|
|
83
|
+
if (!parsed.ok) {
|
|
84
|
+
if (parsed.json) {
|
|
85
|
+
writeJson(stdout, createInvalidUsageEnvelope(parsed.command));
|
|
86
|
+
return 2;
|
|
87
|
+
}
|
|
88
|
+
writeLine(stderr, USAGE);
|
|
89
|
+
writeLine(stderr, `Usage error: ${parsed.error}`);
|
|
90
|
+
return 2;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const cwd = parsed.options.cwd || baseCwd;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
if (parsed.command === 'routes check') {
|
|
97
|
+
return await runCheck(parsed.options, cwd, { stdout, stderr });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return await runGenerate(parsed.options, cwd, { stdout, stderr });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (parsed.options.json) {
|
|
103
|
+
writeJson(stdout, createJsonEnvelope({
|
|
104
|
+
ok: false,
|
|
105
|
+
command: parsed.command,
|
|
106
|
+
status: 'failed',
|
|
107
|
+
root: parsed.options.root,
|
|
108
|
+
output: parsed.options.out,
|
|
109
|
+
packageName: parsed.options.packageName,
|
|
110
|
+
routes: 0,
|
|
111
|
+
scopes: 0,
|
|
112
|
+
diagnostics: [{
|
|
113
|
+
code: 'POTENTIA_FILE_ROUTE_UNEXPECTED',
|
|
114
|
+
message: safeErrorMessage(error)
|
|
115
|
+
}]
|
|
116
|
+
}));
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
writeLine(stderr, 'File route generation failed:');
|
|
120
|
+
writeLine(stderr, `- POTENTIA_FILE_ROUTE_UNEXPECTED ${safeErrorMessage(error)}`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function checkFileRoutes(options = {}) {
|
|
126
|
+
const expected = generateFileRouteSource({
|
|
127
|
+
rootDir: options.rootDir,
|
|
128
|
+
outputFile: options.outputFile,
|
|
129
|
+
packageName: options.packageName,
|
|
130
|
+
cwd: options.cwd
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!expected.ok) {
|
|
134
|
+
return {
|
|
135
|
+
...expected,
|
|
136
|
+
ok: false,
|
|
137
|
+
status: 'failed'
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let currentSource;
|
|
142
|
+
try {
|
|
143
|
+
currentSource = await readFile(expected.outputFile, 'utf8');
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (isMissingFileError(error)) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
status: 'missing',
|
|
149
|
+
rootDir: expected.rootDir,
|
|
150
|
+
outputFile: expected.outputFile,
|
|
151
|
+
written: false,
|
|
152
|
+
routes: expected.routes,
|
|
153
|
+
scopes: expected.scopes,
|
|
154
|
+
diagnostics: [],
|
|
155
|
+
errors: []
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
status: 'failed',
|
|
161
|
+
rootDir: expected.rootDir,
|
|
162
|
+
outputFile: expected.outputFile,
|
|
163
|
+
written: false,
|
|
164
|
+
routes: expected.routes,
|
|
165
|
+
scopes: expected.scopes,
|
|
166
|
+
diagnostics: [{
|
|
167
|
+
code: 'POTENTIA_FILE_ROUTE_READ_FAILED',
|
|
168
|
+
message: 'Failed to read generated route module',
|
|
169
|
+
outputFile: expected.outputFile,
|
|
170
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
171
|
+
}],
|
|
172
|
+
errors: [{
|
|
173
|
+
code: 'POTENTIA_FILE_ROUTE_READ_FAILED',
|
|
174
|
+
message: 'Failed to read generated route module',
|
|
175
|
+
outputFile: expected.outputFile,
|
|
176
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
177
|
+
}]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const current = normalizeNewlines(currentSource);
|
|
182
|
+
const generated = normalizeNewlines(expected.source);
|
|
183
|
+
const currentStatus = current === generated ? 'current' : 'stale';
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
ok: currentStatus === 'current',
|
|
187
|
+
status: currentStatus,
|
|
188
|
+
rootDir: expected.rootDir,
|
|
189
|
+
outputFile: expected.outputFile,
|
|
190
|
+
written: false,
|
|
191
|
+
routes: expected.routes,
|
|
192
|
+
scopes: expected.scopes,
|
|
193
|
+
diagnostics: [],
|
|
194
|
+
errors: []
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function formatDiagnostic(diagnostic = {}) {
|
|
199
|
+
const code = diagnostic.code || 'POTENTIA_FILE_ROUTE_FAILED';
|
|
200
|
+
const location = diagnostic.filePath || diagnostic.relativePath || diagnostic.routePath || diagnostic.rootDir || diagnostic.outputFile || '';
|
|
201
|
+
const message = diagnostic.message || 'File route generation failed';
|
|
202
|
+
return location ? `- ${code} ${location} ${message}` : `- ${code} ${message}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function createJsonEnvelope({
|
|
206
|
+
ok,
|
|
207
|
+
command,
|
|
208
|
+
status,
|
|
209
|
+
root,
|
|
210
|
+
output,
|
|
211
|
+
packageName,
|
|
212
|
+
routes,
|
|
213
|
+
scopes,
|
|
214
|
+
diagnostics,
|
|
215
|
+
hint
|
|
216
|
+
}) {
|
|
217
|
+
const envelope = {
|
|
218
|
+
ok: Boolean(ok),
|
|
219
|
+
command,
|
|
220
|
+
status,
|
|
221
|
+
root,
|
|
222
|
+
output,
|
|
223
|
+
package: packageName,
|
|
224
|
+
routes: Number.isFinite(routes) ? routes : 0,
|
|
225
|
+
scopes: Number.isFinite(scopes) ? scopes : 0,
|
|
226
|
+
diagnostics: toJsonDiagnostics(diagnostics)
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
if (hint) {
|
|
230
|
+
envelope.hint = hint;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return envelope;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function toJsonDiagnostics(diagnostics = []) {
|
|
237
|
+
return diagnostics.map((diagnostic = {}) => {
|
|
238
|
+
const jsonDiagnostic = {
|
|
239
|
+
code: diagnostic.code || 'POTENTIA_CLI_ERROR',
|
|
240
|
+
message: diagnostic.message || 'Unknown diagnostic'
|
|
241
|
+
};
|
|
242
|
+
const path = diagnostic.path || diagnostic.filePath || diagnostic.relativePath || diagnostic.routePath || diagnostic.rootDir || diagnostic.outputFile;
|
|
243
|
+
if (path && !isAbsolutePath(path)) {
|
|
244
|
+
jsonDiagnostic.path = path;
|
|
245
|
+
}
|
|
246
|
+
return jsonDiagnostic;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function runGenerate(options, cwd, io) {
|
|
251
|
+
const result = await generateFileRoutes({
|
|
252
|
+
rootDir: options.root,
|
|
253
|
+
outputFile: options.out,
|
|
254
|
+
packageName: options.packageName,
|
|
255
|
+
cwd
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (!result.ok) {
|
|
259
|
+
if (options.json) {
|
|
260
|
+
writeJson(io.stdout, createJsonEnvelope({
|
|
261
|
+
ok: false,
|
|
262
|
+
command: 'routes generate',
|
|
263
|
+
status: 'failed',
|
|
264
|
+
root: options.root,
|
|
265
|
+
output: options.out,
|
|
266
|
+
packageName: options.packageName,
|
|
267
|
+
routes: result.routes,
|
|
268
|
+
scopes: result.scopes,
|
|
269
|
+
diagnostics: result.errors?.length ? result.errors : result.diagnostics
|
|
270
|
+
}));
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
writeDiagnostics(io.stderr, result);
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (options.json) {
|
|
278
|
+
writeJson(io.stdout, createJsonEnvelope({
|
|
279
|
+
ok: true,
|
|
280
|
+
command: 'routes generate',
|
|
281
|
+
status: 'generated',
|
|
282
|
+
root: options.root,
|
|
283
|
+
output: options.out,
|
|
284
|
+
packageName: options.packageName,
|
|
285
|
+
routes: result.routes,
|
|
286
|
+
scopes: result.scopes,
|
|
287
|
+
diagnostics: result.diagnostics
|
|
288
|
+
}));
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
writeLine(io.stdout, 'Generated file routes:');
|
|
293
|
+
writeLine(io.stdout, `- root: ${options.root}`);
|
|
294
|
+
writeLine(io.stdout, `- output: ${options.out}`);
|
|
295
|
+
writeLine(io.stdout, `- routes: ${result.routes}`);
|
|
296
|
+
writeLine(io.stdout, `- scopes: ${result.scopes}`);
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function runCheck(options, cwd, io) {
|
|
301
|
+
const result = await checkFileRoutes({
|
|
302
|
+
rootDir: options.root,
|
|
303
|
+
outputFile: options.out,
|
|
304
|
+
packageName: options.packageName,
|
|
305
|
+
cwd
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (options.json) {
|
|
309
|
+
const status = result.status || (result.ok ? 'current' : 'failed');
|
|
310
|
+
writeJson(io.stdout, createJsonEnvelope({
|
|
311
|
+
ok: status === 'current',
|
|
312
|
+
command: 'routes check',
|
|
313
|
+
status,
|
|
314
|
+
root: options.root,
|
|
315
|
+
output: options.out,
|
|
316
|
+
packageName: options.packageName,
|
|
317
|
+
routes: result.routes,
|
|
318
|
+
scopes: result.scopes,
|
|
319
|
+
diagnostics: result.errors?.length ? result.errors : result.diagnostics,
|
|
320
|
+
hint: ['missing', 'stale'].includes(status) ? 'Run potentia routes generate' : undefined
|
|
321
|
+
}));
|
|
322
|
+
return status === 'current' ? 0 : 1;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (result.status === 'current') {
|
|
326
|
+
writeLine(io.stdout, 'File routes are current:');
|
|
327
|
+
writeLine(io.stdout, `- root: ${options.root}`);
|
|
328
|
+
writeLine(io.stdout, `- output: ${options.out}`);
|
|
329
|
+
writeLine(io.stdout, `- routes: ${result.routes}`);
|
|
330
|
+
writeLine(io.stdout, `- scopes: ${result.scopes}`);
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (result.status === 'missing') {
|
|
335
|
+
writeLine(io.stderr, 'File routes are not generated:');
|
|
336
|
+
writeLine(io.stderr, `- output: ${options.out}`);
|
|
337
|
+
writeLine(io.stderr, '- run: potentia routes generate');
|
|
338
|
+
return 1;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (result.status === 'stale') {
|
|
342
|
+
writeLine(io.stderr, 'File routes are stale:');
|
|
343
|
+
writeLine(io.stderr, `- output: ${options.out}`);
|
|
344
|
+
writeLine(io.stderr, '- run: potentia routes generate');
|
|
345
|
+
return 1;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
writeDiagnostics(io.stderr, result);
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function writeDiagnostics(stderr, result) {
|
|
353
|
+
writeLine(stderr, 'File route generation failed:');
|
|
354
|
+
const diagnostics = result.errors?.length ? result.errors : result.diagnostics || [];
|
|
355
|
+
for (const diagnostic of diagnostics) {
|
|
356
|
+
writeLine(stderr, formatDiagnostic(diagnostic));
|
|
357
|
+
}
|
|
358
|
+
if (diagnostics.length === 0) {
|
|
359
|
+
writeLine(stderr, '- POTENTIA_FILE_ROUTE_FAILED File route generation failed');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function usageError(error, options = {}) {
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
error,
|
|
367
|
+
usage: USAGE,
|
|
368
|
+
json: Boolean(options.json),
|
|
369
|
+
command: options.command ?? null
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function createInvalidUsageEnvelope(command) {
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
command,
|
|
377
|
+
status: 'invalid-usage',
|
|
378
|
+
diagnostics: [{
|
|
379
|
+
code: 'POTENTIA_CLI_INVALID_USAGE',
|
|
380
|
+
message: USAGE
|
|
381
|
+
}]
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function writeJson(stream, value) {
|
|
386
|
+
writeLine(stream, JSON.stringify(value));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function writeLine(stream, line) {
|
|
390
|
+
stream.write(`${line}\n`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function safeErrorMessage(error) {
|
|
394
|
+
return error instanceof Error ? error.message : String(error);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function isMissingFileError(error) {
|
|
398
|
+
return error?.code === 'ENOENT';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function normalizeNewlines(value) {
|
|
402
|
+
return String(value).replace(/\r\n/g, '\n');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isAbsolutePath(value) {
|
|
406
|
+
return String(value).startsWith('/') || /^[A-Za-z]:[\\/]/.test(String(value));
|
|
407
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FILE_ROUTE_DIAGNOSTIC_CODES } from './path-mapping.js';
|
|
2
|
+
|
|
3
|
+
export function routeCollisionDiagnostics(routes) {
|
|
4
|
+
const byPath = new Map();
|
|
5
|
+
|
|
6
|
+
for (const route of routes) {
|
|
7
|
+
const existing = byPath.get(route.routePath) || [];
|
|
8
|
+
existing.push(route);
|
|
9
|
+
byPath.set(route.routePath, existing);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const collisions = [];
|
|
13
|
+
for (const [routePath, entries] of byPath.entries()) {
|
|
14
|
+
if (entries.length < 2) continue;
|
|
15
|
+
collisions.push({
|
|
16
|
+
code: FILE_ROUTE_DIAGNOSTIC_CODES.COLLISION,
|
|
17
|
+
message: 'Multiple route files map to the same route path',
|
|
18
|
+
routePath: routePath,
|
|
19
|
+
files: entries.map((entry) => entry.filePath).sort()
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return collisions.sort((a, b) => a.routePath.localeCompare(b.routePath));
|
|
24
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { normalizePath } from './path-mapping.js';
|
|
4
|
+
|
|
5
|
+
export function generateRouteModule(scanResult, options = {}) {
|
|
6
|
+
const outputPath = normalizePath(options.outputPath || '.potentia/routes.generated.js');
|
|
7
|
+
const errors = Array.isArray(scanResult?.errors) ? scanResult.errors : [];
|
|
8
|
+
|
|
9
|
+
if (!scanResult || scanResult.ok === false || errors.length > 0) {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
source: '',
|
|
13
|
+
outputPath: outputPath,
|
|
14
|
+
imports: [],
|
|
15
|
+
errors: errors.length > 0 ? errors : [{ code: 'POTENTIA_FILE_ROUTE_INVALID_SCAN', message: 'Scan result is invalid' }]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const imports = buildImports(scanResult, outputPath);
|
|
20
|
+
const source = renderModule(scanResult, imports, options.packageName || '@potentiajs/core');
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
ok: true,
|
|
24
|
+
source: source,
|
|
25
|
+
outputPath: outputPath,
|
|
26
|
+
imports: imports,
|
|
27
|
+
errors: []
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildImports(scanResult, outputPath) {
|
|
32
|
+
const imports = [];
|
|
33
|
+
const outputDir = normalizePath(path.posix.dirname(outputPath));
|
|
34
|
+
|
|
35
|
+
scanResult.routes.forEach((route, index) => {
|
|
36
|
+
imports.push({
|
|
37
|
+
kind: 'route',
|
|
38
|
+
importName: `route${index}`,
|
|
39
|
+
importPath: relativeImport(outputDir, route.filePath),
|
|
40
|
+
filePath: route.filePath,
|
|
41
|
+
routePath: route.routePath,
|
|
42
|
+
segments: route.segments
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
scanResult.scopes.forEach((scope, index) => {
|
|
47
|
+
imports.push({
|
|
48
|
+
kind: 'scope',
|
|
49
|
+
importName: `scope${index}`,
|
|
50
|
+
importPath: relativeImport(outputDir, scope.filePath),
|
|
51
|
+
filePath: scope.filePath,
|
|
52
|
+
routePath: scope.routePath,
|
|
53
|
+
segments: scope.segments
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return imports;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderModule(scanResult, imports, packageName) {
|
|
61
|
+
const routeImports = imports.filter((entry) => entry.kind === 'route');
|
|
62
|
+
const scopeImports = imports.filter((entry) => entry.kind === 'scope');
|
|
63
|
+
const lines = [
|
|
64
|
+
'// Generated by PotentiaJS. Do not edit by hand.',
|
|
65
|
+
`import { createRoutes, mount } from '${packageName}';`,
|
|
66
|
+
''
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
for (const entry of imports) {
|
|
70
|
+
lines.push(`import ${entry.importName} from '${entry.importPath}';`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
lines.push('', 'export default createRoutes({', ' routes: [');
|
|
74
|
+
|
|
75
|
+
const routeEntries = routeImports.map((entry) => ({
|
|
76
|
+
...scanResult.routes.find((route) => route.filePath === entry.filePath),
|
|
77
|
+
importName: entry.importName
|
|
78
|
+
}));
|
|
79
|
+
const scopeEntries = scopeImports.map((entry) => ({
|
|
80
|
+
...scanResult.scopes.find((scope) => scope.filePath === entry.filePath),
|
|
81
|
+
importName: entry.importName
|
|
82
|
+
}));
|
|
83
|
+
const grouped = routeEntries.filter((route) => shouldGroup(route, routeEntries, scopeEntries));
|
|
84
|
+
const direct = routeEntries.filter((route) => !grouped.includes(route));
|
|
85
|
+
|
|
86
|
+
for (const route of direct) {
|
|
87
|
+
lines.push(` ${route.importName},`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const groupNames = Array.from(new Set(grouped.map((route) => route.segments[0]))).sort();
|
|
91
|
+
for (const groupName of groupNames) {
|
|
92
|
+
const routes = grouped.filter((route) => route.segments[0] === groupName);
|
|
93
|
+
const scope = scopeEntries.find((entry) => entry.segments[0] === groupName && entry.segments.length === 1);
|
|
94
|
+
const prefix = `/${groupName}`;
|
|
95
|
+
lines.push(' mount(createRoutes({');
|
|
96
|
+
lines.push(` prefix: '${prefix}',`);
|
|
97
|
+
if (scope) {
|
|
98
|
+
lines.push(` hooks: ${scope.importName}.hooks,`);
|
|
99
|
+
lines.push(` contracts: ${scope.importName}.contracts,`);
|
|
100
|
+
lines.push(` meta: ${scope.importName}.meta,`);
|
|
101
|
+
}
|
|
102
|
+
lines.push(' routes: [');
|
|
103
|
+
for (const route of routes) {
|
|
104
|
+
lines.push(` ${route.importName},`);
|
|
105
|
+
}
|
|
106
|
+
lines.push(' ]');
|
|
107
|
+
lines.push(' })),');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
lines.push(' ]', '});', '');
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function shouldGroup(route, allRoutes, scopes) {
|
|
115
|
+
if (route.segments.length < 2 && route.routePath !== `/${route.segments[0]}`) return false;
|
|
116
|
+
const first = route.segments[0];
|
|
117
|
+
if (!first) return false;
|
|
118
|
+
if (scopes.some((scope) => scope.segments[0] === first && scope.segments.length === 1)) return true;
|
|
119
|
+
return allRoutes.filter((candidate) => candidate.segments[0] === first).length > 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function relativeImport(fromDir, filePath) {
|
|
123
|
+
const relative = path.posix.relative(fromDir, normalizePath(filePath));
|
|
124
|
+
const normalized = relative.startsWith('.') ? relative : `./${relative}`;
|
|
125
|
+
return normalized.replaceAll('\\\\', '/');
|
|
126
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { routeCollisionDiagnostics } from './diagnostics.js';
|
|
2
|
+
export { generateRouteModule } from './generator.js';
|
|
3
|
+
export { FILE_ROUTE_DIAGNOSTIC_CODES, mapRouteFile, normalizePath } from './path-mapping.js';
|
|
4
|
+
export { scanRouteTree } from './scanner.js';
|
|
5
|
+
export { generateFileRoutes } from './writer.js';
|