@valbuild/cli 0.92.1 → 0.94.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.
Files changed (26) hide show
  1. package/cli/dist/valbuild-cli-cli.cjs.dev.js +415 -160
  2. package/cli/dist/valbuild-cli-cli.cjs.prod.js +415 -160
  3. package/cli/dist/valbuild-cli-cli.esm.js +413 -158
  4. package/package.json +5 -5
  5. package/src/__fixtures__/basic/content/basic-errors.val.ts +7 -0
  6. package/src/__fixtures__/basic/content/basic-files.val.ts +14 -0
  7. package/src/__fixtures__/basic/content/basic-gallery-2.val.ts +17 -0
  8. package/src/__fixtures__/basic/content/basic-gallery-fail-on-non-unique-dir.val.ts +17 -0
  9. package/src/__fixtures__/basic/content/basic-gallery-missing-tracked.val.ts +17 -0
  10. package/src/__fixtures__/basic/content/basic-gallery-wrong-metadata.val.ts +17 -0
  11. package/src/__fixtures__/basic/content/basic-gallery.val.ts +18 -0
  12. package/src/__fixtures__/basic/content/basic-image-from-galleries.val.ts +15 -0
  13. package/src/__fixtures__/basic/content/basic-image-from-gallery.val.ts +12 -0
  14. package/src/__fixtures__/basic/content/basic-image.val.ts +7 -0
  15. package/src/__fixtures__/basic/content/basic-valid.val.ts +7 -0
  16. package/src/__fixtures__/basic/public/val/files/tracked.txt +1 -0
  17. package/src/__fixtures__/basic/public/val/files/untracked.txt +1 -0
  18. package/src/__fixtures__/basic/public/val/image.png +0 -0
  19. package/src/__fixtures__/basic/public/val/images/image.png +0 -0
  20. package/src/__fixtures__/basic/public/val/images2/image.png +0 -0
  21. package/src/__fixtures__/basic/public/val/images3/image.png +0 -0
  22. package/src/__fixtures__/basic/tsconfig.json +12 -0
  23. package/src/__fixtures__/basic/val.config.ts +5 -0
  24. package/src/runValidation.test.ts +386 -0
  25. package/src/runValidation.ts +1096 -0
  26. package/src/validate.ts +131 -887
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@valbuild/cli",
3
3
  "private": false,
4
- "version": "0.92.1",
4
+ "version": "0.94.0",
5
5
  "description": "Val CLI tools",
