@beignet/cli 0.0.1 → 0.0.4
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 +231 -0
- package/README.md +497 -90
- package/dist/ansi.d.ts +10 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +20 -0
- package/dist/ansi.js.map +1 -0
- package/dist/choices.d.ts +72 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +88 -0
- package/dist/choices.js.map +1 -0
- package/dist/completion.d.ts +47 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +123 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +42 -0
- package/dist/create-prompts.d.ts.map +1 -0
- package/dist/create-prompts.js +136 -0
- package/dist/create-prompts.js.map +1 -0
- package/dist/create.d.ts +12 -0
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +19 -24
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +37 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +146 -0
- package/dist/db.js.map +1 -0
- package/dist/github-annotations.d.ts +18 -0
- package/dist/github-annotations.d.ts.map +1 -0
- package/dist/github-annotations.js +22 -0
- package/dist/github-annotations.js.map +1 -0
- package/dist/index.d.ts +1 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +710 -400
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +45 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +2191 -100
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +20 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +17 -0
- package/dist/lib.js.map +1 -0
- package/dist/lint.d.ts +22 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +370 -38
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +109 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +2225 -333
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +24 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +138 -0
- package/dist/outbox.js.map +1 -0
- package/dist/schedule.d.ts +36 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +155 -0
- package/dist/schedule.js.map +1 -0
- package/dist/task.d.ts +26 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +106 -0
- package/dist/task.js.map +1 -0
- package/dist/templates.d.ts +12 -8
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +2144 -385
- package/dist/templates.js.map +1 -1
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -8
- package/src/ansi.ts +30 -0
- package/src/choices.ts +137 -0
- package/src/completion.ts +169 -0
- package/src/config.ts +47 -0
- package/src/create-prompts.ts +182 -0
- package/src/create.ts +32 -28
- package/src/db.ts +222 -0
- package/src/github-annotations.ts +37 -0
- package/src/index.ts +1119 -535
- package/src/inspect.ts +3372 -134
- package/src/lib.ts +45 -0
- package/src/lint.ts +533 -45
- package/src/make.ts +3010 -397
- package/src/outbox.ts +249 -0
- package/src/schedule.ts +272 -0
- package/src/task.ts +169 -0
- package/src/templates.ts +2282 -462
- package/src/version.ts +20 -0
- package/dist/create-bin.d.ts +0 -3
- package/dist/create-bin.d.ts.map +0 -1
- package/dist/create-bin.js +0 -9
- package/dist/create-bin.js.map +0 -1
- package/src/create-bin.ts +0 -11
package/dist/lint.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { createPainter } from "./ansi.js";
|
|
3
4
|
import { directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
|
|
5
|
+
import { formatGithubAnnotation } from "./github-annotations.js";
|
|
6
|
+
/**
|
|
7
|
+
* Inspect app imports and report dependency-direction violations.
|
|
8
|
+
*/
|
|
4
9
|
export async function lintApp(options = {}) {
|
|
5
10
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
6
11
|
await assertDirectory(targetDir);
|
|
@@ -9,34 +14,98 @@ export async function lintApp(options = {}) {
|
|
|
9
14
|
? resolveConfig(options.config)
|
|
10
15
|
: await loadBeignetConfig(targetDir, files);
|
|
11
16
|
const diagnostics = [];
|
|
12
|
-
|
|
17
|
+
const sourceFiles = lintSourceFiles(files);
|
|
18
|
+
const sourceFileSet = new Set(sourceFiles);
|
|
19
|
+
const sourceByFile = new Map();
|
|
20
|
+
const importGraph = new Map();
|
|
21
|
+
for (const file of sourceFiles) {
|
|
13
22
|
const sourceLayer = classifyPath(file, config);
|
|
23
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
24
|
+
sourceByFile.set(file, source);
|
|
25
|
+
importGraph.set(file, parseImports(source, file, sourceFileSet));
|
|
14
26
|
if (sourceLayer === "test" || sourceLayer === "unknown")
|
|
15
27
|
continue;
|
|
16
|
-
const
|
|
17
|
-
for (const reference of
|
|
28
|
+
const lineStarts = computeLineStarts(source);
|
|
29
|
+
for (const reference of importGraph.get(file) ?? []) {
|
|
18
30
|
const diagnostic = lintImport(file, sourceLayer, reference, config);
|
|
19
|
-
if (diagnostic)
|
|
20
|
-
diagnostics.push(
|
|
31
|
+
if (diagnostic) {
|
|
32
|
+
diagnostics.push({
|
|
33
|
+
...diagnostic,
|
|
34
|
+
...positionFromOffset(lineStarts, reference.offset),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
21
37
|
}
|
|
22
38
|
}
|
|
39
|
+
diagnostics.push(...lintRuntimeBoundary({
|
|
40
|
+
config,
|
|
41
|
+
graph: importGraph,
|
|
42
|
+
sourceByFile,
|
|
43
|
+
}));
|
|
23
44
|
return {
|
|
45
|
+
schemaVersion: 1,
|
|
24
46
|
targetDir,
|
|
25
47
|
config,
|
|
26
48
|
diagnostics: dedupeDiagnostics(diagnostics),
|
|
27
49
|
};
|
|
28
50
|
}
|
|
29
|
-
|
|
51
|
+
function computeLineStarts(source) {
|
|
52
|
+
const lineStarts = [0];
|
|
53
|
+
for (let index = 0; index < source.length; index++) {
|
|
54
|
+
if (source[index] === "\n") {
|
|
55
|
+
lineStarts.push(index + 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return lineStarts;
|
|
59
|
+
}
|
|
60
|
+
function positionFromOffset(lineStarts, offset) {
|
|
61
|
+
let low = 0;
|
|
62
|
+
let high = lineStarts.length - 1;
|
|
63
|
+
while (low < high) {
|
|
64
|
+
const middle = Math.ceil((low + high) / 2);
|
|
65
|
+
if (lineStarts[middle] <= offset) {
|
|
66
|
+
low = middle;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
high = middle - 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { line: low + 1, column: offset - lineStarts[low] + 1 };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Format dependency-direction lint diagnostics for CLI output.
|
|
76
|
+
*/
|
|
77
|
+
export function formatLint(result, options = {}) {
|
|
78
|
+
const paint = createPainter(options.color);
|
|
30
79
|
if (result.diagnostics.length === 0) {
|
|
31
80
|
return `No Beignet lint issues found in ${result.targetDir}.`;
|
|
32
81
|
}
|
|
33
82
|
return [
|
|
34
83
|
`Found ${result.diagnostics.length} Beignet lint issue${result.diagnostics.length === 1 ? "" : "s"} in ${result.targetDir}:`,
|
|
35
84
|
"",
|
|
36
|
-
...result.diagnostics.map((diagnostic) =>
|
|
85
|
+
...result.diagnostics.map((diagnostic) => `${paint("ERROR", "red")} ${diagnostic.code} ${lintDiagnosticLocation(diagnostic)}
|
|
37
86
|
${diagnostic.message}`),
|
|
38
87
|
].join("\n");
|
|
39
88
|
}
|
|
89
|
+
function lintDiagnosticLocation(diagnostic) {
|
|
90
|
+
if (diagnostic.line === undefined || diagnostic.column === undefined) {
|
|
91
|
+
return diagnostic.file;
|
|
92
|
+
}
|
|
93
|
+
return `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}`;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Format dependency-direction lint diagnostics as GitHub Actions annotations.
|
|
97
|
+
*/
|
|
98
|
+
export function formatLintGithub(result) {
|
|
99
|
+
return result.diagnostics
|
|
100
|
+
.map((diagnostic) => formatGithubAnnotation({
|
|
101
|
+
severity: "error",
|
|
102
|
+
file: diagnostic.file,
|
|
103
|
+
line: diagnostic.line,
|
|
104
|
+
col: diagnostic.column,
|
|
105
|
+
message: `${diagnostic.code}: ${diagnostic.message}`,
|
|
106
|
+
}))
|
|
107
|
+
.join("\n");
|
|
108
|
+
}
|
|
40
109
|
async function assertDirectory(targetDir) {
|
|
41
110
|
try {
|
|
42
111
|
const stats = await stat(targetDir);
|
|
@@ -86,11 +155,11 @@ function lintSourceFiles(files) {
|
|
|
86
155
|
return /\.(c|m)?tsx?$/.test(file);
|
|
87
156
|
});
|
|
88
157
|
}
|
|
89
|
-
function parseImports(source, importerFile) {
|
|
158
|
+
function parseImports(source, importerFile, files) {
|
|
90
159
|
const references = parseImportSpecifiers(source);
|
|
91
|
-
return references.map((
|
|
92
|
-
|
|
93
|
-
...resolveImport(importPath, importerFile),
|
|
160
|
+
return references.map((reference) => ({
|
|
161
|
+
...reference,
|
|
162
|
+
...resolveImport(reference.importPath, importerFile, files),
|
|
94
163
|
}));
|
|
95
164
|
}
|
|
96
165
|
function parseImportSpecifiers(source) {
|
|
@@ -100,15 +169,25 @@ function parseImportSpecifiers(source) {
|
|
|
100
169
|
index = skipNonCode(source, index);
|
|
101
170
|
if (isKeywordAt(source, index, "import")) {
|
|
102
171
|
const parsed = parseImportSpecifierAt(source, index + "import".length);
|
|
103
|
-
if (parsed?.specifier)
|
|
104
|
-
specifiers.push(
|
|
172
|
+
if (parsed?.specifier) {
|
|
173
|
+
specifiers.push({
|
|
174
|
+
importPath: parsed.specifier,
|
|
175
|
+
kind: parsed.kind ?? "value",
|
|
176
|
+
offset: index,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
105
179
|
index = parsed?.end ?? index + "import".length;
|
|
106
180
|
continue;
|
|
107
181
|
}
|
|
108
182
|
if (isKeywordAt(source, index, "export")) {
|
|
109
183
|
const parsed = parseExportSpecifierAt(source, index + "export".length);
|
|
110
|
-
if (parsed?.specifier)
|
|
111
|
-
specifiers.push(
|
|
184
|
+
if (parsed?.specifier) {
|
|
185
|
+
specifiers.push({
|
|
186
|
+
importPath: parsed.specifier,
|
|
187
|
+
kind: parsed.kind ?? "value",
|
|
188
|
+
offset: index,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
112
191
|
index = parsed?.end ?? index + "export".length;
|
|
113
192
|
continue;
|
|
114
193
|
}
|
|
@@ -132,9 +211,16 @@ function parseImportSpecifierAt(source, index) {
|
|
|
132
211
|
if (sideEffectImport) {
|
|
133
212
|
return {
|
|
134
213
|
specifier: sideEffectImport.value,
|
|
214
|
+
kind: "value",
|
|
135
215
|
end: sideEffectImport.end,
|
|
136
216
|
};
|
|
137
217
|
}
|
|
218
|
+
const importClauseStart = cursor;
|
|
219
|
+
let isImportType = false;
|
|
220
|
+
if (isKeywordAt(source, cursor, "type")) {
|
|
221
|
+
isImportType = true;
|
|
222
|
+
cursor = cursorAfterKeyword("type", cursor);
|
|
223
|
+
}
|
|
138
224
|
while (cursor < source.length) {
|
|
139
225
|
cursor = skipWhitespaceAndComments(source, cursor);
|
|
140
226
|
if (source[cursor] === ";" || source[cursor] === "\n") {
|
|
@@ -147,6 +233,9 @@ function parseImportSpecifierAt(source, index) {
|
|
|
147
233
|
return { end: fromCursor };
|
|
148
234
|
return {
|
|
149
235
|
specifier: stringLiteral.value,
|
|
236
|
+
kind: isImportType
|
|
237
|
+
? "type"
|
|
238
|
+
: inferImportClauseKind(source.slice(importClauseStart, cursor)),
|
|
150
239
|
end: stringLiteral.end,
|
|
151
240
|
};
|
|
152
241
|
}
|
|
@@ -156,8 +245,15 @@ function parseImportSpecifierAt(source, index) {
|
|
|
156
245
|
}
|
|
157
246
|
function parseExportSpecifierAt(source, index) {
|
|
158
247
|
let cursor = index;
|
|
248
|
+
let isExportType = false;
|
|
249
|
+
const exportClauseStart = cursor;
|
|
159
250
|
while (cursor < source.length) {
|
|
160
251
|
cursor = skipWhitespaceAndComments(source, cursor);
|
|
252
|
+
if (isKeywordAt(source, cursor, "type")) {
|
|
253
|
+
isExportType = true;
|
|
254
|
+
cursor = cursorAfterKeyword("type", cursor);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
161
257
|
if (source[cursor] === ";" || source[cursor] === "\n") {
|
|
162
258
|
return { end: cursor + 1 };
|
|
163
259
|
}
|
|
@@ -168,6 +264,9 @@ function parseExportSpecifierAt(source, index) {
|
|
|
168
264
|
return { end: fromCursor };
|
|
169
265
|
return {
|
|
170
266
|
specifier: stringLiteral.value,
|
|
267
|
+
kind: isExportType
|
|
268
|
+
? "type"
|
|
269
|
+
: inferImportClauseKind(source.slice(exportClauseStart, cursor)),
|
|
171
270
|
end: stringLiteral.end,
|
|
172
271
|
};
|
|
173
272
|
}
|
|
@@ -175,6 +274,13 @@ function parseExportSpecifierAt(source, index) {
|
|
|
175
274
|
}
|
|
176
275
|
return undefined;
|
|
177
276
|
}
|
|
277
|
+
function inferImportClauseKind(importClause) {
|
|
278
|
+
const trimmed = importClause.trim();
|
|
279
|
+
if (/^\{\s*type\s+[^,}]+(?:,\s*type\s+[^,}]+)*,?\s*\}$/.test(trimmed)) {
|
|
280
|
+
return "type";
|
|
281
|
+
}
|
|
282
|
+
return "value";
|
|
283
|
+
}
|
|
178
284
|
function cursorAfterKeyword(keyword, index) {
|
|
179
285
|
return index + keyword.length;
|
|
180
286
|
}
|
|
@@ -292,21 +398,42 @@ function isKeywordAt(source, index, keyword) {
|
|
|
292
398
|
function isIdentifierChar(char) {
|
|
293
399
|
return Boolean(char && /[A-Za-z0-9_$]/.test(char));
|
|
294
400
|
}
|
|
295
|
-
function resolveImport(importPath, importerFile) {
|
|
401
|
+
function resolveImport(importPath, importerFile, files) {
|
|
296
402
|
if (importPath.startsWith("@/")) {
|
|
297
403
|
return {
|
|
298
|
-
resolvedPath: stripKnownExtension(importPath.slice(2)),
|
|
404
|
+
resolvedPath: resolveLocalImportPath(stripKnownExtension(importPath.slice(2)), files),
|
|
299
405
|
};
|
|
300
406
|
}
|
|
301
407
|
if (importPath.startsWith(".")) {
|
|
302
408
|
return {
|
|
303
|
-
resolvedPath: stripKnownExtension(normalizePath(path.join(path.dirname(importerFile), importPath))),
|
|
409
|
+
resolvedPath: resolveLocalImportPath(stripKnownExtension(normalizePath(path.join(path.dirname(importerFile), importPath))), files),
|
|
304
410
|
};
|
|
305
411
|
}
|
|
306
412
|
return {
|
|
307
413
|
packageName: packageName(importPath),
|
|
308
414
|
};
|
|
309
415
|
}
|
|
416
|
+
function resolveLocalImportPath(importPath, files) {
|
|
417
|
+
const candidates = [
|
|
418
|
+
importPath,
|
|
419
|
+
`${importPath}.ts`,
|
|
420
|
+
`${importPath}.tsx`,
|
|
421
|
+
`${importPath}.mts`,
|
|
422
|
+
`${importPath}.cts`,
|
|
423
|
+
`${importPath}/index.ts`,
|
|
424
|
+
`${importPath}/index.tsx`,
|
|
425
|
+
`${importPath}/index.mts`,
|
|
426
|
+
`${importPath}/index.cts`,
|
|
427
|
+
];
|
|
428
|
+
for (const candidate of candidates) {
|
|
429
|
+
for (const file of files) {
|
|
430
|
+
if (stripKnownExtension(file) === stripKnownExtension(candidate)) {
|
|
431
|
+
return file;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return stripKnownExtension(importPath);
|
|
436
|
+
}
|
|
310
437
|
function packageName(importPath) {
|
|
311
438
|
if (!importPath.startsWith("@"))
|
|
312
439
|
return importPath.split("/")[0];
|
|
@@ -317,29 +444,45 @@ function stripKnownExtension(filePath) {
|
|
|
317
444
|
return filePath.replace(/\.(c|m)?tsx?$/, "");
|
|
318
445
|
}
|
|
319
446
|
function lintImport(file, sourceLayer, reference, config) {
|
|
447
|
+
if (reference.kind === "type")
|
|
448
|
+
return undefined;
|
|
320
449
|
const targetLayer = reference.resolvedPath
|
|
321
450
|
? classifyPath(reference.resolvedPath, config)
|
|
322
451
|
: undefined;
|
|
452
|
+
const featureImport = reference.resolvedPath
|
|
453
|
+
? featureImportViolation(file, sourceLayer, reference.resolvedPath, config)
|
|
454
|
+
: undefined;
|
|
455
|
+
if (featureImport) {
|
|
456
|
+
return {
|
|
457
|
+
severity: "error",
|
|
458
|
+
code: "BEIGNET_IMPORT_DIRECTION",
|
|
459
|
+
file,
|
|
460
|
+
importPath: reference.importPath,
|
|
461
|
+
message: featureImport,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
323
464
|
const appImport = reference.resolvedPath
|
|
324
465
|
? appImportViolation(sourceLayer, targetLayer)
|
|
325
466
|
: undefined;
|
|
326
467
|
if (appImport) {
|
|
327
468
|
return {
|
|
328
469
|
severity: "error",
|
|
329
|
-
code: "
|
|
470
|
+
code: "BEIGNET_IMPORT_DIRECTION",
|
|
330
471
|
file,
|
|
331
472
|
importPath: reference.importPath,
|
|
332
473
|
message: appImportMessage(sourceLayer, reference.importPath, appImport),
|
|
333
474
|
};
|
|
334
475
|
}
|
|
335
|
-
|
|
336
|
-
packageImportViolation(sourceLayer, reference.
|
|
476
|
+
const bannedPackage = reference.packageName
|
|
477
|
+
? packageImportViolation(sourceLayer, reference.importPath)
|
|
478
|
+
: undefined;
|
|
479
|
+
if (bannedPackage) {
|
|
337
480
|
return {
|
|
338
481
|
severity: "error",
|
|
339
|
-
code: "
|
|
482
|
+
code: "BEIGNET_IMPORT_DIRECTION",
|
|
340
483
|
file,
|
|
341
484
|
importPath: reference.importPath,
|
|
342
|
-
message: packageImportMessage(sourceLayer,
|
|
485
|
+
message: packageImportMessage(sourceLayer, bannedPackage),
|
|
343
486
|
};
|
|
344
487
|
}
|
|
345
488
|
return undefined;
|
|
@@ -362,42 +505,211 @@ function appImportViolation(sourceLayer, targetLayer) {
|
|
|
362
505
|
"route",
|
|
363
506
|
"server",
|
|
364
507
|
"use-case",
|
|
508
|
+
"workflow",
|
|
365
509
|
].includes(targetLayer)) {
|
|
366
510
|
return targetLayer;
|
|
367
511
|
}
|
|
368
|
-
if (sourceLayer === "use-case" &&
|
|
512
|
+
if ((sourceLayer === "use-case" || sourceLayer === "workflow") &&
|
|
369
513
|
["app", "client", "component", "infra", "route", "server"].includes(targetLayer)) {
|
|
370
514
|
return targetLayer;
|
|
371
515
|
}
|
|
372
516
|
if (["feature-port", "policy", "port"].includes(sourceLayer) &&
|
|
373
|
-
[
|
|
517
|
+
[
|
|
518
|
+
"app",
|
|
519
|
+
"client",
|
|
520
|
+
"component",
|
|
521
|
+
"infra",
|
|
522
|
+
"route",
|
|
523
|
+
"server",
|
|
524
|
+
"workflow",
|
|
525
|
+
].includes(targetLayer)) {
|
|
374
526
|
return targetLayer;
|
|
375
527
|
}
|
|
376
528
|
if (sourceLayer === "contract" &&
|
|
377
|
-
["app", "client", "component", "infra", "route"].includes(targetLayer)) {
|
|
529
|
+
["app", "client", "component", "infra", "route", "workflow"].includes(targetLayer)) {
|
|
378
530
|
return targetLayer;
|
|
379
531
|
}
|
|
380
532
|
if (sourceLayer === "route" &&
|
|
381
|
-
["app", "client", "component", "infra"].includes(targetLayer)) {
|
|
533
|
+
["app", "client", "component", "infra", "workflow"].includes(targetLayer)) {
|
|
534
|
+
return targetLayer;
|
|
535
|
+
}
|
|
536
|
+
if (sourceLayer === "infra" &&
|
|
537
|
+
["app", "client", "component", "route", "server"].includes(targetLayer)) {
|
|
382
538
|
return targetLayer;
|
|
383
539
|
}
|
|
384
540
|
return undefined;
|
|
385
541
|
}
|
|
386
|
-
function packageImportViolation(sourceLayer,
|
|
542
|
+
function packageImportViolation(sourceLayer, importPath) {
|
|
387
543
|
if (sourceLayer === "domain") {
|
|
388
|
-
return (domainBannedPackages
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
if ([
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
544
|
+
return (matchBannedPackage(domainBannedPackages, importPath) ??
|
|
545
|
+
matchBeignetProviderPackage(importPath));
|
|
546
|
+
}
|
|
547
|
+
if ([
|
|
548
|
+
"contract",
|
|
549
|
+
"feature-port",
|
|
550
|
+
"policy",
|
|
551
|
+
"port",
|
|
552
|
+
"use-case",
|
|
553
|
+
"workflow",
|
|
554
|
+
].includes(sourceLayer)) {
|
|
555
|
+
return (matchBannedPackage(coreBannedPackages, importPath) ??
|
|
556
|
+
matchBannedPackage(serverRuntimePackages, importPath) ??
|
|
557
|
+
matchBeignetProviderPackage(importPath));
|
|
395
558
|
}
|
|
396
559
|
if (sourceLayer === "route") {
|
|
397
|
-
return (coreBannedPackages
|
|
398
|
-
|
|
560
|
+
return (matchBannedPackage(coreBannedPackages, importPath) ??
|
|
561
|
+
matchBeignetProviderPackage(importPath));
|
|
562
|
+
}
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
function matchBannedPackage(bannedPackages, importPath) {
|
|
566
|
+
let candidate = importPath;
|
|
567
|
+
while (candidate.length > 0) {
|
|
568
|
+
if (bannedPackages.has(candidate))
|
|
569
|
+
return candidate;
|
|
570
|
+
const separator = candidate.lastIndexOf("/");
|
|
571
|
+
if (separator === -1)
|
|
572
|
+
return undefined;
|
|
573
|
+
candidate = candidate.slice(0, separator);
|
|
574
|
+
}
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
577
|
+
function matchBeignetProviderPackage(importPath) {
|
|
578
|
+
const name = packageName(importPath);
|
|
579
|
+
return name.startsWith("@beignet/provider-") ? name : undefined;
|
|
580
|
+
}
|
|
581
|
+
function lintRuntimeBoundary({ config, graph, sourceByFile, }) {
|
|
582
|
+
const diagnostics = [];
|
|
583
|
+
for (const file of graph.keys()) {
|
|
584
|
+
const rootKind = runtimeBoundaryRootKind(file, sourceByFile.get(file) ?? "", config);
|
|
585
|
+
if (!rootKind)
|
|
586
|
+
continue;
|
|
587
|
+
diagnostics.push(...lintRuntimeBoundaryRoot({
|
|
588
|
+
root: file,
|
|
589
|
+
rootKind,
|
|
590
|
+
config,
|
|
591
|
+
graph,
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
return diagnostics;
|
|
595
|
+
}
|
|
596
|
+
function lintRuntimeBoundaryRoot({ root, rootKind, config, graph, }) {
|
|
597
|
+
const diagnostics = [];
|
|
598
|
+
const visited = new Set();
|
|
599
|
+
const queue = [
|
|
600
|
+
{ file: root, chain: [root] },
|
|
601
|
+
];
|
|
602
|
+
while (queue.length > 0) {
|
|
603
|
+
const current = queue.shift();
|
|
604
|
+
if (!current)
|
|
605
|
+
continue;
|
|
606
|
+
if (visited.has(current.file))
|
|
607
|
+
continue;
|
|
608
|
+
visited.add(current.file);
|
|
609
|
+
for (const reference of graph.get(current.file) ?? []) {
|
|
610
|
+
if (reference.kind === "type" &&
|
|
611
|
+
reference.importPath !== "@beignet/core/server-only") {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const chain = reference.resolvedPath
|
|
615
|
+
? [...current.chain, reference.resolvedPath]
|
|
616
|
+
: current.chain;
|
|
617
|
+
const violation = runtimeBoundaryViolation(reference, config);
|
|
618
|
+
if (violation) {
|
|
619
|
+
diagnostics.push({
|
|
620
|
+
severity: "error",
|
|
621
|
+
code: "BEIGNET_RUNTIME_BOUNDARY",
|
|
622
|
+
file: root,
|
|
623
|
+
importPath: reference.importPath,
|
|
624
|
+
message: runtimeBoundaryMessage(rootKind, violation, chain),
|
|
625
|
+
});
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (reference.resolvedPath && graph.has(reference.resolvedPath)) {
|
|
629
|
+
queue.push({
|
|
630
|
+
file: reference.resolvedPath,
|
|
631
|
+
chain,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return diagnostics;
|
|
637
|
+
}
|
|
638
|
+
function runtimeBoundaryRootKind(file, source, config) {
|
|
639
|
+
const layer = classifyPath(file, config);
|
|
640
|
+
if (layer === "contract")
|
|
641
|
+
return "contract";
|
|
642
|
+
if (layer === "client")
|
|
643
|
+
return "client";
|
|
644
|
+
if (hasUseClientDirective(source))
|
|
645
|
+
return "client";
|
|
646
|
+
if (isGeneratedClientAdapter(file))
|
|
647
|
+
return "client";
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
function hasUseClientDirective(source) {
|
|
651
|
+
const trimmed = source.trimStart();
|
|
652
|
+
return (trimmed.startsWith('"use client";') ||
|
|
653
|
+
trimmed.startsWith('"use client"') ||
|
|
654
|
+
trimmed.startsWith("'use client';") ||
|
|
655
|
+
trimmed.startsWith("'use client'"));
|
|
656
|
+
}
|
|
657
|
+
function isGeneratedClientAdapter(file) {
|
|
658
|
+
return (file === "client/index.ts" ||
|
|
659
|
+
file === "client/api-client.ts" ||
|
|
660
|
+
file === "client/rhf.ts" ||
|
|
661
|
+
file === "client/rq.ts" ||
|
|
662
|
+
file === "client/uploads.ts");
|
|
663
|
+
}
|
|
664
|
+
function runtimeBoundaryViolation(reference, config) {
|
|
665
|
+
if (reference.resolvedPath) {
|
|
666
|
+
const targetLayer = classifyPath(reference.resolvedPath, config);
|
|
667
|
+
if (targetLayer === "use-case" ||
|
|
668
|
+
targetLayer === "route" ||
|
|
669
|
+
targetLayer === "infra" ||
|
|
670
|
+
targetLayer === "server") {
|
|
671
|
+
return `${layerLabel(targetLayer)} module ${reference.resolvedPath}`;
|
|
672
|
+
}
|
|
673
|
+
if (targetLayer === "app-context") {
|
|
674
|
+
return `app context module ${reference.resolvedPath}`;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (reference.importPath === "@beignet/core/server-only") {
|
|
678
|
+
return "server-only marker @beignet/core/server-only";
|
|
679
|
+
}
|
|
680
|
+
if (reference.importPath.startsWith("node:")) {
|
|
681
|
+
return `Node runtime module ${reference.importPath}`;
|
|
682
|
+
}
|
|
683
|
+
if (serverRuntimePackages.has(reference.importPath) ||
|
|
684
|
+
reference.importPath.startsWith("@beignet/provider-")) {
|
|
685
|
+
return `server runtime package ${reference.importPath}`;
|
|
686
|
+
}
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
function runtimeBoundaryMessage(rootKind, violation, chain) {
|
|
690
|
+
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.`;
|
|
691
|
+
}
|
|
692
|
+
function rootKindLabel(rootKind) {
|
|
693
|
+
return rootKind === "contract" ? "contract" : "client";
|
|
694
|
+
}
|
|
695
|
+
function formatImportChain(chain) {
|
|
696
|
+
return chain.join(" -> ");
|
|
697
|
+
}
|
|
698
|
+
function featureImportViolation(file, sourceLayer, resolvedPath, config) {
|
|
699
|
+
if (sourceLayer !== "domain")
|
|
700
|
+
return undefined;
|
|
701
|
+
const sourceFeature = featureLayerInfo(file, config);
|
|
702
|
+
const targetFeature = featureLayerInfo(resolvedPath, config);
|
|
703
|
+
if (!sourceFeature || !targetFeature)
|
|
704
|
+
return undefined;
|
|
705
|
+
if (sourceFeature.layer !== "domain" || targetFeature.layer !== "domain") {
|
|
706
|
+
return undefined;
|
|
399
707
|
}
|
|
400
|
-
|
|
708
|
+
if (sourceFeature.feature === targetFeature.feature)
|
|
709
|
+
return undefined;
|
|
710
|
+
if (targetFeature.feature === "shared")
|
|
711
|
+
return undefined;
|
|
712
|
+
return `feature-specific domain files must not import another feature's domain (${stripKnownExtension(resolvedPath)}). Move shared concepts to features/shared/domain.`;
|
|
401
713
|
}
|
|
402
714
|
const domainBannedPackages = new Set([
|
|
403
715
|
"@aws-sdk/client-s3",
|
|
@@ -520,6 +832,16 @@ function classifyPath(filePath, config) {
|
|
|
520
832
|
return "policy";
|
|
521
833
|
if (layerSegment === "use-cases")
|
|
522
834
|
return "use-case";
|
|
835
|
+
if ([
|
|
836
|
+
"jobs",
|
|
837
|
+
"listeners",
|
|
838
|
+
"notifications",
|
|
839
|
+
"schedules",
|
|
840
|
+
"tasks",
|
|
841
|
+
"uploads",
|
|
842
|
+
].includes(layerSegment)) {
|
|
843
|
+
return "workflow";
|
|
844
|
+
}
|
|
523
845
|
if (layerSegment === "components")
|
|
524
846
|
return "component";
|
|
525
847
|
}
|
|
@@ -551,6 +873,16 @@ function featureRelativePath(filePath, featuresPath) {
|
|
|
551
873
|
const prefix = `${directoryPath(featuresPath)}/`;
|
|
552
874
|
return filePath.slice(prefix.length);
|
|
553
875
|
}
|
|
876
|
+
function featureLayerInfo(filePath, config) {
|
|
877
|
+
const normalizedPath = stripKnownExtension(normalizePath(filePath));
|
|
878
|
+
const relativePath = featureRelativePath(normalizedPath, directoryPath(config.paths.features));
|
|
879
|
+
if (!relativePath)
|
|
880
|
+
return undefined;
|
|
881
|
+
const [feature, layer] = relativePath.split("/");
|
|
882
|
+
if (!feature)
|
|
883
|
+
return undefined;
|
|
884
|
+
return { feature, layer };
|
|
885
|
+
}
|
|
554
886
|
function sharedDomainPath(config) {
|
|
555
887
|
const featuresDir = directoryPath(config.paths.features);
|
|
556
888
|
const featuresParent = directoryPath(path.dirname(featuresDir));
|