@tileflow/dev 0.1.0-alpha.3 → 0.1.0-alpha.30

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 (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +201 -5
  3. package/THIRD_PARTY_NOTICES.md +87 -0
  4. package/dist/artifacts.d.ts +265 -0
  5. package/dist/artifacts.js +81 -0
  6. package/dist/chunk-6MGFMKG3.js +284 -0
  7. package/dist/chunk-7C4F6GAX.js +170 -0
  8. package/dist/chunk-A2VTI43Y.js +1824 -0
  9. package/dist/chunk-D4OE4ITL.js +13 -0
  10. package/dist/chunk-KDFTE4Q5.js +156 -0
  11. package/dist/chunk-L56FLV4R.js +3061 -0
  12. package/dist/chunk-ONYQNLNA.js +1108 -0
  13. package/dist/chunk-UTRO4IBQ.js +671 -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.d.ts +337 -0
  20. package/dist/icons.js +39 -0
  21. package/dist/inspect.d.ts +149 -0
  22. package/dist/inspect.js +805 -0
  23. package/dist/preview-assets-MVUJJ5XR.js +148 -0
  24. package/dist/preview.d.ts +71 -0
  25. package/dist/preview.js +4286 -0
  26. package/dist/server.d.ts +82 -0
  27. package/dist/server.js +2094 -0
  28. package/dist/tilesets.d.ts +89 -0
  29. package/dist/tilesets.js +6 -0
  30. package/dist/validation.d.ts +65 -0
  31. package/dist/validation.js +24 -0
  32. package/docs/artifact-lifecycle.md +152 -0
  33. package/docs/bounded-pmtiles-resource-inspection.md +21 -0
  34. package/docs/bounded-vector-feature-inspection.md +100 -0
  35. package/docs/local-comparison-workbench-and-compiler-inspection.md +249 -0
  36. package/docs/native-artifacts.md +285 -0
  37. package/docs/native-icons.md +129 -0
  38. package/docs/structured-config-inspection-and-diagnostics.md +39 -0
  39. package/package.json +77 -13
  40. package/dist/index.d.ts +0 -54
  41. package/dist/index.js +0 -314
@@ -0,0 +1,1824 @@
1
+ import {
2
+ TileflowAssetDirectoryError,
3
+ resolveTileflowAssetDirectories
4
+ } from "./chunk-7C4F6GAX.js";
5
+
6
+ // src/icons.ts
7
+ import { readdir as readdir2, readFile, realpath as realpath3, stat } from "fs/promises";
8
+ import { extname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolvePath, sep as sep2 } from "path";
9
+ import { SaxesParser } from "saxes";
10
+ import {
11
+ assertTileflowIconLayoutBounds as assertTileflowIconLayoutBounds2,
12
+ collectTileflowIconSetReferences as collectTileflowIconSetReferences2,
13
+ tileflowIconAuthorMetadataSchema,
14
+ tileflowIconMetadataFileName,
15
+ TileflowIconSetError as TileflowIconSetError5,
16
+ tileflowIconsLockfileName as tileflowIconsLockfileName3,
17
+ tileflowIconSpriteIndexSchema
18
+ } from "@tileflow/core";
19
+ import {
20
+ compareCodeUnits as compareCodeUnits4,
21
+ hashTileflowRenderedIconPixels as hashTileflowRenderedIconPixels3,
22
+ parseResolvedTileflowMap,
23
+ parseTileflowIconJson as parseTileflowIconJson2,
24
+ serializeCanonicalJson as serializeCanonicalJson3,
25
+ sha256Hex as sha256Hex3,
26
+ tileflowIconIdPattern,
27
+ tileflowIconPackageLimits as tileflowIconPackageLimits4
28
+ } from "@tileflow/core";
29
+
30
+ // src/icon-composition.ts
31
+ import { realpath as realpath2 } from "fs/promises";
32
+ import { isAbsolute, relative, resolve as resolve3, sep } from "path";
33
+ import { tileflowIconsLockfileName as tileflowIconsLockfileName2 } from "@tileflow/core";
34
+ import {
35
+ collectTileflowIconSetReferences,
36
+ compareCodeUnits as compareCodeUnits3,
37
+ parseTileflowIconComposition,
38
+ parseTileflowIconsLockfile as parseTileflowIconsLockfile2,
39
+ tileflowIconPackageLimits as tileflowIconPackageLimits3,
40
+ TileflowIconSetError as TileflowIconSetError4
41
+ } from "@tileflow/core";
42
+
43
+ // src/icon-cache.ts
44
+ import { hashTileflowIconPackageManifest as hashTileflowIconPackageManifest3 } from "@tileflow/core";
45
+ import { constants } from "fs";
46
+ import { lstat, mkdir, mkdtemp, open, readdir, realpath, rename, rm } from "fs/promises";
47
+ import { homedir, platform } from "os";
48
+ import { join, resolve } from "path";
49
+ import {
50
+ TileflowIconSetError as TileflowIconSetError2,
51
+ tileflowIconSetPinSchema
52
+ } from "@tileflow/core";
53
+
54
+ // src/icon-artifact.ts
55
+ import {
56
+ tileflowIconSpriteIndexSchema as indexSchema
57
+ } from "@tileflow/core";
58
+ import {
59
+ assertTileflowSdfPixels as assertTileflowSdfPixels2,
60
+ compareCodeUnits as compareCodeUnits2,
61
+ hashTileflowIconPackageManifest as hashTileflowIconPackageManifest2,
62
+ hashTileflowRenderedIconPixels as hashTileflowRenderedIconPixels2,
63
+ parseTileflowIconJson,
64
+ readTileflowIconLayout,
65
+ scaleTileflowIconLayout as scaleTileflowIconLayout2,
66
+ serializeCanonicalJson as serializeCanonicalJson2,
67
+ sha256Hex as sha256Hex2,
68
+ tileflowIconPackageLimits as tileflowIconPackageLimits2,
69
+ tileflowIconPackageManifestSchema as tileflowIconPackageManifestSchema2,
70
+ TileflowIconSetError
71
+ } from "@tileflow/core";
72
+
73
+ // src/icon-sprite.ts
74
+ import {
75
+ assertTileflowIconLayoutBounds,
76
+ assertTileflowSdfPixels,
77
+ compareCodeUnits,
78
+ hashTileflowIconPackageManifest,
79
+ hashTileflowRenderedIconPixels,
80
+ scaleTileflowIconLayout,
81
+ serializeCanonicalJson,
82
+ sha256Hex,
83
+ tileflowIconIdSchema,
84
+ tileflowIconPackageLimits,
85
+ tileflowIconPackageManifestSchema
86
+ } from "@tileflow/core";
87
+ async function packTileflowRenderedIcons(input) {
88
+ const icons = [...input].sort((left, right) => compareCodeUnits(left.id, right.id));
89
+ if (icons.length < 1 || icons.length > tileflowIconPackageLimits.maxIconCount || new Set(icons.map((icon) => icon.id)).size !== icons.length)
90
+ throw new Error(
91
+ `Expected 1 through ${tileflowIconPackageLimits.maxIconCount} unique rendered icons`
92
+ );
93
+ const renderedIcons = [];
94
+ for (const icon of icons) {
95
+ tileflowIconIdSchema.parse(icon.id);
96
+ if (icon.layout) assertTileflowIconLayoutBounds(icon.layout, icon.oneX.width, icon.oneX.height);
97
+ if (icon.appearance) {
98
+ assertTileflowSdfPixels({ ...icon.oneX, pixelRatio: 1 });
99
+ assertTileflowSdfPixels({ ...icon.twoX, pixelRatio: 2 });
100
+ }
101
+ if (icon.twoX.width !== icon.oneX.width * 2 || icon.twoX.height !== icon.oneX.height * 2)
102
+ throw new Error("Rendered density geometry must double exactly");
103
+ renderedIcons.push({
104
+ name: icon.id,
105
+ ...icon.appearance ? { appearance: icon.appearance } : {},
106
+ ...icon.layout ? { layout: icon.layout } : {},
107
+ pixelSha256: {
108
+ oneX: await hashTileflowRenderedIconPixels({ ...icon.oneX, pixelRatio: 1 }),
109
+ twoX: await hashTileflowRenderedIconPixels({ ...icon.twoX, pixelRatio: 2 })
110
+ }
111
+ });
112
+ }
113
+ const oneX = icons.map((icon) => ({
114
+ ...icon.oneX,
115
+ name: icon.id,
116
+ appearance: icon.appearance,
117
+ layout: icon.layout
118
+ }));
119
+ const twoX = icons.map((icon) => ({
120
+ ...icon.twoX,
121
+ name: icon.id,
122
+ appearance: icon.appearance,
123
+ layout: icon.layout
124
+ }));
125
+ const layoutOneX = createSpriteLayout(oneX, 1);
126
+ const layoutTwoX = createSpriteLayout(twoX, 2);
127
+ if (layoutTwoX.width !== layoutOneX.width * 2 || layoutTwoX.height !== layoutOneX.height * 2)
128
+ throw new Error("Composed atlas exceeds the paired density geometry limit");
129
+ const files = [
130
+ {
131
+ contentType: "application/json",
132
+ fileName: "sprite.json",
133
+ source: new TextEncoder().encode(`${serializeCanonicalJson(layoutOneX.index)}
134
+ `)
135
+ },
136
+ {
137
+ contentType: "image/png",
138
+ fileName: "sprite.png",
139
+ source: await createSpriteImage(oneX, layoutOneX)
140
+ },
141
+ {
142
+ contentType: "application/json",
143
+ fileName: "sprite@2x.json",
144
+ source: new TextEncoder().encode(`${serializeCanonicalJson(layoutTwoX.index)}
145
+ `)
146
+ },
147
+ {
148
+ contentType: "image/png",
149
+ fileName: "sprite@2x.png",
150
+ source: await createSpriteImage(twoX, layoutTwoX)
151
+ }
152
+ ];
153
+ assertGeneratedFileLimits(files);
154
+ const manifest = tileflowIconPackageManifestSchema.parse({
155
+ format: icons.some((icon) => icon.appearance || icon.layout) ? "tileflow-icon-package-v2" : "tileflow-icon-package-v1",
156
+ files: await Promise.all(
157
+ files.map(async (file) => ({
158
+ name: file.fileName,
159
+ byteLength: file.source.byteLength,
160
+ contentType: file.contentType,
161
+ sha256: await sha256Hex(file.source)
162
+ }))
163
+ ),
164
+ iconNames: icons.map((icon) => icon.id),
165
+ renderedIcons,
166
+ sprites: {
167
+ oneX: { width: layoutOneX.width, height: layoutOneX.height, pixelRatio: 1 },
168
+ twoX: { width: layoutTwoX.width, height: layoutTwoX.height, pixelRatio: 2 }
169
+ }
170
+ });
171
+ return { contentHash: await hashTileflowIconPackageManifest(manifest), manifest, files };
172
+ }
173
+ function createSpriteLayout(icons, pixelRatio) {
174
+ const columns = Math.ceil(Math.sqrt(icons.length));
175
+ const widest = Math.max(...icons.map((icon) => icon.width));
176
+ const maxDimension = tileflowIconPackageLimits.maxAtlasDimension / 2 * pixelRatio;
177
+ const targetWidth = Math.min(maxDimension, columns * widest);
178
+ const placements = [];
179
+ let left = 0;
180
+ let rowHeight = 0;
181
+ let top = 0;
182
+ for (const icon of icons) {
183
+ if (icon.width > maxDimension)
184
+ throw new Error(
185
+ `Generated sprite exceeds ${maxDimension} pixels per dimension at ${pixelRatio}x`
186
+ );
187
+ if (left > 0 && left + icon.width > targetWidth) {
188
+ top += rowHeight;
189
+ left = 0;
190
+ rowHeight = 0;
191
+ }
192
+ placements.push({ left, top });
193
+ left += icon.width;
194
+ rowHeight = Math.max(rowHeight, icon.height);
195
+ }
196
+ const width = targetWidth;
197
+ const height = top + rowHeight;
198
+ if (width > maxDimension || height > maxDimension)
199
+ throw new Error(
200
+ `Generated sprite exceeds ${maxDimension} pixels per dimension at ${pixelRatio}x`
201
+ );
202
+ const index = Object.fromEntries(
203
+ icons.map((icon, ordinal) => {
204
+ const placement = placements[ordinal];
205
+ return [
206
+ icon.name,
207
+ {
208
+ height: icon.height,
209
+ pixelRatio,
210
+ width: icon.width,
211
+ x: placement.left,
212
+ y: placement.top,
213
+ ...icon.appearance ? { sdf: true, tileflow: icon.appearance } : {},
214
+ ...icon.layout ? scaleTileflowIconLayout(icon.layout, pixelRatio) : {}
215
+ }
216
+ ];
217
+ })
218
+ );
219
+ return { height, index, width };
220
+ }
221
+ async function createSpriteImage(icons, layout) {
222
+ const sharp = await loadSharp();
223
+ const atlas = new Uint8Array(layout.width * layout.height * 4);
224
+ for (const icon of icons) {
225
+ const placement = layout.index[icon.name];
226
+ if (!placement || icon.width !== placement.width || icon.height !== placement.height || icon.rgba.byteLength !== icon.width * icon.height * 4)
227
+ throw new Error("Rendered icon dimensions do not match the sprite layout");
228
+ const rowBytes = icon.width * 4;
229
+ for (let row = 0; row < icon.height; row += 1) {
230
+ const sourceStart = row * rowBytes;
231
+ const targetStart = ((placement.y + row) * layout.width + placement.x) * 4;
232
+ atlas.set(icon.rgba.subarray(sourceStart, sourceStart + rowBytes), targetStart);
233
+ }
234
+ }
235
+ return sharp(atlas, { raw: { channels: 4, height: layout.height, width: layout.width } }).png({ adaptiveFiltering: false, compressionLevel: 9, palette: false }).toBuffer();
236
+ }
237
+ function assertGeneratedFileLimits(files) {
238
+ let totalBytes = 0;
239
+ for (const file of files) {
240
+ if (file.source.byteLength > tileflowIconPackageLimits.maxGeneratedFileBytes)
241
+ throw new Error(
242
+ `${file.fileName} exceeds ${tileflowIconPackageLimits.maxGeneratedFileBytes} generated bytes`
243
+ );
244
+ totalBytes += file.source.byteLength;
245
+ }
246
+ if (totalBytes > tileflowIconPackageLimits.maxGeneratedPackageBytes)
247
+ throw new Error(
248
+ `Generated package exceeds ${tileflowIconPackageLimits.maxGeneratedPackageBytes} bytes`
249
+ );
250
+ }
251
+ async function loadSharp() {
252
+ try {
253
+ return (await import("sharp")).default;
254
+ } catch (cause) {
255
+ throw new Error(
256
+ 'Local icon sprites require the optional "sharp" package. Install sharp or disable local icons.',
257
+ { cause }
258
+ );
259
+ }
260
+ }
261
+
262
+ // src/icon-artifact.ts
263
+ async function verifyTileflowIconArtifact(input) {
264
+ try {
265
+ const manifest = tileflowIconPackageManifestSchema2.parse(input.manifest);
266
+ const contentHash = input.contentHash;
267
+ if (input.files.length !== 4 || new Set(input.files.map((file) => file.fileName)).size !== 4)
268
+ throw new Error("An artifact must contain exactly the four generated files");
269
+ const files = manifest.files.map((expected) => {
270
+ const source = input.files.find((file) => file.fileName === expected.name);
271
+ if (!source || source.contentType !== expected.contentType || source.source.byteLength !== expected.byteLength)
272
+ throw new Error("Generated file identity does not match the manifest");
273
+ return {
274
+ contentType: source.contentType,
275
+ fileName: source.fileName,
276
+ source: new Uint8Array(source.source)
277
+ };
278
+ });
279
+ if (await hashTileflowIconPackageManifest2(manifest) !== contentHash)
280
+ throw new Error("Artifact manifest hash mismatch");
281
+ for (const [index, file] of files.entries()) {
282
+ if (await sha256Hex2(file.source) !== manifest.files[index].sha256)
283
+ throw new Error(`Generated ${file.fileName} checksum mismatch`);
284
+ }
285
+ const decoder = new TextDecoder("utf-8", { fatal: true });
286
+ const oneIndex = indexSchema.parse(
287
+ parseTileflowIconJson(
288
+ decoder.decode(files[0].source),
289
+ tileflowIconPackageLimits2.maxGeneratedFileBytes
290
+ )
291
+ );
292
+ const twoIndex = indexSchema.parse(
293
+ parseTileflowIconJson(
294
+ decoder.decode(files[2].source),
295
+ tileflowIconPackageLimits2.maxGeneratedFileBytes
296
+ )
297
+ );
298
+ for (const index of [oneIndex, twoIndex]) {
299
+ const names = Object.keys(index).sort(compareCodeUnits2);
300
+ if (names.length !== manifest.iconNames.length || names.some((name, ordinal) => name !== manifest.iconNames[ordinal]))
301
+ throw new Error("Sprite index does not exactly match the manifest icon names");
302
+ }
303
+ for (const [index, dimensions, ratio] of [
304
+ [oneIndex, manifest.sprites.oneX, 1],
305
+ [twoIndex, manifest.sprites.twoX, 2]
306
+ ]) {
307
+ const rectangles = Object.values(index);
308
+ for (const [ordinal, rect] of rectangles.entries()) {
309
+ if (rect.pixelRatio !== ratio || rect.x + rect.width > dimensions.width || rect.y + rect.height > dimensions.height)
310
+ throw new Error("Sprite rectangle is outside the atlas or has the wrong density");
311
+ for (const other of rectangles.slice(0, ordinal)) {
312
+ if (rect.x < other.x + other.width && other.x < rect.x + rect.width && rect.y < other.y + other.height && other.y < rect.y + rect.height)
313
+ throw new Error("Sprite rectangles must not overlap");
314
+ }
315
+ }
316
+ }
317
+ for (const name of manifest.iconNames) {
318
+ const one = oneIndex[name];
319
+ const two = twoIndex[name];
320
+ const declared = manifest.renderedIcons.find((icon) => icon.name === name).appearance;
321
+ const declaredLayout = manifest.renderedIcons.find((icon) => icon.name === name).layout;
322
+ const oneLayout = readTileflowIconLayout(one);
323
+ const twoLayout = readTileflowIconLayout(two);
324
+ if (serializeCanonicalJson2(oneLayout ?? null) !== serializeCanonicalJson2(declaredLayout ?? null) || serializeCanonicalJson2(twoLayout ?? null) !== serializeCanonicalJson2(oneLayout ? scaleTileflowIconLayout2(oneLayout, 2) : null))
325
+ throw new Error(
326
+ "Sprite layout must match the manifest and scale exactly between densities"
327
+ );
328
+ if (serializeCanonicalJson2(one.tileflow ?? null) !== serializeCanonicalJson2(two.tileflow ?? null) || serializeCanonicalJson2(one.tileflow ?? null) !== serializeCanonicalJson2(declared ?? null))
329
+ throw new Error("Sprite appearance must match at both densities and in the manifest");
330
+ if (two.x !== one.x * 2 || two.y !== one.y * 2 || two.width !== one.width * 2 || two.height !== one.height * 2)
331
+ throw new Error("Sprite densities must have exactly doubled geometry");
332
+ }
333
+ const oneAtlas = await decodeAtlas(files[1].source, manifest.sprites.oneX);
334
+ const twoAtlas = await decodeAtlas(files[3].source, manifest.sprites.twoX);
335
+ const icons = [];
336
+ for (const [ordinal, id] of manifest.iconNames.entries()) {
337
+ const oneX = extractCell(oneAtlas, oneIndex[id]);
338
+ const twoX = extractCell(twoAtlas, twoIndex[id]);
339
+ const expected = manifest.renderedIcons[ordinal].pixelSha256;
340
+ if (await hashTileflowRenderedIconPixels2({ ...oneX, pixelRatio: 1 }) !== expected.oneX || await hashTileflowRenderedIconPixels2({ ...twoX, pixelRatio: 2 }) !== expected.twoX)
341
+ throw new Error(`Rendered pixels do not match the manifest for ${id}`);
342
+ const appearance = oneIndex[id].tileflow;
343
+ if (appearance) {
344
+ assertTileflowSdfPixels2({ ...oneX, pixelRatio: 1 });
345
+ assertTileflowSdfPixels2({ ...twoX, pixelRatio: 2 });
346
+ }
347
+ const layout = readTileflowIconLayout(oneIndex[id]);
348
+ icons.push({
349
+ id,
350
+ oneX,
351
+ twoX,
352
+ ...appearance ? { appearance } : {},
353
+ ...layout ? { layout } : {}
354
+ });
355
+ }
356
+ return { package: { contentHash, manifest, files }, icons };
357
+ } catch (cause) {
358
+ throw new TileflowIconSetError(
359
+ "ICON_PACKAGE_INTEGRITY",
360
+ "Generated icon artifact verification failed",
361
+ { cause }
362
+ );
363
+ }
364
+ }
365
+ async function decodeAtlas(bytes, dimensions) {
366
+ if ([137, 80, 78, 71, 13, 10, 26, 10].some((byte, index) => bytes[index] !== byte))
367
+ throw new Error("Expected PNG bytes");
368
+ const sharp = await loadSharp();
369
+ const image = sharp(bytes, {
370
+ failOn: "error",
371
+ limitInputPixels: tileflowIconPackageLimits2.maxAtlasDimension ** 2
372
+ });
373
+ const metadata = await image.metadata();
374
+ if (metadata.format !== "png" || (metadata.pages ?? 1) !== 1 || metadata.width !== dimensions.width || metadata.height !== dimensions.height)
375
+ throw new Error("PNG geometry does not match the manifest");
376
+ const { data, info } = await image.ensureAlpha().raw().toBuffer({ resolveWithObject: true });
377
+ if (info.channels !== 4 || info.width !== dimensions.width || info.height !== dimensions.height || data.length !== dimensions.width * dimensions.height * 4)
378
+ throw new Error("Expected bounded RGBA atlas pixels");
379
+ return { ...dimensions, rgba: new Uint8Array(data) };
380
+ }
381
+ function extractCell(atlas, rect) {
382
+ const rgba = new Uint8Array(rect.width * rect.height * 4);
383
+ const rowBytes = rect.width * 4;
384
+ for (let row = 0; row < rect.height; row += 1) {
385
+ const start = ((rect.y + row) * atlas.width + rect.x) * 4;
386
+ rgba.set(atlas.rgba.subarray(start, start + rowBytes), row * rowBytes);
387
+ }
388
+ return { height: rect.height, width: rect.width, rgba };
389
+ }
390
+
391
+ // src/icon-cache.ts
392
+ function getTileflowIconCacheDirectory(options = {}) {
393
+ const defaultRoot = platform() === "darwin" ? join(homedir(), "Library", "Caches") : platform() === "win32" ? process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
394
+ return join(
395
+ resolve(options.cacheRoot ?? process.env.TILEFLOW_ICON_CACHE_DIR ?? defaultRoot),
396
+ "tileflow",
397
+ "icons",
398
+ "v1"
399
+ );
400
+ }
401
+ async function loadTileflowIconSetArtifact(input, options = {}) {
402
+ const pin = tileflowIconSetPinSchema.parse(input);
403
+ if (await hashTileflowIconPackageManifest3(pin.manifest) !== pin.contentHash)
404
+ throw new TileflowIconSetError2("ICON_LOCK_INVALID", "Locked manifest hash mismatch");
405
+ let cacheFailure;
406
+ try {
407
+ return await readCached(pin, options);
408
+ } catch (cause) {
409
+ if (cause instanceof TileflowIconSetError2 && cause.code === "ICON_CACHE_UNSAFE") throw cause;
410
+ cacheFailure = cause;
411
+ }
412
+ if (options.offline) {
413
+ throw new TileflowIconSetError2(
414
+ isMissing(cacheFailure) ? "ICON_CACHE_MISS" : "ICON_PACKAGE_INTEGRITY",
415
+ "The exact verified icon artifact is unavailable offline; hydrate this pin explicitly",
416
+ { cause: cacheFailure }
417
+ );
418
+ }
419
+ const url = new URL(pin.spriteUrl);
420
+ const allowed = options.deliveryOrigins ?? ["https://api.tileflow.dev"];
421
+ if (!allowed.some((origin) => origin === url.origin && new URL(origin).href === `${origin}/`)) {
422
+ throw new TileflowIconSetError2(
423
+ "ICON_DOWNLOAD_FAILED",
424
+ "Icon artifact delivery origin is not trusted"
425
+ );
426
+ }
427
+ const files = [];
428
+ const signal = options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4);
429
+ try {
430
+ for (const expected of pin.manifest.files) {
431
+ signal.throwIfAborted();
432
+ const fileUrl = `${pin.spriteUrl}${expected.name.slice("sprite".length)}`;
433
+ const response = await (options.fetch ?? globalThis.fetch)(fileUrl, {
434
+ method: "GET",
435
+ credentials: "omit",
436
+ redirect: "error",
437
+ headers: { Accept: expected.contentType, "Accept-Encoding": "identity" },
438
+ signal
439
+ });
440
+ if (response.status !== 200 || response.redirected || response.url && response.url !== fileUrl || !response.body)
441
+ throw new Error("Unexpected artifact response");
442
+ const type = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
443
+ const length = response.headers.get("content-length");
444
+ const encoding = response.headers.get("content-encoding")?.trim().toLowerCase();
445
+ const isIdentityEncoding = encoding === void 0 || encoding === "" || encoding === "identity";
446
+ if (type !== expected.contentType || length !== null && (!/^\d+$/u.test(length) || isIdentityEncoding && Number(length) !== expected.byteLength)) {
447
+ await response.body.cancel();
448
+ throw new Error("Artifact response headers do not match the manifest");
449
+ }
450
+ const reader = response.body.getReader();
451
+ const source = new Uint8Array(expected.byteLength);
452
+ let offset = 0;
453
+ try {
454
+ while (true) {
455
+ const chunk = await reader.read();
456
+ if (chunk.done) break;
457
+ if (offset + chunk.value.byteLength > source.byteLength)
458
+ throw new Error("Artifact response exceeds its exact byte length");
459
+ source.set(chunk.value, offset);
460
+ offset += chunk.value.byteLength;
461
+ }
462
+ if (offset !== source.byteLength) throw new Error("Artifact response was truncated");
463
+ } catch (cause) {
464
+ await reader.cancel().catch(() => void 0);
465
+ throw cause;
466
+ } finally {
467
+ reader.releaseLock();
468
+ }
469
+ files.push({ fileName: expected.name, contentType: expected.contentType, source });
470
+ }
471
+ } catch (cause) {
472
+ throw new TileflowIconSetError2("ICON_DOWNLOAD_FAILED", "Exact icon artifact download failed", {
473
+ cause
474
+ });
475
+ }
476
+ return storeTileflowIconSetArtifact(
477
+ pin,
478
+ { contentHash: pin.contentHash, manifest: pin.manifest, files },
479
+ options
480
+ );
481
+ }
482
+ async function storeTileflowIconSetArtifact(input, artifact, options = {}) {
483
+ const pin = tileflowIconSetPinSchema.parse(input);
484
+ if (await hashTileflowIconPackageManifest3(pin.manifest) !== pin.contentHash)
485
+ throw new TileflowIconSetError2("ICON_LOCK_INVALID", "Locked manifest hash mismatch");
486
+ if (artifact.contentHash !== pin.contentHash)
487
+ throw new TileflowIconSetError2(
488
+ "ICON_PACKAGE_INTEGRITY",
489
+ "Artifact does not match the selected pin"
490
+ );
491
+ const verified = await verifyTileflowIconArtifact({
492
+ contentHash: pin.contentHash,
493
+ manifest: pin.manifest,
494
+ files: artifact.files
495
+ });
496
+ const root = await safeCacheRoot(options, true);
497
+ const target = join(root, pin.contentHash);
498
+ const temporary = await mkdtemp(join(root, `.incoming-${pin.contentHash}-`));
499
+ try {
500
+ for (const file of verified.package.files) {
501
+ const handle = await open(join(temporary, file.fileName), "wx", 384);
502
+ try {
503
+ await handle.writeFile(file.source);
504
+ await handle.sync();
505
+ } finally {
506
+ await handle.close();
507
+ }
508
+ }
509
+ for (let attempt = 0; attempt < 8; attempt += 1) {
510
+ try {
511
+ await rename(temporary, target);
512
+ return verified;
513
+ } catch (cause) {
514
+ if (!hasCode(cause, "EEXIST") && !hasCode(cause, "ENOTEMPTY") && !hasCode(cause, "EPERM"))
515
+ throw cause;
516
+ }
517
+ try {
518
+ return await readCached(pin, options);
519
+ } catch (cause) {
520
+ if (cause instanceof TileflowIconSetError2 && cause.code === "ICON_CACHE_UNSAFE")
521
+ throw cause;
522
+ }
523
+ try {
524
+ await assertDirectory(target);
525
+ const retired = await mkdtemp(join(root, `.retired-${pin.contentHash}-`));
526
+ try {
527
+ await rename(target, join(retired, "entry"));
528
+ } catch (cause) {
529
+ if (!isMissing(cause)) throw cause;
530
+ } finally {
531
+ await rm(retired, { recursive: true, force: true });
532
+ }
533
+ } catch (cause) {
534
+ if (!isMissing(cause)) throw cause;
535
+ }
536
+ }
537
+ throw new TileflowIconSetError2(
538
+ "ICON_CACHE_UNSAFE",
539
+ "Concurrent cache repairs did not converge; retry this exact pin"
540
+ );
541
+ } finally {
542
+ await rm(temporary, { recursive: true, force: true });
543
+ }
544
+ }
545
+ async function readCached(pin, options) {
546
+ const root = await safeCacheRoot(options, false);
547
+ const directory = join(root, pin.contentHash);
548
+ await assertDirectory(directory);
549
+ const entries = (await readdir(directory)).sort();
550
+ const expectedNames = pin.manifest.files.map((file) => file.name).sort();
551
+ if (entries.length !== 4 || entries.some((name, index) => name !== expectedNames[index]))
552
+ throw new TileflowIconSetError2(
553
+ "ICON_PACKAGE_INTEGRITY",
554
+ "Cache entry does not contain exactly four generated files"
555
+ );
556
+ const files = [];
557
+ for (const expected of pin.manifest.files) {
558
+ files.push({
559
+ fileName: expected.name,
560
+ contentType: expected.contentType,
561
+ source: await readSafeFile(join(directory, expected.name), expected.byteLength)
562
+ });
563
+ }
564
+ return verifyTileflowIconArtifact({ contentHash: pin.contentHash, manifest: pin.manifest, files });
565
+ }
566
+ async function safeCacheRoot(options, create) {
567
+ const root = getTileflowIconCacheDirectory(options);
568
+ const base = resolve(root, "..", "..", "..");
569
+ if (create) await mkdir(base, { recursive: true, mode: 448 });
570
+ await assertDirectory(base);
571
+ let current = await realpath(base);
572
+ for (const segment of ["tileflow", "icons", "v1"]) {
573
+ current = join(current, segment);
574
+ if (create)
575
+ await mkdir(current, { mode: 448 }).catch((cause) => {
576
+ if (!hasCode(cause, "EEXIST")) throw cause;
577
+ });
578
+ await assertDirectory(current);
579
+ }
580
+ return current;
581
+ }
582
+ async function assertDirectory(path) {
583
+ const entry = await lstat(path);
584
+ if (!entry.isDirectory() || entry.isSymbolicLink())
585
+ throw new TileflowIconSetError2(
586
+ "ICON_CACHE_UNSAFE",
587
+ "Icon cache paths must be directories, not symlinks"
588
+ );
589
+ }
590
+ async function readSafeFile(path, maximumBytes, exact = true) {
591
+ const entry = await lstat(path);
592
+ if (!entry.isFile() || entry.isSymbolicLink())
593
+ throw new TileflowIconSetError2(
594
+ "ICON_CACHE_UNSAFE",
595
+ "Icon files must be regular files, not symlinks"
596
+ );
597
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
598
+ try {
599
+ const opened = await handle.stat();
600
+ if (!opened.isFile() || opened.ino !== entry.ino || opened.dev !== entry.dev || opened.size > maximumBytes || exact && opened.size !== maximumBytes)
601
+ throw new TileflowIconSetError2(
602
+ "ICON_PACKAGE_INTEGRITY",
603
+ "Icon file size or identity changed"
604
+ );
605
+ const buffer = new Uint8Array(maximumBytes + 1);
606
+ let offset = 0;
607
+ while (offset < buffer.length) {
608
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, null);
609
+ if (bytesRead === 0) break;
610
+ offset += bytesRead;
611
+ }
612
+ if (offset > maximumBytes || exact && offset !== maximumBytes)
613
+ throw new TileflowIconSetError2("ICON_PACKAGE_INTEGRITY", "Icon file byte length changed");
614
+ return buffer.slice(0, offset);
615
+ } finally {
616
+ await handle.close();
617
+ }
618
+ }
619
+ function hasCode(error, code) {
620
+ return error !== null && typeof error === "object" && "code" in error && error.code === code;
621
+ }
622
+ function isMissing(error) {
623
+ return hasCode(error, "ENOENT");
624
+ }
625
+
626
+ // src/icon-lockfile.ts
627
+ import { lstat as lstat2, mkdir as mkdir2, mkdtemp as mkdtemp2, open as open2, rename as rename2, rm as rm2, rmdir } from "fs/promises";
628
+ import { join as join2, resolve as resolve2 } from "path";
629
+ import {
630
+ parseTileflowIconsLockfile,
631
+ serializeTileflowIconsLockfile,
632
+ TileflowIconSetError as TileflowIconSetError3,
633
+ tileflowIconsLockfileMaximumBytes,
634
+ tileflowIconsLockfileName
635
+ } from "@tileflow/core";
636
+ async function readTileflowIconsLockfileText(baseDirectory) {
637
+ return readLockText(join2(resolve2(baseDirectory), tileflowIconsLockfileName));
638
+ }
639
+ async function readTileflowIconsLockfile(baseDirectory, sources) {
640
+ const text = await readLockText(join2(resolve2(baseDirectory), tileflowIconsLockfileName));
641
+ if (text === null)
642
+ throw new TileflowIconSetError3(
643
+ "ICON_LOCK_MISSING",
644
+ "Missing tileflow.icons.lock.json; install exact icon set revisions explicitly"
645
+ );
646
+ return parseTileflowIconsLockfile(text, sources);
647
+ }
648
+ async function writeTileflowIconsLockfile(baseDirectory, input, expectedContents) {
649
+ const content = await serializeTileflowIconsLockfile(input);
650
+ const directory = resolve2(baseDirectory);
651
+ const destination = join2(directory, tileflowIconsLockfileName);
652
+ const guard = join2(directory, `.${tileflowIconsLockfileName}.writing`);
653
+ try {
654
+ await mkdir2(guard, { mode: 448 });
655
+ } catch (cause) {
656
+ throw new TileflowIconSetError3(
657
+ "ICON_LOCK_CONFLICT",
658
+ "Another icon lock writer is active; a stale .writing guard may be removed only after confirming its writer has exited",
659
+ { cause }
660
+ );
661
+ }
662
+ let temporary;
663
+ try {
664
+ const actual = await readLockText(destination);
665
+ if (actual !== expectedContents)
666
+ throw new TileflowIconSetError3(
667
+ "ICON_LOCK_CONFLICT",
668
+ "Icon lock changed since it was read; reload and retry without discarding other pins"
669
+ );
670
+ if (actual === content) return;
671
+ temporary = await mkdtemp2(join2(directory, ".tileflow-icons-lock-"));
672
+ const file = join2(temporary, "lock");
673
+ const handle = await open2(file, "wx", 384);
674
+ try {
675
+ await handle.writeFile(content, "utf8");
676
+ await handle.sync();
677
+ } finally {
678
+ await handle.close();
679
+ }
680
+ if (await readLockText(destination) !== actual)
681
+ throw new TileflowIconSetError3(
682
+ "ICON_LOCK_CONFLICT",
683
+ "Icon lock changed while preparing an update"
684
+ );
685
+ await rename2(file, destination);
686
+ } finally {
687
+ if (temporary) await rm2(temporary, { recursive: true, force: true });
688
+ await rmdir(guard);
689
+ }
690
+ }
691
+ async function readLockText(path) {
692
+ try {
693
+ const entry = await lstat2(path);
694
+ if (!entry.isFile() || entry.isSymbolicLink())
695
+ throw new TileflowIconSetError3(
696
+ "ICON_LOCK_INVALID",
697
+ "The icon lock must be a regular file, not a symlink"
698
+ );
699
+ return new TextDecoder("utf-8", { fatal: true }).decode(
700
+ await readSafeFile(path, tileflowIconsLockfileMaximumBytes, false)
701
+ );
702
+ } catch (cause) {
703
+ if (cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT")
704
+ return null;
705
+ throw cause;
706
+ }
707
+ }
708
+
709
+ // src/icon-composition.ts
710
+ async function composeTileflowIconSources(input, options) {
711
+ const sources = structuredClone(input);
712
+ const references = collectTileflowIconSetReferences(sources);
713
+ let realCwd;
714
+ const lock = references.length > 0 ? options.lock === void 0 ? await readTileflowIconsLockfile(options.baseDirectory ?? options.cwd, sources) : await parseTileflowIconsLockfile2(options.lock, sources) : await parseTileflowIconsLockfile2(options.lock ?? { lockfileVersion: 1, sets: {} }, sources);
715
+ const contributors = new Array(sources.length);
716
+ const details = new Array(sources.length);
717
+ const winners = /* @__PURE__ */ new Map();
718
+ const watchPaths = /* @__PURE__ */ new Set();
719
+ const watchFiles = /* @__PURE__ */ new Set();
720
+ if (references.length > 0 && options.lock === void 0)
721
+ watchFiles.add(resolve3(options.baseDirectory ?? options.cwd, tileflowIconsLockfileName2));
722
+ let reused;
723
+ let pixelBytes = 0;
724
+ let localSourceBytes = 0;
725
+ for (let ordinal = sources.length - 1; ordinal >= 0; ordinal -= 1) {
726
+ const source = sources[ordinal];
727
+ if (typeof source === "object" && source.kind === "icon-set") {
728
+ const pin = lock.sets[source.reference];
729
+ const loaded = await loadTileflowIconSetArtifact(pin, options);
730
+ contributors[ordinal] = {
731
+ kind: "icon-set",
732
+ reference: source.reference,
733
+ teamId: pin.teamId,
734
+ setId: pin.setId,
735
+ versionId: pin.versionId,
736
+ version: pin.version,
737
+ packageId: pin.packageId,
738
+ contentHash: pin.contentHash,
739
+ iconIds: [...pin.manifest.iconNames]
740
+ };
741
+ details[ordinal] = {
742
+ iconIds: [...pin.manifest.iconNames],
743
+ kind: "icon-set",
744
+ label: source.reference,
745
+ packageId: pin.packageId,
746
+ reference: source.reference,
747
+ setId: pin.setId,
748
+ version: pin.version,
749
+ versionId: pin.versionId
750
+ };
751
+ if (sources.length === 1) reused = loaded.package;
752
+ for (const icon of loaded.icons) {
753
+ if (winners.has(icon.id)) continue;
754
+ const pixels = pin.manifest.renderedIcons.find(
755
+ (entry) => entry.name === icon.id
756
+ ).pixelSha256;
757
+ addWinner(
758
+ ordinal,
759
+ icon,
760
+ {
761
+ kind: "rendered-icon",
762
+ id: icon.id,
763
+ width: icon.oneX.width,
764
+ height: icon.oneX.height,
765
+ pixelSha256: { ...pixels },
766
+ ...icon.appearance ? { appearance: icon.appearance } : {},
767
+ ...icon.layout ? { layout: icon.layout } : {}
768
+ },
769
+ null
770
+ );
771
+ }
772
+ } else {
773
+ const directory = await readTileflowIconDirectory(source, {
774
+ cwd: options.cwd,
775
+ baseDirectory: options.baseDirectory ?? options.cwd,
776
+ ...options.configPath === void 0 ? {} : { configPath: options.configPath },
777
+ ordinal,
778
+ target: options.target ?? "local"
779
+ });
780
+ if (directory.watchPath) watchPaths.add(directory.watchPath);
781
+ contributors[ordinal] = typeof source === "string" ? { kind: "local", iconIds: directory.iconIds } : { kind: "package", package: source.package, iconIds: directory.iconIds };
782
+ details[ordinal] = {
783
+ entries: directory.entries,
784
+ iconIds: directory.iconIds,
785
+ insideWorkingTree: isPathInside(
786
+ realCwd ??= await realpath2(options.cwd),
787
+ directory.realPath
788
+ ),
789
+ kind: typeof source === "string" ? "local" : "package",
790
+ label: directory.label
791
+ };
792
+ const selected = directory.iconIds.filter((id) => !winners.has(id));
793
+ if (winners.size + selected.length > tileflowIconPackageLimits3.maxIconCount)
794
+ throw new TileflowIconSetError4(
795
+ "ICON_COMPOSITION_INVALID",
796
+ `Composed icon set exceeds ${tileflowIconPackageLimits3.maxIconCount} effective icons`
797
+ );
798
+ for (const item of await directory.render(selected)) {
799
+ localSourceBytes += item.sourceBytes;
800
+ if (localSourceBytes > tileflowIconPackageLimits3.maxSourceBytes)
801
+ throw new TileflowIconSetError4(
802
+ "ICON_COMPOSITION_INVALID",
803
+ "Effective local icon originals exceed the source-byte limit"
804
+ );
805
+ addWinner(ordinal, item.icon, item.identity, item.source);
806
+ }
807
+ }
808
+ }
809
+ const sorted = [...winners.values()].sort(
810
+ (left, right) => compareCodeUnits3(left.icon.id, right.icon.id)
811
+ );
812
+ const iconPackage = sorted.length ? reused ?? await packTileflowRenderedIcons(sorted.map((winner) => winner.icon)) : null;
813
+ const composition = references.length && iconPackage ? parseTileflowIconComposition({
814
+ format: sorted.some((winner) => winner.icon.appearance || winner.icon.layout) ? "tileflow-icon-composition-v2" : "tileflow-icon-composition-v1",
815
+ compositionVersion: sorted.some((winner) => winner.icon.appearance || winner.icon.layout) ? 2 : 1,
816
+ packageHash: iconPackage.contentHash,
817
+ contributors,
818
+ winners: sorted.map(({ ordinal, icon }, index) => ({
819
+ id: icon.id,
820
+ contributor: ordinal,
821
+ width: icon.oneX.width,
822
+ height: icon.oneX.height,
823
+ pixelSha256: iconPackage.manifest.renderedIcons[index].pixelSha256,
824
+ ...icon.appearance ? { appearance: icon.appearance } : {},
825
+ ...icon.layout ? { layout: icon.layout } : {}
826
+ }))
827
+ }) : null;
828
+ const previous = /* @__PURE__ */ new Map();
829
+ const replacements = [];
830
+ for (const [ordinal, contributor] of contributors.entries()) {
831
+ for (const id of contributor.iconIds) {
832
+ const replaced = previous.get(id);
833
+ if (replaced !== void 0) replacements.push({ id, replaced, winner: ordinal });
834
+ previous.set(id, ordinal);
835
+ }
836
+ }
837
+ return {
838
+ package: iconPackage,
839
+ composition,
840
+ contributors: details,
841
+ sourceIdentities: sorted.map((winner) => winner.identity),
842
+ replacements,
843
+ watchFiles: [...watchFiles].sort(compareCodeUnits3),
844
+ watchPaths: [...watchPaths].sort(compareCodeUnits3),
845
+ winners: sorted.map((winner) => ({
846
+ contributor: winner.ordinal,
847
+ id: winner.icon.id,
848
+ identity: winner.identity,
849
+ source: winner.source
850
+ }))
851
+ };
852
+ function addWinner(ordinal, icon, identity, source) {
853
+ pixelBytes += icon.oneX.rgba.byteLength + icon.twoX.rgba.byteLength;
854
+ const maximumPixelBytes = (tileflowIconPackageLimits3.maxAtlasDimension ** 2 + (tileflowIconPackageLimits3.maxAtlasDimension / 2) ** 2) * 4;
855
+ if (winners.size >= tileflowIconPackageLimits3.maxIconCount || pixelBytes > maximumPixelBytes)
856
+ throw new TileflowIconSetError4(
857
+ "ICON_COMPOSITION_INVALID",
858
+ "Composed rendered cells exceed the final sprite capacity"
859
+ );
860
+ winners.set(icon.id, { ordinal, icon, identity, source });
861
+ }
862
+ }
863
+ function isPathInside(root, candidate) {
864
+ const pathFromRoot = relative(root, candidate);
865
+ return pathFromRoot === "" || !isAbsolute(pathFromRoot) && pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`);
866
+ }
867
+
868
+ // src/icon-distance.ts
869
+ var infinity = 1e20;
870
+ function createIconDistanceField(rgba, width, height, ratio) {
871
+ if (width !== 24 * ratio || height !== 24 * ratio || rgba.length !== width * height * 4)
872
+ throw new Error("SDF icons require a 24px logical cell at each density.");
873
+ const outer = new Float64Array(width * height);
874
+ const inner = new Float64Array(width * height);
875
+ let occupied = false;
876
+ for (let y = 0; y < height; y++) {
877
+ for (let x = 0; x < width; x++) {
878
+ const index = y * width + x;
879
+ const alpha = rgba[index * 4 + 3] / 255;
880
+ if (alpha && (x < 6 * ratio || y < 6 * ratio || x >= width - 6 * ratio || y >= height - 6 * ratio))
881
+ throw new Error(
882
+ "SDF icons require six transparent pixels of padding inside the 24px cell. Add a transparent border to the source."
883
+ );
884
+ occupied ||= alpha > 0;
885
+ outer[index] = alpha === 1 ? 0 : alpha === 0 ? infinity : Math.max(0, 0.5 - alpha) ** 2;
886
+ inner[index] = alpha === 1 ? infinity : alpha === 0 ? 0 : Math.max(0, alpha - 0.5) ** 2;
887
+ }
888
+ }
889
+ if (!occupied) throw new Error("An SDF silhouette must contain visible pixels.");
890
+ transform(outer, width, height);
891
+ transform(inner, width, height);
892
+ const result = new Uint8Array(rgba.length);
893
+ for (let i = 0; i < outer.length; i++) {
894
+ const distance = Math.sqrt(outer[i]) - Math.sqrt(inner[i]);
895
+ const alpha = Math.round(255 * Math.max(0, Math.min(1, 0.75 - distance / (8 * ratio))));
896
+ result.set([255, 255, 255, alpha], i * 4);
897
+ }
898
+ return result;
899
+ }
900
+ function transform(grid, width, height) {
901
+ const size = Math.max(width, height);
902
+ const samples = new Float64Array(size);
903
+ const result = new Float64Array(size);
904
+ const sites = new Uint32Array(size);
905
+ const bounds = new Float64Array(size + 1);
906
+ const line = (length) => {
907
+ let count = 0;
908
+ sites[0] = 0;
909
+ bounds[0] = -infinity;
910
+ bounds[1] = infinity;
911
+ for (let position = 1; position < length; position++) {
912
+ let intersection;
913
+ do {
914
+ const site = sites[count];
915
+ intersection = (samples[position] + position ** 2 - samples[site] - site ** 2) / (2 * (position - site));
916
+ if (intersection > bounds[count]) break;
917
+ count--;
918
+ } while (count >= 0);
919
+ count++;
920
+ sites[count] = position;
921
+ bounds[count] = intersection;
922
+ bounds[count + 1] = infinity;
923
+ }
924
+ count = 0;
925
+ for (let position = 0; position < length; position++) {
926
+ while (bounds[count + 1] < position) count++;
927
+ result[position] = (position - sites[count]) ** 2 + samples[sites[count]];
928
+ }
929
+ };
930
+ for (let x = 0; x < width; x++) {
931
+ for (let y = 0; y < height; y++) samples[y] = grid[y * width + x];
932
+ line(height);
933
+ for (let y = 0; y < height; y++) grid[y * width + x] = result[y];
934
+ }
935
+ for (let y = 0; y < height; y++) {
936
+ samples.set(grid.subarray(y * width, (y + 1) * width));
937
+ line(width);
938
+ grid.set(result.subarray(0, width), y * width);
939
+ }
940
+ }
941
+
942
+ // src/icon-preview.ts
943
+ import { renderTileflowIconAtlasPixels } from "@tileflow/core";
944
+ async function renderTileflowIconAtlasPreview(source, index) {
945
+ if (!Object.values(index).some((entry) => entry.sdf)) return source;
946
+ const sharp = await loadSharp();
947
+ const { data, info } = await sharp(source, { limitInputPixels: 2048 * 2048 }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
948
+ const rendered = renderTileflowIconAtlasPixels(data, info.width, info.height, index);
949
+ return sharp(rendered, { raw: { width: info.width, height: info.height, channels: 4 } }).png().toBuffer();
950
+ }
951
+
952
+ // src/icons.ts
953
+ var TileflowIconCompilationError = class extends Error {
954
+ code = "ICON_INVALID";
955
+ issues;
956
+ phase = "icon-compilation";
957
+ constructor(issues) {
958
+ super(
959
+ [
960
+ "Invalid Tileflow icon package",
961
+ ...issues.map((issue) => `- ${issue.path}: ${issue.message}`)
962
+ ].join("\n")
963
+ );
964
+ this.name = "TileflowIconCompilationError";
965
+ this.issues = issues;
966
+ }
967
+ };
968
+ var iconFileExtensions = /* @__PURE__ */ new Set([".svg", ".png", ".jpg", ".jpeg", ".webp"]);
969
+ var iconSpriteSize = 24;
970
+ async function compileTileflowIconPackages(project, options) {
971
+ const result = await composeMapIconSources(project, options);
972
+ const packagesByHash = /* @__PURE__ */ new Map();
973
+ for (const composed of result.composedBySequence.values()) {
974
+ if (composed.package) packagesByHash.set(composed.package.contentHash, composed.package);
975
+ }
976
+ const compositions = {};
977
+ const sourceIdentities = {};
978
+ for (const request of result.mapRequests) {
979
+ const composed = result.composedBySequence.get(request.sequenceKey);
980
+ sourceIdentities[request.mapName] = composed?.sourceIdentities ?? [];
981
+ if (composed?.composition) compositions[request.mapName] = composed.composition;
982
+ }
983
+ const bindings = result.mapRequests.flatMap((request) => {
984
+ if (request.sources.length === 0) return [];
985
+ const composed = result.composedBySequence.get(request.sequenceKey);
986
+ if (!composed?.package) {
987
+ throw new Error(`Missing compiled icon package for map ${request.mapName}`);
988
+ }
989
+ const appearances = Object.fromEntries(
990
+ composed.package.manifest.renderedIcons.flatMap(
991
+ (icon) => icon.appearance ? [[icon.name, icon.appearance]] : []
992
+ )
993
+ );
994
+ return [
995
+ {
996
+ iconIds: composed.package.manifest.iconNames,
997
+ ...Object.keys(appearances).length ? { appearances } : {},
998
+ label: request.mapName,
999
+ mapName: request.mapName,
1000
+ packageHash: composed.package.contentHash
1001
+ }
1002
+ ];
1003
+ });
1004
+ return {
1005
+ bindings,
1006
+ compositions,
1007
+ packages: [...packagesByHash.values()].sort(
1008
+ (left, right) => compareCodeUnits4(left.contentHash, right.contentHash)
1009
+ ),
1010
+ sourceIdentities,
1011
+ watchFiles: uniqueStrings(
1012
+ result.mapRequests.flatMap(
1013
+ (request) => result.composedBySequence.get(request.sequenceKey)?.watchFiles ?? []
1014
+ )
1015
+ ).sort(compareCodeUnits4),
1016
+ watchPaths: uniqueStrings(
1017
+ result.mapRequests.flatMap(
1018
+ (request) => result.composedBySequence.get(request.sequenceKey)?.watchPaths ?? []
1019
+ )
1020
+ ).sort(compareCodeUnits4)
1021
+ };
1022
+ }
1023
+ async function inspectTileflowIconCatalogs(project, options) {
1024
+ const result = await composeMapIconSources(project, {
1025
+ ...options,
1026
+ target: "local"
1027
+ });
1028
+ const maps = result.mapRequests.map((request) => {
1029
+ if (request.sources.length === 0) return { name: request.mapName, icons: { kind: "none" } };
1030
+ const composed = result.composedBySequence.get(request.sequenceKey);
1031
+ if (!composed?.package)
1032
+ throw new Error(`Missing compiled icon catalog for map ${request.mapName}`);
1033
+ return {
1034
+ name: request.mapName,
1035
+ icons: {
1036
+ composition: composed.composition,
1037
+ contributors: composed.contributors.map(describeComposedContributor),
1038
+ iconIds: [...composed.package.manifest.iconNames],
1039
+ kind: "sources",
1040
+ label: request.mapName,
1041
+ packageHash: composed.package.contentHash
1042
+ }
1043
+ };
1044
+ });
1045
+ const requestBySequence = new Map(
1046
+ result.mapRequests.filter((request) => request.sources.length > 0).map((request) => [request.sequenceKey, request])
1047
+ );
1048
+ const catalogs = await Promise.all(
1049
+ [...result.composedBySequence.entries()].map(
1050
+ async ([sequenceKey, composed]) => {
1051
+ const request = requestBySequence.get(sequenceKey);
1052
+ if (!request || !composed.package)
1053
+ throw new Error(`Missing icon contributor sequence ${sequenceKey}`);
1054
+ const atlas = await readComposedAtlasIndexes(composed.package);
1055
+ return {
1056
+ composition: composed.composition,
1057
+ compiledPackage: composed.package,
1058
+ contributors: composed.contributors.map(describeComposedContributor),
1059
+ icons: composed.winners.map((winner, index) => {
1060
+ const manifestIcon = composed.package.manifest.renderedIcons[index];
1061
+ const oneXAtlas = atlas.oneX[winner.id];
1062
+ const twoXAtlas = atlas.twoX[winner.id];
1063
+ const contributor = composed.contributors[winner.contributor];
1064
+ if (!manifestIcon || manifestIcon.name !== winner.id || !oneXAtlas || !twoXAtlas || !contributor) {
1065
+ throw new Error(`Missing compiled catalog metadata for icon ${winner.id}`);
1066
+ }
1067
+ return {
1068
+ id: winner.id,
1069
+ ...manifestIcon.appearance ? { appearance: manifestIcon.appearance } : {},
1070
+ ...manifestIcon.layout ? { layout: manifestIcon.layout } : {},
1071
+ rendered: {
1072
+ oneX: {
1073
+ atlas: atlasRectangle(oneXAtlas),
1074
+ height: oneXAtlas.height,
1075
+ pixelRatio: 1,
1076
+ pixelSha256: manifestIcon.pixelSha256.oneX,
1077
+ width: oneXAtlas.width
1078
+ },
1079
+ twoX: {
1080
+ atlas: atlasRectangle(twoXAtlas),
1081
+ height: twoXAtlas.height,
1082
+ pixelRatio: 2,
1083
+ pixelSha256: manifestIcon.pixelSha256.twoX,
1084
+ width: twoXAtlas.width
1085
+ }
1086
+ },
1087
+ source: winner.source === null ? {
1088
+ contributor: winner.contributor,
1089
+ kind: "icon-set",
1090
+ reference: contributor.kind === "icon-set" ? contributor.reference : "",
1091
+ version: contributor.kind === "icon-set" ? contributor.version : 0
1092
+ } : {
1093
+ byteLength: winner.source.byteLength,
1094
+ contributor: winner.contributor,
1095
+ dimensions: { ...winner.source.dimensions },
1096
+ format: winner.source.format,
1097
+ kind: "file",
1098
+ path: winner.source.path
1099
+ }
1100
+ };
1101
+ }),
1102
+ insideWorkingTree: composed.contributors.every(
1103
+ (contributor) => contributor.kind === "icon-set" || contributor.insideWorkingTree
1104
+ ),
1105
+ replacements: composed.replacements.map(
1106
+ (replacement) => describeComposedReplacement(replacement, composed.contributors)
1107
+ )
1108
+ };
1109
+ }
1110
+ )
1111
+ );
1112
+ catalogs.sort(
1113
+ (left, right) => compareCodeUnits4(
1114
+ left.contributors.map((contributor) => contributor.label).join("\0"),
1115
+ right.contributors.map((contributor) => contributor.label).join("\0")
1116
+ )
1117
+ );
1118
+ return { catalogs, maps };
1119
+ }
1120
+ async function composeMapIconSources(project, options) {
1121
+ const mapRequests = getMapIconRequests(project, options.mapNames);
1122
+ const workspaceRequests = options.mapNames === void 0 ? mapRequests : getMapIconRequests(project);
1123
+ const baseDirectory = options.baseDirectory ?? options.cwd;
1124
+ const workspaceLock = await readWorkspaceIconSetLock(workspaceRequests, baseDirectory);
1125
+ const lockPath = workspaceLock ? resolvePath(baseDirectory, tileflowIconsLockfileName3) : void 0;
1126
+ const issues = [];
1127
+ const composedBySequence = /* @__PURE__ */ new Map();
1128
+ for (const request of uniqueSequences(mapRequests)) {
1129
+ if (request.sources.length === 0) continue;
1130
+ try {
1131
+ const lock = workspaceLock ? selectMapIconSetLock(workspaceLock, request.sources) : void 0;
1132
+ const composed = await composeTileflowIconSources(request.sources, {
1133
+ ...options.icons,
1134
+ baseDirectory,
1135
+ configPath: `maps.${request.mapName}.icons`,
1136
+ cwd: options.cwd,
1137
+ ...lock ? { lock } : {},
1138
+ target: options.target
1139
+ });
1140
+ composedBySequence.set(
1141
+ request.sequenceKey,
1142
+ lockPath && lock ? { ...composed, watchFiles: uniqueStrings([...composed.watchFiles, lockPath]) } : composed
1143
+ );
1144
+ } catch (error) {
1145
+ if (error instanceof TileflowIconSetError5) throw error;
1146
+ if (error instanceof TileflowIconCompilationError) issues.push(...error.issues);
1147
+ else if (error instanceof TileflowAssetDirectoryError) issues.push(...error.issues);
1148
+ else
1149
+ issues.push({
1150
+ message: error instanceof Error ? error.message : "Icon compilation failed",
1151
+ path: `maps.${request.mapName}.icons`
1152
+ });
1153
+ }
1154
+ }
1155
+ if (issues.length > 0) throw new TileflowIconCompilationError(issues);
1156
+ return { composedBySequence, mapRequests };
1157
+ }
1158
+ async function readWorkspaceIconSetLock(requests, baseDirectory) {
1159
+ const references = /* @__PURE__ */ new Set();
1160
+ for (const request of requests) {
1161
+ for (const reference of collectTileflowIconSetReferences2(request.sources))
1162
+ references.add(reference);
1163
+ }
1164
+ if (references.size === 0) return void 0;
1165
+ const sources = [...references].sort(compareCodeUnits4).map((reference) => ({ kind: "icon-set", reference }));
1166
+ return readTileflowIconsLockfile(baseDirectory, sources);
1167
+ }
1168
+ function selectMapIconSetLock(lock, sources) {
1169
+ const references = collectTileflowIconSetReferences2(sources);
1170
+ if (references.length === 0) return void 0;
1171
+ return {
1172
+ lockfileVersion: 1,
1173
+ sets: Object.fromEntries(references.map((reference) => [reference, lock.sets[reference]]))
1174
+ };
1175
+ }
1176
+ async function prepareTileflowCatalogIcons(project, options) {
1177
+ const compiled = await compileTileflowIconPackages(project, {
1178
+ baseDirectory: options.baseDirectory,
1179
+ cwd: options.cwd,
1180
+ ...options.icons ? { icons: options.icons } : {},
1181
+ target: "local"
1182
+ });
1183
+ const packagesByHash = new Map(
1184
+ compiled.packages.map((iconPackage) => [iconPackage.contentHash, iconPackage])
1185
+ );
1186
+ const assets = [];
1187
+ const mapAssets = {};
1188
+ const mapIconCompositions = {};
1189
+ const mapIconSources = {};
1190
+ for (const mapName of Object.keys(project.maps)) mapIconSources[mapName] = [];
1191
+ for (const binding of compiled.bindings) {
1192
+ const iconPackage = packagesByHash.get(binding.packageHash);
1193
+ if (!iconPackage) throw new Error(`Missing compiled icon package for map ${binding.mapName}`);
1194
+ const spriteUrl = joinUrl(options.assetBaseUrl, `icons/${binding.mapName}/sprite`);
1195
+ mapAssets[binding.mapName] = {
1196
+ icons: {
1197
+ ids: binding.iconIds,
1198
+ sprite: spriteUrl,
1199
+ ...binding.appearances ? { appearances: binding.appearances } : {}
1200
+ }
1201
+ };
1202
+ mapIconSources[binding.mapName] = compiled.sourceIdentities[binding.mapName] ?? [];
1203
+ const composition = compiled.compositions[binding.mapName];
1204
+ if (composition) mapIconCompositions[binding.mapName] = composition;
1205
+ for (const file of iconPackage.files) {
1206
+ assets.push({
1207
+ contentType: file.contentType === "application/json" ? "application/json; charset=utf-8" : file.contentType,
1208
+ fileName: `icons/${binding.mapName}/${file.fileName}`,
1209
+ source: file.contentType === "application/json" ? new TextDecoder().decode(file.source) : file.source
1210
+ });
1211
+ }
1212
+ }
1213
+ return {
1214
+ assets,
1215
+ baseDirectory: await realpath3(options.baseDirectory ?? options.cwd),
1216
+ cwd: await realpath3(options.cwd),
1217
+ mapAssets,
1218
+ mapIconCompositions,
1219
+ mapIconSources,
1220
+ project,
1221
+ sourceProject: project,
1222
+ watchFiles: compiled.watchFiles,
1223
+ watchPaths: compiled.watchPaths
1224
+ };
1225
+ }
1226
+ async function getTileflowIconWatchPaths(project, cwd, baseDirectory = cwd) {
1227
+ const requests = getMapIconRequests(project);
1228
+ const paths = /* @__PURE__ */ new Set();
1229
+ let realBaseDirectory;
1230
+ for (const request of uniqueSequences(requests)) {
1231
+ for (const source of request.sources) {
1232
+ if (typeof source === "object" && source.kind === "icon-set") {
1233
+ realBaseDirectory ??= await realpath3(baseDirectory);
1234
+ paths.add(resolvePath(realBaseDirectory, tileflowIconsLockfileName3));
1235
+ continue;
1236
+ }
1237
+ try {
1238
+ const directory = await readTileflowIconDirectory(source, {
1239
+ baseDirectory,
1240
+ configPath: `maps.${request.mapName}.icons`,
1241
+ cwd,
1242
+ target: "local"
1243
+ });
1244
+ if (directory.watchPath) paths.add(directory.watchPath);
1245
+ } catch {
1246
+ }
1247
+ }
1248
+ }
1249
+ return [...paths].sort(compareCodeUnits4);
1250
+ }
1251
+ function getMapIconRequests(project, mapNames) {
1252
+ const selectedNames = (mapNames === void 0 ? Object.keys(project.maps) : [...new Set(mapNames)]).sort(compareCodeUnits4);
1253
+ return selectedNames.map((mapName) => {
1254
+ const mapConfig = project.maps[mapName];
1255
+ if (!mapConfig) throw new Error(`Unknown Tileflow map: ${mapName}`);
1256
+ const sources = parseResolvedTileflowMap(mapConfig).icons ?? [];
1257
+ return { mapName, sequenceKey: serializeCanonicalJson3(sources), sources };
1258
+ });
1259
+ }
1260
+ function uniqueSequences(requests) {
1261
+ return [...new Map(requests.map((request) => [request.sequenceKey, request])).values()].sort(
1262
+ (left, right) => compareCodeUnits4(left.sequenceKey, right.sequenceKey)
1263
+ );
1264
+ }
1265
+ function describeComposedContributor(contributor) {
1266
+ return contributor.kind === "icon-set" ? {
1267
+ iconIds: [...contributor.iconIds],
1268
+ kind: "icon-set",
1269
+ label: contributor.label,
1270
+ reference: contributor.reference,
1271
+ version: contributor.version
1272
+ } : {
1273
+ iconIds: [...contributor.iconIds],
1274
+ insideWorkingTree: contributor.insideWorkingTree,
1275
+ kind: contributor.kind,
1276
+ label: contributor.label
1277
+ };
1278
+ }
1279
+ function describeComposedReplacement(replacement, contributors) {
1280
+ return {
1281
+ id: replacement.id,
1282
+ replaced: describeContributorExport(contributors[replacement.replaced], replacement.id),
1283
+ winner: describeContributorExport(contributors[replacement.winner], replacement.id)
1284
+ };
1285
+ }
1286
+ function describeContributorExport(contributor, id) {
1287
+ if (!contributor) return id;
1288
+ if (contributor.kind === "icon-set") return contributor.label;
1289
+ return contributor.entries.find((entry) => entry.id === id)?.path ?? contributor.label;
1290
+ }
1291
+ async function readComposedAtlasIndexes(iconPackage) {
1292
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1293
+ const read = (fileName) => {
1294
+ const file = iconPackage.files.find((candidate) => candidate.fileName === fileName);
1295
+ if (!file) throw new Error(`Missing generated ${fileName}`);
1296
+ return tileflowIconSpriteIndexSchema.parse(
1297
+ parseTileflowIconJson2(
1298
+ decoder.decode(file.source),
1299
+ tileflowIconPackageLimits4.maxGeneratedFileBytes
1300
+ )
1301
+ );
1302
+ };
1303
+ return { oneX: read("sprite.json"), twoX: read("sprite@2x.json") };
1304
+ }
1305
+ function describeAssetDirectory(directory) {
1306
+ return typeof directory.authoring === "string" ? directory.authoring : `npm:${directory.authoring.package}/${directory.authoring.path}`;
1307
+ }
1308
+ function describeAssetFile(directory, fileName) {
1309
+ return `${describeAssetDirectory(directory).replace(/\/+$/u, "")}/${fileName}`;
1310
+ }
1311
+ async function inspectIconSource(directory, target, issues) {
1312
+ let entries;
1313
+ const issueCount = issues.length;
1314
+ try {
1315
+ entries = await readdir2(directory.realPath, { withFileTypes: true });
1316
+ } catch (error) {
1317
+ issues.push({
1318
+ message: describeFileSystemError(error, "Icon source could not be read"),
1319
+ path: directory.configPath
1320
+ });
1321
+ return null;
1322
+ }
1323
+ entries.sort((left, right) => compareCodeUnits4(left.name, right.name));
1324
+ const candidates = [];
1325
+ const names = /* @__PURE__ */ new Set();
1326
+ let metadata = {
1327
+ schemaVersion: 1,
1328
+ icons: {}
1329
+ };
1330
+ for (const entry of entries) {
1331
+ const entryPath = resolvePath(directory.realPath, entry.name);
1332
+ const path = `${directory.configPath}/${entry.name}`;
1333
+ if (entry.isDirectory()) {
1334
+ issues.push({ message: "Nested directories are not supported", path });
1335
+ continue;
1336
+ }
1337
+ let entryStat;
1338
+ let realEntryPath = entryPath;
1339
+ try {
1340
+ if (entry.isSymbolicLink()) {
1341
+ realEntryPath = await realpath3(entryPath);
1342
+ if (!isPathInside2(directory.containmentRoot, realEntryPath)) {
1343
+ issues.push({ message: "Icon symlink escapes its owning directory boundary", path });
1344
+ continue;
1345
+ }
1346
+ }
1347
+ entryStat = await stat(realEntryPath);
1348
+ } catch (error) {
1349
+ issues.push({ message: describeFileSystemError(error, "Icon entry could not be read"), path });
1350
+ continue;
1351
+ }
1352
+ if (!entryStat.isFile()) {
1353
+ issues.push({ message: "Icon entries must be regular files", path });
1354
+ continue;
1355
+ }
1356
+ const extension = extname(entry.name).toLowerCase();
1357
+ if (entry.name === tileflowIconMetadataFileName) {
1358
+ try {
1359
+ if (entryStat.size > tileflowIconPackageLimits4.maxSourceFileBytes)
1360
+ throw new Error("Icon metadata exceeds its byte limit");
1361
+ metadata = tileflowIconAuthorMetadataSchema.parse(
1362
+ parseTileflowIconJson2(
1363
+ await readFile(realEntryPath, "utf8"),
1364
+ tileflowIconPackageLimits4.maxSourceFileBytes
1365
+ )
1366
+ );
1367
+ } catch (error) {
1368
+ issues.push({
1369
+ path,
1370
+ message: `Invalid icon metadata: ${error instanceof Error ? error.message : String(error)}`
1371
+ });
1372
+ }
1373
+ continue;
1374
+ }
1375
+ if (!iconFileExtensions.has(extension)) {
1376
+ continue;
1377
+ }
1378
+ const sourceName = entry.name.slice(0, -extname(entry.name).length);
1379
+ const kind = sourceName.endsWith(".pattern") ? "pattern" : "icon";
1380
+ const name = kind === "pattern" ? sourceName.slice(0, -".pattern".length) : sourceName;
1381
+ if (names.has(name)) {
1382
+ issues.push({
1383
+ message: `Duplicate icon basename "${name}"`,
1384
+ path
1385
+ });
1386
+ continue;
1387
+ }
1388
+ names.add(name);
1389
+ if (!tileflowIconIdPattern.test(name) || name.length > 64) {
1390
+ issues.push({
1391
+ message: "Icon filenames must produce a lower-kebab ID of at most 64 characters",
1392
+ path
1393
+ });
1394
+ }
1395
+ if (entryStat.size > tileflowIconPackageLimits4.maxSourceFileBytes) {
1396
+ issues.push({
1397
+ message: `Icon source exceeds ${tileflowIconPackageLimits4.maxSourceFileBytes} bytes`,
1398
+ path
1399
+ });
1400
+ }
1401
+ candidates.push({
1402
+ displayPath: describeAssetFile(directory, entry.name),
1403
+ fileName: entry.name,
1404
+ format: iconSourceFormat(extension),
1405
+ kind,
1406
+ name,
1407
+ path: realEntryPath,
1408
+ size: entryStat.size
1409
+ });
1410
+ }
1411
+ candidates.sort((left, right) => compareCodeUnits4(left.name, right.name));
1412
+ for (const id of Object.keys(metadata.icons)) {
1413
+ if (!names.has(id))
1414
+ issues.push({
1415
+ path: `${directory.configPath}/${tileflowIconMetadataFileName}`,
1416
+ message: `Icon metadata references unknown ID "${id}"`
1417
+ });
1418
+ }
1419
+ if (candidates.length === 0) {
1420
+ issues.push({
1421
+ message: "No supported .svg, .png, .jpg, .jpeg, or .webp icon files were found",
1422
+ path: directory.configPath
1423
+ });
1424
+ }
1425
+ if (issues.length > issueCount) {
1426
+ return null;
1427
+ }
1428
+ const icons = [];
1429
+ for (const candidate of candidates) {
1430
+ const displayPath = candidate.displayPath;
1431
+ try {
1432
+ const source = await readFile(candidate.path);
1433
+ if (source.byteLength !== candidate.size || source.byteLength > tileflowIconPackageLimits4.maxSourceFileBytes) {
1434
+ throw new Error("Icon changed while it was being validated");
1435
+ }
1436
+ if (target === "hosted" && extname(candidate.fileName).toLowerCase() === ".svg") {
1437
+ validateHostedSvg(source, displayPath);
1438
+ }
1439
+ const declared = metadata.icons[candidate.name];
1440
+ icons.push({
1441
+ ...declared?.representation === "sdf" ? { appearance: { representation: declared.representation, defaults: declared.defaults } } : {},
1442
+ ...declared?.layout ? { layout: declared.layout } : {},
1443
+ displayPath,
1444
+ fileName: candidate.fileName,
1445
+ format: candidate.format,
1446
+ kind: candidate.kind,
1447
+ name: candidate.name,
1448
+ path: candidate.path,
1449
+ source
1450
+ });
1451
+ } catch (error) {
1452
+ issues.push({
1453
+ message: error instanceof Error ? error.message : "Icon could not be read",
1454
+ path: displayPath
1455
+ });
1456
+ }
1457
+ }
1458
+ if (issues.length > issueCount) {
1459
+ return null;
1460
+ }
1461
+ return { icons };
1462
+ }
1463
+ async function validateDecodedDimensions(icon) {
1464
+ const sharp = await loadSharp();
1465
+ const metadata = await sharp(icon.source, {
1466
+ density: 72,
1467
+ failOn: "error",
1468
+ limitInputPixels: tileflowIconPackageLimits4.maxDecodedPixelsPerIcon
1469
+ }).metadata();
1470
+ const width = metadata.width ?? 0;
1471
+ const height = metadata.height ?? 0;
1472
+ if (width <= 0 || height <= 0) {
1473
+ throw new Error(`Unable to determine decoded dimensions for ${icon.fileName}`);
1474
+ }
1475
+ if (width * height > tileflowIconPackageLimits4.maxDecodedPixelsPerIcon) {
1476
+ throw new Error(
1477
+ `${icon.fileName} exceeds ${tileflowIconPackageLimits4.maxDecodedPixelsPerIcon} decoded pixels`
1478
+ );
1479
+ }
1480
+ return { height, width };
1481
+ }
1482
+ async function renderIcon(icon, dimensions, pixelRatio) {
1483
+ const sharp = await loadSharp();
1484
+ const targetWidth = (icon.kind === "pattern" ? dimensions.width : iconSpriteSize) * pixelRatio;
1485
+ const targetHeight = (icon.kind === "pattern" ? dimensions.height : iconSpriteSize) * pixelRatio;
1486
+ const { data, info } = await sharp(icon.source, {
1487
+ density: 72 * pixelRatio,
1488
+ failOn: "error",
1489
+ limitInputPixels: tileflowIconPackageLimits4.maxDecodedPixelsPerIcon
1490
+ }).resize(targetWidth, targetHeight, {
1491
+ background: { alpha: 0, b: 0, g: 0, r: 0 },
1492
+ fit: "contain"
1493
+ }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1494
+ if (info.channels !== 4 || info.width !== targetWidth || info.height !== targetHeight) {
1495
+ throw new Error(`Rendered icon ${icon.fileName} did not produce canonical RGBA pixels`);
1496
+ }
1497
+ let rgba;
1498
+ try {
1499
+ if (icon.appearance && icon.kind === "pattern")
1500
+ throw new Error("SDF metadata is only supported for icons, not patterns.");
1501
+ if (icon.layout) {
1502
+ if (icon.kind === "pattern")
1503
+ throw new Error("Text-fitting metadata is only supported for icons, not patterns.");
1504
+ assertTileflowIconLayoutBounds2(
1505
+ icon.layout,
1506
+ info.width / pixelRatio,
1507
+ info.height / pixelRatio
1508
+ );
1509
+ }
1510
+ rgba = icon.appearance ? createIconDistanceField(new Uint8Array(data), info.width, info.height, pixelRatio) : new Uint8Array(data);
1511
+ } catch (error) {
1512
+ throw new Error(
1513
+ `${icon.displayPath}: ${error instanceof Error ? error.message : "Invalid icon capabilities"}`,
1514
+ { cause: error }
1515
+ );
1516
+ }
1517
+ const png = await sharp(rgba, {
1518
+ raw: { channels: 4, height: info.height, width: info.width }
1519
+ }).png({ adaptiveFiltering: false, compressionLevel: 9, palette: false }).toBuffer();
1520
+ return { height: info.height, png, rgba, width: info.width };
1521
+ }
1522
+ function validateHostedSvg(source, displayPath) {
1523
+ const parser = new SaxesParser({ fileName: displayPath, xmlns: true });
1524
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1525
+ let styleDepth = 0;
1526
+ let styleText = "";
1527
+ parser.on("doctype", () => {
1528
+ throw new Error("Hosted SVGs cannot contain a document type or entity declarations");
1529
+ });
1530
+ parser.on("processinginstruction", (instruction) => {
1531
+ if (instruction.target.toLowerCase() === "xml-stylesheet") {
1532
+ throw new Error("Hosted SVGs cannot load external stylesheets");
1533
+ }
1534
+ });
1535
+ parser.on("opentag", (tag) => {
1536
+ const localName = tag.local.toLowerCase();
1537
+ if (localName === "script") {
1538
+ throw new Error("Hosted SVGs cannot contain scripts");
1539
+ }
1540
+ if (localName === "style") {
1541
+ styleDepth += 1;
1542
+ }
1543
+ for (const attribute of Object.values(tag.attributes)) {
1544
+ const attributeName = attribute.local.toLowerCase();
1545
+ const value = attribute.value.trim();
1546
+ if (attributeName.startsWith("on")) {
1547
+ throw new Error("Hosted SVGs cannot contain script event handlers");
1548
+ }
1549
+ if (attributeName === "href" && value.length > 0 && !value.startsWith("#")) {
1550
+ throw new Error("Hosted SVG resource references must use local fragments");
1551
+ }
1552
+ if (containsExternalCssUrl(value)) {
1553
+ throw new Error("Hosted SVG resource references must use local fragments");
1554
+ }
1555
+ }
1556
+ });
1557
+ parser.on("text", (text) => {
1558
+ if (styleDepth > 0) {
1559
+ styleText += text;
1560
+ }
1561
+ });
1562
+ parser.on("cdata", (text) => {
1563
+ if (styleDepth > 0) {
1564
+ styleText += text;
1565
+ }
1566
+ });
1567
+ parser.on("closetag", (tag) => {
1568
+ if (tag.local.toLowerCase() !== "style") {
1569
+ return;
1570
+ }
1571
+ styleDepth -= 1;
1572
+ if (styleDepth === 0) {
1573
+ if (styleText.toLowerCase().includes("@import") || containsExternalCssUrl(styleText)) {
1574
+ throw new Error("Hosted SVGs cannot load external stylesheets or resources");
1575
+ }
1576
+ styleText = "";
1577
+ }
1578
+ });
1579
+ parser.on("error", (error) => {
1580
+ throw error;
1581
+ });
1582
+ const chunkBytes = 64 * 1024;
1583
+ for (let offset = 0; offset < source.byteLength; offset += chunkBytes) {
1584
+ parser.write(decoder.decode(source.subarray(offset, offset + chunkBytes), { stream: true }));
1585
+ }
1586
+ parser.write(decoder.decode());
1587
+ parser.close();
1588
+ }
1589
+ function containsExternalCssUrl(value) {
1590
+ const lowerValue = value.toLowerCase();
1591
+ let cursor = 0;
1592
+ while (cursor < value.length) {
1593
+ const start = lowerValue.indexOf("url(", cursor);
1594
+ if (start === -1) {
1595
+ return false;
1596
+ }
1597
+ const end = value.indexOf(")", start + 4);
1598
+ if (end === -1) {
1599
+ return true;
1600
+ }
1601
+ const rawTarget = value.slice(start + 4, end).trim();
1602
+ const target = stripMatchingQuotes(rawTarget);
1603
+ if (!target.startsWith("#")) {
1604
+ return true;
1605
+ }
1606
+ cursor = end + 1;
1607
+ }
1608
+ return false;
1609
+ }
1610
+ function stripMatchingQuotes(value) {
1611
+ if (value.length < 2) {
1612
+ return value;
1613
+ }
1614
+ const first = value[0];
1615
+ const last = value[value.length - 1];
1616
+ return first === '"' && last === '"' || first === "'" && last === "'" ? value.slice(1, -1).trim() : value;
1617
+ }
1618
+ async function mapWithConcurrency(values, concurrency, map) {
1619
+ const results = new Array(values.length);
1620
+ let nextIndex = 0;
1621
+ async function worker() {
1622
+ while (nextIndex < values.length) {
1623
+ const index = nextIndex;
1624
+ nextIndex += 1;
1625
+ const value = values[index];
1626
+ if (value !== void 0) {
1627
+ results[index] = await map(value, index);
1628
+ }
1629
+ }
1630
+ }
1631
+ await Promise.all(
1632
+ Array.from({ length: Math.min(concurrency, values.length) }, async () => worker())
1633
+ );
1634
+ return results;
1635
+ }
1636
+ function atlasRectangle(entry) {
1637
+ return { height: entry.height, width: entry.width, x: entry.x, y: entry.y };
1638
+ }
1639
+ function iconSourceFormat(extension) {
1640
+ switch (extension) {
1641
+ case ".svg":
1642
+ return "svg";
1643
+ case ".png":
1644
+ return "png";
1645
+ case ".jpg":
1646
+ case ".jpeg":
1647
+ return "jpeg";
1648
+ case ".webp":
1649
+ return "webp";
1650
+ default:
1651
+ throw new Error(`Unsupported icon source format: ${extension}`);
1652
+ }
1653
+ }
1654
+ function isPathInside2(root, candidate) {
1655
+ const pathFromRoot = relative2(root, candidate);
1656
+ return pathFromRoot === "" || !isAbsolute2(pathFromRoot) && pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep2}`);
1657
+ }
1658
+ function describeFileSystemError(error, fallback) {
1659
+ if (error && typeof error === "object" && "code" in error) {
1660
+ const code = String(error.code ?? "");
1661
+ if (code === "ENOENT") {
1662
+ return fallback;
1663
+ }
1664
+ if (code === "EACCES" || code === "EPERM") {
1665
+ return "Icon source is not readable";
1666
+ }
1667
+ }
1668
+ return error instanceof Error && error.message ? error.message : fallback;
1669
+ }
1670
+ function joinUrl(base, path) {
1671
+ const trimmedBase = base.replace(/\/+$/g, "");
1672
+ const trimmedPath = path.replace(/^\/+/g, "");
1673
+ return trimmedBase ? `${trimmedBase}/${trimmedPath}` : trimmedPath;
1674
+ }
1675
+ function uniqueStrings(values) {
1676
+ return [...new Set(values)];
1677
+ }
1678
+ async function renderIconInputs(inputs) {
1679
+ let renderedOneXPixels = 0;
1680
+ return mapWithConcurrency(inputs, tileflowIconPackageLimits4.decodeConcurrency, async (icon) => {
1681
+ const dimensions = await validateDecodedDimensions(icon);
1682
+ const oneXWidth = icon.kind === "pattern" ? dimensions.width : iconSpriteSize;
1683
+ const oneXHeight = icon.kind === "pattern" ? dimensions.height : iconSpriteSize;
1684
+ const maximumOneXDimension = tileflowIconPackageLimits4.maxAtlasDimension / 2;
1685
+ renderedOneXPixels += oneXWidth * oneXHeight;
1686
+ if (oneXWidth > maximumOneXDimension || oneXHeight > maximumOneXDimension || renderedOneXPixels > maximumOneXDimension ** 2) {
1687
+ throw new Error(
1688
+ "Rendered icon inputs exceed the paired sprite capacity before rasterization"
1689
+ );
1690
+ }
1691
+ if (icon.kind === "pattern" && (dimensions.width < 2 || dimensions.width > 512 || (dimensions.width & dimensions.width - 1) !== 0)) {
1692
+ throw new Error(
1693
+ `${icon.fileName} pattern width must be a power of two from 2 through 512 pixels`
1694
+ );
1695
+ }
1696
+ const oneX = await renderIcon(icon, dimensions, 1);
1697
+ const twoX = await renderIcon(icon, dimensions, 2);
1698
+ return {
1699
+ dimensions,
1700
+ input: icon,
1701
+ oneX,
1702
+ pixelSha256: {
1703
+ oneX: await hashTileflowRenderedIconPixels3({
1704
+ height: oneX.height,
1705
+ pixelRatio: 1,
1706
+ rgba: oneX.rgba,
1707
+ width: oneX.width
1708
+ }),
1709
+ twoX: await hashTileflowRenderedIconPixels3({
1710
+ height: twoX.height,
1711
+ pixelRatio: 2,
1712
+ rgba: twoX.rgba,
1713
+ width: twoX.width
1714
+ })
1715
+ },
1716
+ sourceSha256: icon.appearance || icon.layout ? await sha256Hex3(
1717
+ `tileflow-icon-source-v2\0${await sha256Hex3(icon.source)}\0${serializeCanonicalJson3({ ...icon.appearance ? { appearance: icon.appearance } : {}, ...icon.layout ? { layout: icon.layout } : {} })}`
1718
+ ) : await sha256Hex3(icon.source),
1719
+ twoX
1720
+ };
1721
+ });
1722
+ }
1723
+ async function readTileflowIconDirectory(source, options) {
1724
+ const configPath = options.configPath ?? "icons";
1725
+ const directory = (await resolveTileflowAssetDirectories([source], {
1726
+ baseDirectory: options.baseDirectory,
1727
+ configPath,
1728
+ cwd: options.cwd,
1729
+ kind: "icons",
1730
+ ordinalOffset: options.ordinal ?? 0,
1731
+ target: options.target
1732
+ }))[0];
1733
+ if (!directory)
1734
+ throw new TileflowIconCompilationError([{ path: configPath, message: "Missing icon directory" }]);
1735
+ const issues = [];
1736
+ const inspected = await inspectIconSource(directory, options.target, issues);
1737
+ if (!inspected || issues.length) throw new TileflowIconCompilationError(issues);
1738
+ if (inspected.icons.length > tileflowIconPackageLimits4.maxIconCount)
1739
+ throw new TileflowIconCompilationError([
1740
+ {
1741
+ path: configPath,
1742
+ message: `One icon contributor supports at most ${tileflowIconPackageLimits4.maxIconCount} exports`
1743
+ }
1744
+ ]);
1745
+ const entries = inspected.icons.map((icon) => ({
1746
+ byteLength: icon.source.byteLength,
1747
+ format: icon.format,
1748
+ id: icon.name,
1749
+ kind: icon.kind,
1750
+ path: icon.displayPath
1751
+ }));
1752
+ return {
1753
+ containmentRoot: directory.containmentRoot,
1754
+ entries,
1755
+ iconIds: entries.map((entry) => entry.id),
1756
+ label: describeAssetDirectory(directory),
1757
+ realPath: directory.realPath,
1758
+ ...directory.watch ? { watchPath: directory.realPath } : {},
1759
+ render: async (ids) => {
1760
+ const selected = new Set(ids);
1761
+ const inputs = inspected.icons.filter((icon) => selected.has(icon.name));
1762
+ if (inputs.length !== ids.length || inputs.reduce((sum, icon) => sum + icon.source.byteLength, 0) > tileflowIconPackageLimits4.maxSourceBytes)
1763
+ throw new TileflowIconCompilationError([
1764
+ {
1765
+ path: configPath,
1766
+ message: "Selected icon inputs exceed their source budget or have invalid IDs"
1767
+ }
1768
+ ]);
1769
+ return (await renderIconInputs(inputs)).map((rendered) => ({
1770
+ icon: {
1771
+ id: rendered.input.name,
1772
+ ...rendered.input.appearance ? { appearance: rendered.input.appearance } : {},
1773
+ ...rendered.input.layout ? { layout: rendered.input.layout } : {},
1774
+ oneX: {
1775
+ height: rendered.oneX.height,
1776
+ width: rendered.oneX.width,
1777
+ rgba: rendered.oneX.rgba
1778
+ },
1779
+ twoX: {
1780
+ height: rendered.twoX.height,
1781
+ width: rendered.twoX.width,
1782
+ rgba: rendered.twoX.rgba
1783
+ }
1784
+ },
1785
+ // Canonical key order keeps the in-memory build manifest byte-identical to its artifact.
1786
+ identity: {
1787
+ format: rendered.input.format,
1788
+ id: rendered.input.name,
1789
+ kind: rendered.input.kind,
1790
+ sha256: rendered.sourceSha256
1791
+ },
1792
+ source: {
1793
+ byteLength: rendered.input.source.byteLength,
1794
+ dimensions: { ...rendered.dimensions },
1795
+ format: rendered.input.format,
1796
+ id: rendered.input.name,
1797
+ kind: rendered.input.kind,
1798
+ path: rendered.input.displayPath
1799
+ },
1800
+ sourceBytes: rendered.input.source.byteLength
1801
+ }));
1802
+ }
1803
+ };
1804
+ }
1805
+
1806
+ export {
1807
+ packTileflowRenderedIcons,
1808
+ verifyTileflowIconArtifact,
1809
+ getTileflowIconCacheDirectory,
1810
+ loadTileflowIconSetArtifact,
1811
+ storeTileflowIconSetArtifact,
1812
+ readTileflowIconsLockfileText,
1813
+ readTileflowIconsLockfile,
1814
+ writeTileflowIconsLockfile,
1815
+ composeTileflowIconSources,
1816
+ renderTileflowIconAtlasPreview,
1817
+ TileflowIconCompilationError,
1818
+ iconFileExtensions,
1819
+ compileTileflowIconPackages,
1820
+ inspectTileflowIconCatalogs,
1821
+ prepareTileflowCatalogIcons,
1822
+ getTileflowIconWatchPaths,
1823
+ readTileflowIconDirectory
1824
+ };