@bleedingdev/modern-js-code-tools 3.9.0-ultramodern.11 → 3.9.0-ultramodern.13
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/dist/cjs/microvertical-api-baseline.cjs +108 -78
- package/dist/cjs/microvertical-api-boundary.cjs +90 -36
- package/dist/cjs/oxlint-plugin/rules/strict-effect-api-boundaries.cjs +1 -1
- package/dist/cjs/source-analysis.cjs +104 -0
- package/dist/cjs/strict-effect-runtime.cjs +12 -47
- package/dist/esm/microvertical-api-baseline.js +99 -65
- package/dist/esm/microvertical-api-boundary.js +90 -36
- package/dist/esm/oxlint-plugin/rules/strict-effect-api-boundaries.js +1 -1
- package/dist/esm/source-analysis.js +44 -0
- package/dist/esm/strict-effect-runtime.js +12 -46
- package/dist/esm-node/microvertical-api-baseline.js +99 -65
- package/dist/esm-node/microvertical-api-boundary.js +90 -36
- package/dist/esm-node/oxlint-plugin/rules/strict-effect-api-boundaries.js +1 -1
- package/dist/esm-node/source-analysis.js +45 -0
- package/dist/esm-node/strict-effect-runtime.js +12 -46
- package/dist/types/microvertical-api-baseline.d.ts +0 -5
- package/dist/types/source-analysis.d.ts +10 -0
- package/package.json +8 -7
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import node_fs from "node:fs";
|
|
2
2
|
import node_path from "node:path";
|
|
3
3
|
import { parse } from "@babel/parser";
|
|
4
|
-
import { configuredMicroVerticalApiStem,
|
|
4
|
+
import { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation } from "./microvertical-api-baseline.js";
|
|
5
5
|
import { baselinePackage, resolveBaselinePackageDirectory } from "./microvertical-api-owner.js";
|
|
6
|
+
import { consumerParserPlugins } from "./source-analysis.js";
|
|
6
7
|
import { createEffectApiImportResolver, strictEffectRuntimeTopologyViolation } from "./strict-effect-runtime.js";
|
|
7
8
|
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
8
9
|
const ignored = new Set([
|
|
@@ -19,13 +20,42 @@ const normalize = (file)=>file.split(node_path.sep).join('/').replace(/^\.\//u,
|
|
|
19
20
|
const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
20
21
|
const apiApp = (app)=>'vertical' === app.kind && 'ui-only' !== app.surfaceProfile && 'horizontal-remote' !== app.deliveryUnitKind;
|
|
21
22
|
function configuredApps(root, supplied) {
|
|
22
|
-
const
|
|
23
|
-
if (!Array.isArray(
|
|
23
|
+
const topology = supplied ? void 0 : JSON.parse(node_fs.readFileSync(node_path.join(root, 'topology/reference-topology.json'), 'utf8'));
|
|
24
|
+
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');
|
|
25
|
+
const entries = topology?.shell ? [
|
|
26
|
+
topology.shell,
|
|
27
|
+
...topology.shells ?? [],
|
|
28
|
+
...topology.verticals ?? []
|
|
29
|
+
] : void 0;
|
|
30
|
+
const source = supplied ?? entries?.map((entry)=>{
|
|
31
|
+
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');
|
|
32
|
+
const packageFile = node_path.join(root, entry.path, 'package.json');
|
|
33
|
+
const manifest = JSON.parse(node_fs.readFileSync(packageFile, 'utf8'));
|
|
34
|
+
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`);
|
|
35
|
+
const api = entry.api;
|
|
36
|
+
const stem = api?.stem ?? api?.readiness?.endpoint?.match(/^\/([a-z0-9]+(?:-[a-z0-9]+)*)\/readiness$/u)?.[1];
|
|
37
|
+
return {
|
|
38
|
+
id: entry.id,
|
|
39
|
+
kind: entry.kind,
|
|
40
|
+
path: entry.path,
|
|
41
|
+
package: manifest.name,
|
|
42
|
+
surfaceProfile: entry.surfaceProfile,
|
|
43
|
+
deliveryUnitKind: entry.deliveryUnitKind,
|
|
44
|
+
...void 0 === api ? {} : {
|
|
45
|
+
api: {
|
|
46
|
+
stem,
|
|
47
|
+
prefix: api.bff?.prefix,
|
|
48
|
+
protocol: api.protocol
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
if (!Array.isArray(source)) throw new Error('topology/reference-topology.json: shell and verticals must describe apps');
|
|
24
54
|
const paths = new Set();
|
|
25
55
|
for (const app of source){
|
|
26
|
-
if (!app || 'object' != typeof app || 'string' != typeof app.path || !app.path || node_path.isAbsolute(app.path) || app.path.split(/[\\/]/u).includes('..')) throw new Error('
|
|
56
|
+
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');
|
|
27
57
|
const key = normalize(app.path);
|
|
28
|
-
if (paths.has(key)) throw new Error(
|
|
58
|
+
if (paths.has(key)) throw new Error(`topology/reference-topology.json: duplicate app path ${key}`);
|
|
29
59
|
paths.add(key);
|
|
30
60
|
for (const name of [
|
|
31
61
|
'id',
|
|
@@ -34,7 +64,7 @@ function configuredApps(root, supplied) {
|
|
|
34
64
|
'surfaceProfile',
|
|
35
65
|
'deliveryUnitKind'
|
|
36
66
|
])if (void 0 !== app[name] && 'string' != typeof app[name]) throw new Error(`${key}: ${name} must be a string`);
|
|
37
|
-
|
|
67
|
+
const allowedFields = {
|
|
38
68
|
kind: [
|
|
39
69
|
'shell',
|
|
40
70
|
'vertical'
|
|
@@ -48,7 +78,11 @@ function configuredApps(root, supplied) {
|
|
|
48
78
|
'microvertical',
|
|
49
79
|
'horizontal-remote'
|
|
50
80
|
]
|
|
51
|
-
}
|
|
81
|
+
};
|
|
82
|
+
for (const field of Object.keys(allowedFields)){
|
|
83
|
+
const value = app[field];
|
|
84
|
+
if (void 0 !== value && !allowedFields[field].includes(value)) throw new Error(`${key}: invalid ${field}`);
|
|
85
|
+
}
|
|
52
86
|
if (void 0 !== app.api) {
|
|
53
87
|
if (!app.api || 'object' != typeof app.api || Array.isArray(app.api)) throw new Error(`${key}: api must be an object`);
|
|
54
88
|
if (void 0 !== app.api.protocol && ![
|
|
@@ -136,12 +170,27 @@ function check(options, topology) {
|
|
|
136
170
|
`${base}/index.tsx`
|
|
137
171
|
].find((candidate)=>exists(candidate));
|
|
138
172
|
};
|
|
139
|
-
const
|
|
173
|
+
const clientExportFile = (appPath, exported)=>{
|
|
174
|
+
if ('string' != typeof exported || !exported.startsWith('./') || exported.split(/[\\/]/u).includes('..') || !/\.[cm]?[jt]sx?$/u.test(exported)) return;
|
|
175
|
+
const appDirectory = absolute(appPath);
|
|
176
|
+
const candidate = node_path.resolve(appDirectory, exported);
|
|
177
|
+
try {
|
|
178
|
+
const workspace = node_fs.realpathSync(root);
|
|
179
|
+
const owner = node_fs.realpathSync(appDirectory);
|
|
180
|
+
const real = node_fs.realpathSync(candidate);
|
|
181
|
+
if (!owner.startsWith(`${workspace}${node_path.sep}`) || !real.startsWith(`${owner}${node_path.sep}`) || !node_fs.statSync(real).isFile()) return;
|
|
182
|
+
} catch {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
return normalize(node_path.relative(root, candidate));
|
|
186
|
+
};
|
|
187
|
+
const composedModules = (file, appPath)=>{
|
|
140
188
|
const order = [];
|
|
141
189
|
const seen = new Set();
|
|
142
190
|
const queue = [
|
|
143
191
|
file
|
|
144
192
|
];
|
|
193
|
+
const owner = node_fs.realpathSync(absolute(appPath));
|
|
145
194
|
while(queue.length > 0 && order.length < 128){
|
|
146
195
|
const current = queue.shift();
|
|
147
196
|
if (seen.has(current)) continue;
|
|
@@ -152,16 +201,19 @@ function check(options, topology) {
|
|
|
152
201
|
for (const statement of ast.program.body){
|
|
153
202
|
const source = (__rspack_external__babel_types_6e8337dd.isImportDeclaration(statement) || __rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || __rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement)) && statement.source ? statement.source.value : void 0;
|
|
154
203
|
const resolved = void 0 === source ? void 0 : resolveRelative(current, source);
|
|
155
|
-
if (void 0 !== resolved && !seen.has(resolved))
|
|
204
|
+
if (void 0 !== resolved && !seen.has(resolved)) {
|
|
205
|
+
const real = node_fs.realpathSync(absolute(resolved));
|
|
206
|
+
if (real.startsWith(`${owner}${node_path.sep}`)) queue.push(resolved);
|
|
207
|
+
}
|
|
156
208
|
}
|
|
157
209
|
}
|
|
158
210
|
return order;
|
|
159
211
|
};
|
|
160
|
-
const moduleShape = (file, imports, calls, patterns = [], defaultExport,
|
|
212
|
+
const moduleShape = (file, imports, calls, patterns = [], defaultExport, composedAppPath)=>{
|
|
161
213
|
if (!exists(file)) return;
|
|
162
214
|
const ast = parseFile(file);
|
|
163
215
|
if (!ast) return;
|
|
164
|
-
const shapeFiles =
|
|
216
|
+
const shapeFiles = composedAppPath ? composedModules(file, composedAppPath) : [
|
|
165
217
|
file
|
|
166
218
|
];
|
|
167
219
|
const body = shapeFiles.flatMap((shapeFile)=>parseFile(shapeFile)?.program.body ?? []);
|
|
@@ -271,19 +323,25 @@ function check(options, topology) {
|
|
|
271
323
|
}
|
|
272
324
|
return;
|
|
273
325
|
}
|
|
274
|
-
if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in
|
|
326
|
+
if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in topology/reference-topology.json`);
|
|
275
327
|
const stem = app?.api?.stem ?? node_path.posix.basename(appPath);
|
|
276
328
|
const rpc = app?.api?.protocol === 'rpc';
|
|
277
329
|
const contract = `${appPath}/shared/${rpc ? 'rpc' : 'api'}.ts`;
|
|
278
|
-
const client = `${appPath}/src/api/${stem}-${rpc ? 'rpc-client' : 'client'}.ts`;
|
|
279
330
|
const entry = `${appPath}/api/index.ts`;
|
|
280
331
|
for (const file of [
|
|
281
332
|
entry,
|
|
282
|
-
contract
|
|
283
|
-
client
|
|
333
|
+
contract
|
|
284
334
|
])assert(exists(file), `${file}: required API surface is missing`);
|
|
335
|
+
const packageFile = `${appPath}/package.json`;
|
|
336
|
+
assert(exists(packageFile), `${packageFile}: required API package is missing`);
|
|
337
|
+
if (!exists(packageFile)) return;
|
|
338
|
+
const manifest = JSON.parse(read(packageFile));
|
|
339
|
+
assert(manifest.exports?.['./api'] === `./shared/${rpc ? 'rpc' : 'api'}.ts`, `${packageFile}: invalid ./api export`);
|
|
340
|
+
const clientKey = `./api/${rpc ? 'rpc-client' : 'client'}`;
|
|
341
|
+
const client = clientExportFile(appPath, manifest.exports?.[clientKey]);
|
|
342
|
+
assert(void 0 !== client, `${packageFile}: invalid API client export`);
|
|
343
|
+
assert(manifest.exports?.[`./api/${rpc ? 'client' : 'rpc-client'}`] === void 0, `${packageFile}: forbidden opposite protocol client export`);
|
|
285
344
|
noPath(`${appPath}/shared/${rpc ? 'api' : 'rpc'}.ts`, `must not emit a ${rpc ? 'REST' : 'RPC'} contract`);
|
|
286
|
-
noPath(`${appPath}/src/api/${stem}-${rpc ? 'client' : 'rpc-client'}.ts`, `must not emit a ${rpc ? 'REST' : 'RPC'} client`);
|
|
287
345
|
if (rpc) {
|
|
288
346
|
moduleShape(entry, [
|
|
289
347
|
[
|
|
@@ -349,21 +407,24 @@ function check(options, topology) {
|
|
|
349
407
|
const violation = microVerticalApiBaselineViolation(stem, absolute(contract), expectation);
|
|
350
408
|
if (violation) diagnostics.push(`${contract}: ${violation}`);
|
|
351
409
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
410
|
+
if (client) {
|
|
411
|
+
const relativeContract = node_path.posix.relative(node_path.posix.dirname(client), contract);
|
|
412
|
+
moduleShape(client, [
|
|
413
|
+
[
|
|
414
|
+
effect,
|
|
415
|
+
[
|
|
416
|
+
'Effect',
|
|
417
|
+
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
418
|
+
]
|
|
419
|
+
],
|
|
355
420
|
[
|
|
356
|
-
'
|
|
357
|
-
|
|
421
|
+
relativeContract.startsWith('.') ? relativeContract : `./${relativeContract}`,
|
|
422
|
+
[]
|
|
358
423
|
]
|
|
359
|
-
],
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
]
|
|
364
|
-
], [
|
|
365
|
-
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
366
|
-
], [], void 0, true);
|
|
424
|
+
], [
|
|
425
|
+
rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
|
|
426
|
+
], [], void 0, appPath);
|
|
427
|
+
}
|
|
367
428
|
moduleShape(`${appPath}/api/effect-api.ts`, [], [], [
|
|
368
429
|
[
|
|
369
430
|
/\bbackendFederationContract\b/u,
|
|
@@ -404,13 +465,6 @@ function check(options, topology) {
|
|
|
404
465
|
'must enable strictEffectApproach'
|
|
405
466
|
]
|
|
406
467
|
]);
|
|
407
|
-
const packageFile = `${appPath}/package.json`;
|
|
408
|
-
if (exists(packageFile)) {
|
|
409
|
-
const manifest = JSON.parse(read(packageFile));
|
|
410
|
-
assert(manifest.exports?.['./api'] === `./shared/${rpc ? 'rpc' : 'api'}.ts`, `${packageFile}: invalid ./api export`);
|
|
411
|
-
assert(manifest.exports?.[`./api/${rpc ? 'rpc-client' : 'client'}`] === `./src/api/${stem}-${rpc ? 'rpc-client' : 'client'}.ts`, `${packageFile}: invalid API client export`);
|
|
412
|
-
assert(manifest.exports?.[`./api/${rpc ? 'client' : 'rpc-client'}`] === void 0, `${packageFile}: forbidden opposite protocol client export`);
|
|
413
|
-
}
|
|
414
468
|
});
|
|
415
469
|
const shell = 'apps/shell-super-app';
|
|
416
470
|
const verticals = apps.filter((app)=>app.path.startsWith('verticals/') && apiApp(app));
|
|
@@ -20,7 +20,7 @@ const createStrictEffectApiBoundariesRule = ()=>({
|
|
|
20
20
|
});
|
|
21
21
|
reportProgramPattern(context, node, source, /@modern-js\/plugin-bff\/(?:server|hono-server)(?![\w-])/u, 'UltraModern API workspaces must not import Hono server helpers; use @modern-js/bff-effect/effect-edge and HttpApi.');
|
|
22
22
|
reportProgramPattern(context, node, source, /\bruntimeFramework\s*(?::|=)\s*['"]hono['"]/u, 'UltraModern API apps must use bff.runtimeFramework: "effect".');
|
|
23
|
-
reportProgramPattern(context, node, source, /(?:from|import)\s*['"][^'"]*(?:api\/effect|shared\/effect)[^'"]*['"]|shared-effect-api/u, 'Import API code from direct api/index.ts, shared/api.ts or src/api/* paths, not api/effect, shared/effect or shared Effect API packages.');
|
|
23
|
+
reportProgramPattern(context, node, source, /(?:from|import)\s*['"][^'"]*(?:api\/effect|shared\/effect)(?=\/|\.[cm]?[jt]sx?['"]|['"])[^'"]*['"]|shared-effect-api/u, 'Import API code from direct api/index.ts, shared/api.ts or src/api/* paths, not api/effect, shared/effect or shared Effect API packages.');
|
|
24
24
|
reportProgramPattern(context, node, source, /\bstrictEffectApproach\s*(?::|=)\s*false\b/u, 'UltraModern API apps must keep strictEffectApproach enabled.');
|
|
25
25
|
if (isApiEntryFile(filename)) {
|
|
26
26
|
const violation = strictEffectRuntimeTopologyViolation(source, createEffectApiImportResolver(filename));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
|
+
import traverse, { Hub, NodePath } from "@babel/traverse";
|
|
3
|
+
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
4
|
+
const consumerParserPlugins = (filePath)=>/\.[jt]sx$/u.test(filePath) ? [
|
|
5
|
+
"typescript",
|
|
6
|
+
'jsx'
|
|
7
|
+
] : [
|
|
8
|
+
"typescript"
|
|
9
|
+
];
|
|
10
|
+
class SourceSyntaxError extends Error {
|
|
11
|
+
}
|
|
12
|
+
class SourceValidationHub extends Hub {
|
|
13
|
+
buildError(_node, message) {
|
|
14
|
+
return new SourceSyntaxError(message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function parseSource(source, filename, syntaxMessage) {
|
|
18
|
+
try {
|
|
19
|
+
return parse(source, {
|
|
20
|
+
sourceType: 'module',
|
|
21
|
+
sourceFilename: filename,
|
|
22
|
+
plugins: consumerParserPlugins(filename)
|
|
23
|
+
});
|
|
24
|
+
} catch (cause) {
|
|
25
|
+
if ('object' == typeof cause && null !== cause && 'code' in cause && 'BABEL_PARSER_SYNTAX_ERROR' === cause.code && 'reasonCode' in cause && 'string' == typeof cause.reasonCode) throw new SourceSyntaxError(syntaxMessage ?? (cause instanceof Error ? cause.message : String(cause)));
|
|
26
|
+
throw cause;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function traverseSource(file, visitor) {
|
|
30
|
+
const program = NodePath.get({
|
|
31
|
+
hub: new SourceValidationHub(),
|
|
32
|
+
parentPath: void 0,
|
|
33
|
+
parent: file,
|
|
34
|
+
container: file,
|
|
35
|
+
key: 'program'
|
|
36
|
+
});
|
|
37
|
+
program.setContext();
|
|
38
|
+
traverse(file, visitor, program.scope, void 0, program);
|
|
39
|
+
}
|
|
40
|
+
function unwrapExpression(node, includeAssertions = false) {
|
|
41
|
+
while(__rspack_external__babel_types_6e8337dd.isTSAsExpression(node) || __rspack_external__babel_types_6e8337dd.isParenthesizedExpression(node) || __rspack_external__babel_types_6e8337dd.isTSSatisfiesExpression(node) || includeAssertions && (__rspack_external__babel_types_6e8337dd.isTSTypeAssertion(node) || __rspack_external__babel_types_6e8337dd.isTSNonNullExpression(node)))node = node.expression;
|
|
42
|
+
return node;
|
|
43
|
+
}
|
|
44
|
+
export { SourceSyntaxError, consumerParserPlugins, parseSource, traverseSource, unwrapExpression };
|
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
import node_fs from "node:fs";
|
|
2
2
|
import node_path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import traverse, { Hub, NodePath } from "@babel/traverse";
|
|
3
|
+
import { SourceSyntaxError, parseSource, traverseSource, unwrapExpression } from "./source-analysis.js";
|
|
5
4
|
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
6
5
|
const edge = '@modern-js/bff-effect/effect-edge';
|
|
7
6
|
const nodeRuntime = '@modern-js/bff-effect/effect';
|
|
8
|
-
const
|
|
9
|
-
const failure = 'Generated API entries must export defineEffectBff(...) or the
|
|
10
|
-
const
|
|
11
|
-
function invalidSource(message) {
|
|
12
|
-
const error = new Error(message);
|
|
13
|
-
invalidSources.add(error);
|
|
14
|
-
return error;
|
|
15
|
-
}
|
|
16
|
-
class SourceValidationHub extends Hub {
|
|
17
|
-
buildError(_node, message) {
|
|
18
|
-
return invalidSource(message);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
7
|
+
const assembly = '@modern-js/bff-effect/assembly';
|
|
8
|
+
const failure = 'Generated API entries must export defineEffectBff(...) or the native Effect BFF assembly helper with an explicitly composed handler Layer and an unshadowed executable root; entries must implement handlers through HttpApiBuilder.group.';
|
|
9
|
+
const MAX_API_SOURCE_MODULES = 256;
|
|
21
10
|
function createEffectApiImportResolver(filename) {
|
|
22
11
|
const directory = node_fs.realpathSync(node_path.dirname(filename));
|
|
23
12
|
const owner = node_fs.realpathSync(node_path.resolve(directory, '..'));
|
|
@@ -49,34 +38,14 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
49
38
|
const load = (input)=>{
|
|
50
39
|
const existing = modules.get(input.id);
|
|
51
40
|
if (existing) return existing;
|
|
52
|
-
if (modules.size >=
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
file = parse(input.source, {
|
|
56
|
-
sourceType: 'module',
|
|
57
|
-
sourceFilename: input.id,
|
|
58
|
-
plugins: [
|
|
59
|
-
"typescript"
|
|
60
|
-
]
|
|
61
|
-
});
|
|
62
|
-
} catch (cause) {
|
|
63
|
-
if ('object' == typeof cause && null !== cause && 'code' in cause && 'BABEL_PARSER_SYNTAX_ERROR' === cause.code && 'reasonCode' in cause && 'string' == typeof cause.reasonCode) throw invalidSource('Invalid API syntax');
|
|
64
|
-
throw cause;
|
|
65
|
-
}
|
|
41
|
+
if (modules.size >= MAX_API_SOURCE_MODULES || input.source.length > 1000000) throw new SourceSyntaxError('API source budget exceeded');
|
|
42
|
+
const file = parseSource(input.source, input.id, 'Invalid API syntax');
|
|
66
43
|
const module = {
|
|
67
44
|
...input,
|
|
68
45
|
file
|
|
69
46
|
};
|
|
70
47
|
modules.set(input.id, module);
|
|
71
|
-
|
|
72
|
-
hub: new SourceValidationHub(),
|
|
73
|
-
parentPath: void 0,
|
|
74
|
-
parent: file,
|
|
75
|
-
container: file,
|
|
76
|
-
key: 'program'
|
|
77
|
-
});
|
|
78
|
-
program.setContext();
|
|
79
|
-
traverse(file, {
|
|
48
|
+
traverseSource(file, {
|
|
80
49
|
enter (nodePath) {
|
|
81
50
|
paths.set(nodePath.node, nodePath);
|
|
82
51
|
owners.set(nodePath.node, module);
|
|
@@ -95,7 +64,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
95
64
|
if (binding) mutated.add(binding);
|
|
96
65
|
}
|
|
97
66
|
}
|
|
98
|
-
}
|
|
67
|
+
});
|
|
99
68
|
return module;
|
|
100
69
|
};
|
|
101
70
|
try {
|
|
@@ -104,10 +73,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
104
73
|
source,
|
|
105
74
|
resolveImport
|
|
106
75
|
});
|
|
107
|
-
const unwrap = (node)=>
|
|
108
|
-
while(__rspack_external__babel_types_6e8337dd.isParenthesizedExpression(node) || __rspack_external__babel_types_6e8337dd.isTSSatisfiesExpression(node) || __rspack_external__babel_types_6e8337dd.isTSAsExpression(node) || __rspack_external__babel_types_6e8337dd.isTSTypeAssertion(node) || __rspack_external__babel_types_6e8337dd.isTSNonNullExpression(node))node = node.expression;
|
|
109
|
-
return node;
|
|
110
|
-
};
|
|
76
|
+
const unwrap = (node)=>unwrapExpression(node, true);
|
|
111
77
|
const binding = (node)=>{
|
|
112
78
|
node = unwrap(node);
|
|
113
79
|
if (!__rspack_external__babel_types_6e8337dd.isIdentifier(node)) return;
|
|
@@ -236,7 +202,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
236
202
|
if (!__rspack_external__babel_types_6e8337dd.isBlockStatement(body)) return body;
|
|
237
203
|
const statements = body.body;
|
|
238
204
|
const last = statements.at(-1);
|
|
239
|
-
return statements.slice(0, -1).every((statement)=>__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement)) && __rspack_external__babel_types_6e8337dd.isReturnStatement(last) ? last.argument : void 0;
|
|
205
|
+
return statements.slice(0, -1).every((statement)=>__rspack_external__babel_types_6e8337dd.isVariableDeclaration(statement) || __rspack_external__babel_types_6e8337dd.isTSTypeAliasDeclaration(statement) || __rspack_external__babel_types_6e8337dd.isTSInterfaceDeclaration(statement)) && __rspack_external__babel_types_6e8337dd.isReturnStatement(last) ? last.argument : void 0;
|
|
240
206
|
};
|
|
241
207
|
const pipe = (node, check)=>__rspack_external__babel_types_6e8337dd.isMemberExpression(node.callee) && !node.callee.computed && __rspack_external__babel_types_6e8337dd.isIdentifier(node.callee.property, {
|
|
242
208
|
name: 'pipe'
|
|
@@ -325,7 +291,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
325
291
|
if (value?.name === 'defineEffectBff' && [
|
|
326
292
|
edge,
|
|
327
293
|
nodeRuntime
|
|
328
|
-
].includes(value.specifier) || value?.name === 'assembleEffectBffRuntime' &&
|
|
294
|
+
].includes(value.specifier) || value?.name === 'assembleEffectBffRuntime' && value.specifier === assembly) {
|
|
329
295
|
if (1 !== node.arguments.length) return false;
|
|
330
296
|
const values = properties(node.arguments[0]);
|
|
331
297
|
if (!values) return false;
|
|
@@ -348,7 +314,7 @@ function strictEffectRuntimeTopologyViolation(source, resolveImport) {
|
|
|
348
314
|
const root = entry.file.program.body.find((statement)=>__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(statement));
|
|
349
315
|
return root && runtime(root.declaration) ? void 0 : failure;
|
|
350
316
|
} catch (cause) {
|
|
351
|
-
if (
|
|
317
|
+
if (cause instanceof SourceSyntaxError) return failure;
|
|
352
318
|
const message = 'object' == typeof cause && null !== cause && 'message' in cause && 'string' == typeof cause.message ? cause.message : String(cause);
|
|
353
319
|
throw new Error(`Strict Effect API analysis failed: ${message}`, {
|
|
354
320
|
cause
|
|
@@ -1,55 +1,25 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import node_fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import node_path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import traverse, { Hub, NodePath } from "@babel/traverse";
|
|
5
|
+
import { exports as external_resolve_exports_exports } from "resolve.exports";
|
|
6
6
|
import { baselinePublicIdentityIsExact } from "./microvertical-api-owner.js";
|
|
7
|
+
import { SourceSyntaxError, parseSource, traverseSource, unwrapExpression } from "./source-analysis.js";
|
|
7
8
|
import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
|
|
8
|
-
const consumerParserPlugins = (filePath)=>filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? [
|
|
9
|
-
"typescript",
|
|
10
|
-
'jsx'
|
|
11
|
-
] : [
|
|
12
|
-
"typescript"
|
|
13
|
-
];
|
|
14
|
-
class ConsumerSyntaxError extends Error {
|
|
15
|
-
}
|
|
16
|
-
class ConsumerValidationHub extends Hub {
|
|
17
|
-
buildError(_node, message) {
|
|
18
|
-
return new ConsumerSyntaxError(message);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
9
|
function parseConsumer(filePath) {
|
|
22
10
|
if (node_fs.statSync(filePath).size > 1000000) throw new Error(`${filePath}: consumer source exceeds 1 MB analysis budget`);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
file = parse(node_fs.readFileSync(filePath, 'utf8'), {
|
|
26
|
-
sourceType: 'module',
|
|
27
|
-
sourceFilename: filePath,
|
|
28
|
-
plugins: consumerParserPlugins(filePath)
|
|
29
|
-
});
|
|
30
|
-
} catch (error) {
|
|
31
|
-
if ('object' == typeof error && null !== error && 'code' in error && 'BABEL_PARSER_SYNTAX_ERROR' === error.code) throw new ConsumerSyntaxError(error instanceof Error ? error.message : String(error));
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
const program = NodePath.get({
|
|
35
|
-
hub: new ConsumerValidationHub(),
|
|
36
|
-
parentPath: void 0,
|
|
37
|
-
parent: file,
|
|
38
|
-
container: file,
|
|
39
|
-
key: 'program'
|
|
40
|
-
});
|
|
41
|
-
program.setContext();
|
|
42
|
-
traverse(file, {
|
|
11
|
+
const file = parseSource(node_fs.readFileSync(filePath, 'utf8'), filePath);
|
|
12
|
+
traverseSource(file, {
|
|
43
13
|
AssignmentExpression (p) {
|
|
44
|
-
throw new
|
|
14
|
+
throw new SourceSyntaxError(`contract bindings must be immutable at ${p.node.start}`);
|
|
45
15
|
},
|
|
46
16
|
UpdateExpression (p) {
|
|
47
|
-
throw new
|
|
17
|
+
throw new SourceSyntaxError(`contract bindings must be immutable at ${p.node.start}`);
|
|
48
18
|
},
|
|
49
19
|
TSModuleDeclaration () {
|
|
50
|
-
throw new
|
|
20
|
+
throw new SourceSyntaxError('contract bindings must not be merged with namespaces');
|
|
51
21
|
}
|
|
52
|
-
}
|
|
22
|
+
});
|
|
53
23
|
return file;
|
|
54
24
|
}
|
|
55
25
|
const camelCaseStem = (stem)=>stem.replaceAll(/-([a-z0-9])/gu, (_match, letter)=>letter.toUpperCase());
|
|
@@ -74,11 +44,6 @@ const accessPath = (node)=>{
|
|
|
74
44
|
}
|
|
75
45
|
};
|
|
76
46
|
const isAccessPath = (node, expected)=>accessPath(node)?.join('.') === expected.join('.');
|
|
77
|
-
const unwrapExpression = (expression)=>{
|
|
78
|
-
let current = expression;
|
|
79
|
-
while(__rspack_external__babel_types_6e8337dd.isTSAsExpression(current) || __rspack_external__babel_types_6e8337dd.isParenthesizedExpression(current) || __rspack_external__babel_types_6e8337dd.isTSSatisfiesExpression(current))current = current.expression;
|
|
80
|
-
return current;
|
|
81
|
-
};
|
|
82
47
|
const stringLiteral = (expression)=>{
|
|
83
48
|
if (null == expression) return;
|
|
84
49
|
const unwrapped = unwrapExpression(expression);
|
|
@@ -119,16 +84,81 @@ const exportedConst = (sourceFile, name)=>{
|
|
|
119
84
|
}))) return localConst(sourceFile, name);
|
|
120
85
|
}
|
|
121
86
|
};
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
87
|
+
const inside = (file, boundary)=>file === boundary || file.startsWith(`${boundary}${node_path.sep}`);
|
|
88
|
+
const containingWorkspace = (file)=>{
|
|
89
|
+
for(let directory = node_path.dirname(file);; directory = node_path.dirname(directory)){
|
|
90
|
+
if (node_fs.existsSync(node_path.join(directory, 'pnpm-workspace.yaml'))) return node_fs.realpathSync(directory);
|
|
91
|
+
if (directory === node_path.dirname(directory)) return;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const sourceBoundary = (file, workspace)=>{
|
|
95
|
+
for(let directory = node_path.dirname(file);; directory = node_path.dirname(directory)){
|
|
96
|
+
if (node_fs.existsSync(node_path.join(directory, 'package.json')) && directory !== workspace) return node_fs.realpathSync(directory);
|
|
97
|
+
if (directory === workspace || directory === node_path.dirname(directory)) return node_fs.realpathSync(node_path.dirname(file));
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const sourceFileAt = (target, boundary)=>{
|
|
101
|
+
const stem = target.replace(/\.(?:[cm]?[jt]sx?)$/u, '');
|
|
126
102
|
for (const candidate of [
|
|
127
103
|
`${stem}.ts`,
|
|
128
104
|
`${stem}.tsx`,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
105
|
+
`${stem}.mts`,
|
|
106
|
+
`${stem}.cts`,
|
|
107
|
+
target,
|
|
108
|
+
node_path.join(target, 'index.ts'),
|
|
109
|
+
node_path.join(target, 'index.tsx')
|
|
110
|
+
])try {
|
|
111
|
+
const real = node_fs.realpathSync(candidate);
|
|
112
|
+
if (inside(real, boundary) && node_fs.statSync(real).isFile()) return real;
|
|
113
|
+
} catch {}
|
|
114
|
+
};
|
|
115
|
+
const packageEntry = (specifier)=>{
|
|
116
|
+
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#')) return;
|
|
117
|
+
const parts = specifier.split('/');
|
|
118
|
+
const count = specifier.startsWith('@') ? 2 : 1;
|
|
119
|
+
if (parts.length < count || parts.slice(0, count).some((part)=>!part)) return;
|
|
120
|
+
return {
|
|
121
|
+
name: parts.slice(0, count).join('/'),
|
|
122
|
+
exportKey: parts.length === count ? '.' : `./${parts.slice(count).join('/')}`
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const resolveModulePath = (module, specifier, workspace)=>{
|
|
126
|
+
if (/^\.\.?\//u.test(specifier)) {
|
|
127
|
+
const file = sourceFileAt(node_path.resolve(node_path.dirname(module.path), specifier), module.boundary);
|
|
128
|
+
return void 0 === file ? void 0 : {
|
|
129
|
+
path: file,
|
|
130
|
+
boundary: module.boundary
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const entry = packageEntry(specifier);
|
|
134
|
+
if (!entry || !workspace) return;
|
|
135
|
+
const paths = createRequire(module.path).resolve.paths(entry.name) ?? [];
|
|
136
|
+
for (const lookup of paths){
|
|
137
|
+
const directory = node_path.join(lookup, entry.name);
|
|
138
|
+
const manifest = node_path.join(directory, 'package.json');
|
|
139
|
+
if (!node_fs.existsSync(manifest)) continue;
|
|
140
|
+
const boundary = node_fs.realpathSync(directory);
|
|
141
|
+
if (!inside(boundary, workspace) || boundary.split(node_path.sep).includes('node_modules')) break;
|
|
142
|
+
const pkg = JSON.parse(node_fs.readFileSync(manifest, 'utf8'));
|
|
143
|
+
if (pkg.name !== entry.name || void 0 === pkg.exports) break;
|
|
144
|
+
let target;
|
|
145
|
+
try {
|
|
146
|
+
const targets = external_resolve_exports_exports(pkg, entry.exportKey, {
|
|
147
|
+
conditions: [
|
|
148
|
+
'modern:source'
|
|
149
|
+
]
|
|
150
|
+
});
|
|
151
|
+
target = Array.isArray(targets) ? targets[0] : void 0;
|
|
152
|
+
} catch {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
if (!target || !target.startsWith('./')) break;
|
|
156
|
+
const file = sourceFileAt(node_path.resolve(boundary, target), boundary);
|
|
157
|
+
return void 0 === file ? void 0 : {
|
|
158
|
+
path: file,
|
|
159
|
+
boundary
|
|
160
|
+
};
|
|
161
|
+
}
|
|
132
162
|
};
|
|
133
163
|
const createModuleReader = (root)=>{
|
|
134
164
|
const modules = new Map([
|
|
@@ -137,14 +167,15 @@ const createModuleReader = (root)=>{
|
|
|
137
167
|
root
|
|
138
168
|
]
|
|
139
169
|
]);
|
|
140
|
-
return (filePath)=>{
|
|
170
|
+
return (filePath, boundary)=>{
|
|
141
171
|
if (modules.has(filePath)) return modules.get(filePath);
|
|
142
172
|
if (modules.size >= 256) return;
|
|
143
173
|
let module;
|
|
144
174
|
try {
|
|
145
175
|
module = {
|
|
146
176
|
file: parseConsumer(filePath),
|
|
147
|
-
path: filePath
|
|
177
|
+
path: filePath,
|
|
178
|
+
boundary
|
|
148
179
|
};
|
|
149
180
|
} catch {
|
|
150
181
|
module = void 0;
|
|
@@ -174,7 +205,7 @@ const reexportedBinding = (module, name)=>{
|
|
|
174
205
|
if (!__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(statement) || 'type' === statement.exportKind) continue;
|
|
175
206
|
const specifier = statement.specifiers.find((value)=>__rspack_external__babel_types_6e8337dd.isExportSpecifier(value) && 'type' !== value.exportKind && propertyName(value.exported) === name);
|
|
176
207
|
if (void 0 !== specifier && __rspack_external__babel_types_6e8337dd.isExportSpecifier(specifier)) return {
|
|
177
|
-
local: specifier.local.name,
|
|
208
|
+
local: __rspack_external__babel_types_6e8337dd.isIdentifier(specifier.local) ? specifier.local.name : specifier.local.value,
|
|
178
209
|
...statement.source ? {
|
|
179
210
|
specifier: statement.source.value
|
|
180
211
|
} : {}
|
|
@@ -189,7 +220,7 @@ const defaultExport = (module)=>{
|
|
|
189
220
|
return __rspack_external__babel_types_6e8337dd.isExpression(declaration) ? declaration : void 0;
|
|
190
221
|
}
|
|
191
222
|
};
|
|
192
|
-
const resolveBinding = (module, name, scope, readModule, seen)=>{
|
|
223
|
+
const resolveBinding = (module, name, scope, readModule, workspace, seen)=>{
|
|
193
224
|
const key = `${scope}:${module.path}#${name}`;
|
|
194
225
|
if (seen.has(key) || seen.size >= 512) return;
|
|
195
226
|
seen.add(key);
|
|
@@ -199,9 +230,9 @@ const resolveBinding = (module, name, scope, readModule, seen)=>{
|
|
|
199
230
|
module
|
|
200
231
|
};
|
|
201
232
|
const throughModule = (specifier, imported)=>{
|
|
202
|
-
const
|
|
203
|
-
const target =
|
|
204
|
-
return void 0 === target ? void 0 : resolveBinding(target, imported, 'export', readModule, seen);
|
|
233
|
+
const resolved = resolveModulePath(module, specifier, workspace);
|
|
234
|
+
const target = resolved && readModule(resolved.path, resolved.boundary);
|
|
235
|
+
return void 0 === target ? void 0 : resolveBinding(target, imported, 'export', readModule, workspace, seen);
|
|
205
236
|
};
|
|
206
237
|
if ('local' === scope) {
|
|
207
238
|
const imported = importedBinding(module, name);
|
|
@@ -209,14 +240,14 @@ const resolveBinding = (module, name, scope, readModule, seen)=>{
|
|
|
209
240
|
}
|
|
210
241
|
if ('default' === name) {
|
|
211
242
|
const exported = defaultExport(module);
|
|
212
|
-
if ('string' == typeof exported) return resolveBinding(module, exported, 'local', readModule, seen);
|
|
243
|
+
if ('string' == typeof exported) return resolveBinding(module, exported, 'local', readModule, workspace, seen);
|
|
213
244
|
return void 0 === exported ? void 0 : {
|
|
214
245
|
expression: exported,
|
|
215
246
|
module
|
|
216
247
|
};
|
|
217
248
|
}
|
|
218
249
|
const reexported = reexportedBinding(module, name);
|
|
219
|
-
if (void 0 !== reexported) return void 0 === reexported.specifier ? resolveBinding(module, reexported.local, 'local', readModule, seen) : throughModule(reexported.specifier, reexported.local);
|
|
250
|
+
if (void 0 !== reexported) return void 0 === reexported.specifier ? resolveBinding(module, reexported.local, 'local', readModule, workspace, seen) : throughModule(reexported.specifier, reexported.local);
|
|
220
251
|
for (const statement of module.file.program.body){
|
|
221
252
|
if (!__rspack_external__babel_types_6e8337dd.isExportAllDeclaration(statement) || 'type' === statement.exportKind) continue;
|
|
222
253
|
const resolved = throughModule(statement.source.value, name);
|
|
@@ -370,6 +401,7 @@ const nonEndpointCombinators = {
|
|
|
370
401
|
};
|
|
371
402
|
const reachableEndpoints = (rootModule, declaration)=>{
|
|
372
403
|
const readModule = createModuleReader(rootModule);
|
|
404
|
+
const workspace = containingWorkspace(rootModule.path);
|
|
373
405
|
const active = new Set();
|
|
374
406
|
const endpoints = [];
|
|
375
407
|
const identities = new Set();
|
|
@@ -391,7 +423,7 @@ const reachableEndpoints = (rootModule, declaration)=>{
|
|
|
391
423
|
active.add(node);
|
|
392
424
|
try {
|
|
393
425
|
if (__rspack_external__babel_types_6e8337dd.isIdentifier(node)) {
|
|
394
|
-
const resolved = resolveBinding(module, node.name, 'local', readModule, new Set());
|
|
426
|
+
const resolved = resolveBinding(module, node.name, 'local', readModule, workspace, new Set());
|
|
395
427
|
return void 0 !== resolved && visit(resolved.expression, resolved.module, kind, group);
|
|
396
428
|
}
|
|
397
429
|
if ('endpoint' === kind) {
|
|
@@ -608,15 +640,17 @@ const configuredMicroVerticalApiStem = (verticalPath, verticals)=>{
|
|
|
608
640
|
};
|
|
609
641
|
const microVerticalApiBaselineViolation = (stem, filePath, expectation)=>{
|
|
610
642
|
try {
|
|
611
|
-
const
|
|
643
|
+
const realPath = node_fs.realpathSync(filePath);
|
|
644
|
+
const sourceFile = parseConsumer(realPath);
|
|
612
645
|
if (!baselinePublicIdentityIsExact(filePath, expectation)) return 'MicroVertical baseline imports must resolve the exact framework owner public export and schema identity';
|
|
613
646
|
return validateParsedContract({
|
|
614
647
|
file: sourceFile,
|
|
615
|
-
path:
|
|
648
|
+
path: realPath,
|
|
649
|
+
boundary: sourceBoundary(realPath, containingWorkspace(realPath))
|
|
616
650
|
}, stem, expectation);
|
|
617
651
|
} catch (error) {
|
|
618
|
-
if (error instanceof
|
|
652
|
+
if (error instanceof SourceSyntaxError) return `MicroVertical root contract must be valid TypeScript syntax (${error.message})`;
|
|
619
653
|
throw error;
|
|
620
654
|
}
|
|
621
655
|
};
|
|
622
|
-
export { configuredMicroVerticalApiStem,
|
|
656
|
+
export { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation };
|