@agentuity/cli 1.0.38 → 1.0.40
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/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/bundle-files.d.ts +12 -0
- package/dist/cmd/build/vite/bundle-files.d.ts.map +1 -0
- package/dist/cmd/build/vite/bundle-files.js +107 -0
- package/dist/cmd/build/vite/bundle-files.js.map +1 -0
- 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/vite-builder.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-builder.js +9 -0
- package/dist/cmd/build/vite/vite-builder.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/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.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/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/bundle-files.ts +135 -0
- 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/vite-builder.ts +11 -0
- package/src/cmd/build/vite-bundler.ts +4 -0
- package/src/cmd/dev/index.ts +34 -0
- package/src/types.ts +9 -0
- package/src/utils/route-migration.ts +793 -0
package/src/cmd/build/ast.ts
CHANGED
|
@@ -1520,16 +1520,124 @@ function resolveImportPath(fromDir: string, importPath: string): string | null {
|
|
|
1520
1520
|
return null;
|
|
1521
1521
|
}
|
|
1522
1522
|
|
|
1523
|
+
/**
|
|
1524
|
+
* Check if a CallExpression is a chained router initializer.
|
|
1525
|
+
* Walks up the callee chain looking for createRouter() or new Hono() at the root.
|
|
1526
|
+
*
|
|
1527
|
+
* Example AST for `createRouter().get('/foo', handler).post('/bar', handler)`:
|
|
1528
|
+
* ```
|
|
1529
|
+
* CallExpression (.post)
|
|
1530
|
+
* callee: MemberExpression
|
|
1531
|
+
* object: CallExpression (.get)
|
|
1532
|
+
* callee: MemberExpression
|
|
1533
|
+
* object: CallExpression (createRouter)
|
|
1534
|
+
* property: "post"
|
|
1535
|
+
* ```
|
|
1536
|
+
*/
|
|
1537
|
+
function isChainedRouterInit(node: ASTNode): boolean {
|
|
1538
|
+
let current: ASTNode = node;
|
|
1539
|
+
|
|
1540
|
+
// Walk down the chain: each link is CallExpression → MemberExpression → CallExpression
|
|
1541
|
+
while (current.type === 'CallExpression') {
|
|
1542
|
+
const callee = (current as ASTCallExpression).callee as ASTNode;
|
|
1543
|
+
if (!callee) return false;
|
|
1544
|
+
|
|
1545
|
+
// Direct createRouter()
|
|
1546
|
+
if (callee.type === 'Identifier' && (callee as ASTNodeIdentifier).name === 'createRouter')
|
|
1547
|
+
return true;
|
|
1548
|
+
|
|
1549
|
+
// Chained: .method() → MemberExpression
|
|
1550
|
+
if (callee.type === 'MemberExpression' && (callee as ASTMemberExpression).object) {
|
|
1551
|
+
current = (callee as ASTMemberExpression).object as ASTNode;
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
break;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Check if we landed on createRouter() or new Hono()
|
|
1559
|
+
if (current.type === 'CallExpression') {
|
|
1560
|
+
const callee = (current as ASTCallExpression).callee as ASTNode;
|
|
1561
|
+
if (callee?.type === 'Identifier' && (callee as ASTNodeIdentifier).name === 'createRouter')
|
|
1562
|
+
return true;
|
|
1563
|
+
}
|
|
1564
|
+
if (current.type === 'NewExpression') {
|
|
1565
|
+
const callee = (current as ASTCallExpression).callee as ASTNode;
|
|
1566
|
+
if (callee?.type === 'Identifier' && (callee as ASTNodeIdentifier).name === 'Hono')
|
|
1567
|
+
return true;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/**
|
|
1574
|
+
* Flatten a chained call expression into individual method calls.
|
|
1575
|
+
*
|
|
1576
|
+
* Given `createRouter().get('/a', h1).post('/b', h2).route('/c', sub)`, returns:
|
|
1577
|
+
* ```
|
|
1578
|
+
* [
|
|
1579
|
+
* { method: 'get', arguments: ['/a', h1] },
|
|
1580
|
+
* { method: 'post', arguments: ['/b', h2] },
|
|
1581
|
+
* { method: 'route', arguments: ['/c', sub] },
|
|
1582
|
+
* ]
|
|
1583
|
+
* ```
|
|
1584
|
+
*/
|
|
1585
|
+
function flattenChainedCalls(node: ASTNode): Array<{ method: string; arguments: unknown[] }> {
|
|
1586
|
+
const calls: Array<{ method: string; arguments: unknown[] }> = [];
|
|
1587
|
+
|
|
1588
|
+
let current: ASTNode = node;
|
|
1589
|
+
while (current.type === 'CallExpression') {
|
|
1590
|
+
const callee = (current as ASTCallExpression).callee as ASTNode;
|
|
1591
|
+
if (!callee) break;
|
|
1592
|
+
|
|
1593
|
+
if (
|
|
1594
|
+
callee.type === 'MemberExpression' &&
|
|
1595
|
+
((callee as ASTMemberExpression).property as ASTNode)?.type === 'Identifier'
|
|
1596
|
+
) {
|
|
1597
|
+
calls.unshift({
|
|
1598
|
+
method: ((callee as ASTMemberExpression).property as ASTNodeIdentifier).name,
|
|
1599
|
+
arguments: (current as ASTCallExpression).arguments || [],
|
|
1600
|
+
});
|
|
1601
|
+
current = (callee as ASTMemberExpression).object as ASTNode;
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
break; // Reached the root (createRouter() / new Hono())
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
return calls;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
export interface ParseRouteOptions {
|
|
1612
|
+
visitedFiles?: Set<string>;
|
|
1613
|
+
mountedSubrouters?: Set<string>;
|
|
1614
|
+
/**
|
|
1615
|
+
* Explicit mount prefix for this router file, derived from code-based routing
|
|
1616
|
+
* (e.g., from `createApp({ router })` or `.route()` calls).
|
|
1617
|
+
* When provided, overrides the filesystem-derived mount path.
|
|
1618
|
+
*/
|
|
1619
|
+
mountPrefix?: string;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1523
1622
|
export async function parseRoute(
|
|
1524
1623
|
rootDir: string,
|
|
1525
1624
|
filename: string,
|
|
1526
1625
|
projectId: string,
|
|
1527
1626
|
deploymentId: string,
|
|
1528
|
-
|
|
1627
|
+
visitedFilesOrOptions?: Set<string> | ParseRouteOptions,
|
|
1529
1628
|
mountedSubrouters?: Set<string>
|
|
1530
1629
|
): Promise<BuildMetadata['routes']> {
|
|
1630
|
+
// Support both old positional args and new options object
|
|
1631
|
+
let options: ParseRouteOptions;
|
|
1632
|
+
if (visitedFilesOrOptions instanceof Set) {
|
|
1633
|
+
options = { visitedFiles: visitedFilesOrOptions, mountedSubrouters };
|
|
1634
|
+
} else if (visitedFilesOrOptions && typeof visitedFilesOrOptions === 'object') {
|
|
1635
|
+
options = visitedFilesOrOptions;
|
|
1636
|
+
} else {
|
|
1637
|
+
options = { mountedSubrouters };
|
|
1638
|
+
}
|
|
1531
1639
|
// Track visited files to prevent infinite recursion
|
|
1532
|
-
const visited = visitedFiles ?? new Set<string>();
|
|
1640
|
+
const visited = options.visitedFiles ?? new Set<string>();
|
|
1533
1641
|
const resolvedFilename = resolve(filename);
|
|
1534
1642
|
if (visited.has(resolvedFilename)) {
|
|
1535
1643
|
return []; // Already parsed this file, avoid infinite loop
|
|
@@ -1643,6 +1751,10 @@ export async function parseRoute(
|
|
|
1643
1751
|
message: `could not find default export for ${filename} using ${rootDir}`,
|
|
1644
1752
|
});
|
|
1645
1753
|
}
|
|
1754
|
+
// Track the chained init expression (e.g., createRouter().get(...).post(...))
|
|
1755
|
+
// so we can extract routes from it later
|
|
1756
|
+
let chainedInitExpr: ASTNode | undefined;
|
|
1757
|
+
|
|
1646
1758
|
for (const body of ast.body) {
|
|
1647
1759
|
if (body.type === 'VariableDeclaration') {
|
|
1648
1760
|
for (const vardecl of body.declarations) {
|
|
@@ -1656,6 +1768,14 @@ export async function parseRoute(
|
|
|
1656
1768
|
variableName = identifier.name;
|
|
1657
1769
|
break;
|
|
1658
1770
|
}
|
|
1771
|
+
// Support chained calls: createRouter().get(...).post(...)
|
|
1772
|
+
// The init is a CallExpression whose callee is a MemberExpression chain
|
|
1773
|
+
// that eventually roots at createRouter() or new Hono()
|
|
1774
|
+
if (isChainedRouterInit(call)) {
|
|
1775
|
+
variableName = identifier.name;
|
|
1776
|
+
chainedInitExpr = call;
|
|
1777
|
+
break;
|
|
1778
|
+
}
|
|
1659
1779
|
} else if (vardecl.init?.type === 'NewExpression') {
|
|
1660
1780
|
const newExpr = vardecl.init as ASTCallExpression;
|
|
1661
1781
|
// Support new Hono() pattern
|
|
@@ -1678,16 +1798,18 @@ export async function parseRoute(
|
|
|
1678
1798
|
|
|
1679
1799
|
const rel = toForwardSlash(relative(rootDir, filename));
|
|
1680
1800
|
|
|
1681
|
-
//
|
|
1682
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1801
|
+
// Determine the base mount path for routes in this file.
|
|
1802
|
+
// When mountPrefix is provided (explicit routing via createApp({ router })),
|
|
1803
|
+
// use it directly — the mount path comes from the code, not the filesystem.
|
|
1804
|
+
// Otherwise, derive it from the file's position in src/api/ (file-based routing).
|
|
1805
|
+
let basePath: string;
|
|
1806
|
+
if (options.mountPrefix !== undefined) {
|
|
1807
|
+
basePath = options.mountPrefix;
|
|
1808
|
+
} else {
|
|
1809
|
+
const srcDir = join(rootDir, 'src');
|
|
1810
|
+
const relativeApiPath = extractRelativeApiPath(filename, srcDir);
|
|
1811
|
+
basePath = computeApiMountPath(relativeApiPath);
|
|
1812
|
+
}
|
|
1691
1813
|
|
|
1692
1814
|
const routes: RouteDefinition = [];
|
|
1693
1815
|
|
|
@@ -1773,40 +1895,33 @@ export async function parseRoute(
|
|
|
1773
1895
|
}
|
|
1774
1896
|
|
|
1775
1897
|
try {
|
|
1776
|
-
//
|
|
1898
|
+
// The combined mount point for this sub-router
|
|
1899
|
+
const combinedBase = joinMountAndRoute(basePath, mountPath);
|
|
1900
|
+
|
|
1901
|
+
// Parse sub-router's routes with the code-derived mount prefix.
|
|
1902
|
+
// This ensures the sub-router's routes are prefixed correctly
|
|
1903
|
+
// regardless of where the file lives on disk.
|
|
1777
1904
|
const subRoutes = await parseRoute(
|
|
1778
1905
|
rootDir,
|
|
1779
1906
|
resolvedFile,
|
|
1780
1907
|
projectId,
|
|
1781
1908
|
deploymentId,
|
|
1782
|
-
|
|
1783
|
-
|
|
1909
|
+
{
|
|
1910
|
+
visitedFiles: visited,
|
|
1911
|
+
mountedSubrouters: options.mountedSubrouters,
|
|
1912
|
+
mountPrefix: combinedBase,
|
|
1913
|
+
}
|
|
1784
1914
|
);
|
|
1785
1915
|
|
|
1786
1916
|
// Track this file as a mounted sub-router
|
|
1787
|
-
if (mountedSubrouters) {
|
|
1788
|
-
mountedSubrouters.add(resolve(resolvedFile));
|
|
1917
|
+
if (options.mountedSubrouters) {
|
|
1918
|
+
options.mountedSubrouters.add(resolve(resolvedFile));
|
|
1789
1919
|
}
|
|
1790
1920
|
|
|
1791
|
-
// Compute the sub-router's own basePath so we can strip it
|
|
1792
|
-
const subSrcDir = join(rootDir, 'src');
|
|
1793
|
-
const subRelativeApiPath = extractRelativeApiPath(
|
|
1794
|
-
resolvedFile,
|
|
1795
|
-
subSrcDir
|
|
1796
|
-
);
|
|
1797
|
-
const subBasePath = computeApiMountPath(subRelativeApiPath);
|
|
1798
|
-
|
|
1799
|
-
// The combined mount point for sub-routes
|
|
1800
|
-
const combinedBase = joinMountAndRoute(basePath, mountPath);
|
|
1801
|
-
|
|
1802
1921
|
for (const subRoute of subRoutes) {
|
|
1803
|
-
//
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
routeSuffix = routeSuffix.slice(subBasePath.length) || '/';
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
const fullPath = joinMountAndRoute(combinedBase, routeSuffix);
|
|
1922
|
+
// Sub-routes already have the correct full path
|
|
1923
|
+
// (the recursive call used combinedBase as mountPrefix)
|
|
1924
|
+
const fullPath = subRoute.path;
|
|
1810
1925
|
const id = generateRouteId(
|
|
1811
1926
|
projectId,
|
|
1812
1927
|
deploymentId,
|
|
@@ -1817,11 +1932,20 @@ export async function parseRoute(
|
|
|
1817
1932
|
subRoute.version
|
|
1818
1933
|
);
|
|
1819
1934
|
|
|
1935
|
+
// Preserve the sub-route's original filename for schema
|
|
1936
|
+
// import resolution. The registry generator needs to know
|
|
1937
|
+
// which file actually defines/exports the schema variable.
|
|
1938
|
+
const config = { ...subRoute.config };
|
|
1939
|
+
if (subRoute.filename && subRoute.filename !== rel) {
|
|
1940
|
+
config.schemaSourceFile = subRoute.filename;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1820
1943
|
routes.push({
|
|
1821
1944
|
...subRoute,
|
|
1822
1945
|
id,
|
|
1823
1946
|
path: fullPath,
|
|
1824
|
-
filename: rel,
|
|
1947
|
+
filename: rel,
|
|
1948
|
+
config: Object.keys(config).length > 0 ? config : undefined,
|
|
1825
1949
|
});
|
|
1826
1950
|
}
|
|
1827
1951
|
} catch {
|
|
@@ -2309,6 +2433,185 @@ export async function parseRoute(
|
|
|
2309
2433
|
}
|
|
2310
2434
|
}
|
|
2311
2435
|
}
|
|
2436
|
+
// Process routes from chained initialization expressions
|
|
2437
|
+
// e.g., const router = createRouter().get('/foo', handler).post('/bar', handler)
|
|
2438
|
+
if (chainedInitExpr) {
|
|
2439
|
+
const chainedCalls = flattenChainedCalls(chainedInitExpr);
|
|
2440
|
+
for (const chainedCall of chainedCalls) {
|
|
2441
|
+
const { method: chainMethod, arguments: chainArgs } = chainedCall;
|
|
2442
|
+
|
|
2443
|
+
// Skip non-route methods
|
|
2444
|
+
if (
|
|
2445
|
+
chainMethod === 'use' ||
|
|
2446
|
+
chainMethod === 'onError' ||
|
|
2447
|
+
chainMethod === 'notFound' ||
|
|
2448
|
+
chainMethod === 'basePath' ||
|
|
2449
|
+
chainMethod === 'mount'
|
|
2450
|
+
) {
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
// Handle .route() for sub-router mounting (same as the ExpressionStatement case)
|
|
2455
|
+
if (chainMethod === 'route') {
|
|
2456
|
+
const mountPathArg = chainArgs[0] as ASTNode | undefined;
|
|
2457
|
+
const subRouterArg = chainArgs[1] as ASTNode | undefined;
|
|
2458
|
+
|
|
2459
|
+
if (
|
|
2460
|
+
mountPathArg &&
|
|
2461
|
+
(mountPathArg as ASTLiteral).type === 'Literal' &&
|
|
2462
|
+
subRouterArg &&
|
|
2463
|
+
(subRouterArg as ASTNodeIdentifier).type === 'Identifier'
|
|
2464
|
+
) {
|
|
2465
|
+
const mountPath = String((mountPathArg as ASTLiteral).value);
|
|
2466
|
+
const subRouterName = (subRouterArg as ASTNodeIdentifier).name;
|
|
2467
|
+
const subRouterImportPath = importMap.get(subRouterName);
|
|
2468
|
+
|
|
2469
|
+
if (subRouterImportPath) {
|
|
2470
|
+
const resolvedFile = resolveImportPath(dirname(filename), subRouterImportPath);
|
|
2471
|
+
if (resolvedFile && !visited.has(resolve(resolvedFile))) {
|
|
2472
|
+
try {
|
|
2473
|
+
const combinedBase = joinMountAndRoute(basePath, mountPath);
|
|
2474
|
+
const subRoutes = await parseRoute(
|
|
2475
|
+
rootDir,
|
|
2476
|
+
resolvedFile,
|
|
2477
|
+
projectId,
|
|
2478
|
+
deploymentId,
|
|
2479
|
+
{
|
|
2480
|
+
visitedFiles: visited,
|
|
2481
|
+
mountedSubrouters: options.mountedSubrouters,
|
|
2482
|
+
mountPrefix: combinedBase,
|
|
2483
|
+
}
|
|
2484
|
+
);
|
|
2485
|
+
|
|
2486
|
+
if (options.mountedSubrouters) {
|
|
2487
|
+
options.mountedSubrouters.add(resolve(resolvedFile));
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
for (const subRoute of subRoutes) {
|
|
2491
|
+
const fullPath = subRoute.path;
|
|
2492
|
+
const id = generateRouteId(
|
|
2493
|
+
projectId,
|
|
2494
|
+
deploymentId,
|
|
2495
|
+
subRoute.type,
|
|
2496
|
+
subRoute.method,
|
|
2497
|
+
rel,
|
|
2498
|
+
fullPath,
|
|
2499
|
+
subRoute.version
|
|
2500
|
+
);
|
|
2501
|
+
|
|
2502
|
+
const config = { ...subRoute.config };
|
|
2503
|
+
if (subRoute.filename && subRoute.filename !== rel) {
|
|
2504
|
+
config.schemaSourceFile = subRoute.filename;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
routes.push({
|
|
2508
|
+
...subRoute,
|
|
2509
|
+
id,
|
|
2510
|
+
path: fullPath,
|
|
2511
|
+
filename: rel,
|
|
2512
|
+
config: Object.keys(config).length > 0 ? config : undefined,
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
} catch {
|
|
2516
|
+
// Sub-router parse failure — skip
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// Handle HTTP methods: get, post, put, patch, delete
|
|
2525
|
+
const CHAINED_HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch'] as const;
|
|
2526
|
+
if (
|
|
2527
|
+
CHAINED_HTTP_METHODS.includes(
|
|
2528
|
+
chainMethod.toLowerCase() as (typeof CHAINED_HTTP_METHODS)[number]
|
|
2529
|
+
)
|
|
2530
|
+
) {
|
|
2531
|
+
const pathArg = chainArgs[0] as ASTNode | undefined;
|
|
2532
|
+
if (!pathArg || (pathArg as ASTLiteral).type !== 'Literal') continue;
|
|
2533
|
+
|
|
2534
|
+
const suffix = String((pathArg as ASTLiteral).value);
|
|
2535
|
+
let type = 'api';
|
|
2536
|
+
|
|
2537
|
+
// Check for websocket/sse/stream wrappers in handler args
|
|
2538
|
+
for (const arg of chainArgs.slice(1)) {
|
|
2539
|
+
if ((arg as ASTCallExpression)?.type === 'CallExpression') {
|
|
2540
|
+
const callExpr = arg as ASTCallExpression;
|
|
2541
|
+
if (callExpr.callee?.type === 'Identifier') {
|
|
2542
|
+
const calleeName = (callExpr.callee as ASTNodeIdentifier).name;
|
|
2543
|
+
if (
|
|
2544
|
+
calleeName === 'websocket' ||
|
|
2545
|
+
calleeName === 'sse' ||
|
|
2546
|
+
calleeName === 'stream'
|
|
2547
|
+
) {
|
|
2548
|
+
type = calleeName;
|
|
2549
|
+
break;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
const thepath = joinMountAndRoute(basePath, suffix);
|
|
2556
|
+
const id = generateRouteId(
|
|
2557
|
+
projectId,
|
|
2558
|
+
deploymentId,
|
|
2559
|
+
type,
|
|
2560
|
+
chainMethod,
|
|
2561
|
+
rel,
|
|
2562
|
+
thepath,
|
|
2563
|
+
version
|
|
2564
|
+
);
|
|
2565
|
+
|
|
2566
|
+
// Check for validators in chained args
|
|
2567
|
+
const validatorInfo = hasValidatorCall(chainArgs as unknown[]);
|
|
2568
|
+
const routeConfig: Record<string, unknown> = {};
|
|
2569
|
+
if (validatorInfo.hasValidator) {
|
|
2570
|
+
routeConfig.hasValidator = true;
|
|
2571
|
+
if (validatorInfo.agentVariable) {
|
|
2572
|
+
routeConfig.agentVariable = validatorInfo.agentVariable;
|
|
2573
|
+
const agentImportPath = importMap.get(validatorInfo.agentVariable);
|
|
2574
|
+
if (agentImportPath) routeConfig.agentImportPath = agentImportPath;
|
|
2575
|
+
}
|
|
2576
|
+
if (validatorInfo.inputSchemaVariable) {
|
|
2577
|
+
routeConfig.inputSchemaVariable = validatorInfo.inputSchemaVariable;
|
|
2578
|
+
const inputImportInfo = importInfoMap.get(validatorInfo.inputSchemaVariable);
|
|
2579
|
+
if (inputImportInfo) {
|
|
2580
|
+
routeConfig.inputSchemaImportPath = inputImportInfo.modulePath;
|
|
2581
|
+
routeConfig.inputSchemaImportedName = inputImportInfo.importedName;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
if (validatorInfo.outputSchemaVariable) {
|
|
2585
|
+
routeConfig.outputSchemaVariable = validatorInfo.outputSchemaVariable;
|
|
2586
|
+
const outputImportInfo = importInfoMap.get(validatorInfo.outputSchemaVariable);
|
|
2587
|
+
if (outputImportInfo) {
|
|
2588
|
+
routeConfig.outputSchemaImportPath = outputImportInfo.modulePath;
|
|
2589
|
+
routeConfig.outputSchemaImportedName = outputImportInfo.importedName;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
if (validatorInfo.stream !== undefined) routeConfig.stream = validatorInfo.stream;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// Fall back to exported schemas
|
|
2596
|
+
if (!routeConfig.inputSchemaVariable && exportedInputSchemaName) {
|
|
2597
|
+
routeConfig.inputSchemaVariable = exportedInputSchemaName;
|
|
2598
|
+
}
|
|
2599
|
+
if (!routeConfig.outputSchemaVariable && exportedOutputSchemaName) {
|
|
2600
|
+
routeConfig.outputSchemaVariable = exportedOutputSchemaName;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
routes.push({
|
|
2604
|
+
id,
|
|
2605
|
+
method: chainMethod as 'get' | 'post' | 'put' | 'delete' | 'patch',
|
|
2606
|
+
type: type as 'api' | 'sms' | 'email' | 'cron',
|
|
2607
|
+
filename: rel,
|
|
2608
|
+
path: thepath,
|
|
2609
|
+
version,
|
|
2610
|
+
config: Object.keys(routeConfig).length > 0 ? routeConfig : undefined,
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2312
2615
|
} catch (error) {
|
|
2313
2616
|
if (error instanceof InvalidRouterConfigError || error instanceof SchemaNotExportedError) {
|
|
2314
2617
|
throw error;
|
|
@@ -84,6 +84,7 @@ export async function generateEntryFile(options: GenerateEntryOptions): Promise<
|
|
|
84
84
|
` createCompressionMiddleware,`,
|
|
85
85
|
` getAppState,`,
|
|
86
86
|
` getAppConfig,`,
|
|
87
|
+
` getUserRouter,`,
|
|
87
88
|
` register,`,
|
|
88
89
|
` getSpanProcessors,`,
|
|
89
90
|
` createServices,`,
|
|
@@ -151,8 +152,25 @@ export async function generateEntryFile(options: GenerateEntryOptions): Promise<
|
|
|
151
152
|
const apiMount =
|
|
152
153
|
routeImportsAndMounts.length > 0
|
|
153
154
|
? `
|
|
154
|
-
//
|
|
155
|
-
|
|
155
|
+
// Apply middleware and mount API routes
|
|
156
|
+
// If user passed router(s) via createApp({ router }), mount those instead of discovered files
|
|
157
|
+
const __userMounts = getUserRouter();
|
|
158
|
+
if (__userMounts) {
|
|
159
|
+
for (const mount of __userMounts) {
|
|
160
|
+
// Apply Agentuity middleware (CORS, OTel, agent context) to each user-provided prefix
|
|
161
|
+
const prefix = mount.path.endsWith('/') ? mount.path + '*' : mount.path + '/*';
|
|
162
|
+
app.use(prefix, createCorsMiddleware());
|
|
163
|
+
app.use(prefix, createOtelMiddleware());
|
|
164
|
+
app.use(prefix, createAgentMiddleware(''));
|
|
165
|
+
app.route(mount.path, mount.router);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
// File-based routing: apply middleware to /api/* and mount discovered route files
|
|
169
|
+
app.use('/api/*', createCorsMiddleware());
|
|
170
|
+
app.use('/api/*', createOtelMiddleware());
|
|
171
|
+
app.use('/api/*', createAgentMiddleware(''));
|
|
172
|
+
${routeImportsAndMounts.map((line) => `\t${line}`).join('\n')}
|
|
173
|
+
}
|
|
156
174
|
`
|
|
157
175
|
: '';
|
|
158
176
|
|
|
@@ -682,22 +700,11 @@ app.use('*', createBaseMiddleware({
|
|
|
682
700
|
meter: otel.meter,
|
|
683
701
|
}));
|
|
684
702
|
|
|
685
|
-
//
|
|
686
|
-
// which includes signature headers for production authentication
|
|
687
|
-
app.use('/api/*', createCorsMiddleware());
|
|
688
|
-
|
|
689
|
-
// Critical: otelMiddleware creates session/thread/waitUntilHandler
|
|
690
|
-
// Only apply to routes that need full session tracking:
|
|
691
|
-
// - /api/* routes (agent/API invocations)
|
|
692
|
-
// - /_agentuity/workbench/* routes (workbench API)
|
|
693
|
-
// Explicitly excluded (no session tracking, no Catalyst events):
|
|
694
|
-
// - /_agentuity/webanalytics/* (web analytics - uses lightweight cookie-only middleware)
|
|
695
|
-
// - /_agentuity/health, /_agentuity/ready, /_agentuity/idle (health checks)
|
|
703
|
+
// Workbench routes always get OTel middleware for session tracking
|
|
696
704
|
app.use('/_agentuity/workbench/*', createOtelMiddleware());
|
|
697
|
-
app.use('/api/*', createOtelMiddleware());
|
|
698
705
|
|
|
699
|
-
//
|
|
700
|
-
app.
|
|
706
|
+
// Note: /api/* middleware (CORS, OTel, agent context) is applied in Step 6
|
|
707
|
+
// after app.ts import, so user-provided routers can specify custom prefixes.
|
|
701
708
|
|
|
702
709
|
// Step 4: Import user's app.ts (runs createApp, gets state/config)
|
|
703
710
|
await import('../../app.js');
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { join, dirname } from 'node:path';
|
|
2
|
+
import { mkdirSync, cpSync } from 'node:fs';
|
|
3
|
+
import type { Logger } from '../../../types';
|
|
4
|
+
|
|
5
|
+
/** Paths that are always excluded regardless of .gitignore */
|
|
6
|
+
function isHardExcluded(match: string): boolean {
|
|
7
|
+
return (
|
|
8
|
+
match.startsWith('.agentuity/') ||
|
|
9
|
+
match.startsWith('.agentuity\\') ||
|
|
10
|
+
match.startsWith('node_modules/') ||
|
|
11
|
+
match.startsWith('node_modules\\') ||
|
|
12
|
+
match.startsWith('.git/') ||
|
|
13
|
+
match.startsWith('.git\\') ||
|
|
14
|
+
match === '.env' ||
|
|
15
|
+
match.startsWith('.env.')
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Use `git check-ignore --stdin` to filter out files that are ignored by .gitignore.
|
|
21
|
+
* Returns the subset of `files` that are NOT gitignored.
|
|
22
|
+
* Falls back to returning all files if not in a git repo or git is unavailable.
|
|
23
|
+
*/
|
|
24
|
+
async function filterGitIgnored(
|
|
25
|
+
rootDir: string,
|
|
26
|
+
files: string[],
|
|
27
|
+
logger: Logger
|
|
28
|
+
): Promise<string[]> {
|
|
29
|
+
if (files.length === 0) return files;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Check if we're in a git repo
|
|
33
|
+
const gitCheck = Bun.spawnSync(['git', 'rev-parse', '--git-dir'], {
|
|
34
|
+
cwd: rootDir,
|
|
35
|
+
stderr: 'pipe',
|
|
36
|
+
});
|
|
37
|
+
if (gitCheck.exitCode !== 0) {
|
|
38
|
+
logger.debug('Not a git repository, skipping .gitignore filtering');
|
|
39
|
+
return files;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Use git check-ignore to find which files are ignored
|
|
43
|
+
const proc = Bun.spawn(['git', 'check-ignore', '--stdin'], {
|
|
44
|
+
cwd: rootDir,
|
|
45
|
+
stdin: 'pipe',
|
|
46
|
+
stdout: 'pipe',
|
|
47
|
+
stderr: 'pipe',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Write all file paths to stdin, one per line
|
|
51
|
+
proc.stdin.write(files.join('\n'));
|
|
52
|
+
proc.stdin.end();
|
|
53
|
+
|
|
54
|
+
const output = await new Response(proc.stdout).text();
|
|
55
|
+
await proc.exited;
|
|
56
|
+
|
|
57
|
+
// git check-ignore exits 0 if some files are ignored, 1 if none are ignored.
|
|
58
|
+
// Both are fine. Other exit codes mean an error.
|
|
59
|
+
|
|
60
|
+
const ignoredFiles = output
|
|
61
|
+
.split('\n')
|
|
62
|
+
.map((line) => line.trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
const ignoredSet = new Set(ignoredFiles);
|
|
65
|
+
|
|
66
|
+
if (ignoredSet.size > 0) {
|
|
67
|
+
logger.debug(`Filtered ${ignoredSet.size} gitignored file(s) from bundle`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return files.filter((f) => !ignoredSet.has(f));
|
|
71
|
+
} catch {
|
|
72
|
+
logger.debug('git not available, skipping .gitignore filtering');
|
|
73
|
+
return files;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Copy files matching glob patterns into the build output directory.
|
|
79
|
+
* Files are copied preserving their relative directory structure from the project root.
|
|
80
|
+
* This runs BEFORE the build steps so that build output can overwrite any conflicts.
|
|
81
|
+
*
|
|
82
|
+
* Filtering layers:
|
|
83
|
+
* 1. Hard exclusions: .agentuity/, node_modules/, .git/, .env* (always skipped)
|
|
84
|
+
* 2. .gitignore: files ignored by git are skipped (falls back if not a git repo)
|
|
85
|
+
*/
|
|
86
|
+
export async function copyBundleFiles(
|
|
87
|
+
rootDir: string,
|
|
88
|
+
outDir: string,
|
|
89
|
+
patterns: string[],
|
|
90
|
+
logger: Logger
|
|
91
|
+
): Promise<number> {
|
|
92
|
+
let totalCopied = 0;
|
|
93
|
+
|
|
94
|
+
// Ensure output directory exists
|
|
95
|
+
mkdirSync(outDir, { recursive: true });
|
|
96
|
+
|
|
97
|
+
for (const pattern of patterns) {
|
|
98
|
+
const glob = new Bun.Glob(pattern);
|
|
99
|
+
const candidates: string[] = [];
|
|
100
|
+
|
|
101
|
+
// Phase 1: Glob match + hard exclusions
|
|
102
|
+
for await (const match of glob.scan({ cwd: rootDir, onlyFiles: true })) {
|
|
103
|
+
if (!isHardExcluded(match)) {
|
|
104
|
+
candidates.push(match);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Phase 2: Filter out gitignored files
|
|
109
|
+
const filesToCopy = await filterGitIgnored(rootDir, candidates, logger);
|
|
110
|
+
|
|
111
|
+
// Phase 3: Copy files
|
|
112
|
+
for (const match of filesToCopy) {
|
|
113
|
+
const src = join(rootDir, match);
|
|
114
|
+
const dest = join(outDir, match);
|
|
115
|
+
try {
|
|
116
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
117
|
+
cpSync(src, dest);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Failed to copy bundle file '${match}' (pattern '${pattern}'): ${(err as Error).message}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (filesToCopy.length === 0) {
|
|
126
|
+
logger.warn(`Bundle pattern '${pattern}' matched no files`);
|
|
127
|
+
} else {
|
|
128
|
+
logger.debug(`Bundle pattern '${pattern}': ${filesToCopy.length} file(s)`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
totalCopied += filesToCopy.length;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return totalCopied;
|
|
135
|
+
}
|