@bleedingdev/modern-js-code-tools 3.9.3-ultramodern.2 → 3.9.3-ultramodern.4

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.
@@ -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>>;
package/package.json CHANGED
@@ -23,7 +23,7 @@
23
23
  "engines": {
24
24
  "node": ">=20"
25
25
  },
26
- "version": "3.9.3-ultramodern.2",
26
+ "version": "3.9.3-ultramodern.4",
27
27
  "types": "./dist/types/index.d.ts",
28
28
  "main": "./dist/esm-node/index.js",
29
29
  "typesVersions": {