@tileflow/capture 0.1.0-alpha.18 → 0.1.0-alpha.20
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 +8 -0
- package/dist/index.js +113 -23
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -70,6 +70,14 @@ Capture verifies the concrete theme before and after screenshot production. A fu
|
|
|
70
70
|
props own application camera state; capture includes that camera in scene identity but does not
|
|
71
71
|
rewrite it.
|
|
72
72
|
|
|
73
|
+
The application's development server also remains authoritative for local PMTiles. Each range is
|
|
74
|
+
served from one immutable snapshot and strong ETags prevent one PMTiles read from silently mixing
|
|
75
|
+
generations. Application Capture deliberately adds no lease or token that freezes every local
|
|
76
|
+
dataset for the complete screenshot. A source edit during that exact window can therefore move a
|
|
77
|
+
later read to the next valid generation; application receipts already classify style and data as
|
|
78
|
+
`expected-unverified`. Standalone Capture is stricter because it owns and retains its artifact
|
|
79
|
+
snapshot for the complete render.
|
|
80
|
+
|
|
73
81
|
## Prepared/offline browser setup
|
|
74
82
|
|
|
75
83
|
```ts
|
package/dist/index.js
CHANGED
|
@@ -296,7 +296,7 @@ import { resolve } from "path";
|
|
|
296
296
|
import {
|
|
297
297
|
compareCodeUnits as compareCodeUnits3,
|
|
298
298
|
normalizeTileflowCaptureScene,
|
|
299
|
-
|
|
299
|
+
parseResolvedTileflowMap,
|
|
300
300
|
resolveThemeSelection,
|
|
301
301
|
serializeCanonicalJson,
|
|
302
302
|
sha256Hex,
|
|
@@ -313,6 +313,7 @@ import { tileflowCaptureSceneLimits } from "@tileflow/core";
|
|
|
313
313
|
|
|
314
314
|
// src/standalone.ts
|
|
315
315
|
import { readFileSync as readFileSync2 } from "fs";
|
|
316
|
+
import { open } from "fs/promises";
|
|
316
317
|
import { createRequire as createRequire2 } from "module";
|
|
317
318
|
import { dirname as dirname2, join as join2 } from "path";
|
|
318
319
|
import { getTileflowStyleFontFaces } from "@tileflow/core/runtime";
|
|
@@ -326,7 +327,8 @@ var tileflowBrowserModule = `${readFileSync2(
|
|
|
326
327
|
join2(dirname2(tileflowCorePackagePath), "dist", "browser.js"),
|
|
327
328
|
"utf8"
|
|
328
329
|
)}
|
|
329
|
-
globalThis.__tileflowRegisterContourProtocol = registerTileflowContourProtocol
|
|
330
|
+
globalThis.__tileflowRegisterContourProtocol = registerTileflowContourProtocol;
|
|
331
|
+
globalThis.__tileflowRegisterPmtilesProtocol = registerTileflowPmtilesProtocol;`;
|
|
330
332
|
async function captureStandaloneTileflowScene(input) {
|
|
331
333
|
if (input.scene.target.kind !== "map") {
|
|
332
334
|
throw new TileflowCaptureError(
|
|
@@ -387,6 +389,9 @@ async function captureStandaloneTileflowScene(input) {
|
|
|
387
389
|
}, timeoutMs);
|
|
388
390
|
timeout.unref?.();
|
|
389
391
|
const assets = new Map(input.assets.map((asset) => [asset.fileName, asset]));
|
|
392
|
+
const localTilesets = new Map(
|
|
393
|
+
(input.localTilesets ?? []).map((tileset) => [tileset.fileName, tileset])
|
|
394
|
+
);
|
|
390
395
|
const fontFaces = getTileflowStyleFontFaces(input.style);
|
|
391
396
|
const remoteOrigins = /* @__PURE__ */ new Set();
|
|
392
397
|
let syntheticAssetFailure;
|
|
@@ -404,6 +409,15 @@ async function captureStandaloneTileflowScene(input) {
|
|
|
404
409
|
const url = new URL(route.request().url());
|
|
405
410
|
if (url.origin === tileflowSyntheticAssetOrigin) {
|
|
406
411
|
const fileName = decodeURIComponent(url.pathname.slice(1));
|
|
412
|
+
const localTileset = url.search === "" ? localTilesets.get(fileName) : void 0;
|
|
413
|
+
if (localTileset) {
|
|
414
|
+
const response = await localTilesetCaptureResponse(
|
|
415
|
+
route.request().headers()["range"],
|
|
416
|
+
localTileset
|
|
417
|
+
);
|
|
418
|
+
await route.fulfill(response);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
407
421
|
const asset = url.search === "" ? assets.get(fileName) : void 0;
|
|
408
422
|
if (!asset) {
|
|
409
423
|
syntheticAssetFailure = fileName || "unknown";
|
|
@@ -545,6 +559,58 @@ async function captureStandaloneTileflowScene(input) {
|
|
|
545
559
|
await context?.close().catch(() => void 0);
|
|
546
560
|
}
|
|
547
561
|
}
|
|
562
|
+
async function localTilesetCaptureResponse(rangeHeader, file) {
|
|
563
|
+
const match = rangeHeader?.match(/^bytes=(\d+)-(\d*)$/u);
|
|
564
|
+
if (!match) {
|
|
565
|
+
return {
|
|
566
|
+
body: Buffer.alloc(0),
|
|
567
|
+
headers: {
|
|
568
|
+
"Accept-Ranges": "bytes",
|
|
569
|
+
"Content-Range": `bytes */${file.byteLength}`,
|
|
570
|
+
ETag: file.etag
|
|
571
|
+
},
|
|
572
|
+
status: 416
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
const start = Number(match[1]);
|
|
576
|
+
const requestedEnd = match[2] ? Number(match[2]) : file.byteLength - 1;
|
|
577
|
+
const end = Math.min(requestedEnd, file.byteLength - 1);
|
|
578
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(requestedEnd) || start < 0 || start >= file.byteLength || requestedEnd < start || end - start + 1 > 16 * 1024 * 1024) {
|
|
579
|
+
return {
|
|
580
|
+
body: Buffer.alloc(0),
|
|
581
|
+
headers: {
|
|
582
|
+
"Accept-Ranges": "bytes",
|
|
583
|
+
"Content-Range": `bytes */${file.byteLength}`,
|
|
584
|
+
ETag: file.etag
|
|
585
|
+
},
|
|
586
|
+
status: 416
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const length = end - start + 1;
|
|
590
|
+
const handle = await open(file.sourcePath, "r");
|
|
591
|
+
try {
|
|
592
|
+
const body = Buffer.alloc(length);
|
|
593
|
+
let read = 0;
|
|
594
|
+
while (read < length) {
|
|
595
|
+
const result = await handle.read(body, read, length - read, start + read);
|
|
596
|
+
if (result.bytesRead === 0) throw new Error("Local PMTiles snapshot ended during capture.");
|
|
597
|
+
read += result.bytesRead;
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
body,
|
|
601
|
+
contentType: "application/vnd.pmtiles",
|
|
602
|
+
headers: {
|
|
603
|
+
"Accept-Ranges": "bytes",
|
|
604
|
+
"Content-Length": String(length),
|
|
605
|
+
"Content-Range": `bytes ${start}-${end}/${file.byteLength}`,
|
|
606
|
+
ETag: file.etag
|
|
607
|
+
},
|
|
608
|
+
status: 206
|
|
609
|
+
};
|
|
610
|
+
} finally {
|
|
611
|
+
await handle.close();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
548
614
|
function assertPhaseSucceeded(result, phase, syntheticAssetFailure, resourceFailures, pageFailed) {
|
|
549
615
|
if (syntheticAssetFailure) {
|
|
550
616
|
throw new TileflowCaptureError(
|
|
@@ -722,6 +788,7 @@ window.__tileflowCaptureLoad = async function(input) {
|
|
|
722
788
|
try {
|
|
723
789
|
await loadTileflowFontFaces(input.fontFaces || []);
|
|
724
790
|
window.__tileflowRegisterContourProtocol?.({addProtocol: window.maplibregl.addProtocol});
|
|
791
|
+
window.__tileflowRegisterPmtilesProtocol?.({addProtocol: window.maplibregl.addProtocol});
|
|
725
792
|
window.__tileflowCaptureLoopbackSourceIds = Object.entries(input.style.sources || {})
|
|
726
793
|
.filter(([, source]) => isLoopbackSourceUrl(source?.url))
|
|
727
794
|
.map(([sourceId]) => sourceId);
|
|
@@ -1577,16 +1644,24 @@ var TileflowCaptureSessionImpl = class {
|
|
|
1577
1644
|
async capture(scenes, signal) {
|
|
1578
1645
|
this.#assertOpen();
|
|
1579
1646
|
const artifacts = await this.#createArtifacts();
|
|
1580
|
-
|
|
1647
|
+
try {
|
|
1648
|
+
return await this.captureArtifacts(artifacts, scenes, signal ?? this.#options.signal);
|
|
1649
|
+
} finally {
|
|
1650
|
+
await disposeCaptureArtifacts(artifacts);
|
|
1651
|
+
}
|
|
1581
1652
|
}
|
|
1582
1653
|
async captureAll(signal) {
|
|
1583
1654
|
this.#assertOpen();
|
|
1584
1655
|
const artifacts = await this.#createArtifacts();
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1656
|
+
try {
|
|
1657
|
+
return await this.captureArtifacts(
|
|
1658
|
+
artifacts,
|
|
1659
|
+
Object.keys(artifacts.project.scenes ?? {}),
|
|
1660
|
+
signal ?? this.#options.signal
|
|
1661
|
+
);
|
|
1662
|
+
} finally {
|
|
1663
|
+
await disposeCaptureArtifacts(artifacts);
|
|
1664
|
+
}
|
|
1590
1665
|
}
|
|
1591
1666
|
async captureDefinitions(scenes, signal) {
|
|
1592
1667
|
this.#assertOpen();
|
|
@@ -1595,11 +1670,15 @@ var TileflowCaptureSessionImpl = class {
|
|
|
1595
1670
|
...artifacts,
|
|
1596
1671
|
project: { ...artifacts.project, scenes }
|
|
1597
1672
|
};
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1673
|
+
try {
|
|
1674
|
+
return await this.captureArtifacts(
|
|
1675
|
+
withDefinitions,
|
|
1676
|
+
Object.keys(scenes),
|
|
1677
|
+
signal ?? this.#options.signal
|
|
1678
|
+
);
|
|
1679
|
+
} finally {
|
|
1680
|
+
await disposeCaptureArtifacts(artifacts);
|
|
1681
|
+
}
|
|
1603
1682
|
}
|
|
1604
1683
|
async captureArtifacts(artifacts, requestedScenes, signal) {
|
|
1605
1684
|
this.#assertOpen();
|
|
@@ -1649,7 +1728,7 @@ var TileflowCaptureSessionImpl = class {
|
|
|
1649
1728
|
);
|
|
1650
1729
|
}
|
|
1651
1730
|
const colorScheme = resolveThemeSelection(
|
|
1652
|
-
|
|
1731
|
+
parseResolvedTileflowMap(artifacts.project.maps[scene.map]),
|
|
1653
1732
|
scene.theme
|
|
1654
1733
|
).theme.colorScheme;
|
|
1655
1734
|
const rendered = scene.target.kind === "application" ? await captureApplicationTileflowScene({
|
|
@@ -1663,6 +1742,7 @@ var TileflowCaptureSessionImpl = class {
|
|
|
1663
1742
|
}) : await captureStandaloneTileflowScene({
|
|
1664
1743
|
assets: artifacts.assets,
|
|
1665
1744
|
browser,
|
|
1745
|
+
...artifacts.localTilesets ? { localTilesets: artifacts.localTilesets } : {},
|
|
1666
1746
|
scene,
|
|
1667
1747
|
signal,
|
|
1668
1748
|
style,
|
|
@@ -1716,13 +1796,18 @@ var TileflowCaptureSessionImpl = class {
|
|
|
1716
1796
|
this.#closed = true;
|
|
1717
1797
|
await this.#browserManager.close();
|
|
1718
1798
|
}
|
|
1719
|
-
#createArtifacts() {
|
|
1799
|
+
async #createArtifacts() {
|
|
1720
1800
|
const cwd = resolve(this.#options.cwd ?? process.cwd());
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1801
|
+
try {
|
|
1802
|
+
const artifacts = await createTileflowBuildArtifacts({
|
|
1803
|
+
assetBaseUrl: tileflowSyntheticAssetOrigin,
|
|
1804
|
+
config: this.#options.config ?? defaultTileflowConfigPath,
|
|
1805
|
+
cwd
|
|
1806
|
+
});
|
|
1807
|
+
return artifacts;
|
|
1808
|
+
} catch (error) {
|
|
1809
|
+
return throwCaptureStyleValidationError(error);
|
|
1810
|
+
}
|
|
1726
1811
|
}
|
|
1727
1812
|
#assertOpen() {
|
|
1728
1813
|
if (this.#closed) {
|
|
@@ -1760,6 +1845,11 @@ var TileflowCaptureSessionImpl = class {
|
|
|
1760
1845
|
};
|
|
1761
1846
|
}
|
|
1762
1847
|
};
|
|
1848
|
+
async function disposeCaptureArtifacts(artifacts) {
|
|
1849
|
+
const dispose = artifacts.dispose;
|
|
1850
|
+
artifacts.dispose = void 0;
|
|
1851
|
+
await dispose?.();
|
|
1852
|
+
}
|
|
1763
1853
|
function selectTileflowCaptureSceneNames(artifacts, requestedScenes) {
|
|
1764
1854
|
const availableScenes = Object.keys(artifacts.project.scenes ?? {}).filter((sceneName) => tileflowCaptureSceneNameSchema.safeParse(sceneName).success).sort(compareCodeUnits3);
|
|
1765
1855
|
const sceneNames = [...new Set(requestedScenes)].sort(compareCodeUnits3);
|
|
@@ -1792,14 +1882,14 @@ function normalizeScene(artifacts, sceneName) {
|
|
|
1792
1882
|
throw new TileflowCaptureError("SCENE_NOT_FOUND", `Unknown Tileflow scene "${sceneName}".`);
|
|
1793
1883
|
}
|
|
1794
1884
|
const normalized = normalizeTileflowCaptureScene(scene);
|
|
1795
|
-
const
|
|
1796
|
-
if (!
|
|
1885
|
+
const resolvedMap = artifacts.project.maps[normalized.map];
|
|
1886
|
+
if (!resolvedMap) {
|
|
1797
1887
|
throw new TileflowCaptureError(
|
|
1798
1888
|
"SCENE_NOT_FOUND",
|
|
1799
1889
|
`Scene "${sceneName}" references unavailable map "${normalized.map}".`
|
|
1800
1890
|
);
|
|
1801
1891
|
}
|
|
1802
|
-
const map =
|
|
1892
|
+
const map = parseResolvedTileflowMap(resolvedMap);
|
|
1803
1893
|
let theme;
|
|
1804
1894
|
try {
|
|
1805
1895
|
theme = resolveThemeSelection(map, normalized.theme).name;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tileflow/capture",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.20",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Deterministic local visual capture and comparison for Tileflow maps.",
|
|
6
6
|
"engines": {
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@tileflow/core": ">=0.1.0-alpha.
|
|
54
|
-
"@tileflow/dev": ">=0.1.0-alpha.
|
|
53
|
+
"@tileflow/core": ">=0.1.0-alpha.23 <0.1.0-beta.0",
|
|
54
|
+
"@tileflow/dev": ">=0.1.0-alpha.21 <0.1.0-beta.0",
|
|
55
55
|
"maplibre-gl": "5.24.0",
|
|
56
56
|
"pixelmatch": "7.2.0",
|
|
57
57
|
"playwright": "1.62.1",
|
|
@@ -69,13 +69,13 @@
|
|
|
69
69
|
"vite": "^8.0.16",
|
|
70
70
|
"vue": "^3.5.39",
|
|
71
71
|
"webpack": "5.108.3",
|
|
72
|
-
"@tileflow/maps": "0.1.0-alpha.
|
|
73
|
-
"@tileflow/
|
|
74
|
-
"@tileflow/
|
|
75
|
-
"@tileflow/
|
|
76
|
-
"@tileflow/
|
|
77
|
-
"@tileflow/
|
|
78
|
-
"@tileflow/
|
|
72
|
+
"@tileflow/maps": "0.1.0-alpha.1",
|
|
73
|
+
"@tileflow/vite": "0.1.0-alpha.19",
|
|
74
|
+
"@tileflow/vue": "0.1.0-alpha.21",
|
|
75
|
+
"@tileflow/next": "0.1.0-alpha.19",
|
|
76
|
+
"@tileflow/react": "0.1.0-alpha.21",
|
|
77
|
+
"@tileflow/webpack": "0.1.0-alpha.19",
|
|
78
|
+
"@tileflow/svelte": "0.1.0-alpha.21"
|
|
79
79
|
},
|
|
80
80
|
"scripts": {
|
|
81
81
|
"build": "tsup src/index.ts src/receipt.ts --format esm --dts --clean --platform node",
|