@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.
Files changed (3) hide show
  1. package/README.md +8 -0
  2. package/dist/index.js +113 -23
  3. 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
- parseTileflowMap,
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
- return this.captureArtifacts(artifacts, scenes, signal ?? this.#options.signal);
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
- return this.captureArtifacts(
1586
- artifacts,
1587
- Object.keys(artifacts.project.scenes ?? {}),
1588
- signal ?? this.#options.signal
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
- return this.captureArtifacts(
1599
- withDefinitions,
1600
- Object.keys(scenes),
1601
- signal ?? this.#options.signal
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
- parseTileflowMap(artifacts.project.maps[scene.map]),
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
- return createTileflowBuildArtifacts({
1722
- assetBaseUrl: tileflowSyntheticAssetOrigin,
1723
- config: this.#options.config ?? defaultTileflowConfigPath,
1724
- cwd
1725
- }).catch((error) => throwCaptureStyleValidationError(error));
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 authoredMap = artifacts.project.maps[normalized.map];
1796
- if (!authoredMap) {
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 = parseTileflowMap(authoredMap);
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.18",
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.21 <0.1.0-beta.0",
54
- "@tileflow/dev": ">=0.1.0-alpha.19 <0.1.0-beta.0",
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.0",
73
- "@tileflow/next": "0.1.0-alpha.18",
74
- "@tileflow/react": "0.1.0-alpha.20",
75
- "@tileflow/svelte": "0.1.0-alpha.20",
76
- "@tileflow/vue": "0.1.0-alpha.20",
77
- "@tileflow/vite": "0.1.0-alpha.18",
78
- "@tileflow/webpack": "0.1.0-alpha.18"
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",