proteum 2.1.9-8 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +11 -8
  2. package/agents/project/AGENTS.md +7 -7
  3. package/agents/project/CODING_STYLE.md +1 -1
  4. package/agents/project/client/AGENTS.md +1 -1
  5. package/agents/project/client/pages/AGENTS.md +10 -10
  6. package/agents/project/optimizations.md +5 -6
  7. package/agents/project/root/AGENTS.md +7 -7
  8. package/cli/commands/migrate.ts +51 -0
  9. package/cli/compiler/artifacts/manifest.ts +4 -4
  10. package/cli/compiler/artifacts/routing.ts +2 -2
  11. package/cli/compiler/common/generatedRouteModules.ts +31 -38
  12. package/cli/context.ts +1 -1
  13. package/cli/migrate/pageContract.ts +516 -0
  14. package/cli/presentation/commands.ts +27 -1
  15. package/cli/runtime/commands.ts +25 -0
  16. package/cli/scaffold/templates.ts +4 -2
  17. package/client/dev/profiler/index.tsx +1 -2
  18. package/client/services/router/index.tsx +6 -22
  19. package/common/dev/console.ts +1 -0
  20. package/common/dev/diagnostics.ts +4 -4
  21. package/common/dev/inspection.ts +1 -1
  22. package/common/dev/proteumManifest.ts +4 -4
  23. package/common/dev/requestTrace.ts +0 -1
  24. package/common/router/contracts.ts +8 -11
  25. package/common/router/index.ts +2 -2
  26. package/common/router/pageData.ts +72 -0
  27. package/common/router/register.ts +10 -46
  28. package/common/router/response/page.ts +28 -16
  29. package/package.json +5 -1
  30. package/server/services/router/index.ts +48 -14
  31. package/server/services/router/request/api.ts +1 -1
  32. package/server/services/router/response/index.ts +0 -27
  33. package/types/global/vendors.d.ts +12 -0
  34. package/types/vendors.d.ts +12 -0
  35. package/common/router/pageSetup.ts +0 -51