6
6
  "repository": {
7
7
  "type": "git",
@@ -25,10 +25,10 @@
25
25
  "open": "^9.1.0",
26
26
  "picocolors": "^1.1.1",
27
27
  "zod": "^4.3.5",
28
- "@valbuild/core": "0.92.0",
29
- "@valbuild/eslint-plugin": "0.92.0",
30
- "@valbuild/server": "0.92.1",
31
- "@valbuild/shared": "0.92.0"
28
+ "@valbuild/core": "0.94.0",
29
+ "@valbuild/eslint-plugin": "0.93.0",
30
+ "@valbuild/server": "0.94.0",
31
+ "@valbuild/shared": "0.94.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "prettier": "*",
@@ -0,0 +1,7 @@
1
+ import { c, s } from "../val.config";
2
+
3
+ export default c.define(
4
+ "/content/basic-errors.val.ts",
5
+ s.string().minLength(30),
6
+ "Hello World",
7
+ );
@@ -0,0 +1,14 @@
1
+ import { c, s } from "../val.config";
2
+
3
+ export default c.define(
4
+ "/content/basic-files.val.ts",
5
+ s.files({
6
+ directory: "/public/val/files",
7
+ accept: "*/*",
8
+ }),
9
+ {
10
+ "/public/val/files/tracked.txt": {
11
+ mimeType: "text/plain",
12
+ },
13
+ },
14
+ );
@@ -0,0 +1,17 @@
1
+ import { c, s } from "../val.config";
2
+
3
+ export default c.define(
4
+ "/content/basic-gallery-2.val.ts",
5
+ s.images({
6
+ directory: "/public/val/images2",
7
+ accept: "image/*",
8
+ }),
9
+ {
10
+ "/public/val/images2/image.png": {
11
+ width: 1,
12
+ height: 1,
13
+ mimeType: "image/png",
14
+ alt: null,
15
+ },
16
+ },
17
+ );
@@ -0,0 +1,17 @@
1
+ import { c, s } from "../val.config";
2
+
3
+ export default c.define(
4
+ "/content/basic-gallery-fail-on-non-unique-dir.val.ts",
5
+ s.images({
6
+ directory: "/public/val/images",
7
+ accept: "image/*",
8
+ }),
9
+ {
10
+ "/public/val/images/image.png": {
11
+ width: 1,
12
+ height: 1,
13
+ mimeType: "image/png",
14
+ alt: null,
15
+ },
16
+ },
17
+ );
@@ -0,0 +1,17 @@
1
+ import { c, s } from "../val.config";
2
+
3
+ export default c.define(
4
+ "/content/basic-gallery-missing-tracked.val.ts",
5
+ s.images({
6
+ directory: "/public/val/images4",
7
+ accept: "image/*",
8
+ }),
9
+ {
10
+ "/public/val/images4/missing.png": {
11
+ width: 1,
12
+ height: 1,
13
+ mimeType: "image/png",
14
+ alt: null,
15
+ },
16
+ },
17
+ );
@@ -0,0 +1,17 @@
1
+ import { c, s } from "../val.config";
2
+
3
+ export default c.define(
4
+ "/content/basic-gallery-wrong-metadata.val.ts",
5
+ s.images({
6
+ directory: "/public/val/images3",
7
+ accept: "image/*",
8
+ }),
9
+ {
10
+ "/public/val/images3/image.png": {
11
+ width: 999,
12
+ height: 999,
13
+ mimeType: "image/png",
14
+ alt: null,
15
+ },
16
+ },
17
+ );
@@ -0,0 +1,18 @@
1
+ import { c, s } from "../val.config";
2
+
3
+ export default c.define(
4
+ "/content/basic-gallery.val.ts",
5
+ s.images({
6
+ directory: "/public/val/images",
7
+ accept: "image/*",
8
+ }),
9
+
10
+ {
11
+ "/public/val/images/image.png": {
12
+ width: 1,
13
+ height: 1,
14
+ mimeType: "image/png",
15
+ alt: null,
16
+ },
17
+ },
18
+ );
@@ -0,0 +1,15 @@
1
+ import { c, s } from "../val.config";
2
+ import basicGalleryVal from "./basic-gallery.val";
3
+ import basicGallery2Val from "./basic-gallery-2.val";
4
+
5
+ export default c.define(
6
+ "/content/basic-image-from-galleries.val.ts",
7
+ s.object({
8
+ image1: s.image(basicGalleryVal),
9
+ image2: s.image(basicGallery2Val),
10
+ }),
11
+ {
12
+ image1: c.image("/public/val/images/image.png"),
13
+ image2: c.image("/public/val/images2/image.png"),
14
+ },
15
+ );
@@ -0,0 +1,12 @@
1
+ import { c, s } from "../val.config";
2
+ import basicGalleryVal from "./basic-gallery.val";
3
+
4
+ export default c.define(
5
+ "/content/basic-image-from-gallery.val.ts",
6
+ s.object({
7
+ image: s.image(basicGalleryVal),
8
+ }),
9
+ {
10
+ image: c.image("/public/val/images/image.png"),
11
+ },
12
+ );
@@ -0,0 +1,7 @@
1
+ import { c, s } from "../val.config";
2
+
3
+ export default c.define(
4
+ "/content/basic-image.val.ts",
5
+ s.image(),
6
+ c.image("/public/val/image.png"),
7
+ );
@@ -0,0 +1,7 @@
1
+ import { c, s } from "../val.config";
2
+
3
+ export default c.define(
4
+ "/content/basic-valid.val.ts",
5
+ s.string(),
6
+ "Hello World",
7
+ );
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "allowJs": true,
5
+ "skipLibCheck": true,
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "module": "esnext",
9
+ "moduleResolution": "node"
10
+ },
11
+ "exclude": ["node_modules"]
12
+ }
@@ -0,0 +1,5 @@
1
+ import { initVal } from "@valbuild/core";
2
+
3
+ const { s, c } = initVal();
4
+
5
+ export { s, c };
@@ -0,0 +1,386 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "@jest/globals";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import {
5
+ DEFAULT_VAL_REMOTE_HOST,
6
+ type ModuleFilePath,
7
+ type ModulePath,
8
+ } from "@valbuild/core";
9
+ import { createService } from "@valbuild/server";
10
+ import {
11
+ createDefaultValFSHost,
12
+ runValidation,
13
+ ValidationEvent,
14
+ IValRemote,
15
+ } from "./runValidation";
16
+
17
+ const BASIC_FIXTURE = path.resolve(__dirname, "__fixtures__/basic");
18
+
19
+ const mockRemote: IValRemote = {
20
+ remoteHost: DEFAULT_VAL_REMOTE_HOST,
21
+ getSettings: async () => {
22
+ throw new Error("Not expected to be called");
23
+ },
24
+ uploadFile: async () => {
25
+ throw new Error("Not expected to be called");
26
+ },
27
+ };
28
+
29
+ describe("runValidation", () => {
30
+ let tmpDir: string;
31
+
32
+ beforeEach(() => {
33
+ const tmpBase = path.join(__dirname, ".tmp");
34
+ fs.mkdirSync(tmpBase, { recursive: true });
35
+ tmpDir = fs.mkdtempSync(path.join(tmpBase, "runValidation-"));
36
+ fs.cpSync(BASIC_FIXTURE, tmpDir, { recursive: true });
37
+ });
38
+
39
+ afterEach(() => {
40
+ fs.rmSync(tmpDir, { recursive: true, force: true });
41
+ });
42
+
43
+ test("returns summary-success for a valid module", async () => {
44
+ const events: ValidationEvent[] = [];
45
+
46
+ for await (const event of runValidation({
47
+ root: tmpDir,
48
+ fix: false,
49
+ valFiles: ["content/basic-valid.val.ts"],
50
+ project: undefined,
51
+ remote: mockRemote,
52
+ fs: createDefaultValFSHost(),
53
+ })) {
54
+ events.push(event);
55
+ }
56
+
57
+ expect(events.at(-1)).toEqual({ type: "summary-success" });
58
+ expect(events.filter((e) => e.type === "validation-error")).toHaveLength(0);
59
+ });
60
+
61
+ test("returns validation-error for a module with minLength violation", async () => {
62
+ const events: ValidationEvent[] = [];
63
+
64
+ for await (const event of runValidation({
65
+ root: tmpDir,
66
+ fix: false,
67
+ valFiles: ["content/basic-errors.val.ts"],
68
+ project: undefined,
69
+ remote: mockRemote,
70
+ fs: createDefaultValFSHost(),
71
+ })) {
72
+ events.push(event);
73
+ }
74
+
75
+ expect(events.at(-1)).toEqual({ type: "summary-errors", count: 1 });
76
+ expect(events.filter((e) => e.type === "validation-error")).toHaveLength(1);
77
+ });
78
+
79
+ test("applies metadata fix for image without metadata", async () => {
80
+ const events: ValidationEvent[] = [];
81
+
82
+ for await (const event of runValidation({
83
+ root: tmpDir,
84
+ fix: true,
85
+ valFiles: ["content/basic-image.val.ts"],
86
+ project: undefined,
87
+ remote: mockRemote,
88
+ fs: createDefaultValFSHost(),
89
+ })) {
90
+ events.push(event);
91
+ }
92
+
93
+ expect(events.at(-1)).toEqual({ type: "summary-success" });
94
+ expect(events.filter((e) => e.type === "validation-error")).toHaveLength(0);
95
+ expect(events.filter((e) => e.type === "fix-applied")).toHaveLength(1);
96
+ expect(events.find((e) => e.type === "fix-applied")).toMatchObject({
97
+ type: "fix-applied",
98
+ sourcePath: "/content/basic-image.val.ts",
99
+ });
100
+ });
101
+
102
+ test("reports fixable error for image without metadata when fix is false", async () => {
103
+ const events: ValidationEvent[] = [];
104
+
105
+ for await (const event of runValidation({
106
+ root: tmpDir,
107
+ fix: false,
108
+ valFiles: ["content/basic-image.val.ts"],
109
+ project: undefined,
110
+ remote: mockRemote,
111
+ fs: createDefaultValFSHost(),
112
+ })) {
113
+ events.push(event);
114
+ }
115
+
116
+ expect(events.at(-1)).toEqual({ type: "summary-errors", count: 1 });
117
+ const fixableErrors = events.filter(
118
+ (e) => e.type === "validation-fixable-error",
119
+ );
120
+ expect(fixableErrors).toHaveLength(1);
121
+ expect(fixableErrors[0]).toMatchObject({
122
+ type: "validation-fixable-error",
123
+ sourcePath: "/content/basic-image.val.ts",
124
+ fixable: true,
125
+ });
126
+ });
127
+
128
+ test("handles module with both s.image and s.images", async () => {
129
+ const events: ValidationEvent[] = [];
130
+
131
+ for await (const event of runValidation({
132
+ root: tmpDir,
133
+ fix: false,
134
+ valFiles: ["content/basic-image-from-gallery.val.ts"],
135
+ project: undefined,
136
+ remote: mockRemote,
137
+ fs: createDefaultValFSHost(),
138
+ })) {
139
+ events.push(event);
140
+ }
141
+ const lastEvent = events.at(-1);
142
+ expect(["summary-success", "summary-errors"]).toContain(lastEvent?.type);
143
+ });
144
+
145
+ test("handles module with two gallery val files", async () => {
146
+ const events: ValidationEvent[] = [];
147
+
148
+ for await (const event of runValidation({
149
+ root: tmpDir,
150
+ fix: false,
151
+ valFiles: [
152
+ "content/basic-image-from-galleries.val.ts",
153
+ "content/basic-gallery.val.ts",
154
+ "content/basic-gallery-2.val.ts",
155
+ ],
156
+ project: undefined,
157
+ remote: mockRemote,
158
+ fs: createDefaultValFSHost(),
159
+ })) {
160
+ events.push(event);
161
+ }
162
+
163
+ const lastEvent = events.at(-1);
164
+ expect(["summary-success", "summary-errors"]).toContain(lastEvent?.type);
165
+ });
166
+
167
+ test("basic-gallery-fail-on-non-unique-dir returns error for duplicate directory", async () => {
168
+ const events: ValidationEvent[] = [];
169
+
170
+ for await (const event of runValidation({
171
+ root: tmpDir,
172
+ fix: false,
173
+ valFiles: [
174
+ "content/basic-gallery.val.ts",
175
+ "content/basic-gallery-fail-on-non-unique-dir.val.ts",
176
+ ],
177
+ project: undefined,
178
+ remote: mockRemote,
179
+ fs: createDefaultValFSHost(),
180
+ })) {
181
+ events.push(event);
182
+ }
183
+
184
+ expect(events.at(-1)).toEqual({
185
+ type: "summary-errors",
186
+ count: expect.any(Number),
187
+ });
188
+ const errors = events.filter((e) => e.type === "validation-error");
189
+ expect(errors.length).toBeGreaterThan(0);
190
+ expect(
191
+ errors.some(
192
+ (e) =>
193
+ "message" in e &&
194
+ (e.message as string).includes("/public/val/images"),
195
+ ),
196
+ ).toBe(true);
197
+ });
198
+
199
+ test("returns validation-error for s.files gallery with untracked file in directory", async () => {
200
+ const events: ValidationEvent[] = [];
201
+
202
+ for await (const event of runValidation({
203
+ root: tmpDir,
204
+ fix: false,
205
+ valFiles: ["content/basic-files.val.ts"],
206
+ project: undefined,
207
+ remote: mockRemote,
208
+ fs: createDefaultValFSHost(),
209
+ })) {
210
+ events.push(event);
211
+ }
212
+
213
+ expect(events.at(-1)).toEqual({
214
+ type: "summary-errors",
215
+ count: expect.any(Number),
216
+ });
217
+ const errors = events.filter((e) => e.type === "validation-error");
218
+ expect(errors.length).toBeGreaterThan(0);
219
+ expect(
220
+ errors.some(
221
+ (e) =>
222
+ "message" in e && (e.message as string).includes("untracked.txt"),
223
+ ),
224
+ ).toBe(true);
225
+ });
226
+
227
+ test("returns validation-error for gallery with tracked file missing from disk", async () => {
228
+ const events: ValidationEvent[] = [];
229
+
230
+ for await (const event of runValidation({
231
+ root: tmpDir,
232
+ fix: false,
233
+ valFiles: ["content/basic-gallery-missing-tracked.val.ts"],
234
+ project: undefined,
235
+ remote: mockRemote,
236
+ fs: createDefaultValFSHost(),
237
+ })) {
238
+ events.push(event);
239
+ }
240
+
241
+ expect(events.at(-1)).toEqual({
242
+ type: "summary-errors",
243
+ count: expect.any(Number),
244
+ });
245
+ const errors = events.filter((e) => e.type === "validation-error");
246
+ expect(errors.length).toBeGreaterThan(0);
247
+ expect(
248
+ errors.some(
249
+ (e) => "message" in e && (e.message as string).includes("missing.png"),
250
+ ),
251
+ ).toBe(true);
252
+ });
253
+
254
+ test("removes missing tracked file entry from gallery when fix is true", async () => {
255
+ const gen = runValidation({
256
+ root: tmpDir,
257
+ fix: true,
258
+ valFiles: ["content/basic-gallery-missing-tracked.val.ts"],
259
+ project: undefined,
260
+ remote: mockRemote,
261
+ fs: createDefaultValFSHost(),
262
+ });
263
+ let next = await gen.next();
264
+ while (!next.done) {
265
+ next = await gen.next();
266
+ }
267
+
268
+ const service = await createService(tmpDir, {}, createDefaultValFSHost());
269
+ try {
270
+ const result = await service.get(
271
+ "/content/basic-gallery-missing-tracked.val.ts" as ModuleFilePath,
272
+ "" as ModulePath,
273
+ { source: true, schema: true, validate: true },
274
+ );
275
+ expect(result.source).not.toHaveProperty(
276
+ "/public/val/images4/missing.png",
277
+ );
278
+ } finally {
279
+ service.dispose();
280
+ }
281
+ });
282
+
283
+ test("returns validation-fixable-error for gallery with wrong stored metadata", async () => {
284
+ const events: ValidationEvent[] = [];
285
+
286
+ for await (const event of runValidation({
287
+ root: tmpDir,
288
+ fix: false,
289
+ valFiles: ["content/basic-gallery-wrong-metadata.val.ts"],
290
+ project: undefined,
291
+ remote: mockRemote,
292
+ fs: createDefaultValFSHost(),
293
+ })) {
294
+ events.push(event);
295
+ }
296
+
297
+ expect(events.at(-1)).toEqual({
298
+ type: "summary-errors",
299
+ count: expect.any(Number),
300
+ });
301
+ const fixableErrors = events.filter(
302
+ (e) => e.type === "validation-fixable-error",
303
+ );
304
+ expect(fixableErrors.length).toBeGreaterThan(0);
305
+ expect(fixableErrors[0]).toMatchObject({
306
+ type: "validation-fixable-error",
307
+ fixable: true,
308
+ });
309
+ });
310
+
311
+ test("fixes wrong metadata for gallery entry when fix is true", async () => {
312
+ const gen = runValidation({
313
+ root: tmpDir,
314
+ fix: true,
315
+ valFiles: ["content/basic-gallery-wrong-metadata.val.ts"],
316
+ project: undefined,
317
+ remote: mockRemote,
318
+ fs: createDefaultValFSHost(),
319
+ });
320
+ let next = await gen.next();
321
+ while (!next.done) {
322
+ next = await gen.next();
323
+ }
324
+
325
+ const service = await createService(tmpDir, {}, createDefaultValFSHost());
326
+ try {
327
+ const result = await service.get(
328
+ "/content/basic-gallery-wrong-metadata.val.ts" as ModuleFilePath,
329
+ "" as ModulePath,
330
+ { source: true, schema: true, validate: true },
331
+ );
332
+ expect(result.source).toMatchObject({
333
+ "/public/val/images3/image.png": {
334
+ width: 1,
335
+ height: 1,
336
+ mimeType: "image/png",
337
+ },
338
+ });
339
+ } finally {
340
+ service.dispose();
341
+ }
342
+ });
343
+
344
+ test("image has metadata after applying fix", async () => {
345
+ const gen = runValidation({
346
+ root: tmpDir,
347
+ fix: true,
348
+ valFiles: ["content/basic-image.val.ts"],
349
+ project: undefined,
350
+ remote: mockRemote,
351
+ fs: createDefaultValFSHost(),
352
+ });
353
+ // consume all events to apply fixes
354
+ let next = await gen.next();
355
+ while (!next.done) {
356
+ next = await gen.next();
357
+ }
358
+
359
+ const service = await createService(tmpDir, {}, createDefaultValFSHost());
360
+ try {
361
+ const result = await service.get(
362
+ "/content/basic-image.val.ts" as ModuleFilePath,
363
+ "" as ModulePath,
364
+ { source: true, schema: true, validate: true },
365
+ );
366
+ // The schema always emits image:check-metadata when metadata exists
367
+ // (actual metadata verification happens in the fix handler).
368
+ // Verify no image:add-metadata errors remain (fix was applied):
369
+ if (result.errors && result.errors.validation) {
370
+ const allFixes = Object.values(result.errors.validation)
371
+ .flat()
372
+ .flatMap((e) => e.fixes ?? []);
373
+ expect(allFixes).not.toContain("image:add-metadata");
374
+ }
375
+ expect(result.source).toMatchObject({
376
+ metadata: {
377
+ width: 1,
378
+ height: 1,
379
+ mimeType: "image/png",
380
+ },
381
+ });
382
+ } finally {
383
+ service.dispose();
384
+ }
385
+ });
386
+ });