proteum 2.4.4 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +81 -52
  2. package/agents/project/AGENTS.md +112 -31
  3. package/agents/project/CODING_STYLE.md +2 -2
  4. package/agents/project/app-root/AGENTS.md +1 -3
  5. package/agents/project/client/AGENTS.md +5 -1
  6. package/agents/project/client/pages/AGENTS.md +21 -9
  7. package/agents/project/diagnostics.md +2 -2
  8. package/agents/project/optimizations.md +1 -1
  9. package/agents/project/root/AGENTS.md +105 -22
  10. package/agents/project/server/routes/AGENTS.md +30 -1
  11. package/agents/project/server/services/AGENTS.md +4 -0
  12. package/agents/project/tests/AGENTS.md +1 -1
  13. package/cli/commands/doctor.ts +54 -3
  14. package/cli/commands/runtime.ts +6 -0
  15. package/cli/commands/worktree.ts +116 -0
  16. package/cli/compiler/artifacts/controllers.ts +16 -15
  17. package/cli/compiler/artifacts/discovery.ts +129 -17
  18. package/cli/compiler/artifacts/routing.ts +0 -5
  19. package/cli/compiler/artifacts/services.ts +253 -76
  20. package/cli/compiler/common/controllers.ts +159 -57
  21. package/cli/compiler/common/generatedRouteModules.ts +457 -363
  22. package/cli/mcp/router.ts +47 -3
  23. package/cli/presentation/commands.ts +25 -15
  24. package/cli/runtime/commands.ts +39 -12
  25. package/cli/runtime/worktreeBootstrap.ts +608 -0
  26. package/cli/scaffold/index.ts +28 -18
  27. package/cli/scaffold/templates.ts +44 -33
  28. package/cli/utils/agents.ts +14 -1
  29. package/client/app/index.ts +22 -5
  30. package/client/services/router/index.tsx +23 -3
  31. package/client/services/router/request/api.ts +16 -6
  32. package/common/dev/contractsDoctor.ts +1 -1
  33. package/common/dev/mcpPayloads.ts +8 -1
  34. package/common/env/proteumEnv.ts +14 -2
  35. package/common/router/contracts.ts +1 -1
  36. package/common/router/definitions.ts +177 -0
  37. package/common/router/index.ts +23 -12
  38. package/common/router/pageData.ts +5 -5
  39. package/common/router/register.ts +2 -2
  40. package/common/router/request/api.ts +12 -2
  41. package/docs/agent-routing.md +5 -2
  42. package/docs/diagnostics.md +2 -0
  43. package/docs/mcp.md +6 -3
  44. package/docs/migration-2.5.md +226 -0
  45. package/eslint.js +89 -42
  46. package/package.json +1 -1
  47. package/server/app/commands.ts +5 -1
  48. package/server/app/container/console/index.ts +1 -1
  49. package/server/app/controller/index.ts +98 -40
  50. package/server/app/index.ts +120 -3
  51. package/server/app/service/index.ts +5 -1
  52. package/server/index.ts +6 -2
  53. package/server/services/router/index.ts +50 -41
  54. package/server/services/router/response/index.ts +2 -2
  55. package/tests/agents-utils.test.cjs +14 -1
  56. package/tests/cli-mcp-command.test.cjs +84 -0
  57. package/tests/client-app-error-handling.test.cjs +100 -0
  58. package/tests/definition-contracts.test.cjs +453 -0
  59. package/tests/dev-transpile-watch.test.cjs +37 -31
  60. package/tests/eslint-rules.test.cjs +185 -8
  61. package/tests/mcp.test.cjs +90 -0
  62. package/tests/scaffold-templates.test.cjs +18 -0
  63. package/tests/server-app-report-error.test.cjs +135 -0
  64. package/tests/worktree-bootstrap.test.cjs +206 -0
  65. package/types/aliases.d.ts +0 -5
  66. package/types/controller-input.test.ts +23 -17
  67. package/types/controller-request-context.test.ts +10 -11
  68. package/cli/commands/migrate.ts +0 -51
  69. package/cli/migrate/pageContract.ts +0 -516
  70. package/docs/migrate-from-2.1.3.md +0 -396
  71. package/scripts/cleanup-generated-controllers.ts +0 -62
  72. package/scripts/fix-reference-app-typing.ts +0 -490
  73. package/scripts/format-router-registrations.ts +0 -119
  74. package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
  75. package/scripts/refactor-client-app-imports.ts +0 -244
  76. package/scripts/refactor-client-pages.ts +0 -587
  77. package/scripts/refactor-server-controllers.ts +0 -471
  78. package/scripts/refactor-server-runtime-aliases.ts +0 -360
  79. package/scripts/restore-client-app-import-files.ts +0 -41
@@ -10,13 +10,13 @@ type TRouteRuntime = 'client' | 'server';
10
10
  export type TIndexedSourceLocation = { line: number; column: number };
11
11
  export type TIndexedRouteTargetResolution = 'literal' | 'static-expression' | 'dynamic-expression';
12
12
 