@@ -0,0 +1,516 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { parse } from '@babel/parser';
4
+ import generate from '@babel/generator';
5
+ import traverse, { type Binding, type NodePath } from '@babel/traverse';
6
+ import * as t from '@babel/types';
7
+
8
+ import { routeOptionKeys } from '../../common/router/pageData';
9
+
10
+ const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.cjs', '.mjs']);
11
+ const routeOptionKeysSet: ReadonlySet<string> = new Set(routeOptionKeys);
12
+
13
+ export type TPageContractManualFix = {
14
+ filepath: string;
15
+ routeLabel: string;
16
+ line: number;
17
+ column: number;
18
+ reason: string;
19
+ };
20
+
21
+ export type TPageContractMigrationSummary = {
22
+ appRoot: string;
23
+ changedFiles: string[];
24
+ dryRun: boolean;
25
+ manualFixes: TPageContractManualFix[];
26
+ scannedFiles: number;
27
+ };
28
+
29
+ type TDataAnalysis =
30
+ | {
31
+ kind: 'ok';
32
+ hasDataAfterMigration: boolean;
33
+ movedOptionProperties: t.ObjectProperty[];
34
+ }
35
+ | {
36
+ kind: 'manual';
37
+ reason: string;
38
+ };
39
+
40
+ const findFiles = (dir: string): string[] => {
41
+ if (!fs.existsSync(dir)) return [];
42
+
43
+ const files: string[] = [];
44
+
45
+ for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
46
+ const filepath = path.join(dir, dirent.name);
47
+
48
+ if (dirent.isDirectory()) {
49
+ files.push(...findFiles(filepath));
50
+ continue;
51
+ }
52
+
53
+ if (dirent.isFile() && SUPPORTED_EXTENSIONS.has(path.extname(filepath))) files.push(filepath);
54
+ }
55
+
56
+ return files;
57
+ };
58
+
59
+ const isRouterPageCall = (callPath: NodePath<t.CallExpression>) => {
60
+ const calleePath = callPath.get('callee');
61
+
62
+ return (
63
+ calleePath.isMemberExpression() &&
64
+ calleePath.get('object').isIdentifier({ name: 'Router' }) &&
65
+ calleePath.get('property').isIdentifier({ name: 'page' })
66
+ );
67
+ };
68
+
69
+ const unwrapExpressionPath = (expressionPath: NodePath<t.Expression>): NodePath<t.Expression> => {
70
+ let currentPath = expressionPath;
71
+
72
+ while (true) {
73
+ if (currentPath.isTSAsExpression() || currentPath.isTSTypeAssertion()) {
74
+ currentPath = currentPath.get('expression') as NodePath<t.Expression>;
75
+ continue;
76
+ }
77
+
78
+ if (currentPath.isTSNonNullExpression() || currentPath.isParenthesizedExpression()) {
79
+ currentPath = currentPath.get('expression') as NodePath<t.Expression>;
80
+ continue;
81
+ }
82
+
83
+ return currentPath;
84
+ }
85
+ };
86
+
87
+ const getObjectPropertyKeyName = (property: t.ObjectProperty) => {
88
+ if (t.isIdentifier(property.key)) return property.key.name;
89
+ if (t.isStringLiteral(property.key)) return property.key.value;
90
+ return null;
91
+ };
92
+
93
+ const normalizeLegacyRouteOptionKey = (key: string) => {
94
+ if (routeOptionKeysSet.has(key)) return key;
95
+
96
+ if (key.startsWith('_')) {
97
+ const normalizedKey = key.slice(1);
98
+ if (routeOptionKeysSet.has(normalizedKey)) return normalizedKey;
99
+ }
100
+
101
+ return null;
102
+ };
103
+
104
+ const createRouteOptionProperty = (property: t.ObjectProperty, normalizedKey: string) => {
105
+ const value = t.cloneNode(property.value, true);
106
+ const shorthand = t.isIdentifier(value) && value.name === normalizedKey;
107
+
108
+ return t.objectProperty(t.identifier(normalizedKey), value, false, shorthand);
109
+ };
110
+
111
+ const getRouteLabel = (pathNode: t.Expression) =>
112
+ (t.isStringLiteral(pathNode) || t.isNumericLiteral(pathNode) ? String(pathNode.value) : generate(pathNode).code).trim();
113
+
114
+ const getRouteLocation = (callPath: NodePath<t.CallExpression>) => ({
115
+ line: callPath.node.loc?.start.line || 1,
116
+ column: callPath.node.loc?.start.column ? callPath.node.loc.start.column + 1 : 1,
117
+ });
118
+
119
+ const resolveFunctionPathFromBinding = (binding: Binding) => {
120
+ if (binding.path.isFunctionDeclaration()) {
121
+ return binding.path as NodePath<t.FunctionDeclaration>;
122
+ }
123
+
124
+ if (!binding.path.isVariableDeclarator()) return null;
125
+
126
+ const initPath = binding.path.get('init');
127
+ if (!initPath.node) return null;
128
+
129
+ const unwrappedInitPath = unwrapExpressionPath(initPath as NodePath<t.Expression>);
130
+ if (unwrappedInitPath.isFunctionExpression() || unwrappedInitPath.isArrowFunctionExpression()) return unwrappedInitPath;
131
+
132
+ return null;
133
+ };
134
+
135
+ const resolveObjectExpressionFromBinding = (binding: Binding) => {
136
+ if (!binding.path.isVariableDeclarator()) return null;
137
+
138
+ const initPath = binding.path.get('init');
139
+ if (!initPath.node) return null;
140
+
141
+ const unwrappedInitPath = unwrapExpressionPath(initPath as NodePath<t.Expression>);
142
+ if (unwrappedInitPath.isObjectExpression()) return unwrappedInitPath;
143
+
144
+ return null;
145
+ };
146
+
147
+ const classifyLegacySecondArg = (expressionPath: NodePath<t.Expression>) => {
148
+ const unwrappedPath = unwrapExpressionPath(expressionPath);
149
+
150
+ if (unwrappedPath.isObjectExpression()) return 'options' as const;
151
+ if (unwrappedPath.isArrowFunctionExpression() || unwrappedPath.isFunctionExpression()) return 'data' as const;
152
+
153
+ if (!unwrappedPath.isIdentifier()) return 'manual' as const;
154
+
155
+ const binding = expressionPath.scope.getBinding(unwrappedPath.node.name);
156
+ if (!binding) return 'manual' as const;
157
+
158
+ if (resolveFunctionPathFromBinding(binding)) return 'data' as const;
159
+ if (resolveObjectExpressionFromBinding(binding)) return 'options' as const;
160
+
161
+ return 'manual' as const;
162
+ };
163
+
164
+ const findReturnedObjectPath = (
165
+ functionPath: NodePath<t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression>,
166
+ ) => {
167
+ if (functionPath.isArrowFunctionExpression() && !functionPath.get('body').isBlockStatement()) {
168
+ const bodyPath = unwrapExpressionPath(functionPath.get('body') as NodePath<t.Expression>);
169
+ if (bodyPath.isObjectExpression()) return { kind: 'ok' as const, objectPath: bodyPath };
170
+
171
+ return { kind: 'manual' as const, reason: 'Data function must directly return an object expression.' };
172
+ }
173
+
174
+ const bodyPath = functionPath.get('body');
175
+ if (!bodyPath.isBlockStatement()) {
176
+ return { kind: 'manual' as const, reason: 'Data function must directly return an object expression.' };
177
+ }
178
+
179
+ let returnedObjectPath: NodePath<t.ObjectExpression> | null = null;
180
+
181
+ for (const statementPath of bodyPath.get('body')) {
182
+ if (!statementPath.isReturnStatement()) continue;
183
+
184
+ const argumentPath = statementPath.get('argument');
185
+ if (!argumentPath.node) {
186
+ return { kind: 'manual' as const, reason: 'Data function cannot return without an object value.' };
187
+ }
188
+
189
+ const unwrappedArgumentPath = unwrapExpressionPath(argumentPath as NodePath<t.Expression>);
190
+ if (!unwrappedArgumentPath.isObjectExpression()) {
191
+ return { kind: 'manual' as const, reason: 'Data function must directly return an object expression.' };
192
+ }
193
+
194
+ if (returnedObjectPath) {
195
+ return { kind: 'manual' as const, reason: 'Data function with multiple direct returns requires a manual rewrite.' };
196
+ }
197
+
198
+ returnedObjectPath = unwrappedArgumentPath;
199
+ }
200
+
201
+ if (!returnedObjectPath) {
202
+ return { kind: 'manual' as const, reason: 'Data function must directly return an object expression.' };
203
+ }
204
+
205
+ return { kind: 'ok' as const, objectPath: returnedObjectPath };
206
+ };
207
+
208
+ const analyzeFunctionPath = (
209
+ functionPath: NodePath<t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression>,
210
+ analysisCache: Map<string, TDataAnalysis>,
211
+ ): TDataAnalysis => {
212
+ const cacheKey = `${functionPath.node.start || 0}:${functionPath.node.end || 0}`;
213
+ const cachedResult = analysisCache.get(cacheKey);
214
+ if (cachedResult) return cachedResult;
215
+
216
+ const objectResult = findReturnedObjectPath(functionPath);
217
+ if (objectResult.kind === 'manual') {
218
+ const result: TDataAnalysis = { kind: 'manual', reason: objectResult.reason };
219
+ analysisCache.set(cacheKey, result);
220
+ return result;
221
+ }
222
+
223
+ const keptProperties: Array<t.ObjectProperty | t.SpreadElement> = [];
224
+ const movedOptionProperties: t.ObjectProperty[] = [];
225
+
226
+ for (const propertyPath of objectResult.objectPath.get('properties')) {
227
+ if (propertyPath.isSpreadElement()) {
228
+ const result: TDataAnalysis = {
229
+ kind: 'manual',
230
+ reason: 'Data function using object spreads requires a manual rewrite.',
231
+ };
232
+ analysisCache.set(cacheKey, result);
233
+ return result;
234
+ }
235
+
236
+ if (!propertyPath.isObjectProperty() || propertyPath.node.computed) {
237
+ const result: TDataAnalysis = {
238
+ kind: 'manual',
239
+ reason: 'Data function must return a plain object with explicit property keys.',
240
+ };
241
+ analysisCache.set(cacheKey, result);
242
+ return result;
243
+ }
244
+
245
+ const propertyKeyName = getObjectPropertyKeyName(propertyPath.node);
246
+ if (!propertyKeyName) {
247
+ const result: TDataAnalysis = {
248
+ kind: 'manual',
249
+ reason: 'Data function must return a plain object with explicit property keys.',
250
+ };
251
+ analysisCache.set(cacheKey, result);
252
+ return result;
253
+ }
254
+
255
+ const normalizedRouteOptionKey = normalizeLegacyRouteOptionKey(propertyKeyName);
256
+ if (normalizedRouteOptionKey) {
257
+ movedOptionProperties.push(createRouteOptionProperty(propertyPath.node, normalizedRouteOptionKey));
258
+ continue;
259
+ }
260
+
261
+ keptProperties.push(t.cloneNode(propertyPath.node, true));
262
+ }
263
+
264
+ if (movedOptionProperties.length > 0) objectResult.objectPath.node.properties = keptProperties;
265
+
266
+ const result: TDataAnalysis = {
267
+ kind: 'ok',
268
+ hasDataAfterMigration: keptProperties.length > 0,
269
+ movedOptionProperties,
270
+ };
271
+ analysisCache.set(cacheKey, result);
272
+ return result;
273
+ };
274
+
275
+ const analyzeDataExpression = (
276
+ expressionPath: NodePath<t.Expression>,
277
+ analysisCache: Map<string, TDataAnalysis>,
278
+ ): TDataAnalysis => {
279
+ const unwrappedPath = unwrapExpressionPath(expressionPath);
280
+
281
+ if (unwrappedPath.isNullLiteral()) {
282
+ return {
283
+ kind: 'ok',
284
+ hasDataAfterMigration: false,
285
+ movedOptionProperties: [],
286
+ };
287
+ }
288
+
289
+ if (unwrappedPath.isArrowFunctionExpression() || unwrappedPath.isFunctionExpression()) {
290
+ return analyzeFunctionPath(unwrappedPath, analysisCache);
291
+ }
292
+
293
+ if (!unwrappedPath.isIdentifier()) {
294
+ return {
295
+ kind: 'manual',
296
+ reason: 'Data provider must be a local function expression/reference or null.',
297
+ };
298
+ }
299
+
300
+ const binding = expressionPath.scope.getBinding(unwrappedPath.node.name);
301
+ if (!binding) {
302
+ return {
303
+ kind: 'manual',
304
+ reason: `Could not resolve local data provider "${unwrappedPath.node.name}".`,
305
+ };
306
+ }
307
+
308
+ const functionPath = resolveFunctionPathFromBinding(binding);
309
+ if (!functionPath) {
310
+ return {
311
+ kind: 'manual',
312
+ reason: `Data provider "${unwrappedPath.node.name}" is not a directly analyzable local function.`,
313
+ };
314
+ }
315
+
316
+ return analyzeFunctionPath(functionPath, analysisCache);
317
+ };
318
+
319
+ const cloneObjectProperties = (properties: t.ObjectProperty[]) =>
320
+ properties.map((property) => t.cloneNode(property, true));
321
+
322
+ const buildNextOptionsExpression = (optionsNode: t.Expression, movedOptionProperties: t.ObjectProperty[]) => {
323
+ if (movedOptionProperties.length === 0) return t.cloneNode(optionsNode, true);
324
+
325
+ if (t.isObjectExpression(optionsNode) && optionsNode.properties.length === 0) {
326
+ return t.objectExpression(cloneObjectProperties(movedOptionProperties));
327
+ }
328
+
329
+ return t.objectExpression([
330
+ t.spreadElement(t.cloneNode(optionsNode, true)),
331
+ ...cloneObjectProperties(movedOptionProperties),
332
+ ]);
333
+ };
334
+
335
+ const transformFile = (filepath: string) => {
336
+ const source = fs.readFileSync(filepath, 'utf8');
337
+ if (!source.includes('Router.page(')) {
338
+ return {
339
+ changed: false,
340
+ manualFixes: [] as TPageContractManualFix[],
341
+ output: source,
342
+ };
343
+ }
344
+
345
+ const ast = parse(source, {
346
+ sourceType: 'module',
347
+ errorRecovery: true,
348
+ plugins: ['typescript', 'jsx', 'decorators-legacy', 'classProperties'],
349
+ });
350
+
351
+ const routeCalls: NodePath<t.CallExpression>[] = [];
352
+ const manualFixes: TPageContractManualFix[] = [];
353
+ const analysisCache = new Map<string, TDataAnalysis>();
354
+
355
+ traverse(ast, {
356
+ CallExpression(callPath: NodePath<t.CallExpression>) {
357
+ if (!isRouterPageCall(callPath)) return;
358
+ routeCalls.push(callPath);
359
+ },
360
+ });
361
+
362
+ let mutated = false;
363
+
364
+ for (const callPath of routeCalls) {
365
+ const argumentPaths = callPath.get('arguments') as NodePath<t.Expression>[];
366
+ if (argumentPaths.length < 2 || argumentPaths.length > 4) {
367
+ const pathArgument = argumentPaths[0]?.node;
368
+ const routeLabel = pathArgument ? getRouteLabel(pathArgument) : '(unknown route)';
369
+ const location = getRouteLocation(callPath);
370
+
371
+ manualFixes.push({
372
+ filepath,
373
+ routeLabel,
374
+ line: location.line,
375
+ column: location.column,
376
+ reason: 'Unsupported Router.page signature.',
377
+ });
378
+ continue;
379
+ }
380
+
381
+ const pathArgument = argumentPaths[0].node;
382
+ const routeLabel = getRouteLabel(pathArgument);
383
+ const location = getRouteLocation(callPath);
384
+
385
+ let optionsNode: t.Expression;
386
+ let dataPath: NodePath<t.Expression> | null;
387
+ let renderNode: t.Expression;
388
+ const isExplicitSignature = argumentPaths.length === 4;
389
+
390
+ if (argumentPaths.length === 2) {
391
+ optionsNode = t.objectExpression([]);
392
+ dataPath = null;
393
+ renderNode = t.cloneNode(argumentPaths[1].node, true);
394
+ } else if (argumentPaths.length === 3) {
395
+ const secondArgKind = classifyLegacySecondArg(argumentPaths[1]);
396
+ if (secondArgKind === 'manual') {
397
+ manualFixes.push({
398
+ filepath,
399
+ routeLabel,
400
+ line: location.line,
401
+ column: location.column,
402
+ reason: 'Could not classify the legacy second Router.page argument as options or data.',
403
+ });
404
+ continue;
405
+ }
406
+
407
+ if (secondArgKind === 'options') {
408
+ optionsNode = t.cloneNode(argumentPaths[1].node, true);
409
+ dataPath = null;
410
+ renderNode = t.cloneNode(argumentPaths[2].node, true);
411
+ } else {
412
+ optionsNode = t.objectExpression([]);
413
+ dataPath = argumentPaths[1];
414
+ renderNode = t.cloneNode(argumentPaths[2].node, true);
415
+ }
416
+ } else {
417
+ optionsNode = t.cloneNode(argumentPaths[1].node, true);
418
+ dataPath = argumentPaths[2];
419
+ renderNode = t.cloneNode(argumentPaths[3].node, true);
420
+ }
421
+
422
+ let dataNode: t.Expression = t.nullLiteral();
423
+ let movedOptionProperties: t.ObjectProperty[] = [];
424
+ let shouldRewrite = !isExplicitSignature;
425
+
426
+ if (dataPath) {
427
+ const analysis = analyzeDataExpression(dataPath, analysisCache);
428
+ if (analysis.kind === 'manual') {
429
+ if (isExplicitSignature) continue;
430
+
431
+ manualFixes.push({
432
+ filepath,
433
+ routeLabel,
434
+ line: location.line,
435
+ column: location.column,
436
+ reason: analysis.reason,
437
+ });
438
+ continue;
439
+ }
440
+
441
+ movedOptionProperties = analysis.movedOptionProperties;
442
+ dataNode = analysis.hasDataAfterMigration ? t.cloneNode(dataPath.node, true) : t.nullLiteral();
443
+ shouldRewrite =
444
+ shouldRewrite ||
445
+ movedOptionProperties.length > 0 ||
446
+ (!analysis.hasDataAfterMigration && !unwrapExpressionPath(dataPath).isNullLiteral());
447
+ }
448
+
449
+ if (!shouldRewrite) continue;
450
+
451
+ callPath.node.arguments = [
452
+ t.cloneNode(pathArgument, true),
453
+ buildNextOptionsExpression(optionsNode, movedOptionProperties),
454
+ dataNode,
455
+ renderNode,
456
+ ];
457
+ mutated = true;
458
+ }
459
+
460
+ if (manualFixes.length > 0) {
461
+ return {
462
+ changed: false,
463
+ manualFixes,
464
+ output: source,
465
+ };
466
+ }
467
+
468
+ if (!mutated) {
469
+ return {
470
+ changed: false,
471
+ manualFixes,
472
+ output: source,
473
+ };
474
+ }
475
+
476
+ return {
477
+ changed: true,
478
+ manualFixes,
479
+ output: generate(ast, {
480
+ comments: true,
481
+ compact: false,
482
+ retainLines: false,
483
+ }).code,
484
+ };
485
+ };
486
+
487
+ export const runPageContractMigration = ({
488
+ appRoot,
489
+ dryRun,
490
+ }: {
491
+ appRoot: string;
492
+ dryRun: boolean;
493
+ }): TPageContractMigrationSummary => {
494
+ const pagesRoot = path.join(appRoot, 'client', 'pages');
495
+ const changedFiles: string[] = [];
496
+ const manualFixes: TPageContractManualFix[] = [];
497
+ const files = findFiles(pagesRoot);
498
+
499
+ for (const filepath of files) {
500
+ const result = transformFile(filepath);
501
+ manualFixes.push(...result.manualFixes);
502
+
503
+ if (!result.changed) continue;
504
+
505
+ changedFiles.push(filepath);
506
+ if (!dryRun) fs.writeFileSync(filepath, result.output);
507
+ }
508
+
509
+ return {
510
+ appRoot,
511
+ changedFiles,
512
+ dryRun,
513
+ manualFixes,
514
+ scannedFiles: files.length,
515
+ };
516
+ };
@@ -7,6 +7,7 @@ export const proteumCommandNames = [
7
7
  'init',
8
8
  'create',
9
9
  'configure',
10
+ 'migrate',
10
11
  'dev',
11
12
  'refresh',
12
13
  'build',
@@ -56,7 +57,7 @@ export const proteumCommandGroups: Array<{ title: string; names: TProteumCommand
56
57
  { title: 'Daily workflow', names: ['dev', 'refresh', 'build'] },
57
58
  { title: 'Quality gates', names: ['typecheck', 'lint', 'check'] },
58
59
  { title: 'Manifest and contracts', names: ['connect', 'doctor', 'explain', 'orient', 'diagnose', 'perf', 'trace', 'command', 'session', 'verify'] },
59
- { title: 'Project scaffolding', names: ['init', 'configure', 'create'] },
60
+ { title: 'Project scaffolding', names: ['init', 'configure', 'create', 'migrate'] },
60
61
  ];
61
62
 
62
63
  export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> = {
@@ -130,6 +131,31 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
130
131
  ],
131
132
  status: 'experimental',
132
133
  },
