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.
- package/README.md +11 -8
- package/agents/project/AGENTS.md +7 -7
- package/agents/project/CODING_STYLE.md +1 -1
- package/agents/project/client/AGENTS.md +1 -1
- package/agents/project/client/pages/AGENTS.md +10 -10
- package/agents/project/optimizations.md +5 -6
- package/agents/project/root/AGENTS.md +7 -7
- package/cli/commands/migrate.ts +51 -0
- package/cli/compiler/artifacts/manifest.ts +4 -4
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/common/generatedRouteModules.ts +31 -38
- package/cli/context.ts +1 -1
- package/cli/migrate/pageContract.ts +516 -0
- package/cli/presentation/commands.ts +27 -1
- package/cli/runtime/commands.ts +25 -0
- package/cli/scaffold/templates.ts +4 -2
- package/client/dev/profiler/index.tsx +1 -2
- package/client/services/router/index.tsx +6 -22
- package/common/dev/console.ts +1 -0
- package/common/dev/diagnostics.ts +4 -4
- package/common/dev/inspection.ts +1 -1
- package/common/dev/proteumManifest.ts +4 -4
- package/common/dev/requestTrace.ts +0 -1
- package/common/router/contracts.ts +8 -11
- package/common/router/index.ts +2 -2
- package/common/router/pageData.ts +72 -0
- package/common/router/register.ts +10 -46
- package/common/router/response/page.ts +28 -16
- package/package.json +5 -1
- package/server/services/router/index.ts +48 -14
- package/server/services/router/request/api.ts +1 -1
- package/server/services/router/response/index.ts +0 -27
- package/types/global/vendors.d.ts +12 -0
- package/types/vendors.d.ts +12 -0
- 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',
|
package/cli/runtime/commands.ts
CHANGED
|
@@ -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', '
|
|
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, ' '))),
|