proteum 2.4.4 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +81 -52
  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 +5 -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/server/services/AGENTS.md +4 -0
  12. package/agents/project/tests/AGENTS.md +1 -1
  13. package/cli/commands/doctor.ts +54 -3
  14. package/cli/commands/runtime.ts +6 -0
  15. package/cli/commands/worktree.ts +116 -0
  16. package/cli/compiler/artifacts/controllers.ts +16 -15
  17. package/cli/compiler/artifacts/discovery.ts +129 -17
  18. package/cli/compiler/artifacts/routing.ts +0 -5
  19. package/cli/compiler/artifacts/services.ts +253 -76
  20. package/cli/compiler/common/controllers.ts +159 -57
  21. package/cli/compiler/common/generatedRouteModules.ts +457 -363
  22. package/cli/mcp/router.ts +47 -3
  23. package/cli/presentation/commands.ts +25 -15
  24. package/cli/runtime/commands.ts +39 -12
  25. package/cli/runtime/worktreeBootstrap.ts +608 -0
  26. package/cli/scaffold/index.ts +28 -18
  27. package/cli/scaffold/templates.ts +44 -33
  28. package/cli/utils/agents.ts +14 -1
  29. package/client/app/index.ts +22 -5
  30. package/client/services/router/index.tsx +23 -3
  31. package/client/services/router/request/api.ts +16 -6
  32. package/common/dev/contractsDoctor.ts +1 -1
  33. package/common/dev/mcpPayloads.ts +8 -1
  34. package/common/env/proteumEnv.ts +14 -2
  35. package/common/router/contracts.ts +1 -1
  36. package/common/router/definitions.ts +177 -0
  37. package/common/router/index.ts +23 -12
  38. package/common/router/pageData.ts +5 -5
  39. package/common/router/register.ts +2 -2
  40. package/common/router/request/api.ts +12 -2
  41. package/docs/agent-routing.md +5 -2
  42. package/docs/diagnostics.md +2 -0
  43. package/docs/mcp.md +6 -3
  44. package/docs/migration-2.5.md +226 -0
  45. package/eslint.js +89 -42
  46. package/package.json +1 -1
  47. package/server/app/commands.ts +5 -1
  48. package/server/app/container/console/index.ts +1 -1
  49. package/server/app/controller/index.ts +98 -40
  50. package/server/app/index.ts +120 -3
  51. package/server/app/service/index.ts +5 -1
  52. package/server/index.ts +6 -2
  53. package/server/services/router/index.ts +50 -41
  54. package/server/services/router/response/index.ts +2 -2
  55. package/tests/agents-utils.test.cjs +14 -1
  56. package/tests/cli-mcp-command.test.cjs +84 -0
  57. package/tests/client-app-error-handling.test.cjs +100 -0
  58. package/tests/definition-contracts.test.cjs +453 -0
  59. package/tests/dev-transpile-watch.test.cjs +37 -31
  60. package/tests/eslint-rules.test.cjs +185 -8
  61. package/tests/mcp.test.cjs +90 -0
  62. package/tests/scaffold-templates.test.cjs +18 -0
  63. package/tests/server-app-report-error.test.cjs +135 -0
  64. package/tests/worktree-bootstrap.test.cjs +206 -0
  65. package/types/aliases.d.ts +0 -5
  66. package/types/controller-input.test.ts +23 -17
  67. package/types/controller-request-context.test.ts +10 -11
  68. package/cli/commands/migrate.ts +0 -51
  69. package/cli/migrate/pageContract.ts +0 -516
  70. package/docs/migrate-from-2.1.3.md +0 -396
  71. package/scripts/cleanup-generated-controllers.ts +0 -62
  72. package/scripts/fix-reference-app-typing.ts +0 -490
  73. package/scripts/format-router-registrations.ts +0 -119
  74. package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
  75. package/scripts/refactor-client-app-imports.ts +0 -244
  76. package/scripts/refactor-client-pages.ts +0 -587
  77. package/scripts/refactor-server-controllers.ts +0 -471
  78. package/scripts/refactor-server-runtime-aliases.ts +0 -360
  79. package/scripts/restore-client-app-import-files.ts +0 -41
@@ -8,7 +8,7 @@ import writeIfChanged from '../writeIfChanged';
8
8
  import { resolveConnectedProjectContracts, writeConnectedProjectContract } from './connectedProjects';
9
9
  import { normalizeAbsolutePath } from './shared';
10
10
 
