@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.
- 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
|
@@ -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
|
-
|
|
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>]
|
|
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
|
|
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
|
|
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
|
|
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 :
|
|
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
|
|
143
|
-
|
|
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 {
|
|
218
|
+
export { runWorkspaceSourceCheck, runWorkspaceSourceCheckCli };
|