@beignet/cli 0.0.48 → 0.0.49
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/CHANGELOG.md +34 -0
- package/README.md +76 -21
- package/dist/analysis/contract-factories.d.ts +17 -0
- package/dist/analysis/contract-factories.d.ts.map +1 -0
- package/dist/analysis/contract-factories.js +176 -0
- package/dist/analysis/contract-factories.js.map +1 -0
- package/dist/analysis/layers.d.ts +5 -0
- package/dist/analysis/layers.d.ts.map +1 -0
- package/dist/analysis/layers.js +172 -0
- package/dist/analysis/layers.js.map +1 -0
- package/dist/analysis/port-wiring.d.ts +23 -0
- package/dist/analysis/port-wiring.d.ts.map +1 -0
- package/dist/analysis/port-wiring.js +379 -0
- package/dist/analysis/port-wiring.js.map +1 -0
- package/dist/analysis/source-index.d.ts +5 -0
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +171 -42
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace.d.ts +10 -4
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +66 -6
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +311 -92
- package/dist/app-map.js.map +1 -1
- package/dist/check.js +37 -3
- package/dist/check.js.map +1 -1
- package/dist/explain.js +4 -4
- package/dist/explain.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +415 -168
- package/dist/inspect.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +309 -487
- package/dist/lint.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +11 -4
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.js +2 -2
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +144 -8
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +19 -19
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +142 -7
- package/dist/provider-add.js.map +1 -1
- package/dist/registry-edits.d.ts +7 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +237 -53
- package/dist/registry-edits.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +16 -0
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +9 -1
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +2 -0
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +1 -0
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +3 -2
- package/dist/templates/shared.js.map +1 -1
- package/package.json +7 -6
- package/skills/app-structure/SKILL.md +46 -5
- package/src/analysis/contract-factories.ts +256 -0
- package/src/analysis/layers.ts +266 -0
- package/src/analysis/port-wiring.ts +506 -0
- package/src/analysis/source-index.ts +225 -57
- package/src/analysis/workspace.ts +115 -6
- package/src/app-map.ts +418 -109
- package/src/check.ts +51 -3
- package/src/explain.ts +4 -4
- package/src/inspect.ts +583 -215
- package/src/lint.ts +393 -643
- package/src/make/inbox.ts +11 -4
- package/src/make/payments.ts +2 -2
- package/src/make.ts +225 -8
- package/src/mcp.ts +19 -22
- package/src/provider-add.ts +186 -7
- package/src/registry-edits.ts +313 -63
- package/src/templates/agents.ts +16 -0
- package/src/templates/base.ts +9 -1
- package/src/templates/server.ts +2 -0
- package/src/templates/shared.ts +3 -2
package/src/lint.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { builtinModules } from "node:module";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import ts from "typescript";
|
|
4
|
+
import {
|
|
5
|
+
canonicalFeatureLayerPath,
|
|
6
|
+
classifySourcePath as classifyPath,
|
|
7
|
+
type SourceLayer,
|
|
8
|
+
} from "./analysis/layers.js";
|
|
9
|
+
import {
|
|
10
|
+
type ModuleReference,
|
|
11
|
+
moduleReferences,
|
|
12
|
+
} from "./analysis/source-index.js";
|
|
13
|
+
import {
|
|
14
|
+
createAnalysisWorkspace,
|
|
15
|
+
resolveLocalModulePath,
|
|
16
|
+
} from "./analysis/workspace.js";
|
|
4
17
|
import { createPainter } from "./ansi.js";
|
|
5
18
|
import {
|
|
6
19
|
type BeignetConfig,
|
|
7
20
|
clientDirPath,
|
|
8
21
|
directoryPath,
|
|
9
|
-
loadBeignetConfig,
|
|
10
22
|
normalizePath,
|
|
11
23
|
type ResolvedBeignetConfig,
|
|
12
|
-
resolveConfig,
|
|
13
24
|
} from "./config.js";
|
|
14
25
|
import { formatGithubAnnotation } from "./github-annotations.js";
|
|
15
26
|
|
|
@@ -18,38 +29,14 @@ type LintAppOptions = {
|
|
|
18
29
|
config?: BeignetConfig;
|
|
19
30
|
};
|
|
20
31
|
|
|
21
|
-
type
|
|
22
|
-
| "app"
|
|
23
|
-
| "app-context"
|
|
24
|
-
| "client"
|
|
25
|
-
| "component"
|
|
26
|
-
| "contract"
|
|
27
|
-
| "domain"
|
|
28
|
-
| "feature-port"
|
|
29
|
-
| "infra"
|
|
30
|
-
| "lib"
|
|
31
|
-
| "policy"
|
|
32
|
-
| "port"
|
|
33
|
-
| "route"
|
|
34
|
-
| "server"
|
|
35
|
-
| "test"
|
|
36
|
-
| "use-case"
|
|
37
|
-
| "workflow"
|
|
38
|
-
| "unknown";
|
|
39
|
-
|
|
40
|
-
type ImportReference = {
|
|
41
|
-
importPath: string;
|
|
42
|
-
kind: ImportKind;
|
|
43
|
-
offset: number;
|
|
32
|
+
type ImportReference = Omit<ModuleReference, "file" | "resolvedFile"> & {
|
|
44
33
|
resolvedPath?: string;
|
|
45
34
|
packageName?: string;
|
|
46
35
|
};
|
|
47
36
|
|
|
48
|
-
type ImportKind = "type" | "value";
|
|
49
|
-
|
|
50
37
|
type ImportGraph = Map<string, ImportReference[]>;
|
|
51
38
|
|
|
52
|
-
type RuntimeBoundaryRootKind = "client" | "contract";
|
|
39
|
+
type RuntimeBoundaryRootKind = "client" | "contract" | "schema";
|
|
53
40
|
|
|
54
41
|
/**
|
|
55
42
|
* Dependency-direction lint diagnostic.
|
|
@@ -80,28 +67,33 @@ export type LintAppResult = {
|
|
|
80
67
|
export async function lintApp(
|
|
81
68
|
options: LintAppOptions = {},
|
|
82
69
|
): Promise<LintAppResult> {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const config = options.config
|
|
88
|
-
? resolveConfig(options.config)
|
|
89
|
-
: await loadBeignetConfig(targetDir, files);
|
|
70
|
+
const workspace = await createAnalysisWorkspace(options.cwd, {
|
|
71
|
+
config: options.config,
|
|
72
|
+
});
|
|
73
|
+
const { config, targetDir } = workspace;
|
|
90
74
|
const diagnostics: LintDiagnostic[] = [];
|
|
91
|
-
const sourceFiles = lintSourceFiles(files);
|
|
92
|
-
const
|
|
93
|
-
const atAliasRoot = resolveAtAliasRoot(targetDir);
|
|
94
|
-
const sourceByFile = new Map<string, string>();
|
|
75
|
+
const sourceFiles = lintSourceFiles(workspace.files);
|
|
76
|
+
const sourceFileByPath = new Map<string, ts.SourceFile>();
|
|
95
77
|
const importGraph: ImportGraph = new Map();
|
|
96
78
|
|
|
97
79
|
for (const file of sourceFiles) {
|
|
98
80
|
const sourceLayer = classifyPath(file, config);
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
importGraph.set(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
81
|
+
const sourceFile = await workspace.readSourceFile(file);
|
|
82
|
+
sourceFileByPath.set(file, sourceFile);
|
|
83
|
+
importGraph.set(file, await parseImports(workspace, file));
|
|
84
|
+
|
|
85
|
+
const canonicalLayerPath = canonicalFeatureLayerPath(file, config);
|
|
86
|
+
if (canonicalLayerPath) {
|
|
87
|
+
diagnostics.push({
|
|
88
|
+
severity: "error",
|
|
89
|
+
code: "BEIGNET_CANONICAL_STRUCTURE",
|
|
90
|
+
file,
|
|
91
|
+
importPath: canonicalLayerPath,
|
|
92
|
+
line: 1,
|
|
93
|
+
column: 1,
|
|
94
|
+
message: `feature files that belong to a canonical layer must use ${canonicalLayerPath}.`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
105
97
|
|
|
106
98
|
if (sourceLayer === "test" || sourceLayer === "unknown") continue;
|
|
107
99
|
|
|
@@ -118,23 +110,34 @@ export async function lintApp(
|
|
|
118
110
|
});
|
|
119
111
|
}
|
|
120
112
|
|
|
121
|
-
const lineStarts = computeLineStarts(source);
|
|
122
113
|
for (const reference of importGraph.get(file) ?? []) {
|
|
123
|
-
const diagnostic =
|
|
114
|
+
const diagnostic =
|
|
115
|
+
lintUnresolvedModuleReference(file, sourceLayer, reference) ??
|
|
116
|
+
(sourceLayer === "schema"
|
|
117
|
+
? undefined
|
|
118
|
+
: lintImport(file, sourceLayer, reference, config));
|
|
124
119
|
if (diagnostic) {
|
|
125
120
|
diagnostics.push({
|
|
126
121
|
...diagnostic,
|
|
127
|
-
|
|
122
|
+
line: reference.line,
|
|
123
|
+
column: reference.column,
|
|
128
124
|
});
|
|
129
125
|
}
|
|
130
126
|
}
|
|
131
127
|
}
|
|
132
128
|
|
|
129
|
+
diagnostics.push(
|
|
130
|
+
...lintTransitiveBoundaries({
|
|
131
|
+
config,
|
|
132
|
+
graph: importGraph,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
133
136
|
diagnostics.push(
|
|
134
137
|
...lintRuntimeBoundary({
|
|
135
138
|
config,
|
|
136
139
|
graph: importGraph,
|
|
137
|
-
|
|
140
|
+
sourceFileByPath,
|
|
138
141
|
}),
|
|
139
142
|
);
|
|
140
143
|
|
|
@@ -146,37 +149,6 @@ export async function lintApp(
|
|
|
146
149
|
};
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
function computeLineStarts(source: string): number[] {
|
|
150
|
-
const lineStarts = [0];
|
|
151
|
-
|
|
152
|
-
for (let index = 0; index < source.length; index++) {
|
|
153
|
-
if (source[index] === "\n") {
|
|
154
|
-
lineStarts.push(index + 1);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return lineStarts;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function positionFromOffset(
|
|
162
|
-
lineStarts: number[],
|
|
163
|
-
offset: number,
|
|
164
|
-
): { line: number; column: number } {
|
|
165
|
-
let low = 0;
|
|
166
|
-
let high = lineStarts.length - 1;
|
|
167
|
-
|
|
168
|
-
while (low < high) {
|
|
169
|
-
const middle = Math.ceil((low + high) / 2);
|
|
170
|
-
if (lineStarts[middle] <= offset) {
|
|
171
|
-
low = middle;
|
|
172
|
-
} else {
|
|
173
|
-
high = middle - 1;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return { line: low + 1, column: offset - lineStarts[low] + 1 };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
152
|
/**
|
|
181
153
|
* Format dependency-direction lint diagnostics for CLI output.
|
|
182
154
|
*/
|
|
@@ -229,449 +201,32 @@ export function formatLintGithub(result: LintAppResult): string {
|
|
|
229
201
|
.join("\n");
|
|
230
202
|
}
|
|
231
203
|
|
|
232
|
-
async function assertDirectory(targetDir: string): Promise<void> {
|
|
233
|
-
try {
|
|
234
|
-
const stats = await stat(targetDir);
|
|
235
|
-
if (!stats.isDirectory()) {
|
|
236
|
-
throw new Error(`${targetDir} is not a directory.`);
|
|
237
|
-
}
|
|
238
|
-
} catch (error) {
|
|
239
|
-
if (error instanceof Error && error.message.includes("not a directory")) {
|
|
240
|
-
throw error;
|
|
241
|
-
}
|
|
242
|
-
throw new Error(`Directory ${targetDir} does not exist.`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async function listFiles(targetDir: string): Promise<string[]> {
|
|
247
|
-
const files: string[] = [];
|
|
248
|
-
|
|
249
|
-
async function visit(relativeDir: string): Promise<void> {
|
|
250
|
-
const absoluteDir = path.join(targetDir, relativeDir);
|
|
251
|
-
const entries = await readdir(absoluteDir, { withFileTypes: true });
|
|
252
|
-
|
|
253
|
-
for (const entry of entries) {
|
|
254
|
-
if (ignoredDirectoryNames.has(entry.name)) continue;
|
|
255
|
-
|
|
256
|
-
const relativePath = normalizePath(path.join(relativeDir, entry.name));
|
|
257
|
-
if (entry.isDirectory()) {
|
|
258
|
-
await visit(relativePath);
|
|
259
|
-
} else if (entry.isFile()) {
|
|
260
|
-
files.push(relativePath);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
await visit("");
|
|
266
|
-
return files.sort();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const ignoredDirectoryNames = new Set([
|
|
270
|
-
".git",
|
|
271
|
-
".next",
|
|
272
|
-
".turbo",
|
|
273
|
-
"coverage",
|
|
274
|
-
"dist",
|
|
275
|
-
"node_modules",
|
|
276
|
-
]);
|
|
277
|
-
|
|
278
204
|
function lintSourceFiles(files: string[]): string[] {
|
|
279
205
|
return files.filter((file) => {
|
|
280
|
-
if (
|
|
206
|
+
if (/\.d\.(c|m)?ts$/.test(file)) return false;
|
|
281
207
|
return /\.(c|m)?tsx?$/.test(file);
|
|
282
208
|
});
|
|
283
209
|
}
|
|
284
210
|
|
|
285
|
-
function parseImports(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
...resolveImport(reference.importPath, importerFile, files, atAliasRoot),
|
|
296
|
-
}));
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function resolveAtAliasRoot(targetDir: string): string {
|
|
300
|
-
const tsconfigPath = path.join(targetDir, "tsconfig.json");
|
|
301
|
-
const parsed = ts.getParsedCommandLineOfConfigFile(
|
|
302
|
-
tsconfigPath,
|
|
303
|
-
{},
|
|
304
|
-
{
|
|
305
|
-
...ts.sys,
|
|
306
|
-
onUnRecoverableConfigFileDiagnostic: () => {},
|
|
307
|
-
},
|
|
308
|
-
);
|
|
309
|
-
const aliasTarget = parsed?.options.paths?.["@/*"]?.[0];
|
|
310
|
-
if (!aliasTarget) return "";
|
|
311
|
-
|
|
312
|
-
const targetWithoutWildcard = aliasTarget.replace(/[/\\]?\*$/, "");
|
|
313
|
-
const baseUrl = parsed.options.baseUrl ?? targetDir;
|
|
314
|
-
const absoluteTarget = path.resolve(baseUrl, targetWithoutWildcard);
|
|
315
|
-
const relativeTarget = normalizePath(
|
|
316
|
-
path.relative(targetDir, absoluteTarget),
|
|
317
|
-
);
|
|
318
|
-
if (relativeTarget === "" || relativeTarget === ".") return "";
|
|
319
|
-
if (relativeTarget === ".." || relativeTarget.startsWith("../")) return "";
|
|
320
|
-
return directoryPath(relativeTarget);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function parseImportSpecifiers(
|
|
324
|
-
source: string,
|
|
325
|
-
): Array<{ importPath: string; kind: ImportKind; offset: number }> {
|
|
326
|
-
const specifiers: Array<{
|
|
327
|
-
importPath: string;
|
|
328
|
-
kind: ImportKind;
|
|
329
|
-
offset: number;
|
|
330
|
-
}> = [];
|
|
331
|
-
let index = 0;
|
|
332
|
-
|
|
333
|
-
while (index < source.length) {
|
|
334
|
-
index = skipNonCode(source, index);
|
|
335
|
-
|
|
336
|
-
if (isKeywordAt(source, index, "import")) {
|
|
337
|
-
const parsed = parseImportSpecifierAt(source, index + "import".length);
|
|
338
|
-
if (parsed?.specifier) {
|
|
339
|
-
specifiers.push({
|
|
340
|
-
importPath: parsed.specifier,
|
|
341
|
-
kind: parsed.kind ?? "value",
|
|
342
|
-
offset: index,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
index = parsed?.end ?? index + "import".length;
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (isKeywordAt(source, index, "export")) {
|
|
350
|
-
const parsed = parseExportSpecifierAt(source, index + "export".length);
|
|
351
|
-
if (parsed?.specifier) {
|
|
352
|
-
specifiers.push({
|
|
353
|
-
importPath: parsed.specifier,
|
|
354
|
-
kind: parsed.kind ?? "value",
|
|
355
|
-
offset: index,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
index = parsed?.end ?? index + "export".length;
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
index++;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return specifiers;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function parseImportSpecifierAt(
|
|
369
|
-
source: string,
|
|
370
|
-
index: number,
|
|
371
|
-
): { specifier?: string; kind?: ImportKind; end: number } | undefined {
|
|
372
|
-
let cursor = skipWhitespaceAndComments(source, index);
|
|
373
|
-
if (source[cursor] === "(") {
|
|
374
|
-
cursor = skipWhitespaceAndComments(source, cursor + 1);
|
|
375
|
-
const stringLiteral = readStringLiteral(source, cursor);
|
|
376
|
-
if (!stringLiteral) return { end: cursor };
|
|
377
|
-
return {
|
|
378
|
-
specifier: stringLiteral.value,
|
|
379
|
-
end: stringLiteral.end,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const sideEffectImport = readStringLiteral(source, cursor);
|
|
384
|
-
if (sideEffectImport) {
|
|
385
|
-
return {
|
|
386
|
-
specifier: sideEffectImport.value,
|
|
387
|
-
kind: "value",
|
|
388
|
-
end: sideEffectImport.end,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const importClauseStart = cursor;
|
|
393
|
-
let isImportType = false;
|
|
394
|
-
if (isKeywordAt(source, cursor, "type")) {
|
|
395
|
-
isImportType = true;
|
|
396
|
-
cursor = cursorAfterKeyword("type", cursor);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
while (cursor < source.length) {
|
|
400
|
-
cursor = skipWhitespaceAndComments(source, cursor);
|
|
401
|
-
|
|
402
|
-
if (source[cursor] === ";" || source[cursor] === "\n") {
|
|
403
|
-
return { end: cursor + 1 };
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (isKeywordAt(source, cursor, "from")) {
|
|
407
|
-
const fromCursor = skipWhitespaceAndComments(
|
|
408
|
-
source,
|
|
409
|
-
cursorAfterKeyword("from", cursor),
|
|
410
|
-
);
|
|
411
|
-
const stringLiteral = readStringLiteral(source, fromCursor);
|
|
412
|
-
if (!stringLiteral) return { end: fromCursor };
|
|
413
|
-
return {
|
|
414
|
-
specifier: stringLiteral.value,
|
|
415
|
-
kind: isImportType
|
|
416
|
-
? "type"
|
|
417
|
-
: inferImportClauseKind(source.slice(importClauseStart, cursor)),
|
|
418
|
-
end: stringLiteral.end,
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
cursor = skipToken(source, cursor);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return undefined;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function parseExportSpecifierAt(
|
|
429
|
-
source: string,
|
|
430
|
-
index: number,
|
|
431
|
-
): { specifier?: string; kind?: ImportKind; end: number } | undefined {
|
|
432
|
-
let cursor = index;
|
|
433
|
-
let isExportType = false;
|
|
434
|
-
const exportClauseStart = cursor;
|
|
435
|
-
|
|
436
|
-
while (cursor < source.length) {
|
|
437
|
-
cursor = skipWhitespaceAndComments(source, cursor);
|
|
438
|
-
|
|
439
|
-
if (isKeywordAt(source, cursor, "type")) {
|
|
440
|
-
isExportType = true;
|
|
441
|
-
cursor = cursorAfterKeyword("type", cursor);
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (source[cursor] === ";" || source[cursor] === "\n") {
|
|
446
|
-
return { end: cursor + 1 };
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (isKeywordAt(source, cursor, "from")) {
|
|
450
|
-
const fromCursor = skipWhitespaceAndComments(
|
|
451
|
-
source,
|
|
452
|
-
cursorAfterKeyword("from", cursor),
|
|
453
|
-
);
|
|
454
|
-
const stringLiteral = readStringLiteral(source, fromCursor);
|
|
455
|
-
if (!stringLiteral) return { end: fromCursor };
|
|
211
|
+
async function parseImports(
|
|
212
|
+
workspace: Awaited<ReturnType<typeof createAnalysisWorkspace>>,
|
|
213
|
+
file: string,
|
|
214
|
+
): Promise<ImportReference[]> {
|
|
215
|
+
return (await moduleReferences(workspace, file)).map(
|
|
216
|
+
({ file: _file, resolvedFile, ...reference }) => {
|
|
217
|
+
const resolvedPath = reference.staticallyResolvable
|
|
218
|
+
? (resolvedFile ??
|
|
219
|
+
resolveLocalModulePath(workspace, file, reference.importPath))
|
|
220
|
+
: undefined;
|
|
456
221
|
return {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
:
|
|
461
|
-
|
|
222
|
+
...reference,
|
|
223
|
+
...(resolvedPath ? { resolvedPath } : {}),
|
|
224
|
+
...(!resolvedPath && reference.staticallyResolvable
|
|
225
|
+
? { packageName: packageName(reference.importPath) }
|
|
226
|
+
: {}),
|
|
462
227
|
};
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
cursor = skipToken(source, cursor);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return undefined;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function inferImportClauseKind(importClause: string): ImportKind {
|
|
472
|
-
const trimmed = importClause.trim();
|
|
473
|
-
if (/^\{\s*type\s+[^,}]+(?:,\s*type\s+[^,}]+)*,?\s*\}$/.test(trimmed)) {
|
|
474
|
-
return "type";
|
|
475
|
-
}
|
|
476
|
-
return "value";
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function cursorAfterKeyword(keyword: string, index: number): number {
|
|
480
|
-
return index + keyword.length;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function skipNonCode(source: string, index: number): number {
|
|
484
|
-
let cursor = index;
|
|
485
|
-
|
|
486
|
-
while (cursor < source.length) {
|
|
487
|
-
if (source.startsWith("//", cursor)) {
|
|
488
|
-
cursor = skipLineComment(source, cursor);
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
if (source.startsWith("/*", cursor)) {
|
|
492
|
-
cursor = skipBlockComment(source, cursor);
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
if (source[cursor] === '"' || source[cursor] === "'") {
|
|
496
|
-
cursor = skipStringLiteral(source, cursor);
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
if (source[cursor] === "`") {
|
|
500
|
-
cursor = skipTemplateLiteral(source, cursor);
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
break;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
return cursor;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function skipWhitespaceAndComments(source: string, index: number): number {
|
|
510
|
-
let cursor = index;
|
|
511
|
-
|
|
512
|
-
while (cursor < source.length) {
|
|
513
|
-
if (/\s/.test(source[cursor])) {
|
|
514
|
-
cursor++;
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
if (source.startsWith("//", cursor)) {
|
|
518
|
-
cursor = skipLineComment(source, cursor);
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
if (source.startsWith("/*", cursor)) {
|
|
522
|
-
cursor = skipBlockComment(source, cursor);
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
break;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return cursor;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function skipToken(source: string, index: number): number {
|
|
532
|
-
if (source[index] === '"' || source[index] === "'") {
|
|
533
|
-
return skipStringLiteral(source, index);
|
|
534
|
-
}
|
|
535
|
-
if (source[index] === "`") {
|
|
536
|
-
return skipTemplateLiteral(source, index);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
return index + 1;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function readStringLiteral(
|
|
543
|
-
source: string,
|
|
544
|
-
index: number,
|
|
545
|
-
): { value: string; end: number } | undefined {
|
|
546
|
-
const quote = source[index];
|
|
547
|
-
if (quote !== '"' && quote !== "'") return undefined;
|
|
548
|
-
|
|
549
|
-
let value = "";
|
|
550
|
-
let escaped = false;
|
|
551
|
-
|
|
552
|
-
for (let cursor = index + 1; cursor < source.length; cursor++) {
|
|
553
|
-
const char = source[cursor];
|
|
554
|
-
if (escaped) {
|
|
555
|
-
value += char;
|
|
556
|
-
escaped = false;
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
if (char === "\\") {
|
|
560
|
-
escaped = true;
|
|
561
|
-
continue;
|
|
562
|
-
}
|
|
563
|
-
if (char === quote) {
|
|
564
|
-
return { value, end: cursor + 1 };
|
|
565
|
-
}
|
|
566
|
-
value += char;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
return undefined;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function skipStringLiteral(source: string, index: number): number {
|
|
573
|
-
return readStringLiteral(source, index)?.end ?? source.length;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function skipTemplateLiteral(source: string, index: number): number {
|
|
577
|
-
let escaped = false;
|
|
578
|
-
|
|
579
|
-
for (let cursor = index + 1; cursor < source.length; cursor++) {
|
|
580
|
-
const char = source[cursor];
|
|
581
|
-
if (escaped) {
|
|
582
|
-
escaped = false;
|
|
583
|
-
continue;
|
|
584
|
-
}
|
|
585
|
-
if (char === "\\") {
|
|
586
|
-
escaped = true;
|
|
587
|
-
continue;
|
|
588
|
-
}
|
|
589
|
-
if (char === "`") {
|
|
590
|
-
return cursor + 1;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
return source.length;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function skipLineComment(source: string, index: number): number {
|
|
598
|
-
const end = source.indexOf("\n", index + 2);
|
|
599
|
-
return end === -1 ? source.length : end + 1;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function skipBlockComment(source: string, index: number): number {
|
|
603
|
-
const end = source.indexOf("*/", index + 2);
|
|
604
|
-
return end === -1 ? source.length : end + 2;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function isKeywordAt(source: string, index: number, keyword: string): boolean {
|
|
608
|
-
if (!source.startsWith(keyword, index)) return false;
|
|
609
|
-
|
|
610
|
-
const before = source[index - 1];
|
|
611
|
-
const after = source[index + keyword.length];
|
|
612
|
-
return !isIdentifierChar(before) && !isIdentifierChar(after);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function isIdentifierChar(char: string | undefined): boolean {
|
|
616
|
-
return Boolean(char && /[A-Za-z0-9_$]/.test(char));
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
function resolveImport(
|
|
620
|
-
importPath: string,
|
|
621
|
-
importerFile: string,
|
|
622
|
-
files: Set<string>,
|
|
623
|
-
atAliasRoot: string,
|
|
624
|
-
): Pick<ImportReference, "packageName" | "resolvedPath"> {
|
|
625
|
-
if (importPath.startsWith("@/")) {
|
|
626
|
-
return {
|
|
627
|
-
resolvedPath: resolveLocalImportPath(
|
|
628
|
-
stripKnownExtension(path.posix.join(atAliasRoot, importPath.slice(2))),
|
|
629
|
-
files,
|
|
630
|
-
),
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
if (importPath.startsWith(".")) {
|
|
635
|
-
return {
|
|
636
|
-
resolvedPath: resolveLocalImportPath(
|
|
637
|
-
stripKnownExtension(
|
|
638
|
-
normalizePath(path.join(path.dirname(importerFile), importPath)),
|
|
639
|
-
),
|
|
640
|
-
files,
|
|
641
|
-
),
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
return {
|
|
646
|
-
packageName: packageName(importPath),
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function resolveLocalImportPath(
|
|
651
|
-
importPath: string,
|
|
652
|
-
files: Set<string>,
|
|
653
|
-
): string | undefined {
|
|
654
|
-
const candidates = [
|
|
655
|
-
importPath,
|
|
656
|
-
`${importPath}.ts`,
|
|
657
|
-
`${importPath}.tsx`,
|
|
658
|
-
`${importPath}.mts`,
|
|
659
|
-
`${importPath}.cts`,
|
|
660
|
-
`${importPath}/index.ts`,
|
|
661
|
-
`${importPath}/index.tsx`,
|
|
662
|
-
`${importPath}/index.mts`,
|
|
663
|
-
`${importPath}/index.cts`,
|
|
664
|
-
];
|
|
665
|
-
|
|
666
|
-
for (const candidate of candidates) {
|
|
667
|
-
for (const file of files) {
|
|
668
|
-
if (stripKnownExtension(file) === stripKnownExtension(candidate)) {
|
|
669
|
-
return file;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
return stripKnownExtension(importPath);
|
|
228
|
+
},
|
|
229
|
+
);
|
|
675
230
|
}
|
|
676
231
|
|
|
677
232
|
function packageName(importPath: string): string {
|
|
@@ -740,6 +295,43 @@ function lintImport(
|
|
|
740
295
|
return undefined;
|
|
741
296
|
}
|
|
742
297
|
|
|
298
|
+
function lintUnresolvedModuleReference(
|
|
299
|
+
file: string,
|
|
300
|
+
sourceLayer: SourceLayer,
|
|
301
|
+
reference: ImportReference,
|
|
302
|
+
): LintDiagnostic | undefined {
|
|
303
|
+
if (
|
|
304
|
+
reference.staticallyResolvable ||
|
|
305
|
+
![
|
|
306
|
+
"agent-capability",
|
|
307
|
+
"domain",
|
|
308
|
+
"feature-helper",
|
|
309
|
+
"feature-port",
|
|
310
|
+
"infra",
|
|
311
|
+
"policy",
|
|
312
|
+
"port",
|
|
313
|
+
"route",
|
|
314
|
+
"seed",
|
|
315
|
+
"use-case",
|
|
316
|
+
"workflow",
|
|
317
|
+
].includes(sourceLayer)
|
|
318
|
+
) {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
severity: "error",
|
|
324
|
+
code: "BEIGNET_MODULE_REFERENCE_UNRESOLVED",
|
|
325
|
+
file,
|
|
326
|
+
importPath: reference.importPath,
|
|
327
|
+
message: `${layerLabel(sourceLayer)} files must use a string literal for ${moduleSyntaxLabel(reference.syntax)} so Beignet can verify the dependency boundary (${reference.importPath}).`,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function moduleSyntaxLabel(syntax: ModuleReference["syntax"]): string {
|
|
332
|
+
return syntax === "require" ? "require()" : "dynamic import()";
|
|
333
|
+
}
|
|
334
|
+
|
|
743
335
|
function appImportViolation(
|
|
744
336
|
sourceLayer: SourceLayer,
|
|
745
337
|
targetLayer: SourceLayer | undefined,
|
|
@@ -751,15 +343,19 @@ function appImportViolation(
|
|
|
751
343
|
[
|
|
752
344
|
"app",
|
|
753
345
|
"app-context",
|
|
346
|
+
"agent-capability",
|
|
754
347
|
"client",
|
|
755
348
|
"component",
|
|
756
349
|
"contract",
|
|
350
|
+
"feature-helper",
|
|
757
351
|
"feature-port",
|
|
758
352
|
"infra",
|
|
759
353
|
"lib",
|
|
760
354
|
"policy",
|
|
761
355
|
"port",
|
|
762
356
|
"route",
|
|
357
|
+
"schema",
|
|
358
|
+
"seed",
|
|
763
359
|
"server",
|
|
764
360
|
"use-case",
|
|
765
361
|
"workflow",
|
|
@@ -769,10 +365,73 @@ function appImportViolation(
|
|
|
769
365
|
}
|
|
770
366
|
|
|
771
367
|
if (
|
|
772
|
-
|
|
773
|
-
[
|
|
774
|
-
|
|
775
|
-
|
|
368
|
+
sourceLayer === "agent-capability" &&
|
|
369
|
+
[
|
|
370
|
+
"agent-capability",
|
|
371
|
+
"app",
|
|
372
|
+
"app-context",
|
|
373
|
+
"client",
|
|
374
|
+
"component",
|
|
375
|
+
"infra",
|
|
376
|
+
"route",
|
|
377
|
+
"seed",
|
|
378
|
+
"server",
|
|
379
|
+
"workflow",
|
|
380
|
+
].includes(targetLayer)
|
|
381
|
+
) {
|
|
382
|
+
return targetLayer;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (
|
|
386
|
+
sourceLayer === "feature-helper" &&
|
|
387
|
+
[
|
|
388
|
+
"agent-capability",
|
|
389
|
+
"app",
|
|
390
|
+
"app-context",
|
|
391
|
+
"client",
|
|
392
|
+
"component",
|
|
393
|
+
"infra",
|
|
394
|
+
"route",
|
|
395
|
+
"seed",
|
|
396
|
+
"server",
|
|
397
|
+
"use-case",
|
|
398
|
+
"workflow",
|
|
399
|
+
].includes(targetLayer)
|
|
400
|
+
) {
|
|
401
|
+
return targetLayer;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (
|
|
405
|
+
["use-case", "workflow"].includes(sourceLayer) &&
|
|
406
|
+
[
|
|
407
|
+
"agent-capability",
|
|
408
|
+
"app",
|
|
409
|
+
"app-context",
|
|
410
|
+
"client",
|
|
411
|
+
"component",
|
|
412
|
+
"infra",
|
|
413
|
+
"route",
|
|
414
|
+
"seed",
|
|
415
|
+
"server",
|
|
416
|
+
].includes(targetLayer)
|
|
417
|
+
) {
|
|
418
|
+
return targetLayer;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (
|
|
422
|
+
sourceLayer === "seed" &&
|
|
423
|
+
[
|
|
424
|
+
"agent-capability",
|
|
425
|
+
"app",
|
|
426
|
+
"app-context",
|
|
427
|
+
"client",
|
|
428
|
+
"component",
|
|
429
|
+
"infra",
|
|
430
|
+
"route",
|
|
431
|
+
"server",
|
|
432
|
+
"use-case",
|
|
433
|
+
"workflow",
|
|
434
|
+
].includes(targetLayer)
|
|
776
435
|
) {
|
|
777
436
|
return targetLayer;
|
|
778
437
|
}
|
|
@@ -781,6 +440,7 @@ function appImportViolation(
|
|
|
781
440
|
["feature-port", "policy", "port"].includes(sourceLayer) &&
|
|
782
441
|
[
|
|
783
442
|
"app",
|
|
443
|
+
"agent-capability",
|
|
784
444
|
"client",
|
|
785
445
|
"component",
|
|
786
446
|
"infra",
|
|
@@ -794,16 +454,52 @@ function appImportViolation(
|
|
|
794
454
|
|
|
795
455
|
if (
|
|
796
456
|
sourceLayer === "contract" &&
|
|
797
|
-
[
|
|
798
|
-
|
|
799
|
-
|
|
457
|
+
[
|
|
458
|
+
"agent-capability",
|
|
459
|
+
"app",
|
|
460
|
+
"client",
|
|
461
|
+
"component",
|
|
462
|
+
"infra",
|
|
463
|
+
"route",
|
|
464
|
+
"seed",
|
|
465
|
+
"workflow",
|
|
466
|
+
].includes(targetLayer)
|
|
800
467
|
) {
|
|
801
468
|
return targetLayer;
|
|
802
469
|
}
|
|
803
470
|
|
|
804
471
|
if (
|
|
805
472
|
sourceLayer === "route" &&
|
|
806
|
-
[
|
|
473
|
+
[
|
|
474
|
+
"agent-capability",
|
|
475
|
+
"app",
|
|
476
|
+
"app-context",
|
|
477
|
+
"client",
|
|
478
|
+
"component",
|
|
479
|
+
"infra",
|
|
480
|
+
"seed",
|
|
481
|
+
"server",
|
|
482
|
+
"workflow",
|
|
483
|
+
].includes(targetLayer)
|
|
484
|
+
) {
|
|
485
|
+
return targetLayer;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (
|
|
489
|
+
sourceLayer === "schema" &&
|
|
490
|
+
[
|
|
491
|
+
"agent-capability",
|
|
492
|
+
"app",
|
|
493
|
+
"app-context",
|
|
494
|
+
"client",
|
|
495
|
+
"component",
|
|
496
|
+
"infra",
|
|
497
|
+
"route",
|
|
498
|
+
"seed",
|
|
499
|
+
"server",
|
|
500
|
+
"use-case",
|
|
501
|
+
"workflow",
|
|
502
|
+
].includes(targetLayer)
|
|
807
503
|
) {
|
|
808
504
|
return targetLayer;
|
|
809
505
|
}
|
|
@@ -832,9 +528,13 @@ function packageImportViolation(
|
|
|
832
528
|
if (
|
|
833
529
|
[
|
|
834
530
|
"contract",
|
|
531
|
+
"agent-capability",
|
|
532
|
+
"feature-helper",
|
|
835
533
|
"feature-port",
|
|
836
534
|
"policy",
|
|
837
535
|
"port",
|
|
536
|
+
"schema",
|
|
537
|
+
"seed",
|
|
838
538
|
"use-case",
|
|
839
539
|
"workflow",
|
|
840
540
|
].includes(sourceLayer)
|
|
@@ -878,21 +578,121 @@ function matchBeignetProviderPackage(importPath: string): string | undefined {
|
|
|
878
578
|
return name.startsWith("@beignet/provider-") ? name : undefined;
|
|
879
579
|
}
|
|
880
580
|
|
|
581
|
+
function lintTransitiveBoundaries({
|
|
582
|
+
config,
|
|
583
|
+
graph,
|
|
584
|
+
}: {
|
|
585
|
+
config: ResolvedBeignetConfig;
|
|
586
|
+
graph: ImportGraph;
|
|
587
|
+
}): LintDiagnostic[] {
|
|
588
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
589
|
+
|
|
590
|
+
for (const root of graph.keys()) {
|
|
591
|
+
const rootLayer = classifyPath(root, config);
|
|
592
|
+
if (
|
|
593
|
+
![
|
|
594
|
+
"agent-capability",
|
|
595
|
+
"domain",
|
|
596
|
+
"feature-helper",
|
|
597
|
+
"feature-port",
|
|
598
|
+
"infra",
|
|
599
|
+
"policy",
|
|
600
|
+
"port",
|
|
601
|
+
"route",
|
|
602
|
+
"seed",
|
|
603
|
+
"use-case",
|
|
604
|
+
"workflow",
|
|
605
|
+
].includes(rootLayer)
|
|
606
|
+
) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
diagnostics.push(
|
|
611
|
+
...lintTransitiveBoundaryRoot({ config, graph, root, rootLayer }),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return diagnostics;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function lintTransitiveBoundaryRoot({
|
|
619
|
+
config,
|
|
620
|
+
graph,
|
|
621
|
+
root,
|
|
622
|
+
rootLayer,
|
|
623
|
+
}: {
|
|
624
|
+
config: ResolvedBeignetConfig;
|
|
625
|
+
graph: ImportGraph;
|
|
626
|
+
root: string;
|
|
627
|
+
rootLayer: SourceLayer;
|
|
628
|
+
}): LintDiagnostic[] {
|
|
629
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
630
|
+
const visited = new Set<string>();
|
|
631
|
+
const queue: Array<{ file: string; chain: string[] }> = [
|
|
632
|
+
{ file: root, chain: [root] },
|
|
633
|
+
];
|
|
634
|
+
let queueIndex = 0;
|
|
635
|
+
|
|
636
|
+
while (queueIndex < queue.length) {
|
|
637
|
+
const current = queue[queueIndex++];
|
|
638
|
+
if (visited.has(current.file)) continue;
|
|
639
|
+
visited.add(current.file);
|
|
640
|
+
|
|
641
|
+
for (const reference of graph.get(current.file) ?? []) {
|
|
642
|
+
if (reference.kind === "type") continue;
|
|
643
|
+
|
|
644
|
+
const chain = reference.resolvedPath
|
|
645
|
+
? [...current.chain, reference.resolvedPath]
|
|
646
|
+
: current.chain;
|
|
647
|
+
const violation =
|
|
648
|
+
current.file === root
|
|
649
|
+
? undefined
|
|
650
|
+
: (lintUnresolvedModuleReference(root, rootLayer, reference) ??
|
|
651
|
+
lintImport(root, rootLayer, reference, config));
|
|
652
|
+
|
|
653
|
+
if (violation) {
|
|
654
|
+
diagnostics.push({
|
|
655
|
+
...violation,
|
|
656
|
+
file: root,
|
|
657
|
+
message: `${layerLabel(rootLayer)} files must not reach forbidden dependencies through ${formatImportChain(chain)}. ${violation.message}`,
|
|
658
|
+
});
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (
|
|
663
|
+
reference.resolvedPath &&
|
|
664
|
+
graph.has(reference.resolvedPath) &&
|
|
665
|
+
isTransparentArchitectureLayer(
|
|
666
|
+
classifyPath(reference.resolvedPath, config),
|
|
667
|
+
)
|
|
668
|
+
) {
|
|
669
|
+
queue.push({ file: reference.resolvedPath, chain });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return diagnostics;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function isTransparentArchitectureLayer(layer: SourceLayer): boolean {
|
|
678
|
+
return ["feature-helper", "lib", "unknown"].includes(layer);
|
|
679
|
+
}
|
|
680
|
+
|
|
881
681
|
function lintRuntimeBoundary({
|
|
882
682
|
config,
|
|
883
683
|
graph,
|
|
884
|
-
|
|
684
|
+
sourceFileByPath,
|
|
885
685
|
}: {
|
|
886
686
|
config: ResolvedBeignetConfig;
|
|
887
687
|
graph: ImportGraph;
|
|
888
|
-
|
|
688
|
+
sourceFileByPath: Map<string, ts.SourceFile>;
|
|
889
689
|
}): LintDiagnostic[] {
|
|
890
690
|
const diagnostics: LintDiagnostic[] = [];
|
|
891
691
|
|
|
892
692
|
for (const file of graph.keys()) {
|
|
893
693
|
const rootKind = runtimeBoundaryRootKind(
|
|
894
694
|
file,
|
|
895
|
-
|
|
695
|
+
sourceFileByPath.get(file),
|
|
896
696
|
config,
|
|
897
697
|
);
|
|
898
698
|
if (!rootKind) continue;
|
|
@@ -926,10 +726,10 @@ function lintRuntimeBoundaryRoot({
|
|
|
926
726
|
const queue: Array<{ file: string; chain: string[] }> = [
|
|
927
727
|
{ file: root, chain: [root] },
|
|
928
728
|
];
|
|
729
|
+
let queueIndex = 0;
|
|
929
730
|
|
|
930
|
-
while (queue.length
|
|
931
|
-
const current = queue
|
|
932
|
-
if (!current) continue;
|
|
731
|
+
while (queueIndex < queue.length) {
|
|
732
|
+
const current = queue[queueIndex++];
|
|
933
733
|
if (visited.has(current.file)) continue;
|
|
934
734
|
visited.add(current.file);
|
|
935
735
|
|
|
@@ -944,7 +744,7 @@ function lintRuntimeBoundaryRoot({
|
|
|
944
744
|
const chain = reference.resolvedPath
|
|
945
745
|
? [...current.chain, reference.resolvedPath]
|
|
946
746
|
: current.chain;
|
|
947
|
-
const violation = runtimeBoundaryViolation(reference, config);
|
|
747
|
+
const violation = runtimeBoundaryViolation(reference, config, rootKind);
|
|
948
748
|
|
|
949
749
|
if (violation) {
|
|
950
750
|
diagnostics.push({
|
|
@@ -971,25 +771,29 @@ function lintRuntimeBoundaryRoot({
|
|
|
971
771
|
|
|
972
772
|
function runtimeBoundaryRootKind(
|
|
973
773
|
file: string,
|
|
974
|
-
|
|
774
|
+
sourceFile: ts.SourceFile | undefined,
|
|
975
775
|
config: ResolvedBeignetConfig,
|
|
976
776
|
): RuntimeBoundaryRootKind | undefined {
|
|
977
777
|
const layer = classifyPath(file, config);
|
|
978
778
|
if (layer === "contract") return "contract";
|
|
779
|
+
if (layer === "schema") return "schema";
|
|
979
780
|
if (layer === "client") return "client";
|
|
980
|
-
if (hasUseClientDirective(
|
|
781
|
+
if (sourceFile && hasUseClientDirective(sourceFile)) return "client";
|
|
981
782
|
if (isGeneratedClientAdapter(file, config)) return "client";
|
|
982
783
|
return undefined;
|
|
983
784
|
}
|
|
984
785
|
|
|
985
|
-
function hasUseClientDirective(
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
786
|
+
function hasUseClientDirective(sourceFile: ts.SourceFile): boolean {
|
|
787
|
+
for (const statement of sourceFile.statements) {
|
|
788
|
+
if (
|
|
789
|
+
!ts.isExpressionStatement(statement) ||
|
|
790
|
+
!ts.isStringLiteral(statement.expression)
|
|
791
|
+
) {
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
if (statement.expression.text === "use client") return true;
|
|
795
|
+
}
|
|
796
|
+
return false;
|
|
993
797
|
}
|
|
994
798
|
|
|
995
799
|
function isGeneratedClientAdapter(
|
|
@@ -1005,14 +809,29 @@ function isGeneratedClientAdapter(
|
|
|
1005
809
|
function runtimeBoundaryViolation(
|
|
1006
810
|
reference: ImportReference,
|
|
1007
811
|
config: ResolvedBeignetConfig,
|
|
812
|
+
rootKind: RuntimeBoundaryRootKind,
|
|
1008
813
|
): string | undefined {
|
|
814
|
+
if (!reference.staticallyResolvable) {
|
|
815
|
+
return `computed ${moduleSyntaxLabel(reference.syntax)} ${reference.importPath}, which cannot be verified statically`;
|
|
816
|
+
}
|
|
817
|
+
|
|
1009
818
|
if (reference.resolvedPath) {
|
|
1010
819
|
const targetLayer = classifyPath(reference.resolvedPath, config);
|
|
820
|
+
const prohibitedLayer =
|
|
821
|
+
rootKind === "client"
|
|
822
|
+
? undefined
|
|
823
|
+
: appImportViolation(rootKind, targetLayer);
|
|
824
|
+
if (prohibitedLayer) {
|
|
825
|
+
return `${layerLabel(prohibitedLayer)} module ${reference.resolvedPath}`;
|
|
826
|
+
}
|
|
1011
827
|
if (
|
|
828
|
+
targetLayer === "agent-capability" ||
|
|
1012
829
|
targetLayer === "use-case" ||
|
|
1013
830
|
targetLayer === "route" ||
|
|
1014
831
|
targetLayer === "infra" ||
|
|
1015
|
-
targetLayer === "
|
|
832
|
+
targetLayer === "seed" ||
|
|
833
|
+
targetLayer === "server" ||
|
|
834
|
+
targetLayer === "workflow"
|
|
1016
835
|
) {
|
|
1017
836
|
return `${layerLabel(targetLayer)} module ${reference.resolvedPath}`;
|
|
1018
837
|
}
|
|
@@ -1025,12 +844,22 @@ function runtimeBoundaryViolation(
|
|
|
1025
844
|
return "server-only marker @beignet/core/server-only";
|
|
1026
845
|
}
|
|
1027
846
|
|
|
1028
|
-
if (reference.importPath
|
|
847
|
+
if (isNodeRuntimeModule(reference.importPath)) {
|
|
1029
848
|
return `Node runtime module ${reference.importPath}`;
|
|
1030
849
|
}
|
|
1031
850
|
|
|
851
|
+
if (rootKind !== "client" && reference.packageName) {
|
|
852
|
+
const prohibitedPackage = packageImportViolation(
|
|
853
|
+
rootKind,
|
|
854
|
+
reference.importPath,
|
|
855
|
+
);
|
|
856
|
+
if (prohibitedPackage) {
|
|
857
|
+
return `runtime or provider package ${prohibitedPackage}`;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
1032
861
|
if (
|
|
1033
|
-
serverRuntimePackages
|
|
862
|
+
matchBannedPackage(serverRuntimePackages, reference.importPath) ||
|
|
1034
863
|
reference.importPath.startsWith("@beignet/provider-")
|
|
1035
864
|
) {
|
|
1036
865
|
return `server runtime package ${reference.importPath}`;
|
|
@@ -1039,6 +868,12 @@ function runtimeBoundaryViolation(
|
|
|
1039
868
|
return undefined;
|
|
1040
869
|
}
|
|
1041
870
|
|
|
871
|
+
const nodeRuntimeModules = new Set(builtinModules);
|
|
872
|
+
|
|
873
|
+
function isNodeRuntimeModule(importPath: string): boolean {
|
|
874
|
+
return importPath.startsWith("node:") || nodeRuntimeModules.has(importPath);
|
|
875
|
+
}
|
|
876
|
+
|
|
1042
877
|
function runtimeBoundaryMessage(
|
|
1043
878
|
rootKind: RuntimeBoundaryRootKind,
|
|
1044
879
|
violation: string,
|
|
@@ -1052,7 +887,9 @@ function runtimeBoundaryMessage(
|
|
|
1052
887
|
}
|
|
1053
888
|
|
|
1054
889
|
function rootKindLabel(rootKind: RuntimeBoundaryRootKind): string {
|
|
1055
|
-
|
|
890
|
+
if (rootKind === "contract") return "contract";
|
|
891
|
+
if (rootKind === "schema") return "schema";
|
|
892
|
+
return "client";
|
|
1056
893
|
}
|
|
1057
894
|
|
|
1058
895
|
function formatImportChain(chain: string[]): string {
|
|
@@ -1177,98 +1014,6 @@ function packageImportMessage(
|
|
|
1177
1014
|
)} files must not import runtime or provider package ${packageName}. Depend on Beignet ports or app-owned ports instead.`;
|
|
1178
1015
|
}
|
|
1179
1016
|
|
|
1180
|
-
function classifyPath(
|
|
1181
|
-
filePath: string,
|
|
1182
|
-
config: ResolvedBeignetConfig,
|
|
1183
|
-
): SourceLayer {
|
|
1184
|
-
const normalizedPath = stripKnownExtension(normalizePath(filePath));
|
|
1185
|
-
const featuresPath = directoryPath(config.paths.features);
|
|
1186
|
-
const portsDir = directoryPath(path.dirname(config.paths.ports));
|
|
1187
|
-
|
|
1188
|
-
if (isTestPath(normalizedPath)) return "test";
|
|
1189
|
-
if (normalizedPath === stripKnownExtension(config.paths.appContext)) {
|
|
1190
|
-
return "app-context";
|
|
1191
|
-
}
|
|
1192
|
-
if (
|
|
1193
|
-
isUnder(normalizedPath, directoryPath(path.dirname(config.paths.server)))
|
|
1194
|
-
) {
|
|
1195
|
-
return "server";
|
|
1196
|
-
}
|
|
1197
|
-
if (
|
|
1198
|
-
isUnder(normalizedPath, directoryPath(path.dirname(config.paths.routes)))
|
|
1199
|
-
) {
|
|
1200
|
-
return "app";
|
|
1201
|
-
}
|
|
1202
|
-
if (
|
|
1203
|
-
isUnder(
|
|
1204
|
-
normalizedPath,
|
|
1205
|
-
directoryPath(path.dirname(config.paths.portWiring)),
|
|
1206
|
-
)
|
|
1207
|
-
) {
|
|
1208
|
-
return "infra";
|
|
1209
|
-
}
|
|
1210
|
-
if (isUnder(normalizedPath, portsDir)) return "port";
|
|
1211
|
-
if (isUnder(normalizedPath, clientDirPath(config))) return "client";
|
|
1212
|
-
if (isUnder(normalizedPath, "components")) return "component";
|
|
1213
|
-
if (isUnder(normalizedPath, legacySharedDomainPath(config))) return "domain";
|
|
1214
|
-
if (isUnder(normalizedPath, "lib")) return "lib";
|
|
1215
|
-
|
|
1216
|
-
const featurePath = featureRelativePath(normalizedPath, featuresPath);
|
|
1217
|
-
if (featurePath) {
|
|
1218
|
-
const layerSegment = featurePath.split("/")[1];
|
|
1219
|
-
if (layerSegment === "domain") return "domain";
|
|
1220
|
-
if (layerSegment === "contracts") return "contract";
|
|
1221
|
-
if (layerSegment?.endsWith("-contracts")) return "contract";
|
|
1222
|
-
if (layerSegment === "routes") return "route";
|
|
1223
|
-
if (layerSegment === "ports") return "feature-port";
|
|
1224
|
-
if (layerSegment === "policy") return "policy";
|
|
1225
|
-
if (layerSegment === "use-cases") return "use-case";
|
|
1226
|
-
if (
|
|
1227
|
-
[
|
|
1228
|
-
"jobs",
|
|
1229
|
-
"listeners",
|
|
1230
|
-
"notifications",
|
|
1231
|
-
"schedules",
|
|
1232
|
-
"tasks",
|
|
1233
|
-
"uploads",
|
|
1234
|
-
].includes(layerSegment)
|
|
1235
|
-
) {
|
|
1236
|
-
return "workflow";
|
|
1237
|
-
}
|
|
1238
|
-
if (layerSegment === "components") return "component";
|
|
1239
|
-
if (layerSegment === "client") return "client";
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
if (
|
|
1243
|
-
directoryPath(config.paths.contracts) !== featuresPath &&
|
|
1244
|
-
isUnder(normalizedPath, directoryPath(config.paths.contracts))
|
|
1245
|
-
) {
|
|
1246
|
-
return "contract";
|
|
1247
|
-
}
|
|
1248
|
-
if (
|
|
1249
|
-
directoryPath(config.paths.useCases) !== featuresPath &&
|
|
1250
|
-
isUnder(normalizedPath, directoryPath(config.paths.useCases))
|
|
1251
|
-
) {
|
|
1252
|
-
return "use-case";
|
|
1253
|
-
}
|
|
1254
|
-
if (
|
|
1255
|
-
directoryPath(config.paths.policies) !== featuresPath &&
|
|
1256
|
-
isUnder(normalizedPath, directoryPath(config.paths.policies))
|
|
1257
|
-
) {
|
|
1258
|
-
return "policy";
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
return "unknown";
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
function isTestPath(filePath: string): boolean {
|
|
1265
|
-
return (
|
|
1266
|
-
filePath.includes("/tests/") ||
|
|
1267
|
-
filePath.endsWith(".test") ||
|
|
1268
|
-
filePath.endsWith(".spec")
|
|
1269
|
-
);
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
1017
|
function isUnder(filePath: string, directory: string): boolean {
|
|
1273
1018
|
const normalizedDir = directoryPath(directory);
|
|
1274
1019
|
if (!normalizedDir || normalizedDir === ".") return false;
|
|
@@ -1288,7 +1033,7 @@ function featureRelativePath(
|
|
|
1288
1033
|
function featureLayerInfo(
|
|
1289
1034
|
filePath: string,
|
|
1290
1035
|
config: ResolvedBeignetConfig,
|
|
1291
|
-
): { feature: string; layer:
|
|
1036
|
+
): { feature: string; layer: SourceLayer } | undefined {
|
|
1292
1037
|
const normalizedPath = stripKnownExtension(normalizePath(filePath));
|
|
1293
1038
|
const relativePath = featureRelativePath(
|
|
1294
1039
|
normalizedPath,
|
|
@@ -1296,10 +1041,10 @@ function featureLayerInfo(
|
|
|
1296
1041
|
);
|
|
1297
1042
|
if (!relativePath) return undefined;
|
|
1298
1043
|
|
|
1299
|
-
const [feature
|
|
1044
|
+
const [feature] = relativePath.split("/");
|
|
1300
1045
|
if (!feature) return undefined;
|
|
1301
1046
|
|
|
1302
|
-
return { feature, layer };
|
|
1047
|
+
return { feature, layer: classifyPath(filePath, config) };
|
|
1303
1048
|
}
|
|
1304
1049
|
|
|
1305
1050
|
function legacySharedDomainPath(config: ResolvedBeignetConfig): string {
|
|
@@ -1331,9 +1076,14 @@ function dedupeDiagnostics(diagnostics: LintDiagnostic[]): LintDiagnostic[] {
|
|
|
1331
1076
|
const deduped: LintDiagnostic[] = [];
|
|
1332
1077
|
|
|
1333
1078
|
for (const diagnostic of diagnostics) {
|
|
1334
|
-
const key =
|
|
1335
|
-
|
|
1336
|
-
|
|
1079
|
+
const key = JSON.stringify([
|
|
1080
|
+
diagnostic.code,
|
|
1081
|
+
diagnostic.file,
|
|
1082
|
+
diagnostic.importPath,
|
|
1083
|
+
diagnostic.line,
|
|
1084
|
+
diagnostic.column,
|
|
1085
|
+
diagnostic.message,
|
|
1086
|
+
]);
|
|
1337
1087
|
if (seen.has(key)) continue;
|
|
1338
1088
|
seen.add(key);
|
|
1339
1089
|
deduped.push(diagnostic);
|