@valbuild/cli 0.146.4 → 0.148.0
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 +38 -0
- package/cli/dist/valbuild-cli-cli.cjs.dev.js +295 -230
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +295 -230
- package/cli/dist/valbuild-cli-cli.esm.js +297 -232
- package/package.json +7 -7
- package/src/cli.ts +6 -0
- package/src/publish.test.ts +84 -0
- package/src/publish.ts +37 -0
- package/src/runValidation.imageset.test.ts +23 -0
- package/src/runValidation.ts +15 -1
- package/src/validate.ts +325 -303
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valbuild/cli",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.148.0",
|
|
5
5
|
"description": "Val CLI tools",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
"picocolors": "^1.1.1",
|
|
27
27
|
"zod": "^4.4.3",
|
|
28
28
|
"zod-validation-error": "^5.0.0",
|
|
29
|
-
"@valbuild/core": "0.146.2",
|
|
30
29
|
"@valbuild/eslint-plugin": "0.125.0",
|
|
31
|
-
"@valbuild/language-server": "0.
|
|
32
|
-
"@valbuild/
|
|
33
|
-
"@valbuild/
|
|
30
|
+
"@valbuild/language-server": "0.148.0",
|
|
31
|
+
"@valbuild/core": "0.148.0",
|
|
32
|
+
"@valbuild/server": "0.148.0",
|
|
33
|
+
"@valbuild/shared": "0.148.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"prettier": "*",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"rolldown": ">=1.2.5",
|
|
39
39
|
"@tanstack/router-generator": ">=1.167.0",
|
|
40
40
|
"@tanstack/router-plugin": ">=1.168.0",
|
|
41
|
-
"@valbuild/tanstack-build": "^0.
|
|
41
|
+
"@valbuild/tanstack-build": "^0.148.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"@valbuild/tanstack-build": {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"prettier": "~3.8.5",
|
|
71
71
|
"rolldown": "^1.2.5",
|
|
72
72
|
"tsx": "^4.23.12",
|
|
73
|
-
"@valbuild/tanstack-build": "0.
|
|
73
|
+
"@valbuild/tanstack-build": "0.148.0"
|
|
74
74
|
},
|
|
75
75
|
"scripts": {
|
|
76
76
|
"start": "tsx src/cli.ts --",
|
package/src/cli.ts
CHANGED
|
@@ -93,6 +93,8 @@ async function main(): Promise<void> {
|
|
|
93
93
|
--links-own-css The app links its own CSS (--no-links-own-css for the
|
|
94
94
|
opposite; omit it when the build did not say)
|
|
95
95
|
--dry-run Verify, and stop before the site changes
|
|
96
|
+
--skip-validation Publish without running "val validate" first. It runs by
|
|
97
|
+
default, and an error stops the publish
|
|
96
98
|
|
|
97
99
|
Command: list-unused-files
|
|
98
100
|
Description: EXPERIMENTAL.
|
|
@@ -185,6 +187,9 @@ async function main(): Promise<void> {
|
|
|
185
187
|
dryRun: {
|
|
186
188
|
type: "boolean",
|
|
187
189
|
},
|
|
190
|
+
skipValidation: {
|
|
191
|
+
type: "boolean",
|
|
192
|
+
},
|
|
188
193
|
yes: {
|
|
189
194
|
type: "boolean",
|
|
190
195
|
},
|
|
@@ -263,6 +268,7 @@ async function main(): Promise<void> {
|
|
|
263
268
|
buildHash: flags.buildHash,
|
|
264
269
|
linksOwnCss: flags.linksOwnCss,
|
|
265
270
|
dryRun: flags.dryRun,
|
|
271
|
+
skipValidation: flags.skipValidation,
|
|
266
272
|
});
|
|
267
273
|
case "login":
|
|
268
274
|
return login({
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { error } from "./logger";
|
|
2
|
+
import { publish } from "./publish";
|
|
3
|
+
import { runPublish } from "./publish/runPublish";
|
|
4
|
+
import { validateOnce } from "./validate";
|
|
5
|
+
|
|
6
|
+
jest.mock("./validate");
|
|
7
|
+
// chalk is ESM-only, which jest cannot load as CommonJS.
|
|
8
|
+
jest.mock("./logger", () => ({ error: jest.fn() }));
|
|
9
|
+
jest.mock("./publish/runPublish", () => ({
|
|
10
|
+
...jest.requireActual("./publish/runPublish"),
|
|
11
|
+
runPublish: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const mockValidateOnce = jest.mocked(validateOnce);
|
|
15
|
+
const mockRunPublish = jest.mocked(runPublish);
|
|
16
|
+
const mockError = jest.mocked(error);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `val publish` validates first: content that does not validate does not go
|
|
20
|
+
* live. It is what lets the Studio trust a connected project's published
|
|
21
|
+
* content without checking every file itself (docs/plans/studio-validate.md).
|
|
22
|
+
*/
|
|
23
|
+
describe("val publish validates before it publishes", () => {
|
|
24
|
+
let log: jest.SpyInstance;
|
|
25
|
+
let errorLog: jest.SpyInstance;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.resetAllMocks();
|
|
29
|
+
process.exitCode = undefined;
|
|
30
|
+
log = jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
31
|
+
errorLog = jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
32
|
+
mockRunPublish.mockResolvedValue({
|
|
33
|
+
status: "verified",
|
|
34
|
+
publishId: "p1",
|
|
35
|
+
artifacts: 1,
|
|
36
|
+
uploaded: 0,
|
|
37
|
+
uploadedBytes: 0,
|
|
38
|
+
previewUrl: null,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
log.mockRestore();
|
|
44
|
+
errorLog.mockRestore();
|
|
45
|
+
process.exitCode = undefined;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a validation error stops the publish before anything is built", async () => {
|
|
49
|
+
mockValidateOnce.mockResolvedValue(2);
|
|
50
|
+
await publish({ root: "/project", dryRun: true });
|
|
51
|
+
expect(mockValidateOnce).toHaveBeenCalledWith({
|
|
52
|
+
projectRoot: "/project",
|
|
53
|
+
fix: false,
|
|
54
|
+
// Refused on what the Studio's Publish refuses on, and no more.
|
|
55
|
+
blockingOnly: true,
|
|
56
|
+
});
|
|
57
|
+
expect(mockRunPublish).not.toHaveBeenCalled();
|
|
58
|
+
expect(mockError).toHaveBeenCalledWith(
|
|
59
|
+
"Not published: 2 validation errors.",
|
|
60
|
+
);
|
|
61
|
+
expect(process.exitCode).toBe(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("it never fixes: CI must not rewrite what it was asked to publish", async () => {
|
|
65
|
+
mockValidateOnce.mockResolvedValue(0);
|
|
66
|
+
await publish({ root: "/project" });
|
|
67
|
+
expect(mockValidateOnce).toHaveBeenCalledWith(
|
|
68
|
+
expect.objectContaining({ fix: false }),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("content that validates is published", async () => {
|
|
73
|
+
mockValidateOnce.mockResolvedValue(0);
|
|
74
|
+
await publish({ root: "/project", dryRun: true });
|
|
75
|
+
expect(mockRunPublish).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(process.exitCode).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("--skip-validation publishes without validating", async () => {
|
|
80
|
+
await publish({ root: "/project", dryRun: true, skipValidation: true });
|
|
81
|
+
expect(mockValidateOnce).not.toHaveBeenCalled();
|
|
82
|
+
expect(mockRunPublish).toHaveBeenCalledTimes(1);
|
|
83
|
+
});
|
|
84
|
+
});
|
package/src/publish.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import path from "path";
|
|
1
2
|
import pc from "picocolors";
|
|
2
3
|
import { error } from "./logger";
|
|
3
4
|
import { PublishProblem } from "@valbuild/shared/internal";
|
|
4
5
|
import { formatBytes, runPublish } from "./publish/runPublish";
|
|
6
|
+
import { validateOnce } from "./validate";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* `val publish` - a built project in, a live site out.
|
|
@@ -19,7 +21,42 @@ export async function publish(options: {
|
|
|
19
21
|
buildHash?: string;
|
|
20
22
|
linksOwnCss?: boolean;
|
|
21
23
|
dryRun?: boolean;
|
|
24
|
+
skipValidation?: boolean;
|
|
22
25
|
}): Promise<void> {
|
|
26
|
+
// Content that does not validate is not published. This is the check that
|
|
27
|
+
// lets the Studio trust a connected project's published content without
|
|
28
|
+
// re-checking every file itself: whatever reached the site went through
|
|
29
|
+
// here. Without `--fix`, so CI never rewrites what it was asked to publish.
|
|
30
|
+
//
|
|
31
|
+
// Refused on what the Studio's Publish refuses on, and no more: an error
|
|
32
|
+
// the Studio leaves to the server to repair (a local image in a remote
|
|
33
|
+
// project, metadata to fill in) is printed and published, as it would be
|
|
34
|
+
// from the Studio. Every site made on /new starts with its template's
|
|
35
|
+
// images local, so counting those would refuse its first publish.
|
|
36
|
+
if (!options.skipValidation) {
|
|
37
|
+
const projectRoot = options.root
|
|
38
|
+
? path.resolve(options.root)
|
|
39
|
+
: process.cwd();
|
|
40
|
+
const errorCount = await validateOnce({
|
|
41
|
+
projectRoot,
|
|
42
|
+
fix: false,
|
|
43
|
+
blockingOnly: true,
|
|
44
|
+
});
|
|
45
|
+
if (errorCount > 0) {
|
|
46
|
+
error(
|
|
47
|
+
`Not published: ${errorCount} validation error${errorCount === 1 ? "" : "s"}.`,
|
|
48
|
+
);
|
|
49
|
+
console.error(
|
|
50
|
+
pc.dim(
|
|
51
|
+
` Run "val validate --fix" in the project, commit what it changes and push, ` +
|
|
52
|
+
`and fix by hand what it cannot. --skip-validation publishes anyway.`,
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
23
60
|
const result = await runPublish({
|
|
24
61
|
...(options.root ? { root: options.root } : {}),
|
|
25
62
|
...(options.artifacts ? { artifacts: options.artifacts } : {}),
|
|
@@ -213,6 +213,29 @@ describe("val validate with remote image and file collections", () => {
|
|
|
213
213
|
fs.rmSync(root, { recursive: true, force: true });
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
+
// What `val publish` refuses on (`validateOnce({ blockingOnly })`): a
|
|
217
|
+
// local file still to upload is the Studio's to repair, and is published;
|
|
218
|
+
// a gallery keyed by a ref it does not know is a person's to fix.
|
|
219
|
+
test("an error the Studio repairs on save is marked as such, and a person's is not", async () => {
|
|
220
|
+
const events = errorsOf(
|
|
221
|
+
await run(
|
|
222
|
+
root,
|
|
223
|
+
["content/images.val.ts", "content/local-images.val.ts"],
|
|
224
|
+
false,
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
const repaired = (sourcePath: string) =>
|
|
228
|
+
events
|
|
229
|
+
.filter((e) => e.sourcePath === sourcePath)
|
|
230
|
+
.map((e) => e.repairedOnSave === true);
|
|
231
|
+
expect(repaired(`/content/images.val.ts?p="${LOGO}"`)).toEqual([true]);
|
|
232
|
+
const local = events.filter((e) =>
|
|
233
|
+
e.sourcePath.startsWith("/content/local-images.val.ts"),
|
|
234
|
+
);
|
|
235
|
+
expect(local.length).toBeGreaterThan(0);
|
|
236
|
+
expect(local.every((e) => e.repairedOnSave !== true)).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
216
239
|
test("--fix uploads the entries, renames their keys, and points the page's field and rich text image at them", async () => {
|
|
217
240
|
fs.mkdirSync(path.join(root, ".val"), { recursive: true });
|
|
218
241
|
fs.writeFileSync(
|
package/src/runValidation.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import {
|
|
3
|
+
askOncePerHost,
|
|
3
4
|
createDefaultValFSHost,
|
|
4
5
|
createFixPatch,
|
|
5
6
|
createService,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
SourcePath,
|
|
20
21
|
} from "@valbuild/core";
|
|
21
22
|
import {
|
|
23
|
+
isRepairedOnSave,
|
|
22
24
|
resolveSchemaSourceFixes,
|
|
23
25
|
type SchemaSourceSnapshot,
|
|
24
26
|
} from "@valbuild/shared/internal";
|
|
@@ -58,6 +60,10 @@ export async function* runValidation({
|
|
|
58
60
|
fs: IValFSHost;
|
|
59
61
|
}): AsyncGenerator<ValidationEvent> {
|
|
60
62
|
const projectRoot = path.resolve(root);
|
|
63
|
+
// One question per platform host for the whole run: each error is fixed by
|
|
64
|
+
// its own createFixPatch call, and a host that does not answer would
|
|
65
|
+
// otherwise cost a timeout per ref.
|
|
66
|
+
const askHostProject = askOncePerHost();
|
|
61
67
|
|
|
62
68
|
const service = await createService(projectRoot, fs);
|
|
63
69
|
|
|
@@ -260,6 +266,7 @@ export async function* runValidation({
|
|
|
260
266
|
sourcePath,
|
|
261
267
|
message: result.errorMessage ?? "Unknown error",
|
|
262
268
|
...(v.keyError ? { keyError: true } : {}),
|
|
269
|
+
...(isRepairedOnSave(v) ? { repairedOnSave: true } : {}),
|
|
263
270
|
};
|
|
264
271
|
fileErrors += 1;
|
|
265
272
|
continue;
|
|
@@ -277,13 +284,18 @@ export async function* runValidation({
|
|
|
277
284
|
message: result.fixableErrorMessage,
|
|
278
285
|
fixable: true,
|
|
279
286
|
...(v.keyError ? { keyError: true } : {}),
|
|
287
|
+
...(isRepairedOnSave(v) ? { repairedOnSave: true } : {}),
|
|
280
288
|
};
|
|
281
289
|
}
|
|
282
290
|
|
|
283
291
|
// Apply patch if needed
|
|
284
292
|
if (result.shouldApplyPatch) {
|
|
285
293
|
const fixPatch = await createFixPatch(
|
|
286
|
-
{
|
|
294
|
+
{
|
|
295
|
+
projectRoot,
|
|
296
|
+
remoteHost: remote.remoteHost,
|
|
297
|
+
askHostProject,
|
|
298
|
+
},
|
|
287
299
|
!!fix,
|
|
288
300
|
sourcePath as SourcePath,
|
|
289
301
|
v,
|
|
@@ -319,6 +331,7 @@ export async function* runValidation({
|
|
|
319
331
|
message: v.message,
|
|
320
332
|
fixable: true,
|
|
321
333
|
...(v.keyError ? { keyError: true } : {}),
|
|
334
|
+
...(isRepairedOnSave(v) ? { repairedOnSave: true } : {}),
|
|
322
335
|
};
|
|
323
336
|
}
|
|
324
337
|
|
|
@@ -332,6 +345,7 @@ export async function* runValidation({
|
|
|
332
345
|
message: e.message,
|
|
333
346
|
fixable: !!(e.fixes && e.fixes.length),
|
|
334
347
|
...(e.keyError ? { keyError: true } : {}),
|
|
348
|
+
...(isRepairedOnSave(e) ? { repairedOnSave: true } : {}),
|
|
335
349
|
};
|
|
336
350
|
}
|
|
337
351
|
}
|