@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.
- package/bin/modern-api-check-files.mjs +4 -1
- package/bin/modern-api-check.mjs +1 -1
- package/bin/modern-i18n-check.mjs +4 -0
- package/dist/cjs/cli/microvertical-api-check.cjs +36 -7
- package/dist/cjs/cli/workspace-source-check.cjs +90 -17
- package/dist/cjs/microvertical-api-baseline.cjs +24 -255
- package/dist/cjs/microvertical-api-boundary.cjs +25 -0
- package/dist/cjs/module-graph.cjs +321 -0
- package/dist/esm/cli/microvertical-api-check.js +26 -7
- package/dist/esm/cli/workspace-source-check.js +86 -13
- package/dist/esm/microvertical-api-baseline.js +17 -237
- package/dist/esm/microvertical-api-boundary.js +24 -2
- package/dist/esm/module-graph.js +260 -0
- package/dist/esm-node/cli/microvertical-api-check.js +26 -7
- package/dist/esm-node/cli/workspace-source-check.js +86 -13
- package/dist/esm-node/microvertical-api-baseline.js +17 -237
- package/dist/esm-node/microvertical-api-boundary.js +24 -2
- package/dist/esm-node/module-graph.js +261 -0
- package/dist/types/cli/microvertical-api-check.d.ts +1 -1
- package/dist/types/cli/workspace-source-check.d.ts +1 -1
- package/dist/types/microvertical-api-boundary.d.ts +14 -0
- package/dist/types/module-graph.d.ts +52 -0
- package/package.json +4 -3
|
@@ -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
|
-
|
|
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>]
|
|
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
|
|
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
|
|
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
|
|
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 :
|
|
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
|
|
144
|
-
|
|
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 {
|
|
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 {
|
|
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)=>
|
|
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
|
-
]))
|
|
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 = (
|
|
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 =
|
|
427
|
-
return
|
|
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,
|
|
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 = (
|
|
614
|
-
const sourceFile =
|
|
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(
|
|
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
|
|
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 {
|
|
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 };
|