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

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.
Files changed (38) hide show
  1. package/bin/modern-api-check-files.mjs +4 -1
  2. package/bin/modern-api-check.mjs +1 -1
  3. package/bin/modern-i18n-check.mjs +4 -0
  4. package/dist/cjs/cli/microvertical-api-check.cjs +36 -7
  5. package/dist/cjs/cli/workspace-source-check.cjs +90 -17
  6. package/dist/cjs/microvertical-api-baseline.cjs +81 -244
  7. package/dist/cjs/microvertical-api-boundary.cjs +352 -64
  8. package/dist/cjs/microvertical-api-owner.cjs +5 -2
  9. package/dist/cjs/module-graph.cjs +1862 -0
  10. package/dist/cjs/oxlint-plugin/rules/strict-effect-api-boundaries.cjs +1 -1
  11. package/dist/cjs/source-analysis.cjs +108 -0
  12. package/dist/cjs/strict-effect-runtime.cjs +75 -57
  13. package/dist/esm/cli/microvertical-api-check.js +26 -7
  14. package/dist/esm/cli/workspace-source-check.js +86 -13
  15. package/dist/esm/microvertical-api-baseline.js +66 -214
  16. package/dist/esm/microvertical-api-boundary.js +350 -65
  17. package/dist/esm/microvertical-api-owner.js +5 -2
  18. package/dist/esm/module-graph.js +1804 -0
  19. package/dist/esm/oxlint-plugin/rules/strict-effect-api-boundaries.js +1 -1
  20. package/dist/esm/source-analysis.js +48 -0
  21. package/dist/esm/strict-effect-runtime.js +75 -56
  22. package/dist/esm-node/cli/microvertical-api-check.js +26 -7
  23. package/dist/esm-node/cli/workspace-source-check.js +86 -13
  24. package/dist/esm-node/microvertical-api-baseline.js +66 -214
  25. package/dist/esm-node/microvertical-api-boundary.js +350 -65
  26. package/dist/esm-node/microvertical-api-owner.js +5 -2
  27. package/dist/esm-node/module-graph.js +1805 -0
  28. package/dist/esm-node/oxlint-plugin/rules/strict-effect-api-boundaries.js +1 -1
  29. package/dist/esm-node/source-analysis.js +49 -0
  30. package/dist/esm-node/strict-effect-runtime.js +75 -56
  31. package/dist/types/cli/microvertical-api-check.d.ts +1 -1
  32. package/dist/types/cli/workspace-source-check.d.ts +1 -1
  33. package/dist/types/microvertical-api-baseline.d.ts +0 -5
  34. package/dist/types/microvertical-api-boundary.d.ts +17 -2
  35. package/dist/types/microvertical-api-owner.d.ts +1 -1
  36. package/dist/types/module-graph.d.ts +100 -0
  37. package/dist/types/source-analysis.d.ts +13 -0
  38. package/package.json +17 -13
@@ -1,8 +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, consumerParserPlugins, microVerticalApiBaselineViolation } from "./microvertical-api-baseline.js";
4
+ import { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation } from "./microvertical-api-baseline.js";
5
5
  import { baselinePackage, resolveBaselinePackageDirectory } from "./microvertical-api-owner.js";
6
+ import { createModuleGraph } from "./module-graph.js";
7
+ import { SourceSyntaxError, consumerParserPlugins, unwrapExpression } from "./source-analysis.js";
6
8
  import { createEffectApiImportResolver, strictEffectRuntimeTopologyViolation } from "./strict-effect-runtime.js";
7
9
  import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
