@bleedingdev/modern-js-code-tools 3.9.3-ultramodern.1 → 3.9.3-ultramodern.11
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/dist/cjs/microvertical-api-baseline.cjs +8 -2
- package/dist/cjs/microvertical-api-boundary.cjs +397 -32
- package/dist/cjs/module-graph.cjs +44 -2
- package/dist/cjs/strict-effect-runtime.cjs +59 -6
- package/dist/esm/microvertical-api-baseline.js +1 -1
- package/dist/esm/microvertical-api-boundary.js +399 -34
- package/dist/esm/module-graph.js +44 -2
- package/dist/esm/strict-effect-runtime.js +59 -6
- package/dist/esm-node/microvertical-api-baseline.js +1 -1
- package/dist/esm-node/microvertical-api-boundary.js +399 -34
- package/dist/esm-node/module-graph.js +44 -2
- package/dist/esm-node/strict-effect-runtime.js +59 -6
- package/dist/types/microvertical-api-baseline.d.ts +4 -0
- package/dist/types/microvertical-api-boundary.d.ts +3 -2
- package/dist/types/module-graph.d.ts +8 -0
- package/package.json +6 -6
|
@@ -51,6 +51,19 @@ const nodeRuntime = '@modern-js/bff-effect/effect';
|
|
|
51
51
|
const assembly = '@modern-js/bff-effect/assembly';
|
|
52
52
|
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.';
|
|
53
53
|
const MAX_API_SOURCE_MODULES = 256;
|
|
54
|
+
const httpRouteMethods = new Set([
|
|
55
|
+
'*',
|
|
56
|
+
'GET',
|
|
57
|
+
'POST',
|
|
58
|
+
'PUT',
|
|
59
|
+
'PATCH',
|
|
60
|
+
'DELETE',
|
|
61
|
+
'HEAD',
|
|
62
|
+
'OPTIONS',
|
|
63
|
+
'TRACE',
|
|
64
|
+
'CONNECT',
|
|
65
|
+
'QUERY'
|
|
66
|
+
]);
|
|
54
67
|
function createEffectApiImportResolver(filename) {
|
|
55
68
|
const directory = external_node_fs_default().realpathSync(external_node_path_default().dirname(filename));
|
|
56
69
|
const owner = external_node_fs_default().realpathSync(external_node_path_default().resolve(directory, '..'));
|
|
@@ -98,9 +111,12 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
98
111
|
})) {
|
|
99
112
|
let target = nodePath.isAssignmentExpression() ? nodePath.node.left : nodePath.node.argument;
|
|
100
113
|
while(types_namespaceObject.isMemberExpression(target))target = target.object;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
let binding = types_namespaceObject.isIdentifier(target) ? nodePath.scope.getBinding(target.name) : void 0;
|
|
115
|
+
while(binding && !mutated.has(binding)){
|
|
116
|
+
mutated.add(binding);
|
|
117
|
+
const declarator = binding.path.node;
|
|
118
|
+
const alias = 'const' === binding.kind && types_namespaceObject.isVariableDeclarator(declarator) && declarator.init ? (0, external_source_analysis_cjs_namespaceObject.unwrapExpression)(declarator.init, true) : void 0;
|
|
119
|
+
binding = types_namespaceObject.isIdentifier(alias) ? binding.path.scope.getBinding(alias.name) : void 0;
|
|
104
120
|
}
|
|
105
121
|
}
|
|
106
122
|
if (nodePath.isTSModuleDeclaration() && types_namespaceObject.isIdentifier(nodePath.node.id)) {
|
|
@@ -275,11 +291,48 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
275
291
|
if (method(node.callee, 'Layer', 'mergeAll', layerSources)) return node.arguments.length > 0 && node.arguments.every((argument)=>handlers(argument, api));
|
|
276
292
|
return method(node.callee, 'HttpApiBuilder', 'group') && 3 === node.arguments.length && apiIdentity(node.arguments[0]) === api && handled(node.arguments[2]);
|
|
277
293
|
});
|
|
278
|
-
const
|
|
294
|
+
const effectSources = [
|
|
295
|
+
edge,
|
|
296
|
+
'effect',
|
|
297
|
+
'effect/Effect'
|
|
298
|
+
];
|
|
299
|
+
const routeHandler = (node)=>{
|
|
300
|
+
node = unwrap(node);
|
|
301
|
+
if (!types_namespaceObject.isCallExpression(node) || !method(node.callee, 'HttpRouter', 'add') || 3 !== node.arguments.length) return;
|
|
302
|
+
const [verb, route, handler] = node.arguments;
|
|
303
|
+
return types_namespaceObject.isStringLiteral(verb) && httpRouteMethods.has(verb.value) && types_namespaceObject.isStringLiteral(route) && (route.value.startsWith('/') || '*' === route.value) && (types_namespaceObject.isArrowFunctionExpression(handler) || types_namespaceObject.isFunctionExpression(handler)) ? handler : void 0;
|
|
304
|
+
};
|
|
305
|
+
const effectReference = (node)=>guarded(node, (node)=>{
|
|
279
306
|
const init = initialized(node);
|
|
280
|
-
if (init) return
|
|
307
|
+
if (init) return types_namespaceObject.isIdentifier(unwrap(init)) && effectReference(init);
|
|
308
|
+
const value = imported(node);
|
|
309
|
+
return !!value && !value.namespace;
|
|
310
|
+
});
|
|
311
|
+
const capturedRouteFactory = (node)=>guarded(node, (node)=>{
|
|
312
|
+
const init = initialized(node) ?? externalValue(node);
|
|
313
|
+
if (init) return capturedRouteFactory(init);
|
|
314
|
+
if (!types_namespaceObject.isArrowFunctionExpression(node) || node.async || 1 !== node.params.length || !types_namespaceObject.isIdentifier(node.params[0]) || types_namespaceObject.isBlockStatement(node.body)) return false;
|
|
315
|
+
const handler = routeHandler(node.body);
|
|
316
|
+
const parameter = binding(node.params[0]);
|
|
317
|
+
return !!handler && !!parameter && parameter.referencePaths.some((reference)=>!!reference.findParent((path)=>path.node === handler));
|
|
318
|
+
});
|
|
319
|
+
const capturedTransport = (node)=>guarded(node, (node)=>{
|
|
320
|
+
const init = initialized(node);
|
|
321
|
+
if (init) return capturedTransport(init);
|
|
322
|
+
if (!types_namespaceObject.isCallExpression(node)) return false;
|
|
323
|
+
if (method(node.callee, 'Effect', 'map', effectSources)) return 2 === node.arguments.length && effectReference(node.arguments[0]) && capturedRouteFactory(node.arguments[1]);
|
|
324
|
+
if (!types_namespaceObject.isMemberExpression(node.callee) || node.callee.computed || !types_namespaceObject.isIdentifier(node.callee.property, {
|
|
325
|
+
name: 'pipe'
|
|
326
|
+
}) || 1 !== node.arguments.length || !effectReference(node.callee.object)) return false;
|
|
327
|
+
const map = unwrap(node.arguments[0]);
|
|
328
|
+
return types_namespaceObject.isCallExpression(map) && method(map.callee, 'Effect', 'map', effectSources) && 1 === map.arguments.length && capturedRouteFactory(map.arguments[0]);
|
|
329
|
+
});
|
|
330
|
+
const transport = (node, provided = false)=>guarded(node, (node)=>{
|
|
331
|
+
const init = initialized(node) ?? externalValue(node);
|
|
332
|
+
if (init) return transport(init, provided);
|
|
281
333
|
if (!types_namespaceObject.isCallExpression(node)) return method(node, 'Layer', 'empty', layerSources);
|
|
282
|
-
|
|
334
|
+
const providedHere = provided || node.arguments.some((argument)=>types_namespaceObject.isCallExpression(argument) && method(argument.callee, 'Layer', 'provide', layerSources) && 1 === argument.arguments.length);
|
|
335
|
+
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));
|
|
283
336
|
});
|
|
284
337
|
const directLayer = (node, api)=>guarded(node, (node)=>{
|
|
285
338
|
const init = initialized(node);
|
|
@@ -470,4 +470,4 @@ const microVerticalApiBaselineViolation = (stem, filePath, expectation)=>{
|
|
|
470
470
|
throw error;
|
|
471
471
|
}
|
|
472
472
|
};
|
|
473
|
-
export { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation };
|
|
473
|
+
export { camelCaseStem, configuredMicroVerticalApiStem, microVerticalApiBaselineViolation, pascalCaseStem };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import node_fs from "node:fs";
|
|
2
2
|
import node_path from "node:path";
|
|
3
3
|
import { parse } from "@babel/parser";
|
|
4
|
-
import { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation } from "./microvertical-api-baseline.js";
|
|
4
|
+
import { camelCaseStem, configuredMicroVerticalApiStem, microVerticalApiBaselineViolation, pascalCaseStem } from "./microvertical-api-baseline.js";
|
|
5
5
|
import { baselinePackage, resolveBaselinePackageDirectory } from "./microvertical-api-owner.js";
|
|
6
6
|
import { createModuleGraph } from "./module-graph.js";
|
|
7
|
-
import { SourceSyntaxError, consumerParserPlugins } from "./source-analysis.js";
|
|
7
|
+
import { SourceSyntaxError, consumerParserPlugins, unwrapExpression } from "./source-analysis.js";
|
|
8
8
|
import { createEffectApiImportResolver, strictEffectRuntimeTopologyViolation } from "./strict-effect-runtime.js";
|
|
9
9
|
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
10
10
|
const ignored = new Set([
|
|
@@ -17,6 +17,14 @@ const ignored = new Set([
|
|
|
17
17
|
'node_modules',
|
|
18
18
|
'repos'
|
|
19
19
|
]);
|
|
20
|
+
const MAX_CLOSURE_DEPTH = 128;
|
|
21
|
+
const MAX_CLOSURE_MODULES = 2048;
|
|
22
|
+
const MAX_API_HOPS = 128;
|
|
23
|
+
const MAX_API_VISITS = 2048;
|
|
24
|
+
const identitySources = [
|
|
25
|
+
'effect',
|
|
26
|
+
'effect/Function'
|
|
27
|
+
];
|
|
20
28
|
const normalize = (file)=>file.split(node_path.sep).join('/').replace(/^\.\//u, '');
|
|
21
29
|
const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
22
30
|
const apiApp = (app)=>'vertical' === app.kind && 'ui-only' !== app.surfaceProfile && 'horizontal-remote' !== app.deliveryUnitKind;
|
|
@@ -82,7 +90,7 @@ function configuredApps(root, supplied) {
|
|
|
82
90
|
};
|
|
83
91
|
for (const field of Object.keys(allowedFields)){
|
|
84
92
|
const value = app[field];
|
|
85
|
-
if (void 0 !== value && !allowedFields[field].includes(value)) throw new Error(`${key}: invalid ${field}`);
|
|
93
|
+
if ((void 0 !== value || 'kind' === field) && ('string' != typeof value || !allowedFields[field].includes(value))) throw new Error(`${key}: invalid ${field}`);
|
|
86
94
|
}
|
|
87
95
|
if (void 0 !== app.api) {
|
|
88
96
|
if (!app.api || 'object' != typeof app.api || Array.isArray(app.api)) throw new Error(`${key}: api must be an object`);
|
|
@@ -124,6 +132,9 @@ function check(options, topology) {
|
|
|
124
132
|
};
|
|
125
133
|
try {
|
|
126
134
|
const apps = configuredApps(root, options.configuredApps);
|
|
135
|
+
const workspace = node_fs.realpathSync(root);
|
|
136
|
+
const realRelative = (file)=>normalize(node_path.relative(workspace, node_fs.realpathSync(absolute(file))));
|
|
137
|
+
const inside = (directory, real)=>real.startsWith(`${directory}${node_path.sep}`);
|
|
127
138
|
const byPath = new Map(apps.map((app)=>[
|
|
128
139
|
normalize(app.path),
|
|
129
140
|
app
|
|
@@ -164,12 +175,19 @@ function check(options, topology) {
|
|
|
164
175
|
const resolveRelative = (fromFile, specifier)=>{
|
|
165
176
|
if (!/^\.\.?\//u.test(specifier)) return;
|
|
166
177
|
const base = node_path.posix.join(node_path.posix.dirname(fromFile), specifier).replace(/\.(?:[cm]?[jt]sx?)$/u, '');
|
|
167
|
-
|
|
178
|
+
const candidate = [
|
|
168
179
|
`${base}.ts`,
|
|
169
180
|
`${base}.tsx`,
|
|
170
181
|
`${base}/index.ts`,
|
|
171
182
|
`${base}/index.tsx`
|
|
172
|
-
].find((
|
|
183
|
+
].find((file)=>exists(file));
|
|
184
|
+
return void 0 === candidate ? void 0 : realRelative(candidate);
|
|
185
|
+
};
|
|
186
|
+
const graphBinding = (graph, module, expression, seen = new Set())=>{
|
|
187
|
+
if (seen.has(expression) || seen.size >= 128) return;
|
|
188
|
+
seen.add(expression);
|
|
189
|
+
const binding = __rspack_external__babel_types_6e8337dd.isIdentifier(expression) || __rspack_external__babel_types_6e8337dd.isMemberExpression(expression) ? graph.resolveReference(module, expression) : void 0;
|
|
190
|
+
return binding?.kind === 'declaration' && (__rspack_external__babel_types_6e8337dd.isIdentifier(binding.expression) || __rspack_external__babel_types_6e8337dd.isMemberExpression(binding.expression)) ? graphBinding(graph, binding.module, binding.expression, seen) : binding;
|
|
173
191
|
};
|
|
174
192
|
const clientExportFile = (appPath, exported)=>{
|
|
175
193
|
if ('string' != typeof exported || !exported.startsWith('./') || exported.split(/[\\/]/u).includes('..') || !/\.[cm]?[jt]sx?$/u.test(exported)) return;
|
|
@@ -186,29 +204,209 @@ function check(options, topology) {
|
|
|
186
204
|
return normalize(node_path.relative(root, candidate));
|
|
187
205
|
};
|
|
188
206
|
const composedModules = (file, appPath)=>{
|
|
189
|
-
const
|
|
190
|
-
|
|
207
|
+
const owner = node_fs.realpathSync(absolute(appPath));
|
|
208
|
+
if (!inside(owner, node_fs.realpathSync(absolute(file)))) throw new Error(`consumer module closure escapes its owner: ${file}`);
|
|
209
|
+
const entry = realRelative(file);
|
|
210
|
+
const discovered = new Set([
|
|
211
|
+
entry
|
|
212
|
+
]);
|
|
191
213
|
const queue = [
|
|
192
|
-
|
|
214
|
+
{
|
|
215
|
+
file: entry,
|
|
216
|
+
depth: 0
|
|
217
|
+
}
|
|
193
218
|
];
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const current = queue.shift();
|
|
197
|
-
if (seen.has(current)) continue;
|
|
198
|
-
seen.add(current);
|
|
199
|
-
order.push(current);
|
|
219
|
+
let graph;
|
|
220
|
+
for (const { file: current, depth } of queue){
|
|
200
221
|
const ast = parseFile(current);
|
|
201
222
|
if (!ast) continue;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (
|
|
223
|
+
const sources = [];
|
|
224
|
+
let callsRequire = false;
|
|
225
|
+
for (const statement of ast.program.body)if (__rspack_external__babel_types_6e8337dd.isImportDeclaration(statement) && 'type' !== statement.importKind && 'typeof' !== statement.importKind || (__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || __rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement)) && 'type' !== statement.exportKind && statement.source) sources.push(statement.source.value);
|
|
226
|
+
__rspack_external__babel_types_6e8337dd.traverseFast(ast, (node)=>{
|
|
227
|
+
if (__rspack_external__babel_types_6e8337dd.isImportExpression(node)) {
|
|
228
|
+
if (!__rspack_external__babel_types_6e8337dd.isStringLiteral(node.source)) throw new Error(`consumer module closure has nonliteral runtime dependency: ${current}`);
|
|
229
|
+
sources.push(node.source.value);
|
|
230
|
+
}
|
|
231
|
+
if ((__rspack_external__babel_types_6e8337dd.isCallExpression(node) || __rspack_external__babel_types_6e8337dd.isOptionalCallExpression(node)) && __rspack_external__babel_types_6e8337dd.isIdentifier(unwrapExpression(node.callee, true), {
|
|
232
|
+
name: 'require'
|
|
233
|
+
})) callsRequire = true;
|
|
234
|
+
});
|
|
235
|
+
if (callsRequire) {
|
|
236
|
+
graph ??= createModuleGraph();
|
|
237
|
+
const module = graph.module(absolute(current));
|
|
238
|
+
__rspack_external__babel_types_6e8337dd.traverseFast(module.file, (node)=>{
|
|
239
|
+
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node) && !__rspack_external__babel_types_6e8337dd.isOptionalCallExpression(node)) return;
|
|
240
|
+
const callee = unwrapExpression(node.callee, true);
|
|
241
|
+
if (!__rspack_external__babel_types_6e8337dd.isIdentifier(callee, {
|
|
242
|
+
name: 'require'
|
|
243
|
+
}) || !graph.isUnboundReference(module, callee)) return;
|
|
244
|
+
const [argument] = node.arguments;
|
|
245
|
+
if (1 !== node.arguments.length || !__rspack_external__babel_types_6e8337dd.isStringLiteral(argument)) throw new Error(`consumer module closure has nonliteral runtime dependency: ${current}`);
|
|
246
|
+
sources.push(argument.value);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
for (const source of sources){
|
|
250
|
+
if (!/^\.\.?\//u.test(source)) continue;
|
|
251
|
+
const resolved = resolveRelative(current, source);
|
|
252
|
+
if (void 0 === resolved) throw new Error(`consumer module closure has unresolved relative dependency: ${current} -> ${source}`);
|
|
253
|
+
if (!inside(owner, node_path.join(workspace, resolved))) throw new Error(`consumer module closure escapes its owner: ${current} -> ${source}`);
|
|
254
|
+
if (!discovered.has(resolved)) {
|
|
255
|
+
if (depth + 1 >= MAX_CLOSURE_DEPTH) throw new Error(`consumer module closure exceeds ${MAX_CLOSURE_DEPTH} import-depth analysis budget`);
|
|
256
|
+
if (discovered.size >= MAX_CLOSURE_MODULES) throw new Error(`consumer module closure exceeds ${MAX_CLOSURE_MODULES}-module analysis budget`);
|
|
257
|
+
discovered.add(resolved);
|
|
258
|
+
queue.push({
|
|
259
|
+
file: resolved,
|
|
260
|
+
depth: depth + 1
|
|
261
|
+
});
|
|
208
262
|
}
|
|
209
263
|
}
|
|
210
264
|
}
|
|
211
|
-
return
|
|
265
|
+
return queue.map((item)=>item.file);
|
|
266
|
+
};
|
|
267
|
+
const foreignClientConstructions = (client, appPath, contract, stem, rpc)=>{
|
|
268
|
+
const graph = createModuleGraph();
|
|
269
|
+
const contractModule = graph.module(absolute(contract));
|
|
270
|
+
const factory = rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient';
|
|
271
|
+
const ownExport = graph.resolve(contractModule, rpc ? `${pascalCaseStem(stem)}RpcGroup` : `${camelCaseStem(stem)}Api`, 'export');
|
|
272
|
+
const isIdentity = (value)=>'external' === value.kind && identitySources.includes(value.specifier) && 'identity' === value.name && 0 === value.members.length;
|
|
273
|
+
const identityPipe = (module, node)=>{
|
|
274
|
+
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node) || !__rspack_external__babel_types_6e8337dd.isMemberExpression(node.callee) || node.callee.computed || !__rspack_external__babel_types_6e8337dd.isIdentifier(node.callee.property, {
|
|
275
|
+
name: 'pipe'
|
|
276
|
+
}) || 1 !== node.arguments.length) return false;
|
|
277
|
+
const values = graph.evaluate(module, node.arguments[0]);
|
|
278
|
+
return values.length > 0 && values.every(isIdentity);
|
|
279
|
+
};
|
|
280
|
+
const withoutIdentity = (module, value)=>{
|
|
281
|
+
let node = unwrapExpression(value, true);
|
|
282
|
+
for(let pipes = 0; identityPipe(module, node); pipes += 1){
|
|
283
|
+
if (pipes >= MAX_API_HOPS) return value;
|
|
284
|
+
node = unwrapExpression(node.callee.object, true);
|
|
285
|
+
}
|
|
286
|
+
return node;
|
|
287
|
+
};
|
|
288
|
+
const normalizedApi = (initial)=>{
|
|
289
|
+
let binding = initial;
|
|
290
|
+
const seen = new Set();
|
|
291
|
+
for(let hops = 0; hops < MAX_API_HOPS; hops += 1){
|
|
292
|
+
const expression = withoutIdentity(binding.module, binding.expression);
|
|
293
|
+
if (seen.has(expression)) break;
|
|
294
|
+
seen.add(expression);
|
|
295
|
+
if (!__rspack_external__babel_types_6e8337dd.isIdentifier(expression) && !__rspack_external__babel_types_6e8337dd.isMemberExpression(expression)) return {
|
|
296
|
+
...binding,
|
|
297
|
+
expression: expression
|
|
298
|
+
};
|
|
299
|
+
const next = graphBinding(graph, binding.module, expression);
|
|
300
|
+
if (next?.kind !== 'declaration') break;
|
|
301
|
+
binding = next;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const apiBinding = (module, value)=>{
|
|
305
|
+
const binding = graphBinding(graph, module, withoutIdentity(module, value));
|
|
306
|
+
return binding?.kind === 'declaration' ? normalizedApi(binding) : void 0;
|
|
307
|
+
};
|
|
308
|
+
const ownedApis = new Map();
|
|
309
|
+
const own = (binding)=>{
|
|
310
|
+
const identities = ownedApis.get(binding.module.path) ?? new Set();
|
|
311
|
+
identities.add(binding.expression);
|
|
312
|
+
ownedApis.set(binding.module.path, identities);
|
|
313
|
+
};
|
|
314
|
+
const proven = new Set();
|
|
315
|
+
const active = new Set();
|
|
316
|
+
let visits = 0;
|
|
317
|
+
const builtApi = (module, expression)=>{
|
|
318
|
+
const initial = withoutIdentity(module, expression);
|
|
319
|
+
if (active.has(initial) || active.size >= MAX_API_HOPS) return false;
|
|
320
|
+
active.add(initial);
|
|
321
|
+
try {
|
|
322
|
+
const children = [];
|
|
323
|
+
for(let node = initial;;){
|
|
324
|
+
if (++visits > MAX_API_VISITS) return false;
|
|
325
|
+
if (__rspack_external__babel_types_6e8337dd.isIdentifier(node) || __rspack_external__babel_types_6e8337dd.isMemberExpression(node)) {
|
|
326
|
+
const receiver = apiBinding(module, node);
|
|
327
|
+
if (!receiver || !builtApi(receiver.module, receiver.expression)) return false;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node)) return false;
|
|
331
|
+
const callee = graph.evaluate(module, node.callee);
|
|
332
|
+
if (callee.length > 0 && callee.every((value)=>'external' === value.kind && httpApiSources.includes(value.specifier) && 'HttpApi' === value.name && 1 === value.members.length && 'make' === value.members[0])) {
|
|
333
|
+
if (1 !== node.arguments.length || !__rspack_external__babel_types_6e8337dd.isStringLiteral(node.arguments[0])) return false;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
if (!__rspack_external__babel_types_6e8337dd.isMemberExpression(node.callee) || node.callee.computed || !__rspack_external__babel_types_6e8337dd.isIdentifier(node.callee.property)) return false;
|
|
337
|
+
const method = node.callee.property.name;
|
|
338
|
+
const count = node.arguments.length;
|
|
339
|
+
const [first] = node.arguments;
|
|
340
|
+
if ('addHttpApi' === method && 1 === count) children.push(first);
|
|
341
|
+
else if (!('add' === method && count > 0 || 'annotate' === method && 2 === count || [
|
|
342
|
+
'annotateMerge',
|
|
343
|
+
'middleware'
|
|
344
|
+
].includes(method) && 1 === count || 'prefix' === method && 1 === count && __rspack_external__babel_types_6e8337dd.isStringLiteral(first) && first.value.startsWith('/'))) return false;
|
|
345
|
+
node = withoutIdentity(module, node.callee.object);
|
|
346
|
+
}
|
|
347
|
+
return children.every((child)=>{
|
|
348
|
+
const binding = apiBinding(module, child);
|
|
349
|
+
return void 0 !== binding && collectApi(binding);
|
|
350
|
+
});
|
|
351
|
+
} finally{
|
|
352
|
+
active.delete(initial);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
const collectApi = (binding)=>{
|
|
356
|
+
if (proven.has(binding.expression)) return true;
|
|
357
|
+
if (!builtApi(binding.module, binding.expression)) return false;
|
|
358
|
+
proven.add(binding.expression);
|
|
359
|
+
if (binding.module.boundary === contractModule.boundary) own(binding);
|
|
360
|
+
return true;
|
|
361
|
+
};
|
|
362
|
+
if (!rpc && ownExport?.kind === 'declaration') {
|
|
363
|
+
const root = normalizedApi(ownExport);
|
|
364
|
+
if (root && collectApi(root)) own(root);
|
|
365
|
+
else ownedApis.clear();
|
|
366
|
+
}
|
|
367
|
+
const immutablePipeAliases = (module, target)=>{
|
|
368
|
+
if (0 === module.mutations.length) return true;
|
|
369
|
+
let valid = true;
|
|
370
|
+
__rspack_external__babel_types_6e8337dd.traverseFast(module.file, (node)=>{
|
|
371
|
+
if (!__rspack_external__babel_types_6e8337dd.isVariableDeclarator(node) || !__rspack_external__babel_types_6e8337dd.isIdentifier(node.id) || !node.init) return;
|
|
372
|
+
let initializer = unwrapExpression(node.init, true);
|
|
373
|
+
let identityOnly = true;
|
|
374
|
+
let piped = false;
|
|
375
|
+
while(__rspack_external__babel_types_6e8337dd.isCallExpression(initializer) && __rspack_external__babel_types_6e8337dd.isMemberExpression(initializer.callee) && !initializer.callee.computed && __rspack_external__babel_types_6e8337dd.isIdentifier(initializer.callee.property, {
|
|
376
|
+
name: 'pipe'
|
|
377
|
+
})){
|
|
378
|
+
piped = true;
|
|
379
|
+
identityOnly &&= identityPipe(module, initializer);
|
|
380
|
+
initializer = unwrapExpression(initializer.callee.object, true);
|
|
381
|
+
}
|
|
382
|
+
if (!piped) return;
|
|
383
|
+
const receiver = apiBinding(module, initializer);
|
|
384
|
+
if (receiver?.module.path !== target.module.path || receiver.expression !== target.expression) return;
|
|
385
|
+
if (!identityOnly || graph.resolveReference(module, node.id)?.kind !== 'declaration') valid = false;
|
|
386
|
+
});
|
|
387
|
+
return valid;
|
|
388
|
+
};
|
|
389
|
+
const ownedBy = (module, argument)=>{
|
|
390
|
+
if (!argument) return false;
|
|
391
|
+
if (rpc) {
|
|
392
|
+
const binding = graphBinding(graph, module, argument);
|
|
393
|
+
return binding?.kind === 'declaration' && ownExport?.kind === 'declaration' && binding.module.path === ownExport.module.path && binding.expression === ownExport.expression;
|
|
394
|
+
}
|
|
395
|
+
const binding = apiBinding(module, argument);
|
|
396
|
+
return void 0 !== binding && !!ownedApis.get(binding.module.path)?.has(binding.expression) && immutablePipeAliases(module, binding);
|
|
397
|
+
};
|
|
398
|
+
const violations = new Set();
|
|
399
|
+
for (const source of composedModules(client, appPath)){
|
|
400
|
+
const module = graph.module(absolute(source));
|
|
401
|
+
__rspack_external__babel_types_6e8337dd.traverseFast(module.file, (node)=>{
|
|
402
|
+
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node) && !__rspack_external__babel_types_6e8337dd.isOptionalCallExpression(node)) return;
|
|
403
|
+
const native = graph.evaluate(module, node.callee).some((value)=>'external' === value.kind && value.specifier === effect && value.name === factory);
|
|
404
|
+
if (native && !ownedBy(module, node.arguments[0])) violations.add(`${source}: must construct the client from its own API contract`);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return [
|
|
408
|
+
...violations
|
|
409
|
+
];
|
|
212
410
|
};
|
|
213
411
|
const moduleShape = (file, imports, calls, patterns = [], defaultExport, composedAppPath)=>{
|
|
214
412
|
if (!exists(file)) return;
|
|
@@ -247,6 +445,14 @@ function check(options, topology) {
|
|
|
247
445
|
})), `${file}: must default-export ${defaultExport}`);
|
|
248
446
|
};
|
|
249
447
|
const effect = '@modern-js/bff-effect/effect-client';
|
|
448
|
+
const httpApiSources = [
|
|
449
|
+
effect,
|
|
450
|
+
'effect/http-api'
|
|
451
|
+
];
|
|
452
|
+
const clientFactories = new Set([
|
|
453
|
+
'makeEffectHttpApiClient',
|
|
454
|
+
'makeEffectRpcClient'
|
|
455
|
+
]);
|
|
250
456
|
const appDirectories = [
|
|
251
457
|
...new Set([
|
|
252
458
|
...directories('apps'),
|
|
@@ -424,6 +630,7 @@ function check(options, topology) {
|
|
|
424
630
|
], [
|
|
425
631
|
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
426
632
|
], [], void 0, appPath);
|
|
633
|
+
if (exists(contract)) diagnostics.push(...foreignClientConstructions(client, appPath, contract, stem, rpc));
|
|
427
634
|
}
|
|
428
635
|
moduleShape(`${appPath}/api/effect-api.ts`, [], [], [
|
|
429
636
|
[
|
|
@@ -467,8 +674,8 @@ function check(options, topology) {
|
|
|
467
674
|
]);
|
|
468
675
|
});
|
|
469
676
|
const sourceRules = options.sourceRules ?? [];
|
|
470
|
-
const graph = createModuleGraph();
|
|
471
677
|
for (const file of sourceRules.length ? sourceFiles : [])if (/\.[cm]?[jt]sx?$/u.test(file)) guarded(file, ()=>{
|
|
678
|
+
const graph = createModuleGraph();
|
|
472
679
|
let module;
|
|
473
680
|
try {
|
|
474
681
|
module = graph.module(absolute(file));
|
|
@@ -487,20 +694,178 @@ function check(options, topology) {
|
|
|
487
694
|
for (const message of messages)diagnostics.push(`${file}: ${message}`);
|
|
488
695
|
}
|
|
489
696
|
});
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
assert(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
697
|
+
const checkShellExports = (shellPath)=>{
|
|
698
|
+
const packageFile = `${shellPath}/package.json`;
|
|
699
|
+
const manifest = JSON.parse(read(packageFile));
|
|
700
|
+
noPath(`${shellPath}/src/api/vertical-clients.ts`, 'Shell client registries are forbidden; import each owner client directly');
|
|
701
|
+
assert(!Object.hasOwn(manifest.exports ?? {}, './api/clients'), `${packageFile}: Shell ./api/clients exports are forbidden`);
|
|
702
|
+
const owner = node_fs.realpathSync(absolute(shellPath));
|
|
703
|
+
const invalidExport = ()=>assert(false, `${packageFile}: invalid Shell public source export`);
|
|
704
|
+
const targets = (exported)=>'string' == typeof exported ? [
|
|
705
|
+
exported
|
|
706
|
+
] : exported && 'object' == typeof exported ? Object.values(exported).flatMap(targets) : [];
|
|
707
|
+
const filesUnder = (directory, seen = new Set())=>{
|
|
708
|
+
const real = node_fs.realpathSync(absolute(directory));
|
|
709
|
+
if (real !== owner && !inside(owner, real)) {
|
|
710
|
+
invalidExport();
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
if (seen.has(real)) return [];
|
|
714
|
+
seen.add(real);
|
|
715
|
+
return node_fs.readdirSync(absolute(directory), {
|
|
716
|
+
withFileTypes: true
|
|
717
|
+
}).flatMap((entry)=>{
|
|
718
|
+
if (ignored.has(entry.name)) return [];
|
|
719
|
+
const file = `${directory}/${entry.name}`;
|
|
720
|
+
const stat = node_fs.statSync(absolute(file));
|
|
721
|
+
return stat.isDirectory() ? filesUnder(file, seen) : stat.isFile() ? [
|
|
722
|
+
file
|
|
723
|
+
] : [];
|
|
724
|
+
});
|
|
725
|
+
};
|
|
726
|
+
const exportedFiles = (target)=>{
|
|
727
|
+
if (!target.includes('*')) return [
|
|
728
|
+
`${shellPath}/${target.slice(2)}`
|
|
729
|
+
];
|
|
730
|
+
const prefix = target.slice(0, target.indexOf('*'));
|
|
731
|
+
const pattern = new RegExp(`^${target.split('*').map((part)=>part.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&')).join('.*')}$`, 'u');
|
|
732
|
+
return filesUnder(`${shellPath}/${prefix.slice(2, prefix.lastIndexOf('/'))}`).filter((file)=>pattern.test(`./${node_path.posix.relative(shellPath, file)}`));
|
|
733
|
+
};
|
|
734
|
+
const ownClientPrefix = `${manifest.name}/api/`;
|
|
735
|
+
const foreignSpecifier = (specifier)=>/\/api\/(?:rpc-)?client$/u.test(specifier) && !specifier.startsWith(ownClientPrefix);
|
|
736
|
+
for (const target of targets(manifest.exports))if (/\.[cm]?[jt]sx?$/u.test(target)) {
|
|
737
|
+
if (!target.startsWith('./') || target.split(/[\\/]/u).includes('..')) {
|
|
738
|
+
invalidExport();
|
|
739
|
+
continue;
|
|
500
740
|
}
|
|
501
|
-
|
|
741
|
+
for (const candidate of exportedFiles(target)){
|
|
742
|
+
const file = clientExportFile(shellPath, `./${node_path.posix.relative(shellPath, candidate)}`);
|
|
743
|
+
if (!file) {
|
|
744
|
+
invalidExport();
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
assert(!exposesForeignClient(file, owner, foreignSpecifier), `${file}: Shell public exports must not expose another owner's API client`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
const exposesForeignClient = (entry, owner, foreignSpecifier)=>{
|
|
752
|
+
const graph = createModuleGraph();
|
|
753
|
+
const seenValues = new Set();
|
|
754
|
+
const seenExports = new Map();
|
|
755
|
+
const workspacePath = (module)=>normalize(node_path.relative(workspace, module.path));
|
|
756
|
+
const resolveSource = (from, specifier)=>resolveRelative(from, specifier) ?? ownerClients.get(specifier);
|
|
757
|
+
const returnExpressions = (node)=>{
|
|
758
|
+
if (__rspack_external__babel_types_6e8337dd.isReturnStatement(node)) return node.argument ? [
|
|
759
|
+
node.argument
|
|
760
|
+
] : [];
|
|
761
|
+
if (__rspack_external__babel_types_6e8337dd.isFunction(node)) return [];
|
|
762
|
+
return (__rspack_external__babel_types_6e8337dd.VISITOR_KEYS[node.type] ?? []).flatMap((key)=>{
|
|
763
|
+
const value = Reflect.get(node, key);
|
|
764
|
+
return (Array.isArray(value) ? value : [
|
|
765
|
+
value
|
|
766
|
+
]).flatMap((child)=>__rspack_external__babel_types_6e8337dd.isNode(child) ? returnExpressions(child) : []);
|
|
767
|
+
});
|
|
768
|
+
};
|
|
769
|
+
const nestedValues = (node)=>{
|
|
770
|
+
if (__rspack_external__babel_types_6e8337dd.isObjectExpression(node)) return node.properties.flatMap((property)=>__rspack_external__babel_types_6e8337dd.isObjectProperty(property) ? [
|
|
771
|
+
property.value
|
|
772
|
+
] : __rspack_external__babel_types_6e8337dd.isObjectMethod(property) ? returnExpressions(property.body) : __rspack_external__babel_types_6e8337dd.isSpreadElement(property) ? [
|
|
773
|
+
property.argument
|
|
774
|
+
] : []);
|
|
775
|
+
if (__rspack_external__babel_types_6e8337dd.isArrayExpression(node)) return node.elements.filter((element)=>null !== element);
|
|
776
|
+
if (__rspack_external__babel_types_6e8337dd.isFunction(node)) return __rspack_external__babel_types_6e8337dd.isBlockStatement(node.body) ? returnExpressions(node.body) : [
|
|
777
|
+
node.body
|
|
778
|
+
];
|
|
779
|
+
return [];
|
|
780
|
+
};
|
|
781
|
+
const foreignValue = (value, from)=>{
|
|
782
|
+
if ('external' === value.kind) {
|
|
783
|
+
const source = resolveSource(workspacePath(from), value.specifier);
|
|
784
|
+
if (!source) return foreignSpecifier(value.specifier);
|
|
785
|
+
return '*' === value.name ? inspect(source) : exportedValue(source, value.name);
|
|
786
|
+
}
|
|
787
|
+
if ('unresolved' === value.kind && __rspack_external__babel_types_6e8337dd.isImportDeclaration(value.node) && value.node.specifiers.some((specifier)=>__rspack_external__babel_types_6e8337dd.isImportNamespaceSpecifier(specifier))) {
|
|
788
|
+
const specifier = value.node.source.value;
|
|
789
|
+
const source = resolveSource(workspacePath(value.module), specifier);
|
|
790
|
+
return source ? inspect(source) : foreignSpecifier(specifier);
|
|
791
|
+
}
|
|
792
|
+
if ('node' !== value.kind || seenValues.has(value.node)) return false;
|
|
793
|
+
seenValues.add(value.node);
|
|
794
|
+
const { node, module } = value;
|
|
795
|
+
if ((__rspack_external__babel_types_6e8337dd.isCallExpression(node) || __rspack_external__babel_types_6e8337dd.isOptionalCallExpression(node)) && graph.evaluate(module, node.callee).some((callee)=>'external' === callee.kind && callee.specifier === effect && clientFactories.has(callee.name))) {
|
|
796
|
+
const [argument] = node.arguments;
|
|
797
|
+
const binding = argument ? graphBinding(graph, module, argument) : void 0;
|
|
798
|
+
if (module.boundary !== owner || binding?.kind !== 'declaration' || binding.module.boundary !== owner) return true;
|
|
799
|
+
}
|
|
800
|
+
return nestedValues(node).some((expression)=>graph.evaluate(module, expression).some((nested)=>foreignValue(nested, module)));
|
|
801
|
+
};
|
|
802
|
+
const exportName = (node)=>__rspack_external__babel_types_6e8337dd.isIdentifier(node) ? node.name : node.value;
|
|
803
|
+
const names = (source, visited = new Set())=>{
|
|
804
|
+
if (visited.has(source)) return [];
|
|
805
|
+
visited.add(source);
|
|
806
|
+
return graph.module(absolute(source)).file.program.body.flatMap((statement)=>{
|
|
807
|
+
if (__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement)) return [
|
|
808
|
+
'default'
|
|
809
|
+
];
|
|
810
|
+
if (!__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) && !__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement) || 'type' === statement.exportKind) return [];
|
|
811
|
+
if (__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement)) {
|
|
812
|
+
const target = resolveSource(source, statement.source.value);
|
|
813
|
+
return target ? names(target, visited).filter((name)=>'default' !== name) : [];
|
|
814
|
+
}
|
|
815
|
+
return statement.declaration ? Object.keys(__rspack_external__babel_types_6e8337dd.getOuterBindingIdentifiers(statement.declaration)) : statement.specifiers.flatMap((specifier)=>__rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier) && 'type' === specifier.exportKind ? [] : [
|
|
816
|
+
exportName(specifier.exported)
|
|
817
|
+
]);
|
|
818
|
+
});
|
|
819
|
+
};
|
|
820
|
+
const exportedValue = (source, name)=>{
|
|
821
|
+
const visited = seenExports.get(source) ?? new Set();
|
|
822
|
+
if (visited.has(name)) return false;
|
|
823
|
+
visited.add(name);
|
|
824
|
+
seenExports.set(source, visited);
|
|
825
|
+
const module = graph.module(absolute(source));
|
|
826
|
+
const evaluates = (from, node)=>graph.evaluate(from, node).some((value)=>foreignValue(value, from));
|
|
827
|
+
const binding = graph.resolve(module, name, 'export');
|
|
828
|
+
if (binding?.kind === 'external') return foreignValue({
|
|
829
|
+
...binding,
|
|
830
|
+
members: []
|
|
831
|
+
}, module);
|
|
832
|
+
if (binding?.kind === 'declaration') return evaluates(binding.module, binding.expression);
|
|
833
|
+
for (const statement of module.file.program.body){
|
|
834
|
+
if (__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement) && 'default' === name) return evaluates(module, statement.declaration);
|
|
835
|
+
if (!__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || 'type' === statement.exportKind) continue;
|
|
836
|
+
if (__rspack_external__babel_types_6e8337dd.isFunctionDeclaration(statement.declaration) && statement.declaration.id?.name === name) return evaluates(module, statement.declaration);
|
|
837
|
+
const specifier = statement.specifiers.find((item)=>exportName(item.exported) === name);
|
|
838
|
+
if (!specifier) continue;
|
|
839
|
+
if (__rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier) && 'type' === specifier.exportKind) return false;
|
|
840
|
+
if (!statement.source) return __rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier) ? evaluates(module, specifier.local) : false;
|
|
841
|
+
const target = resolveSource(source, statement.source.value);
|
|
842
|
+
if (!target) return foreignSpecifier(statement.source.value);
|
|
843
|
+
if (__rspack_external__babel_types_6e8337dd.isExportNamespaceSpecifier(specifier)) return inspect(target);
|
|
844
|
+
if (__rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier)) return exportedValue(target, exportName(specifier.local));
|
|
845
|
+
}
|
|
846
|
+
return module.file.program.body.some((statement)=>{
|
|
847
|
+
if (!__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement) || 'type' === statement.exportKind) return false;
|
|
848
|
+
const target = resolveSource(source, statement.source.value);
|
|
849
|
+
return !!target && names(target).includes(name) && exportedValue(target, name);
|
|
850
|
+
});
|
|
851
|
+
};
|
|
852
|
+
const inspect = (source)=>[
|
|
853
|
+
...new Set(names(source))
|
|
854
|
+
].some((name)=>exportedValue(source, name));
|
|
855
|
+
return inspect(entry);
|
|
856
|
+
};
|
|
857
|
+
const ownerClients = new Map();
|
|
858
|
+
for (const app of apps)if (exists(`${app.path}/package.json`)) {
|
|
859
|
+
const manifest = JSON.parse(read(`${app.path}/package.json`));
|
|
860
|
+
for (const key of [
|
|
861
|
+
'./api/client',
|
|
862
|
+
'./api/rpc-client'
|
|
863
|
+
]){
|
|
864
|
+
const file = clientExportFile(app.path, manifest.exports?.[key]);
|
|
865
|
+
if (file) ownerClients.set(`${manifest.name}${key.slice(1)}`, file);
|
|
866
|
+
}
|
|
502
867
|
}
|
|
503
|
-
|
|
868
|
+
for (const shell of apps.filter((app)=>'shell' === app.kind))guarded(shell.path, ()=>checkShellExports(normalize(shell.path)));
|
|
504
869
|
if (exists('topology/reference-topology.json')) guarded('topology/reference-topology.json', ()=>{
|
|
505
870
|
for (const vertical of JSON.parse(read('topology/reference-topology.json')).verticals ?? []){
|
|
506
871
|
if (vertical.api?.runtime === 'effect') {
|