13
- type TImportedService = { importedName: string; localName: string };
14
-
15
- type TRouteDefinition = {
16
- args: ts.NodeArray<ts.Expression>;
13
+ type TExplicitRouteDefinition = {
17
14
  methodName: string;
18
- serviceLocalName: string;
19
- callExpression: ts.CallExpression;
15
+ sourceLocation: TIndexedSourceLocation;
16
+ targetExpression: ts.Expression;
17
+ optionsExpression?: ts.Expression;
18
+ optionsArg?: ts.ObjectLiteralExpression;
19
+ hasData: boolean;
20
20
  };
21
21
 
22
22
  export type TIndexedRouteDefinition = {
@@ -44,11 +44,16 @@ type TWriteGeneratedRouteModuleOptions = {
44
44
  side: TRouteSide;
45
45
  sourceFilepath: string;
46
46
  clientRoute?: TGeneratedClientRouteModuleOptions;
47
- routeSourceFilepaths?: Set<string>;
48
47
  };
49
48
 
50
- const clientRouterImportSources = new Set(['@client/router', '@/client/router']);
51
- const routerMethods = new Set(['page', 'error', 'get', 'post', 'put', 'delete', 'patch']);
49
+ const legacyRouterMethods = new Set(['page', 'error', 'all', 'options', 'get', 'post', 'put', 'delete', 'patch', 'express']);
50
+ const routeDefinitionHelpers = new Set([
51
+ 'definePageRoute',
52
+ 'defineErrorRoute',
53
+ 'defineServerRoute',
54
+ 'defineServerRoutes',
55
+ ]);
56
+ const serverRouteMethods = new Set(['*', 'all', 'options', 'get', 'post', 'put', 'patch', 'delete']);
52
57
 
53
58
  const parseSourceFile = (filepath: string, code: string) =>
54
59
  ts.createSourceFile(
@@ -168,6 +173,108 @@ const tryEvaluateStaticExpression = (
168
173
  return undefined;
169
174
  };
170
175
 
176
+ const unwrapStaticExpression = (node: ts.Expression): ts.Expression => {
177
+ if (ts.isParenthesizedExpression(node)) return unwrapStaticExpression(node.expression);
178
+ if (ts.isAsExpression(node)) return unwrapStaticExpression(node.expression);
179
+ if (ts.isTypeAssertionExpression(node)) return unwrapStaticExpression(node.expression);
180
+ if (ts.isSatisfiesExpression(node)) return unwrapStaticExpression(node.expression);
181
+ if (ts.isNonNullExpression(node)) return unwrapStaticExpression(node.expression);
182
+
183
+ return node;
184
+ };
185
+
186
+ const isStaticSerializableExpression = (
187
+ node: ts.Expression,
188
+ bindingInitializers: Map<string, ts.Expression>,
189
+ resolvedBindings: Map<string, string | number | undefined>,
190
+ activeBindings = new Set<string>(),
191
+ ): boolean => {
192
+ const expression = unwrapStaticExpression(node);
193
+
194
+ if (
195
+ ts.isStringLiteral(expression) ||
196
+ ts.isNoSubstitutionTemplateLiteral(expression) ||
197
+ ts.isNumericLiteral(expression) ||
198
+ expression.kind === ts.SyntaxKind.TrueKeyword ||
199
+ expression.kind === ts.SyntaxKind.FalseKeyword ||
200
+ expression.kind === ts.SyntaxKind.NullKeyword
201
+ ) {
202
+ return true;
203
+ }
204
+
205
+ if (
206
+ ts.isPrefixUnaryExpression(expression) ||
207
+ ts.isBinaryExpression(expression) ||
208
+ ts.isTemplateExpression(expression)
209
+ ) {
210
+ return tryEvaluateStaticExpression(expression, bindingInitializers, resolvedBindings) !== undefined;
211
+ }
212
+
213
+ if (ts.isIdentifier(expression)) {
214
+ if (activeBindings.has(expression.text)) return false;
215
+
216
+ const initializer = bindingInitializers.get(expression.text);
217
+ if (!initializer) return false;
218
+
219
+ activeBindings.add(expression.text);
220
+ const isStatic = isStaticSerializableExpression(initializer, bindingInitializers, resolvedBindings, activeBindings);
221
+ activeBindings.delete(expression.text);
222
+
223
+ return isStatic;
224
+ }
225
+
226
+ if (ts.isArrayLiteralExpression(expression)) {
227
+ return expression.elements.every((element) => {
228
+ if (ts.isSpreadElement(element)) return false;
229
+
230
+ return isStaticSerializableExpression(element, bindingInitializers, resolvedBindings, activeBindings);
231
+ });
232
+ }
233
+
234
+ if (ts.isObjectLiteralExpression(expression)) {
235
+ return expression.properties.every((property) => {
236
+ if (ts.isPropertyAssignment(property)) {
237
+ if (ts.isComputedPropertyName(property.name)) return false;
238
+
239
+ return isStaticSerializableExpression(
240
+ property.initializer,
241
+ bindingInitializers,
242
+ resolvedBindings,
243
+ activeBindings,
244
+ );
245
+ }
246
+
247
+ if (ts.isShorthandPropertyAssignment(property)) {
248
+ return isStaticSerializableExpression(
249
+ property.name,
250
+ bindingInitializers,
251
+ resolvedBindings,
252
+ activeBindings,
253
+ );
254
+ }
255
+
256
+ return false;
257
+ });
258
+ }
259
+
260
+ return false;
261
+ };
262
+
263
+ const assertStaticSerializableMetadata = (
264
+ sourceFile: ts.SourceFile,
265
+ expression: ts.Expression,
266
+ label: string,
267
+ bindingInitializers: Map<string, ts.Expression>,
268
+ resolvedBindings: Map<string, string | number | undefined>,
269
+ ) => {
270
+ if (isStaticSerializableExpression(expression, bindingInitializers, resolvedBindings)) return;
271
+
272
+ const location = getNodeLocation(sourceFile, expression);
273
+ throw new Error(
274
+ `${sourceFile.fileName}:${location.line}:${location.column} ${label} must be a serializable static literal or const-only expression. Runtime app, request, service, import, function, and property references are only allowed inside data/render/handler callbacks.`,
275
+ );
276
+ };
277
+
171
278
  const collectStaticBindings = (sourceFile: ts.SourceFile) => {
172
279
  const bindingInitializers = new Map<string, ts.Expression>();
173
280
  const resolvedBindings = new Map<string, string | number | undefined>();
@@ -198,369 +305,336 @@ const collectStaticBindings = (sourceFile: ts.SourceFile) => {
198
305
  return resolvedBindings;
199
306
  };
200
307
 
201
- const getRouteOptionMetadata = (node: ts.ObjectLiteralExpression | undefined) => {
202
- const optionKeys = node ? getObjectLiteralPropertyKeys(node) : [];
203
- const normalizedOptionKeys: string[] = [];
204
- const invalidOptionKeys: string[] = [];
205
- const reservedOptionKeys: string[] = [];
308
+ const collectStaticBindingInitializers = (sourceFile: ts.SourceFile) => {
309
+ const bindingInitializers = new Map<string, ts.Expression>();
206
310
 
207
- for (const optionKey of optionKeys) {
208
- try {
209
- const normalizedOptionKey = getRouteOptionKey(optionKey);
311
+ for (const statement of sourceFile.statements) {
312
+ if (!ts.isVariableStatement(statement)) continue;
313
+ if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
210
314
 
211
- if (normalizedOptionKey) {
212
- normalizedOptionKeys.push(normalizedOptionKey);
213
- continue;
214
- }
315
+ for (const declaration of statement.declarationList.declarations) {
316
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
215
317
 
216
- invalidOptionKeys.push(optionKey);
217
- } catch (error) {
218
- reservedOptionKeys.push(optionKey);
318
+ bindingInitializers.set(declaration.name.text, declaration.initializer);
219
319
  }
220
320
  }
221
321
 
222
- return { optionKeys, normalizedOptionKeys, invalidOptionKeys, reservedOptionKeys };
322
+ return bindingInitializers;
223
323
  };
224
324
 
225
- const buildInjectedRouteMetadata = (sourceFilepath: string, sourceLocation: TIndexedSourceLocation, extra: string[] = []) =>
226
- `{ filepath: ${JSON.stringify(normalizeFilepath(sourceFilepath))}, sourceLocation: { line: ${sourceLocation.line}, column: ${sourceLocation.column} }${extra.length > 0 ? `, ${extra.join(', ')}` : ''} }`;
227
-
228
- const routeModuleExtensions = ['.ts', '.tsx', '.js', '.jsx'];
325
+ const getPropertyAssignment = (node: ts.ObjectLiteralExpression, propertyName: string) => {
326
+ for (const property of node.properties) {
327
+ if (!ts.isPropertyAssignment(property)) continue;
229
328
 
230
- const resolveRouteImport = (sourceFilepath: string, moduleSpecifier: string, routeSourceFilepaths?: Set<string>) => {
231
- if (!routeSourceFilepaths || !moduleSpecifier.startsWith('.')) return undefined;
232
-
233
- const absoluteBasePath = path.resolve(path.dirname(sourceFilepath), moduleSpecifier);
234
- const candidates = [
235
- absoluteBasePath,
236
- ...routeModuleExtensions.map((extension) => absoluteBasePath + extension),
237
- ...routeModuleExtensions.map((extension) => path.join(absoluteBasePath, `index${extension}`)),
238
- ];
329
+ const key = getObjectLiteralPropertyKey(property.name);
330
+ if (key === propertyName) return property.initializer;
331
+ }
239
332
 
240
- return candidates.find((candidate) => routeSourceFilepaths.has(normalizeFilepath(candidate)));
333
+ return undefined;
241
334
  };
242
335
 
243
- const addImportedService = (importedServices: TImportedService[], importedName: string, localName: string) => {
244
- if (
245
- importedServices.some(
246
- (importedService) =>
247
- importedService.importedName === importedName && importedService.localName === localName,
248
- )
249
- ) {
250
- return;
251
- }
252
-
253
- importedServices.push({ importedName, localName });
336
+ const getCallExpressionName = (node: ts.Expression) => {
337
+ if (ts.isIdentifier(node)) return node.text;
338
+ if (ts.isPropertyAccessExpression(node)) return node.name.text;
339
+ return undefined;
254
340
  };
255
341
 
256
- const collectImportedServices = (
342
+ const resolveIdentifierExpression = (
343
+ expression: ts.Expression,
257
344
  sourceFile: ts.SourceFile,
258
- side: TRouteSide,
259
- stripRanges: Array<{ start: number; end: number }>,
260
- ) => {
261
- const importedServices: TImportedService[] = [];
262
-
263
- for (const statement of sourceFile.statements) {
264
- if (!ts.isImportDeclaration(statement)) continue;
265
- if (!statement.importClause) continue;
266
- if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
345
+ ): ts.Expression => {
346
+ const unwrapped = unwrapStaticExpression(expression);
347
+ if (!ts.isIdentifier(unwrapped)) return unwrapped;
267
348
 
268
- const source = statement.moduleSpecifier.text;
269
- const isClientRouterImport = side === 'client' && clientRouterImportSources.has(source);
270
-
271
- if (source !== '@app' && !isClientRouterImport) continue;
272
-
273
- if (isClientRouterImport && statement.importClause.name) {
274
- addImportedService(importedServices, 'Router', statement.importClause.name.text);
275
- }
349
+ const bindingInitializers = collectStaticBindingInitializers(sourceFile);
350
+ return bindingInitializers.get(unwrapped.text) || unwrapped;
351
+ };
276
352
 
277
- for (const specifier of statement.importClause.namedBindings
278
- ? ts.isNamedImports(statement.importClause.namedBindings)
279
- ? statement.importClause.namedBindings.elements
280
- : []
281
- : []) {
282
- addImportedService(
283
- importedServices,
284
- specifier.propertyName?.text || specifier.name.text,
285
- specifier.name.text,
286
- );
353
+ const getDefaultRouteDefinitionExpression = (sourceFile: ts.SourceFile) => {
354
+ for (const statement of sourceFile.statements) {
355
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
356
+ return resolveIdentifierExpression(statement.expression, sourceFile);
287
357
  }
288
-
289
- stripRanges.push({ start: statement.getStart(sourceFile), end: statement.getEnd() });
290
358
  }
291
359
 
292
- return importedServices;
293
- };
294
-
295
- const collectNestedRouteImports = (
296
- sourceFile: ts.SourceFile,
297
- sourceFilepath: string,
298
- routeSourceFilepaths: Set<string> | undefined,
299
- stripRanges: Array<{ start: number; end: number }>,
300
- ) => {
301
360
  for (const statement of sourceFile.statements) {
302
- if (!ts.isImportDeclaration(statement)) continue;
303
- if (statement.importClause) continue;
304
- if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
305
-
306
- const routeImportPath = resolveRouteImport(
307
- sourceFilepath,
308
- statement.moduleSpecifier.text,
309
- routeSourceFilepaths,
310
- );
311
-
312
- if (!routeImportPath) continue;
361
+ if (!ts.isVariableStatement(statement)) continue;
362
+ if (!statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword)) continue;
313
363
 
314
- stripRanges.push({ start: statement.getStart(sourceFile), end: statement.getEnd() });
364
+ for (const declaration of statement.declarationList.declarations) {
365
+ if (declaration.initializer) return declaration.initializer;
366
+ }
315
367
  }
316
- };
317
368
 
318
- const collectRouteDefinitions = (
319
- sourceFile: ts.SourceFile,
320
- importedServices: TImportedService[],
321
- stripRanges: Array<{ start: number; end: number }>,
322
- ) => {
323
- const importedServiceNames = new Set(importedServices.map((importedService) => importedService.localName));
324
- const definitions: TRouteDefinition[] = [];
369
+ return undefined;
370
+ };
325
371
 
372
+ const assertNoLegacyRouteMagic = (sourceFile: ts.SourceFile, side: TRouteSide) => {
326
373
  for (const statement of sourceFile.statements) {
374
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
375
+ const source = statement.moduleSpecifier.text;
376
+
377
+ if (source === '@app') {
378
+ const location = getNodeLocation(sourceFile, statement);
379
+ throw new Error(
380
+ `${sourceFile.fileName}:${location.line}:${location.column} imports @app. Route modules must export define*Route definitions and receive app services through typed runtime callback context.`,
381
+ );
382
+ }
383
+ }
384
+
327
385
  if (!ts.isExpressionStatement(statement)) continue;
328
386
  if (!ts.isCallExpression(statement.expression)) continue;
329
387
  if (!ts.isPropertyAccessExpression(statement.expression.expression)) continue;
330
388
 
331
389
  const callee = statement.expression.expression;
332
390
  if (!ts.isIdentifier(callee.expression)) continue;
333
- if (!routerMethods.has(callee.name.text)) continue;
334
- if (!importedServiceNames.has(callee.expression.text)) continue;
335
-
336
- definitions.push({
337
- args: statement.expression.arguments,
338
- methodName: callee.name.text,
339
- serviceLocalName: callee.expression.text,
340
- callExpression: statement.expression,
341
- });
391
+ if (callee.expression.text !== 'Router') continue;
392
+ if (!legacyRouterMethods.has(callee.name.text)) continue;
342
393
 
343
- stripRanges.push({ start: statement.getStart(sourceFile), end: statement.getEnd() });
394
+ const location = getNodeLocation(sourceFile, statement);
395
+ throw new Error(
396
+ `${sourceFile.fileName}:${location.line}:${location.column} uses top-level ${callee.expression.text}.${callee.name.text}(...). Route modules must export define*Route definitions instead.`,
397
+ );
344
398
  }
345
-
346
- return definitions;
347
399
  };
348
400
 
349
- const buildRemainingSource = (sourceFile: ts.SourceFile, stripRanges: Array<{ start: number; end: number }>) => {
350
- const sortedRanges = stripRanges.sort((a, b) => a.start - b.start);
351
- const chunks: string[] = [];
352
- let cursor = 0;
401
+ const parseExplicitRouteCall = (
402
+ sourceFile: ts.SourceFile,
403
+ side: TRouteSide,
404
+ node: ts.Expression,
405
+ ): TExplicitRouteDefinition[] => {
406
+ const expression = unwrapStaticExpression(resolveIdentifierExpression(node, sourceFile));
407
+ const staticBindingInitializers = collectStaticBindingInitializers(sourceFile);
408
+ const staticBindings = collectStaticBindings(sourceFile);
353
409
 
354
- for (const range of sortedRanges) {
355
- if (cursor < range.start) chunks.push(sourceFile.text.slice(cursor, range.start));
356
- cursor = Math.max(cursor, range.end);
410
+ if (!ts.isCallExpression(expression)) {
411
+ throw new Error(`Route module ${sourceFile.fileName} must default-export a define*Route(...) call.`);
357
412
  }
358
413
 
359
- if (cursor < sourceFile.text.length) {
360
- chunks.push(sourceFile.text.slice(cursor));
414
+ const helperName = getCallExpressionName(expression.expression);
415
+ if (!helperName || !routeDefinitionHelpers.has(helperName)) {
416
+ throw new Error(`Route module ${sourceFile.fileName} must default-export a define*Route(...) call.`);
361
417
  }
362
418
 
363
- return chunks.join('').trim();
364
- };
419
+ if (helperName === 'defineServerRoutes') {
420
+ if (side !== 'server') {
421
+ throw new Error(`Client route module ${sourceFile.fileName} cannot export defineServerRoutes(...).`);
422
+ }
365
423
 
366
- const normalizeRelativeImportPath = (value: string) => (value.startsWith('.') ? value : `./${value}`);
424
+ const [routesArg] = [...expression.arguments];
425
+ const routeListExpression =
426
+ routesArg && ts.isArrayLiteralExpression(routesArg)
427
+ ? routesArg
428
+ : routesArg &&
429
+ (ts.isArrowFunction(routesArg) || ts.isFunctionExpression(routesArg)) &&
430
+ ts.isArrayLiteralExpression(routesArg.body)
431
+ ? routesArg.body
432
+ : undefined;
433
+
434
+ if (!routeListExpression) {
435
+ throw new Error(`defineServerRoutes(...) in ${sourceFile.fileName} must receive a static array literal or a factory returning one.`);
436
+ }
367
437
 
368
- const rebaseRelativeModuleSpecifiers = (code: string, outputFilepath: string, sourceFilepath: string) => {
369
- const outputDir = path.dirname(outputFilepath);
370
- const sourceDir = path.dirname(sourceFilepath);
371
- const sourceFile = parseSourceFile(outputFilepath, code);
372
- const replacements: Array<{ start: number; end: number; value: string }> = [];
438
+ return routeListExpression.elements.map((element) => parseExplicitRouteCall(sourceFile, side, element)).flat();
439
+ }
373
440
 
374
- const addReplacement = (literal: ts.StringLiteralLike) => {
375
- if (!literal.text.startsWith('.')) return;
441
+ const [definitionArg] = [...expression.arguments];
442
+ if (!definitionArg || !ts.isObjectLiteralExpression(definitionArg)) {
443
+ throw new Error(`${helperName}(...) in ${sourceFile.fileName} must receive an object literal.`);
444
+ }
376
445
 
377
- const absoluteTarget = path.resolve(sourceDir, literal.text);
378
- const nextRelativePath = normalizeRelativeImportPath(
379
- path.relative(outputDir, absoluteTarget).replace(/\\/g, '/'),
446
+ const sourceLocation = getNodeLocation(sourceFile, expression);
447
+ const optionsExpression = getPropertyAssignment(definitionArg, 'options');
448
+ const resolvedOptionsExpression = optionsExpression
449
+ ? unwrapStaticExpression(resolveIdentifierExpression(optionsExpression, sourceFile))
450
+ : undefined;
451
+ const optionsArg =
452
+ resolvedOptionsExpression && ts.isObjectLiteralExpression(resolvedOptionsExpression)
453
+ ? resolvedOptionsExpression
454
+ : undefined;
455
+
456
+ if (optionsExpression) {
457
+ assertStaticSerializableMetadata(
458
+ sourceFile,
459
+ optionsExpression,
460
+ `${helperName} options`,
461
+ staticBindingInitializers,
462
+ staticBindings,
380
463
  );
381
464
 
382
- replacements.push({
383
- start: literal.getStart(sourceFile),
384
- end: literal.getEnd(),
385
- value: JSON.stringify(nextRelativePath),
386
- });
387
- };
388
-
389
- const visit = (node: ts.Node) => {
390
- if (
391
- (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
392
- node.moduleSpecifier &&
393
- ts.isStringLiteral(node.moduleSpecifier)
394
- ) {
395
- addReplacement(node.moduleSpecifier);
396
- }
397
-
398
- if (
399
- ts.isCallExpression(node) &&
400
- node.arguments.length > 0 &&
401
- ts.isStringLiteral(node.arguments[0]) &&
402
- (node.expression.kind === ts.SyntaxKind.ImportKeyword ||
403
- (ts.isIdentifier(node.expression) && node.expression.text === 'require'))
404
- ) {
405
- addReplacement(node.arguments[0]);
406
- }
407
-
408
- if (
409
- ts.isImportTypeNode(node) &&
410
- ts.isLiteralTypeNode(node.argument) &&
411
- ts.isStringLiteral(node.argument.literal)
412
- ) {
413
- addReplacement(node.argument.literal);
465
+ if (!optionsArg) {
466
+ const location = getNodeLocation(sourceFile, optionsExpression);
467
+ throw new Error(
468
+ `${sourceFile.fileName}:${location.line}:${location.column} ${helperName} options must resolve to a static object literal.`,
469
+ );
414
470
  }
471
+ }
415
472
 
416
- ts.forEachChild(node, visit);
417
- };
418
-
419
- ts.forEachChild(sourceFile, visit);
420
-
421
- return replacements
422
- .sort((a, b) => b.start - a.start)
423
- .reduce(
424
- (currentCode, replacement) =>
425
- currentCode.slice(0, replacement.start) + replacement.value + currentCode.slice(replacement.end),
426
- code,
473
+ if (helperName === 'definePageRoute') {
474
+ if (side !== 'client') throw new Error(`Server route module ${sourceFile.fileName} cannot export definePageRoute(...).`);
475
+
476
+ const pathExpression = getPropertyAssignment(definitionArg, 'path');
477
+ const dataExpression = getPropertyAssignment(definitionArg, 'data');
478
+ const renderExpression = getPropertyAssignment(definitionArg, 'render');
479
+ if (!pathExpression) throw new Error(`definePageRoute(...) in ${sourceFile.fileName} is missing path.`);
480
+ if (!optionsExpression) throw new Error(`definePageRoute(...) in ${sourceFile.fileName} is missing options.`);
481
+ if (!dataExpression) throw new Error(`definePageRoute(...) in ${sourceFile.fileName} is missing data.`);
482
+ if (!renderExpression) throw new Error(`definePageRoute(...) in ${sourceFile.fileName} is missing render.`);
483
+
484
+ assertStaticSerializableMetadata(
485
+ sourceFile,
486
+ pathExpression,
487
+ 'definePageRoute path',
488
+ staticBindingInitializers,
489
+ staticBindings,
427
490
  );
428
- };
429
491
 
430
- const buildDestructuring = (importedServices: TImportedService[]) => {
431
- const parts = importedServices.map(({ importedName, localName }) =>
432
- importedName === localName ? importedName : `${importedName}: ${localName}`,
433
- );
434
-
435
- return `const { ${parts.join(', ')} } = app;`;
436
- };
492
+ return [
493
+ {
494
+ methodName: 'page',
495
+ sourceLocation,
496
+ targetExpression: pathExpression,
497
+ optionsExpression,
498
+ optionsArg,
499
+ hasData: dataExpression?.kind !== ts.SyntaxKind.NullKeyword,
500
+ },
501
+ ];
502
+ }
437
503
 
438
- const getClientRouteSignature = (sourceFile: ts.SourceFile, definition: TRouteDefinition) => {
439
- const [, ...routeArgs] = [...definition.args];
504
+ if (helperName === 'defineErrorRoute') {
505
+ if (side !== 'client') throw new Error(`Server route module ${sourceFile.fileName} cannot export defineErrorRoute(...).`);
506
+
507
+ const codeExpression = getPropertyAssignment(definitionArg, 'code');
508
+ const renderExpression = getPropertyAssignment(definitionArg, 'render');
509
+ if (!codeExpression) throw new Error(`defineErrorRoute(...) in ${sourceFile.fileName} is missing code.`);
510
+ if (!optionsExpression) throw new Error(`defineErrorRoute(...) in ${sourceFile.fileName} is missing options.`);
511
+ if (!renderExpression) throw new Error(`defineErrorRoute(...) in ${sourceFile.fileName} is missing render.`);
512
+
513
+ assertStaticSerializableMetadata(
514
+ sourceFile,
515
+ codeExpression,
516
+ 'defineErrorRoute code',
517
+ staticBindingInitializers,
518
+ staticBindings,
519
+ );
440
520
 
441
- if (definition.methodName === 'error') {
442
- if (routeArgs.length === 2) {
443
- return {
521
+ return [
522
+ {
523
+ methodName: 'error',
524
+ sourceLocation,
525
+ targetExpression: codeExpression,
526
+ optionsExpression,
527
+ optionsArg,
444
528
  hasData: false,
445
- optionsExpression: routeArgs[0],
446
- optionsArg: ts.isObjectLiteralExpression(routeArgs[0]) ? routeArgs[0] : undefined,
447
- renderArg: routeArgs[1],
448
- };
449
- }
450
-
451
- throw new Error(
452
- `Unsupported client error route signature in ${sourceFile.fileName}. Expected Router.error(code, options, render).`,
453
- );
529
+ },
530
+ ];
454
531
  }
455
532
 
456
- if (routeArgs.length === 3) {
457
- return {
458
- hasData: routeArgs[1].kind !== ts.SyntaxKind.NullKeyword,
459
- optionsExpression: routeArgs[0],
460
- optionsArg: ts.isObjectLiteralExpression(routeArgs[0]) ? routeArgs[0] : undefined,
461
- dataArg: routeArgs[1],
462
- renderArg: routeArgs[2],
463
- };
464
- }
533
+ const methodExpression = getPropertyAssignment(definitionArg, 'method');
534
+ const pathExpression = getPropertyAssignment(definitionArg, 'path');
535
+ const handlerExpression = getPropertyAssignment(definitionArg, 'handler');
536
+ if (!methodExpression) throw new Error(`defineServerRoute(...) in ${sourceFile.fileName} is missing method.`);
537
+ if (!pathExpression) throw new Error(`defineServerRoute(...) in ${sourceFile.fileName} is missing path.`);
538
+ if (!optionsExpression) throw new Error(`defineServerRoute(...) in ${sourceFile.fileName} is missing options.`);
539
+ if (!handlerExpression) throw new Error(`defineServerRoute(...) in ${sourceFile.fileName} is missing handler.`);
540
+
541
+ assertStaticSerializableMetadata(
542
+ sourceFile,
543
+ methodExpression,
544
+ 'defineServerRoute method',
545
+ staticBindingInitializers,
546
+ staticBindings,
547
+ );
548
+ assertStaticSerializableMetadata(
549
+ sourceFile,
550
+ pathExpression,
551
+ 'defineServerRoute path',
552
+ staticBindingInitializers,
553
+ staticBindings,
554
+ );
465
555
 
466
- throw new Error(
467
- `Unsupported client page route signature in ${sourceFile.fileName}. Expected Router.page(path, options, data, render).`,
556
+ const methodName = tryEvaluateStaticExpression(
557
+ methodExpression,
558
+ staticBindingInitializers,
559
+ staticBindings,
468
560
  );
469
- };
470
561
 
471
- const buildClientRegisterArgs = (
472
- sourceFile: ts.SourceFile,
473
- definition: TRouteDefinition,
474
- clientRoute: TGeneratedClientRouteModuleOptions,
475
- ) => {
476
- const { optionsExpression, renderArg } = getClientRouteSignature(sourceFile, definition);
477
- const sourceLocation = getNodeLocation(sourceFile, definition.callExpression);
478
- const injectedOptions = buildInjectedRouteMetadata(sourceFile.fileName, sourceLocation, [
479
- `id: ${JSON.stringify(clientRoute.chunkId)}`,
480
- ]);
562
+ if (typeof methodName !== 'string') {
563
+ throw new Error(`defineServerRoute(...) in ${sourceFile.fileName} must use a static string method.`);
564
+ }
481
565
 
482
- if (definition.methodName === 'error') {
483
- return [
484
- `{ ...(${getNodeText(sourceFile, optionsExpression)}), ...${injectedOptions} }`,
485
- getNodeText(sourceFile, renderArg),
486
- ];
566
+ const normalizedMethod = methodName === '*' ? 'all' : methodName.toLowerCase();
567
+ if (!serverRouteMethods.has(normalizedMethod)) {
568
+ throw new Error(`defineServerRoute(...) in ${sourceFile.fileName} uses unsupported method "${methodName}".`);
487
569
  }
488
570
 
489
- const { dataArg } = getClientRouteSignature(sourceFile, definition);
490
571
  return [
491
- `{ ...(${getNodeText(sourceFile, optionsExpression)}), ...${injectedOptions} }`,
492
- getNodeText(sourceFile, dataArg!),
493
- getNodeText(sourceFile, renderArg),
572
+ {
573
+ methodName: normalizedMethod,
574
+ sourceLocation,
575
+ targetExpression: pathExpression,
576
+ optionsExpression,
577
+ optionsArg,
578
+ hasData: false,
579
+ },
494
580
  ];
495
581
  };
496
582
 
497
- const buildServerRegisterArgs = (sourceFile: ts.SourceFile, definition: TRouteDefinition) => {
498
- const sourceLocation = getNodeLocation(sourceFile, definition.callExpression);
499
- const [targetArg, ...routeArgs] = [...definition.args];
500
- const controllerArg = routeArgs[routeArgs.length - 1];
501
- const optionsArg =
502
- routeArgs.length >= 2 && ts.isObjectLiteralExpression(routeArgs[0]) ? routeArgs[0] : undefined;
503
- const injectedOptions = buildInjectedRouteMetadata(sourceFile.fileName, sourceLocation);
583
+ const collectExplicitRouteDefinitions = (sourceFile: ts.SourceFile, side: TRouteSide) => {
584
+ assertNoLegacyRouteMagic(sourceFile, side);
504
585
 
505
- if (routeArgs.length === 1) {
506
- return [getNodeText(sourceFile, targetArg), injectedOptions, getNodeText(sourceFile, controllerArg)];
586
+ const definitionExpression = getDefaultRouteDefinitionExpression(sourceFile);
587
+ if (!definitionExpression) {
588
+ throw new Error(`No route definition export was found in ${sourceFile.fileName}. Expected export default define*Route(...).`);
507
589
  }
508
590
 
509
- if (optionsArg) {
510
- return [
511
- getNodeText(sourceFile, targetArg),
512
- `{ ...(${getNodeText(sourceFile, optionsArg)}), ...${injectedOptions} }`,
513
- getNodeText(sourceFile, controllerArg),
514
- ];
515
- }
516
-
517
- return [getNodeText(sourceFile, targetArg), ...routeArgs.map((arg) => getNodeText(sourceFile, arg))];
591
+ return parseExplicitRouteCall(sourceFile, side, definitionExpression);
518
592
  };
519
593
 
520
- const buildRegisterStatements = (
521
- sourceFile: ts.SourceFile,
522
- side: TRouteSide,
523
- definitions: TRouteDefinition[],
524
- clientRoute?: TGeneratedClientRouteModuleOptions,
525
- ) => {
526
- if (side === 'client') {
527
- if (!clientRoute) {
528
- throw new Error(`Missing client route metadata for ${sourceFile.fileName}.`);
529
- }
594
+ const getRouteOptionMetadata = (node: ts.ObjectLiteralExpression | undefined) => {
595
+ const optionKeys = node ? getObjectLiteralPropertyKeys(node) : [];
596
+ const normalizedOptionKeys: string[] = [];
597
+ const invalidOptionKeys: string[] = [];
598
+ const reservedOptionKeys: string[] = [];
530
599
 
531
- if (definitions.length !== 1) {
532
- throw new Error(
533
- `Frontend route definition files can contain only one route definition. ${definitions.length} were found in ${sourceFile.fileName}.`,
534
- );
535
- }
600
+ for (const optionKey of optionKeys) {
601
+ try {
602
+ const normalizedOptionKey = getRouteOptionKey(optionKey);
536
603
 
537
- const definition = definitions[0];
538
- const [routePath, ...routeArgs] = [...definition.args];
539
- const finalArgs = [
540
- getNodeText(sourceFile, routePath),
541
- ...buildClientRegisterArgs(sourceFile, definition, clientRoute),
542
- ];
604
+ if (normalizedOptionKey) {
605
+ normalizedOptionKeys.push(normalizedOptionKey);
606
+ continue;
607
+ }
543
608
 
544
- return [`return ${definition.serviceLocalName}.${definition.methodName}(${finalArgs.join(', ')});`];
609
+ invalidOptionKeys.push(optionKey);
610
+ } catch (error) {
611
+ reservedOptionKeys.push(optionKey);
612
+ }
545
613
  }
546
614
 
547
- return definitions.map((definition) => {
548
- const args = buildServerRegisterArgs(sourceFile, definition);
549
-
550
- return `${definition.serviceLocalName}.${definition.methodName}(${args.join(', ')});`;
551
- });
615
+ return { optionKeys, normalizedOptionKeys, invalidOptionKeys, reservedOptionKeys };
552
616
  };
553
617
 
618
+ const buildInjectedRouteMetadata = (sourceFilepath: string, sourceLocation: TIndexedSourceLocation, extra: string[] = []) =>
619
+ `{ filepath: ${JSON.stringify(normalizeFilepath(sourceFilepath))}, sourceLocation: { line: ${sourceLocation.line}, column: ${sourceLocation.column} }${extra.length > 0 ? `, ${extra.join(', ')}` : ''} }`;
620
+
621
+ const normalizeRelativeImportPath = (value: string) => (value.startsWith('.') ? value : `./${value}`);
622
+
554
623
  export const getGeneratedRouteModuleFilepath = (generatedRoot: string, sourceRoot: string, sourceFilepath: string) =>
555
624
  path.join(generatedRoot, 'route-modules', path.relative(sourceRoot, sourceFilepath));
556
625
 
626
+ const getSourceImportPath = (outputFilepath: string, sourceFilepath: string) =>
627
+ normalizeRelativeImportPath(path.relative(path.dirname(outputFilepath), sourceFilepath).replace(/\\/g, '/')).replace(
628
+ /\.(ts|tsx|js|jsx)$/,
629
+ '',
630
+ );
631
+
557
632
  export const indexRouteDefinitions = ({ side, sourceFilepath }: { side: TRouteSide; sourceFilepath: string }) => {
558
633
  const code = fs.readFileSync(sourceFilepath, 'utf8');
559
634
  const sourceFile = parseSourceFile(sourceFilepath, code);
560
- const stripRanges: Array<{ start: number; end: number }> = [];
561
- const importedServices = collectImportedServices(sourceFile, side, stripRanges);
562
- const definitions = collectRouteDefinitions(sourceFile, importedServices, stripRanges);
635
+ const definitions = collectExplicitRouteDefinitions(sourceFile, side);
563
636
  const staticBindings = collectStaticBindings(sourceFile);
637
+ const staticBindingInitializers = collectStaticBindingInitializers(sourceFile);
564
638
 
565
639
  if (definitions.length === 0) {
566
640
  throw new Error(`No route definitions were found in ${sourceFilepath}.`);
@@ -573,85 +647,96 @@ export const indexRouteDefinitions = ({ side, sourceFilepath }: { side: TRouteSi
573
647
  }
574
648
 
575
649
  return definitions.map<TIndexedRouteDefinition>((definition) => {
576
- const sourceLocation = getNodeLocation(sourceFile, definition.callExpression);
577
- const resolveStaticValue = (node: ts.Expression) => tryEvaluateStaticExpression(node, new Map(), staticBindings);
650
+ const sourceLocation = definition.sourceLocation;
651
+ const resolveStaticValue = (node: ts.Expression) =>
652
+ tryEvaluateStaticExpression(node, staticBindingInitializers, staticBindings);
578
653
 
579
654
  if (side === 'client') {
580
- const targetArg = definition.args[0];
581
- const clientSignature = getClientRouteSignature(sourceFile, definition);
582
- const optionMetadata = getRouteOptionMetadata(clientSignature.optionsArg);
655
+ const targetArg = definition.targetExpression;
656
+ const optionMetadata = getRouteOptionMetadata(definition.optionsArg);
583
657
  const resolvedStaticValue = resolveStaticValue(targetArg);
584
658
 
585
659
  return definition.methodName === 'error'
586
- ? {
587
- methodName: definition.methodName,
588
- serviceLocalName: definition.serviceLocalName,
589
- sourceLocation,
590
- targetResolution:
591
- getLiteralNumberValue(targetArg) !== undefined
592
- ? 'literal'
593
- : typeof resolvedStaticValue === 'number'
594
- ? 'static-expression'
595
- : 'dynamic-expression',
596
- code:
597
- getLiteralNumberValue(targetArg) ??
598
- (typeof resolvedStaticValue === 'number' ? resolvedStaticValue : undefined),
599
- codeRaw: getNodeText(sourceFile, targetArg),
600
- optionKeys: optionMetadata.optionKeys,
601
- normalizedOptionKeys: optionMetadata.normalizedOptionKeys,
602
- invalidOptionKeys: optionMetadata.invalidOptionKeys,
603
- reservedOptionKeys: optionMetadata.reservedOptionKeys,
604
- optionsRaw: getNodeText(sourceFile, clientSignature.optionsExpression),
605
- hasData: false,
606
- }
607
- : {
608
- methodName: definition.methodName,
609
- serviceLocalName: definition.serviceLocalName,
610
- sourceLocation,
611
- targetResolution:
612
- getLiteralStringValue(targetArg) !== undefined
613
- ? 'literal'
614
- : typeof resolvedStaticValue === 'string'
615
- ? 'static-expression'
616
- : 'dynamic-expression',
617
- path:
618
- getLiteralStringValue(targetArg) ??
619
- (typeof resolvedStaticValue === 'string' ? resolvedStaticValue : undefined),
620
- pathRaw: getNodeText(sourceFile, targetArg),
621
- optionKeys: optionMetadata.optionKeys,
622
- normalizedOptionKeys: optionMetadata.normalizedOptionKeys,
623
- invalidOptionKeys: optionMetadata.invalidOptionKeys,
624
- reservedOptionKeys: optionMetadata.reservedOptionKeys,
625
- optionsRaw: getNodeText(sourceFile, clientSignature.optionsExpression),
626
- hasData: clientSignature.hasData,
627
- };
660
+ ? (() => {
661
+ const literalCode = getLiteralNumberValue(targetArg);
662
+ const code = literalCode ?? (typeof resolvedStaticValue === 'number' ? resolvedStaticValue : undefined);
663
+
664
+ if (code === undefined) {
665
+ const location = getNodeLocation(sourceFile, targetArg);
666
+ throw new Error(
667
+ `${sourceFile.fileName}:${location.line}:${location.column} defineErrorRoute code must resolve to a number.`,
668
+ );
669
+ }
670
+
671
+ return {
672
+ methodName: definition.methodName,
673
+ serviceLocalName: 'Router',
674
+ sourceLocation,
675
+ targetResolution: literalCode !== undefined ? 'literal' : 'static-expression',
676
+ code,
677
+ codeRaw: getNodeText(sourceFile, targetArg),
678
+ optionKeys: optionMetadata.optionKeys,
679
+ normalizedOptionKeys: optionMetadata.normalizedOptionKeys,
680
+ invalidOptionKeys: optionMetadata.invalidOptionKeys,
681
+ reservedOptionKeys: optionMetadata.reservedOptionKeys,
682
+ optionsRaw: definition.optionsExpression ? getNodeText(sourceFile, definition.optionsExpression) : undefined,
683
+ hasData: false,
684
+ };
685
+ })()
686
+ : (() => {
687
+ const literalPath = getLiteralStringValue(targetArg);
688
+ const routePath = literalPath ?? (typeof resolvedStaticValue === 'string' ? resolvedStaticValue : undefined);
689
+
690
+ if (routePath === undefined) {
691
+ const location = getNodeLocation(sourceFile, targetArg);
692
+ throw new Error(
693
+ `${sourceFile.fileName}:${location.line}:${location.column} definePageRoute path must resolve to a string.`,
694
+ );
695
+ }
696
+
697
+ return {
698
+ methodName: definition.methodName,
699
+ serviceLocalName: 'Router',
700
+ sourceLocation,
701
+ targetResolution: literalPath !== undefined ? 'literal' : 'static-expression',
702
+ path: routePath,
703
+ pathRaw: getNodeText(sourceFile, targetArg),
704
+ optionKeys: optionMetadata.optionKeys,
705
+ normalizedOptionKeys: optionMetadata.normalizedOptionKeys,
706
+ invalidOptionKeys: optionMetadata.invalidOptionKeys,
707
+ reservedOptionKeys: optionMetadata.reservedOptionKeys,
708
+ optionsRaw: definition.optionsExpression ? getNodeText(sourceFile, definition.optionsExpression) : undefined,
709
+ hasData: definition.hasData,
710
+ };
711
+ })();
628
712
  }
629
713
 
630
- const targetArg = definition.args[0];
631
- const optionsArg =
632
- definition.args.length >= 3 && ts.isObjectLiteralExpression(definition.args[1])
633
- ? definition.args[1]
634
- : undefined;
635
- const optionMetadata = getRouteOptionMetadata(optionsArg);
714
+ const targetArg = definition.targetExpression;
715
+ const optionMetadata = getRouteOptionMetadata(definition.optionsArg);
636
716
  const resolvedPath = getLiteralStringValue(targetArg) ?? resolveStaticValue(targetArg);
637
717
 
718
+ if (typeof resolvedPath !== 'string') {
719
+ const location = getNodeLocation(sourceFile, targetArg);
720
+ throw new Error(
721
+ `${sourceFile.fileName}:${location.line}:${location.column} defineServerRoute path must resolve to a string.`,
722
+ );
723
+ }
724
+
638
725
  return {
639
726
  methodName: definition.methodName,
640
- serviceLocalName: definition.serviceLocalName,
727
+ serviceLocalName: 'Router',
641
728
  sourceLocation,
642
729
  targetResolution:
643
730
  getLiteralStringValue(targetArg) !== undefined
644
731
  ? 'literal'
645
- : typeof resolvedPath === 'string'
646
- ? 'static-expression'
647
- : 'dynamic-expression',
648
- path: typeof resolvedPath === 'string' ? resolvedPath : undefined,
732
+ : 'static-expression',
733
+ path: resolvedPath,
649
734
  pathRaw: getNodeText(sourceFile, targetArg),
650
735
  optionKeys: optionMetadata.optionKeys,
651
736
  normalizedOptionKeys: optionMetadata.normalizedOptionKeys,
652
737
  invalidOptionKeys: optionMetadata.invalidOptionKeys,
653
738
  reservedOptionKeys: optionMetadata.reservedOptionKeys,
654
- optionsRaw: optionsArg ? getNodeText(sourceFile, optionsArg) : undefined,
739
+ optionsRaw: definition.optionsExpression ? getNodeText(sourceFile, definition.optionsExpression) : undefined,
655
740
  hasData: false,
656
741
  };
657
742
  });
@@ -663,26 +748,22 @@ export const writeGeneratedRouteModule = ({
663
748
  side,
664
749
  sourceFilepath,
665
750
  clientRoute,
666
- routeSourceFilepaths,
667
751
  }: TWriteGeneratedRouteModuleOptions) => {
668
752
  const code = fs.readFileSync(sourceFilepath, 'utf8');
669
753
  const sourceFile = parseSourceFile(sourceFilepath, code);
670
- const stripRanges: Array<{ start: number; end: number }> = [];
671
- const importedServices = collectImportedServices(sourceFile, side, stripRanges);
672
- collectNestedRouteImports(sourceFile, sourceFilepath, routeSourceFilepaths, stripRanges);
673
- const definitions = collectRouteDefinitions(sourceFile, importedServices, stripRanges);
754
+ const definitions = collectExplicitRouteDefinitions(sourceFile, side);
674
755
 
675
756
  if (definitions.length === 0) {
676
757
  throw new Error(`No route definitions were found in ${sourceFilepath}.`);
677
758
  }
678
759
 
679
- const remainingSource = rebaseRelativeModuleSpecifiers(
680
- buildRemainingSource(sourceFile, stripRanges),
681
- outputFilepath,
682
- sourceFilepath,
683
- );
684
- const registerStatements = buildRegisterStatements(sourceFile, side, definitions, clientRoute);
685
760
  const runtimeAppImportPath = runtime === 'client' ? '@/client/index' : '@/server/index';
761
+ const sourceImportPath = getSourceImportPath(outputFilepath, sourceFilepath);
762
+ const metadataEntries = definitions.map((definition) => {
763
+ const extra = side === 'client' && clientRoute ? [`id: ${JSON.stringify(clientRoute.chunkId)}`] : [];
764
+
765
+ return buildInjectedRouteMetadata(sourceFilepath, definition.sourceLocation, extra);
766
+ });
686
767
 
687
768
  const content = `/*----------------------------------
688
769
  - GENERATED FILE
@@ -691,12 +772,25 @@ export const writeGeneratedRouteModule = ({
691
772
  // This file is generated by Proteum from ${path.relative(process.cwd(), sourceFilepath).replace(/\\/g, '/')}.
692
773
  // Do not edit it manually.
693
774
 
694
- import type __GeneratedRouteApp from ${JSON.stringify(runtimeAppImportPath)};
695
-
696
- ${remainingSource}
697
- ${remainingSource ? '\n' : ''}export const __register = (app: __GeneratedRouteApp) => {
698
- ${buildDestructuring(importedServices)}
699
- ${registerStatements.join('\n ')}
775
+ import type __GeneratedRouteAppExport from ${JSON.stringify(runtimeAppImportPath)};
776
+ import __routeDefinition from ${JSON.stringify(sourceImportPath)};
777
+ import { normalizeRouteDefinitions, registerRouteDefinition } from '@common/router/definitions';
778
+
779
+ type __GeneratedRouteApp = __GeneratedRouteAppExport extends abstract new (...args: any[]) => infer __GeneratedRouteAppInstance
780
+ ? __GeneratedRouteAppInstance
781
+ : __GeneratedRouteAppExport;
782
+
783
+ const __routeMetadata = [
784
+ ${metadataEntries.join(',\n ')}
785
+ ];
786
+
787
+ export const __register = (app: __GeneratedRouteApp) => {
788
+ const __definitions = normalizeRouteDefinitions(__routeDefinition as any, app as any);
789
+ let __registeredRoute;
790
+ for (let __index = 0; __index < __definitions.length; __index += 1) {
791
+ __registeredRoute = registerRouteDefinition(app.Router as any, __definitions[__index] as any, __routeMetadata[__index]);
792
+ }
793
+ return __registeredRoute;
700
794
  };
701
795
  `;
702
796