@beignet/cli 0.0.3 → 0.0.5
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 +219 -0
- package/README.md +379 -61
- 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 +49 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +53 -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 +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +41 -0
- package/dist/create-prompts.d.ts.map +1 -0
- package/dist/create-prompts.js +78 -0
- package/dist/create-prompts.js.map +1 -0
- package/dist/create.d.ts +10 -5
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +28 -47
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +37 -2
- package/dist/db.js.map +1 -1
- 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 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +665 -603
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +21 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +1938 -131
- 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 +10 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +340 -33
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +20 -3
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +1896 -382
- 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/base.d.ts +13 -0
- package/dist/templates/base.d.ts.map +1 -0
- package/dist/templates/base.js +300 -0
- package/dist/templates/base.js.map +1 -0
- package/dist/templates/db.d.ts +14 -0
- package/dist/templates/db.d.ts.map +1 -0
- package/dist/templates/db.js +962 -0
- package/dist/templates/db.js.map +1 -0
- package/dist/templates/index.d.ts +14 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +123 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/server.d.ts +26 -0
- package/dist/templates/server.d.ts.map +1 -0
- package/dist/templates/server.js +520 -0
- package/dist/templates/server.js.map +1 -0
- package/dist/templates/shadcn.d.ts +5 -0
- package/dist/templates/shadcn.d.ts.map +1 -0
- package/dist/templates/shadcn.js +555 -0
- package/dist/templates/shadcn.js.map +1 -0
- package/dist/templates/shared.d.ts +49 -0
- package/dist/templates/shared.d.ts.map +1 -0
- package/dist/templates/shared.js +57 -0
- package/dist/templates/shared.js.map +1 -0
- package/dist/templates/shell.d.ts +5 -0
- package/dist/templates/shell.d.ts.map +1 -0
- package/dist/templates/shell.js +1190 -0
- package/dist/templates/shell.js.map +1 -0
- package/dist/templates/todos.d.ts +17 -0
- package/dist/templates/todos.d.ts.map +1 -0
- package/dist/templates/todos.js +607 -0
- package/dist/templates/todos.js.map +1 -0
- 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 +83 -0
- package/src/completion.ts +169 -0
- package/src/config.ts +16 -0
- package/src/create-prompts.ts +114 -0
- package/src/create.ts +40 -64
- package/src/db.ts +60 -4
- package/src/github-annotations.ts +37 -0
- package/src/index.ts +1075 -817
- package/src/inspect.ts +2859 -115
- package/src/lib.ts +45 -0
- package/src/lint.ts +493 -39
- package/src/make.ts +2389 -406
- package/src/outbox.ts +249 -0
- package/src/schedule.ts +272 -0
- package/src/task.ts +169 -0
- package/src/templates/base.ts +377 -0
- package/src/templates/db.ts +963 -0
- package/src/templates/index.ts +148 -0
- package/src/templates/server.ts +528 -0
- package/src/templates/shadcn.ts +570 -0
- package/src/templates/shared.ts +90 -0
- package/src/templates/shell.ts +1219 -0
- package/src/templates/todos.ts +607 -0
- 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/dist/templates.d.ts +0 -55
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js +0 -3520
- package/dist/templates.js.map +0 -1
- package/src/create-bin.ts +0 -11
- package/src/templates.ts +0 -3774
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;
|
|
@@ -35,10 +37,18 @@ type SourceLayer =
|
|
|
35
37
|
|
|
36
38
|
type ImportReference = {
|
|
37
39
|
importPath: string;
|
|
40
|
+
kind: ImportKind;
|
|
41
|
+
offset: number;
|
|
38
42
|
resolvedPath?: string;
|
|
39
43
|
packageName?: string;
|
|
40
44
|
};
|
|
41
45
|
|
|
46
|
+
type ImportKind = "type" | "value";
|
|
47
|
+
|
|
48
|
+
type ImportGraph = Map<string, ImportReference[]>;
|
|
49
|
+
|
|
50
|
+
type RuntimeBoundaryRootKind = "client" | "contract";
|
|
51
|
+
|
|
42
52
|
/**
|
|
43
53
|
* Dependency-direction lint diagnostic.
|
|
44
54
|
*/
|
|
@@ -48,12 +58,15 @@ export type LintDiagnostic = {
|
|
|
48
58
|
message: string;
|
|
49
59
|
file: string;
|
|
50
60
|
importPath: string;
|
|
61
|
+
line?: number;
|
|
62
|
+
column?: number;
|
|
51
63
|
};
|
|
52
64
|
|
|
53
65
|
/**
|
|
54
66
|
* Result returned by `beignet lint`.
|
|
55
67
|
*/
|
|
56
68
|
export type LintAppResult = {
|
|
69
|
+
schemaVersion: 1;
|
|
57
70
|
targetDir: string;
|
|
58
71
|
config: ResolvedBeignetConfig;
|
|
59
72
|
diagnostics: LintDiagnostic[];
|
|
@@ -73,29 +86,87 @@ export async function lintApp(
|
|
|
73
86
|
? resolveConfig(options.config)
|
|
74
87
|
: await loadBeignetConfig(targetDir, files);
|
|
75
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();
|
|
76
93
|
|
|
77
|
-
for (const file of
|
|
94
|
+
for (const file of sourceFiles) {
|
|
78
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
|
+
|
|
79
100
|
if (sourceLayer === "test" || sourceLayer === "unknown") continue;
|
|
80
101
|
|
|
81
|
-
const
|
|
82
|
-
for (const reference of
|
|
102
|
+
const lineStarts = computeLineStarts(source);
|
|
103
|
+
for (const reference of importGraph.get(file) ?? []) {
|
|
83
104
|
const diagnostic = lintImport(file, sourceLayer, reference, config);
|
|
84
|
-
if (diagnostic)
|
|
105
|
+
if (diagnostic) {
|
|
106
|
+
diagnostics.push({
|
|
107
|
+
...diagnostic,
|
|
108
|
+
...positionFromOffset(lineStarts, reference.offset),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
85
111
|
}
|
|
86
112
|
}
|
|
87
113
|
|
|
114
|
+
diagnostics.push(
|
|
115
|
+
...lintRuntimeBoundary({
|
|
116
|
+
config,
|
|
117
|
+
graph: importGraph,
|
|
118
|
+
sourceByFile,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
88
122
|
return {
|
|
123
|
+
schemaVersion: 1,
|
|
89
124
|
targetDir,
|
|
90
125
|
config,
|
|
91
126
|
diagnostics: dedupeDiagnostics(diagnostics),
|
|
92
127
|
};
|
|
93
128
|
}
|
|
94
129
|
|
|
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
|
+
|
|
95
161
|
/**
|
|
96
162
|
* Format dependency-direction lint diagnostics for CLI output.
|
|
97
163
|
*/
|
|
98
|
-
export function formatLint(
|
|
164
|
+
export function formatLint(
|
|
165
|
+
result: LintAppResult,
|
|
166
|
+
options: { color?: boolean } = {},
|
|
167
|
+
): string {
|
|
168
|
+
const paint = createPainter(options.color);
|
|
169
|
+
|
|
99
170
|
if (result.diagnostics.length === 0) {
|
|
100
171
|
return `No Beignet lint issues found in ${result.targetDir}.`;
|
|
101
172
|
}
|
|
@@ -106,12 +177,39 @@ export function formatLint(result: LintAppResult): string {
|
|
|
106
177
|
} in ${result.targetDir}:`,
|
|
107
178
|
"",
|
|
108
179
|
...result.diagnostics.map(
|
|
109
|
-
(
|
|
180
|
+
(
|
|
181
|
+
diagnostic,
|
|
182
|
+
) => `${paint("ERROR", "red")} ${diagnostic.code} ${lintDiagnosticLocation(diagnostic)}
|
|
110
183
|
${diagnostic.message}`,
|
|
111
184
|
),
|
|
112
185
|
].join("\n");
|
|
113
186
|
}
|
|
114
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
|
+
|
|
115
213
|
async function assertDirectory(targetDir: string): Promise<void> {
|
|
116
214
|
try {
|
|
117
215
|
const stats = await stat(targetDir);
|
|
@@ -165,17 +263,27 @@ function lintSourceFiles(files: string[]): string[] {
|
|
|
165
263
|
});
|
|
166
264
|
}
|
|
167
265
|
|
|
168
|
-
function parseImports(
|
|
266
|
+
function parseImports(
|
|
267
|
+
source: string,
|
|
268
|
+
importerFile: string,
|
|
269
|
+
files: Set<string>,
|
|
270
|
+
): ImportReference[] {
|
|
169
271
|
const references = parseImportSpecifiers(source);
|
|
170
272
|
|
|
171
|
-
return references.map((
|
|
172
|
-
|
|
173
|
-
...resolveImport(importPath, importerFile),
|
|
273
|
+
return references.map((reference) => ({
|
|
274
|
+
...reference,
|
|
275
|
+
...resolveImport(reference.importPath, importerFile, files),
|
|
174
276
|
}));
|
|
175
277
|
}
|
|
176
278
|
|
|
177
|
-
function parseImportSpecifiers(
|
|
178
|
-
|
|
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
|
+
}> = [];
|
|
179
287
|
let index = 0;
|
|
180
288
|
|
|
181
289
|
while (index < source.length) {
|
|
@@ -183,14 +291,26 @@ function parseImportSpecifiers(source: string): string[] {
|
|
|
183
291
|
|
|
184
292
|
if (isKeywordAt(source, index, "import")) {
|
|
185
293
|
const parsed = parseImportSpecifierAt(source, index + "import".length);
|
|
186
|
-
if (parsed?.specifier)
|
|
294
|
+
if (parsed?.specifier) {
|
|
295
|
+
specifiers.push({
|
|
296
|
+
importPath: parsed.specifier,
|
|
297
|
+
kind: parsed.kind ?? "value",
|
|
298
|
+
offset: index,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
187
301
|
index = parsed?.end ?? index + "import".length;
|
|
188
302
|
continue;
|
|
189
303
|
}
|
|
190
304
|
|
|
191
305
|
if (isKeywordAt(source, index, "export")) {
|
|
192
306
|
const parsed = parseExportSpecifierAt(source, index + "export".length);
|
|
193
|
-
if (parsed?.specifier)
|
|
307
|
+
if (parsed?.specifier) {
|
|
308
|
+
specifiers.push({
|
|
309
|
+
importPath: parsed.specifier,
|
|
310
|
+
kind: parsed.kind ?? "value",
|
|
311
|
+
offset: index,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
194
314
|
index = parsed?.end ?? index + "export".length;
|
|
195
315
|
continue;
|
|
196
316
|
}
|
|
@@ -204,7 +324,7 @@ function parseImportSpecifiers(source: string): string[] {
|
|
|
204
324
|
function parseImportSpecifierAt(
|
|
205
325
|
source: string,
|
|
206
326
|
index: number,
|
|
207
|
-
): { specifier?: string; end: number } | undefined {
|
|
327
|
+
): { specifier?: string; kind?: ImportKind; end: number } | undefined {
|
|
208
328
|
let cursor = skipWhitespaceAndComments(source, index);
|
|
209
329
|
if (source[cursor] === "(") {
|
|
210
330
|
cursor = skipWhitespaceAndComments(source, cursor + 1);
|
|
@@ -220,10 +340,18 @@ function parseImportSpecifierAt(
|
|
|
220
340
|
if (sideEffectImport) {
|
|
221
341
|
return {
|
|
222
342
|
specifier: sideEffectImport.value,
|
|
343
|
+
kind: "value",
|
|
223
344
|
end: sideEffectImport.end,
|
|
224
345
|
};
|
|
225
346
|
}
|
|
226
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
|
+
|
|
227
355
|
while (cursor < source.length) {
|
|
228
356
|
cursor = skipWhitespaceAndComments(source, cursor);
|
|
229
357
|
|
|
@@ -240,6 +368,9 @@ function parseImportSpecifierAt(
|
|
|
240
368
|
if (!stringLiteral) return { end: fromCursor };
|
|
241
369
|
return {
|
|
242
370
|
specifier: stringLiteral.value,
|
|
371
|
+
kind: isImportType
|
|
372
|
+
? "type"
|
|
373
|
+
: inferImportClauseKind(source.slice(importClauseStart, cursor)),
|
|
243
374
|
end: stringLiteral.end,
|
|
244
375
|
};
|
|
245
376
|
}
|
|
@@ -253,12 +384,20 @@ function parseImportSpecifierAt(
|
|
|
253
384
|
function parseExportSpecifierAt(
|
|
254
385
|
source: string,
|
|
255
386
|
index: number,
|
|
256
|
-
): { specifier?: string; end: number } | undefined {
|
|
387
|
+
): { specifier?: string; kind?: ImportKind; end: number } | undefined {
|
|
257
388
|
let cursor = index;
|
|
389
|
+
let isExportType = false;
|
|
390
|
+
const exportClauseStart = cursor;
|
|
258
391
|
|
|
259
392
|
while (cursor < source.length) {
|
|
260
393
|
cursor = skipWhitespaceAndComments(source, cursor);
|
|
261
394
|
|
|
395
|
+
if (isKeywordAt(source, cursor, "type")) {
|
|
396
|
+
isExportType = true;
|
|
397
|
+
cursor = cursorAfterKeyword("type", cursor);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
262
401
|
if (source[cursor] === ";" || source[cursor] === "\n") {
|
|
263
402
|
return { end: cursor + 1 };
|
|
264
403
|
}
|
|
@@ -272,6 +411,9 @@ function parseExportSpecifierAt(
|
|
|
272
411
|
if (!stringLiteral) return { end: fromCursor };
|
|
273
412
|
return {
|
|
274
413
|
specifier: stringLiteral.value,
|
|
414
|
+
kind: isExportType
|
|
415
|
+
? "type"
|
|
416
|
+
: inferImportClauseKind(source.slice(exportClauseStart, cursor)),
|
|
275
417
|
end: stringLiteral.end,
|
|
276
418
|
};
|
|
277
419
|
}
|
|
@@ -282,6 +424,14 @@ function parseExportSpecifierAt(
|
|
|
282
424
|
return undefined;
|
|
283
425
|
}
|
|
284
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
|
+
|
|
285
435
|
function cursorAfterKeyword(keyword: string, index: number): number {
|
|
286
436
|
return index + keyword.length;
|
|
287
437
|
}
|
|
@@ -425,17 +575,24 @@ function isIdentifierChar(char: string | undefined): boolean {
|
|
|
425
575
|
function resolveImport(
|
|
426
576
|
importPath: string,
|
|
427
577
|
importerFile: string,
|
|
578
|
+
files: Set<string>,
|
|
428
579
|
): Pick<ImportReference, "packageName" | "resolvedPath"> {
|
|
429
580
|
if (importPath.startsWith("@/")) {
|
|
430
581
|
return {
|
|
431
|
-
resolvedPath:
|
|
582
|
+
resolvedPath: resolveLocalImportPath(
|
|
583
|
+
stripKnownExtension(importPath.slice(2)),
|
|
584
|
+
files,
|
|
585
|
+
),
|
|
432
586
|
};
|
|
433
587
|
}
|
|
434
588
|
|
|
435
589
|
if (importPath.startsWith(".")) {
|
|
436
590
|
return {
|
|
437
|
-
resolvedPath:
|
|
438
|
-
|
|
591
|
+
resolvedPath: resolveLocalImportPath(
|
|
592
|
+
stripKnownExtension(
|
|
593
|
+
normalizePath(path.join(path.dirname(importerFile), importPath)),
|
|
594
|
+
),
|
|
595
|
+
files,
|
|
439
596
|
),
|
|
440
597
|
};
|
|
441
598
|
}
|
|
@@ -445,6 +602,33 @@ function resolveImport(
|
|
|
445
602
|
};
|
|
446
603
|
}
|
|
447
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
|
+
|
|
448
632
|
function packageName(importPath: string): string {
|
|
449
633
|
if (!importPath.startsWith("@")) return importPath.split("/")[0];
|
|
450
634
|
|
|
@@ -462,9 +646,24 @@ function lintImport(
|
|
|
462
646
|
reference: ImportReference,
|
|
463
647
|
config: ResolvedBeignetConfig,
|
|
464
648
|
): LintDiagnostic | undefined {
|
|
649
|
+
if (reference.kind === "type") return undefined;
|
|
650
|
+
|
|
465
651
|
const targetLayer = reference.resolvedPath
|
|
466
652
|
? classifyPath(reference.resolvedPath, config)
|
|
467
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
|
+
|
|
468
667
|
const appImport = reference.resolvedPath
|
|
469
668
|
? appImportViolation(sourceLayer, targetLayer)
|
|
470
669
|
: undefined;
|
|
@@ -472,23 +671,24 @@ function lintImport(
|
|
|
472
671
|
if (appImport) {
|
|
473
672
|
return {
|
|
474
673
|
severity: "error",
|
|
475
|
-
code: "
|
|
674
|
+
code: "BEIGNET_IMPORT_DIRECTION",
|
|
476
675
|
file,
|
|
477
676
|
importPath: reference.importPath,
|
|
478
677
|
message: appImportMessage(sourceLayer, reference.importPath, appImport),
|
|
479
678
|
};
|
|
480
679
|
}
|
|
481
680
|
|
|
482
|
-
|
|
483
|
-
reference.
|
|
484
|
-
|
|
485
|
-
|
|
681
|
+
const bannedPackage = reference.packageName
|
|
682
|
+
? packageImportViolation(sourceLayer, reference.importPath)
|
|
683
|
+
: undefined;
|
|
684
|
+
|
|
685
|
+
if (bannedPackage) {
|
|
486
686
|
return {
|
|
487
687
|
severity: "error",
|
|
488
|
-
code: "
|
|
688
|
+
code: "BEIGNET_IMPORT_DIRECTION",
|
|
489
689
|
file,
|
|
490
690
|
importPath: reference.importPath,
|
|
491
|
-
message: packageImportMessage(sourceLayer,
|
|
691
|
+
message: packageImportMessage(sourceLayer, bannedPackage),
|
|
492
692
|
};
|
|
493
693
|
}
|
|
494
694
|
|
|
@@ -563,17 +763,24 @@ function appImportViolation(
|
|
|
563
763
|
return targetLayer;
|
|
564
764
|
}
|
|
565
765
|
|
|
766
|
+
if (
|
|
767
|
+
sourceLayer === "infra" &&
|
|
768
|
+
["app", "client", "component", "route", "server"].includes(targetLayer)
|
|
769
|
+
) {
|
|
770
|
+
return targetLayer;
|
|
771
|
+
}
|
|
772
|
+
|
|
566
773
|
return undefined;
|
|
567
774
|
}
|
|
568
775
|
|
|
569
776
|
function packageImportViolation(
|
|
570
777
|
sourceLayer: SourceLayer,
|
|
571
|
-
|
|
572
|
-
):
|
|
778
|
+
importPath: string,
|
|
779
|
+
): string | undefined {
|
|
573
780
|
if (sourceLayer === "domain") {
|
|
574
781
|
return (
|
|
575
|
-
domainBannedPackages
|
|
576
|
-
|
|
782
|
+
matchBannedPackage(domainBannedPackages, importPath) ??
|
|
783
|
+
matchBeignetProviderPackage(importPath)
|
|
577
784
|
);
|
|
578
785
|
}
|
|
579
786
|
|
|
@@ -588,20 +795,245 @@ function packageImportViolation(
|
|
|
588
795
|
].includes(sourceLayer)
|
|
589
796
|
) {
|
|
590
797
|
return (
|
|
591
|
-
coreBannedPackages
|
|
592
|
-
serverRuntimePackages
|
|
593
|
-
|
|
798
|
+
matchBannedPackage(coreBannedPackages, importPath) ??
|
|
799
|
+
matchBannedPackage(serverRuntimePackages, importPath) ??
|
|
800
|
+
matchBeignetProviderPackage(importPath)
|
|
594
801
|
);
|
|
595
802
|
}
|
|
596
803
|
|
|
597
804
|
if (sourceLayer === "route") {
|
|
598
805
|
return (
|
|
599
|
-
coreBannedPackages
|
|
600
|
-
|
|
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
|
+
}),
|
|
601
862
|
);
|
|
602
863
|
}
|
|
603
864
|
|
|
604
|
-
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.`;
|
|
605
1037
|
}
|
|
606
1038
|
|
|
607
1039
|
const domainBannedPackages = new Set([
|
|
@@ -744,9 +1176,14 @@ function classifyPath(
|
|
|
744
1176
|
if (layerSegment === "policy") return "policy";
|
|
745
1177
|
if (layerSegment === "use-cases") return "use-case";
|
|
746
1178
|
if (
|
|
747
|
-
[
|
|
748
|
-
|
|
749
|
-
|
|
1179
|
+
[
|
|
1180
|
+
"jobs",
|
|
1181
|
+
"listeners",
|
|
1182
|
+
"notifications",
|
|
1183
|
+
"schedules",
|
|
1184
|
+
"tasks",
|
|
1185
|
+
"uploads",
|
|
1186
|
+
].includes(layerSegment)
|
|
750
1187
|
) {
|
|
751
1188
|
return "workflow";
|
|
752
1189
|
}
|
|
@@ -790,6 +1227,23 @@ function featureRelativePath(
|
|
|
790
1227
|
return filePath.slice(prefix.length);
|
|
791
1228
|
}
|
|
792
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
|
+
|
|
793
1247
|
function sharedDomainPath(config: ResolvedBeignetConfig): string {
|
|
794
1248
|
const featuresDir = directoryPath(config.paths.features);
|
|
795
1249
|
const featuresParent = directoryPath(path.dirname(featuresDir));
|