@bleedingdev/modern-js-code-tools 3.9.0-ultramodern.3 → 3.9.0-ultramodern.5
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/bin/modern-api-check-files.mjs +4 -0
- package/bin/modern-api-check.mjs +4 -0
- package/dist/cjs/cli/microvertical-api-check.cjs +71 -0
- package/dist/cjs/microvertical-api-baseline.cjs +526 -0
- package/dist/cjs/microvertical-api-boundary.cjs +470 -0
- package/dist/cjs/microvertical-api-owner.cjs +217 -0
- package/dist/cjs/oxlint-plugin/rules/strict-effect-api-boundaries.cjs +1 -1
- package/dist/cjs/strict-effect-runtime.cjs +43 -19
- package/dist/esm/cli/microvertical-api-check.js +33 -0
- package/dist/esm/microvertical-api-baseline.js +474 -0
- package/dist/esm/microvertical-api-boundary.js +412 -0
- package/dist/esm/microvertical-api-owner.js +162 -0
- package/dist/esm/oxlint-plugin/rules/strict-effect-api-boundaries.js +1 -1
- package/dist/esm/strict-effect-runtime.js +43 -19
- package/dist/esm-node/cli/microvertical-api-check.js +34 -0
- package/dist/esm-node/microvertical-api-baseline.js +475 -0
- package/dist/esm-node/microvertical-api-boundary.js +413 -0
- package/dist/esm-node/microvertical-api-owner.js +163 -0
- package/dist/esm-node/oxlint-plugin/rules/strict-effect-api-boundaries.js +1 -1
- package/dist/esm-node/strict-effect-runtime.js +43 -19
- package/dist/types/cli/microvertical-api-check.d.ts +1 -0
- package/dist/types/microvertical-api-baseline.d.ts +23 -0
- package/dist/types/microvertical-api-boundary.d.ts +32 -0
- package/dist/types/microvertical-api-owner.d.ts +6 -0
- package/package.json +15 -2
|
@@ -3,7 +3,8 @@ import node_path from "node:path";
|
|
|
3
3
|
import { parse } from "@babel/parser";
|
|
4
4
|
import traverse, { Hub, NodePath } from "@babel/traverse";
|
|
5
5
|
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
6
|
-
const edge = '@modern-js/
|
|
6
|
+
const edge = '@modern-js/bff-effect/effect-edge';
|
|
7
|
+
const nodeRuntime = '@modern-js/bff-effect/effect';
|
|
7
8
|
const sharedRuntime = /^@[^/]+\/shared-contracts\/server\/effect-bff-runtime$/u;
|
|
8
9
|
const failure = 'Generated API entries must export defineEffectBff(...) or the server-only shared Effect BFF assembly helper with an explicitly composed handler Layer and an unshadowed executable root; entries must implement handlers through HttpApiBuilder.group.';
|
|
9
10
|
const invalidSources = new WeakSet();
|
|
@@ -119,23 +120,38 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
119
120
|
const imported = (node)=>{
|
|
120
121
|
const value = binding(node);
|
|
121
122
|
const decl = value?.path.node;
|
|
122
|
-
if (!decl || !__rspack_external__babel_types_6e8337dd.isImportSpecifier(decl) || 'type' === decl.importKind) return;
|
|
123
|
+
if (!decl || !__rspack_external__babel_types_6e8337dd.isImportSpecifier(decl) && !__rspack_external__babel_types_6e8337dd.isImportNamespaceSpecifier(decl) || __rspack_external__babel_types_6e8337dd.isImportSpecifier(decl) && 'type' === decl.importKind) return;
|
|
123
124
|
const statement = value.path.parent;
|
|
124
125
|
if (!__rspack_external__babel_types_6e8337dd.isImportDeclaration(statement) || 'type' === statement.importKind) return;
|
|
125
126
|
return {
|
|
126
|
-
|
|
127
|
+
namespace: __rspack_external__babel_types_6e8337dd.isImportNamespaceSpecifier(decl),
|
|
128
|
+
name: __rspack_external__babel_types_6e8337dd.isImportNamespaceSpecifier(decl) ? '*' : __rspack_external__babel_types_6e8337dd.isIdentifier(decl.imported) ? decl.imported.name : decl.imported.value,
|
|
127
129
|
specifier: statement.source.value
|
|
128
130
|
};
|
|
129
131
|
};
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
const namespaceSources = {
|
|
133
|
+
HttpApi: [
|
|
134
|
+
edge,
|
|
135
|
+
'effect/unstable/httpapi'
|
|
136
|
+
],
|
|
137
|
+
HttpApiBuilder: [
|
|
138
|
+
edge,
|
|
139
|
+
'effect/unstable/httpapi'
|
|
140
|
+
],
|
|
141
|
+
HttpRouter: [
|
|
142
|
+
edge,
|
|
143
|
+
'effect/unstable/http'
|
|
144
|
+
]
|
|
145
|
+
};
|
|
146
|
+
const native = (node, name, sources)=>{
|
|
133
147
|
const value = imported(node);
|
|
134
|
-
|
|
148
|
+
if (!value) return false;
|
|
149
|
+
const importedNameMatches = value.namespace ? value.specifier === `effect/${name}` : value.name === name && value.specifier !== `effect/${name}`;
|
|
150
|
+
return importedNameMatches && (sources ?? namespaceSources[name] ?? [
|
|
151
|
+
edge
|
|
152
|
+
]).includes(value.specifier);
|
|
135
153
|
};
|
|
136
|
-
const method = (node, namespace, member, sources
|
|
137
|
-
edge
|
|
138
|
-
])=>{
|
|
154
|
+
const method = (node, namespace, member, sources)=>{
|
|
139
155
|
node = unwrap(node);
|
|
140
156
|
return __rspack_external__babel_types_6e8337dd.isMemberExpression(node) && !node.computed && __rspack_external__babel_types_6e8337dd.isIdentifier(node.property, {
|
|
141
157
|
name: member
|
|
@@ -213,8 +229,15 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
213
229
|
};
|
|
214
230
|
const layerSources = [
|
|
215
231
|
edge,
|
|
216
|
-
'effect'
|
|
232
|
+
'effect',
|
|
233
|
+
'effect/Layer'
|
|
217
234
|
];
|
|
235
|
+
const returned = (body)=>{
|
|
236
|
+
if (!__rspack_external__babel_types_6e8337dd.isBlockStatement(body)) return body;
|
|
237
|
+
const statements = body.body;
|
|
238
|
+
const last = statements.at(-1);
|
|
239
|
+
return statements.slice(0, -1).every((statement)=>__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement)) && __rspack_external__babel_types_6e8337dd.isReturnStatement(last) ? last.argument : void 0;
|
|
240
|
+
};
|
|
218
241
|
const pipe = (node, check)=>__rspack_external__babel_types_6e8337dd.isMemberExpression(node.callee) && !node.callee.computed && __rspack_external__babel_types_6e8337dd.isIdentifier(node.callee.property, {
|
|
219
242
|
name: 'pipe'
|
|
220
243
|
}) && check(node.callee.object) && node.arguments.every((argument)=>method(argument, 'Layer', 'orDie', layerSources) || __rspack_external__babel_types_6e8337dd.isCallExpression(argument) && method(argument.callee, 'Layer', 'provide', layerSources) && argument.arguments.length > 0);
|
|
@@ -223,7 +246,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
223
246
|
if (!__rspack_external__babel_types_6e8337dd.isArrowFunctionExpression(node) && !__rspack_external__babel_types_6e8337dd.isFunctionExpression(node)) return false;
|
|
224
247
|
const parameter = node.params[0];
|
|
225
248
|
if (!__rspack_external__babel_types_6e8337dd.isIdentifier(parameter)) return false;
|
|
226
|
-
const body =
|
|
249
|
+
const body = returned(node.body);
|
|
227
250
|
const chain = (value, count = 0)=>{
|
|
228
251
|
value = unwrap(value);
|
|
229
252
|
if (__rspack_external__babel_types_6e8337dd.isIdentifier(value)) return count > 0 && declaration(value) === parameter;
|
|
@@ -279,7 +302,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
279
302
|
const groupInit = groupValue && initialized(groupValue);
|
|
280
303
|
if (!groupInit || !__rspack_external__babel_types_6e8337dd.isCallExpression(groupInit) || !method(groupInit.callee, 'RpcGroup', 'make', [
|
|
281
304
|
'effect/unstable/rpc',
|
|
282
|
-
'@modern-js/
|
|
305
|
+
'@modern-js/bff-effect/effect-client'
|
|
283
306
|
])) return false;
|
|
284
307
|
const layerInit = initialized(layer) ?? unwrap(layer);
|
|
285
308
|
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(layerInit) || !__rspack_external__babel_types_6e8337dd.isMemberExpression(layerInit.callee) || layerInit.callee.computed || !__rspack_external__babel_types_6e8337dd.isIdentifier(layerInit.callee.property, {
|
|
@@ -299,7 +322,10 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
299
322
|
if (init) return runtime(init);
|
|
300
323
|
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node)) return false;
|
|
301
324
|
const value = imported(node.callee);
|
|
302
|
-
if (value?.name === 'defineEffectBff' &&
|
|
325
|
+
if (value?.name === 'defineEffectBff' && [
|
|
326
|
+
edge,
|
|
327
|
+
nodeRuntime
|
|
328
|
+
].includes(value.specifier) || value?.name === 'assembleEffectBffRuntime' && sharedRuntime.test(value.specifier)) {
|
|
303
329
|
if (1 !== node.arguments.length) return false;
|
|
304
330
|
const values = properties(node.arguments[0]);
|
|
305
331
|
if (!values) return false;
|
|
@@ -315,11 +341,9 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
315
341
|
}
|
|
316
342
|
const decl = declaration(node.callee);
|
|
317
343
|
const factory = decl && __rspack_external__babel_types_6e8337dd.isFunctionDeclaration(decl) ? decl : initialized(node.callee);
|
|
318
|
-
if (!factory || !__rspack_external__babel_types_6e8337dd.isFunctionDeclaration(factory) && !__rspack_external__babel_types_6e8337dd.isArrowFunctionExpression(factory) && !__rspack_external__babel_types_6e8337dd.isFunctionExpression(factory) ||
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const last = statements.at(-1);
|
|
322
|
-
return statements.slice(0, -1).every((statement)=>__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement)) && !!last && __rspack_external__babel_types_6e8337dd.isReturnStatement(last) && !!last.argument && runtime(last.argument);
|
|
344
|
+
if (!factory || !__rspack_external__babel_types_6e8337dd.isFunctionDeclaration(factory) && !__rspack_external__babel_types_6e8337dd.isArrowFunctionExpression(factory) && !__rspack_external__babel_types_6e8337dd.isFunctionExpression(factory) || !factory.body) return false;
|
|
345
|
+
const result = returned(factory.body);
|
|
346
|
+
return !!result && runtime(result);
|
|
323
347
|
});
|
|
324
348
|
const root = entry.file.program.body.find((statement)=>__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement));
|
|
325
349
|
return root && runtime(root.declaration) ? void 0 : failure;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles } from "../microvertical-api-boundary.js";
|
|
3
|
+
function runMicroVerticalApiCheckCli(args = process.argv.slice(2), filesOnly = false) {
|
|
4
|
+
let workspaceRoot = process.env.ULTRAMODERN_WORKSPACE_ROOT ?? process.cwd();
|
|
5
|
+
let baselinePackageDirectory;
|
|
6
|
+
for(let index = 0; index < args.length; index += 1){
|
|
7
|
+
const argument = args[index];
|
|
8
|
+
if ('--help' === argument || '-h' === argument) {
|
|
9
|
+
console.log(`${filesOnly ? 'modern-api-check-files' : 'modern-api-check'} [--workspace-root <path>] [--baseline-package-directory <path>]\n${filesOnly ? 'Checks consumer files, contracts, package exports and owner identity. Does not analyze runtime topology; run the strict Effect source phase separately.' : 'Checks consumer files, contracts, package exports, owner identity and runtime topology once per API entry.'}\nWorkspace defaults to ULTRAMODERN_WORKSPACE_ROOT then cwd. Exit codes: 0 valid, 1 consumer violation, 2 tool/configuration failure.`);
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
if (![
|
|
13
|
+
'--workspace-root',
|
|
14
|
+
'--baseline-package-directory'
|
|
15
|
+
].includes(argument ?? '') || !args[index + 1] || args[index + 1]?.startsWith('--')) {
|
|
16
|
+
console.error(`Invalid argument: ${argument ?? ''}. Use --help.`);
|
|
17
|
+
return 2;
|
|
18
|
+
}
|
|
19
|
+
const value = args[++index];
|
|
20
|
+
if ('--workspace-root' === argument) workspaceRoot = value;
|
|
21
|
+
else baselinePackageDirectory = value;
|
|
22
|
+
}
|
|
23
|
+
const result = (filesOnly ? checkMicroVerticalApiConsumerFiles : checkMicroVerticalApiBoundaries)({
|
|
24
|
+
workspaceRoot,
|
|
25
|
+
baselinePackageDirectory
|
|
26
|
+
});
|
|
27
|
+
for (const diagnostic of result.diagnostics)console.error(`API violation: ${diagnostic}`);
|
|
28
|
+
for (const error of result.toolErrors)console.error(`API tool error: ${error}`);
|
|
29
|
+
if (result.toolErrors.length) return 2;
|
|
30
|
+
if (result.diagnostics.length) return 1;
|
|
31
|
+
console.log(`UltraModern API ${filesOnly ? 'consumer files' : 'boundary'} check passed.`);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
export { runMicroVerticalApiCheckCli };
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import node_fs from "node:fs";
|
|
3
|
+
import { parse } from "@babel/parser";
|
|
4
|
+
import traverse, { Hub, NodePath } from "@babel/traverse";
|
|
5
|
+
import { baselinePublicIdentityIsExact } from "./microvertical-api-owner.js";
|
|
6
|
+
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
7
|
+
class ConsumerSyntaxError extends Error {
|
|
8
|
+
}
|
|
9
|
+
class ConsumerValidationHub extends Hub {
|
|
10
|
+
buildError(_node, message) {
|
|
11
|
+
return new ConsumerSyntaxError(message);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function parseConsumer(filePath) {
|
|
15
|
+
if (node_fs.statSync(filePath).size > 1000000) throw new Error(`${filePath}: consumer source exceeds 1 MB analysis budget`);
|
|
16
|
+
let file;
|
|
17
|
+
try {
|
|
18
|
+
file = parse(node_fs.readFileSync(filePath, 'utf8'), {
|
|
19
|
+
sourceType: 'module',
|
|
20
|
+
sourceFilename: filePath,
|
|
21
|
+
plugins: [
|
|
22
|
+
"typescript"
|
|
23
|
+
]
|
|
24
|
+
});
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if ('object' == typeof error && null !== error && 'code' in error && 'BABEL_PARSER_SYNTAX_ERROR' === error.code) throw new ConsumerSyntaxError(error instanceof Error ? error.message : String(error));
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
const program = NodePath.get({
|
|
30
|
+
hub: new ConsumerValidationHub(),
|
|
31
|
+
parentPath: void 0,
|
|
32
|
+
parent: file,
|
|
33
|
+
container: file,
|
|
34
|
+
key: 'program'
|
|
35
|
+
});
|
|
36
|
+
program.setContext();
|
|
37
|
+
traverse(file, {
|
|
38
|
+
AssignmentExpression (p) {
|
|
39
|
+
throw new ConsumerSyntaxError(`contract bindings must be immutable at ${p.node.start}`);
|
|
40
|
+
},
|
|
41
|
+
UpdateExpression (p) {
|
|
42
|
+
throw new ConsumerSyntaxError(`contract bindings must be immutable at ${p.node.start}`);
|
|
43
|
+
},
|
|
44
|
+
TSModuleDeclaration () {
|
|
45
|
+
throw new ConsumerSyntaxError('contract bindings must not be merged with namespaces');
|
|
46
|
+
}
|
|
47
|
+
}, program.scope, void 0, program);
|
|
48
|
+
return file;
|
|
49
|
+
}
|
|
50
|
+
const camelCaseStem = (stem)=>stem.replaceAll(/-([a-z0-9])/gu, (_match, letter)=>letter.toUpperCase());
|
|
51
|
+
const pascalCaseStem = (stem)=>{
|
|
52
|
+
const camelStem = camelCaseStem(stem);
|
|
53
|
+
return `${camelStem.slice(0, 1).toUpperCase()}${camelStem.slice(1)}`;
|
|
54
|
+
};
|
|
55
|
+
const identifierName = (node)=>void 0 !== node && __rspack_external__babel_types_6e8337dd.isIdentifier(node) ? node.name : void 0;
|
|
56
|
+
const propertyName = (node)=>{
|
|
57
|
+
if (void 0 !== node && (__rspack_external__babel_types_6e8337dd.isIdentifier(node) || __rspack_external__babel_types_6e8337dd.isStringLiteral(node) || __rspack_external__babel_types_6e8337dd.isNumericLiteral(node))) return __rspack_external__babel_types_6e8337dd.isIdentifier(node) ? node.name : String(node.value);
|
|
58
|
+
};
|
|
59
|
+
const accessPath = (node)=>{
|
|
60
|
+
if (__rspack_external__babel_types_6e8337dd.isIdentifier(node)) return [
|
|
61
|
+
node.name
|
|
62
|
+
];
|
|
63
|
+
if (__rspack_external__babel_types_6e8337dd.isMemberExpression(node) && !node.computed && __rspack_external__babel_types_6e8337dd.isIdentifier(node.property)) {
|
|
64
|
+
const parent = accessPath(node.object);
|
|
65
|
+
return void 0 === parent ? void 0 : [
|
|
66
|
+
...parent,
|
|
67
|
+
node.property.name
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const isAccessPath = (node, expected)=>accessPath(node)?.join('.') === expected.join('.');
|
|
72
|
+
const unwrapExpression = (expression)=>{
|
|
73
|
+
let current = expression;
|
|
74
|
+
while(__rspack_external__babel_types_6e8337dd.isTSAsExpression(current) || __rspack_external__babel_types_6e8337dd.isParenthesizedExpression(current) || __rspack_external__babel_types_6e8337dd.isTSSatisfiesExpression(current))current = current.expression;
|
|
75
|
+
return current;
|
|
76
|
+
};
|
|
77
|
+
const stringLiteral = (expression)=>{
|
|
78
|
+
if (null == expression) return;
|
|
79
|
+
const unwrapped = unwrapExpression(expression);
|
|
80
|
+
return __rspack_external__babel_types_6e8337dd.isStringLiteral(unwrapped) ? unwrapped.value : void 0;
|
|
81
|
+
};
|
|
82
|
+
const numericLiteral = (expression)=>{
|
|
83
|
+
if (null == expression) return;
|
|
84
|
+
const unwrapped = unwrapExpression(expression);
|
|
85
|
+
return __rspack_external__babel_types_6e8337dd.isNumericLiteral(unwrapped) ? Number(unwrapped.value) : void 0;
|
|
86
|
+
};
|
|
87
|
+
const callExpression = (expression, callee)=>{
|
|
88
|
+
if (null == expression) return;
|
|
89
|
+
const unwrapped = unwrapExpression(expression);
|
|
90
|
+
return __rspack_external__babel_types_6e8337dd.isCallExpression(unwrapped) && isAccessPath(unwrapped.callee, callee) ? unwrapped : void 0;
|
|
91
|
+
};
|
|
92
|
+
const localConst = (sourceFile, name)=>{
|
|
93
|
+
for (const item of sourceFile.program.body){
|
|
94
|
+
const statement = __rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(item) ? item.declaration : item;
|
|
95
|
+
if (!__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement) || 'const' !== statement.kind) continue;
|
|
96
|
+
const matches = statement.declarations.filter((declaration)=>__rspack_external__babel_types_6e8337dd.isIdentifier(declaration.id, {
|
|
97
|
+
name
|
|
98
|
+
}));
|
|
99
|
+
if (1 === matches.length) return matches[0];
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const exportedConst = (sourceFile, name)=>{
|
|
103
|
+
for (const statement of sourceFile.program.body)if (__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) && 'type' !== statement.exportKind) {
|
|
104
|
+
if (__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement.declaration) && 'const' === statement.declaration.kind) {
|
|
105
|
+
const declaration = statement.declaration.declarations.find((value)=>__rspack_external__babel_types_6e8337dd.isIdentifier(value.id, {
|
|
106
|
+
name
|
|
107
|
+
}));
|
|
108
|
+
if (declaration) return declaration;
|
|
109
|
+
}
|
|
110
|
+
if (!statement.source && statement.specifiers.some((value)=>__rspack_external__babel_types_6e8337dd.isExportSpecifier(value) && 'type' !== value.exportKind && __rspack_external__babel_types_6e8337dd.isIdentifier(value.local, {
|
|
111
|
+
name
|
|
112
|
+
}) && __rspack_external__babel_types_6e8337dd.isIdentifier(value.exported, {
|
|
113
|
+
name
|
|
114
|
+
}))) return localConst(sourceFile, name);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const objectLiteral = (expression)=>{
|
|
118
|
+
if (null == expression) return;
|
|
119
|
+
const unwrapped = unwrapExpression(expression);
|
|
120
|
+
return __rspack_external__babel_types_6e8337dd.isObjectExpression(unwrapped) ? unwrapped : void 0;
|
|
121
|
+
};
|
|
122
|
+
const propertyAssignments = (properties)=>{
|
|
123
|
+
const assignments = new Map();
|
|
124
|
+
for (const property of properties){
|
|
125
|
+
if (!__rspack_external__babel_types_6e8337dd.isObjectProperty(property) || property.computed || property.shorthand) return;
|
|
126
|
+
const name = propertyName(property.key);
|
|
127
|
+
if (void 0 === name || assignments.has(name)) return;
|
|
128
|
+
assignments.set(name, property);
|
|
129
|
+
}
|
|
130
|
+
return assignments;
|
|
131
|
+
};
|
|
132
|
+
const exactCall = (expression, callee, argumentCount)=>{
|
|
133
|
+
const call = callExpression(expression, callee);
|
|
134
|
+
return call?.arguments.length === argumentCount ? call : void 0;
|
|
135
|
+
};
|
|
136
|
+
const sharedSchemaObject = (declaration, sharedSchemaName, protectedFields)=>{
|
|
137
|
+
if (declaration?.init == null) return;
|
|
138
|
+
const initializer = unwrapExpression(declaration.init);
|
|
139
|
+
if (__rspack_external__babel_types_6e8337dd.isIdentifier(initializer) && initializer.name === sharedSchemaName) return {
|
|
140
|
+
assignments: new Map(),
|
|
141
|
+
identity: true
|
|
142
|
+
};
|
|
143
|
+
const struct = exactCall(initializer, [
|
|
144
|
+
'Schema',
|
|
145
|
+
'Struct'
|
|
146
|
+
], 1);
|
|
147
|
+
const schemaObject = objectLiteral(struct?.arguments[0]);
|
|
148
|
+
if (void 0 === schemaObject) return;
|
|
149
|
+
const spreads = schemaObject.properties.filter(__rspack_external__babel_types_6e8337dd.isSpreadElement);
|
|
150
|
+
const assignments = propertyAssignments(schemaObject.properties.filter((property)=>!__rspack_external__babel_types_6e8337dd.isSpreadElement(property)));
|
|
151
|
+
if (1 !== spreads.length || !spreads.every((spread)=>isAccessPath(spread.argument, [
|
|
152
|
+
sharedSchemaName,
|
|
153
|
+
'fields'
|
|
154
|
+
])) || void 0 === assignments || protectedFields.some((field)=>assignments.has(field))) return;
|
|
155
|
+
return {
|
|
156
|
+
assignments,
|
|
157
|
+
identity: false
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
const directCallChain = (expression)=>{
|
|
161
|
+
if (null == expression) return;
|
|
162
|
+
let current = unwrapExpression(expression);
|
|
163
|
+
const methods = [];
|
|
164
|
+
while(__rspack_external__babel_types_6e8337dd.isCallExpression(current) && __rspack_external__babel_types_6e8337dd.isMemberExpression(current.callee) && !current.callee.computed && __rspack_external__babel_types_6e8337dd.isIdentifier(current.callee.property) && !__rspack_external__babel_types_6e8337dd.isIdentifier(current.callee.object)){
|
|
165
|
+
methods.unshift({
|
|
166
|
+
arguments: current.arguments,
|
|
167
|
+
name: current.callee.property.name
|
|
168
|
+
});
|
|
169
|
+
current = unwrapExpression(current.callee.object);
|
|
170
|
+
}
|
|
171
|
+
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(current)) return;
|
|
172
|
+
return {
|
|
173
|
+
base: current,
|
|
174
|
+
methods
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const brandedStringSchemaIsExact = (declaration, brand)=>{
|
|
178
|
+
const initializer = declaration?.init;
|
|
179
|
+
if (null == initializer) return false;
|
|
180
|
+
const pipeCall = unwrapExpression(initializer);
|
|
181
|
+
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(pipeCall) || !__rspack_external__babel_types_6e8337dd.isMemberExpression(pipeCall.callee) || pipeCall.callee.computed || !__rspack_external__babel_types_6e8337dd.isIdentifier(pipeCall.callee.property, {
|
|
182
|
+
name: 'pipe'
|
|
183
|
+
}) || !isAccessPath(pipeCall.callee.object, [
|
|
184
|
+
'Schema',
|
|
185
|
+
'String'
|
|
186
|
+
]) || 1 !== pipeCall.arguments.length) return false;
|
|
187
|
+
const brandCall = exactCall(pipeCall.arguments[0], [
|
|
188
|
+
'Schema',
|
|
189
|
+
'brand'
|
|
190
|
+
], 1);
|
|
191
|
+
return stringLiteral(brandCall?.arguments[0]) === brand;
|
|
192
|
+
};
|
|
193
|
+
const importedRuntimeNames = (statement, expectedPackage)=>{
|
|
194
|
+
if (!__rspack_external__babel_types_6e8337dd.isImportDeclaration(statement) || statement.source.value !== expectedPackage || 'type' === statement.importKind) return [];
|
|
195
|
+
return statement.specifiers.flatMap((value)=>__rspack_external__babel_types_6e8337dd.isImportSpecifier(value) && 'type' !== value.importKind && __rspack_external__babel_types_6e8337dd.isIdentifier(value.imported) && value.imported.name === value.local.name ? [
|
|
196
|
+
value.local.name
|
|
197
|
+
] : []);
|
|
198
|
+
};
|
|
199
|
+
const importsExactBindings = (sourceFile, expectedPackage, expectedBindings)=>{
|
|
200
|
+
const names = new Set(sourceFile.program.body.flatMap((statement)=>importedRuntimeNames(statement, expectedPackage)));
|
|
201
|
+
return expectedBindings.every((name)=>names.has(name));
|
|
202
|
+
};
|
|
203
|
+
const baselineBindings = [
|
|
204
|
+
'MicroVerticalBuildMarkerSchema',
|
|
205
|
+
'MicroVerticalReadinessSchema',
|
|
206
|
+
'createMicroVerticalOperationContext'
|
|
207
|
+
];
|
|
208
|
+
const importsSharedBaselinePrimitives = (sourceFile, expectedPackage)=>importsExactBindings(sourceFile, expectedPackage, baselineBindings);
|
|
209
|
+
const singleAddedArgument = (expression, factory, names)=>{
|
|
210
|
+
const chain = directCallChain(expression);
|
|
211
|
+
if (void 0 === chain || !isAccessPath(chain.base.callee, factory)) return;
|
|
212
|
+
const [method] = chain.methods;
|
|
213
|
+
if (1 !== chain.base.arguments.length || !names.includes(stringLiteral(chain.base.arguments[0]) ?? '') || 1 !== chain.methods.length || method?.name !== 'add' || 1 !== method.arguments.length) return;
|
|
214
|
+
return method.arguments[0];
|
|
215
|
+
};
|
|
216
|
+
const foundationIsExact = (declaration, stem, readinessSchemaName)=>{
|
|
217
|
+
const group = singleAddedArgument(declaration?.init, [
|
|
218
|
+
'HttpApi',
|
|
219
|
+
'make'
|
|
220
|
+
], [
|
|
221
|
+
`${pascalCaseStem(stem)}FoundationApi`,
|
|
222
|
+
`${pascalCaseStem(stem)}ApiFoundation`
|
|
223
|
+
]);
|
|
224
|
+
const endpointExpression = singleAddedArgument(group, [
|
|
225
|
+
'HttpApiGroup',
|
|
226
|
+
'make'
|
|
227
|
+
], [
|
|
228
|
+
'foundation'
|
|
229
|
+
]);
|
|
230
|
+
const endpoint = exactCall(endpointExpression, [
|
|
231
|
+
'HttpApiEndpoint',
|
|
232
|
+
'get'
|
|
233
|
+
], 3);
|
|
234
|
+
const endpointOptions = objectLiteral(endpoint?.arguments[2]);
|
|
235
|
+
const endpointProperties = void 0 === endpointOptions ? void 0 : propertyAssignments(endpointOptions.properties);
|
|
236
|
+
return void 0 !== endpoint && 'readiness' === stringLiteral(endpoint.arguments[0]) && stringLiteral(endpoint.arguments[1]) === `/${stem}/readiness` && endpointProperties?.size === 1 && identifierName(endpointProperties.get('success')?.value) === readinessSchemaName;
|
|
237
|
+
};
|
|
238
|
+
const rootComposesFoundation = (sourceFile, declaration, stem, foundationName)=>{
|
|
239
|
+
const chain = directCallChain(declaration?.init);
|
|
240
|
+
const first = chain?.methods[0];
|
|
241
|
+
return void 0 !== chain && isAccessPath(chain.base.callee, [
|
|
242
|
+
'HttpApi',
|
|
243
|
+
'make'
|
|
244
|
+
]) && 1 === chain.base.arguments.length && stringLiteral(chain.base.arguments[0]) === `${pascalCaseStem(stem)}Api` && chain.methods.length > 0 && chain.methods.every((method, index)=>1 === method.arguments.length && ('add' === method.name || 'addHttpApi' === method.name || index === chain.methods.length - 1 && 'pipe' === method.name && 'identity' === identifierName(method.arguments[0]) && importsExactBindings(sourceFile, 'effect', [
|
|
245
|
+
'identity'
|
|
246
|
+
]))) && first?.name === 'addHttpApi' && identifierName(first.arguments[0]) === foundationName;
|
|
247
|
+
};
|
|
248
|
+
const reachableEndpoints = (sourceFile, declaration)=>{
|
|
249
|
+
const active = new Set();
|
|
250
|
+
const endpoints = [];
|
|
251
|
+
const identities = new Set();
|
|
252
|
+
const verbs = {
|
|
253
|
+
get: 'GET',
|
|
254
|
+
post: 'POST',
|
|
255
|
+
put: 'PUT',
|
|
256
|
+
patch: 'PATCH',
|
|
257
|
+
del: 'DELETE',
|
|
258
|
+
delete: 'DELETE',
|
|
259
|
+
head: 'HEAD',
|
|
260
|
+
options: 'OPTIONS'
|
|
261
|
+
};
|
|
262
|
+
let visits = 0;
|
|
263
|
+
const visit = (expression, kind, group = '')=>{
|
|
264
|
+
if (!expression) return false;
|
|
265
|
+
const node = unwrapExpression(expression);
|
|
266
|
+
if (active.has(node) || active.size >= 128 || ++visits > 2048) return false;
|
|
267
|
+
active.add(node);
|
|
268
|
+
try {
|
|
269
|
+
if (__rspack_external__babel_types_6e8337dd.isIdentifier(node)) return visit(localConst(sourceFile, node.name)?.init, kind, group);
|
|
270
|
+
if ('endpoint' === kind) {
|
|
271
|
+
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node) || node.arguments.length < 2 || node.arguments.length > 3) return false;
|
|
272
|
+
const callee = accessPath(node.callee);
|
|
273
|
+
const method = callee?.length === 2 && 'HttpApiEndpoint' === callee[0] ? verbs[callee[1]] : void 0;
|
|
274
|
+
const name = stringLiteral(node.arguments[0]);
|
|
275
|
+
const routePath = stringLiteral(node.arguments[1]);
|
|
276
|
+
if (!method || !name || !routePath) return false;
|
|
277
|
+
const identity = `${group}:${name}`;
|
|
278
|
+
if (identities.has(identity)) return false;
|
|
279
|
+
identities.add(identity);
|
|
280
|
+
endpoints.push({
|
|
281
|
+
group,
|
|
282
|
+
name,
|
|
283
|
+
method,
|
|
284
|
+
routePath
|
|
285
|
+
});
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
const chain = directCallChain(node);
|
|
289
|
+
if (!chain || !isAccessPath(chain.base.callee, [
|
|
290
|
+
'api' === kind ? 'HttpApi' : 'HttpApiGroup',
|
|
291
|
+
'make'
|
|
292
|
+
]) || 1 !== chain.base.arguments.length) return false;
|
|
293
|
+
const name = stringLiteral(chain.base.arguments[0]);
|
|
294
|
+
if (!name) return false;
|
|
295
|
+
return chain.methods.every((method)=>{
|
|
296
|
+
if (1 !== method.arguments.length) return false;
|
|
297
|
+
if ('pipe' === method.name) return 'identity' === identifierName(method.arguments[0]) && importsExactBindings(sourceFile, 'effect', [
|
|
298
|
+
'identity'
|
|
299
|
+
]);
|
|
300
|
+
if ('add' === method.name) return visit(method.arguments[0], 'api' === kind ? 'group' : 'endpoint', 'group' === kind ? name : '');
|
|
301
|
+
return 'api' === kind && 'addHttpApi' === method.name && visit(method.arguments[0], 'api');
|
|
302
|
+
});
|
|
303
|
+
} finally{
|
|
304
|
+
active.delete(node);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
return visit(declaration?.init, 'api') ? endpoints : void 0;
|
|
308
|
+
};
|
|
309
|
+
const operationContextFields = (property)=>{
|
|
310
|
+
const constructorCall = exactCall(property.value, [
|
|
311
|
+
'createMicroVerticalOperationContext'
|
|
312
|
+
], 1);
|
|
313
|
+
const input = objectLiteral(constructorCall?.arguments[0]);
|
|
314
|
+
return void 0 === input ? void 0 : propertyAssignments(input.properties);
|
|
315
|
+
};
|
|
316
|
+
const operationContextIdentity = (property)=>{
|
|
317
|
+
const fields = operationContextFields(property);
|
|
318
|
+
const method = stringLiteral(fields?.get('method')?.value);
|
|
319
|
+
const operationId = stringLiteral(fields?.get('operationId')?.value);
|
|
320
|
+
const routePath = stringLiteral(fields?.get('routePath')?.value);
|
|
321
|
+
if (fields?.size !== 3 || void 0 === method || void 0 === operationId || void 0 === routePath) return;
|
|
322
|
+
return {
|
|
323
|
+
method,
|
|
324
|
+
operationId,
|
|
325
|
+
routePath
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
const operationContextIsConstructed = (property, stem, propertyKey, operationPaths, endpoints)=>{
|
|
329
|
+
const identity = operationContextIdentity(property);
|
|
330
|
+
if (void 0 === identity) return false;
|
|
331
|
+
const { method, operationId, routePath } = identity;
|
|
332
|
+
if (!/^[A-Z]+$/u.test(method) || !/^\/(?!.*(?:^|\/)\.\.?\/)[^\s?#]*$/u.test(routePath)) return false;
|
|
333
|
+
const apiName = `${pascalCaseStem(stem)}Api`;
|
|
334
|
+
if ('readiness' === propertyKey && ('GET' !== method || routePath !== `/${stem}/readiness`)) return false;
|
|
335
|
+
return operationId === `${apiName}:${routePath}` || operationPaths[operationId] === routePath || endpoints.some((endpoint)=>endpoint.name === propertyKey && endpoint.method === method && endpoint.routePath === routePath && (operationId === `${apiName}:${endpoint.group}:${propertyKey}` || 'readiness' === propertyKey && operationId === `${apiName}:${camelCaseStem(stem)}:readiness`));
|
|
336
|
+
};
|
|
337
|
+
const operationMapIsConnected = (declaration, stem, operationPaths, endpoints)=>{
|
|
338
|
+
const map = objectLiteral(declaration?.init);
|
|
339
|
+
const properties = void 0 === map ? void 0 : propertyAssignments(map.properties);
|
|
340
|
+
return void 0 !== properties && properties.has('readiness') && [
|
|
341
|
+
...properties
|
|
342
|
+
].every(([key, property])=>operationContextIsConstructed(property, stem, key, operationPaths, endpoints));
|
|
343
|
+
};
|
|
344
|
+
const constAssertionObject = (declaration)=>{
|
|
345
|
+
const initializer = declaration?.init;
|
|
346
|
+
if (null == initializer || !__rspack_external__babel_types_6e8337dd.isTSAsExpression(initializer) || !__rspack_external__babel_types_6e8337dd.isTSTypeReference(initializer.typeAnnotation) || !__rspack_external__babel_types_6e8337dd.isIdentifier(initializer.typeAnnotation.typeName, {
|
|
347
|
+
name: 'const'
|
|
348
|
+
})) return;
|
|
349
|
+
return objectLiteral(initializer.expression);
|
|
350
|
+
};
|
|
351
|
+
const metadataIsExact = (declaration, expectation, endpoints)=>{
|
|
352
|
+
const object = constAssertionObject(declaration);
|
|
353
|
+
const fields = void 0 === object ? void 0 : propertyAssignments(object.properties);
|
|
354
|
+
const expectedFields = [
|
|
355
|
+
[
|
|
356
|
+
'apiPrefix',
|
|
357
|
+
expectation.apiPrefix
|
|
358
|
+
],
|
|
359
|
+
[
|
|
360
|
+
'basePath',
|
|
361
|
+
expectation.basePath
|
|
362
|
+
],
|
|
363
|
+
[
|
|
364
|
+
'ownerId',
|
|
365
|
+
expectation.ownerId
|
|
366
|
+
],
|
|
367
|
+
[
|
|
368
|
+
'readinessPath',
|
|
369
|
+
expectation.readinessPath
|
|
370
|
+
]
|
|
371
|
+
];
|
|
372
|
+
if (!fields || !expectedFields.every(([field, value])=>stringLiteral(fields.get(field)?.value) === value)) return false;
|
|
373
|
+
const common = new Set(expectedFields.map(([field])=>field));
|
|
374
|
+
return [
|
|
375
|
+
...fields
|
|
376
|
+
].every(([field, property])=>{
|
|
377
|
+
if (common.has(field)) return true;
|
|
378
|
+
const value = stringLiteral(property.value);
|
|
379
|
+
if (void 0 === value) return false;
|
|
380
|
+
const explicit = expectation.additionalPaths[field];
|
|
381
|
+
if (void 0 !== explicit) return value === explicit;
|
|
382
|
+
if (!value.startsWith(`${expectation.apiPrefix}/`) || /[\s?#]/u.test(value) || value.split('/').some((segment)=>'.' === segment || '..' === segment)) return false;
|
|
383
|
+
return endpoints.some((endpoint)=>{
|
|
384
|
+
const route = `${expectation.apiPrefix}${endpoint.routePath}`;
|
|
385
|
+
return route === value || route.startsWith(`${value}/`);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
const markerSchemaIsShared = (sourceFile, declaration)=>{
|
|
390
|
+
const schema = sharedSchemaObject(declaration, 'MicroVerticalBuildMarkerSchema', [
|
|
391
|
+
'build',
|
|
392
|
+
'buildMarker',
|
|
393
|
+
'deployProfile',
|
|
394
|
+
'packageName',
|
|
395
|
+
'sourceRevision',
|
|
396
|
+
'surface',
|
|
397
|
+
'version'
|
|
398
|
+
]);
|
|
399
|
+
if (void 0 === schema || schema.identity) return schema?.identity === true;
|
|
400
|
+
const brandedField = (property, brand)=>identifierName(property.value) === `${brand}Schema` && brandedStringSchemaIsExact(localConst(sourceFile, `${brand}Schema`), brand);
|
|
401
|
+
const validators = new Map([
|
|
402
|
+
[
|
|
403
|
+
'appId',
|
|
404
|
+
(property)=>brandedField(property, 'AppId')
|
|
405
|
+
],
|
|
406
|
+
[
|
|
407
|
+
'unitId',
|
|
408
|
+
(property)=>brandedField(property, 'UnitId')
|
|
409
|
+
],
|
|
410
|
+
[
|
|
411
|
+
'kind',
|
|
412
|
+
(property)=>'microvertical-delivery-unit' === stringLiteral(exactCall(property.value, [
|
|
413
|
+
'Schema',
|
|
414
|
+
'Literal'
|
|
415
|
+
], 1)?.arguments[0])
|
|
416
|
+
],
|
|
417
|
+
[
|
|
418
|
+
'schemaVersion',
|
|
419
|
+
(property)=>1 === numericLiteral(exactCall(property.value, [
|
|
420
|
+
'Schema',
|
|
421
|
+
'Literal'
|
|
422
|
+
], 1)?.arguments[0])
|
|
423
|
+
]
|
|
424
|
+
]);
|
|
425
|
+
return [
|
|
426
|
+
...schema.assignments
|
|
427
|
+
].every(([field, property])=>validators.get(field)?.(property) === true);
|
|
428
|
+
};
|
|
429
|
+
const readinessSchemaIsShared = (declaration, markerSchemaName, ownerMarkerIsIdentity)=>{
|
|
430
|
+
const schema = sharedSchemaObject(declaration, 'MicroVerticalReadinessSchema', [
|
|
431
|
+
'checks',
|
|
432
|
+
'status',
|
|
433
|
+
'versionSkew'
|
|
434
|
+
]);
|
|
435
|
+
return void 0 !== schema && (schema.identity && ownerMarkerIsIdentity || 1 === schema.assignments.size && identifierName(schema.assignments.get('marker')?.value) === markerSchemaName);
|
|
436
|
+
};
|
|
437
|
+
const declarationIsIdentifier = (declaration, identifier)=>declaration?.init != null && identifierName(unwrapExpression(declaration.init)) === identifier;
|
|
438
|
+
const validateParsedContract = (sourceFile, stem, expectation)=>{
|
|
439
|
+
const exportStem = camelCaseStem(stem);
|
|
440
|
+
const foundationName = `${exportStem}FoundationApi`;
|
|
441
|
+
const markerSchemaName = `${exportStem}MarkerSchema`;
|
|
442
|
+
const readinessSchemaName = `${exportStem}ReadinessSchema`;
|
|
443
|
+
const markerDeclaration = exportedConst(sourceFile, markerSchemaName);
|
|
444
|
+
if (!importsExactBindings(sourceFile, expectation.effectClientPackage, [
|
|
445
|
+
'HttpApi',
|
|
446
|
+
'HttpApiEndpoint',
|
|
447
|
+
'HttpApiGroup',
|
|
448
|
+
'Schema'
|
|
449
|
+
])) return 'MicroVertical root contract must import exact Effect API primitives from the framework client package';
|
|
450
|
+
if (!importsSharedBaselinePrimitives(sourceFile, expectation.baselinePackage)) return 'MicroVertical root contract must import exact baseline primitives from the framework baseline package';
|
|
451
|
+
if (!markerSchemaIsShared(sourceFile, markerDeclaration)) return 'MicroVertical root contract must consume the shared build marker schema without overriding shared fields';
|
|
452
|
+
if (!readinessSchemaIsShared(exportedConst(sourceFile, readinessSchemaName), markerSchemaName, declarationIsIdentifier(markerDeclaration, 'MicroVerticalBuildMarkerSchema'))) return 'MicroVertical readiness schema must consume the shared readiness schema without overriding shared fields';
|
|
453
|
+
if (!foundationIsExact(exportedConst(sourceFile, foundationName), stem, readinessSchemaName)) return 'MicroVertical readiness foundation API must directly compose its exact readiness endpoint and foundation identity';
|
|
454
|
+
if (!rootComposesFoundation(sourceFile, exportedConst(sourceFile, `${exportStem}Api`), stem, foundationName)) return 'MicroVertical root API must explicitly compose its readiness foundation API';
|
|
455
|
+
const endpoints = reachableEndpoints(sourceFile, exportedConst(sourceFile, `${exportStem}Api`));
|
|
456
|
+
if (!endpoints) return 'MicroVertical root API must compose bounded native endpoint declarations without ambiguous identities';
|
|
457
|
+
if (!operationMapIsConnected(exportedConst(sourceFile, `${exportStem}OperationContexts`), stem, expectation.operationPaths ?? {}, endpoints)) return 'MicroVertical operation map must construct every operation with the shared context constructor';
|
|
458
|
+
if (!metadataIsExact(exportedConst(sourceFile, `${exportStem}ApiContract`), expectation, endpoints)) return 'MicroVertical root contract must keep exact owner and API path metadata without forbidden fields';
|
|
459
|
+
};
|
|
460
|
+
const configuredMicroVerticalApiStem = (verticalPath, verticals)=>{
|
|
461
|
+
const topologyVertical = verticals.find((vertical)=>(vertical.path ?? `verticals/${vertical.id}`) === verticalPath);
|
|
462
|
+
const endpoint = topologyVertical?.api?.readiness?.endpoint;
|
|
463
|
+
return endpoint?.match(/^\/([a-z0-9]+(?:-[a-z0-9]+)*)\/readiness$/u)?.[1];
|
|
464
|
+
};
|
|
465
|
+
const microVerticalApiBaselineViolation = (stem, filePath, expectation)=>{
|
|
466
|
+
try {
|
|
467
|
+
const sourceFile = parseConsumer(filePath);
|
|
468
|
+
if (!baselinePublicIdentityIsExact(filePath, expectation)) return 'MicroVertical baseline imports must resolve the exact framework owner public export and schema identity';
|
|
469
|
+
return validateParsedContract(sourceFile, stem, expectation);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (error instanceof ConsumerSyntaxError) return `MicroVertical root contract must be valid TypeScript syntax (${error.message})`;
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
export { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation };
|