@bleedingdev/modern-js-code-tools 3.9.0-ultramodern.17 → 3.9.0-ultramodern.18
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 -1
- package/bin/modern-api-check.mjs +1 -1
- package/bin/modern-i18n-check.mjs +4 -0
- package/dist/cjs/cli/microvertical-api-check.cjs +36 -7
- package/dist/cjs/cli/workspace-source-check.cjs +90 -17
- package/dist/cjs/microvertical-api-baseline.cjs +24 -255
- package/dist/cjs/microvertical-api-boundary.cjs +25 -0
- package/dist/cjs/module-graph.cjs +321 -0
- package/dist/esm/cli/microvertical-api-check.js +26 -7
- package/dist/esm/cli/workspace-source-check.js +86 -13
- package/dist/esm/microvertical-api-baseline.js +17 -237
- package/dist/esm/microvertical-api-boundary.js +24 -2
- package/dist/esm/module-graph.js +260 -0
- package/dist/esm-node/cli/microvertical-api-check.js +26 -7
- package/dist/esm-node/cli/workspace-source-check.js +86 -13
- package/dist/esm-node/microvertical-api-baseline.js +17 -237
- package/dist/esm-node/microvertical-api-boundary.js +24 -2
- package/dist/esm-node/module-graph.js +261 -0
- package/dist/types/cli/microvertical-api-check.d.ts +1 -1
- package/dist/types/cli/workspace-source-check.d.ts +1 -1
- package/dist/types/microvertical-api-boundary.d.ts +14 -0
- package/dist/types/module-graph.d.ts +52 -0
- package/package.json +4 -3
|
@@ -1,35 +1,13 @@
|
|
|
1
|
-
import node_fs from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import node_path from "node:path";
|
|
4
|
-
import { exports as external_resolve_exports_exports } from "resolve.exports";
|
|
5
1
|
import { baselinePublicIdentityIsExact } from "./microvertical-api-owner.js";
|
|
6
|
-
import {
|
|
2
|
+
import { createModuleGraph, exportedConst, localConst, propertyName } from "./module-graph.js";
|
|
3
|
+
import { SourceSyntaxError, unwrapExpression } from "./source-analysis.js";
|
|
7
4
|
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
8
|
-
function parseConsumer(filePath) {
|
|
9
|
-
if (node_fs.statSync(filePath).size > 1000000) throw new Error(`${filePath}: consumer source exceeds 1 MB analysis budget`);
|
|
10
|
-
const file = parseSource(node_fs.readFileSync(filePath, 'utf8'), filePath);
|
|
11
|
-
traverseSource(file, {
|
|
12
|
-
AssignmentExpression (p) {
|
|
13
|
-
throw new SourceSyntaxError(`contract bindings must be immutable at ${p.node.start}`);
|
|
14
|
-
},
|
|
15
|
-
UpdateExpression (p) {
|
|
16
|
-
throw new SourceSyntaxError(`contract bindings must be immutable at ${p.node.start}`);
|
|
17
|
-
},
|
|
18
|
-
TSModuleDeclaration () {
|
|
19
|
-
throw new SourceSyntaxError('contract bindings must not be merged with namespaces');
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
return file;
|
|
23
|
-
}
|
|
24
5
|
const camelCaseStem = (stem)=>stem.replaceAll(/-([a-z0-9])/gu, (_match, letter)=>letter.toUpperCase());
|
|
25
6
|
const pascalCaseStem = (stem)=>{
|
|
26
7
|
const camelStem = camelCaseStem(stem);
|
|
27
8
|
return `${camelStem.slice(0, 1).toUpperCase()}${camelStem.slice(1)}`;
|
|
28
9
|
};
|
|
29
10
|
const identifierName = (node)=>void 0 !== node && __rspack_external__babel_types_6e8337dd.isIdentifier(node) ? node.name : void 0;
|
|
30
|
-
const propertyName = (node)=>{
|
|
31
|
-
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);
|
|
32
|
-
};
|
|
33
11
|
const accessPath = (node)=>{
|
|
34
12
|
if (__rspack_external__babel_types_6e8337dd.isIdentifier(node)) return [
|
|
35
13
|
node.name
|
|
@@ -58,201 +36,6 @@ const callExpression = (expression, callee)=>{
|
|
|
58
36
|
const unwrapped = unwrapExpression(expression);
|
|
59
37
|
return __rspack_external__babel_types_6e8337dd.isCallExpression(unwrapped) && isAccessPath(unwrapped.callee, callee) ? unwrapped : void 0;
|
|
60
38
|
};
|
|
61
|
-
const localConst = (sourceFile, name)=>{
|
|
62
|
-
for (const item of sourceFile.program.body){
|
|
63
|
-
const statement = __rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(item) ? item.declaration : item;
|
|
64
|
-
if (!__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement) || 'const' !== statement.kind) continue;
|
|
65
|
-
const matches = statement.declarations.filter((declaration)=>__rspack_external__babel_types_6e8337dd.isIdentifier(declaration.id, {
|
|
66
|
-
name
|
|
67
|
-
}));
|
|
68
|
-
if (1 === matches.length) return matches[0];
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
const exportedConst = (sourceFile, name)=>{
|
|
72
|
-
for (const statement of sourceFile.program.body)if (__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) && 'type' !== statement.exportKind) {
|
|
73
|
-
if (__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement.declaration) && 'const' === statement.declaration.kind) {
|
|
74
|
-
const declaration = statement.declaration.declarations.find((value)=>__rspack_external__babel_types_6e8337dd.isIdentifier(value.id, {
|
|
75
|
-
name
|
|
76
|
-
}));
|
|
77
|
-
if (declaration) return declaration;
|
|
78
|
-
}
|
|
79
|
-
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, {
|
|
80
|
-
name
|
|
81
|
-
}) && __rspack_external__babel_types_6e8337dd.isIdentifier(value.exported, {
|
|
82
|
-
name
|
|
83
|
-
}))) return localConst(sourceFile, name);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
const inside = (file, boundary)=>file === boundary || file.startsWith(`${boundary}${node_path.sep}`);
|
|
87
|
-
const containingWorkspace = (file)=>{
|
|
88
|
-
for(let directory = node_path.dirname(file);; directory = node_path.dirname(directory)){
|
|
89
|
-
if (node_fs.existsSync(node_path.join(directory, 'pnpm-workspace.yaml'))) return node_fs.realpathSync(directory);
|
|
90
|
-
if (directory === node_path.dirname(directory)) return;
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
const sourceBoundary = (file, workspace)=>{
|
|
94
|
-
for(let directory = node_path.dirname(file);; directory = node_path.dirname(directory)){
|
|
95
|
-
if (node_fs.existsSync(node_path.join(directory, 'package.json')) && directory !== workspace) return node_fs.realpathSync(directory);
|
|
96
|
-
if (directory === workspace || directory === node_path.dirname(directory)) return node_fs.realpathSync(node_path.dirname(file));
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
const sourceFileAt = (target, boundary)=>{
|
|
100
|
-
const stem = target.replace(/\.(?:[cm]?[jt]sx?)$/u, '');
|
|
101
|
-
for (const candidate of [
|
|
102
|
-
`${stem}.ts`,
|
|
103
|
-
`${stem}.tsx`,
|
|
104
|
-
`${stem}.mts`,
|
|
105
|
-
`${stem}.cts`,
|
|
106
|
-
target,
|
|
107
|
-
node_path.join(target, 'index.ts'),
|
|
108
|
-
node_path.join(target, 'index.tsx')
|
|
109
|
-
])try {
|
|
110
|
-
const real = node_fs.realpathSync(candidate);
|
|
111
|
-
if (inside(real, boundary) && node_fs.statSync(real).isFile()) return real;
|
|
112
|
-
} catch {}
|
|
113
|
-
};
|
|
114
|
-
const packageEntry = (specifier)=>{
|
|
115
|
-
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#')) return;
|
|
116
|
-
const parts = specifier.split('/');
|
|
117
|
-
const count = specifier.startsWith('@') ? 2 : 1;
|
|
118
|
-
if (parts.length < count || parts.slice(0, count).some((part)=>!part)) return;
|
|
119
|
-
return {
|
|
120
|
-
name: parts.slice(0, count).join('/'),
|
|
121
|
-
exportKey: parts.length === count ? '.' : `./${parts.slice(count).join('/')}`
|
|
122
|
-
};
|
|
123
|
-
};
|
|
124
|
-
const resolveModulePath = (module, specifier, workspace)=>{
|
|
125
|
-
if (/^\.\.?\//u.test(specifier)) {
|
|
126
|
-
const file = sourceFileAt(node_path.resolve(node_path.dirname(module.path), specifier), module.boundary);
|
|
127
|
-
return void 0 === file ? void 0 : {
|
|
128
|
-
path: file,
|
|
129
|
-
boundary: module.boundary
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
const entry = packageEntry(specifier);
|
|
133
|
-
if (!entry || !workspace) return;
|
|
134
|
-
const paths = createRequire(module.path).resolve.paths(entry.name) ?? [];
|
|
135
|
-
for (const lookup of paths){
|
|
136
|
-
const directory = node_path.join(lookup, entry.name);
|
|
137
|
-
const manifest = node_path.join(directory, 'package.json');
|
|
138
|
-
if (!node_fs.existsSync(manifest)) continue;
|
|
139
|
-
const boundary = node_fs.realpathSync(directory);
|
|
140
|
-
if (!inside(boundary, workspace) || boundary.split(node_path.sep).includes('node_modules')) break;
|
|
141
|
-
const pkg = JSON.parse(node_fs.readFileSync(manifest, 'utf8'));
|
|
142
|
-
if (pkg.name !== entry.name || void 0 === pkg.exports) break;
|
|
143
|
-
let target;
|
|
144
|
-
try {
|
|
145
|
-
const targets = external_resolve_exports_exports(pkg, entry.exportKey, {
|
|
146
|
-
conditions: [
|
|
147
|
-
'modern:source'
|
|
148
|
-
]
|
|
149
|
-
});
|
|
150
|
-
target = Array.isArray(targets) ? targets[0] : void 0;
|
|
151
|
-
} catch {
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
if (!target || !target.startsWith('./')) break;
|
|
155
|
-
const file = sourceFileAt(node_path.resolve(boundary, target), boundary);
|
|
156
|
-
return void 0 === file ? void 0 : {
|
|
157
|
-
path: file,
|
|
158
|
-
boundary
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
const createModuleReader = (root)=>{
|
|
163
|
-
const modules = new Map([
|
|
164
|
-
[
|
|
165
|
-
root.path,
|
|
166
|
-
root
|
|
167
|
-
]
|
|
168
|
-
]);
|
|
169
|
-
return (filePath, boundary)=>{
|
|
170
|
-
if (modules.has(filePath)) return modules.get(filePath);
|
|
171
|
-
if (modules.size >= 256) return;
|
|
172
|
-
let module;
|
|
173
|
-
try {
|
|
174
|
-
module = {
|
|
175
|
-
file: parseConsumer(filePath),
|
|
176
|
-
path: filePath,
|
|
177
|
-
boundary
|
|
178
|
-
};
|
|
179
|
-
} catch {
|
|
180
|
-
module = void 0;
|
|
181
|
-
}
|
|
182
|
-
modules.set(filePath, module);
|
|
183
|
-
return module;
|
|
184
|
-
};
|
|
185
|
-
};
|
|
186
|
-
const importedBinding = (module, name)=>{
|
|
187
|
-
for (const statement of module.file.program.body)if (__rspack_external__babel_types_6e8337dd.isImportDeclaration(statement) && 'type' !== statement.importKind) {
|
|
188
|
-
for (const specifier of statement.specifiers)if (__rspack_external__babel_types_6e8337dd.isIdentifier(specifier.local, {
|
|
189
|
-
name
|
|
190
|
-
})) {
|
|
191
|
-
if (__rspack_external__babel_types_6e8337dd.isImportSpecifier(specifier) && 'type' !== specifier.importKind) return {
|
|
192
|
-
imported: propertyName(specifier.imported) ?? name,
|
|
193
|
-
specifier: statement.source.value
|
|
194
|
-
};
|
|
195
|
-
if (__rspack_external__babel_types_6e8337dd.isImportDefaultSpecifier(specifier)) return {
|
|
196
|
-
imported: 'default',
|
|
197
|
-
specifier: statement.source.value
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
const reexportedBinding = (module, name)=>{
|
|
203
|
-
for (const statement of module.file.program.body){
|
|
204
|
-
if (!__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || 'type' === statement.exportKind) continue;
|
|
205
|
-
const specifier = statement.specifiers.find((value)=>__rspack_external__babel_types_6e8337dd.isExportSpecifier(value) && 'type' !== value.exportKind && propertyName(value.exported) === name);
|
|
206
|
-
if (void 0 !== specifier && __rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier)) return {
|
|
207
|
-
local: __rspack_external__babel_types_6e8337dd.isIdentifier(specifier.local) ? specifier.local.name : specifier.local.value,
|
|
208
|
-
...statement.source ? {
|
|
209
|
-
specifier: statement.source.value
|
|
210
|
-
} : {}
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
const defaultExport = (module)=>{
|
|
215
|
-
for (const statement of module.file.program.body){
|
|
216
|
-
if (!__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement)) continue;
|
|
217
|
-
const declaration = statement.declaration;
|
|
218
|
-
if (__rspack_external__babel_types_6e8337dd.isIdentifier(declaration)) return declaration.name;
|
|
219
|
-
return __rspack_external__babel_types_6e8337dd.isExpression(declaration) ? declaration : void 0;
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
const resolveBinding = (module, name, scope, readModule, workspace, seen)=>{
|
|
223
|
-
const key = `${scope}:${module.path}#${name}`;
|
|
224
|
-
if (seen.has(key) || seen.size >= 512) return;
|
|
225
|
-
seen.add(key);
|
|
226
|
-
const declaration = 'export' === scope ? exportedConst(module.file, name) : localConst(module.file, name);
|
|
227
|
-
if (declaration?.init) return {
|
|
228
|
-
expression: declaration.init,
|
|
229
|
-
module
|
|
230
|
-
};
|
|
231
|
-
const throughModule = (specifier, imported)=>{
|
|
232
|
-
const resolved = resolveModulePath(module, specifier, workspace);
|
|
233
|
-
const target = resolved && readModule(resolved.path, resolved.boundary);
|
|
234
|
-
return void 0 === target ? void 0 : resolveBinding(target, imported, 'export', readModule, workspace, seen);
|
|
235
|
-
};
|
|
236
|
-
if ('local' === scope) {
|
|
237
|
-
const imported = importedBinding(module, name);
|
|
238
|
-
return void 0 === imported ? void 0 : throughModule(imported.specifier, imported.imported);
|
|
239
|
-
}
|
|
240
|
-
if ('default' === name) {
|
|
241
|
-
const exported = defaultExport(module);
|
|
242
|
-
if ('string' == typeof exported) return resolveBinding(module, exported, 'local', readModule, workspace, seen);
|
|
243
|
-
return void 0 === exported ? void 0 : {
|
|
244
|
-
expression: exported,
|
|
245
|
-
module
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
const reexported = reexportedBinding(module, name);
|
|
249
|
-
if (void 0 !== reexported) return void 0 === reexported.specifier ? resolveBinding(module, reexported.local, 'local', readModule, workspace, seen) : throughModule(reexported.specifier, reexported.local);
|
|
250
|
-
for (const statement of module.file.program.body){
|
|
251
|
-
if (!__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement) || 'type' === statement.exportKind) continue;
|
|
252
|
-
const resolved = throughModule(statement.source.value, name);
|
|
253
|
-
if (void 0 !== resolved) return resolved;
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
39
|
const objectLiteral = (expression)=>{
|
|
257
40
|
if (null == expression) return;
|
|
258
41
|
const unwrapped = unwrapExpression(expression);
|
|
@@ -380,9 +163,13 @@ const rootComposesFoundation = (sourceFile, declaration, stem, foundationName)=>
|
|
|
380
163
|
return void 0 !== chain && isAccessPath(chain.base.callee, [
|
|
381
164
|
'HttpApi',
|
|
382
165
|
'make'
|
|
383
|
-
]) && 1 === chain.base.arguments.length && stringLiteral(chain.base.arguments[0]) === `${pascalCaseStem(stem)}Api` && chain.methods.length > 0 && chain.methods.every((method, index)=>
|
|
166
|
+
]) && 1 === chain.base.arguments.length && stringLiteral(chain.base.arguments[0]) === `${pascalCaseStem(stem)}Api` && chain.methods.length > 0 && chain.methods.every((method, index)=>{
|
|
167
|
+
const combinatorArity = nonEndpointCombinators.api[method.name];
|
|
168
|
+
if (void 0 !== combinatorArity) return method.arguments.length === combinatorArity;
|
|
169
|
+
return 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', [
|
|
384
170
|
'identity'
|
|
385
|
-
]))
|
|
171
|
+
]));
|
|
172
|
+
}) && first?.name === 'addHttpApi' && identifierName(first.arguments[0]) === foundationName;
|
|
386
173
|
};
|
|
387
174
|
const nonEndpointCombinators = {
|
|
388
175
|
api: {
|
|
@@ -398,9 +185,7 @@ const nonEndpointCombinators = {
|
|
|
398
185
|
middleware: 1
|
|
399
186
|
}
|
|
400
187
|
};
|
|
401
|
-
const reachableEndpoints = (
|
|
402
|
-
const readModule = createModuleReader(rootModule);
|
|
403
|
-
const workspace = containingWorkspace(rootModule.path);
|
|
188
|
+
const reachableEndpoints = (graph, declaration)=>{
|
|
404
189
|
const active = new Set();
|
|
405
190
|
const endpoints = [];
|
|
406
191
|
const identities = new Set();
|
|
@@ -422,8 +207,8 @@ const reachableEndpoints = (rootModule, declaration)=>{
|
|
|
422
207
|
active.add(node);
|
|
423
208
|
try {
|
|
424
209
|
if (__rspack_external__babel_types_6e8337dd.isIdentifier(node)) {
|
|
425
|
-
const resolved =
|
|
426
|
-
return
|
|
210
|
+
const resolved = graph.resolve(module, node.name, 'local');
|
|
211
|
+
return resolved?.kind === 'declaration' && visit(resolved.expression, resolved.module, kind, group);
|
|
427
212
|
}
|
|
428
213
|
if ('endpoint' === kind) {
|
|
429
214
|
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node) || node.arguments.length < 2 || node.arguments.length > 3) return false;
|
|
@@ -478,7 +263,7 @@ const reachableEndpoints = (rootModule, declaration)=>{
|
|
|
478
263
|
active.delete(node);
|
|
479
264
|
}
|
|
480
265
|
};
|
|
481
|
-
return visit(declaration?.init,
|
|
266
|
+
return visit(declaration?.init, graph.root, 'api') ? endpoints : void 0;
|
|
482
267
|
};
|
|
483
268
|
const operationContextFields = (property)=>{
|
|
484
269
|
const constructorCall = exactCall(property.value, [
|
|
@@ -609,8 +394,8 @@ const readinessSchemaIsShared = (declaration, markerSchemaName, ownerMarkerIsIde
|
|
|
609
394
|
return void 0 !== schema && (schema.identity && ownerMarkerIsIdentity || 1 === schema.assignments.size && identifierName(schema.assignments.get('marker')?.value) === markerSchemaName);
|
|
610
395
|
};
|
|
611
396
|
const declarationIsIdentifier = (declaration, identifier)=>declaration?.init != null && identifierName(unwrapExpression(declaration.init)) === identifier;
|
|
612
|
-
const validateParsedContract = (
|
|
613
|
-
const sourceFile =
|
|
397
|
+
const validateParsedContract = (graph, stem, expectation)=>{
|
|
398
|
+
const sourceFile = graph.root.file;
|
|
614
399
|
const exportStem = camelCaseStem(stem);
|
|
615
400
|
const foundationName = `${exportStem}FoundationApi`;
|
|
616
401
|
const markerSchemaName = `${exportStem}MarkerSchema`;
|
|
@@ -627,7 +412,7 @@ const validateParsedContract = (module, stem, expectation)=>{
|
|
|
627
412
|
if (!readinessSchemaIsShared(exportedConst(sourceFile, readinessSchemaName), markerSchemaName, declarationIsIdentifier(markerDeclaration, 'MicroVerticalBuildMarkerSchema'))) return 'MicroVertical readiness schema must consume the shared readiness schema without overriding shared fields';
|
|
628
413
|
if (!foundationIsExact(exportedConst(sourceFile, foundationName), stem, readinessSchemaName)) return 'MicroVertical readiness foundation API must directly compose its exact readiness endpoint and foundation identity';
|
|
629
414
|
if (!rootComposesFoundation(sourceFile, exportedConst(sourceFile, `${exportStem}Api`), stem, foundationName)) return 'MicroVertical root API must explicitly compose its readiness foundation API';
|
|
630
|
-
const endpoints = reachableEndpoints(
|
|
415
|
+
const endpoints = reachableEndpoints(graph, exportedConst(sourceFile, `${exportStem}Api`));
|
|
631
416
|
if (!endpoints) return 'MicroVertical root API must compose bounded native endpoint declarations without ambiguous identities';
|
|
632
417
|
if (!operationMapIsConnected(exportedConst(sourceFile, `${exportStem}OperationContexts`), stem, expectation.operationPaths ?? {}, endpoints)) return 'MicroVertical operation map must construct every operation with the shared context constructor';
|
|
633
418
|
if (!metadataIsExact(exportedConst(sourceFile, `${exportStem}ApiContract`), expectation, endpoints)) return 'MicroVertical root contract must keep exact owner and API path metadata without forbidden fields';
|
|
@@ -639,14 +424,9 @@ const configuredMicroVerticalApiStem = (verticalPath, verticals)=>{
|
|
|
639
424
|
};
|
|
640
425
|
const microVerticalApiBaselineViolation = (stem, filePath, expectation)=>{
|
|
641
426
|
try {
|
|
642
|
-
const
|
|
643
|
-
const sourceFile = parseConsumer(realPath);
|
|
427
|
+
const graph = createModuleGraph(filePath);
|
|
644
428
|
if (!baselinePublicIdentityIsExact(filePath, expectation)) return 'MicroVertical baseline imports must resolve the exact framework owner public export and schema identity';
|
|
645
|
-
return validateParsedContract(
|
|
646
|
-
file: sourceFile,
|
|
647
|
-
path: realPath,
|
|
648
|
-
boundary: sourceBoundary(realPath, containingWorkspace(realPath))
|
|
649
|
-
}, stem, expectation);
|
|
429
|
+
return validateParsedContract(graph, stem, expectation);
|
|
650
430
|
} catch (error) {
|
|
651
431
|
if (error instanceof SourceSyntaxError) return `MicroVertical root contract must be valid TypeScript syntax (${error.message})`;
|
|
652
432
|
throw error;
|
|
@@ -3,7 +3,8 @@ import node_path from "node:path";
|
|
|
3
3
|
import { parse } from "@babel/parser";
|
|
4
4
|
import { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation } from "./microvertical-api-baseline.js";
|
|
5
5
|
import { baselinePackage, resolveBaselinePackageDirectory } from "./microvertical-api-owner.js";
|
|
6
|
-
import {
|
|
6
|
+
import { createModuleGraph } from "./module-graph.js";
|
|
7
|
+
import { SourceSyntaxError, consumerParserPlugins } from "./source-analysis.js";
|
|
7
8
|
import { createEffectApiImportResolver, strictEffectRuntimeTopologyViolation } from "./strict-effect-runtime.js";
|
|
8
9
|
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
9
10
|
const ignored = new Set([
|
|
@@ -407,6 +408,27 @@ function check(options, topology) {
|
|
|
407
408
|
const violation = microVerticalApiBaselineViolation(stem, absolute(contract), expectation);
|
|
408
409
|
if (violation) diagnostics.push(`${contract}: ${violation}`);
|
|
409
410
|
}
|
|
411
|
+
if (options.contractRules?.length && exists(contract)) {
|
|
412
|
+
let graph;
|
|
413
|
+
try {
|
|
414
|
+
graph = createModuleGraph(absolute(contract));
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (!(error instanceof SourceSyntaxError)) throw error;
|
|
417
|
+
diagnostics.push(`${contract}: contract rules need a parseable contract with immutable bindings (${error.message})`);
|
|
418
|
+
}
|
|
419
|
+
if (graph) {
|
|
420
|
+
const context = {
|
|
421
|
+
appPath,
|
|
422
|
+
protocol: rpc ? 'rpc' : 'rest',
|
|
423
|
+
graph
|
|
424
|
+
};
|
|
425
|
+
for (const rule of options.contractRules)guarded(`${contract} (rule ${rule.name || 'anonymous'})`, ()=>{
|
|
426
|
+
const messages = rule(context);
|
|
427
|
+
if (!Array.isArray(messages) || !messages.every((message)=>'string' == typeof message)) throw new Error('contract rule must return an array of violation messages (return [] when the contract passes)');
|
|
428
|
+
for (const message of messages)diagnostics.push(`${contract}: ${message}`);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
410
432
|
if (client) {
|
|
411
433
|
const relativeContract = node_path.posix.relative(node_path.posix.dirname(client), contract);
|
|
412
434
|
moduleShape(client, [
|
|
@@ -500,4 +522,4 @@ function check(options, topology) {
|
|
|
500
522
|
}
|
|
501
523
|
const checkMicroVerticalApiBoundaries = (options)=>check(options, true);
|
|
502
524
|
const checkMicroVerticalApiConsumerFiles = (options)=>check(options, false);
|
|
503
|
-
export { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles, configuredMicroVerticalApiStem, microVerticalApiBaselineViolation };
|
|
525
|
+
export { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles, configuredMicroVerticalApiStem, createModuleGraph, microVerticalApiBaselineViolation };
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import node_fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import node_path from "node:path";
|
|
4
|
+
import { exports as external_resolve_exports_exports } from "resolve.exports";
|
|
5
|
+
import { SourceSyntaxError, parseSource, traverseSource } from "./source-analysis.js";
|
|
6
|
+
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
7
|
+
const MAX_MODULES = 256;
|
|
8
|
+
const MAX_LOOKUPS = 512;
|
|
9
|
+
function parseConsumer(filePath) {
|
|
10
|
+
if (node_fs.statSync(filePath).size > 1000000) throw new Error(`${filePath}: consumer source exceeds 1 MB analysis budget`);
|
|
11
|
+
const file = parseSource(node_fs.readFileSync(filePath, 'utf8'), filePath);
|
|
12
|
+
traverseSource(file, {
|
|
13
|
+
AssignmentExpression (p) {
|
|
14
|
+
throw new SourceSyntaxError(`contract bindings must be immutable at ${p.node.start}`);
|
|
15
|
+
},
|
|
16
|
+
UpdateExpression (p) {
|
|
17
|
+
throw new SourceSyntaxError(`contract bindings must be immutable at ${p.node.start}`);
|
|
18
|
+
},
|
|
19
|
+
TSModuleDeclaration () {
|
|
20
|
+
throw new SourceSyntaxError('contract bindings must not be merged with namespaces');
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return file;
|
|
24
|
+
}
|
|
25
|
+
const propertyName = (node)=>{
|
|
26
|
+
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);
|
|
27
|
+
};
|
|
28
|
+
const localConst = (sourceFile, name)=>{
|
|
29
|
+
for (const item of sourceFile.program.body){
|
|
30
|
+
const statement = __rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(item) ? item.declaration : item;
|
|
31
|
+
if (!__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement) || 'const' !== statement.kind) continue;
|
|
32
|
+
const matches = statement.declarations.filter((declaration)=>__rspack_external__babel_types_6e8337dd.isIdentifier(declaration.id, {
|
|
33
|
+
name
|
|
34
|
+
}));
|
|
35
|
+
if (1 === matches.length) return matches[0];
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const exportedConst = (sourceFile, name)=>{
|
|
39
|
+
for (const statement of sourceFile.program.body)if (__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) && 'type' !== statement.exportKind) {
|
|
40
|
+
if (__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement.declaration) && 'const' === statement.declaration.kind) {
|
|
41
|
+
const declaration = statement.declaration.declarations.find((value)=>__rspack_external__babel_types_6e8337dd.isIdentifier(value.id, {
|
|
42
|
+
name
|
|
43
|
+
}));
|
|
44
|
+
if (declaration) return declaration;
|
|
45
|
+
}
|
|
46
|
+
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, {
|
|
47
|
+
name
|
|
48
|
+
}) && __rspack_external__babel_types_6e8337dd.isIdentifier(value.exported, {
|
|
49
|
+
name
|
|
50
|
+
}))) return localConst(sourceFile, name);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const inside = (file, boundary)=>file === boundary || file.startsWith(`${boundary}${node_path.sep}`);
|
|
54
|
+
const containingWorkspace = (file)=>{
|
|
55
|
+
for(let directory = node_path.dirname(file);; directory = node_path.dirname(directory)){
|
|
56
|
+
if (node_fs.existsSync(node_path.join(directory, 'pnpm-workspace.yaml'))) return node_fs.realpathSync(directory);
|
|
57
|
+
if (directory === node_path.dirname(directory)) return;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const sourceBoundary = (file, workspace)=>{
|
|
61
|
+
for(let directory = node_path.dirname(file);; directory = node_path.dirname(directory)){
|
|
62
|
+
if (node_fs.existsSync(node_path.join(directory, 'package.json')) && directory !== workspace) return node_fs.realpathSync(directory);
|
|
63
|
+
if (directory === workspace || directory === node_path.dirname(directory)) return node_fs.realpathSync(node_path.dirname(file));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const sourceFileAt = (target, boundary)=>{
|
|
67
|
+
const stem = target.replace(/\.(?:[cm]?[jt]sx?)$/u, '');
|
|
68
|
+
for (const candidate of [
|
|
69
|
+
`${stem}.ts`,
|
|
70
|
+
`${stem}.tsx`,
|
|
71
|
+
`${stem}.mts`,
|
|
72
|
+
`${stem}.cts`,
|
|
73
|
+
target,
|
|
74
|
+
node_path.join(target, 'index.ts'),
|
|
75
|
+
node_path.join(target, 'index.tsx')
|
|
76
|
+
])try {
|
|
77
|
+
const real = node_fs.realpathSync(candidate);
|
|
78
|
+
if (inside(real, boundary) && node_fs.statSync(real).isFile()) return real;
|
|
79
|
+
} catch {}
|
|
80
|
+
};
|
|
81
|
+
const packageEntry = (specifier)=>{
|
|
82
|
+
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#')) return;
|
|
83
|
+
const parts = specifier.split('/');
|
|
84
|
+
const count = specifier.startsWith('@') ? 2 : 1;
|
|
85
|
+
if (parts.length < count || parts.slice(0, count).some((part)=>!part)) return;
|
|
86
|
+
return {
|
|
87
|
+
name: parts.slice(0, count).join('/'),
|
|
88
|
+
exportKey: parts.length === count ? '.' : `./${parts.slice(count).join('/')}`
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
const resolveModulePath = (module, specifier, workspace)=>{
|
|
92
|
+
if (/^\.\.?\//u.test(specifier)) {
|
|
93
|
+
const file = sourceFileAt(node_path.resolve(node_path.dirname(module.path), specifier), module.boundary);
|
|
94
|
+
return void 0 === file ? void 0 : {
|
|
95
|
+
kind: 'module',
|
|
96
|
+
path: file,
|
|
97
|
+
boundary: module.boundary
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const entry = packageEntry(specifier);
|
|
101
|
+
if (!entry) return;
|
|
102
|
+
if (!workspace) return {
|
|
103
|
+
kind: 'external'
|
|
104
|
+
};
|
|
105
|
+
const paths = createRequire(module.path).resolve.paths(entry.name) ?? [];
|
|
106
|
+
for (const lookup of paths){
|
|
107
|
+
const directory = node_path.join(lookup, entry.name);
|
|
108
|
+
const manifest = node_path.join(directory, 'package.json');
|
|
109
|
+
if (!node_fs.existsSync(manifest)) continue;
|
|
110
|
+
const boundary = node_fs.realpathSync(directory);
|
|
111
|
+
if (!inside(boundary, workspace) || boundary.split(node_path.sep).includes('node_modules')) break;
|
|
112
|
+
const pkg = JSON.parse(node_fs.readFileSync(manifest, 'utf8'));
|
|
113
|
+
if (pkg.name !== entry.name || void 0 === pkg.exports) return;
|
|
114
|
+
let target;
|
|
115
|
+
try {
|
|
116
|
+
const targets = external_resolve_exports_exports(pkg, entry.exportKey, {
|
|
117
|
+
conditions: [
|
|
118
|
+
'modern:source'
|
|
119
|
+
]
|
|
120
|
+
});
|
|
121
|
+
target = Array.isArray(targets) ? targets[0] : void 0;
|
|
122
|
+
} catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!target || !target.startsWith('./')) return;
|
|
126
|
+
const file = sourceFileAt(node_path.resolve(boundary, target), boundary);
|
|
127
|
+
return void 0 === file ? void 0 : {
|
|
128
|
+
kind: 'module',
|
|
129
|
+
path: file,
|
|
130
|
+
boundary
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
kind: 'external'
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
const importedBinding = (module, name)=>{
|
|
138
|
+
for (const statement of module.file.program.body)if (__rspack_external__babel_types_6e8337dd.isImportDeclaration(statement) && 'type' !== statement.importKind) {
|
|
139
|
+
for (const specifier of statement.specifiers)if (__rspack_external__babel_types_6e8337dd.isIdentifier(specifier.local, {
|
|
140
|
+
name
|
|
141
|
+
})) {
|
|
142
|
+
if (__rspack_external__babel_types_6e8337dd.isImportSpecifier(specifier) && 'type' !== specifier.importKind) return {
|
|
143
|
+
imported: propertyName(specifier.imported) ?? name,
|
|
144
|
+
specifier: statement.source.value
|
|
145
|
+
};
|
|
146
|
+
if (__rspack_external__babel_types_6e8337dd.isImportDefaultSpecifier(specifier)) return {
|
|
147
|
+
imported: 'default',
|
|
148
|
+
specifier: statement.source.value
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const reexportedBinding = (module, name)=>{
|
|
154
|
+
for (const statement of module.file.program.body){
|
|
155
|
+
if (!__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || 'type' === statement.exportKind) continue;
|
|
156
|
+
const specifier = statement.specifiers.find((value)=>__rspack_external__babel_types_6e8337dd.isExportSpecifier(value) && 'type' !== value.exportKind && propertyName(value.exported) === name);
|
|
157
|
+
if (void 0 !== specifier && __rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier)) return {
|
|
158
|
+
local: __rspack_external__babel_types_6e8337dd.isIdentifier(specifier.local) ? specifier.local.name : specifier.local.value,
|
|
159
|
+
...statement.source ? {
|
|
160
|
+
specifier: statement.source.value
|
|
161
|
+
} : {}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const defaultExport = (module)=>{
|
|
166
|
+
for (const statement of module.file.program.body){
|
|
167
|
+
if (!__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement)) continue;
|
|
168
|
+
const declaration = statement.declaration;
|
|
169
|
+
if (__rspack_external__babel_types_6e8337dd.isIdentifier(declaration)) return declaration.name;
|
|
170
|
+
return __rspack_external__babel_types_6e8337dd.isExpression(declaration) ? declaration : void 0;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
function createModuleGraph(filePath) {
|
|
174
|
+
const realPath = node_fs.realpathSync(filePath);
|
|
175
|
+
const workspace = containingWorkspace(realPath);
|
|
176
|
+
const root = {
|
|
177
|
+
file: parseConsumer(realPath),
|
|
178
|
+
path: realPath,
|
|
179
|
+
boundary: sourceBoundary(realPath, workspace)
|
|
180
|
+
};
|
|
181
|
+
const modules = new Map([
|
|
182
|
+
[
|
|
183
|
+
root.path,
|
|
184
|
+
root
|
|
185
|
+
]
|
|
186
|
+
]);
|
|
187
|
+
const readModule = (modulePath, boundary)=>{
|
|
188
|
+
if (modules.has(modulePath)) return modules.get(modulePath);
|
|
189
|
+
if (modules.size >= MAX_MODULES) return;
|
|
190
|
+
let module;
|
|
191
|
+
try {
|
|
192
|
+
module = {
|
|
193
|
+
file: parseConsumer(modulePath),
|
|
194
|
+
path: modulePath,
|
|
195
|
+
boundary
|
|
196
|
+
};
|
|
197
|
+
} catch {
|
|
198
|
+
module = void 0;
|
|
199
|
+
}
|
|
200
|
+
modules.set(modulePath, module);
|
|
201
|
+
return module;
|
|
202
|
+
};
|
|
203
|
+
const resolveBinding = (module, name, scope, chain, seen)=>{
|
|
204
|
+
const key = `${scope}:${module.path}#${name}`;
|
|
205
|
+
if (seen.has(key) || seen.size >= MAX_LOOKUPS) return;
|
|
206
|
+
seen.add(key);
|
|
207
|
+
const last = chain.at(-1);
|
|
208
|
+
const hops = last?.path === module.path && last.name === name ? chain : [
|
|
209
|
+
...chain,
|
|
210
|
+
{
|
|
211
|
+
path: module.path,
|
|
212
|
+
name
|
|
213
|
+
}
|
|
214
|
+
];
|
|
215
|
+
const declaration = 'export' === scope ? exportedConst(module.file, name) : localConst(module.file, name);
|
|
216
|
+
if (declaration?.init) return {
|
|
217
|
+
kind: 'declaration',
|
|
218
|
+
expression: declaration.init,
|
|
219
|
+
module,
|
|
220
|
+
chain: hops
|
|
221
|
+
};
|
|
222
|
+
const throughModule = (specifier, imported, allowExternal)=>{
|
|
223
|
+
const target = resolveModulePath(module, specifier, workspace);
|
|
224
|
+
if (target?.kind === 'external') return allowExternal ? {
|
|
225
|
+
kind: 'external',
|
|
226
|
+
specifier,
|
|
227
|
+
name: imported,
|
|
228
|
+
chain: hops
|
|
229
|
+
} : void 0;
|
|
230
|
+
const next = target && readModule(target.path, target.boundary);
|
|
231
|
+
return void 0 === next ? void 0 : resolveBinding(next, imported, 'export', hops, seen);
|
|
232
|
+
};
|
|
233
|
+
if ('local' === scope) {
|
|
234
|
+
const imported = importedBinding(module, name);
|
|
235
|
+
return void 0 === imported ? void 0 : throughModule(imported.specifier, imported.imported, true);
|
|
236
|
+
}
|
|
237
|
+
if ('default' === name) {
|
|
238
|
+
const exported = defaultExport(module);
|
|
239
|
+
if ('string' == typeof exported) return resolveBinding(module, exported, 'local', hops, seen);
|
|
240
|
+
return void 0 === exported ? void 0 : {
|
|
241
|
+
kind: 'declaration',
|
|
242
|
+
expression: exported,
|
|
243
|
+
module,
|
|
244
|
+
chain: hops
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const reexported = reexportedBinding(module, name);
|
|
248
|
+
if (void 0 !== reexported) return void 0 === reexported.specifier ? resolveBinding(module, reexported.local, 'local', hops, seen) : throughModule(reexported.specifier, reexported.local, true);
|
|
249
|
+
for (const statement of module.file.program.body){
|
|
250
|
+
if (!__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement) || 'type' === statement.exportKind) continue;
|
|
251
|
+
const resolved = throughModule(statement.source.value, name, false);
|
|
252
|
+
if (void 0 !== resolved) return resolved;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
root,
|
|
257
|
+
resolve: (module, name, scope)=>resolveBinding(module, name, scope, [], new Set())
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
export { createModuleGraph, exportedConst, localConst, parseConsumer, propertyName };
|