@tileflow/dev 0.1.0-alpha.4 → 0.1.0-alpha.40

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 (45) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +257 -5
  3. package/THIRD_PARTY_NOTICES.md +309 -0
  4. package/dist/artifacts.d.ts +269 -0
  5. package/dist/artifacts.js +98 -0
  6. package/dist/chunk-AQUGMUZW.js +324 -0
  7. package/dist/chunk-AVJMTXW5.js +3147 -0
  8. package/dist/chunk-C4VPHZ3F.js +4062 -0
  9. package/dist/chunk-D4OE4ITL.js +13 -0
  10. package/dist/chunk-FOMTSCOP.js +671 -0
  11. package/dist/chunk-KDFTE4Q5.js +156 -0
  12. package/dist/chunk-ONYQNLNA.js +1108 -0
  13. package/dist/chunk-RTU7TD4M.js +172 -0
  14. package/dist/chunk-XHJBCVBB.js +241 -0
  15. package/dist/config.d.ts +33 -0
  16. package/dist/config.js +27 -0
  17. package/dist/fonts.d.ts +47 -0
  18. package/dist/fonts.js +16 -0
  19. package/dist/icons-BIHpw5Zy.d.ts +417 -0
  20. package/dist/icons.d.ts +3 -0
  21. package/dist/icons.js +41 -0
  22. package/dist/inspect.d.ts +149 -0
  23. package/dist/inspect.js +805 -0
  24. package/dist/inspector-browser.js +16955 -0
  25. package/dist/inspector-observer.js +95 -0
  26. package/dist/preview-assets-ATLLYY72.js +103 -0
  27. package/dist/preview.d.ts +31 -0
  28. package/dist/preview.js +1972 -0
  29. package/dist/server.d.ts +89 -0
  30. package/dist/server.js +3657 -0
  31. package/dist/tilesets.d.ts +89 -0
  32. package/dist/tilesets.js +6 -0
  33. package/dist/validation.d.ts +65 -0
  34. package/dist/validation.js +24 -0
  35. package/package.json +77 -13
  36. package/ui-fonts/hubot-sans-OFL.txt +93 -0
  37. package/ui-fonts/hubot-sans.woff2 +0 -0
  38. package/ui-fonts/inter-OFL.txt +92 -0
  39. package/ui-fonts/inter.woff2 +0 -0
  40. package/ui-fonts/jetbrains-mono-OFL.txt +93 -0
  41. package/ui-fonts/jetbrains-mono.woff2 +0 -0
  42. package/ui-fonts/mona-sans-OFL.txt +93 -0
  43. package/ui-fonts/mona-sans.woff2 +0 -0
  44. package/dist/index.d.ts +0 -54
  45. package/dist/index.js +0 -314
