@valbuild/cli 0.140.2 → 0.141.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 +71 -0
- package/cli/dist/valbuild-cli-cli.cjs.dev.js +157 -9
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +157 -9
- package/cli/dist/valbuild-cli-cli.esm.js +159 -11
- package/package.json +5 -5
- package/src/cli.ts +1 -1
- package/src/debug/context.ts +1 -1
- package/src/debug/moduleClosure.ts +3 -1
- package/src/listUnusedFiles.ts +107 -9
- package/src/publish/__fixtures__/buildPublishProbe.ts +1 -1
- package/src/publish/runPublish.test.ts +73 -0
- package/src/publish/runPublish.ts +53 -2
- package/src/runValidation.ts +12 -0
- package/src/runValidation.videoset.test.ts +302 -0
|
@@ -0,0 +1,302 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* `val validate` over a project with an `s.videoset()`, end to end: module
|
|
19
|
+
* loading, the fix handlers, the patches and the `.val.ts` rewrite.
|
|
20
|
+
*
|
|
21
|
+
* The project is written here rather than kept as a fixture because its whole
|
|
22
|
+
* point is a set entry WITHOUT the metadata its type requires — which a
|
|
23
|
+
* fixture `.val.ts` cannot hold and still typecheck.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const BASIC_FIXTURE = path.resolve(__dirname, "__fixtures__/basic");
|
|
27
|
+
const TINY_MP4 = path.resolve(
|
|
28
|
+
__dirname,
|
|
29
|
+
"../../server/src/__fixtures__/video/tiny.mp4",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const MASTER = `#EXTM3U
|
|
33
|
+
#EXT-X-STREAM-INF:BANDWIDTH=300000,RESOLUTION=640x360
|
|
34
|
+
playlist-1.m3u8
|
|
35
|
+
`;
|
|
36
|
+
const PLAYLIST = `#EXTM3U
|
|
37
|
+
#EXT-X-MAP:URI="segments-1.mp4",BYTERANGE="10@0"
|
|
38
|
+
#EXTINF:2,
|
|
39
|
+
#EXT-X-BYTERANGE:20@10
|
|
40
|
+
segments-1.mp4
|
|
41
|
+
#EXT-X-ENDLIST
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
const PROJECT: Record<string, string> = {
|
|
45
|
+
"val.modules.ts": `import { modules } from "@valbuild/core";
|
|
46
|
+
import { config } from "./val.config";
|
|
47
|
+
|
|
48
|
+
export default modules(config, [
|
|
49
|
+
{ def: () => import("./content/videos.val") },
|
|
50
|
+
{ def: () => import("./content/videos-remote.val") },
|
|
51
|
+
{ def: () => import("./content/page.val") },
|
|
52
|
+
]);
|
|
53
|
+
`,
|
|
54
|
+
"content/videos.val.ts": `import { c, s } from "../val.config";
|
|
55
|
+
|
|
56
|
+
export default c.define(
|
|
57
|
+
"/content/videos.val.ts",
|
|
58
|
+
s.videoset({ dir: "/public/val/videos" }),
|
|
59
|
+
{
|
|
60
|
+
"/public/val/videos/clip_abcde.mp4": { alt: "A clip" },
|
|
61
|
+
"/public/val/videos/intro_05198/master.m3u8": {
|
|
62
|
+
mimeType: "application/vnd.apple.mpegurl",
|
|
63
|
+
width: 640,
|
|
64
|
+
height: 360,
|
|
65
|
+
duration: 2,
|
|
66
|
+
alt: null,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
`,
|
|
71
|
+
"content/videos-remote.val.ts": `import { c, s } from "../val.config";
|
|
72
|
+
|
|
73
|
+
export default c.define(
|
|
74
|
+
"/content/videos-remote.val.ts",
|
|
75
|
+
s.videoset({ dir: "/public/val/remote-videos" }).remote(),
|
|
76
|
+
{
|
|
77
|
+
"/public/val/remote-videos/clip_fghij.mp4": {
|
|
78
|
+
mimeType: "video/mp4",
|
|
79
|
+
width: 64,
|
|
80
|
+
height: 48,
|
|
81
|
+
duration: 1,
|
|
82
|
+
alt: null,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
`,
|
|
87
|
+
"content/page.val.ts": `import { c, s } from "../val.config";
|
|
88
|
+
import videosVal from "./videos.val";
|
|
89
|
+
import remoteVideosVal from "./videos-remote.val";
|
|
90
|
+
|
|
91
|
+
export default c.define(
|
|
92
|
+
"/content/page.val.ts",
|
|
93
|
+
s.object({ hero: s.video(videosVal), remoteHero: s.video(remoteVideosVal) }),
|
|
94
|
+
{
|
|
95
|
+
hero: {
|
|
96
|
+
path: "/public/val/videos/clip_abcde.mp4",
|
|
97
|
+
alt: "Kept",
|
|
98
|
+
poster: { path: "/public/val/videos/clip-poster_11111.webp" },
|
|
99
|
+
captions: [{ path: "/public/val/videos/clip-en_22222.vtt", srclang: "en" }],
|
|
100
|
+
},
|
|
101
|
+
remoteHero: { path: "/public/val/remote-videos/clip_fghij.mp4" },
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
`,
|
|
105
|
+
"public/val/videos/intro_05198/master.m3u8": MASTER,
|
|
106
|
+
"public/val/videos/intro_05198/playlist-1.m3u8": PLAYLIST,
|
|
107
|
+
"public/val/videos/intro_05198/segments-1.mp4": "segments",
|
|
108
|
+
// The page's poster and captions live where the set keeps its videos, and
|
|
109
|
+
// are not set entries.
|
|
110
|
+
"public/val/videos/clip-poster_11111.webp": "poster",
|
|
111
|
+
"public/val/videos/clip-en_22222.vtt": "WEBVTT\n",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const notExpected: IValRemote = {
|
|
115
|
+
remoteHost: DEFAULT_VAL_REMOTE_HOST,
|
|
116
|
+
getSettings: async () => {
|
|
117
|
+
throw new Error("Not expected to be called");
|
|
118
|
+
},
|
|
119
|
+
uploadFile: async () => {
|
|
120
|
+
throw new Error("Not expected to be called");
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
async function run(
|
|
125
|
+
root: string,
|
|
126
|
+
valFiles: string[],
|
|
127
|
+
fix: boolean,
|
|
128
|
+
remote: IValRemote = notExpected,
|
|
129
|
+
): Promise<ValidationEvent[]> {
|
|
130
|
+
const events: ValidationEvent[] = [];
|
|
131
|
+
for await (const event of runValidation({
|
|
132
|
+
root,
|
|
133
|
+
fix,
|
|
134
|
+
valFiles,
|
|
135
|
+
project: fix ? "test/project" : undefined,
|
|
136
|
+
remote,
|
|
137
|
+
fs: createDefaultValFSHost(),
|
|
138
|
+
})) {
|
|
139
|
+
events.push(event);
|
|
140
|
+
}
|
|
141
|
+
return events;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function sourceOf(root: string, moduleFilePath: string) {
|
|
145
|
+
const service = await createService(root, createDefaultValFSHost());
|
|
146
|
+
try {
|
|
147
|
+
const result = await service.get(
|
|
148
|
+
moduleFilePath as ModuleFilePath,
|
|
149
|
+
"" as ModulePath,
|
|
150
|
+
{ validate: false },
|
|
151
|
+
);
|
|
152
|
+
return result.source;
|
|
153
|
+
} finally {
|
|
154
|
+
service.dispose();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
describe("val validate with an s.videoset()", () => {
|
|
159
|
+
let root: string;
|
|
160
|
+
|
|
161
|
+
beforeEach(() => {
|
|
162
|
+
const tmpBase = path.join(__dirname, ".tmp");
|
|
163
|
+
fs.mkdirSync(tmpBase, { recursive: true });
|
|
164
|
+
root = fs.mkdtempSync(path.join(tmpBase, "videoset-"));
|
|
165
|
+
for (const file of ["val.config.ts", "tsconfig.json"]) {
|
|
166
|
+
fs.copyFileSync(path.join(BASIC_FIXTURE, file), path.join(root, file));
|
|
167
|
+
}
|
|
168
|
+
for (const [file, content] of Object.entries(PROJECT)) {
|
|
169
|
+
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
|
170
|
+
fs.writeFileSync(path.join(root, file), content);
|
|
171
|
+
}
|
|
172
|
+
for (const file of [
|
|
173
|
+
"public/val/videos/clip_abcde.mp4",
|
|
174
|
+
// In the directory, not in the set.
|
|
175
|
+
"public/val/videos/new_33333.mp4",
|
|
176
|
+
"public/val/remote-videos/clip_fghij.mp4",
|
|
177
|
+
]) {
|
|
178
|
+
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
|
179
|
+
fs.copyFileSync(TINY_MP4, path.join(root, file));
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
afterEach(() => {
|
|
184
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("reports the missing metadata and the untracked video as fixable, and nothing about the page's poster", async () => {
|
|
188
|
+
const events = await run(
|
|
189
|
+
root,
|
|
190
|
+
["content/videos.val.ts", "content/page.val.ts"],
|
|
191
|
+
false,
|
|
192
|
+
);
|
|
193
|
+
const fixable = events.filter((e) => e.type === "validation-fixable-error");
|
|
194
|
+
expect(fixable.map((e) => e.sourcePath).sort()).toEqual([
|
|
195
|
+
// The untracked video, on the set: it has no entry to point at yet.
|
|
196
|
+
"/content/videos.val.ts",
|
|
197
|
+
'/content/videos.val.ts?p="/public/val/videos/clip_abcde.mp4"',
|
|
198
|
+
]);
|
|
199
|
+
expect(events.filter((e) => e.type === "validation-error")).toEqual([]);
|
|
200
|
+
expect(
|
|
201
|
+
fixable.some(
|
|
202
|
+
(e) =>
|
|
203
|
+
e.type === "validation-fixable-error" &&
|
|
204
|
+
e.message.includes("/public/val/videos/new_33333.mp4"),
|
|
205
|
+
),
|
|
206
|
+
).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("--fix fills the metadata in and adds the untracked video, and then it is valid", async () => {
|
|
210
|
+
await run(root, ["content/videos.val.ts", "content/page.val.ts"], true);
|
|
211
|
+
expect(await sourceOf(root, "/content/videos.val.ts")).toEqual({
|
|
212
|
+
"/public/val/videos/clip_abcde.mp4": {
|
|
213
|
+
alt: "A clip",
|
|
214
|
+
mimeType: "video/mp4",
|
|
215
|
+
width: 64,
|
|
216
|
+
height: 48,
|
|
217
|
+
duration: 1,
|
|
218
|
+
},
|
|
219
|
+
"/public/val/videos/intro_05198/master.m3u8": {
|
|
220
|
+
mimeType: "application/vnd.apple.mpegurl",
|
|
221
|
+
width: 640,
|
|
222
|
+
height: 360,
|
|
223
|
+
duration: 2,
|
|
224
|
+
alt: null,
|
|
225
|
+
},
|
|
226
|
+
"/public/val/videos/new_33333.mp4": {
|
|
227
|
+
alt: null,
|
|
228
|
+
mimeType: "video/mp4",
|
|
229
|
+
width: 64,
|
|
230
|
+
height: 48,
|
|
231
|
+
duration: 1,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
const again = await run(
|
|
235
|
+
root,
|
|
236
|
+
["content/videos.val.ts", "content/page.val.ts"],
|
|
237
|
+
false,
|
|
238
|
+
);
|
|
239
|
+
expect(again.at(-1)).toEqual({ type: "summary-success" });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("--fix uploads a remote set's local entry, renames its key and points the page at it", async () => {
|
|
243
|
+
fs.mkdirSync(path.join(root, ".val"), { recursive: true });
|
|
244
|
+
fs.writeFileSync(
|
|
245
|
+
path.join(root, ".val", "pat.json"),
|
|
246
|
+
JSON.stringify({ pat: "test-pat" }),
|
|
247
|
+
);
|
|
248
|
+
const uploaded: string[] = [];
|
|
249
|
+
const remote: IValRemote = {
|
|
250
|
+
remoteHost: DEFAULT_VAL_REMOTE_HOST,
|
|
251
|
+
getSettings: async () => ({
|
|
252
|
+
success: true,
|
|
253
|
+
data: {
|
|
254
|
+
publicProjectId: "pubproj",
|
|
255
|
+
remoteFileBuckets: [{ bucket: "01" }],
|
|
256
|
+
},
|
|
257
|
+
}),
|
|
258
|
+
uploadFile: async (_project, _bucket, fileHash) => {
|
|
259
|
+
uploaded.push(fileHash);
|
|
260
|
+
return { success: true };
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
const events = await run(
|
|
264
|
+
root,
|
|
265
|
+
["content/videos-remote.val.ts"],
|
|
266
|
+
true,
|
|
267
|
+
remote,
|
|
268
|
+
);
|
|
269
|
+
expect(uploaded).toHaveLength(1);
|
|
270
|
+
expect(
|
|
271
|
+
events
|
|
272
|
+
.filter((e) => e.type === "fix-applied")
|
|
273
|
+
.map((e) => e.type === "fix-applied" && e.file)
|
|
274
|
+
.sort(),
|
|
275
|
+
).toEqual(["content/page.val.ts", "content/videos-remote.val.ts"]);
|
|
276
|
+
|
|
277
|
+
const setSource = await sourceOf(root, "/content/videos-remote.val.ts");
|
|
278
|
+
const keys = Object.keys(setSource ?? {});
|
|
279
|
+
expect(keys).toHaveLength(1);
|
|
280
|
+
expect(keys[0]).toMatch(
|
|
281
|
+
/^https:\/\/.*pubproj.*public\/val\/remote-videos\/clip_fghij\.mp4$/,
|
|
282
|
+
);
|
|
283
|
+
expect(await sourceOf(root, "/content/page.val.ts")).toMatchObject({
|
|
284
|
+
hero: { path: "/public/val/videos/clip_abcde.mp4", alt: "Kept" },
|
|
285
|
+
remoteHero: { path: keys[0] },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Both modules are valid against each other afterwards.
|
|
289
|
+
const again = await run(
|
|
290
|
+
root,
|
|
291
|
+
["content/videos-remote.val.ts", "content/page.val.ts"],
|
|
292
|
+
false,
|
|
293
|
+
);
|
|
294
|
+
expect(
|
|
295
|
+
again.filter(
|
|
296
|
+
(e) =>
|
|
297
|
+
e.type === "validation-error" ||
|
|
298
|
+
e.type === "validation-fixable-error",
|
|
299
|
+
),
|
|
300
|
+
).toEqual([]);
|
|
301
|
+
});
|
|
302
|
+
});
|