@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
@@ -52,7 +52,7 @@ const createStrictEffectApiBoundariesRule = ()=>({
52
52
  });
53
53
  (0, external_ast_cjs_namespaceObject.reportProgramPattern)(context, node, source, /@modern-js\/plugin-bff\/(?:server|hono-server)(?![\w-])/u, 'UltraModern API workspaces must not import Hono server helpers; use @modern-js/bff-effect/effect-edge and HttpApi.');
54
54
  (0, external_ast_cjs_namespaceObject.reportProgramPattern)(context, node, source, /\bruntimeFramework\s*(?::|=)\s*['"]hono['"]/u, 'UltraModern API apps must use bff.runtimeFramework: "effect".');
55
- (0, external_ast_cjs_namespaceObject.reportProgramPattern)(context, node, source, /(?:from|import)\s*['"][^'"]*(?:api\/effect|shared\/effect)[^'"]*['"]|shared-effect-api/u, 'Import API code from direct api/index.ts, shared/api.ts or src/api/* paths, not api/effect, shared/effect or shared Effect API packages.');
55
+ (0, external_ast_cjs_namespaceObject.reportProgramPattern)(context, node, source, /(?:from|import)\s*['"][^'"]*(?:api\/effect|shared\/effect)(?=\/|\.[cm]?[jt]sx?['"]|['"])[^'"]*['"]|shared-effect-api/u, 'Import API code from direct api/index.ts, shared/api.ts or src/api/* paths, not api/effect, shared/effect or shared Effect API packages.');
56
56
  (0, external_ast_cjs_namespaceObject.reportProgramPattern)(context, node, source, /\bstrictEffectApproach\s*(?::|=)\s*false\b/u, 'UltraModern API apps must keep strictEffectApproach enabled.');
57
57
  if ((0, external_ast_cjs_namespaceObject.isApiEntryFile)(filename)) {
58
58
  const violation = (0, external_strict_effect_runtime_cjs_namespaceObject.strictEffectRuntimeTopologyViolation)(source, (0, external_strict_effect_runtime_cjs_namespaceObject.createEffectApiImportResolver)(filename));
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.n = (module)=>{
5
+ var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
6
+ __webpack_require__.d(getter, {
7
+ a: getter
8
+ });
9
+ return getter;
10
+ };
11
+ })();
12
+ (()=>{
13
+ __webpack_require__.d = (exports1, getters, values)=>{
14
+ var define = (defs, kind)=>{
15
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
16
+ enumerable: true,
17
+ [kind]: defs[key]
18
+ });
19
+ };
20
+ define(getters, "get");
21
+ define(values, "value");
22
+ };
23
+ })();
24
+ (()=>{
25
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
26
+ })();
27
+ (()=>{
28
+ __webpack_require__.r = (exports1)=>{
29
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
30
+ value: 'Module'
31
+ });
32
+ Object.defineProperty(exports1, '__esModule', {
33
+ value: true
34
+ });
35
+ };
36
+ })();
37
+ var __webpack_exports__ = {};
38
+ __webpack_require__.r(__webpack_exports__);
39
+ __webpack_require__.d(__webpack_exports__, {
40
+ SourceSyntaxError: ()=>SourceSyntaxError,
41
+ consumerParserPlugins: ()=>consumerParserPlugins,
42
+ parseSource: ()=>parseSource,
43
+ traverseSource: ()=>traverseSource,
44
+ unwrapExpression: ()=>unwrapExpression
45
+ });
46
+ const parser_namespaceObject = require("@babel/parser");
47
+ const traverse_namespaceObject = require("@babel/traverse");
48
+ var traverse_default = /*#__PURE__*/ __webpack_require__.n(traverse_namespaceObject);
49
+ const types_namespaceObject = require("@babel/types");
50
+ const consumerParserPlugins = (filePath)=>/\.[jt]sx$/u.test(filePath) ? [
51
+ "typescript",
52
+ 'decorators',
53
+ 'decoratorAutoAccessors',
54
+ 'jsx'
55
+ ] : [
56
+ "typescript",
57
+ 'decorators',
58
+ 'decoratorAutoAccessors'
59
+ ];
60
+ class SourceSyntaxError extends Error {
61
+ }
62
+ class SourceValidationHub extends traverse_namespaceObject.Hub {
63
+ buildError(_node, message) {
64
+ return new SourceSyntaxError(message);
65
+ }
66
+ }
67
+ function parseSource(source, filename, syntaxMessage) {
68
+ try {
69
+ return (0, parser_namespaceObject.parse)(source, {
70
+ sourceType: 'module',
71
+ sourceFilename: filename,
72
+ plugins: consumerParserPlugins(filename)
73
+ });
74
+ } catch (cause) {
75
+ if ('object' == typeof cause && null !== cause && 'code' in cause && 'BABEL_PARSER_SYNTAX_ERROR' === cause.code && 'reasonCode' in cause && 'string' == typeof cause.reasonCode) throw new SourceSyntaxError(syntaxMessage ?? (cause instanceof Error ? cause.message : String(cause)));
76
+ throw cause;
77
+ }
78
+ }
79
+ function traverseSource(file, visitor) {
80
+ const program = traverse_namespaceObject.NodePath.get({
81
+ hub: new SourceValidationHub(),
82
+ parentPath: void 0,
83
+ parent: file,
84
+ container: file,
85
+ key: 'program'
86
+ });
87
+ program.setContext();
88
+ traverse_default()(file, visitor, program.scope, void 0, program);
89
+ }
90
+ function unwrapExpression(node, includeAssertions = false) {
91
+ while(types_namespaceObject.isTSAsExpression(node) || types_namespaceObject.isParenthesizedExpression(node) || types_namespaceObject.isTSSatisfiesExpression(node) || includeAssertions && (types_namespaceObject.isTSTypeAssertion(node) || types_namespaceObject.isTSNonNullExpression(node) || types_namespaceObject.isTSInstantiationExpression(node)))node = node.expression;
92
+ return node;
93
+ }
94
+ exports.SourceSyntaxError = __webpack_exports__.SourceSyntaxError;
95
+ exports.consumerParserPlugins = __webpack_exports__.consumerParserPlugins;
96
+ exports.parseSource = __webpack_exports__.parseSource;
97
+ exports.traverseSource = __webpack_exports__.traverseSource;
98
+ exports.unwrapExpression = __webpack_exports__.unwrapExpression;
99
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
100
+ "SourceSyntaxError",
101
+ "consumerParserPlugins",
102
+ "parseSource",
103
+ "traverseSource",
104
+ "unwrapExpression"
105
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
106
+ Object.defineProperty(exports, '__esModule', {
107
+ value: true
108
+ });
@@ -44,25 +44,26 @@ const external_node_fs_namespaceObject = require("node:fs");
44
44
  var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_namespaceObject);
45
45
  const external_node_path_namespaceObject = require("node:path");
46
46
  var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
47
- const parser_namespaceObject = require("@babel/parser");
48
- const traverse_namespaceObject = require("@babel/traverse");
49
- var traverse_default = /*#__PURE__*/ __webpack_require__.n(traverse_namespaceObject);
50
47
  const types_namespaceObject = require("@babel/types");
48
+ const external_source_analysis_cjs_namespaceObject = require("./source-analysis.cjs");
51
49
  const edge = '@modern-js/bff-effect/effect-edge';
52
50
  const nodeRuntime = '@modern-js/bff-effect/effect';
53
- const sharedRuntime = /^@[^/]+\/shared-contracts\/server\/effect-bff-runtime$/u;
54
- const failure = 'Generated API entries must export defineEffectBff(...) or the server-only shared Effect BFF assembly helper with an explicitly composed handler Layer and an unshadowed executable root; entries must implement handlers through HttpApiBuilder.group.';
55
- const invalidSources = new WeakSet();
56
- function invalidSource(message) {
57
- const error = new Error(message);
58
- invalidSources.add(error);
59
- return error;
60
- }
61
- class SourceValidationHub extends traverse_namespaceObject.Hub {
62
- buildError(_node, message) {
63
- return invalidSource(message);
64
- }
65
- }
51
+ const assembly = '@modern-js/bff-effect/assembly';
52
+ const failure = 'Generated API entries must export defineEffectBff(...) or the native Effect BFF assembly helper with an explicitly composed handler Layer and an unshadowed executable root; entries must implement handlers through HttpApiBuilder.group.';
53
+ const MAX_API_SOURCE_MODULES = 256;
54
+ const httpRouteMethods = new Set([
55
+ '*',
56
+ 'GET',
57
+ 'POST',
58
+ 'PUT',
59
+ 'PATCH',
60
+ 'DELETE',
61
+ 'HEAD',
62
+ 'OPTIONS',
63
+ 'TRACE',
64
+ 'CONNECT',
65
+ 'QUERY'
66
+ ]);
66
67
  function createEffectApiImportResolver(filename) {
67
68
  const directory = external_node_fs_default().realpathSync(external_node_path_default().dirname(filename));
68
69
  const owner = external_node_fs_default().realpathSync(external_node_path_default().resolve(directory, '..'));
@@ -94,34 +95,14 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
94
95
  const load = (input)=>{
95
96
  const existing = modules.get(input.id);
96
97
  if (existing) return existing;
97
- if (modules.size >= 64 || input.source.length > 1000000) throw invalidSource('API source budget exceeded');
98
- let file;
99
- try {
100
- file = (0, parser_namespaceObject.parse)(input.source, {
101
- sourceType: 'module',
102
- sourceFilename: input.id,
103
- plugins: [
104
- "typescript"
105
- ]
106
- });
107
- } catch (cause) {
108
- if ('object' == typeof cause && null !== cause && 'code' in cause && 'BABEL_PARSER_SYNTAX_ERROR' === cause.code && 'reasonCode' in cause && 'string' == typeof cause.reasonCode) throw invalidSource('Invalid API syntax');
109
- throw cause;
110
- }
98
+ if (modules.size >= MAX_API_SOURCE_MODULES || input.source.length > 1000000) throw new external_source_analysis_cjs_namespaceObject.SourceSyntaxError('API source budget exceeded');
99
+ const file = (0, external_source_analysis_cjs_namespaceObject.parseSource)(input.source, input.id, 'Invalid API syntax');
111
100
  const module = {
112
101
  ...input,
113
102
  file
114
103
  };
115
104
  modules.set(input.id, module);
116
- const program = traverse_namespaceObject.NodePath.get({
117
- hub: new SourceValidationHub(),
118
- parentPath: void 0,
119
- parent: file,
120
- container: file,
121
- key: 'program'
122
- });
123
- program.setContext();
124
- traverse_default()(file, {
105
+ (0, external_source_analysis_cjs_namespaceObject.traverseSource)(file, {
125
106
  enter (nodePath) {
126
107
  paths.set(nodePath.node, nodePath);
127
108
  owners.set(nodePath.node, module);
@@ -130,9 +111,12 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
130
111
  })) {
131
112
  let target = nodePath.isAssignmentExpression() ? nodePath.node.left : nodePath.node.argument;
132
113
  while(types_namespaceObject.isMemberExpression(target))target = target.object;
133
- if (types_namespaceObject.isIdentifier(target)) {
134
- const binding = nodePath.scope.getBinding(target.name);
135
- if (binding) mutated.add(binding);
114
+ let binding = types_namespaceObject.isIdentifier(target) ? nodePath.scope.getBinding(target.name) : void 0;
115
+ while(binding && !mutated.has(binding)){
116
+ mutated.add(binding);
117
+ const declarator = binding.path.node;
118
+ const alias = 'const' === binding.kind && types_namespaceObject.isVariableDeclarator(declarator) && declarator.init ? (0, external_source_analysis_cjs_namespaceObject.unwrapExpression)(declarator.init, true) : void 0;
119
+ binding = types_namespaceObject.isIdentifier(alias) ? binding.path.scope.getBinding(alias.name) : void 0;
136
120
  }
137
121
  }
138
122
  if (nodePath.isTSModuleDeclaration() && types_namespaceObject.isIdentifier(nodePath.node.id)) {
@@ -140,7 +124,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
140
124
  if (binding) mutated.add(binding);
141
125
  }
142
126
  }
143
- }, program.scope, void 0, program);
127
+ });
144
128
  return module;
145
129
  };
146
130
  try {
@@ -149,10 +133,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
149
133
  source,
150
134
  resolveImport
151
135
  });
152
- const unwrap = (node)=>{
153
- while(types_namespaceObject.isParenthesizedExpression(node) || types_namespaceObject.isTSSatisfiesExpression(node) || types_namespaceObject.isTSAsExpression(node) || types_namespaceObject.isTSTypeAssertion(node) || types_namespaceObject.isTSNonNullExpression(node))node = node.expression;
154
- return node;
155
- };
136
+ const unwrap = (node)=>(0, external_source_analysis_cjs_namespaceObject.unwrapExpression)(node, true);
156
137
  const binding = (node)=>{
157
138
  node = unwrap(node);
158
139
  if (!types_namespaceObject.isIdentifier(node)) return;
@@ -177,15 +158,15 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
177
158
  const namespaceSources = {
178
159
  HttpApi: [
179
160
  edge,
180
- 'effect/unstable/httpapi'
161
+ 'effect/http-api'
181
162
  ],
182
163
  HttpApiBuilder: [
183
164
  edge,
184
- 'effect/unstable/httpapi'
165
+ 'effect/http-api'
185
166
  ],
186
167
  HttpRouter: [
187
168
  edge,
188
- 'effect/unstable/http'
169
+ 'effect/http'
189
170
  ]
190
171
  };
191
172
  const native = (node, name, sources)=>{
@@ -281,7 +262,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
281
262
  if (!types_namespaceObject.isBlockStatement(body)) return body;
282
263
  const statements = body.body;
283
264
  const last = statements.at(-1);
284
- return statements.slice(0, -1).every((statement)=>types_namespaceObject.isVariableDeclaration(statement)) && types_namespaceObject.isReturnStatement(last) ? last.argument : void 0;
265
+ return statements.slice(0, -1).every((statement)=>types_namespaceObject.isVariableDeclaration(statement) || types_namespaceObject.isTSTypeAliasDeclaration(statement) || types_namespaceObject.isTSInterfaceDeclaration(statement)) && types_namespaceObject.isReturnStatement(last) ? last.argument : void 0;
285
266
  };
286
267
  const pipe = (node, check)=>types_namespaceObject.isMemberExpression(node.callee) && !node.callee.computed && types_namespaceObject.isIdentifier(node.callee.property, {
287
268
  name: 'pipe'
@@ -310,11 +291,48 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
310
291
  if (method(node.callee, 'Layer', 'mergeAll', layerSources)) return node.arguments.length > 0 && node.arguments.every((argument)=>handlers(argument, api));
311
292
  return method(node.callee, 'HttpApiBuilder', 'group') && 3 === node.arguments.length && apiIdentity(node.arguments[0]) === api && handled(node.arguments[2]);
312
293
  });
313
- const transport = (node)=>guarded(node, (node)=>{
294
+ const effectSources = [
295
+ edge,
296
+ 'effect',
297
+ 'effect/Effect'
298
+ ];
299
+ const routeHandler = (node)=>{
300
+ node = unwrap(node);
301
+ if (!types_namespaceObject.isCallExpression(node) || !method(node.callee, 'HttpRouter', 'add') || 3 !== node.arguments.length) return;
302
+ const [verb, route, handler] = node.arguments;
303
+ return types_namespaceObject.isStringLiteral(verb) && httpRouteMethods.has(verb.value) && types_namespaceObject.isStringLiteral(route) && (route.value.startsWith('/') || '*' === route.value) && (types_namespaceObject.isArrowFunctionExpression(handler) || types_namespaceObject.isFunctionExpression(handler)) ? handler : void 0;
304
+ };
305
+ const effectReference = (node)=>guarded(node, (node)=>{
306
+ const init = initialized(node);
307
+ if (init) return types_namespaceObject.isIdentifier(unwrap(init)) && effectReference(init);
308
+ const value = imported(node);
309
+ return !!value && !value.namespace;
310
+ });
311
+ const capturedRouteFactory = (node)=>guarded(node, (node)=>{
312
+ const init = initialized(node) ?? externalValue(node);
313
+ if (init) return capturedRouteFactory(init);
314
+ if (!types_namespaceObject.isArrowFunctionExpression(node) || node.async || 1 !== node.params.length || !types_namespaceObject.isIdentifier(node.params[0]) || types_namespaceObject.isBlockStatement(node.body)) return false;
315
+ const handler = routeHandler(node.body);
316
+ const parameter = binding(node.params[0]);
317
+ return !!handler && !!parameter && parameter.referencePaths.some((reference)=>!!reference.findParent((path)=>path.node === handler));
318
+ });
319
+ const capturedTransport = (node)=>guarded(node, (node)=>{
314
320
  const init = initialized(node);
315
- if (init) return transport(init);
321
+ if (init) return capturedTransport(init);
322
+ if (!types_namespaceObject.isCallExpression(node)) return false;
323
+ if (method(node.callee, 'Effect', 'map', effectSources)) return 2 === node.arguments.length && effectReference(node.arguments[0]) && capturedRouteFactory(node.arguments[1]);
324
+ if (!types_namespaceObject.isMemberExpression(node.callee) || node.callee.computed || !types_namespaceObject.isIdentifier(node.callee.property, {
325
+ name: 'pipe'
326
+ }) || 1 !== node.arguments.length || !effectReference(node.callee.object)) return false;
327
+ const map = unwrap(node.arguments[0]);
328
+ return types_namespaceObject.isCallExpression(map) && method(map.callee, 'Effect', 'map', effectSources) && 1 === map.arguments.length && capturedRouteFactory(map.arguments[0]);
329
+ });
330
+ const transport = (node, provided = false)=>guarded(node, (node)=>{
331
+ const init = initialized(node) ?? externalValue(node);
332
+ if (init) return transport(init, provided);
316
333
  if (!types_namespaceObject.isCallExpression(node)) return method(node, 'Layer', 'empty', layerSources);
317
- return pipe(node, transport) || method(node.callee, 'HttpRouter', 'cors') || method(node.callee, 'Layer', 'mergeAll', layerSources) && node.arguments.length > 0 && node.arguments.every(transport);
334
+ const providedHere = provided || node.arguments.some((argument)=>types_namespaceObject.isCallExpression(argument) && method(argument.callee, 'Layer', 'provide', layerSources) && 1 === argument.arguments.length);
335
+ return pipe(node, (value)=>transport(value, providedHere)) || void 0 !== routeHandler(node) || provided && method(node.callee, 'Layer', 'unwrap', layerSources) && 1 === node.arguments.length && capturedTransport(node.arguments[0]) || method(node.callee, 'HttpRouter', 'cors') || method(node.callee, 'Layer', 'mergeAll', layerSources) && node.arguments.length > 0 && node.arguments.every((argument)=>transport(argument, provided));
318
336
  });
319
337
  const directLayer = (node, api)=>guarded(node, (node)=>{
320
338
  const init = initialized(node);
@@ -346,7 +364,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
346
364
  const groupValue = externalValue(group);
347
365
  const groupInit = groupValue && initialized(groupValue);
348
366
  if (!groupInit || !types_namespaceObject.isCallExpression(groupInit) || !method(groupInit.callee, 'RpcGroup', 'make', [
349
- 'effect/unstable/rpc',
367
+ 'effect/rpc',
350
368
  '@modern-js/bff-effect/effect-client'
351
369
  ])) return false;
352
370
  const layerInit = initialized(layer) ?? unwrap(layer);
@@ -370,7 +388,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
370
388
  if (value?.name === 'defineEffectBff' && [
371
389
  edge,
372
390
  nodeRuntime
373
- ].includes(value.specifier) || value?.name === 'assembleEffectBffRuntime' && sharedRuntime.test(value.specifier)) {
391
+ ].includes(value.specifier) || value?.name === 'assembleEffectBffRuntime' && value.specifier === assembly) {
374
392
  if (1 !== node.arguments.length) return false;
375
393
  const values = properties(node.arguments[0]);
376
394
  if (!values) return false;
@@ -393,7 +411,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
393
411
  const root = entry.file.program.body.find((statement)=>types_namespaceObject.isExportDefaultDeclaration(statement));
394
412
  return root && runtime(root.declaration) ? void 0 : failure;
395
413
  } catch (cause) {
396
- if ('object' == typeof cause && null !== cause && invalidSources.has(cause)) return failure;
414
+ if (cause instanceof external_source_analysis_cjs_namespaceObject.SourceSyntaxError) return failure;
397
415
  const message = 'object' == typeof cause && null !== cause && 'message' in cause && 'string' == typeof cause.message ? cause.message : String(cause);
398
416
  throw new Error(`Strict Effect API analysis failed: ${message}`, {
399
417
  cause
@@ -1,27 +1,46 @@
1
+ import node_path from "node:path";
2
+ import { pathToFileURL } from "node:url";
1
3
  import { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles } from "../microvertical-api-boundary.js";
2
- function runMicroVerticalApiCheckCli(args = process.argv.slice(2), filesOnly = false) {
4
+ const flags = [
5
+ '--workspace-root',
6
+ '--baseline-package-directory',
7
+ '--rules'
8
+ ];
9
+ async function loadSourceRules(file) {
10
+ const rules = (await import(pathToFileURL(node_path.resolve(file)).href)).default;
11
+ if (!Array.isArray(rules) || !rules.every((rule)=>'function' == typeof rule)) throw new Error(`${file}: default export must be an array of source rule functions, e.g. \`export default [myRule]\``);
12
+ return rules;
13
+ }
14
+ async function runMicroVerticalApiCheckCli(args = process.argv.slice(2), filesOnly = false) {
3
15
  let workspaceRoot = process.env.ULTRAMODERN_WORKSPACE_ROOT ?? process.cwd();
4
16
  let baselinePackageDirectory;
17
+ const ruleFiles = [];
5
18
  for(let index = 0; index < args.length; index += 1){
6
19
  const argument = args[index];
7
20
  if ('--help' === argument || '-h' === argument) {
8
- console.log(`${filesOnly ? 'modern-api-check-files' : 'modern-api-check'} [--workspace-root <path>] [--baseline-package-directory <path>]\n${filesOnly ? 'Checks consumer files, contracts, package exports and owner identity. Does not analyze runtime topology; run the strict Effect source phase separately.' : 'Checks consumer files, contracts, package exports, owner identity and runtime topology once per API entry.'}\nWorkspace defaults to ULTRAMODERN_WORKSPACE_ROOT then cwd. Exit codes: 0 valid, 1 consumer violation, 2 tool/configuration failure.`);
21
+ console.log(`${filesOnly ? 'modern-api-check-files' : 'modern-api-check'} [--workspace-root <path>] [--baseline-package-directory <path>] [--rules <module>]...\n${filesOnly ? 'Checks consumer files, contracts, package exports and owner identity. Does not analyze runtime topology; run the strict Effect source phase separately.' : 'Checks consumer files, contracts, package exports, owner identity and runtime topology once per API entry.'}\nThe baseline owner defaults to each contract's native package resolver. --baseline-package-directory requires that exact installed owner for every contract.\n--rules loads a module whose default export is an array of source rules; each rule runs once per workspace source file with the shared module graph.\nWorkspace defaults to ULTRAMODERN_WORKSPACE_ROOT then cwd. Exit codes: 0 valid, 1 consumer violation, 2 tool/configuration failure.`);
9
22
  return 0;
10
23
  }
11
- if (![
12
- '--workspace-root',
13
- '--baseline-package-directory'
14
- ].includes(argument ?? '') || !args[index + 1] || args[index + 1]?.startsWith('--')) {
24
+ if (!flags.includes(argument ?? '') || !args[index + 1] || args[index + 1]?.startsWith('--')) {
15
25
  console.error(`Invalid argument: ${argument ?? ''}. Use --help.`);
16
26
  return 2;
17
27
  }
18
28
  const value = args[++index];
19
29
  if ('--workspace-root' === argument) workspaceRoot = value;
30
+ else if ('--rules' === argument) ruleFiles.push(value);
20
31
  else baselinePackageDirectory = value;
21
32
  }
33
+ const sourceRules = [];
34
+ for (const file of ruleFiles)try {
35
+ sourceRules.push(...await loadSourceRules(file));
36
+ } catch (error) {
37
+ console.error(`API tool error: --rules ${error instanceof Error ? error.message : String(error)}`);
38
+ return 2;
39
+ }
22
40
  const result = (filesOnly ? checkMicroVerticalApiConsumerFiles : checkMicroVerticalApiBoundaries)({
23
41
  workspaceRoot,
24
- baselinePackageDirectory
42
+ baselinePackageDirectory,
43
+ sourceRules
25
44
  });
26
45
  for (const diagnostic of result.diagnostics)console.error(`API violation: ${diagnostic}`);
27
46
  for (const error of result.toolErrors)console.error(`API tool error: ${error}`);
@@ -6,6 +6,11 @@ const DEFAULT_LOCALES = [
6
6
  'en',
7
7
  'cs'
8
8
  ];
9
+ const DEFAULT_SOURCE_ROOTS = [
10
+ 'apps',
11
+ 'verticals',
12
+ 'packages'
13
+ ];
9
14
  const ignoredDirectories = new Set([
10
15
  '.modern',
11
16
  '.modernjs',
@@ -39,6 +44,8 @@ const createLocaleJsonMatcher = (locales)=>{
39
44
  const resolvePluralCategories = (locale, overrides)=>overrides?.[locale] ?? new Intl.PluralRules(locale).resolvedOptions().pluralCategories;
40
45
  const readText = (filePath)=>node_fs.readFileSync(filePath, 'utf-8');
41
46
  const localeImportPattern = (locale)=>new RegExp(`import\\s+(?:\\*\\s+as\\s+)?[A-Za-z_$][\\w$]*\\s+from\\s+['"]\\.\\./locales/${escapeRegExp(locale)}/[^'"]+\\.json['"]`, 'u');
47
+ class SourceViolation extends Error {
48
+ }
42
49
  const checkRuntimeResources = (root, filePath, text, locales)=>{
43
50
  const relative = relativePath(root, filePath);
44
51
  if (!relative.endsWith('/src/modern.runtime.ts')) return;
@@ -46,7 +53,7 @@ const checkRuntimeResources = (root, filePath, text, locales)=>{
46
53
  const registersResources = /initOptions\s*:\s*\{[\s\S]*?\bresources\s*[,:}]/u.test(text);
47
54
  if (missingLocales.length > 0 || !registersResources) {
48
55
  const detail = missingLocales.length > 0 ? `missing locale JSON imports for: ${missingLocales.join(', ')}` : 'initOptions does not register a `resources` entry';
49
- throw new Error(`${relative} must register locale JSON resources in modern.runtime.ts so Worker SSR and hydration use the same first-render translations (${detail}).`);
56
+ throw new SourceViolation(`${relative} must register locale JSON resources in modern.runtime.ts so Worker SSR and hydration use the same first-render translations (${detail}).`);
50
57
  }
51
58
  };
52
59
  const visitLocaleKeys = (value, visitor, pathParts = [])=>{
@@ -66,7 +73,7 @@ const checkPluralResources = (root, filePath, json, requiredSuffixes, pluralSuff
66
73
  visitLocaleKeys(json, (key, value, pathParts)=>{
67
74
  if ('string' != typeof value || !value.includes('{{count}}')) return;
68
75
  const suffixMatch = key.match(pluralSuffixPattern);
69
- if (!suffixMatch) throw new Error(`${relative} key ${pathParts.join('.')} contains {{count}} but is not plural-suffixed.`);
76
+ if (!suffixMatch) throw new SourceViolation(`${relative} key ${pathParts.join('.')} contains {{count}} but is not plural-suffixed.`);
70
77
  const [, base = '', suffix = ''] = suffixMatch;
71
78
  const parentPath = pathParts.slice(0, -1).join('.');
72
79
  const groupKey = `${parentPath}.${base}`;
@@ -74,7 +81,7 @@ const checkPluralResources = (root, filePath, json, requiredSuffixes, pluralSuff
74
81
  existing.add(suffix);
75
82
  groups.set(groupKey, existing);
76
83
  });
77
- for (const [group, suffixes] of groups)for (const suffix of requiredSuffixes)if (!suffixes.has(suffix)) throw new Error(`${relative} plural group ${group} is missing _${suffix}.`);
84
+ for (const [group, suffixes] of groups)for (const suffix of requiredSuffixes)if (!suffixes.has(suffix)) throw new SourceViolation(`${relative} plural group ${group} is missing _${suffix}.`);
78
85
  };
79
86
  const runRuntimeAndLocaleResourceChecks = (root, sourceRoots, locales, pluralCategories)=>{
80
87
  if (0 === locales.length) return;
@@ -96,11 +103,7 @@ const runRuntimeAndLocaleResourceChecks = (root, sourceRoots, locales, pluralCat
96
103
  checkPluralResources(root, filePath, JSON.parse(readText(filePath)), localeCategories.get(language) ?? [], pluralSuffixPattern);
97
104
  }
98
105
  };
99
- const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
100
- 'apps',
101
- 'verticals',
102
- 'packages'
103
- ], locales = DEFAULT_LOCALES, pluralCategories } = {})=>{
106
+ const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = DEFAULT_SOURCE_ROOTS, locales = DEFAULT_LOCALES, pluralCategories } = {})=>{
104
107
  const oxlintResult = runOxlintRules({
105
108
  cwd,
106
109
  targets: sourceRoots,
@@ -133,13 +136,83 @@ const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
133
136
  try {
134
137
  runRuntimeAndLocaleResourceChecks(cwd, sourceRoots, locales, pluralCategories);
135
138
  } catch (error) {
136
- console.error(error instanceof Error ? error.message : 'UltraModern workspace source checks failed.');
137
- return 1;
139
+ console.error(error instanceof Error ? error.message : String(error));
140
+ return error instanceof SourceViolation ? 1 : 2;
138
141
  }
139
142
  console.log(WORKSPACE_SOURCE_SUCCESS);
140
143
  return 0;
141
144
  };
142
- const main = ()=>{
143
- process.exitCode = runWorkspaceSourceCheck();
145
+ const USAGE = `modern-i18n-check [--workspace-root <path>]
146
+ Runs the UltraModern i18n and boundary source checks for a workspace.
147
+ Workspace defaults to ULTRAMODERN_WORKSPACE_ROOT then cwd. Options come from
148
+ package.json "modernjs.i18nCheck": { sourceRoots, locales, pluralCategories }.
149
+ Exit codes: 0 valid, 1 source violation, 2 tool/configuration failure.`;
150
+ const isStringArray = (value)=>Array.isArray(value) && value.every((entry)=>'string' == typeof entry && entry.length > 0);
151
+ const isLocaleArray = (value)=>{
152
+ if (!isStringArray(value)) return false;
153
+ try {
154
+ Intl.getCanonicalLocales(value);
155
+ return true;
156
+ } catch {
157
+ return false;
158
+ }
159
+ };
160
+ const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
161
+ const isWorkspaceDirectory = (root, entry)=>{
162
+ if (node_path.isAbsolute(entry)) return false;
163
+ const target = node_path.resolve(root, entry);
164
+ if (!node_fs.existsSync(target) || !node_fs.statSync(target).isDirectory()) return false;
165
+ const relative = node_path.relative(node_fs.realpathSync(root), node_fs.realpathSync(target));
166
+ return '' !== relative && '..' !== relative && !relative.startsWith(`..${node_path.sep}`) && !node_path.isAbsolute(relative);
167
+ };
168
+ const CLDR_PLURAL_CATEGORIES = new Set([
169
+ 'zero',
170
+ 'one',
171
+ 'two',
172
+ 'few',
173
+ 'many',
174
+ 'other'
175
+ ]);
176
+ const isPluralCategories = (value)=>isRecord(value) && Object.values(value).every((categories)=>isStringArray(categories) && categories.includes('other') && categories.every((category)=>CLDR_PLURAL_CATEGORIES.has(category)));
177
+ const readWorkspaceCheckOptions = (root)=>{
178
+ const manifestPath = node_path.join(root, 'package.json');
179
+ const modernjs = JSON.parse(readText(manifestPath)).modernjs;
180
+ const config = isRecord(modernjs) && Object.hasOwn(modernjs, 'i18nCheck') ? modernjs.i18nCheck : {};
181
+ const invalid = (field, expected)=>new Error(`${manifestPath} "modernjs.i18nCheck${field}" must be ${expected}.`);
182
+ if (!isRecord(config)) throw invalid('', 'an object');
183
+ const { locales, pluralCategories } = config;
184
+ if (void 0 !== locales && !isLocaleArray(locales)) throw invalid('.locales', 'an array of BCP 47 locale codes');
185
+ if (void 0 !== pluralCategories && !isPluralCategories(pluralCategories)) throw invalid('.pluralCategories', 'an object of locale -> CLDR plural categories including "other"');
186
+ const sourceRoots = Object.hasOwn(config, 'sourceRoots') ? config.sourceRoots : DEFAULT_SOURCE_ROOTS.filter((entry)=>isWorkspaceDirectory(root, entry));
187
+ if (!isStringArray(sourceRoots) || 0 === sourceRoots.length || !sourceRoots.every((entry)=>isWorkspaceDirectory(root, entry))) throw invalid('.sourceRoots', `a non-empty array of existing workspace-relative directories (default: whichever of ${DEFAULT_SOURCE_ROOTS.join(', ')} exist)`);
188
+ return {
189
+ cwd: root,
190
+ sourceRoots,
191
+ locales,
192
+ pluralCategories
193
+ };
194
+ };
195
+ const runWorkspaceSourceCheckCli = (args = process.argv.slice(2))=>{
196
+ let workspaceRoot = process.env.ULTRAMODERN_WORKSPACE_ROOT ?? process.cwd();
197
+ for(let index = 0; index < args.length; index += 1){
198
+ const argument = args[index];
199
+ if ('--help' === argument || '-h' === argument) {
200
+ console.log(USAGE);
201
+ return 0;
202
+ }
203
+ const value = args[index + 1];
204
+ if ('--workspace-root' !== argument || !value || value.startsWith('--')) {
205
+ console.error(`Invalid argument: ${argument ?? ''}. Use --help.`);
206
+ return 2;
207
+ }
208
+ workspaceRoot = value;
209
+ index += 1;
210
+ }
211
+ try {
212
+ return runWorkspaceSourceCheck(readWorkspaceCheckOptions(node_path.resolve(workspaceRoot)));
213
+ } catch (error) {
214
+ console.error(error instanceof Error ? error.message : String(error));
215
+ return 2;
216
+ }
144
217
  };
145
- export { main, runWorkspaceSourceCheck };
218
+ export { runWorkspaceSourceCheck, runWorkspaceSourceCheckCli };