134
+ migrate: {
135
+ name: 'migrate',
136
+ category: 'Project scaffolding',
137
+ summary: 'Rewrite legacy page registrations to the explicit Router.page(path, options, data, render) contract.',
138
+ usage: 'proteum migrate page-contract [--cwd <path>] [--dry-run] [--json]',
139
+ bestFor:
140
+ 'Upgrading existing Proteum apps to the single explicit page contract without hand-editing every `client/pages/**` file.',
141
+ examples: [
142
+ { description: 'Rewrite the current app in place', command: 'proteum migrate page-contract' },
143
+ {
144
+ description: 'Preview the migration without writing files',
145
+ command: 'proteum migrate page-contract --dry-run --json',
146
+ },
147
+ {
148
+ description: 'Migrate another app root from the current shell',
149
+ command: 'proteum migrate page-contract --cwd /path/to/app',
150
+ },
151
+ ],
152
+ notes: [
153
+ 'This migration rewrites supported legacy Router.page signatures to the explicit 4-argument form.',
154
+ 'If a page data function cannot be analyzed safely, Proteum leaves the file unchanged and reports a manual-fix location.',
155
+ 'Run `proteum typecheck` and `proteum build --strict` after the rewrite.',
156
+ ],
157
+ status: 'experimental',
158
+ },
133
159
  dev: {
134
160
  name: 'dev',
135
161
  category: 'Daily workflow',
@@ -92,6 +92,29 @@ class ConfigureCommand extends ProteumCommand {
92
92
  }
93
93
  }
