@tileflow/dev 0.1.0-alpha.22 → 0.1.0-alpha.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  readBoundedTileflowResponse,
20
20
  renderTileflowPreviewHtml,
21
21
  resolveTileflowPreview
22
- } from "./chunk-MIGBHCXX.js";
22
+ } from "./chunk-6OJICHB3.js";
23
23
  import {
24
24
  createTileflowArtifactStatus,
25
25
  createTileflowComparisonRequestHandler,
@@ -31,7 +31,7 @@ import {
31
31
  isTileflowRequestUrl,
32
32
  tileflowComparisonSchemaVersion,
33
33
  writeTileflowNodeResponse
34
- } from "./chunk-KH7CMHT3.js";
34
+ } from "./chunk-XS5Q4IS2.js";
35
35
  import {
36
36
  TileflowHostedManifestOverwriteError,
37
37
  TileflowLocalTilesetProductionError,
@@ -56,7 +56,7 @@ import {
56
56
  tileflowArtifactSessionSchemaVersion,
57
57
  writeTileflowArtifactPlan,
58
58
  writeTileflowBuildArtifacts
59
- } from "./chunk-XTRHREXO.js";
59
+ } from "./chunk-XXVI42UZ.js";
60
60
  import {
61
61
  inspectTileflowPmtiles,
62
62
  inspectTileflowPmtilesForLocalUse
@@ -79,8 +79,11 @@ import {
79
79
  getTileflowIconWatchPaths,
80
80
  iconFileExtensions,
81
81
  inspectTileflowIconCatalogs,
82
- prepareTileflowCatalogIcons
83
- } from "./chunk-CENQR5Q2.js";
82
+ loadSharp,
83
+ packTileflowRenderedIcons,
84
+ prepareTileflowCatalogIcons,
85
+ readTileflowIconDirectory
86
+ } from "./chunk-NUHSYLLS.js";
84
87
  import "./chunk-BL3FK4GO.js";
