@bleedingdev/modern-js-code-tools 3.9.0-ultramodern.9 → 3.9.3-ultramodern.10
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 +89 -246
- package/dist/cjs/microvertical-api-boundary.cjs +507 -64
- package/dist/cjs/microvertical-api-owner.cjs +5 -2
- package/dist/cjs/module-graph.cjs +1862 -0
- package/dist/cjs/oxlint-plugin/rules/strict-effect-api-boundaries.cjs +1 -1
- package/dist/cjs/source-analysis.cjs +108 -0
- package/dist/cjs/strict-effect-runtime.cjs +75 -57
- 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 +66 -214
- package/dist/esm/microvertical-api-boundary.js +505 -65
- package/dist/esm/microvertical-api-owner.js +5 -2
- package/dist/esm/module-graph.js +1804 -0
- package/dist/esm/oxlint-plugin/rules/strict-effect-api-boundaries.js +1 -1
- package/dist/esm/source-analysis.js +48 -0
- package/dist/esm/strict-effect-runtime.js +75 -56
- 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 +66 -214
- package/dist/esm-node/microvertical-api-boundary.js +505 -65
- package/dist/esm-node/microvertical-api-owner.js +5 -2
- package/dist/esm-node/module-graph.js +1805 -0
- package/dist/esm-node/oxlint-plugin/rules/strict-effect-api-boundaries.js +1 -1
- package/dist/esm-node/source-analysis.js +49 -0
- package/dist/esm-node/strict-effect-runtime.js +75 -56
- 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-baseline.d.ts +4 -5
- package/dist/types/microvertical-api-boundary.d.ts +17 -2
- package/dist/types/microvertical-api-owner.d.ts +1 -1
- package/dist/types/module-graph.d.ts +100 -0
- package/dist/types/source-analysis.d.ts +13 -0
- package/package.json +17 -13
|
@@ -21,7 +21,7 @@ const createStrictEffectApiBoundariesRule = ()=>({
|
|
|
21
21
|
});
|
|
22
22
|
reportProgramPattern(context, node, source, /@modern-js\/plugin-bff\/(?:server|hono-server)(?![\w-])/u, 'UltraModern API workspaces must not import Hono server helpers; use @modern-js/bff-effect/effect-edge and HttpApi.');
|
|
23
23
|
reportProgramPattern(context, node, source, /\bruntimeFramework\s*(?::|=)\s*['"]hono['"]/u, 'UltraModern API apps must use bff.runtimeFramework: "effect".');
|
|
24
|
-
reportProgramPattern(context, node, source, /(?:from|import)\s*['"][^'"]*(?:api\/effect|shared\/effect)[^'"]*['"]|shared-effect-api/u, 'Import API code from direct api/index.ts, shared/api.ts or src/api/* paths, not api/effect, shared/effect or shared Effect API packages.');
|
|
24
|
+
reportProgramPattern(context, node, source, /(?:from|import)\s*['"][^'"]*(?:api\/effect|shared\/effect)(?=\/|\.[cm]?[jt]sx?['"]|['"])[^'"]*['"]|shared-effect-api/u, 'Import API code from direct api/index.ts, shared/api.ts or src/api/* paths, not api/effect, shared/effect or shared Effect API packages.');
|
|
25
25
|
reportProgramPattern(context, node, source, /\bstrictEffectApproach\s*(?::|=)\s*false\b/u, 'UltraModern API apps must keep strictEffectApproach enabled.');
|
|
26
26
|
if (isApiEntryFile(filename)) {
|
|
27
27
|
const violation = strictEffectRuntimeTopologyViolation(source, createEffectApiImportResolver(filename));
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { parse } from "@babel/parser";
|
|
3
|
+
import traverse, { Hub, NodePath } from "@babel/traverse";
|
|
4
|
+
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
5
|
+
const consumerParserPlugins = (filePath)=>/\.[jt]sx$/u.test(filePath) ? [
|
|
6
|
+
"typescript",
|
|
7
|
+
'decorators',
|
|
8
|
+
'decoratorAutoAccessors',
|
|
9
|
+
'jsx'
|
|
10
|
+
] : [
|
|
11
|
+
"typescript",
|
|
12
|
+
'decorators',
|
|
13
|
+
'decoratorAutoAccessors'
|
|
14
|
+
];
|
|
15
|
+
class SourceSyntaxError extends Error {
|
|
16
|
+
}
|
|
17
|
+
class SourceValidationHub extends Hub {
|
|
18
|
+
buildError(_node, message) {
|
|
19
|
+
return new SourceSyntaxError(message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function parseSource(source, filename, syntaxMessage) {
|
|
23
|
+
try {
|
|
24
|
+
return parse(source, {
|
|
25
|
+
sourceType: 'module',
|
|
26
|
+
sourceFilename: filename,
|
|
27
|
+
plugins: consumerParserPlugins(filename)
|
|
28
|
+
});
|
|
29
|
+
} catch (cause) {
|
|
30
|
+
if ('object' == typeof cause && null !== cause && 'code' in cause && 'BABEL_PARSER_SYNTAX_ERROR' === cause.code && 'reasonCode' in cause && 'string' == typeof cause.reasonCode) throw new SourceSyntaxError(syntaxMessage ?? (cause instanceof Error ? cause.message : String(cause)));
|
|
31
|
+
throw cause;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function traverseSource(file, visitor) {
|
|
35
|
+
const program = NodePath.get({
|
|
36
|
+
hub: new SourceValidationHub(),
|
|
37
|
+
parentPath: void 0,
|
|
38
|
+
parent: file,
|
|
39
|
+
container: file,
|
|
40
|
+
key: 'program'
|
|
41
|
+
});
|
|
42
|
+
program.setContext();
|
|
43
|
+
traverse(file, visitor, program.scope, void 0, program);
|
|
44
|
+
}
|
|
45
|
+
function unwrapExpression(node, includeAssertions = false) {
|
|
46
|
+
while(__rspack_external__babel_types_6e8337dd.isTSAsExpression(node) || __rspack_external__babel_types_6e8337dd.isParenthesizedExpression(node) || __rspack_external__babel_types_6e8337dd.isTSSatisfiesExpression(node) || includeAssertions && (__rspack_external__babel_types_6e8337dd.isTSTypeAssertion(node) || __rspack_external__babel_types_6e8337dd.isTSNonNullExpression(node) || __rspack_external__babel_types_6e8337dd.isTSInstantiationExpression(node)))node = node.expression;
|
|
47
|
+
return node;
|
|
48
|
+
}
|
|
49
|
+
export { SourceSyntaxError, consumerParserPlugins, parseSource, traverseSource, unwrapExpression };
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import node_fs from "node:fs";
|
|
3
3
|
import node_path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import traverse, { Hub, NodePath } from "@babel/traverse";
|
|
4
|
+
import { SourceSyntaxError, parseSource, traverseSource, unwrapExpression } from "./source-analysis.js";
|
|
6
5
|
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
7
6
|
const edge = '@modern-js/bff-effect/effect-edge';
|
|
8
7
|
const nodeRuntime = '@modern-js/bff-effect/effect';
|
|
9
|
-
const
|
|
10
|
-
const failure = 'Generated API entries must export defineEffectBff(...) or the
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
const assembly = '@modern-js/bff-effect/assembly';
|
|
9
|
+
const failure = 'Generated API entries must export defineEffectBff(...) or the native Effect BFF assembly helper with an explicitly composed handler Layer and an unshadowed executable root; entries must implement handlers through HttpApiBuilder.group.';
|
|
10
|
+
const MAX_API_SOURCE_MODULES = 256;
|
|
11
|
+
const httpRouteMethods = new Set([
|
|
12
|
+
'*',
|
|
13
|
+
'GET',
|
|
14
|
+
'POST',
|
|
15
|
+
'PUT',
|
|
16
|
+
'PATCH',
|
|
17
|
+
'DELETE',
|
|
18
|
+
'HEAD',
|
|
19
|
+
'OPTIONS',
|
|
20
|
+
'TRACE',
|
|
21
|
+
'CONNECT',
|
|
22
|
+
'QUERY'
|
|
23
|
+
]);
|
|
22
24
|
function createEffectApiImportResolver(filename) {
|
|
23
25
|
const directory = node_fs.realpathSync(node_path.dirname(filename));
|
|
24
26
|
const owner = node_fs.realpathSync(node_path.resolve(directory, '..'));
|
|
@@ -50,34 +52,14 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
50
52
|
const load = (input)=>{
|
|
51
53
|
const existing = modules.get(input.id);
|
|
52
54
|
if (existing) return existing;
|
|
53
|
-
if (modules.size >=
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
file = parse(input.source, {
|
|
57
|
-
sourceType: 'module',
|
|
58
|
-
sourceFilename: input.id,
|
|
59
|
-
plugins: [
|
|
60
|
-
"typescript"
|
|
61
|
-
]
|
|
62
|
-
});
|
|
63
|
-
} catch (cause) {
|
|
64
|
-
if ('object' == typeof cause && null !== cause && 'code' in cause && 'BABEL_PARSER_SYNTAX_ERROR' === cause.code && 'reasonCode' in cause && 'string' == typeof cause.reasonCode) throw invalidSource('Invalid API syntax');
|
|
65
|
-
throw cause;
|
|
66
|
-
}
|
|
55
|
+
if (modules.size >= MAX_API_SOURCE_MODULES || input.source.length > 1000000) throw new SourceSyntaxError('API source budget exceeded');
|
|
56
|
+
const file = parseSource(input.source, input.id, 'Invalid API syntax');
|
|
67
57
|
const module = {
|
|
68
58
|
...input,
|
|
69
59
|
file
|
|
70
60
|
};
|
|
71
61
|
modules.set(input.id, module);
|
|
72
|
-
|
|
73
|
-
hub: new SourceValidationHub(),
|
|
74
|
-
parentPath: void 0,
|
|
75
|
-
parent: file,
|
|
76
|
-
container: file,
|
|
77
|
-
key: 'program'
|
|
78
|
-
});
|
|
79
|
-
program.setContext();
|
|
80
|
-
traverse(file, {
|
|
62
|
+
traverseSource(file, {
|
|
81
63
|
enter (nodePath) {
|
|
82
64
|
paths.set(nodePath.node, nodePath);
|
|
83
65
|
owners.set(nodePath.node, module);
|
|
@@ -86,9 +68,12 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
86
68
|
})) {
|
|
87
69
|
let target = nodePath.isAssignmentExpression() ? nodePath.node.left : nodePath.node.argument;
|
|
88
70
|
while(__rspack_external__babel_types_6e8337dd.isMemberExpression(target))target = target.object;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
71
|
+
let binding = __rspack_external__babel_types_6e8337dd.isIdentifier(target) ? nodePath.scope.getBinding(target.name) : void 0;
|
|
72
|
+
while(binding && !mutated.has(binding)){
|
|
73
|
+
mutated.add(binding);
|
|
74
|
+
const declarator = binding.path.node;
|
|
75
|
+
const alias = 'const' === binding.kind && __rspack_external__babel_types_6e8337dd.isVariableDeclarator(declarator) && declarator.init ? unwrapExpression(declarator.init, true) : void 0;
|
|
76
|
+
binding = __rspack_external__babel_types_6e8337dd.isIdentifier(alias) ? binding.path.scope.getBinding(alias.name) : void 0;
|
|
92
77
|
}
|
|
93
78
|
}
|
|
94
79
|
if (nodePath.isTSModuleDeclaration() && __rspack_external__babel_types_6e8337dd.isIdentifier(nodePath.node.id)) {
|
|
@@ -96,7 +81,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
96
81
|
if (binding) mutated.add(binding);
|
|
97
82
|
}
|
|
98
83
|
}
|
|
99
|
-
}
|
|
84
|
+
});
|
|
100
85
|
return module;
|
|
101
86
|
};
|
|
102
87
|
try {
|
|
@@ -105,10 +90,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
105
90
|
source,
|
|
106
91
|
resolveImport
|
|
107
92
|
});
|
|
108
|
-
const unwrap = (node)=>
|
|
109
|
-
while(__rspack_external__babel_types_6e8337dd.isParenthesizedExpression(node) || __rspack_external__babel_types_6e8337dd.isTSSatisfiesExpression(node) || __rspack_external__babel_types_6e8337dd.isTSAsExpression(node) || __rspack_external__babel_types_6e8337dd.isTSTypeAssertion(node) || __rspack_external__babel_types_6e8337dd.isTSNonNullExpression(node))node = node.expression;
|
|
110
|
-
return node;
|
|
111
|
-
};
|
|
93
|
+
const unwrap = (node)=>unwrapExpression(node, true);
|
|
112
94
|
const binding = (node)=>{
|
|
113
95
|
node = unwrap(node);
|
|
114
96
|
if (!__rspack_external__babel_types_6e8337dd.isIdentifier(node)) return;
|
|
@@ -133,15 +115,15 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
133
115
|
const namespaceSources = {
|
|
134
116
|
HttpApi: [
|
|
135
117
|
edge,
|
|
136
|
-
'effect/
|
|
118
|
+
'effect/http-api'
|
|
137
119
|
],
|
|
138
120
|
HttpApiBuilder: [
|
|
139
121
|
edge,
|
|
140
|
-
'effect/
|
|
122
|
+
'effect/http-api'
|
|
141
123
|
],
|
|
142
124
|
HttpRouter: [
|
|
143
125
|
edge,
|
|
144
|
-
'effect/
|
|
126
|
+
'effect/http'
|
|
145
127
|
]
|
|
146
128
|
};
|
|
147
129
|
const native = (node, name, sources)=>{
|
|
@@ -237,7 +219,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
237
219
|
if (!__rspack_external__babel_types_6e8337dd.isBlockStatement(body)) return body;
|
|
238
220
|
const statements = body.body;
|
|
239
221
|
const last = statements.at(-1);
|
|
240
|
-
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;
|
|
222
|
+
return statements.slice(0, -1).every((statement)=>__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement) || __rspack_external__babel_types_6e8337dd.isTSTypeAliasDeclaration(statement) || __rspack_external__babel_types_6e8337dd.isTSInterfaceDeclaration(statement)) && __rspack_external__babel_types_6e8337dd.isReturnStatement(last) ? last.argument : void 0;
|
|
241
223
|
};
|
|
242
224
|
const pipe = (node, check)=>__rspack_external__babel_types_6e8337dd.isMemberExpression(node.callee) && !node.callee.computed && __rspack_external__babel_types_6e8337dd.isIdentifier(node.callee.property, {
|
|
243
225
|
name: 'pipe'
|
|
@@ -266,11 +248,48 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
266
248
|
if (method(node.callee, 'Layer', 'mergeAll', layerSources)) return node.arguments.length > 0 && node.arguments.every((argument)=>handlers(argument, api));
|
|
267
249
|
return method(node.callee, 'HttpApiBuilder', 'group') && 3 === node.arguments.length && apiIdentity(node.arguments[0]) === api && handled(node.arguments[2]);
|
|
268
250
|
});
|
|
269
|
-
const
|
|
251
|
+
const effectSources = [
|
|
252
|
+
edge,
|
|
253
|
+
'effect',
|
|
254
|
+
'effect/Effect'
|
|
255
|
+
];
|
|
256
|
+
const routeHandler = (node)=>{
|
|
257
|
+
node = unwrap(node);
|
|
258
|
+
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node) || !method(node.callee, 'HttpRouter', 'add') || 3 !== node.arguments.length) return;
|
|
259
|
+
const [verb, route, handler] = node.arguments;
|
|
260
|
+
return __rspack_external__babel_types_6e8337dd.isStringLiteral(verb) && httpRouteMethods.has(verb.value) && __rspack_external__babel_types_6e8337dd.isStringLiteral(route) && (route.value.startsWith('/') || '*' === route.value) && (__rspack_external__babel_types_6e8337dd.isArrowFunctionExpression(handler) || __rspack_external__babel_types_6e8337dd.isFunctionExpression(handler)) ? handler : void 0;
|
|
261
|
+
};
|
|
262
|
+
const effectReference = (node)=>guarded(node, (node)=>{
|
|
263
|
+
const init = initialized(node);
|
|
264
|
+
if (init) return __rspack_external__babel_types_6e8337dd.isIdentifier(unwrap(init)) && effectReference(init);
|
|
265
|
+
const value = imported(node);
|
|
266
|
+
return !!value && !value.namespace;
|
|
267
|
+
});
|
|
268
|
+
const capturedRouteFactory = (node)=>guarded(node, (node)=>{
|
|
269
|
+
const init = initialized(node) ?? externalValue(node);
|
|
270
|
+
if (init) return capturedRouteFactory(init);
|
|
271
|
+
if (!__rspack_external__babel_types_6e8337dd.isArrowFunctionExpression(node) || node.async || 1 !== node.params.length || !__rspack_external__babel_types_6e8337dd.isIdentifier(node.params[0]) || __rspack_external__babel_types_6e8337dd.isBlockStatement(node.body)) return false;
|
|
272
|
+
const handler = routeHandler(node.body);
|
|
273
|
+
const parameter = binding(node.params[0]);
|
|
274
|
+
return !!handler && !!parameter && parameter.referencePaths.some((reference)=>!!reference.findParent((path)=>path.node === handler));
|
|
275
|
+
});
|
|
276
|
+
const capturedTransport = (node)=>guarded(node, (node)=>{
|
|
270
277
|
const init = initialized(node);
|
|
271
|
-
if (init) return
|
|
278
|
+
if (init) return capturedTransport(init);
|
|
279
|
+
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node)) return false;
|
|
280
|
+
if (method(node.callee, 'Effect', 'map', effectSources)) return 2 === node.arguments.length && effectReference(node.arguments[0]) && capturedRouteFactory(node.arguments[1]);
|
|
281
|
+
if (!__rspack_external__babel_types_6e8337dd.isMemberExpression(node.callee) || node.callee.computed || !__rspack_external__babel_types_6e8337dd.isIdentifier(node.callee.property, {
|
|
282
|
+
name: 'pipe'
|
|
283
|
+
}) || 1 !== node.arguments.length || !effectReference(node.callee.object)) return false;
|
|
284
|
+
const map = unwrap(node.arguments[0]);
|
|
285
|
+
return __rspack_external__babel_types_6e8337dd.isCallExpression(map) && method(map.callee, 'Effect', 'map', effectSources) && 1 === map.arguments.length && capturedRouteFactory(map.arguments[0]);
|
|
286
|
+
});
|
|
287
|
+
const transport = (node, provided = false)=>guarded(node, (node)=>{
|
|
288
|
+
const init = initialized(node) ?? externalValue(node);
|
|
289
|
+
if (init) return transport(init, provided);
|
|
272
290
|
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node)) return method(node, 'Layer', 'empty', layerSources);
|
|
273
|
-
|
|
291
|
+
const providedHere = provided || node.arguments.some((argument)=>__rspack_external__babel_types_6e8337dd.isCallExpression(argument) && method(argument.callee, 'Layer', 'provide', layerSources) && 1 === argument.arguments.length);
|
|
292
|
+
return pipe(node, (value)=>transport(value, providedHere)) || void 0 !== routeHandler(node) || provided && method(node.callee, 'Layer', 'unwrap', layerSources) && 1 === node.arguments.length && capturedTransport(node.arguments[0]) || method(node.callee, 'HttpRouter', 'cors') || method(node.callee, 'Layer', 'mergeAll', layerSources) && node.arguments.length > 0 && node.arguments.every((argument)=>transport(argument, provided));
|
|
274
293
|
});
|
|
275
294
|
const directLayer = (node, api)=>guarded(node, (node)=>{
|
|
276
295
|
const init = initialized(node);
|
|
@@ -302,7 +321,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
302
321
|
const groupValue = externalValue(group);
|
|
303
322
|
const groupInit = groupValue && initialized(groupValue);
|
|
304
323
|
if (!groupInit || !__rspack_external__babel_types_6e8337dd.isCallExpression(groupInit) || !method(groupInit.callee, 'RpcGroup', 'make', [
|
|
305
|
-
'effect/
|
|
324
|
+
'effect/rpc',
|
|
306
325
|
'@modern-js/bff-effect/effect-client'
|
|
307
326
|
])) return false;
|
|
308
327
|
const layerInit = initialized(layer) ?? unwrap(layer);
|
|
@@ -326,7 +345,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
326
345
|
if (value?.name === 'defineEffectBff' && [
|
|
327
346
|
edge,
|
|
328
347
|
nodeRuntime
|
|
329
|
-
].includes(value.specifier) || value?.name === 'assembleEffectBffRuntime' &&
|
|
348
|
+
].includes(value.specifier) || value?.name === 'assembleEffectBffRuntime' && value.specifier === assembly) {
|
|
330
349
|
if (1 !== node.arguments.length) return false;
|
|
331
350
|
const values = properties(node.arguments[0]);
|
|
332
351
|
if (!values) return false;
|
|
@@ -349,7 +368,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
349
368
|
const root = entry.file.program.body.find((statement)=>__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement));
|
|
350
369
|
return root && runtime(root.declaration) ? void 0 : failure;
|
|
351
370
|
} catch (cause) {
|
|
352
|
-
if (
|
|
371
|
+
if (cause instanceof SourceSyntaxError) return failure;
|
|
353
372
|
const message = 'object' == typeof cause && null !== cause && 'message' in cause && 'string' == typeof cause.message ? cause.message : String(cause);
|
|
354
373
|
throw new Error(`Strict Effect API analysis failed: ${message}`, {
|
|
355
374
|
cause
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function runMicroVerticalApiCheckCli(args?: string[], filesOnly?: boolean): number
|
|
1
|
+
export declare function runMicroVerticalApiCheckCli(args?: string[], filesOnly?: boolean): Promise<number>;
|
|
@@ -17,4 +17,4 @@ export type WorkspaceSourceCheckOptions = {
|
|
|
17
17
|
readonly pluralCategories?: Readonly<Record<string, readonly string[]>>;
|
|
18
18
|
};
|
|
19
19
|
export declare const runWorkspaceSourceCheck: ({ cwd, sourceRoots, locales, pluralCategories, }?: WorkspaceSourceCheckOptions) => number;
|
|
20
|
-
export declare const
|
|
20
|
+
export declare const runWorkspaceSourceCheckCli: (args?: readonly string[]) => number;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export declare const consumerParserPlugins: (filePath: string) => ('typescript' | 'jsx')[];
|
|
1
|
+
/** `order-items` -> `orderItems`: the stem as contract export names spell it. */
|
|
2
|
+
export declare const camelCaseStem: (stem: string) => string;
|
|
3
|
+
/** `order-items` -> `OrderItems`. */
|
|
4
|
+
export declare const pascalCaseStem: (stem: string) => string;
|
|
6
5
|
export interface MicroVerticalApiBaselineExpectation {
|
|
7
6
|
/** Optional override for operation IDs outside reachable endpoint naming conventions. */
|
|
8
7
|
readonly operationPaths?: Readonly<Record<string, string>>;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { type ModuleGraph, type SourceModule } from './module-graph.js';
|
|
1
2
|
export type { MicroVerticalApiBaselineExpectation, MicroVerticalTopologyEntry, } from './microvertical-api-baseline.js';
|
|
2
3
|
export { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation, } from './microvertical-api-baseline.js';
|
|
4
|
+
export type { GraphValue, ModuleGraph, ModuleGraphHop, ResolvedBinding, SourceModule, } from './module-graph.js';
|
|
5
|
+
export { createModuleGraph } from './module-graph.js';
|
|
3
6
|
export interface MicroVerticalConfiguredApp {
|
|
4
7
|
readonly path: string;
|
|
5
8
|
readonly id?: string;
|
|
6
|
-
|
|
9
|
+
/** `shell` or `vertical`; selects the rules the app is checked against. */
|
|
10
|
+
readonly kind: string;
|
|
7
11
|
readonly package?: string;
|
|
8
12
|
readonly surfaceProfile?: string;
|
|
9
13
|
readonly deliveryUnitKind?: string;
|
|
@@ -15,11 +19,22 @@ export interface MicroVerticalConfiguredApp {
|
|
|
15
19
|
readonly additionalPaths?: Readonly<Record<string, string>>;
|
|
16
20
|
};
|
|
17
21
|
}
|
|
22
|
+
export interface MicroVerticalApiSourceRuleContext {
|
|
23
|
+
/** Workspace-relative path of `module`. */
|
|
24
|
+
readonly file: string;
|
|
25
|
+
readonly module: SourceModule;
|
|
26
|
+
/** One graph per source file, shared by every rule for that file. */
|
|
27
|
+
readonly graph: ModuleGraph;
|
|
28
|
+
}
|
|
29
|
+
/** A consumer-owned source rule; each returned message is a violation. */
|
|
30
|
+
export type MicroVerticalApiSourceRule = (context: MicroVerticalApiSourceRuleContext) => readonly string[];
|
|
18
31
|
export interface MicroVerticalApiCheckOptions {
|
|
19
32
|
readonly workspaceRoot: string;
|
|
20
33
|
readonly configuredApps?: readonly MicroVerticalConfiguredApp[];
|
|
21
|
-
/**
|
|
34
|
+
/** Exact installed owner constraint; otherwise resolve each contract’s native owner. */
|
|
22
35
|
readonly baselinePackageDirectory?: string;
|
|
36
|
+
/** Rules run once per workspace TypeScript/JavaScript source file. */
|
|
37
|
+
readonly sourceRules?: readonly MicroVerticalApiSourceRule[];
|
|
23
38
|
}
|
|
24
39
|
export interface MicroVerticalApiCheckResult {
|
|
25
40
|
readonly diagnostics: readonly string[];
|
|
@@ -3,4 +3,4 @@ export declare const baselinePackage = "@modern-js/bff-effect/microvertical-api"
|
|
|
3
3
|
/** Locate the installed package from this consumer's resolver, never this tool's installation. */
|
|
4
4
|
export declare function resolveBaselinePackageDirectory(fromFile: string): string;
|
|
5
5
|
/** Follow only public bindings and local barrels. Never examine schema implementation expressions. */
|
|
6
|
-
export declare function baselinePublicIdentityIsExact(fromFile: string, expectation: MicroVerticalApiBaselineExpectation): boolean;
|
|
6
|
+
export declare function baselinePublicIdentityIsExact(fromFile: string, expectation: MicroVerticalApiBaselineExpectation, reportMismatch?: (message: string) => void): boolean;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
/** A parsed workspace module and the package directory it may traverse privately. */
|
|
3
|
+
export interface SourceModule {
|
|
4
|
+
readonly file: t.File;
|
|
5
|
+
/** Real absolute path. */
|
|
6
|
+
readonly path: string;
|
|
7
|
+
/** Real package directory; relative imports may not leave it. */
|
|
8
|
+
readonly boundary: string;
|
|
9
|
+
/**
|
|
10
|
+
* Every assignment, update and namespace merge in the module. Validation
|
|
11
|
+
* that reads initializers directly must reject a module that has any.
|
|
12
|
+
*/
|
|
13
|
+
readonly mutations: readonly t.Node[];
|
|
14
|
+
}
|
|
15
|
+
/** One `name` looked up in the module at `path`, in traversal order. */
|
|
16
|
+
export interface ModuleGraphHop {
|
|
17
|
+
readonly path: string;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Where a binding ends up. `declaration` is a workspace `const` initializer;
|
|
22
|
+
* `external` is a named import or re-export from a package outside the
|
|
23
|
+
* workspace source graph. `chain` starts at the requesting module and ends at
|
|
24
|
+
* the declaring module (or the last workspace module before the package).
|
|
25
|
+
*/
|
|
26
|
+
export type ResolvedBinding = {
|
|
27
|
+
readonly kind: 'declaration';
|
|
28
|
+
readonly expression: t.Expression;
|
|
29
|
+
readonly module: SourceModule;
|
|
30
|
+
readonly chain: readonly ModuleGraphHop[];
|
|
31
|
+
} | {
|
|
32
|
+
readonly kind: 'external';
|
|
33
|
+
readonly specifier: string;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly chain: readonly ModuleGraphHop[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* One value an expression can evaluate to.
|
|
39
|
+
* - `external`: `name` imported from package `specifier`, then `members`
|
|
40
|
+
* accessed on it (`Schema.Unknown` is `effect` / `Schema` / `['Unknown']`;
|
|
41
|
+
* a namespace import of the package itself is `name: '*'`).
|
|
42
|
+
* - `node`: a workspace expression, function or class the source spells out
|
|
43
|
+
* (a call to an external factory, a literal, a function).
|
|
44
|
+
* - `unresolved`: a parameter, global, dynamic access or anything beyond the
|
|
45
|
+
* analysis budget. Rules that must be sound treat it as a violation.
|
|
46
|
+
*/
|
|
47
|
+
export type GraphValue = {
|
|
48
|
+
readonly kind: 'external';
|
|
49
|
+
readonly specifier: string;
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly members: readonly string[];
|
|
52
|
+
} | {
|
|
53
|
+
readonly kind: 'node' | 'unresolved';
|
|
54
|
+
readonly node: t.Node;
|
|
55
|
+
readonly module: SourceModule;
|
|
56
|
+
};
|
|
57
|
+
export interface ModuleGraph {
|
|
58
|
+
/**
|
|
59
|
+
* The parsed workspace module at `filePath`, parsed once per graph. Throws
|
|
60
|
+
* `SourceSyntaxError` when the file is not readable consumer source.
|
|
61
|
+
*/
|
|
62
|
+
module(filePath: string): SourceModule;
|
|
63
|
+
/**
|
|
64
|
+
* Follow a local binding (`scope: 'local'`) or an export (`'export'`) of
|
|
65
|
+
* `module` through relative imports, re-exports and public workspace
|
|
66
|
+
* package exports to an unmutated `const`. Unresolvable, mutated, cyclic or
|
|
67
|
+
* over-budget bindings return undefined so callers fail closed.
|
|
68
|
+
*/
|
|
69
|
+
resolve(module: SourceModule, name: string, scope: 'export' | 'local'): ResolvedBinding | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* `resolve` for one lexical occurrence: an identifier `module` references
|
|
72
|
+
* or declares, or `namespace.member` on a namespace import. The binding is
|
|
73
|
+
* the one in scope at that node, not the module-scope name.
|
|
74
|
+
*/
|
|
75
|
+
resolveReference(module: SourceModule, reference: t.Node): ResolvedBinding | undefined;
|
|
76
|
+
/** True only for an identifier `module` references that has no binding (a global). */
|
|
77
|
+
isUnboundReference(module: SourceModule, reference: t.Node): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Every value `expression` (a node of `module`) can evaluate to, following
|
|
80
|
+
* any binding kind, destructuring, member access into object literals,
|
|
81
|
+
* member assignments (`obj.key = value`), reassignments, conditional
|
|
82
|
+
* branches, and calls into workspace functions through their returns.
|
|
83
|
+
*/
|
|
84
|
+
evaluate(module: SourceModule, expression: t.Node): readonly GraphValue[];
|
|
85
|
+
/**
|
|
86
|
+
* Every `external` and `unresolved` value reachable from `expression`: each
|
|
87
|
+
* reference inside it is evaluated, and every workspace node it evaluates
|
|
88
|
+
* to is walked in turn, across modules. `Schema.Struct({ a: shared })`
|
|
89
|
+
* reaches `Schema.Struct` plus whatever `shared` is built from.
|
|
90
|
+
*/
|
|
91
|
+
reachable(module: SourceModule, expression: t.Node): readonly GraphValue[];
|
|
92
|
+
}
|
|
93
|
+
export declare const propertyName: (node: t.Node | undefined) => string | undefined;
|
|
94
|
+
export declare const localConst: (sourceFile: t.File, name: string) => t.VariableDeclarator | undefined;
|
|
95
|
+
export declare const exportedConst: (sourceFile: t.File, name: string) => t.VariableDeclarator | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* The workspace module graph. Each module is parsed once per graph; an
|
|
98
|
+
* unreadable module stays unresolved instead of assumed valid.
|
|
99
|
+
*/
|
|
100
|
+
export declare function createModuleGraph(): ModuleGraph;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Visitor } from '@babel/traverse';
|
|
2
|
+
import * as t from '@babel/types';
|
|
3
|
+
/**
|
|
4
|
+
* Only JSX extensions enable JSX: generic arrows in .ts must remain valid.
|
|
5
|
+
* Decorators are standard TypeScript syntax.
|
|
6
|
+
*/
|
|
7
|
+
export declare const consumerParserPlugins: (filePath: string) => ('typescript' | 'jsx' | 'decorators' | 'decoratorAutoAccessors')[];
|
|
8
|
+
/** Only parser/binder diagnostics are policy violations; tool failures escape. */
|
|
9
|
+
export declare class SourceSyntaxError extends Error {
|
|
10
|
+
}
|
|
11
|
+
export declare function parseSource(source: string, filename: string, syntaxMessage?: string): t.File;
|
|
12
|
+
export declare function traverseSource(file: t.File, visitor: Visitor): void;
|
|
13
|
+
export declare function unwrapExpression(node: t.Node, includeAssertions?: boolean): t.Node;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bleedingdev/modern-js-code-tools",
|
|
3
3
|
"description": "Code analysis tooling for Modern.js projects.",
|
|
4
|
-
"homepage": "https://github.com/
|
|
4
|
+
"homepage": "https://github.com/ultramodernjs/ultramodern.js#readme",
|
|
5
5
|
"bugs": {
|
|
6
|
-
"url": "https://github.com/
|
|
6
|
+
"url": "https://github.com/ultramodernjs/ultramodern.js/issues"
|
|
7
7
|
},
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/ultramodernjs/ultramodern.js.git",
|
|
11
11
|
"directory": "packages/toolkit/code-tools"
|
|
12
12
|
},
|
|
13
13
|
"license": "MIT",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=20"
|
|
25
25
|
},
|
|
26
|
-
"version": "3.9.
|
|
26
|
+
"version": "3.9.3-ultramodern.10",
|
|
27
27
|
"types": "./dist/types/index.d.ts",
|
|
28
28
|
"main": "./dist/esm-node/index.js",
|
|
29
29
|
"typesVersions": {
|
|
@@ -72,16 +72,16 @@
|
|
|
72
72
|
"bin"
|
|
73
73
|
],
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@babel/parser": "^8.0.
|
|
76
|
-
"@babel/traverse": "^8.0.
|
|
77
|
-
"@babel/types": "^8.0.
|
|
78
|
-
"oxlint": "1.
|
|
75
|
+
"@babel/parser": "^8.0.7",
|
|
76
|
+
"@babel/traverse": "^8.0.7",
|
|
77
|
+
"@babel/types": "^8.0.6",
|
|
78
|
+
"oxlint": "1.88.0",
|
|
79
|
+
"resolve.exports": "2.0.4"
|
|
79
80
|
},
|
|
80
81
|
"devDependencies": {
|
|
81
|
-
"@rslib/core": "1.0.
|
|
82
|
+
"@rslib/core": "1.0.3",
|
|
82
83
|
"@scripts/rstest-config": "2.66.0",
|
|
83
|
-
"@types/node": "^26.
|
|
84
|
-
"@typescript/native-preview": "7.0.0-dev.20260707.2"
|
|
84
|
+
"@types/node": "^26.6.5"
|
|
85
85
|
},
|
|
86
86
|
"sideEffects": false,
|
|
87
87
|
"publishConfig": {
|
|
@@ -90,11 +90,15 @@
|
|
|
90
90
|
"modern:source": "./src/index.ts",
|
|
91
91
|
"bin": {
|
|
92
92
|
"modern-api-check": "bin/modern-api-check.mjs",
|
|
93
|
-
"modern-api-check-files": "bin/modern-api-check-files.mjs"
|
|
93
|
+
"modern-api-check-files": "bin/modern-api-check-files.mjs",
|
|
94
|
+
"modern-i18n-check": "bin/modern-i18n-check.mjs"
|
|
94
95
|
},
|
|
95
96
|
"scripts": {
|
|
96
97
|
"build": "rm -rf dist && rslib build -c rslib.config.mts",
|
|
97
98
|
"dev": "rslib build -c rslib.config.mts -w",
|
|
98
|
-
"test": "rstest
|
|
99
|
+
"test": "rstest"
|
|
100
|
+
},
|
|
101
|
+
"ultramodern": {
|
|
102
|
+
"sourceName": "@modern-js/code-tools"
|
|
99
103
|
}
|
|
100
104
|
}
|