@valbuild/cli 0.97.3 → 0.97.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/cli/dist/valbuild-cli-cli.cjs.dev.js +1471 -118
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +1471 -118
- package/cli/dist/valbuild-cli-cli.esm.js +1469 -118
- package/package.json +6 -4
- package/src/__fixtures__/basic/val.config.ts +2 -2
- package/src/__fixtures__/basic/val.modules.ts +16 -0
- package/src/__fixtures__/debug-snapshot/.val/patches/11111111-1111-4111-8111-111111111111/patch.json +19 -0
- package/src/__fixtures__/debug-snapshot/.val/patches/22222222-2222-4222-8222-222222222222/patch.json +20 -0
- package/src/__fixtures__/debug-snapshot/.val/patches/head/patch.json +20 -0
- package/src/__fixtures__/debug-snapshot/content/projects.val.ts +23 -0
- package/src/__fixtures__/debug-snapshot/content/summary.ts +6 -0
- package/src/__fixtures__/debug-snapshot/content/tags.val.ts +10 -0
- package/src/__fixtures__/debug-snapshot/content/unrelated.val.ts +5 -0
- package/src/__fixtures__/debug-snapshot/tsconfig.json +12 -0
- package/src/__fixtures__/debug-snapshot/val.config.ts +5 -0
- package/src/__fixtures__/debug-snapshot/val.modules.ts +8 -0
- package/src/cli.ts +89 -2
- package/src/debug/context.ts +173 -0
- package/src/debug/importGraph.ts +126 -0
- package/src/debug/moduleClosure.ts +167 -0
- package/src/debug/report.ts +80 -0
- package/src/debug/snapshot.ts +497 -0
- package/src/debug/snapshotRoundTrip.test.ts +95 -0
- package/src/debug.test.ts +107 -0
- package/src/debug.ts +120 -0
- package/src/deleteUnappliablePatches.ts +139 -0
- package/src/listUnusedFiles.ts +16 -4
- package/src/runValidation.test.ts +6 -6
- package/src/runValidation.ts +40 -15
- package/src/utils/evalValConfigFile.ts +13 -5
- package/src/utils/sourcePathToFileLocation.ts +184 -0
- package/src/validate.ts +415 -154
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import nodeFs from "fs";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import picocolors from "picocolors";
|
|
5
|
+
import { Internal, type SourcePath } from "@valbuild/core";
|
|
6
|
+
import {
|
|
7
|
+
createModulePathMap,
|
|
8
|
+
getModulePathRange,
|
|
9
|
+
type ModulePathMap,
|
|
10
|
+
} from "@valbuild/server";
|
|
11
|
+
|
|
12
|
+
type CachedFile = { lines: string[]; map: ModulePathMap | undefined };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Per-run cache mapping a moduleFilePath to its parsed source. `null` means the
|
|
16
|
+
* file could not be read. Each val file is read and parsed at most once.
|
|
17
|
+
*/
|
|
18
|
+
export type SourceFileCache = Map<string, CachedFile | null>;
|
|
19
|
+
|
|
20
|
+
type Position = { line: number; character: number };
|
|
21
|
+
type Range = { start: Position; end: Position };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolves a validation `sourcePath` (e.g.
|
|
25
|
+
* `/content/page.val.ts?p="image"."metadata"`) to a clickable
|
|
26
|
+
* `relativePath:line:col` location pointing at the offending source literal in
|
|
27
|
+
* the val file.
|
|
28
|
+
*
|
|
29
|
+
* Falls back to the raw `sourcePath` when the file cannot be read or no range
|
|
30
|
+
* can be resolved.
|
|
31
|
+
*/
|
|
32
|
+
export function sourcePathToFileLocation(
|
|
33
|
+
sourcePath: string,
|
|
34
|
+
projectRoot: string,
|
|
35
|
+
cache: SourceFileCache,
|
|
36
|
+
target: "key" | "value" = "value",
|
|
37
|
+
): string {
|
|
38
|
+
const resolved = resolveRange(sourcePath, projectRoot, cache, target);
|
|
39
|
+
if (!resolved) {
|
|
40
|
+
return sourcePath;
|
|
41
|
+
}
|
|
42
|
+
// TS line/character are 0-indexed; editors/terminals expect 1-indexed.
|
|
43
|
+
const { relativeFile, range } = resolved;
|
|
44
|
+
return `${relativeFile}:${range.start.line + 1}:${range.start.character + 1}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolves a validation `sourcePath` to its individual location parts: the
|
|
49
|
+
* relative file path and the 1-indexed line/column of the offending literal.
|
|
50
|
+
* Returns `undefined` when the location cannot be resolved (the caller should
|
|
51
|
+
* then fall back to the raw `sourcePath`).
|
|
52
|
+
*/
|
|
53
|
+
export function sourcePathToLocationParts(
|
|
54
|
+
sourcePath: string,
|
|
55
|
+
projectRoot: string,
|
|
56
|
+
cache: SourceFileCache,
|
|
57
|
+
target: "key" | "value" = "value",
|
|
58
|
+
): { relativeFile: string; line: number; character: number } | undefined {
|
|
59
|
+
const resolved = resolveRange(sourcePath, projectRoot, cache, target);
|
|
60
|
+
if (!resolved) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
// TS line/character are 0-indexed; editors/terminals expect 1-indexed.
|
|
64
|
+
const { relativeFile, range } = resolved;
|
|
65
|
+
return {
|
|
66
|
+
relativeFile,
|
|
67
|
+
line: range.start.line + 1,
|
|
68
|
+
character: range.start.character + 1,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Renders a Rust-style code frame for the offending `sourcePath`: the line
|
|
74
|
+
* above, the offending line, and the line below, with red carets underlining
|
|
75
|
+
* the offending span. Returns `undefined` when the location cannot be resolved
|
|
76
|
+
* (the caller should then skip the frame).
|
|
77
|
+
*/
|
|
78
|
+
export function sourcePathToCodeFrame(
|
|
79
|
+
sourcePath: string,
|
|
80
|
+
projectRoot: string,
|
|
81
|
+
cache: SourceFileCache,
|
|
82
|
+
target: "key" | "value" = "value",
|
|
83
|
+
): string | undefined {
|
|
84
|
+
const resolved = resolveRange(sourcePath, projectRoot, cache, target);
|
|
85
|
+
if (!resolved) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const { lines, range } = resolved;
|
|
89
|
+
const startLine = range.start.line;
|
|
90
|
+
const firstLine = Math.max(0, startLine - 1);
|
|
91
|
+
const lastLine = Math.min(lines.length - 1, startLine + 1);
|
|
92
|
+
const gutterWidth = String(lastLine + 1).length;
|
|
93
|
+
|
|
94
|
+
const out: string[] = [];
|
|
95
|
+
for (let i = firstLine; i <= lastLine; i++) {
|
|
96
|
+
const lineText = lines[i] ?? "";
|
|
97
|
+
const gutter = picocolors.dim(
|
|
98
|
+
`${String(i + 1).padStart(gutterWidth, " ")} | `,
|
|
99
|
+
);
|
|
100
|
+
out.push(`${gutter}${lineText}`);
|
|
101
|
+
if (i === startLine) {
|
|
102
|
+
const caretStart = range.start.character;
|
|
103
|
+
const sameLine = range.end.line === range.start.line;
|
|
104
|
+
const caretEnd = sameLine ? range.end.character : lineText.length;
|
|
105
|
+
const caretCount = Math.max(1, caretEnd - caretStart);
|
|
106
|
+
const emptyGutter = picocolors.dim(`${" ".repeat(gutterWidth)} | `);
|
|
107
|
+
out.push(
|
|
108
|
+
`${emptyGutter}${" ".repeat(caretStart)}${picocolors.red(
|
|
109
|
+
"^".repeat(caretCount),
|
|
110
|
+
)}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out.join("\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveRange(
|
|
118
|
+
sourcePath: string,
|
|
119
|
+
projectRoot: string,
|
|
120
|
+
cache: SourceFileCache,
|
|
121
|
+
target: "key" | "value",
|
|
122
|
+
): { relativeFile: string; lines: string[]; range: Range } | undefined {
|
|
123
|
+
const [moduleFilePath, modulePath] =
|
|
124
|
+
Internal.splitModuleFilePathAndModulePath(sourcePath as SourcePath);
|
|
125
|
+
|
|
126
|
+
const cached = getCachedFile(moduleFilePath, projectRoot, cache);
|
|
127
|
+
if (!cached || !cached.map) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const range = getModulePathRange(modulePath, cached.map, target);
|
|
132
|
+
if (!range) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
relativeFile: moduleFilePath.replace(/^\//, ""),
|
|
138
|
+
lines: cached.lines,
|
|
139
|
+
// Defensive: a bad range must degrade to an odd-looking frame, never a
|
|
140
|
+
// crash - the caret math below does `" ".repeat(start.character)`, which
|
|
141
|
+
// throws a RangeError on a negative count.
|
|
142
|
+
range: {
|
|
143
|
+
start: clampPosition(range.start),
|
|
144
|
+
end: clampPosition(range.end),
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function clampPosition(position: Position): Position {
|
|
150
|
+
return {
|
|
151
|
+
line: Math.max(0, position.line),
|
|
152
|
+
character: Math.max(0, position.character),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getCachedFile(
|
|
157
|
+
moduleFilePath: string,
|
|
158
|
+
projectRoot: string,
|
|
159
|
+
cache: SourceFileCache,
|
|
160
|
+
): CachedFile | null {
|
|
161
|
+
const existing = cache.get(moduleFilePath);
|
|
162
|
+
if (existing !== undefined) {
|
|
163
|
+
return existing;
|
|
164
|
+
}
|
|
165
|
+
const filePath = path.join(projectRoot, moduleFilePath);
|
|
166
|
+
let fileContent: string;
|
|
167
|
+
try {
|
|
168
|
+
fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
169
|
+
} catch {
|
|
170
|
+
cache.set(moduleFilePath, null);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const sourceFile = ts.createSourceFile(
|
|
174
|
+
filePath,
|
|
175
|
+
fileContent,
|
|
176
|
+
ts.ScriptTarget.ES2015,
|
|
177
|
+
);
|
|
178
|
+
const entry: CachedFile = {
|
|
179
|
+
lines: fileContent.split(/\r?\n/),
|
|
180
|
+
map: createModulePathMap(sourceFile),
|
|
181
|
+
};
|
|
182
|
+
cache.set(moduleFilePath, entry);
|
|
183
|
+
return entry;
|
|
184
|
+
}
|