@agentuity/cli 1.0.39 → 1.0.41
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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -1
- package/dist/cli.js.map +1 -1
- package/dist/cmd/build/app-router-detector.d.ts +42 -0
- package/dist/cmd/build/app-router-detector.d.ts.map +1 -0
- package/dist/cmd/build/app-router-detector.js +253 -0
- package/dist/cmd/build/app-router-detector.js.map +1 -0
- package/dist/cmd/build/ast.d.ts +11 -1
- package/dist/cmd/build/ast.d.ts.map +1 -1
- package/dist/cmd/build/ast.js +273 -29
- package/dist/cmd/build/ast.js.map +1 -1
- package/dist/cmd/build/entry-generator.d.ts.map +1 -1
- package/dist/cmd/build/entry-generator.js +23 -16
- package/dist/cmd/build/entry-generator.js.map +1 -1
- package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
- package/dist/cmd/build/vite/registry-generator.js +37 -13
- package/dist/cmd/build/vite/registry-generator.js.map +1 -1
- package/dist/cmd/build/vite/route-discovery.d.ts +17 -3
- package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
- package/dist/cmd/build/vite/route-discovery.js +91 -3
- package/dist/cmd/build/vite/route-discovery.js.map +1 -1
- package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
- package/dist/cmd/build/vite-bundler.js +3 -0
- package/dist/cmd/build/vite-bundler.js.map +1 -1
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/index.js +30 -0
- package/dist/cmd/dev/index.js.map +1 -1
- package/dist/cmd/project/import.d.ts.map +1 -1
- package/dist/cmd/project/import.js +11 -1
- package/dist/cmd/project/import.js.map +1 -1
- package/dist/cmd/project/reconcile.d.ts +8 -0
- package/dist/cmd/project/reconcile.d.ts.map +1 -1
- package/dist/cmd/project/reconcile.js +150 -61
- package/dist/cmd/project/reconcile.js.map +1 -1
- package/dist/cmd/project/remote-import.d.ts +2 -0
- package/dist/cmd/project/remote-import.d.ts.map +1 -1
- package/dist/cmd/project/remote-import.js +2 -2
- package/dist/cmd/project/remote-import.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/schema-parser.d.ts.map +1 -1
- package/dist/schema-parser.js +6 -2
- package/dist/schema-parser.js.map +1 -1
- package/dist/utils/route-migration.d.ts +61 -0
- package/dist/utils/route-migration.d.ts.map +1 -0
- package/dist/utils/route-migration.js +662 -0
- package/dist/utils/route-migration.js.map +1 -0
- package/package.json +6 -6
- package/src/cli.ts +9 -1
- package/src/cmd/build/app-router-detector.ts +350 -0
- package/src/cmd/build/ast.ts +339 -36
- package/src/cmd/build/entry-generator.ts +23 -16
- package/src/cmd/build/vite/registry-generator.ts +38 -13
- package/src/cmd/build/vite/route-discovery.ts +151 -3
- package/src/cmd/build/vite-bundler.ts +4 -0
- package/src/cmd/dev/index.ts +34 -0
- package/src/cmd/project/import.ts +11 -1
- package/src/cmd/project/reconcile.ts +147 -61
- package/src/cmd/project/remote-import.ts +4 -2
- package/src/config.ts +8 -1
- package/src/schema-parser.ts +8 -2
- package/src/utils/route-migration.ts +793 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Router Detector
|
|
3
|
+
*
|
|
4
|
+
* Parses the user's `src/app.ts` to detect whether they pass a `router` property
|
|
5
|
+
* to `createApp()`. If detected, resolves the router variable(s) to their import
|
|
6
|
+
* sources and mount paths.
|
|
7
|
+
*
|
|
8
|
+
* This allows the build tooling to derive route metadata from the actual code-based
|
|
9
|
+
* route tree instead of relying on filesystem-based discovery.
|
|
10
|
+
*/
|
|
11
|
+
import * as acornLoose from 'acorn-loose';
|
|
12
|
+
import { join, dirname, resolve } from 'node:path';
|
|
13
|
+
import { existsSync, statSync } from 'node:fs';
|
|
14
|
+
/**
|
|
15
|
+
* Resolve an import path to an actual file on disk.
|
|
16
|
+
* Tries the path as-is, then with common extensions.
|
|
17
|
+
*/
|
|
18
|
+
function resolveImportFile(fromDir, importPath) {
|
|
19
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
20
|
+
return null; // Package import — can't resolve
|
|
21
|
+
}
|
|
22
|
+
const basePath = resolve(fromDir, importPath);
|
|
23
|
+
const extensions = ['.ts', '.tsx', '/index.ts', '/index.tsx'];
|
|
24
|
+
if (existsSync(basePath)) {
|
|
25
|
+
try {
|
|
26
|
+
if (statSync(basePath).isFile())
|
|
27
|
+
return basePath;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// ignore
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const ext of extensions) {
|
|
34
|
+
const candidate = basePath + ext;
|
|
35
|
+
if (existsSync(candidate)) {
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Extract the `router` property value from a `createApp()` call's argument object.
|
|
43
|
+
*
|
|
44
|
+
* Handles three forms:
|
|
45
|
+
* - `createApp({ router: myVar })` → plain Hono, default /api mount
|
|
46
|
+
* - `createApp({ router: { path: '/v1', router: myVar } })` → single RouteMount
|
|
47
|
+
* - `createApp({ router: [{ path: '/v1', router: v1 }, ...] })` → array of RouteMounts
|
|
48
|
+
*/
|
|
49
|
+
function extractRouterFromCreateApp(callNode) {
|
|
50
|
+
// createApp() must have at least one argument (the config object)
|
|
51
|
+
if (!callNode.arguments || callNode.arguments.length === 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const configArg = callNode.arguments[0];
|
|
55
|
+
if (configArg.type !== 'ObjectExpression') {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
// Find the `router` property
|
|
59
|
+
const routerProp = configArg.properties?.find((p) => p.type === 'Property' && p.key?.type === 'Identifier' && p.key?.name === 'router');
|
|
60
|
+
if (!routerProp) {
|
|
61
|
+
return null; // No router property — file-based routing
|
|
62
|
+
}
|
|
63
|
+
const routerValue = routerProp.value;
|
|
64
|
+
// Form 1: Plain Hono variable → createApp({ router: myRouter })
|
|
65
|
+
if (routerValue.type === 'Identifier') {
|
|
66
|
+
return [{ path: '/api', varName: routerValue.name }];
|
|
67
|
+
}
|
|
68
|
+
// Form 2: Single RouteMount → createApp({ router: { path: '/v1', router: myRouter } })
|
|
69
|
+
if (routerValue.type === 'ObjectExpression') {
|
|
70
|
+
const mount = extractRouteMountFromObject(routerValue);
|
|
71
|
+
return mount ? [mount] : null;
|
|
72
|
+
}
|
|
73
|
+
// Form 3: Array of RouteMounts → createApp({ router: [{ path: '/v1', router: v1 }, ...] })
|
|
74
|
+
if (routerValue.type === 'ArrayExpression') {
|
|
75
|
+
const mounts = [];
|
|
76
|
+
for (const element of routerValue.elements || []) {
|
|
77
|
+
if (element?.type === 'ObjectExpression') {
|
|
78
|
+
const mount = extractRouteMountFromObject(element);
|
|
79
|
+
if (mount)
|
|
80
|
+
mounts.push(mount);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return mounts.length > 0 ? mounts : null;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Extract { path, router } from an object literal AST node.
|
|
89
|
+
*/
|
|
90
|
+
function extractRouteMountFromObject(objNode) {
|
|
91
|
+
let path;
|
|
92
|
+
let varName;
|
|
93
|
+
for (const prop of objNode.properties || []) {
|
|
94
|
+
if (prop.type !== 'Property' || prop.key?.type !== 'Identifier')
|
|
95
|
+
continue;
|
|
96
|
+
if (prop.key.name === 'path' && prop.value?.type === 'Literal') {
|
|
97
|
+
path = String(prop.value.value);
|
|
98
|
+
}
|
|
99
|
+
if (prop.key.name === 'router' && prop.value?.type === 'Identifier') {
|
|
100
|
+
varName = prop.value.name;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return path && varName ? { path, varName } : null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Detect whether `src/app.ts` uses `createApp({ router })`.
|
|
107
|
+
*
|
|
108
|
+
* Parses the file with acorn-loose, finds `createApp()` calls,
|
|
109
|
+
* and resolves router variables to their import source files.
|
|
110
|
+
*
|
|
111
|
+
* Returns `{ detected: false, mounts: [] }` when:
|
|
112
|
+
* - `src/app.ts` doesn't exist
|
|
113
|
+
* - `createApp()` is called without a `router` property
|
|
114
|
+
* - Router variables can't be resolved to files
|
|
115
|
+
*/
|
|
116
|
+
export async function detectExplicitRouter(rootDir, logger) {
|
|
117
|
+
const noDetection = { detected: false, mounts: [] };
|
|
118
|
+
// Look for app.ts in src/ (standard location)
|
|
119
|
+
const appFile = join(rootDir, 'src', 'app.ts');
|
|
120
|
+
if (!existsSync(appFile)) {
|
|
121
|
+
// Also try root-level app.ts
|
|
122
|
+
const rootAppFile = join(rootDir, 'app.ts');
|
|
123
|
+
if (!existsSync(rootAppFile)) {
|
|
124
|
+
logger.trace('[router-detect] No app.ts found');
|
|
125
|
+
return noDetection;
|
|
126
|
+
}
|
|
127
|
+
return detectInFile(rootAppFile, logger);
|
|
128
|
+
}
|
|
129
|
+
return detectInFile(appFile, logger);
|
|
130
|
+
}
|
|
131
|
+
async function detectInFile(appFile, logger) {
|
|
132
|
+
const noDetection = { detected: false, mounts: [] };
|
|
133
|
+
const appDir = dirname(appFile);
|
|
134
|
+
try {
|
|
135
|
+
const source = await Bun.file(appFile).text();
|
|
136
|
+
const transpiler = new Bun.Transpiler({ loader: 'ts', target: 'bun' });
|
|
137
|
+
const contents = transpiler.transformSync(source);
|
|
138
|
+
// Quick check: skip AST parsing if createApp is not called with router
|
|
139
|
+
if (!contents.includes('createApp') || !contents.includes('router')) {
|
|
140
|
+
logger.trace('[router-detect] No createApp + router pattern found in %s', appFile);
|
|
141
|
+
return noDetection;
|
|
142
|
+
}
|
|
143
|
+
const ast = acornLoose.parse(contents, {
|
|
144
|
+
locations: true,
|
|
145
|
+
ecmaVersion: 'latest',
|
|
146
|
+
sourceType: 'module',
|
|
147
|
+
});
|
|
148
|
+
// Build import map: variable name → import path
|
|
149
|
+
const importMap = new Map();
|
|
150
|
+
for (const node of ast.body || []) {
|
|
151
|
+
if (node.type === 'ImportDeclaration' && node.source?.value) {
|
|
152
|
+
for (const spec of node.specifiers || []) {
|
|
153
|
+
if (spec.local?.name) {
|
|
154
|
+
importMap.set(spec.local.name, String(node.source.value));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Walk all statements looking for createApp() calls
|
|
160
|
+
const routerMounts = findCreateAppRouterCalls(ast, importMap);
|
|
161
|
+
if (!routerMounts || routerMounts.length === 0) {
|
|
162
|
+
logger.trace('[router-detect] createApp() found but no router property');
|
|
163
|
+
return noDetection;
|
|
164
|
+
}
|
|
165
|
+
// Resolve each router variable to its file
|
|
166
|
+
const mounts = [];
|
|
167
|
+
for (const { path, varName } of routerMounts) {
|
|
168
|
+
const importPath = importMap.get(varName);
|
|
169
|
+
if (!importPath) {
|
|
170
|
+
logger.debug('[router-detect] Router variable %s is not imported — may be defined locally', varName);
|
|
171
|
+
// Could be defined in the same file — skip for now
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const resolvedFile = resolveImportFile(appDir, importPath);
|
|
175
|
+
if (!resolvedFile) {
|
|
176
|
+
logger.warn('[router-detect] Could not resolve import %s for router variable %s', importPath, varName);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
logger.trace('[router-detect] Found router mount: %s → %s (%s)', path, varName, resolvedFile);
|
|
180
|
+
mounts.push({ path, routerFile: resolvedFile });
|
|
181
|
+
}
|
|
182
|
+
if (mounts.length === 0) {
|
|
183
|
+
logger.debug('[router-detect] Router variables found but none could be resolved to files');
|
|
184
|
+
return noDetection;
|
|
185
|
+
}
|
|
186
|
+
logger.debug('[router-detect] Detected %d explicit router mount(s) in %s', mounts.length, appFile);
|
|
187
|
+
return { detected: true, mounts };
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
logger.warn('[router-detect] Failed to parse %s: %s', appFile, error instanceof Error ? error.message : String(error));
|
|
191
|
+
return noDetection;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Walk the AST looking for `createApp({ router: ... })` calls.
|
|
196
|
+
* Handles:
|
|
197
|
+
* - `createApp({ router })` (top-level expression)
|
|
198
|
+
* - `const app = await createApp({ router })` (variable declaration)
|
|
199
|
+
* - `export const app = await createApp({ router })` (exported)
|
|
200
|
+
*/
|
|
201
|
+
function findCreateAppRouterCalls(ast, importMap) {
|
|
202
|
+
for (const node of ast.body || []) {
|
|
203
|
+
// Check expression statements: createApp({ router })
|
|
204
|
+
if (node.type === 'ExpressionStatement') {
|
|
205
|
+
const result = checkForCreateAppCall(node.expression, importMap);
|
|
206
|
+
if (result)
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
// Check variable declarations: const app = await createApp({ router })
|
|
210
|
+
if (node.type === 'VariableDeclaration') {
|
|
211
|
+
for (const decl of node.declarations || []) {
|
|
212
|
+
if (decl.init) {
|
|
213
|
+
const result = checkForCreateAppCall(decl.init, importMap);
|
|
214
|
+
if (result)
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Check exports: export const app = await createApp({ router })
|
|
220
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
221
|
+
if (node.declaration.type === 'VariableDeclaration') {
|
|
222
|
+
for (const decl of node.declaration.declarations || []) {
|
|
223
|
+
if (decl.init) {
|
|
224
|
+
const result = checkForCreateAppCall(decl.init, importMap);
|
|
225
|
+
if (result)
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Check if an expression node is a `createApp({ router })` call.
|
|
236
|
+
* Unwraps `await` expressions.
|
|
237
|
+
*/
|
|
238
|
+
function checkForCreateAppCall(expr, importMap) {
|
|
239
|
+
if (!expr)
|
|
240
|
+
return null;
|
|
241
|
+
// Unwrap AwaitExpression: await createApp(...)
|
|
242
|
+
if (expr.type === 'AwaitExpression' && expr.argument) {
|
|
243
|
+
return checkForCreateAppCall(expr.argument, importMap);
|
|
244
|
+
}
|
|
245
|
+
// Check for createApp({ router })
|
|
246
|
+
if (expr.type === 'CallExpression' &&
|
|
247
|
+
expr.callee?.type === 'Identifier' &&
|
|
248
|
+
expr.callee?.name === 'createApp') {
|
|
249
|
+
return extractRouterFromCreateApp(expr);
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=app-router-detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-router-detector.js","sourceRoot":"","sources":["../../../src/cmd/build/app-router-detector.ts"],"names":[],"mappings":"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"}
|
package/dist/cmd/build/ast.d.ts
CHANGED
|
@@ -13,7 +13,17 @@ export declare function parseEvalMetadata(rootDir: string, filename: string, con
|
|
|
13
13
|
}>
|
|
14
14
|
]>;
|
|
15
15
|
export declare function parseAgentMetadata(rootDir: string, filename: string, contents: string, projectId: string, deploymentId: string): Promise<[string, Map<string, string>] | undefined>;
|
|
16
|
-
export
|
|
16
|
+
export interface ParseRouteOptions {
|
|
17
|
+
visitedFiles?: Set<string>;
|
|
18
|
+
mountedSubrouters?: Set<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Explicit mount prefix for this router file, derived from code-based routing
|
|
21
|
+
* (e.g., from `createApp({ router })` or `.route()` calls).
|
|
22
|
+
* When provided, overrides the filesystem-derived mount path.
|
|
23
|
+
*/
|
|
24
|
+
mountPrefix?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function parseRoute(rootDir: string, filename: string, projectId: string, deploymentId: string, visitedFilesOrOptions?: Set<string> | ParseRouteOptions, mountedSubrouters?: Set<string>): Promise<BuildMetadata['routes']>;
|
|
17
27
|
/**
|
|
18
28
|
* Result of workbench analysis
|
|
19
29
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../../src/cmd/build/ast.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAqIxE,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEpF;AAoTD,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,MAAM,EAChB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC9C,OAAO,CACT;IACC,MAAM;IACN,KAAK,CAAC;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACF,CACD,CAyNA;AAID,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GAClB,OAAO,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC,CAqPpD;
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../../src/cmd/build/ast.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAqIxE,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEpF;AAoTD,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,MAAM,EAChB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC9C,OAAO,CACT;IACC,MAAM;IACN,KAAK,CAAC;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACF,CACD,CAyNA;AAID,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GAClB,OAAO,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC,CAqPpD;AA4pBD,MAAM,WAAW,iBAAiB;IACjC,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAChC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,UAAU,CAC/B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,qBAAqB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,iBAAiB,EACvD,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAC7B,OAAO,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAo+BlC;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAoCjF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,OAAO,CA+BvF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgOlE;AA+DD;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC3C,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CA8LlB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAsLnE;AA+BD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0EhE"}
|
package/dist/cmd/build/ast.js
CHANGED
|
@@ -1068,9 +1068,96 @@ function resolveImportPath(fromDir, importPath) {
|
|
|
1068
1068
|
}
|
|
1069
1069
|
return null;
|
|
1070
1070
|
}
|
|
1071
|
-
|
|
1071
|
+
/**
|
|
1072
|
+
* Check if a CallExpression is a chained router initializer.
|
|
1073
|
+
* Walks up the callee chain looking for createRouter() or new Hono() at the root.
|
|
1074
|
+
*
|
|
1075
|
+
* Example AST for `createRouter().get('/foo', handler).post('/bar', handler)`:
|
|
1076
|
+
* ```
|
|
1077
|
+
* CallExpression (.post)
|
|
1078
|
+
* callee: MemberExpression
|
|
1079
|
+
* object: CallExpression (.get)
|
|
1080
|
+
* callee: MemberExpression
|
|
1081
|
+
* object: CallExpression (createRouter)
|
|
1082
|
+
* property: "post"
|
|
1083
|
+
* ```
|
|
1084
|
+
*/
|
|
1085
|
+
function isChainedRouterInit(node) {
|
|
1086
|
+
let current = node;
|
|
1087
|
+
// Walk down the chain: each link is CallExpression → MemberExpression → CallExpression
|
|
1088
|
+
while (current.type === 'CallExpression') {
|
|
1089
|
+
const callee = current.callee;
|
|
1090
|
+
if (!callee)
|
|
1091
|
+
return false;
|
|
1092
|
+
// Direct createRouter()
|
|
1093
|
+
if (callee.type === 'Identifier' && callee.name === 'createRouter')
|
|
1094
|
+
return true;
|
|
1095
|
+
// Chained: .method() → MemberExpression
|
|
1096
|
+
if (callee.type === 'MemberExpression' && callee.object) {
|
|
1097
|
+
current = callee.object;
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
// Check if we landed on createRouter() or new Hono()
|
|
1103
|
+
if (current.type === 'CallExpression') {
|
|
1104
|
+
const callee = current.callee;
|
|
1105
|
+
if (callee?.type === 'Identifier' && callee.name === 'createRouter')
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
if (current.type === 'NewExpression') {
|
|
1109
|
+
const callee = current.callee;
|
|
1110
|
+
if (callee?.type === 'Identifier' && callee.name === 'Hono')
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Flatten a chained call expression into individual method calls.
|
|
1117
|
+
*
|
|
1118
|
+
* Given `createRouter().get('/a', h1).post('/b', h2).route('/c', sub)`, returns:
|
|
1119
|
+
* ```
|
|
1120
|
+
* [
|
|
1121
|
+
* { method: 'get', arguments: ['/a', h1] },
|
|
1122
|
+
* { method: 'post', arguments: ['/b', h2] },
|
|
1123
|
+
* { method: 'route', arguments: ['/c', sub] },
|
|
1124
|
+
* ]
|
|
1125
|
+
* ```
|
|
1126
|
+
*/
|
|
1127
|
+
function flattenChainedCalls(node) {
|
|
1128
|
+
const calls = [];
|
|
1129
|
+
let current = node;
|
|
1130
|
+
while (current.type === 'CallExpression') {
|
|
1131
|
+
const callee = current.callee;
|
|
1132
|
+
if (!callee)
|
|
1133
|
+
break;
|
|
1134
|
+
if (callee.type === 'MemberExpression' &&
|
|
1135
|
+
callee.property?.type === 'Identifier') {
|
|
1136
|
+
calls.unshift({
|
|
1137
|
+
method: callee.property.name,
|
|
1138
|
+
arguments: current.arguments || [],
|
|
1139
|
+
});
|
|
1140
|
+
current = callee.object;
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
break; // Reached the root (createRouter() / new Hono())
|
|
1144
|
+
}
|
|
1145
|
+
return calls;
|
|
1146
|
+
}
|
|
1147
|
+
export async function parseRoute(rootDir, filename, projectId, deploymentId, visitedFilesOrOptions, mountedSubrouters) {
|
|
1148
|
+
// Support both old positional args and new options object
|
|
1149
|
+
let options;
|
|
1150
|
+
if (visitedFilesOrOptions instanceof Set) {
|
|
1151
|
+
options = { visitedFiles: visitedFilesOrOptions, mountedSubrouters };
|
|
1152
|
+
}
|
|
1153
|
+
else if (visitedFilesOrOptions && typeof visitedFilesOrOptions === 'object') {
|
|
1154
|
+
options = visitedFilesOrOptions;
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
options = { mountedSubrouters };
|
|
1158
|
+
}
|
|
1072
1159
|
// Track visited files to prevent infinite recursion
|
|
1073
|
-
const visited = visitedFiles ?? new Set();
|
|
1160
|
+
const visited = options.visitedFiles ?? new Set();
|
|
1074
1161
|
const resolvedFilename = resolve(filename);
|
|
1075
1162
|
if (visited.has(resolvedFilename)) {
|
|
1076
1163
|
return []; // Already parsed this file, avoid infinite loop
|
|
@@ -1159,6 +1246,9 @@ export async function parseRoute(rootDir, filename, projectId, deploymentId, vis
|
|
|
1159
1246
|
message: `could not find default export for ${filename} using ${rootDir}`,
|
|
1160
1247
|
});
|
|
1161
1248
|
}
|
|
1249
|
+
// Track the chained init expression (e.g., createRouter().get(...).post(...))
|
|
1250
|
+
// so we can extract routes from it later
|
|
1251
|
+
let chainedInitExpr;
|
|
1162
1252
|
for (const body of ast.body) {
|
|
1163
1253
|
if (body.type === 'VariableDeclaration') {
|
|
1164
1254
|
for (const vardecl of body.declarations) {
|
|
@@ -1172,6 +1262,14 @@ export async function parseRoute(rootDir, filename, projectId, deploymentId, vis
|
|
|
1172
1262
|
variableName = identifier.name;
|
|
1173
1263
|
break;
|
|
1174
1264
|
}
|
|
1265
|
+
// Support chained calls: createRouter().get(...).post(...)
|
|
1266
|
+
// The init is a CallExpression whose callee is a MemberExpression chain
|
|
1267
|
+
// that eventually roots at createRouter() or new Hono()
|
|
1268
|
+
if (isChainedRouterInit(call)) {
|
|
1269
|
+
variableName = identifier.name;
|
|
1270
|
+
chainedInitExpr = call;
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1175
1273
|
}
|
|
1176
1274
|
else if (vardecl.init?.type === 'NewExpression') {
|
|
1177
1275
|
const newExpr = vardecl.init;
|
|
@@ -1193,16 +1291,19 @@ export async function parseRoute(rootDir, filename, projectId, deploymentId, vis
|
|
|
1193
1291
|
});
|
|
1194
1292
|
}
|
|
1195
1293
|
const rel = toForwardSlash(relative(rootDir, filename));
|
|
1196
|
-
//
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
//
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1294
|
+
// Determine the base mount path for routes in this file.
|
|
1295
|
+
// When mountPrefix is provided (explicit routing via createApp({ router })),
|
|
1296
|
+
// use it directly — the mount path comes from the code, not the filesystem.
|
|
1297
|
+
// Otherwise, derive it from the file's position in src/api/ (file-based routing).
|
|
1298
|
+
let basePath;
|
|
1299
|
+
if (options.mountPrefix !== undefined) {
|
|
1300
|
+
basePath = options.mountPrefix;
|
|
1301
|
+
}
|
|
1302
|
+
else {
|
|
1303
|
+
const srcDir = join(rootDir, 'src');
|
|
1304
|
+
const relativeApiPath = extractRelativeApiPath(filename, srcDir);
|
|
1305
|
+
basePath = computeApiMountPath(relativeApiPath);
|
|
1306
|
+
}
|
|
1206
1307
|
const routes = [];
|
|
1207
1308
|
try {
|
|
1208
1309
|
for (const body of ast.body) {
|
|
@@ -1268,31 +1369,38 @@ export async function parseRoute(rootDir, filename, projectId, deploymentId, vis
|
|
|
1268
1369
|
continue;
|
|
1269
1370
|
}
|
|
1270
1371
|
try {
|
|
1271
|
-
//
|
|
1272
|
-
const
|
|
1372
|
+
// The combined mount point for this sub-router
|
|
1373
|
+
const combinedBase = joinMountAndRoute(basePath, mountPath);
|
|
1374
|
+
// Parse sub-router's routes with the code-derived mount prefix.
|
|
1375
|
+
// This ensures the sub-router's routes are prefixed correctly
|
|
1376
|
+
// regardless of where the file lives on disk.
|
|
1377
|
+
const subRoutes = await parseRoute(rootDir, resolvedFile, projectId, deploymentId, {
|
|
1378
|
+
visitedFiles: visited,
|
|
1379
|
+
mountedSubrouters: options.mountedSubrouters,
|
|
1380
|
+
mountPrefix: combinedBase,
|
|
1381
|
+
});
|
|
1273
1382
|
// Track this file as a mounted sub-router
|
|
1274
|
-
if (mountedSubrouters) {
|
|
1275
|
-
mountedSubrouters.add(resolve(resolvedFile));
|
|
1383
|
+
if (options.mountedSubrouters) {
|
|
1384
|
+
options.mountedSubrouters.add(resolve(resolvedFile));
|
|
1276
1385
|
}
|
|
1277
|
-
// Compute the sub-router's own basePath so we can strip it
|
|
1278
|
-
const subSrcDir = join(rootDir, 'src');
|
|
1279
|
-
const subRelativeApiPath = extractRelativeApiPath(resolvedFile, subSrcDir);
|
|
1280
|
-
const subBasePath = computeApiMountPath(subRelativeApiPath);
|
|
1281
|
-
// The combined mount point for sub-routes
|
|
1282
|
-
const combinedBase = joinMountAndRoute(basePath, mountPath);
|
|
1283
1386
|
for (const subRoute of subRoutes) {
|
|
1284
|
-
//
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
routeSuffix = routeSuffix.slice(subBasePath.length) || '/';
|
|
1288
|
-
}
|
|
1289
|
-
const fullPath = joinMountAndRoute(combinedBase, routeSuffix);
|
|
1387
|
+
// Sub-routes already have the correct full path
|
|
1388
|
+
// (the recursive call used combinedBase as mountPrefix)
|
|
1389
|
+
const fullPath = subRoute.path;
|
|
1290
1390
|
const id = generateRouteId(projectId, deploymentId, subRoute.type, subRoute.method, rel, fullPath, subRoute.version);
|
|
1391
|
+
// Preserve the sub-route's original filename for schema
|
|
1392
|
+
// import resolution. The registry generator needs to know
|
|
1393
|
+
// which file actually defines/exports the schema variable.
|
|
1394
|
+
const config = { ...subRoute.config };
|
|
1395
|
+
if (subRoute.filename && subRoute.filename !== rel) {
|
|
1396
|
+
config.schemaSourceFile = subRoute.filename;
|
|
1397
|
+
}
|
|
1291
1398
|
routes.push({
|
|
1292
1399
|
...subRoute,
|
|
1293
1400
|
id,
|
|
1294
1401
|
path: fullPath,
|
|
1295
|
-
filename: rel,
|
|
1402
|
+
filename: rel,
|
|
1403
|
+
config: Object.keys(config).length > 0 ? config : undefined,
|
|
1296
1404
|
});
|
|
1297
1405
|
}
|
|
1298
1406
|
}
|
|
@@ -1672,6 +1780,142 @@ export async function parseRoute(rootDir, filename, projectId, deploymentId, vis
|
|
|
1672
1780
|
}
|
|
1673
1781
|
}
|
|
1674
1782
|
}
|
|
1783
|
+
// Process routes from chained initialization expressions
|
|
1784
|
+
// e.g., const router = createRouter().get('/foo', handler).post('/bar', handler)
|
|
1785
|
+
if (chainedInitExpr) {
|
|
1786
|
+
const chainedCalls = flattenChainedCalls(chainedInitExpr);
|
|
1787
|
+
for (const chainedCall of chainedCalls) {
|
|
1788
|
+
const { method: chainMethod, arguments: chainArgs } = chainedCall;
|
|
1789
|
+
// Skip non-route methods
|
|
1790
|
+
if (chainMethod === 'use' ||
|
|
1791
|
+
chainMethod === 'onError' ||
|
|
1792
|
+
chainMethod === 'notFound' ||
|
|
1793
|
+
chainMethod === 'basePath' ||
|
|
1794
|
+
chainMethod === 'mount') {
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
// Handle .route() for sub-router mounting (same as the ExpressionStatement case)
|
|
1798
|
+
if (chainMethod === 'route') {
|
|
1799
|
+
const mountPathArg = chainArgs[0];
|
|
1800
|
+
const subRouterArg = chainArgs[1];
|
|
1801
|
+
if (mountPathArg &&
|
|
1802
|
+
mountPathArg.type === 'Literal' &&
|
|
1803
|
+
subRouterArg &&
|
|
1804
|
+
subRouterArg.type === 'Identifier') {
|
|
1805
|
+
const mountPath = String(mountPathArg.value);
|
|
1806
|
+
const subRouterName = subRouterArg.name;
|
|
1807
|
+
const subRouterImportPath = importMap.get(subRouterName);
|
|
1808
|
+
if (subRouterImportPath) {
|
|
1809
|
+
const resolvedFile = resolveImportPath(dirname(filename), subRouterImportPath);
|
|
1810
|
+
if (resolvedFile && !visited.has(resolve(resolvedFile))) {
|
|
1811
|
+
try {
|
|
1812
|
+
const combinedBase = joinMountAndRoute(basePath, mountPath);
|
|
1813
|
+
const subRoutes = await parseRoute(rootDir, resolvedFile, projectId, deploymentId, {
|
|
1814
|
+
visitedFiles: visited,
|
|
1815
|
+
mountedSubrouters: options.mountedSubrouters,
|
|
1816
|
+
mountPrefix: combinedBase,
|
|
1817
|
+
});
|
|
1818
|
+
if (options.mountedSubrouters) {
|
|
1819
|
+
options.mountedSubrouters.add(resolve(resolvedFile));
|
|
1820
|
+
}
|
|
1821
|
+
for (const subRoute of subRoutes) {
|
|
1822
|
+
const fullPath = subRoute.path;
|
|
1823
|
+
const id = generateRouteId(projectId, deploymentId, subRoute.type, subRoute.method, rel, fullPath, subRoute.version);
|
|
1824
|
+
const config = { ...subRoute.config };
|
|
1825
|
+
if (subRoute.filename && subRoute.filename !== rel) {
|
|
1826
|
+
config.schemaSourceFile = subRoute.filename;
|
|
1827
|
+
}
|
|
1828
|
+
routes.push({
|
|
1829
|
+
...subRoute,
|
|
1830
|
+
id,
|
|
1831
|
+
path: fullPath,
|
|
1832
|
+
filename: rel,
|
|
1833
|
+
config: Object.keys(config).length > 0 ? config : undefined,
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
catch {
|
|
1838
|
+
// Sub-router parse failure — skip
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
// Handle HTTP methods: get, post, put, patch, delete
|
|
1846
|
+
const CHAINED_HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch'];
|
|
1847
|
+
if (CHAINED_HTTP_METHODS.includes(chainMethod.toLowerCase())) {
|
|
1848
|
+
const pathArg = chainArgs[0];
|
|
1849
|
+
if (!pathArg || pathArg.type !== 'Literal')
|
|
1850
|
+
continue;
|
|
1851
|
+
const suffix = String(pathArg.value);
|
|
1852
|
+
let type = 'api';
|
|
1853
|
+
// Check for websocket/sse/stream wrappers in handler args
|
|
1854
|
+
for (const arg of chainArgs.slice(1)) {
|
|
1855
|
+
if (arg?.type === 'CallExpression') {
|
|
1856
|
+
const callExpr = arg;
|
|
1857
|
+
if (callExpr.callee?.type === 'Identifier') {
|
|
1858
|
+
const calleeName = callExpr.callee.name;
|
|
1859
|
+
if (calleeName === 'websocket' ||
|
|
1860
|
+
calleeName === 'sse' ||
|
|
1861
|
+
calleeName === 'stream') {
|
|
1862
|
+
type = calleeName;
|
|
1863
|
+
break;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
const thepath = joinMountAndRoute(basePath, suffix);
|
|
1869
|
+
const id = generateRouteId(projectId, deploymentId, type, chainMethod, rel, thepath, version);
|
|
1870
|
+
// Check for validators in chained args
|
|
1871
|
+
const validatorInfo = hasValidatorCall(chainArgs);
|
|
1872
|
+
const routeConfig = {};
|
|
1873
|
+
if (validatorInfo.hasValidator) {
|
|
1874
|
+
routeConfig.hasValidator = true;
|
|
1875
|
+
if (validatorInfo.agentVariable) {
|
|
1876
|
+
routeConfig.agentVariable = validatorInfo.agentVariable;
|
|
1877
|
+
const agentImportPath = importMap.get(validatorInfo.agentVariable);
|
|
1878
|
+
if (agentImportPath)
|
|
1879
|
+
routeConfig.agentImportPath = agentImportPath;
|
|
1880
|
+
}
|
|
1881
|
+
if (validatorInfo.inputSchemaVariable) {
|
|
1882
|
+
routeConfig.inputSchemaVariable = validatorInfo.inputSchemaVariable;
|
|
1883
|
+
const inputImportInfo = importInfoMap.get(validatorInfo.inputSchemaVariable);
|
|
1884
|
+
if (inputImportInfo) {
|
|
1885
|
+
routeConfig.inputSchemaImportPath = inputImportInfo.modulePath;
|
|
1886
|
+
routeConfig.inputSchemaImportedName = inputImportInfo.importedName;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (validatorInfo.outputSchemaVariable) {
|
|
1890
|
+
routeConfig.outputSchemaVariable = validatorInfo.outputSchemaVariable;
|
|
1891
|
+
const outputImportInfo = importInfoMap.get(validatorInfo.outputSchemaVariable);
|
|
1892
|
+
if (outputImportInfo) {
|
|
1893
|
+
routeConfig.outputSchemaImportPath = outputImportInfo.modulePath;
|
|
1894
|
+
routeConfig.outputSchemaImportedName = outputImportInfo.importedName;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
if (validatorInfo.stream !== undefined)
|
|
1898
|
+
routeConfig.stream = validatorInfo.stream;
|
|
1899
|
+
}
|
|
1900
|
+
// Fall back to exported schemas
|
|
1901
|
+
if (!routeConfig.inputSchemaVariable && exportedInputSchemaName) {
|
|
1902
|
+
routeConfig.inputSchemaVariable = exportedInputSchemaName;
|
|
1903
|
+
}
|
|
1904
|
+
if (!routeConfig.outputSchemaVariable && exportedOutputSchemaName) {
|
|
1905
|
+
routeConfig.outputSchemaVariable = exportedOutputSchemaName;
|
|
1906
|
+
}
|
|
1907
|
+
routes.push({
|
|
1908
|
+
id,
|
|
1909
|
+
method: chainMethod,
|
|
1910
|
+
type: type,
|
|
1911
|
+
filename: rel,
|
|
1912
|
+
path: thepath,
|
|
1913
|
+
version,
|
|
1914
|
+
config: Object.keys(routeConfig).length > 0 ? routeConfig : undefined,
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1675
1919
|
}
|
|
1676
1920
|
catch (error) {
|
|
1677
1921
|
if (error instanceof InvalidRouterConfigError || error instanceof SchemaNotExportedError) {
|