@tileflow/dev 0.1.0-alpha.23 → 0.1.0-alpha.25

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