@bleedingdev/modern-js-code-tools 3.9.0-ultramodern.9 → 3.9.3-ultramodern.2
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 +81 -244
- package/dist/cjs/microvertical-api-boundary.cjs +352 -64
- package/dist/cjs/microvertical-api-owner.cjs +5 -2
- package/dist/cjs/module-graph.cjs +1862 -0
- package/dist/cjs/oxlint-plugin/rules/strict-effect-api-boundaries.cjs +1 -1
- package/dist/cjs/source-analysis.cjs +108 -0
- package/dist/cjs/strict-effect-runtime.cjs +75 -57
- 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 +66 -214
- package/dist/esm/microvertical-api-boundary.js +350 -65
- package/dist/esm/microvertical-api-owner.js +5 -2
- package/dist/esm/module-graph.js +1804 -0
- package/dist/esm/oxlint-plugin/rules/strict-effect-api-boundaries.js +1 -1
- package/dist/esm/source-analysis.js +48 -0
- package/dist/esm/strict-effect-runtime.js +75 -56
- 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 +66 -214
- package/dist/esm-node/microvertical-api-boundary.js +350 -65
- package/dist/esm-node/microvertical-api-owner.js +5 -2
- package/dist/esm-node/module-graph.js +1805 -0
- package/dist/esm-node/oxlint-plugin/rules/strict-effect-api-boundaries.js +1 -1
- package/dist/esm-node/source-analysis.js +49 -0
- package/dist/esm-node/strict-effect-runtime.js +75 -56
- 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-baseline.d.ts +0 -5
- package/dist/types/microvertical-api-boundary.d.ts +17 -2
- package/dist/types/microvertical-api-owner.d.ts +1 -1
- package/dist/types/module-graph.d.ts +100 -0
- package/dist/types/source-analysis.d.ts +13 -0
- package/package.json +17 -13
|
@@ -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,8 @@ 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");
|
|
55
|
+
const external_source_analysis_cjs_namespaceObject = require("./source-analysis.cjs");
|
|
53
56
|
const external_strict_effect_runtime_cjs_namespaceObject = require("./strict-effect-runtime.cjs");
|
|
54
57
|
const ignored = new Set([
|
|
55
58
|
'.git',
|
|
@@ -61,17 +64,48 @@ const ignored = new Set([
|
|
|
61
64
|
'node_modules',
|
|
62
65
|
'repos'
|
|
63
66
|
]);
|
|
67
|
+
const MAX_CLOSURE_DEPTH = 128;
|
|
68
|
+
const MAX_CLOSURE_MODULES = 2048;
|
|
64
69
|
const normalize = (file)=>file.split(external_node_path_default().sep).join('/').replace(/^\.\//u, '');
|
|
65
70
|
const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
66
71
|
const apiApp = (app)=>'vertical' === app.kind && 'ui-only' !== app.surfaceProfile && 'horizontal-remote' !== app.deliveryUnitKind;
|
|
67
72
|
function configuredApps(root, supplied) {
|
|
68
|
-
const
|
|
69
|
-
if (!Array.isArray(
|
|
73
|
+
const topology = supplied ? void 0 : JSON.parse(external_node_fs_default().readFileSync(external_node_path_default().join(root, 'topology/reference-topology.json'), 'utf8'));
|
|
74
|
+
if (topology && (!topology.shell || !Array.isArray(topology.verticals) || void 0 !== topology.shells && !Array.isArray(topology.shells))) throw new Error('topology/reference-topology.json: shell and verticals must be present');
|
|
75
|
+
const entries = topology?.shell ? [
|
|
76
|
+
topology.shell,
|
|
77
|
+
...topology.shells ?? [],
|
|
78
|
+
...topology.verticals ?? []
|
|
79
|
+
] : void 0;
|
|
80
|
+
const source = supplied ?? entries?.map((entry)=>{
|
|
81
|
+
if (!entry || 'string' != typeof entry.path || !entry.path || external_node_path_default().isAbsolute(entry.path) || entry.path.split(/[\\/]/u).includes('..')) throw new Error('topology/reference-topology.json: each app must have an explicit workspace path');
|
|
82
|
+
const packageFile = external_node_path_default().join(root, entry.path, 'package.json');
|
|
83
|
+
const manifest = JSON.parse(external_node_fs_default().readFileSync(packageFile, 'utf8'));
|
|
84
|
+
if ('string' != typeof manifest.name || !manifest.name || void 0 !== entry.package && entry.package !== manifest.name) throw new Error(`${entry.path}/package.json: package name must match topology`);
|
|
85
|
+
const api = entry.api;
|
|
86
|
+
const stem = api?.stem ?? api?.readiness?.endpoint?.match(/^\/([a-z0-9]+(?:-[a-z0-9]+)*)\/readiness$/u)?.[1];
|
|
87
|
+
return {
|
|
88
|
+
id: entry.id,
|
|
89
|
+
kind: entry.kind,
|
|
90
|
+
path: entry.path,
|
|
91
|
+
package: manifest.name,
|
|
92
|
+
surfaceProfile: entry.surfaceProfile,
|
|
93
|
+
deliveryUnitKind: entry.deliveryUnitKind,
|
|
94
|
+
...void 0 === api ? {} : {
|
|
95
|
+
api: {
|
|
96
|
+
stem,
|
|
97
|
+
prefix: api.bff?.prefix,
|
|
98
|
+
protocol: api.protocol
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
if (!Array.isArray(source)) throw new Error('topology/reference-topology.json: shell and verticals must describe apps');
|
|
70
104
|
const paths = new Set();
|
|
71
105
|
for (const app of source){
|
|
72
|
-
if (!app || 'object' != typeof app || 'string' != typeof app.path || !app.path || external_node_path_default().isAbsolute(app.path) || app.path.split(/[\\/]/u).includes('..')) throw new Error('
|
|
106
|
+
if (!app || 'object' != typeof app || 'string' != typeof app.path || !app.path || external_node_path_default().isAbsolute(app.path) || app.path.split(/[\\/]/u).includes('..')) throw new Error('topology/reference-topology.json: each app must have a relative workspace path');
|
|
73
107
|
const key = normalize(app.path);
|
|
74
|
-
if (paths.has(key)) throw new Error(
|
|
108
|
+
if (paths.has(key)) throw new Error(`topology/reference-topology.json: duplicate app path ${key}`);
|
|
75
109
|
paths.add(key);
|
|
76
110
|
for (const name of [
|
|
77
111
|
'id',
|
|
@@ -80,7 +114,7 @@ function configuredApps(root, supplied) {
|
|
|
80
114
|
'surfaceProfile',
|
|
81
115
|
'deliveryUnitKind'
|
|
82
116
|
])if (void 0 !== app[name] && 'string' != typeof app[name]) throw new Error(`${key}: ${name} must be a string`);
|
|
83
|
-
|
|
117
|
+
const allowedFields = {
|
|
84
118
|
kind: [
|
|
85
119
|
'shell',
|
|
86
120
|
'vertical'
|
|
@@ -94,7 +128,11 @@ function configuredApps(root, supplied) {
|
|
|
94
128
|
'microvertical',
|
|
95
129
|
'horizontal-remote'
|
|
96
130
|
]
|
|
97
|
-
}
|
|
131
|
+
};
|
|
132
|
+
for (const field of Object.keys(allowedFields)){
|
|
133
|
+
const value = app[field];
|
|
134
|
+
if ((void 0 !== value || 'kind' === field) && ('string' != typeof value || !allowedFields[field].includes(value))) throw new Error(`${key}: invalid ${field}`);
|
|
135
|
+
}
|
|
98
136
|
if (void 0 !== app.api) {
|
|
99
137
|
if (!app.api || 'object' != typeof app.api || Array.isArray(app.api)) throw new Error(`${key}: api must be an object`);
|
|
100
138
|
if (void 0 !== app.api.protocol && ![
|
|
@@ -135,6 +173,9 @@ function check(options, topology) {
|
|
|
135
173
|
};
|
|
136
174
|
try {
|
|
137
175
|
const apps = configuredApps(root, options.configuredApps);
|
|
176
|
+
const workspace = external_node_fs_default().realpathSync(root);
|
|
177
|
+
const realRelative = (file)=>normalize(external_node_path_default().relative(workspace, external_node_fs_default().realpathSync(absolute(file))));
|
|
178
|
+
const inside = (directory, real)=>real.startsWith(`${directory}${external_node_path_default().sep}`);
|
|
138
179
|
const byPath = new Map(apps.map((app)=>[
|
|
139
180
|
normalize(app.path),
|
|
140
181
|
app
|
|
@@ -159,7 +200,7 @@ function check(options, topology) {
|
|
|
159
200
|
const ast = (0, parser_namespaceObject.parse)(read(file), {
|
|
160
201
|
sourceType: 'module',
|
|
161
202
|
sourceFilename: file,
|
|
162
|
-
plugins: (0,
|
|
203
|
+
plugins: (0, external_source_analysis_cjs_namespaceObject.consumerParserPlugins)(file)
|
|
163
204
|
});
|
|
164
205
|
parsed.set(file, ast);
|
|
165
206
|
return ast;
|
|
@@ -175,39 +216,100 @@ function check(options, topology) {
|
|
|
175
216
|
const resolveRelative = (fromFile, specifier)=>{
|
|
176
217
|
if (!/^\.\.?\//u.test(specifier)) return;
|
|
177
218
|
const base = external_node_path_default().posix.join(external_node_path_default().posix.dirname(fromFile), specifier).replace(/\.(?:[cm]?[jt]sx?)$/u, '');
|
|
178
|
-
|
|
219
|
+
const candidate = [
|
|
179
220
|
`${base}.ts`,
|
|
180
221
|
`${base}.tsx`,
|
|
181
222
|
`${base}/index.ts`,
|
|
182
223
|
`${base}/index.tsx`
|
|
183
|
-
].find((
|
|
224
|
+
].find((file)=>exists(file));
|
|
225
|
+
return void 0 === candidate ? void 0 : realRelative(candidate);
|
|
226
|
+
};
|
|
227
|
+
const graphBinding = (graph, module, expression, seen = new Set())=>{
|
|
228
|
+
if (seen.has(expression) || seen.size >= 128) return;
|
|
229
|
+
seen.add(expression);
|
|
230
|
+
const binding = types_namespaceObject.isIdentifier(expression) || types_namespaceObject.isMemberExpression(expression) ? graph.resolveReference(module, expression) : void 0;
|
|
231
|
+
return binding?.kind === 'declaration' && (types_namespaceObject.isIdentifier(binding.expression) || types_namespaceObject.isMemberExpression(binding.expression)) ? graphBinding(graph, binding.module, binding.expression, seen) : binding;
|
|
232
|
+
};
|
|
233
|
+
const clientExportFile = (appPath, exported)=>{
|
|
234
|
+
if ('string' != typeof exported || !exported.startsWith('./') || exported.split(/[\\/]/u).includes('..') || !/\.[cm]?[jt]sx?$/u.test(exported)) return;
|
|
235
|
+
const appDirectory = absolute(appPath);
|
|
236
|
+
const candidate = external_node_path_default().resolve(appDirectory, exported);
|
|
237
|
+
try {
|
|
238
|
+
const workspace = external_node_fs_default().realpathSync(root);
|
|
239
|
+
const owner = external_node_fs_default().realpathSync(appDirectory);
|
|
240
|
+
const real = external_node_fs_default().realpathSync(candidate);
|
|
241
|
+
if (!owner.startsWith(`${workspace}${external_node_path_default().sep}`) || !real.startsWith(`${owner}${external_node_path_default().sep}`) || !external_node_fs_default().statSync(real).isFile()) return;
|
|
242
|
+
} catch {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
return normalize(external_node_path_default().relative(root, candidate));
|
|
184
246
|
};
|
|
185
|
-
const composedModules = (file)=>{
|
|
186
|
-
const
|
|
187
|
-
|
|
247
|
+
const composedModules = (file, appPath)=>{
|
|
248
|
+
const owner = external_node_fs_default().realpathSync(absolute(appPath));
|
|
249
|
+
if (!inside(owner, external_node_fs_default().realpathSync(absolute(file)))) throw new Error(`consumer module closure escapes its owner: ${file}`);
|
|
250
|
+
const entry = realRelative(file);
|
|
251
|
+
const discovered = new Set([
|
|
252
|
+
entry
|
|
253
|
+
]);
|
|
188
254
|
const queue = [
|
|
189
|
-
|
|
255
|
+
{
|
|
256
|
+
file: entry,
|
|
257
|
+
depth: 0
|
|
258
|
+
}
|
|
190
259
|
];
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (seen.has(current)) continue;
|
|
194
|
-
seen.add(current);
|
|
195
|
-
order.push(current);
|
|
260
|
+
let graph;
|
|
261
|
+
for (const { file: current, depth } of queue){
|
|
196
262
|
const ast = parseFile(current);
|
|
197
263
|
if (!ast) continue;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
264
|
+
const sources = [];
|
|
265
|
+
let callsRequire = false;
|
|
266
|
+
for (const statement of ast.program.body)if (types_namespaceObject.isImportDeclaration(statement) && 'type' !== statement.importKind && 'typeof' !== statement.importKind || (types_namespaceObject.isExportNamedDeclaration(statement) || types_namespaceObject.isExportAllDeclaration(statement)) && 'type' !== statement.exportKind && statement.source) sources.push(statement.source.value);
|
|
267
|
+
types_namespaceObject.traverseFast(ast, (node)=>{
|
|
268
|
+
if (types_namespaceObject.isImportExpression(node)) {
|
|
269
|
+
if (!types_namespaceObject.isStringLiteral(node.source)) throw new Error(`consumer module closure has nonliteral runtime dependency: ${current}`);
|
|
270
|
+
sources.push(node.source.value);
|
|
271
|
+
}
|
|
272
|
+
if ((types_namespaceObject.isCallExpression(node) || types_namespaceObject.isOptionalCallExpression(node)) && types_namespaceObject.isIdentifier((0, external_source_analysis_cjs_namespaceObject.unwrapExpression)(node.callee, true), {
|
|
273
|
+
name: 'require'
|
|
274
|
+
})) callsRequire = true;
|
|
275
|
+
});
|
|
276
|
+
if (callsRequire) {
|
|
277
|
+
graph ??= (0, external_module_graph_cjs_namespaceObject.createModuleGraph)();
|
|
278
|
+
const module = graph.module(absolute(current));
|
|
279
|
+
types_namespaceObject.traverseFast(module.file, (node)=>{
|
|
280
|
+
if (!types_namespaceObject.isCallExpression(node) && !types_namespaceObject.isOptionalCallExpression(node)) return;
|
|
281
|
+
const callee = (0, external_source_analysis_cjs_namespaceObject.unwrapExpression)(node.callee, true);
|
|
282
|
+
if (!types_namespaceObject.isIdentifier(callee, {
|
|
283
|
+
name: 'require'
|
|
284
|
+
}) || !graph.isUnboundReference(module, callee)) return;
|
|
285
|
+
const [argument] = node.arguments;
|
|
286
|
+
if (1 !== node.arguments.length || !types_namespaceObject.isStringLiteral(argument)) throw new Error(`consumer module closure has nonliteral runtime dependency: ${current}`);
|
|
287
|
+
sources.push(argument.value);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
for (const source of sources){
|
|
291
|
+
if (!/^\.\.?\//u.test(source)) continue;
|
|
292
|
+
const resolved = resolveRelative(current, source);
|
|
293
|
+
if (void 0 === resolved) throw new Error(`consumer module closure has unresolved relative dependency: ${current} -> ${source}`);
|
|
294
|
+
if (!inside(owner, external_node_path_default().join(workspace, resolved))) throw new Error(`consumer module closure escapes its owner: ${current} -> ${source}`);
|
|
295
|
+
if (!discovered.has(resolved)) {
|
|
296
|
+
if (depth + 1 >= MAX_CLOSURE_DEPTH) throw new Error(`consumer module closure exceeds ${MAX_CLOSURE_DEPTH} import-depth analysis budget`);
|
|
297
|
+
if (discovered.size >= MAX_CLOSURE_MODULES) throw new Error(`consumer module closure exceeds ${MAX_CLOSURE_MODULES}-module analysis budget`);
|
|
298
|
+
discovered.add(resolved);
|
|
299
|
+
queue.push({
|
|
300
|
+
file: resolved,
|
|
301
|
+
depth: depth + 1
|
|
302
|
+
});
|
|
303
|
+
}
|
|
202
304
|
}
|
|
203
305
|
}
|
|
204
|
-
return
|
|
306
|
+
return queue.map((item)=>item.file);
|
|
205
307
|
};
|
|
206
|
-
const moduleShape = (file, imports, calls, patterns = [], defaultExport,
|
|
308
|
+
const moduleShape = (file, imports, calls, patterns = [], defaultExport, composedAppPath)=>{
|
|
207
309
|
if (!exists(file)) return;
|
|
208
310
|
const ast = parseFile(file);
|
|
209
311
|
if (!ast) return;
|
|
210
|
-
const shapeFiles =
|
|
312
|
+
const shapeFiles = composedAppPath ? composedModules(file, composedAppPath) : [
|
|
211
313
|
file
|
|
212
314
|
];
|
|
213
315
|
const body = shapeFiles.flatMap((shapeFile)=>parseFile(shapeFile)?.program.body ?? []);
|
|
@@ -240,6 +342,10 @@ function check(options, topology) {
|
|
|
240
342
|
})), `${file}: must default-export ${defaultExport}`);
|
|
241
343
|
};
|
|
242
344
|
const effect = '@modern-js/bff-effect/effect-client';
|
|
345
|
+
const clientFactories = new Set([
|
|
346
|
+
'makeEffectHttpApiClient',
|
|
347
|
+
'makeEffectRpcClient'
|
|
348
|
+
]);
|
|
243
349
|
const appDirectories = [
|
|
244
350
|
...new Set([
|
|
245
351
|
...directories('apps'),
|
|
@@ -300,7 +406,6 @@ function check(options, topology) {
|
|
|
300
406
|
if (violation) diagnostics.push(`${file}: ${violation}`);
|
|
301
407
|
}
|
|
302
408
|
});
|
|
303
|
-
let owner;
|
|
304
409
|
for (const appPath of appDirectories)guarded(appPath, ()=>{
|
|
305
410
|
const app = byPath.get(appPath);
|
|
306
411
|
const emits = app ? apiApp(app) || 'vertical' !== app.kind && void 0 !== app.api : appPath.startsWith('verticals/') || exists(`${appPath}/api/index.ts`) || exists(`${appPath}/shared/api.ts`);
|
|
@@ -317,19 +422,25 @@ function check(options, topology) {
|
|
|
317
422
|
}
|
|
318
423
|
return;
|
|
319
424
|
}
|
|
320
|
-
if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in
|
|
425
|
+
if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in topology/reference-topology.json`);
|
|
321
426
|
const stem = app?.api?.stem ?? external_node_path_default().posix.basename(appPath);
|
|
322
427
|
const rpc = app?.api?.protocol === 'rpc';
|
|
323
428
|
const contract = `${appPath}/shared/${rpc ? 'rpc' : 'api'}.ts`;
|
|
324
|
-
const client = `${appPath}/src/api/${stem}-${rpc ? 'rpc-client' : 'client'}.ts`;
|
|
325
429
|
const entry = `${appPath}/api/index.ts`;
|
|
326
430
|
for (const file of [
|
|
327
431
|
entry,
|
|
328
|
-
contract
|
|
329
|
-
client
|
|
432
|
+
contract
|
|
330
433
|
])assert(exists(file), `${file}: required API surface is missing`);
|
|
434
|
+
const packageFile = `${appPath}/package.json`;
|
|
435
|
+
assert(exists(packageFile), `${packageFile}: required API package is missing`);
|
|
436
|
+
if (!exists(packageFile)) return;
|
|
437
|
+
const manifest = JSON.parse(read(packageFile));
|
|
438
|
+
assert(manifest.exports?.['./api'] === `./shared/${rpc ? 'rpc' : 'api'}.ts`, `${packageFile}: invalid ./api export`);
|
|
439
|
+
const clientKey = `./api/${rpc ? 'rpc-client' : 'client'}`;
|
|
440
|
+
const client = clientExportFile(appPath, manifest.exports?.[clientKey]);
|
|
441
|
+
assert(void 0 !== client, `${packageFile}: invalid API client export`);
|
|
442
|
+
assert(manifest.exports?.[`./api/${rpc ? 'client' : 'rpc-client'}`] === void 0, `${packageFile}: forbidden opposite protocol client export`);
|
|
331
443
|
noPath(`${appPath}/shared/${rpc ? 'api' : 'rpc'}.ts`, `must not emit a ${rpc ? 'REST' : 'RPC'} contract`);
|
|
332
|
-
noPath(`${appPath}/src/api/${stem}-${rpc ? 'client' : 'rpc-client'}.ts`, `must not emit a ${rpc ? 'REST' : 'RPC'} client`);
|
|
333
444
|
if (rpc) {
|
|
334
445
|
moduleShape(entry, [
|
|
335
446
|
[
|
|
@@ -360,7 +471,7 @@ function check(options, topology) {
|
|
|
360
471
|
], 'apiRuntime');
|
|
361
472
|
moduleShape(contract, [
|
|
362
473
|
[
|
|
363
|
-
'effect/
|
|
474
|
+
'effect/rpc',
|
|
364
475
|
[
|
|
365
476
|
'Rpc',
|
|
366
477
|
'RpcGroup'
|
|
@@ -378,7 +489,7 @@ function check(options, topology) {
|
|
|
378
489
|
'Schema.Struct'
|
|
379
490
|
]);
|
|
380
491
|
} else if (exists(contract)) {
|
|
381
|
-
owner
|
|
492
|
+
const owner = options.baselinePackageDirectory ?? (0, external_microvertical_api_owner_cjs_namespaceObject.resolveBaselinePackageDirectory)(absolute(contract));
|
|
382
493
|
const apiPrefix = app?.api?.prefix ?? `/${stem}-api`;
|
|
383
494
|
const basePath = `${apiPrefix}/${stem}`;
|
|
384
495
|
const expectation = {
|
|
@@ -395,21 +506,24 @@ function check(options, topology) {
|
|
|
395
506
|
const violation = (0, external_microvertical_api_baseline_cjs_namespaceObject.microVerticalApiBaselineViolation)(stem, absolute(contract), expectation);
|
|
396
507
|
if (violation) diagnostics.push(`${contract}: ${violation}`);
|
|
397
508
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
509
|
+
if (client) {
|
|
510
|
+
const relativeContract = external_node_path_default().posix.relative(external_node_path_default().posix.dirname(client), contract);
|
|
511
|
+
moduleShape(client, [
|
|
512
|
+
[
|
|
513
|
+
effect,
|
|
514
|
+
[
|
|
515
|
+
'Effect',
|
|
516
|
+
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
517
|
+
]
|
|
518
|
+
],
|
|
401
519
|
[
|
|
402
|
-
'
|
|
403
|
-
|
|
520
|
+
relativeContract.startsWith('.') ? relativeContract : `./${relativeContract}`,
|
|
521
|
+
[]
|
|
404
522
|
]
|
|
405
|
-
],
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
]
|
|
410
|
-
], [
|
|
411
|
-
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
412
|
-
], [], void 0, true);
|
|
523
|
+
], [
|
|
524
|
+
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
525
|
+
], [], void 0, appPath);
|
|
526
|
+
}
|
|
413
527
|
moduleShape(`${appPath}/api/effect-api.ts`, [], [], [
|
|
414
528
|
[
|
|
415
529
|
/\bbackendFederationContract\b/u,
|
|
@@ -450,28 +564,200 @@ function check(options, topology) {
|
|
|
450
564
|
'must enable strictEffectApproach'
|
|
451
565
|
]
|
|
452
566
|
]);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
567
|
+
});
|
|
568
|
+
const sourceRules = options.sourceRules ?? [];
|
|
569
|
+
for (const file of sourceRules.length ? sourceFiles : [])if (/\.[cm]?[jt]sx?$/u.test(file)) guarded(file, ()=>{
|
|
570
|
+
const graph = (0, external_module_graph_cjs_namespaceObject.createModuleGraph)();
|
|
571
|
+
let module;
|
|
572
|
+
try {
|
|
573
|
+
module = graph.module(absolute(file));
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (!(error instanceof external_source_analysis_cjs_namespaceObject.SourceSyntaxError)) throw error;
|
|
576
|
+
diagnostics.push(`${file}: source rules need parseable source (${error.message})`);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
for (const rule of sourceRules){
|
|
580
|
+
const messages = rule({
|
|
581
|
+
file,
|
|
582
|
+
module,
|
|
583
|
+
graph
|
|
584
|
+
});
|
|
585
|
+
if (!Array.isArray(messages) || !messages.every((message)=>'string' == typeof message)) throw new Error(`rule ${rule.name || 'anonymous'} must return an array of violation messages (return [] when the file passes)`);
|
|
586
|
+
for (const message of messages)diagnostics.push(`${file}: ${message}`);
|
|
459
587
|
}
|
|
460
588
|
});
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
assert(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
589
|
+
const checkShellExports = (shellPath)=>{
|
|
590
|
+
const packageFile = `${shellPath}/package.json`;
|
|
591
|
+
const manifest = JSON.parse(read(packageFile));
|
|
592
|
+
noPath(`${shellPath}/src/api/vertical-clients.ts`, 'Shell client registries are forbidden; import each owner client directly');
|
|
593
|
+
assert(!Object.hasOwn(manifest.exports ?? {}, './api/clients'), `${packageFile}: Shell ./api/clients exports are forbidden`);
|
|
594
|
+
const owner = external_node_fs_default().realpathSync(absolute(shellPath));
|
|
595
|
+
const invalidExport = ()=>assert(false, `${packageFile}: invalid Shell public source export`);
|
|
596
|
+
const targets = (exported)=>'string' == typeof exported ? [
|
|
597
|
+
exported
|
|
598
|
+
] : exported && 'object' == typeof exported ? Object.values(exported).flatMap(targets) : [];
|
|
599
|
+
const filesUnder = (directory, seen = new Set())=>{
|
|
600
|
+
const real = external_node_fs_default().realpathSync(absolute(directory));
|
|
601
|
+
if (real !== owner && !inside(owner, real)) {
|
|
602
|
+
invalidExport();
|
|
603
|
+
return [];
|
|
471
604
|
}
|
|
472
|
-
|
|
605
|
+
if (seen.has(real)) return [];
|
|
606
|
+
seen.add(real);
|
|
607
|
+
return external_node_fs_default().readdirSync(absolute(directory), {
|
|
608
|
+
withFileTypes: true
|
|
609
|
+
}).flatMap((entry)=>{
|
|
610
|
+
if (ignored.has(entry.name)) return [];
|
|
611
|
+
const file = `${directory}/${entry.name}`;
|
|
612
|
+
const stat = external_node_fs_default().statSync(absolute(file));
|
|
613
|
+
return stat.isDirectory() ? filesUnder(file, seen) : stat.isFile() ? [
|
|
614
|
+
file
|
|
615
|
+
] : [];
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
const exportedFiles = (target)=>{
|
|
619
|
+
if (!target.includes('*')) return [
|
|
620
|
+
`${shellPath}/${target.slice(2)}`
|
|
621
|
+
];
|
|
622
|
+
const prefix = target.slice(0, target.indexOf('*'));
|
|
623
|
+
const pattern = new RegExp(`^${target.split('*').map((part)=>part.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&')).join('.*')}$`, 'u');
|
|
624
|
+
return filesUnder(`${shellPath}/${prefix.slice(2, prefix.lastIndexOf('/'))}`).filter((file)=>pattern.test(`./${external_node_path_default().posix.relative(shellPath, file)}`));
|
|
625
|
+
};
|
|
626
|
+
const ownClientPrefix = `${manifest.name}/api/`;
|
|
627
|
+
const foreignSpecifier = (specifier)=>/\/api\/(?:rpc-)?client$/u.test(specifier) && !specifier.startsWith(ownClientPrefix);
|
|
628
|
+
for (const target of targets(manifest.exports))if (/\.[cm]?[jt]sx?$/u.test(target)) {
|
|
629
|
+
if (!target.startsWith('./') || target.split(/[\\/]/u).includes('..')) {
|
|
630
|
+
invalidExport();
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
for (const candidate of exportedFiles(target)){
|
|
634
|
+
const file = clientExportFile(shellPath, `./${external_node_path_default().posix.relative(shellPath, candidate)}`);
|
|
635
|
+
if (!file) {
|
|
636
|
+
invalidExport();
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
assert(!exposesForeignClient(file, owner, foreignSpecifier), `${file}: Shell public exports must not expose another owner's API client`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
const exposesForeignClient = (entry, owner, foreignSpecifier)=>{
|
|
644
|
+
const graph = (0, external_module_graph_cjs_namespaceObject.createModuleGraph)();
|
|
645
|
+
const seenValues = new Set();
|
|
646
|
+
const seenExports = new Map();
|
|
647
|
+
const workspacePath = (module)=>normalize(external_node_path_default().relative(workspace, module.path));
|
|
648
|
+
const resolveSource = (from, specifier)=>resolveRelative(from, specifier) ?? ownerClients.get(specifier);
|
|
649
|
+
const returnExpressions = (node)=>{
|
|
650
|
+
if (types_namespaceObject.isReturnStatement(node)) return node.argument ? [
|
|
651
|
+
node.argument
|
|
652
|
+
] : [];
|
|
653
|
+
if (types_namespaceObject.isFunction(node)) return [];
|
|
654
|
+
return (types_namespaceObject.VISITOR_KEYS[node.type] ?? []).flatMap((key)=>{
|
|
655
|
+
const value = Reflect.get(node, key);
|
|
656
|
+
return (Array.isArray(value) ? value : [
|
|
657
|
+
value
|
|
658
|
+
]).flatMap((child)=>types_namespaceObject.isNode(child) ? returnExpressions(child) : []);
|
|
659
|
+
});
|
|
660
|
+
};
|
|
661
|
+
const nestedValues = (node)=>{
|
|
662
|
+
if (types_namespaceObject.isObjectExpression(node)) return node.properties.flatMap((property)=>types_namespaceObject.isObjectProperty(property) ? [
|
|
663
|
+
property.value
|
|
664
|
+
] : types_namespaceObject.isObjectMethod(property) ? returnExpressions(property.body) : types_namespaceObject.isSpreadElement(property) ? [
|
|
665
|
+
property.argument
|
|
666
|
+
] : []);
|
|
667
|
+
if (types_namespaceObject.isArrayExpression(node)) return node.elements.filter((element)=>null !== element);
|
|
668
|
+
if (types_namespaceObject.isFunction(node)) return types_namespaceObject.isBlockStatement(node.body) ? returnExpressions(node.body) : [
|
|
669
|
+
node.body
|
|
670
|
+
];
|
|
671
|
+
return [];
|
|
672
|
+
};
|
|
673
|
+
const foreignValue = (value, from)=>{
|
|
674
|
+
if ('external' === value.kind) {
|
|
675
|
+
const source = resolveSource(workspacePath(from), value.specifier);
|
|
676
|
+
if (!source) return foreignSpecifier(value.specifier);
|
|
677
|
+
return '*' === value.name ? inspect(source) : exportedValue(source, value.name);
|
|
678
|
+
}
|
|
679
|
+
if ('unresolved' === value.kind && types_namespaceObject.isImportDeclaration(value.node) && value.node.specifiers.some((specifier)=>types_namespaceObject.isImportNamespaceSpecifier(specifier))) {
|
|
680
|
+
const specifier = value.node.source.value;
|
|
681
|
+
const source = resolveSource(workspacePath(value.module), specifier);
|
|
682
|
+
return source ? inspect(source) : foreignSpecifier(specifier);
|
|
683
|
+
}
|
|
684
|
+
if ('node' !== value.kind || seenValues.has(value.node)) return false;
|
|
685
|
+
seenValues.add(value.node);
|
|
686
|
+
const { node, module } = value;
|
|
687
|
+
if ((types_namespaceObject.isCallExpression(node) || types_namespaceObject.isOptionalCallExpression(node)) && graph.evaluate(module, node.callee).some((callee)=>'external' === callee.kind && callee.specifier === effect && clientFactories.has(callee.name))) {
|
|
688
|
+
const [argument] = node.arguments;
|
|
689
|
+
const binding = argument ? graphBinding(graph, module, argument) : void 0;
|
|
690
|
+
if (module.boundary !== owner || binding?.kind !== 'declaration' || binding.module.boundary !== owner) return true;
|
|
691
|
+
}
|
|
692
|
+
return nestedValues(node).some((expression)=>graph.evaluate(module, expression).some((nested)=>foreignValue(nested, module)));
|
|
693
|
+
};
|
|
694
|
+
const exportName = (node)=>types_namespaceObject.isIdentifier(node) ? node.name : node.value;
|
|
695
|
+
const names = (source, visited = new Set())=>{
|
|
696
|
+
if (visited.has(source)) return [];
|
|
697
|
+
visited.add(source);
|
|
698
|
+
return graph.module(absolute(source)).file.program.body.flatMap((statement)=>{
|
|
699
|
+
if (types_namespaceObject.isExportDefaultDeclaration(statement)) return [
|
|
700
|
+
'default'
|
|
701
|
+
];
|
|
702
|
+
if (!types_namespaceObject.isExportNamedDeclaration(statement) && !types_namespaceObject.isExportAllDeclaration(statement) || 'type' === statement.exportKind) return [];
|
|
703
|
+
if (types_namespaceObject.isExportAllDeclaration(statement)) {
|
|
704
|
+
const target = resolveSource(source, statement.source.value);
|
|
705
|
+
return target ? names(target, visited).filter((name)=>'default' !== name) : [];
|
|
706
|
+
}
|
|
707
|
+
return statement.declaration ? Object.keys(types_namespaceObject.getOuterBindingIdentifiers(statement.declaration)) : statement.specifiers.flatMap((specifier)=>types_namespaceObject.isExportSpecifier(specifier) && 'type' === specifier.exportKind ? [] : [
|
|
708
|
+
exportName(specifier.exported)
|
|
709
|
+
]);
|
|
710
|
+
});
|
|
711
|
+
};
|
|
712
|
+
const exportedValue = (source, name)=>{
|
|
713
|
+
const visited = seenExports.get(source) ?? new Set();
|
|
714
|
+
if (visited.has(name)) return false;
|
|
715
|
+
visited.add(name);
|
|
716
|
+
seenExports.set(source, visited);
|
|
717
|
+
const module = graph.module(absolute(source));
|
|
718
|
+
const evaluates = (from, node)=>graph.evaluate(from, node).some((value)=>foreignValue(value, from));
|
|
719
|
+
const binding = graph.resolve(module, name, 'export');
|
|
720
|
+
if (binding?.kind === 'external') return foreignValue({
|
|
721
|
+
...binding,
|
|
722
|
+
members: []
|
|
723
|
+
}, module);
|
|
724
|
+
if (binding?.kind === 'declaration') return evaluates(binding.module, binding.expression);
|
|
725
|
+
for (const statement of module.file.program.body){
|
|
726
|
+
if (types_namespaceObject.isExportDefaultDeclaration(statement) && 'default' === name) return evaluates(module, statement.declaration);
|
|
727
|
+
if (!types_namespaceObject.isExportNamedDeclaration(statement) || 'type' === statement.exportKind) continue;
|
|
728
|
+
if (types_namespaceObject.isFunctionDeclaration(statement.declaration) && statement.declaration.id?.name === name) return evaluates(module, statement.declaration);
|
|
729
|
+
const specifier = statement.specifiers.find((item)=>exportName(item.exported) === name);
|
|
730
|
+
if (!specifier) continue;
|
|
731
|
+
if (types_namespaceObject.isExportSpecifier(specifier) && 'type' === specifier.exportKind) return false;
|
|
732
|
+
if (!statement.source) return types_namespaceObject.isExportSpecifier(specifier) ? evaluates(module, specifier.local) : false;
|
|
733
|
+
const target = resolveSource(source, statement.source.value);
|
|
734
|
+
if (!target) return foreignSpecifier(statement.source.value);
|
|
735
|
+
if (types_namespaceObject.isExportNamespaceSpecifier(specifier)) return inspect(target);
|
|
736
|
+
if (types_namespaceObject.isExportSpecifier(specifier)) return exportedValue(target, exportName(specifier.local));
|
|
737
|
+
}
|
|
738
|
+
return module.file.program.body.some((statement)=>{
|
|
739
|
+
if (!types_namespaceObject.isExportAllDeclaration(statement) || 'type' === statement.exportKind) return false;
|
|
740
|
+
const target = resolveSource(source, statement.source.value);
|
|
741
|
+
return !!target && names(target).includes(name) && exportedValue(target, name);
|
|
742
|
+
});
|
|
743
|
+
};
|
|
744
|
+
const inspect = (source)=>[
|
|
745
|
+
...new Set(names(source))
|
|
746
|
+
].some((name)=>exportedValue(source, name));
|
|
747
|
+
return inspect(entry);
|
|
748
|
+
};
|
|
749
|
+
const ownerClients = new Map();
|
|
750
|
+
for (const app of apps)if (exists(`${app.path}/package.json`)) {
|
|
751
|
+
const manifest = JSON.parse(read(`${app.path}/package.json`));
|
|
752
|
+
for (const key of [
|
|
753
|
+
'./api/client',
|
|
754
|
+
'./api/rpc-client'
|
|
755
|
+
]){
|
|
756
|
+
const file = clientExportFile(app.path, manifest.exports?.[key]);
|
|
757
|
+
if (file) ownerClients.set(`${manifest.name}${key.slice(1)}`, file);
|
|
758
|
+
}
|
|
473
759
|
}
|
|
474
|
-
|
|
760
|
+
for (const shell of apps.filter((app)=>'shell' === app.kind))guarded(shell.path, ()=>checkShellExports(normalize(shell.path)));
|
|
475
761
|
if (exists('topology/reference-topology.json')) guarded('topology/reference-topology.json', ()=>{
|
|
476
762
|
for (const vertical of JSON.parse(read('topology/reference-topology.json')).verticals ?? []){
|
|
477
763
|
if (vertical.api?.runtime === 'effect') {
|
|
@@ -495,11 +781,13 @@ const checkMicroVerticalApiConsumerFiles = (options)=>check(options, false);
|
|
|
495
781
|
exports.checkMicroVerticalApiBoundaries = __webpack_exports__.checkMicroVerticalApiBoundaries;
|
|
496
782
|
exports.checkMicroVerticalApiConsumerFiles = __webpack_exports__.checkMicroVerticalApiConsumerFiles;
|
|
497
783
|
exports.configuredMicroVerticalApiStem = __webpack_exports__.configuredMicroVerticalApiStem;
|
|
784
|
+
exports.createModuleGraph = __webpack_exports__.createModuleGraph;
|
|
498
785
|
exports.microVerticalApiBaselineViolation = __webpack_exports__.microVerticalApiBaselineViolation;
|
|
499
786
|
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
500
787
|
"checkMicroVerticalApiBoundaries",
|
|
501
788
|
"checkMicroVerticalApiConsumerFiles",
|
|
502
789
|
"configuredMicroVerticalApiStem",
|
|
790
|
+
"createModuleGraph",
|
|
503
791
|
"microVerticalApiBaselineViolation"
|
|
504
792
|
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
505
793
|
Object.defineProperty(exports, '__esModule', {
|
|
@@ -81,7 +81,7 @@ function importTarget(value) {
|
|
|
81
81
|
if (resolved) return resolved;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
function baselinePublicIdentityIsExact(fromFile, expectation) {
|
|
84
|
+
function baselinePublicIdentityIsExact(fromFile, expectation, reportMismatch) {
|
|
85
85
|
const owner = external_node_fs_default().realpathSync(expectation.baselinePackageDirectory);
|
|
86
86
|
const manifest = JSON.parse(external_node_fs_default().readFileSync(external_node_path_default().join(owner, 'package.json'), 'utf8'));
|
|
87
87
|
if (expectation.baselinePackage !== baselinePackage || ![
|
|
@@ -95,7 +95,10 @@ function baselinePublicIdentityIsExact(fromFile, expectation) {
|
|
|
95
95
|
if (error instanceof Error && error.message.includes('resolved to unrelated package')) return false;
|
|
96
96
|
throw error;
|
|
97
97
|
}
|
|
98
|
-
if (resolvedOwner !== owner)
|
|
98
|
+
if (resolvedOwner !== owner) {
|
|
99
|
+
reportMismatch?.(`selected owner ${owner}; consumer resolves ${resolvedOwner}`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
99
102
|
const target = importTarget(manifest.exports?.['./microvertical-api']);
|
|
100
103
|
if (!target?.startsWith('./')) return false;
|
|
101
104
|
const modules = new Map();
|