85
88
  import {
86
89
  TileflowStyleValidationError,
@@ -102,6 +105,577 @@ import {
102
105
  loadValidTileflowConfigWithInputs
103
106
  } from "./chunk-6MGFMKG3.js";
104
107
  import "./chunk-D4OE4ITL.js";
108
+
109
+ // src/icon-composition.ts
110
+ import { resolve as resolve3 } from "path";
111
+ import { tileflowIconsLockfileName as tileflowIconsLockfileName2 } from "@tileflow/core";
112
+ import {
113
+ collectTileflowIconSetReferences,
114
+ compareCodeUnits as compareCodeUnits2,
115
+ parseTileflowIconComposition,
116
+ parseTileflowIconsLockfile as parseTileflowIconsLockfile2,
117
+ TileflowIconSetError as TileflowIconSetError4,
118
+ tileflowIconPackageLimits as tileflowIconPackageLimits2
119
+ } from "@tileflow/core";
120
+
121
+ // src/icon-cache.ts
122
+ import { hashTileflowIconPackageManifest as hashTileflowIconPackageManifest2 } from "@tileflow/core";
123
+ import { constants } from "fs";
124
+ import { lstat, mkdir, mkdtemp, open, readdir, realpath, rename, rm } from "fs/promises";
125
+ import { homedir, platform } from "os";
126
+ import { join, resolve } from "path";
127
+ import {
128
+ TileflowIconSetError as TileflowIconSetError2,
129
+ tileflowIconSetPinSchema
130
+ } from "@tileflow/core";
131
+
132
+ // src/icon-artifact.ts
133
+ import {
134
+ tileflowIconSpriteIndexSchema as indexSchema
135
+ } from "@tileflow/core";
136
+ import {
137
+ compareCodeUnits,
138
+ hashTileflowIconPackageManifest,
139
+ hashTileflowRenderedIconPixels,
140
+ parseTileflowIconJson,
141
+ sha256Hex,
142
+ TileflowIconSetError,
143
+ tileflowIconPackageLimits,
144
+ tileflowIconPackageManifestSchema
145
+ } from "@tileflow/core";
146
+ async function verifyTileflowIconArtifact(input) {
147
+ try {
148
+ const manifest = tileflowIconPackageManifestSchema.parse(input.manifest);
149
+ const contentHash = input.contentHash;
150
+ if (input.files.length !== 4 || new Set(input.files.map((file) => file.fileName)).size !== 4)
151
+ throw new Error("An artifact must contain exactly the four generated files");
152
+ const files = manifest.files.map((expected) => {
153
+ const source = input.files.find((file) => file.fileName === expected.name);
154
+ if (!source || source.contentType !== expected.contentType || source.source.byteLength !== expected.byteLength)
155
+ throw new Error("Generated file identity does not match the manifest");
156
+ return {
157
+ contentType: source.contentType,
158
+ fileName: source.fileName,
159
+ source: new Uint8Array(source.source)
160
+ };
161
+ });
162
+ if (await hashTileflowIconPackageManifest(manifest) !== contentHash)
163
+ throw new Error("Artifact manifest hash mismatch");
164
+ for (const [index, file] of files.entries()) {
165
+ if (await sha256Hex(file.source) !== manifest.files[index].sha256)
166
+ throw new Error(`Generated ${file.fileName} checksum mismatch`);
167
+ }
168
+ const decoder = new TextDecoder("utf-8", { fatal: true });
169
+ const oneIndex = indexSchema.parse(
170
+ parseTileflowIconJson(
171
+ decoder.decode(files[0].source),
172
+ tileflowIconPackageLimits.maxGeneratedFileBytes
173
+ )
174
+ );
175
+ const twoIndex = indexSchema.parse(
176
+ parseTileflowIconJson(
177
+ decoder.decode(files[2].source),
178
+ tileflowIconPackageLimits.maxGeneratedFileBytes
179
+ )
180
+ );
181
+ for (const index of [oneIndex, twoIndex]) {
182
+ const names = Object.keys(index).sort(compareCodeUnits);
183
+ if (names.length !== manifest.iconNames.length || names.some((name, ordinal) => name !== manifest.iconNames[ordinal]))
184
+ throw new Error("Sprite index does not exactly match the manifest icon names");
185
+ }
186
+ for (const [index, dimensions, ratio] of [
187
+ [oneIndex, manifest.sprites.oneX, 1],
188
+ [twoIndex, manifest.sprites.twoX, 2]
189
+ ]) {
190
+ const rectangles = Object.values(index);
191
+ for (const [ordinal, rect] of rectangles.entries()) {
192
+ if (rect.pixelRatio !== ratio || rect.x + rect.width > dimensions.width || rect.y + rect.height > dimensions.height)
193
+ throw new Error("Sprite rectangle is outside the atlas or has the wrong density");
194
+ for (const other of rectangles.slice(0, ordinal)) {
195
+ 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)
196
+ throw new Error("Sprite rectangles must not overlap");
197
+ }
198
+ }
199
+ }
200
+ for (const name of manifest.iconNames) {
201
+ const one = oneIndex[name];
202
+ const two = twoIndex[name];
203
+ if (two.x !== one.x * 2 || two.y !== one.y * 2 || two.width !== one.width * 2 || two.height !== one.height * 2)
204
+ throw new Error("Sprite densities must have exactly doubled geometry");
205
+ }
206
+ const oneAtlas = await decodeAtlas(files[1].source, manifest.sprites.oneX);
207
+ const twoAtlas = await decodeAtlas(files[3].source, manifest.sprites.twoX);
208
+ const icons = [];
209
+ for (const [ordinal, id] of manifest.iconNames.entries()) {
210
+ const oneX = extractCell(oneAtlas, oneIndex[id]);
211
+ const twoX = extractCell(twoAtlas, twoIndex[id]);
212
+ const expected = manifest.renderedIcons[ordinal].pixelSha256;
213
+ if (await hashTileflowRenderedIconPixels({ ...oneX, pixelRatio: 1 }) !== expected.oneX || await hashTileflowRenderedIconPixels({ ...twoX, pixelRatio: 2 }) !== expected.twoX)
214
+ throw new Error(`Rendered pixels do not match the manifest for ${id}`);
215
+ icons.push({ id, oneX, twoX });
216
+ }
217
+ return { package: { contentHash, manifest, files }, icons };
218
+ } catch (cause) {
219
+ throw new TileflowIconSetError(
220
+ "ICON_PACKAGE_INTEGRITY",
221
+ "Generated icon artifact verification failed",
222
+ { cause }
223
+ );
224
+ }
225
+ }
226
+ async function decodeAtlas(bytes, dimensions) {
227
+ if ([137, 80, 78, 71, 13, 10, 26, 10].some((byte, index) => bytes[index] !== byte))
228
+ throw new Error("Expected PNG bytes");
229
+ const sharp = await loadSharp();
230
+ const image = sharp(bytes, {
231
+ failOn: "error",
232
+ limitInputPixels: tileflowIconPackageLimits.maxAtlasDimension ** 2
233
+ });
234
+ const metadata = await image.metadata();
235
+ if (metadata.format !== "png" || (metadata.pages ?? 1) !== 1 || metadata.width !== dimensions.width || metadata.height !== dimensions.height)
236
+ throw new Error("PNG geometry does not match the manifest");
237
+ const { data, info } = await image.ensureAlpha().raw().toBuffer({ resolveWithObject: true });
238
+ if (info.channels !== 4 || info.width !== dimensions.width || info.height !== dimensions.height || data.length !== dimensions.width * dimensions.height * 4)
239
+ throw new Error("Expected bounded RGBA atlas pixels");
240
+ return { ...dimensions, rgba: new Uint8Array(data) };
241
+ }
242
+ function extractCell(atlas, rect) {
243
+ const rgba = new Uint8Array(rect.width * rect.height * 4);
244
+ const rowBytes = rect.width * 4;
245
+ for (let row = 0; row < rect.height; row += 1) {
246
+ const start = ((rect.y + row) * atlas.width + rect.x) * 4;
247
+ rgba.set(atlas.rgba.subarray(start, start + rowBytes), row * rowBytes);
248
+ }
249
+ return { height: rect.height, width: rect.width, rgba };
250
+ }
251
+
252
+ // src/icon-cache.ts
253
+ function getTileflowIconCacheDirectory(options = {}) {
254
+ 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");
255
+ return join(
256
+ resolve(options.cacheRoot ?? process.env.TILEFLOW_ICON_CACHE_DIR ?? defaultRoot),
257
+ "tileflow",
258
+ "icons",
259
+ "v1"
260
+ );
261
+ }
262
+ async function loadTileflowIconSetArtifact(input, options = {}) {
263
+ const pin = tileflowIconSetPinSchema.parse(input);
264
+ if (await hashTileflowIconPackageManifest2(pin.manifest) !== pin.contentHash)
265
+ throw new TileflowIconSetError2("ICON_LOCK_INVALID", "Locked manifest hash mismatch");
266
+ let cacheFailure;
267
+ try {
268
+ return await readCached(pin, options);
269
+ } catch (cause) {
270
+ if (cause instanceof TileflowIconSetError2 && cause.code === "ICON_CACHE_UNSAFE") throw cause;
271
+ cacheFailure = cause;
272
+ }
273
+ if (options.offline) {
274
+ throw new TileflowIconSetError2(
275
+ isMissing(cacheFailure) ? "ICON_CACHE_MISS" : "ICON_PACKAGE_INTEGRITY",
276
+ "The exact verified icon artifact is unavailable offline; hydrate this pin explicitly",
277
+ { cause: cacheFailure }
278
+ );
279
+ }
280
+ const url = new URL(pin.spriteUrl);
281
+ const allowed = options.deliveryOrigins ?? ["https://api.tileflow.dev"];
282
+ if (!allowed.some((origin) => origin === url.origin && new URL(origin).href === `${origin}/`)) {
283
+ throw new TileflowIconSetError2(
284
+ "ICON_DOWNLOAD_FAILED",
285
+ "Icon artifact delivery origin is not trusted"
286
+ );
287
+ }
288
+ const files = [];
289
+ const signal = options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4);
290
+ try {
291
+ for (const expected of pin.manifest.files) {
292
+ signal.throwIfAborted();
293
+ const fileUrl = `${pin.spriteUrl}${expected.name.slice("sprite".length)}`;
294
+ const response = await (options.fetch ?? globalThis.fetch)(fileUrl, {
295
+ method: "GET",
296
+ credentials: "omit",
297
+ redirect: "error",
298
+ headers: { Accept: expected.contentType, "Accept-Encoding": "identity" },
299
+ signal
300
+ });
301
+ if (response.status !== 200 || response.redirected || response.url && response.url !== fileUrl || !response.body)
302
+ throw new Error("Unexpected artifact response");
303
+ const type = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
304
+ const length = response.headers.get("content-length");
305
+ const encoding = response.headers.get("content-encoding")?.trim().toLowerCase();
306
+ const isIdentityEncoding = encoding === void 0 || encoding === "" || encoding === "identity";
307
+ if (type !== expected.contentType || length !== null && (!/^\d+$/u.test(length) || isIdentityEncoding && Number(length) !== expected.byteLength)) {
308
+ await response.body.cancel();
309
+ throw new Error("Artifact response headers do not match the manifest");
310
+ }
311
+ const reader = response.body.getReader();
312
+ const source = new Uint8Array(expected.byteLength);
313
+ let offset = 0;
314
+ try {
315
+ while (true) {
316
+ const chunk = await reader.read();
317
+ if (chunk.done) break;
318
+ if (offset + chunk.value.byteLength > source.byteLength)
319
+ throw new Error("Artifact response exceeds its exact byte length");
320
+ source.set(chunk.value, offset);
321
+ offset += chunk.value.byteLength;
322
+ }
323
+ if (offset !== source.byteLength) throw new Error("Artifact response was truncated");
324
+ } catch (cause) {
325
+ await reader.cancel().catch(() => void 0);
326
+ throw cause;
327
+ } finally {
328
+ reader.releaseLock();
329
+ }
330
+ files.push({ fileName: expected.name, contentType: expected.contentType, source });
331
+ }
332
+ } catch (cause) {
333
+ throw new TileflowIconSetError2("ICON_DOWNLOAD_FAILED", "Exact icon artifact download failed", {
334
+ cause
335
+ });
336
+ }
337
+ return storeTileflowIconSetArtifact(
338
+ pin,
339
+ { contentHash: pin.contentHash, manifest: pin.manifest, files },
340
+ options
341
+ );
342
+ }
343
+ async function storeTileflowIconSetArtifact(input, artifact, options = {}) {
344
+ const pin = tileflowIconSetPinSchema.parse(input);
345
+ if (await hashTileflowIconPackageManifest2(pin.manifest) !== pin.contentHash)
346
+ throw new TileflowIconSetError2("ICON_LOCK_INVALID", "Locked manifest hash mismatch");
347
+ if (artifact.contentHash !== pin.contentHash)
348
+ throw new TileflowIconSetError2(
349
+ "ICON_PACKAGE_INTEGRITY",
350
+ "Artifact does not match the selected pin"
351
+ );
352
+ const verified = await verifyTileflowIconArtifact({
353
+ contentHash: pin.contentHash,
354
+ manifest: pin.manifest,
355
+ files: artifact.files
356
+ });
357
+ const root = await safeCacheRoot(options, true);
358
+ const target = join(root, pin.contentHash);
359
+ const temporary = await mkdtemp(join(root, `.incoming-${pin.contentHash}-`));
360
+ try {
361
+ for (const file of verified.package.files) {
362
+ const handle = await open(join(temporary, file.fileName), "wx", 384);
363
+ try {
364
+ await handle.writeFile(file.source);
365
+ await handle.sync();
366
+ } finally {
367
+ await handle.close();
368
+ }
369
+ }
370
+ for (let attempt = 0; attempt < 8; attempt += 1) {
371
+ try {
372
+ await rename(temporary, target);
373
+ return verified;
374
+ } catch (cause) {
375
+ if (!hasCode(cause, "EEXIST") && !hasCode(cause, "ENOTEMPTY") && !hasCode(cause, "EPERM"))
376
+ throw cause;
377
+ }
378
+ try {
379
+ return await readCached(pin, options);
380
+ } catch (cause) {
381
+ if (cause instanceof TileflowIconSetError2 && cause.code === "ICON_CACHE_UNSAFE")
382
+ throw cause;
383
+ }
384
+ try {
385
+ await assertDirectory(target);
386
+ const retired = await mkdtemp(join(root, `.retired-${pin.contentHash}-`));
387
+ try {
388
+ await rename(target, join(retired, "entry"));
389
+ } catch (cause) {
390
+ if (!isMissing(cause)) throw cause;
391
+ } finally {
392
+ await rm(retired, { recursive: true, force: true });
393
+ }
394
+ } catch (cause) {
395
+ if (!isMissing(cause)) throw cause;
396
+ }
397
+ }
398
+ throw new TileflowIconSetError2(
399
+ "ICON_CACHE_UNSAFE",
400
+ "Concurrent cache repairs did not converge; retry this exact pin"
401
+ );
402
+ } finally {
403
+ await rm(temporary, { recursive: true, force: true });
404
+ }
405
+ }
406
+ async function readCached(pin, options) {
407
+ const root = await safeCacheRoot(options, false);
408
+ const directory = join(root, pin.contentHash);
409
+ await assertDirectory(directory);
410
+ const entries = (await readdir(directory)).sort();
411
+ const expectedNames = pin.manifest.files.map((file) => file.name).sort();
412
+ if (entries.length !== 4 || entries.some((name, index) => name !== expectedNames[index]))
413
+ throw new TileflowIconSetError2(
414
+ "ICON_PACKAGE_INTEGRITY",
415
+ "Cache entry does not contain exactly four generated files"
416
+ );
417
+ const files = [];
418
+ for (const expected of pin.manifest.files) {
419
+ files.push({
420
+ fileName: expected.name,
421
+ contentType: expected.contentType,
422
+ source: await readSafeFile(join(directory, expected.name), expected.byteLength)
423
+ });
424
+ }
425
+ return verifyTileflowIconArtifact({ contentHash: pin.contentHash, manifest: pin.manifest, files });
426
+ }
427
+ async function safeCacheRoot(options, create) {
428
+ const root = getTileflowIconCacheDirectory(options);
429
+ const base = resolve(root, "..", "..", "..");
430
+ if (create) await mkdir(base, { recursive: true, mode: 448 });
431
+ await assertDirectory(base);
432
+ let current = await realpath(base);
433
+ for (const segment of ["tileflow", "icons", "v1"]) {
434
+ current = join(current, segment);
435
+ if (create)
436
+ await mkdir(current, { mode: 448 }).catch((cause) => {
437
+ if (!hasCode(cause, "EEXIST")) throw cause;
438
+ });
439
+ await assertDirectory(current);
440
+ }
441
+ return current;
442
+ }
443
+ async function assertDirectory(path) {
444
+ const entry = await lstat(path);
445
+ if (!entry.isDirectory() || entry.isSymbolicLink())
446
+ throw new TileflowIconSetError2(
447
+ "ICON_CACHE_UNSAFE",
448
+ "Icon cache paths must be directories, not symlinks"
449
+ );
450
+ }
451
+ async function readSafeFile(path, maximumBytes, exact = true) {
452
+ const entry = await lstat(path);
453
+ if (!entry.isFile() || entry.isSymbolicLink())
454
+ throw new TileflowIconSetError2(
455
+ "ICON_CACHE_UNSAFE",
456
+ "Icon files must be regular files, not symlinks"
457
+ );
458
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
459
+ try {
460
+ const opened = await handle.stat();
461
+ if (!opened.isFile() || opened.ino !== entry.ino || opened.dev !== entry.dev || opened.size > maximumBytes || exact && opened.size !== maximumBytes)
462
+ throw new TileflowIconSetError2(
463
+ "ICON_PACKAGE_INTEGRITY",
464
+ "Icon file size or identity changed"
465
+ );
466
+ const buffer = new Uint8Array(maximumBytes + 1);
467
+ let offset = 0;
468
+ while (offset < buffer.length) {
469
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, null);
470
+ if (bytesRead === 0) break;
471
+ offset += bytesRead;
472
+ }
473
+ if (offset > maximumBytes || exact && offset !== maximumBytes)
474
+ throw new TileflowIconSetError2("ICON_PACKAGE_INTEGRITY", "Icon file byte length changed");
475
+ return buffer.slice(0, offset);
476
+ } finally {
477
+ await handle.close();
478
+ }
479
+ }
480
+ function hasCode(error, code) {
481
+ return error !== null && typeof error === "object" && "code" in error && error.code === code;
482
+ }
483
+ function isMissing(error) {
484
+ return hasCode(error, "ENOENT");
485
+ }
486
+
487
+ // src/icon-lockfile.ts
488
+ import { lstat as lstat2, mkdir as mkdir2, mkdtemp as mkdtemp2, open as open2, rename as rename2, rm as rm2, rmdir } from "fs/promises";
489
+ import { join as join2, resolve as resolve2 } from "path";
490
+ import {
491
+ parseTileflowIconsLockfile,
492
+ serializeTileflowIconsLockfile,
493
+ TileflowIconSetError as TileflowIconSetError3,
494
+ tileflowIconsLockfileMaximumBytes,
495
+ tileflowIconsLockfileName
496
+ } from "@tileflow/core";
497
+ async function readTileflowIconsLockfile(baseDirectory, sources) {
498
+ const text = await readLockText(join2(resolve2(baseDirectory), tileflowIconsLockfileName));
499
+ if (text === null)
500
+ throw new TileflowIconSetError3(
501
+ "ICON_LOCK_MISSING",
502
+ "Missing tileflow.icons.lock.json; install exact icon set revisions explicitly"
503
+ );
504
+ return parseTileflowIconsLockfile(text, sources);
505
+ }
506
+ async function writeTileflowIconsLockfile(baseDirectory, input, expectedContents) {
507
+ const content = await serializeTileflowIconsLockfile(input);
508
+ const directory = resolve2(baseDirectory);
509
+ const destination = join2(directory, tileflowIconsLockfileName);
510
+ const guard = join2(directory, `.${tileflowIconsLockfileName}.writing`);
511
+ try {
512
+ await mkdir2(guard, { mode: 448 });
513
+ } catch (cause) {
514
+ throw new TileflowIconSetError3(
515
+ "ICON_LOCK_CONFLICT",
516
+ "Another icon lock writer is active; a stale .writing guard may be removed only after confirming its writer has exited",
517
+ { cause }
518
+ );
519
+ }
520
+ let temporary;
521
+ try {
522
+ const actual = await readLockText(destination);
523
+ if (actual !== expectedContents)
524
+ throw new TileflowIconSetError3(
525
+ "ICON_LOCK_CONFLICT",
526
+ "Icon lock changed since it was read; reload and retry without discarding other pins"
527
+ );
528
+ if (actual === content) return;
529
+ temporary = await mkdtemp2(join2(directory, ".tileflow-icons-lock-"));
530
+ const file = join2(temporary, "lock");
531
+ const handle = await open2(file, "wx", 384);
532
+ try {
533
+ await handle.writeFile(content, "utf8");
534
+ await handle.sync();
535
+ } finally {
536
+ await handle.close();
537
+ }
538
+ if (await readLockText(destination) !== actual)
539
+ throw new TileflowIconSetError3(
540
+ "ICON_LOCK_CONFLICT",
541
+ "Icon lock changed while preparing an update"
542
+ );
543
+ await rename2(file, destination);
544
+ } finally {
545
+ if (temporary) await rm2(temporary, { recursive: true, force: true });
546
+ await rmdir(guard);
547
+ }
548
+ }
549
+ async function readLockText(path) {
550
+ try {
551
+ const entry = await lstat2(path);
552
+ if (!entry.isFile() || entry.isSymbolicLink())
553
+ throw new TileflowIconSetError3(
554
+ "ICON_LOCK_INVALID",
555
+ "The icon lock must be a regular file, not a symlink"
556
+ );
557
+ return new TextDecoder("utf-8", { fatal: true }).decode(
558
+ await readSafeFile(path, tileflowIconsLockfileMaximumBytes, false)
559
+ );
560
+ } catch (cause) {
561
+ if (cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT")
562
+ return null;
563
+ throw cause;
564
+ }
565
+ }
566
+
567
+ // src/icon-composition.ts
568
+ async function composeTileflowIconSources(input, options) {
569
+ const sources = structuredClone(input);
570
+ const references = collectTileflowIconSetReferences(sources);
571
+ 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);
572
+ const contributors = new Array(sources.length);
573
+ const winners = /* @__PURE__ */ new Map();
574
+ const watchPaths = /* @__PURE__ */ new Set();
575
+ if (references.length > 0 && options.lock === void 0)
576
+ watchPaths.add(resolve3(options.baseDirectory ?? options.cwd, tileflowIconsLockfileName2));
577
+ let reused;
578
+ let pixelBytes = 0;
579
+ let localSourceBytes = 0;
580
+ for (let ordinal = sources.length - 1; ordinal >= 0; ordinal -= 1) {
581
+ const source = sources[ordinal];
582
+ if (typeof source === "object" && source.kind === "icon-set") {
583
+ const pin = lock.sets[source.reference];
584
+ const loaded = await loadTileflowIconSetArtifact(pin, options);
585
+ contributors[ordinal] = {
586
+ kind: "icon-set",
587
+ reference: source.reference,
588
+ teamId: pin.teamId,
589
+ setId: pin.setId,
590
+ versionId: pin.versionId,
591
+ version: pin.version,
592
+ packageId: pin.packageId,
593
+ contentHash: pin.contentHash,
594
+ iconIds: [...pin.manifest.iconNames]
595
+ };
596
+ if (sources.length === 1) reused = loaded.package;
597
+ for (const icon of loaded.icons) {
598
+ if (winners.has(icon.id)) continue;
599
+ const pixels = pin.manifest.renderedIcons.find(
600
+ (entry) => entry.name === icon.id
601
+ ).pixelSha256;
602
+ addWinner(ordinal, icon, {
603
+ kind: "rendered-icon",
604
+ id: icon.id,
605
+ width: icon.oneX.width,
606
+ height: icon.oneX.height,
607
+ pixelSha256: { ...pixels }
608
+ });
609
+ }
610
+ } else {
611
+ const directory = await readTileflowIconDirectory(source, {
612
+ cwd: options.cwd,
613
+ baseDirectory: options.baseDirectory ?? options.cwd,
614
+ target: options.target ?? "local"
615
+ });
616
+ if (directory.watchPath) watchPaths.add(directory.watchPath);
617
+ contributors[ordinal] = typeof source === "string" ? { kind: "local", iconIds: directory.iconIds } : { kind: "package", package: source.package, iconIds: directory.iconIds };
618
+ const selected = directory.iconIds.filter((id) => !winners.has(id));
619
+ if (winners.size + selected.length > tileflowIconPackageLimits2.maxIconCount)
620
+ throw new TileflowIconSetError4(
621
+ "ICON_COMPOSITION_INVALID",
622
+ "Composed icon set exceeds 256 effective icons"
623
+ );
624
+ for (const item of await directory.render(selected)) {
625
+ localSourceBytes += item.sourceBytes;
626
+ if (localSourceBytes > tileflowIconPackageLimits2.maxSourceBytes)
627
+ throw new TileflowIconSetError4(
628
+ "ICON_COMPOSITION_INVALID",
629
+ "Effective local icon originals exceed the source-byte limit"
630
+ );
631
+ addWinner(ordinal, item.icon, item.identity);
632
+ }
633
+ }
634
+ }
635
+ const sorted = [...winners.values()].sort(
636
+ (left, right) => compareCodeUnits2(left.icon.id, right.icon.id)
637
+ );
638
+ const iconPackage = sorted.length ? reused ?? await packTileflowRenderedIcons(sorted.map((winner) => winner.icon)) : null;
639
+ const composition = references.length && iconPackage ? parseTileflowIconComposition({
640
+ format: "tileflow-icon-composition-v1",
641
+ compositionVersion: 1,
642
+ packageHash: iconPackage.contentHash,
643
+ contributors,
644
+ winners: sorted.map(({ ordinal, icon }, index) => ({
645
+ id: icon.id,
646
+ contributor: ordinal,
647
+ width: icon.oneX.width,
648
+ height: icon.oneX.height,
649
+ pixelSha256: iconPackage.manifest.renderedIcons[index].pixelSha256
650
+ }))
651
+ }) : null;
652
+ const previous = /* @__PURE__ */ new Map();
653
+ const replacements = [];
654
+ for (const [ordinal, contributor] of contributors.entries()) {
655
+ for (const id of contributor.iconIds) {
656
+ const replaced = previous.get(id);
657
+ if (replaced !== void 0) replacements.push({ id, replaced, winner: ordinal });
658
+ previous.set(id, ordinal);
659
+ }
660
+ }
661
+ return {
662
+ package: iconPackage,
663
+ composition,
664
+ sourceIdentities: sorted.map((winner) => winner.identity),
665
+ replacements,
666
+ watchPaths: [...watchPaths].sort(compareCodeUnits2)
667
+ };
668
+ function addWinner(ordinal, icon, identity) {
669
+ pixelBytes += icon.oneX.rgba.byteLength + icon.twoX.rgba.byteLength;
670
+ const maximumPixelBytes = (tileflowIconPackageLimits2.maxAtlasDimension ** 2 + (tileflowIconPackageLimits2.maxAtlasDimension / 2) ** 2) * 4;
671
+ if (winners.size >= tileflowIconPackageLimits2.maxIconCount || pixelBytes > maximumPixelBytes)
672
+ throw new TileflowIconSetError4(
673
+ "ICON_COMPOSITION_INVALID",
674
+ "Composed rendered cells exceed the final sprite capacity"
675
+ );
676
+ winners.set(icon.id, { ordinal, icon, identity });
677
+ }
678
+ }
105
679
  export {
106
680
  TileflowConfigInspectionError,
107
681
  TileflowFontCompilationError,
@@ -117,6 +691,7 @@ export {
117
691
  assertValidTileflowStyle,
118
692
  bindTileflowStyleFontBundle,
119
693
  compileTileflowIconPackages,
694
+ composeTileflowIconSources,
120
695
  createTileflowArtifactDiagnostics,
121
696
  createTileflowArtifactPlan,
122
697
  createTileflowArtifactSession,
@@ -141,6 +716,7 @@ export {
141
716
  getTileflowAssetBasePath,
142
717
  getTileflowAssetFileName,
143
718
  getTileflowFontWatchPaths,
719
+ getTileflowIconCacheDirectory,
144
720
  getTileflowIconWatchPaths,
145
721
  getTileflowMapNames,
146
722
  getTileflowRequestPath,
@@ -159,15 +735,19 @@ export {
159
735
  joinTileflowPublicUrl,
160
736
  loadTileflowConfig,
161
737
  loadTileflowConfigWithInputs,
738
+ loadTileflowIconSetArtifact,
162
739
  loadValidTileflowConfig,
163
740
  loadValidTileflowConfigWithInputs,
164
741
  normalizeTileflowBasePath,
165
742
  normalizeTileflowLandmarkManifest,
743
+ packTileflowRenderedIcons,
166
744
  prepareTileflowCatalogIcons,
167
745
  prepareTileflowLocalTilesets,
168
746
  prepareTileflowStyleFonts,
169
747
  readBoundedTileflowJsonResponse,
170
748
  readBoundedTileflowResponse,
749
+ readTileflowIconDirectory,
750
+ readTileflowIconsLockfile,
171
751
  refreshTileflowArtifactSession,
172
752
  renderTileflowPreviewHtml,
173
753
  replaceTileflowStyleFontSources,
@@ -176,13 +756,16 @@ export {
176
756
  sanitizeDiagnosticSecrets,
177
757
  sanitizeTileflowInspectableValue,
178
758
  serializeTileflowCommandDocument,
759
+ storeTileflowIconSetArtifact,
179
760
  tileflowArtifactInventoryFileName,
180
761
  tileflowArtifactPlanSchemaVersion,
181
762
  tileflowArtifactSessionSchemaVersion,
182
763
  tileflowCommandDocumentSchemaVersion,
183
764
  tileflowComparisonSchemaVersion,
184
765
  validateTileflowStyle,
766
+ verifyTileflowIconArtifact,
185
767
  writeTileflowArtifactPlan,
186
768
  writeTileflowBuildArtifacts,
769
+ writeTileflowIconsLockfile,
187
770
  writeTileflowNodeResponse
188
771
  };