@bleedingdev/modern-js-code-tools 3.9.0-ultramodern.17 → 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.
@@ -1,28 +1,47 @@
1
1
  import "node:module";
2
+ import node_path from "node:path";
3
+ import { pathToFileURL } from "node:url";
2
4
  import { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles } from "../microvertical-api-boundary.js";
3
- function runMicroVerticalApiCheckCli(args = process.argv.slice(2), filesOnly = false) {
5
+ const flags = [
6
+ '--workspace-root',
7
+ '--baseline-package-directory',
8
+ '--rules'
9
+ ];
10
+ async function loadContractRules(file) {
11
+ const rules = (await import(pathToFileURL(node_path.resolve(file)).href)).default;
12
+ 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]\``);
13
+ return rules;
14
+ }
15
+ async function runMicroVerticalApiCheckCli(args = process.argv.slice(2), filesOnly = false) {
4
16
  let workspaceRoot = process.env.ULTRAMODERN_WORKSPACE_ROOT ?? process.cwd();
5
17
  let baselinePackageDirectory;
18
+ const ruleFiles = [];
6
19
  for(let index = 0; index < args.length; index += 1){
7
20
  const argument = args[index];
8
21
  if ('--help' === argument || '-h' === argument) {
9
- 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.`);
22
+ 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.`);
10
23
  return 0;
11
24
  }
12
- if (![
13
- '--workspace-root',
14
- '--baseline-package-directory'
15
- ].includes(argument ?? '') || !args[index + 1] || args[index + 1]?.startsWith('--')) {
25
+ if (!flags.includes(argument ?? '') || !args[index + 1] || args[index + 1]?.startsWith('--')) {
16
26
  console.error(`Invalid argument: ${argument ?? ''}. Use --help.`);
17
27
  return 2;
18
28
  }
19
29
  const value = args[++index];
20
30
  if ('--workspace-root' === argument) workspaceRoot = value;
31
+ else if ('--rules' === argument) ruleFiles.push(value);
21
32
  else baselinePackageDirectory = value;
22
33
  }
34
+ const contractRules = [];
35
+ for (const file of ruleFiles)try {
36
+ contractRules.push(...await loadContractRules(file));
37
+ } catch (error) {
38
+ console.error(`API tool error: --rules ${error instanceof Error ? error.message : String(error)}`);
39
+ return 2;
40
+ }
23
41
  const result = (filesOnly ? checkMicroVerticalApiConsumerFiles : checkMicroVerticalApiBoundaries)({
24
42
  workspaceRoot,
25
- baselinePackageDirectory
43
+ baselinePackageDirectory,
44
+ contractRules
26
45
  });
27
46
  for (const diagnostic of result.diagnostics)console.error(`API violation: ${diagnostic}`);
28
47
  for (const error of result.toolErrors)console.error(`API tool error: ${error}`);
@@ -7,6 +7,11 @@ const DEFAULT_LOCALES = [
7
7
  'en',
8
8
  'cs'
9
9
  ];
10
+ const DEFAULT_SOURCE_ROOTS = [
11
+ 'apps',
12
+ 'verticals',
13
+ 'packages'
14
+ ];
10
15
  const ignoredDirectories = new Set([
11
16
  '.modern',
12
17
  '.modernjs',
@@ -40,6 +45,8 @@ const createLocaleJsonMatcher = (locales)=>{
40
45
  const resolvePluralCategories = (locale, overrides)=>overrides?.[locale] ?? new Intl.PluralRules(locale).resolvedOptions().pluralCategories;
41
46
  const readText = (filePath)=>node_fs.readFileSync(filePath, 'utf-8');
42
47
  const localeImportPattern = (locale)=>new RegExp(`import\\s+(?:\\*\\s+as\\s+)?[A-Za-z_$][\\w$]*\\s+from\\s+['"]\\.\\./locales/${escapeRegExp(locale)}/[^'"]+\\.json['"]`, 'u');
48
+ class SourceViolation extends Error {
49
+ }
43
50
  const checkRuntimeResources = (root, filePath, text, locales)=>{
44
51
  const relative = relativePath(root, filePath);
45
52
  if (!relative.endsWith('/src/modern.runtime.ts')) return;
@@ -47,7 +54,7 @@ const checkRuntimeResources = (root, filePath, text, locales)=>{
47
54
  const registersResources = /initOptions\s*:\s*\{[\s\S]*?\bresources\s*[,:}]/u.test(text);
48
55
  if (missingLocales.length > 0 || !registersResources) {
49
56
  const detail = missingLocales.length > 0 ? `missing locale JSON imports for: ${missingLocales.join(', ')}` : 'initOptions does not register a `resources` entry';
50
- 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}).`);
57
+ 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}).`);
51
58
  }
52
59
  };
53
60
  const visitLocaleKeys = (value, visitor, pathParts = [])=>{
@@ -67,7 +74,7 @@ const checkPluralResources = (root, filePath, json, requiredSuffixes, pluralSuff
67
74
  visitLocaleKeys(json, (key, value, pathParts)=>{
68
75
  if ('string' != typeof value || !value.includes('{{count}}')) return;
69
76
  const suffixMatch = key.match(pluralSuffixPattern);
70
- if (!suffixMatch) throw new Error(`${relative} key ${pathParts.join('.')} contains {{count}} but is not plural-suffixed.`);
77
+ if (!suffixMatch) throw new SourceViolation(`${relative} key ${pathParts.join('.')} contains {{count}} but is not plural-suffixed.`);
71
78
  const [, base = '', suffix = ''] = suffixMatch;
72
79
  const parentPath = pathParts.slice(0, -1).join('.');
73
80
  const groupKey = `${parentPath}.${base}`;
@@ -75,7 +82,7 @@ const checkPluralResources = (root, filePath, json, requiredSuffixes, pluralSuff
75
82
  existing.add(suffix);
76
83
  groups.set(groupKey, existing);
77
84
  });
78
- 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}.`);
85
+ 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}.`);
79
86
  };
80
87
  const runRuntimeAndLocaleResourceChecks = (root, sourceRoots, locales, pluralCategories)=>{
81
88
  if (0 === locales.length) return;
@@ -97,11 +104,7 @@ const runRuntimeAndLocaleResourceChecks = (root, sourceRoots, locales, pluralCat
97
104
  checkPluralResources(root, filePath, JSON.parse(readText(filePath)), localeCategories.get(language) ?? [], pluralSuffixPattern);
98
105
  }
99
106
  };
100
- const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
101
- 'apps',
102
- 'verticals',
103
- 'packages'
104
- ], locales = DEFAULT_LOCALES, pluralCategories } = {})=>{
107
+ const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = DEFAULT_SOURCE_ROOTS, locales = DEFAULT_LOCALES, pluralCategories } = {})=>{
105
108
  const oxlintResult = runOxlintRules({
106
109
  cwd,
107
110
  targets: sourceRoots,
@@ -134,13 +137,83 @@ const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
134
137
  try {
135
138
  runRuntimeAndLocaleResourceChecks(cwd, sourceRoots, locales, pluralCategories);
136
139
  } catch (error) {
137
- console.error(error instanceof Error ? error.message : 'UltraModern workspace source checks failed.');
138
- return 1;
140
+ console.error(error instanceof Error ? error.message : String(error));
141
+ return error instanceof SourceViolation ? 1 : 2;
139
142
  }
140
143
  console.log(WORKSPACE_SOURCE_SUCCESS);
141
144
  return 0;
142
145
  };
143
- const main = ()=>{
144
- process.exitCode = runWorkspaceSourceCheck();
146
+ const USAGE = `modern-i18n-check [--workspace-root <path>]
147
+ Runs the UltraModern i18n and boundary source checks for a workspace.
148
+ Workspace defaults to ULTRAMODERN_WORKSPACE_ROOT then cwd. Options come from
149
+ package.json "modernjs.i18nCheck": { sourceRoots, locales, pluralCategories }.
150
+ Exit codes: 0 valid, 1 source violation, 2 tool/configuration failure.`;
151
+ const isStringArray = (value)=>Array.isArray(value) && value.every((entry)=>'string' == typeof entry && entry.length > 0);
152
+ const isLocaleArray = (value)=>{
153
+ if (!isStringArray(value)) return false;
154
+ try {
155
+ Intl.getCanonicalLocales(value);
156
+ return true;
157
+ } catch {
158
+ return false;
159
+ }
160
+ };
161
+ const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
162
+ const isWorkspaceDirectory = (root, entry)=>{
163
+ if (node_path.isAbsolute(entry)) return false;
164
+ const target = node_path.resolve(root, entry);
165
+ if (!node_fs.existsSync(target) || !node_fs.statSync(target).isDirectory()) return false;
166
+ const relative = node_path.relative(node_fs.realpathSync(root), node_fs.realpathSync(target));
167
+ return '' !== relative && '..' !== relative && !relative.startsWith(`..${node_path.sep}`) && !node_path.isAbsolute(relative);
168
+ };
169
+ const CLDR_PLURAL_CATEGORIES = new Set([
170
+ 'zero',
171
+ 'one',
172
+ 'two',
173
+ 'few',
174
+ 'many',
175
+ 'other'
176
+ ]);
177
+ const isPluralCategories = (value)=>isRecord(value) && Object.values(value).every((categories)=>isStringArray(categories) && categories.includes('other') && categories.every((category)=>CLDR_PLURAL_CATEGORIES.has(category)));
178
+ const readWorkspaceCheckOptions = (root)=>{
179
+ const manifestPath = node_path.join(root, 'package.json');
180
+ const modernjs = JSON.parse(readText(manifestPath)).modernjs;
181
+ const config = isRecord(modernjs) && Object.hasOwn(modernjs, 'i18nCheck') ? modernjs.i18nCheck : {};
182
+ const invalid = (field, expected)=>new Error(`${manifestPath} "modernjs.i18nCheck${field}" must be ${expected}.`);
183
+ if (!isRecord(config)) throw invalid('', 'an object');
184
+ const { locales, pluralCategories } = config;
185
+ if (void 0 !== locales && !isLocaleArray(locales)) throw invalid('.locales', 'an array of BCP 47 locale codes');
186
+ if (void 0 !== pluralCategories && !isPluralCategories(pluralCategories)) throw invalid('.pluralCategories', 'an object of locale -> CLDR plural categories including "other"');
187
+ const sourceRoots = Object.hasOwn(config, 'sourceRoots') ? config.sourceRoots : DEFAULT_SOURCE_ROOTS.filter((entry)=>isWorkspaceDirectory(root, entry));
188
+ 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)`);
189
+ return {
190
+ cwd: root,
191
+ sourceRoots,
192
+ locales,
193
+ pluralCategories
194
+ };
195
+ };
196
+ const runWorkspaceSourceCheckCli = (args = process.argv.slice(2))=>{
197
+ let workspaceRoot = process.env.ULTRAMODERN_WORKSPACE_ROOT ?? process.cwd();
198
+ for(let index = 0; index < args.length; index += 1){
199
+ const argument = args[index];
200
+ if ('--help' === argument || '-h' === argument) {
201
+ console.log(USAGE);
202
+ return 0;
203
+ }
204
+ const value = args[index + 1];
205
+ if ('--workspace-root' !== argument || !value || value.startsWith('--')) {
206
+ console.error(`Invalid argument: ${argument ?? ''}. Use --help.`);
207
+ return 2;
208
+ }
209
+ workspaceRoot = value;
210
+ index += 1;
211
+ }
212
+ try {
213
+ return runWorkspaceSourceCheck(readWorkspaceCheckOptions(node_path.resolve(workspaceRoot)));
214
+ } catch (error) {
215
+ console.error(error instanceof Error ? error.message : String(error));
216
+ return 2;
217
+ }
145
218
  };
146
- export { main, runWorkspaceSourceCheck };
219
+ export { runWorkspaceSourceCheck, runWorkspaceSourceCheckCli };
@@ -1,36 +1,14 @@
1
1
  import "node:module";
2
- import node_fs from "node:fs";
3
- import { createRequire } from "node:module";
4
- import node_path from "node:path";
5
- import { exports as external_resolve_exports_exports } from "resolve.exports";
6
2
  import { baselinePublicIdentityIsExact } from "./microvertical-api-owner.js";
7
- import { SourceSyntaxError, parseSource, traverseSource, unwrapExpression } from "./source-analysis.js";
3
+ import { createModuleGraph, exportedConst, localConst, propertyName } from "./module-graph.js";
4
+ import { SourceSyntaxError, unwrapExpression } from "./source-analysis.js";
8
5
  import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
9
- function parseConsumer(filePath) {
10
- if (node_fs.statSync(filePath).size > 1000000) throw new Error(`${filePath}: consumer source exceeds 1 MB analysis budget`);
11
- const file = parseSource(node_fs.readFileSync(filePath, 'utf8'), filePath);
12
- traverseSource(file, {
13
- AssignmentExpression (p) {
14
- throw new SourceSyntaxError(`contract bindings must be immutable at ${p.node.start}`);
15
- },
16
- UpdateExpression (p) {
17
- throw new SourceSyntaxError(`contract bindings must be immutable at ${p.node.start}`);
18
- },
19
- TSModuleDeclaration () {
20
- throw new SourceSyntaxError('contract bindings must not be merged with namespaces');
21
- }
22
- });
23
- return file;
24
- }
25
6
  const camelCaseStem = (stem)=>stem.replaceAll(/-([a-z0-9])/gu, (_match, letter)=>letter.toUpperCase());
26
7
  const pascalCaseStem = (stem)=>{
27
8
  const camelStem = camelCaseStem(stem);
28
9
  return `${camelStem.slice(0, 1).toUpperCase()}${camelStem.slice(1)}`;
29
10
  };
30
11
  const identifierName = (node)=>void 0 !== node && __rspack_external__babel_types_6e8337dd.isIdentifier(node) ? node.name : void 0;
31
- const propertyName = (node)=>{
32
- if (void 0 !== node && (__rspack_external__babel_types_6e8337dd.isIdentifier(node) || __rspack_external__babel_types_6e8337dd.isStringLiteral(node) || __rspack_external__babel_types_6e8337dd.isNumericLiteral(node))) return __rspack_external__babel_types_6e8337dd.isIdentifier(node) ? node.name : String(node.value);
33
- };
34
12
  const accessPath = (node)=>{
35
13
  if (__rspack_external__babel_types_6e8337dd.isIdentifier(node)) return [
36
14
  node.name
@@ -59,201 +37,6 @@ const callExpression = (expression, callee)=>{
59
37
  const unwrapped = unwrapExpression(expression);
60
38
  return __rspack_external__babel_types_6e8337dd.isCallExpression(unwrapped) && isAccessPath(unwrapped.callee, callee) ? unwrapped : void 0;
61
39
  };
62
- const localConst = (sourceFile, name)=>{
63
- for (const item of sourceFile.program.body){
64
- const statement = __rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(item) ? item.declaration : item;
65
- if (!__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement) || 'const' !== statement.kind) continue;
66
- const matches = statement.declarations.filter((declaration)=>__rspack_external__babel_types_6e8337dd.isIdentifier(declaration.id, {
67
- name
68
- }));
69
- if (1 === matches.length) return matches[0];
70
- }
71
- };
72
- const exportedConst = (sourceFile, name)=>{
73
- for (const statement of sourceFile.program.body)if (__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) && 'type' !== statement.exportKind) {
74
- if (__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement.declaration) && 'const' === statement.declaration.kind) {
75
- const declaration = statement.declaration.declarations.find((value)=>__rspack_external__babel_types_6e8337dd.isIdentifier(value.id, {
76
- name
77
- }));
78
- if (declaration) return declaration;
79
- }
80
- if (!statement.source && statement.specifiers.some((value)=>__rspack_external__babel_types_6e8337dd.isExportSpecifier(value) && 'type' !== value.exportKind && __rspack_external__babel_types_6e8337dd.isIdentifier(value.local, {
81
- name
82
- }) && __rspack_external__babel_types_6e8337dd.isIdentifier(value.exported, {
83
- name
84
- }))) return localConst(sourceFile, name);
85
- }
86
- };
87
- const inside = (file, boundary)=>file === boundary || file.startsWith(`${boundary}${node_path.sep}`);
88
- const containingWorkspace = (file)=>{
89
- for(let directory = node_path.dirname(file);; directory = node_path.dirname(directory)){
90
- if (node_fs.existsSync(node_path.join(directory, 'pnpm-workspace.yaml'))) return node_fs.realpathSync(directory);
91
- if (directory === node_path.dirname(directory)) return;
92
- }
93
- };
94
- const sourceBoundary = (file, workspace)=>{
95
- for(let directory = node_path.dirname(file);; directory = node_path.dirname(directory)){
96
- if (node_fs.existsSync(node_path.join(directory, 'package.json')) && directory !== workspace) return node_fs.realpathSync(directory);
97
- if (directory === workspace || directory === node_path.dirname(directory)) return node_fs.realpathSync(node_path.dirname(file));
98
- }
99
- };
100
- const sourceFileAt = (target, boundary)=>{
101
- const stem = target.replace(/\.(?:[cm]?[jt]sx?)$/u, '');
102
- for (const candidate of [
103
- `${stem}.ts`,
104
- `${stem}.tsx`,
105
- `${stem}.mts`,
106
- `${stem}.cts`,
107
- target,
108
- node_path.join(target, 'index.ts'),
109
- node_path.join(target, 'index.tsx')
110
- ])try {
111
- const real = node_fs.realpathSync(candidate);
112
- if (inside(real, boundary) && node_fs.statSync(real).isFile()) return real;
113
- } catch {}
114
- };
115
- const packageEntry = (specifier)=>{
116
- if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#')) return;
117
- const parts = specifier.split('/');
118
- const count = specifier.startsWith('@') ? 2 : 1;
119
- if (parts.length < count || parts.slice(0, count).some((part)=>!part)) return;
120
- return {
121
- name: parts.slice(0, count).join('/'),
122
- exportKey: parts.length === count ? '.' : `./${parts.slice(count).join('/')}`
123
- };
124
- };
125
- const resolveModulePath = (module, specifier, workspace)=>{
126
- if (/^\.\.?\//u.test(specifier)) {
127
- const file = sourceFileAt(node_path.resolve(node_path.dirname(module.path), specifier), module.boundary);
128
- return void 0 === file ? void 0 : {
129
- path: file,
130
- boundary: module.boundary
131
- };
132
- }
133
- const entry = packageEntry(specifier);
134
- if (!entry || !workspace) return;
135
- const paths = createRequire(module.path).resolve.paths(entry.name) ?? [];
136
- for (const lookup of paths){
137
- const directory = node_path.join(lookup, entry.name);
138
- const manifest = node_path.join(directory, 'package.json');
139
- if (!node_fs.existsSync(manifest)) continue;
140
- const boundary = node_fs.realpathSync(directory);
141
- if (!inside(boundary, workspace) || boundary.split(node_path.sep).includes('node_modules')) break;
142
- const pkg = JSON.parse(node_fs.readFileSync(manifest, 'utf8'));
143
- if (pkg.name !== entry.name || void 0 === pkg.exports) break;
144
- let target;
145
- try {
146
- const targets = external_resolve_exports_exports(pkg, entry.exportKey, {
147
- conditions: [
148
- 'modern:source'
149
- ]
150
- });
151
- target = Array.isArray(targets) ? targets[0] : void 0;
152
- } catch {
153
- break;
154
- }
155
- if (!target || !target.startsWith('./')) break;
156
- const file = sourceFileAt(node_path.resolve(boundary, target), boundary);
157
- return void 0 === file ? void 0 : {
158
- path: file,
159
- boundary
160
- };
161
- }
162
- };
163
- const createModuleReader = (root)=>{
164
- const modules = new Map([
165
- [
166
- root.path,
167
- root
168
- ]
169
- ]);
170
- return (filePath, boundary)=>{
171
- if (modules.has(filePath)) return modules.get(filePath);
172
- if (modules.size >= 256) return;
173
- let module;
174
- try {
175
- module = {
176
- file: parseConsumer(filePath),
177
- path: filePath,
178
- boundary
179
- };
180
- } catch {
181
- module = void 0;
182
- }
183
- modules.set(filePath, module);
184
- return module;
185
- };
186
- };
187
- const importedBinding = (module, name)=>{
188
- for (const statement of module.file.program.body)if (__rspack_external__babel_types_6e8337dd.isImportDeclaration(statement) && 'type' !== statement.importKind) {
189
- for (const specifier of statement.specifiers)if (__rspack_external__babel_types_6e8337dd.isIdentifier(specifier.local, {
190
- name
191
- })) {
192
- if (__rspack_external__babel_types_6e8337dd.isImportSpecifier(specifier) && 'type' !== specifier.importKind) return {
193
- imported: propertyName(specifier.imported) ?? name,
194
- specifier: statement.source.value
195
- };
196
- if (__rspack_external__babel_types_6e8337dd.isImportDefaultSpecifier(specifier)) return {
197
- imported: 'default',
198
- specifier: statement.source.value
199
- };
200
- }
201
- }
202
- };
203
- const reexportedBinding = (module, name)=>{
204
- for (const statement of module.file.program.body){
205
- if (!__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || 'type' === statement.exportKind) continue;
206
- const specifier = statement.specifiers.find((value)=>__rspack_external__babel_types_6e8337dd.isExportSpecifier(value) && 'type' !== value.exportKind && propertyName(value.exported) === name);
207
- if (void 0 !== specifier && __rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier)) return {
208
- local: __rspack_external__babel_types_6e8337dd.isIdentifier(specifier.local) ? specifier.local.name : specifier.local.value,
209
- ...statement.source ? {
210
- specifier: statement.source.value
211
- } : {}
212
- };
213
- }
214
- };
215
- const defaultExport = (module)=>{
216
- for (const statement of module.file.program.body){
217
- if (!__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement)) continue;
218
- const declaration = statement.declaration;
219
- if (__rspack_external__babel_types_6e8337dd.isIdentifier(declaration)) return declaration.name;
220
- return __rspack_external__babel_types_6e8337dd.isExpression(declaration) ? declaration : void 0;
221
- }
222
- };
223
- const resolveBinding = (module, name, scope, readModule, workspace, seen)=>{
224
- const key = `${scope}:${module.path}#${name}`;
225
- if (seen.has(key) || seen.size >= 512) return;
226
- seen.add(key);
227
- const declaration = 'export' === scope ? exportedConst(module.file, name) : localConst(module.file, name);
228
- if (declaration?.init) return {
229
- expression: declaration.init,
230
- module
231
- };
232
- const throughModule = (specifier, imported)=>{
233
- const resolved = resolveModulePath(module, specifier, workspace);
234
- const target = resolved && readModule(resolved.path, resolved.boundary);
235
- return void 0 === target ? void 0 : resolveBinding(target, imported, 'export', readModule, workspace, seen);
236
- };
237
- if ('local' === scope) {
238
- const imported = importedBinding(module, name);
239
- return void 0 === imported ? void 0 : throughModule(imported.specifier, imported.imported);
240
- }
241
- if ('default' === name) {
242
- const exported = defaultExport(module);
243
- if ('string' == typeof exported) return resolveBinding(module, exported, 'local', readModule, workspace, seen);
244
- return void 0 === exported ? void 0 : {
245
- expression: exported,
246
- module
247
- };
248
- }
249
- const reexported = reexportedBinding(module, name);
250
- if (void 0 !== reexported) return void 0 === reexported.specifier ? resolveBinding(module, reexported.local, 'local', readModule, workspace, seen) : throughModule(reexported.specifier, reexported.local);
251
- for (const statement of module.file.program.body){
252
- if (!__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement) || 'type' === statement.exportKind) continue;
253
- const resolved = throughModule(statement.source.value, name);
254
- if (void 0 !== resolved) return resolved;
255
- }
256
- };
257
40
  const objectLiteral = (expression)=>{
258
41
  if (null == expression) return;
259
42
  const unwrapped = unwrapExpression(expression);
@@ -381,9 +164,13 @@ const rootComposesFoundation = (sourceFile, declaration, stem, foundationName)=>
381
164
  return void 0 !== chain && isAccessPath(chain.base.callee, [
382
165
  'HttpApi',
383
166
  'make'
384
- ]) && 1 === chain.base.arguments.length && stringLiteral(chain.base.arguments[0]) === `${pascalCaseStem(stem)}Api` && chain.methods.length > 0 && chain.methods.every((method, index)=>1 === method.arguments.length && ('add' === method.name || 'addHttpApi' === method.name || index === chain.methods.length - 1 && 'pipe' === method.name && 'identity' === identifierName(method.arguments[0]) && importsExactBindings(sourceFile, 'effect', [
167
+ ]) && 1 === chain.base.arguments.length && stringLiteral(chain.base.arguments[0]) === `${pascalCaseStem(stem)}Api` && chain.methods.length > 0 && chain.methods.every((method, index)=>{
168
+ const combinatorArity = nonEndpointCombinators.api[method.name];
169
+ if (void 0 !== combinatorArity) return method.arguments.length === combinatorArity;
170
+ return 1 === method.arguments.length && ('add' === method.name || 'addHttpApi' === method.name || index === chain.methods.length - 1 && 'pipe' === method.name && 'identity' === identifierName(method.arguments[0]) && importsExactBindings(sourceFile, 'effect', [
385
171
  'identity'
386
- ]))) && first?.name === 'addHttpApi' && identifierName(first.arguments[0]) === foundationName;
172
+ ]));
173
+ }) && first?.name === 'addHttpApi' && identifierName(first.arguments[0]) === foundationName;
387
174
  };
