@valbuild/cli 0.97.3 → 0.97.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.
Files changed (32) hide show
  1. package/cli/dist/valbuild-cli-cli.cjs.dev.js +1471 -118
  2. package/cli/dist/valbuild-cli-cli.cjs.prod.js +1471 -118
  3. package/cli/dist/valbuild-cli-cli.esm.js +1469 -118
  4. package/package.json +6 -4
  5. package/src/__fixtures__/basic/val.config.ts +2 -2
  6. package/src/__fixtures__/basic/val.modules.ts +16 -0
  7. package/src/__fixtures__/debug-snapshot/.val/patches/11111111-1111-4111-8111-111111111111/patch.json +19 -0
  8. package/src/__fixtures__/debug-snapshot/.val/patches/22222222-2222-4222-8222-222222222222/patch.json +20 -0
  9. package/src/__fixtures__/debug-snapshot/.val/patches/head/patch.json +20 -0
  10. package/src/__fixtures__/debug-snapshot/content/projects.val.ts +23 -0
  11. package/src/__fixtures__/debug-snapshot/content/summary.ts +6 -0
  12. package/src/__fixtures__/debug-snapshot/content/tags.val.ts +10 -0
  13. package/src/__fixtures__/debug-snapshot/content/unrelated.val.ts +5 -0
  14. package/src/__fixtures__/debug-snapshot/tsconfig.json +12 -0
  15. package/src/__fixtures__/debug-snapshot/val.config.ts +5 -0
  16. package/src/__fixtures__/debug-snapshot/val.modules.ts +8 -0
  17. package/src/cli.ts +89 -2
  18. package/src/debug/context.ts +173 -0
  19. package/src/debug/importGraph.ts +126 -0
  20. package/src/debug/moduleClosure.ts +167 -0
  21. package/src/debug/report.ts +80 -0
  22. package/src/debug/snapshot.ts +497 -0
  23. package/src/debug/snapshotRoundTrip.test.ts +95 -0
  24. package/src/debug.test.ts +107 -0
  25. package/src/debug.ts +120 -0
  26. package/src/deleteUnappliablePatches.ts +139 -0
  27. package/src/listUnusedFiles.ts +16 -4
  28. package/src/runValidation.test.ts +6 -6
  29. package/src/runValidation.ts +40 -15
  30. package/src/utils/evalValConfigFile.ts +13 -5
  31. package/src/utils/sourcePathToFileLocation.ts +184 -0
  32. 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
+ }