@tileflow/capture 0.1.0-alpha.29 → 0.1.0-alpha.31
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/README.md +70 -2
- package/dist/{chunk-VNDAYGXY.js → chunk-BI5PWIH4.js} +255 -1
- package/dist/index.d.ts +75 -12
- package/dist/index.js +1005 -92
- package/dist/{receipt-DhFOATRO.d.ts → receipt-CfZkzyXd.d.ts} +92 -1
- package/dist/receipt.d.ts +1 -1
- package/dist/receipt.js +15 -3
- package/docs/receipts-and-comparison.md +8 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @tileflow/capture
|
|
2
2
|
|
|
3
|
+
For the complete CLI workflow, use `describe`, `inspect`, `capture`, `compare`, and Map DevTools (`preview`)
|
|
4
|
+
as described in the [CLI guide](https://github.com/tileflow/tileflow-sdk/blob/main/packages/cli/README.md).
|
|
5
|
+
New still captures include their normalized `definition` for later review. The CLI stores immutable
|
|
6
|
+
capture IDs so `compare --before <id> --after <id>` can reuse evidence without rendering again.
|
|
7
|
+
|
|
3
8
|
Headless Tileflow map capture for Node.js. Produce PNGs with versioned receipts, compare reviewed
|
|
4
9
|
baselines, or inspect differences between deliberate renders. Use the
|
|
5
10
|
[CLI](https://github.com/tileflow/tileflow-sdk/blob/main/packages/cli/README.md) for ordinary terminal
|
|
@@ -106,6 +111,66 @@ The equivalent CLI command is `tileflow setup capture`. These settings control b
|
|
|
106
111
|
not the network access required by a map's remote resources. Tileflow never deletes the shared
|
|
107
112
|
per-user browser cache.
|
|
108
113
|
|
|
114
|
+
## Render a timed shot
|
|
115
|
+
|
|
116
|
+
A [capture shot](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/capture-shots.md)
|
|
117
|
+
is a committed camera move with keyframes and an optional theme blend. This module assumes a
|
|
118
|
+
`dusk-flyover` shot in `tileflow.config.ts` and writes its WebM video and receipt:
|
|
119
|
+
|
|
120
|
+
<!-- docs:check -->
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import {mkdir, writeFile} from 'node:fs/promises';
|
|
124
|
+
import {captureTileflowShot, serializeTileflowShotReceipt} from '@tileflow/capture';
|
|
125
|
+
|
|
126
|
+
const shot = await captureTileflowShot({shot: 'dusk-flyover', allowBrowserInstall: true});
|
|
127
|
+
if (!shot.video) throw new Error('The shot produced no video.');
|
|
128
|
+
|
|
129
|
+
await mkdir('.tileflow/captures/shots', {recursive: true});
|
|
130
|
+
await writeFile('.tileflow/captures/shots/dusk-flyover.webm', shot.video);
|
|
131
|
+
await writeFile(
|
|
132
|
+
'.tileflow/captures/shots/dusk-flyover.receipt.json',
|
|
133
|
+
serializeTileflowShotReceipt(shot.receipt),
|
|
134
|
+
);
|
|
135
|
+
console.log(shot.frameCount, shot.codec, shot.framesSha256);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Every frame moves the camera with MapLibre `jumpTo`, moves the blend position, and waits for
|
|
139
|
+
MapLibre `idle` and two animation frames before its screenshot, exactly as a scene does. A slow
|
|
140
|
+
machine or network makes the render take longer; the video keeps its frame rate and shows every
|
|
141
|
+
frame. `timeoutMs` bounds page setup and each frame separately.
|
|
142
|
+
|
|
143
|
+
The pinned browser encodes the captured PNGs with WebCodecs: VP9, or VP8 where VP9 is unavailable.
|
|
144
|
+
A browser that supports neither fails before the first frame with `VIDEO_ENCODER_UNAVAILABLE`.
|
|
145
|
+
Use `format: 'frames'` to receive the PNG frames through `onFrame` instead of a video; frames are
|
|
146
|
+
delivered in order and are not kept in memory:
|
|
147
|
+
|
|
148
|
+
<!-- docs:check -->
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import {mkdir, writeFile} from 'node:fs/promises';
|
|
152
|
+
import {createTileflowCaptureSession} from '@tileflow/capture';
|
|
153
|
+
|
|
154
|
+
await mkdir('frames', {recursive: true});
|
|
155
|
+
const session = createTileflowCaptureSession({allowBrowserInstall: true});
|
|
156
|
+
try {
|
|
157
|
+
const shot = await session.captureShot('dusk-flyover', {
|
|
158
|
+
format: 'frames',
|
|
159
|
+
onFrame: (frame) =>
|
|
160
|
+
writeFile(`frames/frame-${String(frame.index).padStart(4, '0')}.png`, frame.png),
|
|
161
|
+
});
|
|
162
|
+
console.log(shot.frameCount, shot.framesSha256);
|
|
163
|
+
} finally {
|
|
164
|
+
await session.close();
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`captureShots` and `captureAllShots` render several shots from one compiled snapshot. A shot
|
|
169
|
+
receipt records the shot, theme and blend styles, timing, dimensions, renderer, data identity, and
|
|
170
|
+
`frames.sha256`, a digest of every frame's PNG SHA-256. Frames are reproducible under the same
|
|
171
|
+
conditions as scene captures; encoder bytes may differ between runs, so `output.sha256` identifies
|
|
172
|
+
the written video while `frames.sha256` identifies its pixels.
|
|
173
|
+
|
|
109
174
|
## Capture the running application
|
|
110
175
|
|
|
111
176
|
Application capture uses the app's normal loopback server; it does not start a server or discover a
|
|
@@ -183,7 +248,9 @@ the perceptual metric and optional diff image are additional evidence. Never upd
|
|
|
183
248
|
merely to make a failing comparison pass.
|
|
184
249
|
|
|
185
250
|
`compareTileflowCapturesForReview` compares two deliberate renders with different styles while
|
|
186
|
-
requiring compatible framing, renderer, and data identity.
|
|
251
|
+
requiring compatible framing, renderer, and data identity. It accepts captures with or without a
|
|
252
|
+
retained `definition`; when present, that definition must match the explicit review definition.
|
|
253
|
+
`analyzeTileflowCaptureReference` analyzes
|
|
187
254
|
an external PNG without claiming receipt compatibility. Neither operation approves a baseline.
|
|
188
255
|
See the [receipt and comparison reference](https://github.com/tileflow/tileflow-sdk/blob/main/packages/capture/docs/receipts-and-comparison.md),
|
|
189
256
|
also included under `docs/` in the installed package, for identities, metrics, and limits.
|
|
@@ -195,7 +262,8 @@ validation, browser startup, resource loading, MapLibre readiness, and screensho
|
|
|
195
262
|
Resource diagnostics contain safe classification and origin/status information, not credentials,
|
|
196
263
|
query strings, response bodies, raw browser stacks, or DOM contents.
|
|
197
264
|
|
|
198
|
-
Receipts use schema version 4; parsing rejects any other version.
|
|
265
|
+
Receipts use schema version 4; parsing rejects any other version. Shot receipts use their own
|
|
266
|
+
schema version 1. Import
|
|
199
267
|
`@tileflow/capture/receipt` for receipt-only tooling without loading the capture runtime. World selectors resolve once per capture session to
|
|
200
268
|
an immutable release. Capture retains the complete resolved TileJSON, including its zoom range,
|
|
201
269
|
bounds, attribution and vector-layer metadata. Above the data's maximum zoom, MapLibre enlarges
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
serializeCanonicalJson,
|
|
7
7
|
tileflowCaptureSceneLimits,
|
|
8
8
|
tileflowCaptureSceneNameSchema,
|
|
9
|
+
tileflowCaptureShotLimits,
|
|
9
10
|
tileflowPortableIdSchema,
|
|
10
11
|
tileflowThemeNameSchema
|
|
11
12
|
} from "@tileflow/core";
|
|
@@ -231,6 +232,219 @@ function validateTileflowCaptureReceipt(value) {
|
|
|
231
232
|
assertCanonicalReceiptByteLimit(normalized);
|
|
232
233
|
return normalized;
|
|
233
234
|
}
|
|
235
|
+
var tileflowShotReceiptSchemaVersion = 1;
|
|
236
|
+
var tileflowShotReceiptLimits = Object.freeze({ maximumBytes: 64 * 1024 });
|
|
237
|
+
function createTileflowShotReceipt(input) {
|
|
238
|
+
return validateTileflowShotReceipt({
|
|
239
|
+
schemaVersion: tileflowShotReceiptSchemaVersion,
|
|
240
|
+
kind: "shot",
|
|
241
|
+
shot: { name: input.shot, map: input.map, theme: input.theme, sha256: input.shotSha256 },
|
|
242
|
+
style: { sha256: input.styleSha256 },
|
|
243
|
+
...input.blend ? { blend: { themes: input.blend } } : {},
|
|
244
|
+
timing: { fps: input.fps, frameCount: input.frameCount, durationMs: input.durationMs },
|
|
245
|
+
image: {
|
|
246
|
+
cssWidth: input.width,
|
|
247
|
+
cssHeight: input.height,
|
|
248
|
+
physicalWidth: input.width * input.dpr,
|
|
249
|
+
physicalHeight: input.height * input.dpr,
|
|
250
|
+
dpr: input.dpr
|
|
251
|
+
},
|
|
252
|
+
frames: { sha256: input.framesSha256 },
|
|
253
|
+
output: input.output,
|
|
254
|
+
renderer: input.renderer,
|
|
255
|
+
platform: { os: process.platform, architecture: process.arch },
|
|
256
|
+
data: normalizeCaptureDataInput(input.data),
|
|
257
|
+
verification: { data: "rendered", style: "rendered" },
|
|
258
|
+
networkDependent: input.networkDependent
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function serializeTileflowShotReceipt(receipt) {
|
|
262
|
+
const serialized = `${serializeCanonicalJson(validateTileflowShotReceipt(receipt))}
|
|
263
|
+
`;
|
|
264
|
+
if (new TextEncoder().encode(serialized).byteLength > tileflowShotReceiptLimits.maximumBytes) {
|
|
265
|
+
throw invalidReceipt("The shot receipt exceeds the supported byte limit.");
|
|
266
|
+
}
|
|
267
|
+
return serialized;
|
|
268
|
+
}
|
|
269
|
+
function parseTileflowShotReceipt(source) {
|
|
270
|
+
const byteLength = typeof source === "string" ? new TextEncoder().encode(source).byteLength : source.byteLength;
|
|
271
|
+
if (byteLength > tileflowShotReceiptLimits.maximumBytes) {
|
|
272
|
+
throw invalidReceipt("The shot receipt exceeds the supported byte limit.");
|
|
273
|
+
}
|
|
274
|
+
let value;
|
|
275
|
+
let text;
|
|
276
|
+
try {
|
|
277
|
+
text = typeof source === "string" ? source : new TextDecoder("utf-8", { fatal: true }).decode(source);
|
|
278
|
+
value = JSON.parse(text);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw invalidReceipt("The shot receipt is not valid UTF-8 JSON.", error);
|
|
281
|
+
}
|
|
282
|
+
const receipt = validateTileflowShotReceipt(value);
|
|
283
|
+
if (text.trim() !== serializeCanonicalJson(value)) {
|
|
284
|
+
throw invalidReceipt("The shot receipt is not canonical JSON.");
|
|
285
|
+
}
|
|
286
|
+
return receipt;
|
|
287
|
+
}
|
|
288
|
+
function validateTileflowShotReceipt(value) {
|
|
289
|
+
assertPlainReceiptData(value, "receipt", {
|
|
290
|
+
allowArrays: true,
|
|
291
|
+
ancestors: /* @__PURE__ */ new WeakSet(),
|
|
292
|
+
nodes: 0
|
|
293
|
+
});
|
|
294
|
+
const receipt = requireRecord(value, "receipt");
|
|
295
|
+
if (receipt.schemaVersion !== tileflowShotReceiptSchemaVersion || receipt.kind !== "shot") {
|
|
296
|
+
throw invalidReceipt("The shot receipt schema version is unsupported.");
|
|
297
|
+
}
|
|
298
|
+
requireExactKeys(receipt, [
|
|
299
|
+
"schemaVersion",
|
|
300
|
+
"kind",
|
|
301
|
+
"shot",
|
|
302
|
+
"style",
|
|
303
|
+
...receipt.blend === void 0 ? [] : ["blend"],
|
|
304
|
+
"timing",
|
|
305
|
+
"image",
|
|
306
|
+
"frames",
|
|
307
|
+
"output",
|
|
308
|
+
"renderer",
|
|
309
|
+
"platform",
|
|
310
|
+
"data",
|
|
311
|
+
"verification",
|
|
312
|
+
"networkDependent"
|
|
313
|
+
]);
|
|
314
|
+
const shot = requireRecord(receipt.shot, "shot");
|
|
315
|
+
requireExactKeys(shot, ["name", "map", "theme", "sha256"]);
|
|
316
|
+
const style = requireRecord(receipt.style, "style");
|
|
317
|
+
requireExactKeys(style, ["sha256"]);
|
|
318
|
+
const blend = receipt.blend === void 0 ? void 0 : validateShotBlend(receipt.blend);
|
|
319
|
+
const timing = requireRecord(receipt.timing, "timing");
|
|
320
|
+
requireExactKeys(timing, ["fps", "frameCount", "durationMs"]);
|
|
321
|
+
const fps = timing.fps;
|
|
322
|
+
if (fps !== 24 && fps !== 25 && fps !== 30 && fps !== 50 && fps !== 60) {
|
|
323
|
+
throw invalidReceipt("The shot receipt has an invalid timing.fps.");
|
|
324
|
+
}
|
|
325
|
+
const frameCount = requirePositiveInteger(timing.frameCount, "timing.frameCount");
|
|
326
|
+
const durationMs = requirePositiveInteger(timing.durationMs, "timing.durationMs");
|
|
327
|
+
if (durationMs > tileflowCaptureShotLimits.durationMs.maximum || frameCount > tileflowCaptureShotLimits.maximumFrames || frameCount !== Math.floor(durationMs * fps / 1e3 + 1e-9) + 1) {
|
|
328
|
+
throw invalidReceipt("The shot receipt timing is inconsistent.");
|
|
329
|
+
}
|
|
330
|
+
const image = requireRecord(receipt.image, "image");
|
|
331
|
+
requireExactKeys(image, ["cssWidth", "cssHeight", "physicalWidth", "physicalHeight", "dpr"]);
|
|
332
|
+
const cssWidth = requirePositiveInteger(image.cssWidth, "image.cssWidth");
|
|
333
|
+
const cssHeight = requirePositiveInteger(image.cssHeight, "image.cssHeight");
|
|
334
|
+
const physicalWidth = requirePositiveInteger(image.physicalWidth, "image.physicalWidth");
|
|
335
|
+
const physicalHeight = requirePositiveInteger(image.physicalHeight, "image.physicalHeight");
|
|
336
|
+
if (image.dpr !== 1 && image.dpr !== 2) {
|
|
337
|
+
throw invalidReceipt("The shot receipt has an invalid image DPR.");
|
|
338
|
+
}
|
|
339
|
+
if (cssWidth * image.dpr !== physicalWidth || cssHeight * image.dpr !== physicalHeight) {
|
|
340
|
+
throw invalidReceipt("The shot receipt image dimensions are inconsistent.");
|
|
341
|
+
}
|
|
342
|
+
if (physicalWidth > tileflowCaptureSceneLimits.viewport.maximum || physicalHeight > tileflowCaptureSceneLimits.viewport.maximum || physicalWidth * physicalHeight > tileflowCaptureSceneLimits.maximumPhysicalPixels) {
|
|
343
|
+
throw invalidReceipt("The shot receipt image exceeds the supported pixel limit.");
|
|
344
|
+
}
|
|
345
|
+
const frames = requireRecord(receipt.frames, "frames");
|
|
346
|
+
requireExactKeys(frames, ["sha256"]);
|
|
347
|
+
const framesSha256 = requireHash(frames.sha256, "frames.sha256");
|
|
348
|
+
const output = requireRecord(receipt.output, "output");
|
|
349
|
+
if (output.kind === "webm") {
|
|
350
|
+
requireExactKeys(output, ["kind", "codec", "sha256"]);
|
|
351
|
+
if (output.codec !== "vp8" && output.codec !== "vp9") {
|
|
352
|
+
throw invalidReceipt("The shot receipt has an invalid output.codec.");
|
|
353
|
+
}
|
|
354
|
+
} else if (output.kind === "frames") {
|
|
355
|
+
requireExactKeys(output, ["kind", "sha256"]);
|
|
356
|
+
if (output.sha256 !== framesSha256) {
|
|
357
|
+
throw invalidReceipt("The shot receipt frame output does not match its frames.");
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
throw invalidReceipt("The shot receipt has an invalid output.kind.");
|
|
361
|
+
}
|
|
362
|
+
const renderer = requireRecord(receipt.renderer, "renderer");
|
|
363
|
+
requireExactKeys(renderer, [
|
|
364
|
+
"tileflow",
|
|
365
|
+
"maplibre",
|
|
366
|
+
"playwright",
|
|
367
|
+
"chromiumRevision",
|
|
368
|
+
"chromiumVersion"
|
|
369
|
+
]);
|
|
370
|
+
const platform = requireRecord(receipt.platform, "platform");
|
|
371
|
+
requireExactKeys(platform, ["os", "architecture"]);
|
|
372
|
+
const verification = requireRecord(receipt.verification, "verification");
|
|
373
|
+
requireExactKeys(verification, ["data", "style"]);
|
|
374
|
+
if (verification.data !== "rendered" || verification.style !== "rendered") {
|
|
375
|
+
throw invalidReceipt("The shot receipt verification must record rendered style and data.");
|
|
376
|
+
}
|
|
377
|
+
if (typeof receipt.networkDependent !== "boolean") {
|
|
378
|
+
throw invalidReceipt("The shot receipt has an invalid network dependency flag.");
|
|
379
|
+
}
|
|
380
|
+
const normalized = {
|
|
381
|
+
schemaVersion: tileflowShotReceiptSchemaVersion,
|
|
382
|
+
kind: "shot",
|
|
383
|
+
shot: {
|
|
384
|
+
name: requireSceneName(shot.name),
|
|
385
|
+
map: requirePortableIdentifier(shot.map, "shot.map"),
|
|
386
|
+
theme: requireShotTheme(shot.theme, "shot.theme"),
|
|
387
|
+
sha256: requireHash(shot.sha256, "shot.sha256")
|
|
388
|
+
},
|
|
389
|
+
style: { sha256: requireHash(style.sha256, "style.sha256") },
|
|
390
|
+
...blend ? { blend } : {},
|
|
391
|
+
timing: { fps, frameCount, durationMs },
|
|
392
|
+
image: { cssWidth, cssHeight, physicalWidth, physicalHeight, dpr: image.dpr },
|
|
393
|
+
frames: { sha256: framesSha256 },
|
|
394
|
+
output: output.kind === "webm" ? {
|
|
395
|
+
kind: "webm",
|
|
396
|
+
codec: output.codec,
|
|
397
|
+
sha256: requireHash(output.sha256, "output.sha256")
|
|
398
|
+
} : { kind: "frames", sha256: framesSha256 },
|
|
399
|
+
renderer: {
|
|
400
|
+
tileflow: requireBoundedString(renderer.tileflow, "renderer.tileflow"),
|
|
401
|
+
maplibre: requireBoundedString(renderer.maplibre, "renderer.maplibre"),
|
|
402
|
+
playwright: requireBoundedString(renderer.playwright, "renderer.playwright"),
|
|
403
|
+
chromiumRevision: requireBoundedString(
|
|
404
|
+
renderer.chromiumRevision,
|
|
405
|
+
"renderer.chromiumRevision"
|
|
406
|
+
),
|
|
407
|
+
chromiumVersion: requireBoundedString(renderer.chromiumVersion, "renderer.chromiumVersion")
|
|
408
|
+
},
|
|
409
|
+
platform: {
|
|
410
|
+
os: requireBoundedString(platform.os, "platform.os"),
|
|
411
|
+
architecture: requireBoundedString(platform.architecture, "platform.architecture")
|
|
412
|
+
},
|
|
413
|
+
data: validateDataIdentity(receipt.data),
|
|
414
|
+
verification: { data: "rendered", style: "rendered" },
|
|
415
|
+
networkDependent: receipt.networkDependent
|
|
416
|
+
};
|
|
417
|
+
const serialized = `${serializeCanonicalJson(normalized)}
|
|
418
|
+
`;
|
|
419
|
+
if (new TextEncoder().encode(serialized).byteLength > tileflowShotReceiptLimits.maximumBytes) {
|
|
420
|
+
throw invalidReceipt("The shot receipt exceeds the supported byte limit.");
|
|
421
|
+
}
|
|
422
|
+
return normalized;
|
|
423
|
+
}
|
|
424
|
+
function requireShotTheme(value, field) {
|
|
425
|
+
const result = tileflowThemeNameSchema.safeParse(value);
|
|
426
|
+
if (!result.success) throw invalidReceipt(`The shot receipt has an invalid ${field}.`);
|
|
427
|
+
return result.data;
|
|
428
|
+
}
|
|
429
|
+
function validateShotBlend(value) {
|
|
430
|
+
const blend = requireRecord(value, "blend");
|
|
431
|
+
requireExactKeys(blend, ["themes"]);
|
|
432
|
+
if (!Array.isArray(blend.themes) || blend.themes.length < tileflowCaptureShotLimits.blendThemes.minimum || blend.themes.length > tileflowCaptureShotLimits.blendThemes.maximum) {
|
|
433
|
+
throw invalidReceipt("The shot receipt has an invalid blend.themes list.");
|
|
434
|
+
}
|
|
435
|
+
const themes = blend.themes.map((entry, index) => {
|
|
436
|
+
const theme = requireRecord(entry, `blend.themes.${index}`);
|
|
437
|
+
requireExactKeys(theme, ["name", "styleSha256"]);
|
|
438
|
+
return {
|
|
439
|
+
name: requireShotTheme(theme.name, `blend.themes.${index}.name`),
|
|
440
|
+
styleSha256: requireHash(theme.styleSha256, `blend.themes.${index}.styleSha256`)
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
if (new Set(themes.map((theme) => theme.name)).size !== themes.length) {
|
|
444
|
+
throw invalidReceipt("The shot receipt blends a theme more than once.");
|
|
445
|
+
}
|
|
446
|
+
return { themes };
|
|
447
|
+
}
|
|
234
448
|
function validateDataIdentity(value) {
|
|
235
449
|
const data = requireRecord(value, "data");
|
|
236
450
|
if (data.kind === "tileflow-world") {
|
|
@@ -390,12 +604,17 @@ function assertCanonicalReceiptByteLimit(receipt) {
|
|
|
390
604
|
}
|
|
391
605
|
}
|
|
392
606
|
function assertPlainReceiptData(value, path = "receipt", state = {
|
|
607
|
+
allowArrays: false,
|
|
393
608
|
ancestors: /* @__PURE__ */ new WeakSet(),
|
|
394
609
|
nodes: 0
|
|
395
610
|
}, depth = 0) {
|
|
396
611
|
if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
397
612
|
return;
|
|
398
613
|
}
|
|
614
|
+
if (typeof value === "object" && Array.isArray(value) && state.allowArrays) {
|
|
615
|
+
assertPlainReceiptArray(value, path, state, depth);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
399
618
|
if (typeof value !== "object" || Array.isArray(value)) {
|
|
400
619
|
throw invalidReceipt(`The baseline receipt has an executable or non-plain ${path} value.`);
|
|
401
620
|
}
|
|
@@ -434,6 +653,35 @@ function assertPlainReceiptData(value, path = "receipt", state = {
|
|
|
434
653
|
state.ancestors.delete(value);
|
|
435
654
|
}
|
|
436
655
|
}
|
|
656
|
+
function assertPlainReceiptArray(value, path, state, depth) {
|
|
657
|
+
if (nodeUtilTypes.isProxy(value)) {
|
|
658
|
+
throw invalidReceipt(`The baseline receipt has an executable proxy at ${path}.`);
|
|
659
|
+
}
|
|
660
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) {
|
|
661
|
+
throw invalidReceipt(`The baseline receipt has a non-plain ${path} array.`);
|
|
662
|
+
}
|
|
663
|
+
if (depth > 32 || ++state.nodes > 4096 || value.length > 4096) {
|
|
664
|
+
throw invalidReceipt("The baseline receipt exceeds the supported structural limit.");
|
|
665
|
+
}
|
|
666
|
+
if (state.ancestors.has(value)) {
|
|
667
|
+
throw invalidReceipt("The baseline receipt must not contain cyclic data.");
|
|
668
|
+
}
|
|
669
|
+
if (Reflect.ownKeys(value).length !== value.length + 1) {
|
|
670
|
+
throw invalidReceipt(`The baseline receipt has a sparse or extended ${path} array.`);
|
|
671
|
+
}
|
|
672
|
+
state.ancestors.add(value);
|
|
673
|
+
try {
|
|
674
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
675
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, index);
|
|
676
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
677
|
+
throw invalidReceipt("The baseline receipt contains an accessor or non-enumerable field.");
|
|
678
|
+
}
|
|
679
|
+
assertPlainReceiptData(descriptor.value, `${path}.${index}`, state, depth + 1);
|
|
680
|
+
}
|
|
681
|
+
} finally {
|
|
682
|
+
state.ancestors.delete(value);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
437
685
|
function requireRecord(value, field) {
|
|
438
686
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
439
687
|
throw invalidReceipt(`The baseline receipt has an invalid ${field} object.`);
|
|
@@ -544,5 +792,11 @@ export {
|
|
|
544
792
|
createTileflowCaptureReceipt,
|
|
545
793
|
serializeTileflowCaptureReceipt,
|
|
546
794
|
parseTileflowCaptureReceipt,
|
|
547
|
-
validateTileflowCaptureReceipt
|
|
795
|
+
validateTileflowCaptureReceipt,
|
|
796
|
+
tileflowShotReceiptSchemaVersion,
|
|
797
|
+
tileflowShotReceiptLimits,
|
|
798
|
+
createTileflowShotReceipt,
|
|
799
|
+
serializeTileflowShotReceipt,
|
|
800
|
+
parseTileflowShotReceipt,
|
|
801
|
+
validateTileflowShotReceipt
|
|
548
802
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Browser } from 'playwright';
|
|
2
|
-
import { T as TileflowCaptureRendererIdentity, a as
|
|
3
|
-
export { C as CreateTileflowCaptureReceiptInput,
|
|
4
|
-
import { TileflowCaptureScene, MapLibreStyle
|
|
5
|
-
import { TileflowIconResolutionOptions, TileflowBuildArtifacts
|
|
2
|
+
import { T as TileflowCaptureRendererIdentity, a as TileflowShotReceiptV1, b as TileflowCaptureReceipt, c as TileflowCaptureDataInput, d as TileflowCaptureWorldIdentity } from './receipt-CfZkzyXd.js';
|
|
3
|
+
export { C as CreateTileflowCaptureReceiptInput, e as CreateTileflowShotReceiptInput, f as TileflowCaptureDataBindings, g as TileflowCaptureDataCapabilities, h as TileflowCaptureDataIdentity, i as TileflowCaptureDataSemantics, j as TileflowCaptureDataSource, k as TileflowCaptureVectorIdentity, l as TileflowCaptureVerification, m as createTileflowCaptureReceipt, n as createTileflowCaptureRendererIdentity, o as createTileflowShotReceipt, p as parseTileflowCaptureReceipt, q as parseTileflowShotReceipt, s as serializeTileflowCaptureReceipt, r as serializeTileflowShotReceipt, t as tileflowCaptureReceiptLimits, u as tileflowCaptureReceiptSchemaVersion, v as tileflowCaptureRuntime, w as tileflowShotReceiptLimits, x as tileflowShotReceiptSchemaVersion, y as validateTileflowCaptureReceipt, z as validateTileflowShotReceipt } from './receipt-CfZkzyXd.js';
|
|
4
|
+
import { NormalizedTileflowCaptureShot, NormalizedTileflowCaptureScene, TileflowCaptureScene, MapLibreStyle } from '@tileflow/core';
|
|
5
|
+
import { TileflowBuildAsset, TileflowIconResolutionOptions, TileflowBuildArtifacts } from '@tileflow/dev/artifacts';
|
|
6
6
|
|
|
7
7
|
type TileflowBrowserInstallProgress = 'installing' | 'installed';
|
|
8
8
|
type TileflowBrowserLaunchOptions = {
|
|
@@ -24,6 +24,49 @@ declare class TileflowCaptureBrowserManager {
|
|
|
24
24
|
close(): Promise<void>;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
declare const tileflowSyntheticAssetOrigin = "https://tileflow.local.invalid";
|
|
28
|
+
type TileflowCaptureAsset = TileflowBuildAsset & {
|
|
29
|
+
url?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type TileflowShotVideoCodec = 'vp8' | 'vp9';
|
|
33
|
+
|
|
34
|
+
type TileflowShotFormat = 'frames' | 'webm';
|
|
35
|
+
/** One rendered frame, delivered in order while a shot renders. */
|
|
36
|
+
type TileflowShotFrame = {
|
|
37
|
+
shot: string;
|
|
38
|
+
index: number;
|
|
39
|
+
png: Uint8Array;
|
|
40
|
+
sha256: string;
|
|
41
|
+
};
|
|
42
|
+
type TileflowShotCapture = {
|
|
43
|
+
shot: string;
|
|
44
|
+
map: string;
|
|
45
|
+
theme: string;
|
|
46
|
+
format: TileflowShotFormat;
|
|
47
|
+
/** WebM bytes for the `webm` format. Frames reach `onFrame` and are not retained. */
|
|
48
|
+
video?: Uint8Array;
|
|
49
|
+
codec?: TileflowShotVideoCodec;
|
|
50
|
+
/** SHA-256 of the video, or of the frame set (`framesSha256`) for the `frames` format. */
|
|
51
|
+
sha256: string;
|
|
52
|
+
framesSha256: string;
|
|
53
|
+
frameSha256s: string[];
|
|
54
|
+
shotSha256: string;
|
|
55
|
+
styleSha256: string;
|
|
56
|
+
fps: NormalizedTileflowCaptureShot['fps'];
|
|
57
|
+
frameCount: number;
|
|
58
|
+
durationMs: number;
|
|
59
|
+
width: number;
|
|
60
|
+
height: number;
|
|
61
|
+
dpr: 1 | 2;
|
|
62
|
+
networkDependent: boolean;
|
|
63
|
+
renderer: TileflowCaptureRendererIdentity;
|
|
64
|
+
receipt: TileflowShotReceiptV1;
|
|
65
|
+
warnings: string[];
|
|
66
|
+
};
|
|
67
|
+
/** SHA-256 of each frame's PNG SHA-256 in lowercase hex, one per line, in frame order. */
|
|
68
|
+
declare function tileflowShotFramesSha256(frameSha256s: readonly string[]): Promise<string>;
|
|
69
|
+
|
|
27
70
|
declare const tileflowCaptureResultSchemaVersion: 1;
|
|
28
71
|
type TileflowCaptureOptions = {
|
|
29
72
|
allowBrowserInstall?: boolean;
|
|
@@ -47,6 +90,10 @@ type TileflowCaptureOptions = {
|
|
|
47
90
|
};
|
|
48
91
|
type CreateTileflowCaptureSessionOptions = Omit<TileflowCaptureOptions, 'scenes'>;
|
|
49
92
|
type TileflowCapture = {
|
|
93
|
+
/** Concrete normalized frame retained for later review without rendering again. */
|
|
94
|
+
definition?: NormalizedTileflowCaptureScene & {
|
|
95
|
+
theme: string;
|
|
96
|
+
};
|
|
50
97
|
scene: string;
|
|
51
98
|
map: string;
|
|
52
99
|
theme: string;
|
|
@@ -68,18 +115,39 @@ type TileflowCaptureResult = {
|
|
|
68
115
|
schemaVersion: 1;
|
|
69
116
|
captures: TileflowCapture[];
|
|
70
117
|
};
|
|
118
|
+
type TileflowShotCaptureOptions = {
|
|
119
|
+
/** `webm` (the default) encodes a VP9 or VP8 video; `frames` delivers only PNG frames. */
|
|
120
|
+
format?: TileflowShotFormat;
|
|
121
|
+
/** Receives each frame's PNG in order while the shot renders. */
|
|
122
|
+
onFrame?: (frame: TileflowShotFrame) => Promise<void> | void;
|
|
123
|
+
signal?: AbortSignal;
|
|
124
|
+
};
|
|
125
|
+
type TileflowShotCaptureResult = {
|
|
126
|
+
schemaVersion: 1;
|
|
127
|
+
shots: TileflowShotCapture[];
|
|
128
|
+
};
|
|
129
|
+
type CaptureTileflowShotOptions = CreateTileflowCaptureSessionOptions & Omit<TileflowShotCaptureOptions, 'signal'> & {
|
|
130
|
+
shot: string;
|
|
131
|
+
};
|
|
71
132
|
type TileflowCaptureSession = {
|
|
72
133
|
capture(scenes: string[], signal?: AbortSignal): Promise<TileflowCaptureResult>;
|
|
73
134
|
captureAll(signal?: AbortSignal): Promise<TileflowCaptureResult>;
|
|
74
135
|
captureDefinitions(scenes: Record<string, TileflowCaptureScene>, signal?: AbortSignal): Promise<TileflowCaptureResult>;
|
|
75
136
|
captureArtifacts(artifacts: TileflowBuildArtifacts, requestedScenes: string[], signal?: AbortSignal): Promise<TileflowCaptureResult>;
|
|
137
|
+
/** Renders one committed shot. */
|
|
138
|
+
captureShot(shot: string, options?: TileflowShotCaptureOptions): Promise<TileflowShotCapture>;
|
|
139
|
+
/** Renders committed shots in name order from one compiled artifact snapshot. */
|
|
140
|
+
captureShots(shots: string[], options?: TileflowShotCaptureOptions): Promise<TileflowShotCaptureResult>;
|
|
141
|
+
captureAllShots(options?: TileflowShotCaptureOptions): Promise<TileflowShotCaptureResult>;
|
|
76
142
|
close(): Promise<void>;
|
|
77
143
|
};
|
|
78
144
|
declare function captureTileflowScenes(options: TileflowCaptureOptions): Promise<TileflowCaptureResult>;
|
|
145
|
+
/** Renders one committed shot with a temporary session. */
|
|
146
|
+
declare function captureTileflowShot(options: CaptureTileflowShotOptions): Promise<TileflowShotCapture>;
|
|
79
147
|
declare function createTileflowCaptureSession(options?: CreateTileflowCaptureSessionOptions): TileflowCaptureSession;
|
|
80
148
|
|
|
81
|
-
type TileflowCaptureErrorCode = 'ABORTED' | 'APPLICATION_ORIGIN_REQUIRED' | 'APPLICATION_ORIGIN_INVALID' | 'APPLICATION_NAVIGATION_FAILED' | 'APPLICATION_TARGET_NOT_FOUND' | 'APPLICATION_TARGET_AMBIGUOUS' | 'APPLICATION_ERROR' | 'BASELINE_INVALID' | 'BROWSER_INSTALL_FAILED' | 'BROWSER_MISSING' | 'BROWSER_START_FAILED' | 'CAPTURE_TIMEOUT' | 'INVALID_PNG' | 'MAP_LOAD_FAILED' | 'RENDER_FAILED' | 'RESOURCE_FAILED' | 'SCENE_NOT_FOUND' | 'SCREENSHOT_FAILED' | 'STYLE_INVALID' | 'SYNTHETIC_ASSET_NOT_FOUND' | 'WORLD_RESOLUTION_FAILED';
|
|
82
|
-
type TileflowCapturePhase = 'style-validation' | 'browser-start' | 'resource-load' | 'map-load' | 'map-idle' | 'screenshot';
|
|
149
|
+
type TileflowCaptureErrorCode = 'ABORTED' | 'APPLICATION_ORIGIN_REQUIRED' | 'APPLICATION_ORIGIN_INVALID' | 'APPLICATION_NAVIGATION_FAILED' | 'APPLICATION_TARGET_NOT_FOUND' | 'APPLICATION_TARGET_AMBIGUOUS' | 'APPLICATION_ERROR' | 'BASELINE_INVALID' | 'BROWSER_INSTALL_FAILED' | 'BROWSER_MISSING' | 'BROWSER_START_FAILED' | 'CAPTURE_TIMEOUT' | 'INVALID_PNG' | 'MAP_LOAD_FAILED' | 'RENDER_FAILED' | 'RESOURCE_FAILED' | 'SCENE_NOT_FOUND' | 'SCREENSHOT_FAILED' | 'SHOT_NOT_FOUND' | 'STYLE_INVALID' | 'SYNTHETIC_ASSET_NOT_FOUND' | 'VIDEO_ENCODE_FAILED' | 'VIDEO_ENCODER_UNAVAILABLE' | 'WORLD_RESOLUTION_FAILED';
|
|
150
|
+
type TileflowCapturePhase = 'style-validation' | 'browser-start' | 'resource-load' | 'map-load' | 'theme-blend' | 'map-idle' | 'screenshot' | 'video-encode';
|
|
83
151
|
type TileflowCaptureDiagnostic = {
|
|
84
152
|
message: string;
|
|
85
153
|
path: string;
|
|
@@ -106,11 +174,6 @@ declare class TileflowCaptureError extends Error {
|
|
|
106
174
|
}
|
|
107
175
|
declare function captureErrorMessage(error: unknown, fallback: string): string;
|
|
108
176
|
|
|
109
|
-
declare const tileflowSyntheticAssetOrigin = "https://tileflow.local.invalid";
|
|
110
|
-
type TileflowCaptureAsset = TileflowBuildAsset & {
|
|
111
|
-
url?: string;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
177
|
declare const tileflowWorldCurrentTileJsonUrl: "https://api.tileflow.dev/tiles/world/tiles.json";
|
|
115
178
|
type FetchTileJson = (input: string, init: {
|
|
116
179
|
cache: 'no-store';
|
|
@@ -365,4 +428,4 @@ declare const tileflowVisualReviewLimits: Readonly<{
|
|
|
365
428
|
maximumAggregatePngBytes: number;
|
|
366
429
|
}>;
|
|
367
430
|
|
|
368
|
-
export { type CreateTileflowCaptureSessionOptions, type PreparedTileflowCaptureStyle, type TileflowBrowserInstallProgress, type TileflowBrowserLaunchOptions, type TileflowCapture, TileflowCaptureBrowserManager, TileflowCaptureDataInput, type TileflowCaptureDiagnostic, TileflowCaptureError, type TileflowCaptureErrorCode, type TileflowCaptureErrorDetails, type TileflowCaptureOptions, type TileflowCapturePhase, TileflowCaptureReceipt, TileflowCaptureRendererIdentity, type TileflowCaptureResolvedWorldV1, type TileflowCaptureResourceDiagnostic, type TileflowCaptureResourceKind, type TileflowCaptureResult, type TileflowCaptureSession, TileflowCaptureWorldIdentity, TileflowCaptureWorldSession, type TileflowVisualAppearanceDelta, type TileflowVisualAppearanceProfile, type TileflowVisualAppearanceStatistics, type TileflowVisualBaseline, type TileflowVisualComparison, type TileflowVisualComparisonDocument, type TileflowVisualComparisonStatus, type TileflowVisualImageIdentity, type TileflowVisualPaletteColor, type TileflowVisualPixelMetric, type TileflowVisualReferenceAnalysis, type TileflowVisualReferenceAnalysisDocument, type TileflowVisualReferenceAnalysisOptions, type TileflowVisualReferenceAppearance, type TileflowVisualRegion, type TileflowVisualReviewAppearance, type TileflowVisualReviewCapture, type TileflowVisualReviewComparison, type TileflowVisualReviewDefinition, type TileflowVisualReviewDocument, TileflowVisualReviewError, type TileflowVisualReviewFrameIdentity, type TileflowVisualReviewOptions, type TileflowVisualReviewSide, type TileflowVisualReviewStatus, type TileflowVisualRuntimeIdentity, type TileflowVisualSceneIdentity, analyzeTileflowCaptureReference, captureErrorMessage, captureTileflowScenes, compareTileflowCaptureToBaseline, compareTileflowCapturesForReview, createTileflowCaptureBrowserEnvironment, createTileflowCaptureSession, createTileflowVisualComparisonDocument, createTileflowVisualReferenceAnalysisDocument, createTileflowVisualReviewDocument, launchTileflowCaptureBrowser, resolveTileflowApplicationUrl, resolveTileflowCaptureWorldTileJson, serializeTileflowVisualComparison, setupTileflowCaptureBrowser, tileflowCaptureResultSchemaVersion, tileflowSyntheticAssetOrigin, tileflowVisualArtifactLimits, tileflowVisualComparisonSchemaVersion, tileflowVisualEdgeThreshold, tileflowVisualPerceptualThreshold, tileflowVisualReviewLimits, tileflowVisualReviewSchemaVersion, tileflowWorldCurrentTileJsonUrl, validateTileflowVisualReferencePng };
|
|
431
|
+
export { type CaptureTileflowShotOptions, type CreateTileflowCaptureSessionOptions, type PreparedTileflowCaptureStyle, type TileflowBrowserInstallProgress, type TileflowBrowserLaunchOptions, type TileflowCapture, TileflowCaptureBrowserManager, TileflowCaptureDataInput, type TileflowCaptureDiagnostic, TileflowCaptureError, type TileflowCaptureErrorCode, type TileflowCaptureErrorDetails, type TileflowCaptureOptions, type TileflowCapturePhase, TileflowCaptureReceipt, TileflowCaptureRendererIdentity, type TileflowCaptureResolvedWorldV1, type TileflowCaptureResourceDiagnostic, type TileflowCaptureResourceKind, type TileflowCaptureResult, type TileflowCaptureSession, TileflowCaptureWorldIdentity, TileflowCaptureWorldSession, type TileflowShotCapture, type TileflowShotCaptureOptions, type TileflowShotCaptureResult, type TileflowShotFormat, type TileflowShotFrame, TileflowShotReceiptV1, type TileflowShotVideoCodec, type TileflowVisualAppearanceDelta, type TileflowVisualAppearanceProfile, type TileflowVisualAppearanceStatistics, type TileflowVisualBaseline, type TileflowVisualComparison, type TileflowVisualComparisonDocument, type TileflowVisualComparisonStatus, type TileflowVisualImageIdentity, type TileflowVisualPaletteColor, type TileflowVisualPixelMetric, type TileflowVisualReferenceAnalysis, type TileflowVisualReferenceAnalysisDocument, type TileflowVisualReferenceAnalysisOptions, type TileflowVisualReferenceAppearance, type TileflowVisualRegion, type TileflowVisualReviewAppearance, type TileflowVisualReviewCapture, type TileflowVisualReviewComparison, type TileflowVisualReviewDefinition, type TileflowVisualReviewDocument, TileflowVisualReviewError, type TileflowVisualReviewFrameIdentity, type TileflowVisualReviewOptions, type TileflowVisualReviewSide, type TileflowVisualReviewStatus, type TileflowVisualRuntimeIdentity, type TileflowVisualSceneIdentity, analyzeTileflowCaptureReference, captureErrorMessage, captureTileflowScenes, captureTileflowShot, compareTileflowCaptureToBaseline, compareTileflowCapturesForReview, createTileflowCaptureBrowserEnvironment, createTileflowCaptureSession, createTileflowVisualComparisonDocument, createTileflowVisualReferenceAnalysisDocument, createTileflowVisualReviewDocument, launchTileflowCaptureBrowser, resolveTileflowApplicationUrl, resolveTileflowCaptureWorldTileJson, serializeTileflowVisualComparison, setupTileflowCaptureBrowser, tileflowCaptureResultSchemaVersion, tileflowShotFramesSha256, tileflowSyntheticAssetOrigin, tileflowVisualArtifactLimits, tileflowVisualComparisonSchemaVersion, tileflowVisualEdgeThreshold, tileflowVisualPerceptualThreshold, tileflowVisualReviewLimits, tileflowVisualReviewSchemaVersion, tileflowWorldCurrentTileJsonUrl, validateTileflowVisualReferencePng };
|