@bleedingdev/modern-js-code-tools 3.9.0-ultramodern.2 → 3.9.0-ultramodern.20

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.
Files changed (42) hide show
  1. package/bin/modern-api-check-files.mjs +7 -0
  2. package/bin/modern-api-check.mjs +4 -0
  3. package/bin/modern-i18n-check.mjs +4 -0
  4. package/dist/cjs/cli/microvertical-api-check.cjs +100 -0
  5. package/dist/cjs/cli/oxlint.cjs +9 -5
  6. package/dist/cjs/cli/workspace-source-check.cjs +90 -17
  7. package/dist/cjs/microvertical-api-baseline.cjs +505 -0
  8. package/dist/cjs/microvertical-api-boundary.cjs +586 -0
  9. package/dist/cjs/microvertical-api-owner.cjs +217 -0
  10. package/dist/cjs/module-graph.cjs +321 -0
  11. package/dist/cjs/oxlint-plugin/rules/strict-effect-api-boundaries.cjs +8 -6
  12. package/dist/cjs/source-analysis.cjs +104 -0
  13. package/dist/cjs/strict-effect-runtime.cjs +376 -0
  14. package/dist/esm/cli/microvertical-api-check.js +52 -0
  15. package/dist/esm/cli/oxlint.js +10 -6
  16. package/dist/esm/cli/workspace-source-check.js +86 -13
  17. package/dist/esm/microvertical-api-baseline.js +464 -0
  18. package/dist/esm/microvertical-api-boundary.js +525 -0
  19. package/dist/esm/microvertical-api-owner.js +162 -0
  20. package/dist/esm/module-graph.js +260 -0
  21. package/dist/esm/oxlint-plugin/rules/strict-effect-api-boundaries.js +8 -6
  22. package/dist/esm/source-analysis.js +44 -0
  23. package/dist/esm/strict-effect-runtime.js +324 -0
  24. package/dist/esm-node/cli/microvertical-api-check.js +53 -0
  25. package/dist/esm-node/cli/oxlint.js +10 -6
  26. package/dist/esm-node/cli/workspace-source-check.js +86 -13
  27. package/dist/esm-node/microvertical-api-baseline.js +465 -0
  28. package/dist/esm-node/microvertical-api-boundary.js +526 -0
  29. package/dist/esm-node/microvertical-api-owner.js +163 -0
  30. package/dist/esm-node/module-graph.js +261 -0
  31. package/dist/esm-node/oxlint-plugin/rules/strict-effect-api-boundaries.js +8 -6
  32. package/dist/esm-node/source-analysis.js +45 -0
  33. package/dist/esm-node/strict-effect-runtime.js +325 -0
  34. package/dist/types/cli/microvertical-api-check.d.ts +1 -0
  35. package/dist/types/cli/workspace-source-check.d.ts +1 -1
  36. package/dist/types/microvertical-api-baseline.d.ts +23 -0
  37. package/dist/types/microvertical-api-boundary.d.ts +46 -0
  38. package/dist/types/microvertical-api-owner.d.ts +6 -0
  39. package/dist/types/module-graph.d.ts +52 -0
  40. package/dist/types/source-analysis.d.ts +10 -0
  41. package/dist/types/strict-effect-runtime.d.ts +10 -0
  42. package/package.json +29 -6
