@scenar/cli 0.8.0 → 0.9.1
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/example-bundle/assets/index-DlmG1Uh7.js +388 -0
- package/example-bundle/assets/style-BjquDnTE.css +1 -0
- package/example-bundle/index.html +2 -2
- package/example-bundle/pack-manifest.json +9 -9
- package/example-bundle/scenario.json +6 -2
- package/package.json +9 -5
- package/src/__tests__/collect-pack-shots.test.ts +97 -0
- package/src/__tests__/pack-generate-embed-entry.test.ts +68 -3
- package/src/__tests__/pack-manifest.test.ts +17 -0
- package/src/__tests__/read-shots.test.ts +57 -0
- package/src/__tests__/run-shoot.test.ts +319 -0
- package/src/api.d.ts +3 -0
- package/src/api.d.ts.map +1 -1
- package/src/api.js +2 -0
- package/src/api.js.map +1 -1
- package/src/api.ts +5 -0
- package/src/bundle/read-shots.d.ts +28 -0
- package/src/bundle/read-shots.d.ts.map +1 -0
- package/src/bundle/read-shots.js +28 -0
- package/src/bundle/read-shots.js.map +1 -0
- package/src/bundle/read-shots.ts +38 -0
- package/src/commands/shoot.d.ts +3 -0
- package/src/commands/shoot.d.ts.map +1 -0
- package/src/commands/shoot.js +52 -0
- package/src/commands/shoot.js.map +1 -0
- package/src/commands/shoot.ts +72 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +2 -0
- package/src/index.js.map +1 -1
- package/src/index.ts +2 -0
- package/src/pack/build.d.ts +33 -1
- package/src/pack/build.d.ts.map +1 -1
- package/src/pack/build.js +35 -10
- package/src/pack/build.js.map +1 -1
- package/src/pack/build.ts +63 -17
- package/src/pack/collect-pack-shots.d.ts +40 -0
- package/src/pack/collect-pack-shots.d.ts.map +1 -0
- package/src/pack/collect-pack-shots.js +59 -0
- package/src/pack/collect-pack-shots.js.map +1 -0
- package/src/pack/collect-pack-shots.ts +76 -0
- package/src/pack/generate-embed-entry.d.ts +18 -0
- package/src/pack/generate-embed-entry.d.ts.map +1 -1
- package/src/pack/generate-embed-entry.js +92 -3
- package/src/pack/generate-embed-entry.js.map +1 -1
- package/src/pack/generate-embed-entry.ts +94 -3
- package/src/pack/pack-manifest.d.ts +12 -3
- package/src/pack/pack-manifest.d.ts.map +1 -1
- package/src/pack/pack-manifest.js +13 -3
- package/src/pack/pack-manifest.js.map +1 -1
- package/src/pack/pack-manifest.ts +13 -2
- package/src/pack/run-pack.d.ts +6 -0
- package/src/pack/run-pack.d.ts.map +1 -1
- package/src/pack/run-pack.js +41 -14
- package/src/pack/run-pack.js.map +1 -1
- package/src/pack/run-pack.ts +55 -14
- package/src/shoot/playwright-browser.d.ts +9 -0
- package/src/shoot/playwright-browser.d.ts.map +1 -0
- package/src/shoot/playwright-browser.js +92 -0
- package/src/shoot/playwright-browser.js.map +1 -0
- package/src/shoot/playwright-browser.ts +121 -0
- package/src/shoot/run-shoot.d.ts +58 -0
- package/src/shoot/run-shoot.d.ts.map +1 -0
- package/src/shoot/run-shoot.js +190 -0
- package/src/shoot/run-shoot.js.map +1 -0
- package/src/shoot/run-shoot.ts +270 -0
- package/src/shoot/types.d.ts +40 -0
- package/src/shoot/types.d.ts.map +1 -0
- package/src/shoot/types.js +2 -0
- package/src/shoot/types.js.map +1 -0
- package/src/shoot/types.ts +40 -0
- package/src/util/load-ts.d.ts +12 -2
- package/src/util/load-ts.d.ts.map +1 -1
- package/src/util/load-ts.js +25 -10
- package/src/util/load-ts.js.map +1 -1
- package/src/util/load-ts.ts +25 -11
- package/tsconfig.tsbuildinfo +1 -1
- package/example-bundle/assets/index-CBaiZ4ek.js +0 -147
- package/example-bundle/assets/style-Ci-h7jik.css +0 -1
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import type { ScenarioShot } from "@scenar/core";
|
|
7
|
+
import { STILLS_DIR, runShoot } from "../shoot/run-shoot.js";
|
|
8
|
+
import type { PackManifest } from "../pack/pack-manifest.js";
|
|
9
|
+
import type { ShotBrowser, ShotTheme } from "../shoot/types.js";
|
|
10
|
+
|
|
11
|
+
const SHOTS: ScenarioShot[] = [
|
|
12
|
+
{ name: "opening", timeMs: 2999, stepIndex: 0 },
|
|
13
|
+
{ name: "detail-open", timeMs: 6999, stepIndex: 2 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Write the minimal three files that make a directory a shootable bundle.
|
|
18
|
+
* By default scenario.json carries no `shots` key (a pre-0.0.2 bundle:
|
|
19
|
+
* shots unknown, so runShoot must boot the capture page); pass
|
|
20
|
+
* `recordedShots` to emulate a bundle whose pack recorded the list.
|
|
21
|
+
*/
|
|
22
|
+
async function makeBundle(recordedShots?: readonly string[]): Promise<string> {
|
|
23
|
+
const dir = await mkdtemp(join(tmpdir(), "scenar-shoot-"));
|
|
24
|
+
await writeFile(join(dir, "index.html"), "<!doctype html><div id='root'></div>", "utf-8");
|
|
25
|
+
await writeFile(
|
|
26
|
+
join(dir, "scenario.json"),
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
schemaVersion: "1",
|
|
29
|
+
id: "lab-tour",
|
|
30
|
+
generator: "@scenar/cli pack 0.0.1",
|
|
31
|
+
viewport: { width: 896, height: 480 },
|
|
32
|
+
...(recordedShots !== undefined ? { shots: recordedShots } : {}),
|
|
33
|
+
}),
|
|
34
|
+
"utf-8",
|
|
35
|
+
);
|
|
36
|
+
await writeFile(
|
|
37
|
+
join(dir, "pack-manifest.json"),
|
|
38
|
+
JSON.stringify({ schemaVersion: 1, scenarioId: "lab-tour", files: [] }),
|
|
39
|
+
"utf-8",
|
|
40
|
+
);
|
|
41
|
+
return dir;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface FakeKnobs {
|
|
45
|
+
/** Shot list every session reports (default {@link SHOTS}). */
|
|
46
|
+
shots?: ScenarioShot[];
|
|
47
|
+
/** Shot list per theme — for the cross-theme divergence test. */
|
|
48
|
+
shotsFor?: (theme: ShotTheme) => ScenarioShot[];
|
|
49
|
+
/** Error thrown from open() — simulates window.__scenarShotError. */
|
|
50
|
+
openError?: string;
|
|
51
|
+
/** Image bytes per (theme, shot, per-theme session number). */
|
|
52
|
+
imageFor?: (theme: ShotTheme, shotName: string, sessionIndex: number) => Buffer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SessionLog {
|
|
56
|
+
theme: ShotTheme;
|
|
57
|
+
url: string;
|
|
58
|
+
walks: number[];
|
|
59
|
+
closed: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A ShotBrowser fake that records every interaction for assertions. */
|
|
63
|
+
function makeFakeBrowser(knobs: FakeKnobs = {}) {
|
|
64
|
+
const sessions: SessionLog[] = [];
|
|
65
|
+
const perThemeCount = new Map<ShotTheme, number>();
|
|
66
|
+
let browserClosed = false;
|
|
67
|
+
|
|
68
|
+
const imageFor =
|
|
69
|
+
knobs.imageFor ??
|
|
70
|
+
((theme: ShotTheme, shotName: string) => Buffer.from(`png:${theme}:${shotName}`));
|
|
71
|
+
|
|
72
|
+
const browser: ShotBrowser = {
|
|
73
|
+
async newSession({ theme }) {
|
|
74
|
+
const sessionIndex = perThemeCount.get(theme) ?? 0;
|
|
75
|
+
perThemeCount.set(theme, sessionIndex + 1);
|
|
76
|
+
const log: SessionLog = { theme, url: "", walks: [], closed: false };
|
|
77
|
+
sessions.push(log);
|
|
78
|
+
const shots = knobs.shotsFor?.(theme) ?? knobs.shots ?? SHOTS;
|
|
79
|
+
let lastWalkedShot = "";
|
|
80
|
+
return {
|
|
81
|
+
async open(url) {
|
|
82
|
+
log.url = url;
|
|
83
|
+
if (knobs.openError) throw new Error(`the capture page reported: ${knobs.openError}`);
|
|
84
|
+
return { shots, frameSelector: "[data-scenar-shot-frame]" };
|
|
85
|
+
},
|
|
86
|
+
async walkTo(timeMs) {
|
|
87
|
+
log.walks.push(timeMs);
|
|
88
|
+
lastWalkedShot = shots.find((s) => s.timeMs === timeMs)?.name ?? "unknown";
|
|
89
|
+
},
|
|
90
|
+
async screenshotFrame() {
|
|
91
|
+
return imageFor(theme, lastWalkedShot, sessionIndex);
|
|
92
|
+
},
|
|
93
|
+
async close() {
|
|
94
|
+
log.closed = true;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
async close() {
|
|
99
|
+
browserClosed = true;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return { browser, sessions, isClosed: () => browserClosed };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function readManifest(bundleDir: string): Promise<PackManifest> {
|
|
107
|
+
return JSON.parse(await readFile(join(bundleDir, "pack-manifest.json"), "utf-8"));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe("runShoot", () => {
|
|
111
|
+
it("captures every shot per theme, in timeline order, into stills/", async () => {
|
|
112
|
+
const bundleDir = await makeBundle();
|
|
113
|
+
const fake = makeFakeBrowser();
|
|
114
|
+
|
|
115
|
+
const result = await runShoot({ bundleDir, browserFactory: async () => fake.browser });
|
|
116
|
+
|
|
117
|
+
expect(result.scenarioId).toBe("lab-tour");
|
|
118
|
+
expect(result.shots).toEqual(SHOTS);
|
|
119
|
+
expect(result.files).toEqual([
|
|
120
|
+
"stills/opening.light.png",
|
|
121
|
+
"stills/detail-open.light.png",
|
|
122
|
+
"stills/opening.dark.png",
|
|
123
|
+
"stills/detail-open.dark.png",
|
|
124
|
+
]);
|
|
125
|
+
// One session per theme, each walking the timeline in order.
|
|
126
|
+
expect(fake.sessions.map((s) => s.theme)).toEqual(["light", "dark"]);
|
|
127
|
+
for (const session of fake.sessions) {
|
|
128
|
+
expect(session.walks).toEqual([2999, 6999]);
|
|
129
|
+
expect(session.closed).toBe(true);
|
|
130
|
+
}
|
|
131
|
+
expect(fake.isClosed()).toBe(true);
|
|
132
|
+
// The written bytes are the session's screenshots.
|
|
133
|
+
const opening = await readFile(join(bundleDir, STILLS_DIR, "opening.light.png"));
|
|
134
|
+
expect(opening.toString()).toBe("png:light:opening");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("passes ?shot to every session and adds ?theme=dark only for dark", async () => {
|
|
138
|
+
const bundleDir = await makeBundle();
|
|
139
|
+
const fake = makeFakeBrowser();
|
|
140
|
+
|
|
141
|
+
await runShoot({ bundleDir, browserFactory: async () => fake.browser });
|
|
142
|
+
|
|
143
|
+
const [light, dark] = fake.sessions;
|
|
144
|
+
expect(light!.url).toMatch(/\?shot$/);
|
|
145
|
+
expect(dark!.url).toMatch(/\?shot&theme=dark$/);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("rebuilds the pack manifest so deploys ship the stills", async () => {
|
|
149
|
+
const bundleDir = await makeBundle();
|
|
150
|
+
const fake = makeFakeBrowser();
|
|
151
|
+
|
|
152
|
+
await runShoot({ bundleDir, browserFactory: async () => fake.browser });
|
|
153
|
+
|
|
154
|
+
const manifest = await readManifest(bundleDir);
|
|
155
|
+
const stillEntries = manifest.files.filter((f) => f.path.startsWith("stills/"));
|
|
156
|
+
expect(stillEntries.map((f) => f.path).sort()).toEqual([
|
|
157
|
+
"stills/detail-open.dark.png",
|
|
158
|
+
"stills/detail-open.light.png",
|
|
159
|
+
"stills/opening.dark.png",
|
|
160
|
+
"stills/opening.light.png",
|
|
161
|
+
]);
|
|
162
|
+
for (const entry of stillEntries) {
|
|
163
|
+
expect(entry.contentType).toBe("image/png");
|
|
164
|
+
}
|
|
165
|
+
// Declared-then-verified: the recorded sha256 is the file's real hash.
|
|
166
|
+
const opening = stillEntries.find((f) => f.path === "stills/opening.light.png")!;
|
|
167
|
+
expect(opening.sha256).toBe(
|
|
168
|
+
createHash("sha256").update(Buffer.from("png:light:opening")).digest("hex"),
|
|
169
|
+
);
|
|
170
|
+
// The bundle's own files are (re)listed too.
|
|
171
|
+
expect(manifest.files.some((f) => f.path === "index.html")).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("clears stale stills and rebuilds the manifest even when no shots are declared", async () => {
|
|
175
|
+
const bundleDir = await makeBundle();
|
|
176
|
+
await mkdir(join(bundleDir, STILLS_DIR), { recursive: true });
|
|
177
|
+
await writeFile(join(bundleDir, STILLS_DIR, "removed-shot.light.png"), "stale");
|
|
178
|
+
const fake = makeFakeBrowser({ shots: [] });
|
|
179
|
+
|
|
180
|
+
const result = await runShoot({ bundleDir, browserFactory: async () => fake.browser });
|
|
181
|
+
|
|
182
|
+
expect(result.shots).toEqual([]);
|
|
183
|
+
expect(result.files).toEqual([]);
|
|
184
|
+
// The stale still is gone from disk AND from the manifest — a manifest
|
|
185
|
+
// listing a deleted file would fail its existence check at publish.
|
|
186
|
+
expect(await stat(join(bundleDir, STILLS_DIR)).catch(() => null)).toBeNull();
|
|
187
|
+
const manifest = await readManifest(bundleDir);
|
|
188
|
+
expect(manifest.files.some((f) => f.path.startsWith("stills/"))).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("skips the browser entirely when scenario.json records an empty shot list", async () => {
|
|
192
|
+
const bundleDir = await makeBundle([]);
|
|
193
|
+
await mkdir(join(bundleDir, STILLS_DIR), { recursive: true });
|
|
194
|
+
await writeFile(join(bundleDir, STILLS_DIR, "removed-shot.light.png"), "stale");
|
|
195
|
+
let browserRequested = false;
|
|
196
|
+
|
|
197
|
+
const result = await runShoot({
|
|
198
|
+
bundleDir,
|
|
199
|
+
browserFactory: async () => {
|
|
200
|
+
browserRequested = true;
|
|
201
|
+
throw new Error("the short-circuit must return before the browser factory");
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Pack's record is authoritative: no session, no browser, same outcome.
|
|
206
|
+
expect(browserRequested).toBe(false);
|
|
207
|
+
expect(result).toEqual({
|
|
208
|
+
scenarioId: "lab-tour",
|
|
209
|
+
bundleDir,
|
|
210
|
+
shots: [],
|
|
211
|
+
files: [],
|
|
212
|
+
themes: ["light", "dark"],
|
|
213
|
+
verified: false,
|
|
214
|
+
});
|
|
215
|
+
// Both invariants hold on the short-circuit path too: stale stills are
|
|
216
|
+
// cleared, and the manifest is rebuilt so it never lists deleted files.
|
|
217
|
+
expect(await stat(join(bundleDir, STILLS_DIR)).catch(() => null)).toBeNull();
|
|
218
|
+
const manifest = await readManifest(bundleDir);
|
|
219
|
+
expect(manifest.files.some((f) => f.path.startsWith("stills/"))).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("still boots the capture page when scenario.json records a non-empty list", async () => {
|
|
223
|
+
// The record proves there is something to shoot; the running bundle
|
|
224
|
+
// remains the runtime truth for what actually gets captured.
|
|
225
|
+
const bundleDir = await makeBundle(SHOTS.map((s) => s.name));
|
|
226
|
+
const fake = makeFakeBrowser();
|
|
227
|
+
|
|
228
|
+
const result = await runShoot({ bundleDir, browserFactory: async () => fake.browser });
|
|
229
|
+
|
|
230
|
+
expect(fake.sessions.length).toBeGreaterThan(0);
|
|
231
|
+
expect(result.shots).toEqual(SHOTS);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("captures a single theme when asked", async () => {
|
|
235
|
+
const bundleDir = await makeBundle();
|
|
236
|
+
const fake = makeFakeBrowser();
|
|
237
|
+
|
|
238
|
+
const result = await runShoot({
|
|
239
|
+
bundleDir,
|
|
240
|
+
themes: ["light"],
|
|
241
|
+
browserFactory: async () => fake.browser,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(result.files).toEqual(["stills/opening.light.png", "stills/detail-open.light.png"]);
|
|
245
|
+
expect(fake.sessions).toHaveLength(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("surfaces the capture page's own error (e.g. invalid shot declarations)", async () => {
|
|
249
|
+
const bundleDir = await makeBundle();
|
|
250
|
+
const fake = makeFakeBrowser({
|
|
251
|
+
openError: 'steps 0 and 2 both declare shot "opening"; shot names must be unique within a scenario',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await expect(
|
|
255
|
+
runShoot({ bundleDir, browserFactory: async () => fake.browser }),
|
|
256
|
+
).rejects.toThrow(/steps 0 and 2 both declare shot "opening"/);
|
|
257
|
+
// The browser is still torn down on the failure path.
|
|
258
|
+
expect(fake.isClosed()).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("verify: passes when a second fresh session reproduces every byte", async () => {
|
|
262
|
+
const bundleDir = await makeBundle();
|
|
263
|
+
// Image content ignores the session index — deterministic.
|
|
264
|
+
const fake = makeFakeBrowser();
|
|
265
|
+
|
|
266
|
+
const result = await runShoot({
|
|
267
|
+
bundleDir,
|
|
268
|
+
verify: true,
|
|
269
|
+
browserFactory: async () => fake.browser,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(result.verified).toBe(true);
|
|
273
|
+
// Two sessions per theme: capture + verify, all fresh.
|
|
274
|
+
expect(fake.sessions.map((s) => s.theme)).toEqual(["light", "dark", "light", "dark"]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("verify: fails naming the differing shot when sessions disagree", async () => {
|
|
278
|
+
const bundleDir = await makeBundle();
|
|
279
|
+
const fake = makeFakeBrowser({
|
|
280
|
+
imageFor: (theme, shotName, sessionIndex) =>
|
|
281
|
+
// "detail-open" renders differently on every session — the exact
|
|
282
|
+
// nondeterminism --verify exists to catch.
|
|
283
|
+
Buffer.from(
|
|
284
|
+
shotName === "detail-open"
|
|
285
|
+
? `png:${theme}:${shotName}:${sessionIndex}`
|
|
286
|
+
: `png:${theme}:${shotName}`,
|
|
287
|
+
),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await expect(
|
|
291
|
+
runShoot({ bundleDir, verify: true, browserFactory: async () => fake.browser }),
|
|
292
|
+
).rejects.toThrow(/determinism check failed \(light\): shot\(s\) detail-open differed/);
|
|
293
|
+
// Nothing was written: a failed verify must not leave untrusted stills.
|
|
294
|
+
expect(await stat(join(bundleDir, STILLS_DIR)).catch(() => null)).toBeNull();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("fails when themes disagree on the shot list (bundle nondeterminism)", async () => {
|
|
298
|
+
const bundleDir = await makeBundle();
|
|
299
|
+
const fake = makeFakeBrowser({
|
|
300
|
+
shotsFor: (theme) => (theme === "dark" ? SHOTS.slice(0, 1) : SHOTS),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await expect(
|
|
304
|
+
runShoot({ bundleDir, browserFactory: async () => fake.browser }),
|
|
305
|
+
).rejects.toThrow(/different shot list/);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("rejects a directory that is not a packed bundle, with the fix named", async () => {
|
|
309
|
+
const dir = await mkdtemp(join(tmpdir(), "scenar-shoot-notbundle-"));
|
|
310
|
+
await writeFile(join(dir, "index.html"), "<!doctype html>", "utf-8");
|
|
311
|
+
|
|
312
|
+
const fake = makeFakeBrowser();
|
|
313
|
+
await expect(
|
|
314
|
+
runShoot({ bundleDir: dir, browserFactory: async () => fake.browser }),
|
|
315
|
+
).rejects.toThrow(/no scenario\.json in .*Run `scenar pack`/);
|
|
316
|
+
// Validation fails before any browser work.
|
|
317
|
+
expect(fake.sessions).toHaveLength(0);
|
|
318
|
+
});
|
|
319
|
+
});
|
package/src/api.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export { runNarrate } from "./narrate/run-narrate.js";
|
|
|
12
12
|
export type { RunNarrateOptions, NarrateResult, NarratedScenario } from "./narrate/run-narrate.js";
|
|
13
13
|
export { runRender } from "./render/run-render.js";
|
|
14
14
|
export type { RunRenderOptions, RenderResult } from "./render/run-render.js";
|
|
15
|
+
export { runShoot, STILLS_DIR } from "./shoot/run-shoot.js";
|
|
16
|
+
export type { RunShootOptions, ShootResult } from "./shoot/run-shoot.js";
|
|
17
|
+
export type { ShotBrowser, ShotSession, ShotCaptureInfo, ShotTheme } from "./shoot/types.js";
|
|
15
18
|
export { runServe } from "./serve/run-serve.js";
|
|
16
19
|
export type { RunServeOptions, ServeResult } from "./serve/run-serve.js";
|
|
17
20
|
export { startBundleServer, createBundleServer } from "./serve/static-server.js";
|
package/src/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrE,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG5D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGnG,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAG7E,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACjF,YAAY,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAG7F,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACpF,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,QAAQ,EACR,OAAO,GACR,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,cAAc,GACf,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAGvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,YAAY,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACrE,YAAY,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrE,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG5D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGnG,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAG7E,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC5D,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAG7F,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACjF,YAAY,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAG7F,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACpF,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,QAAQ,EACR,OAAO,GACR,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,cAAc,GACf,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAGvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,YAAY,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACrE,YAAY,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC"}
|
package/src/api.js
CHANGED
|
@@ -11,6 +11,8 @@ export { runPack } from "./pack/run-pack.js";
|
|
|
11
11
|
export { runNarrate } from "./narrate/run-narrate.js";
|
|
12
12
|
// Render
|
|
13
13
|
export { runRender } from "./render/run-render.js";
|
|
14
|
+
// Shoot (render a bundle's declared shots to stills)
|
|
15
|
+
export { runShoot, STILLS_DIR } from "./shoot/run-shoot.js";
|
|
14
16
|
// Serve
|
|
15
17
|
export { runServe } from "./serve/run-serve.js";
|
|
16
18
|
export { startBundleServer, createBundleServer } from "./serve/static-server.js";
|
package/src/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO;AACP,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAI7C,UAAU;AACV,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAGtD,SAAS;AACT,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGnD,QAAQ;AACR,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEhD,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAGjF,yBAAyB;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAEtD,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,QAAQ,EACR,OAAO,GACR,MAAM,2BAA2B,CAAC;AAMnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,qFAAqF;AACrF,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAQtD,WAAW;AACX,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,4CAA4C;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO;AACP,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAI7C,UAAU;AACV,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAGtD,SAAS;AACT,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGnD,qDAAqD;AACrD,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAI5D,QAAQ;AACR,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEhD,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAGjF,yBAAyB;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAEtD,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,QAAQ,EACR,OAAO,GACR,MAAM,2BAA2B,CAAC;AAMnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,qFAAqF;AACrF,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAQtD,WAAW;AACX,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,4CAA4C;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
|
package/src/api.ts
CHANGED
|
@@ -19,6 +19,11 @@ export type { RunNarrateOptions, NarrateResult, NarratedScenario } from "./narra
|
|
|
19
19
|
export { runRender } from "./render/run-render.js";
|
|
20
20
|
export type { RunRenderOptions, RenderResult } from "./render/run-render.js";
|
|
21
21
|
|
|
22
|
+
// Shoot (render a bundle's declared shots to stills)
|
|
23
|
+
export { runShoot, STILLS_DIR } from "./shoot/run-shoot.js";
|
|
24
|
+
export type { RunShootOptions, ShootResult } from "./shoot/run-shoot.js";
|
|
25
|
+
export type { ShotBrowser, ShotSession, ShotCaptureInfo, ShotTheme } from "./shoot/types.js";
|
|
26
|
+
|
|
22
27
|
// Serve
|
|
23
28
|
export { runServe } from "./serve/run-serve.js";
|
|
24
29
|
export type { RunServeOptions, ServeResult } from "./serve/run-serve.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The outcome of reading a bundle's recorded shot names. */
|
|
2
|
+
export type BundleShots =
|
|
3
|
+
/** scenario.json carries an authoritative list (possibly empty). */
|
|
4
|
+
{
|
|
5
|
+
readonly recorded: true;
|
|
6
|
+
readonly shots: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* No usable `shots` record — the bundle predates the key, pack could not
|
|
10
|
+
* load the steps module, or the file is missing/malformed. The shots are
|
|
11
|
+
* unknown; only booting the capture page can answer.
|
|
12
|
+
*/
|
|
13
|
+
| {
|
|
14
|
+
readonly recorded: false;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Read the declared shot names recorded in a bundle's scenario.json.
|
|
18
|
+
*
|
|
19
|
+
* The sibling of {@link readBundleViewport}, with the same forgiving posture:
|
|
20
|
+
* any missing/unreadable/malformed state degrades to `recorded: false` rather
|
|
21
|
+
* than throwing, because an absent record only costs the caller a browser
|
|
22
|
+
* boot — it must never fail an operation that a pre-`shots` bundle could
|
|
23
|
+
* complete. Present-and-empty is meaningfully different: it is pack's
|
|
24
|
+
* authoritative statement that the scenario declares no shots, which is what
|
|
25
|
+
* lets `runShoot` skip the browser entirely.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readBundleShots(bundleDir: string): Promise<BundleShots>;
|
|
28
|
+
//# sourceMappingURL=read-shots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-shots.d.ts","sourceRoot":"","sources":["../../../src/bundle/read-shots.ts"],"names":[],"mappings":"AAIA,6DAA6D;AAC7D,MAAM,MAAM,WAAW;AACrB,oEAAoE;AAClE;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE;AAChE;;;;GAIG;GACD;IAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAA;CAAE,CAAC;AAEjC;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAW7E"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { SCENARIO_JSON_FILE } from "../pack/pack-manifest.js";
|
|
4
|
+
/**
|
|
5
|
+
* Read the declared shot names recorded in a bundle's scenario.json.
|
|
6
|
+
*
|
|
7
|
+
* The sibling of {@link readBundleViewport}, with the same forgiving posture:
|
|
8
|
+
* any missing/unreadable/malformed state degrades to `recorded: false` rather
|
|
9
|
+
* than throwing, because an absent record only costs the caller a browser
|
|
10
|
+
* boot — it must never fail an operation that a pre-`shots` bundle could
|
|
11
|
+
* complete. Present-and-empty is meaningfully different: it is pack's
|
|
12
|
+
* authoritative statement that the scenario declares no shots, which is what
|
|
13
|
+
* lets `runShoot` skip the browser entirely.
|
|
14
|
+
*/
|
|
15
|
+
export async function readBundleShots(bundleDir) {
|
|
16
|
+
try {
|
|
17
|
+
const raw = await readFile(join(bundleDir, SCENARIO_JSON_FILE), "utf-8");
|
|
18
|
+
const shots = JSON.parse(raw).shots;
|
|
19
|
+
if (Array.isArray(shots) && shots.every((name) => typeof name === "string")) {
|
|
20
|
+
return { recorded: true, shots };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Missing/unreadable/invalid scenario.json — the shots are unknown.
|
|
25
|
+
}
|
|
26
|
+
return { recorded: false };
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=read-shots.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-shots.js","sourceRoot":"","sources":["../../../src/bundle/read-shots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAa9D;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,SAAiB;IACrD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,kBAAkB,CAAC,EAAE,OAAO,CAAC,CAAC;QACzE,MAAM,KAAK,GAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAyB,CAAC,KAAK,CAAC;QAC7D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC;YAC5E,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QACnC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;IACtE,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { SCENARIO_JSON_FILE } from "../pack/pack-manifest.js";
|
|
4
|
+
|
|
5
|
+
/** The outcome of reading a bundle's recorded shot names. */
|
|
6
|
+
export type BundleShots =
|
|
7
|
+
/** scenario.json carries an authoritative list (possibly empty). */
|
|
8
|
+
| { readonly recorded: true; readonly shots: readonly string[] }
|
|
9
|
+
/**
|
|
10
|
+
* No usable `shots` record — the bundle predates the key, pack could not
|
|
11
|
+
* load the steps module, or the file is missing/malformed. The shots are
|
|
12
|
+
* unknown; only booting the capture page can answer.
|
|
13
|
+
*/
|
|
14
|
+
| { readonly recorded: false };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read the declared shot names recorded in a bundle's scenario.json.
|
|
18
|
+
*
|
|
19
|
+
* The sibling of {@link readBundleViewport}, with the same forgiving posture:
|
|
20
|
+
* any missing/unreadable/malformed state degrades to `recorded: false` rather
|
|
21
|
+
* than throwing, because an absent record only costs the caller a browser
|
|
22
|
+
* boot — it must never fail an operation that a pre-`shots` bundle could
|
|
23
|
+
* complete. Present-and-empty is meaningfully different: it is pack's
|
|
24
|
+
* authoritative statement that the scenario declares no shots, which is what
|
|
25
|
+
* lets `runShoot` skip the browser entirely.
|
|
26
|
+
*/
|
|
27
|
+
export async function readBundleShots(bundleDir: string): Promise<BundleShots> {
|
|
28
|
+
try {
|
|
29
|
+
const raw = await readFile(join(bundleDir, SCENARIO_JSON_FILE), "utf-8");
|
|
30
|
+
const shots = (JSON.parse(raw) as { shots?: unknown }).shots;
|
|
31
|
+
if (Array.isArray(shots) && shots.every((name) => typeof name === "string")) {
|
|
32
|
+
return { recorded: true, shots };
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Missing/unreadable/invalid scenario.json — the shots are unknown.
|
|
36
|
+
}
|
|
37
|
+
return { recorded: false };
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shoot.d.ts","sourceRoot":"","sources":["../../../src/commands/shoot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2D3D"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { runShoot } from "../shoot/run-shoot.js";
|
|
2
|
+
const THEME_CHOICES = ["light", "dark", "both"];
|
|
3
|
+
export function registerShootCommand(program) {
|
|
4
|
+
program
|
|
5
|
+
.command("shoot")
|
|
6
|
+
.description("Render a packed bundle's declared shots to still images.\n\n" +
|
|
7
|
+
"Reads the steps' `shot` names from the running bundle, walks the\n" +
|
|
8
|
+
"timeline sequentially (a deterministic TimeSource walk — the same\n" +
|
|
9
|
+
"model as video export), and screenshots each shot's settled\n" +
|
|
10
|
+
"end-of-step frame into <bundle>/stills/<shot>.<theme>.png, one\n" +
|
|
11
|
+
"image per theme. The pack manifest is rebuilt afterwards, so\n" +
|
|
12
|
+
"`scenar publish` and deploys ship the stills with the bundle.\n\n" +
|
|
13
|
+
"Requires Playwright (optional peer): npm install -D playwright,\n" +
|
|
14
|
+
"then npx playwright install chromium. The bundle must be packed by\n" +
|
|
15
|
+
"the same CLI version (all @scenar packages release in lockstep).\n\n" +
|
|
16
|
+
"Tip: open <bundle-url>?shot=<name> in a browser to eyeball any\n" +
|
|
17
|
+
"declared shot without capturing.")
|
|
18
|
+
.argument("<dir>", "a packed bundle directory (from scenar pack)")
|
|
19
|
+
.option("--theme <theme>", `themes to capture: ${THEME_CHOICES.join(" | ")} (default: both)`, "both")
|
|
20
|
+
.option("--verify", "capture everything twice in fresh browser sessions and fail unless every still is byte-identical")
|
|
21
|
+
.option("--timeout <ms>", "per-page readiness timeout in milliseconds (default: 30000)", "30000")
|
|
22
|
+
.action(async (dir, options) => {
|
|
23
|
+
try {
|
|
24
|
+
if (!THEME_CHOICES.includes(options.theme)) {
|
|
25
|
+
throw new Error(`--theme must be one of: ${THEME_CHOICES.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
const themes = options.theme === "both" ? ["light", "dark"] : [options.theme];
|
|
28
|
+
const result = await runShoot({
|
|
29
|
+
bundleDir: dir,
|
|
30
|
+
themes,
|
|
31
|
+
verify: options.verify ?? false,
|
|
32
|
+
timeoutMs: Number(options.timeout) || undefined,
|
|
33
|
+
onLog: (message) => process.stderr.write(`${message}\n`),
|
|
34
|
+
});
|
|
35
|
+
if (result.shots.length === 0) {
|
|
36
|
+
process.stderr.write(`\n\x1b[32m✓\x1b[0m No shots declared in ${result.scenarioId} — nothing captured.\n` +
|
|
37
|
+
` Name a capture point by setting \`shot: "<kebab-name>"\` on a step.\n`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
process.stderr.write(`\n\x1b[32m✓\x1b[0m Shot ${result.files.length} stills ` +
|
|
41
|
+
`(${result.shots.length} shots x ${result.themes.length} themes)` +
|
|
42
|
+
`${result.verified ? ", byte-identical across sessions" : ""} ` +
|
|
43
|
+
`to ${result.bundleDir}/stills\n`);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
47
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=shoot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shoot.js","sourceRoot":"","sources":["../../../src/commands/shoot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AASjD,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAU,CAAC;AAEzD,MAAM,UAAU,oBAAoB,CAAC,OAAgB;IACnD,OAAO;SACJ,OAAO,CAAC,OAAO,CAAC;SAChB,WAAW,CACV,8DAA8D;QAC5D,oEAAoE;QACpE,qEAAqE;QACrE,+DAA+D;QAC/D,kEAAkE;QAClE,gEAAgE;QAChE,mEAAmE;QACnE,mEAAmE;QACnE,sEAAsE;QACtE,sEAAsE;QACtE,kEAAkE;QAClE,kCAAkC,CACrC;SACA,QAAQ,CAAC,OAAO,EAAE,8CAA8C,CAAC;SACjE,MAAM,CAAC,iBAAiB,EAAE,sBAAsB,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,kBAAkB,EAAE,MAAM,CAAC;SACpG,MAAM,CACL,UAAU,EACV,kGAAkG,CACnG;SACA,MAAM,CAAC,gBAAgB,EAAE,6DAA6D,EAAE,OAAO,CAAC;SAChG,MAAM,CAAC,KAAK,EAAE,GAAW,EAAE,OAAqB,EAAE,EAAE;QACnD,IAAI,CAAC;YACH,IAAI,CAAE,aAAmC,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBAClE,MAAM,IAAI,KAAK,CAAC,2BAA2B,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACzE,CAAC;YACD,MAAM,MAAM,GACV,OAAO,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAkB,CAAC,CAAC;YAE9E,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC;gBAC5B,SAAS,EAAE,GAAG;gBACd,MAAM;gBACN,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,KAAK;gBAC/B,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,SAAS;gBAC/C,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC;aACzD,CAAC,CAAC;YAEH,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,2CAA2C,MAAM,CAAC,UAAU,wBAAwB;oBAClF,yEAAyE,CAC5E,CAAC;gBACF,OAAO;YACT,CAAC;YACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,2BAA2B,MAAM,CAAC,KAAK,CAAC,MAAM,UAAU;gBACtD,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,YAAY,MAAM,CAAC,MAAM,CAAC,MAAM,UAAU;gBACjE,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,kCAAkC,CAAC,CAAC,CAAC,EAAE,GAAG;gBAC/D,MAAM,MAAM,CAAC,SAAS,WAAW,CACpC,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACnE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAAC;YACvD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACvB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { runShoot } from "../shoot/run-shoot.js";
|
|
3
|
+
import type { ShotTheme } from "../shoot/types.js";
|
|
4
|
+
|
|
5
|
+
interface ShootOptions {
|
|
6
|
+
theme: string;
|
|
7
|
+
verify?: boolean;
|
|
8
|
+
timeout: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const THEME_CHOICES = ["light", "dark", "both"] as const;
|
|
12
|
+
|
|
13
|
+
export function registerShootCommand(program: Command): void {
|
|
14
|
+
program
|
|
15
|
+
.command("shoot")
|
|
16
|
+
.description(
|
|
17
|
+
"Render a packed bundle's declared shots to still images.\n\n" +
|
|
18
|
+
"Reads the steps' `shot` names from the running bundle, walks the\n" +
|
|
19
|
+
"timeline sequentially (a deterministic TimeSource walk — the same\n" +
|
|
20
|
+
"model as video export), and screenshots each shot's settled\n" +
|
|
21
|
+
"end-of-step frame into <bundle>/stills/<shot>.<theme>.png, one\n" +
|
|
22
|
+
"image per theme. The pack manifest is rebuilt afterwards, so\n" +
|
|
23
|
+
"`scenar publish` and deploys ship the stills with the bundle.\n\n" +
|
|
24
|
+
"Requires Playwright (optional peer): npm install -D playwright,\n" +
|
|
25
|
+
"then npx playwright install chromium. The bundle must be packed by\n" +
|
|
26
|
+
"the same CLI version (all @scenar packages release in lockstep).\n\n" +
|
|
27
|
+
"Tip: open <bundle-url>?shot=<name> in a browser to eyeball any\n" +
|
|
28
|
+
"declared shot without capturing.",
|
|
29
|
+
)
|
|
30
|
+
.argument("<dir>", "a packed bundle directory (from scenar pack)")
|
|
31
|
+
.option("--theme <theme>", `themes to capture: ${THEME_CHOICES.join(" | ")} (default: both)`, "both")
|
|
32
|
+
.option(
|
|
33
|
+
"--verify",
|
|
34
|
+
"capture everything twice in fresh browser sessions and fail unless every still is byte-identical",
|
|
35
|
+
)
|
|
36
|
+
.option("--timeout <ms>", "per-page readiness timeout in milliseconds (default: 30000)", "30000")
|
|
37
|
+
.action(async (dir: string, options: ShootOptions) => {
|
|
38
|
+
try {
|
|
39
|
+
if (!(THEME_CHOICES as readonly string[]).includes(options.theme)) {
|
|
40
|
+
throw new Error(`--theme must be one of: ${THEME_CHOICES.join(", ")}`);
|
|
41
|
+
}
|
|
42
|
+
const themes: readonly ShotTheme[] =
|
|
43
|
+
options.theme === "both" ? ["light", "dark"] : [options.theme as ShotTheme];
|
|
44
|
+
|
|
45
|
+
const result = await runShoot({
|
|
46
|
+
bundleDir: dir,
|
|
47
|
+
themes,
|
|
48
|
+
verify: options.verify ?? false,
|
|
49
|
+
timeoutMs: Number(options.timeout) || undefined,
|
|
50
|
+
onLog: (message) => process.stderr.write(`${message}\n`),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (result.shots.length === 0) {
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
`\n\x1b[32m✓\x1b[0m No shots declared in ${result.scenarioId} — nothing captured.\n` +
|
|
56
|
+
` Name a capture point by setting \`shot: "<kebab-name>"\` on a step.\n`,
|
|
57
|
+
);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
process.stderr.write(
|
|
61
|
+
`\n\x1b[32m✓\x1b[0m Shot ${result.files.length} stills ` +
|
|
62
|
+
`(${result.shots.length} shots x ${result.themes.length} themes)` +
|
|
63
|
+
`${result.verified ? ", byte-identical across sessions" : ""} ` +
|
|
64
|
+
`to ${result.bundleDir}/stills\n`,
|
|
65
|
+
);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
68
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
package/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,aAAa,IAAI,OAAO,CAmBvC;AAED,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAGxC"}
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { registerNarrateCommand } from "./commands/narrate.js";
|
|
|
4
4
|
import { registerInstallCommand } from "./commands/install.js";
|
|
5
5
|
import { registerRenderCommand } from "./commands/render.js";
|
|
6
6
|
import { registerPackCommand } from "./commands/pack.js";
|
|
7
|
+
import { registerShootCommand } from "./commands/shoot.js";
|
|
7
8
|
import { registerServeCommand } from "./commands/serve.js";
|
|
8
9
|
import { registerTryCommand } from "./commands/try.js";
|
|
9
10
|
import { registerPublishCommand } from "./commands/publish.js";
|
|
@@ -18,6 +19,7 @@ export function createProgram() {
|
|
|
18
19
|
registerRenderCommand(program);
|
|
19
20
|
registerInstallCommand(program);
|
|
20
21
|
registerPackCommand(program);
|
|
22
|
+
registerShootCommand(program);
|
|
21
23
|
registerServeCommand(program);
|
|
22
24
|
registerTryCommand(program);
|
|
23
25
|
registerPublishCommand(program);
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,MAAM,UAAU,aAAa;IAC3B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,OAAO;SACJ,IAAI,CAAC,QAAQ,CAAC;SACd,WAAW,CAAC,+FAA+F,CAAC;SAC5G,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpB,uBAAuB,CAAC,OAAO,CAAC,CAAC;IACjC,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAChC,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAChC,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC7B,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC5B,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAEhC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,IAAc;IAChC,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,MAAM,UAAU,aAAa;IAC3B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,OAAO;SACJ,IAAI,CAAC,QAAQ,CAAC;SACd,WAAW,CAAC,+FAA+F,CAAC;SAC5G,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpB,uBAAuB,CAAC,OAAO,CAAC,CAAC;IACjC,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAChC,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAChC,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC7B,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC5B,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAEhC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,IAAc;IAChC,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC"}
|