@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +441 -0
  4. package/bin/potentia.js +14 -0
  5. package/examples/file-routing-basic/README.md +156 -0
  6. package/examples/file-routing-basic/app.js +16 -0
  7. package/examples/file-routing-basic/generate.js +21 -0
  8. package/examples/file-routing-basic/routes/health.js +3 -0
  9. package/examples/file-routing-basic/routes/index.js +3 -0
  10. package/examples/file-routing-basic/routes/users/[id].js +6 -0
  11. package/examples/file-routing-basic/routes/users/_routes.js +15 -0
  12. package/examples/form-rendering-basic/README.md +43 -0
  13. package/examples/form-rendering-basic/index.js +120 -0
  14. package/examples/full-flow-basic/README.md +141 -0
  15. package/examples/full-flow-basic/app.js +16 -0
  16. package/examples/full-flow-basic/form.js +205 -0
  17. package/examples/full-flow-basic/generate.js +21 -0
  18. package/examples/full-flow-basic/routes/index.js +5 -0
  19. package/examples/full-flow-basic/routes/users/[id].js +5 -0
  20. package/examples/full-flow-basic/routes/users/_routes.js +8 -0
  21. package/examples/full-flow-basic/routes/users/index.js +5 -0
  22. package/examples/full-flow-basic/routes/users/new.js +5 -0
  23. package/package.json +90 -0
  24. package/src/cli.js +407 -0
  25. package/src/dev/file-routing/diagnostics.js +24 -0
  26. package/src/dev/file-routing/generator.js +126 -0
  27. package/src/dev/file-routing/index.js +5 -0
  28. package/src/dev/file-routing/path-mapping.js +125 -0
  29. package/src/dev/file-routing/scanner.js +154 -0
  30. package/src/dev/file-routing/writer.js +100 -0
  31. package/src/file-routing.d.ts +36 -0
  32. package/src/file-routing.js +1 -0
  33. package/src/forms.d.ts +9 -0
  34. package/src/forms.js +334 -0
  35. package/src/index.d.ts +185 -0
  36. package/src/index.js +15 -0
  37. package/src/index.mjs +25 -0
  38. package/src/kernel/action-projection.js +35 -0
  39. package/src/kernel/action.js +172 -0
  40. package/src/kernel/app.js +144 -0
  41. package/src/kernel/context.js +37 -0
  42. package/src/kernel/contract-projection.js +129 -0
  43. package/src/kernel/contract.js +135 -0
  44. package/src/kernel/diagnostics.js +147 -0
  45. package/src/kernel/effect.js +119 -0
  46. package/src/kernel/error.js +93 -0
  47. package/src/kernel/form-projection.js +251 -0
  48. package/src/kernel/form-state.js +173 -0
  49. package/src/kernel/plugin.js +46 -0
  50. package/src/kernel/response.js +187 -0
  51. package/src/kernel/result.js +45 -0
  52. package/src/kernel/route-collection.js +148 -0
  53. package/src/kernel/route-id.js +11 -0
  54. package/src/kernel/route-manifest.js +129 -0
  55. package/src/kernel/route-projection.js +139 -0
  56. 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';