94
94
 
95
+ class MigrateCommand extends ProteumCommand {
96
+ public static paths = [['migrate']];
97
+
98
+ public static usage = buildUsage('migrate');
99
+
100
+ public cwd = Option.String('--cwd', { description: 'Run the migration against another Proteum app root.' });
101
+ public dryRun = Option.Boolean('--dry-run', false, { description: 'Print the rewrite plan without writing files.' });
102
+ public json = Option.Boolean('--json', false, { description: 'Print machine-readable migration output.' });
103
+ public args = Option.Rest();
104
+
105
+ public async execute() {
106
+ const [action = ''] = this.args;
107
+
108
+ this.setCliArgs({
109
+ action,
110
+ workdir: this.cwd ?? '',
111
+ dryRun: this.dryRun,
112
+ json: this.json,
113
+ });
114
+ await runCommandModule(() => import('../commands/migrate'));
115
+ }
116
+ }
117
+
95
118
  class DevCommand extends ProteumCommand {
96
119
  public static paths = [['dev']];
97
120
 
@@ -578,6 +601,7 @@ export const registeredCommands = {
578
601
  init: InitCommand,
579
602
  create: CreateCommand,
580
603
  configure: ConfigureCommand,
604
+ migrate: MigrateCommand,
581
605
  dev: DevCommand,
582
606
  refresh: RefreshCommand,
583
607
  build: BuildCommand,
@@ -609,6 +633,7 @@ export const createCli = (version: string) => {
609
633
  clipanion.register(InitCommand);
610
634
  clipanion.register(CreateCommand);
611
635
  clipanion.register(ConfigureCommand);
636
+ clipanion.register(MigrateCommand);
612
637
  clipanion.register(DevCommand);
613
638
  clipanion.register(RefreshCommand);
614
639
  clipanion.register(BuildCommand);
@@ -26,9 +26,11 @@ export const createPageTemplate = ({
26
26
 
27
27
  Router.page(
28
28
  ${JSON.stringify(routePath)},
29
+ {
30
+ auth: false,
31
+ layout: false,
32
+ },
29
33
  () => ({
30
- _auth: false,
31
- _layout: false,
32
34
  heading: ${JSON.stringify(heading)},
33
35
  message: ${JSON.stringify(message)},
34
36
  }),
@@ -1166,7 +1166,6 @@ const traceEventDepths: Record<TTraceEventType, number> = {
1166
1166
  'resolve.not-found': 1,
1167
1167
  'controller.start': 2,
1168
1168
  'controller.result': 2,
1169
- 'setup.options': 3,
1170
1169
  'context.create': 3,
1171
1170
  'page.data': 3,
1172
1171
  'ssr.payload': 3,
@@ -3801,7 +3800,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3801
3800
  }
3802
3801
 
3803
3802
  if (panel === 'controller') {
3804
- const controllerEvents = findTraceEvents(primaryTrace, ['controller.start', 'controller.result', 'setup.options', 'context.create']);
3803
+ const controllerEvents = findTraceEvents(primaryTrace, ['controller.start', 'controller.result', 'context.create']);
3805
3804
  const controllerFlowChart = buildHorizontalBarChartOptions({
3806
3805
  color: profilerChartTheme.indigo,
3807
3806
  entries: buildCountEntries(controllerEvents.map((event) => event.type.replace(/\./g, ' '))),