@bleedingdev/modern-js-code-tools 3.9.3-ultramodern.2 → 3.9.3-ultramodern.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/microvertical-api-baseline.cjs +8 -2
- package/dist/cjs/microvertical-api-boundary.cjs +155 -0
- package/dist/esm/microvertical-api-baseline.js +1 -1
- package/dist/esm/microvertical-api-boundary.js +156 -1
- package/dist/esm-node/microvertical-api-baseline.js +1 -1
- package/dist/esm-node/microvertical-api-boundary.js +156 -1
- package/dist/types/microvertical-api-baseline.d.ts +4 -0
- package/package.json +1 -1
|
@@ -28,8 +28,10 @@ var __webpack_require__ = {};
|
|
|
28
28
|
var __webpack_exports__ = {};
|
|
29
29
|
__webpack_require__.r(__webpack_exports__);
|
|
30
30
|
__webpack_require__.d(__webpack_exports__, {
|
|
31
|
+
camelCaseStem: ()=>camelCaseStem,
|
|
31
32
|
configuredMicroVerticalApiStem: ()=>configuredMicroVerticalApiStem,
|
|
32
|
-
microVerticalApiBaselineViolation: ()=>microVerticalApiBaselineViolation
|
|
33
|
+
microVerticalApiBaselineViolation: ()=>microVerticalApiBaselineViolation,
|
|
34
|
+
pascalCaseStem: ()=>pascalCaseStem
|
|
33
35
|
});
|
|
34
36
|
const types_namespaceObject = require("@babel/types");
|
|
35
37
|
const external_microvertical_api_owner_cjs_namespaceObject = require("./microvertical-api-owner.cjs");
|
|
@@ -503,11 +505,15 @@ const microVerticalApiBaselineViolation = (stem, filePath, expectation)=>{
|
|
|
503
505
|
throw error;
|
|
504
506
|
}
|
|
505
507
|
};
|
|
508
|
+
exports.camelCaseStem = __webpack_exports__.camelCaseStem;
|
|
506
509
|
exports.configuredMicroVerticalApiStem = __webpack_exports__.configuredMicroVerticalApiStem;
|
|
507
510
|
exports.microVerticalApiBaselineViolation = __webpack_exports__.microVerticalApiBaselineViolation;
|
|
511
|
+
exports.pascalCaseStem = __webpack_exports__.pascalCaseStem;
|
|
508
512
|
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
513
|
+
"camelCaseStem",
|
|
509
514
|
"configuredMicroVerticalApiStem",
|
|
510
|
-
"microVerticalApiBaselineViolation"
|
|
515
|
+
"microVerticalApiBaselineViolation",
|
|
516
|
+
"pascalCaseStem"
|
|
511
517
|
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
512
518
|
Object.defineProperty(exports, '__esModule', {
|
|
513
519
|
value: true
|
|
@@ -66,6 +66,12 @@ const ignored = new Set([
|
|
|
66
66
|
]);
|
|
67
67
|
const MAX_CLOSURE_DEPTH = 128;
|
|
68
68
|
const MAX_CLOSURE_MODULES = 2048;
|
|
69
|
+
const MAX_API_HOPS = 128;
|
|
70
|
+
const MAX_API_VISITS = 2048;
|
|
71
|
+
const identitySources = [
|
|
72
|
+
'effect',
|
|
73
|
+
'effect/Function'
|
|
74
|
+
];
|
|
69
75
|
const normalize = (file)=>file.split(external_node_path_default().sep).join('/').replace(/^\.\//u, '');
|
|
70
76
|
const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
71
77
|
const apiApp = (app)=>'vertical' === app.kind && 'ui-only' !== app.surfaceProfile && 'horizontal-remote' !== app.deliveryUnitKind;
|
|
@@ -305,6 +311,150 @@ function check(options, topology) {
|
|
|
305
311
|
}
|
|
306
312
|
return queue.map((item)=>item.file);
|
|
307
313
|
};
|
|
314
|
+
const foreignClientConstructions = (client, appPath, contract, stem, rpc)=>{
|
|
315
|
+
const graph = (0, external_module_graph_cjs_namespaceObject.createModuleGraph)();
|
|
316
|
+
const contractModule = graph.module(absolute(contract));
|
|
317
|
+
const factory = rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient';
|
|
318
|
+
const ownExport = graph.resolve(contractModule, rpc ? `${(0, external_microvertical_api_baseline_cjs_namespaceObject.pascalCaseStem)(stem)}RpcGroup` : `${(0, external_microvertical_api_baseline_cjs_namespaceObject.camelCaseStem)(stem)}Api`, 'export');
|
|
319
|
+
const isIdentity = (value)=>'external' === value.kind && identitySources.includes(value.specifier) && 'identity' === value.name && 0 === value.members.length;
|
|
320
|
+
const identityPipe = (module, node)=>{
|
|
321
|
+
if (!types_namespaceObject.isCallExpression(node) || !types_namespaceObject.isMemberExpression(node.callee) || node.callee.computed || !types_namespaceObject.isIdentifier(node.callee.property, {
|
|
322
|
+
name: 'pipe'
|
|
323
|
+
}) || 1 !== node.arguments.length) return false;
|
|
324
|
+
const values = graph.evaluate(module, node.arguments[0]);
|
|
325
|
+
return values.length > 0 && values.every(isIdentity);
|
|
326
|
+
};
|
|
327
|
+
const withoutIdentity = (module, value)=>{
|
|
328
|
+
let node = (0, external_source_analysis_cjs_namespaceObject.unwrapExpression)(value, true);
|
|
329
|
+
for(let pipes = 0; identityPipe(module, node); pipes += 1){
|
|
330
|
+
if (pipes >= MAX_API_HOPS) return value;
|
|
331
|
+
node = (0, external_source_analysis_cjs_namespaceObject.unwrapExpression)(node.callee.object, true);
|
|
332
|
+
}
|
|
333
|
+
return node;
|
|
334
|
+
};
|
|
335
|
+
const normalizedApi = (initial)=>{
|
|
336
|
+
let binding = initial;
|
|
337
|
+
const seen = new Set();
|
|
338
|
+
for(let hops = 0; hops < MAX_API_HOPS; hops += 1){
|
|
339
|
+
const expression = withoutIdentity(binding.module, binding.expression);
|
|
340
|
+
if (seen.has(expression)) break;
|
|
341
|
+
seen.add(expression);
|
|
342
|
+
if (!types_namespaceObject.isIdentifier(expression) && !types_namespaceObject.isMemberExpression(expression)) return {
|
|
343
|
+
...binding,
|
|
344
|
+
expression: expression
|
|
345
|
+
};
|
|
346
|
+
const next = graphBinding(graph, binding.module, expression);
|
|
347
|
+
if (next?.kind !== 'declaration') break;
|
|
348
|
+
binding = next;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const apiBinding = (module, value)=>{
|
|
352
|
+
const binding = graphBinding(graph, module, withoutIdentity(module, value));
|
|
353
|
+
return binding?.kind === 'declaration' ? normalizedApi(binding) : void 0;
|
|
354
|
+
};
|
|
355
|
+
const ownedApis = new Map();
|
|
356
|
+
const own = (binding)=>{
|
|
357
|
+
const identities = ownedApis.get(binding.module.path) ?? new Set();
|
|
358
|
+
identities.add(binding.expression);
|
|
359
|
+
ownedApis.set(binding.module.path, identities);
|
|
360
|
+
};
|
|
361
|
+
const proven = new Set();
|
|
362
|
+
const active = new Set();
|
|
363
|
+
let visits = 0;
|
|
364
|
+
const builtApi = (module, expression)=>{
|
|
365
|
+
const initial = withoutIdentity(module, expression);
|
|
366
|
+
if (active.has(initial) || active.size >= MAX_API_HOPS) return false;
|
|
367
|
+
active.add(initial);
|
|
368
|
+
try {
|
|
369
|
+
const children = [];
|
|
370
|
+
for(let node = initial;;){
|
|
371
|
+
if (++visits > MAX_API_VISITS) return false;
|
|
372
|
+
if (types_namespaceObject.isIdentifier(node) || types_namespaceObject.isMemberExpression(node)) {
|
|
373
|
+
const receiver = apiBinding(module, node);
|
|
374
|
+
if (!receiver || !builtApi(receiver.module, receiver.expression)) return false;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
if (!types_namespaceObject.isCallExpression(node)) return false;
|
|
378
|
+
const callee = graph.evaluate(module, node.callee);
|
|
379
|
+
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])) {
|
|
380
|
+
if (1 !== node.arguments.length || !types_namespaceObject.isStringLiteral(node.arguments[0])) return false;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
if (!types_namespaceObject.isMemberExpression(node.callee) || node.callee.computed || !types_namespaceObject.isIdentifier(node.callee.property)) return false;
|
|
384
|
+
const method = node.callee.property.name;
|
|
385
|
+
const count = node.arguments.length;
|
|
386
|
+
const [first] = node.arguments;
|
|
387
|
+
if ('addHttpApi' === method && 1 === count) children.push(first);
|
|
388
|
+
else if (!('add' === method && count > 0 || 'annotate' === method && 2 === count || [
|
|
389
|
+
'annotateMerge',
|
|
390
|
+
'middleware'
|
|
391
|
+
].includes(method) && 1 === count || 'prefix' === method && 1 === count && types_namespaceObject.isStringLiteral(first) && first.value.startsWith('/'))) return false;
|
|
392
|
+
node = withoutIdentity(module, node.callee.object);
|
|
393
|
+
}
|
|
394
|
+
return children.every((child)=>{
|
|
395
|
+
const binding = apiBinding(module, child);
|
|
396
|
+
return void 0 !== binding && collectApi(binding);
|
|
397
|
+
});
|
|
398
|
+
} finally{
|
|
399
|
+
active.delete(initial);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
const collectApi = (binding)=>{
|
|
403
|
+
if (proven.has(binding.expression)) return true;
|
|
404
|
+
if (!builtApi(binding.module, binding.expression)) return false;
|
|
405
|
+
proven.add(binding.expression);
|
|
406
|
+
if (binding.module.boundary === contractModule.boundary) own(binding);
|
|
407
|
+
return true;
|
|
408
|
+
};
|
|
409
|
+
if (!rpc && ownExport?.kind === 'declaration') {
|
|
410
|
+
const root = normalizedApi(ownExport);
|
|
411
|
+
if (root && collectApi(root)) own(root);
|
|
412
|
+
else ownedApis.clear();
|
|
413
|
+
}
|
|
414
|
+
const immutablePipeAliases = (module, target)=>{
|
|
415
|
+
if (0 === module.mutations.length) return true;
|
|
416
|
+
let valid = true;
|
|
417
|
+
types_namespaceObject.traverseFast(module.file, (node)=>{
|
|
418
|
+
if (!types_namespaceObject.isVariableDeclarator(node) || !types_namespaceObject.isIdentifier(node.id) || !node.init) return;
|
|
419
|
+
let initializer = (0, external_source_analysis_cjs_namespaceObject.unwrapExpression)(node.init, true);
|
|
420
|
+
let identityOnly = true;
|
|
421
|
+
let piped = false;
|
|
422
|
+
while(types_namespaceObject.isCallExpression(initializer) && types_namespaceObject.isMemberExpression(initializer.callee) && !initializer.callee.computed && types_namespaceObject.isIdentifier(initializer.callee.property, {
|
|
423
|
+
name: 'pipe'
|
|
424
|
+
})){
|
|
425
|
+
piped = true;
|
|
426
|
+
identityOnly &&= identityPipe(module, initializer);
|
|
427
|
+
initializer = (0, external_source_analysis_cjs_namespaceObject.unwrapExpression)(initializer.callee.object, true);
|
|
428
|
+
}
|
|
429
|
+
if (!piped) return;
|
|
430
|
+
const receiver = apiBinding(module, initializer);
|
|
431
|
+
if (receiver?.module.path !== target.module.path || receiver.expression !== target.expression) return;
|
|
432
|
+
if (!identityOnly || graph.resolveReference(module, node.id)?.kind !== 'declaration') valid = false;
|
|
433
|
+
});
|
|
434
|
+
return valid;
|
|
435
|
+
};
|
|
436
|
+
const ownedBy = (module, argument)=>{
|
|
437
|
+
if (!argument) return false;
|
|
438
|
+
if (rpc) {
|
|
439
|
+
const binding = graphBinding(graph, module, argument);
|
|
440
|
+
return binding?.kind === 'declaration' && ownExport?.kind === 'declaration' && binding.module.path === ownExport.module.path && binding.expression === ownExport.expression;
|
|
441
|
+
}
|
|
442
|
+
const binding = apiBinding(module, argument);
|
|
443
|
+
return void 0 !== binding && !!ownedApis.get(binding.module.path)?.has(binding.expression) && immutablePipeAliases(module, binding);
|
|
444
|
+
};
|
|
445
|
+
const violations = new Set();
|
|
446
|
+
for (const source of composedModules(client, appPath)){
|
|
447
|
+
const module = graph.module(absolute(source));
|
|
448
|
+
types_namespaceObject.traverseFast(module.file, (node)=>{
|
|
449
|
+
if (!types_namespaceObject.isCallExpression(node) && !types_namespaceObject.isOptionalCallExpression(node)) return;
|
|
450
|
+
const native = graph.evaluate(module, node.callee).some((value)=>'external' === value.kind && value.specifier === effect && value.name === factory);
|
|
451
|
+
if (native && !ownedBy(module, node.arguments[0])) violations.add(`${source}: must construct the client from its own API contract`);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return [
|
|
455
|
+
...violations
|
|
456
|
+
];
|
|
457
|
+
};
|
|
308
458
|
const moduleShape = (file, imports, calls, patterns = [], defaultExport, composedAppPath)=>{
|
|
309
459
|
if (!exists(file)) return;
|
|
310
460
|
const ast = parseFile(file);
|
|
@@ -342,6 +492,10 @@ function check(options, topology) {
|
|
|
342
492
|
})), `${file}: must default-export ${defaultExport}`);
|
|
343
493
|
};
|
|
344
494
|
const effect = '@modern-js/bff-effect/effect-client';
|
|
495
|
+
const httpApiSources = [
|
|
496
|
+
effect,
|
|
497
|
+
'effect/http-api'
|
|
498
|
+
];
|
|
345
499
|
const clientFactories = new Set([
|
|
346
500
|
'makeEffectHttpApiClient',
|
|
347
501
|
'makeEffectRpcClient'
|
|
@@ -523,6 +677,7 @@ function check(options, topology) {
|
|
|
523
677
|
], [
|
|
524
678
|
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
525
679
|
], [], void 0, appPath);
|
|
680
|
+
if (exists(contract)) diagnostics.push(...foreignClientConstructions(client, appPath, contract, stem, rpc));
|
|
526
681
|
}
|
|
527
682
|
moduleShape(`${appPath}/api/effect-api.ts`, [], [], [
|
|
528
683
|
[
|
|
@@ -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,7 +1,7 @@
|
|
|
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
7
|
import { SourceSyntaxError, consumerParserPlugins, unwrapExpression } from "./source-analysis.js";
|
|
@@ -19,6 +19,12 @@ const ignored = new Set([
|
|
|
19
19
|
]);
|
|
20
20
|
const MAX_CLOSURE_DEPTH = 128;
|
|
21
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
|
+
];
|
|
22
28
|
const normalize = (file)=>file.split(node_path.sep).join('/').replace(/^\.\//u, '');
|
|
23
29
|
const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
24
30
|
const apiApp = (app)=>'vertical' === app.kind && 'ui-only' !== app.surfaceProfile && 'horizontal-remote' !== app.deliveryUnitKind;
|
|
@@ -258,6 +264,150 @@ function check(options, topology) {
|
|
|
258
264
|
}
|
|
259
265
|
return queue.map((item)=>item.file);
|
|
260
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
|
+
];
|
|
410
|
+
};
|
|
261
411
|
const moduleShape = (file, imports, calls, patterns = [], defaultExport, composedAppPath)=>{
|
|
262
412
|
if (!exists(file)) return;
|
|
263
413
|
const ast = parseFile(file);
|
|
@@ -295,6 +445,10 @@ function check(options, topology) {
|
|
|
295
445
|
})), `${file}: must default-export ${defaultExport}`);
|
|
296
446
|
};
|
|
297
447
|
const effect = '@modern-js/bff-effect/effect-client';
|
|
448
|
+
const httpApiSources = [
|
|
449
|
+
effect,
|
|
450
|
+
'effect/http-api'
|
|
451
|
+
];
|
|
298
452
|
const clientFactories = new Set([
|
|
299
453
|
'makeEffectHttpApiClient',
|
|
300
454
|
'makeEffectRpcClient'
|
|
@@ -476,6 +630,7 @@ function check(options, topology) {
|
|
|
476
630
|
], [
|
|
477
631
|
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
478
632
|
], [], void 0, appPath);
|
|
633
|
+
if (exists(contract)) diagnostics.push(...foreignClientConstructions(client, appPath, contract, stem, rpc));
|
|
479
634
|
}
|
|
480
635
|
moduleShape(`${appPath}/api/effect-api.ts`, [], [], [
|
|
481
636
|
[
|
|
@@ -471,4 +471,4 @@ const microVerticalApiBaselineViolation = (stem, filePath, expectation)=>{
|
|
|
471
471
|
throw error;
|
|
472
472
|
}
|
|
473
473
|
};
|
|
474
|
-
export { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation };
|
|
474
|
+
export { camelCaseStem, configuredMicroVerticalApiStem, microVerticalApiBaselineViolation, pascalCaseStem };
|
|
@@ -2,7 +2,7 @@ import "node:module";
|
|
|
2
2
|
import node_fs from "node:fs";
|
|
3
3
|
import node_path from "node:path";
|
|
4
4
|
import { parse } from "@babel/parser";
|
|
5
|
-
import { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation } from "./microvertical-api-baseline.js";
|
|
5
|
+
import { camelCaseStem, configuredMicroVerticalApiStem, microVerticalApiBaselineViolation, pascalCaseStem } from "./microvertical-api-baseline.js";
|
|
6
6
|
import { baselinePackage, resolveBaselinePackageDirectory } from "./microvertical-api-owner.js";
|
|
7
7
|
import { createModuleGraph } from "./module-graph.js";
|
|
8
8
|
import { SourceSyntaxError, consumerParserPlugins, unwrapExpression } from "./source-analysis.js";
|
|
@@ -20,6 +20,12 @@ const ignored = new Set([
|
|
|
20
20
|
]);
|
|
21
21
|
const MAX_CLOSURE_DEPTH = 128;
|
|
22
22
|
const MAX_CLOSURE_MODULES = 2048;
|
|
23
|
+
const MAX_API_HOPS = 128;
|
|
24
|
+
const MAX_API_VISITS = 2048;
|
|
25
|
+
const identitySources = [
|
|
26
|
+
'effect',
|
|
27
|
+
'effect/Function'
|
|
28
|
+
];
|
|
23
29
|
const normalize = (file)=>file.split(node_path.sep).join('/').replace(/^\.\//u, '');
|
|
24
30
|
const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
25
31
|
const apiApp = (app)=>'vertical' === app.kind && 'ui-only' !== app.surfaceProfile && 'horizontal-remote' !== app.deliveryUnitKind;
|
|
@@ -259,6 +265,150 @@ function check(options, topology) {
|
|
|
259
265
|
}
|
|
260
266
|
return queue.map((item)=>item.file);
|
|
261
267
|
};
|
|
268
|
+
const foreignClientConstructions = (client, appPath, contract, stem, rpc)=>{
|
|
269
|
+
const graph = createModuleGraph();
|
|
270
|
+
const contractModule = graph.module(absolute(contract));
|
|
271
|
+
const factory = rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient';
|
|
272
|
+
const ownExport = graph.resolve(contractModule, rpc ? `${pascalCaseStem(stem)}RpcGroup` : `${camelCaseStem(stem)}Api`, 'export');
|
|
273
|
+
const isIdentity = (value)=>'external' === value.kind && identitySources.includes(value.specifier) && 'identity' === value.name && 0 === value.members.length;
|
|
274
|
+
const identityPipe = (module, node)=>{
|
|
275
|
+
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, {
|
|
276
|
+
name: 'pipe'
|
|
277
|
+
}) || 1 !== node.arguments.length) return false;
|
|
278
|
+
const values = graph.evaluate(module, node.arguments[0]);
|
|
279
|
+
return values.length > 0 && values.every(isIdentity);
|
|
280
|
+
};
|
|
281
|
+
const withoutIdentity = (module, value)=>{
|
|
282
|
+
let node = unwrapExpression(value, true);
|
|
283
|
+
for(let pipes = 0; identityPipe(module, node); pipes += 1){
|
|
284
|
+
if (pipes >= MAX_API_HOPS) return value;
|
|
285
|
+
node = unwrapExpression(node.callee.object, true);
|
|
286
|
+
}
|
|
287
|
+
return node;
|
|
288
|
+
};
|
|
289
|
+
const normalizedApi = (initial)=>{
|
|
290
|
+
let binding = initial;
|
|
291
|
+
const seen = new Set();
|
|
292
|
+
for(let hops = 0; hops < MAX_API_HOPS; hops += 1){
|
|
293
|
+
const expression = withoutIdentity(binding.module, binding.expression);
|
|
294
|
+
if (seen.has(expression)) break;
|
|
295
|
+
seen.add(expression);
|
|
296
|
+
if (!__rspack_external__babel_types_6e8337dd.isIdentifier(expression) && !__rspack_external__babel_types_6e8337dd.isMemberExpression(expression)) return {
|
|
297
|
+
...binding,
|
|
298
|
+
expression: expression
|
|
299
|
+
};
|
|
300
|
+
const next = graphBinding(graph, binding.module, expression);
|
|
301
|
+
if (next?.kind !== 'declaration') break;
|
|
302
|
+
binding = next;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const apiBinding = (module, value)=>{
|
|
306
|
+
const binding = graphBinding(graph, module, withoutIdentity(module, value));
|
|
307
|
+
return binding?.kind === 'declaration' ? normalizedApi(binding) : void 0;
|
|
308
|
+
};
|
|
309
|
+
const ownedApis = new Map();
|
|
310
|
+
const own = (binding)=>{
|
|
311
|
+
const identities = ownedApis.get(binding.module.path) ?? new Set();
|
|
312
|
+
identities.add(binding.expression);
|
|
313
|
+
ownedApis.set(binding.module.path, identities);
|
|
314
|
+
};
|
|
315
|
+
const proven = new Set();
|
|
316
|
+
const active = new Set();
|
|
317
|
+
let visits = 0;
|
|
318
|
+
const builtApi = (module, expression)=>{
|
|
319
|
+
const initial = withoutIdentity(module, expression);
|
|
320
|
+
if (active.has(initial) || active.size >= MAX_API_HOPS) return false;
|
|
321
|
+
active.add(initial);
|
|
322
|
+
try {
|
|
323
|
+
const children = [];
|
|
324
|
+
for(let node = initial;;){
|
|
325
|
+
if (++visits > MAX_API_VISITS) return false;
|
|
326
|
+
if (__rspack_external__babel_types_6e8337dd.isIdentifier(node) || __rspack_external__babel_types_6e8337dd.isMemberExpression(node)) {
|
|
327
|
+
const receiver = apiBinding(module, node);
|
|
328
|
+
if (!receiver || !builtApi(receiver.module, receiver.expression)) return false;
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node)) return false;
|
|
332
|
+
const callee = graph.evaluate(module, node.callee);
|
|
333
|
+
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])) {
|
|
334
|
+
if (1 !== node.arguments.length || !__rspack_external__babel_types_6e8337dd.isStringLiteral(node.arguments[0])) return false;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
if (!__rspack_external__babel_types_6e8337dd.isMemberExpression(node.callee) || node.callee.computed || !__rspack_external__babel_types_6e8337dd.isIdentifier(node.callee.property)) return false;
|
|
338
|
+
const method = node.callee.property.name;
|
|
339
|
+
const count = node.arguments.length;
|
|
340
|
+
const [first] = node.arguments;
|
|
341
|
+
if ('addHttpApi' === method && 1 === count) children.push(first);
|
|
342
|
+
else if (!('add' === method && count > 0 || 'annotate' === method && 2 === count || [
|
|
343
|
+
'annotateMerge',
|
|
344
|
+
'middleware'
|
|
345
|
+
].includes(method) && 1 === count || 'prefix' === method && 1 === count && __rspack_external__babel_types_6e8337dd.isStringLiteral(first) && first.value.startsWith('/'))) return false;
|
|
346
|
+
node = withoutIdentity(module, node.callee.object);
|
|
347
|
+
}
|
|
348
|
+
return children.every((child)=>{
|
|
349
|
+
const binding = apiBinding(module, child);
|
|
350
|
+
return void 0 !== binding && collectApi(binding);
|
|
351
|
+
});
|
|
352
|
+
} finally{
|
|
353
|
+
active.delete(initial);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const collectApi = (binding)=>{
|
|
357
|
+
if (proven.has(binding.expression)) return true;
|
|
358
|
+
if (!builtApi(binding.module, binding.expression)) return false;
|
|
359
|
+
proven.add(binding.expression);
|
|
360
|
+
if (binding.module.boundary === contractModule.boundary) own(binding);
|
|
361
|
+
return true;
|
|
362
|
+
};
|
|
363
|
+
if (!rpc && ownExport?.kind === 'declaration') {
|
|
364
|
+
const root = normalizedApi(ownExport);
|
|
365
|
+
if (root && collectApi(root)) own(root);
|
|
366
|
+
else ownedApis.clear();
|
|
367
|
+
}
|
|
368
|
+
const immutablePipeAliases = (module, target)=>{
|
|
369
|
+
if (0 === module.mutations.length) return true;
|
|
370
|
+
let valid = true;
|
|
371
|
+
__rspack_external__babel_types_6e8337dd.traverseFast(module.file, (node)=>{
|
|
372
|
+
if (!__rspack_external__babel_types_6e8337dd.isVariableDeclarator(node) || !__rspack_external__babel_types_6e8337dd.isIdentifier(node.id) || !node.init) return;
|
|
373
|
+
let initializer = unwrapExpression(node.init, true);
|
|
374
|
+
let identityOnly = true;
|
|
375
|
+
let piped = false;
|
|
376
|
+
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, {
|
|
377
|
+
name: 'pipe'
|
|
378
|
+
})){
|
|
379
|
+
piped = true;
|
|
380
|
+
identityOnly &&= identityPipe(module, initializer);
|
|
381
|
+
initializer = unwrapExpression(initializer.callee.object, true);
|
|
382
|
+
}
|
|
383
|
+
if (!piped) return;
|
|
384
|
+
const receiver = apiBinding(module, initializer);
|
|
385
|
+
if (receiver?.module.path !== target.module.path || receiver.expression !== target.expression) return;
|
|
386
|
+
if (!identityOnly || graph.resolveReference(module, node.id)?.kind !== 'declaration') valid = false;
|
|
387
|
+
});
|
|
388
|
+
return valid;
|
|
389
|
+
};
|
|
390
|
+
const ownedBy = (module, argument)=>{
|
|
391
|
+
if (!argument) return false;
|
|
392
|
+
if (rpc) {
|
|
393
|
+
const binding = graphBinding(graph, module, argument);
|
|
394
|
+
return binding?.kind === 'declaration' && ownExport?.kind === 'declaration' && binding.module.path === ownExport.module.path && binding.expression === ownExport.expression;
|
|
395
|
+
}
|
|
396
|
+
const binding = apiBinding(module, argument);
|
|
397
|
+
return void 0 !== binding && !!ownedApis.get(binding.module.path)?.has(binding.expression) && immutablePipeAliases(module, binding);
|
|
398
|
+
};
|
|
399
|
+
const violations = new Set();
|
|
400
|
+
for (const source of composedModules(client, appPath)){
|
|
401
|
+
const module = graph.module(absolute(source));
|
|
402
|
+
__rspack_external__babel_types_6e8337dd.traverseFast(module.file, (node)=>{
|
|
403
|
+
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node) && !__rspack_external__babel_types_6e8337dd.isOptionalCallExpression(node)) return;
|
|
404
|
+
const native = graph.evaluate(module, node.callee).some((value)=>'external' === value.kind && value.specifier === effect && value.name === factory);
|
|
405
|
+
if (native && !ownedBy(module, node.arguments[0])) violations.add(`${source}: must construct the client from its own API contract`);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return [
|
|
409
|
+
...violations
|
|
410
|
+
];
|
|
411
|
+
};
|
|
262
412
|
const moduleShape = (file, imports, calls, patterns = [], defaultExport, composedAppPath)=>{
|
|
263
413
|
if (!exists(file)) return;
|
|
264
414
|
const ast = parseFile(file);
|
|
@@ -296,6 +446,10 @@ function check(options, topology) {
|
|
|
296
446
|
})), `${file}: must default-export ${defaultExport}`);
|
|
297
447
|
};
|
|
298
448
|
const effect = '@modern-js/bff-effect/effect-client';
|
|
449
|
+
const httpApiSources = [
|
|
450
|
+
effect,
|
|
451
|
+
'effect/http-api'
|
|
452
|
+
];
|
|
299
453
|
const clientFactories = new Set([
|
|
300
454
|
'makeEffectHttpApiClient',
|
|
301
455
|
'makeEffectRpcClient'
|
|
@@ -477,6 +631,7 @@ function check(options, topology) {
|
|
|
477
631
|
], [
|
|
478
632
|
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
479
633
|
], [], void 0, appPath);
|
|
634
|
+
if (exists(contract)) diagnostics.push(...foreignClientConstructions(client, appPath, contract, stem, rpc));
|
|
480
635
|
}
|
|
481
636
|
moduleShape(`${appPath}/api/effect-api.ts`, [], [], [
|
|
482
637
|
[
|
|
@@ -1,3 +1,7 @@
|
|
|
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;
|
|
1
5
|
export interface MicroVerticalApiBaselineExpectation {
|
|
2
6
|
/** Optional override for operation IDs outside reachable endpoint naming conventions. */
|
|
3
7
|
readonly operationPaths?: Readonly<Record<string, string>>;
|