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,471 +0,0 @@
1
- /*----------------------------------
2
- - DEPENDANCES
3
- ----------------------------------*/
4
-
5
- // Npm
6
- import fs from 'fs-extra';
7
- import path from 'path';
8
- import { parse } from '@babel/parser';
9
- import traverse, { NodePath } from '@babel/traverse';
10
- import generate from '@babel/generator';
11
- import * as types from '@babel/types';
12
-
13
- /*----------------------------------
14
- - TYPES
15
- ----------------------------------*/
16
-
17
- type TRouteMethod = { methodName: string; routePath: string; schemaSource?: string; schemaImports: string[] };
18
-
19
- type TServiceRoot = { alias: string; id: string; dir: string };
20
-
21
- type TImportBinding = { source: '@app' | '@models' | '@request'; imported: string };
22
-
23
- /*----------------------------------
24
- - HELPERS
25
- ----------------------------------*/
26
-
27
- const lowerFirst = (value: string) => (value.length ? value[0].toLowerCase() + value.substring(1) : value);
28
-
29
- const findFiles = (dir: string, predicate: (filepath: string) => boolean): string[] => {
30
- if (!fs.existsSync(dir)) return [];
31
-
32
- const files: string[] = [];
33
-
34
- for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
35
- const filepath = path.join(dir, dirent.name);
36
-
37
- if (dirent.isDirectory()) {
38
- files.push(...findFiles(filepath, predicate));
39
- continue;
40
- }
41
-
42
- if (dirent.isFile() && predicate(filepath)) files.push(filepath);
43
- }
44
-
45
- return files;
46
- };
47
-
48
- const findNearestServiceRoot = (serviceRoots: TServiceRoot[], filepath: string) => {
49
- const normalizedFilepath = filepath.replace(/\\/g, '/');
50
-
51
- return serviceRoots
52
- .filter((serviceRoot) => normalizedFilepath.startsWith(serviceRoot.dir.replace(/\\/g, '/') + '/'))
53
- .sort((a, b) => b.dir.length - a.dir.length)[0];
54
- };
55
-
56
- const getRelativeServiceSegments = (serviceRootDir: string, filepath: string) => {
57
- const relativePath = path.relative(serviceRootDir, filepath).replace(/\\/g, '/');
58
- const segments = relativePath.split('/');
59
- const filename = segments.pop() as string;
60
- const basename = filename.replace(/\.(tsx?|jsx?)$/, '');
61
-
62
- if (basename !== 'index') segments.push(basename);
63
-
64
- return segments.filter(Boolean);
65
- };
66
-
67
- const getControllerSegments = (relativePath: string) => {
68
- const segments = relativePath
69
- .replace(/\.(tsx?|jsx?)$/, '')
70
- .split('/')
71
- .filter(Boolean);
72
-
73
- if (segments.length > 1 && segments[segments.length - 1] === segments[segments.length - 2]) {
74
- segments.pop();
75
- }
76
-
77
- return segments;
78
- };
79
-
80
- const getControllerBasePath = (repoRoot: string, controllerFilepath: string) => {
81
- const segments = getControllerSegments(
82
- path.relative(path.join(repoRoot, 'server', 'controllers'), controllerFilepath).replace(/\\/g, '/'),
83
- );
84
-
85
- return segments.join('/');
86
- };
87
-
88
- const findServiceAliases = (repoRoot: string) => {
89
- const serviceAliasById = new Map<string, string>();
90
- const configFiles = findFiles(path.join(repoRoot, 'server', 'config'), (filepath) => filepath.endsWith('.ts'));
91
-
92
- for (const configFile of configFiles) {
93
- const content = fs.readFileSync(configFile, 'utf8');
94
- const regex = /app\.setup\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/g;
95
-
96
- for (let match = regex.exec(content); match; match = regex.exec(content))
97
- serviceAliasById.set(match[2], match[1]);
98
- }
99
-
100
- return serviceAliasById;
101
- };
102
-
103
- const findServiceRoots = (repoRoot: string) => {
104
- const aliasById = findServiceAliases(repoRoot);
105
- const serviceJsonFiles = findFiles(
106
- path.join(repoRoot, 'server', 'services'),
107
- (filepath) => path.basename(filepath) === 'service.json',
108
- );
109
-
110
- return serviceJsonFiles
111
- .map<TServiceRoot | null>((serviceJsonFile) => {
112
- const metas = fs.readJsonSync(serviceJsonFile) as { id?: string };
113
- if (!metas.id) return null;
114
-
115
- const alias = aliasById.get(metas.id);
116
- if (!alias) return null;
117
-
118
- return { alias, id: metas.id, dir: path.dirname(serviceJsonFile) };
119
- })
120
- .filter((serviceRoot): serviceRoot is TServiceRoot => !!serviceRoot);
121
- };
122
-
123
- const getRouteDecoratorMeta = (decorator: types.Decorator) => {
124
- if (decorator.expression.type !== 'CallExpression') return null;
125
-
126
- const callee = decorator.expression.callee;
127
- if (callee.type !== 'Identifier' || callee.name !== 'Route') return null;
128
-
129
- const [firstArg, secondArg] = decorator.expression.arguments;
130
- let routePath: string | undefined;
131
- let schemaExpression: types.Expression | undefined;
132
-
133
- if (!firstArg) return null;
134
-
135
- if (types.isStringLiteral(firstArg)) {
136
- routePath = firstArg.value;
137
- if (secondArg && types.isExpression(secondArg)) schemaExpression = secondArg;
138
- } else if (types.isObjectExpression(firstArg)) {
139
- for (const property of firstArg.properties) {
140
- if (property.type !== 'ObjectProperty' || property.key.type !== 'Identifier') continue;
141
-
142
- if (property.key.name === 'path' && property.value.type === 'StringLiteral')
143
- routePath = property.value.value;
144
- else if (property.key.name === 'schema' && types.isExpression(property.value))
145
- schemaExpression = property.value;
146
- }
147
- }
148
-
149
- if (!routePath) return null;
150
-
151
- return { routePath, schemaExpression };
152
- };
153
-
154
- const ensureNamedExport = (programPath: NodePath<types.Program>, identifierName: string) => {
155
- let alreadyExported = false;
156
-
157
- for (const statement of programPath.node.body) {
158
- if (
159
- statement.type === 'ExportNamedDeclaration' &&
160
- statement.specifiers.some(
161
- (specifier) =>
162
- specifier.type === 'ExportSpecifier' &&
163
- specifier.exported.type === 'Identifier' &&
164
- specifier.exported.name === identifierName,
165
- )
166
- ) {
167
- alreadyExported = true;
168
- break;
169
- }
170
- }
171
-
172
- if (alreadyExported) return;
173
-
174
- const binding = programPath.scope.getBinding(identifierName);
175
- if (!binding) return;
176
-
177
- const declarationPath = binding.path.parentPath;
178
- if (!declarationPath) return;
179
-
180
- if (declarationPath.isExportNamedDeclaration()) return;
181
-
182
- if (
183
- declarationPath.isVariableDeclaration() ||
184
- declarationPath.isFunctionDeclaration() ||
185
- declarationPath.isClassDeclaration()
186
- ) {
187
- declarationPath.replaceWith(types.exportNamedDeclaration(declarationPath.node, []));
188
- return;
189
- }
190
-
191
- programPath.pushContainer(
192
- 'body',
193
- types.exportNamedDeclaration(undefined, [
194
- types.exportSpecifier(types.identifier(identifierName), types.identifier(identifierName)),
195
- ]),
196
- );
197
- };
198
-
199
- const buildMemberExpression = (...segments: string[]) => {
200
- let expression: types.Expression = segments[0] === 'this' ? types.thisExpression() : types.identifier(segments[0]);
201
-
202
- for (const segment of segments.slice(1)) expression = types.memberExpression(expression, types.identifier(segment));
203
-
204
- return expression;
205
- };
206
-
207
- const replaceReferencedIdentifier = (identifierPath: NodePath<types.Identifier>, binding: TImportBinding) => {
208
- if (!identifierPath.isReferencedIdentifier()) return;
209
-
210
- if (binding.source === '@app') {
211
- identifierPath.replaceWith(
212
- types.memberExpression(
213
- types.memberExpression(types.thisExpression(), types.identifier('services')),
214
- types.identifier(binding.imported),
215
- ),
216
- );
217
- return;
218
- }
219
-
220
- if (binding.source === '@models') {
221
- identifierPath.replaceWith(
222
- types.memberExpression(
223
- types.memberExpression(types.thisExpression(), types.identifier('models')),
224
- types.identifier(lowerFirst(binding.imported)),
225
- ),
226
- );
227
- return;
228
- }
229
-
230
- const requestPathByImport: Record<string, string[]> = {
231
- auth: ['this', 'request', 'auth'],
232
- request: ['this', 'request', 'request'],
233
- response: ['this', 'request', 'response'],
234
- user: ['this', 'request', 'user'],
235
- context: ['this', 'request', 'context'],
236
- };
237
-
238
- const memberSegments = requestPathByImport[binding.imported];
239
- if (!memberSegments) return;
240
-
241
- identifierPath.replaceWith(buildMemberExpression(...memberSegments));
242
- };
243
-
244
- const getControllerClassName = (serviceClassName: string, filepath: string) => {
245
- const rawBasename = path.basename(filepath, path.extname(filepath));
246
- const baseName =
247
- serviceClassName || (rawBasename === 'index' ? path.basename(path.dirname(filepath)) : rawBasename);
248
- return baseName.endsWith('Controller') ? baseName : `${baseName}Controller`;
249
- };
250
-
251
- /*----------------------------------
252
- - TRANSFORM
253
- ----------------------------------*/
254
-
255
- const migrateServiceFile = (filepath: string, serviceRoots: TServiceRoot[], repoRoot: string) => {
256
- const code = fs.readFileSync(filepath, 'utf8');
257
- if (!/@Route\(|from ['"]@app['"]|from ['"]@models['"]|from ['"]@request['"]/.test(code)) return false;
258
-
259
- const ast = parse(code, {
260
- sourceType: 'module',
261
- plugins: ['typescript', 'jsx', 'decorators-legacy', 'classProperties'],
262
- });
263
-
264
- const importBindings = new Map<string, TImportBinding>();
265
- const schemaExports = new Set<string>();
266
- const routeMethods: TRouteMethod[] = [];
267
- let fileChanged = false;
268
-
269
- const serviceRoot = findNearestServiceRoot(serviceRoots, filepath);
270
-
271
- let serviceClassName = '';
272
-
273
- traverse(ast, {
274
- ImportDeclaration(path) {
275
- const source = path.node.source.value;
276
-
277
- if (source === '@server/app/service') {
278
- const beforeLength = path.node.specifiers.length;
279
- path.node.specifiers = path.node.specifiers.filter(
280
- (specifier) =>
281
- !(
282
- specifier.type === 'ImportSpecifier' &&
283
- specifier.imported.type === 'Identifier' &&
284
- specifier.imported.name === 'Route'
285
- ),
286
- );
287
-
288
- if (path.node.specifiers.length !== beforeLength) fileChanged = true;
289
-
290
- if (!path.node.specifiers.length) path.remove();
291
-
292
- return;
293
- }
294
-
295
- if (source !== '@app' && source !== '@models' && source !== '@request') return;
296
-
297
- for (const specifier of path.node.specifiers) {
298
- if (specifier.type !== 'ImportSpecifier' || specifier.imported.type !== 'Identifier') continue;
299
-
300
- importBindings.set(specifier.local.name, {
301
- source: source as TImportBinding['source'],
302
- imported: specifier.imported.name,
303
- });
304
- }
305
-
306
- path.remove();
307
- fileChanged = true;
308
- },
309
-
310
- ExportDefaultDeclaration(path) {
311
- const declaration = path.node.declaration;
312
-
313
- if (declaration.type === 'ClassDeclaration' && declaration.id?.name) serviceClassName = declaration.id.name;
314
- },
315
-
316
- ClassMethod(path) {
317
- if (path.node.key.type !== 'Identifier' || !path.node.decorators?.length) return;
318
-
319
- const nextDecorators: types.Decorator[] = [];
320
-
321
- for (const decorator of path.node.decorators) {
322
- const routeMeta = getRouteDecoratorMeta(decorator);
323
- if (!routeMeta) {
324
- nextDecorators.push(decorator);
325
- continue;
326
- }
327
-
328
- const schemaImports: string[] = [];
329
- const methodName = path.node.key.name;
330
- const schemaExpression = routeMeta.schemaExpression;
331
- const schemaSource = schemaExpression ? generate(schemaExpression).code : undefined;
332
-
333
- if (schemaExpression?.type === 'Identifier') {
334
- schemaExports.add(schemaExpression.name);
335
- schemaImports.push(schemaExpression.name);
336
- }
337
-
338
- routeMethods.push({ methodName, routePath: routeMeta.routePath, schemaSource, schemaImports });
339
-
340
- fileChanged = true;
341
- }
342
-
343
- path.node.decorators = nextDecorators.length ? nextDecorators : undefined;
344
-
345
- path.traverse({
346
- Identifier(identifierPath) {
347
- const binding = importBindings.get(identifierPath.node.name);
348
- if (!binding) return;
349
-
350
- if (identifierPath.scope.getBinding(identifierPath.node.name)?.path.isImportSpecifier() !== true)
351
- return;
352
-
353
- replaceReferencedIdentifier(identifierPath, binding);
354
- fileChanged = true;
355
- },
356
- });
357
- },
358
- });
359
-
360
- if (!fileChanged) return false;
361
-
362
- traverse(ast, {
363
- Program(programPath) {
364
- for (const schemaExport of schemaExports) ensureNamedExport(programPath, schemaExport);
365
- },
366
- });
367
-
368
- fs.writeFileSync(filepath, generate(ast, { decoratorsBeforeExport: true }, code).code);
369
-
370
- if (!routeMethods.length) return true;
371
-
372
- if (!serviceRoot) throw new Error(`Unable to find the parent service root for ${filepath}`);
373
-
374
- const serviceSegments = getRelativeServiceSegments(serviceRoot.dir, filepath);
375
- const serviceAccessPath = [serviceRoot.alias, ...serviceSegments].join('.');
376
-
377
- const controllerRelativePath = [serviceRoot.alias, ...serviceSegments].join('/');
378
- const controllerFilepath = path.join(repoRoot, 'server', 'controllers', `${controllerRelativePath}.ts`);
379
- const relativeServiceImportPath = path
380
- .relative(path.dirname(controllerFilepath), filepath)
381
- .replace(/\\/g, '/')
382
- .replace(/\.(tsx?|jsx?)$/, '');
383
- const normalizedServiceImportPath =
384
- relativeServiceImportPath.startsWith('.') ? relativeServiceImportPath : `./${relativeServiceImportPath}`;
385
- const schemaImports = [...new Set(routeMethods.flatMap((routeMethod) => routeMethod.schemaImports))];
386
- const controllerClassName = getControllerClassName(serviceClassName, filepath);
387
- const needsSchemaHelperImport = routeMethods.some((routeMethod) => routeMethod.schemaSource?.includes('schema.'));
388
- const defaultControllerPath = getControllerBasePath(repoRoot, controllerFilepath);
389
- const routeBasePaths = new Set<string>();
390
-
391
- for (const routeMethod of routeMethods) {
392
- const routeSegments = routeMethod.routePath.split('/').filter(Boolean);
393
- const routeMethodName = routeSegments.pop();
394
-
395
- if (routeMethodName !== routeMethod.methodName) {
396
- throw new Error(
397
- `Unable to migrate ${filepath}#${routeMethod.methodName}: route path ${JSON.stringify(routeMethod.routePath)} renames the method. ` +
398
- 'Rename the method or split the controller manually.',
399
- );
400
- }
401
-
402
- routeBasePaths.add(routeSegments.join('/'));
403
- }
404
-
405
- if (routeBasePaths.size > 1) {
406
- throw new Error(
407
- `Unable to migrate ${filepath}: methods use multiple route bases (${[...routeBasePaths].join(', ')}). ` +
408
- 'Split the service into multiple controllers before migration.',
409
- );
410
- }
411
-
412
- const controllerPath = [...routeBasePaths][0] || '';
413
- const controllerPathExport =
414
- controllerPath && controllerPath !== defaultControllerPath
415
- ? `export const controllerPath = ${JSON.stringify(controllerPath)};\n\n`
416
- : '';
417
-
418
- const methodBlocks = routeMethods
419
- .map((routeMethod) => {
420
- const inputLine = routeMethod.schemaSource
421
- ? ` const data = this.input(${routeMethod.schemaSource});\n`
422
- : '';
423
- const callArgs = routeMethod.schemaSource ? 'data' : '';
424
-
425
- return ` public async ${routeMethod.methodName}() {
426
- const { ${serviceRoot.alias} } = this.services;
427
- ${inputLine}
428
- return ${serviceAccessPath}.${routeMethod.methodName}(${callArgs});
429
- }`;
430
- })
431
- .join('\n\n');
432
-
433
- fs.ensureDirSync(path.dirname(controllerFilepath));
434
- fs.writeFileSync(
435
- controllerFilepath,
436
- `import Controller${needsSchemaHelperImport ? ', { schema }' : ''} from '@server/app/controller';
437
- ${schemaImports.length ? `import { ${schemaImports.join(', ')} } from ${JSON.stringify(normalizedServiceImportPath)};\n` : ''}
438
- ${controllerPathExport}
439
- export default class ${controllerClassName} extends Controller {
440
-
441
- ${methodBlocks}
442
- }
443
- `,
444
- );
445
-
446
- return true;
447
- };
448
-
449
- /*----------------------------------
450
- - RUN
451
- ----------------------------------*/
452
-
453
- const repoRoots = process.argv.slice(2);
454
- if (!repoRoots.length)
455
- throw new Error('Usage: ts-node scripts/refactor-server-controllers.ts <repo-root> [repo-root...]');
456
-
457
- for (const repoRoot of repoRoots) {
458
- const serviceRoots = findServiceRoots(repoRoot);
459
- const serviceFiles = findFiles(
460
- path.join(repoRoot, 'server', 'services'),
461
- (filepath) => /\.(tsx?|jsx?)$/.test(filepath) && !filepath.endsWith('.controller.ts'),
462
- );
463
-
464
- let migratedFiles = 0;
465
-
466
- for (const serviceFile of serviceFiles) {
467
- if (migrateServiceFile(serviceFile, serviceRoots, repoRoot)) migratedFiles++;
468
- }
469
-
470
- console.log(`[refactor-server-controllers] ${repoRoot}: migrated ${migratedFiles} service files`);
471
- }