388
175
  const nonEndpointCombinators = {
389
176
  api: {
@@ -399,9 +186,7 @@ const nonEndpointCombinators = {
399
186
  middleware: 1
400
187
  }
401
188
  };
402
- const reachableEndpoints = (rootModule, declaration)=>{
403
- const readModule = createModuleReader(rootModule);
404
- const workspace = containingWorkspace(rootModule.path);
189
+ const reachableEndpoints = (graph, declaration)=>{
405
190
  const active = new Set();
406
191
  const endpoints = [];
407
192
  const identities = new Set();
@@ -423,8 +208,8 @@ const reachableEndpoints = (rootModule, declaration)=>{
423
208
  active.add(node);
424
209
  try {
425
210
  if (__rspack_external__babel_types_6e8337dd.isIdentifier(node)) {
426
- const resolved = resolveBinding(module, node.name, 'local', readModule, workspace, new Set());
427
- return void 0 !== resolved && visit(resolved.expression, resolved.module, kind, group);
211
+ const resolved = graph.resolve(module, node.name, 'local');
212
+ return resolved?.kind === 'declaration' && visit(resolved.expression, resolved.module, kind, group);
428
213
  }
429
214
  if ('endpoint' === kind) {
430
215
  if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node) || node.arguments.length < 2 || node.arguments.length > 3) return false;
@@ -479,7 +264,7 @@ const reachableEndpoints = (rootModule, declaration)=>{
479
264
  active.delete(node);
480
265
  }
481
266
  };
482
- return visit(declaration?.init, rootModule, 'api') ? endpoints : void 0;
267
+ return visit(declaration?.init, graph.root, 'api') ? endpoints : void 0;
483
268
  };
484
269
  const operationContextFields = (property)=>{
485
270
  const constructorCall = exactCall(property.value, [
@@ -610,8 +395,8 @@ const readinessSchemaIsShared = (declaration, markerSchemaName, ownerMarkerIsIde
610
395
  return void 0 !== schema && (schema.identity && ownerMarkerIsIdentity || 1 === schema.assignments.size && identifierName(schema.assignments.get('marker')?.value) === markerSchemaName);
611
396
  };
612
397
  const declarationIsIdentifier = (declaration, identifier)=>declaration?.init != null && identifierName(unwrapExpression(declaration.init)) === identifier;
613
- const validateParsedContract = (module, stem, expectation)=>{
614
- const sourceFile = module.file;
398
+ const validateParsedContract = (graph, stem, expectation)=>{
399
+ const sourceFile = graph.root.file;
615
400
  const exportStem = camelCaseStem(stem);
616
401
  const foundationName = `${exportStem}FoundationApi`;
617
402
  const markerSchemaName = `${exportStem}MarkerSchema`;
@@ -628,7 +413,7 @@ const validateParsedContract = (module, stem, expectation)=>{
628
413
  if (!readinessSchemaIsShared(exportedConst(sourceFile, readinessSchemaName), markerSchemaName, declarationIsIdentifier(markerDeclaration, 'MicroVerticalBuildMarkerSchema'))) return 'MicroVertical readiness schema must consume the shared readiness schema without overriding shared fields';
629
414
  if (!foundationIsExact(exportedConst(sourceFile, foundationName), stem, readinessSchemaName)) return 'MicroVertical readiness foundation API must directly compose its exact readiness endpoint and foundation identity';
630
415
  if (!rootComposesFoundation(sourceFile, exportedConst(sourceFile, `${exportStem}Api`), stem, foundationName)) return 'MicroVertical root API must explicitly compose its readiness foundation API';
631
- const endpoints = reachableEndpoints(module, exportedConst(sourceFile, `${exportStem}Api`));
416
+ const endpoints = reachableEndpoints(graph, exportedConst(sourceFile, `${exportStem}Api`));
632
417
  if (!endpoints) return 'MicroVertical root API must compose bounded native endpoint declarations without ambiguous identities';
633
418
  if (!operationMapIsConnected(exportedConst(sourceFile, `${exportStem}OperationContexts`), stem, expectation.operationPaths ?? {}, endpoints)) return 'MicroVertical operation map must construct every operation with the shared context constructor';
634
419
  if (!metadataIsExact(exportedConst(sourceFile, `${exportStem}ApiContract`), expectation, endpoints)) return 'MicroVertical root contract must keep exact owner and API path metadata without forbidden fields';
@@ -640,14 +425,9 @@ const configuredMicroVerticalApiStem = (verticalPath, verticals)=>{
640
425
  };
641
426
  const microVerticalApiBaselineViolation = (stem, filePath, expectation)=>{
642
427
  try {
643
- const realPath = node_fs.realpathSync(filePath);
644
- const sourceFile = parseConsumer(realPath);
428
+ const graph = createModuleGraph(filePath);
645
429
  if (!baselinePublicIdentityIsExact(filePath, expectation)) return 'MicroVertical baseline imports must resolve the exact framework owner public export and schema identity';
646
- return validateParsedContract({
647
- file: sourceFile,
648
- path: realPath,
649
- boundary: sourceBoundary(realPath, containingWorkspace(realPath))
650
- }, stem, expectation);
430
+ return validateParsedContract(graph, stem, expectation);
651
431
  } catch (error) {
652
432
  if (error instanceof SourceSyntaxError) return `MicroVertical root contract must be valid TypeScript syntax (${error.message})`;
653
433
  throw error;
@@ -4,7 +4,8 @@ import node_path from "node:path";
4
4
  import { parse } from "@babel/parser";
5
5
  import { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation } from "./microvertical-api-baseline.js";
6
6
  import { baselinePackage, resolveBaselinePackageDirectory } from "./microvertical-api-owner.js";
7
- import { consumerParserPlugins } from "./source-analysis.js";
7
+ import { createModuleGraph } from "./module-graph.js";
8
+ import { SourceSyntaxError, consumerParserPlugins } from "./source-analysis.js";
8
9
  import { createEffectApiImportResolver, strictEffectRuntimeTopologyViolation } from "./strict-effect-runtime.js";
9
10
  import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
10
11
  const ignored = new Set([
@@ -408,6 +409,27 @@ function check(options, topology) {
408
409
  const violation = microVerticalApiBaselineViolation(stem, absolute(contract), expectation);
409
410
  if (violation) diagnostics.push(`${contract}: ${violation}`);
410
411
  }
412
+ if (options.contractRules?.length && exists(contract)) {
413
+ let graph;
414
+ try {
415
+ graph = createModuleGraph(absolute(contract));
416
+ } catch (error) {
417
+ if (!(error instanceof SourceSyntaxError)) throw error;
418
+ diagnostics.push(`${contract}: contract rules need a parseable contract with immutable bindings (${error.message})`);
419
+ }
420
+ if (graph) {
421
+ const context = {
422
+ appPath,
423
+ protocol: rpc ? 'rpc' : 'rest',
424
+ graph
425
+ };
426
+ for (const rule of options.contractRules)guarded(`${contract} (rule ${rule.name || 'anonymous'})`, ()=>{
427
+ const messages = rule(context);
428
+ 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)');
429
+ for (const message of messages)diagnostics.push(`${contract}: ${message}`);
430
+ });
431
+ }
432
+ }
411
433
  if (client) {
412
434
  const relativeContract = node_path.posix.relative(node_path.posix.dirname(client), contract);
413
435
  moduleShape(client, [
@@ -501,4 +523,4 @@ function check(options, topology) {
501
523
  }
502
524
  const checkMicroVerticalApiBoundaries = (options)=>check(options, true);
503
525
  const checkMicroVerticalApiConsumerFiles = (options)=>check(options, false);
504
- export { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles, configuredMicroVerticalApiStem, microVerticalApiBaselineViolation };
526
+ export { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles, configuredMicroVerticalApiStem, createModuleGraph, microVerticalApiBaselineViolation };