11
- const reservedConnectedContextKeys = new Set(['app', 'context', 'request', 'response', 'route', 'api', 'Router']);
11
+ const reservedConnectedContextKeys = new Set(['app', 'services', 'context', 'request', 'response', 'route', 'api', 'Router']);
12
12
 
13
13
  const getManifestScopeFromImportPath = (importPath: string) =>
14
14
  importPath.startsWith('@server/controllers/') ? 'framework' : 'app';
@@ -70,10 +70,11 @@ export const generateControllerArtifacts = async () => {
70
70
  const typeImports: string[] = [];
71
71
 
72
72
  localControllers.forEach((controller, index) => {
73
- typeImports.push(`import type Controller${index} from ${JSON.stringify(controller.importPath)};`);
73
+ typeImports.push(`type Controller${index} = typeof import(${JSON.stringify(controller.importPath)}).default;`);
74
74
 
75
75
  controller.methods.forEach((method) => {
76
- const resultType = `TControllerResult<Controller${index}, ${JSON.stringify(method.name)}>`;
76
+ const resultType = `TControllerActionResult<Controller${index}, ${JSON.stringify(method.name)}>`;
77
+ const inputType = `TControllerActionInput<Controller${index}, ${JSON.stringify(method.name)}>`;
77
78
  const clientAccessor = method.routePath.split('/').join('.');
78
79
 
79
80
  manifestControllers.push({
@@ -98,6 +99,7 @@ export const generateControllerArtifacts = async () => {
98
99
  connected: undefined,
99
100
  hasInput: method.inputCallsCount > 0,
100
101
  httpPath: '/api/' + method.routePath,
102
+ inputType,
101
103
  methodName: method.name,
102
104
  resultType,
103
105
  typeName: `Controller${index}`,
@@ -109,6 +111,7 @@ export const generateControllerArtifacts = async () => {
109
111
  clientAccessor,
110
112
  JSON.stringify({
111
113
  hasInput: method.inputCallsCount > 0,
114
+ inputType,
112
115
  methodName: method.name,
113
116
  typeName: `Controller${index}`,
114
117
  }),
@@ -183,6 +186,7 @@ export const generateControllerArtifacts = async () => {
183
186
  };
184
187
  hasInput: boolean;
185
188
  httpPath: string;
189
+ inputType?: string;
186
190
  resultType: string;
187
191
  };
188
192
 
@@ -191,7 +195,7 @@ export const generateControllerArtifacts = async () => {
191
195
  : '';
192
196
 
193
197
  return meta.hasInput
194
- ? `(data) => api.createFetcher<${meta.resultType}>('POST', ${JSON.stringify(meta.httpPath)}, data${connectedOptions})`
198
+ ? `(data: ${meta.inputType || 'any'}) => api.createFetcher<${meta.resultType}>('POST', ${JSON.stringify(meta.httpPath)}, data as TPostDataWithFile${connectedOptions})`
195
199
  : `() => api.createFetcher<${meta.resultType}>('POST', ${JSON.stringify(meta.httpPath)}, undefined${connectedOptions})`;
196
200
  };
197
201
 
@@ -205,15 +209,16 @@ export const generateControllerArtifacts = async () => {
205
209
  }
206
210
  | {
207
211
  hasInput: boolean;
212
+ inputType?: string;
208
213
  methodName: string;
209
214
  typeName: string;
210
215
  };
211
216
 
212
217
  if ('rawType' in meta) return meta.rawType;
213
218
  if ('runtimeOnly' in meta) return 'any';
214
- const fetcherType = `TControllerFetcher<${meta.typeName}, ${JSON.stringify(meta.methodName)}>`;
219
+ const fetcherType = `TFetcher<TControllerActionResult<${meta.typeName}, ${JSON.stringify(meta.methodName)}>>`;
215
220
 
216
- return meta.hasInput ? `(data: any) => ${fetcherType}` : `() => ${fetcherType}`;
221
+ return meta.hasInput ? `(data: ${meta.inputType || 'any'}) => ${fetcherType}` : `() => ${fetcherType}`;
217
222
  };
218
223
 
219
224
  const createControllersContent = `/*----------------------------------
@@ -224,14 +229,10 @@ export const generateControllerArtifacts = async () => {
224
229
  // Do not edit it manually.
225
230
 
226
231
  import type ApiClient from '@common/router/request/api';
227
- import type { TFetcher } from '@common/router/request/api';
232
+ import type { TFetcher, TPostDataWithFile } from '@common/router/request/api';
233
+ import type { TControllerActionInput, TControllerActionResult } from '@server/app/controller';
228
234
  ${[...typeImports, ...connectedControllerTypeImports].join('\n') ? '\n' + [...typeImports, ...connectedControllerTypeImports].join('\n') : ''}
229
235
 
230
- type TControllerResult<TController, TMethod extends keyof TController> =
231
- TController[TMethod] extends (...args: any[]) => infer TResult ? Awaited<TResult> : never;
232
-
233
- type TControllerFetcher<TController, TMethod extends keyof TController> = TFetcher<TControllerResult<TController, TMethod>>;
234
-
235
236
  type TConnectedFallbackValue =
236
237
  | string
237
238
  | number
@@ -284,7 +285,7 @@ export type { TControllers } from '@generated/common/controllers';
284
285
  path: ${JSON.stringify('/api/' + method.routePath)},
285
286
  filepath: ${JSON.stringify(normalizeAbsolutePath(controller.filepath))},
286
287
  sourceLocation: { line: ${method.sourceLocation.line}, column: ${method.sourceLocation.column} },
287
- Controller: Controller${controllerIndex},
288
+ action: Controller${controllerIndex}.actions[${JSON.stringify(method.name)}],
288
289
  method: ${JSON.stringify(method.name)},
289
290
  },`,
290
291
  ),
@@ -299,14 +300,14 @@ export type { TControllers } from '@generated/common/controllers';
299
300
  // This file is generated by Proteum from server controller files.
300
301
  // Do not edit it manually.
301
302
 
302
- import type Controller from '@server/app/controller';
303
+ import type { TControllerActionDefinition } from '@server/app/controller';
303
304
  ${controllerImports ? '\n' + controllerImports : ''}
304
305
 
305
306
  export type TGeneratedControllerDefinition = {
306
307
  path: string,
307
308
  filepath: string,
308
309
  sourceLocation: { line: number, column: number },
309
- Controller: new (request: any) => Controller,
310
+ action: TControllerActionDefinition<any, any, any, any>,
310
311
  method: string,
311
312
  }
312
313
 
@@ -2,11 +2,32 @@ import path from 'path';
2
2
  import fs from 'fs-extra';
3
3
  import ts from 'typescript';
4
4
 
5
- import app from '../../app';
6
5
  import { normalizePath } from './shared';
7
6
 
8
- const hasRegisteredRouteDefinitions = (filepath: string, content: string) => {
9
- const sourceFile = ts.createSourceFile(
7
+ type TRouteSide = 'client' | 'server';
8
+
9
+ const legacyRouterMethods = new Set([
10
+ 'page',
11
+ 'error',
12
+ 'all',
13
+ 'options',
14
+ 'get',
15
+ 'post',
16
+ 'put',
17
+ 'delete',
18
+ 'patch',
19
+ 'express',
20
+ ]);
21
+ const routeDefinitionHelpers = new Set([
22
+ 'definePageRoute',
23
+ 'defineErrorRoute',
24
+ 'defineServerRoute',
25
+ 'defineServerRoutes',
26
+ ]);
27
+ const routeDefinitionImportSources = new Set(['@common/router', '@common/router/definitions']);
28
+
29
+ const parseSourceFile = (filepath: string, content: string) =>
30
+ ts.createSourceFile(
10
31
  filepath,
11
32
  content,
12
33
  ts.ScriptTarget.Latest,
@@ -14,22 +35,110 @@ const hasRegisteredRouteDefinitions = (filepath: string, content: string) => {
14
35
  filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
15
36
  );
16
37
 
17
- return sourceFile.statements.some((statement) => {
18
- if (!ts.isExpressionStatement(statement)) return false;
19
- if (!ts.isCallExpression(statement.expression)) return false;
20
- if (!ts.isPropertyAccessExpression(statement.expression.expression)) return false;
38
+ const getCallExpressionName = (node: ts.Expression) => {
39
+ if (ts.isIdentifier(node)) return node.text;
40
+ if (ts.isPropertyAccessExpression(node)) return node.name.text;
41
+ return undefined;
42
+ };
21
43
 
22
- const callee = statement.expression.expression;
44
+ const collectConstInitializers = (sourceFile: ts.SourceFile) => {
45
+ const initializers = new Map<string, ts.Expression>();
46
+
47
+ for (const statement of sourceFile.statements) {
48
+ if (!ts.isVariableStatement(statement)) continue;
49
+ if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
50
+
51
+ for (const declaration of statement.declarationList.declarations) {
52
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
53
+
54
+ initializers.set(declaration.name.text, declaration.initializer);
55
+ }
56
+ }
57
+
58
+ return initializers;
59
+ };
60
+
61
+ const resolveIdentifierExpression = (
62
+ expression: ts.Expression,
63
+ constInitializers: Map<string, ts.Expression>,
64
+ ): ts.Expression => {
65
+ if (!ts.isIdentifier(expression)) return expression;
66
+
67
+ return constInitializers.get(expression.text) || expression;
68
+ };
69
+
70
+ const getDefaultExportExpression = (sourceFile: ts.SourceFile) => {
71
+ const constInitializers = collectConstInitializers(sourceFile);
72
+
73
+ for (const statement of sourceFile.statements) {
74
+ if (!ts.isExportAssignment(statement) || statement.isExportEquals) continue;
75
+
76
+ return resolveIdentifierExpression(statement.expression, constInitializers);
77
+ }
78
+
79
+ return undefined;
80
+ };
23
81
 
24
- return (
25
- ts.isIdentifier(callee.expression) &&
26
- callee.expression.text === 'Router' &&
27
- ['page', 'error', 'get', 'post', 'put', 'delete', 'patch'].includes(callee.name.text)
82
+ const hasRouteDefinitionHelperImport = (sourceFile: ts.SourceFile) =>
83
+ sourceFile.statements.some((statement) => {
84
+ if (!ts.isImportDeclaration(statement)) return false;
85
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) return false;
86
+ if (!routeDefinitionImportSources.has(statement.moduleSpecifier.text)) return false;
87
+ if (!statement.importClause?.namedBindings) return false;
88
+ if (!ts.isNamedImports(statement.importClause.namedBindings)) return false;
89
+
90
+ return statement.importClause.namedBindings.elements.some((specifier) =>
91
+ routeDefinitionHelpers.has(specifier.propertyName?.text || specifier.name.text),
28
92
  );
29
93
  });
94
+
95
+ const hasExplicitRouteDefinitionExport = (sourceFile: ts.SourceFile) => {
96
+ const defaultExportExpression = getDefaultExportExpression(sourceFile);
97
+
98
+ return (
99
+ !!defaultExportExpression &&
100
+ ts.isCallExpression(defaultExportExpression) &&
101
+ !!getCallExpressionName(defaultExportExpression.expression) &&
102
+ routeDefinitionHelpers.has(getCallExpressionName(defaultExportExpression.expression)!)
103
+ );
104
+ };
105
+
106
+ const hasLegacyRouteMagic = (sourceFile: ts.SourceFile, side: TRouteSide) => {
107
+ for (const statement of sourceFile.statements) {
108
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
109
+ if (statement.moduleSpecifier.text === '@app') return true;
110
+ }
111
+
112
+ if (!ts.isExpressionStatement(statement)) continue;
113
+ if (!ts.isCallExpression(statement.expression)) continue;
114
+ if (!ts.isPropertyAccessExpression(statement.expression.expression)) continue;
115
+
116
+ const callee = statement.expression.expression;
117
+ if (!ts.isIdentifier(callee.expression)) continue;
118
+ if (callee.expression.text !== 'Router') continue;
119
+ if (!legacyRouterMethods.has(callee.name.text)) continue;
120
+
121
+ return true;
122
+ }
123
+
124
+ return false;
30
125
  };
31
126
 
32
- const findRegisteredRouteFiles = (dir: string, options: { excludeLayoutDirectories?: boolean } = {}): string[] => {
127
+ const hasRegisteredRouteDefinitions = (filepath: string, content: string, side: TRouteSide) => {
128
+ const sourceFile = parseSourceFile(filepath, content);
129
+
130
+ return (
131
+ hasExplicitRouteDefinitionExport(sourceFile) ||
132
+ hasRouteDefinitionHelperImport(sourceFile) ||
133
+ hasLegacyRouteMagic(sourceFile, side)
134
+ );
135
+ };
136
+
137
+ const findRegisteredRouteFiles = (
138
+ dir: string,
139
+ side: TRouteSide,
140
+ options: { excludeLayoutDirectories?: boolean } = {},
141
+ ): string[] => {
33
142
  if (!fs.existsSync(dir)) return [];
34
143
 
35
144
  const files: string[] = [];
@@ -40,7 +149,7 @@ const findRegisteredRouteFiles = (dir: string, options: { excludeLayoutDirectori
40
149
  if (dirent.isDirectory()) {
41
150
  if (options.excludeLayoutDirectories && dirent.name === '_layout') continue;
42
151
 
43
- files.push(...findRegisteredRouteFiles(filePath, options));
152
+ files.push(...findRegisteredRouteFiles(filePath, side, options));
44
153
  continue;
45
154
  }
46
155
 
@@ -48,7 +157,7 @@ const findRegisteredRouteFiles = (dir: string, options: { excludeLayoutDirectori
48
157
  if (!/\.(ts|tsx)$/.test(dirent.name)) continue;
49
158
 
50
159
  const content = fs.readFileSync(filePath, 'utf8');
51
- if (!hasRegisteredRouteDefinitions(filePath, content)) continue;
160
+ if (!hasRegisteredRouteDefinitions(filePath, content, side)) continue;
52
161
 
53
162
  files.push(filePath);
54
163
  }
@@ -56,9 +165,11 @@ const findRegisteredRouteFiles = (dir: string, options: { excludeLayoutDirectori
56
165
  return files;
57
166
  };
58
167
 
59
- export const findClientRouteFiles = (dir: string) => findRegisteredRouteFiles(dir, { excludeLayoutDirectories: true });
168
+ export const findClientRouteFiles = (dir: string) => findRegisteredRouteFiles(dir, 'client', { excludeLayoutDirectories: true });
169
+
170
+ export const findServerRouteFiles = (dir: string) => findRegisteredRouteFiles(dir, 'server');
60
171
 
61
- export const findServerRouteFiles = (dir: string) => findRegisteredRouteFiles(dir);
172
+ const getApp = () => require('../../app').default as typeof import('../../app').default;
62
173
 
63
174
  export const findLayoutFiles = (dir: string): string[] => {
64
175
  if (!fs.existsSync(dir)) return [];
@@ -84,6 +195,7 @@ export const findLayoutFiles = (dir: string): string[] => {
84
195
  };
85
196
 
86
197
  export const readPreloadedRouteChunks = () => {
198
+ const app = getApp();
87
199
  const preloadPath = path.join(app.paths.pages, 'preload.json');
88
200
 
89
201
  if (!fs.existsSync(preloadPath)) return new Set<string>();
@@ -89,7 +89,6 @@ const buildServerRouteManifestEntries = (filepath: string) =>
89
89
 
90
90
  const generateClientRouteWrapperModules = () => {
91
91
  const clientRouteFiles = findClientRouteFiles(app.paths.pages).sort((a, b) => a.localeCompare(b));
92
- const routeSourceFilepaths = new Set(clientRouteFiles.map((filepath) => normalizePath(path.resolve(filepath))));
93
92
  const routes = clientRouteFiles.map((filepath) => buildClientRouteManifestEntry(filepath));
94
93
 
95
94
  for (const filepath of clientRouteFiles) {
@@ -101,7 +100,6 @@ const generateClientRouteWrapperModules = () => {
101
100
  side: 'client',
102
101
  sourceFilepath: filepath,
103
102
  clientRoute: { chunkId: pageChunk.chunkId },
104
- routeSourceFilepaths,
105
103
  });
106
104
 
107
105
  writeGeneratedRouteModule({
@@ -110,7 +108,6 @@ const generateClientRouteWrapperModules = () => {
110
108
  side: 'client',
111
109
  sourceFilepath: filepath,
112
110
  clientRoute: { chunkId: pageChunk.chunkId },
113
- routeSourceFilepaths,
114
111
  });
115
112
  }
116
113
 
@@ -121,7 +118,6 @@ const generateServerRouteWrapperModules = () => {
121
118
  const serverRouteFiles = findServerRouteFiles(path.join(app.paths.root, 'server', 'routes')).sort((a, b) =>
122
119
  a.localeCompare(b),
123
120
  );
124
- const routeSourceFilepaths = new Set(serverRouteFiles.map((filepath) => normalizePath(path.resolve(filepath))));
125
121
  const routes = serverRouteFiles.flatMap((filepath) => buildServerRouteManifestEntries(filepath));
126
122
 
127
123
  for (const filepath of serverRouteFiles) {
@@ -130,7 +126,6 @@ const generateServerRouteWrapperModules = () => {
130
126
  runtime: 'server',
131
127
  side: 'server',
132
128
  sourceFilepath: filepath,
133
- routeSourceFilepaths,
134
129
  });
135
130
  }
136
131