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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +89 -246
  7. package/dist/cjs/microvertical-api-boundary.cjs +507 -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 +505 -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 +505 -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 +4 -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 { camelCaseStem, configuredMicroVerticalApiStem, microVerticalApiBaselineViolation, pascalCaseStem } 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,54 @@ 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;
22
+ const MAX_API_HOPS = 128;
23
+ const MAX_API_VISITS = 2048;
24
+ const identitySources = [
25
+ 'effect',
26
+ 'effect/Function'
27
+ ];
18
28
  const normalize = (file)=>file.split(node_path.sep).join('/').replace(/^\.\//u, '');
19
29
  const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
20
30
  const apiApp = (app)=>'vertical' === app.kind && 'ui-only' !== app.surfaceProfile && 'horizontal-remote' !== app.deliveryUnitKind;
21
31
  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');
32
+ const topology = supplied ? void 0 : JSON.parse(node_fs.readFileSync(node_path.join(root, 'topology/reference-topology.json'), 'utf8'));
33
+ 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');
34
+ const entries = topology?.shell ? [
35
+ topology.shell,
36
+ ...topology.shells ?? [],
37
+ ...topology.verticals ?? []
38
+ ] : void 0;
39
+ const source = supplied ?? entries?.map((entry)=>{
40
+ 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');
41
+ const packageFile = node_path.join(root, entry.path, 'package.json');
42
+ const manifest = JSON.parse(node_fs.readFileSync(packageFile, 'utf8'));
43
+ 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`);
44
+ const api = entry.api;
45
+ const stem = api?.stem ?? api?.readiness?.endpoint?.match(/^\/([a-z0-9]+(?:-[a-z0-9]+)*)\/readiness$/u)?.[1];
46
+ return {
47
+ id: entry.id,
48
+ kind: entry.kind,
49
+ path: entry.path,
50
+ package: manifest.name,
51
+ surfaceProfile: entry.surfaceProfile,
52
+ deliveryUnitKind: entry.deliveryUnitKind,
53
+ ...void 0 === api ? {} : {
54
+ api: {
55
+ stem,
56
+ prefix: api.bff?.prefix,
57
+ protocol: api.protocol
58
+ }
59
+ }
60
+ };
61
+ });
62
+ if (!Array.isArray(source)) throw new Error('topology/reference-topology.json: shell and verticals must describe apps');
24
63
  const paths = new Set();
25
64
  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');
65
+ 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
66
  const key = normalize(app.path);
28
- if (paths.has(key)) throw new Error(`.modernjs/ultramodern.json: duplicate app path ${key}`);
67
+ if (paths.has(key)) throw new Error(`topology/reference-topology.json: duplicate app path ${key}`);
29
68
  paths.add(key);
30
69
  for (const name of [
31
70
  'id',
@@ -34,7 +73,7 @@ function configuredApps(root, supplied) {
34
73
  'surfaceProfile',
35
74
  'deliveryUnitKind'
36
75
  ])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({
76
+ const allowedFields = {
38
77
  kind: [
39
78
  'shell',
40
79
  'vertical'
@@ -48,7 +87,11 @@ function configuredApps(root, supplied) {
48
87
  'microvertical',
49
88
  'horizontal-remote'
50
89
  ]
51
- }))if (void 0 !== app[field] && !allowed.includes(app[field])) throw new Error(`${key}: invalid ${field}`);
90
+ };
91
+ for (const field of Object.keys(allowedFields)){
92
+ const value = app[field];
93
+ if ((void 0 !== value || 'kind' === field) && ('string' != typeof value || !allowedFields[field].includes(value))) throw new Error(`${key}: invalid ${field}`);
94
+ }
52
95
  if (void 0 !== app.api) {
53
96
  if (!app.api || 'object' != typeof app.api || Array.isArray(app.api)) throw new Error(`${key}: api must be an object`);
54
97
  if (void 0 !== app.api.protocol && ![
@@ -89,6 +132,9 @@ function check(options, topology) {
89
132
  };
90
133
  try {
91
134
  const apps = configuredApps(root, options.configuredApps);
135
+ const workspace = node_fs.realpathSync(root);
136
+ const realRelative = (file)=>normalize(node_path.relative(workspace, node_fs.realpathSync(absolute(file))));
137
+ const inside = (directory, real)=>real.startsWith(`${directory}${node_path.sep}`);
92
138
  const byPath = new Map(apps.map((app)=>[
93
139
  normalize(app.path),
94
140
  app
@@ -129,39 +175,244 @@ function check(options, topology) {
129
175
  const resolveRelative = (fromFile, specifier)=>{
130
176
  if (!/^\.\.?\//u.test(specifier)) return;
131
177
  const base = node_path.posix.join(node_path.posix.dirname(fromFile), specifier).replace(/\.(?:[cm]?[jt]sx?)$/u, '');
132
- return [
178
+ const candidate = [
133
179
  `${base}.ts`,
134
180
  `${base}.tsx`,
135
181
  `${base}/index.ts`,
136
182
  `${base}/index.tsx`
137
- ].find((candidate)=>exists(candidate));
183
+ ].find((file)=>exists(file));
184
+ return void 0 === candidate ? void 0 : realRelative(candidate);
185
+ };
186
+ const graphBinding = (graph, module, expression, seen = new Set())=>{
187
+ if (seen.has(expression) || seen.size >= 128) return;
188
+ seen.add(expression);
189
+ const binding = __rspack_external__babel_types_6e8337dd.isIdentifier(expression) || __rspack_external__babel_types_6e8337dd.isMemberExpression(expression) ? graph.resolveReference(module, expression) : void 0;
190
+ return binding?.kind === 'declaration' && (__rspack_external__babel_types_6e8337dd.isIdentifier(binding.expression) || __rspack_external__babel_types_6e8337dd.isMemberExpression(binding.expression)) ? graphBinding(graph, binding.module, binding.expression, seen) : binding;
191
+ };
192
+ const clientExportFile = (appPath, exported)=>{
193
+ if ('string' != typeof exported || !exported.startsWith('./') || exported.split(/[\\/]/u).includes('..') || !/\.[cm]?[jt]sx?$/u.test(exported)) return;
194
+ const appDirectory = absolute(appPath);
195
+ const candidate = node_path.resolve(appDirectory, exported);
196
+ try {
197
+ const workspace = node_fs.realpathSync(root);
198
+ const owner = node_fs.realpathSync(appDirectory);
199
+ const real = node_fs.realpathSync(candidate);
200
+ if (!owner.startsWith(`${workspace}${node_path.sep}`) || !real.startsWith(`${owner}${node_path.sep}`) || !node_fs.statSync(real).isFile()) return;
201
+ } catch {
202
+ return;
203
+ }
204
+ return normalize(node_path.relative(root, candidate));
138
205
  };
139
- const composedModules = (file)=>{
140
- const order = [];
141
- const seen = new Set();
206
+ const composedModules = (file, appPath)=>{
207
+ const owner = node_fs.realpathSync(absolute(appPath));
208
+ if (!inside(owner, node_fs.realpathSync(absolute(file)))) throw new Error(`consumer module closure escapes its owner: ${file}`);
209
+ const entry = realRelative(file);
210
+ const discovered = new Set([
211
+ entry
212
+ ]);
142
213
  const queue = [
143
- file
214
+ {
215
+ file: entry,
216
+ depth: 0
217
+ }
144
218
  ];
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);
219
+ let graph;
220
+ for (const { file: current, depth } of queue){
150
221
  const ast = parseFile(current);
151
222
  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);
223
+ const sources = [];
224
+ let callsRequire = false;
225
+ for (const statement of ast.program.body)if (__rspack_external__babel_types_6e8337dd.isImportDeclaration(statement) && 'type' !== statement.importKind && 'typeof' !== statement.importKind || (__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || __rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement)) && 'type' !== statement.exportKind && statement.source) sources.push(statement.source.value);
226
+ __rspack_external__babel_types_6e8337dd.traverseFast(ast, (node)=>{
227
+ if (__rspack_external__babel_types_6e8337dd.isImportExpression(node)) {
228
+ if (!__rspack_external__babel_types_6e8337dd.isStringLiteral(node.source)) throw new Error(`consumer module closure has nonliteral runtime dependency: ${current}`);
229
+ sources.push(node.source.value);
230
+ }
231
+ if ((__rspack_external__babel_types_6e8337dd.isCallExpression(node) || __rspack_external__babel_types_6e8337dd.isOptionalCallExpression(node)) && __rspack_external__babel_types_6e8337dd.isIdentifier(unwrapExpression(node.callee, true), {
232
+ name: 'require'
233
+ })) callsRequire = true;
234
+ });
235
+ if (callsRequire) {
236
+ graph ??= createModuleGraph();
237
+ const module = graph.module(absolute(current));
238
+ __rspack_external__babel_types_6e8337dd.traverseFast(module.file, (node)=>{
239
+ if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node) && !__rspack_external__babel_types_6e8337dd.isOptionalCallExpression(node)) return;
240
+ const callee = unwrapExpression(node.callee, true);
241
+ if (!__rspack_external__babel_types_6e8337dd.isIdentifier(callee, {
242
+ name: 'require'
243
+ }) || !graph.isUnboundReference(module, callee)) return;
244
+ const [argument] = node.arguments;
245
+ if (1 !== node.arguments.length || !__rspack_external__babel_types_6e8337dd.isStringLiteral(argument)) throw new Error(`consumer module closure has nonliteral runtime dependency: ${current}`);
246
+ sources.push(argument.value);
247
+ });
248
+ }
249
+ for (const source of sources){
250
+ if (!/^\.\.?\//u.test(source)) continue;
251
+ const resolved = resolveRelative(current, source);
252
+ if (void 0 === resolved) throw new Error(`consumer module closure has unresolved relative dependency: ${current} -> ${source}`);
253
+ if (!inside(owner, node_path.join(workspace, resolved))) throw new Error(`consumer module closure escapes its owner: ${current} -> ${source}`);
254
+ if (!discovered.has(resolved)) {
255
+ if (depth + 1 >= MAX_CLOSURE_DEPTH) throw new Error(`consumer module closure exceeds ${MAX_CLOSURE_DEPTH} import-depth analysis budget`);
256
+ if (discovered.size >= MAX_CLOSURE_MODULES) throw new Error(`consumer module closure exceeds ${MAX_CLOSURE_MODULES}-module analysis budget`);
257
+ discovered.add(resolved);
258
+ queue.push({
259
+ file: resolved,
260
+ depth: depth + 1
261
+ });
262
+ }
156
263
  }
157
264
  }
158
- return order;
265
+ return queue.map((item)=>item.file);
159
266
  };
160
- const moduleShape = (file, imports, calls, patterns = [], defaultExport, composed = false)=>{
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
+ };
411
+ const moduleShape = (file, imports, calls, patterns = [], defaultExport, composedAppPath)=>{
161
412
  if (!exists(file)) return;
162
413
  const ast = parseFile(file);
163
414
  if (!ast) return;
164
- const shapeFiles = composed ? composedModules(file) : [
415
+ const shapeFiles = composedAppPath ? composedModules(file, composedAppPath) : [
165
416
  file
166
417
  ];
167
418
  const body = shapeFiles.flatMap((shapeFile)=>parseFile(shapeFile)?.program.body ?? []);
@@ -194,6 +445,14 @@ function check(options, topology) {
194
445
  })), `${file}: must default-export ${defaultExport}`);
195
446
  };
196
447
  const effect = '@modern-js/bff-effect/effect-client';
448
+ const httpApiSources = [
449
+ effect,
450
+ 'effect/http-api'
451
+ ];
452
+ const clientFactories = new Set([
453
+ 'makeEffectHttpApiClient',
454
+ 'makeEffectRpcClient'
455
+ ]);
197
456
  const appDirectories = [
198
457
  ...new Set([
199
458
  ...directories('apps'),
@@ -254,7 +513,6 @@ function check(options, topology) {
254
513
  if (violation) diagnostics.push(`${file}: ${violation}`);
255
514
  }
256
515
  });
257
- let owner;
258
516
  for (const appPath of appDirectories)guarded(appPath, ()=>{
259
517
  const app = byPath.get(appPath);
260
518
  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 +529,25 @@ function check(options, topology) {
271
529
  }
272
530
  return;
273
531
  }
274
- if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in .modernjs/ultramodern.json`);
532
+ if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in topology/reference-topology.json`);
275
533
  const stem = app?.api?.stem ?? node_path.posix.basename(appPath);
276
534
  const rpc = app?.api?.protocol === 'rpc';
277
535
  const contract = `${appPath}/shared/${rpc ? 'rpc' : 'api'}.ts`;
278
- const client = `${appPath}/src/api/${stem}-${rpc ? 'rpc-client' : 'client'}.ts`;
279
536
  const entry = `${appPath}/api/index.ts`;
280
537
  for (const file of [
281
538
  entry,
282
- contract,
283
- client
539
+ contract
284
540
  ])assert(exists(file), `${file}: required API surface is missing`);
541
+ const packageFile = `${appPath}/package.json`;
542
+ assert(exists(packageFile), `${packageFile}: required API package is missing`);
543
+ if (!exists(packageFile)) return;
544
+ const manifest = JSON.parse(read(packageFile));
545
+ assert(manifest.exports?.['./api'] === `./shared/${rpc ? 'rpc' : 'api'}.ts`, `${packageFile}: invalid ./api export`);
546
+ const clientKey = `./api/${rpc ? 'rpc-client' : 'client'}`;
547
+ const client = clientExportFile(appPath, manifest.exports?.[clientKey]);
548
+ assert(void 0 !== client, `${packageFile}: invalid API client export`);
549
+ assert(manifest.exports?.[`./api/${rpc ? 'client' : 'rpc-client'}`] === void 0, `${packageFile}: forbidden opposite protocol client export`);
285
550
  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
551
  if (rpc) {
288
552
  moduleShape(entry, [
289
553
  [
@@ -314,7 +578,7 @@ function check(options, topology) {
314
578
  ], 'apiRuntime');
315
579
  moduleShape(contract, [
316
580
  [
317
- 'effect/unstable/rpc',
581
+ 'effect/rpc',
318
582
  [
319
583
  'Rpc',
320
584
  'RpcGroup'
@@ -332,7 +596,7 @@ function check(options, topology) {
332
596
  'Schema.Struct'
333
597
  ]);
334
598
  } else if (exists(contract)) {
335
- owner ??= options.baselinePackageDirectory ?? resolveBaselinePackageDirectory(node_path.join(root, 'package.json'));
599
+ const owner = options.baselinePackageDirectory ?? resolveBaselinePackageDirectory(absolute(contract));
336
600
  const apiPrefix = app?.api?.prefix ?? `/${stem}-api`;
337
601
  const basePath = `${apiPrefix}/${stem}`;
338
602
  const expectation = {
@@ -349,21 +613,25 @@ function check(options, topology) {
349
613
  const violation = microVerticalApiBaselineViolation(stem, absolute(contract), expectation);
350
614
  if (violation) diagnostics.push(`${contract}: ${violation}`);
351
615
  }
352
- moduleShape(client, [
353
- [
354
- effect,
616
+ if (client) {
617
+ const relativeContract = node_path.posix.relative(node_path.posix.dirname(client), contract);
618
+ moduleShape(client, [
619
+ [
620
+ effect,
621
+ [
622
+ 'Effect',
623
+ rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
624
+ ]
625
+ ],
355
626
  [
356
- 'Effect',
357
- rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
627
+ relativeContract.startsWith('.') ? relativeContract : `./${relativeContract}`,
628
+ []
358
629
  ]
359
- ],
360
- [
361
- rpc ? '../../shared/rpc.ts' : '../../shared/api',
362
- []
363
- ]
364
- ], [
365
- rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
366
- ], [], void 0, true);
630
+ ], [
631
+ rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
632
+ ], [], void 0, appPath);
633
+ if (exists(contract)) diagnostics.push(...foreignClientConstructions(client, appPath, contract, stem, rpc));
634
+ }
367
635
  moduleShape(`${appPath}/api/effect-api.ts`, [], [], [
368
636
  [
369
637
  /\bbackendFederationContract\b/u,
@@ -404,28 +672,200 @@ function check(options, topology) {
404
672
  'must enable strictEffectApproach'
405
673
  ]
406
674
  ]);
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`);
675
+ });
676
+ const sourceRules = options.sourceRules ?? [];
677
+ for (const file of sourceRules.length ? sourceFiles : [])if (/\.[cm]?[jt]sx?$/u.test(file)) guarded(file, ()=>{
678
+ const graph = createModuleGraph();
679
+ let module;
680
+ try {
681
+ module = graph.module(absolute(file));
682
+ } catch (error) {
683
+ if (!(error instanceof SourceSyntaxError)) throw error;
684
+ diagnostics.push(`${file}: source rules need parseable source (${error.message})`);
685
+ return;
686
+ }
687
+ for (const rule of sourceRules){
688
+ const messages = rule({
689
+ file,
690
+ module,
691
+ graph
692
+ });
693
+ 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)`);
694
+ for (const message of messages)diagnostics.push(`${file}: ${message}`);
413
695
  }
414
696
  });
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}`);
697
+ const checkShellExports = (shellPath)=>{
698
+ const packageFile = `${shellPath}/package.json`;
699
+ const manifest = JSON.parse(read(packageFile));
700
+ noPath(`${shellPath}/src/api/vertical-clients.ts`, 'Shell client registries are forbidden; import each owner client directly');
701
+ assert(!Object.hasOwn(manifest.exports ?? {}, './api/clients'), `${packageFile}: Shell ./api/clients exports are forbidden`);
702
+ const owner = node_fs.realpathSync(absolute(shellPath));
703
+ const invalidExport = ()=>assert(false, `${packageFile}: invalid Shell public source export`);
704
+ const targets = (exported)=>'string' == typeof exported ? [
705
+ exported
706
+ ] : exported && 'object' == typeof exported ? Object.values(exported).flatMap(targets) : [];
707
+ const filesUnder = (directory, seen = new Set())=>{
708
+ const real = node_fs.realpathSync(absolute(directory));
709
+ if (real !== owner && !inside(owner, real)) {
710
+ invalidExport();
711
+ return [];
425
712
  }
426
- });
713
+ if (seen.has(real)) return [];
714
+ seen.add(real);
715
+ return node_fs.readdirSync(absolute(directory), {
716
+ withFileTypes: true
717
+ }).flatMap((entry)=>{
718
+ if (ignored.has(entry.name)) return [];
719
+ const file = `${directory}/${entry.name}`;
720
+ const stat = node_fs.statSync(absolute(file));
721
+ return stat.isDirectory() ? filesUnder(file, seen) : stat.isFile() ? [
722
+ file
723
+ ] : [];
724
+ });
725
+ };
726
+ const exportedFiles = (target)=>{
727
+ if (!target.includes('*')) return [
728
+ `${shellPath}/${target.slice(2)}`
729
+ ];
730
+ const prefix = target.slice(0, target.indexOf('*'));
731
+ const pattern = new RegExp(`^${target.split('*').map((part)=>part.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&')).join('.*')}$`, 'u');
732
+ return filesUnder(`${shellPath}/${prefix.slice(2, prefix.lastIndexOf('/'))}`).filter((file)=>pattern.test(`./${node_path.posix.relative(shellPath, file)}`));
733
+ };
734
+ const ownClientPrefix = `${manifest.name}/api/`;
735
+ const foreignSpecifier = (specifier)=>/\/api\/(?:rpc-)?client$/u.test(specifier) && !specifier.startsWith(ownClientPrefix);
736
+ for (const target of targets(manifest.exports))if (/\.[cm]?[jt]sx?$/u.test(target)) {
737
+ if (!target.startsWith('./') || target.split(/[\\/]/u).includes('..')) {
738
+ invalidExport();
739
+ continue;
740
+ }
741
+ for (const candidate of exportedFiles(target)){
742
+ const file = clientExportFile(shellPath, `./${node_path.posix.relative(shellPath, candidate)}`);
743
+ if (!file) {
744
+ invalidExport();
745
+ continue;
746
+ }
747
+ assert(!exposesForeignClient(file, owner, foreignSpecifier), `${file}: Shell public exports must not expose another owner's API client`);
748
+ }
749
+ }
750
+ };
751
+ const exposesForeignClient = (entry, owner, foreignSpecifier)=>{
752
+ const graph = createModuleGraph();
753
+ const seenValues = new Set();
754
+ const seenExports = new Map();
755
+ const workspacePath = (module)=>normalize(node_path.relative(workspace, module.path));
756
+ const resolveSource = (from, specifier)=>resolveRelative(from, specifier) ?? ownerClients.get(specifier);
757
+ const returnExpressions = (node)=>{
758
+ if (__rspack_external__babel_types_6e8337dd.isReturnStatement(node)) return node.argument ? [
759
+ node.argument
760
+ ] : [];
761
+ if (__rspack_external__babel_types_6e8337dd.isFunction(node)) return [];
762
+ return (__rspack_external__babel_types_6e8337dd.VISITOR_KEYS[node.type] ?? []).flatMap((key)=>{
763
+ const value = Reflect.get(node, key);
764
+ return (Array.isArray(value) ? value : [
765
+ value
766
+ ]).flatMap((child)=>__rspack_external__babel_types_6e8337dd.isNode(child) ? returnExpressions(child) : []);
767
+ });
768
+ };
769
+ const nestedValues = (node)=>{
770
+ if (__rspack_external__babel_types_6e8337dd.isObjectExpression(node)) return node.properties.flatMap((property)=>__rspack_external__babel_types_6e8337dd.isObjectProperty(property) ? [
771
+ property.value
772
+ ] : __rspack_external__babel_types_6e8337dd.isObjectMethod(property) ? returnExpressions(property.body) : __rspack_external__babel_types_6e8337dd.isSpreadElement(property) ? [
773
+ property.argument
774
+ ] : []);
775
+ if (__rspack_external__babel_types_6e8337dd.isArrayExpression(node)) return node.elements.filter((element)=>null !== element);
776
+ if (__rspack_external__babel_types_6e8337dd.isFunction(node)) return __rspack_external__babel_types_6e8337dd.isBlockStatement(node.body) ? returnExpressions(node.body) : [
777
+ node.body
778
+ ];
779
+ return [];
780
+ };
781
+ const foreignValue = (value, from)=>{
782
+ if ('external' === value.kind) {
783
+ const source = resolveSource(workspacePath(from), value.specifier);
784
+ if (!source) return foreignSpecifier(value.specifier);
785
+ return '*' === value.name ? inspect(source) : exportedValue(source, value.name);
786
+ }
787
+ if ('unresolved' === value.kind && __rspack_external__babel_types_6e8337dd.isImportDeclaration(value.node) && value.node.specifiers.some((specifier)=>__rspack_external__babel_types_6e8337dd.isImportNamespaceSpecifier(specifier))) {
788
+ const specifier = value.node.source.value;
789
+ const source = resolveSource(workspacePath(value.module), specifier);
790
+ return source ? inspect(source) : foreignSpecifier(specifier);
791
+ }
792
+ if ('node' !== value.kind || seenValues.has(value.node)) return false;
793
+ seenValues.add(value.node);
794
+ const { node, module } = value;
795
+ if ((__rspack_external__babel_types_6e8337dd.isCallExpression(node) || __rspack_external__babel_types_6e8337dd.isOptionalCallExpression(node)) && graph.evaluate(module, node.callee).some((callee)=>'external' === callee.kind && callee.specifier === effect && clientFactories.has(callee.name))) {
796
+ const [argument] = node.arguments;
797
+ const binding = argument ? graphBinding(graph, module, argument) : void 0;
798
+ if (module.boundary !== owner || binding?.kind !== 'declaration' || binding.module.boundary !== owner) return true;
799
+ }
800
+ return nestedValues(node).some((expression)=>graph.evaluate(module, expression).some((nested)=>foreignValue(nested, module)));
801
+ };
802
+ const exportName = (node)=>__rspack_external__babel_types_6e8337dd.isIdentifier(node) ? node.name : node.value;
803
+ const names = (source, visited = new Set())=>{
804
+ if (visited.has(source)) return [];
805
+ visited.add(source);
806
+ return graph.module(absolute(source)).file.program.body.flatMap((statement)=>{
807
+ if (__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement)) return [
808
+ 'default'
809
+ ];
810
+ if (!__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) && !__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement) || 'type' === statement.exportKind) return [];
811
+ if (__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement)) {
812
+ const target = resolveSource(source, statement.source.value);
813
+ return target ? names(target, visited).filter((name)=>'default' !== name) : [];
814
+ }
815
+ return statement.declaration ? Object.keys(__rspack_external__babel_types_6e8337dd.getOuterBindingIdentifiers(statement.declaration)) : statement.specifiers.flatMap((specifier)=>__rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier) && 'type' === specifier.exportKind ? [] : [
816
+ exportName(specifier.exported)
817
+ ]);
818
+ });
819
+ };
820
+ const exportedValue = (source, name)=>{
821
+ const visited = seenExports.get(source) ?? new Set();
822
+ if (visited.has(name)) return false;
823
+ visited.add(name);
824
+ seenExports.set(source, visited);
825
+ const module = graph.module(absolute(source));
826
+ const evaluates = (from, node)=>graph.evaluate(from, node).some((value)=>foreignValue(value, from));
827
+ const binding = graph.resolve(module, name, 'export');
828
+ if (binding?.kind === 'external') return foreignValue({
829
+ ...binding,
830
+ members: []
831
+ }, module);
832
+ if (binding?.kind === 'declaration') return evaluates(binding.module, binding.expression);
833
+ for (const statement of module.file.program.body){
834
+ if (__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement) && 'default' === name) return evaluates(module, statement.declaration);
835
+ if (!__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || 'type' === statement.exportKind) continue;
836
+ if (__rspack_external__babel_types_6e8337dd.isFunctionDeclaration(statement.declaration) && statement.declaration.id?.name === name) return evaluates(module, statement.declaration);
837
+ const specifier = statement.specifiers.find((item)=>exportName(item.exported) === name);
838
+ if (!specifier) continue;
839
+ if (__rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier) && 'type' === specifier.exportKind) return false;
840
+ if (!statement.source) return __rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier) ? evaluates(module, specifier.local) : false;
841
+ const target = resolveSource(source, statement.source.value);
842
+ if (!target) return foreignSpecifier(statement.source.value);
843
+ if (__rspack_external__babel_types_6e8337dd.isExportNamespaceSpecifier(specifier)) return inspect(target);
844
+ if (__rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier)) return exportedValue(target, exportName(specifier.local));
845
+ }
846
+ return module.file.program.body.some((statement)=>{
847
+ if (!__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement) || 'type' === statement.exportKind) return false;
848
+ const target = resolveSource(source, statement.source.value);
849
+ return !!target && names(target).includes(name) && exportedValue(target, name);
850
+ });
851
+ };
852
+ const inspect = (source)=>[
853
+ ...new Set(names(source))
854
+ ].some((name)=>exportedValue(source, name));
855
+ return inspect(entry);
856
+ };
857
+ const ownerClients = new Map();
858
+ for (const app of apps)if (exists(`${app.path}/package.json`)) {
859
+ const manifest = JSON.parse(read(`${app.path}/package.json`));
860
+ for (const key of [
861
+ './api/client',
862
+ './api/rpc-client'
863
+ ]){
864
+ const file = clientExportFile(app.path, manifest.exports?.[key]);
865
+ if (file) ownerClients.set(`${manifest.name}${key.slice(1)}`, file);
866
+ }
427
867
  }
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`));
868
+ for (const shell of apps.filter((app)=>'shell' === app.kind))guarded(shell.path, ()=>checkShellExports(normalize(shell.path)));
429
869
  if (exists('topology/reference-topology.json')) guarded('topology/reference-topology.json', ()=>{
430
870
  for (const vertical of JSON.parse(read('topology/reference-topology.json')).verticals ?? []){
431
871
  if (vertical.api?.runtime === 'effect') {
@@ -446,4 +886,4 @@ function check(options, topology) {
446
886
  }
447
887
  const checkMicroVerticalApiBoundaries = (options)=>check(options, true);
448
888
  const checkMicroVerticalApiConsumerFiles = (options)=>check(options, false);
449
- export { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles, configuredMicroVerticalApiStem, microVerticalApiBaselineViolation };
889
+ export { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles, configuredMicroVerticalApiStem, createModuleGraph, microVerticalApiBaselineViolation };