8
10
  const ignored = new Set([
@@ -15,17 +17,48 @@ const ignored = new Set([
15
17
  'node_modules',
16
18
  'repos'
17
19
  ]);
20
+ const MAX_CLOSURE_DEPTH = 128;
21
+ const MAX_CLOSURE_MODULES = 2048;
18
22
  const normalize = (file)=>file.split(node_path.sep).join('/').replace(/^\.\//u, '');
19
23
  const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
20
24
  const apiApp = (app)=>'vertical' === app.kind && 'ui-only' !== app.surfaceProfile && 'horizontal-remote' !== app.deliveryUnitKind;
21
25
  function configuredApps(root, supplied) {
22
- const source = supplied ?? JSON.parse(node_fs.readFileSync(node_path.join(root, '.modernjs/ultramodern.json'), 'utf8')).topology?.apps;
23
- if (!Array.isArray(source)) throw new Error('.modernjs/ultramodern.json: topology.apps must be an array');
26
+ const topology = supplied ? void 0 : JSON.parse(node_fs.readFileSync(node_path.join(root, 'topology/reference-topology.json'), 'utf8'));
27
+ if (topology && (!topology.shell || !Array.isArray(topology.verticals) || void 0 !== topology.shells && !Array.isArray(topology.shells))) throw new Error('topology/reference-topology.json: shell and verticals must be present');
28
+ const entries = topology?.shell ? [
29
+ topology.shell,
30
+ ...topology.shells ?? [],
31
+ ...topology.verticals ?? []
32
+ ] : void 0;
33
+ const source = supplied ?? entries?.map((entry)=>{
34
+ if (!entry || 'string' != typeof entry.path || !entry.path || node_path.isAbsolute(entry.path) || entry.path.split(/[\\/]/u).includes('..')) throw new Error('topology/reference-topology.json: each app must have an explicit workspace path');
35
+ const packageFile = node_path.join(root, entry.path, 'package.json');
36
+ const manifest = JSON.parse(node_fs.readFileSync(packageFile, 'utf8'));
37
+ if ('string' != typeof manifest.name || !manifest.name || void 0 !== entry.package && entry.package !== manifest.name) throw new Error(`${entry.path}/package.json: package name must match topology`);
38
+ const api = entry.api;
39
+ const stem = api?.stem ?? api?.readiness?.endpoint?.match(/^\/([a-z0-9]+(?:-[a-z0-9]+)*)\/readiness$/u)?.[1];
40
+ return {
41
+ id: entry.id,
42
+ kind: entry.kind,
43
+ path: entry.path,
44
+ package: manifest.name,
45
+ surfaceProfile: entry.surfaceProfile,
46
+ deliveryUnitKind: entry.deliveryUnitKind,
47
+ ...void 0 === api ? {} : {
48
+ api: {
49
+ stem,
50
+ prefix: api.bff?.prefix,
51
+ protocol: api.protocol
52
+ }
53
+ }
54
+ };
55
+ });
56
+ if (!Array.isArray(source)) throw new Error('topology/reference-topology.json: shell and verticals must describe apps');
24
57
  const paths = new Set();
25
58
  for (const app of source){
26
- if (!app || 'object' != typeof app || 'string' != typeof app.path || !app.path || node_path.isAbsolute(app.path) || app.path.split(/[\\/]/u).includes('..')) throw new Error('.modernjs/ultramodern.json: each app must have a relative workspace path');
59
+ if (!app || 'object' != typeof app || 'string' != typeof app.path || !app.path || node_path.isAbsolute(app.path) || app.path.split(/[\\/]/u).includes('..')) throw new Error('topology/reference-topology.json: each app must have a relative workspace path');
27
60
  const key = normalize(app.path);
28
- if (paths.has(key)) throw new Error(`.modernjs/ultramodern.json: duplicate app path ${key}`);
61
+ if (paths.has(key)) throw new Error(`topology/reference-topology.json: duplicate app path ${key}`);
29
62
  paths.add(key);
30
63
  for (const name of [
31
64
  'id',
@@ -34,7 +67,7 @@ function configuredApps(root, supplied) {
34
67
  'surfaceProfile',
35
68
  'deliveryUnitKind'
36
69
  ])if (void 0 !== app[name] && 'string' != typeof app[name]) throw new Error(`${key}: ${name} must be a string`);
37
- for (const [field, allowed] of Object.entries({
70
+ const allowedFields = {
38
71
  kind: [
39
72
  'shell',
40
73
  'vertical'
@@ -48,7 +81,11 @@ function configuredApps(root, supplied) {
48
81
  'microvertical',
49
82
  'horizontal-remote'
50
83
  ]
51
- }))if (void 0 !== app[field] && !allowed.includes(app[field])) throw new Error(`${key}: invalid ${field}`);
84
+ };
85
+ for (const field of Object.keys(allowedFields)){
86
+ const value = app[field];
87
+ if ((void 0 !== value || 'kind' === field) && ('string' != typeof value || !allowedFields[field].includes(value))) throw new Error(`${key}: invalid ${field}`);
88
+ }
52
89
  if (void 0 !== app.api) {
53
90
  if (!app.api || 'object' != typeof app.api || Array.isArray(app.api)) throw new Error(`${key}: api must be an object`);
54
91
  if (void 0 !== app.api.protocol && ![
@@ -89,6 +126,9 @@ function check(options, topology) {
89
126
  };
90
127
  try {
91
128
  const apps = configuredApps(root, options.configuredApps);
129
+ const workspace = node_fs.realpathSync(root);
130
+ const realRelative = (file)=>normalize(node_path.relative(workspace, node_fs.realpathSync(absolute(file))));
131
+ const inside = (directory, real)=>real.startsWith(`${directory}${node_path.sep}`);
92
132
  const byPath = new Map(apps.map((app)=>[
93
133
  normalize(app.path),
94
134
  app
@@ -129,39 +169,100 @@ function check(options, topology) {
129
169
  const resolveRelative = (fromFile, specifier)=>{
130
170
  if (!/^\.\.?\//u.test(specifier)) return;
131
171
  const base = node_path.posix.join(node_path.posix.dirname(fromFile), specifier).replace(/\.(?:[cm]?[jt]sx?)$/u, '');
132
- return [
172
+ const candidate = [
133
173
  `${base}.ts`,
134
174
  `${base}.tsx`,
135
175
  `${base}/index.ts`,
136
176
  `${base}/index.tsx`
137
- ].find((candidate)=>exists(candidate));
177
+ ].find((file)=>exists(file));
178
+ return void 0 === candidate ? void 0 : realRelative(candidate);
179
+ };
180
+ const graphBinding = (graph, module, expression, seen = new Set())=>{
181
+ if (seen.has(expression) || seen.size >= 128) return;
182
+ seen.add(expression);
183
+ const binding = __rspack_external__babel_types_6e8337dd.isIdentifier(expression) || __rspack_external__babel_types_6e8337dd.isMemberExpression(expression) ? graph.resolveReference(module, expression) : void 0;
184
+ 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;
138
185
  };
139
- const composedModules = (file)=>{
140
- const order = [];
141
- const seen = new Set();
186
+ const clientExportFile = (appPath, exported)=>{
187
+ if ('string' != typeof exported || !exported.startsWith('./') || exported.split(/[\\/]/u).includes('..') || !/\.[cm]?[jt]sx?$/u.test(exported)) return;
188
+ const appDirectory = absolute(appPath);
189
+ const candidate = node_path.resolve(appDirectory, exported);
190
+ try {
191
+ const workspace = node_fs.realpathSync(root);
192
+ const owner = node_fs.realpathSync(appDirectory);
193
+ const real = node_fs.realpathSync(candidate);
194
+ if (!owner.startsWith(`${workspace}${node_path.sep}`) || !real.startsWith(`${owner}${node_path.sep}`) || !node_fs.statSync(real).isFile()) return;
195
+ } catch {
196
+ return;
197
+ }
198
+ return normalize(node_path.relative(root, candidate));
199
+ };
200
+ const composedModules = (file, appPath)=>{
201
+ const owner = node_fs.realpathSync(absolute(appPath));
202
+ if (!inside(owner, node_fs.realpathSync(absolute(file)))) throw new Error(`consumer module closure escapes its owner: ${file}`);
203
+ const entry = realRelative(file);
204
+ const discovered = new Set([
205
+ entry
206
+ ]);
142
207
  const queue = [
143
- file
208
+ {
209
+ file: entry,
210
+ depth: 0
211
+ }
144
212
  ];
145
- while(queue.length > 0 && order.length < 128){
146
- const current = queue.shift();
147
- if (seen.has(current)) continue;
148
- seen.add(current);
149
- order.push(current);
213
+ let graph;
214
+ for (const { file: current, depth } of queue){
150
215
  const ast = parseFile(current);
151
216
  if (!ast) continue;
152
- for (const statement of ast.program.body){
153
- const source = (__rspack_external__babel_types_6e8337dd.isImportDeclaration(statement) || __rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || __rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement)) && statement.source ? statement.source.value : void 0;
154
- const resolved = void 0 === source ? void 0 : resolveRelative(current, source);
155
- if (void 0 !== resolved && !seen.has(resolved)) queue.push(resolved);
217
+ const sources = [];
218
+ let callsRequire = false;
219
+ 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);
220
+ __rspack_external__babel_types_6e8337dd.traverseFast(ast, (node)=>{
221
+ if (__rspack_external__babel_types_6e8337dd.isImportExpression(node)) {
222
+ if (!__rspack_external__babel_types_6e8337dd.isStringLiteral(node.source)) throw new Error(`consumer module closure has nonliteral runtime dependency: ${current}`);
223
+ sources.push(node.source.value);
224
+ }
225
+ 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), {
226
+ name: 'require'
227
+ })) callsRequire = true;
228
+ });
229
+ if (callsRequire) {
230
+ graph ??= createModuleGraph();
231
+ const module = graph.module(absolute(current));
232
+ __rspack_external__babel_types_6e8337dd.traverseFast(module.file, (node)=>{
233
+ if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node) && !__rspack_external__babel_types_6e8337dd.isOptionalCallExpression(node)) return;
234
+ const callee = unwrapExpression(node.callee, true);
235
+ if (!__rspack_external__babel_types_6e8337dd.isIdentifier(callee, {
236
+ name: 'require'
237
+ }) || !graph.isUnboundReference(module, callee)) return;
238
+ const [argument] = node.arguments;
239
+ if (1 !== node.arguments.length || !__rspack_external__babel_types_6e8337dd.isStringLiteral(argument)) throw new Error(`consumer module closure has nonliteral runtime dependency: ${current}`);
240
+ sources.push(argument.value);
241
+ });
242
+ }
243
+ for (const source of sources){
244
+ if (!/^\.\.?\//u.test(source)) continue;
245
+ const resolved = resolveRelative(current, source);
246
+ if (void 0 === resolved) throw new Error(`consumer module closure has unresolved relative dependency: ${current} -> ${source}`);
247
+ if (!inside(owner, node_path.join(workspace, resolved))) throw new Error(`consumer module closure escapes its owner: ${current} -> ${source}`);
248
+ if (!discovered.has(resolved)) {
249
+ if (depth + 1 >= MAX_CLOSURE_DEPTH) throw new Error(`consumer module closure exceeds ${MAX_CLOSURE_DEPTH} import-depth analysis budget`);
250
+ if (discovered.size >= MAX_CLOSURE_MODULES) throw new Error(`consumer module closure exceeds ${MAX_CLOSURE_MODULES}-module analysis budget`);
251
+ discovered.add(resolved);
252
+ queue.push({
253
+ file: resolved,
254
+ depth: depth + 1
255
+ });
256
+ }
156
257
  }
157
258
  }
158
- return order;
259
+ return queue.map((item)=>item.file);
159
260
  };
160
- const moduleShape = (file, imports, calls, patterns = [], defaultExport, composed = false)=>{
261
+ const moduleShape = (file, imports, calls, patterns = [], defaultExport, composedAppPath)=>{
161
262
  if (!exists(file)) return;
162
263
  const ast = parseFile(file);
163
264
  if (!ast) return;
164
- const shapeFiles = composed ? composedModules(file) : [
265
+ const shapeFiles = composedAppPath ? composedModules(file, composedAppPath) : [
165
266
  file
166
267
  ];
167
268
  const body = shapeFiles.flatMap((shapeFile)=>parseFile(shapeFile)?.program.body ?? []);
@@ -194,6 +295,10 @@ function check(options, topology) {
194
295
  })), `${file}: must default-export ${defaultExport}`);
195
296
  };
196
297
  const effect = '@modern-js/bff-effect/effect-client';
298
+ const clientFactories = new Set([
299
+ 'makeEffectHttpApiClient',
300
+ 'makeEffectRpcClient'
301
+ ]);
197
302
  const appDirectories = [
198
303
  ...new Set([
199
304
  ...directories('apps'),
@@ -254,7 +359,6 @@ function check(options, topology) {
254
359
  if (violation) diagnostics.push(`${file}: ${violation}`);
255
360
  }
256
361
  });
257
- let owner;
258
362
  for (const appPath of appDirectories)guarded(appPath, ()=>{
259
363
  const app = byPath.get(appPath);
260
364
  const emits = app ? apiApp(app) || 'vertical' !== app.kind && void 0 !== app.api : appPath.startsWith('verticals/') || exists(`${appPath}/api/index.ts`) || exists(`${appPath}/shared/api.ts`);
@@ -271,19 +375,25 @@ function check(options, topology) {
271
375
  }
272
376
  return;
273
377
  }
274
- if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in .modernjs/ultramodern.json`);
378
+ if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in topology/reference-topology.json`);
275
379
  const stem = app?.api?.stem ?? node_path.posix.basename(appPath);
276
380
  const rpc = app?.api?.protocol === 'rpc';
277
381
  const contract = `${appPath}/shared/${rpc ? 'rpc' : 'api'}.ts`;
278
- const client = `${appPath}/src/api/${stem}-${rpc ? 'rpc-client' : 'client'}.ts`;
279
382
  const entry = `${appPath}/api/index.ts`;
280
383
  for (const file of [
281
384
  entry,
282
- contract,
283
- client
385
+ contract
284
386
  ])assert(exists(file), `${file}: required API surface is missing`);
387
+ const packageFile = `${appPath}/package.json`;
388
+ assert(exists(packageFile), `${packageFile}: required API package is missing`);
389
+ if (!exists(packageFile)) return;
390
+ const manifest = JSON.parse(read(packageFile));
391
+ assert(manifest.exports?.['./api'] === `./shared/${rpc ? 'rpc' : 'api'}.ts`, `${packageFile}: invalid ./api export`);
392
+ const clientKey = `./api/${rpc ? 'rpc-client' : 'client'}`;
393
+ const client = clientExportFile(appPath, manifest.exports?.[clientKey]);
394
+ assert(void 0 !== client, `${packageFile}: invalid API client export`);
395
+ assert(manifest.exports?.[`./api/${rpc ? 'client' : 'rpc-client'}`] === void 0, `${packageFile}: forbidden opposite protocol client export`);
285
396
  noPath(`${appPath}/shared/${rpc ? 'api' : 'rpc'}.ts`, `must not emit a ${rpc ? 'REST' : 'RPC'} contract`);
286
- noPath(`${appPath}/src/api/${stem}-${rpc ? 'client' : 'rpc-client'}.ts`, `must not emit a ${rpc ? 'REST' : 'RPC'} client`);
287
397
  if (rpc) {
288
398
  moduleShape(entry, [
289
399
  [
@@ -314,7 +424,7 @@ function check(options, topology) {
314
424
  ], 'apiRuntime');
315
425
  moduleShape(contract, [
316
426
  [
317
- 'effect/unstable/rpc',
427
+ 'effect/rpc',
318
428
  [
319
429
  'Rpc',
320
430
  'RpcGroup'
@@ -332,7 +442,7 @@ function check(options, topology) {
332
442
  'Schema.Struct'
333
443
  ]);
334
444
  } else if (exists(contract)) {
335
- owner ??= options.baselinePackageDirectory ?? resolveBaselinePackageDirectory(node_path.join(root, 'package.json'));
445
+ const owner = options.baselinePackageDirectory ?? resolveBaselinePackageDirectory(absolute(contract));
336
446
  const apiPrefix = app?.api?.prefix ?? `/${stem}-api`;
337
447
  const basePath = `${apiPrefix}/${stem}`;
338
448
  const expectation = {
@@ -349,21 +459,24 @@ function check(options, topology) {
349
459
  const violation = microVerticalApiBaselineViolation(stem, absolute(contract), expectation);
350
460
  if (violation) diagnostics.push(`${contract}: ${violation}`);
351
461
  }
352
- moduleShape(client, [
353
- [
354
- effect,
462
+ if (client) {
463
+ const relativeContract = node_path.posix.relative(node_path.posix.dirname(client), contract);
464
+ moduleShape(client, [
465
+ [
466
+ effect,
467
+ [
468
+ 'Effect',
469
+ rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
470
+ ]
471
+ ],
355
472
  [
356
- 'Effect',
357
- rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
473
+ relativeContract.startsWith('.') ? relativeContract : `./${relativeContract}`,
474
+ []
358
475
  ]
359
- ],
360
- [
361
- rpc ? '../../shared/rpc.ts' : '../../shared/api',
362
- []
363
- ]
364
- ], [
365
- rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
366
- ], [], void 0, true);
476
+ ], [
477
+ rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
478
+ ], [], void 0, appPath);
479
+ }
367
480
  moduleShape(`${appPath}/api/effect-api.ts`, [], [], [
368
481
  [
369
482
  /\bbackendFederationContract\b/u,
@@ -404,28 +517,200 @@ function check(options, topology) {
404
517
  'must enable strictEffectApproach'
405
518
  ]
406
519
  ]);
407
- const packageFile = `${appPath}/package.json`;
408
- if (exists(packageFile)) {
409
- const manifest = JSON.parse(read(packageFile));
410
- assert(manifest.exports?.['./api'] === `./shared/${rpc ? 'rpc' : 'api'}.ts`, `${packageFile}: invalid ./api export`);
411
- assert(manifest.exports?.[`./api/${rpc ? 'rpc-client' : 'client'}`] === `./src/api/${stem}-${rpc ? 'rpc-client' : 'client'}.ts`, `${packageFile}: invalid API client export`);
412
- assert(manifest.exports?.[`./api/${rpc ? 'client' : 'rpc-client'}`] === void 0, `${packageFile}: forbidden opposite protocol client export`);
520
+ });
521
+ const sourceRules = options.sourceRules ?? [];
522
+ for (const file of sourceRules.length ? sourceFiles : [])if (/\.[cm]?[jt]sx?$/u.test(file)) guarded(file, ()=>{
523
+ const graph = createModuleGraph();
524
+ let module;
525
+ try {
526
+ module = graph.module(absolute(file));
527
+ } catch (error) {
528
+ if (!(error instanceof SourceSyntaxError)) throw error;
529
+ diagnostics.push(`${file}: source rules need parseable source (${error.message})`);
530
+ return;
531
+ }
532
+ for (const rule of sourceRules){
533
+ const messages = rule({
534
+ file,
535
+ module,
536
+ graph
537
+ });
538
+ if (!Array.isArray(messages) || !messages.every((message)=>'string' == typeof message)) throw new Error(`rule ${rule.name || 'anonymous'} must return an array of violation messages (return [] when the file passes)`);
539
+ for (const message of messages)diagnostics.push(`${file}: ${message}`);
413
540
  }
414
541
  });
415
- const shell = 'apps/shell-super-app';
416
- const verticals = apps.filter((app)=>app.path.startsWith('verticals/') && apiApp(app));
417
- if (exists(shell) && verticals.length) {
418
- const file = `${shell}/src/api/vertical-clients.ts`;
419
- assert(exists(file), `${file}: must aggregate vertical API clients`);
420
- if (exists(file)) guarded(file, ()=>{
421
- const ast = parseFile(file);
422
- for (const app of verticals){
423
- const specifier = `${app.package}/api/${app.api?.protocol === 'rpc' ? 'rpc-client' : 'client'}`;
424
- assert(ast?.program.body.some((item)=>__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(item) && item.source?.value === specifier), `${file}: must re-export ${specifier}`);
542
+ const checkShellExports = (shellPath)=>{
543
+ const packageFile = `${shellPath}/package.json`;
544
+ const manifest = JSON.parse(read(packageFile));
545
+ noPath(`${shellPath}/src/api/vertical-clients.ts`, 'Shell client registries are forbidden; import each owner client directly');
546
+ assert(!Object.hasOwn(manifest.exports ?? {}, './api/clients'), `${packageFile}: Shell ./api/clients exports are forbidden`);
547
+ const owner = node_fs.realpathSync(absolute(shellPath));
548
+ const invalidExport = ()=>assert(false, `${packageFile}: invalid Shell public source export`);
549
+ const targets = (exported)=>'string' == typeof exported ? [
550
+ exported
551
+ ] : exported && 'object' == typeof exported ? Object.values(exported).flatMap(targets) : [];
552
+ const filesUnder = (directory, seen = new Set())=>{
553
+ const real = node_fs.realpathSync(absolute(directory));
554
+ if (real !== owner && !inside(owner, real)) {
555
+ invalidExport();
556
+ return [];
557
+ }
558
+ if (seen.has(real)) return [];
559
+ seen.add(real);
560
+ return node_fs.readdirSync(absolute(directory), {
561
+ withFileTypes: true
562
+ }).flatMap((entry)=>{
563
+ if (ignored.has(entry.name)) return [];
564
+ const file = `${directory}/${entry.name}`;
565
+ const stat = node_fs.statSync(absolute(file));
566
+ return stat.isDirectory() ? filesUnder(file, seen) : stat.isFile() ? [
567
+ file
568
+ ] : [];
569
+ });
570
+ };
571
+ const exportedFiles = (target)=>{
572
+ if (!target.includes('*')) return [
573
+ `${shellPath}/${target.slice(2)}`
574
+ ];
575
+ const prefix = target.slice(0, target.indexOf('*'));
576
+ const pattern = new RegExp(`^${target.split('*').map((part)=>part.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&')).join('.*')}$`, 'u');
577
+ return filesUnder(`${shellPath}/${prefix.slice(2, prefix.lastIndexOf('/'))}`).filter((file)=>pattern.test(`./${node_path.posix.relative(shellPath, file)}`));
578
+ };
579
+ const ownClientPrefix = `${manifest.name}/api/`;
580
+ const foreignSpecifier = (specifier)=>/\/api\/(?:rpc-)?client$/u.test(specifier) && !specifier.startsWith(ownClientPrefix);
581
+ for (const target of targets(manifest.exports))if (/\.[cm]?[jt]sx?$/u.test(target)) {
582
+ if (!target.startsWith('./') || target.split(/[\\/]/u).includes('..')) {
583
+ invalidExport();
584
+ continue;
585
+ }
586
+ for (const candidate of exportedFiles(target)){
587
+ const file = clientExportFile(shellPath, `./${node_path.posix.relative(shellPath, candidate)}`);
588
+ if (!file) {
589
+ invalidExport();
590
+ continue;
591
+ }
592
+ assert(!exposesForeignClient(file, owner, foreignSpecifier), `${file}: Shell public exports must not expose another owner's API client`);
593
+ }
594
+ }
595
+ };
596
+ const exposesForeignClient = (entry, owner, foreignSpecifier)=>{
597
+ const graph = createModuleGraph();
598
+ const seenValues = new Set();
599
+ const seenExports = new Map();
600
+ const workspacePath = (module)=>normalize(node_path.relative(workspace, module.path));
601
+ const resolveSource = (from, specifier)=>resolveRelative(from, specifier) ?? ownerClients.get(specifier);
602
+ const returnExpressions = (node)=>{
603
+ if (__rspack_external__babel_types_6e8337dd.isReturnStatement(node)) return node.argument ? [
604
+ node.argument
605
+ ] : [];
606
+ if (__rspack_external__babel_types_6e8337dd.isFunction(node)) return [];
607
+ return (__rspack_external__babel_types_6e8337dd.VISITOR_KEYS[node.type] ?? []).flatMap((key)=>{
608
+ const value = Reflect.get(node, key);
609
+ return (Array.isArray(value) ? value : [
610
+ value
611
+ ]).flatMap((child)=>__rspack_external__babel_types_6e8337dd.isNode(child) ? returnExpressions(child) : []);
612
+ });
613
+ };
614
+ const nestedValues = (node)=>{
615
+ if (__rspack_external__babel_types_6e8337dd.isObjectExpression(node)) return node.properties.flatMap((property)=>__rspack_external__babel_types_6e8337dd.isObjectProperty(property) ? [
616
+ property.value
617
+ ] : __rspack_external__babel_types_6e8337dd.isObjectMethod(property) ? returnExpressions(property.body) : __rspack_external__babel_types_6e8337dd.isSpreadElement(property) ? [
618
+ property.argument
619
+ ] : []);
620
+ if (__rspack_external__babel_types_6e8337dd.isArrayExpression(node)) return node.elements.filter((element)=>null !== element);
621
+ if (__rspack_external__babel_types_6e8337dd.isFunction(node)) return __rspack_external__babel_types_6e8337dd.isBlockStatement(node.body) ? returnExpressions(node.body) : [
622
+ node.body
623
+ ];
624
+ return [];
625
+ };
626
+ const foreignValue = (value, from)=>{
627
+ if ('external' === value.kind) {
628
+ const source = resolveSource(workspacePath(from), value.specifier);
629
+ if (!source) return foreignSpecifier(value.specifier);
630
+ return '*' === value.name ? inspect(source) : exportedValue(source, value.name);
425
631
  }
426
- });
632
+ if ('unresolved' === value.kind && __rspack_external__babel_types_6e8337dd.isImportDeclaration(value.node) && value.node.specifiers.some((specifier)=>__rspack_external__babel_types_6e8337dd.isImportNamespaceSpecifier(specifier))) {
633
+ const specifier = value.node.source.value;
634
+ const source = resolveSource(workspacePath(value.module), specifier);
635
+ return source ? inspect(source) : foreignSpecifier(specifier);
636
+ }
637
+ if ('node' !== value.kind || seenValues.has(value.node)) return false;
638
+ seenValues.add(value.node);
639
+ const { node, module } = value;
640
+ 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))) {
641
+ const [argument] = node.arguments;
642
+ const binding = argument ? graphBinding(graph, module, argument) : void 0;
643
+ if (module.boundary !== owner || binding?.kind !== 'declaration' || binding.module.boundary !== owner) return true;
644
+ }
645
+ return nestedValues(node).some((expression)=>graph.evaluate(module, expression).some((nested)=>foreignValue(nested, module)));
646
+ };
647
+ const exportName = (node)=>__rspack_external__babel_types_6e8337dd.isIdentifier(node) ? node.name : node.value;
648
+ const names = (source, visited = new Set())=>{
649
+ if (visited.has(source)) return [];
650
+ visited.add(source);
651
+ return graph.module(absolute(source)).file.program.body.flatMap((statement)=>{
652
+ if (__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement)) return [
653
+ 'default'
654
+ ];
655
+ if (!__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) && !__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement) || 'type' === statement.exportKind) return [];
656
+ if (__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement)) {
657
+ const target = resolveSource(source, statement.source.value);
658
+ return target ? names(target, visited).filter((name)=>'default' !== name) : [];
659
+ }
660
+ 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 ? [] : [
661
+ exportName(specifier.exported)
662
+ ]);
663
+ });
664
+ };
665
+ const exportedValue = (source, name)=>{
666
+ const visited = seenExports.get(source) ?? new Set();
667
+ if (visited.has(name)) return false;
668
+ visited.add(name);
669
+ seenExports.set(source, visited);
670
+ const module = graph.module(absolute(source));
671
+ const evaluates = (from, node)=>graph.evaluate(from, node).some((value)=>foreignValue(value, from));
672
+ const binding = graph.resolve(module, name, 'export');
673
+ if (binding?.kind === 'external') return foreignValue({
674
+ ...binding,
675
+ members: []
676
+ }, module);
677
+ if (binding?.kind === 'declaration') return evaluates(binding.module, binding.expression);
678
+ for (const statement of module.file.program.body){
679
+ if (__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement) && 'default' === name) return evaluates(module, statement.declaration);
680
+ if (!__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || 'type' === statement.exportKind) continue;
681
+ if (__rspack_external__babel_types_6e8337dd.isFunctionDeclaration(statement.declaration) && statement.declaration.id?.name === name) return evaluates(module, statement.declaration);
682
+ const specifier = statement.specifiers.find((item)=>exportName(item.exported) === name);
683
+ if (!specifier) continue;
684
+ if (__rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier) && 'type' === specifier.exportKind) return false;
685
+ if (!statement.source) return __rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier) ? evaluates(module, specifier.local) : false;
686
+ const target = resolveSource(source, statement.source.value);
687
+ if (!target) return foreignSpecifier(statement.source.value);
688
+ if (__rspack_external__babel_types_6e8337dd.isExportNamespaceSpecifier(specifier)) return inspect(target);
689
+ if (__rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier)) return exportedValue(target, exportName(specifier.local));
690
+ }
691
+ return module.file.program.body.some((statement)=>{
692
+ if (!__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement) || 'type' === statement.exportKind) return false;
693
+ const target = resolveSource(source, statement.source.value);
694
+ return !!target && names(target).includes(name) && exportedValue(target, name);
695
+ });
696
+ };
697
+ const inspect = (source)=>[
698
+ ...new Set(names(source))
699
+ ].some((name)=>exportedValue(source, name));
700
+ return inspect(entry);
701
+ };
702
+ const ownerClients = new Map();
703
+ for (const app of apps)if (exists(`${app.path}/package.json`)) {
704
+ const manifest = JSON.parse(read(`${app.path}/package.json`));
705
+ for (const key of [
706
+ './api/client',
707
+ './api/rpc-client'
708
+ ]){
709
+ const file = clientExportFile(app.path, manifest.exports?.[key]);
710
+ if (file) ownerClients.set(`${manifest.name}${key.slice(1)}`, file);
711
+ }
427
712
  }
428
- if (exists(`${shell}/package.json`)) guarded(`${shell}/package.json`, ()=>assert(JSON.parse(read(`${shell}/package.json`)).exports?.['./api/clients'] === './src/api/vertical-clients.ts', `${shell}/package.json: must export ./api/clients`));
713
+ for (const shell of apps.filter((app)=>'shell' === app.kind))guarded(shell.path, ()=>checkShellExports(normalize(shell.path)));
429
714
  if (exists('topology/reference-topology.json')) guarded('topology/reference-topology.json', ()=>{
430
715
  for (const vertical of JSON.parse(read('topology/reference-topology.json')).verticals ?? []){
431
716
  if (vertical.api?.runtime === 'effect') {
@@ -446,4 +731,4 @@ function check(options, topology) {
446
731
  }
447
732
  const checkMicroVerticalApiBoundaries = (options)=>check(options, true);
448
733
  const checkMicroVerticalApiConsumerFiles = (options)=>check(options, false);
449
- export { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles, configuredMicroVerticalApiStem, microVerticalApiBaselineViolation };
734
+ export { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles, configuredMicroVerticalApiStem, createModuleGraph, microVerticalApiBaselineViolation };
@@ -36,7 +36,7 @@ function importTarget(value) {
36
36
  if (resolved) return resolved;
37
37
  }
38
38
  }
39
- function baselinePublicIdentityIsExact(fromFile, expectation) {
39
+ function baselinePublicIdentityIsExact(fromFile, expectation, reportMismatch) {
40
40
  const owner = node_fs.realpathSync(expectation.baselinePackageDirectory);
41
41
  const manifest = JSON.parse(node_fs.readFileSync(node_path.join(owner, 'package.json'), 'utf8'));
42
42
  if (expectation.baselinePackage !== baselinePackage || ![
@@ -50,7 +50,10 @@ function baselinePublicIdentityIsExact(fromFile, expectation) {
50
50
  if (error instanceof Error && error.message.includes('resolved to unrelated package')) return false;
51
51
  throw error;
52
52
  }
53
- if (resolvedOwner !== owner) return false;
53
+ if (resolvedOwner !== owner) {
54
+ reportMismatch?.(`selected owner ${owner}; consumer resolves ${resolvedOwner}`);
55
+ return false;
56
+ }
54
57
  const target = importTarget(manifest.exports?.['./microvertical-api']);
55
58
  if (!target?.startsWith('./')) return false;
56
59
  const modules = new Map();