@@ -0,0 +1,3147 @@
1
+ import {
2
+ TileflowFontCompilationError,
3
+ getTileflowFontWatchPaths,
4
+ joinTileflowPublicUrl,
5
+ prepareTileflowStyleFonts,
6
+ replaceTileflowStyleFontSources
7
+ } from "./chunk-FOMTSCOP.js";
8
+ import {
9
+ getTileflowIconWatchPaths,
10
+ prepareTileflowCatalogIcons
11
+ } from "./chunk-C4VPHZ3F.js";
12
+ import {
13
+ TileflowStyleValidationError,
14
+ normalizeTileflowStyleValidationIssues,
15
+ sanitizeDiagnosticSecrets,
16
+ validateTileflowStyle
17
+ } from "./chunk-KDFTE4Q5.js";
18
+ import {
19
+ defaultTileflowConfigPath,
20
+ getTileflowMapNames,
21
+ loadValidTileflowConfigWithInputs
22
+ } from "./chunk-AQUGMUZW.js";
23
+ import {
24
+ isPathWithin
25
+ } from "./chunk-D4OE4ITL.js";
26
+ import {
27
+ inspectTileflowPmtilesForLocalUse
28
+ } from "./chunk-ONYQNLNA.js";
29
+
30
+ // src/artifacts.ts
31
+ import { createHash as createHash5 } from "crypto";
32
+ import { realpathSync as realpathSync2 } from "fs";
33
+ import {
34
+ copyFile as copyFile2,
35
+ lstat as lstat3,
36
+ mkdir as mkdir3,
37
+ mkdtemp,
38
+ open as open2,
39
+ readFile as readFile2,
40
+ realpath as realpath2,
41
+ rename as rename2,
42
+ rm as rm3,
43
+ unlink
44
+ } from "fs/promises";
45
+ import { createRequire } from "module";
46
+ import { dirname as dirname2, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
47
+ import {
48
+ compareCodeUnits as compareCodeUnits2,
49
+ getTileflowStyleFontFaces as getTileflowStyleFontFaces2,
50
+ serializeCanonicalJson as serializeCanonicalJson3,
51
+ tileflowMapIdSchema,
52
+ tileflowThemeNameSchema
53
+ } from "@tileflow/core";
54
+ import {
55
+ createManifest,
56
+ createStyleFromCatalog,
57
+ createStylesFromCatalog,
58
+ createStylesFromCatalogWithInspection,
59
+ createTileflowMapBuildManifest,
60
+ tileflowMapBuildManifestFileName
61
+ } from "@tileflow/core/build";
62
+ import { parseTileflowRuntimeManifest } from "@tileflow/core/manifest";
63
+ import {
64
+ createTileflowNativeBuildRecord as createTileflowNativeBuildRecord2,
65
+ createTileflowNativeDiagnostic as createTileflowNativeDiagnostic3,
66
+ resolveTileflowRenderer,
67
+ tileflowNativeBuildRecordFileName,
68
+ TileflowNativeCompatibilityError as TileflowNativeCompatibilityError4
69
+ } from "@tileflow/core/native-profile";
70
+
71
+ // src/image-source-assets.ts
72
+ import { createHash } from "crypto";
73
+ function prepareEmbeddedImageSources(input) {
74
+ const styles = structuredClone(input);
75
+ const assets = /* @__PURE__ */ new Map();
76
+ for (const [map, themes] of Object.entries(styles)) {
77
+ for (const style of Object.values(themes)) {
78
+ for (const source of Object.values(style.sources)) {
79
+ if (source.type !== "image" || typeof source.url !== "string") continue;
80
+ const match = /^data:image\/png;base64,([A-Za-z0-9+/]+={0,2})$/u.exec(source.url);
81
+ if (!match || match[1].length > 14 * 1024 * 1024) continue;
82
+ const bytes = Buffer.from(match[1], "base64");
83
+ if (bytes.length < 24 || !bytes.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex")))
84
+ continue;
85
+ const hash = createHash("sha256").update(bytes).digest("hex");
86
+ const fileName = `images/${map}/${hash}.png`;
87
+ assets.set(fileName, { contentType: "image/png", fileName, source: bytes });
88
+ source.url = `../../${fileName}`;
89
+ }
90
+ }
91
+ }
92
+ return { styles, assets: [...assets.values()] };
93
+ }
94
+
95
+ // src/line-background-glyphs.ts
96
+ import { createHash as createHash2, randomUUID } from "crypto";
97
+ import { lstat, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
98
+ import { resolve } from "path";
99
+ import {
100
+ deriveTileflowLineBackgroundGlyphs,
101
+ parseTileflowLineBackgroundFontStack
102
+ } from "@tileflow/core";
103
+ var tileflowLineBackgroundGlyphRoute = "__glyphs";
104
+ var RANGE = /^\d{1,6}-\d{1,6}$/u;
105
+ var FONT_STACK = /^[\p{L}\p{N} _.,-]{1,256}$/u;
106
+ var MAP_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
107
+ var MAX_RANGE_BYTES = 8 * 1024 * 1024;
108
+ var MAX_DERIVED_ENTRIES = 512;
109
+ var CACHE_SEGMENTS = [".tileflow", "cache", "glyphs", "v1"];
110
+ function usesTileflowLineBackgroundGlyphs(style) {
111
+ return style.layers.some((layer) => {
112
+ const layout = layer.layout;
113
+ const font = layout?.["text-font"];
114
+ return Array.isArray(font) && font.length === 1 && typeof font[0] === "string" && parseTileflowLineBackgroundFontStack(font[0]) !== void 0;
115
+ });
116
+ }
117
+ function prepareTileflowLineBackgroundGlyphs(styles, options) {
118
+ const base = absoluteHttpUrl(options.assetBaseUrl);
119
+ if (!base) return { providers: {}, styles };
120
+ const providers = {};
121
+ const output = {};
122
+ for (const [mapName, themes] of Object.entries(styles)) {
123
+ const family = Object.values(themes);
124
+ const upstream = family.find((style) => typeof style.glyphs === "string")?.glyphs;
125
+ if (!MAP_NAME.test(mapName) || !upstream || !absoluteHttpUrl(upstream) || !family.some(usesTileflowLineBackgroundGlyphs) || family.some((style) => style.glyphs !== upstream)) {
126
+ output[mapName] = themes;
127
+ continue;
128
+ }
129
+ providers[mapName] = upstream;
130
+ const glyphs = `${base.replace(/\/+$/u, "")}/${tileflowLineBackgroundGlyphRoute}/${mapName}/{fontstack}/{range}.pbf`;
131
+ output[mapName] = Object.fromEntries(
132
+ Object.entries(themes).map(([themeName, style]) => [themeName, { ...style, glyphs }])
133
+ );
134
+ }
135
+ return { providers, styles: output };
136
+ }
137
+ function parseTileflowLineBackgroundGlyphPath(path) {
138
+ const match = /^\/__glyphs\/([^/]+)\/([^/]+)\/([^/]+)\.pbf$/u.exec(path);
139
+ if (!match) return void 0;
140
+ let fontStack;
141
+ try {
142
+ fontStack = decodeURIComponent(match[2]);
143
+ } catch {
144
+ return void 0;
145
+ }
146
+ if (!MAP_NAME.test(match[1]) || !FONT_STACK.test(fontStack) || !RANGE.test(match[3])) {
147
+ return void 0;
148
+ }
149
+ return { fontStack, mapName: match[1], range: match[3] };
150
+ }
151
+ function createTileflowLineBackgroundGlyphResponder(options) {
152
+ const fetchRange = options.fetch ?? globalThis.fetch;
153
+ const derived = /* @__PURE__ */ new Map();
154
+ const upstreamRanges = /* @__PURE__ */ new Map();
155
+ const readUpstream = (upstream, fontStack, range) => {
156
+ const key2 = `${upstream}
157
+ ${fontStack}
158
+ ${range}`;
159
+ let pending = upstreamRanges.get(key2);
160
+ if (!pending) {
161
+ pending = loadUpstreamRange(options.cwd, fetchRange, upstream, fontStack, range).finally(
162
+ () => upstreamRanges.delete(key2)
163
+ );
164
+ upstreamRanges.set(key2, pending);
165
+ }
166
+ return pending;
167
+ };
168
+ return async (upstream, fontStack, range) => {
169
+ if (!absoluteHttpUrl(upstream) || !FONT_STACK.test(fontStack) || !RANGE.test(range)) {
170
+ return notFound();
171
+ }
172
+ const backed = parseTileflowLineBackgroundFontStack(fontStack);
173
+ if (!backed) {
174
+ if (fontStack.split(",").some((stack) => parseTileflowLineBackgroundFontStack(stack))) {
175
+ return notFound();
176
+ }
177
+ return glyphResponse(await readUpstream(upstream, fontStack, range));
178
+ }
179
+ const key2 = `${upstream}
180
+ ${fontStack}
181
+ ${range}`;
182
+ let pending = derived.get(key2);
183
+ if (!pending) {
184
+ pending = readUpstream(upstream, backed.source, range).then(
185
+ (source) => source ? deriveTileflowLineBackgroundGlyphs(source, backed.metrics) : void 0
186
+ );
187
+ pending.catch(() => derived.delete(key2));
188
+ derived.set(key2, pending);
189
+ if (derived.size > MAX_DERIVED_ENTRIES) derived.delete(derived.keys().next().value);
190
+ }
191
+ return glyphResponse(await pending);
192
+ };
193
+ }
194
+ async function loadUpstreamRange(cwd, fetchRange, upstream, fontStack, range) {
195
+ const directory = await cacheDirectory(cwd, upstream);
196
+ const file = resolve(directory, `${digest(fontStack)}-${range}.pbf`);
197
+ const cached = await readFile(file).catch((error) => {
198
+ if (hasCode(error, "ENOENT")) return void 0;
199
+ throw error;
200
+ });
201
+ if (cached) return new Uint8Array(cached);
202
+ const url = upstream.replace("{fontstack}", encodeURIComponent(fontStack)).replace("{range}", range);
203
+ const response = await fetchRange(url, {
204
+ credentials: "omit",
205
+ headers: { Accept: "application/x-protobuf" },
206
+ redirect: "error",
207
+ signal: AbortSignal.timeout(3e4)
208
+ });
209
+ if (response.status === 404) return void 0;
210
+ if (response.status !== 200) {
211
+ throw new Error(`Glyph provider responded ${response.status} for ${fontStack} ${range}.`);
212
+ }
213
+ const length = Number(response.headers.get("content-length") ?? 0);
214
+ if (length > MAX_RANGE_BYTES) throw new Error(`Glyph range ${range} exceeds the size limit.`);
215
+ const bytes = new Uint8Array(await response.arrayBuffer());
216
+ if (bytes.byteLength > MAX_RANGE_BYTES)
217
+ throw new Error(`Glyph range ${range} exceeds the size limit.`);
218
+ const temporary = `${file}.${randomUUID()}.tmp`;
219
+ try {
220
+ await writeFile(temporary, bytes, { flag: "wx" });
221
+ await rename(temporary, file);
222
+ } catch {
223
+ await rm(temporary, { force: true }).catch(() => void 0);
224
+ }
225
+ return bytes;
226
+ }
227
+ async function cacheDirectory(cwd, upstream) {
228
+ let current = resolve(cwd);
229
+ for (const segment of [...CACHE_SEGMENTS, digest(upstream)]) {
230
+ current = resolve(current, segment);
231
+ try {
232
+ const stats = await lstat(current);
233
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
234
+ throw new Error(`Glyph cache path is unsafe: ${current}`);
235
+ }
236
+ } catch (error) {
237
+ if (!hasCode(error, "ENOENT")) throw error;
238
+ await mkdir(current).catch((mkdirError) => {
239
+ if (!hasCode(mkdirError, "EEXIST")) throw mkdirError;
240
+ });
241
+ }
242
+ }
243
+ return current;
244
+ }
245
+ function glyphResponse(bytes) {
246
+ if (!bytes) return notFound();
247
+ return new Response(new Uint8Array(bytes), {
248
+ headers: {
249
+ "Access-Control-Allow-Origin": "*",
250
+ "Cache-Control": "no-cache",
251
+ "Content-Type": "application/x-protobuf"
252
+ },
253
+ status: 200
254
+ });
255
+ }
256
+ function notFound() {
257
+ return new Response(JSON.stringify({ error: "Unknown glyph range." }), {
258
+ headers: { "Content-Type": "application/json; charset=utf-8" },
259
+ status: 404
260
+ });
261
+ }
262
+ function absoluteHttpUrl(value) {
263
+ if (!value) return void 0;
264
+ try {
265
+ const url = new URL(value.replace("{fontstack}", "stack").replace("{range}", "0-255"));
266
+ return url.protocol === "http:" || url.protocol === "https:" ? value : void 0;
267
+ } catch {
268
+ return void 0;
269
+ }
270
+ }
271
+ function digest(value) {
272
+ return createHash2("sha256").update(value).digest("hex").slice(0, 32);
273
+ }
274
+ function hasCode(error, code) {
275
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
276
+ }
277
+
278
+ // src/local-tilesets.ts
279
+ import { randomUUID as randomUUID2 } from "crypto";
280
+ import { constants } from "fs";
281
+ import { chmod, copyFile, lstat as lstat2, mkdir as mkdir2, open, readdir, realpath, rm as rm2, stat } from "fs/promises";
282
+ import { basename, resolve as resolve2 } from "path";
283
+ import { compareCodeUnits } from "@tileflow/core";
284
+ var minimumLocalPmtilesBytes = 127n;
285
+ var maximumLocalPmtilesBytes = 8000000000n;
286
+ async function prepareTileflowLocalTilesets(project, styles, options) {
287
+ const hasSources = Object.values(project.maps).some(
288
+ (map) => Object.keys(map.sources ?? {}).length > 0
289
+ );
290
+ if (!hasSources) {
291
+ return { dispose: async () => void 0, files: [], styles, watchPaths: [] };
292
+ }
293
+ const logicalProjectRoot = resolve2(options.cwd);
294
+ const projectRoot = await realpath(logicalProjectRoot).catch(() => logicalProjectRoot);
295
+ const snapshotStore = resolve2(logicalProjectRoot, ".tileflow/cache/pmtiles-snapshots/v1");
296
+ await assertSafeSnapshotStore(logicalProjectRoot, snapshotStore);
297
+ await collectAbandonedSnapshotGenerations(snapshotStore);
298
+ const generationId = `snapshot-${process.pid}-${randomUUID2().replaceAll("-", "")}`;
299
+ const generationDirectory = resolve2(snapshotStore, generationId);
300
+ await mkdir2(generationDirectory);
301
+ const bindings = /* @__PURE__ */ new Map();
302
+ const files = /* @__PURE__ */ new Map();
303
+ const snapshots = /* @__PURE__ */ new Map();
304
+ const output = Object.fromEntries(
305
+ Object.entries(styles).map(([mapName, family]) => [
306
+ mapName,
307
+ Object.fromEntries(
308
+ Object.entries(family).map(([themeName, style]) => [themeName, cloneStyle(style)])
309
+ )
310
+ ])
311
+ );
312
+ let disposed = false;
313
+ const dispose = async () => {
314
+ if (disposed) return;
315
+ disposed = true;
316
+ if (!isPathWithin(snapshotStore, generationDirectory)) {
317
+ throw new Error("Local PMTiles snapshot generation escaped its store.");
318
+ }
319
+ await rm2(generationDirectory, { force: true, recursive: true });
320
+ };
321
+ try {
322
+ for (const mapName of Object.keys(project.maps).sort(compareCodeUnits)) {
323
+ const map = project.maps[mapName];
324
+ for (const [sourceId, source] of Object.entries(map.sources ?? {}).sort(
325
+ ([left], [right]) => compareCodeUnits(left, right)
326
+ )) {
327
+ const requestedPath = resolve2(options.baseDirectory, source.local);
328
+ const sourcePath = await realpath(requestedPath).catch(() => requestedPath);
329
+ if (!isPathWithin(projectRoot, sourcePath)) {
330
+ throw localTilesetError(
331
+ "TF_LOCAL_TILESET_OUTSIDE_ROOT",
332
+ `maps.${mapName}.sources.${sourceId}.local`,
333
+ "Local hosted tileset must stay inside the project root."
334
+ );
335
+ }
336
+ const sourceStat = await lstat2(sourcePath).catch(() => null);
337
+ if (!sourceStat?.isFile() || sourceStat.isSymbolicLink()) {
338
+ throw localTilesetError(
339
+ "TF_LOCAL_TILESET_NOT_FOUND",
340
+ `maps.${mapName}.sources.${sourceId}.local`,
341
+ `Local hosted tileset "${sourceId}" was not found.`
342
+ );
343
+ }
344
+ let snapshot = snapshots.get(sourcePath);
345
+ if (!snapshot) {
346
+ snapshot = await createLocalTilesetSnapshot(
347
+ sourcePath,
348
+ generationDirectory,
349
+ snapshots.size
350
+ );
351
+ snapshots.set(sourcePath, snapshot);
352
+ }
353
+ const file = {
354
+ byteLength: snapshot.byteLength,
355
+ etag: snapshot.etag,
356
+ fileName: `tilesets/${source.tileset}.pmtiles`,
357
+ logicalId: source.tileset,
358
+ sourcePath: snapshot.sourcePath
359
+ };
360
+ const existing = bindings.get(source.tileset);
361
+ if (existing && existing.originalSourcePath !== sourcePath) {
362
+ throw localTilesetError(
363
+ "TF_LOCAL_TILESET_BINDING_CONFLICT",
364
+ `maps.${mapName}.sources.${sourceId}.local`,
365
+ `Logical tileset "${source.tileset}" resolves to different local archives in one generation.`
366
+ );
367
+ }
368
+ bindings.set(source.tileset, { file, originalSourcePath: sourcePath });
369
+ files.set(file.fileName, files.get(file.fileName) ?? file);
370
+ validateSourceContract(
371
+ mapName,
372
+ sourceId,
373
+ source.type,
374
+ output[mapName] ?? {},
375
+ snapshot.inspection
376
+ );
377
+ const publicUrl = joinTileflowPublicUrl(options.assetBaseUrl, file.fileName);
378
+ for (const style of Object.values(output[mapName] ?? {})) {
379
+ const definition = style.sources[sourceId];
380
+ if (!definition) {
381
+ throw localTilesetError(
382
+ "TF_LOCAL_TILESET_SOURCE_MISSING",
383
+ `maps.${mapName}.sources.${sourceId}`,
384
+ `Compiled style is missing hosted source "${sourceId}".`
385
+ );
386
+ }
387
+ definition.url = `tileflow-pmtiles://${publicUrl}`;
388
+ delete definition.tiles;
389
+ }
390
+ }
391
+ }
392
+ return {
393
+ dispose,
394
+ files: [...files.values()].sort(
395
+ (left, right) => compareCodeUnits(left.fileName, right.fileName)
396
+ ),
397
+ styles: output,
398
+ watchPaths: [...snapshots.keys()].sort(compareCodeUnits)
399
+ };
400
+ } catch (error) {
401
+ await dispose().catch(() => void 0);
402
+ throw error;
403
+ }
404
+ }
405
+ async function createLocalTilesetSnapshot(sourcePath, generationDirectory, index) {
406
+ const sourceHandle = await open(sourcePath, "r");
407
+ const snapshotPath = resolve2(generationDirectory, `${index}.pmtiles`);
408
+ try {
409
+ const before = sourceIdentity(await sourceHandle.stat({ bigint: true }));
410
+ if (before.size < minimumLocalPmtilesBytes || before.size > maximumLocalPmtilesBytes) {
411
+ throw localTilesetError(
412
+ "TF_TILESET_ARCHIVE_INVALID",
413
+ "archive",
414
+ "Expected one bounded PMTiles file."
415
+ );
416
+ }
417
+ await copyFile(sourcePath, snapshotPath, constants.COPYFILE_FICLONE);
418
+ await syncFile(snapshotPath);
419
+ const [afterHandle, afterPath, snapshotStat] = await Promise.all([
420
+ sourceHandle.stat({ bigint: true }).then(sourceIdentity),
421
+ stat(sourcePath, { bigint: true }).then(sourceIdentity),
422
+ stat(snapshotPath, { bigint: true })
423
+ ]);
424
+ assertSourceIdentity(before, afterHandle);
425
+ assertSourceIdentity(before, afterPath);
426
+ if (!snapshotStat.isFile() || snapshotStat.size !== before.size) {
427
+ throw localTilesetError(
428
+ "TF_LOCAL_TILESET_CHANGED",
429
+ "local",
430
+ "Local hosted tileset changed while its immutable snapshot was prepared."
431
+ );
432
+ }
433
+ const inspection = await inspectTileflowPmtilesForLocalUse(snapshotPath);
434
+ await chmod(snapshotPath, 292);
435
+ return Object.freeze({
436
+ byteLength: Number(before.size),
437
+ etag: `"${generationIdFromDirectory(generationDirectory)}-${index}"`,
438
+ inspection,
439
+ sourcePath: snapshotPath
440
+ });
441
+ } finally {
442
+ await sourceHandle.close();
443
+ }
444
+ }
445
+ function generationIdFromDirectory(path) {
446
+ const generationId = basename(path);
447
+ if (!/^snapshot-\d+-[a-f0-9]{32}$/u.test(generationId)) {
448
+ throw new Error("Local PMTiles snapshot generation is invalid.");
449
+ }
450
+ return generationId;
451
+ }
452
+ function sourceIdentity(value) {
453
+ if (!value.isFile()) throw new Error("Local PMTiles source is not a file.");
454
+ return Object.freeze({
455
+ device: BigInt(value.dev),
456
+ inode: BigInt(value.ino),
457
+ mtimeNanoseconds: "mtimeNs" in value ? BigInt(value.mtimeNs) : BigInt(Math.round(value.mtimeMs * 1e6)),
458
+ size: BigInt(value.size)
459
+ });
460
+ }
461
+ function assertSourceIdentity(before, after) {
462
+ if (before.device !== after.device || before.inode !== after.inode || before.mtimeNanoseconds !== after.mtimeNanoseconds || before.size !== after.size) {
463
+ throw localTilesetError(
464
+ "TF_LOCAL_TILESET_CHANGED",
465
+ "local",
466
+ "Local hosted tileset changed while its immutable snapshot was prepared."
467
+ );
468
+ }
469
+ }
470
+ function validateSourceContract(mapName, sourceId, sourceType, styles, inspection) {
471
+ const isVectorArchive = inspection.contract.tileType === "mlt" || inspection.contract.tileType === "mvt";
472
+ if (sourceType === "vector" !== isVectorArchive) {
473
+ throw localTilesetError(
474
+ "TF_SOURCE_TILE_TYPE_MISMATCH",
475
+ `maps.${mapName}.sources.${sourceId}.type`,
476
+ `Source "${sourceId}" declares ${sourceType} but the PMTiles archive contains ${inspection.contract.tileType}.`
477
+ );
478
+ }
479
+ if (!isVectorArchive || !inspection.contract.sourceLayersDeclared) return;
480
+ const availableLayers = new Map(
481
+ inspection.contract.sourceLayers.map((layer) => [layer.id, layer])
482
+ );
483
+ for (const style of Object.values(styles)) {
484
+ const requirements = asRecord(style.metadata?.["tileflow:sourceRequirements"]);
485
+ const sources = asRecord(requirements?.sources);
486
+ const source = asRecord(sources?.[sourceId]);
487
+ const sourceLayers = Array.isArray(source?.sourceLayers) ? source.sourceLayers : [];
488
+ for (const candidate of sourceLayers) {
489
+ const required = asRecord(candidate);
490
+ const layerId = required?.id;
491
+ if (typeof layerId !== "string") continue;
492
+ const available = availableLayers.get(layerId);
493
+ if (!available) {
494
+ const candidates = [...availableLayers.keys()].sort(compareCodeUnits);
495
+ throw localTilesetError(
496
+ "TF_SOURCE_LAYER_NOT_FOUND",
497
+ `maps.${mapName}.sources.${sourceId}.sourceLayers.${layerId}`,
498
+ `Source "${sourceId}" has no source-layer "${layerId}". Available: ${candidates.join(", ") || "(none)"}.`
499
+ );
500
+ }
501
+ if (!available.fieldsDeclared) continue;
502
+ const availableFields = new Map(available.fields.map((field) => [field.name, field.type]));
503
+ const requiredFields = Array.isArray(required?.fields) ? required.fields : [];
504
+ for (const fieldCandidate of requiredFields) {
505
+ const field = asRecord(fieldCandidate);
506
+ const fieldName = field?.name;
507
+ if (typeof fieldName !== "string") continue;
508
+ if (!availableFields.has(fieldName)) {
509
+ const candidates = [...availableFields.keys()].sort(compareCodeUnits);
510
+ throw localTilesetError(
511
+ "TF_SOURCE_FIELD_NOT_FOUND",
512
+ `maps.${mapName}.sources.${sourceId}.sourceLayers.${layerId}.fields.${fieldName}`,
513
+ `Source-layer "${layerId}" has no field "${fieldName}". Available: ${candidates.join(", ") || "(none)"}.`
514
+ );
515
+ }
516
+ const requiredType = field?.type;
517
+ const actualType = availableFields.get(fieldName);
518
+ if (typeof requiredType === "string" && actualType !== void 0 && requiredType !== actualType) {
519
+ throw localTilesetError(
520
+ "TF_SOURCE_FIELD_TYPE_MISMATCH",
521
+ `maps.${mapName}.sources.${sourceId}.sourceLayers.${layerId}.fields.${fieldName}`,
522
+ `Field "${layerId}.${fieldName}" is ${actualType}; ${requiredType} is required.`
523
+ );
524
+ }
525
+ }
526
+ }
527
+ }
528
+ }
529
+ async function collectAbandonedSnapshotGenerations(store) {
530
+ const entries = await readdir(store, { withFileTypes: true }).catch(() => []);
531
+ for (const entry of entries) {
532
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
533
+ const match = /^snapshot-(\d+)-[a-f0-9]{32}$/u.exec(entry.name);
534
+ if (!match?.[1] || processIsAlive(Number(match[1]))) continue;
535
+ const path = resolve2(store, entry.name);
536
+ if (isPathWithin(store, path)) await rm2(path, { force: true, recursive: true });
537
+ }
538
+ }
539
+ async function assertSafeSnapshotStore(projectRoot, store) {
540
+ const segments = [".tileflow", "cache", "pmtiles-snapshots", "v1"];
541
+ let current = projectRoot;
542
+ for (const segment of segments) {
543
+ current = resolve2(current, segment);
544
+ try {
545
+ const value = await lstat2(current);
546
+ if (!value.isDirectory() || value.isSymbolicLink()) {
547
+ throw new Error(`Local PMTiles snapshot path is unsafe: ${current}`);
548
+ }
549
+ } catch (error) {
550
+ if (!hasErrorCode(error, "ENOENT")) throw error;
551
+ await mkdir2(current).catch((mkdirError) => {
552
+ if (!hasErrorCode(mkdirError, "EEXIST")) throw mkdirError;
553
+ });
554
+ const created = await lstat2(current);
555
+ if (!created.isDirectory() || created.isSymbolicLink()) {
556
+ throw new Error(`Local PMTiles snapshot path is unsafe: ${current}`);
557
+ }
558
+ }
559
+ }
560
+ if (resolve2(current) !== resolve2(store)) {
561
+ throw new Error("Local PMTiles snapshot path escaped its project root.");
562
+ }
563
+ }
564
+ function processIsAlive(pid) {
565
+ try {
566
+ process.kill(pid, 0);
567
+ return true;
568
+ } catch (error) {
569
+ return !hasErrorCode(error, "ESRCH");
570
+ }
571
+ }
572
+ async function syncFile(path) {
573
+ const handle = await open(path, "r");
574
+ try {
575
+ await handle.sync();
576
+ } finally {
577
+ await handle.close();
578
+ }
579
+ }
580
+ function cloneStyle(style) {
581
+ return JSON.parse(JSON.stringify(style));
582
+ }
583
+ function localTilesetError(code, path, message) {
584
+ return Object.assign(new Error(message), { code, path, phase: "local-tileset-resolution" });
585
+ }
586
+ function asRecord(value) {
587
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
588
+ }
589
+ function hasErrorCode(error, code) {
590
+ return Boolean(error && typeof error === "object" && error.code === code);
591
+ }
592
+
593
+ // src/native-artifacts.ts
594
+ import {
595
+ convertFilter,
596
+ validateStyleMin
597
+ } from "@maplibre/maplibre-gl-style-spec";
598
+ import { createHash as createHash3 } from "crypto";
599
+ import {
600
+ getTileflowStyleFontFaces,
601
+ serializeCanonicalJson,
602
+ steadyTileflowNauticalLights,
603
+ tileflowIconIdPattern,
604
+ tileflowIconPackageLimits,
605
+ tileflowStyleFontFacesMetadataKey
606
+ } from "@tileflow/core";
607
+ import {
608
+ prepareTileflowNativePathGround
609
+ } from "@tileflow/core/build";
610
+ import {
611
+ createTileflowNativeDiagnostic,
612
+ TileflowNativeCompatibilityError,
613
+ tileflowNativeProfileLimits,
614
+ validateTileflowNativePreparedStyle,
615
+ validateTileflowNativeStyle
616
+ } from "@tileflow/core/native-profile";
617
+
618
+ // src/native-layer-references.ts
619
+ var overlayKeys = [
620
+ "above-water",
621
+ "below-roads",
622
+ "above-roads",
623
+ "above-buildings",
624
+ "below-labels",
625
+ "above-labels"
626
+ ];
627
+ function object(value) {
628
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error();
629
+ return value;
630
+ }
631
+ function identifier(layer) {
632
+ if (typeof layer.id !== "string" || !layer.id.length) throw new Error();
633
+ return layer.id;
634
+ }
635
+ function rebindNativeLayerReferences(input, lowered, changes) {
636
+ try {
637
+ const originalIndexes = /* @__PURE__ */ new Map();
638
+ const finalIndexes = /* @__PURE__ */ new Map();
639
+ const starts = /* @__PURE__ */ new Map();
640
+ const transformed = /* @__PURE__ */ new Map();
641
+ let previous = -1;
642
+ for (const change of changes) {
643
+ if (!Number.isSafeInteger(change.inputLayer) || change.inputLayer <= previous || change.inputLayer >= input.layers.length || !Number.isSafeInteger(change.outputStart) || !Number.isSafeInteger(change.outputCount) || change.outputCount < 1 || change.outputCount > 32 || !Array.isArray(change.properties) || !["line-cap", "line-dasharray", "line-cap,line-dasharray"].includes(
644
+ change.properties.join(",")
645
+ ))
646
+ throw new Error();
647
+ previous = change.inputLayer;
648
+ transformed.set(change.inputLayer, change);
649
+ }
650
+ for (const [index, layer] of lowered.layers.entries()) {
651
+ const id = identifier(layer);
652
+ if (finalIndexes.has(id)) throw new Error();
653
+ finalIndexes.set(id, index);
654
+ }
655
+ let offset = 0;
656
+ for (const [index, layer] of input.layers.entries()) {
657
+ const id = identifier(layer);
658
+ if (originalIndexes.has(id)) throw new Error();
659
+ originalIndexes.set(id, index);
660
+ starts.set(id, offset);
661
+ const change = transformed.get(index);
662
+ if (change && (change.outputStart !== offset || layer.type !== "line")) throw new Error();
663
+ const count = change?.outputCount ?? 1;
664
+ if (offset + count > lowered.layers.length) throw new Error();
665
+ for (let branch = 0; branch < count; branch++) {
666
+ const candidate = lowered.layers[offset + branch];
667
+ if (!change && identifier(candidate) !== id) throw new Error();
668
+ if (candidate.type !== layer.type || candidate.source !== layer.source || candidate["source-layer"] !== layer["source-layer"])
669
+ throw new Error();
670
+ }
671
+ offset += count;
672
+ }
673
+ if (offset !== lowered.layers.length) throw new Error();
674
+ const metadata = lowered.metadata === void 0 ? void 0 : { ...object(lowered.metadata) };
675
+ const originalMetadata = input.metadata === void 0 ? void 0 : object(input.metadata);
676
+ if (originalMetadata?.["tileflow:overlay-placement-manifest"] !== void 0) {
677
+ if (!metadata) throw new Error();
678
+ const overlay = object(originalMetadata["tileflow:overlay-placement-manifest"]);
679
+ const anchors = object(overlay.anchors);
680
+ if (overlay.schemaVersion !== 1 || Object.keys(anchors).length !== overlayKeys.length || overlayKeys.some((key2) => !Object.hasOwn(anchors, key2)))
681
+ throw new Error();
682
+ const updated = {};
683
+ for (const key2 of overlayKeys) {
684
+ const anchor = anchors[key2];
685
+ if (anchor === null) {
686
+ updated[key2] = null;
687
+ continue;
688
+ }
689
+ if (typeof anchor !== "string" || !starts.has(anchor)) throw new Error();
690
+ updated[key2] = identifier(lowered.layers[starts.get(anchor)]);
691
+ }
692
+ metadata["tileflow:overlay-placement-manifest"] = { ...overlay, anchors: updated };
693
+ }
694
+ if (originalMetadata?.["tileflow:interaction-manifest"] !== void 0) {
695
+ if (!metadata) throw new Error();
696
+ const manifest = object(originalMetadata["tileflow:interaction-manifest"]);
697
+ if (manifest.version !== 3) throw new Error();
698
+ const domains = object(manifest.domains);
699
+ let rebound = manifest;
700
+ if (domains.poi !== void 0) {
701
+ const poi = object(domains.poi);
702
+ if (!Array.isArray(poi.layers) || poi.layers.length > 256) throw new Error();
703
+ const seen = /* @__PURE__ */ new Set();
704
+ const layers = poi.layers.map((value) => {
705
+ const entry = object(value);
706
+ if (typeof entry.layerId !== "string" || seen.has(entry.layerId)) throw new Error();
707
+ const index = originalIndexes.get(entry.layerId);
708
+ if (index === void 0 || index !== entry.priority || transformed.has(index) || !finalIndexes.has(entry.layerId))
709
+ throw new Error();
710
+ const original = input.layers[index];
711
+ if (original.source !== entry.source || original["source-layer"] !== entry.sourceLayer)
712
+ throw new Error();
713
+ seen.add(entry.layerId);
714
+ return { ...entry, priority: finalIndexes.get(entry.layerId) };
715
+ });
716
+ rebound = { ...manifest, domains: { ...domains, poi: { ...poi, layers } } };
717
+ }
718
+ metadata["tileflow:interaction-manifest"] = rebound;
719
+ }
720
+ return metadata === void 0 ? { ...lowered } : { ...lowered, metadata };
721
+ } catch {
722
+ return void 0;
723
+ }
724
+ }
725
+
726
+ // src/native-lowering.ts
727
+ var nativeLoweringLimits = Object.freeze({
728
+ maximumDecisionDepth: 16,
729
+ maximumDecisionNodes: 512,
730
+ maximumPropertyBranches: 16,
731
+ maximumPathsPerProperty: 64,
732
+ maximumLayerVariants: 32,
733
+ maximumZoomIntervals: 32,
734
+ maximumLayers: 4096,
735
+ maximumDashLength: 64
736
+ });
737
+ var overlapProperties = ["icon-overlap", "text-overlap"];
738
+ var NativeLoweringError = class extends Error {
739
+ constructor(path, reason = "shape") {
740
+ super(
741
+ reason === "budget" ? "Native decision lowering exceeds its finite budget." : "This native decision cannot be lowered without changing its meaning."
742
+ );
743
+ this.path = path;
744
+ this.reason = reason;
745
+ this.name = "NativeLoweringError";
746
+ }
747
+ path;
748
+ reason;
749
+ };
750
+ function fail(path, reason = "shape") {
751
+ throw new NativeLoweringError(path, reason);
752
+ }
753
+ function object2(value) {
754
+ return value !== null && typeof value === "object" && !Array.isArray(value);
755
+ }
756
+ function operation(value, name, length) {
757
+ return Array.isArray(value) && value[0] === name && (length === void 0 || value.length === length);
758
+ }
759
+ function finite(value) {
760
+ return typeof value === "number" && Number.isFinite(value);
761
+ }
762
+ function key(value) {
763
+ return JSON.stringify(value);
764
+ }
765
+ function negate(value) {
766
+ return typeof value === "boolean" ? !value : operation(value, "!", 2) ? value[1] : ["!", value];
767
+ }
768
+ function combine(operator, values) {
769
+ const identity = operator === "all";
770
+ const result = [];
771
+ const seen = /* @__PURE__ */ new Set();
772
+ for (const value of values.flatMap(
773
+ (item) => operation(item, operator) ? item.slice(1) : [item]
774
+ )) {
775
+ if (value === identity) continue;
776
+ if (value === !identity || seen.has(key(negate(value)))) return !identity;
777
+ const serialized = key(value);
778
+ if (!seen.has(serialized)) {
779
+ seen.add(serialized);
780
+ result.push(value);
781
+ }
782
+ }
783
+ return result.length === 0 ? identity : result.length === 1 ? result[0] : [operator, ...result];
784
+ }
785
+ function choose(when, yes, no) {
786
+ if (when === true) return yes;
787
+ if (when === false) return no;
788
+ if (key(yes) === key(no)) return yes;
789
+ if (yes === true) return combine("any", [when, no]);
790
+ if (yes === false) return combine("all", [negate(when), no]);
791
+ if (no === true) return combine("any", [negate(when), yes]);
792
+ if (no === false) return combine("all", [when, yes]);
793
+ return ["case", when, yes, no];
794
+ }
795
+ function decision(arms, fallback) {
796
+ const active = [];
797
+ for (const arm of arms) {
798
+ if (arm.when === false) continue;
799
+ if (arm.when === true) {
800
+ fallback = arm.value;
801
+ break;
802
+ }
803
+ const previous = active.at(-1);
804
+ if (previous && key(previous.value) === key(arm.value)) {
805
+ previous.when = combine("any", [previous.when, arm.when]);
806
+ } else active.push({ ...arm });
807
+ }
808
+ while (active.length && key(active.at(-1).value) === key(fallback)) active.pop();
809
+ if (!active.length) return fallback;
810
+ if (active.length === 1) return choose(active[0].when, active[0].value, fallback);
811
+ return ["case", ...active.flatMap(({ when, value }) => [when, value]), fallback];
812
+ }
813
+ var Decisions = class {
814
+ constructor(path, vector) {
815
+ this.path = path;
816
+ this.vector = vector;
817
+ }
818
+ path;
819
+ vector;
820
+ visited = 0;
821
+ cuts = /* @__PURE__ */ new Set();
822
+ tick(depth) {
823
+ if (++this.visited > nativeLoweringLimits.maximumDecisionNodes || depth > nativeLoweringLimits.maximumDecisionDepth) {
824
+ fail(this.path, "budget");
825
+ }
826
+ }
827
+ tree(value, property, depth = 0) {
828
+ this.tick(depth);
829
+ const leaf = operation(value, "literal", 2) ? value[1] : value;
830
+ if (property === "line-cap" && typeof leaf === "string" && ["butt", "round", "square"].includes(leaf)) {
831
+ return { kind: "leaf", value: leaf };
832
+ }
833
+ if (property === "line-dasharray" && Array.isArray(leaf) && leaf.length > 0 && leaf.length <= nativeLoweringLimits.maximumDashLength && leaf.every((item) => finite(item) && item >= 0)) {
834
+ return { kind: "leaf", value: [...leaf] };
835
+ }
836
+ if (operation(value, "case") && value.length >= 4 && value.length % 2 === 0) {
837
+ const arms = [];
838
+ for (let index = 1; index < value.length - 1; index += 2) {
839
+ const when = this.predicate(value[index], depth + 1);
840
+ arms.push({ when, value: this.tree(value[index + 1], property, depth + 1) });
841
+ }
842
+ return { kind: "case", arms, fallback: this.tree(value.at(-1), property, depth + 1) };
843
+ }
844
+ if (operation(value, "match") && value.length >= 5 && value.length % 2 === 1) {
845
+ this.scalar(value[1], depth + 1);
846
+ const labels = this.matchLabels(value, depth + 1);
847
+ return {
848
+ kind: "case",
849
+ arms: labels.map((when, index) => ({
850
+ when,
851
+ value: this.tree(value[index * 2 + 3], property, depth + 1)
852
+ })),
853
+ fallback: this.tree(value.at(-1), property, depth + 1)
854
+ };
855
+ }
856
+ if (operation(value, "step") && value.length >= 5 && value.length % 2 === 1) {
857
+ if (this.scalar(value[1], depth + 1) !== "number") fail(this.path);
858
+ const stops = [];
859
+ let previous = -Infinity;
860
+ for (let index = 3; index < value.length; index += 2) {
861
+ const at = value[index];
862
+ if (!finite(at) || at <= previous) fail(this.path);
863
+ previous = at;
864
+ if (operation(value[1], "zoom", 1)) this.cuts.add(Math.ceil(at));
865
+ stops.push({ at, value: this.tree(value[index + 1], property, depth + 1) });
866
+ }
867
+ return {
868
+ kind: "step",
869
+ input: value[1],
870
+ fallback: this.tree(value[2], property, depth + 1),
871
+ stops
872
+ };
873
+ }
874
+ return fail(this.path);
875
+ }
876
+ scalar(value, depth) {
877
+ this.tick(depth);
878
+ if (finite(value)) return "number";
879
+ if (value === null || typeof value === "boolean" || typeof value === "string") return "scalar";
880
+ if (operation(value, "zoom", 1)) return "number";
881
+ if (operation(value, "get", 2) && typeof value[1] === "string" && this.vector) return "scalar";
882
+ if (operation(value, "geometry-type", 1) || operation(value, "id", 1) && this.vector)
883
+ return "scalar";
884
+ if (operation(value, "to-string", 2)) {
885
+ if (operation(value[1], "zoom", 1)) fail(this.path);
886
+ this.scalar(value[1], depth + 1);
887
+ return "scalar";
888
+ }
889
+ if (operation(value, "coalesce") && value.length >= 3) {
890
+ for (const child of value.slice(1)) {
891
+ if (operation(child, "zoom", 1)) fail(this.path);
892
+ this.scalar(child, depth + 1);
893
+ }
894
+ return "scalar";
895
+ }
896
+ if (operation(value, "to-number", 3) && finite(value[2])) {
897
+ if (operation(value[1], "zoom", 1)) fail(this.path);
898
+ this.scalar(value[1], depth + 1);
899
+ return "number";
900
+ }
901
+ return fail(this.path);
902
+ }
903
+ matchLabels(value, depth) {
904
+ const seen = /* @__PURE__ */ new Set();
905
+ let labelType;
906
+ const predicates = [];
907
+ for (let index = 2; index < value.length - 1; index += 2) {
908
+ const labels = Array.isArray(value[index]) ? value[index] : [value[index]];
909
+ if (!labels.length) fail(this.path);
910
+ const terms = [];
911
+ for (const label of labels) {
912
+ this.tick(depth);
913
+ if (!(typeof label === "string" || finite(label) && Number.isInteger(label)))
914
+ fail(this.path);
915
+ if (labelType !== void 0 && labelType !== typeof label) fail(this.path);
916
+ labelType = typeof label;
917
+ const encoded = key(label);
918
+ if (seen.has(encoded)) fail(this.path);
919
+ seen.add(encoded);
920
+ if (operation(value[1], "zoom", 1)) {
921
+ if (!finite(label)) fail(this.path);
922
+ this.cuts.add(label);
923
+ this.cuts.add(label + 1);
924
+ }
925
+ terms.push(["==", value[1], label]);
926
+ }
927
+ predicates.push(combine("any", terms));
928
+ }
929
+ return predicates;
930
+ }
931
+ predicate(value, depth) {
932
+ this.tick(depth);
933
+ if (typeof value === "boolean") return value;
934
+ if (operation(value, "literal", 2) && typeof value[1] === "boolean") return value[1];
935
+ if (operation(value, "all") || operation(value, "any")) {
936
+ return combine(
937
+ value[0],
938
+ value.slice(1).map((child) => this.predicate(child, depth + 1))
939
+ );
940
+ }
941
+ if (operation(value, "!", 2)) return negate(this.predicate(value[1], depth + 1));
942
+ if (operation(value, "has", 2) && typeof value[1] === "string") return value;
943
+ if (operation(value, "match") && value.length >= 5 && value.length % 2 === 1) {
944
+ this.scalar(value[1], depth + 1);
945
+ const conditions = this.matchLabels(value, depth + 1);
946
+ let remaining = true;
947
+ const terms = [];
948
+ for (const [index, condition] of conditions.entries()) {
949
+ terms.push(combine("all", [condition, this.predicate(value[index * 2 + 3], depth + 1)]));
950
+ remaining = combine("all", [remaining, negate(condition)]);
951
+ }
952
+ terms.push(combine("all", [remaining, this.predicate(value.at(-1), depth + 1)]));
953
+ return combine("any", terms);
954
+ }
955
+ if (operation(value, "case") && value.length >= 4 && value.length % 2 === 0) {
956
+ let remaining = true;
957
+ const terms = [];
958
+ for (let index = 1; index < value.length - 1; index += 2) {
959
+ const condition = this.predicate(value[index], depth + 1);
960
+ terms.push(
961
+ combine("all", [remaining, condition, this.predicate(value[index + 1], depth + 1)])
962
+ );
963
+ remaining = combine("all", [remaining, negate(condition)]);
964
+ }
965
+ terms.push(combine("all", [remaining, this.predicate(value.at(-1), depth + 1)]));
966
+ return combine("any", terms);
967
+ }
968
+ if (operation(value, "step") && value.length >= 5 && value.length % 2 === 1) {
969
+ if (this.scalar(value[1], depth + 1) !== "number") fail(this.path);
970
+ let previous = -Infinity;
971
+ let remaining = true;
972
+ let output = this.predicate(value[2], depth + 1);
973
+ const terms = [];
974
+ for (let index = 3; index < value.length; index += 2) {
975
+ const at = value[index];
976
+ if (!finite(at) || at <= previous) fail(this.path);
977
+ previous = at;
978
+ if (operation(value[1], "zoom", 1)) this.cuts.add(Math.ceil(at));
979
+ const below = ["<", value[1], at];
980
+ terms.push(combine("all", [remaining, below, output]));
981
+ remaining = combine("all", [remaining, negate(below)]);
982
+ output = this.predicate(value[index + 1], depth + 1);
983
+ }
984
+ terms.push(combine("all", [remaining, output]));
985
+ return combine("any", terms);
986
+ }
987
+ if (operation(value, "in", 3) && operation(value[2], "literal", 2) && Array.isArray(value[2][1])) {
988
+ this.scalar(value[1], depth + 1);
989
+ if (operation(value[1], "zoom", 1)) fail(this.path);
990
+ for (const item of value[2][1]) {
991
+ this.tick(depth + 1);
992
+ if (!(item === null || typeof item === "boolean" || typeof item === "string" || finite(item)))
993
+ fail(this.path);
994
+ }
995
+ return value;
996
+ }
997
+ if (Array.isArray(value) && value.length === 3 && ["==", "!=", "<", "<=", ">", ">="].includes(String(value[0]))) {
998
+ const left = this.scalar(value[1], depth + 1);
999
+ const right = this.scalar(value[2], depth + 1);
1000
+ if (!["==", "!="].includes(String(value[0])) && (left !== "number" || right !== "number"))
1001
+ fail(this.path);
1002
+ if (operation(value[1], "zoom", 1) || operation(value[2], "zoom", 1)) {
1003
+ const constant = operation(value[1], "zoom", 1) ? value[2] : value[1];
1004
+ if (!finite(constant)) fail(this.path);
1005
+ this.cuts.add(Math.ceil(constant));
1006
+ this.cuts.add(Math.floor(constant) + 1);
1007
+ }
1008
+ return value;
1009
+ }
1010
+ return fail(this.path);
1011
+ }
1012
+ atZoom(value, zoom) {
1013
+ if (typeof value === "boolean") return value;
1014
+ const expr = value;
1015
+ if (expr[0] === "all" || expr[0] === "any")
1016
+ return combine(
1017
+ expr[0],
1018
+ expr.slice(1).map((child) => this.atZoom(child, zoom))
1019
+ );
1020
+ if (expr[0] === "!") return negate(this.atZoom(expr[1], zoom));
1021
+ if (operation(expr[1], "zoom", 1) || operation(expr[2], "zoom", 1)) {
1022
+ const left = operation(expr[1], "zoom", 1) ? zoom : expr[1];
1023
+ const right = operation(expr[2], "zoom", 1) ? zoom : expr[2];
1024
+ switch (expr[0]) {
1025
+ case "==":
1026
+ return left === right;
1027
+ case "!=":
1028
+ return left !== right;
1029
+ case "<":
1030
+ return left < right;
1031
+ case "<=":
1032
+ return left <= right;
1033
+ case ">":
1034
+ return left > right;
1035
+ case ">=":
1036
+ return left >= right;
1037
+ }
1038
+ }
1039
+ return expr;
1040
+ }
1041
+ outcomes(tree, zoom) {
1042
+ const leaves = [];
1043
+ const walk = (node, guard) => {
1044
+ if (guard === false) return;
1045
+ if (node.kind === "leaf") {
1046
+ leaves.push({ when: guard, value: node.value });
1047
+ if (leaves.length > nativeLoweringLimits.maximumPathsPerProperty) fail(this.path, "budget");
1048
+ } else if (node.kind === "case") {
1049
+ let remaining = guard;
1050
+ for (const arm of node.arms) {
1051
+ const condition = this.atZoom(arm.when, zoom);
1052
+ walk(arm.value, combine("all", [remaining, condition]));
1053
+ remaining = combine("all", [remaining, negate(condition)]);
1054
+ }
1055
+ walk(node.fallback, remaining);
1056
+ } else if (operation(node.input, "zoom", 1)) {
1057
+ let selected = node.fallback;
1058
+ for (const stop of node.stops) {
1059
+ if (zoom < stop.at) break;
1060
+ selected = stop.value;
1061
+ }
1062
+ walk(selected, guard);
1063
+ } else {
1064
+ let previous = node.fallback;
1065
+ let remaining = guard;
1066
+ for (const stop of node.stops) {
1067
+ const below = ["<", node.input, stop.at];
1068
+ walk(previous, combine("all", [remaining, below]));
1069
+ remaining = combine("all", [remaining, negate(below)]);
1070
+ previous = stop.value;
1071
+ }
1072
+ walk(previous, remaining);
1073
+ }
1074
+ };
1075
+ walk(tree, true);
1076
+ const grouped = /* @__PURE__ */ new Map();
1077
+ for (const outcome of leaves) {
1078
+ const id = key(outcome.value);
1079
+ if (!grouped.has(id)) grouped.set(id, { when: true, value: outcome.value });
1080
+ }
1081
+ if (grouped.size > nativeLoweringLimits.maximumPropertyBranches) fail(this.path, "budget");
1082
+ return [...grouped.entries()].map(([id, outcome]) => ({
1083
+ value: outcome.value,
1084
+ when: this.accepts(tree, id, zoom)
1085
+ }));
1086
+ }
1087
+ accepts(tree, outcome, zoom) {
1088
+ if (tree.kind === "leaf") return key(tree.value) === outcome;
1089
+ if (tree.kind === "case") {
1090
+ return decision(
1091
+ tree.arms.map((arm) => ({
1092
+ when: this.atZoom(arm.when, zoom),
1093
+ value: this.accepts(arm.value, outcome, zoom)
1094
+ })),
1095
+ this.accepts(tree.fallback, outcome, zoom)
1096
+ );
1097
+ }
1098
+ if (operation(tree.input, "zoom", 1)) {
1099
+ let selected = tree.fallback;
1100
+ for (const stop of tree.stops) {
1101
+ if (zoom < stop.at) break;
1102
+ selected = stop.value;
1103
+ }
1104
+ return this.accepts(selected, outcome, zoom);
1105
+ }
1106
+ return decision(
1107
+ tree.stops.map((stop, index) => ({
1108
+ when: ["<", tree.input, stop.at],
1109
+ value: this.accepts(
1110
+ index === 0 ? tree.fallback : tree.stops[index - 1].value,
1111
+ outcome,
1112
+ zoom
1113
+ )
1114
+ })),
1115
+ this.accepts(tree.stops.at(-1).value, outcome, zoom)
1116
+ );
1117
+ }
1118
+ };
1119
+ function isConstant(value, property) {
1120
+ return value === void 0 || (property === "line-cap" ? typeof value === "string" : Array.isArray(value) && value.every((item) => typeof item === "number"));
1121
+ }
1122
+ function lowerLayer(layer, index, vector) {
1123
+ if (layer.type !== "line") return;
1124
+ const properties = [];
1125
+ let cap;
1126
+ let dash;
1127
+ for (const [group, property] of [
1128
+ ["layout", "line-cap"],
1129
+ ["paint", "line-dasharray"]
1130
+ ]) {
1131
+ const value = layer[group]?.[property];
1132
+ if (isConstant(value, property)) continue;
1133
+ const decisions = new Decisions(`/layers/${index}/${group}/${property}`, vector);
1134
+ const tree = decisions.tree(value, property);
1135
+ properties.push(property);
1136
+ if (property === "line-cap") cap = { decisions, tree };
1137
+ else dash = { decisions, tree };
1138
+ }
1139
+ if (!properties.length) return;
1140
+ const minimum = layer.minzoom ?? 0;
1141
+ const maximum = layer.maxzoom ?? 24;
1142
+ if (!finite(minimum) || !finite(maximum) || minimum < 0 || maximum > 24 || minimum >= maximum) {
1143
+ fail(`/layers/${index}/minzoom`);
1144
+ }
1145
+ const cuts = [
1146
+ .../* @__PURE__ */ new Set([
1147
+ minimum,
1148
+ maximum,
1149
+ ...[...cap?.decisions.cuts ?? [], ...dash?.decisions.cuts ?? []].filter(
1150
+ (cut) => cut > minimum && cut < maximum
1151
+ )
1152
+ ])
1153
+ ].sort((a, b) => a - b);
1154
+ if (cuts.length - 1 > nativeLoweringLimits.maximumZoomIntervals)
1155
+ fail(`/layers/${index}`, "budget");
1156
+ const variants = [];
1157
+ const tails = /* @__PURE__ */ new Map();
1158
+ for (let part = 0; part < cuts.length - 1; part++) {
1159
+ const low = cuts[part];
1160
+ const high = cuts[part + 1];
1161
+ const caps = cap ? cap.decisions.outcomes(cap.tree, Math.floor(low)) : [{ when: true, value: void 0 }];
1162
+ const dashes = dash ? dash.decisions.outcomes(dash.tree, Math.floor(low)) : [{ when: true, value: void 0 }];
1163
+ for (const c of caps)
1164
+ for (const d of dashes) {
1165
+ const when = combine("all", [c.when, d.when]);
1166
+ if (when === false) continue;
1167
+ const signature = key([when, c.value, d.value]);
1168
+ const tail = tails.get(signature);
1169
+ if (tail && tail.maximum === low) {
1170
+ tail.maximum = high;
1171
+ continue;
1172
+ }
1173
+ const variant = {
1174
+ minimum: low,
1175
+ maximum: high,
1176
+ when,
1177
+ ...cap ? { cap: c.value } : {},
1178
+ ...dash ? { dash: d.value } : {}
1179
+ };
1180
+ variants.push(variant);
1181
+ tails.set(signature, variant);
1182
+ if (variants.length > nativeLoweringLimits.maximumLayerVariants)
1183
+ fail(`/layers/${index}`, "budget");
1184
+ }
1185
+ }
1186
+ return { variants, properties };
1187
+ }
1188
+ function replaceSymbolOverlap(layer, index) {
1189
+ if (layer.type !== "symbol" || !object2(layer.layout)) return;
1190
+ const properties = overlapProperties.filter((property) => layer.layout[property] !== void 0);
1191
+ if (!properties.length) return;
1192
+ const layout = { ...layer.layout };
1193
+ for (const property of properties) {
1194
+ const mode = layout[property];
1195
+ if (mode !== "never" && mode !== "always" && mode !== "cooperative") {
1196
+ fail(`/layers/${index}/layout/${property}`);
1197
+ }
1198
+ delete layout[property];
1199
+ layout[property === "icon-overlap" ? "icon-allow-overlap" : "text-allow-overlap"] = mode !== "never";
1200
+ }
1201
+ return { layer: { ...layer, layout }, properties };
1202
+ }
1203
+ function lowerNativeStyleRepresentation(input, normalizeFilter = (filter) => filter) {
1204
+ if (!Array.isArray(input.layers) || input.layers.length > nativeLoweringLimits.maximumLayers)
1205
+ fail("/layers", "budget");
1206
+ const originalLayers = input.layers;
1207
+ const reserved = /* @__PURE__ */ new Set();
1208
+ for (const [index, layer] of originalLayers.entries()) {
1209
+ if (!object2(layer) || typeof layer.id !== "string" || reserved.has(layer.id))
1210
+ fail(`/layers/${index}/id`);
1211
+ reserved.add(layer.id);
1212
+ }
1213
+ let projection = "none";
1214
+ if (input.projection !== void 0) {
1215
+ if (!object2(input.projection) || Object.keys(input.projection).length !== 1 || typeof input.projection.type !== "string" || !["globe", "mercator"].includes(input.projection.type))
1216
+ fail("/projection");
1217
+ projection = input.projection.type === "globe" ? "globe-to-mercator" : "mercator-to-implicit";
1218
+ }
1219
+ const layers = [];
1220
+ const transformations = [];
1221
+ const symbolOverlap = [];
1222
+ for (const [index, layer] of originalLayers.entries()) {
1223
+ const source = object2(input.sources) && typeof layer.source === "string" ? input.sources[layer.source] : void 0;
1224
+ const lowered = lowerLayer(layer, index, object2(source) && source.type === "vector");
1225
+ if (!lowered) {
1226
+ const replaced = replaceSymbolOverlap(layer, index);
1227
+ if (replaced) symbolOverlap.push({ inputLayer: index, properties: replaced.properties });
1228
+ layers.push(replaced?.layer ?? layer);
1229
+ continue;
1230
+ }
1231
+ if (layers.length + lowered.variants.length + originalLayers.length - index - 1 > nativeLoweringLimits.maximumLayers) {
1232
+ fail(`/layers/${index}`, "budget");
1233
+ }
1234
+ const originalFilter = layer.filter === void 0 ? void 0 : normalizeFilter(layer.filter);
1235
+ const outputStart = layers.length;
1236
+ for (const [branch, variant] of lowered.variants.entries()) {
1237
+ const stem = `${layer.id}--native-v1-${branch}`;
1238
+ let id = stem;
1239
+ let collision = 0;
1240
+ while (reserved.has(id)) id = `${stem}-${++collision}`;
1241
+ reserved.add(id);
1242
+ const next = { ...layer, id };
1243
+ if (variant.minimum !== (layer.minzoom ?? 0)) next.minzoom = variant.minimum;
1244
+ if (variant.maximum !== (layer.maxzoom ?? 24)) next.maxzoom = variant.maximum;
1245
+ if (variant.when !== true)
1246
+ next.filter = originalFilter === void 0 ? variant.when : ["all", originalFilter, variant.when];
1247
+ if (variant.cap !== void 0) next.layout = { ...layer.layout, "line-cap": variant.cap };
1248
+ if (variant.dash !== void 0) next.paint = { ...layer.paint, "line-dasharray": variant.dash };
1249
+ layers.push(next);
1250
+ }
1251
+ transformations.push({
1252
+ inputLayer: index,
1253
+ outputStart,
1254
+ outputCount: lowered.variants.length,
1255
+ properties: lowered.properties
1256
+ });
1257
+ }
1258
+ if (input.sky !== void 0 && !object2(input.sky)) fail("/sky");
1259
+ const sky = input.sky !== void 0;
1260
+ const changed = projection !== "none" || sky || transformations.length > 0 || symbolOverlap.length > 0;
1261
+ const output = changed ? { ...input, layers } : input;
1262
+ if (changed && projection !== "none") delete output.projection;
1263
+ if (sky) delete output.sky;
1264
+ return {
1265
+ style: output,
1266
+ projection,
1267
+ ...sky ? { sky: "removed" } : {},
1268
+ layers: transformations,
1269
+ symbolOverlap,
1270
+ inputLayers: originalLayers.length,
1271
+ outputLayers: layers.length
1272
+ };
1273
+ }
1274
+
1275
+ // src/native-artifacts.ts
1276
+ function documentUrl(map, theme) {
1277
+ return `https://artifacts.invalid/native/styles/${map}/${theme}.json`;
1278
+ }
1279
+ function urlOptions(map, theme, policy) {
1280
+ return {
1281
+ documentUrl: documentUrl(map, theme),
1282
+ ...policy.developmentOrigin === void 0 ? {} : { developmentOrigin: policy.developmentOrigin }
1283
+ };
1284
+ }
1285
+ function withContext(issue, map, theme) {
1286
+ const prefix = `/maps/${map}/themes/${theme}/style`;
1287
+ return { ...issue, path: prefix.length + issue.path.length <= 300 ? prefix + issue.path : prefix };
1288
+ }
1289
+ function lowerTileflowNativeCompiledStyles(input, policy = {}) {
1290
+ const styles = {};
1291
+ const transformations = [];
1292
+ for (const map of Object.keys(input).sort()) {
1293
+ styles[map] = {};
1294
+ for (const theme of Object.keys(input[map]).sort()) {
1295
+ const web = input[map][theme];
1296
+ const steady = steadyTileflowNauticalLights(web);
1297
+ const original = prepareTileflowNativePathGround(steady);
1298
+ const pathGroundFallback = original !== steady;
1299
+ try {
1300
+ const options = { ...urlOptions(map, theme, policy), deferFontClosure: true };
1301
+ const pending = validateTileflowNativeStyle(original, options);
1302
+ const blockers = pending.filter(
1303
+ (issue) => issue.code !== "NATIVE_UNSUPPORTED_STYLE" || !(issue.path === "/projection" || issue.path === "/sky" || /^\/layers\/\d+\/(?:layout\/line-cap|paint\/line-dasharray)(?:\/|$)/u.test(
1304
+ issue.path
1305
+ ) || /^\/layers\/\d+\/layout\/(?:icon|text)-overlap$/u.test(issue.path))
1306
+ );
1307
+ if (blockers.length)
1308
+ throw new TileflowNativeCompatibilityError(
1309
+ blockers.map((issue) => withContext(issue, map, theme))
1310
+ );
1311
+ const syntax = validateStyleMin(JSON.parse(serializeCanonicalJson(original)));
1312
+ if (syntax.length)
1313
+ throw new TileflowNativeCompatibilityError(
1314
+ syntax.map((issue) => {
1315
+ const match = /^layers\[(\d+)\]\.(layout\.line-cap|paint\.line-dasharray)(?=[:.[]|$)/u.exec(
1316
+ issue.message
1317
+ );
1318
+ const path = match ? `/layers/${match[1]}/${match[2].replace(".", "/")}` : issue.message.startsWith("projection") ? "/projection" : "";
1319
+ return withContext(
1320
+ createTileflowNativeDiagnostic("NATIVE_UNSUPPORTED_STYLE", path),
1321
+ map,
1322
+ theme
1323
+ );
1324
+ })
1325
+ );
1326
+ const lowered = lowerNativeStyleRepresentation(
1327
+ original,
1328
+ (filter) => convertFilter(structuredClone(filter))
1329
+ );
1330
+ const style = rebindNativeLayerReferences(
1331
+ original,
1332
+ lowered.style,
1333
+ lowered.layers
1334
+ );
1335
+ if (!style)
1336
+ throw new TileflowNativeCompatibilityError([
1337
+ withContext(
1338
+ {
1339
+ ...createTileflowNativeDiagnostic("NATIVE_UNSUPPORTED_STYLE", "/metadata"),
1340
+ message: "Tileflow POI interaction or overlay placement metadata does not match the compiled layers.",
1341
+ suggestion: "Rebuild with the Tileflow compiler; native lowering only rebinds POI priorities and overlay anchors that match the compiled layer order."
1342
+ },
1343
+ map,
1344
+ theme
1345
+ )
1346
+ ]);
1347
+ const issues = validateTileflowNativePreparedStyle(style, options);
1348
+ if (issues.length)
1349
+ throw new TileflowNativeCompatibilityError(
1350
+ issues.map((issue) => withContext(issue, map, theme))
1351
+ );
1352
+ styles[map][theme] = style;
1353
+ transformations.push({
1354
+ map,
1355
+ theme,
1356
+ inputStyleSha256: createHash3("sha256").update(serializeCanonicalJson(web)).digest("hex"),
1357
+ loweredStyleSha256: createHash3("sha256").update(serializeCanonicalJson(style)).digest("hex"),
1358
+ inputLayers: lowered.inputLayers,
1359
+ outputLayers: lowered.outputLayers,
1360
+ projection: lowered.projection,
1361
+ ...lowered.sky ? { sky: lowered.sky } : {},
1362
+ ...pathGroundFallback ? { pathGround: "ground-fallback" } : {},
1363
+ layers: lowered.layers,
1364
+ symbolOverlap: lowered.symbolOverlap
1365
+ });
1366
+ } catch (error) {
1367
+ if (!(error instanceof NativeLoweringError)) throw error;
1368
+ throw new TileflowNativeCompatibilityError([
1369
+ withContext(
1370
+ {
1371
+ ...createTileflowNativeDiagnostic("NATIVE_UNSUPPORTED_STYLE", error.path),
1372
+ message: error.message,
1373
+ suggestion: error.reason === "budget" ? "Reduce the decision expansion or style size; native lowering never raises its resource limits automatically." : error.path === "/projection" ? "Use a fixed globe or Mercator projection for native-v1; adaptive projections are not supported." : "Use finite case/match/step decisions with constant leaves and supported scalar predicates, or select web."
1374
+ },
1375
+ map,
1376
+ theme
1377
+ )
1378
+ ]);
1379
+ }
1380
+ }
1381
+ }
1382
+ return { styles, transformations };
1383
+ }
1384
+ function assertTileflowNativeCompiledStyles(project, styles, policy = {}) {
1385
+ const issues = [];
1386
+ for (const mapName of Object.keys(project.maps).sort()) {
1387
+ for (const theme of Object.keys(styles[mapName] ?? {}).sort()) {
1388
+ issues.push(
1389
+ ...validateTileflowNativePreparedStyle(styles[mapName][theme], {
1390
+ ...urlOptions(mapName, theme, policy),
1391
+ deferFontClosure: true
1392
+ }).map((issue) => withContext(issue, mapName, theme))
1393
+ );
1394
+ }
1395
+ }
1396
+ if (issues.length) throw new TileflowNativeCompatibilityError(issues);
1397
+ }
1398
+ function prepareTileflowNativeStyles(input, assets, policy = {}) {
1399
+ const issues = [];
1400
+ const result = {};
1401
+ for (const map of Object.keys(input).sort()) {
1402
+ result[map] = {};
1403
+ if (!validNativeSprites(map, assets)) {
1404
+ issues.push({
1405
+ ...createTileflowNativeDiagnostic("NATIVE_UNSUPPORTED_STYLE", `/maps/${map}/icons`),
1406
+ suggestion: "Rebuild the icon sources into a complete, bounded 1x/2x sprite pair."
1407
+ });
1408
+ }
1409
+ for (const theme of Object.keys(input[map]).sort()) {
1410
+ const original = input[map][theme];
1411
+ const preflight = validateTileflowNativePreparedStyle(original, {
1412
+ ...urlOptions(map, theme, policy),
1413
+ deferFontClosure: true
1414
+ });
1415
+ if (preflight.length) {
1416
+ issues.push(...preflight.map((issue) => withContext(issue, map, theme)));
1417
+ continue;
1418
+ }
1419
+ const metadata = { ...original.metadata };
1420
+ const faces = getTileflowStyleFontFaces(original);
1421
+ const nativeFaces = {};
1422
+ for (const face of faces) {
1423
+ const asset = assets.find(
1424
+ (item) => item.fileName.startsWith("fonts/") && (face.source === item.fileName || face.source.endsWith(`/${item.fileName}`))
1425
+ );
1426
+ if (!asset || !isNativeFontAsset(asset)) {
1427
+ issues.push(
1428
+ withContext(
1429
+ createTileflowNativeDiagnostic("NATIVE_FONT_UNAVAILABLE", "/font-faces"),
1430
+ map,
1431
+ theme
1432
+ )
1433
+ );
1434
+ continue;
1435
+ }
1436
+ if (Object.hasOwn(nativeFaces, face.family)) {
1437
+ issues.push(
1438
+ withContext(
1439
+ createTileflowNativeDiagnostic("NATIVE_FONT_UNAVAILABLE", "/font-faces"),
1440
+ map,
1441
+ theme
1442
+ )
1443
+ );
1444
+ continue;
1445
+ }
1446
+ Object.defineProperty(nativeFaces, face.family, { value: face.source, enumerable: true });
1447
+ }
1448
+ delete metadata[tileflowStyleFontFacesMetadataKey];
1449
+ const { projection: _projection, ...rest } = original;
1450
+ const style = {
1451
+ ...rest,
1452
+ metadata,
1453
+ ...faces.length ? { "font-faces": nativeFaces } : {}
1454
+ };
1455
+ issues.push(
1456
+ ...validateTileflowNativePreparedStyle(style, urlOptions(map, theme, policy)).map(
1457
+ (issue) => withContext(issue, map, theme)
1458
+ )
1459
+ );
1460
+ result[map][theme] = style;
1461
+ }
1462
+ }
1463
+ if (issues.length) throw new TileflowNativeCompatibilityError(issues);
1464
+ return result;
1465
+ }
1466
+ function isNativeFontAsset(asset) {
1467
+ const bytes = typeof asset.source === "string" ? Buffer.from(asset.source) : asset.source;
1468
+ if (bytes.byteLength < 12 || bytes.byteLength > tileflowNativeProfileLimits.maximumFontBytes)
1469
+ return false;
1470
+ const signature = Buffer.from(bytes.subarray(0, 4)).toString("hex");
1471
+ if (!/\.(?:ttf|otf)$/u.test(asset.fileName) || !["00010000", "4f54544f"].includes(signature))
1472
+ return false;
1473
+ const expectedHash = /-([a-f0-9]{64})\.(?:ttf|otf)$/u.exec(asset.fileName)?.[1];
1474
+ return expectedHash === createHash3("sha256").update(bytes).digest("hex");
1475
+ }
1476
+ function assertTileflowNativeGeneratedStyle(style, map, theme, policy = {}) {
1477
+ const issues = validateTileflowNativePreparedStyle(style, urlOptions(map, theme, policy));
1478
+ if (issues.length) {
1479
+ throw new TileflowNativeCompatibilityError(
1480
+ issues.map((issue) => withContext(issue, map, theme))
1481
+ );
1482
+ }
1483
+ }
1484
+ function replaceTileflowNativeFontSources(style, replace) {
1485
+ const faces = style["font-faces"];
1486
+ if (!faces) return style;
1487
+ return {
1488
+ ...style,
1489
+ "font-faces": Object.fromEntries(
1490
+ Object.entries(faces).map(([name, source]) => [name, replace(source)])
1491
+ )
1492
+ };
1493
+ }
1494
+ function validNativeSprites(map, assets) {
1495
+ const prefix = `icons/${map}/`;
1496
+ const files = assets.filter((asset) => asset.fileName.startsWith(prefix));
1497
+ if (!files.length) return true;
1498
+ try {
1499
+ if (files.length !== 4) return false;
1500
+ let totalBytes = 0;
1501
+ const indexes = [];
1502
+ for (const ratio of [1, 2]) {
1503
+ const suffix = ratio === 1 ? "" : "@2x";
1504
+ const image = files.find((asset) => asset.fileName === `${prefix}sprite${suffix}.png`);
1505
+ const index = files.find((asset) => asset.fileName === `${prefix}sprite${suffix}.json`);
1506
+ if (!image || !index) return false;
1507
+ const imageBytes = Buffer.from(
1508
+ typeof image.source === "string" ? new TextEncoder().encode(image.source) : image.source
1509
+ );
1510
+ const indexBytes = Buffer.from(
1511
+ typeof index.source === "string" ? new TextEncoder().encode(index.source) : index.source
1512
+ );
1513
+ for (const bytes of [imageBytes, indexBytes]) {
1514
+ if (bytes.length > tileflowIconPackageLimits.maxGeneratedFileBytes) return false;
1515
+ totalBytes += bytes.length;
1516
+ }
1517
+ if (imageBytes.length < 33 || imageBytes.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a" || imageBytes.toString("ascii", 12, 16) !== "IHDR")
1518
+ return false;
1519
+ const width = imageBytes.readUInt32BE(16);
1520
+ const height = imageBytes.readUInt32BE(20);
1521
+ if (!width || !height || width > tileflowIconPackageLimits.maxAtlasDimension || height > tileflowIconPackageLimits.maxAtlasDimension)
1522
+ return false;
1523
+ const data = JSON.parse(indexBytes.toString("utf8"));
1524
+ if (!data || typeof data !== "object" || Array.isArray(data)) return false;
1525
+ const names = Object.keys(data).sort();
1526
+ if (!names.length || names.length > tileflowIconPackageLimits.maxIconCount) return false;
1527
+ indexes.push(names);
1528
+ for (const name of names) {
1529
+ if (!tileflowIconIdPattern.test(name)) return false;
1530
+ const entry = data[name];
1531
+ if (!entry || entry.pixelRatio !== ratio) return false;
1532
+ const { x, y, width: w, height: h } = entry;
1533
+ if (![x, y, w, h].every((value) => typeof value === "number" && Number.isSafeInteger(value)))
1534
+ return false;
1535
+ if (x < 0 || y < 0 || w <= 0 || h <= 0 || x + w > width || y + h > height)
1536
+ return false;
1537
+ }
1538
+ }
1539
+ return totalBytes <= tileflowIconPackageLimits.maxGeneratedPackageBytes && JSON.stringify(indexes[0]) === JSON.stringify(indexes[1]);
1540
+ } catch {
1541
+ return false;
1542
+ }
1543
+ }
1544
+
1545
+ // src/session.ts
1546
+ import { watch } from "chokidar";
1547
+ import { realpathSync, statSync } from "fs";
1548
+ import { basename as basename2, dirname, extname, isAbsolute, relative, resolve as resolve3, sep, win32 } from "path";
1549
+ import { tileflowIconMetadataFileName } from "@tileflow/core";
1550
+ import { TileflowNativeCompatibilityError as TileflowNativeCompatibilityError2 } from "@tileflow/core/native-profile";
1551
+ var tileflowArtifactSessionSchemaVersion = 1;
1552
+ var configExtensions = /* @__PURE__ */ new Set([".cjs", ".cts", ".js", ".json", ".mjs", ".mts", ".ts", ".tsx"]);
1553
+ var iconExtensions = /* @__PURE__ */ new Set([".jpeg", ".jpg", ".png", ".svg", ".webp"]);
1554
+ var fontExtensions = /* @__PURE__ */ new Set([".otf", ".ttf", ".woff2"]);
1555
+ var writeStabilityMs = 50;
1556
+ var excludedDirectories = /* @__PURE__ */ new Set([
1557
+ ".cache",
1558
+ ".git",
1559
+ ".hg",
1560
+ ".next",
1561
+ ".nuxt",
1562
+ ".svn",
1563
+ ".svelte-kit",
1564
+ ".tileflow",
1565
+ ".turbo",
1566
+ "build",
1567
+ "coverage",
1568
+ "dist",
1569
+ "node_modules"
1570
+ ]);
1571
+ async function createTileflowArtifactSessionWithBuilder(options, buildArtifacts, discoverWatchPaths) {
1572
+ const session = new TileflowArtifactSessionImpl(options, buildArtifacts, discoverWatchPaths);
1573
+ await session.start();
1574
+ return session;
1575
+ }
1576
+ var TileflowArtifactSessionImpl = class {
1577
+ #buildArtifacts;
1578
+ #buildOptions;
1579
+ #configPath;
1580
+ #cwd;
1581
+ #debounceMs;
1582
+ #discoverWatchPaths;
1583
+ #listeners = /* @__PURE__ */ new Set();
1584
+ #ignoredPaths;
1585
+ #watchEnabled;
1586
+ #closed = false;
1587
+ #closing;
1588
+ #debounceTimer;
1589
+ #startupCheckTimer;
1590
+ #generation = 0;
1591
+ #current;
1592
+ #artifactGenerations = /* @__PURE__ */ new Set();
1593
+ #lastGoodGeneration;
1594
+ #refreshes = /* @__PURE__ */ new Set();
1595
+ #refreshToken = 0;
1596
+ #state = { generation: 0, status: "building" };
1597
+ #watcher;
1598
+ #watchedArtifactDirectories = /* @__PURE__ */ new Map();
1599
+ #watchedArtifactPaths = /* @__PURE__ */ new Set();
1600
+ constructor(options, buildArtifacts, discoverWatchPaths) {
1601
+ this.#cwd = resolve3(options.cwd ?? process.cwd());
1602
+ const configPath = resolve3(this.#cwd, options.config ?? "tileflow.config.ts");
1603
+ this.#configPath = resolve3(canonicalWatchPath(dirname(configPath)), basename2(configPath));
1604
+ this.#debounceMs = Math.max(0, Math.min(options.debounceMs ?? 75, 1e3));
1605
+ this.#watchEnabled = options.watch ?? false;
1606
+ this.#ignoredPaths = (options.ignoredPaths ?? []).map(
1607
+ (path) => canonicalWatchPath(resolve3(this.#cwd, path))
1608
+ );
1609
+ this.#buildArtifacts = buildArtifacts;
1610
+ this.#discoverWatchPaths = discoverWatchPaths;
1611
+ this.#buildOptions = {
1612
+ apiBaseUrl: options.apiBaseUrl,
1613
+ assetBaseUrl: options.assetBaseUrl,
1614
+ config: options.config,
1615
+ cwd: this.#cwd,
1616
+ ...options.developmentOrigin === void 0 ? {} : { developmentOrigin: options.developmentOrigin },
1617
+ // Trusted shared Icon Set resolution settings, supplied once by the owning command.
1618
+ ...options.icons ? { icons: options.icons } : {},
1619
+ inspection: options.inspection,
1620
+ ...options.renderer === void 0 ? {} : { renderer: options.renderer },
1621
+ styleBaseUrl: options.styleBaseUrl,
1622
+ target: options.target
1623
+ };
1624
+ }
1625
+ async start() {
1626
+ if (this.#watchEnabled) {
1627
+ this.#watcher = watch(dirname(this.#configPath), {
1628
+ awaitWriteFinish: { pollInterval: 20, stabilityThreshold: writeStabilityMs },
1629
+ followSymlinks: false,
1630
+ ignoreInitial: true,
1631
+ ignored: (path, stats) => this.#ignoreWatchPath(path, stats?.isDirectory()),
1632
+ usePolling: process.platform === "win32"
1633
+ });
1634
+ const watcherReady = new Promise((resolveReady, rejectReady) => {
1635
+ const watcher = this.#watcher;
1636
+ const cleanup = () => {
1637
+ watcher.off("ready", onReady);
1638
+ watcher.off("error", onError);
1639
+ };
1640
+ const onReady = () => {
1641
+ cleanup();
1642
+ resolveReady();
1643
+ };
1644
+ const onError = (error) => {
1645
+ cleanup();
1646
+ rejectReady(error);
1647
+ };
1648
+ watcher.once("ready", onReady);
1649
+ watcher.once("error", onError);
1650
+ });
1651
+ this.#watcher.on("all", (event, path) => {
1652
+ if (event === "addDir" || event === "unlinkDir") return;
1653
+ if (!this.#shouldRefreshForWatchEvent(path)) return;
1654
+ this.#scheduleRefresh(path);
1655
+ });
1656
+ this.#watcher.on("raw", (event, path, details) => {
1657
+ if (event !== "rename" && event !== "change") return;
1658
+ const resolvedPath = resolveRawWatchPath(path, details);
1659
+ if (!resolvedPath || this.#ignoreWatchPath(resolvedPath, void 0)) return;
1660
+ const directoryChanged = this.#watchedArtifactDirectories.has(resolvedPath);
1661
+ const metadataChanged = event === "rename" && basename2(resolvedPath) === tileflowIconMetadataFileName && [...this.#watchedArtifactDirectories.keys()].some(
1662
+ (directory) => isSameOrInside(directory, resolvedPath)
1663
+ );
1664
+ if (!directoryChanged && !metadataChanged) return;
1665
+ if (directoryChanged && event === "change") {
1666
+ const modified = directoryModificationTime(resolvedPath);
1667
+ if (modified === this.#watchedArtifactDirectories.get(resolvedPath)) return;
1668
+ this.#watchedArtifactDirectories.set(resolvedPath, modified);
1669
+ }
1670
+ this.#scheduleRefresh(resolvedPath);
1671
+ });
1672
+ this.#watcher.on("error", (error) => {
1673
+ if (this.#closed) return;
1674
+ this.#refreshToken += 1;
1675
+ this.#publishInvalid(this.#generation + 1, error);
1676
+ });
1677
+ await watcherReady;
1678
+ }
1679
+ await this.refresh("initial");
1680
+ while (!this.#current && this.#state.status === "building" && !this.#closed) {
1681
+ await Promise.all([...this.#refreshes]);
1682
+ }
1683
+ if (this.#watcher && !this.#closed) {
1684
+ this.#startupCheckTimer = setTimeout(() => {
1685
+ this.#startupCheckTimer = void 0;
1686
+ if (this.#closed) return;
1687
+ for (const [path, observed] of this.#watchedArtifactDirectories) {
1688
+ if (this.#ignoreWatchPath(path, true)) continue;
1689
+ const modified = directoryModificationTime(path);
1690
+ if (modified === observed) continue;
1691
+ this.#watchedArtifactDirectories.set(path, modified);
1692
+ this.#scheduleRefresh(path);
1693
+ }
1694
+ }, writeStabilityMs);
1695
+ }
1696
+ }
1697
+ getState() {
1698
+ return this.#state;
1699
+ }
1700
+ getLastGoodArtifacts() {
1701
+ return this.#current?.artifacts;
1702
+ }
1703
+ acquireArtifacts(generation) {
1704
+ const current = this.#current;
1705
+ if (this.#closed || !current?.accepting || generation !== void 0 && generation !== current.generation) {
1706
+ return void 0;
1707
+ }
1708
+ current.references += 1;
1709
+ let released = false;
1710
+ return Object.freeze({
1711
+ artifacts: current.artifacts,
1712
+ generation: current.generation,
1713
+ release: async () => {
1714
+ if (released) return;
1715
+ released = true;
1716
+ current.references -= 1;
1717
+ if (!current.accepting && current.references === 0) {
1718
+ await this.#disposeGeneration(current);
1719
+ }
1720
+ }
1721
+ });
1722
+ }
1723
+ subscribe(listener) {
1724
+ if (this.#closed) return () => void 0;
1725
+ this.#listeners.add(listener);
1726
+ return () => this.#listeners.delete(listener);
1727
+ }
1728
+ async refresh(_reason = "manual") {
1729
+ const refresh = this.#refresh();
1730
+ this.#refreshes.add(refresh);
1731
+ try {
1732
+ await refresh;
1733
+ } finally {
1734
+ this.#refreshes.delete(refresh);
1735
+ }
1736
+ }
1737
+ async #refresh() {
1738
+ if (this.#closed) return;
1739
+ const generation = ++this.#generation;
1740
+ const token = ++this.#refreshToken;
1741
+ this.#publish({
1742
+ generation,
1743
+ status: "building",
1744
+ ...this.#lastGoodGeneration === void 0 ? {} : { lastGoodGeneration: this.#lastGoodGeneration }
1745
+ });
1746
+ try {
1747
+ const artifacts = await this.#buildArtifacts(this.#buildOptions);
1748
+ if (this.#closed || token !== this.#refreshToken) {
1749
+ await disposeArtifacts(artifacts);
1750
+ return;
1751
+ }
1752
+ const previous = this.#current;
1753
+ const current = createArtifactGeneration(generation, artifacts);
1754
+ this.#artifactGenerations.add(current);
1755
+ this.#current = current;
1756
+ this.#lastGoodGeneration = generation;
1757
+ this.#updateArtifactWatchPaths(artifacts.watchPaths);
1758
+ if (previous) {
1759
+ previous.accepting = false;
1760
+ if (previous.references === 0) await this.#disposeGeneration(previous);
1761
+ }
1762
+ this.#publish({
1763
+ artifacts,
1764
+ generation,
1765
+ lastGoodGeneration: generation,
1766
+ status: "ready"
1767
+ });
1768
+ } catch (error) {
1769
+ if (this.#closed || token !== this.#refreshToken) return;
1770
+ if (this.#watcher && this.#discoverWatchPaths) {
1771
+ try {
1772
+ const watchPaths = await this.#discoverWatchPaths();
1773
+ if (this.#closed || token !== this.#refreshToken) return;
1774
+ this.#updateArtifactWatchPaths(watchPaths);
1775
+ } catch {
1776
+ }
1777
+ }
1778
+ this.#publishInvalid(generation, error);
1779
+ }
1780
+ }
1781
+ close() {
1782
+ this.#closing ??= this.#close();
1783
+ return this.#closing;
1784
+ }
1785
+ async #close() {
1786
+ this.#closed = true;
1787
+ this.#refreshToken += 1;
1788
+ if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
1789
+ if (this.#startupCheckTimer) clearTimeout(this.#startupCheckTimer);
1790
+ this.#listeners.clear();
1791
+ await this.#watcher?.close();
1792
+ const retained = [...this.#artifactGenerations];
1793
+ this.#current = void 0;
1794
+ for (const generation of retained) {
1795
+ generation.accepting = false;
1796
+ if (generation.references === 0) void this.#disposeGeneration(generation);
1797
+ }
1798
+ await Promise.allSettled([...this.#refreshes]);
1799
+ await Promise.all(retained.map((generation) => generation.completion));
1800
+ }
1801
+ #ignoreWatchPath(path, isDirectory) {
1802
+ const resolvedPath = canonicalWatchPath(path);
1803
+ const relativePath = relative(dirname(this.#configPath), resolvedPath);
1804
+ const segments = relativePath.split(sep).filter(Boolean);
1805
+ if (this.#ignoredPaths.some((path2) => isSameOrInside(path2, resolvedPath))) return true;
1806
+ if (segments.some((segment) => excludedDirectories.has(segment))) return true;
1807
+ if (isDirectory !== false) return false;
1808
+ if (resolvedPath === this.#configPath || this.#watchedArtifactPaths.has(resolvedPath))
1809
+ return false;
1810
+ const extension = extname(resolvedPath).toLowerCase();
1811
+ if (configExtensions.has(extension)) return false;
1812
+ if (iconExtensions.has(extension) || fontExtensions.has(extension)) return false;
1813
+ return true;
1814
+ }
1815
+ #shouldRefreshForWatchEvent(path) {
1816
+ const resolvedPath = canonicalWatchPath(path);
1817
+ const extension = extname(resolvedPath).toLowerCase();
1818
+ if (!iconExtensions.has(extension) && !fontExtensions.has(extension)) return true;
1819
+ return [...this.#watchedArtifactPaths].some((path2) => isSameOrInside(path2, resolvedPath));
1820
+ }
1821
+ #publish(state) {
1822
+ this.#state = state;
1823
+ for (const listener of this.#listeners) listener(state);
1824
+ }
1825
+ #publishInvalid(generation, error) {
1826
+ this.#generation = Math.max(this.#generation, generation);
1827
+ this.#publish({
1828
+ diagnostics: createTileflowArtifactDiagnostics(error, this.#cwd),
1829
+ generation,
1830
+ status: "invalid",
1831
+ ...this.#lastGoodGeneration === void 0 ? {} : { lastGoodGeneration: this.#lastGoodGeneration }
1832
+ });
1833
+ }
1834
+ #scheduleRefresh(_path) {
1835
+ if (this.#closed) return;
1836
+ if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
1837
+ this.#debounceTimer = setTimeout(() => {
1838
+ this.#debounceTimer = void 0;
1839
+ void this.refresh("filesystem");
1840
+ }, this.#debounceMs);
1841
+ }
1842
+ #updateArtifactWatchPaths(paths) {
1843
+ if (!this.#watcher) return;
1844
+ const next = new Set(paths.map((path) => canonicalWatchPath(resolve3(this.#cwd, path))));
1845
+ const previous = this.#watchedArtifactPaths;
1846
+ this.#watchedArtifactPaths = next;
1847
+ const previousDirectories = this.#watchedArtifactDirectories;
1848
+ this.#watchedArtifactDirectories = /* @__PURE__ */ new Map();
1849
+ for (const path of next) {
1850
+ const modified = directoryModificationTime(path);
1851
+ if (modified !== void 0) {
1852
+ this.#watchedArtifactDirectories.set(
1853
+ path,
1854
+ previousDirectories.has(path) ? previousDirectories.get(path) : modified
1855
+ );
1856
+ }
1857
+ }
1858
+ for (const path of previous) {
1859
+ if (!next.has(path) && !isSameOrInside(dirname(this.#configPath), path)) {
1860
+ void this.#watcher.unwatch(path);
1861
+ }
1862
+ }
1863
+ for (const path of next) {
1864
+ if (!previous.has(path) && !isSameOrInside(dirname(this.#configPath), path)) {
1865
+ this.#watcher.add(path);
1866
+ }
1867
+ }
1868
+ }
1869
+ #disposeGeneration(generation) {
1870
+ if (generation.disposeStarted) return generation.completion;
1871
+ generation.disposeStarted = true;
1872
+ void disposeArtifacts(generation.artifacts).then(
1873
+ () => {
1874
+ this.#artifactGenerations.delete(generation);
1875
+ generation.resolveCompletion();
1876
+ },
1877
+ (error) => {
1878
+ this.#artifactGenerations.delete(generation);
1879
+ generation.rejectCompletion(error);
1880
+ }
1881
+ );
1882
+ return generation.completion;
1883
+ }
1884
+ };
1885
+ function createArtifactGeneration(generation, artifacts) {
1886
+ let rejectCompletion;
1887
+ let resolveCompletion;
1888
+ const completion = new Promise((resolvePromise, rejectPromise) => {
1889
+ rejectCompletion = rejectPromise;
1890
+ resolveCompletion = resolvePromise;
1891
+ });
1892
+ return {
1893
+ accepting: true,
1894
+ artifacts,
1895
+ completion,
1896
+ disposeStarted: false,
1897
+ generation,
1898
+ references: 0,
1899
+ rejectCompletion,
1900
+ resolveCompletion
1901
+ };
1902
+ }
1903
+ async function disposeArtifacts(artifacts) {
1904
+ const dispose = artifacts.dispose;
1905
+ artifacts.dispose = void 0;
1906
+ await dispose?.();
1907
+ }
1908
+ function isSameOrInside(root, candidate) {
1909
+ const path = relative(resolve3(root), resolve3(candidate));
1910
+ return path === "" || !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
1911
+ }
1912
+ function directoryModificationTime(path) {
1913
+ try {
1914
+ const stats = statSync(path);
1915
+ return stats.isDirectory() ? stats.mtimeMs : void 0;
1916
+ } catch {
1917
+ return void 0;
1918
+ }
1919
+ }
1920
+ function resolveRawWatchPath(path, details) {
1921
+ if (typeof path !== "string" || !path) return void 0;
1922
+ if (isAbsolute(path)) return canonicalWatchPath(path);
1923
+ if (!details || typeof details !== "object") return void 0;
1924
+ const watchedPath = details.watchedPath;
1925
+ if (typeof watchedPath !== "string" || !isAbsolute(watchedPath)) return void 0;
1926
+ return canonicalWatchPath(
1927
+ basename2(watchedPath) === path ? watchedPath : resolve3(watchedPath, path)
1928
+ );
1929
+ }
1930
+ function canonicalWatchPath(path) {
1931
+ const resolved = resolve3(path);
1932
+ let ancestor = resolved;
1933
+ while (true) {
1934
+ try {
1935
+ return resolve3(realpathSync.native(ancestor), relative(ancestor, resolved));
1936
+ } catch {
1937
+ const parent = dirname(ancestor);
1938
+ if (parent === ancestor) return resolved;
1939
+ ancestor = parent;
1940
+ }
1941
+ }
1942
+ }
1943
+ function createTileflowArtifactDiagnostics(error, cwd) {
1944
+ if (error instanceof TileflowNativeCompatibilityError2) {
1945
+ return error.issues.map((issue) => ({ ...issue }));
1946
+ }
1947
+ const inheritedCode = optionalDiagnosticField(error, "code");
1948
+ const inheritedPath = optionalDiagnosticPath(error);
1949
+ const inheritedPhase = optionalDiagnosticField(error, "phase") ?? optionalNestedDiagnosticField(error, "phase");
1950
+ const semanticResolutionPath = isTileflowMapResolutionDiagnostic(error, inheritedPath) ? sanitizeDiagnosticSecrets(inheritedPath) : void 0;
1951
+ const issueList = getIssueList(error);
1952
+ if (issueList.length > 0) {
1953
+ return normalizeDiagnostics(
1954
+ issueList.map((issue) => ({
1955
+ ...issue.code ?? inheritedCode ? { code: issue.code ?? inheritedCode } : {},
1956
+ message: sanitizeMessage(issue.message, cwd),
1957
+ path: sanitizePath(issue.path, cwd),
1958
+ ...issue.phase ?? inheritedPhase ? { phase: issue.phase ?? inheritedPhase } : {}
1959
+ }))
1960
+ );
1961
+ }
1962
+ return normalizeDiagnostics([
1963
+ {
1964
+ ...inheritedCode ? { code: inheritedCode } : {},
1965
+ message: sanitizeMessage(
1966
+ error instanceof Error ? removeSemanticResolutionPath(error.message, semanticResolutionPath) : "Unknown build error",
1967
+ cwd
1968
+ ),
1969
+ path: semanticResolutionPath ?? sanitizePath(inheritedPath ?? "", cwd),
1970
+ ...inheritedPhase ? { phase: inheritedPhase } : {}
1971
+ }
1972
+ ]);
1973
+ }
1974
+ function getIssueList(error) {
1975
+ if (!error || typeof error !== "object") return [];
1976
+ const containers = [];
1977
+ for (const key2 of ["messages", "issues"]) {
1978
+ const value = error[key2];
1979
+ if (Array.isArray(value)) containers.push(value);
1980
+ }
1981
+ const details = error.details;
1982
+ if (details && typeof details === "object") {
1983
+ const diagnostics = details.diagnostics;
1984
+ if (Array.isArray(diagnostics)) containers.push(diagnostics);
1985
+ }
1986
+ return containers.flatMap(
1987
+ (value) => value.flatMap((entry) => {
1988
+ if (!entry || typeof entry !== "object") return [];
1989
+ const message = entry.message;
1990
+ const path = entry.path;
1991
+ const code = optionalDiagnosticField(entry, "code");
1992
+ const phase = optionalDiagnosticField(entry, "phase");
1993
+ return typeof message === "string" ? [
1994
+ {
1995
+ ...code ? { code } : {},
1996
+ message,
1997
+ path: typeof path === "string" ? path : "",
1998
+ ...phase ? { phase } : {}
1999
+ }
2000
+ ] : [];
2001
+ })
2002
+ );
2003
+ }
2004
+ function sanitizeMessage(value, cwd) {
2005
+ let sanitized = value;
2006
+ for (const root of localPathAliases(cwd)) sanitized = sanitized.replaceAll(root, ".");
2007
+ sanitized = sanitized.replace(/[\r\n]+/g, " ").trim();
2008
+ sanitized = sanitized.replace(/https?:\/\/[^\s'")<>]+/gi, (url) => {
2009
+ try {
2010
+ return new URL(url.replace(/[),.;]+$/, "")).origin;
2011
+ } catch {
2012
+ return "(resource URL)";
2013
+ }
2014
+ });
2015
+ sanitized = sanitized.replace(/(^|[\s'"(])[A-Za-z]:[\\/][^\s'")]+/g, "$1(external path)");
2016
+ sanitized = sanitized.replace(/(^|[\s'"(])(?:\\\\|\/\/)[^\s'")]+/g, "$1(external path)");
2017
+ sanitized = sanitized.replace(/(^|[\s'"(])\/[^\s'")]+/g, "$1(external path)");
2018
+ return sanitizeDiagnosticSecrets(sanitized);
2019
+ }
2020
+ function sanitizePath(value, cwd) {
2021
+ if (!value) return "";
2022
+ if (win32.isAbsolute(value) && !isAbsolute(value)) return "(external)";
2023
+ if (!isAbsolute(value)) return sanitizeDiagnosticSecrets(value.replaceAll("\\", "/"));
2024
+ const local = relative(canonicalPath(cwd), canonicalPath(value)).replaceAll(sep, "/");
2025
+ return local.startsWith("../") || local === ".." ? "(external)" : sanitizeDiagnosticSecrets(local || ".");
2026
+ }
2027
+ function optionalDiagnosticPath(value) {
2028
+ if (!value || typeof value !== "object") return void 0;
2029
+ const path = value.path;
2030
+ return typeof path === "string" && path ? path : void 0;
2031
+ }
2032
+ function isTileflowMapResolutionDiagnostic(value, path) {
2033
+ if (!value || typeof value !== "object" || !path) return false;
2034
+ const record = value;
2035
+ return record.name === "TileflowMapResolutionError" && typeof record.code === "string" && typeof record.mapId === "string" && path.startsWith("/modules/") && !/[\p{Cc}]/u.test(path) && !/~(?:[^01]|$)/u.test(path);
2036
+ }
2037
+ function removeSemanticResolutionPath(message, path) {
2038
+ if (!path) return message;
2039
+ return message.replace(` at ${path}:`, ":");
2040
+ }
2041
+ function localPathAliases(path) {
2042
+ return [.../* @__PURE__ */ new Set([resolve3(path), canonicalPath(path)])].sort(
2043
+ (left, right) => right.length - left.length
2044
+ );
2045
+ }
2046
+ function canonicalPath(path) {
2047
+ try {
2048
+ return realpathSync.native(resolve3(path));
2049
+ } catch {
2050
+ return resolve3(path);
2051
+ }
2052
+ }
2053
+ function normalizeDiagnostics(diagnostics) {
2054
+ const bounded = diagnostics.map((diagnostic) => ({
2055
+ ...diagnostic.code ? { code: boundDiagnosticField(diagnostic.code) } : {},
2056
+ message: boundDiagnosticText(diagnostic.message, 300),
2057
+ path: boundDiagnosticText(diagnostic.path, 300),
2058
+ ...diagnostic.phase ? { phase: boundDiagnosticField(diagnostic.phase) } : {}
2059
+ }));
2060
+ bounded.sort((left, right) => {
2061
+ const leftKey = `${left.path}\0${left.message}\0${left.code ?? ""}\0${left.phase ?? ""}`;
2062
+ const rightKey = `${right.path}\0${right.message}\0${right.code ?? ""}\0${right.phase ?? ""}`;
2063
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
2064
+ });
2065
+ return bounded.filter(
2066
+ (diagnostic, index) => index === 0 || JSON.stringify(diagnostic) !== JSON.stringify(bounded[index - 1])
2067
+ ).slice(0, 32);
2068
+ }
2069
+ function optionalDiagnosticField(value, key2) {
2070
+ if (!value || typeof value !== "object") return void 0;
2071
+ const field = value[key2];
2072
+ return typeof field === "string" && field.trim() ? boundDiagnosticField(field) : void 0;
2073
+ }
2074
+ function optionalNestedDiagnosticField(value, key2) {
2075
+ if (!value || typeof value !== "object") return void 0;
2076
+ return optionalDiagnosticField(value.details, key2);
2077
+ }
2078
+ function boundDiagnosticField(value) {
2079
+ return boundDiagnosticText(value.replace(/[^A-Za-z0-9_-]/g, ""), 64);
2080
+ }
2081
+ function boundDiagnosticText(value, limit) {
2082
+ return value.length <= limit ? value : `${value.slice(0, limit - 1)}\u2026`;
2083
+ }
2084
+
2085
+ // src/hosted-native-artifacts.ts
2086
+ import { createHash as createHash4 } from "crypto";
2087
+ import { serializeCanonicalJson as serializeCanonicalJson2 } from "@tileflow/core";
2088
+ import {
2089
+ inferTileflowDataRequirements,
2090
+ inferTileflowSourceRequirements,
2091
+ parseTileflowRendererDeploymentArtifact
2092
+ } from "@tileflow/core/build";
2093
+ import {
2094
+ createTileflowNativeBuildRecord,
2095
+ TileflowNativeCompatibilityError as TileflowNativeCompatibilityError3,
2096
+ createTileflowNativeDiagnostic as createTileflowNativeDiagnostic2
2097
+ } from "@tileflow/core/native-profile";
2098
+ var digest2 = (value) => createHash4("sha256").update(serializeCanonicalJson2(value)).digest("hex");
2099
+ var invalidSource = () => new TileflowNativeCompatibilityError3([
2100
+ createTileflowNativeDiagnostic2("NATIVE_UNSUPPORTED_SOURCE", "/sources")
2101
+ ]);
2102
+ async function prepareTileflowHostedNativeDeployment(input) {
2103
+ const webStyles = JSON.parse(serializeCanonicalJson2(input.styles));
2104
+ const validationStyles = JSON.parse(
2105
+ serializeCanonicalJson2(webStyles)
2106
+ );
2107
+ for (const style of Object.values(validationStyles)) {
2108
+ for (const [sourceId, binding] of Object.entries(input.teamSources)) {
2109
+ const source = style.sources[sourceId];
2110
+ if (!source || source.type !== binding.type || source.url !== `tileflow://hosted-sources/${sourceId}` || source.tiles !== void 0)
2111
+ throw invalidSource();
2112
+ source.url = `https://artifacts.invalid/tiles/${sourceId}/tiles.json`;
2113
+ }
2114
+ }
2115
+ const lowered = lowerTileflowNativeCompiledStyles({ [input.mapId]: validationStyles });
2116
+ const projected = prepareTileflowNativeStyles(lowered.styles, input.assets)[input.mapId];
2117
+ for (const [name, style] of Object.entries(projected)) {
2118
+ for (const sourceId of Object.keys(input.teamSources)) {
2119
+ const source = style.sources[sourceId];
2120
+ if (!source) throw invalidSource();
2121
+ source.url = `tileflow://hosted-sources/${sourceId}`;
2122
+ }
2123
+ if (serializeCanonicalJson2(style.sources) !== serializeCanonicalJson2(webStyles[name].sources))
2124
+ throw invalidSource();
2125
+ }
2126
+ const nativeBuild = JSON.parse(
2127
+ serializeCanonicalJson2(input.buildManifest)
2128
+ );
2129
+ const mapBuild = nativeBuild.maps[input.mapId];
2130
+ if (!mapBuild) throw invalidSource();
2131
+ for (const [name, style] of Object.entries(projected)) {
2132
+ const theme = mapBuild.themes[name];
2133
+ if (!theme) throw invalidSource();
2134
+ mapBuild.themes[name] = {
2135
+ ...theme,
2136
+ styleSha256: digest2(style),
2137
+ dataRequirements: inferTileflowDataRequirements(style),
2138
+ sourceRequirements: inferTileflowSourceRequirements(style)
2139
+ };
2140
+ }
2141
+ const transformations = lowered.transformations.map((item) => ({
2142
+ ...item,
2143
+ inputStyleSha256: digest2(webStyles[item.theme]),
2144
+ loweredStyleSha256: digest2({
2145
+ ...lowered.styles[input.mapId][item.theme],
2146
+ sources: webStyles[item.theme].sources
2147
+ })
2148
+ }));
2149
+ return parseTileflowRendererDeploymentArtifact({
2150
+ kind: "tileflow-map-deployment",
2151
+ schemaVersion: 2,
2152
+ mapId: input.mapId,
2153
+ teamSources: input.teamSources,
2154
+ ...input.view ? { view: input.view } : {},
2155
+ renderers: {
2156
+ web: { buildManifest: input.buildManifest, styles: webStyles },
2157
+ native: {
2158
+ buildManifest: nativeBuild,
2159
+ buildRecord: createTileflowNativeBuildRecord(digest2(nativeBuild), transformations),
2160
+ styles: projected
2161
+ }
2162
+ }
2163
+ });
2164
+ }
2165
+
2166
+ // src/artifacts.ts
2167
+ import {
2168
+ readTileflowLandmarkTileFootprints,
2169
+ tileflowLandmarkRegionOf,
2170
+ tileflowLandmarkRegionZoom,
2171
+ tileflowLandmarkTileOf,
2172
+ tileflowLandmarkTileZoom
2173
+ } from "@tileflow/core";
2174
+ var tileflowArtifactPlanSchemaVersion = 1;
2175
+ var tileflowArtifactInventoryFileName = ".tileflow-artifacts.json";
2176
+ var TileflowHostedManifestOverwriteError = class extends Error {
2177
+ code = "HOSTED_MANIFEST_OVERWRITE_REFUSED";
2178
+ constructor(path) {
2179
+ super(
2180
+ `Refusing to overwrite Hosted manifest at ${path}. Remove it, choose another output directory, or explicitly set overwriteHostedManifest: true.`
2181
+ );
2182
+ this.name = "TileflowHostedManifestOverwriteError";
2183
+ }
2184
+ };
2185
+ var TileflowLocalTilesetProductionError = class extends Error {
2186
+ code = "TF_LOCAL_TILESET_PRODUCTION_UNRESOLVED";
2187
+ constructor() {
2188
+ super(
2189
+ "Production builds cannot publish local PMTiles sources. Publish the tileset explicitly or provide an application-owned production source."
2190
+ );
2191
+ this.name = "TileflowLocalTilesetProductionError";
2192
+ }
2193
+ };
2194
+ function createTileflowManifest(project, options = {}) {
2195
+ return createManifest(project, options);
2196
+ }
2197
+ function createTileflowStyle(project, mapName, options = {}) {
2198
+ const style = createStyleFromCatalog(project, mapName, options);
2199
+ assertValidTileflowStyle(style, mapName);
2200
+ return style;
2201
+ }
2202
+ function createTileflowStyles(project, options = {}) {
2203
+ const { mapAssets, ...styleOptions } = options;
2204
+ const styles = createStylesFromCatalog(project, {
2205
+ ...styleOptions,
2206
+ mapAssets
2207
+ });
2208
+ assertValidTileflowStyles(project, styles);
2209
+ return styles;
2210
+ }
2211
+ function createTileflowStylesWithInspection(project, options = {}) {
2212
+ const { mapAssets, ...styleOptions } = options;
2213
+ const inspected = createStylesFromCatalogWithInspection(project, {
2214
+ ...styleOptions,
2215
+ mapAssets
2216
+ });
2217
+ const styles = Object.fromEntries(
2218
+ Object.entries(inspected).map(([mapName, family]) => [
2219
+ mapName,
2220
+ Object.fromEntries(
2221
+ Object.entries(family).map(([themeName, result]) => [themeName, result.style])
2222
+ )
2223
+ ])
2224
+ );
2225
+ const inspections = Object.fromEntries(
2226
+ Object.entries(inspected).map(([mapName, family]) => [
2227
+ mapName,
2228
+ Object.fromEntries(
2229
+ Object.entries(family).map(([themeName, result]) => [themeName, result.inspection])
2230
+ )
2231
+ ])
2232
+ );
2233
+ assertValidTileflowStyles(project, styles);
2234
+ return { inspections, styles };
2235
+ }
2236
+ function assertValidTileflowStyles(project, styles) {
2237
+ const issues = [];
2238
+ for (const mapName of getTileflowMapNames(project).sort(compareCodeUnits2)) {
2239
+ const family = styles[mapName];
2240
+ if (!family) continue;
2241
+ for (const themeName of Object.keys(family).sort(compareCodeUnits2)) {
2242
+ const style = family[themeName];
2243
+ issues.push(...validateTileflowStyle(style, `${mapName}/${themeName}`));
2244
+ }
2245
+ }
2246
+ if (issues.length > 0) {
2247
+ throw new TileflowStyleValidationError(normalizeTileflowStyleValidationIssues(issues));
2248
+ }
2249
+ }
2250
+ async function createTileflowArtifactPlan(prepared, options = {}) {
2251
+ const renderer = resolveTileflowRenderer(options.renderer);
2252
+ if (renderer === "native" && options.inspection) {
2253
+ throw new TileflowNativeCompatibilityError4([
2254
+ createTileflowNativeDiagnostic3("NATIVE_RENDERER_UNSUPPORTED", "/inspection")
2255
+ ]);
2256
+ }
2257
+ assertNativeDevelopmentOrigin(options);
2258
+ const nativeUrls = {
2259
+ developmentOrigin: options.developmentOrigin
2260
+ };
2261
+ if (renderer === "native") {
2262
+ assertTileflowNativeCompiledStyles(prepared.project, {}, nativeUrls);
2263
+ } else if (options.target === "production" && hasLocalTilesetSources(prepared.project)) {
2264
+ throw new TileflowLocalTilesetProductionError();
2265
+ }
2266
+ const inspected = options.inspection ? createTileflowStylesWithInspection(prepared.project, {
2267
+ apiBaseUrl: options.apiBaseUrl,
2268
+ mapAssets: prepared.mapAssets
2269
+ }) : void 0;
2270
+ const compileStyles = renderer === "native" ? createStylesFromCatalog : createTileflowStyles;
2271
+ const compiledStyles = inspected?.styles ?? compileStyles(prepared.project, {
2272
+ apiBaseUrl: options.apiBaseUrl,
2273
+ mapAssets: prepared.mapAssets
2274
+ });
2275
+ const embeddedImages = renderer === "native" ? prepareEmbeddedImageSources(compiledStyles) : { styles: compiledStyles, assets: [] };
2276
+ const nativeLowering = renderer === "native" ? lowerTileflowNativeCompiledStyles(embeddedImages.styles, nativeUrls) : void 0;
2277
+ const rendererStyles = nativeLowering?.styles ?? compiledStyles;
2278
+ if (renderer === "native")
2279
+ assertTileflowNativeCompiledStyles(prepared.project, rendererStyles, nativeUrls);
2280
+ const localTilesets = await prepareTileflowLocalTilesets(prepared.project, rendererStyles, {
2281
+ assetBaseUrl: resolveRendererAssetBaseUrl(options),
2282
+ baseDirectory: prepared.baseDirectory,
2283
+ cwd: prepared.cwd
2284
+ });
2285
+ try {
2286
+ const preparedFonts = await prepareTileflowStyleFonts(prepared.project, localTilesets.styles, {
2287
+ assetBaseUrl: resolveRendererAssetBaseUrl(options),
2288
+ baseDirectory: prepared.baseDirectory,
2289
+ cwd: prepared.cwd,
2290
+ target: "local"
2291
+ }).catch((error) => {
2292
+ if (renderer === "native" && error instanceof TileflowFontCompilationError) {
2293
+ throw new TileflowNativeCompatibilityError4([
2294
+ createTileflowNativeDiagnostic3("NATIVE_FONT_UNAVAILABLE", "/fonts")
2295
+ ]);
2296
+ }
2297
+ throw error;
2298
+ });
2299
+ const assets = [...prepared.assets, ...preparedFonts.assets, ...embeddedImages.assets];
2300
+ const lineBackgroundGlyphs = renderer === "native" || options.target === "production" ? { providers: {}, styles: preparedFonts.styles } : prepareTileflowLineBackgroundGlyphs(preparedFonts.styles, {
2301
+ assetBaseUrl: resolveRendererAssetBaseUrl(options)
2302
+ });
2303
+ const styles = renderer === "native" ? prepareTileflowNativeStyles(preparedFonts.styles, assets, nativeUrls) : lineBackgroundGlyphs.styles;
2304
+ const provenance = await createTileflowBuildProvenance(prepared.cwd);
2305
+ const buildManifest = await createTileflowMapBuildManifest(
2306
+ Object.fromEntries(
2307
+ Object.keys(prepared.project.maps).sort(compareCodeUnits2).map((mapName) => {
2308
+ const map = prepared.project.maps[mapName];
2309
+ const style = styles[mapName];
2310
+ return [
2311
+ mapName,
2312
+ {
2313
+ assets: getMapBuildAssets(
2314
+ mapName,
2315
+ assets,
2316
+ preparedFonts.bundles[mapName]?.files ?? []
2317
+ ),
2318
+ lineage: prepared.project.mapMetadata?.[mapName]?.lineage ?? [
2319
+ { id: map.id, mapVersion: map.version }
2320
+ ],
2321
+ map,
2322
+ sourceAssets: {
2323
+ fonts: preparedFonts.sourceIdentities[mapName] ?? [],
2324
+ icons: prepared.mapIconSources[mapName] ?? [],
2325
+ ...prepared.mapIconCompositions[mapName] ? { iconComposition: prepared.mapIconCompositions[mapName] } : {},
2326
+ ...prepared.mapModelSources?.[mapName]?.length ? { models: prepared.mapModelSources[mapName] } : {}
2327
+ },
2328
+ styles: style
2329
+ }
2330
+ ];
2331
+ })
2332
+ ),
2333
+ { provenance }
2334
+ );
2335
+ const stableManifest = renderer === "native" ? createManifest(prepared.project, {
2336
+ styleBaseUrl: nativePublicBase(options.styleBaseUrl ?? ".")
2337
+ }) : createFontAwareManifest(
2338
+ createManifest(prepared.project, { styleBaseUrl: options.styleBaseUrl }),
2339
+ styles
2340
+ );
2341
+ const inputs = {
2342
+ directories: uniqueStrings(
2343
+ [...prepared.watchPaths, ...preparedFonts.watchPaths].map(canonicalInputPath)
2344
+ ),
2345
+ files: uniqueStrings(
2346
+ [...options.inputFiles ?? [], ...prepared.watchFiles, ...localTilesets.watchPaths].map(
2347
+ canonicalInputPath
2348
+ )
2349
+ )
2350
+ };
2351
+ const partial = {
2352
+ mapAssets: prepared.mapAssets,
2353
+ assets,
2354
+ buildManifest,
2355
+ ...renderer === "native" ? {
2356
+ nativeBuild: createTileflowNativeBuildRecord2(
2357
+ hashBytes(serializeCanonicalJson3(buildManifest)),
2358
+ nativeLowering.transformations
2359
+ )
2360
+ } : {},
2361
+ dispose: localTilesets.dispose,
2362
+ manifest: stableManifest,
2363
+ project: prepared.project,
2364
+ ...localTilesets.files.length > 0 ? { localTilesets: localTilesets.files } : {},
2365
+ ...Object.keys(lineBackgroundGlyphs.providers).length > 0 ? { lineBackgroundGlyphs: lineBackgroundGlyphs.providers } : {},
2366
+ ...inspected ? { styleInspections: inspected.inspections } : {},
2367
+ styles,
2368
+ watchPaths: uniqueStrings([...inputs.files, ...inputs.directories])
2369
+ };
2370
+ const generation = hashArtifactGeneration(getStableTileflowArtifactFiles(partial));
2371
+ const generatedManifest = createGenerationManifest(stableManifest, generation, assets);
2372
+ const manifest = renderer === "native" ? parseTileflowRuntimeManifest(generatedManifest) : generatedManifest;
2373
+ const generationArtifacts = { ...partial, manifest };
2374
+ return {
2375
+ ...generationArtifacts,
2376
+ files: createGenerationArtifactFiles(generationArtifacts, generation, nativeUrls),
2377
+ inputs,
2378
+ project: prepared.project,
2379
+ schemaVersion: tileflowArtifactPlanSchemaVersion,
2380
+ sourceProject: prepared.sourceProject
2381
+ };
2382
+ } catch (error) {
2383
+ await localTilesets.dispose().catch(() => void 0);
2384
+ throw error;
2385
+ }
2386
+ }
2387
+ async function createTileflowBuildArtifacts(options = {}) {
2388
+ resolveTileflowRenderer(options.renderer);
2389
+ assertNativeDevelopmentOrigin(options);
2390
+ const loaded = await loadValidTileflowConfigWithInputs(
2391
+ options.config ?? defaultTileflowConfigPath,
2392
+ {
2393
+ cwd: options.cwd,
2394
+ fresh: true
2395
+ }
2396
+ );
2397
+ const prepared = await prepareTileflowCatalogIcons(loaded.project, {
2398
+ assetBaseUrl: resolveRendererAssetBaseUrl(options),
2399
+ baseDirectory: dirname2(loaded.configFile),
2400
+ cwd: options.cwd ?? process.cwd(),
2401
+ ...options.icons ? { icons: options.icons } : {}
2402
+ });
2403
+ return await createTileflowArtifactPlan(prepared, { ...options, inputFiles: loaded.inputFiles });
2404
+ }
2405
+ function getTileflowArtifactFiles(artifacts) {
2406
+ if ("files" in artifacts && Array.isArray(artifacts.files)) {
2407
+ return validateArtifactFiles(artifacts.files);
2408
+ }
2409
+ return getStableTileflowArtifactFiles(artifacts);
2410
+ }
2411
+ function getStableTileflowArtifactFiles(artifacts) {
2412
+ const files = [
2413
+ {
2414
+ contentType: "application/json; charset=utf-8",
2415
+ fileName: tileflowMapBuildManifestFileName,
2416
+ source: `${serializeCanonicalJson3(artifacts.buildManifest)}
2417
+ `
2418
+ },
2419
+ {
2420
+ contentType: "application/json; charset=utf-8",
2421
+ fileName: "manifest.json",
2422
+ source: `${JSON.stringify(artifacts.manifest, null, 2)}
2423
+ `
2424
+ },
2425
+ ...Object.entries(artifacts.styles).flatMap(
2426
+ ([mapName, themes]) => Object.entries(themes).map(([themeName, style]) => ({
2427
+ contentType: "application/json; charset=utf-8",
2428
+ fileName: `styles/${mapName}/${themeName}.json`,
2429
+ source: `${JSON.stringify(style, null, 2)}
2430
+ `
2431
+ }))
2432
+ ),
2433
+ ...artifacts.assets,
2434
+ ...artifacts.nativeBuild ? [
2435
+ {
2436
+ contentType: "application/json; charset=utf-8",
2437
+ fileName: tileflowNativeBuildRecordFileName,
2438
+ source: `${serializeCanonicalJson3(artifacts.nativeBuild)}
2439
+ `
2440
+ }
2441
+ ] : []
2442
+ ];
2443
+ return validateArtifactFiles(files);
2444
+ }
2445
+ function validateArtifactFiles(files) {
2446
+ const seen = /* @__PURE__ */ new Set();
2447
+ for (const file of files) {
2448
+ assertTileflowArtifactFileName(file.fileName);
2449
+ if (file.fileName === tileflowArtifactInventoryFileName) {
2450
+ throw new Error(`${file.fileName} is reserved for the Tileflow artifact inventory.`);
2451
+ }
2452
+ if (seen.has(file.fileName)) {
2453
+ throw new Error(`Duplicate Tileflow artifact file: ${file.fileName}`);
2454
+ }
2455
+ seen.add(file.fileName);
2456
+ }
2457
+ return [...files].sort((left, right) => compareCodeUnits2(left.fileName, right.fileName));
2458
+ }
2459
+ function createGenerationManifest(manifest, generation, assets) {
2460
+ return {
2461
+ ...manifest,
2462
+ maps: Object.fromEntries(
2463
+ Object.entries(manifest.maps).map(([mapName, map]) => [
2464
+ mapName,
2465
+ {
2466
+ ...map,
2467
+ themes: Object.fromEntries(
2468
+ Object.entries(map.themes).map(([themeName, theme]) => [
2469
+ themeName,
2470
+ {
2471
+ ...theme,
2472
+ styleUrl: insertGenerationInPublicUrl(
2473
+ theme.styleUrl,
2474
+ `styles/${mapName}/${themeName}.json`,
2475
+ generation
2476
+ ),
2477
+ ...theme.fontFaces ? {
2478
+ fontFaces: theme.fontFaces.map((fontFace) => ({
2479
+ ...fontFace,
2480
+ source: retargetLocalFontAssetUrl(fontFace.source, assets, generation)
2481
+ }))
2482
+ } : {}
2483
+ }
2484
+ ])
2485
+ )
2486
+ }
2487
+ ])
2488
+ )
2489
+ };
2490
+ }
2491
+ function createGenerationArtifactFiles(artifacts, generation, nativeUrls) {
2492
+ const prefix = `generations/${generation}`;
2493
+ const stableFiles = getStableTileflowArtifactFiles(artifacts);
2494
+ const immutableStyles = Object.entries(artifacts.styles).flatMap(
2495
+ ([mapName, themes]) => Object.entries(themes).map(([themeName, style]) => {
2496
+ const hasLocalSprite = artifacts.assets.some(
2497
+ (asset) => asset.fileName.startsWith(`icons/${mapName}/`)
2498
+ );
2499
+ const sprite = style.sprite;
2500
+ const generationStyleWithSprite = hasLocalSprite && typeof sprite === "string" ? {
2501
+ ...style,
2502
+ // Every theme in one logical map shares the same immutable sprite closure.
2503
+ sprite: isRelativePublicUrl(sprite) ? sprite : insertGenerationInPublicUrl(sprite, `icons/${mapName}/sprite`, generation)
2504
+ } : style;
2505
+ const replaceFontSources = artifacts.nativeBuild ? replaceTileflowNativeFontSources : replaceTileflowStyleFontSources;
2506
+ const generationStyle = replaceFontSources(
2507
+ withGenerationBuildingModels(
2508
+ generationStyleWithSprite,
2509
+ mapName,
2510
+ generation,
2511
+ artifacts.assets
2512
+ ),
2513
+ (source) => retargetLocalFontAssetUrl(source, artifacts.assets, generation)
2514
+ );
2515
+ if (artifacts.nativeBuild)
2516
+ assertTileflowNativeGeneratedStyle(generationStyle, mapName, themeName, nativeUrls);
2517
+ return {
2518
+ contentType: "application/json; charset=utf-8",
2519
+ fileName: `${prefix}/styles/${mapName}/${themeName}.json`,
2520
+ source: `${JSON.stringify(generationStyle, null, 2)}
2521
+ `
2522
+ };
2523
+ })
2524
+ );
2525
+ const immutableAssets = artifacts.assets.map((asset) => ({
2526
+ ...asset,
2527
+ fileName: `${prefix}/${asset.fileName}`
2528
+ }));
2529
+ return validateArtifactFiles([...stableFiles, ...immutableStyles, ...immutableAssets]);
2530
+ }
2531
+ function createFontAwareManifest(manifest, styles) {
2532
+ const maps = Object.fromEntries(
2533
+ Object.entries(manifest.maps).map(([mapName, map]) => [
2534
+ mapName,
2535
+ {
2536
+ ...map,
2537
+ themes: Object.fromEntries(
2538
+ Object.entries(map.themes).map(([themeName, theme]) => {
2539
+ const style = styles[mapName]?.[themeName];
2540
+ if (!style) {
2541
+ throw new Error(`Missing compiled Tileflow style for ${mapName}/${themeName}.`);
2542
+ }
2543
+ const definitions = getTileflowStyleFontFaces2(style);
2544
+ return [themeName, { ...theme, fontFaces: definitions }];
2545
+ })
2546
+ )
2547
+ }
2548
+ ])
2549
+ );
2550
+ return parseTileflowRuntimeManifest({ ...manifest, maps });
2551
+ }
2552
+ function retargetLocalFontAssetUrl(source, assets, generation) {
2553
+ if (isRelativePublicUrl(source)) return source;
2554
+ const asset = assets.find(
2555
+ (candidate) => candidate.fileName.startsWith("fonts/") && (source === candidate.fileName || source.endsWith(`/${candidate.fileName}`))
2556
+ );
2557
+ if (!asset) throw new Error(`Unable to resolve Tileflow font artifact URL: ${source}`);
2558
+ return insertGenerationInPublicUrl(source, asset.fileName, generation);
2559
+ }
2560
+ function getMapBuildAssets(mapName, assets, fontAssets) {
2561
+ const selected = /* @__PURE__ */ new Map();
2562
+ for (const asset of assets) {
2563
+ if (asset.fileName.startsWith(`icons/${mapName}/`) || asset.fileName.startsWith(`models/${mapName}/`) || asset.fileName.startsWith(`images/${mapName}/`))
2564
+ selected.set(asset.fileName, asset);
2565
+ }
2566
+ for (const asset of fontAssets) {
2567
+ selected.set(asset.fileName, asset);
2568
+ }
2569
+ return [...selected.values()].sort(
2570
+ (left, right) => compareCodeUnits2(left.fileName, right.fileName)
2571
+ );
2572
+ }
2573
+ function hashBytes(source) {
2574
+ return createHash5("sha256").update(typeof source === "string" ? Buffer.from(source, "utf8") : Buffer.from(source)).digest("hex");
2575
+ }
2576
+ async function createTileflowBuildProvenance(cwd) {
2577
+ const projectDirectory = resolve4(cwd);
2578
+ const projectRequire = createRequire(
2579
+ resolve4(projectDirectory, "__tileflow_build_provenance__.cjs")
2580
+ );
2581
+ const toolRequire = createRequire(import.meta.url);
2582
+ const packages = {};
2583
+ packages["@tileflow/core"] = await readPackageVersion(
2584
+ "@tileflow/core",
2585
+ resolvePackageJson(projectRequire, "@tileflow/core") ?? resolvePackageJson(toolRequire, "@tileflow/core")
2586
+ );
2587
+ packages["@tileflow/dev"] = await readPackageVersion(
2588
+ "@tileflow/dev",
2589
+ new URL("../package.json", import.meta.url)
2590
+ );
2591
+ const mapsPackage = resolvePackageJson(projectRequire, "@tileflow/maps");
2592
+ if (mapsPackage) {
2593
+ packages["@tileflow/maps"] = await readPackageVersion("@tileflow/maps", mapsPackage);
2594
+ }
2595
+ const lockfile = await findNearestLockfile(projectDirectory);
2596
+ return {
2597
+ ...lockfile ? { lockfile: { format: lockfile.format, sha256: hashBytes(lockfile.source) } } : {},
2598
+ packages,
2599
+ schemaVersion: 1
2600
+ };
2601
+ }
2602
+ function resolvePackageJson(require2, packageName) {
2603
+ try {
2604
+ return require2.resolve(`${packageName}/package.json`);
2605
+ } catch {
2606
+ return void 0;
2607
+ }
2608
+ }
2609
+ async function readPackageVersion(expectedName, path) {
2610
+ if (!path) throw new Error(`Unable to resolve build package provenance for ${expectedName}.`);
2611
+ let parsed;
2612
+ try {
2613
+ parsed = JSON.parse(await readFile2(path, "utf8"));
2614
+ } catch {
2615
+ throw new Error(`Unable to read build package provenance for ${expectedName}.`);
2616
+ }
2617
+ if (!parsed || typeof parsed !== "object" || parsed.name !== expectedName || typeof parsed.version !== "string") {
2618
+ throw new Error(`Invalid build package provenance for ${expectedName}.`);
2619
+ }
2620
+ return parsed.version;
2621
+ }
2622
+ async function findNearestLockfile(cwd) {
2623
+ const candidates = [
2624
+ { fileName: "pnpm-lock.yaml", format: "pnpm" },
2625
+ { fileName: "package-lock.json", format: "npm" },
2626
+ { fileName: "yarn.lock", format: "yarn" },
2627
+ { fileName: "bun.lock", format: "bun" },
2628
+ { fileName: "bun.lockb", format: "bun" }
2629
+ ];
2630
+ let directory = cwd;
2631
+ while (true) {
2632
+ for (const candidate of candidates) {
2633
+ try {
2634
+ return {
2635
+ format: candidate.format,
2636
+ source: await readFile2(resolve4(directory, candidate.fileName))
2637
+ };
2638
+ } catch (error) {
2639
+ if (!hasErrorCode2(error, "ENOENT")) throw error;
2640
+ }
2641
+ }
2642
+ const parent = dirname2(directory);
2643
+ if (parent === directory) return void 0;
2644
+ directory = parent;
2645
+ }
2646
+ }
2647
+ function withGenerationBuildingModels(style, mapName, generation, assets) {
2648
+ const metadata = style.metadata;
2649
+ const buildings = metadata?.["tileflow:buildings"];
2650
+ if (!buildings || !Array.isArray(buildings.models)) return style;
2651
+ const models = buildings.models.map((entry) => {
2652
+ const url = entry.url;
2653
+ if (typeof url !== "string" || isRelativePublicUrl(url)) return entry;
2654
+ const file = url.slice(url.lastIndexOf("/") + 1);
2655
+ const logical = `models/${mapName}/${file}`;
2656
+ if (!url.endsWith(`/${logical}`) || !assets.some((asset) => asset.fileName === logical))
2657
+ return entry;
2658
+ return {
2659
+ ...entry,
2660
+ url: insertGenerationInPublicUrl(url, logical, generation)
2661
+ };
2662
+ });
2663
+ return {
2664
+ ...style,
2665
+ metadata: { ...metadata, "tileflow:buildings": { ...buildings, models } }
2666
+ };
2667
+ }
2668
+ function insertGenerationInPublicUrl(url, logicalPath, generation) {
2669
+ const marker = `/${logicalPath}`;
2670
+ const index = url.lastIndexOf(marker);
2671
+ if (index >= 0 && index + marker.length === url.length) {
2672
+ return `${url.slice(0, index)}/generations/${generation}${marker}`;
2673
+ }
2674
+ if (url === logicalPath) return `generations/${generation}/${logicalPath}`;
2675
+ throw new Error(`Unable to content-address Tileflow artifact URL: ${url}`);
2676
+ }
2677
+ function createTileflowArtifactSession(options = {}) {
2678
+ return createTileflowArtifactSessionWithBuilder(
2679
+ options,
2680
+ createTileflowBuildArtifacts,
2681
+ () => getTileflowWatchPaths(options)
2682
+ );
2683
+ }
2684
+ async function getTileflowWatchPaths(options = {}) {
2685
+ const cwd = options.cwd ?? process.cwd();
2686
+ const loaded = await loadValidTileflowConfigWithInputs(
2687
+ options.config ?? defaultTileflowConfigPath,
2688
+ { cwd, fresh: true }
2689
+ );
2690
+ const [iconPaths, fontPaths] = await Promise.all([
2691
+ getTileflowIconWatchPaths(loaded.project, cwd, dirname2(loaded.configFile)),
2692
+ getTileflowFontWatchPaths(loaded.project, cwd, dirname2(loaded.configFile))
2693
+ ]);
2694
+ return uniqueStrings([...loaded.inputFiles, ...iconPaths, ...fontPaths]);
2695
+ }
2696
+ function isTileflowArtifactInputPath(inputs, path) {
2697
+ const candidate = canonicalInputPath(path);
2698
+ return inputs.files.some((input) => canonicalInputPath(input) === candidate) || inputs.directories.some((input) => isPathWithin(canonicalInputPath(input), candidate));
2699
+ }
2700
+ async function refreshTileflowArtifactSession(session, options) {
2701
+ const inputs = getArtifactInputGraph(session.getLastGoodArtifacts());
2702
+ if (options.inputPath && inputs && !isTileflowArtifactInputPath(inputs, options.inputPath)) {
2703
+ return false;
2704
+ }
2705
+ await session.refresh(options.reason);
2706
+ return true;
2707
+ }
2708
+ function getArtifactInputGraph(artifacts) {
2709
+ if (!artifacts || !("inputs" in artifacts)) return void 0;
2710
+ const inputs = artifacts.inputs;
2711
+ return inputs && Array.isArray(inputs.files) && Array.isArray(inputs.directories) ? inputs : void 0;
2712
+ }
2713
+ async function writeTileflowBuildArtifacts(options) {
2714
+ const artifacts = await createTileflowBuildArtifacts({ ...options, target: "production" });
2715
+ try {
2716
+ await writeTileflowArtifactPlan(artifacts, options);
2717
+ return { ...artifacts, dispose: void 0, localTilesets: void 0 };
2718
+ } finally {
2719
+ await disposeTileflowBuildArtifacts(artifacts);
2720
+ }
2721
+ }
2722
+ async function disposeTileflowBuildArtifacts(artifacts) {
2723
+ const dispose = artifacts.dispose;
2724
+ artifacts.dispose = void 0;
2725
+ await dispose?.();
2726
+ }
2727
+ async function writeTileflowArtifactPlan(artifacts, options) {
2728
+ assertNoLocalTilesetsForProduction(artifacts);
2729
+ const cwd = options.cwd ?? process.cwd();
2730
+ const outDir = artifacts.nativeBuild ? `${options.outDir.replace(/[\\/]$/u, "")}/native` : options.outDir;
2731
+ const output = await prepareTileflowOutputDirectory(cwd, outDir);
2732
+ const files = getTileflowArtifactFiles(artifacts);
2733
+ await assertTileflowSelfHostedManifestTarget(resolve4(output.outDir, "manifest.json"), options);
2734
+ await commitTileflowArtifactFiles(output, files);
2735
+ }
2736
+ function assertNoLocalTilesetsForProduction(artifacts) {
2737
+ if (artifacts.localTilesets?.length) throw new TileflowLocalTilesetProductionError();
2738
+ }
2739
+ function hasLocalTilesetSources(project) {
2740
+ return Object.values(project.maps).some((map) => Object.keys(map.sources ?? {}).length > 0);
2741
+ }
2742
+ async function assertTileflowSelfHostedManifestTarget(manifestPath, options = {}) {
2743
+ if (options.overwriteHostedManifest) return;
2744
+ let source;
2745
+ try {
2746
+ source = await readFile2(resolve4(manifestPath), "utf8");
2747
+ } catch (error) {
2748
+ if (hasErrorCode2(error, "ENOENT")) return;
2749
+ throw error;
2750
+ }
2751
+ try {
2752
+ const manifest = parseTileflowRuntimeManifest(JSON.parse(source));
2753
+ if (isHostedRuntimeManifest(manifest)) {
2754
+ throw new TileflowHostedManifestOverwriteError(resolve4(manifestPath));
2755
+ }
2756
+ } catch (error) {
2757
+ if (error instanceof TileflowHostedManifestOverwriteError) throw error;
2758
+ }
2759
+ }
2760
+ function isHostedRuntimeManifest(manifest) {
2761
+ return Boolean(
2762
+ manifest.apiUrl || Object.values(manifest.maps).some(
2763
+ (map) => map.apiUrl || map.environment || map.mapId || map.usageMode || map.worldGeneration || Object.values(map.themes).some((theme) => theme.styleId)
2764
+ )
2765
+ );
2766
+ }
2767
+ function assertValidTileflowStyle(style, mapName) {
2768
+ const issues = validateTileflowStyle(style, mapName);
2769
+ if (issues.length > 0) {
2770
+ throw new TileflowStyleValidationError(normalizeTileflowStyleValidationIssues(issues));
2771
+ }
2772
+ }
2773
+ async function commitTileflowArtifactFiles(output, files) {
2774
+ await assertTileflowOutputDirectoryIsSafe(output);
2775
+ const { outDir } = output;
2776
+ const previous = await readTileflowArtifactInventory(outDir);
2777
+ const desiredNames = files.map((file) => file.fileName);
2778
+ const generation = getArtifactGeneration(files);
2779
+ const retainedGenerations = previous ? generation === previous.generation ? getManagedGenerations(previous.files) : [previous.generation] : [];
2780
+ const retainedPrefixes = retainedGenerations.map((item) => `generations/${item}/`);
2781
+ const retainedGenerationNames = previous ? previous.files.filter((name) => retainedPrefixes.some((prefix) => name.startsWith(prefix))) : [];
2782
+ const retainedNames = uniqueStrings(retainedGenerationNames);
2783
+ const inventoryNames = uniqueStrings([...desiredNames, ...retainedNames]);
2784
+ const inventory = {
2785
+ files: inventoryNames,
2786
+ generation,
2787
+ schemaVersion: 1
2788
+ };
2789
+ const inventoryFile = {
2790
+ contentType: "application/json; charset=utf-8",
2791
+ fileName: tileflowArtifactInventoryFileName,
2792
+ source: `${JSON.stringify(inventory, null, 2)}
2793
+ `
2794
+ };
2795
+ const stagedFiles = [...files, inventoryFile];
2796
+ const staleNames = (previous?.files ?? []).filter((name) => !inventoryNames.includes(name));
2797
+ const affectedNames = uniqueStrings([
2798
+ ...desiredNames,
2799
+ ...staleNames,
2800
+ tileflowArtifactInventoryFileName
2801
+ ]);
2802
+ const stageDir = await mkdtemp(resolve4(outDir, ".tileflow-stage-"));
2803
+ const newDir = resolve4(stageDir, "new");
2804
+ const backupDir = resolve4(stageDir, "backup");
2805
+ const backedUp = /* @__PURE__ */ new Set();
2806
+ const installed = /* @__PURE__ */ new Set();
2807
+ try {
2808
+ for (const file of stagedFiles) {
2809
+ const stagePath = managedPath(newDir, file.fileName);
2810
+ await mkdir3(dirname2(stagePath), { recursive: true });
2811
+ await writeFileDurably(stagePath, file.source);
2812
+ }
2813
+ for (const name of affectedNames) {
2814
+ await assertManagedTargetIsSafe(outDir, name);
2815
+ const targetPath = managedPath(outDir, name);
2816
+ if (!await isFile(targetPath)) continue;
2817
+ const backupPath = managedPath(backupDir, name);
2818
+ await mkdir3(dirname2(backupPath), { recursive: true });
2819
+ await copyFile2(targetPath, backupPath);
2820
+ backedUp.add(name);
2821
+ }
2822
+ const commitOrder = stagedFiles.map((file) => file.fileName).sort((left, right) => {
2823
+ const leftRank = commitRank(left);
2824
+ const rightRank = commitRank(right);
2825
+ return leftRank - rightRank || compareCodeUnits2(left, right);
2826
+ });
2827
+ for (const name of commitOrder.filter(
2828
+ (candidate) => candidate !== tileflowArtifactInventoryFileName
2829
+ )) {
2830
+ const sourcePath = managedPath(newDir, name);
2831
+ const targetPath = managedPath(outDir, name);
2832
+ await mkdir3(dirname2(targetPath), { recursive: true });
2833
+ await rename2(sourcePath, targetPath);
2834
+ installed.add(name);
2835
+ }
2836
+ for (const name of staleNames) {
2837
+ await removeManagedFile(managedPath(outDir, name));
2838
+ }
2839
+ const stagedInventoryPath = managedPath(newDir, tileflowArtifactInventoryFileName);
2840
+ const inventoryPath = managedPath(outDir, tileflowArtifactInventoryFileName);
2841
+ await rename2(stagedInventoryPath, inventoryPath);
2842
+ installed.add(tileflowArtifactInventoryFileName);
2843
+ } catch (error) {
2844
+ await rollbackManagedFiles(outDir, backupDir, affectedNames, backedUp, installed);
2845
+ throw error;
2846
+ } finally {
2847
+ await rm3(stageDir, { force: true, recursive: true });
2848
+ }
2849
+ }
2850
+ function getArtifactGeneration(files) {
2851
+ const generations = getManagedGenerations(files.map((file) => file.fileName));
2852
+ if (generations.length !== 1) {
2853
+ throw new Error("A Tileflow artifact plan must contain exactly one content generation.");
2854
+ }
2855
+ return generations[0];
2856
+ }
2857
+ function getManagedGenerations(fileNames) {
2858
+ return uniqueStrings(
2859
+ fileNames.flatMap((fileName) => {
2860
+ const match = /^generations\/([a-f0-9]{64})\//.exec(fileName);
2861
+ return match?.[1] ? [match[1]] : [];
2862
+ })
2863
+ );
2864
+ }
2865
+ function commitRank(fileName) {
2866
+ if (fileName === tileflowArtifactInventoryFileName) return 2;
2867
+ if (fileName === "manifest.json") return 1;
2868
+ return 0;
2869
+ }
2870
+ async function rollbackManagedFiles(outDir, backupDir, affectedNames, backedUp, installed) {
2871
+ const rollbackErrors = [];
2872
+ for (const name of [...affectedNames].reverse()) {
2873
+ try {
2874
+ const targetPath = managedPath(outDir, name);
2875
+ if (installed.has(name) && !backedUp.has(name)) {
2876
+ await removeManagedFile(targetPath);
2877
+ }
2878
+ if (backedUp.has(name)) {
2879
+ await mkdir3(dirname2(targetPath), { recursive: true });
2880
+ await copyFile2(managedPath(backupDir, name), targetPath);
2881
+ }
2882
+ } catch (error) {
2883
+ rollbackErrors.push(error);
2884
+ }
2885
+ }
2886
+ if (rollbackErrors.length > 0) {
2887
+ throw new AggregateError(rollbackErrors, "Unable to restore the previous Tileflow generation.");
2888
+ }
2889
+ }
2890
+ async function readTileflowArtifactInventory(outDir) {
2891
+ const inventoryPath = managedPath(outDir, tileflowArtifactInventoryFileName);
2892
+ let source;
2893
+ try {
2894
+ source = await readFile2(inventoryPath, "utf8");
2895
+ } catch (error) {
2896
+ if (hasErrorCode2(error, "ENOENT")) return void 0;
2897
+ throw error;
2898
+ }
2899
+ let candidate;
2900
+ try {
2901
+ candidate = JSON.parse(source);
2902
+ } catch {
2903
+ throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
2904
+ }
2905
+ if (!candidate || typeof candidate !== "object" || candidate.schemaVersion !== 1 || typeof candidate.generation !== "string" || !/^[a-f0-9]{64}$/.test(candidate.generation) || !Array.isArray(candidate.files) || candidate.files.length > 1e4) {
2906
+ throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
2907
+ }
2908
+ const files = candidate.files;
2909
+ if (!files.every((name) => typeof name === "string")) {
2910
+ throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
2911
+ }
2912
+ try {
2913
+ for (const name of files) assertTileflowArtifactFileName(name);
2914
+ } catch {
2915
+ throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
2916
+ }
2917
+ if (new Set(files).size !== files.length || files.includes(tileflowArtifactInventoryFileName)) {
2918
+ throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
2919
+ }
2920
+ return {
2921
+ files: [...files],
2922
+ generation: candidate.generation,
2923
+ schemaVersion: 1
2924
+ };
2925
+ }
2926
+ async function assertManagedTargetIsSafe(outDir, fileName) {
2927
+ assertSafeManagedFileName(fileName);
2928
+ const rootStats = await lstat3(outDir);
2929
+ if (rootStats.isSymbolicLink()) {
2930
+ throw new Error(`Tileflow refuses to write through a symbolic link: ${outDir}`);
2931
+ }
2932
+ if (!rootStats.isDirectory()) {
2933
+ throw new Error(`Tileflow artifact output is not a directory: ${outDir}`);
2934
+ }
2935
+ const segments = fileName.split("/");
2936
+ let current = outDir;
2937
+ for (let index = 0; index < segments.length; index += 1) {
2938
+ current = resolve4(current, segments[index]);
2939
+ try {
2940
+ const stats = await lstat3(current);
2941
+ if (stats.isSymbolicLink()) {
2942
+ throw new Error(`Tileflow refuses to write through a symbolic link: ${fileName}`);
2943
+ }
2944
+ if (index < segments.length - 1 && !stats.isDirectory()) {
2945
+ throw new Error(`Tileflow artifact parent is not a directory: ${fileName}`);
2946
+ }
2947
+ if (index === segments.length - 1 && !stats.isFile()) {
2948
+ throw new Error(`Tileflow artifact target is not a file: ${fileName}`);
2949
+ }
2950
+ } catch (error) {
2951
+ if (hasErrorCode2(error, "ENOENT")) return;
2952
+ throw error;
2953
+ }
2954
+ }
2955
+ }
2956
+ async function prepareTileflowOutputDirectory(cwd, requestedOutDir) {
2957
+ const logicalRoot = resolve4(cwd);
2958
+ const logicalOutDir = resolve4(logicalRoot, requestedOutDir);
2959
+ if (!isPathWithin(logicalRoot, logicalOutDir)) {
2960
+ throw new Error(`Tileflow artifact output escapes its working directory: ${logicalOutDir}`);
2961
+ }
2962
+ const rootStats = await lstat3(logicalRoot);
2963
+ if (!rootStats.isDirectory()) {
2964
+ throw new Error(`Tileflow artifact working directory is not a directory: ${logicalRoot}`);
2965
+ }
2966
+ const rootDir = await realpath2(logicalRoot);
2967
+ const relativeOutDir = relative2(logicalRoot, logicalOutDir);
2968
+ const segments = relativeOutDir ? relativeOutDir.split(sep2) : [];
2969
+ let current = rootDir;
2970
+ for (const segment of segments) {
2971
+ current = resolve4(current, segment);
2972
+ let stats;
2973
+ try {
2974
+ stats = await lstat3(current);
2975
+ } catch (error) {
2976
+ if (!hasErrorCode2(error, "ENOENT")) throw error;
2977
+ await mkdir3(current);
2978
+ stats = await lstat3(current);
2979
+ }
2980
+ if (stats.isSymbolicLink()) {
2981
+ throw new Error(`Tileflow refuses to write through a symbolic link: ${current}`);
2982
+ }
2983
+ if (!stats.isDirectory()) {
2984
+ throw new Error(`Tileflow artifact output ancestor is not a directory: ${current}`);
2985
+ }
2986
+ }
2987
+ const outDir = await realpath2(current);
2988
+ const output = { outDir, rootDir };
2989
+ await assertTileflowOutputDirectoryIsSafe(output);
2990
+ return output;
2991
+ }
2992
+ async function assertTileflowOutputDirectoryIsSafe(output) {
2993
+ if (!isPathWithin(output.rootDir, output.outDir)) {
2994
+ throw new Error(`Tileflow artifact output escapes its working directory: ${output.outDir}`);
2995
+ }
2996
+ const stats = await lstat3(output.outDir);
2997
+ if (stats.isSymbolicLink()) {
2998
+ throw new Error(`Tileflow refuses to write through a symbolic link: ${output.outDir}`);
2999
+ }
3000
+ if (!stats.isDirectory()) {
3001
+ throw new Error(`Tileflow artifact output is not a directory: ${output.outDir}`);
3002
+ }
3003
+ const canonicalOutDir = await realpath2(output.outDir);
3004
+ if (canonicalOutDir !== output.outDir || !isPathWithin(output.rootDir, canonicalOutDir)) {
3005
+ throw new Error(`Tileflow artifact output escapes its working directory: ${output.outDir}`);
3006
+ }
3007
+ }
3008
+ function assertSafeManagedFileName(fileName) {
3009
+ if (!fileName || fileName.includes("\\") || fileName.includes("\0") || fileName.startsWith("/") || fileName.endsWith("/") || fileName.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
3010
+ throw new Error(`Unsafe Tileflow artifact file name: ${fileName}`);
3011
+ }
3012
+ }
3013
+ function assertTileflowArtifactFileName(fileName) {
3014
+ assertSafeManagedFileName(fileName);
3015
+ const logicalName = fileName.replace(/^generations\/[a-f0-9]{64}\//, "");
3016
+ const styleMatch = /^styles\/([^/]+)\/([^/]+)\.json$/u.exec(logicalName);
3017
+ const iconMatch = /^icons\/([^/]+)\/sprite(?:@2x)?\.(?:json|png)$/u.exec(logicalName);
3018
+ const imageMatch = /^images\/([^/]+)\/[a-f0-9]{64}\.png$/u.exec(logicalName);
3019
+ const modelMatch = /^models\/([^/]+)\/[a-z0-9]+(?:-[a-z0-9]+)*-[a-f0-9]{16}\.glb$/u.exec(
3020
+ logicalName
3021
+ );
3022
+ if (fileName === "manifest.json" || fileName === tileflowMapBuildManifestFileName || fileName === tileflowNativeBuildRecordFileName || styleMatch && tileflowMapIdSchema.safeParse(styleMatch[1]).success && tileflowThemeNameSchema.safeParse(styleMatch[2]).success || iconMatch && tileflowMapIdSchema.safeParse(iconMatch[1]).success || imageMatch && tileflowMapIdSchema.safeParse(imageMatch[1]).success || modelMatch && tileflowMapIdSchema.safeParse(modelMatch[1]).success || /^tilesets\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.pmtiles$/u.test(logicalName) || /^fonts\/[a-z0-9]+(?:-[a-z0-9]+)*-[a-f0-9]{64}\.(?:otf|ttf|woff2)$/.test(logicalName) || /^fonts\/licenses\/license-[a-f0-9]{64}\.txt$/.test(logicalName)) {
3023
+ return;
3024
+ }
3025
+ throw new Error(`Unexpected Tileflow managed artifact file name: ${fileName}`);
3026
+ }
3027
+ function managedPath(root, fileName) {
3028
+ assertSafeManagedFileName(fileName);
3029
+ const path = resolve4(root, ...fileName.split("/"));
3030
+ if (!isPathWithin(root, path)) {
3031
+ throw new Error(`Unsafe Tileflow artifact file name: ${fileName}`);
3032
+ }
3033
+ return path;
3034
+ }
3035
+ async function writeFileDurably(path, source) {
3036
+ const handle = await open2(path, "wx");
3037
+ try {
3038
+ await handle.writeFile(source);
3039
+ await handle.sync();
3040
+ } finally {
3041
+ await handle.close();
3042
+ }
3043
+ }
3044
+ async function isFile(path) {
3045
+ try {
3046
+ return (await lstat3(path)).isFile();
3047
+ } catch (error) {
3048
+ if (hasErrorCode2(error, "ENOENT")) return false;
3049
+ throw error;
3050
+ }
3051
+ }
3052
+ async function removeManagedFile(path) {
3053
+ try {
3054
+ const stats = await lstat3(path);
3055
+ if (!stats.isFile()) throw new Error(`Refusing to remove non-file artifact target: ${path}`);
3056
+ await unlink(path);
3057
+ } catch (error) {
3058
+ if (!hasErrorCode2(error, "ENOENT")) throw error;
3059
+ }
3060
+ }
3061
+ function hashArtifactGeneration(files) {
3062
+ const hash = createHash5("sha256");
3063
+ for (const file of files) {
3064
+ const source = typeof file.source === "string" ? Buffer.from(file.source, "utf8") : Buffer.from(file.source);
3065
+ hash.update(`${file.fileName}\0${file.contentType}\0${source.byteLength}\0`, "utf8");
3066
+ hash.update(source);
3067
+ }
3068
+ return hash.digest("hex");
3069
+ }
3070
+ function assertNativeDevelopmentOrigin(options) {
3071
+ if (options.developmentOrigin !== void 0 && (resolveTileflowRenderer(options.renderer) !== "native" || options.target === "production")) {
3072
+ throw new TileflowNativeCompatibilityError4([
3073
+ createTileflowNativeDiagnostic3("NATIVE_RENDERER_UNSUPPORTED", "/developmentOrigin")
3074
+ ]);
3075
+ }
3076
+ }
3077
+ function resolveRendererAssetBaseUrl(options) {
3078
+ const base = resolveAssetBaseUrl(options);
3079
+ return options.renderer === "native" ? nativePublicBase(base) : base;
3080
+ }
3081
+ function nativePublicBase(base) {
3082
+ return isRelativePublicUrl(base) ? base : `${base.replace(/\/+$/u, "")}/native`;
3083
+ }
3084
+ function resolveAssetBaseUrl(options) {
3085
+ if (options.assetBaseUrl !== void 0) return options.assetBaseUrl;
3086
+ if (options.styleBaseUrl && !isRelativePublicUrl(options.styleBaseUrl)) {
3087
+ return options.styleBaseUrl;
3088
+ }
3089
+ return "../..";
3090
+ }
3091
+ function isRelativePublicUrl(value) {
3092
+ return !(value.startsWith("http://") || value.startsWith("https://") || value.startsWith("/") || value.startsWith("data:"));
3093
+ }
3094
+ function uniqueStrings(values) {
3095
+ return [...new Set(values)].sort(compareCodeUnits2);
3096
+ }
3097
+ function hasErrorCode2(error, code) {
3098
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
3099
+ }
3100
+ function canonicalInputPath(path) {
3101
+ const resolved = resolve4(path);
3102
+ try {
3103
+ return realpathSync2.native(resolved);
3104
+ } catch {
3105
+ try {
3106
+ return resolve4(realpathSync2.native(dirname2(resolved)), resolved.split(/[\\/]/).at(-1));
3107
+ } catch {
3108
+ return resolved;
3109
+ }
3110
+ }
3111
+ }
3112
+
3113
+ export {
3114
+ tileflowLineBackgroundGlyphRoute,
3115
+ usesTileflowLineBackgroundGlyphs,
3116
+ parseTileflowLineBackgroundGlyphPath,
3117
+ createTileflowLineBackgroundGlyphResponder,
3118
+ prepareTileflowLocalTilesets,
3119
+ tileflowArtifactSessionSchemaVersion,
3120
+ createTileflowArtifactDiagnostics,
3121
+ prepareTileflowHostedNativeDeployment,
3122
+ tileflowArtifactPlanSchemaVersion,
3123
+ tileflowArtifactInventoryFileName,
3124
+ TileflowHostedManifestOverwriteError,
3125
+ TileflowLocalTilesetProductionError,
3126
+ createTileflowManifest,
3127
+ createTileflowStyle,
3128
+ createTileflowStyles,
3129
+ createTileflowArtifactPlan,
3130
+ createTileflowBuildArtifacts,
3131
+ getTileflowArtifactFiles,
3132
+ createTileflowBuildProvenance,
3133
+ createTileflowArtifactSession,
3134
+ getTileflowWatchPaths,
3135
+ isTileflowArtifactInputPath,
3136
+ refreshTileflowArtifactSession,
3137
+ writeTileflowBuildArtifacts,
3138
+ disposeTileflowBuildArtifacts,
3139
+ writeTileflowArtifactPlan,
3140
+ assertNoLocalTilesetsForProduction,
3141
+ assertTileflowSelfHostedManifestTarget,
3142
+ readTileflowLandmarkTileFootprints,
3143
+ tileflowLandmarkRegionOf,
3144
+ tileflowLandmarkRegionZoom,
3145
+ tileflowLandmarkTileOf,
3146
+ tileflowLandmarkTileZoom
3147
+ };