proteum 2.5.1 → 2.5.3
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/AGENTS.md +3 -3
- package/README.md +12 -9
- package/agents/project/AGENTS.md +10 -8
- package/agents/project/CODING_STYLE.md +1 -1
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/root/AGENTS.md +10 -8
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/configure.ts +5 -5
- package/cli/commands/dev.ts +3 -3
- package/cli/commands/verify.ts +117 -4
- package/cli/compiler/artifacts/controllerHelper.ts +66 -0
- package/cli/compiler/artifacts/controllers.ts +3 -0
- package/cli/compiler/artifacts/services.ts +14 -8
- package/cli/compiler/common/generatedRouteModules.ts +270 -53
- package/cli/presentation/commands.ts +14 -3
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +6 -0
- package/cli/scaffold/templates.ts +11 -3
- package/cli/utils/agents.ts +271 -2
- package/cli/verification/changed.ts +460 -0
- package/client/app/index.ts +1 -1
- package/client/services/router/index.tsx +1 -1
- package/common/applicationConfig.ts +177 -0
- package/common/applicationConfigLoader.ts +33 -1
- package/common/dev/contractsDoctor.ts +16 -0
- package/config.ts +5 -1
- package/docs/migration-2.5.md +54 -11
- package/eslint.js +42 -8
- package/package.json +1 -1
- package/tests/agents-utils.test.cjs +72 -0
- package/tests/cli-mcp-command.test.cjs +14 -0
- package/tests/contracts-doctor.test.cjs +98 -0
- package/tests/definition-contracts.test.cjs +129 -0
- package/tests/eslint-rules.test.cjs +100 -0
- package/tests/scaffold-templates.test.cjs +27 -2
- package/tests/verify-changed.test.cjs +200 -0
|
@@ -615,10 +615,14 @@ const parseDefineApplicationBootstrap = (
|
|
|
615
615
|
if (!definitionArg) return undefined;
|
|
616
616
|
|
|
617
617
|
const servicesProperty = getObjectLiteralProperty(definitionArg, 'services');
|
|
618
|
-
const
|
|
618
|
+
const servicesInitializer =
|
|
619
619
|
servicesProperty && ts.isPropertyAssignment(servicesProperty)
|
|
620
|
-
?
|
|
620
|
+
? ts.isIdentifier(unwrapExpression(servicesProperty.initializer)) &&
|
|
621
|
+
topLevelInitializers.has((unwrapExpression(servicesProperty.initializer) as ts.Identifier).text)
|
|
622
|
+
? topLevelInitializers.get((unwrapExpression(servicesProperty.initializer) as ts.Identifier).text)!
|
|
623
|
+
: servicesProperty.initializer
|
|
621
624
|
: undefined;
|
|
625
|
+
const servicesDetails = servicesInitializer ? getObjectLiteralFactoryDetails(servicesInitializer) : undefined;
|
|
622
626
|
const servicesObject = servicesDetails?.object;
|
|
623
627
|
const localServiceInitializers = servicesDetails?.localInitializers || new Map<string, ts.Expression>();
|
|
624
628
|
const rootServices = parseRootServiceObject(servicesObject, imports, sourceFile.fileName, localServiceInitializers);
|
|
@@ -826,6 +830,7 @@ const createCommandServiceStubDeclarations = (rootServices: TParsedService[]): T
|
|
|
826
830
|
const typeNamesByAliasImportPath = new Map<string, string>();
|
|
827
831
|
const pendingSources: TCommandServiceStubSource[] = [];
|
|
828
832
|
const seenSources = new Set<string>();
|
|
833
|
+
const appTypeReference = `import("@server/app/index").Application & import("@/server/index").${app.identity.identifier}`;
|
|
829
834
|
const getStubTypeName = (source: TCommandServiceStubSource) => {
|
|
830
835
|
const existingTypeName = typeNamesByAliasImportPath.get(source.aliasImportPath);
|
|
831
836
|
if (existingTypeName) return existingTypeName;
|
|
@@ -863,7 +868,7 @@ const createCommandServiceStubDeclarations = (rootServices: TParsedService[]): T
|
|
|
863
868
|
stubs.set(
|
|
864
869
|
source.aliasImportPath,
|
|
865
870
|
`declare class ${getStubTypeName(source)} {
|
|
866
|
-
app:
|
|
871
|
+
app: ${appTypeReference};
|
|
867
872
|
[key: string]: any;
|
|
868
873
|
}`,
|
|
869
874
|
);
|
|
@@ -871,7 +876,7 @@ const createCommandServiceStubDeclarations = (rootServices: TParsedService[]): T
|
|
|
871
876
|
}
|
|
872
877
|
|
|
873
878
|
const className = getStubTypeName(source);
|
|
874
|
-
const classMembers = [` app:
|
|
879
|
+
const classMembers = [` app: ${appTypeReference};`];
|
|
875
880
|
|
|
876
881
|
for (const member of defaultClass.members) {
|
|
877
882
|
if (isPrivateOrProtectedInstanceMember(member)) continue;
|
|
@@ -994,6 +999,7 @@ export const generateServiceArtifacts = () => {
|
|
|
994
999
|
const appServices = rootServices.map((service) => resolveManifestService(service, 'app'));
|
|
995
1000
|
const routerPluginServices = routerPlugins.map((service) => resolveManifestService(service, 'Router.plugins'));
|
|
996
1001
|
const commandServiceStubs = createCommandServiceStubDeclarations(rootServices);
|
|
1002
|
+
const appTypeReference = `import("@server/app/index").Application & import("@/server/index").${appIdentifier}`;
|
|
997
1003
|
|
|
998
1004
|
writeIfChanged(
|
|
999
1005
|
path.join(app.paths.client.generated, 'server-index.d.ts'),
|
|
@@ -1002,7 +1008,7 @@ export const generateServiceArtifacts = () => {
|
|
|
1002
1008
|
|
|
1003
1009
|
writeIfChanged(
|
|
1004
1010
|
path.join(app.paths.client.generated, 'services.d.ts'),
|
|
1005
|
-
`declare type ${appIdentifier} =
|
|
1011
|
+
`declare type ${appIdentifier} = ${appTypeReference};
|
|
1006
1012
|
|
|
1007
1013
|
declare module '@models/types' {
|
|
1008
1014
|
export * from '@generated/client/models';
|
|
@@ -1066,7 +1072,7 @@ export default (): ClientContext => {
|
|
|
1066
1072
|
|
|
1067
1073
|
writeIfChanged(
|
|
1068
1074
|
path.join(app.paths.common.generated, 'services.d.ts'),
|
|
1069
|
-
`declare type ${appIdentifier} =
|
|
1075
|
+
`declare type ${appIdentifier} = ${appTypeReference};
|
|
1070
1076
|
|
|
1071
1077
|
declare module '@models/types' {
|
|
1072
1078
|
export * from '@generated/common/models';
|
|
@@ -1083,7 +1089,7 @@ declare module '@models/types' {
|
|
|
1083
1089
|
|
|
1084
1090
|
writeIfChanged(
|
|
1085
1091
|
path.join(app.paths.server.generated, 'commands.d.ts'),
|
|
1086
|
-
`declare type ${appIdentifier} =
|
|
1092
|
+
`declare type ${appIdentifier} = ${appTypeReference};
|
|
1087
1093
|
|
|
1088
1094
|
declare module "@models/types" {
|
|
1089
1095
|
const Models: any;
|
|
@@ -1127,7 +1133,7 @@ export default ${appIdentifier};
|
|
|
1127
1133
|
|
|
1128
1134
|
writeIfChanged(
|
|
1129
1135
|
path.join(app.paths.server.generated, 'services.d.ts'),
|
|
1130
|
-
`declare type ${appIdentifier} =
|
|
1136
|
+
`declare type ${appIdentifier} = ${appTypeReference};
|
|
1131
1137
|
|
|
1132
1138
|
declare module '@common/errors' {
|
|
1133
1139
|
|
|
@@ -64,8 +64,75 @@ const parseSourceFile = (filepath: string, code: string) =>
|
|
|
64
64
|
filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
65
65
|
);
|
|
66
66
|
|
|
67
|
+
const parsedSourceFileCache = new Map<string, ts.SourceFile | null>();
|
|
68
|
+
|
|
67
69
|
const normalizeFilepath = (value: string) => path.resolve(value).replace(/\\/g, '/');
|
|
68
70
|
|
|
71
|
+
const resolveExistingModuleFilepath = (baseFilepath: string) => {
|
|
72
|
+
const candidates = [
|
|
73
|
+
baseFilepath,
|
|
74
|
+
`${baseFilepath}.ts`,
|
|
75
|
+
`${baseFilepath}.tsx`,
|
|
76
|
+
`${baseFilepath}.js`,
|
|
77
|
+
`${baseFilepath}.jsx`,
|
|
78
|
+
path.join(baseFilepath, 'index.ts'),
|
|
79
|
+
path.join(baseFilepath, 'index.tsx'),
|
|
80
|
+
path.join(baseFilepath, 'index.js'),
|
|
81
|
+
path.join(baseFilepath, 'index.jsx'),
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getAppRootFromSourceFile = (sourceFilepath: string) => {
|
|
88
|
+
const normalized = normalizeFilepath(sourceFilepath);
|
|
89
|
+
const markers = ['/client/', '/server/', '/common/', '/commands/'];
|
|
90
|
+
const markerIndex = markers
|
|
91
|
+
.map((marker) => ({ marker, index: normalized.indexOf(marker) }))
|
|
92
|
+
.filter(({ index }) => index >= 0)
|
|
93
|
+
.sort((left, right) => left.index - right.index)[0];
|
|
94
|
+
|
|
95
|
+
return markerIndex ? normalized.slice(0, markerIndex.index) : path.dirname(sourceFilepath);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const resolveStaticImportFilepath = (sourceFile: ts.SourceFile, moduleSpecifier: string) => {
|
|
99
|
+
if (moduleSpecifier.startsWith('.')) {
|
|
100
|
+
return resolveExistingModuleFilepath(path.resolve(path.dirname(sourceFile.fileName), moduleSpecifier));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const appRoot = getAppRootFromSourceFile(sourceFile.fileName);
|
|
104
|
+
const aliases: Array<[string, string]> = [
|
|
105
|
+
['@/', appRoot],
|
|
106
|
+
['@client/', path.join(appRoot, 'client')],
|
|
107
|
+
['@server/', path.join(appRoot, 'server')],
|
|
108
|
+
['@common/', path.join(appRoot, 'common')],
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
for (const [prefix, root] of aliases) {
|
|
112
|
+
if (!moduleSpecifier.startsWith(prefix)) continue;
|
|
113
|
+
|
|
114
|
+
const relativeImport = moduleSpecifier.slice(prefix.length);
|
|
115
|
+
return resolveExistingModuleFilepath(path.join(root, relativeImport));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return undefined;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const readStaticImportSourceFile = (filepath: string) => {
|
|
122
|
+
const normalized = normalizeFilepath(filepath);
|
|
123
|
+
if (parsedSourceFileCache.has(normalized)) return parsedSourceFileCache.get(normalized) || undefined;
|
|
124
|
+
|
|
125
|
+
if (!fs.existsSync(normalized)) {
|
|
126
|
+
parsedSourceFileCache.set(normalized, null);
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const sourceFile = parseSourceFile(normalized, fs.readFileSync(normalized, 'utf8'));
|
|
131
|
+
parsedSourceFileCache.set(normalized, sourceFile);
|
|
132
|
+
|
|
133
|
+
return sourceFile;
|
|
134
|
+
};
|
|
135
|
+
|
|
69
136
|
const getNodeText = (sourceFile: ts.SourceFile, node: ts.Node) =>
|
|
70
137
|
sourceFile.text.slice(node.getStart(sourceFile), node.getEnd());
|
|
71
138
|
|
|
@@ -115,39 +182,46 @@ const tryEvaluateStaticExpression = (
|
|
|
115
182
|
resolvedBindings: Map<string, string | number | undefined>,
|
|
116
183
|
activeBindings = new Set<string>(),
|
|
117
184
|
): string | number | undefined => {
|
|
118
|
-
|
|
185
|
+
const expression = unwrapStaticExpression(node);
|
|
186
|
+
const resolvedExpression = resolveStaticExpressionNode(expression, bindingInitializers, activeBindings);
|
|
119
187
|
|
|
120
|
-
if (
|
|
121
|
-
|
|
188
|
+
if (resolvedExpression !== expression) {
|
|
189
|
+
return tryEvaluateStaticExpression(resolvedExpression, bindingInitializers, resolvedBindings, activeBindings);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
|
|
193
|
+
|
|
194
|
+
if (ts.isNumericLiteral(expression)) {
|
|
195
|
+
const value = Number(expression.text);
|
|
122
196
|
return Number.isFinite(value) ? value : undefined;
|
|
123
197
|
}
|
|
124
198
|
|
|
125
|
-
if (ts.isParenthesizedExpression(
|
|
126
|
-
return tryEvaluateStaticExpression(
|
|
199
|
+
if (ts.isParenthesizedExpression(expression)) {
|
|
200
|
+
return tryEvaluateStaticExpression(expression.expression, bindingInitializers, resolvedBindings, activeBindings);
|
|
127
201
|
}
|
|
128
202
|
|
|
129
|
-
if (ts.isIdentifier(
|
|
130
|
-
if (resolvedBindings.has(
|
|
203
|
+
if (ts.isIdentifier(expression)) {
|
|
204
|
+
if (resolvedBindings.has(expression.text)) return resolvedBindings.get(expression.text);
|
|
131
205
|
|
|
132
|
-
const initializer = bindingInitializers.get(
|
|
133
|
-
if (!initializer || activeBindings.has(
|
|
206
|
+
const initializer = bindingInitializers.get(expression.text);
|
|
207
|
+
if (!initializer || activeBindings.has(expression.text)) return undefined;
|
|
134
208
|
|
|
135
|
-
activeBindings.add(
|
|
209
|
+
activeBindings.add(expression.text);
|
|
136
210
|
const value = tryEvaluateStaticExpression(initializer, bindingInitializers, resolvedBindings, activeBindings);
|
|
137
|
-
activeBindings.delete(
|
|
138
|
-
resolvedBindings.set(
|
|
211
|
+
activeBindings.delete(expression.text);
|
|
212
|
+
resolvedBindings.set(expression.text, value);
|
|
139
213
|
|
|
140
214
|
return value;
|
|
141
215
|
}
|
|
142
216
|
|
|
143
|
-
if (ts.isPrefixUnaryExpression(
|
|
144
|
-
const operand = tryEvaluateStaticExpression(
|
|
217
|
+
if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.MinusToken) {
|
|
218
|
+
const operand = tryEvaluateStaticExpression(expression.operand, bindingInitializers, resolvedBindings, activeBindings);
|
|
145
219
|
return typeof operand === 'number' ? -operand : undefined;
|
|
146
220
|
}
|
|
147
221
|
|
|
148
|
-
if (ts.isBinaryExpression(
|
|
149
|
-
const left = tryEvaluateStaticExpression(
|
|
150
|
-
const right = tryEvaluateStaticExpression(
|
|
222
|
+
if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
223
|
+
const left = tryEvaluateStaticExpression(expression.left, bindingInitializers, resolvedBindings, activeBindings);
|
|
224
|
+
const right = tryEvaluateStaticExpression(expression.right, bindingInitializers, resolvedBindings, activeBindings);
|
|
151
225
|
|
|
152
226
|
if (left === undefined || right === undefined) return undefined;
|
|
153
227
|
|
|
@@ -157,10 +231,10 @@ const tryEvaluateStaticExpression = (
|
|
|
157
231
|
return undefined;
|
|
158
232
|
}
|
|
159
233
|
|
|
160
|
-
if (ts.isTemplateExpression(
|
|
161
|
-
let output =
|
|
234
|
+
if (ts.isTemplateExpression(expression)) {
|
|
235
|
+
let output = expression.head.text;
|
|
162
236
|
|
|
163
|
-
for (const span of
|
|
237
|
+
for (const span of expression.templateSpans) {
|
|
164
238
|
const value = tryEvaluateStaticExpression(span.expression, bindingInitializers, resolvedBindings, activeBindings);
|
|
165
239
|
if (value === undefined) return undefined;
|
|
166
240
|
|
|
@@ -183,6 +257,70 @@ const unwrapStaticExpression = (node: ts.Expression): ts.Expression => {
|
|
|
183
257
|
return node;
|
|
184
258
|
};
|
|
185
259
|
|
|
260
|
+
const getStaticPropertyName = (node: ts.PropertyName | ts.Expression) => {
|
|
261
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
|
|
262
|
+
return undefined;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const getStaticObjectPropertyInitializer = (node: ts.ObjectLiteralExpression, propertyName: string) => {
|
|
266
|
+
for (const property of node.properties) {
|
|
267
|
+
if (ts.isPropertyAssignment(property)) {
|
|
268
|
+
const key = getObjectLiteralPropertyKey(property.name);
|
|
269
|
+
if (key === propertyName) return property.initializer;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (ts.isShorthandPropertyAssignment(property) && property.name.text === propertyName) {
|
|
274
|
+
return property.name;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return undefined;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const resolveStaticExpressionNode = (
|
|
282
|
+
node: ts.Expression,
|
|
283
|
+
bindingInitializers: Map<string, ts.Expression>,
|
|
284
|
+
activeBindings = new Set<string>(),
|
|
285
|
+
): ts.Expression => {
|
|
286
|
+
const expression = unwrapStaticExpression(node);
|
|
287
|
+
|
|
288
|
+
if (ts.isIdentifier(expression)) {
|
|
289
|
+
const initializer = bindingInitializers.get(expression.text);
|
|
290
|
+
if (!initializer || activeBindings.has(expression.text)) return expression;
|
|
291
|
+
|
|
292
|
+
activeBindings.add(expression.text);
|
|
293
|
+
const resolved = resolveStaticExpressionNode(initializer, bindingInitializers, activeBindings);
|
|
294
|
+
activeBindings.delete(expression.text);
|
|
295
|
+
|
|
296
|
+
return resolved;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
300
|
+
const container = resolveStaticExpressionNode(expression.expression, bindingInitializers, activeBindings);
|
|
301
|
+
const unwrappedContainer = unwrapStaticExpression(container);
|
|
302
|
+
if (!ts.isObjectLiteralExpression(unwrappedContainer)) return expression;
|
|
303
|
+
|
|
304
|
+
const initializer = getStaticObjectPropertyInitializer(unwrappedContainer, expression.name.text);
|
|
305
|
+
return initializer ? resolveStaticExpressionNode(initializer, bindingInitializers, activeBindings) : expression;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (ts.isElementAccessExpression(expression)) {
|
|
309
|
+
const argument = expression.argumentExpression && unwrapStaticExpression(expression.argumentExpression);
|
|
310
|
+
const propertyName = argument && getStaticPropertyName(argument);
|
|
311
|
+
if (!propertyName) return expression;
|
|
312
|
+
|
|
313
|
+
const container = resolveStaticExpressionNode(expression.expression, bindingInitializers, activeBindings);
|
|
314
|
+
const unwrappedContainer = unwrapStaticExpression(container);
|
|
315
|
+
if (!ts.isObjectLiteralExpression(unwrappedContainer)) return expression;
|
|
316
|
+
|
|
317
|
+
const initializer = getStaticObjectPropertyInitializer(unwrappedContainer, propertyName);
|
|
318
|
+
return initializer ? resolveStaticExpressionNode(initializer, bindingInitializers, activeBindings) : expression;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return expression;
|
|
322
|
+
};
|
|
323
|
+
|
|
186
324
|
const isStaticSerializableExpression = (
|
|
187
325
|
node: ts.Expression,
|
|
188
326
|
bindingInitializers: Map<string, ts.Expression>,
|
|
@@ -190,6 +328,11 @@ const isStaticSerializableExpression = (
|
|
|
190
328
|
activeBindings = new Set<string>(),
|
|
191
329
|
): boolean => {
|
|
192
330
|
const expression = unwrapStaticExpression(node);
|
|
331
|
+
const resolvedExpression = resolveStaticExpressionNode(expression, bindingInitializers, activeBindings);
|
|
332
|
+
|
|
333
|
+
if (resolvedExpression !== expression) {
|
|
334
|
+
return isStaticSerializableExpression(resolvedExpression, bindingInitializers, resolvedBindings, activeBindings);
|
|
335
|
+
}
|
|
193
336
|
|
|
194
337
|
if (
|
|
195
338
|
ts.isStringLiteral(expression) ||
|
|
@@ -275,21 +418,9 @@ const assertStaticSerializableMetadata = (
|
|
|
275
418
|
);
|
|
276
419
|
};
|
|
277
420
|
|
|
278
|
-
const
|
|
279
|
-
const bindingInitializers = new Map<string, ts.Expression>();
|
|
421
|
+
const resolveStaticBindings = (bindingInitializers: Map<string, ts.Expression>) => {
|
|
280
422
|
const resolvedBindings = new Map<string, string | number | undefined>();
|
|
281
423
|
|
|
282
|
-
for (const statement of sourceFile.statements) {
|
|
283
|
-
if (!ts.isVariableStatement(statement)) continue;
|
|
284
|
-
if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
|
|
285
|
-
|
|
286
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
287
|
-
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
288
|
-
|
|
289
|
-
bindingInitializers.set(declaration.name.text, declaration.initializer);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
424
|
for (const bindingName of bindingInitializers.keys()) {
|
|
294
425
|
if (resolvedBindings.has(bindingName)) continue;
|
|
295
426
|
|
|
@@ -305,11 +436,15 @@ const collectStaticBindings = (sourceFile: ts.SourceFile) => {
|
|
|
305
436
|
return resolvedBindings;
|
|
306
437
|
};
|
|
307
438
|
|
|
308
|
-
const
|
|
439
|
+
const collectStaticBindings = (sourceFile: ts.SourceFile) =>
|
|
440
|
+
resolveStaticBindings(collectStaticBindingInitializers(sourceFile));
|
|
441
|
+
|
|
442
|
+
const collectExportedStaticBindingInitializers = (sourceFile: ts.SourceFile) => {
|
|
309
443
|
const bindingInitializers = new Map<string, ts.Expression>();
|
|
310
444
|
|
|
311
445
|
for (const statement of sourceFile.statements) {
|
|
312
446
|
if (!ts.isVariableStatement(statement)) continue;
|
|
447
|
+
if (!statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue;
|
|
313
448
|
if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
|
|
314
449
|
|
|
315
450
|
for (const declaration of statement.declarationList.declarations) {
|
|
@@ -322,6 +457,61 @@ const collectStaticBindingInitializers = (sourceFile: ts.SourceFile) => {
|
|
|
322
457
|
return bindingInitializers;
|
|
323
458
|
};
|
|
324
459
|
|
|
460
|
+
const collectImportedStaticBindingInitializers = (
|
|
461
|
+
sourceFile: ts.SourceFile,
|
|
462
|
+
visitedFiles = new Set<string>([normalizeFilepath(sourceFile.fileName)]),
|
|
463
|
+
) => {
|
|
464
|
+
const bindingInitializers = new Map<string, ts.Expression>();
|
|
465
|
+
|
|
466
|
+
for (const statement of sourceFile.statements) {
|
|
467
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
468
|
+
|
|
469
|
+
const namedBindings = statement.importClause?.namedBindings;
|
|
470
|
+
if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
|
|
471
|
+
|
|
472
|
+
const importedFilepath = resolveStaticImportFilepath(sourceFile, statement.moduleSpecifier.text);
|
|
473
|
+
if (!importedFilepath) continue;
|
|
474
|
+
|
|
475
|
+
const normalizedImportFilepath = normalizeFilepath(importedFilepath);
|
|
476
|
+
if (visitedFiles.has(normalizedImportFilepath)) continue;
|
|
477
|
+
|
|
478
|
+
const importedSourceFile = readStaticImportSourceFile(normalizedImportFilepath);
|
|
479
|
+
if (!importedSourceFile) continue;
|
|
480
|
+
|
|
481
|
+
const exportedInitializers = collectExportedStaticBindingInitializers(importedSourceFile);
|
|
482
|
+
for (const element of namedBindings.elements) {
|
|
483
|
+
const importedName = element.propertyName?.text || element.name.text;
|
|
484
|
+
const initializer = exportedInitializers.get(importedName);
|
|
485
|
+
if (!initializer) continue;
|
|
486
|
+
|
|
487
|
+
bindingInitializers.set(element.name.text, initializer);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return bindingInitializers;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const collectStatementStaticBindingInitializers = (
|
|
495
|
+
statements: readonly ts.Statement[],
|
|
496
|
+
bindingInitializers = new Map<string, ts.Expression>(),
|
|
497
|
+
) => {
|
|
498
|
+
for (const statement of statements) {
|
|
499
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
500
|
+
if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
|
|
501
|
+
|
|
502
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
503
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
504
|
+
|
|
505
|
+
bindingInitializers.set(declaration.name.text, declaration.initializer);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return bindingInitializers;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const collectStaticBindingInitializers = (sourceFile: ts.SourceFile) =>
|
|
513
|
+
collectStatementStaticBindingInitializers(sourceFile.statements, collectImportedStaticBindingInitializers(sourceFile));
|
|
514
|
+
|
|
325
515
|
const getPropertyAssignment = (node: ts.ObjectLiteralExpression, propertyName: string) => {
|
|
326
516
|
for (const property of node.properties) {
|
|
327
517
|
if (!ts.isPropertyAssignment(property)) continue;
|
|
@@ -339,15 +529,42 @@ const getCallExpressionName = (node: ts.Expression) => {
|
|
|
339
529
|
return undefined;
|
|
340
530
|
};
|
|
341
531
|
|
|
532
|
+
const collectServerRouteDefinitionExpressions = (routesArg: ts.Expression | undefined) => {
|
|
533
|
+
if (!routesArg) return undefined;
|
|
534
|
+
|
|
535
|
+
if (ts.isArrayLiteralExpression(routesArg)) return [...routesArg.elements];
|
|
536
|
+
|
|
537
|
+
if (!(ts.isArrowFunction(routesArg) || ts.isFunctionExpression(routesArg))) return undefined;
|
|
538
|
+
|
|
539
|
+
if (ts.isArrayLiteralExpression(routesArg.body)) return [...routesArg.body.elements];
|
|
540
|
+
if (!ts.isBlock(routesArg.body)) return undefined;
|
|
541
|
+
|
|
542
|
+
const routeExpressions: ts.Expression[] = [];
|
|
543
|
+
|
|
544
|
+
for (const statement of routesArg.body.statements) {
|
|
545
|
+
if (!ts.isExpressionStatement(statement)) continue;
|
|
546
|
+
if (!ts.isCallExpression(statement.expression)) continue;
|
|
547
|
+
if (!ts.isPropertyAccessExpression(statement.expression.expression)) continue;
|
|
548
|
+
if (statement.expression.expression.name.text !== 'push') continue;
|
|
549
|
+
|
|
550
|
+
for (const argument of statement.expression.arguments) {
|
|
551
|
+
const unwrappedArgument = unwrapStaticExpression(argument);
|
|
552
|
+
if (!ts.isCallExpression(unwrappedArgument)) continue;
|
|
553
|
+
|
|
554
|
+
const helperName = getCallExpressionName(unwrappedArgument.expression);
|
|
555
|
+
if (helperName === 'defineServerRoute') routeExpressions.push(unwrappedArgument);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return routeExpressions.length > 0 ? routeExpressions : undefined;
|
|
560
|
+
};
|
|
561
|
+
|
|
342
562
|
const resolveIdentifierExpression = (
|
|
343
563
|
expression: ts.Expression,
|
|
344
564
|
sourceFile: ts.SourceFile,
|
|
345
565
|
): ts.Expression => {
|
|
346
|
-
const unwrapped = unwrapStaticExpression(expression);
|
|
347
|
-
if (!ts.isIdentifier(unwrapped)) return unwrapped;
|
|
348
|
-
|
|
349
566
|
const bindingInitializers = collectStaticBindingInitializers(sourceFile);
|
|
350
|
-
return bindingInitializers
|
|
567
|
+
return resolveStaticExpressionNode(expression, bindingInitializers);
|
|
351
568
|
};
|
|
352
569
|
|
|
353
570
|
const getDefaultRouteDefinitionExpression = (sourceFile: ts.SourceFile) => {
|
|
@@ -402,10 +619,11 @@ const parseExplicitRouteCall = (
|
|
|
402
619
|
sourceFile: ts.SourceFile,
|
|
403
620
|
side: TRouteSide,
|
|
404
621
|
node: ts.Expression,
|
|
622
|
+
scopedStaticBindingInitializers?: Map<string, ts.Expression>,
|
|
405
623
|
): TExplicitRouteDefinition[] => {
|
|
406
624
|
const expression = unwrapStaticExpression(resolveIdentifierExpression(node, sourceFile));
|
|
407
|
-
const staticBindingInitializers = collectStaticBindingInitializers(sourceFile);
|
|
408
|
-
const staticBindings =
|
|
625
|
+
const staticBindingInitializers = scopedStaticBindingInitializers || collectStaticBindingInitializers(sourceFile);
|
|
626
|
+
const staticBindings = resolveStaticBindings(staticBindingInitializers);
|
|
409
627
|
|
|
410
628
|
if (!ts.isCallExpression(expression)) {
|
|
411
629
|
throw new Error(`Route module ${sourceFile.fileName} must default-export a define*Route(...) call.`);
|
|
@@ -422,20 +640,19 @@ const parseExplicitRouteCall = (
|
|
|
422
640
|
}
|
|
423
641
|
|
|
424
642
|
const [routesArg] = [...expression.arguments];
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
throw new Error(`defineServerRoutes(...) in ${sourceFile.fileName} must receive a static array literal or a factory returning one.`);
|
|
643
|
+
const routeExpressions = collectServerRouteDefinitionExpressions(routesArg);
|
|
644
|
+
const routeBindingInitializers =
|
|
645
|
+
routesArg && (ts.isArrowFunction(routesArg) || ts.isFunctionExpression(routesArg)) && ts.isBlock(routesArg.body)
|
|
646
|
+
? collectStatementStaticBindingInitializers(routesArg.body.statements, new Map(staticBindingInitializers))
|
|
647
|
+
: staticBindingInitializers;
|
|
648
|
+
|
|
649
|
+
if (!routeExpressions) {
|
|
650
|
+
throw new Error(
|
|
651
|
+
`defineServerRoutes(...) in ${sourceFile.fileName} must receive a static array literal, a factory returning one, or a factory that pushes defineServerRoute(...) entries into a local routes array.`,
|
|
652
|
+
);
|
|
436
653
|
}
|
|
437
654
|
|
|
438
|
-
return
|
|
655
|
+
return routeExpressions.map((element) => parseExplicitRouteCall(sourceFile, side, element, routeBindingInitializers)).flat();
|
|
439
656
|
}
|
|
440
657
|
|
|
441
658
|
const [definitionArg] = [...expression.arguments];
|
|
@@ -446,7 +663,7 @@ const parseExplicitRouteCall = (
|
|
|
446
663
|
const sourceLocation = getNodeLocation(sourceFile, expression);
|
|
447
664
|
const optionsExpression = getPropertyAssignment(definitionArg, 'options');
|
|
448
665
|
const resolvedOptionsExpression = optionsExpression
|
|
449
|
-
? unwrapStaticExpression(
|
|
666
|
+
? unwrapStaticExpression(resolveStaticExpressionNode(optionsExpression, staticBindingInitializers))
|
|
450
667
|
: undefined;
|
|
451
668
|
const optionsArg =
|
|
452
669
|
resolvedOptionsExpression && ts.isObjectLiteralExpression(resolvedOptionsExpression)
|
|
@@ -116,7 +116,7 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
|
|
|
116
116
|
configure: {
|
|
117
117
|
name: 'configure',
|
|
118
118
|
category: 'Project scaffolding',
|
|
119
|
-
summary: 'Interactively configure tracked Proteum instruction files
|
|
119
|
+
summary: 'Interactively configure tracked Proteum instruction files and Claude aliases for an app.',
|
|
120
120
|
usage: 'proteum configure agents',
|
|
121
121
|
bestFor:
|
|
122
122
|
'Creating or switching the tracked instruction layout intentionally while keeping Proteum-owned instructions embedded in managed sections.',
|
|
@@ -128,8 +128,9 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
|
|
|
128
128
|
],
|
|
129
129
|
notes: [
|
|
130
130
|
'This command is interactive. It asks whether the current Proteum app belongs to a monorepo and, if so, which ancestor path should receive the reusable root instruction files.',
|
|
131
|
-
'Standalone mode writes tracked instruction files into the current Proteum app root.',
|
|
131
|
+
'Standalone mode writes tracked instruction files into the current Proteum app root and creates `CLAUDE.md` symlinks beside each `AGENTS.md`.',
|
|
132
132
|
'Monorepo mode writes reusable root documents such as `AGENTS.md`, `DOCUMENTATION.md`, `CODING_STYLE.md`, `diagnostics.md`, and `optimizations.md` into the chosen monorepo root, then writes only app-root and area instruction files into the current Proteum app root.',
|
|
133
|
+
'Every generated `CLAUDE.md` is a sibling symlink pointing to `AGENTS.md`.',
|
|
133
134
|
'Every managed instruction file contains a `# Proteum Instructions` section with the full embedded Proteum project instruction corpus.',
|
|
134
135
|
'Existing content outside `# Proteum Instructions` is preserved. Directories and foreign symlinks are replaced only after confirmation.',
|
|
135
136
|
],
|
|
@@ -612,10 +613,18 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
|
|
|
612
613
|
name: 'verify',
|
|
613
614
|
category: 'Manifest and contracts',
|
|
614
615
|
summary: 'Run focused owner/request/browser verification or the full framework reference-app validation pass.',
|
|
615
|
-
usage: 'proteum verify [framework-change|owner <query>|request <path>|browser <path>] [--port <port>|--url <baseUrl>] [--session-email <email>] [--session-role <role>] [--method <verb>] [--data-json <json>] [--strict-global] [--crosspath <path>] [--product <path>] [--website <path>] [--crosspath-port <port>] [--product-port <port>] [--website-port <port>] [--route <path>] [--json]',
|
|
616
|
+
usage: 'proteum verify [changed|framework-change|owner <query>|request <path>|browser <path>] [--staged] [--base <ref>] [--dry-run] [--port <port>|--url <baseUrl>] [--session-email <email>] [--session-role <role>] [--method <verb>] [--data-json <json>] [--strict-global] [--crosspath <path>] [--product <path>] [--website <path>] [--crosspath-port <port>] [--product-port <port>] [--website-port <port>] [--route <path>] [--json]',
|
|
616
617
|
bestFor:
|
|
617
618
|
'Choosing the smallest trustworthy verification surface first, then separating introduced blocking findings from unrelated pre-existing diagnostics.',
|
|
618
619
|
examples: [
|
|
620
|
+
{
|
|
621
|
+
description: 'Plan targeted checks for changed files without running them',
|
|
622
|
+
command: 'proteum verify changed --dry-run',
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
description: 'Run targeted checks for staged changes only',
|
|
626
|
+
command: 'proteum verify changed --staged',
|
|
627
|
+
},
|
|
619
628
|
{
|
|
620
629
|
description: 'Run the default framework smoke verification against the reference apps',
|
|
621
630
|
command: 'proteum verify framework-change',
|
|
@@ -638,6 +647,8 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
|
|
|
638
647
|
},
|
|
639
648
|
],
|
|
640
649
|
notes: [
|
|
650
|
+
'`proteum verify changed` reads optional `proteum.verify.config.ts`, combines changed-file detection with built-in Vitest defaults, prints the selected plan, then runs every selected check.',
|
|
651
|
+
'`proteum verify changed --dry-run --json` is the safest agent entrypoint when you need to inspect selected checks without changing cache or test artifacts.',
|
|
641
652
|
'`proteum verify owner` starts from `proteum orient`, then chooses the smallest trustworthy verify path instead of defaulting to broad global checks.',
|
|
642
653
|
'`proteum verify owner`, `request`, and `browser` emit `introducedFindings`, `preExistingFindings`, `verificationSteps`, and `result` in JSON.',
|
|
643
654
|
'Focused verification fails on introduced blocking findings by default and only fails on unrelated pre-existing blockers when `--strict-global` is passed.',
|
package/cli/presentation/help.ts
CHANGED
|
@@ -139,7 +139,7 @@ export const renderCliOverview = async ({
|
|
|
139
139
|
indent: ' ',
|
|
140
140
|
nextIndent: ' ',
|
|
141
141
|
}),
|
|
142
|
-
wrapText('Before the dev loop starts, `proteum dev` ensures tracked instruction files contain the current managed `# Proteum Instructions` section.', {
|
|
142
|
+
wrapText('Before the dev loop starts, `proteum dev` ensures tracked instruction files contain the current managed `# Proteum Instructions` section and `CLAUDE.md` symlinks point to sibling `AGENTS.md` files.', {
|
|
143
143
|
indent: ' ',
|
|
144
144
|
nextIndent: ' ',
|
|
145
145
|
}),
|
package/cli/runtime/commands.ts
CHANGED
|
@@ -846,6 +846,8 @@ class VerifyCommand extends ProteumCommand {
|
|
|
846
846
|
|
|
847
847
|
public static usage = buildUsage('verify');
|
|
848
848
|
|
|
849
|
+
public base = Option.String('--base', { description: 'Base ref used by `proteum verify changed`.' });
|
|
850
|
+
public dryRun = Option.Boolean('--dry-run', false, { description: 'Print the changed-file verification plan without running checks.' });
|
|
849
851
|
public json = Option.Boolean('--json', false, { description: 'Print JSON output.' });
|
|
850
852
|
public port = Option.String('--port', { description: 'Target an existing dev server on the given port for focused verify actions.' });
|
|
851
853
|
public url = Option.String('--url', { description: 'Target an existing dev server at the given base URL for focused verify actions.' });
|
|
@@ -869,6 +871,7 @@ class VerifyCommand extends ProteumCommand {
|
|
|
869
871
|
description: 'Port used for the Unique Domains Website validation server.',
|
|
870
872
|
});
|
|
871
873
|
public route = Option.String('--route', { description: 'Route loaded in both apps during validation.' });
|
|
874
|
+
public staged = Option.Boolean('--staged', false, { description: 'Verify staged changes only.' });
|
|
872
875
|
public args = Option.Rest();
|
|
873
876
|
|
|
874
877
|
public async execute() {
|
|
@@ -877,9 +880,11 @@ class VerifyCommand extends ProteumCommand {
|
|
|
877
880
|
|
|
878
881
|
this.setCliArgs({
|
|
879
882
|
action,
|
|
883
|
+
base: this.base ?? '',
|
|
880
884
|
crosspath: this.crosspath ?? '',
|
|
881
885
|
crosspathPort: this.crosspathPort ?? '',
|
|
882
886
|
dataJson: this.dataJson ?? '',
|
|
887
|
+
dryRun: this.dryRun,
|
|
883
888
|
json: this.json,
|
|
884
889
|
method: this.method ?? '',
|
|
885
890
|
port: this.port ?? '',
|
|
@@ -888,6 +893,7 @@ class VerifyCommand extends ProteumCommand {
|
|
|
888
893
|
route: this.route ?? '',
|
|
889
894
|
sessionEmail: this.sessionEmail ?? '',
|
|
890
895
|
sessionRole: this.sessionRole ?? '',
|
|
896
|
+
staged: this.staged,
|
|
891
897
|
strictGlobal: this.strictGlobal,
|
|
892
898
|
target,
|
|
893
899
|
url: this.url ?? '',
|
|
@@ -53,7 +53,7 @@ export const createControllerTemplate = ({
|
|
|
53
53
|
appIdentifier: string;
|
|
54
54
|
className: string;
|
|
55
55
|
methodName: string;
|
|
56
|
-
}) => `import { defineAction, defineController } from '@server/
|
|
56
|
+
}) => `import { defineAction, defineController } from '@generated/server/controller';
|
|
57
57
|
|
|
58
58
|
export default defineController({
|
|
59
59
|
actions: {
|
|
@@ -75,7 +75,9 @@ export const createCommandTemplate = ({
|
|
|
75
75
|
className: string;
|
|
76
76
|
methodName: string;
|
|
77
77
|
}) => `import { Commands } from '@server/app/commands';
|
|
78
|
-
import type
|
|
78
|
+
import type AppApplication from '@/server/index';
|
|
79
|
+
|
|
80
|
+
type App = InstanceType<typeof AppApplication>;
|
|
79
81
|
|
|
80
82
|
export default class ${className} extends Commands<App> {
|
|
81
83
|
public async ${methodName}() {
|
|
@@ -171,7 +173,9 @@ import SchemaRouter from '@server/services/schema/router';
|
|
|
171
173
|
|
|
172
174
|
import * as appConfig from '@/server/config/app';
|
|
173
175
|
|
|
174
|
-
export
|
|
176
|
+
export type TControllerRequestServices = {};
|
|
177
|
+
|
|
178
|
+
const ${_args.appIdentifier}Application = defineApplication({
|
|
175
179
|
services: () => ({}),
|
|
176
180
|
router: (app) =>
|
|
177
181
|
new Router(
|
|
@@ -185,6 +189,10 @@ export default defineApplication({
|
|
|
185
189
|
app,
|
|
186
190
|
),
|
|
187
191
|
});
|
|
192
|
+
|
|
193
|
+
export type ${_args.appIdentifier} = InstanceType<typeof ${_args.appIdentifier}Application>;
|
|
194
|
+
|
|
195
|
+
export default ${_args.appIdentifier}Application;
|
|
188
196
|
`;
|
|
189
197
|
|
|
190
198
|
export const createClientTsconfigTemplate = (paths: TTsconfigTemplatePaths) => `{
|