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.
- package/README.md +60 -55
- package/agents/project/AGENTS.md +112 -31
- package/agents/project/CODING_STYLE.md +2 -2
- package/agents/project/app-root/AGENTS.md +1 -3
- package/agents/project/client/AGENTS.md +1 -1
- package/agents/project/client/pages/AGENTS.md +21 -9
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/optimizations.md +1 -1
- package/agents/project/root/AGENTS.md +105 -22
- package/agents/project/server/routes/AGENTS.md +30 -1
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/doctor.ts +54 -3
- package/cli/commands/runtime.ts +6 -0
- package/cli/commands/worktree.ts +116 -0
- package/cli/compiler/artifacts/controllers.ts +16 -15
- package/cli/compiler/artifacts/discovery.ts +129 -17
- package/cli/compiler/artifacts/routing.ts +0 -5
- package/cli/compiler/artifacts/services.ts +253 -76
- package/cli/compiler/common/controllers.ts +159 -57
- package/cli/compiler/common/generatedRouteModules.ts +457 -363
- package/cli/mcp/router.ts +47 -3
- package/cli/presentation/commands.ts +25 -15
- package/cli/runtime/commands.ts +39 -12
- package/cli/runtime/worktreeBootstrap.ts +608 -0
- package/cli/scaffold/index.ts +28 -18
- package/cli/scaffold/templates.ts +44 -33
- package/cli/utils/agents.ts +14 -1
- package/client/services/router/index.tsx +23 -3
- package/client/services/router/request/api.ts +14 -4
- package/common/dev/contractsDoctor.ts +1 -1
- package/common/dev/mcpPayloads.ts +8 -1
- package/common/env/proteumEnv.ts +14 -2
- package/common/router/contracts.ts +1 -1
- package/common/router/definitions.ts +177 -0
- package/common/router/index.ts +23 -12
- package/common/router/pageData.ts +5 -5
- package/common/router/register.ts +2 -2
- package/common/router/request/api.ts +12 -2
- package/docs/agent-routing.md +5 -2
- package/docs/diagnostics.md +2 -0
- package/docs/mcp.md +6 -3
- package/eslint.js +36 -1
- package/package.json +1 -1
- package/server/app/commands.ts +5 -1
- package/server/app/container/console/http-client-error-context.test.cjs +10 -1
- package/server/app/container/console/index.ts +2 -1
- package/server/app/controller/index.ts +98 -40
- package/server/app/index.ts +92 -1
- package/server/app/service/index.ts +5 -1
- package/server/index.ts +6 -2
- package/server/services/router/index.ts +47 -38
- package/server/services/router/response/index.ts +2 -2
- package/tests/agents-utils.test.cjs +14 -1
- package/tests/cli-mcp-command.test.cjs +84 -0
- package/tests/definition-contracts.test.cjs +453 -0
- package/tests/dev-transpile-watch.test.cjs +37 -28
- package/tests/eslint-rules.test.cjs +39 -1
- package/tests/mcp.test.cjs +90 -0
- package/tests/worktree-bootstrap.test.cjs +206 -0
- package/types/aliases.d.ts +0 -5
- package/types/controller-input.test.ts +23 -17
- package/types/controller-request-context.test.ts +10 -11
- package/cli/commands/migrate.ts +0 -51
- package/cli/migrate/pageContract.ts +0 -516
- package/docs/migrate-from-2.1.3.md +0 -396
- package/scripts/cleanup-generated-controllers.ts +0 -62
- package/scripts/fix-reference-app-typing.ts +0 -490
- package/scripts/format-router-registrations.ts +0 -119
- package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
- package/scripts/refactor-client-app-imports.ts +0 -244
- package/scripts/refactor-client-pages.ts +0 -587
- package/scripts/refactor-server-controllers.ts +0 -471
- package/scripts/refactor-server-runtime-aliases.ts +0 -360
- package/scripts/restore-client-app-import-files.ts +0 -41
|
@@ -98,8 +98,8 @@ const getNodeLocation = (sourceFile: ts.SourceFile, node: ts.Node): TControllerS
|
|
|
98
98
|
return { line: line + 1, column: character + 1 };
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
-
const hasModifier = (node: ts.Node, kind: ts.SyntaxKind) =>
|
|
102
|
-
!!node.modifiers?.some((modifier) => modifier.kind === kind);
|
|
101
|
+
const hasModifier = (node: ts.Node & { modifiers?: ts.NodeArray<ts.ModifierLike> }, kind: ts.SyntaxKind) =>
|
|
102
|
+
!!node.modifiers?.some((modifier: ts.ModifierLike) => modifier.kind === kind);
|
|
103
103
|
|
|
104
104
|
const getDefaultExportClass = (sourceFile: ts.SourceFile) => {
|
|
105
105
|
const classes = new Map<string, ts.ClassDeclaration>();
|
|
@@ -125,42 +125,165 @@ const getDefaultExportClass = (sourceFile: ts.SourceFile) => {
|
|
|
125
125
|
return undefined;
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
-
const
|
|
128
|
+
const getPropertyKey = (name: ts.PropertyName) => {
|
|
129
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
|
130
|
+
return undefined;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const getPropertyAssignment = (node: ts.ObjectLiteralExpression, propertyName: string) => {
|
|
134
|
+
for (const property of node.properties) {
|
|
135
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
136
|
+
|
|
137
|
+
const key = getPropertyKey(property.name);
|
|
138
|
+
if (key === propertyName) return property.initializer;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return undefined;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const collectConstInitializers = (sourceFile: ts.SourceFile) => {
|
|
145
|
+
const initializers = new Map<string, ts.Expression>();
|
|
146
|
+
|
|
129
147
|
for (const statement of sourceFile.statements) {
|
|
130
148
|
if (!ts.isVariableStatement(statement)) continue;
|
|
131
|
-
if (!
|
|
149
|
+
if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
|
|
132
150
|
|
|
133
151
|
for (const declaration of statement.declarationList.declarations) {
|
|
134
|
-
if (!ts.isIdentifier(declaration.name)) continue;
|
|
135
|
-
if (declaration.name.text !== exportName) continue;
|
|
136
|
-
if (!declaration.initializer || !ts.isStringLiteral(declaration.initializer)) continue;
|
|
152
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
137
153
|
|
|
138
|
-
|
|
154
|
+
initializers.set(declaration.name.text, declaration.initializer);
|
|
139
155
|
}
|
|
140
156
|
}
|
|
141
157
|
|
|
158
|
+
return initializers;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const tryEvaluateStaticString = (
|
|
162
|
+
sourceFile: ts.SourceFile,
|
|
163
|
+
expression: ts.Expression,
|
|
164
|
+
activeNames = new Set<string>(),
|
|
165
|
+
): string | undefined => {
|
|
166
|
+
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
|
|
167
|
+
|
|
168
|
+
if (ts.isIdentifier(expression)) {
|
|
169
|
+
if (activeNames.has(expression.text)) return undefined;
|
|
170
|
+
|
|
171
|
+
const initializer = collectConstInitializers(sourceFile).get(expression.text);
|
|
172
|
+
if (!initializer) return undefined;
|
|
173
|
+
|
|
174
|
+
activeNames.add(expression.text);
|
|
175
|
+
const value = tryEvaluateStaticString(sourceFile, initializer, activeNames);
|
|
176
|
+
activeNames.delete(expression.text);
|
|
177
|
+
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return undefined;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const resolveIdentifierExpression = (sourceFile: ts.SourceFile, expression: ts.Expression): ts.Expression => {
|
|
185
|
+
if (!ts.isIdentifier(expression)) return expression;
|
|
186
|
+
|
|
187
|
+
return collectConstInitializers(sourceFile).get(expression.text) || expression;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const getDefaultControllerExpression = (sourceFile: ts.SourceFile) => {
|
|
191
|
+
for (const statement of sourceFile.statements) {
|
|
192
|
+
if (!ts.isExportAssignment(statement) || statement.isExportEquals) continue;
|
|
193
|
+
|
|
194
|
+
return resolveIdentifierExpression(sourceFile, statement.expression);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return undefined;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const getCallExpressionName = (node: ts.Expression) => {
|
|
201
|
+
if (ts.isIdentifier(node)) return node.text;
|
|
202
|
+
if (ts.isPropertyAccessExpression(node)) return node.name.text;
|
|
142
203
|
return undefined;
|
|
143
204
|
};
|
|
144
205
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
155
|
-
|
|
206
|
+
const assertNoControllerMagicImports = (sourceFile: ts.SourceFile) => {
|
|
207
|
+
for (const statement of sourceFile.statements) {
|
|
208
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
209
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
210
|
+
if (statement.moduleSpecifier.text !== '@app') continue;
|
|
211
|
+
|
|
212
|
+
const location = getNodeLocation(sourceFile, statement);
|
|
213
|
+
throw new Error(
|
|
214
|
+
`${sourceFile.fileName}:${location.line}:${location.column} imports @app. Controller modules must export defineController(...) and receive services through typed action context.`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const readExplicitController = (
|
|
220
|
+
sourceFile: ts.SourceFile,
|
|
221
|
+
filepath: string,
|
|
222
|
+
root: string,
|
|
223
|
+
): TControllerFileMeta | undefined => {
|
|
224
|
+
const expression = getDefaultControllerExpression(sourceFile);
|
|
225
|
+
if (!expression) return undefined;
|
|
226
|
+
|
|
227
|
+
assertNoControllerMagicImports(sourceFile);
|
|
228
|
+
|
|
229
|
+
if (!ts.isCallExpression(expression) || getCallExpressionName(expression.expression) !== 'defineController') {
|
|
230
|
+
const location = getNodeLocation(sourceFile, expression);
|
|
231
|
+
throw new Error(
|
|
232
|
+
`${filepath}:${location.line}:${location.column} must default-export defineController({ path, actions }). Legacy controller classes are no longer supported.`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const [definitionArg] = [...expression.arguments];
|
|
237
|
+
if (!definitionArg || !ts.isObjectLiteralExpression(definitionArg)) {
|
|
238
|
+
throw new Error(`defineController(...) in ${filepath} must receive an object literal.`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const pathExpression = getPropertyAssignment(definitionArg, 'path');
|
|
242
|
+
const routeBasePath =
|
|
243
|
+
pathExpression ? tryEvaluateStaticString(sourceFile, pathExpression) || getControllerBasePathFromFilepath(filepath, root) : getControllerBasePathFromFilepath(filepath, root);
|
|
244
|
+
const actionsExpression = getPropertyAssignment(definitionArg, 'actions');
|
|
245
|
+
|
|
246
|
+
if (!actionsExpression || !ts.isObjectLiteralExpression(actionsExpression)) {
|
|
247
|
+
throw new Error(`defineController(...) in ${filepath} must declare an actions object literal.`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const methods: TControllerMethodMeta[] = [];
|
|
251
|
+
|
|
252
|
+
for (const property of actionsExpression.properties) {
|
|
253
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
254
|
+
|
|
255
|
+
const methodName = getPropertyKey(property.name);
|
|
256
|
+
if (!methodName) continue;
|
|
257
|
+
|
|
258
|
+
const actionExpression = resolveIdentifierExpression(sourceFile, property.initializer);
|
|
259
|
+
if (!ts.isCallExpression(actionExpression) || getCallExpressionName(actionExpression.expression) !== 'defineAction') {
|
|
260
|
+
const location = getNodeLocation(sourceFile, property);
|
|
261
|
+
throw new Error(`${filepath}:${location.line}:${location.column} controller actions must use defineAction(...).`);
|
|
156
262
|
}
|
|
157
263
|
|
|
158
|
-
|
|
159
|
-
|
|
264
|
+
const [actionArg] = [...actionExpression.arguments];
|
|
265
|
+
const hasInput =
|
|
266
|
+
!!actionArg &&
|
|
267
|
+
ts.isObjectLiteralExpression(actionArg) &&
|
|
268
|
+
getPropertyAssignment(actionArg, 'input') !== undefined;
|
|
269
|
+
|
|
270
|
+
methods.push({
|
|
271
|
+
name: methodName,
|
|
272
|
+
inputCallsCount: hasInput ? 1 : 0,
|
|
273
|
+
routePath: [routeBasePath, methodName].filter(Boolean).join('/'),
|
|
274
|
+
sourceLocation: getNodeLocation(sourceFile, property.name),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
160
277
|
|
|
161
|
-
if (
|
|
278
|
+
if (!methods.length) return undefined;
|
|
162
279
|
|
|
163
|
-
return
|
|
280
|
+
return {
|
|
281
|
+
filepath,
|
|
282
|
+
importPath: '',
|
|
283
|
+
className: path.basename(filepath, '.ts').replace(/[^A-Za-z0-9_$]+/g, '_') || 'Controller',
|
|
284
|
+
routeBasePath,
|
|
285
|
+
methods,
|
|
286
|
+
};
|
|
164
287
|
};
|
|
165
288
|
|
|
166
289
|
/*----------------------------------
|
|
@@ -176,45 +299,24 @@ export const indexControllers = (searchDirs: TControllerSearchDir[]) => {
|
|
|
176
299
|
for (const filepath of controllerFiles.sort((a, b) => a.localeCompare(b))) {
|
|
177
300
|
const code = fs.readFileSync(filepath, 'utf8');
|
|
178
301
|
const sourceFile = parseSourceFile(filepath, code);
|
|
302
|
+
const explicitController = readExplicitController(sourceFile, filepath, searchDir.root);
|
|
179
303
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const className = defaultClass.name?.text || getGeneratedClassName(filepath);
|
|
186
|
-
const routeBasePath = controllerPathOverride || getControllerBasePathFromFilepath(filepath, searchDir.root);
|
|
187
|
-
const methods: TControllerMethodMeta[] = [];
|
|
188
|
-
|
|
189
|
-
for (const member of defaultClass.members) {
|
|
190
|
-
if (!ts.isMethodDeclaration(member)) continue;
|
|
191
|
-
if (!member.body) continue;
|
|
192
|
-
if (!member.name || !ts.isIdentifier(member.name)) continue;
|
|
193
|
-
|
|
194
|
-
const methodName = member.name.text;
|
|
195
|
-
const inputCallsCount = countInputCalls(member);
|
|
196
|
-
|
|
197
|
-
if (inputCallsCount > 1) {
|
|
198
|
-
throw new Error(`${filepath}#${methodName} uses this.input() more than once.`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
methods.push({
|
|
202
|
-
name: methodName,
|
|
203
|
-
inputCallsCount,
|
|
204
|
-
routePath: [routeBasePath, methodName].filter(Boolean).join('/'),
|
|
205
|
-
sourceLocation: getNodeLocation(sourceFile, member.name),
|
|
304
|
+
if (explicitController) {
|
|
305
|
+
controllers.push({
|
|
306
|
+
...explicitController,
|
|
307
|
+
importPath: buildImportPath(searchDir, filepath),
|
|
206
308
|
});
|
|
309
|
+
continue;
|
|
207
310
|
}
|
|
208
311
|
|
|
209
|
-
|
|
312
|
+
const defaultClass = getDefaultExportClass(sourceFile);
|
|
210
313
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
});
|
|
314
|
+
if (!defaultClass) continue;
|
|
315
|
+
|
|
316
|
+
const location = getNodeLocation(sourceFile, defaultClass);
|
|
317
|
+
throw new Error(
|
|
318
|
+
`${filepath}:${location.line}:${location.column} uses a legacy controller class. Export defineController({ path, actions }) instead.`,
|
|
319
|
+
);
|
|
218
320
|
}
|
|
219
321
|
}
|
|
220
322
|
|