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.
- package/README.md +81 -52
- package/agents/project/AGENTS.md +112 -31
- package/agents/project/CODING_STYLE.md +2 -2
- package/agents/project/app-root/AGENTS.md +1 -3
- package/agents/project/client/AGENTS.md +5 -1
- package/agents/project/client/pages/AGENTS.md +21 -9
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/optimizations.md +1 -1
- package/agents/project/root/AGENTS.md +105 -22
- package/agents/project/server/routes/AGENTS.md +30 -1
- package/agents/project/server/services/AGENTS.md +4 -0
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/doctor.ts +54 -3
- package/cli/commands/runtime.ts +6 -0
- package/cli/commands/worktree.ts +116 -0
- package/cli/compiler/artifacts/controllers.ts +16 -15
- package/cli/compiler/artifacts/discovery.ts +129 -17
- package/cli/compiler/artifacts/routing.ts +0 -5
- package/cli/compiler/artifacts/services.ts +253 -76
- package/cli/compiler/common/controllers.ts +159 -57
- package/cli/compiler/common/generatedRouteModules.ts +457 -363
- package/cli/mcp/router.ts +47 -3
- package/cli/presentation/commands.ts +25 -15
- package/cli/runtime/commands.ts +39 -12
- package/cli/runtime/worktreeBootstrap.ts +608 -0
- package/cli/scaffold/index.ts +28 -18
- package/cli/scaffold/templates.ts +44 -33
- package/cli/utils/agents.ts +14 -1
- package/client/app/index.ts +22 -5
- package/client/services/router/index.tsx +23 -3
- package/client/services/router/request/api.ts +16 -6
- package/common/dev/contractsDoctor.ts +1 -1
- package/common/dev/mcpPayloads.ts +8 -1
- package/common/env/proteumEnv.ts +14 -2
- package/common/router/contracts.ts +1 -1
- package/common/router/definitions.ts +177 -0
- package/common/router/index.ts +23 -12
- package/common/router/pageData.ts +5 -5
- package/common/router/register.ts +2 -2
- package/common/router/request/api.ts +12 -2
- package/docs/agent-routing.md +5 -2
- package/docs/diagnostics.md +2 -0
- package/docs/mcp.md +6 -3
- package/docs/migration-2.5.md +226 -0
- package/eslint.js +89 -42
- package/package.json +1 -1
- package/server/app/commands.ts +5 -1
- package/server/app/container/console/index.ts +1 -1
- package/server/app/controller/index.ts +98 -40
- package/server/app/index.ts +120 -3
- package/server/app/service/index.ts +5 -1
- package/server/index.ts +6 -2
- package/server/services/router/index.ts +50 -41
- package/server/services/router/response/index.ts +2 -2
- package/tests/agents-utils.test.cjs +14 -1
- package/tests/cli-mcp-command.test.cjs +84 -0
- package/tests/client-app-error-handling.test.cjs +100 -0
- package/tests/definition-contracts.test.cjs +453 -0
- package/tests/dev-transpile-watch.test.cjs +37 -31
- package/tests/eslint-rules.test.cjs +185 -8
- package/tests/mcp.test.cjs +90 -0
- package/tests/scaffold-templates.test.cjs +18 -0
- package/tests/server-app-report-error.test.cjs +135 -0
- package/tests/worktree-bootstrap.test.cjs +206 -0
- package/types/aliases.d.ts +0 -5
- package/types/controller-input.test.ts +23 -17
- package/types/controller-request-context.test.ts +10 -11
- package/cli/commands/migrate.ts +0 -51
- package/cli/migrate/pageContract.ts +0 -516
- package/docs/migrate-from-2.1.3.md +0 -396
- package/scripts/cleanup-generated-controllers.ts +0 -62
- package/scripts/fix-reference-app-typing.ts +0 -490
- package/scripts/format-router-registrations.ts +0 -119
- package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
- package/scripts/refactor-client-app-imports.ts +0 -244
- package/scripts/refactor-client-pages.ts +0 -587
- package/scripts/refactor-server-controllers.ts +0 -471
- package/scripts/refactor-server-runtime-aliases.ts +0 -360
- 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
|
|
14
|
-
|
|
15
|
-
type TRouteDefinition = {
|
|
16
|
-
args: ts.NodeArray<ts.Expression>;
|
|
13
|
+
type TExplicitRouteDefinition = {
|
|
17
14
|
methodName: string;
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
51
|
-
const
|
|
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
|
|
202
|
-
const
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
315
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
316
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
215
317
|
|
|
216
|
-
|
|
217
|
-
} catch (error) {
|
|
218
|
-
reservedOptionKeys.push(optionKey);
|
|
318
|
+
bindingInitializers.set(declaration.name.text, declaration.initializer);
|
|
219
319
|
}
|
|
220
320
|
}
|
|
221
321
|
|
|
222
|
-
return
|
|
322
|
+
return bindingInitializers;
|
|
223
323
|
};
|
|
224
324
|
|
|
225
|
-
const
|
|
226
|
-
|
|
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
|
|
231
|
-
|
|
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
|
|
333
|
+
return undefined;
|
|
241
334
|
};
|
|
242
335
|
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
342
|
+
const resolveIdentifierExpression = (
|
|
343
|
+
expression: ts.Expression,
|
|
257
344
|
sourceFile: ts.SourceFile,
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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.
|
|
303
|
-
if (statement.
|
|
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
|
-
|
|
364
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
365
|
+
if (declaration.initializer) return declaration.initializer;
|
|
366
|
+
}
|
|
315
367
|
}
|
|
316
|
-
};
|
|
317
368
|
|
|
318
|
-
|
|
319
|
-
|
|
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 (
|
|
334
|
-
if (!
|
|
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
|
-
|
|
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
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
369
|
-
|
|
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
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
439
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
521
|
+
return [
|
|
522
|
+
{
|
|
523
|
+
methodName: 'error',
|
|
524
|
+
sourceLocation,
|
|
525
|
+
targetExpression: codeExpression,
|
|
526
|
+
optionsExpression,
|
|
527
|
+
optionsArg,
|
|
444
528
|
hasData: false,
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
467
|
-
|
|
556
|
+
const methodName = tryEvaluateStaticExpression(
|
|
557
|
+
methodExpression,
|
|
558
|
+
staticBindingInitializers,
|
|
559
|
+
staticBindings,
|
|
468
560
|
);
|
|
469
|
-
};
|
|
470
561
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
|
498
|
-
|
|
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
|
-
|
|
506
|
-
|
|
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
|
-
|
|
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
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
);
|
|
535
|
-
}
|
|
600
|
+
for (const optionKey of optionKeys) {
|
|
601
|
+
try {
|
|
602
|
+
const normalizedOptionKey = getRouteOptionKey(optionKey);
|
|
536
603
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
...buildClientRegisterArgs(sourceFile, definition, clientRoute),
|
|
542
|
-
];
|
|
604
|
+
if (normalizedOptionKey) {
|
|
605
|
+
normalizedOptionKeys.push(normalizedOptionKey);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
543
608
|
|
|
544
|
-
|
|
609
|
+
invalidOptionKeys.push(optionKey);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
reservedOptionKeys.push(optionKey);
|
|
612
|
+
}
|
|
545
613
|
}
|
|
546
614
|
|
|
547
|
-
return
|
|
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
|
|
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 =
|
|
577
|
-
const resolveStaticValue = (node: ts.Expression) =>
|
|
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.
|
|
581
|
-
const
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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.
|
|
631
|
-
const
|
|
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:
|
|
727
|
+
serviceLocalName: 'Router',
|
|
641
728
|
sourceLocation,
|
|
642
729
|
targetResolution:
|
|
643
730
|
getLiteralStringValue(targetArg) !== undefined
|
|
644
731
|
? 'literal'
|
|
645
|
-
:
|
|
646
|
-
|
|
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:
|
|
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
|
|
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
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
|