proteum 2.4.3 → 2.5.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 (74) hide show
  1. package/README.md +60 -55
  2. package/agents/project/AGENTS.md +112 -31
  3. package/agents/project/CODING_STYLE.md +2 -2
  4. package/agents/project/app-root/AGENTS.md +1 -3
  5. package/agents/project/client/AGENTS.md +1 -1
  6. package/agents/project/client/pages/AGENTS.md +21 -9
  7. package/agents/project/diagnostics.md +2 -2
  8. package/agents/project/optimizations.md +1 -1
  9. package/agents/project/root/AGENTS.md +105 -22
  10. package/agents/project/server/routes/AGENTS.md +30 -1
  11. package/agents/project/tests/AGENTS.md +1 -1
  12. package/cli/commands/doctor.ts +54 -3
  13. package/cli/commands/runtime.ts +6 -0
  14. package/cli/commands/worktree.ts +116 -0
  15. package/cli/compiler/artifacts/controllers.ts +16 -15
  16. package/cli/compiler/artifacts/discovery.ts +129 -17
  17. package/cli/compiler/artifacts/routing.ts +0 -5
  18. package/cli/compiler/artifacts/services.ts +253 -76
  19. package/cli/compiler/common/controllers.ts +159 -57
  20. package/cli/compiler/common/generatedRouteModules.ts +457 -363
  21. package/cli/mcp/router.ts +47 -3
  22. package/cli/presentation/commands.ts +25 -15
  23. package/cli/runtime/commands.ts +39 -12
  24. package/cli/runtime/worktreeBootstrap.ts +608 -0
  25. package/cli/scaffold/index.ts +28 -18
  26. package/cli/scaffold/templates.ts +44 -33
  27. package/cli/utils/agents.ts +14 -1
  28. package/client/services/router/index.tsx +23 -3
  29. package/client/services/router/request/api.ts +14 -4
  30. package/common/dev/contractsDoctor.ts +1 -1
  31. package/common/dev/mcpPayloads.ts +8 -1
  32. package/common/env/proteumEnv.ts +14 -2
  33. package/common/router/contracts.ts +1 -1
  34. package/common/router/definitions.ts +177 -0
  35. package/common/router/index.ts +23 -12
  36. package/common/router/pageData.ts +5 -5
  37. package/common/router/register.ts +2 -2
  38. package/common/router/request/api.ts +12 -2
  39. package/docs/agent-routing.md +5 -2
  40. package/docs/diagnostics.md +2 -0
  41. package/docs/mcp.md +6 -3
  42. package/eslint.js +36 -1
  43. package/package.json +1 -1
  44. package/server/app/commands.ts +5 -1
  45. package/server/app/container/console/http-client-error-context.test.cjs +10 -1
  46. package/server/app/container/console/index.ts +2 -1
  47. package/server/app/controller/index.ts +98 -40
  48. package/server/app/index.ts +92 -1
  49. package/server/app/service/index.ts +5 -1
  50. package/server/index.ts +6 -2
  51. package/server/services/router/index.ts +47 -38
  52. package/server/services/router/response/index.ts +2 -2
  53. package/tests/agents-utils.test.cjs +14 -1
  54. package/tests/cli-mcp-command.test.cjs +84 -0
  55. package/tests/definition-contracts.test.cjs +453 -0
  56. package/tests/dev-transpile-watch.test.cjs +37 -28
  57. package/tests/eslint-rules.test.cjs +39 -1
  58. package/tests/mcp.test.cjs +90 -0
  59. package/tests/worktree-bootstrap.test.cjs +206 -0
  60. package/types/aliases.d.ts +0 -5
  61. package/types/controller-input.test.ts +23 -17
  62. package/types/controller-request-context.test.ts +10 -11
  63. package/cli/commands/migrate.ts +0 -51
  64. package/cli/migrate/pageContract.ts +0 -516
  65. package/docs/migrate-from-2.1.3.md +0 -396
  66. package/scripts/cleanup-generated-controllers.ts +0 -62
  67. package/scripts/fix-reference-app-typing.ts +0 -490
  68. package/scripts/format-router-registrations.ts +0 -119
  69. package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
  70. package/scripts/refactor-client-app-imports.ts +0 -244
  71. package/scripts/refactor-client-pages.ts +0 -587
  72. package/scripts/refactor-server-controllers.ts +0 -471
  73. package/scripts/refactor-server-runtime-aliases.ts +0 -360
  74. package/scripts/restore-client-app-import-files.ts +0 -41
@@ -1,516 +0,0 @@
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
- };