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

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 +116 -38
  8. package/dist/cjs/microvertical-api-owner.cjs +5 -2
  9. package/dist/cjs/module-graph.cjs +1820 -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 +16 -51
  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 +114 -39
  17. package/dist/esm/microvertical-api-owner.js +5 -2
  18. package/dist/esm/module-graph.js +1762 -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 +16 -50
  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 +114 -39
  26. package/dist/esm-node/microvertical-api-owner.js +5 -2
  27. package/dist/esm-node/module-graph.js +1763 -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 +16 -50
  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 +15 -1
  35. package/dist/types/microvertical-api-owner.d.ts +1 -1
  36. package/dist/types/module-graph.d.ts +92 -0
  37. package/dist/types/source-analysis.d.ts +13 -0
  38. package/package.json +17 -13
@@ -40,6 +40,7 @@ __webpack_require__.d(__webpack_exports__, {
40
40
  checkMicroVerticalApiBoundaries: ()=>checkMicroVerticalApiBoundaries,
41
41
  checkMicroVerticalApiConsumerFiles: ()=>checkMicroVerticalApiConsumerFiles,
42
42
  configuredMicroVerticalApiStem: ()=>external_microvertical_api_baseline_cjs_namespaceObject.configuredMicroVerticalApiStem,
43
+ createModuleGraph: ()=>external_module_graph_cjs_namespaceObject.createModuleGraph,
43
44
  microVerticalApiBaselineViolation: ()=>external_microvertical_api_baseline_cjs_namespaceObject.microVerticalApiBaselineViolation
44
45
  });
45
46
  const external_node_fs_namespaceObject = require("node:fs");
@@ -50,6 +51,8 @@ const parser_namespaceObject = require("@babel/parser");
50
51
  const types_namespaceObject = require("@babel/types");
51
52
  const external_microvertical_api_baseline_cjs_namespaceObject = require("./microvertical-api-baseline.cjs");
52
53
  const external_microvertical_api_owner_cjs_namespaceObject = require("./microvertical-api-owner.cjs");
54
+ const external_module_graph_cjs_namespaceObject = require("./module-graph.cjs");
55
+ const external_source_analysis_cjs_namespaceObject = require("./source-analysis.cjs");
53
56
  const external_strict_effect_runtime_cjs_namespaceObject = require("./strict-effect-runtime.cjs");
