@valbuild/cli 0.97.2 → 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
package/src/validate.ts
CHANGED
|
@@ -6,181 +6,442 @@ import { DEFAULT_CONTENT_HOST, DEFAULT_VAL_REMOTE_HOST } from "@valbuild/core";
|
|
|
6
6
|
import { getSettings, uploadRemoteFile } from "@valbuild/server";
|
|
7
7
|
import { evalValConfigFile } from "./utils/evalValConfigFile";
|
|
8
8
|
import { createDefaultValFSHost, runValidation } from "./runValidation";
|
|
9
|
+
import {
|
|
10
|
+
sourcePathToCodeFrame,
|
|
11
|
+
sourcePathToLocationParts,
|
|
12
|
+
type SourceFileCache,
|
|
13
|
+
} from "./utils/sourcePathToFileLocation";
|
|
14
|
+
|
|
15
|
+
type Diagnostic = {
|
|
16
|
+
// "fixable" => ⚠ (run --fix), "error" => ✘
|
|
17
|
+
severity: "fixable" | "error";
|
|
18
|
+
sourcePath: string;
|
|
19
|
+
message: string;
|
|
20
|
+
keyError?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ModuleReport = {
|
|
24
|
+
// Relative file path (no leading slash).
|
|
25
|
+
file: string;
|
|
26
|
+
durationMs: number;
|
|
27
|
+
diagnostics: Diagnostic[];
|
|
28
|
+
};
|
|
9
29
|
|
|
10
30
|
export async function validate({
|
|
11
31
|
root,
|
|
12
32
|
fix,
|
|
33
|
+
watch,
|
|
13
34
|
}: {
|
|
14
35
|
root?: string;
|
|
15
36
|
fix?: boolean;
|
|
37
|
+
watch?: boolean;
|
|
16
38
|
}) {
|
|
17
39
|
const projectRoot = root ? path.resolve(root) : process.cwd();
|
|
18
40
|
|
|
19
|
-
|
|
20
|
-
(await evalValConfigFile(projectRoot, "val.config.ts")) ||
|
|
21
|
-
(await evalValConfigFile(projectRoot, "val.config.js"));
|
|
22
|
-
|
|
23
|
-
const resolvedValConfigFile = valConfigFile
|
|
24
|
-
? {
|
|
25
|
-
...valConfigFile,
|
|
26
|
-
project: process.env.VAL_PROJECT || valConfigFile.project,
|
|
27
|
-
}
|
|
28
|
-
: process.env.VAL_PROJECT
|
|
29
|
-
? { project: process.env.VAL_PROJECT }
|
|
30
|
-
: undefined;
|
|
31
|
-
|
|
32
|
-
console.log(
|
|
33
|
-
picocolors.greenBright(
|
|
34
|
-
`Validating project${resolvedValConfigFile?.project ? ` '${picocolors.inverse(resolvedValConfigFile.project)}'` : ""}...`,
|
|
35
|
-
),
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const valFiles: string[] = await glob("**/*.val.{js,ts}", {
|
|
39
|
-
ignore: ["node_modules/**"],
|
|
40
|
-
cwd: projectRoot,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
console.log(picocolors.greenBright(`Found ${valFiles.length} files...`));
|
|
44
|
-
|
|
45
|
-
let prettier;
|
|
41
|
+
let prettier: typeof import("prettier") | undefined;
|
|
46
42
|
try {
|
|
47
|
-
prettier =
|
|
43
|
+
prettier = await import("prettier");
|
|
48
44
|
} catch {
|
|
49
45
|
console.log("Prettier not found, skipping formatting");
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
48
|
+
// Runs a single validation pass over the project and returns the number of
|
|
49
|
+
// errors found. Re-reads config and val files each call so it always reflects
|
|
50
|
+
// the latest state on disk (used both for one-shot and watch mode).
|
|
51
|
+
async function runOnce(): Promise<number> {
|
|
52
|
+
const valConfigFile =
|
|
53
|
+
(await evalValConfigFile(projectRoot, "val.config.ts")) ||
|
|
54
|
+
(await evalValConfigFile(projectRoot, "val.config.js"));
|
|
55
|
+
|
|
56
|
+
const resolvedValConfigFile = valConfigFile
|
|
57
|
+
? {
|
|
58
|
+
...valConfigFile,
|
|
59
|
+
project: process.env.VAL_PROJECT || valConfigFile.project,
|
|
60
|
+
}
|
|
61
|
+
: process.env.VAL_PROJECT
|
|
62
|
+
? { project: process.env.VAL_PROJECT }
|
|
63
|
+
: undefined;
|
|
64
|
+
|
|
65
|
+
console.log(
|
|
66
|
+
picocolors.greenBright(
|
|
67
|
+
`Validating project${resolvedValConfigFile?.project ? ` '${picocolors.inverse(resolvedValConfigFile.project)}'` : ""}...`,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const valFiles: string[] = await glob("**/*.val.{js,ts}", {
|
|
72
|
+
ignore: ["node_modules/**"],
|
|
73
|
+
cwd: projectRoot,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
console.log(picocolors.greenBright(`Found ${valFiles.length} files...`));
|
|
77
|
+
|
|
78
|
+
const fixedFiles = new Set<string>();
|
|
79
|
+
let totalErrors = 0;
|
|
80
|
+
|
|
81
|
+
// Caches each val file's parsed source so files are read/parsed at most once
|
|
82
|
+
// per pass when resolving sourcePaths to file locations and code frames.
|
|
83
|
+
const sourceFileCache: SourceFileCache = new Map();
|
|
84
|
+
|
|
85
|
+
// Diagnostics are buffered per module (keyed by relative file path) so we can
|
|
86
|
+
// render them grouped and prioritised after the run, rather than streaming
|
|
87
|
+
// them out interleaved. Transient progress (remote/fix-applied) still streams
|
|
88
|
+
// live below.
|
|
89
|
+
const reports = new Map<string, ModuleReport>();
|
|
90
|
+
const valid: { file: string; durationMs: number }[] = [];
|
|
91
|
+
const skipped: string[] = [];
|
|
92
|
+
|
|
93
|
+
// Relative file path (no leading slash), matching the code frame's
|
|
94
|
+
// relativeFile so headers, diagnostics and frames all agree.
|
|
95
|
+
const relFile = (file: string) => file.replace(/^\//, "");
|
|
96
|
+
// The module a sourcePath/file belongs to is the part before the `?p=...`.
|
|
97
|
+
const moduleOf = (sourcePathOrFile: string) =>
|
|
98
|
+
relFile(sourcePathOrFile.split("?")[0]);
|
|
99
|
+
|
|
100
|
+
const reportFor = (file: string): ModuleReport => {
|
|
101
|
+
const key = relFile(file);
|
|
102
|
+
let report = reports.get(key);
|
|
103
|
+
if (!report) {
|
|
104
|
+
report = { file: key, durationMs: 0, diagnostics: [] };
|
|
105
|
+
reports.set(key, report);
|
|
106
|
+
}
|
|
107
|
+
return report;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
for await (const event of runValidation({
|
|
111
|
+
root: projectRoot,
|
|
112
|
+
fix: !!fix,
|
|
113
|
+
valFiles,
|
|
114
|
+
project: resolvedValConfigFile?.project,
|
|
115
|
+
remote: {
|
|
116
|
+
remoteHost: process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST,
|
|
117
|
+
getSettings: (projectName, options) =>
|
|
118
|
+
getSettings(projectName, options),
|
|
119
|
+
uploadFile: (project, bucket, fileHash, fileExt, fileBuffer, options) =>
|
|
120
|
+
uploadRemoteFile(
|
|
121
|
+
process.env.VAL_CONTENT_URL || DEFAULT_CONTENT_HOST,
|
|
122
|
+
project,
|
|
123
|
+
bucket,
|
|
124
|
+
fileHash,
|
|
125
|
+
fileExt ?? "",
|
|
126
|
+
fileBuffer,
|
|
127
|
+
options,
|
|
128
|
+
),
|
|
129
|
+
},
|
|
130
|
+
fs: createDefaultValFSHost(),
|
|
131
|
+
})) {
|
|
132
|
+
switch (event.type) {
|
|
133
|
+
case "file-valid":
|
|
134
|
+
valid.push({
|
|
135
|
+
file: relFile(event.file),
|
|
136
|
+
durationMs: event.durationMs,
|
|
137
|
+
});
|
|
138
|
+
break;
|
|
139
|
+
case "file-error-count":
|
|
140
|
+
reportFor(event.file).durationMs = event.durationMs;
|
|
141
|
+
totalErrors += event.errorCount;
|
|
142
|
+
break;
|
|
143
|
+
case "validation-error":
|
|
144
|
+
reportFor(moduleOf(event.sourcePath)).diagnostics.push({
|
|
145
|
+
severity: "error",
|
|
146
|
+
sourcePath: event.sourcePath,
|
|
147
|
+
message: event.message,
|
|
148
|
+
...(event.keyError ? { keyError: true } : {}),
|
|
149
|
+
});
|
|
150
|
+
break;
|
|
151
|
+
case "validation-fixable-error":
|
|
152
|
+
reportFor(moduleOf(event.sourcePath)).diagnostics.push({
|
|
153
|
+
severity: event.fixable ? "fixable" : "error",
|
|
154
|
+
sourcePath: event.sourcePath,
|
|
155
|
+
message: event.message,
|
|
156
|
+
...(event.keyError ? { keyError: true } : {}),
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
case "unknown-fix":
|
|
160
|
+
reportFor(moduleOf(event.sourcePath)).diagnostics.push({
|
|
161
|
+
severity: "error",
|
|
162
|
+
sourcePath: event.sourcePath,
|
|
163
|
+
message: `Unknown fix: ${event.fixes.join(", ")}`,
|
|
164
|
+
...(event.keyError ? { keyError: true } : {}),
|
|
165
|
+
});
|
|
166
|
+
break;
|
|
167
|
+
case "unregistered-module":
|
|
168
|
+
skipped.push(event.file);
|
|
169
|
+
break;
|
|
170
|
+
case "fatal-error":
|
|
171
|
+
// No sourcePath for fatal errors; group by file, render message only.
|
|
172
|
+
reportFor(event.file).diagnostics.push({
|
|
173
|
+
severity: "error",
|
|
174
|
+
sourcePath: event.file,
|
|
175
|
+
message: event.message,
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
case "fix-applied":
|
|
179
|
+
console.log(
|
|
180
|
+
picocolors.yellow("⚠"),
|
|
181
|
+
"Applied fix for",
|
|
182
|
+
event.sourcePath,
|
|
183
|
+
);
|
|
184
|
+
fixedFiles.add(event.file);
|
|
185
|
+
break;
|
|
186
|
+
case "remote-uploading":
|
|
187
|
+
console.log(
|
|
188
|
+
picocolors.yellow("⚠"),
|
|
189
|
+
`Uploading remote file: '${event.ref}'...`,
|
|
190
|
+
);
|
|
191
|
+
break;
|
|
192
|
+
case "remote-uploaded":
|
|
193
|
+
console.log(
|
|
194
|
+
picocolors.green("✔"),
|
|
195
|
+
`Completed upload of remote file: '${event.ref}'`,
|
|
196
|
+
);
|
|
197
|
+
break;
|
|
198
|
+
case "remote-already-uploaded":
|
|
199
|
+
console.log(
|
|
200
|
+
picocolors.yellow("⚠"),
|
|
201
|
+
`Remote file ${event.filePath} already uploaded`,
|
|
202
|
+
);
|
|
203
|
+
break;
|
|
204
|
+
case "remote-downloading":
|
|
205
|
+
console.log(
|
|
206
|
+
picocolors.yellow("⚠"),
|
|
207
|
+
`Downloading remote file in ${event.sourcePath}...`,
|
|
208
|
+
);
|
|
209
|
+
break;
|
|
210
|
+
case "summary-errors":
|
|
211
|
+
case "summary-success":
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Renders a module's diagnostics under a single left "│" gutter bar, with the
|
|
217
|
+
// file name on top and blank gutter lines for air. Output flows least- to
|
|
218
|
+
// most-actionable top-to-bottom, so fixable diagnostics are shown last (at the
|
|
219
|
+
// bottom, nearest the prompt).
|
|
220
|
+
const renderModule = (report: ModuleReport) => {
|
|
221
|
+
const bar = picocolors.dim("│");
|
|
222
|
+
const diagnostics = [...report.diagnostics].sort((a, b) =>
|
|
223
|
+
a.severity === b.severity ? 0 : a.severity === "fixable" ? 1 : -1,
|
|
224
|
+
);
|
|
225
|
+
const fixableCount = diagnostics.filter(
|
|
226
|
+
(d) => d.severity === "fixable",
|
|
227
|
+
).length;
|
|
228
|
+
const total = diagnostics.length;
|
|
229
|
+
const hasError = fixableCount < total;
|
|
230
|
+
const symbol = hasError ? picocolors.red("✘") : picocolors.yellow("⚠");
|
|
231
|
+
let label: string;
|
|
232
|
+
if (fixableCount === total) {
|
|
233
|
+
label = `${fixableCount} fixable`;
|
|
234
|
+
} else if (fixableCount > 0) {
|
|
235
|
+
label = `${total} error${total > 1 ? "s" : ""} (${fixableCount} fixable)`;
|
|
236
|
+
} else {
|
|
237
|
+
label = `${total} error${total > 1 ? "s" : ""}`;
|
|
238
|
+
}
|
|
239
|
+
console.log(
|
|
240
|
+
`${picocolors.bold(report.file)} ${symbol} ${label} ${picocolors.dim(
|
|
241
|
+
`(${report.durationMs}ms)`,
|
|
242
|
+
)}`,
|
|
243
|
+
);
|
|
244
|
+
for (const d of diagnostics) {
|
|
245
|
+
const target = d.keyError ? "key" : "value";
|
|
246
|
+
const dsym =
|
|
247
|
+
d.severity === "fixable"
|
|
248
|
+
? picocolors.yellow("⚠")
|
|
249
|
+
: picocolors.red("✘");
|
|
250
|
+
const parts = sourcePathToLocationParts(
|
|
251
|
+
d.sourcePath,
|
|
252
|
+
projectRoot,
|
|
253
|
+
sourceFileCache,
|
|
254
|
+
target,
|
|
149
255
|
);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
256
|
+
console.log(bar);
|
|
257
|
+
if (parts) {
|
|
258
|
+
// `file:line:col` (no key/value label) so VS Code's terminal links it.
|
|
259
|
+
console.log(
|
|
260
|
+
`${bar} ${dsym} ${parts.relativeFile}:${parts.line}:${parts.character}`,
|
|
261
|
+
);
|
|
262
|
+
console.log(`${bar} ${d.message}`);
|
|
263
|
+
} else {
|
|
264
|
+
console.log(`${bar} ${dsym} ${d.message}`);
|
|
265
|
+
}
|
|
266
|
+
const frame = sourcePathToCodeFrame(
|
|
267
|
+
d.sourcePath,
|
|
268
|
+
projectRoot,
|
|
269
|
+
sourceFileCache,
|
|
270
|
+
target,
|
|
155
271
|
);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
272
|
+
if (frame !== undefined) {
|
|
273
|
+
console.log(bar);
|
|
274
|
+
for (const frameLine of frame.split("\n")) {
|
|
275
|
+
console.log(`${bar} ${frameLine}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (d.severity === "fixable") {
|
|
279
|
+
console.log(
|
|
280
|
+
`${bar} ${picocolors.dim("→ run with --fix to apply")}`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
console.log(bar);
|
|
285
|
+
console.log("");
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Run prettier on files that had fixes applied
|
|
289
|
+
if (prettier) {
|
|
290
|
+
for (const file of fixedFiles) {
|
|
291
|
+
const filePath = path.join(projectRoot, file);
|
|
292
|
+
const fileContent = await fs.readFile(filePath, "utf-8");
|
|
293
|
+
const formattedContent = await prettier.format(fileContent, {
|
|
294
|
+
filepath: filePath,
|
|
295
|
+
});
|
|
296
|
+
await fs.writeFile(filePath, formattedContent);
|
|
297
|
+
}
|
|
160
298
|
}
|
|
161
|
-
}
|
|
162
299
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
300
|
+
// Render the grouped report least- to most-actionable, top-to-bottom, so the
|
|
301
|
+
// most important things end up at the bottom nearest the prompt: valid files
|
|
302
|
+
// and skipped modules first, then error-only modules, then fixable modules.
|
|
303
|
+
const allReports = [...reports.values()];
|
|
304
|
+
const fixableModules = allReports.filter((r) =>
|
|
305
|
+
r.diagnostics.some((d) => d.severity === "fixable"),
|
|
306
|
+
);
|
|
307
|
+
const errorModules = allReports.filter(
|
|
308
|
+
(r) => !r.diagnostics.some((d) => d.severity === "fixable"),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
for (const v of valid) {
|
|
312
|
+
console.log(
|
|
313
|
+
picocolors.green("✔"),
|
|
314
|
+
picocolors.dim(`${v.file} valid (${v.durationMs}ms)`),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
for (const file of skipped) {
|
|
318
|
+
console.log(
|
|
319
|
+
picocolors.yellow("⚠"),
|
|
320
|
+
picocolors.dim(`/${file} is not registered in val.modules - skipping`),
|
|
321
|
+
);
|
|
172
322
|
}
|
|
173
|
-
}
|
|
174
323
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
324
|
+
if (allReports.length > 0) {
|
|
325
|
+
console.log("");
|
|
326
|
+
}
|
|
327
|
+
for (const report of [...errorModules, ...fixableModules]) {
|
|
328
|
+
renderModule(report);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const fixableTotal = allReports.reduce(
|
|
332
|
+
(n, r) =>
|
|
333
|
+
n + r.diagnostics.filter((d) => d.severity === "fixable").length,
|
|
334
|
+
0,
|
|
181
335
|
);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
336
|
+
if (totalErrors > 0) {
|
|
337
|
+
let summary = `${totalErrors} error${totalErrors > 1 ? "s" : ""}`;
|
|
338
|
+
if (fixableTotal > 0) {
|
|
339
|
+
summary += ` (${fixableTotal} fixable)`;
|
|
340
|
+
}
|
|
341
|
+
summary += ` across ${allReports.length} file${
|
|
342
|
+
allReports.length > 1 ? "s" : ""
|
|
343
|
+
}`;
|
|
344
|
+
if (valid.length > 0) {
|
|
345
|
+
summary += ` · ${valid.length} valid`;
|
|
346
|
+
}
|
|
347
|
+
if (skipped.length > 0) {
|
|
348
|
+
summary += ` · ${skipped.length} skipped`;
|
|
349
|
+
}
|
|
350
|
+
console.log(picocolors.red("✘"), summary);
|
|
351
|
+
} else {
|
|
352
|
+
let summary = "No validation errors found";
|
|
353
|
+
if (valid.length > 0) {
|
|
354
|
+
summary += ` · ${valid.length} valid`;
|
|
355
|
+
}
|
|
356
|
+
if (skipped.length > 0) {
|
|
357
|
+
summary += ` · ${skipped.length} skipped`;
|
|
358
|
+
}
|
|
359
|
+
console.log(picocolors.green("✔"), summary);
|
|
360
|
+
}
|
|
361
|
+
return totalErrors;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!watch) {
|
|
365
|
+
const totalErrors = await runOnce();
|
|
366
|
+
if (totalErrors > 0) {
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
185
370
|
}
|
|
371
|
+
|
|
372
|
+
await watchAndValidate(projectRoot, runOnce);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Directory names anywhere in the path that should never be watched.
|
|
376
|
+
const WATCH_IGNORED = /(^|[\\/])(node_modules|\.git|dist)([\\/]|$)/;
|
|
377
|
+
|
|
378
|
+
function isRelevantValFile(filePath: string): boolean {
|
|
379
|
+
const base = path.basename(filePath);
|
|
380
|
+
return (
|
|
381
|
+
base === "val.modules.ts" ||
|
|
382
|
+
base === "val.modules.js" ||
|
|
383
|
+
base === "val.config.ts" ||
|
|
384
|
+
base === "val.config.js" ||
|
|
385
|
+
/\.val\.(ts|js)$/.test(base)
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function watchAndValidate(
|
|
390
|
+
projectRoot: string,
|
|
391
|
+
runOnce: () => Promise<number>,
|
|
392
|
+
) {
|
|
393
|
+
// Initial pass.
|
|
394
|
+
await runOnce();
|
|
395
|
+
const watchingMessage = picocolors.dim(
|
|
396
|
+
"Watching for changes... (Ctrl+C to exit)",
|
|
397
|
+
);
|
|
398
|
+
console.log(watchingMessage);
|
|
399
|
+
|
|
400
|
+
// chokidar 5 is ESM-only; load it dynamically (like prettier above).
|
|
401
|
+
const { watch } = await import("chokidar");
|
|
402
|
+
|
|
403
|
+
let running = false;
|
|
404
|
+
let pending = false;
|
|
405
|
+
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
406
|
+
|
|
407
|
+
const triggerRun = async () => {
|
|
408
|
+
if (running) {
|
|
409
|
+
pending = true;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
running = true;
|
|
413
|
+
// Clear the screen (and scrollback) so only the latest result shows.
|
|
414
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
415
|
+
console.log(picocolors.cyanBright("Re-validating..."));
|
|
416
|
+
try {
|
|
417
|
+
await runOnce();
|
|
418
|
+
} catch (err) {
|
|
419
|
+
console.error(err);
|
|
420
|
+
}
|
|
421
|
+
console.log(watchingMessage);
|
|
422
|
+
running = false;
|
|
423
|
+
if (pending) {
|
|
424
|
+
pending = false;
|
|
425
|
+
void triggerRun();
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const watcher = watch(projectRoot, {
|
|
430
|
+
ignoreInitial: true,
|
|
431
|
+
ignored: (watchedPath: string) => WATCH_IGNORED.test(watchedPath),
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
watcher.on("all", (_event, changedPath) => {
|
|
435
|
+
if (!isRelevantValFile(changedPath)) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (debounceTimer) {
|
|
439
|
+
clearTimeout(debounceTimer);
|
|
440
|
+
}
|
|
441
|
+
debounceTimer = setTimeout(() => void triggerRun(), 150);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
process.on("SIGINT", () => {
|
|
445
|
+
void watcher.close().then(() => process.exit(0));
|
|
446
|
+
});
|
|
186
447
|
}
|