@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
|
@@ -2,8 +2,10 @@ import "node:module";
|
|
|
2
2
|
import node_fs from "node:fs";
|
|
3
3
|
import node_path from "node:path";
|
|
4
4
|
import { parse } from "@babel/parser";
|
|
5
|
-
import { configuredMicroVerticalApiStem,
|
|
5
|
+
import { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation } from "./microvertical-api-baseline.js";
|
|
6
6
|
import { baselinePackage, resolveBaselinePackageDirectory } from "./microvertical-api-owner.js";
|
|
7
|
+
import { createModuleGraph } from "./module-graph.js";
|
|
8
|
+
import { SourceSyntaxError, consumerParserPlugins, unwrapExpression } from "./source-analysis.js";
|
|
7
9
|
import { createEffectApiImportResolver, strictEffectRuntimeTopologyViolation } from "./strict-effect-runtime.js";
|
|
8
10
|
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
9
11
|
const ignored = new Set([
|
|
@@ -16,17 +18,48 @@ const ignored = new Set([
|
|
|
16
18
|
'node_modules',
|
|
17
19
|
'repos'
|
|
18
20
|
]);
|
|
21
|
+
const MAX_CLOSURE_DEPTH = 128;
|
|
22
|
+
const MAX_CLOSURE_MODULES = 2048;
|
|
19
23
|
const normalize = (file)=>file.split(node_path.sep).join('/').replace(/^\.\//u, '');
|
|
20
24
|
const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
21
25
|
const apiApp = (app)=>'vertical' === app.kind && 'ui-only' !== app.surfaceProfile && 'horizontal-remote' !== app.deliveryUnitKind;
|
|
22
26
|
function configuredApps(root, supplied) {
|
|
23
|
-
const
|
|
24
|
-
if (!Array.isArray(
|
|
27
|
+
const topology = supplied ? void 0 : JSON.parse(node_fs.readFileSync(node_path.join(root, 'topology/reference-topology.json'), 'utf8'));
|
|
28
|
+
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');
|
|
29
|
+
const entries = topology?.shell ? [
|
|
30
|
+
topology.shell,
|
|
31
|
+
...topology.shells ?? [],
|
|
32
|
+
...topology.verticals ?? []
|
|
33
|
+
] : void 0;
|
|
34
|
+
const source = supplied ?? entries?.map((entry)=>{
|
|
35
|
+
if (!entry || 'string' != typeof entry.path || !entry.path || node_path.isAbsolute(entry.path) || entry.path.split(/[\\/]/u).includes('..')) throw new Error('topology/reference-topology.json: each app must have an explicit workspace path');
|
|
36
|
+
const packageFile = node_path.join(root, entry.path, 'package.json');
|
|
37
|
+
const manifest = JSON.parse(node_fs.readFileSync(packageFile, 'utf8'));
|
|
38
|
+
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`);
|
|
39
|
+
const api = entry.api;
|
|
40
|
+
const stem = api?.stem ?? api?.readiness?.endpoint?.match(/^\/([a-z0-9]+(?:-[a-z0-9]+)*)\/readiness$/u)?.[1];
|
|
41
|
+
return {
|
|
42
|
+
id: entry.id,
|
|
43
|
+
kind: entry.kind,
|
|
44
|
+
path: entry.path,
|
|
45
|
+
package: manifest.name,
|
|
46
|
+
surfaceProfile: entry.surfaceProfile,
|
|
47
|
+
deliveryUnitKind: entry.deliveryUnitKind,
|
|
48
|
+
...void 0 === api ? {} : {
|
|
49
|
+
api: {
|
|
50
|
+
stem,
|
|
51
|
+
prefix: api.bff?.prefix,
|
|
52
|
+
protocol: api.protocol
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
if (!Array.isArray(source)) throw new Error('topology/reference-topology.json: shell and verticals must describe apps');
|
|
25
58
|
const paths = new Set();
|
|
26
59
|
for (const app of source){
|
|
27
|
-
if (!app || 'object' != typeof app || 'string' != typeof app.path || !app.path || node_path.isAbsolute(app.path) || app.path.split(/[\\/]/u).includes('..')) throw new Error('
|
|
60
|
+
if (!app || 'object' != typeof app || 'string' != typeof app.path || !app.path || node_path.isAbsolute(app.path) || app.path.split(/[\\/]/u).includes('..')) throw new Error('topology/reference-topology.json: each app must have a relative workspace path');
|
|
28
61
|
const key = normalize(app.path);
|
|
29
|
-
if (paths.has(key)) throw new Error(
|
|
62
|
+
if (paths.has(key)) throw new Error(`topology/reference-topology.json: duplicate app path ${key}`);
|
|
30
63
|
paths.add(key);
|
|
31
64
|
for (const name of [
|
|
32
65
|
'id',
|
|
@@ -35,7 +68,7 @@ function configuredApps(root, supplied) {
|
|
|
35
68
|
'surfaceProfile',
|
|
36
69
|
'deliveryUnitKind'
|
|
37
70
|
])if (void 0 !== app[name] && 'string' != typeof app[name]) throw new Error(`${key}: ${name} must be a string`);
|
|
38
|
-
|
|
71
|
+
const allowedFields = {
|
|
39
72
|
kind: [
|
|
40
73
|
'shell',
|
|
41
74
|
'vertical'
|
|
@@ -49,7 +82,11 @@ function configuredApps(root, supplied) {
|
|
|
49
82
|
'microvertical',
|
|
50
83
|
'horizontal-remote'
|
|
51
84
|
]
|
|
52
|
-
}
|
|
85
|
+
};
|
|
86
|
+
for (const field of Object.keys(allowedFields)){
|
|
87
|
+
const value = app[field];
|
|
88
|
+
if ((void 0 !== value || 'kind' === field) && ('string' != typeof value || !allowedFields[field].includes(value))) throw new Error(`${key}: invalid ${field}`);
|
|
89
|
+
}
|
|
53
90
|
if (void 0 !== app.api) {
|
|
54
91
|
if (!app.api || 'object' != typeof app.api || Array.isArray(app.api)) throw new Error(`${key}: api must be an object`);
|
|
55
92
|
if (void 0 !== app.api.protocol && ![
|
|
@@ -90,6 +127,9 @@ function check(options, topology) {
|
|
|
90
127
|
};
|
|
91
128
|
try {
|
|
92
129
|
const apps = configuredApps(root, options.configuredApps);
|
|
130
|
+
const workspace = node_fs.realpathSync(root);
|
|
131
|
+
const realRelative = (file)=>normalize(node_path.relative(workspace, node_fs.realpathSync(absolute(file))));
|
|
132
|
+
const inside = (directory, real)=>real.startsWith(`${directory}${node_path.sep}`);
|
|
93
133
|
const byPath = new Map(apps.map((app)=>[
|
|
94
134
|
normalize(app.path),
|
|
95
135
|
app
|
|
@@ -130,39 +170,100 @@ function check(options, topology) {
|
|
|
130
170
|
const resolveRelative = (fromFile, specifier)=>{
|
|
131
171
|
if (!/^\.\.?\//u.test(specifier)) return;
|
|
132
172
|
const base = node_path.posix.join(node_path.posix.dirname(fromFile), specifier).replace(/\.(?:[cm]?[jt]sx?)$/u, '');
|
|
133
|
-
|
|
173
|
+
const candidate = [
|
|
134
174
|
`${base}.ts`,
|
|
135
175
|
`${base}.tsx`,
|
|
136
176
|
`${base}/index.ts`,
|
|
137
177
|
`${base}/index.tsx`
|
|
138
|
-
].find((
|
|
178
|
+
].find((file)=>exists(file));
|
|
179
|
+
return void 0 === candidate ? void 0 : realRelative(candidate);
|
|
180
|
+
};
|
|
181
|
+
const graphBinding = (graph, module, expression, seen = new Set())=>{
|
|
182
|
+
if (seen.has(expression) || seen.size >= 128) return;
|
|
183
|
+
seen.add(expression);
|
|
184
|
+
const binding = __rspack_external__babel_types_6e8337dd.isIdentifier(expression) || __rspack_external__babel_types_6e8337dd.isMemberExpression(expression) ? graph.resolveReference(module, expression) : void 0;
|
|
185
|
+
return binding?.kind === 'declaration' && (__rspack_external__babel_types_6e8337dd.isIdentifier(binding.expression) || __rspack_external__babel_types_6e8337dd.isMemberExpression(binding.expression)) ? graphBinding(graph, binding.module, binding.expression, seen) : binding;
|
|
139
186
|
};
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
const
|
|
187
|
+
const clientExportFile = (appPath, exported)=>{
|
|
188
|
+
if ('string' != typeof exported || !exported.startsWith('./') || exported.split(/[\\/]/u).includes('..') || !/\.[cm]?[jt]sx?$/u.test(exported)) return;
|
|
189
|
+
const appDirectory = absolute(appPath);
|
|
190
|
+
const candidate = node_path.resolve(appDirectory, exported);
|
|
191
|
+
try {
|
|
192
|
+
const workspace = node_fs.realpathSync(root);
|
|
193
|
+
const owner = node_fs.realpathSync(appDirectory);
|
|
194
|
+
const real = node_fs.realpathSync(candidate);
|
|
195
|
+
if (!owner.startsWith(`${workspace}${node_path.sep}`) || !real.startsWith(`${owner}${node_path.sep}`) || !node_fs.statSync(real).isFile()) return;
|
|
196
|
+
} catch {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
return normalize(node_path.relative(root, candidate));
|
|
200
|
+
};
|
|
201
|
+
const composedModules = (file, appPath)=>{
|
|
202
|
+
const owner = node_fs.realpathSync(absolute(appPath));
|
|
203
|
+
if (!inside(owner, node_fs.realpathSync(absolute(file)))) throw new Error(`consumer module closure escapes its owner: ${file}`);
|
|
204
|
+
const entry = realRelative(file);
|
|
205
|
+
const discovered = new Set([
|
|
206
|
+
entry
|
|
207
|
+
]);
|
|
143
208
|
const queue = [
|
|
144
|
-
|
|
209
|
+
{
|
|
210
|
+
file: entry,
|
|
211
|
+
depth: 0
|
|
212
|
+
}
|
|
145
213
|
];
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (seen.has(current)) continue;
|
|
149
|
-
seen.add(current);
|
|
150
|
-
order.push(current);
|
|
214
|
+
let graph;
|
|
215
|
+
for (const { file: current, depth } of queue){
|
|
151
216
|
const ast = parseFile(current);
|
|
152
217
|
if (!ast) continue;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
218
|
+
const sources = [];
|
|
219
|
+
let callsRequire = false;
|
|
220
|
+
for (const statement of ast.program.body)if (__rspack_external__babel_types_6e8337dd.isImportDeclaration(statement) && 'type' !== statement.importKind && 'typeof' !== statement.importKind || (__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || __rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement)) && 'type' !== statement.exportKind && statement.source) sources.push(statement.source.value);
|
|
221
|
+
__rspack_external__babel_types_6e8337dd.traverseFast(ast, (node)=>{
|
|
222
|
+
if (__rspack_external__babel_types_6e8337dd.isImportExpression(node)) {
|
|
223
|
+
if (!__rspack_external__babel_types_6e8337dd.isStringLiteral(node.source)) throw new Error(`consumer module closure has nonliteral runtime dependency: ${current}`);
|
|
224
|
+
sources.push(node.source.value);
|
|
225
|
+
}
|
|
226
|
+
if ((__rspack_external__babel_types_6e8337dd.isCallExpression(node) || __rspack_external__babel_types_6e8337dd.isOptionalCallExpression(node)) && __rspack_external__babel_types_6e8337dd.isIdentifier(unwrapExpression(node.callee, true), {
|
|
227
|
+
name: 'require'
|
|
228
|
+
})) callsRequire = true;
|
|
229
|
+
});
|
|
230
|
+
if (callsRequire) {
|
|
231
|
+
graph ??= createModuleGraph();
|
|
232
|
+
const module = graph.module(absolute(current));
|
|
233
|
+
__rspack_external__babel_types_6e8337dd.traverseFast(module.file, (node)=>{
|
|
234
|
+
if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node) && !__rspack_external__babel_types_6e8337dd.isOptionalCallExpression(node)) return;
|
|
235
|
+
const callee = unwrapExpression(node.callee, true);
|
|
236
|
+
if (!__rspack_external__babel_types_6e8337dd.isIdentifier(callee, {
|
|
237
|
+
name: 'require'
|
|
238
|
+
}) || !graph.isUnboundReference(module, callee)) return;
|
|
239
|
+
const [argument] = node.arguments;
|
|
240
|
+
if (1 !== node.arguments.length || !__rspack_external__babel_types_6e8337dd.isStringLiteral(argument)) throw new Error(`consumer module closure has nonliteral runtime dependency: ${current}`);
|
|
241
|
+
sources.push(argument.value);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
for (const source of sources){
|
|
245
|
+
if (!/^\.\.?\//u.test(source)) continue;
|
|
246
|
+
const resolved = resolveRelative(current, source);
|
|
247
|
+
if (void 0 === resolved) throw new Error(`consumer module closure has unresolved relative dependency: ${current} -> ${source}`);
|
|
248
|
+
if (!inside(owner, node_path.join(workspace, resolved))) throw new Error(`consumer module closure escapes its owner: ${current} -> ${source}`);
|
|
249
|
+
if (!discovered.has(resolved)) {
|
|
250
|
+
if (depth + 1 >= MAX_CLOSURE_DEPTH) throw new Error(`consumer module closure exceeds ${MAX_CLOSURE_DEPTH} import-depth analysis budget`);
|
|
251
|
+
if (discovered.size >= MAX_CLOSURE_MODULES) throw new Error(`consumer module closure exceeds ${MAX_CLOSURE_MODULES}-module analysis budget`);
|
|
252
|
+
discovered.add(resolved);
|
|
253
|
+
queue.push({
|
|
254
|
+
file: resolved,
|
|
255
|
+
depth: depth + 1
|
|
256
|
+
});
|
|
257
|
+
}
|
|
157
258
|
}
|
|
158
259
|
}
|
|
159
|
-
return
|
|
260
|
+
return queue.map((item)=>item.file);
|
|
160
261
|
};
|
|
161
|
-
const moduleShape = (file, imports, calls, patterns = [], defaultExport,
|
|
262
|
+
const moduleShape = (file, imports, calls, patterns = [], defaultExport, composedAppPath)=>{
|
|
162
263
|
if (!exists(file)) return;
|
|
163
264
|
const ast = parseFile(file);
|
|
164
265
|
if (!ast) return;
|
|
165
|
-
const shapeFiles =
|
|
266
|
+
const shapeFiles = composedAppPath ? composedModules(file, composedAppPath) : [
|
|
166
267
|
file
|
|
167
268
|
];
|
|
168
269
|
const body = shapeFiles.flatMap((shapeFile)=>parseFile(shapeFile)?.program.body ?? []);
|
|
@@ -195,6 +296,10 @@ function check(options, topology) {
|
|
|
195
296
|
})), `${file}: must default-export ${defaultExport}`);
|
|
196
297
|
};
|
|
197
298
|
const effect = '@modern-js/bff-effect/effect-client';
|
|
299
|
+
const clientFactories = new Set([
|
|
300
|
+
'makeEffectHttpApiClient',
|
|
301
|
+
'makeEffectRpcClient'
|
|
302
|
+
]);
|
|
198
303
|
const appDirectories = [
|
|
199
304
|
...new Set([
|
|
200
305
|
...directories('apps'),
|
|
@@ -255,7 +360,6 @@ function check(options, topology) {
|
|
|
255
360
|
if (violation) diagnostics.push(`${file}: ${violation}`);
|
|
256
361
|
}
|
|
257
362
|
});
|
|
258
|
-
let owner;
|
|
259
363
|
for (const appPath of appDirectories)guarded(appPath, ()=>{
|
|
260
364
|
const app = byPath.get(appPath);
|
|
261
365
|
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`);
|
|
@@ -272,19 +376,25 @@ function check(options, topology) {
|
|
|
272
376
|
}
|
|
273
377
|
return;
|
|
274
378
|
}
|
|
275
|
-
if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in
|
|
379
|
+
if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in topology/reference-topology.json`);
|
|
276
380
|
const stem = app?.api?.stem ?? node_path.posix.basename(appPath);
|
|
277
381
|
const rpc = app?.api?.protocol === 'rpc';
|
|
278
382
|
const contract = `${appPath}/shared/${rpc ? 'rpc' : 'api'}.ts`;
|
|
279
|
-
const client = `${appPath}/src/api/${stem}-${rpc ? 'rpc-client' : 'client'}.ts`;
|
|
280
383
|
const entry = `${appPath}/api/index.ts`;
|
|
281
384
|
for (const file of [
|
|
282
385
|
entry,
|
|
283
|
-
contract
|
|
284
|
-
client
|
|
386
|
+
contract
|
|
285
387
|
])assert(exists(file), `${file}: required API surface is missing`);
|
|
388
|
+
const packageFile = `${appPath}/package.json`;
|
|
389
|
+
assert(exists(packageFile), `${packageFile}: required API package is missing`);
|
|
390
|
+
if (!exists(packageFile)) return;
|
|
391
|
+
const manifest = JSON.parse(read(packageFile));
|
|
392
|
+
assert(manifest.exports?.['./api'] === `./shared/${rpc ? 'rpc' : 'api'}.ts`, `${packageFile}: invalid ./api export`);
|
|
393
|
+
const clientKey = `./api/${rpc ? 'rpc-client' : 'client'}`;
|
|
394
|
+
const client = clientExportFile(appPath, manifest.exports?.[clientKey]);
|
|
395
|
+
assert(void 0 !== client, `${packageFile}: invalid API client export`);
|
|
396
|
+
assert(manifest.exports?.[`./api/${rpc ? 'client' : 'rpc-client'}`] === void 0, `${packageFile}: forbidden opposite protocol client export`);
|
|
286
397
|
noPath(`${appPath}/shared/${rpc ? 'api' : 'rpc'}.ts`, `must not emit a ${rpc ? 'REST' : 'RPC'} contract`);
|
|
287
|
-
noPath(`${appPath}/src/api/${stem}-${rpc ? 'client' : 'rpc-client'}.ts`, `must not emit a ${rpc ? 'REST' : 'RPC'} client`);
|
|
288
398
|
if (rpc) {
|
|
289
399
|
moduleShape(entry, [
|
|
290
400
|
[
|
|
@@ -315,7 +425,7 @@ function check(options, topology) {
|
|
|
315
425
|
], 'apiRuntime');
|
|
316
426
|
moduleShape(contract, [
|
|
317
427
|
[
|
|
318
|
-
'effect/
|
|
428
|
+
'effect/rpc',
|
|
319
429
|
[
|
|
320
430
|
'Rpc',
|
|
321
431
|
'RpcGroup'
|
|
@@ -333,7 +443,7 @@ function check(options, topology) {
|
|
|
333
443
|
'Schema.Struct'
|
|
334
444
|
]);
|
|
335
445
|
} else if (exists(contract)) {
|
|
336
|
-
owner
|
|
446
|
+
const owner = options.baselinePackageDirectory ?? resolveBaselinePackageDirectory(absolute(contract));
|
|
337
447
|
const apiPrefix = app?.api?.prefix ?? `/${stem}-api`;
|
|
338
448
|
const basePath = `${apiPrefix}/${stem}`;
|
|
339
449
|
const expectation = {
|
|
@@ -350,21 +460,24 @@ function check(options, topology) {
|
|
|
350
460
|
const violation = microVerticalApiBaselineViolation(stem, absolute(contract), expectation);
|
|
351
461
|
if (violation) diagnostics.push(`${contract}: ${violation}`);
|
|
352
462
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
463
|
+
if (client) {
|
|
464
|
+
const relativeContract = node_path.posix.relative(node_path.posix.dirname(client), contract);
|
|
465
|
+
moduleShape(client, [
|
|
466
|
+
[
|
|
467
|
+
effect,
|
|
468
|
+
[
|
|
469
|
+
'Effect',
|
|
470
|
+
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
471
|
+
]
|
|
472
|
+
],
|
|
356
473
|
[
|
|
357
|
-
'
|
|
358
|
-
|
|
474
|
+
relativeContract.startsWith('.') ? relativeContract : `./${relativeContract}`,
|
|
475
|
+
[]
|
|
359
476
|
]
|
|
360
|
-
],
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
]
|
|
365
|
-
], [
|
|
366
|
-
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
367
|
-
], [], void 0, true);
|
|
477
|
+
], [
|
|
478
|
+
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
479
|
+
], [], void 0, appPath);
|
|
480
|
+
}
|
|
368
481
|
moduleShape(`${appPath}/api/effect-api.ts`, [], [], [
|
|
369
482
|
[
|
|
370
483
|
/\bbackendFederationContract\b/u,
|
|
@@ -405,28 +518,200 @@ function check(options, topology) {
|
|
|
405
518
|
'must enable strictEffectApproach'
|
|
406
519
|
]
|
|
407
520
|
]);
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
521
|
+
});
|
|
522
|
+
const sourceRules = options.sourceRules ?? [];
|
|
523
|
+
for (const file of sourceRules.length ? sourceFiles : [])if (/\.[cm]?[jt]sx?$/u.test(file)) guarded(file, ()=>{
|
|
524
|
+
const graph = createModuleGraph();
|
|
525
|
+
let module;
|
|
526
|
+
try {
|
|
527
|
+
module = graph.module(absolute(file));
|
|
528
|
+
} catch (error) {
|
|
529
|
+
if (!(error instanceof SourceSyntaxError)) throw error;
|
|
530
|
+
diagnostics.push(`${file}: source rules need parseable source (${error.message})`);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
for (const rule of sourceRules){
|
|
534
|
+
const messages = rule({
|
|
535
|
+
file,
|
|
536
|
+
module,
|
|
537
|
+
graph
|
|
538
|
+
});
|
|
539
|
+
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)`);
|
|
540
|
+
for (const message of messages)diagnostics.push(`${file}: ${message}`);
|
|
414
541
|
}
|
|
415
542
|
});
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
assert(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
543
|
+
const checkShellExports = (shellPath)=>{
|
|
544
|
+
const packageFile = `${shellPath}/package.json`;
|
|
545
|
+
const manifest = JSON.parse(read(packageFile));
|
|
546
|
+
noPath(`${shellPath}/src/api/vertical-clients.ts`, 'Shell client registries are forbidden; import each owner client directly');
|
|
547
|
+
assert(!Object.hasOwn(manifest.exports ?? {}, './api/clients'), `${packageFile}: Shell ./api/clients exports are forbidden`);
|
|
548
|
+
const owner = node_fs.realpathSync(absolute(shellPath));
|
|
549
|
+
const invalidExport = ()=>assert(false, `${packageFile}: invalid Shell public source export`);
|
|
550
|
+
const targets = (exported)=>'string' == typeof exported ? [
|
|
551
|
+
exported
|
|
552
|
+
] : exported && 'object' == typeof exported ? Object.values(exported).flatMap(targets) : [];
|
|
553
|
+
const filesUnder = (directory, seen = new Set())=>{
|
|
554
|
+
const real = node_fs.realpathSync(absolute(directory));
|
|
555
|
+
if (real !== owner && !inside(owner, real)) {
|
|
556
|
+
invalidExport();
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
if (seen.has(real)) return [];
|
|
560
|
+
seen.add(real);
|
|
561
|
+
return node_fs.readdirSync(absolute(directory), {
|
|
562
|
+
withFileTypes: true
|
|
563
|
+
}).flatMap((entry)=>{
|
|
564
|
+
if (ignored.has(entry.name)) return [];
|
|
565
|
+
const file = `${directory}/${entry.name}`;
|
|
566
|
+
const stat = node_fs.statSync(absolute(file));
|
|
567
|
+
return stat.isDirectory() ? filesUnder(file, seen) : stat.isFile() ? [
|
|
568
|
+
file
|
|
569
|
+
] : [];
|
|
570
|
+
});
|
|
571
|
+
};
|
|
572
|
+
const exportedFiles = (target)=>{
|
|
573
|
+
if (!target.includes('*')) return [
|
|
574
|
+
`${shellPath}/${target.slice(2)}`
|
|
575
|
+
];
|
|
576
|
+
const prefix = target.slice(0, target.indexOf('*'));
|
|
577
|
+
const pattern = new RegExp(`^${target.split('*').map((part)=>part.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&')).join('.*')}$`, 'u');
|
|
578
|
+
return filesUnder(`${shellPath}/${prefix.slice(2, prefix.lastIndexOf('/'))}`).filter((file)=>pattern.test(`./${node_path.posix.relative(shellPath, file)}`));
|
|
579
|
+
};
|
|
580
|
+
const ownClientPrefix = `${manifest.name}/api/`;
|
|
581
|
+
const foreignSpecifier = (specifier)=>/\/api\/(?:rpc-)?client$/u.test(specifier) && !specifier.startsWith(ownClientPrefix);
|
|
582
|
+
for (const target of targets(manifest.exports))if (/\.[cm]?[jt]sx?$/u.test(target)) {
|
|
583
|
+
if (!target.startsWith('./') || target.split(/[\\/]/u).includes('..')) {
|
|
584
|
+
invalidExport();
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
for (const candidate of exportedFiles(target)){
|
|
588
|
+
const file = clientExportFile(shellPath, `./${node_path.posix.relative(shellPath, candidate)}`);
|
|
589
|
+
if (!file) {
|
|
590
|
+
invalidExport();
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
assert(!exposesForeignClient(file, owner, foreignSpecifier), `${file}: Shell public exports must not expose another owner's API client`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
const exposesForeignClient = (entry, owner, foreignSpecifier)=>{
|
|
598
|
+
const graph = createModuleGraph();
|
|
599
|
+
const seenValues = new Set();
|
|
600
|
+
const seenExports = new Map();
|
|
601
|
+
const workspacePath = (module)=>normalize(node_path.relative(workspace, module.path));
|
|
602
|
+
const resolveSource = (from, specifier)=>resolveRelative(from, specifier) ?? ownerClients.get(specifier);
|
|
603
|
+
const returnExpressions = (node)=>{
|
|
604
|
+
if (__rspack_external__babel_types_6e8337dd.isReturnStatement(node)) return node.argument ? [
|
|
605
|
+
node.argument
|
|
606
|
+
] : [];
|
|
607
|
+
if (__rspack_external__babel_types_6e8337dd.isFunction(node)) return [];
|
|
608
|
+
return (__rspack_external__babel_types_6e8337dd.VISITOR_KEYS[node.type] ?? []).flatMap((key)=>{
|
|
609
|
+
const value = Reflect.get(node, key);
|
|
610
|
+
return (Array.isArray(value) ? value : [
|
|
611
|
+
value
|
|
612
|
+
]).flatMap((child)=>__rspack_external__babel_types_6e8337dd.isNode(child) ? returnExpressions(child) : []);
|
|
613
|
+
});
|
|
614
|
+
};
|
|
615
|
+
const nestedValues = (node)=>{
|
|
616
|
+
if (__rspack_external__babel_types_6e8337dd.isObjectExpression(node)) return node.properties.flatMap((property)=>__rspack_external__babel_types_6e8337dd.isObjectProperty(property) ? [
|
|
617
|
+
property.value
|
|
618
|
+
] : __rspack_external__babel_types_6e8337dd.isObjectMethod(property) ? returnExpressions(property.body) : __rspack_external__babel_types_6e8337dd.isSpreadElement(property) ? [
|
|
619
|
+
property.argument
|
|
620
|
+
] : []);
|
|
621
|
+
if (__rspack_external__babel_types_6e8337dd.isArrayExpression(node)) return node.elements.filter((element)=>null !== element);
|
|
622
|
+
if (__rspack_external__babel_types_6e8337dd.isFunction(node)) return __rspack_external__babel_types_6e8337dd.isBlockStatement(node.body) ? returnExpressions(node.body) : [
|
|
623
|
+
node.body
|
|
624
|
+
];
|
|
625
|
+
return [];
|
|
626
|
+
};
|
|
627
|
+
const foreignValue = (value, from)=>{
|
|
628
|
+
if ('external' === value.kind) {
|
|
629
|
+
const source = resolveSource(workspacePath(from), value.specifier);
|
|
630
|
+
if (!source) return foreignSpecifier(value.specifier);
|
|
631
|
+
return '*' === value.name ? inspect(source) : exportedValue(source, value.name);
|
|
426
632
|
}
|
|
427
|
-
|
|
633
|
+
if ('unresolved' === value.kind && __rspack_external__babel_types_6e8337dd.isImportDeclaration(value.node) && value.node.specifiers.some((specifier)=>__rspack_external__babel_types_6e8337dd.isImportNamespaceSpecifier(specifier))) {
|
|
634
|
+
const specifier = value.node.source.value;
|
|
635
|
+
const source = resolveSource(workspacePath(value.module), specifier);
|
|
636
|
+
return source ? inspect(source) : foreignSpecifier(specifier);
|
|
637
|
+
}
|
|
638
|
+
if ('node' !== value.kind || seenValues.has(value.node)) return false;
|
|
639
|
+
seenValues.add(value.node);
|
|
640
|
+
const { node, module } = value;
|
|
641
|
+
if ((__rspack_external__babel_types_6e8337dd.isCallExpression(node) || __rspack_external__babel_types_6e8337dd.isOptionalCallExpression(node)) && graph.evaluate(module, node.callee).some((callee)=>'external' === callee.kind && callee.specifier === effect && clientFactories.has(callee.name))) {
|
|
642
|
+
const [argument] = node.arguments;
|
|
643
|
+
const binding = argument ? graphBinding(graph, module, argument) : void 0;
|
|
644
|
+
if (module.boundary !== owner || binding?.kind !== 'declaration' || binding.module.boundary !== owner) return true;
|
|
645
|
+
}
|
|
646
|
+
return nestedValues(node).some((expression)=>graph.evaluate(module, expression).some((nested)=>foreignValue(nested, module)));
|
|
647
|
+
};
|
|
648
|
+
const exportName = (node)=>__rspack_external__babel_types_6e8337dd.isIdentifier(node) ? node.name : node.value;
|
|
649
|
+
const names = (source, visited = new Set())=>{
|
|
650
|
+
if (visited.has(source)) return [];
|
|
651
|
+
visited.add(source);
|
|
652
|
+
return graph.module(absolute(source)).file.program.body.flatMap((statement)=>{
|
|
653
|
+
if (__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement)) return [
|
|
654
|
+
'default'
|
|
655
|
+
];
|
|
656
|
+
if (!__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) && !__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement) || 'type' === statement.exportKind) return [];
|
|
657
|
+
if (__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement)) {
|
|
658
|
+
const target = resolveSource(source, statement.source.value);
|
|
659
|
+
return target ? names(target, visited).filter((name)=>'default' !== name) : [];
|
|
660
|
+
}
|
|
661
|
+
return statement.declaration ? Object.keys(__rspack_external__babel_types_6e8337dd.getOuterBindingIdentifiers(statement.declaration)) : statement.specifiers.flatMap((specifier)=>__rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier) && 'type' === specifier.exportKind ? [] : [
|
|
662
|
+
exportName(specifier.exported)
|
|
663
|
+
]);
|
|
664
|
+
});
|
|
665
|
+
};
|
|
666
|
+
const exportedValue = (source, name)=>{
|
|
667
|
+
const visited = seenExports.get(source) ?? new Set();
|
|
668
|
+
if (visited.has(name)) return false;
|
|
669
|
+
visited.add(name);
|
|
670
|
+
seenExports.set(source, visited);
|
|
671
|
+
const module = graph.module(absolute(source));
|
|
672
|
+
const evaluates = (from, node)=>graph.evaluate(from, node).some((value)=>foreignValue(value, from));
|
|
673
|
+
const binding = graph.resolve(module, name, 'export');
|
|
674
|
+
if (binding?.kind === 'external') return foreignValue({
|
|
675
|
+
...binding,
|
|
676
|
+
members: []
|
|
677
|
+
}, module);
|
|
678
|
+
if (binding?.kind === 'declaration') return evaluates(binding.module, binding.expression);
|
|
679
|
+
for (const statement of module.file.program.body){
|
|
680
|
+
if (__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement) && 'default' === name) return evaluates(module, statement.declaration);
|
|
681
|
+
if (!__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || 'type' === statement.exportKind) continue;
|
|
682
|
+
if (__rspack_external__babel_types_6e8337dd.isFunctionDeclaration(statement.declaration) && statement.declaration.id?.name === name) return evaluates(module, statement.declaration);
|
|
683
|
+
const specifier = statement.specifiers.find((item)=>exportName(item.exported) === name);
|
|
684
|
+
if (!specifier) continue;
|
|
685
|
+
if (__rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier) && 'type' === specifier.exportKind) return false;
|
|
686
|
+
if (!statement.source) return __rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier) ? evaluates(module, specifier.local) : false;
|
|
687
|
+
const target = resolveSource(source, statement.source.value);
|
|
688
|
+
if (!target) return foreignSpecifier(statement.source.value);
|
|
689
|
+
if (__rspack_external__babel_types_6e8337dd.isExportNamespaceSpecifier(specifier)) return inspect(target);
|
|
690
|
+
if (__rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier)) return exportedValue(target, exportName(specifier.local));
|
|
691
|
+
}
|
|
692
|
+
return module.file.program.body.some((statement)=>{
|
|
693
|
+
if (!__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement) || 'type' === statement.exportKind) return false;
|
|
694
|
+
const target = resolveSource(source, statement.source.value);
|
|
695
|
+
return !!target && names(target).includes(name) && exportedValue(target, name);
|
|
696
|
+
});
|
|
697
|
+
};
|
|
698
|
+
const inspect = (source)=>[
|
|
699
|
+
...new Set(names(source))
|
|
700
|
+
].some((name)=>exportedValue(source, name));
|
|
701
|
+
return inspect(entry);
|
|
702
|
+
};
|
|
703
|
+
const ownerClients = new Map();
|
|
704
|
+
for (const app of apps)if (exists(`${app.path}/package.json`)) {
|
|
705
|
+
const manifest = JSON.parse(read(`${app.path}/package.json`));
|
|
706
|
+
for (const key of [
|
|
707
|
+
'./api/client',
|
|
708
|
+
'./api/rpc-client'
|
|
709
|
+
]){
|
|
710
|
+
const file = clientExportFile(app.path, manifest.exports?.[key]);
|
|
711
|
+
if (file) ownerClients.set(`${manifest.name}${key.slice(1)}`, file);
|
|
712
|
+
}
|
|
428
713
|
}
|
|
429
|
-
|
|
714
|
+
for (const shell of apps.filter((app)=>'shell' === app.kind))guarded(shell.path, ()=>checkShellExports(normalize(shell.path)));
|
|
430
715
|
if (exists('topology/reference-topology.json')) guarded('topology/reference-topology.json', ()=>{
|
|
431
716
|
for (const vertical of JSON.parse(read('topology/reference-topology.json')).verticals ?? []){
|
|
432
717
|
if (vertical.api?.runtime === 'effect') {
|
|
@@ -447,4 +732,4 @@ function check(options, topology) {
|
|
|
447
732
|
}
|
|
448
733
|
const checkMicroVerticalApiBoundaries = (options)=>check(options, true);
|
|
449
734
|
const checkMicroVerticalApiConsumerFiles = (options)=>check(options, false);
|
|
450
|
-
export { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles, configuredMicroVerticalApiStem, microVerticalApiBaselineViolation };
|
|
735
|
+
export { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles, configuredMicroVerticalApiStem, createModuleGraph, microVerticalApiBaselineViolation };
|
|
@@ -37,7 +37,7 @@ function importTarget(value) {
|
|
|
37
37
|
if (resolved) return resolved;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
function baselinePublicIdentityIsExact(fromFile, expectation) {
|
|
40
|
+
function baselinePublicIdentityIsExact(fromFile, expectation, reportMismatch) {
|
|
41
41
|
const owner = node_fs.realpathSync(expectation.baselinePackageDirectory);
|
|
42
42
|
const manifest = JSON.parse(node_fs.readFileSync(node_path.join(owner, 'package.json'), 'utf8'));
|
|
43
43
|
if (expectation.baselinePackage !== baselinePackage || ![
|
|
@@ -51,7 +51,10 @@ function baselinePublicIdentityIsExact(fromFile, expectation) {
|
|
|
51
51
|
if (error instanceof Error && error.message.includes('resolved to unrelated package')) return false;
|
|
52
52
|
throw error;
|
|
53
53
|
}
|
|
54
|
-
if (resolvedOwner !== owner)
|
|
54
|
+
if (resolvedOwner !== owner) {
|
|
55
|
+
reportMismatch?.(`selected owner ${owner}; consumer resolves ${resolvedOwner}`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
55
58
|
const target = importTarget(manifest.exports?.['./microvertical-api']);
|
|
56
59
|
if (!target?.startsWith('./')) return false;
|
|
57
60
|
const modules = new Map();
|