@bleedingdev/modern-js-code-tools 3.9.0-ultramodern.16 → 3.9.0-ultramodern.18

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.
@@ -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,7 @@ 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");
53
55
  const external_source_analysis_cjs_namespaceObject = require("./source-analysis.cjs");
54
56
  const external_strict_effect_runtime_cjs_namespaceObject = require("./strict-effect-runtime.cjs");
55
57
  const ignored = new Set([
@@ -453,6 +455,27 @@ function check(options, topology) {
453
455
  const violation = (0, external_microvertical_api_baseline_cjs_namespaceObject.microVerticalApiBaselineViolation)(stem, absolute(contract), expectation);
454
456
  if (violation) diagnostics.push(`${contract}: ${violation}`);
455
457
  }
458
+ if (options.contractRules?.length && exists(contract)) {
459
+ let graph;
460
+ try {
461
+ graph = (0, external_module_graph_cjs_namespaceObject.createModuleGraph)(absolute(contract));
462
+ } catch (error) {
463
+ if (!(error instanceof external_source_analysis_cjs_namespaceObject.SourceSyntaxError)) throw error;
464
+ diagnostics.push(`${contract}: contract rules need a parseable contract with immutable bindings (${error.message})`);
465
+ }
466
+ if (graph) {
467
+ const context = {
468
+ appPath,
469
+ protocol: rpc ? 'rpc' : 'rest',
470
+ graph
471
+ };
472
+ for (const rule of options.contractRules)guarded(`${contract} (rule ${rule.name || 'anonymous'})`, ()=>{
473
+ const messages = rule(context);
474
+ if (!Array.isArray(messages) || !messages.every((message)=>'string' == typeof message)) throw new Error('contract rule must return an array of violation messages (return [] when the contract passes)');
475
+ for (const message of messages)diagnostics.push(`${contract}: ${message}`);
476
+ });
477
+ }
478
+ }
456
479
  if (client) {
457
480
  const relativeContract = external_node_path_default().posix.relative(external_node_path_default().posix.dirname(client), contract);
458
481
  moduleShape(client, [
@@ -549,11 +572,13 @@ const checkMicroVerticalApiConsumerFiles = (options)=>check(options, false);
549
572
  exports.checkMicroVerticalApiBoundaries = __webpack_exports__.checkMicroVerticalApiBoundaries;
550
573
  exports.checkMicroVerticalApiConsumerFiles = __webpack_exports__.checkMicroVerticalApiConsumerFiles;
551
574
  exports.configuredMicroVerticalApiStem = __webpack_exports__.configuredMicroVerticalApiStem;
575
+ exports.createModuleGraph = __webpack_exports__.createModuleGraph;
552
576
  exports.microVerticalApiBaselineViolation = __webpack_exports__.microVerticalApiBaselineViolation;
553
577
  for(var __rspack_i in __webpack_exports__)if (-1 === [
554
578
  "checkMicroVerticalApiBoundaries",
555
579
  "checkMicroVerticalApiConsumerFiles",
556
580
  "configuredMicroVerticalApiStem",
581
+ "createModuleGraph",
557
582
  "microVerticalApiBaselineViolation"
558
583
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
559
584
  Object.defineProperty(exports, '__esModule', {
@@ -0,0 +1,321 @@
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
+ createModuleGraph: ()=>createModuleGraph,
41
+ exportedConst: ()=>exportedConst,
42
+ localConst: ()=>localConst,
43
+ parseConsumer: ()=>parseConsumer,
44
+ propertyName: ()=>propertyName
45
+ });
46
+ const external_node_fs_namespaceObject = require("node:fs");
47
+ var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_namespaceObject);
48
+ const external_node_module_namespaceObject = require("node:module");
49
+ const external_node_path_namespaceObject = require("node:path");
50
+ var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
51
+ const types_namespaceObject = require("@babel/types");
52
+ const external_resolve_exports_namespaceObject = require("resolve.exports");
53
+ const external_source_analysis_cjs_namespaceObject = require("./source-analysis.cjs");
54
+ const MAX_MODULES = 256;
55
+ const MAX_LOOKUPS = 512;
56
+ function parseConsumer(filePath) {
57
+ if (external_node_fs_default().statSync(filePath).size > 1000000) throw new Error(`${filePath}: consumer source exceeds 1 MB analysis budget`);
58
+ const file = (0, external_source_analysis_cjs_namespaceObject.parseSource)(external_node_fs_default().readFileSync(filePath, 'utf8'), filePath);
59
+ (0, external_source_analysis_cjs_namespaceObject.traverseSource)(file, {
60
+ AssignmentExpression (p) {
61
+ throw new external_source_analysis_cjs_namespaceObject.SourceSyntaxError(`contract bindings must be immutable at ${p.node.start}`);
62
+ },
63
+ UpdateExpression (p) {
64
+ throw new external_source_analysis_cjs_namespaceObject.SourceSyntaxError(`contract bindings must be immutable at ${p.node.start}`);
65
+ },
66
+ TSModuleDeclaration () {
67
+ throw new external_source_analysis_cjs_namespaceObject.SourceSyntaxError('contract bindings must not be merged with namespaces');
68
+ }
69
+ });
70
+ return file;
71
+ }
72
+ const propertyName = (node)=>{
73
+ if (void 0 !== node && (types_namespaceObject.isIdentifier(node) || types_namespaceObject.isStringLiteral(node) || types_namespaceObject.isNumericLiteral(node))) return types_namespaceObject.isIdentifier(node) ? node.name : String(node.value);
74
+ };
75
+ const localConst = (sourceFile, name)=>{
76
+ for (const item of sourceFile.program.body){
77
+ const statement = types_namespaceObject.isExportNamedDeclaration(item) ? item.declaration : item;
78
+ if (!types_namespaceObject.isVariableDeclaration(statement) || 'const' !== statement.kind) continue;
79
+ const matches = statement.declarations.filter((declaration)=>types_namespaceObject.isIdentifier(declaration.id, {
80
+ name
81
+ }));
82
+ if (1 === matches.length) return matches[0];
83
+ }
84
+ };
85
+ const exportedConst = (sourceFile, name)=>{
86
+ for (const statement of sourceFile.program.body)if (types_namespaceObject.isExportNamedDeclaration(statement) && 'type' !== statement.exportKind) {
87
+ if (types_namespaceObject.isVariableDeclaration(statement.declaration) && 'const' === statement.declaration.kind) {
88
+ const declaration = statement.declaration.declarations.find((value)=>types_namespaceObject.isIdentifier(value.id, {
89
+ name
90
+ }));
91
+ if (declaration) return declaration;
92
+ }
93
+ if (!statement.source && statement.specifiers.some((value)=>types_namespaceObject.isExportSpecifier(value) && 'type' !== value.exportKind && types_namespaceObject.isIdentifier(value.local, {
94
+ name
95
+ }) && types_namespaceObject.isIdentifier(value.exported, {
96
+ name
97
+ }))) return localConst(sourceFile, name);
98
+ }
99
+ };
100
+ const inside = (file, boundary)=>file === boundary || file.startsWith(`${boundary}${external_node_path_default().sep}`);
101
+ const containingWorkspace = (file)=>{
102
+ for(let directory = external_node_path_default().dirname(file);; directory = external_node_path_default().dirname(directory)){
103
+ if (external_node_fs_default().existsSync(external_node_path_default().join(directory, 'pnpm-workspace.yaml'))) return external_node_fs_default().realpathSync(directory);
104
+ if (directory === external_node_path_default().dirname(directory)) return;
105
+ }
106
+ };
107
+ const sourceBoundary = (file, workspace)=>{
108
+ for(let directory = external_node_path_default().dirname(file);; directory = external_node_path_default().dirname(directory)){
109
+ if (external_node_fs_default().existsSync(external_node_path_default().join(directory, 'package.json')) && directory !== workspace) return external_node_fs_default().realpathSync(directory);
110
+ if (directory === workspace || directory === external_node_path_default().dirname(directory)) return external_node_fs_default().realpathSync(external_node_path_default().dirname(file));
111
+ }
112
+ };
113
+ const sourceFileAt = (target, boundary)=>{
114
+ const stem = target.replace(/\.(?:[cm]?[jt]sx?)$/u, '');
115
+ for (const candidate of [
116
+ `${stem}.ts`,
117
+ `${stem}.tsx`,
118
+ `${stem}.mts`,
119
+ `${stem}.cts`,
120
+ target,
121
+ external_node_path_default().join(target, 'index.ts'),
122
+ external_node_path_default().join(target, 'index.tsx')
123
+ ])try {
124
+ const real = external_node_fs_default().realpathSync(candidate);
125
+ if (inside(real, boundary) && external_node_fs_default().statSync(real).isFile()) return real;
126
+ } catch {}
127
+ };
128
+ const packageEntry = (specifier)=>{
129
+ if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#')) return;
130
+ const parts = specifier.split('/');
131
+ const count = specifier.startsWith('@') ? 2 : 1;
132
+ if (parts.length < count || parts.slice(0, count).some((part)=>!part)) return;
133
+ return {
134
+ name: parts.slice(0, count).join('/'),
135
+ exportKey: parts.length === count ? '.' : `./${parts.slice(count).join('/')}`
136
+ };
137
+ };
138
+ const resolveModulePath = (module, specifier, workspace)=>{
139
+ if (/^\.\.?\//u.test(specifier)) {
140
+ const file = sourceFileAt(external_node_path_default().resolve(external_node_path_default().dirname(module.path), specifier), module.boundary);
141
+ return void 0 === file ? void 0 : {
142
+ kind: 'module',
143
+ path: file,
144
+ boundary: module.boundary
145
+ };
146
+ }
147
+ const entry = packageEntry(specifier);
148
+ if (!entry) return;
149
+ if (!workspace) return {
150
+ kind: 'external'
151
+ };
152
+ const paths = (0, external_node_module_namespaceObject.createRequire)(module.path).resolve.paths(entry.name) ?? [];
153
+ for (const lookup of paths){
154
+ const directory = external_node_path_default().join(lookup, entry.name);
155
+ const manifest = external_node_path_default().join(directory, 'package.json');
156
+ if (!external_node_fs_default().existsSync(manifest)) continue;
157
+ const boundary = external_node_fs_default().realpathSync(directory);
158
+ if (!inside(boundary, workspace) || boundary.split(external_node_path_default().sep).includes('node_modules')) break;
159
+ const pkg = JSON.parse(external_node_fs_default().readFileSync(manifest, 'utf8'));
160
+ if (pkg.name !== entry.name || void 0 === pkg.exports) return;
161
+ let target;
162
+ try {
163
+ const targets = (0, external_resolve_exports_namespaceObject.exports)(pkg, entry.exportKey, {
164
+ conditions: [
165
+ 'modern:source'
166
+ ]
167
+ });
168
+ target = Array.isArray(targets) ? targets[0] : void 0;
169
+ } catch {
170
+ return;
171
+ }
172
+ if (!target || !target.startsWith('./')) return;
173
+ const file = sourceFileAt(external_node_path_default().resolve(boundary, target), boundary);
174
+ return void 0 === file ? void 0 : {
175
+ kind: 'module',
176
+ path: file,
177
+ boundary
178
+ };
179
+ }
180
+ return {
181
+ kind: 'external'
182
+ };
183
+ };
184
+ const importedBinding = (module, name)=>{
185
+ for (const statement of module.file.program.body)if (types_namespaceObject.isImportDeclaration(statement) && 'type' !== statement.importKind) {
186
+ for (const specifier of statement.specifiers)if (types_namespaceObject.isIdentifier(specifier.local, {
187
+ name
188
+ })) {
189
+ if (types_namespaceObject.isImportSpecifier(specifier) && 'type' !== specifier.importKind) return {
190
+ imported: propertyName(specifier.imported) ?? name,
191
+ specifier: statement.source.value
192
+ };
193
+ if (types_namespaceObject.isImportDefaultSpecifier(specifier)) return {
194
+ imported: 'default',
195
+ specifier: statement.source.value
196
+ };
197
+ }
198
+ }
199
+ };
200
+ const reexportedBinding = (module, name)=>{
201
+ for (const statement of module.file.program.body){
202
+ if (!types_namespaceObject.isExportNamedDeclaration(statement) || 'type' === statement.exportKind) continue;
203
+ const specifier = statement.specifiers.find((value)=>types_namespaceObject.isExportSpecifier(value) && 'type' !== value.exportKind && propertyName(value.exported) === name);
204
+ if (void 0 !== specifier && types_namespaceObject.isExportSpecifier(specifier)) return {
205
+ local: types_namespaceObject.isIdentifier(specifier.local) ? specifier.local.name : specifier.local.value,
206
+ ...statement.source ? {
207
+ specifier: statement.source.value
208
+ } : {}
209
+ };
210
+ }
211
+ };
212
+ const defaultExport = (module)=>{
213
+ for (const statement of module.file.program.body){
214
+ if (!types_namespaceObject.isExportDefaultDeclaration(statement)) continue;
215
+ const declaration = statement.declaration;
216
+ if (types_namespaceObject.isIdentifier(declaration)) return declaration.name;
217
+ return types_namespaceObject.isExpression(declaration) ? declaration : void 0;
218
+ }
219
+ };
220
+ function createModuleGraph(filePath) {
221
+ const realPath = external_node_fs_default().realpathSync(filePath);
222
+ const workspace = containingWorkspace(realPath);
223
+ const root = {
224
+ file: parseConsumer(realPath),
225
+ path: realPath,
226
+ boundary: sourceBoundary(realPath, workspace)
227
+ };
228
+ const modules = new Map([
229
+ [
230
+ root.path,
231
+ root
232
+ ]
233
+ ]);
234
+ const readModule = (modulePath, boundary)=>{
235
+ if (modules.has(modulePath)) return modules.get(modulePath);
236
+ if (modules.size >= MAX_MODULES) return;
237
+ let module;
238
+ try {
239
+ module = {
240
+ file: parseConsumer(modulePath),
241
+ path: modulePath,
242
+ boundary
243
+ };
244
+ } catch {
245
+ module = void 0;
246
+ }
247
+ modules.set(modulePath, module);
248
+ return module;
249
+ };
250
+ const resolveBinding = (module, name, scope, chain, seen)=>{
251
+ const key = `${scope}:${module.path}#${name}`;
252
+ if (seen.has(key) || seen.size >= MAX_LOOKUPS) return;
253
+ seen.add(key);
254
+ const last = chain.at(-1);
255
+ const hops = last?.path === module.path && last.name === name ? chain : [
256
+ ...chain,
257
+ {
258
+ path: module.path,
259
+ name
260
+ }
261
+ ];
262
+ const declaration = 'export' === scope ? exportedConst(module.file, name) : localConst(module.file, name);
263
+ if (declaration?.init) return {
264
+ kind: 'declaration',
265
+ expression: declaration.init,
266
+ module,
267
+ chain: hops
268
+ };
269
+ const throughModule = (specifier, imported, allowExternal)=>{
270
+ const target = resolveModulePath(module, specifier, workspace);
271
+ if (target?.kind === 'external') return allowExternal ? {
272
+ kind: 'external',
273
+ specifier,
274
+ name: imported,
275
+ chain: hops
276
+ } : void 0;
277
+ const next = target && readModule(target.path, target.boundary);
278
+ return void 0 === next ? void 0 : resolveBinding(next, imported, 'export', hops, seen);
279
+ };
280
+ if ('local' === scope) {
281
+ const imported = importedBinding(module, name);
282
+ return void 0 === imported ? void 0 : throughModule(imported.specifier, imported.imported, true);
283
+ }
284
+ if ('default' === name) {
285
+ const exported = defaultExport(module);
286
+ if ('string' == typeof exported) return resolveBinding(module, exported, 'local', hops, seen);
287
+ return void 0 === exported ? void 0 : {
288
+ kind: 'declaration',
289
+ expression: exported,
290
+ module,
291
+ chain: hops
292
+ };
293
+ }
294
+ const reexported = reexportedBinding(module, name);
295
+ if (void 0 !== reexported) return void 0 === reexported.specifier ? resolveBinding(module, reexported.local, 'local', hops, seen) : throughModule(reexported.specifier, reexported.local, true);
296
+ for (const statement of module.file.program.body){
297
+ if (!types_namespaceObject.isExportAllDeclaration(statement) || 'type' === statement.exportKind) continue;
298
+ const resolved = throughModule(statement.source.value, name, false);
299
+ if (void 0 !== resolved) return resolved;
300
+ }
301
+ };
302
+ return {
303
+ root,
304
+ resolve: (module, name, scope)=>resolveBinding(module, name, scope, [], new Set())
305
+ };
306
+ }
307
+ exports.createModuleGraph = __webpack_exports__.createModuleGraph;
308
+ exports.exportedConst = __webpack_exports__.exportedConst;
309
+ exports.localConst = __webpack_exports__.localConst;
310
+ exports.parseConsumer = __webpack_exports__.parseConsumer;
311
+ exports.propertyName = __webpack_exports__.propertyName;
312
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
313
+ "createModuleGraph",
314
+ "exportedConst",
315
+ "localConst",
316
+ "parseConsumer",
317
+ "propertyName"
318
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
319
+ Object.defineProperty(exports, '__esModule', {
320
+ value: true
321
+ });
@@ -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 loadContractRules(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 contract 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.'}\n--rules loads a module whose default export is an array of contract rules; each rule receives the module graph of every API contract.\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 contractRules = [];
34
+ for (const file of ruleFiles)try {
35
+ contractRules.push(...await loadContractRules(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
+ contractRules
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 };