@@ -0,0 +1,525 @@
1
+ import node_fs from "node:fs";
2
+ import node_path from "node:path";
3
+ import { parse } from "@babel/parser";
4
+ import { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation } from "./microvertical-api-baseline.js";
5
+ import { baselinePackage, resolveBaselinePackageDirectory } from "./microvertical-api-owner.js";
6
+ import { createModuleGraph } from "./module-graph.js";
7
+ import { SourceSyntaxError, consumerParserPlugins } from "./source-analysis.js";
8
+ import { createEffectApiImportResolver, strictEffectRuntimeTopologyViolation } from "./strict-effect-runtime.js";
9
+ import * as __rspack_external__babel_types_6e8337dd from "@babel/types";
10
+ const ignored = new Set([
11
+ '.git',
12
+ '.modern',
13
+ '.modernjs',
14
+ '.output',
15
+ 'coverage',
16
+ 'dist',
17
+ 'node_modules',
18
+ 'repos'
19
+ ]);
20
+ const normalize = (file)=>file.split(node_path.sep).join('/').replace(/^\.\//u, '');
21
+ const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
22
+ const apiApp = (app)=>'vertical' === app.kind && 'ui-only' !== app.surfaceProfile && 'horizontal-remote' !== app.deliveryUnitKind;
23
+ function configuredApps(root, supplied) {
24
+ const topology = supplied ? void 0 : JSON.parse(node_fs.readFileSync(node_path.join(root, 'topology/reference-topology.json'), 'utf8'));
25
+ 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');
26
+ const entries = topology?.shell ? [
27
+ topology.shell,
28
+ ...topology.shells ?? [],
29
+ ...topology.verticals ?? []
30
+ ] : void 0;
31
+ const source = supplied ?? entries?.map((entry)=>{
32
+ 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');
33
+ const packageFile = node_path.join(root, entry.path, 'package.json');
34
+ const manifest = JSON.parse(node_fs.readFileSync(packageFile, 'utf8'));
35
+ 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`);
36
+ const api = entry.api;
37
+ const stem = api?.stem ?? api?.readiness?.endpoint?.match(/^\/([a-z0-9]+(?:-[a-z0-9]+)*)\/readiness$/u)?.[1];
38
+ return {
39
+ id: entry.id,
40
+ kind: entry.kind,
41
+ path: entry.path,
42
+ package: manifest.name,
43
+ surfaceProfile: entry.surfaceProfile,
44
+ deliveryUnitKind: entry.deliveryUnitKind,
45
+ ...void 0 === api ? {} : {
46
+ api: {
47
+ stem,
48
+ prefix: api.bff?.prefix,
49
+ protocol: api.protocol
50
+ }
51
+ }
52
+ };
53
+ });
54
+ if (!Array.isArray(source)) throw new Error('topology/reference-topology.json: shell and verticals must describe apps');
55
+ const paths = new Set();
56
+ for (const app of source){
57
+ 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');
58
+ const key = normalize(app.path);
59
+ if (paths.has(key)) throw new Error(`topology/reference-topology.json: duplicate app path ${key}`);
60
+ paths.add(key);
61
+ for (const name of [
62
+ 'id',
63
+ 'kind',
64
+ 'package',
65
+ 'surfaceProfile',
66
+ 'deliveryUnitKind'
67
+ ])if (void 0 !== app[name] && 'string' != typeof app[name]) throw new Error(`${key}: ${name} must be a string`);
68
+ const allowedFields = {
69
+ kind: [
70
+ 'shell',
71
+ 'vertical'
72
+ ],
73
+ surfaceProfile: [
74
+ 'full-stack',
75
+ 'api-only',
76
+ 'ui-only'
77
+ ],
78
+ deliveryUnitKind: [
79
+ 'microvertical',
80
+ 'horizontal-remote'
81
+ ]
82
+ };
83
+ for (const field of Object.keys(allowedFields)){
84
+ const value = app[field];
85
+ if (void 0 !== value && !allowedFields[field].includes(value)) throw new Error(`${key}: invalid ${field}`);
86
+ }
87
+ if (void 0 !== app.api) {
88
+ if (!app.api || 'object' != typeof app.api || Array.isArray(app.api)) throw new Error(`${key}: api must be an object`);
89
+ if (void 0 !== app.api.protocol && ![
90
+ 'rest',
91
+ 'rpc'
92
+ ].includes(app.api.protocol)) throw new Error(`${key}: api.protocol must be rest or rpc`);
93
+ if (void 0 !== app.api.stem && ('string' != typeof app.api.stem || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(app.api.stem))) throw new Error(`${key}: invalid api.stem`);
94
+ if (void 0 !== app.api.prefix && ('string' != typeof app.api.prefix || !/^\/[^\s?#]*$/u.test(app.api.prefix))) throw new Error(`${key}: invalid api.prefix`);
95
+ for (const field of [
96
+ 'additionalPaths',
97
+ 'operationPaths'
98
+ ]){
99
+ const value = app.api[field];
100
+ if (void 0 !== value && (!value || 'object' != typeof value || Array.isArray(value) || Object.values(value).some((entry)=>'string' != typeof entry))) throw new Error(`${key}: api.${field} must map names to paths`);
101
+ }
102
+ }
103
+ }
104
+ return source;
105
+ }
106
+ function check(options, topology) {
107
+ const diagnostics = [];
108
+ const toolErrors = [];
109
+ let topologyFilesAnalyzed = 0;
110
+ const root = node_path.resolve(options.workspaceRoot);
111
+ const absolute = (file)=>node_path.join(root, file);
112
+ const exists = (file)=>node_fs.existsSync(absolute(file));
113
+ const read = (file)=>node_fs.readFileSync(absolute(file), 'utf8');
114
+ const assert = (value, message)=>{
115
+ if (!value) diagnostics.push(message);
116
+ };
117
+ const noPath = (file, reason)=>assert(!exists(file), `${file}: ${reason}`);
118
+ const guarded = (file, work)=>{
119
+ try {
120
+ work();
121
+ } catch (error) {
122
+ toolErrors.push(`${file}: ${errorMessage(error)}`);
123
+ }
124
+ };
125
+ try {
126
+ const apps = configuredApps(root, options.configuredApps);
127
+ const byPath = new Map(apps.map((app)=>[
128
+ normalize(app.path),
129
+ app
130
+ ]));
131
+ const directories = (base)=>exists(base) ? node_fs.readdirSync(absolute(base), {
132
+ withFileTypes: true
133
+ }).filter((entry)=>entry.isDirectory() && !ignored.has(entry.name)).map((entry)=>`${base}/${entry.name}`) : [];
134
+ const walk = (directory)=>exists(directory) ? node_fs.readdirSync(absolute(directory), {
135
+ withFileTypes: true
136
+ }).flatMap((entry)=>{
137
+ if (ignored.has(entry.name) || 'dist-cloudflare' === entry.name && byPath.has(directory)) return [];
138
+ const file = `${directory}/${entry.name}`;
139
+ return entry.isDirectory() ? walk(file) : entry.isFile() ? [
140
+ file
141
+ ] : [];
142
+ }) : [];
143
+ const parsed = new Map();
144
+ const parseFile = (file)=>{
145
+ if (parsed.has(file)) return parsed.get(file);
146
+ if (node_fs.statSync(absolute(file)).size > 1000000) throw new Error('consumer source exceeds 1 MB analysis budget');
147
+ try {
148
+ const ast = parse(read(file), {
149
+ sourceType: 'module',
150
+ sourceFilename: file,
151
+ plugins: consumerParserPlugins(file)
152
+ });
153
+ parsed.set(file, ast);
154
+ return ast;
155
+ } catch (error) {
156
+ if ('object' == typeof error && null !== error && 'code' in error && 'BABEL_PARSER_SYNTAX_ERROR' === error.code) {
157
+ diagnostics.push(`${file}: invalid source syntax (${errorMessage(error)})`);
158
+ parsed.set(file, void 0);
159
+ return;
160
+ }
161
+ throw error;
162
+ }
163
+ };
164
+ const resolveRelative = (fromFile, specifier)=>{
165
+ if (!/^\.\.?\//u.test(specifier)) return;
166
+ const base = node_path.posix.join(node_path.posix.dirname(fromFile), specifier).replace(/\.(?:[cm]?[jt]sx?)$/u, '');
167
+ return [
168
+ `${base}.ts`,
169
+ `${base}.tsx`,
170
+ `${base}/index.ts`,
171
+ `${base}/index.tsx`
172
+ ].find((candidate)=>exists(candidate));
173
+ };
174
+ const clientExportFile = (appPath, exported)=>{
175
+ if ('string' != typeof exported || !exported.startsWith('./') || exported.split(/[\\/]/u).includes('..') || !/\.[cm]?[jt]sx?$/u.test(exported)) return;
176
+ const appDirectory = absolute(appPath);
177
+ const candidate = node_path.resolve(appDirectory, exported);
178
+ try {
179
+ const workspace = node_fs.realpathSync(root);
180
+ const owner = node_fs.realpathSync(appDirectory);
181
+ const real = node_fs.realpathSync(candidate);
182
+ if (!owner.startsWith(`${workspace}${node_path.sep}`) || !real.startsWith(`${owner}${node_path.sep}`) || !node_fs.statSync(real).isFile()) return;
183
+ } catch {
184
+ return;
185
+ }
186
+ return normalize(node_path.relative(root, candidate));
187
+ };
188
+ const composedModules = (file, appPath)=>{
189
+ const order = [];
190
+ const seen = new Set();
191
+ const queue = [
192
+ file
193
+ ];
194
+ const owner = node_fs.realpathSync(absolute(appPath));
195
+ while(queue.length > 0 && order.length < 128){
196
+ const current = queue.shift();
197
+ if (seen.has(current)) continue;
198
+ seen.add(current);
199
+ order.push(current);
200
+ const ast = parseFile(current);
201
+ if (!ast) continue;
202
+ for (const statement of ast.program.body){
203
+ 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;
204
+ const resolved = void 0 === source ? void 0 : resolveRelative(current, source);
205
+ if (void 0 !== resolved && !seen.has(resolved)) {
206
+ const real = node_fs.realpathSync(absolute(resolved));
207
+ if (real.startsWith(`${owner}${node_path.sep}`)) queue.push(resolved);
208
+ }
209
+ }
210
+ }
211
+ return order;
212
+ };
213
+ const moduleShape = (file, imports, calls, patterns = [], defaultExport, composedAppPath)=>{
214
+ if (!exists(file)) return;
215
+ const ast = parseFile(file);
216
+ if (!ast) return;
217
+ const shapeFiles = composedAppPath ? composedModules(file, composedAppPath) : [
218
+ file
219
+ ];
220
+ const body = shapeFiles.flatMap((shapeFile)=>parseFile(shapeFile)?.program.body ?? []);
221
+ for (const [specifier, names] of imports){
222
+ const target = resolveRelative(file, specifier);
223
+ const matchesSpecifier = (item, from)=>void 0 === target ? item.source.value === specifier : resolveRelative(from, item.source.value) === target;
224
+ const imported = shapeFiles.flatMap((shapeFile)=>(parseFile(shapeFile)?.program.body ?? []).filter((item)=>__rspack_external__babel_types_6e8337dd.isImportDeclaration(item) && 'type' !== item.importKind && matchesSpecifier(item, shapeFile)));
225
+ assert(imported.length, `${file}: must import from ${specifier}`);
226
+ for (const name of names)assert(imported.some((item)=>__rspack_external__babel_types_6e8337dd.isImportDeclaration(item) && item.specifiers.some((value)=>__rspack_external__babel_types_6e8337dd.isImportSpecifier(value) && 'type' !== value.importKind && __rspack_external__babel_types_6e8337dd.isIdentifier(value.imported, {
227
+ name
228
+ }) && __rspack_external__babel_types_6e8337dd.isIdentifier(value.local, {
229
+ name
230
+ }))), `${file}: must import ${name} from ${specifier}`);
231
+ }
232
+ const found = new Set();
233
+ const collectCalls = (node)=>{
234
+ if (!__rspack_external__babel_types_6e8337dd.isCallExpression(node)) return;
235
+ if (__rspack_external__babel_types_6e8337dd.isIdentifier(node.callee)) found.add(node.callee.name);
236
+ if (__rspack_external__babel_types_6e8337dd.isMemberExpression(node.callee) && !node.callee.computed && __rspack_external__babel_types_6e8337dd.isIdentifier(node.callee.object) && __rspack_external__babel_types_6e8337dd.isIdentifier(node.callee.property)) found.add(`${node.callee.object.name}.${node.callee.property.name}`);
237
+ };
238
+ for (const shapeFile of shapeFiles){
239
+ const shapeAst = parseFile(shapeFile);
240
+ if (shapeAst) __rspack_external__babel_types_6e8337dd.traverseFast(shapeAst, collectCalls);
241
+ }
242
+ for (const call of calls)assert(found.has(call), `${file}: must call ${call}(...)`);
243
+ const source = read(file).replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/gu, '');
244
+ for (const [pattern, reason] of patterns)assert(pattern.test(source), `${file}: ${reason}`);
245
+ if (defaultExport) assert(body.some((item)=>__rspack_external__babel_types_6e8337dd.isExportDefaultDeclaration(item) && __rspack_external__babel_types_6e8337dd.isIdentifier(item.declaration, {
246
+ name: defaultExport
247
+ })), `${file}: must default-export ${defaultExport}`);
248
+ };
249
+ const effect = '@modern-js/bff-effect/effect-client';
250
+ const appDirectories = [
251
+ ...new Set([
252
+ ...directories('apps'),
253
+ ...directories('verticals'),
254
+ ...byPath.keys()
255
+ ])
256
+ ];
257
+ for (const appPath of appDirectories)for (const suffix of [
258
+ 'api/effect',
259
+ 'api/lambda',
260
+ 'shared/effect',
261
+ 'src/effect'
262
+ ])noPath(`${appPath}/${suffix}`, 'legacy nested Effect paths are forbidden; use api/index.ts, shared/api.ts and src/api/*');
263
+ const sourceFiles = [
264
+ ...new Set([
265
+ 'apps',
266
+ 'verticals',
267
+ 'packages',
268
+ ...byPath.keys()
269
+ ].flatMap(walk))
270
+ ].filter((file)=>/\.(?:[cm]?[jt]sx?|json|md)$/u.test(file));
271
+ for (const file of sourceFiles)guarded(file, ()=>{
272
+ const source = read(file);
273
+ const patterns = [
274
+ [
275
+ /@modern-js\/plugin-bff\/(?:server|hono-server)(?![\w-])/u,
276
+ 'use Effect HttpApi instead of Hono helpers'
277
+ ],
278
+ [
279
+ /\bruntimeFramework\s*(?::|=)\s*['"]hono['"]/u,
280
+ 'must use the Effect runtime'
281
+ ],
282
+ [
283
+ /\bstrictEffectApproach\s*(?::|=)\s*false\b/u,
284
+ 'must keep strictEffectApproach enabled'
285
+ ]
286
+ ];
287
+ if (/\/api\//u.test(file)) patterns.push([
288
+ /\bnew\s+Response\s*\(|\bResponse\.json\s*\(/u,
289
+ 'must not hand-build Response objects'
290
+ ], [
291
+ /\b(?:request|req)\.(?:json|text|formData|arrayBuffer)\s*\(/u,
292
+ 'must use endpoint payload/query/params schemas instead of parsing request bodies'
293
+ ], [
294
+ /\bexport\s+const\s+handler\b|\bexport\s+default\s+async\b/u,
295
+ 'must not export raw request handlers'
296
+ ], [
297
+ /\bcreateHandler\s*[:=]\s*(?!defineEffectBff\b)/u,
298
+ 'must use defineEffectBff instead of unbranded handler factories'
299
+ ], [
300
+ /\bSchema\.(?:UnknownFromJsonString|Unknown|Any)\b/u,
301
+ 'must use concrete request, response and error schemas'
302
+ ]);
303
+ for (const [pattern, reason] of patterns)assert(!pattern.test(source), `${file}: ${reason}`);
304
+ if (topology && /\/api\/index\.ts$/u.test(file)) {
305
+ topologyFilesAnalyzed += 1;
306
+ const violation = strictEffectRuntimeTopologyViolation(source, createEffectApiImportResolver(absolute(file)));
307
+ if (violation) diagnostics.push(`${file}: ${violation}`);
308
+ }
309
+ });
310
+ let owner;
311
+ for (const appPath of appDirectories)guarded(appPath, ()=>{
312
+ const app = byPath.get(appPath);
313
+ 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`);
314
+ if (!emits) {
315
+ if (app?.kind === 'vertical') {
316
+ assert(void 0 === app.api, `${appPath}: UI/horizontal unit must not declare an API`);
317
+ for (const suffix of [
318
+ 'api/index.ts',
319
+ 'api/effect-api.ts',
320
+ 'shared/api.ts',
321
+ 'shared/rpc.ts',
322
+ 'src/api'
323
+ ])noPath(`${appPath}/${suffix}`, 'unit has no API surface');
324
+ }
325
+ return;
326
+ }
327
+ if (app?.kind === 'vertical') assert(void 0 !== app.api, `${appPath}: vertical must declare its Effect API in topology/reference-topology.json`);
328
+ const stem = app?.api?.stem ?? node_path.posix.basename(appPath);
329
+ const rpc = app?.api?.protocol === 'rpc';
330
+ const contract = `${appPath}/shared/${rpc ? 'rpc' : 'api'}.ts`;
331
+ const entry = `${appPath}/api/index.ts`;
332
+ for (const file of [
333
+ entry,
334
+ contract
335
+ ])assert(exists(file), `${file}: required API surface is missing`);
336
+ const packageFile = `${appPath}/package.json`;
337
+ assert(exists(packageFile), `${packageFile}: required API package is missing`);
338
+ if (!exists(packageFile)) return;
339
+ const manifest = JSON.parse(read(packageFile));
340
+ assert(manifest.exports?.['./api'] === `./shared/${rpc ? 'rpc' : 'api'}.ts`, `${packageFile}: invalid ./api export`);
341
+ const clientKey = `./api/${rpc ? 'rpc-client' : 'client'}`;
342
+ const client = clientExportFile(appPath, manifest.exports?.[clientKey]);
343
+ assert(void 0 !== client, `${packageFile}: invalid API client export`);
344
+ assert(manifest.exports?.[`./api/${rpc ? 'client' : 'rpc-client'}`] === void 0, `${packageFile}: forbidden opposite protocol client export`);
345
+ noPath(`${appPath}/shared/${rpc ? 'api' : 'rpc'}.ts`, `must not emit a ${rpc ? 'REST' : 'RPC'} contract`);
346
+ if (rpc) {
347
+ moduleShape(entry, [
348
+ [
349
+ '@modern-js/bff-effect/effect-edge',
350
+ [
351
+ 'defineEffectBff',
352
+ 'Effect',
353
+ 'HttpApi',
354
+ 'Layer'
355
+ ]
356
+ ],
357
+ [
358
+ '../shared/rpc.ts',
359
+ []
360
+ ]
361
+ ], [
362
+ 'defineEffectBff',
363
+ 'HttpApi.make'
364
+ ], [
365
+ [
366
+ /\bconst\s+\w+RpcLayer\s*=\s*\w+RpcGroup\s*\.\s*toLayer\s*\(/u,
367
+ 'must implement RpcGroup.toLayer(...)'
368
+ ],
369
+ [
370
+ /\brpc\s*:\s*\{[\s\S]*?\bgroup\s*:[\s\S]*?\blayer\s*:[\s\S]*?\bpath\s*:[\s\S]*?\bserialization\s*:/u,
371
+ 'must register RPC group, layer, path and serialization'
372
+ ]
373
+ ], 'apiRuntime');
374
+ moduleShape(contract, [
375
+ [
376
+ 'effect/unstable/rpc',
377
+ [
378
+ 'Rpc',
379
+ 'RpcGroup'
380
+ ]
381
+ ],
382
+ [
383
+ effect,
384
+ [
385
+ 'Schema'
386
+ ]
387
+ ]
388
+ ], [
389
+ 'RpcGroup.make',
390
+ 'Rpc.make',
391
+ 'Schema.Struct'
392
+ ]);
393
+ } else if (exists(contract)) {
394
+ owner ??= options.baselinePackageDirectory ?? resolveBaselinePackageDirectory(node_path.join(root, 'package.json'));
395
+ const apiPrefix = app?.api?.prefix ?? `/${stem}-api`;
396
+ const basePath = `${apiPrefix}/${stem}`;
397
+ const expectation = {
398
+ operationPaths: app?.api?.operationPaths,
399
+ additionalPaths: app?.api?.additionalPaths ?? {},
400
+ apiPrefix,
401
+ basePath,
402
+ effectClientPackage: effect,
403
+ ownerId: app?.id ?? node_path.posix.basename(appPath),
404
+ readinessPath: `${basePath}/readiness`,
405
+ baselinePackage: baselinePackage,
406
+ baselinePackageDirectory: owner
407
+ };
408
+ const violation = microVerticalApiBaselineViolation(stem, absolute(contract), expectation);
409
+ if (violation) diagnostics.push(`${contract}: ${violation}`);
410
+ }
411
+ if (options.contractRules?.length && exists(contract)) {
412
+ let graph;
413
+ try {
414
+ graph = createModuleGraph(absolute(contract));
415
+ } catch (error) {
416
+ if (!(error instanceof SourceSyntaxError)) throw error;
417
+ diagnostics.push(`${contract}: contract rules need a parseable contract with immutable bindings (${error.message})`);
418
+ }
419
+ if (graph) {
420
+ const context = {
421
+ appPath,
422
+ protocol: rpc ? 'rpc' : 'rest',
423
+ graph
424
+ };
425
+ for (const rule of options.contractRules)guarded(`${contract} (rule ${rule.name || 'anonymous'})`, ()=>{
426
+ const messages = rule(context);
427
+ if (!Array.isArray(messages) || !messages.every((message)=>'string' == typeof message)) throw new Error('contract rule must return an array of violation messages (return [] when the contract passes)');
428
+ for (const message of messages)diagnostics.push(`${contract}: ${message}`);
429
+ });
430
+ }
431
+ }
432
+ if (client) {
433
+ const relativeContract = node_path.posix.relative(node_path.posix.dirname(client), contract);
434
+ moduleShape(client, [
435
+ [
436
+ effect,
437
+ [
438
+ 'Effect',
439
+ rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
440
+ ]
441
+ ],
442
+ [
443
+ relativeContract.startsWith('.') ? relativeContract : `./${relativeContract}`,
444
+ []
445
+ ]
446
+ ], [
447
+ rpc ? 'makeEffectRpcClient' : 'makeEffectHttpApiClient'
448
+ ], [], void 0, appPath);
449
+ }
450
+ moduleShape(`${appPath}/api/effect-api.ts`, [], [], [
451
+ [
452
+ /\bbackendFederationContract\b/u,
453
+ 'must export backendFederationContract metadata'
454
+ ],
455
+ [
456
+ /role:\s*['"]microvertical-server['"]/u,
457
+ 'must describe the MicroVertical server role'
458
+ ],
459
+ [
460
+ /strictEffectApproach:\s*true/u,
461
+ 'must preserve strict Effect backend execution'
462
+ ],
463
+ [
464
+ /contractVersion:\s*['"]microvertical-server-effect-v1['"]/u,
465
+ 'must preserve the server contract version'
466
+ ],
467
+ [
468
+ /export\s*\{\s*default\s*,\s*default\s+as\s+runtime\s*\}\s+from\s+['"]\.\/index\.ts['"]/u,
469
+ 'must re-export the runtime as default and runtime'
470
+ ],
471
+ [
472
+ /^(?![\s\S]*\b(request|handler)\s*:\s*async\s*\()/u,
473
+ 'must not expose raw request handlers'
474
+ ]
475
+ ]);
476
+ moduleShape(`${appPath}/modern.config.ts`, [], [], [
477
+ [
478
+ /runtimeFramework:\s*['"]effect['"]/u,
479
+ 'must use bff.runtimeFramework: effect'
480
+ ],
481
+ [
482
+ /entry:\s*['"]\.\/api\/index['"]/u,
483
+ 'must use ./api/index'
484
+ ],
485
+ [
486
+ /strictEffectApproach:\s*true/u,
487
+ 'must enable strictEffectApproach'
488
+ ]
489
+ ]);
490
+ });
491
+ const shell = 'apps/shell-super-app';
492
+ const verticals = apps.filter((app)=>app.path.startsWith('verticals/') && apiApp(app));
493
+ if (exists(shell) && verticals.length) {
494
+ const file = `${shell}/src/api/vertical-clients.ts`;
495
+ assert(exists(file), `${file}: must aggregate vertical API clients`);
496
+ if (exists(file)) guarded(file, ()=>{
497
+ const ast = parseFile(file);
498
+ for (const app of verticals){
499
+ const specifier = `${app.package}/api/${app.api?.protocol === 'rpc' ? 'rpc-client' : 'client'}`;
500
+ assert(ast?.program.body.some((item)=>__rspack_external__babel_types_6e8337dd.isExportNamedDeclaration(item) && item.source?.value === specifier), `${file}: must re-export ${specifier}`);
501
+ }
502
+ });
503
+ }
504
+ if (exists(`${shell}/package.json`)) guarded(`${shell}/package.json`, ()=>assert(JSON.parse(read(`${shell}/package.json`)).exports?.['./api/clients'] === './src/api/vertical-clients.ts', `${shell}/package.json: must export ./api/clients`));
505
+ if (exists('topology/reference-topology.json')) guarded('topology/reference-topology.json', ()=>{
506
+ for (const vertical of JSON.parse(read('topology/reference-topology.json')).verticals ?? []){
507
+ if (vertical.api?.runtime === 'effect') {
508
+ assert(vertical.api.bff?.strictEffectApproach === true, `${vertical.id}: topology must mark strictEffectApproach true`);
509
+ assert('string' == typeof vertical.api.serverEntry && vertical.api.serverEntry.endsWith('/api/index.ts'), `${vertical.id}: topology must use api/index.ts`);
510
+ }
511
+ assert(!vertical.api?.effect, `${vertical.id}: topology must describe API directly, not api.effect`);
512
+ }
513
+ });
514
+ } catch (error) {
515
+ toolErrors.push(errorMessage(error));
516
+ }
517
+ return {
518
+ diagnostics,
519
+ toolErrors,
520
+ topologyFilesAnalyzed
521
+ };
522
+ }
523
+ const checkMicroVerticalApiBoundaries = (options)=>check(options, true);
524
+ const checkMicroVerticalApiConsumerFiles = (options)=>check(options, false);
525
+ export { checkMicroVerticalApiBoundaries, checkMicroVerticalApiConsumerFiles, configuredMicroVerticalApiStem, createModuleGraph, microVerticalApiBaselineViolation };