54
57
  const ignored = new Set([
55
58
  '.git',
@@ -65,13 +68,42 @@ const normalize = (file)=>file.split(external_node_path_default().sep).join('/')
65
68
  const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
66
69
  const apiApp = (app)=>'vertical' === app.kind && 'ui-only' !== app.surfaceProfile && 'horizontal-remote' !== app.deliveryUnitKind;
67
70
  function configuredApps(root, supplied) {
68
- const source = supplied ?? JSON.parse(external_node_fs_default().readFileSync(external_node_path_default().join(root, '.modernjs/ultramodern.json'), 'utf8')).topology?.apps;
69
- if (!Array.isArray(source)) throw new Error('.modernjs/ultramodern.json: topology.apps must be an array');
71
+ const topology = supplied ? void 0 : JSON.parse(external_node_fs_default().readFileSync(external_node_path_default().join(root, 'topology/reference-topology.json'), 'utf8'));
72
+ 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');
73
+ const entries = topology?.shell ? [
74
+ topology.shell,
75
+ ...topology.shells ?? [],
76
+ ...topology.verticals ?? []
77
+ ] : void 0;
78
+ const source = supplied ?? entries?.map((entry)=>{
79
+ if (!entry || 'string' != typeof entry.path || !entry.path || external_node_path_default().isAbsolute(entry.path) || entry.path.split(/[\\/]/u).includes('..')) throw new Error('topology/reference-topology.json: each app must have an explicit workspace path');
80
+ const packageFile = external_node_path_default().join(root, entry.path, 'package.json');
81
+ const manifest = JSON.parse(external_node_fs_default().readFileSync(packageFile, 'utf8'));
82
+ 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`);
83
+ const api = entry.api;
84
+ const stem = api?.stem ?? api?.readiness?.endpoint?.match(/^\/([a-z0-9]+(?:-[a-z0-9]+)*)\/readiness$/u)?.[1];
85
+ return {
86
+ id: entry.id,
87
+ kind: entry.kind,
88
+ path: entry.path,
89
+ package: manifest.name,
90
+ surfaceProfile: entry.surfaceProfile,
91
+ deliveryUnitKind: entry.deliveryUnitKind,
92
+ ...void 0 === api ? {} : {
93
+ api: {
94
+ stem,
95
+ prefix: api.bff?.prefix,
96
+ protocol: api.protocol
97
+ }
98
+ }
99
+ };
100
+ });
101
+ if (!Array.isArray(source)) throw new Error('topology/reference-topology.json: shell and verticals must describe apps');
70
102
  const paths = new Set();
71
103
  for (const app of source){
72
- if (!app || 'object' != typeof app || 'string' != typeof app.path || !app.path || external_node_path_default().isAbsolute(app.path) || app.path.split(/[\\/]/u).includes('..')) throw new Error('.modernjs/ultramodern.json: each app must have a relative workspace path');
104
+ if (!app || 'object' != typeof app || 'string' != typeof app.path || !app.path || external_node_path_default().isAbsolute(app.path) || app.path.split(/[\\/]/u).includes('..')) throw new Error('topology/reference-topology.json: each app must have a relative workspace path');
73
105
  const key = normalize(app.path);
74
- if (paths.has(key)) throw new Error(`.modernjs/ultramodern.json: duplicate app path ${key}`);
106
+ if (paths.has(key)) throw new Error(`topology/reference-topology.json: duplicate app path ${key}`);
75
107
  paths.add(key);
76
108
  for (const name of [
77
109
  'id',
@@ -80,7 +112,7 @@ function configuredApps(root, supplied) {
80
112
  'surfaceProfile',
81
113
  'deliveryUnitKind'
82
114
  ])if (void 0 !== app[name] && 'string' != typeof app[name]) throw new Error(`${key}: ${name} must be a string`);
83
- for (const [field, allowed] of Object.entries({
115
+ const allowedFields = {
84
116
  kind: [
85
117
  'shell',
86
118
  'vertical'
@@ -94,7 +126,11 @@ function configuredApps(root, supplied) {
94
126
  'microvertical',
95
127
  'horizontal-remote'
96
128
  ]
97
- }))if (void 0 !== app[field] && !allowed.includes(app[field])) throw new Error(`${key}: invalid ${field}`);
129
+ };
130
+ for (const field of Object.keys(allowedFields)){
131
+ const value = app[field];
132
+ if (void 0 !== value && !allowedFields[field].includes(value)) throw new Error(`${key}: invalid ${field}`);
133
+ }
98
134
  if (void 0 !== app.api) {
99
135
  if (!app.api || 'object' != typeof app.api || Array.isArray(app.api)) throw new Error(`${key}: api must be an object`);
100
136
  if (void 0 !== app.api.protocol && ![
@@ -159,7 +195,7 @@ function check(options, topology) {
159
195
  const ast = (0, parser_namespaceObject.parse)(read(file), {
160
196
  sourceType: 'module',
161
197
  sourceFilename: file,
162
- plugins: (0, external_microvertical_api_baseline_cjs_namespaceObject.consumerParserPlugins)(file)
198
+ plugins: (0, external_source_analysis_cjs_namespaceObject.consumerParserPlugins)(file)
163
199
  });
164
200
  parsed.set(file, ast);
165
201
  return ast;
@@ -182,12 +218,27 @@ function check(options, topology) {
182
218
  `${base}/index.tsx`
183
219
  ].find((candidate)=>exists(candidate));
184
220
  };
185
- const composedModules = (file)=>{
221
+ const clientExportFile = (appPath, exported)=>{
222
+ if ('string' != typeof exported || !exported.startsWith('./') || exported.split(/[\\/]/u).includes('..') || !/\.[cm]?[jt]sx?$/u.test(exported)) return;
223
+ const appDirectory = absolute(appPath);
224
+ const candidate = external_node_path_default().resolve(appDirectory, exported);
225
+ try {
226
+ const workspace = external_node_fs_default().realpathSync(root);
227
+ const owner = external_node_fs_default().realpathSync(appDirectory);
228
+ const real = external_node_fs_default().realpathSync(candidate);
229
+ if (!owner.startsWith(`${workspace}${external_node_path_default().sep}`) || !real.startsWith(`${owner}${external_node_path_default().sep}`) || !external_node_fs_default().statSync(real).isFile()) return;
230
+ } catch {
231
+ return;
232
+ }
233
+ return normalize(external_node_path_default().relative(root, candidate));
234
+ };
235
+ const composedModules = (file, appPath)=>{
186
236
  const order = [];
187
237
  const seen = new Set();
188
238
  const queue = [
189
239
  file
190
240
  ];
241
+ const owner = external_node_fs_default().realpathSync(absolute(appPath));
191
242
  while(queue.length > 0 && order.length < 128){
192
243
  const current = queue.shift();
193
244
  if (seen.has(current)) continue;
@@ -198,16 +249,19 @@ function check(options, topology) {
198
249
  for (const statement of ast.program.body){
199
250
  const source = (types_namespaceObject.isImportDeclaration(statement) || types_namespaceObject.isExportNamedDeclaration(statement) || types_namespaceObject.isExportAllDeclaration(statement)) && statement.source ? statement.source.value : void 0;
200
251
  const resolved = void 0 === source ? void 0 : resolveRelative(current, source);
201
- if (void 0 !== resolved && !seen.has(resolved)) queue.push(resolved);
252
+ if (void 0 !== resolved && !seen.has(resolved)) {
253
+ const real = external_node_fs_default().realpathSync(absolute(resolved));
254
+ if (real.startsWith(`${owner}${external_node_path_default().sep}`)) queue.push(resolved);
255
+ }
202
256
  }
203
257
  }
204
258
  return order;
205
259
  };
206
- const moduleShape = (file, imports, calls, patterns = [], defaultExport, composed = false)=>{
260
+ const moduleShape = (file, imports, calls, patterns = [], defaultExport, composedAppPath)=>{
207
261
  if (!exists(file)) return;
208
262
  const ast = parseFile(file);
209
263
  if (!ast) return;
210
- const shapeFiles = composed ? composedModules(file) : [
264
+ const shapeFiles = composedAppPath ? composedModules(file, composedAppPath) : [
211
265
  file
212
266
  ];
213
267
  const body = shapeFiles.flatMap((shapeFile)=>parseFile(shapeFile)?.program.body ?? []);
@@ -300,7 +354,6 @@ function check(options, topology) {
300
354
  if (violation) diagnostics.push(`${file}: ${violation}`);
301
355
  }
302
356
  });
303
- let owner;
304
357
  for (const appPath of appDirectories)guarded(appPath, ()=>{
305
358
  const app = byPath.get(appPath);
306
359
  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`);
@@ -317,19 +370,25 @@ function check(options, topology) {
317
370
  }
318
371
  return;
319
372
  }
320
- if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in .modernjs/ultramodern.json`);
373
+ if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in topology/reference-topology.json`);
321
374
  const stem = app?.api?.stem ?? external_node_path_default().posix.basename(appPath);
322
375
  const rpc = app?.api?.protocol === 'rpc';
323
376
  const contract = `${appPath}/shared/${rpc ? 'rpc' : 'api'}.ts`;
324
- const client = `${appPath}/src/api/${stem}-${rpc ? 'rpc-client' : 'client'}.ts`;
325
377
  const entry = `${appPath}/api/index.ts`;
326
378
  for (const file of [
327
379
  entry,
328
- contract,
329
- client
380
+ contract
330
381
  ])assert(exists(file), `${file}: required API surface is missing`);
382
+ const packageFile = `${appPath}/package.json`;
383
+ assert(exists(packageFile), `${packageFile}: required API package is missing`);
384
+ if (!exists(packageFile)) return;
385
+ const manifest = JSON.parse(read(packageFile));
386
+ assert(manifest.exports?.['./api'] === `./shared/${rpc ? 'rpc' : 'api'}.ts`, `${packageFile}: invalid ./api export`);
387
+ const clientKey = `./api/${rpc ? 'rpc-client' : 'client'}`;
388
+ const client = clientExportFile(appPath, manifest.exports?.[clientKey]);
389
+ assert(void 0 !== client, `${packageFile}: invalid API client export`);
390
+ assert(manifest.exports?.[`./api/${rpc ? 'client' : 'rpc-client'}`] === void 0, `${packageFile}: forbidden opposite protocol client export`);
331
391
  noPath(`${appPath}/shared/${rpc ? 'api' : 'rpc'}.ts`, `must not emit a ${rpc ? 'REST' : 'RPC'} contract`);
332
- noPath(`${appPath}/src/api/${stem}-${rpc ? 'client' : 'rpc-client'}.ts`, `must not emit a ${rpc ? 'REST' : 'RPC'} client`);
333
392
  if (rpc) {
334
393
  moduleShape(entry, [
335
394
  [
@@ -360,7 +419,7 @@ function check(options, topology) {
360
419
  ], 'apiRuntime');
361
420
  moduleShape(contract, [
362
421
  [
363
- 'effect/unstable/rpc',
422
+ 'effect/rpc',
364
423
  [
365
424
  'Rpc',
366
425
  'RpcGroup'
@@ -378,7 +437,7 @@ function check(options, topology) {
378
437
  'Schema.Struct'
379
438
  ]);
380
439
  } else if (exists(contract)) {
381
- owner ??= options.baselinePackageDirectory ?? (0, external_microvertical_api_owner_cjs_namespaceObject.resolveBaselinePackageDirectory)(external_node_path_default().join(root, 'package.json'));
440
+ const owner = options.baselinePackageDirectory ?? (0, external_microvertical_api_owner_cjs_namespaceObject.resolveBaselinePackageDirectory)(absolute(contract));
382
441
  const apiPrefix = app?.api?.prefix ?? `/${stem}-api`;
383
442
  const basePath = `${apiPrefix}/${stem}`;
384
443
  const expectation = {
@@ -395,21 +454,24 @@ function check(options, topology) {
395
454
  const violation = (0, external_microvertical_api_baseline_cjs_namespaceObject.microVerticalApiBaselineViolation)(stem, absolute(contract), expectation);
396
455
  if (violation) diagnostics.push(`${contract}: ${violation}`);
397
456
  }
398
- moduleShape(client, [
399
- [
400
- effect,
457
+ if (client) {
458
+ const relativeContract = external_node_path_default().posix.relative(external_node_path_default().posix.dirname(client), contract);
459
+ moduleShape(client, [
401
460
  [
402
- 'Effect',
403
- rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
461
+ effect,
462
+ [
463
+ 'Effect',
464
+ rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
465
+ ]
466
+ ],
467
+ [
468
+ relativeContract.startsWith('.') ? relativeContract : `./${relativeContract}`,
469
+ []
404
470
  ]
405
- ],
406
- [
407
- rpc ? '../../shared/rpc.ts' : '../../shared/api',
408
- []
409
- ]
410
- ], [
411
- rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
412
- ], [], void 0, true);
471
+ ], [
472
+ rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
473
+ ], [], void 0, appPath);
474
+ }
413
475
  moduleShape(`${appPath}/api/effect-api.ts`, [], [], [
414
476
  [
415
477
  /\bbackendFederationContract\b/u,
@@ -450,12 +512,26 @@ function check(options, topology) {
450
512
  'must enable strictEffectApproach'
451
513
  ]
452
514
  ]);
453
- const packageFile = `${appPath}/package.json`;
454
- if (exists(packageFile)) {
455
- const manifest = JSON.parse(read(packageFile));
456
- assert(manifest.exports?.['./api'] === `./shared/${rpc ? 'rpc' : 'api'}.ts`, `${packageFile}: invalid ./api export`);
457
- assert(manifest.exports?.[`./api/${rpc ? 'rpc-client' : 'client'}`] === `./src/api/${stem}-${rpc ? 'rpc-client' : 'client'}.ts`, `${packageFile}: invalid API client export`);
458
- assert(manifest.exports?.[`./api/${rpc ? 'client' : 'rpc-client'}`] === void 0, `${packageFile}: forbidden opposite protocol client export`);
515
+ });
516
+ const sourceRules = options.sourceRules ?? [];
517
+ const graph = (0, external_module_graph_cjs_namespaceObject.createModuleGraph)();
518
+ for (const file of sourceRules.length ? sourceFiles : [])if (/\.[cm]?[jt]sx?$/u.test(file)) guarded(file, ()=>{
519
+ let module;
520
+ try {
521
+ module = graph.module(absolute(file));
522
+ } catch (error) {
523
+ if (!(error instanceof external_source_analysis_cjs_namespaceObject.SourceSyntaxError)) throw error;
524
+ diagnostics.push(`${file}: source rules need parseable source (${error.message})`);
525
+ return;
526
+ }
527
+ for (const rule of sourceRules){
528
+ const messages = rule({
529
+ file,
530
+ module,
531
+ graph
532
+ });
533
+ 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)`);
534
+ for (const message of messages)diagnostics.push(`${file}: ${message}`);
459
535
  }
460
536
  });
461
537
  const shell = 'apps/shell-super-app';
@@ -495,11 +571,13 @@ const checkMicroVerticalApiConsumerFiles = (options)=>check(options, false);
495
571
  exports.checkMicroVerticalApiBoundaries = __webpack_exports__.checkMicroVerticalApiBoundaries;
496
572
  exports.checkMicroVerticalApiConsumerFiles = __webpack_exports__.checkMicroVerticalApiConsumerFiles;
497
573
  exports.configuredMicroVerticalApiStem = __webpack_exports__.configuredMicroVerticalApiStem;
574
+ exports.createModuleGraph = __webpack_exports__.createModuleGraph;
498
575
  exports.microVerticalApiBaselineViolation = __webpack_exports__.microVerticalApiBaselineViolation;
499
576
  for(var __rspack_i in __webpack_exports__)if (-1 === [
500
577
  "checkMicroVerticalApiBoundaries",
501
578
  "checkMicroVerticalApiConsumerFiles",
502
579
  "configuredMicroVerticalApiStem",
580
+ "createModuleGraph",
503
581
  "microVerticalApiBaselineViolation"
504
582
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
505
583
  Object.defineProperty(exports, '__esModule', {
@@ -81,7 +81,7 @@ function importTarget(value) {
81
81
  if (resolved) return resolved;
82
82
  }
83
83
  }
84
- function baselinePublicIdentityIsExact(fromFile, expectation) {
84
+ function baselinePublicIdentityIsExact(fromFile, expectation, reportMismatch) {
85
85
  const owner = external_node_fs_default().realpathSync(expectation.baselinePackageDirectory);
86
86
  const manifest = JSON.parse(external_node_fs_default().readFileSync(external_node_path_default().join(owner, 'package.json'), 'utf8'));
87
87
  if (expectation.baselinePackage !== baselinePackage || ![
@@ -95,7 +95,10 @@ function baselinePublicIdentityIsExact(fromFile, expectation) {
95
95
  if (error instanceof Error && error.message.includes('resolved to unrelated package')) return false;
96
96
  throw error;
97
97
  }
98
- if (resolvedOwner !== owner) return false;
98
+ if (resolvedOwner !== owner) {
99
+ reportMismatch?.(`selected owner ${owner}; consumer resolves ${resolvedOwner}`);
100
+ return false;
101
+ }
99
102
  const target = importTarget(manifest.exports?.['./microvertical-api']);
100
103
  if (!target?.startsWith('./')) return false;
101
104
  const modules = new Map();