@beignet/cli 0.0.48 → 0.0.50
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 +54 -0
- package/README.md +117 -22
- 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 +11 -4
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +78 -8
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map-changes.d.ts +103 -0
- package/dist/app-map-changes.d.ts.map +1 -0
- package/dist/app-map-changes.js +949 -0
- package/dist/app-map-changes.js.map +1 -0
- 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/git-changes.d.ts +30 -0
- package/dist/git-changes.d.ts.map +1 -0
- package/dist/git-changes.js +367 -0
- package/dist/git-changes.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +706 -186
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +3 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -0
- package/dist/lib.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/shared.d.ts.map +1 -1
- package/dist/make/shared.js +31 -27
- package/dist/make/shared.js.map +1 -1
- 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 +43 -21
- 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 +37 -1
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +13 -2
- 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 +71 -7
- 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 +134 -9
- package/src/app-map-changes.ts +1462 -0
- package/src/app-map.ts +418 -109
- package/src/check.ts +51 -3
- package/src/explain.ts +4 -4
- package/src/git-changes.ts +511 -0
- package/src/index.ts +39 -1
- package/src/inspect.ts +1005 -238
- package/src/lib.ts +21 -0
- package/src/lint.ts +393 -643
- package/src/make/inbox.ts +11 -4
- package/src/make/payments.ts +2 -2
- package/src/make/shared.ts +31 -27
- package/src/make.ts +225 -8
- package/src/mcp.ts +51 -24
- package/src/provider-add.ts +186 -7
- package/src/registry-edits.ts +313 -63
- package/src/templates/agents.ts +37 -1
- package/src/templates/base.ts +13 -2
- package/src/templates/server.ts +2 -0
- package/src/templates/shared.ts +3 -2
package/dist/lint.js
CHANGED
|
@@ -1,30 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { builtinModules } from "node:module";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import ts from "typescript";
|
|
4
|
+
import { canonicalFeatureLayerPath, classifySourcePath as classifyPath, } from "./analysis/layers.js";
|
|
5
|
+
import { moduleReferences, } from "./analysis/source-index.js";
|
|
6
|
+
import { createAnalysisWorkspace, resolveLocalModulePath, } from "./analysis/workspace.js";
|
|
4
7
|
import { createPainter } from "./ansi.js";
|
|
5
|
-
import { clientDirPath, directoryPath,
|
|
8
|
+
import { clientDirPath, directoryPath, normalizePath, } from "./config.js";
|
|
6
9
|
import { formatGithubAnnotation } from "./github-annotations.js";
|
|
7
10
|
/**
|
|
8
11
|
* Inspect app imports and report dependency-direction violations.
|
|
9
12
|
*/
|
|
10
13
|
export async function lintApp(options = {}) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const config =
|
|
15
|
-
? resolveConfig(options.config)
|
|
16
|
-
: await loadBeignetConfig(targetDir, files);
|
|
14
|
+
const workspace = await createAnalysisWorkspace(options.cwd, {
|
|
15
|
+
config: options.config,
|
|
16
|
+
});
|
|
17
|
+
const { config, targetDir } = workspace;
|
|
17
18
|
const diagnostics = [];
|
|
18
|
-
const sourceFiles = lintSourceFiles(files);
|
|
19
|
-
const
|
|
20
|
-
const atAliasRoot = resolveAtAliasRoot(targetDir);
|
|
21
|
-
const sourceByFile = new Map();
|
|
19
|
+
const sourceFiles = lintSourceFiles(workspace.files);
|
|
20
|
+
const sourceFileByPath = new Map();
|
|
22
21
|
const importGraph = new Map();
|
|
23
22
|
for (const file of sourceFiles) {
|
|
24
23
|
const sourceLayer = classifyPath(file, config);
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
importGraph.set(file, parseImports(
|
|
24
|
+
const sourceFile = await workspace.readSourceFile(file);
|
|
25
|
+
sourceFileByPath.set(file, sourceFile);
|
|
26
|
+
importGraph.set(file, await parseImports(workspace, file));
|
|
27
|
+
const canonicalLayerPath = canonicalFeatureLayerPath(file, config);
|
|
28
|
+
if (canonicalLayerPath) {
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
severity: "error",
|
|
31
|
+
code: "BEIGNET_CANONICAL_STRUCTURE",
|
|
32
|
+
file,
|
|
33
|
+
importPath: canonicalLayerPath,
|
|
34
|
+
line: 1,
|
|
35
|
+
column: 1,
|
|
36
|
+
message: `feature files that belong to a canonical layer must use ${canonicalLayerPath}.`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
28
39
|
if (sourceLayer === "test" || sourceLayer === "unknown")
|
|
29
40
|
continue;
|
|
30
41
|
if (isLegacySharedDomainPath(file, config)) {
|
|
@@ -39,21 +50,28 @@ export async function lintApp(options = {}) {
|
|
|
39
50
|
message: `root shared-domain files are legacy. Move shared-kernel domain concepts to ${canonicalSharedDomain}.`,
|
|
40
51
|
});
|
|
41
52
|
}
|
|
42
|
-
const lineStarts = computeLineStarts(source);
|
|
43
53
|
for (const reference of importGraph.get(file) ?? []) {
|
|
44
|
-
const diagnostic =
|
|
54
|
+
const diagnostic = lintUnresolvedModuleReference(file, sourceLayer, reference) ??
|
|
55
|
+
(sourceLayer === "schema"
|
|
56
|
+
? undefined
|
|
57
|
+
: lintImport(file, sourceLayer, reference, config));
|
|
45
58
|
if (diagnostic) {
|
|
46
59
|
diagnostics.push({
|
|
47
60
|
...diagnostic,
|
|
48
|
-
|
|
61
|
+
line: reference.line,
|
|
62
|
+
column: reference.column,
|
|
49
63
|
});
|
|
50
64
|
}
|
|
51
65
|
}
|
|
52
66
|
}
|
|
67
|
+
diagnostics.push(...lintTransitiveBoundaries({
|
|
68
|
+
config,
|
|
69
|
+
graph: importGraph,
|
|
70
|
+
}));
|
|
53
71
|
diagnostics.push(...lintRuntimeBoundary({
|
|
54
72
|
config,
|
|
55
73
|
graph: importGraph,
|
|
56
|
-
|
|
74
|
+
sourceFileByPath,
|
|
57
75
|
}));
|
|
58
76
|
return {
|
|
59
77
|
schemaVersion: 1,
|
|
@@ -62,29 +80,6 @@ export async function lintApp(options = {}) {
|
|
|
62
80
|
diagnostics: dedupeDiagnostics(diagnostics),
|
|
63
81
|
};
|
|
64
82
|
}
|
|
65
|
-
function computeLineStarts(source) {
|
|
66
|
-
const lineStarts = [0];
|
|
67
|
-
for (let index = 0; index < source.length; index++) {
|
|
68
|
-
if (source[index] === "\n") {
|
|
69
|
-
lineStarts.push(index + 1);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return lineStarts;
|
|
73
|
-
}
|
|
74
|
-
function positionFromOffset(lineStarts, offset) {
|
|
75
|
-
let low = 0;
|
|
76
|
-
let high = lineStarts.length - 1;
|
|
77
|
-
while (low < high) {
|
|
78
|
-
const middle = Math.ceil((low + high) / 2);
|
|
79
|
-
if (lineStarts[middle] <= offset) {
|
|
80
|
-
low = middle;
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
high = middle - 1;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return { line: low + 1, column: offset - lineStarts[low] + 1 };
|
|
87
|
-
}
|
|
88
83
|
/**
|
|
89
84
|
* Format dependency-direction lint diagnostics for CLI output.
|
|
90
85
|
*/
|
|
@@ -120,352 +115,27 @@ export function formatLintGithub(result) {
|
|
|
120
115
|
}))
|
|
121
116
|
.join("\n");
|
|
122
117
|
}
|
|
123
|
-
async function assertDirectory(targetDir) {
|
|
124
|
-
try {
|
|
125
|
-
const stats = await stat(targetDir);
|
|
126
|
-
if (!stats.isDirectory()) {
|
|
127
|
-
throw new Error(`${targetDir} is not a directory.`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
if (error instanceof Error && error.message.includes("not a directory")) {
|
|
132
|
-
throw error;
|
|
133
|
-
}
|
|
134
|
-
throw new Error(`Directory ${targetDir} does not exist.`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
async function listFiles(targetDir) {
|
|
138
|
-
const files = [];
|
|
139
|
-
async function visit(relativeDir) {
|
|
140
|
-
const absoluteDir = path.join(targetDir, relativeDir);
|
|
141
|
-
const entries = await readdir(absoluteDir, { withFileTypes: true });
|
|
142
|
-
for (const entry of entries) {
|
|
143
|
-
if (ignoredDirectoryNames.has(entry.name))
|
|
144
|
-
continue;
|
|
145
|
-
const relativePath = normalizePath(path.join(relativeDir, entry.name));
|
|
146
|
-
if (entry.isDirectory()) {
|
|
147
|
-
await visit(relativePath);
|
|
148
|
-
}
|
|
149
|
-
else if (entry.isFile()) {
|
|
150
|
-
files.push(relativePath);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
await visit("");
|
|
155
|
-
return files.sort();
|
|
156
|
-
}
|
|
157
|
-
const ignoredDirectoryNames = new Set([
|
|
158
|
-
".git",
|
|
159
|
-
".next",
|
|
160
|
-
".turbo",
|
|
161
|
-
"coverage",
|
|
162
|
-
"dist",
|
|
163
|
-
"node_modules",
|
|
164
|
-
]);
|
|
165
118
|
function lintSourceFiles(files) {
|
|
166
119
|
return files.filter((file) => {
|
|
167
|
-
if (
|
|
120
|
+
if (/\.d\.(c|m)?ts$/.test(file))
|
|
168
121
|
return false;
|
|
169
122
|
return /\.(c|m)?tsx?$/.test(file);
|
|
170
123
|
});
|
|
171
124
|
}
|
|
172
|
-
function parseImports(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
function resolveAtAliasRoot(targetDir) {
|
|
180
|
-
const tsconfigPath = path.join(targetDir, "tsconfig.json");
|
|
181
|
-
const parsed = ts.getParsedCommandLineOfConfigFile(tsconfigPath, {}, {
|
|
182
|
-
...ts.sys,
|
|
183
|
-
onUnRecoverableConfigFileDiagnostic: () => { },
|
|
184
|
-
});
|
|
185
|
-
const aliasTarget = parsed?.options.paths?.["@/*"]?.[0];
|
|
186
|
-
if (!aliasTarget)
|
|
187
|
-
return "";
|
|
188
|
-
const targetWithoutWildcard = aliasTarget.replace(/[/\\]?\*$/, "");
|
|
189
|
-
const baseUrl = parsed.options.baseUrl ?? targetDir;
|
|
190
|
-
const absoluteTarget = path.resolve(baseUrl, targetWithoutWildcard);
|
|
191
|
-
const relativeTarget = normalizePath(path.relative(targetDir, absoluteTarget));
|
|
192
|
-
if (relativeTarget === "" || relativeTarget === ".")
|
|
193
|
-
return "";
|
|
194
|
-
if (relativeTarget === ".." || relativeTarget.startsWith("../"))
|
|
195
|
-
return "";
|
|
196
|
-
return directoryPath(relativeTarget);
|
|
197
|
-
}
|
|
198
|
-
function parseImportSpecifiers(source) {
|
|
199
|
-
const specifiers = [];
|
|
200
|
-
let index = 0;
|
|
201
|
-
while (index < source.length) {
|
|
202
|
-
index = skipNonCode(source, index);
|
|
203
|
-
if (isKeywordAt(source, index, "import")) {
|
|
204
|
-
const parsed = parseImportSpecifierAt(source, index + "import".length);
|
|
205
|
-
if (parsed?.specifier) {
|
|
206
|
-
specifiers.push({
|
|
207
|
-
importPath: parsed.specifier,
|
|
208
|
-
kind: parsed.kind ?? "value",
|
|
209
|
-
offset: index,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
index = parsed?.end ?? index + "import".length;
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
if (isKeywordAt(source, index, "export")) {
|
|
216
|
-
const parsed = parseExportSpecifierAt(source, index + "export".length);
|
|
217
|
-
if (parsed?.specifier) {
|
|
218
|
-
specifiers.push({
|
|
219
|
-
importPath: parsed.specifier,
|
|
220
|
-
kind: parsed.kind ?? "value",
|
|
221
|
-
offset: index,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
index = parsed?.end ?? index + "export".length;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
index++;
|
|
228
|
-
}
|
|
229
|
-
return specifiers;
|
|
230
|
-
}
|
|
231
|
-
function parseImportSpecifierAt(source, index) {
|
|
232
|
-
let cursor = skipWhitespaceAndComments(source, index);
|
|
233
|
-
if (source[cursor] === "(") {
|
|
234
|
-
cursor = skipWhitespaceAndComments(source, cursor + 1);
|
|
235
|
-
const stringLiteral = readStringLiteral(source, cursor);
|
|
236
|
-
if (!stringLiteral)
|
|
237
|
-
return { end: cursor };
|
|
125
|
+
async function parseImports(workspace, file) {
|
|
126
|
+
return (await moduleReferences(workspace, file)).map(({ file: _file, resolvedFile, ...reference }) => {
|
|
127
|
+
const resolvedPath = reference.staticallyResolvable
|
|
128
|
+
? (resolvedFile ??
|
|
129
|
+
resolveLocalModulePath(workspace, file, reference.importPath))
|
|
130
|
+
: undefined;
|
|
238
131
|
return {
|
|
239
|
-
|
|
240
|
-
|
|
132
|
+
...reference,
|
|
133
|
+
...(resolvedPath ? { resolvedPath } : {}),
|
|
134
|
+
...(!resolvedPath && reference.staticallyResolvable
|
|
135
|
+
? { packageName: packageName(reference.importPath) }
|
|
136
|
+
: {}),
|
|
241
137
|
};
|
|
242
|
-
}
|
|
243
|
-
const sideEffectImport = readStringLiteral(source, cursor);
|
|
244
|
-
if (sideEffectImport) {
|
|
245
|
-
return {
|
|
246
|
-
specifier: sideEffectImport.value,
|
|
247
|
-
kind: "value",
|
|
248
|
-
end: sideEffectImport.end,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
const importClauseStart = cursor;
|
|
252
|
-
let isImportType = false;
|
|
253
|
-
if (isKeywordAt(source, cursor, "type")) {
|
|
254
|
-
isImportType = true;
|
|
255
|
-
cursor = cursorAfterKeyword("type", cursor);
|
|
256
|
-
}
|
|
257
|
-
while (cursor < source.length) {
|
|
258
|
-
cursor = skipWhitespaceAndComments(source, cursor);
|
|
259
|
-
if (source[cursor] === ";" || source[cursor] === "\n") {
|
|
260
|
-
return { end: cursor + 1 };
|
|
261
|
-
}
|
|
262
|
-
if (isKeywordAt(source, cursor, "from")) {
|
|
263
|
-
const fromCursor = skipWhitespaceAndComments(source, cursorAfterKeyword("from", cursor));
|
|
264
|
-
const stringLiteral = readStringLiteral(source, fromCursor);
|
|
265
|
-
if (!stringLiteral)
|
|
266
|
-
return { end: fromCursor };
|
|
267
|
-
return {
|
|
268
|
-
specifier: stringLiteral.value,
|
|
269
|
-
kind: isImportType
|
|
270
|
-
? "type"
|
|
271
|
-
: inferImportClauseKind(source.slice(importClauseStart, cursor)),
|
|
272
|
-
end: stringLiteral.end,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
cursor = skipToken(source, cursor);
|
|
276
|
-
}
|
|
277
|
-
return undefined;
|
|
278
|
-
}
|
|
279
|
-
function parseExportSpecifierAt(source, index) {
|
|
280
|
-
let cursor = index;
|
|
281
|
-
let isExportType = false;
|
|
282
|
-
const exportClauseStart = cursor;
|
|
283
|
-
while (cursor < source.length) {
|
|
284
|
-
cursor = skipWhitespaceAndComments(source, cursor);
|
|
285
|
-
if (isKeywordAt(source, cursor, "type")) {
|
|
286
|
-
isExportType = true;
|
|
287
|
-
cursor = cursorAfterKeyword("type", cursor);
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
if (source[cursor] === ";" || source[cursor] === "\n") {
|
|
291
|
-
return { end: cursor + 1 };
|
|
292
|
-
}
|
|
293
|
-
if (isKeywordAt(source, cursor, "from")) {
|
|
294
|
-
const fromCursor = skipWhitespaceAndComments(source, cursorAfterKeyword("from", cursor));
|
|
295
|
-
const stringLiteral = readStringLiteral(source, fromCursor);
|
|
296
|
-
if (!stringLiteral)
|
|
297
|
-
return { end: fromCursor };
|
|
298
|
-
return {
|
|
299
|
-
specifier: stringLiteral.value,
|
|
300
|
-
kind: isExportType
|
|
301
|
-
? "type"
|
|
302
|
-
: inferImportClauseKind(source.slice(exportClauseStart, cursor)),
|
|
303
|
-
end: stringLiteral.end,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
cursor = skipToken(source, cursor);
|
|
307
|
-
}
|
|
308
|
-
return undefined;
|
|
309
|
-
}
|
|
310
|
-
function inferImportClauseKind(importClause) {
|
|
311
|
-
const trimmed = importClause.trim();
|
|
312
|
-
if (/^\{\s*type\s+[^,}]+(?:,\s*type\s+[^,}]+)*,?\s*\}$/.test(trimmed)) {
|
|
313
|
-
return "type";
|
|
314
|
-
}
|
|
315
|
-
return "value";
|
|
316
|
-
}
|
|
317
|
-
function cursorAfterKeyword(keyword, index) {
|
|
318
|
-
return index + keyword.length;
|
|
319
|
-
}
|
|
320
|
-
function skipNonCode(source, index) {
|
|
321
|
-
let cursor = index;
|
|
322
|
-
while (cursor < source.length) {
|
|
323
|
-
if (source.startsWith("//", cursor)) {
|
|
324
|
-
cursor = skipLineComment(source, cursor);
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
if (source.startsWith("/*", cursor)) {
|
|
328
|
-
cursor = skipBlockComment(source, cursor);
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
if (source[cursor] === '"' || source[cursor] === "'") {
|
|
332
|
-
cursor = skipStringLiteral(source, cursor);
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
if (source[cursor] === "`") {
|
|
336
|
-
cursor = skipTemplateLiteral(source, cursor);
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
return cursor;
|
|
342
|
-
}
|
|
343
|
-
function skipWhitespaceAndComments(source, index) {
|
|
344
|
-
let cursor = index;
|
|
345
|
-
while (cursor < source.length) {
|
|
346
|
-
if (/\s/.test(source[cursor])) {
|
|
347
|
-
cursor++;
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
if (source.startsWith("//", cursor)) {
|
|
351
|
-
cursor = skipLineComment(source, cursor);
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
if (source.startsWith("/*", cursor)) {
|
|
355
|
-
cursor = skipBlockComment(source, cursor);
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
return cursor;
|
|
361
|
-
}
|
|
362
|
-
function skipToken(source, index) {
|
|
363
|
-
if (source[index] === '"' || source[index] === "'") {
|
|
364
|
-
return skipStringLiteral(source, index);
|
|
365
|
-
}
|
|
366
|
-
if (source[index] === "`") {
|
|
367
|
-
return skipTemplateLiteral(source, index);
|
|
368
|
-
}
|
|
369
|
-
return index + 1;
|
|
370
|
-
}
|
|
371
|
-
function readStringLiteral(source, index) {
|
|
372
|
-
const quote = source[index];
|
|
373
|
-
if (quote !== '"' && quote !== "'")
|
|
374
|
-
return undefined;
|
|
375
|
-
let value = "";
|
|
376
|
-
let escaped = false;
|
|
377
|
-
for (let cursor = index + 1; cursor < source.length; cursor++) {
|
|
378
|
-
const char = source[cursor];
|
|
379
|
-
if (escaped) {
|
|
380
|
-
value += char;
|
|
381
|
-
escaped = false;
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
if (char === "\\") {
|
|
385
|
-
escaped = true;
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
if (char === quote) {
|
|
389
|
-
return { value, end: cursor + 1 };
|
|
390
|
-
}
|
|
391
|
-
value += char;
|
|
392
|
-
}
|
|
393
|
-
return undefined;
|
|
394
|
-
}
|
|
395
|
-
function skipStringLiteral(source, index) {
|
|
396
|
-
return readStringLiteral(source, index)?.end ?? source.length;
|
|
397
|
-
}
|
|
398
|
-
function skipTemplateLiteral(source, index) {
|
|
399
|
-
let escaped = false;
|
|
400
|
-
for (let cursor = index + 1; cursor < source.length; cursor++) {
|
|
401
|
-
const char = source[cursor];
|
|
402
|
-
if (escaped) {
|
|
403
|
-
escaped = false;
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
if (char === "\\") {
|
|
407
|
-
escaped = true;
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
if (char === "`") {
|
|
411
|
-
return cursor + 1;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return source.length;
|
|
415
|
-
}
|
|
416
|
-
function skipLineComment(source, index) {
|
|
417
|
-
const end = source.indexOf("\n", index + 2);
|
|
418
|
-
return end === -1 ? source.length : end + 1;
|
|
419
|
-
}
|
|
420
|
-
function skipBlockComment(source, index) {
|
|
421
|
-
const end = source.indexOf("*/", index + 2);
|
|
422
|
-
return end === -1 ? source.length : end + 2;
|
|
423
|
-
}
|
|
424
|
-
function isKeywordAt(source, index, keyword) {
|
|
425
|
-
if (!source.startsWith(keyword, index))
|
|
426
|
-
return false;
|
|
427
|
-
const before = source[index - 1];
|
|
428
|
-
const after = source[index + keyword.length];
|
|
429
|
-
return !isIdentifierChar(before) && !isIdentifierChar(after);
|
|
430
|
-
}
|
|
431
|
-
function isIdentifierChar(char) {
|
|
432
|
-
return Boolean(char && /[A-Za-z0-9_$]/.test(char));
|
|
433
|
-
}
|
|
434
|
-
function resolveImport(importPath, importerFile, files, atAliasRoot) {
|
|
435
|
-
if (importPath.startsWith("@/")) {
|
|
436
|
-
return {
|
|
437
|
-
resolvedPath: resolveLocalImportPath(stripKnownExtension(path.posix.join(atAliasRoot, importPath.slice(2))), files),
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
if (importPath.startsWith(".")) {
|
|
441
|
-
return {
|
|
442
|
-
resolvedPath: resolveLocalImportPath(stripKnownExtension(normalizePath(path.join(path.dirname(importerFile), importPath))), files),
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
return {
|
|
446
|
-
packageName: packageName(importPath),
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
function resolveLocalImportPath(importPath, files) {
|
|
450
|
-
const candidates = [
|
|
451
|
-
importPath,
|
|
452
|
-
`${importPath}.ts`,
|
|
453
|
-
`${importPath}.tsx`,
|
|
454
|
-
`${importPath}.mts`,
|
|
455
|
-
`${importPath}.cts`,
|
|
456
|
-
`${importPath}/index.ts`,
|
|
457
|
-
`${importPath}/index.tsx`,
|
|
458
|
-
`${importPath}/index.mts`,
|
|
459
|
-
`${importPath}/index.cts`,
|
|
460
|
-
];
|
|
461
|
-
for (const candidate of candidates) {
|
|
462
|
-
for (const file of files) {
|
|
463
|
-
if (stripKnownExtension(file) === stripKnownExtension(candidate)) {
|
|
464
|
-
return file;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
return stripKnownExtension(importPath);
|
|
138
|
+
});
|
|
469
139
|
}
|
|
470
140
|
function packageName(importPath) {
|
|
471
141
|
if (!importPath.startsWith("@"))
|
|
@@ -520,6 +190,34 @@ function lintImport(file, sourceLayer, reference, config) {
|
|
|
520
190
|
}
|
|
521
191
|
return undefined;
|
|
522
192
|
}
|
|
193
|
+
function lintUnresolvedModuleReference(file, sourceLayer, reference) {
|
|
194
|
+
if (reference.staticallyResolvable ||
|
|
195
|
+
![
|
|
196
|
+
"agent-capability",
|
|
197
|
+
"domain",
|
|
198
|
+
"feature-helper",
|
|
199
|
+
"feature-port",
|
|
200
|
+
"infra",
|
|
201
|
+
"policy",
|
|
202
|
+
"port",
|
|
203
|
+
"route",
|
|
204
|
+
"seed",
|
|
205
|
+
"use-case",
|
|
206
|
+
"workflow",
|
|
207
|
+
].includes(sourceLayer)) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
severity: "error",
|
|
212
|
+
code: "BEIGNET_MODULE_REFERENCE_UNRESOLVED",
|
|
213
|
+
file,
|
|
214
|
+
importPath: reference.importPath,
|
|
215
|
+
message: `${layerLabel(sourceLayer)} files must use a string literal for ${moduleSyntaxLabel(reference.syntax)} so Beignet can verify the dependency boundary (${reference.importPath}).`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function moduleSyntaxLabel(syntax) {
|
|
219
|
+
return syntax === "require" ? "require()" : "dynamic import()";
|
|
220
|
+
}
|
|
523
221
|
function appImportViolation(sourceLayer, targetLayer) {
|
|
524
222
|
if (!targetLayer)
|
|
525
223
|
return undefined;
|
|
@@ -527,28 +225,89 @@ function appImportViolation(sourceLayer, targetLayer) {
|
|
|
527
225
|
[
|
|
528
226
|
"app",
|
|
529
227
|
"app-context",
|
|
228
|
+
"agent-capability",
|
|
530
229
|
"client",
|
|
531
230
|
"component",
|
|
532
231
|
"contract",
|
|
232
|
+
"feature-helper",
|
|
533
233
|
"feature-port",
|
|
534
234
|
"infra",
|
|
535
235
|
"lib",
|
|
536
236
|
"policy",
|
|
537
237
|
"port",
|
|
538
238
|
"route",
|
|
239
|
+
"schema",
|
|
240
|
+
"seed",
|
|
241
|
+
"server",
|
|
242
|
+
"use-case",
|
|
243
|
+
"workflow",
|
|
244
|
+
].includes(targetLayer)) {
|
|
245
|
+
return targetLayer;
|
|
246
|
+
}
|
|
247
|
+
if (sourceLayer === "agent-capability" &&
|
|
248
|
+
[
|
|
249
|
+
"agent-capability",
|
|
250
|
+
"app",
|
|
251
|
+
"app-context",
|
|
252
|
+
"client",
|
|
253
|
+
"component",
|
|
254
|
+
"infra",
|
|
255
|
+
"route",
|
|
256
|
+
"seed",
|
|
257
|
+
"server",
|
|
258
|
+
"workflow",
|
|
259
|
+
].includes(targetLayer)) {
|
|
260
|
+
return targetLayer;
|
|
261
|
+
}
|
|
262
|
+
if (sourceLayer === "feature-helper" &&
|
|
263
|
+
[
|
|
264
|
+
"agent-capability",
|
|
265
|
+
"app",
|
|
266
|
+
"app-context",
|
|
267
|
+
"client",
|
|
268
|
+
"component",
|
|
269
|
+
"infra",
|
|
270
|
+
"route",
|
|
271
|
+
"seed",
|
|
539
272
|
"server",
|
|
540
273
|
"use-case",
|
|
541
274
|
"workflow",
|
|
542
275
|
].includes(targetLayer)) {
|
|
543
276
|
return targetLayer;
|
|
544
277
|
}
|
|
545
|
-
if (
|
|
546
|
-
[
|
|
278
|
+
if (["use-case", "workflow"].includes(sourceLayer) &&
|
|
279
|
+
[
|
|
280
|
+
"agent-capability",
|
|
281
|
+
"app",
|
|
282
|
+
"app-context",
|
|
283
|
+
"client",
|
|
284
|
+
"component",
|
|
285
|
+
"infra",
|
|
286
|
+
"route",
|
|
287
|
+
"seed",
|
|
288
|
+
"server",
|
|
289
|
+
].includes(targetLayer)) {
|
|
290
|
+
return targetLayer;
|
|
291
|
+
}
|
|
292
|
+
if (sourceLayer === "seed" &&
|
|
293
|
+
[
|
|
294
|
+
"agent-capability",
|
|
295
|
+
"app",
|
|
296
|
+
"app-context",
|
|
297
|
+
"client",
|
|
298
|
+
"component",
|
|
299
|
+
"infra",
|
|
300
|
+
"route",
|
|
301
|
+
"server",
|
|
302
|
+
"use-case",
|
|
303
|
+
"workflow",
|
|
304
|
+
].includes(targetLayer)) {
|
|
547
305
|
return targetLayer;
|
|
548
306
|
}
|
|
549
307
|
if (["feature-port", "policy", "port"].includes(sourceLayer) &&
|
|
550
308
|
[
|
|
551
309
|
"app",
|
|
310
|
+
"agent-capability",
|
|
552
311
|
"client",
|
|
553
312
|
"component",
|
|
554
313
|
"infra",
|
|
@@ -559,11 +318,46 @@ function appImportViolation(sourceLayer, targetLayer) {
|
|
|
559
318
|
return targetLayer;
|
|
560
319
|
}
|
|
561
320
|
if (sourceLayer === "contract" &&
|
|
562
|
-
[
|
|
321
|
+
[
|
|
322
|
+
"agent-capability",
|
|
323
|
+
"app",
|
|
324
|
+
"client",
|
|
325
|
+
"component",
|
|
326
|
+
"infra",
|
|
327
|
+
"route",
|
|
328
|
+
"seed",
|
|
329
|
+
"workflow",
|
|
330
|
+
].includes(targetLayer)) {
|
|
563
331
|
return targetLayer;
|
|
564
332
|
}
|
|
565
333
|
if (sourceLayer === "route" &&
|
|
566
|
-
[
|
|
334
|
+
[
|
|
335
|
+
"agent-capability",
|
|
336
|
+
"app",
|
|
337
|
+
"app-context",
|
|
338
|
+
"client",
|
|
339
|
+
"component",
|
|
340
|
+
"infra",
|
|
341
|
+
"seed",
|
|
342
|
+
"server",
|
|
343
|
+
"workflow",
|
|
344
|
+
].includes(targetLayer)) {
|
|
345
|
+
return targetLayer;
|
|
346
|
+
}
|
|
347
|
+
if (sourceLayer === "schema" &&
|
|
348
|
+
[
|
|
349
|
+
"agent-capability",
|
|
350
|
+
"app",
|
|
351
|
+
"app-context",
|
|
352
|
+
"client",
|
|
353
|
+
"component",
|
|
354
|
+
"infra",
|
|
355
|
+
"route",
|
|
356
|
+
"seed",
|
|
357
|
+
"server",
|
|
358
|
+
"use-case",
|
|
359
|
+
"workflow",
|
|
360
|
+
].includes(targetLayer)) {
|
|
567
361
|
return targetLayer;
|
|
568
362
|
}
|
|
569
363
|
if (sourceLayer === "infra" &&
|
|
@@ -579,9 +373,13 @@ function packageImportViolation(sourceLayer, importPath) {
|
|
|
579
373
|
}
|
|
580
374
|
if ([
|
|
581
375
|
"contract",
|
|
376
|
+
"agent-capability",
|
|
377
|
+
"feature-helper",
|
|
582
378
|
"feature-port",
|
|
583
379
|
"policy",
|
|
584
380
|
"port",
|
|
381
|
+
"schema",
|
|
382
|
+
"seed",
|
|
585
383
|
"use-case",
|
|
586
384
|
"workflow",
|
|
587
385
|
].includes(sourceLayer)) {
|
|
@@ -611,10 +409,75 @@ function matchBeignetProviderPackage(importPath) {
|
|
|
611
409
|
const name = packageName(importPath);
|
|
612
410
|
return name.startsWith("@beignet/provider-") ? name : undefined;
|
|
613
411
|
}
|
|
614
|
-
function
|
|
412
|
+
function lintTransitiveBoundaries({ config, graph, }) {
|
|
413
|
+
const diagnostics = [];
|
|
414
|
+
for (const root of graph.keys()) {
|
|
415
|
+
const rootLayer = classifyPath(root, config);
|
|
416
|
+
if (![
|
|
417
|
+
"agent-capability",
|
|
418
|
+
"domain",
|
|
419
|
+
"feature-helper",
|
|
420
|
+
"feature-port",
|
|
421
|
+
"infra",
|
|
422
|
+
"policy",
|
|
423
|
+
"port",
|
|
424
|
+
"route",
|
|
425
|
+
"seed",
|
|
426
|
+
"use-case",
|
|
427
|
+
"workflow",
|
|
428
|
+
].includes(rootLayer)) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
diagnostics.push(...lintTransitiveBoundaryRoot({ config, graph, root, rootLayer }));
|
|
432
|
+
}
|
|
433
|
+
return diagnostics;
|
|
434
|
+
}
|
|
435
|
+
function lintTransitiveBoundaryRoot({ config, graph, root, rootLayer, }) {
|
|
436
|
+
const diagnostics = [];
|
|
437
|
+
const visited = new Set();
|
|
438
|
+
const queue = [
|
|
439
|
+
{ file: root, chain: [root] },
|
|
440
|
+
];
|
|
441
|
+
let queueIndex = 0;
|
|
442
|
+
while (queueIndex < queue.length) {
|
|
443
|
+
const current = queue[queueIndex++];
|
|
444
|
+
if (visited.has(current.file))
|
|
445
|
+
continue;
|
|
446
|
+
visited.add(current.file);
|
|
447
|
+
for (const reference of graph.get(current.file) ?? []) {
|
|
448
|
+
if (reference.kind === "type")
|
|
449
|
+
continue;
|
|
450
|
+
const chain = reference.resolvedPath
|
|
451
|
+
? [...current.chain, reference.resolvedPath]
|
|
452
|
+
: current.chain;
|
|
453
|
+
const violation = current.file === root
|
|
454
|
+
? undefined
|
|
455
|
+
: (lintUnresolvedModuleReference(root, rootLayer, reference) ??
|
|
456
|
+
lintImport(root, rootLayer, reference, config));
|
|
457
|
+
if (violation) {
|
|
458
|
+
diagnostics.push({
|
|
459
|
+
...violation,
|
|
460
|
+
file: root,
|
|
461
|
+
message: `${layerLabel(rootLayer)} files must not reach forbidden dependencies through ${formatImportChain(chain)}. ${violation.message}`,
|
|
462
|
+
});
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (reference.resolvedPath &&
|
|
466
|
+
graph.has(reference.resolvedPath) &&
|
|
467
|
+
isTransparentArchitectureLayer(classifyPath(reference.resolvedPath, config))) {
|
|
468
|
+
queue.push({ file: reference.resolvedPath, chain });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return diagnostics;
|
|
473
|
+
}
|
|
474
|
+
function isTransparentArchitectureLayer(layer) {
|
|
475
|
+
return ["feature-helper", "lib", "unknown"].includes(layer);
|
|
476
|
+
}
|
|
477
|
+
function lintRuntimeBoundary({ config, graph, sourceFileByPath, }) {
|
|
615
478
|
const diagnostics = [];
|
|
616
479
|
for (const file of graph.keys()) {
|
|
617
|
-
const rootKind = runtimeBoundaryRootKind(file,
|
|
480
|
+
const rootKind = runtimeBoundaryRootKind(file, sourceFileByPath.get(file), config);
|
|
618
481
|
if (!rootKind)
|
|
619
482
|
continue;
|
|
620
483
|
diagnostics.push(...lintRuntimeBoundaryRoot({
|
|
@@ -632,10 +495,9 @@ function lintRuntimeBoundaryRoot({ root, rootKind, config, graph, }) {
|
|
|
632
495
|
const queue = [
|
|
633
496
|
{ file: root, chain: [root] },
|
|
634
497
|
];
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
continue;
|
|
498
|
+
let queueIndex = 0;
|
|
499
|
+
while (queueIndex < queue.length) {
|
|
500
|
+
const current = queue[queueIndex++];
|
|
639
501
|
if (visited.has(current.file))
|
|
640
502
|
continue;
|
|
641
503
|
visited.add(current.file);
|
|
@@ -647,7 +509,7 @@ function lintRuntimeBoundaryRoot({ root, rootKind, config, graph, }) {
|
|
|
647
509
|
const chain = reference.resolvedPath
|
|
648
510
|
? [...current.chain, reference.resolvedPath]
|
|
649
511
|
: current.chain;
|
|
650
|
-
const violation = runtimeBoundaryViolation(reference, config);
|
|
512
|
+
const violation = runtimeBoundaryViolation(reference, config, rootKind);
|
|
651
513
|
if (violation) {
|
|
652
514
|
diagnostics.push({
|
|
653
515
|
severity: "error",
|
|
@@ -668,36 +530,54 @@ function lintRuntimeBoundaryRoot({ root, rootKind, config, graph, }) {
|
|
|
668
530
|
}
|
|
669
531
|
return diagnostics;
|
|
670
532
|
}
|
|
671
|
-
function runtimeBoundaryRootKind(file,
|
|
533
|
+
function runtimeBoundaryRootKind(file, sourceFile, config) {
|
|
672
534
|
const layer = classifyPath(file, config);
|
|
673
535
|
if (layer === "contract")
|
|
674
536
|
return "contract";
|
|
537
|
+
if (layer === "schema")
|
|
538
|
+
return "schema";
|
|
675
539
|
if (layer === "client")
|
|
676
540
|
return "client";
|
|
677
|
-
if (hasUseClientDirective(
|
|
541
|
+
if (sourceFile && hasUseClientDirective(sourceFile))
|
|
678
542
|
return "client";
|
|
679
543
|
if (isGeneratedClientAdapter(file, config))
|
|
680
544
|
return "client";
|
|
681
545
|
return undefined;
|
|
682
546
|
}
|
|
683
|
-
function hasUseClientDirective(
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
547
|
+
function hasUseClientDirective(sourceFile) {
|
|
548
|
+
for (const statement of sourceFile.statements) {
|
|
549
|
+
if (!ts.isExpressionStatement(statement) ||
|
|
550
|
+
!ts.isStringLiteral(statement.expression)) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
if (statement.expression.text === "use client")
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
return false;
|
|
689
557
|
}
|
|
690
558
|
function isGeneratedClientAdapter(file, config) {
|
|
691
559
|
const clientDir = clientDirPath(config);
|
|
692
560
|
return ["index.ts", "api-client.ts", "rhf.ts", "rq.ts", "uploads.ts"].some((adapterFile) => file === path.posix.join(clientDir, adapterFile));
|
|
693
561
|
}
|
|
694
|
-
function runtimeBoundaryViolation(reference, config) {
|
|
562
|
+
function runtimeBoundaryViolation(reference, config, rootKind) {
|
|
563
|
+
if (!reference.staticallyResolvable) {
|
|
564
|
+
return `computed ${moduleSyntaxLabel(reference.syntax)} ${reference.importPath}, which cannot be verified statically`;
|
|
565
|
+
}
|
|
695
566
|
if (reference.resolvedPath) {
|
|
696
567
|
const targetLayer = classifyPath(reference.resolvedPath, config);
|
|
697
|
-
|
|
568
|
+
const prohibitedLayer = rootKind === "client"
|
|
569
|
+
? undefined
|
|
570
|
+
: appImportViolation(rootKind, targetLayer);
|
|
571
|
+
if (prohibitedLayer) {
|
|
572
|
+
return `${layerLabel(prohibitedLayer)} module ${reference.resolvedPath}`;
|
|
573
|
+
}
|
|
574
|
+
if (targetLayer === "agent-capability" ||
|
|
575
|
+
targetLayer === "use-case" ||
|
|
698
576
|
targetLayer === "route" ||
|
|
699
577
|
targetLayer === "infra" ||
|
|
700
|
-
targetLayer === "
|
|
578
|
+
targetLayer === "seed" ||
|
|
579
|
+
targetLayer === "server" ||
|
|
580
|
+
targetLayer === "workflow") {
|
|
701
581
|
return `${layerLabel(targetLayer)} module ${reference.resolvedPath}`;
|
|
702
582
|
}
|
|
703
583
|
if (targetLayer === "app-context") {
|
|
@@ -707,20 +587,34 @@ function runtimeBoundaryViolation(reference, config) {
|
|
|
707
587
|
if (reference.importPath === "@beignet/core/server-only") {
|
|
708
588
|
return "server-only marker @beignet/core/server-only";
|
|
709
589
|
}
|
|
710
|
-
if (reference.importPath
|
|
590
|
+
if (isNodeRuntimeModule(reference.importPath)) {
|
|
711
591
|
return `Node runtime module ${reference.importPath}`;
|
|
712
592
|
}
|
|
713
|
-
if (
|
|
593
|
+
if (rootKind !== "client" && reference.packageName) {
|
|
594
|
+
const prohibitedPackage = packageImportViolation(rootKind, reference.importPath);
|
|
595
|
+
if (prohibitedPackage) {
|
|
596
|
+
return `runtime or provider package ${prohibitedPackage}`;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (matchBannedPackage(serverRuntimePackages, reference.importPath) ||
|
|
714
600
|
reference.importPath.startsWith("@beignet/provider-")) {
|
|
715
601
|
return `server runtime package ${reference.importPath}`;
|
|
716
602
|
}
|
|
717
603
|
return undefined;
|
|
718
604
|
}
|
|
605
|
+
const nodeRuntimeModules = new Set(builtinModules);
|
|
606
|
+
function isNodeRuntimeModule(importPath) {
|
|
607
|
+
return importPath.startsWith("node:") || nodeRuntimeModules.has(importPath);
|
|
608
|
+
}
|
|
719
609
|
function runtimeBoundaryMessage(rootKind, violation, chain) {
|
|
720
610
|
return `${rootKindLabel(rootKind)} roots must not value-import server-only code. Reached ${violation} through ${formatImportChain(chain)}. Move shared DTO and validation schemas to features/<feature>/schemas.ts, keep use cases/routes/infra/server modules behind server entrypoints, or import server-shaped modules with import type only.`;
|
|
721
611
|
}
|
|
722
612
|
function rootKindLabel(rootKind) {
|
|
723
|
-
|
|
613
|
+
if (rootKind === "contract")
|
|
614
|
+
return "contract";
|
|
615
|
+
if (rootKind === "schema")
|
|
616
|
+
return "schema";
|
|
617
|
+
return "client";
|
|
724
618
|
}
|
|
725
619
|
function formatImportChain(chain) {
|
|
726
620
|
return chain.join(" -> ");
|
|
@@ -821,85 +715,6 @@ function appImportMessage(sourceLayer, importPath, targetLayer) {
|
|
|
821
715
|
function packageImportMessage(sourceLayer, packageName) {
|
|
822
716
|
return `${layerLabel(sourceLayer)} files must not import runtime or provider package ${packageName}. Depend on Beignet ports or app-owned ports instead.`;
|
|
823
717
|
}
|
|
824
|
-
function classifyPath(filePath, config) {
|
|
825
|
-
const normalizedPath = stripKnownExtension(normalizePath(filePath));
|
|
826
|
-
const featuresPath = directoryPath(config.paths.features);
|
|
827
|
-
const portsDir = directoryPath(path.dirname(config.paths.ports));
|
|
828
|
-
if (isTestPath(normalizedPath))
|
|
829
|
-
return "test";
|
|
830
|
-
if (normalizedPath === stripKnownExtension(config.paths.appContext)) {
|
|
831
|
-
return "app-context";
|
|
832
|
-
}
|
|
833
|
-
if (isUnder(normalizedPath, directoryPath(path.dirname(config.paths.server)))) {
|
|
834
|
-
return "server";
|
|
835
|
-
}
|
|
836
|
-
if (isUnder(normalizedPath, directoryPath(path.dirname(config.paths.routes)))) {
|
|
837
|
-
return "app";
|
|
838
|
-
}
|
|
839
|
-
if (isUnder(normalizedPath, directoryPath(path.dirname(config.paths.portWiring)))) {
|
|
840
|
-
return "infra";
|
|
841
|
-
}
|
|
842
|
-
if (isUnder(normalizedPath, portsDir))
|
|
843
|
-
return "port";
|
|
844
|
-
if (isUnder(normalizedPath, clientDirPath(config)))
|
|
845
|
-
return "client";
|
|
846
|
-
if (isUnder(normalizedPath, "components"))
|
|
847
|
-
return "component";
|
|
848
|
-
if (isUnder(normalizedPath, legacySharedDomainPath(config)))
|
|
849
|
-
return "domain";
|
|
850
|
-
if (isUnder(normalizedPath, "lib"))
|
|
851
|
-
return "lib";
|
|
852
|
-
const featurePath = featureRelativePath(normalizedPath, featuresPath);
|
|
853
|
-
if (featurePath) {
|
|
854
|
-
const layerSegment = featurePath.split("/")[1];
|
|
855
|
-
if (layerSegment === "domain")
|
|
856
|
-
return "domain";
|
|
857
|
-
if (layerSegment === "contracts")
|
|
858
|
-
return "contract";
|
|
859
|
-
if (layerSegment?.endsWith("-contracts"))
|
|
860
|
-
return "contract";
|
|
861
|
-
if (layerSegment === "routes")
|
|
862
|
-
return "route";
|
|
863
|
-
if (layerSegment === "ports")
|
|
864
|
-
return "feature-port";
|
|
865
|
-
if (layerSegment === "policy")
|
|
866
|
-
return "policy";
|
|
867
|
-
if (layerSegment === "use-cases")
|
|
868
|
-
return "use-case";
|
|
869
|
-
if ([
|
|
870
|
-
"jobs",
|
|
871
|
-
"listeners",
|
|
872
|
-
"notifications",
|
|
873
|
-
"schedules",
|
|
874
|
-
"tasks",
|
|
875
|
-
"uploads",
|
|
876
|
-
].includes(layerSegment)) {
|
|
877
|
-
return "workflow";
|
|
878
|
-
}
|
|
879
|
-
if (layerSegment === "components")
|
|
880
|
-
return "component";
|
|
881
|
-
if (layerSegment === "client")
|
|
882
|
-
return "client";
|
|
883
|
-
}
|
|
884
|
-
if (directoryPath(config.paths.contracts) !== featuresPath &&
|
|
885
|
-
isUnder(normalizedPath, directoryPath(config.paths.contracts))) {
|
|
886
|
-
return "contract";
|
|
887
|
-
}
|
|
888
|
-
if (directoryPath(config.paths.useCases) !== featuresPath &&
|
|
889
|
-
isUnder(normalizedPath, directoryPath(config.paths.useCases))) {
|
|
890
|
-
return "use-case";
|
|
891
|
-
}
|
|
892
|
-
if (directoryPath(config.paths.policies) !== featuresPath &&
|
|
893
|
-
isUnder(normalizedPath, directoryPath(config.paths.policies))) {
|
|
894
|
-
return "policy";
|
|
895
|
-
}
|
|
896
|
-
return "unknown";
|
|
897
|
-
}
|
|
898
|
-
function isTestPath(filePath) {
|
|
899
|
-
return (filePath.includes("/tests/") ||
|
|
900
|
-
filePath.endsWith(".test") ||
|
|
901
|
-
filePath.endsWith(".spec"));
|
|
902
|
-
}
|
|
903
718
|
function isUnder(filePath, directory) {
|
|
904
719
|
const normalizedDir = directoryPath(directory);
|
|
905
720
|
if (!normalizedDir || normalizedDir === ".")
|
|
@@ -917,10 +732,10 @@ function featureLayerInfo(filePath, config) {
|
|
|
917
732
|
const relativePath = featureRelativePath(normalizedPath, directoryPath(config.paths.features));
|
|
918
733
|
if (!relativePath)
|
|
919
734
|
return undefined;
|
|
920
|
-
const [feature
|
|
735
|
+
const [feature] = relativePath.split("/");
|
|
921
736
|
if (!feature)
|
|
922
737
|
return undefined;
|
|
923
|
-
return { feature, layer };
|
|
738
|
+
return { feature, layer: classifyPath(filePath, config) };
|
|
924
739
|
}
|
|
925
740
|
function legacySharedDomainPath(config) {
|
|
926
741
|
const featuresDir = directoryPath(config.paths.features);
|
|
@@ -943,7 +758,14 @@ function dedupeDiagnostics(diagnostics) {
|
|
|
943
758
|
const seen = new Set();
|
|
944
759
|
const deduped = [];
|
|
945
760
|
for (const diagnostic of diagnostics) {
|
|
946
|
-
const key =
|
|
761
|
+
const key = JSON.stringify([
|
|
762
|
+
diagnostic.code,
|
|
763
|
+
diagnostic.file,
|
|
764
|
+
diagnostic.importPath,
|
|
765
|
+
diagnostic.line,
|
|
766
|
+
diagnostic.column,
|
|
767
|
+
diagnostic.message,
|
|
768
|
+
]);
|
|
947
769
|
if (seen.has(key))
|
|
948
770
|
continue;
|
|
949
771
|
seen.add(key);
|