@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/src/lint.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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 {
|
|
4
5
|
type BeignetConfig,
|
|
5
6
|
directoryPath,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
type ResolvedBeignetConfig,
|
|
9
10
|
resolveConfig,
|
|
10
11
|
} from "./config.js";
|
|
12
|
+
import { formatGithubAnnotation } from "./github-annotations.js";
|
|
11
13
|
|
|
12
14
|
type LintAppOptions = {
|
|
13
15
|
cwd?: string;
|
|
@@ -30,28 +32,49 @@ type SourceLayer =
|
|
|
30
32
|
| "server"
|
|
31
33
|
| "test"
|
|
32
34
|
| "use-case"
|
|
35
|
+
| "workflow"
|
|
33
36
|
| "unknown";
|
|
34
37
|
|
|
35
38
|
type ImportReference = {
|
|
36
39
|
importPath: string;
|
|
40
|
+
kind: ImportKind;
|
|
41
|
+
offset: number;
|
|
37
42
|
resolvedPath?: string;
|
|
38
43
|
packageName?: string;
|
|
39
44
|
};
|
|
40
45
|
|
|
46
|
+
type ImportKind = "type" | "value";
|
|
47
|
+
|
|
48
|
+
type ImportGraph = Map<string, ImportReference[]>;
|
|
49
|
+
|
|
50
|
+
type RuntimeBoundaryRootKind = "client" | "contract";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Dependency-direction lint diagnostic.
|
|
54
|
+
*/
|
|
41
55
|
export type LintDiagnostic = {
|
|
42
56
|
severity: "error";
|
|
43
57
|
code: string;
|
|
44
58
|
message: string;
|
|
45
59
|
file: string;
|
|
46
60
|
importPath: string;
|
|
61
|
+
line?: number;
|
|
62
|
+
column?: number;
|
|
47
63
|
};
|
|
48
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Result returned by `beignet lint`.
|
|
67
|
+
*/
|
|
49
68
|
export type LintAppResult = {
|
|
69
|
+
schemaVersion: 1;
|
|
50
70
|
targetDir: string;
|
|
51
71
|
config: ResolvedBeignetConfig;
|
|
52
72
|
diagnostics: LintDiagnostic[];
|
|
53
73
|
};
|
|
54
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Inspect app imports and report dependency-direction violations.
|
|
77
|
+
*/
|
|
55
78
|
export async function lintApp(
|
|
56
79
|
options: LintAppOptions = {},
|
|
57
80
|
): Promise<LintAppResult> {
|
|
@@ -63,26 +86,87 @@ export async function lintApp(
|
|
|
63
86
|
? resolveConfig(options.config)
|
|
64
87
|
: await loadBeignetConfig(targetDir, files);
|
|
65
88
|
const diagnostics: LintDiagnostic[] = [];
|
|
89
|
+
const sourceFiles = lintSourceFiles(files);
|
|
90
|
+
const sourceFileSet = new Set(sourceFiles);
|
|
91
|
+
const sourceByFile = new Map<string, string>();
|
|
92
|
+
const importGraph: ImportGraph = new Map();
|
|
66
93
|
|
|
67
|
-
for (const file of
|
|
94
|
+
for (const file of sourceFiles) {
|
|
68
95
|
const sourceLayer = classifyPath(file, config);
|
|
96
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
97
|
+
sourceByFile.set(file, source);
|
|
98
|
+
importGraph.set(file, parseImports(source, file, sourceFileSet));
|
|
99
|
+
|
|
69
100
|
if (sourceLayer === "test" || sourceLayer === "unknown") continue;
|
|
70
101
|
|
|
71
|
-
const
|
|
72
|
-
for (const reference of
|
|
102
|
+
const lineStarts = computeLineStarts(source);
|
|
103
|
+
for (const reference of importGraph.get(file) ?? []) {
|
|
73
104
|
const diagnostic = lintImport(file, sourceLayer, reference, config);
|
|
74
|
-
if (diagnostic)
|
|
105
|
+
if (diagnostic) {
|
|
106
|
+
diagnostics.push({
|
|
107
|
+
...diagnostic,
|
|
108
|
+
...positionFromOffset(lineStarts, reference.offset),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
75
111
|
}
|
|
76
112
|
}
|
|
77
113
|
|
|
114
|
+
diagnostics.push(
|
|
115
|
+
...lintRuntimeBoundary({
|
|
116
|
+
config,
|
|
117
|
+
graph: importGraph,
|
|
118
|
+
sourceByFile,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
78
122
|
return {
|
|
123
|
+
schemaVersion: 1,
|
|
79
124
|
targetDir,
|
|
80
125
|
config,
|
|
81
126
|
diagnostics: dedupeDiagnostics(diagnostics),
|
|
82
127
|
};
|
|
83
128
|
}
|
|
84
129
|
|
|
85
|
-
|
|
130
|
+
function computeLineStarts(source: string): number[] {
|
|
131
|
+
const lineStarts = [0];
|
|
132
|
+
|
|
133
|
+
for (let index = 0; index < source.length; index++) {
|
|
134
|
+
if (source[index] === "\n") {
|
|
135
|
+
lineStarts.push(index + 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return lineStarts;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function positionFromOffset(
|
|
143
|
+
lineStarts: number[],
|
|
144
|
+
offset: number,
|
|
145
|
+
): { line: number; column: number } {
|
|
146
|
+
let low = 0;
|
|
147
|
+
let high = lineStarts.length - 1;
|
|
148
|
+
|
|
149
|
+
while (low < high) {
|
|
150
|
+
const middle = Math.ceil((low + high) / 2);
|
|
151
|
+
if (lineStarts[middle] <= offset) {
|
|
152
|
+
low = middle;
|
|
153
|
+
} else {
|
|
154
|
+
high = middle - 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { line: low + 1, column: offset - lineStarts[low] + 1 };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Format dependency-direction lint diagnostics for CLI output.
|
|
163
|
+
*/
|
|
164
|
+
export function formatLint(
|
|
165
|
+
result: LintAppResult,
|
|
166
|
+
options: { color?: boolean } = {},
|
|
167
|
+
): string {
|
|
168
|
+
const paint = createPainter(options.color);
|
|
169
|
+
|
|
86
170
|
if (result.diagnostics.length === 0) {
|
|
87
171
|
return `No Beignet lint issues found in ${result.targetDir}.`;
|
|
88
172
|
}
|
|
@@ -93,12 +177,39 @@ export function formatLint(result: LintAppResult): string {
|
|
|
93
177
|
} in ${result.targetDir}:`,
|
|
94
178
|
"",
|
|
95
179
|
...result.diagnostics.map(
|
|
96
|
-
(
|
|
180
|
+
(
|
|
181
|
+
diagnostic,
|
|
182
|
+
) => `${paint("ERROR", "red")} ${diagnostic.code} ${lintDiagnosticLocation(diagnostic)}
|
|
97
183
|
${diagnostic.message}`,
|
|
98
184
|
),
|
|
99
185
|
].join("\n");
|
|
100
186
|
}
|
|
101
187
|
|
|
188
|
+
function lintDiagnosticLocation(diagnostic: LintDiagnostic): string {
|
|
189
|
+
if (diagnostic.line === undefined || diagnostic.column === undefined) {
|
|
190
|
+
return diagnostic.file;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Format dependency-direction lint diagnostics as GitHub Actions annotations.
|
|
198
|
+
*/
|
|
199
|
+
export function formatLintGithub(result: LintAppResult): string {
|
|
200
|
+
return result.diagnostics
|
|
201
|
+
.map((diagnostic) =>
|
|
202
|
+
formatGithubAnnotation({
|
|
203
|
+
severity: "error",
|
|
204
|
+
file: diagnostic.file,
|
|
205
|
+
line: diagnostic.line,
|
|
206
|
+
col: diagnostic.column,
|
|
207
|
+
message: `${diagnostic.code}: ${diagnostic.message}`,
|
|
208
|
+
}),
|
|
209
|
+
)
|
|
210
|
+
.join("\n");
|
|
211
|
+
}
|
|
212
|
+
|
|
102
213
|
async function assertDirectory(targetDir: string): Promise<void> {
|
|
103
214
|
try {
|
|
104
215
|
const stats = await stat(targetDir);
|
|
@@ -152,17 +263,27 @@ function lintSourceFiles(files: string[]): string[] {
|
|
|
152
263
|
});
|
|
153
264
|
}
|
|
154
265
|
|
|
155
|
-
function parseImports(
|
|
266
|
+
function parseImports(
|
|
267
|
+
source: string,
|
|
268
|
+
importerFile: string,
|
|
269
|
+
files: Set<string>,
|
|
270
|
+
): ImportReference[] {
|
|
156
271
|
const references = parseImportSpecifiers(source);
|
|
157
272
|
|
|
158
|
-
return references.map((
|
|
159
|
-
|
|
160
|
-
...resolveImport(importPath, importerFile),
|
|
273
|
+
return references.map((reference) => ({
|
|
274
|
+
...reference,
|
|
275
|
+
...resolveImport(reference.importPath, importerFile, files),
|
|
161
276
|
}));
|
|
162
277
|
}
|
|
163
278
|
|
|
164
|
-
function parseImportSpecifiers(
|
|
165
|
-
|
|
279
|
+
function parseImportSpecifiers(
|
|
280
|
+
source: string,
|
|
281
|
+
): Array<{ importPath: string; kind: ImportKind; offset: number }> {
|
|
282
|
+
const specifiers: Array<{
|
|
283
|
+
importPath: string;
|
|
284
|
+
kind: ImportKind;
|
|
285
|
+
offset: number;
|
|
286
|
+
}> = [];
|
|
166
287
|
let index = 0;
|
|
167
288
|
|
|
168
289
|
while (index < source.length) {
|
|
@@ -170,14 +291,26 @@ function parseImportSpecifiers(source: string): string[] {
|
|
|
170
291
|
|
|
171
292
|
if (isKeywordAt(source, index, "import")) {
|
|
172
293
|
const parsed = parseImportSpecifierAt(source, index + "import".length);
|
|
173
|
-
if (parsed?.specifier)
|
|
294
|
+
if (parsed?.specifier) {
|
|
295
|
+
specifiers.push({
|
|
296
|
+
importPath: parsed.specifier,
|
|
297
|
+
kind: parsed.kind ?? "value",
|
|
298
|
+
offset: index,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
174
301
|
index = parsed?.end ?? index + "import".length;
|
|
175
302
|
continue;
|
|
176
303
|
}
|
|
177
304
|
|
|
178
305
|
if (isKeywordAt(source, index, "export")) {
|
|
179
306
|
const parsed = parseExportSpecifierAt(source, index + "export".length);
|
|
180
|
-
if (parsed?.specifier)
|
|
307
|
+
if (parsed?.specifier) {
|
|
308
|
+
specifiers.push({
|
|
309
|
+
importPath: parsed.specifier,
|
|
310
|
+
kind: parsed.kind ?? "value",
|
|
311
|
+
offset: index,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
181
314
|
index = parsed?.end ?? index + "export".length;
|
|
182
315
|
continue;
|
|
183
316
|
}
|
|
@@ -191,7 +324,7 @@ function parseImportSpecifiers(source: string): string[] {
|
|
|
191
324
|
function parseImportSpecifierAt(
|
|
192
325
|
source: string,
|
|
193
326
|
index: number,
|
|
194
|
-
): { specifier?: string; end: number } | undefined {
|
|
327
|
+
): { specifier?: string; kind?: ImportKind; end: number } | undefined {
|
|
195
328
|
let cursor = skipWhitespaceAndComments(source, index);
|
|
196
329
|
if (source[cursor] === "(") {
|
|
197
330
|
cursor = skipWhitespaceAndComments(source, cursor + 1);
|
|
@@ -207,10 +340,18 @@ function parseImportSpecifierAt(
|
|
|
207
340
|
if (sideEffectImport) {
|
|
208
341
|
return {
|
|
209
342
|
specifier: sideEffectImport.value,
|
|
343
|
+
kind: "value",
|
|
210
344
|
end: sideEffectImport.end,
|
|
211
345
|
};
|
|
212
346
|
}
|
|
213
347
|
|
|
348
|
+
const importClauseStart = cursor;
|
|
349
|
+
let isImportType = false;
|
|
350
|
+
if (isKeywordAt(source, cursor, "type")) {
|
|
351
|
+
isImportType = true;
|
|
352
|
+
cursor = cursorAfterKeyword("type", cursor);
|
|
353
|
+
}
|
|
354
|
+
|
|
214
355
|
while (cursor < source.length) {
|
|
215
356
|
cursor = skipWhitespaceAndComments(source, cursor);
|
|
216
357
|
|
|
@@ -227,6 +368,9 @@ function parseImportSpecifierAt(
|
|
|
227
368
|
if (!stringLiteral) return { end: fromCursor };
|
|
228
369
|
return {
|
|
229
370
|
specifier: stringLiteral.value,
|
|
371
|
+
kind: isImportType
|
|
372
|
+
? "type"
|
|
373
|
+
: inferImportClauseKind(source.slice(importClauseStart, cursor)),
|
|
230
374
|
end: stringLiteral.end,
|
|
231
375
|
};
|
|
232
376
|
}
|
|
@@ -240,12 +384,20 @@ function parseImportSpecifierAt(
|
|
|
240
384
|
function parseExportSpecifierAt(
|
|
241
385
|
source: string,
|
|
242
386
|
index: number,
|
|
243
|
-
): { specifier?: string; end: number } | undefined {
|
|
387
|
+
): { specifier?: string; kind?: ImportKind; end: number } | undefined {
|
|
244
388
|
let cursor = index;
|
|
389
|
+
let isExportType = false;
|
|
390
|
+
const exportClauseStart = cursor;
|
|
245
391
|
|
|
246
392
|
while (cursor < source.length) {
|
|
247
393
|
cursor = skipWhitespaceAndComments(source, cursor);
|
|
248
394
|
|
|
395
|
+
if (isKeywordAt(source, cursor, "type")) {
|
|
396
|
+
isExportType = true;
|
|
397
|
+
cursor = cursorAfterKeyword("type", cursor);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
249
401
|
if (source[cursor] === ";" || source[cursor] === "\n") {
|
|
250
402
|
return { end: cursor + 1 };
|
|
251
403
|
}
|
|
@@ -259,6 +411,9 @@ function parseExportSpecifierAt(
|
|
|
259
411
|
if (!stringLiteral) return { end: fromCursor };
|
|
260
412
|
return {
|
|
261
413
|
specifier: stringLiteral.value,
|
|
414
|
+
kind: isExportType
|
|
415
|
+
? "type"
|
|
416
|
+
: inferImportClauseKind(source.slice(exportClauseStart, cursor)),
|
|
262
417
|
end: stringLiteral.end,
|
|
263
418
|
};
|
|
264
419
|
}
|
|
@@ -269,6 +424,14 @@ function parseExportSpecifierAt(
|
|
|
269
424
|
return undefined;
|
|
270
425
|
}
|
|
271
426
|
|
|
427
|
+
function inferImportClauseKind(importClause: string): ImportKind {
|
|
428
|
+
const trimmed = importClause.trim();
|
|
429
|
+
if (/^\{\s*type\s+[^,}]+(?:,\s*type\s+[^,}]+)*,?\s*\}$/.test(trimmed)) {
|
|
430
|
+
return "type";
|
|
431
|
+
}
|
|
432
|
+
return "value";
|
|
433
|
+
}
|
|
434
|
+
|
|
272
435
|
function cursorAfterKeyword(keyword: string, index: number): number {
|
|
273
436
|
return index + keyword.length;
|
|
274
437
|
}
|
|
@@ -412,17 +575,24 @@ function isIdentifierChar(char: string | undefined): boolean {
|
|
|
412
575
|
function resolveImport(
|
|
413
576
|
importPath: string,
|
|
414
577
|
importerFile: string,
|
|
578
|
+
files: Set<string>,
|
|
415
579
|
): Pick<ImportReference, "packageName" | "resolvedPath"> {
|
|
416
580
|
if (importPath.startsWith("@/")) {
|
|
417
581
|
return {
|
|
418
|
-
resolvedPath:
|
|
582
|
+
resolvedPath: resolveLocalImportPath(
|
|
583
|
+
stripKnownExtension(importPath.slice(2)),
|
|
584
|
+
files,
|
|
585
|
+
),
|
|
419
586
|
};
|
|
420
587
|
}
|
|
421
588
|
|
|
422
589
|
if (importPath.startsWith(".")) {
|
|
423
590
|
return {
|
|
424
|
-
resolvedPath:
|
|
425
|
-
|
|
591
|
+
resolvedPath: resolveLocalImportPath(
|
|
592
|
+
stripKnownExtension(
|
|
593
|
+
normalizePath(path.join(path.dirname(importerFile), importPath)),
|
|
594
|
+
),
|
|
595
|
+
files,
|
|
426
596
|
),
|
|
427
597
|
};
|
|
428
598
|
}
|
|
@@ -432,6 +602,33 @@ function resolveImport(
|
|
|
432
602
|
};
|
|
433
603
|
}
|
|
434
604
|
|
|
605
|
+
function resolveLocalImportPath(
|
|
606
|
+
importPath: string,
|
|
607
|
+
files: Set<string>,
|
|
608
|
+
): string | undefined {
|
|
609
|
+
const candidates = [
|
|
610
|
+
importPath,
|
|
611
|
+
`${importPath}.ts`,
|
|
612
|
+
`${importPath}.tsx`,
|
|
613
|
+
`${importPath}.mts`,
|
|
614
|
+
`${importPath}.cts`,
|
|
615
|
+
`${importPath}/index.ts`,
|
|
616
|
+
`${importPath}/index.tsx`,
|
|
617
|
+
`${importPath}/index.mts`,
|
|
618
|
+
`${importPath}/index.cts`,
|
|
619
|
+
];
|
|
620
|
+
|
|
621
|
+
for (const candidate of candidates) {
|
|
622
|
+
for (const file of files) {
|
|
623
|
+
if (stripKnownExtension(file) === stripKnownExtension(candidate)) {
|
|
624
|
+
return file;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return stripKnownExtension(importPath);
|
|
630
|
+
}
|
|
631
|
+
|
|
435
632
|
function packageName(importPath: string): string {
|
|
436
633
|
if (!importPath.startsWith("@")) return importPath.split("/")[0];
|
|
437
634
|
|
|
@@ -449,9 +646,24 @@ function lintImport(
|
|
|
449
646
|
reference: ImportReference,
|
|
450
647
|
config: ResolvedBeignetConfig,
|
|
451
648
|
): LintDiagnostic | undefined {
|
|
649
|
+
if (reference.kind === "type") return undefined;
|
|
650
|
+
|
|
452
651
|
const targetLayer = reference.resolvedPath
|
|
453
652
|
? classifyPath(reference.resolvedPath, config)
|
|
454
653
|
: undefined;
|
|
654
|
+
const featureImport = reference.resolvedPath
|
|
655
|
+
? featureImportViolation(file, sourceLayer, reference.resolvedPath, config)
|
|
656
|
+
: undefined;
|
|
657
|
+
if (featureImport) {
|
|
658
|
+
return {
|
|
659
|
+
severity: "error",
|
|
660
|
+
code: "BEIGNET_IMPORT_DIRECTION",
|
|
661
|
+
file,
|
|
662
|
+
importPath: reference.importPath,
|
|
663
|
+
message: featureImport,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
455
667
|
const appImport = reference.resolvedPath
|
|
456
668
|
? appImportViolation(sourceLayer, targetLayer)
|
|
457
669
|
: undefined;
|
|
@@ -459,23 +671,24 @@ function lintImport(
|
|
|
459
671
|
if (appImport) {
|
|
460
672
|
return {
|
|
461
673
|
severity: "error",
|
|
462
|
-
code: "
|
|
674
|
+
code: "BEIGNET_IMPORT_DIRECTION",
|
|
463
675
|
file,
|
|
464
676
|
importPath: reference.importPath,
|
|
465
677
|
message: appImportMessage(sourceLayer, reference.importPath, appImport),
|
|
466
678
|
};
|
|
467
679
|
}
|
|
468
680
|
|
|
469
|
-
|
|
470
|
-
reference.
|
|
471
|
-
|
|
472
|
-
|
|
681
|
+
const bannedPackage = reference.packageName
|
|
682
|
+
? packageImportViolation(sourceLayer, reference.importPath)
|
|
683
|
+
: undefined;
|
|
684
|
+
|
|
685
|
+
if (bannedPackage) {
|
|
473
686
|
return {
|
|
474
687
|
severity: "error",
|
|
475
|
-
code: "
|
|
688
|
+
code: "BEIGNET_IMPORT_DIRECTION",
|
|
476
689
|
file,
|
|
477
690
|
importPath: reference.importPath,
|
|
478
|
-
message: packageImportMessage(sourceLayer,
|
|
691
|
+
message: packageImportMessage(sourceLayer, bannedPackage),
|
|
479
692
|
};
|
|
480
693
|
}
|
|
481
694
|
|
|
@@ -504,13 +717,14 @@ function appImportViolation(
|
|
|
504
717
|
"route",
|
|
505
718
|
"server",
|
|
506
719
|
"use-case",
|
|
720
|
+
"workflow",
|
|
507
721
|
].includes(targetLayer)
|
|
508
722
|
) {
|
|
509
723
|
return targetLayer;
|
|
510
724
|
}
|
|
511
725
|
|
|
512
726
|
if (
|
|
513
|
-
sourceLayer === "use-case" &&
|
|
727
|
+
(sourceLayer === "use-case" || sourceLayer === "workflow") &&
|
|
514
728
|
["app", "client", "component", "infra", "route", "server"].includes(
|
|
515
729
|
targetLayer,
|
|
516
730
|
)
|
|
@@ -520,23 +734,38 @@ function appImportViolation(
|
|
|
520
734
|
|
|
521
735
|
if (
|
|
522
736
|
["feature-port", "policy", "port"].includes(sourceLayer) &&
|
|
523
|
-
[
|
|
524
|
-
|
|
525
|
-
|
|
737
|
+
[
|
|
738
|
+
"app",
|
|
739
|
+
"client",
|
|
740
|
+
"component",
|
|
741
|
+
"infra",
|
|
742
|
+
"route",
|
|
743
|
+
"server",
|
|
744
|
+
"workflow",
|
|
745
|
+
].includes(targetLayer)
|
|
526
746
|
) {
|
|
527
747
|
return targetLayer;
|
|
528
748
|
}
|
|
529
749
|
|
|
530
750
|
if (
|
|
531
751
|
sourceLayer === "contract" &&
|
|
532
|
-
["app", "client", "component", "infra", "route"].includes(
|
|
752
|
+
["app", "client", "component", "infra", "route", "workflow"].includes(
|
|
753
|
+
targetLayer,
|
|
754
|
+
)
|
|
533
755
|
) {
|
|
534
756
|
return targetLayer;
|
|
535
757
|
}
|
|
536
758
|
|
|
537
759
|
if (
|
|
538
760
|
sourceLayer === "route" &&
|
|
539
|
-
["app", "client", "component", "infra"].includes(targetLayer)
|
|
761
|
+
["app", "client", "component", "infra", "workflow"].includes(targetLayer)
|
|
762
|
+
) {
|
|
763
|
+
return targetLayer;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (
|
|
767
|
+
sourceLayer === "infra" &&
|
|
768
|
+
["app", "client", "component", "route", "server"].includes(targetLayer)
|
|
540
769
|
) {
|
|
541
770
|
return targetLayer;
|
|
542
771
|
}
|
|
@@ -546,35 +775,265 @@ function appImportViolation(
|
|
|
546
775
|
|
|
547
776
|
function packageImportViolation(
|
|
548
777
|
sourceLayer: SourceLayer,
|
|
549
|
-
|
|
550
|
-
):
|
|
778
|
+
importPath: string,
|
|
779
|
+
): string | undefined {
|
|
551
780
|
if (sourceLayer === "domain") {
|
|
552
781
|
return (
|
|
553
|
-
domainBannedPackages
|
|
554
|
-
|
|
782
|
+
matchBannedPackage(domainBannedPackages, importPath) ??
|
|
783
|
+
matchBeignetProviderPackage(importPath)
|
|
555
784
|
);
|
|
556
785
|
}
|
|
557
786
|
|
|
558
787
|
if (
|
|
559
|
-
[
|
|
560
|
-
|
|
561
|
-
|
|
788
|
+
[
|
|
789
|
+
"contract",
|
|
790
|
+
"feature-port",
|
|
791
|
+
"policy",
|
|
792
|
+
"port",
|
|
793
|
+
"use-case",
|
|
794
|
+
"workflow",
|
|
795
|
+
].includes(sourceLayer)
|
|
562
796
|
) {
|
|
563
797
|
return (
|
|
564
|
-
coreBannedPackages
|
|
565
|
-
serverRuntimePackages
|
|
566
|
-
|
|
798
|
+
matchBannedPackage(coreBannedPackages, importPath) ??
|
|
799
|
+
matchBannedPackage(serverRuntimePackages, importPath) ??
|
|
800
|
+
matchBeignetProviderPackage(importPath)
|
|
567
801
|
);
|
|
568
802
|
}
|
|
569
803
|
|
|
570
804
|
if (sourceLayer === "route") {
|
|
571
805
|
return (
|
|
572
|
-
coreBannedPackages
|
|
573
|
-
|
|
806
|
+
matchBannedPackage(coreBannedPackages, importPath) ??
|
|
807
|
+
matchBeignetProviderPackage(importPath)
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return undefined;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function matchBannedPackage(
|
|
815
|
+
bannedPackages: ReadonlySet<string>,
|
|
816
|
+
importPath: string,
|
|
817
|
+
): string | undefined {
|
|
818
|
+
let candidate = importPath;
|
|
819
|
+
|
|
820
|
+
while (candidate.length > 0) {
|
|
821
|
+
if (bannedPackages.has(candidate)) return candidate;
|
|
822
|
+
|
|
823
|
+
const separator = candidate.lastIndexOf("/");
|
|
824
|
+
if (separator === -1) return undefined;
|
|
825
|
+
candidate = candidate.slice(0, separator);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function matchBeignetProviderPackage(importPath: string): string | undefined {
|
|
832
|
+
const name = packageName(importPath);
|
|
833
|
+
return name.startsWith("@beignet/provider-") ? name : undefined;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function lintRuntimeBoundary({
|
|
837
|
+
config,
|
|
838
|
+
graph,
|
|
839
|
+
sourceByFile,
|
|
840
|
+
}: {
|
|
841
|
+
config: ResolvedBeignetConfig;
|
|
842
|
+
graph: ImportGraph;
|
|
843
|
+
sourceByFile: Map<string, string>;
|
|
844
|
+
}): LintDiagnostic[] {
|
|
845
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
846
|
+
|
|
847
|
+
for (const file of graph.keys()) {
|
|
848
|
+
const rootKind = runtimeBoundaryRootKind(
|
|
849
|
+
file,
|
|
850
|
+
sourceByFile.get(file) ?? "",
|
|
851
|
+
config,
|
|
852
|
+
);
|
|
853
|
+
if (!rootKind) continue;
|
|
854
|
+
|
|
855
|
+
diagnostics.push(
|
|
856
|
+
...lintRuntimeBoundaryRoot({
|
|
857
|
+
root: file,
|
|
858
|
+
rootKind,
|
|
859
|
+
config,
|
|
860
|
+
graph,
|
|
861
|
+
}),
|
|
574
862
|
);
|
|
575
863
|
}
|
|
576
864
|
|
|
577
|
-
return
|
|
865
|
+
return diagnostics;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function lintRuntimeBoundaryRoot({
|
|
869
|
+
root,
|
|
870
|
+
rootKind,
|
|
871
|
+
config,
|
|
872
|
+
graph,
|
|
873
|
+
}: {
|
|
874
|
+
root: string;
|
|
875
|
+
rootKind: RuntimeBoundaryRootKind;
|
|
876
|
+
config: ResolvedBeignetConfig;
|
|
877
|
+
graph: ImportGraph;
|
|
878
|
+
}): LintDiagnostic[] {
|
|
879
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
880
|
+
const visited = new Set<string>();
|
|
881
|
+
const queue: Array<{ file: string; chain: string[] }> = [
|
|
882
|
+
{ file: root, chain: [root] },
|
|
883
|
+
];
|
|
884
|
+
|
|
885
|
+
while (queue.length > 0) {
|
|
886
|
+
const current = queue.shift();
|
|
887
|
+
if (!current) continue;
|
|
888
|
+
if (visited.has(current.file)) continue;
|
|
889
|
+
visited.add(current.file);
|
|
890
|
+
|
|
891
|
+
for (const reference of graph.get(current.file) ?? []) {
|
|
892
|
+
if (
|
|
893
|
+
reference.kind === "type" &&
|
|
894
|
+
reference.importPath !== "@beignet/core/server-only"
|
|
895
|
+
) {
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const chain = reference.resolvedPath
|
|
900
|
+
? [...current.chain, reference.resolvedPath]
|
|
901
|
+
: current.chain;
|
|
902
|
+
const violation = runtimeBoundaryViolation(reference, config);
|
|
903
|
+
|
|
904
|
+
if (violation) {
|
|
905
|
+
diagnostics.push({
|
|
906
|
+
severity: "error",
|
|
907
|
+
code: "BEIGNET_RUNTIME_BOUNDARY",
|
|
908
|
+
file: root,
|
|
909
|
+
importPath: reference.importPath,
|
|
910
|
+
message: runtimeBoundaryMessage(rootKind, violation, chain),
|
|
911
|
+
});
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (reference.resolvedPath && graph.has(reference.resolvedPath)) {
|
|
916
|
+
queue.push({
|
|
917
|
+
file: reference.resolvedPath,
|
|
918
|
+
chain,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return diagnostics;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function runtimeBoundaryRootKind(
|
|
928
|
+
file: string,
|
|
929
|
+
source: string,
|
|
930
|
+
config: ResolvedBeignetConfig,
|
|
931
|
+
): RuntimeBoundaryRootKind | undefined {
|
|
932
|
+
const layer = classifyPath(file, config);
|
|
933
|
+
if (layer === "contract") return "contract";
|
|
934
|
+
if (layer === "client") return "client";
|
|
935
|
+
if (hasUseClientDirective(source)) return "client";
|
|
936
|
+
if (isGeneratedClientAdapter(file)) return "client";
|
|
937
|
+
return undefined;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function hasUseClientDirective(source: string): boolean {
|
|
941
|
+
const trimmed = source.trimStart();
|
|
942
|
+
return (
|
|
943
|
+
trimmed.startsWith('"use client";') ||
|
|
944
|
+
trimmed.startsWith('"use client"') ||
|
|
945
|
+
trimmed.startsWith("'use client';") ||
|
|
946
|
+
trimmed.startsWith("'use client'")
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function isGeneratedClientAdapter(file: string): boolean {
|
|
951
|
+
return (
|
|
952
|
+
file === "client/index.ts" ||
|
|
953
|
+
file === "client/api-client.ts" ||
|
|
954
|
+
file === "client/rhf.ts" ||
|
|
955
|
+
file === "client/rq.ts" ||
|
|
956
|
+
file === "client/uploads.ts"
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function runtimeBoundaryViolation(
|
|
961
|
+
reference: ImportReference,
|
|
962
|
+
config: ResolvedBeignetConfig,
|
|
963
|
+
): string | undefined {
|
|
964
|
+
if (reference.resolvedPath) {
|
|
965
|
+
const targetLayer = classifyPath(reference.resolvedPath, config);
|
|
966
|
+
if (
|
|
967
|
+
targetLayer === "use-case" ||
|
|
968
|
+
targetLayer === "route" ||
|
|
969
|
+
targetLayer === "infra" ||
|
|
970
|
+
targetLayer === "server"
|
|
971
|
+
) {
|
|
972
|
+
return `${layerLabel(targetLayer)} module ${reference.resolvedPath}`;
|
|
973
|
+
}
|
|
974
|
+
if (targetLayer === "app-context") {
|
|
975
|
+
return `app context module ${reference.resolvedPath}`;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (reference.importPath === "@beignet/core/server-only") {
|
|
980
|
+
return "server-only marker @beignet/core/server-only";
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (reference.importPath.startsWith("node:")) {
|
|
984
|
+
return `Node runtime module ${reference.importPath}`;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (
|
|
988
|
+
serverRuntimePackages.has(reference.importPath) ||
|
|
989
|
+
reference.importPath.startsWith("@beignet/provider-")
|
|
990
|
+
) {
|
|
991
|
+
return `server runtime package ${reference.importPath}`;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return undefined;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function runtimeBoundaryMessage(
|
|
998
|
+
rootKind: RuntimeBoundaryRootKind,
|
|
999
|
+
violation: string,
|
|
1000
|
+
chain: string[],
|
|
1001
|
+
): string {
|
|
1002
|
+
return `${rootKindLabel(
|
|
1003
|
+
rootKind,
|
|
1004
|
+
)} roots must not value-import server-only code. Reached ${violation} through ${formatImportChain(
|
|
1005
|
+
chain,
|
|
1006
|
+
)}. 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.`;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function rootKindLabel(rootKind: RuntimeBoundaryRootKind): string {
|
|
1010
|
+
return rootKind === "contract" ? "contract" : "client";
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function formatImportChain(chain: string[]): string {
|
|
1014
|
+
return chain.join(" -> ");
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function featureImportViolation(
|
|
1018
|
+
file: string,
|
|
1019
|
+
sourceLayer: SourceLayer,
|
|
1020
|
+
resolvedPath: string,
|
|
1021
|
+
config: ResolvedBeignetConfig,
|
|
1022
|
+
): string | undefined {
|
|
1023
|
+
if (sourceLayer !== "domain") return undefined;
|
|
1024
|
+
|
|
1025
|
+
const sourceFeature = featureLayerInfo(file, config);
|
|
1026
|
+
const targetFeature = featureLayerInfo(resolvedPath, config);
|
|
1027
|
+
if (!sourceFeature || !targetFeature) return undefined;
|
|
1028
|
+
if (sourceFeature.layer !== "domain" || targetFeature.layer !== "domain") {
|
|
1029
|
+
return undefined;
|
|
1030
|
+
}
|
|
1031
|
+
if (sourceFeature.feature === targetFeature.feature) return undefined;
|
|
1032
|
+
if (targetFeature.feature === "shared") return undefined;
|
|
1033
|
+
|
|
1034
|
+
return `feature-specific domain files must not import another feature's domain (${stripKnownExtension(
|
|
1035
|
+
resolvedPath,
|
|
1036
|
+
)}). Move shared concepts to features/shared/domain.`;
|
|
578
1037
|
}
|
|
579
1038
|
|
|
580
1039
|
const domainBannedPackages = new Set([
|
|
@@ -716,6 +1175,18 @@ function classifyPath(
|
|
|
716
1175
|
if (layerSegment === "ports") return "feature-port";
|
|
717
1176
|
if (layerSegment === "policy") return "policy";
|
|
718
1177
|
if (layerSegment === "use-cases") return "use-case";
|
|
1178
|
+
if (
|
|
1179
|
+
[
|
|
1180
|
+
"jobs",
|
|
1181
|
+
"listeners",
|
|
1182
|
+
"notifications",
|
|
1183
|
+
"schedules",
|
|
1184
|
+
"tasks",
|
|
1185
|
+
"uploads",
|
|
1186
|
+
].includes(layerSegment)
|
|
1187
|
+
) {
|
|
1188
|
+
return "workflow";
|
|
1189
|
+
}
|
|
719
1190
|
if (layerSegment === "components") return "component";
|
|
720
1191
|
}
|
|
721
1192
|
|
|
@@ -756,6 +1227,23 @@ function featureRelativePath(
|
|
|
756
1227
|
return filePath.slice(prefix.length);
|
|
757
1228
|
}
|
|
758
1229
|
|
|
1230
|
+
function featureLayerInfo(
|
|
1231
|
+
filePath: string,
|
|
1232
|
+
config: ResolvedBeignetConfig,
|
|
1233
|
+
): { feature: string; layer: string | undefined } | undefined {
|
|
1234
|
+
const normalizedPath = stripKnownExtension(normalizePath(filePath));
|
|
1235
|
+
const relativePath = featureRelativePath(
|
|
1236
|
+
normalizedPath,
|
|
1237
|
+
directoryPath(config.paths.features),
|
|
1238
|
+
);
|
|
1239
|
+
if (!relativePath) return undefined;
|
|
1240
|
+
|
|
1241
|
+
const [feature, layer] = relativePath.split("/");
|
|
1242
|
+
if (!feature) return undefined;
|
|
1243
|
+
|
|
1244
|
+
return { feature, layer };
|
|
1245
|
+
}
|
|
1246
|
+
|
|
759
1247
|
function sharedDomainPath(config: ResolvedBeignetConfig): string {
|
|
760
1248
|
const featuresDir = directoryPath(config.paths.features);
|
|
761
1249
|
const featuresParent = directoryPath(path.dirname(featuresDir));
|