@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,270 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
PACK_MANIFEST_FILE,
|
|
5
|
+
SCENARIO_JSON_FILE,
|
|
6
|
+
buildPackManifest,
|
|
7
|
+
verifyManifestFilesExist,
|
|
8
|
+
writePackManifest,
|
|
9
|
+
} from "../pack/pack-manifest.js";
|
|
10
|
+
import { readBundleViewport } from "../bundle/read-viewport.js";
|
|
11
|
+
import { readBundleShots } from "../bundle/read-shots.js";
|
|
12
|
+
import { startBundleServer } from "../serve/static-server.js";
|
|
13
|
+
import { createPlaywrightShotBrowser } from "./playwright-browser.js";
|
|
14
|
+
import type { ShotBrowser, ShotCaptureInfo, ShotSession, ShotTheme } from "./types.js";
|
|
15
|
+
|
|
16
|
+
/** Directory inside the bundle where stills land (and deploy from). */
|
|
17
|
+
export const STILLS_DIR = "stills";
|
|
18
|
+
|
|
19
|
+
/** How long to wait for the capture page's driver before failing. */
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extra room around the canonical frame so the element screenshot never has
|
|
24
|
+
* to scroll-stitch (stitching re-rasterizes and would break byte-exactness).
|
|
25
|
+
*/
|
|
26
|
+
const VIEWPORT_MARGIN_PX = 64;
|
|
27
|
+
|
|
28
|
+
/** Options for {@link runShoot}. Paths may be relative; they are resolved here. */
|
|
29
|
+
export interface RunShootOptions {
|
|
30
|
+
/** A packed bundle directory (a `scenar pack` output with a ?shot entry). */
|
|
31
|
+
readonly bundleDir: string;
|
|
32
|
+
/** Themes to capture (default: both — DD-02 D2). */
|
|
33
|
+
readonly themes?: readonly ShotTheme[];
|
|
34
|
+
/**
|
|
35
|
+
* Capture everything twice in fresh browser sessions and byte-compare —
|
|
36
|
+
* the determinism gate. A mismatch means some component renders
|
|
37
|
+
* nondeterministically and the stills cannot be trusted.
|
|
38
|
+
*/
|
|
39
|
+
readonly verify?: boolean;
|
|
40
|
+
/** Per-page readiness timeout in ms (default {@link DEFAULT_TIMEOUT_MS}). */
|
|
41
|
+
readonly timeoutMs?: number;
|
|
42
|
+
/** Progress sink for mid-operation messages. */
|
|
43
|
+
readonly onLog?: (message: string) => void;
|
|
44
|
+
/** Test seam: swap the real Playwright browser for a fake. */
|
|
45
|
+
readonly browserFactory?: () => Promise<ShotBrowser>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The outcome of a successful shoot. */
|
|
49
|
+
export interface ShootResult {
|
|
50
|
+
readonly scenarioId: string;
|
|
51
|
+
readonly bundleDir: string;
|
|
52
|
+
/** The bundle's declared shots, in timeline order (empty = nothing to do). */
|
|
53
|
+
readonly shots: ShotCaptureInfo["shots"];
|
|
54
|
+
/** Bundle-relative paths of the stills written, in write order. */
|
|
55
|
+
readonly files: readonly string[];
|
|
56
|
+
readonly themes: readonly ShotTheme[];
|
|
57
|
+
/** True when `verify` ran and every still was byte-identical. */
|
|
58
|
+
readonly verified: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Render a packed bundle's declared shots to still images — the pure
|
|
63
|
+
* orchestration behind `scenar shoot`, with no process/exit coupling so both
|
|
64
|
+
* the CLI command and the MCP server can call it.
|
|
65
|
+
*
|
|
66
|
+
* Bundle-centric by design: stills are captured from (and written into) the
|
|
67
|
+
* exact bundle that deploys, so they can never depict anything but what
|
|
68
|
+
* ships. The pack manifest is rebuilt afterwards so `publish`/deploy pick
|
|
69
|
+
* the stills up with zero pipeline changes.
|
|
70
|
+
*
|
|
71
|
+
* `stills/` is cleared up front, unconditionally — even when the scenario
|
|
72
|
+
* declares no shots — and the manifest is rebuilt in every path. A removed
|
|
73
|
+
* or renamed shot must never linger in a deployed bundle, and a manifest
|
|
74
|
+
* listing deleted files would fail its own existence check at the next
|
|
75
|
+
* publish.
|
|
76
|
+
*
|
|
77
|
+
* When scenario.json records an authoritatively empty shot list (pack ≥
|
|
78
|
+
* generator 0.0.2), the whole server + browser boot is skipped — both
|
|
79
|
+
* invariants above still hold on that path. A recorded non-empty list does
|
|
80
|
+
* NOT bypass the capture page: the running bundle stays the runtime truth
|
|
81
|
+
* for what gets shot; the record only proves there is something to boot for.
|
|
82
|
+
*/
|
|
83
|
+
export async function runShoot(options: RunShootOptions): Promise<ShootResult> {
|
|
84
|
+
const onLog = options.onLog ?? (() => {});
|
|
85
|
+
const bundleDir = resolve(options.bundleDir);
|
|
86
|
+
const themes = options.themes ?? (["light", "dark"] as const);
|
|
87
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
88
|
+
|
|
89
|
+
const scenarioId = await validateBundle(bundleDir);
|
|
90
|
+
const { viewport } = await readBundleViewport(bundleDir);
|
|
91
|
+
|
|
92
|
+
onLog(`Scenario: ${scenarioId}`);
|
|
93
|
+
onLog(`Bundle: ${bundleDir}`);
|
|
94
|
+
onLog(`Viewport: ${viewport.width}x${viewport.height} (DPR 2)`);
|
|
95
|
+
onLog(`Themes: ${themes.join(", ")}`);
|
|
96
|
+
|
|
97
|
+
await rm(join(bundleDir, STILLS_DIR), { recursive: true, force: true });
|
|
98
|
+
|
|
99
|
+
// Short-circuit on pack's authority: a recorded-and-empty shot list means
|
|
100
|
+
// there is provably nothing to capture, so don't pay for a static server
|
|
101
|
+
// and a Chromium launch just to learn that. Absent record = unknown — fall
|
|
102
|
+
// through and let the capture page answer, exactly as before the record
|
|
103
|
+
// existed.
|
|
104
|
+
const recordedShots = await readBundleShots(bundleDir);
|
|
105
|
+
if (recordedShots.recorded && recordedShots.shots.length === 0) {
|
|
106
|
+
onLog("No steps declare a `shot` — nothing to capture.");
|
|
107
|
+
await rebuildManifest(bundleDir, scenarioId);
|
|
108
|
+
return { scenarioId, bundleDir, shots: [], files: [], themes, verified: false };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const server = await startBundleServer({ rootDir: bundleDir, port: 0 });
|
|
112
|
+
let browser: ShotBrowser | undefined;
|
|
113
|
+
try {
|
|
114
|
+
browser = await (options.browserFactory ?? createPlaywrightShotBrowser)();
|
|
115
|
+
|
|
116
|
+
const sessionViewport = {
|
|
117
|
+
width: viewport.width + VIEWPORT_MARGIN_PX,
|
|
118
|
+
height: viewport.height + VIEWPORT_MARGIN_PX,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const captures = new Map<ShotTheme, { info: ShotCaptureInfo; images: Buffer[] }>();
|
|
122
|
+
for (const theme of themes) {
|
|
123
|
+
captures.set(theme, await captureTheme(browser, server.url, theme, sessionViewport, timeoutMs));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const first = captures.get(themes[0]!)!.info;
|
|
127
|
+
assertShotsAgreeAcrossThemes(captures);
|
|
128
|
+
|
|
129
|
+
if (first.shots.length === 0) {
|
|
130
|
+
onLog("No steps declare a `shot` — nothing to capture.");
|
|
131
|
+
await rebuildManifest(bundleDir, scenarioId);
|
|
132
|
+
return { scenarioId, bundleDir, shots: [], files: [], themes, verified: false };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (options.verify) {
|
|
136
|
+
onLog("Verifying determinism (second capture in fresh sessions)...");
|
|
137
|
+
for (const theme of themes) {
|
|
138
|
+
const rerun = await captureTheme(browser, server.url, theme, sessionViewport, timeoutMs);
|
|
139
|
+
assertByteIdentical(theme, captures.get(theme)!, rerun);
|
|
140
|
+
}
|
|
141
|
+
onLog(`Determinism verified: every still byte-identical across sessions.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await mkdir(join(bundleDir, STILLS_DIR), { recursive: true });
|
|
145
|
+
const files: string[] = [];
|
|
146
|
+
for (const theme of themes) {
|
|
147
|
+
const { info, images } = captures.get(theme)!;
|
|
148
|
+
for (const [i, shot] of info.shots.entries()) {
|
|
149
|
+
const relative = `${STILLS_DIR}/${shot.name}.${theme}.png`;
|
|
150
|
+
await writeFile(join(bundleDir, ...relative.split("/")), images[i]!);
|
|
151
|
+
files.push(relative);
|
|
152
|
+
onLog(`Captured: ${relative}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await rebuildManifest(bundleDir, scenarioId);
|
|
157
|
+
return {
|
|
158
|
+
scenarioId,
|
|
159
|
+
bundleDir,
|
|
160
|
+
shots: first.shots,
|
|
161
|
+
files,
|
|
162
|
+
themes,
|
|
163
|
+
verified: options.verify ?? false,
|
|
164
|
+
};
|
|
165
|
+
} finally {
|
|
166
|
+
await browser?.close().catch(() => {});
|
|
167
|
+
await server.close().catch(() => {});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Walk one themed session through every shot, collecting screenshots. */
|
|
172
|
+
async function captureTheme(
|
|
173
|
+
browser: ShotBrowser,
|
|
174
|
+
baseUrl: string,
|
|
175
|
+
theme: ShotTheme,
|
|
176
|
+
viewport: { width: number; height: number },
|
|
177
|
+
timeoutMs: number,
|
|
178
|
+
): Promise<{ info: ShotCaptureInfo; images: Buffer[] }> {
|
|
179
|
+
// `?shot` (bare) switches the packed entry into capture mode; the scenar
|
|
180
|
+
// token class needs `theme=dark` on top of the context's colorScheme
|
|
181
|
+
// emulation — both halves of a dark variant (DD-02 D2).
|
|
182
|
+
const url = `${baseUrl}?shot${theme === "dark" ? "&theme=dark" : ""}`;
|
|
183
|
+
const session: ShotSession = await browser.newSession({ theme, viewport });
|
|
184
|
+
try {
|
|
185
|
+
const info = await session.open(url, timeoutMs);
|
|
186
|
+
const images: Buffer[] = [];
|
|
187
|
+
// Sequential walk in timeline order — never a cold jump (cross-step
|
|
188
|
+
// React state does not survive one; spike-proven, DD-02).
|
|
189
|
+
for (const shot of info.shots) {
|
|
190
|
+
await session.walkTo(shot.timeMs);
|
|
191
|
+
images.push(await session.screenshotFrame());
|
|
192
|
+
}
|
|
193
|
+
return { info, images };
|
|
194
|
+
} finally {
|
|
195
|
+
await session.close().catch(() => {});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** A bundle is shootable when it has the three files `scenar pack` writes. */
|
|
200
|
+
async function validateBundle(bundleDir: string): Promise<string> {
|
|
201
|
+
const info = await stat(bundleDir).catch(() => null);
|
|
202
|
+
if (!info || !info.isDirectory()) {
|
|
203
|
+
throw new Error(`${bundleDir} is not a directory. Pass a bundle produced by \`scenar pack\`.`);
|
|
204
|
+
}
|
|
205
|
+
for (const required of ["index.html", SCENARIO_JSON_FILE, PACK_MANIFEST_FILE]) {
|
|
206
|
+
const fileInfo = await stat(join(bundleDir, required)).catch(() => null);
|
|
207
|
+
if (!fileInfo || !fileInfo.isFile()) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`no ${required} in ${bundleDir}. Run \`scenar pack\` to produce a shootable bundle first.`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const scenarioJson = JSON.parse(
|
|
214
|
+
await readFile(join(bundleDir, SCENARIO_JSON_FILE), "utf-8"),
|
|
215
|
+
) as { id?: unknown };
|
|
216
|
+
if (typeof scenarioJson.id !== "string" || scenarioJson.id.length === 0) {
|
|
217
|
+
throw new Error(`${SCENARIO_JSON_FILE} in ${bundleDir} has no scenario id.`);
|
|
218
|
+
}
|
|
219
|
+
return scenarioJson.id;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The shot list is derived from the bundled steps, so every theme must
|
|
224
|
+
* report the identical list — a divergence means the bundle itself renders
|
|
225
|
+
* nondeterministically and nothing downstream can be trusted.
|
|
226
|
+
*/
|
|
227
|
+
function assertShotsAgreeAcrossThemes(
|
|
228
|
+
captures: Map<ShotTheme, { info: ShotCaptureInfo; images: Buffer[] }>,
|
|
229
|
+
): void {
|
|
230
|
+
const lists = [...captures.entries()].map(
|
|
231
|
+
([theme, { info }]) => [theme, JSON.stringify(info.shots)] as const,
|
|
232
|
+
);
|
|
233
|
+
const [, reference] = lists[0]!;
|
|
234
|
+
for (const [theme, list] of lists) {
|
|
235
|
+
if (list !== reference) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`internal error: the ${theme} capture reported a different shot list than ${lists[0]![0]}`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Byte-compare two capture passes of the same theme (the `--verify` gate). */
|
|
244
|
+
function assertByteIdentical(
|
|
245
|
+
theme: ShotTheme,
|
|
246
|
+
first: { info: ShotCaptureInfo; images: Buffer[] },
|
|
247
|
+
second: { info: ShotCaptureInfo; images: Buffer[] },
|
|
248
|
+
): void {
|
|
249
|
+
const differing = first.info.shots
|
|
250
|
+
.filter((_, i) => !first.images[i]!.equals(second.images[i]!))
|
|
251
|
+
.map((shot) => shot.name);
|
|
252
|
+
if (differing.length > 0) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`determinism check failed (${theme}): shot(s) ${differing.join(", ")} differed ` +
|
|
255
|
+
`across fresh sessions. Some component renders nondeterministically — ` +
|
|
256
|
+
`fix that before trusting these stills (see DD-02's determinism recipe).`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Re-list the bundle after stills changed. `buildPackManifest` validates
|
|
263
|
+
* every file against the deploy allowlist, so a bad shot filename fails
|
|
264
|
+
* here, locally, rather than mid-upload.
|
|
265
|
+
*/
|
|
266
|
+
async function rebuildManifest(bundleDir: string, scenarioId: string): Promise<void> {
|
|
267
|
+
const manifest = await buildPackManifest(bundleDir, scenarioId);
|
|
268
|
+
await verifyManifestFilesExist(bundleDir, manifest);
|
|
269
|
+
await writePackManifest(bundleDir, manifest);
|
|
270
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ScenarioShot } from "@scenar/core";
|
|
2
|
+
/** The two capture themes — every shot is rendered once per theme (DD-02 D2). */
|
|
3
|
+
export type ShotTheme = "light" | "dark";
|
|
4
|
+
/** What the capture page reports once its driver is ready. */
|
|
5
|
+
export interface ShotCaptureInfo {
|
|
6
|
+
/** The bundle's declared shots, validated and in timeline order. */
|
|
7
|
+
readonly shots: readonly ScenarioShot[];
|
|
8
|
+
/** Selector for the element to screenshot (owned by the capture mount). */
|
|
9
|
+
readonly frameSelector: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One themed browser page driving a capture — the narrow seam between
|
|
13
|
+
* run-shoot's orchestration (unit-tested against a fake) and Playwright
|
|
14
|
+
* (exercised by the real end-to-end capture, not by unit tests).
|
|
15
|
+
*/
|
|
16
|
+
export interface ShotSession {
|
|
17
|
+
/**
|
|
18
|
+
* Navigate to the capture URL and wait for the page to report either a
|
|
19
|
+
* ready driver or an error (`window.__scenarShot` / `__scenarShotError`).
|
|
20
|
+
* Throws with the page's own message on a reported error or on timeout.
|
|
21
|
+
*/
|
|
22
|
+
open(url: string, timeoutMs: number): Promise<ShotCaptureInfo>;
|
|
23
|
+
/** Advance the page's TimeSource walk to `timeMs` (sequential, never back). */
|
|
24
|
+
walkTo(timeMs: number): Promise<void>;
|
|
25
|
+
/** Screenshot the capture frame, animations disabled. */
|
|
26
|
+
screenshotFrame(): Promise<Buffer>;
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/** A launched capture browser that can open themed sessions. */
|
|
30
|
+
export interface ShotBrowser {
|
|
31
|
+
newSession(options: {
|
|
32
|
+
theme: ShotTheme;
|
|
33
|
+
viewport: {
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
};
|
|
37
|
+
}): Promise<ShotSession>;
|
|
38
|
+
close(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/shoot/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,iFAAiF;AACjF,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEzC,8DAA8D;AAC9D,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IACxC,2EAA2E;IAC3E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC/D,+EAA+E;IAC/E,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,yDAAyD;IACzD,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,gEAAgE;AAChE,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,OAAO,EAAE;QAClB,KAAK,EAAE,SAAS,CAAC;QACjB,QAAQ,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;KAC7C,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/shoot/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ScenarioShot } from "@scenar/core";
|
|
2
|
+
|
|
3
|
+
/** The two capture themes — every shot is rendered once per theme (DD-02 D2). */
|
|
4
|
+
export type ShotTheme = "light" | "dark";
|
|
5
|
+
|
|
6
|
+
/** What the capture page reports once its driver is ready. */
|
|
7
|
+
export interface ShotCaptureInfo {
|
|
8
|
+
/** The bundle's declared shots, validated and in timeline order. */
|
|
9
|
+
readonly shots: readonly ScenarioShot[];
|
|
10
|
+
/** Selector for the element to screenshot (owned by the capture mount). */
|
|
11
|
+
readonly frameSelector: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One themed browser page driving a capture — the narrow seam between
|
|
16
|
+
* run-shoot's orchestration (unit-tested against a fake) and Playwright
|
|
17
|
+
* (exercised by the real end-to-end capture, not by unit tests).
|
|
18
|
+
*/
|
|
19
|
+
export interface ShotSession {
|
|
20
|
+
/**
|
|
21
|
+
* Navigate to the capture URL and wait for the page to report either a
|
|
22
|
+
* ready driver or an error (`window.__scenarShot` / `__scenarShotError`).
|
|
23
|
+
* Throws with the page's own message on a reported error or on timeout.
|
|
24
|
+
*/
|
|
25
|
+
open(url: string, timeoutMs: number): Promise<ShotCaptureInfo>;
|
|
26
|
+
/** Advance the page's TimeSource walk to `timeMs` (sequential, never back). */
|
|
27
|
+
walkTo(timeMs: number): Promise<void>;
|
|
28
|
+
/** Screenshot the capture frame, animations disabled. */
|
|
29
|
+
screenshotFrame(): Promise<Buffer>;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A launched capture browser that can open themed sessions. */
|
|
34
|
+
export interface ShotBrowser {
|
|
35
|
+
newSession(options: {
|
|
36
|
+
theme: ShotTheme;
|
|
37
|
+
viewport: { width: number; height: number };
|
|
38
|
+
}): Promise<ShotSession>;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
}
|
package/src/util/load-ts.d.ts
CHANGED
|
@@ -7,10 +7,20 @@ export interface ImportedStep {
|
|
|
7
7
|
delayMs: number;
|
|
8
8
|
narration?: string;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Find the steps array in a loaded module's exports by duck-typing: the
|
|
12
|
+
* first exported array whose first element has a `delayMs` property.
|
|
13
|
+
* Returns null when the module exports no such array.
|
|
14
|
+
*
|
|
15
|
+
* This is THE steps-discovery rule, shared by every Node-side loader and
|
|
16
|
+
* mirrored verbatim by the browser-side `_findSteps` in the generated
|
|
17
|
+
* pack entry (see generate-embed-entry.ts) — the mirrors must agree, or
|
|
18
|
+
* pack-time tooling would see different steps than the packed bundle.
|
|
19
|
+
*/
|
|
20
|
+
export declare function findStepsArray(exports: Record<string, unknown>): ImportedStep[] | null;
|
|
10
21
|
/**
|
|
11
22
|
* Dynamically import a TypeScript steps file and extract the
|
|
12
|
-
* steps array by duck-typing (
|
|
13
|
-
* whose elements have a `delayMs` property).
|
|
23
|
+
* steps array by duck-typing ({@link findStepsArray}).
|
|
14
24
|
*
|
|
15
25
|
* Requires the caller's Node process to have a TypeScript loader
|
|
16
26
|
* active (e.g. running via `tsx`). The CLI itself does not depend
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-ts.d.ts","sourceRoot":"","sources":["../../../src/util/load-ts.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED
|
|
1
|
+
{"version":3,"file":"load-ts.d.ts","sourceRoot":"","sources":["../../../src/util/load-ts.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY,EAAE,GAAG,IAAI,CAatF;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAO/E"}
|
package/src/util/load-ts.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { pathToFileURL } from "node:url";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Find the steps array in a loaded module's exports by duck-typing: the
|
|
4
|
+
* first exported array whose first element has a `delayMs` property.
|
|
5
|
+
* Returns null when the module exports no such array.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* This is THE steps-discovery rule, shared by every Node-side loader and
|
|
8
|
+
* mirrored verbatim by the browser-side `_findSteps` in the generated
|
|
9
|
+
* pack entry (see generate-embed-entry.ts) — the mirrors must agree, or
|
|
10
|
+
* pack-time tooling would see different steps than the packed bundle.
|
|
10
11
|
*/
|
|
11
|
-
export
|
|
12
|
-
const mod = await import(pathToFileURL(filePath).href);
|
|
13
|
-
const exports = mod.default ?? mod;
|
|
12
|
+
export function findStepsArray(exports) {
|
|
14
13
|
for (const value of Object.values(exports)) {
|
|
15
14
|
if (Array.isArray(value) &&
|
|
16
15
|
value.length > 0 &&
|
|
@@ -20,6 +19,22 @@ export async function loadStepsFromTs(filePath) {
|
|
|
20
19
|
return value;
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
|
-
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Dynamically import a TypeScript steps file and extract the
|
|
26
|
+
* steps array by duck-typing ({@link findStepsArray}).
|
|
27
|
+
*
|
|
28
|
+
* Requires the caller's Node process to have a TypeScript loader
|
|
29
|
+
* active (e.g. running via `tsx`). The CLI itself does not depend
|
|
30
|
+
* on any TS compilation tool.
|
|
31
|
+
*/
|
|
32
|
+
export async function loadStepsFromTs(filePath) {
|
|
33
|
+
const mod = await import(pathToFileURL(filePath).href);
|
|
34
|
+
const steps = findStepsArray(mod.default ?? mod);
|
|
35
|
+
if (steps === null) {
|
|
36
|
+
throw new Error(`No steps array found in ${filePath}`);
|
|
37
|
+
}
|
|
38
|
+
return steps;
|
|
24
39
|
}
|
|
25
40
|
//# sourceMappingURL=load-ts.js.map
|
package/src/util/load-ts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-ts.js","sourceRoot":"","sources":["../../../src/util/load-ts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAYzC
|
|
1
|
+
{"version":3,"file":"load-ts.js","sourceRoot":"","sources":["../../../src/util/load-ts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAYzC;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgC;IAC7D,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3C,IACE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YACpB,KAAK,CAAC,MAAM,GAAG,CAAC;YAChB,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ;YAC5B,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;YACjB,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,EACrB,CAAC;YACD,OAAO,KAAuB,CAAC;QACjC,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAgB;IACpD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,CAAC;IACjD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,EAAE,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/src/util/load-ts.ts
CHANGED
|
@@ -11,18 +11,16 @@ export interface ImportedStep {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Find the steps array in a loaded module's exports by duck-typing: the
|
|
15
|
+
* first exported array whose first element has a `delayMs` property.
|
|
16
|
+
* Returns null when the module exports no such array.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* This is THE steps-discovery rule, shared by every Node-side loader and
|
|
19
|
+
* mirrored verbatim by the browser-side `_findSteps` in the generated
|
|
20
|
+
* pack entry (see generate-embed-entry.ts) — the mirrors must agree, or
|
|
21
|
+
* pack-time tooling would see different steps than the packed bundle.
|
|
21
22
|
*/
|
|
22
|
-
export
|
|
23
|
-
const mod = await import(pathToFileURL(filePath).href);
|
|
24
|
-
const exports = mod.default ?? mod;
|
|
25
|
-
|
|
23
|
+
export function findStepsArray(exports: Record<string, unknown>): ImportedStep[] | null {
|
|
26
24
|
for (const value of Object.values(exports)) {
|
|
27
25
|
if (
|
|
28
26
|
Array.isArray(value) &&
|
|
@@ -34,6 +32,22 @@ export async function loadStepsFromTs(filePath: string): Promise<ImportedStep[]>
|
|
|
34
32
|
return value as ImportedStep[];
|
|
35
33
|
}
|
|
36
34
|
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Dynamically import a TypeScript steps file and extract the
|
|
40
|
+
* steps array by duck-typing ({@link findStepsArray}).
|
|
41
|
+
*
|
|
42
|
+
* Requires the caller's Node process to have a TypeScript loader
|
|
43
|
+
* active (e.g. running via `tsx`). The CLI itself does not depend
|
|
44
|
+
* on any TS compilation tool.
|
|
45
|
+
*/
|
|
46
|
+
export async function loadStepsFromTs(filePath: string): Promise<ImportedStep[]> {
|
|
47
|
+
const mod = await import(pathToFileURL(filePath).href);
|
|
48
|
+
const steps = findStepsArray(mod.default ?? mod);
|
|
49
|
+
if (steps === null) {
|
|
50
|
+
throw new Error(`No steps array found in ${filePath}`);
|
|
51
|
+
}
|
|
52
|
+
return steps;
|
|
39
53
|
}
|