@tileflow/dev 0.1.0-alpha.17 → 0.1.0-alpha.19

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,1210 @@
1
+ import {
2
+ getTileflowFontWatchPaths,
3
+ prepareTileflowStyleFonts,
4
+ replaceTileflowStyleFontSources
5
+ } from "./chunk-VEB43KDS.js";
6
+ import {
7
+ getTileflowIconWatchPaths,
8
+ prepareTileflowCatalogIcons
9
+ } from "./chunk-BALG4DSG.js";
10
+ import {
11
+ TileflowStyleValidationError,
12
+ normalizeTileflowStyleValidationIssues,
13
+ sanitizeDiagnosticSecrets,
14
+ validateTileflowStyle
15
+ } from "./chunk-KDFTE4Q5.js";
16
+ import {
17
+ defaultTileflowConfigPath,
18
+ getTileflowMapNames,
19
+ loadValidTileflowConfigWithInputs
20
+ } from "./chunk-BKHIZ7H2.js";
21
+ import {
22
+ isPathWithin
23
+ } from "./chunk-D4OE4ITL.js";
24
+
25
+ // src/artifacts.ts
26
+ import { createHash } from "crypto";
27
+ import { realpathSync as realpathSync2 } from "fs";
28
+ import {
29
+ copyFile,
30
+ lstat,
31
+ mkdir,
32
+ mkdtemp,
33
+ open,
34
+ readFile,
35
+ realpath,
36
+ rename,
37
+ rm,
38
+ unlink
39
+ } from "fs/promises";
40
+ import { createRequire } from "module";
41
+ import { dirname as dirname2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
42
+ import {
43
+ compareCodeUnits,
44
+ getTileflowStyleFontFaces,
45
+ serializeCanonicalJson,
46
+ tileflowMapIdSchema,
47
+ tileflowThemeNameSchema
48
+ } from "@tileflow/core";
49
+ import {
50
+ collectTileflowMapBuildLineage,
51
+ createManifest,
52
+ createStyleFromCatalog,
53
+ createStylesFromCatalog,
54
+ createStylesFromCatalogWithInspection,
55
+ createTileflowMapBuildManifest,
56
+ tileflowMapBuildManifestFileName
57
+ } from "@tileflow/core/build";
58
+ import { parseTileflowRuntimeManifest } from "@tileflow/core/manifest";
59
+
60
+ // src/session.ts
61
+ import { watch } from "chokidar";
62
+ import { realpathSync } from "fs";
63
+ import { dirname, extname, isAbsolute, relative, resolve, sep, win32 } from "path";
64
+ var tileflowArtifactSessionSchemaVersion = 1;
65
+ var configExtensions = /* @__PURE__ */ new Set([".cjs", ".cts", ".js", ".json", ".mjs", ".mts", ".ts", ".tsx"]);
66
+ var iconExtensions = /* @__PURE__ */ new Set([".jpeg", ".jpg", ".png", ".svg", ".webp"]);
67
+ var fontExtensions = /* @__PURE__ */ new Set([".otf", ".ttf", ".woff2"]);
68
+ var excludedDirectories = /* @__PURE__ */ new Set([
69
+ ".cache",
70
+ ".git",
71
+ ".hg",
72
+ ".next",
73
+ ".nuxt",
74
+ ".svn",
75
+ ".svelte-kit",
76
+ ".tileflow",
77
+ ".turbo",
78
+ "build",
79
+ "coverage",
80
+ "dist",
81
+ "node_modules"
82
+ ]);
83
+ async function createTileflowArtifactSessionWithBuilder(options, buildArtifacts, discoverWatchPaths) {
84
+ const session = new TileflowArtifactSessionImpl(options, buildArtifacts, discoverWatchPaths);
85
+ await session.start();
86
+ return session;
87
+ }
88
+ var TileflowArtifactSessionImpl = class {
89
+ #buildArtifacts;
90
+ #buildOptions;
91
+ #configPath;
92
+ #cwd;
93
+ #debounceMs;
94
+ #discoverWatchPaths;
95
+ #listeners = /* @__PURE__ */ new Set();
96
+ #ignoredPaths;
97
+ #watchEnabled;
98
+ #closed = false;
99
+ #debounceTimer;
100
+ #generation = 0;
101
+ #lastGoodArtifacts;
102
+ #lastGoodGeneration;
103
+ #refreshToken = 0;
104
+ #state = { generation: 0, status: "building" };
105
+ #watcher;
106
+ #watchedArtifactPaths = /* @__PURE__ */ new Set();
107
+ constructor(options, buildArtifacts, discoverWatchPaths) {
108
+ this.#cwd = resolve(options.cwd ?? process.cwd());
109
+ this.#configPath = resolve(this.#cwd, options.config ?? "tileflow.config.ts");
110
+ this.#debounceMs = Math.max(0, Math.min(options.debounceMs ?? 75, 1e3));
111
+ this.#watchEnabled = options.watch ?? false;
112
+ this.#ignoredPaths = (options.ignoredPaths ?? []).map((path) => resolve(this.#cwd, path));
113
+ this.#buildArtifacts = buildArtifacts;
114
+ this.#discoverWatchPaths = discoverWatchPaths;
115
+ this.#buildOptions = {
116
+ apiBaseUrl: options.apiBaseUrl,
117
+ assetBaseUrl: options.assetBaseUrl,
118
+ config: options.config,
119
+ cwd: this.#cwd,
120
+ inspection: options.inspection,
121
+ styleBaseUrl: options.styleBaseUrl
122
+ };
123
+ }
124
+ async start() {
125
+ if (this.#watchEnabled) {
126
+ this.#watcher = watch(dirname(this.#configPath), {
127
+ awaitWriteFinish: { pollInterval: 20, stabilityThreshold: 50 },
128
+ followSymlinks: false,
129
+ ignoreInitial: true,
130
+ ignored: (path, stats) => this.#ignoreWatchPath(path, stats?.isDirectory()),
131
+ usePolling: process.platform === "win32"
132
+ });
133
+ const watcherReady = new Promise((resolveReady, rejectReady) => {
134
+ const watcher = this.#watcher;
135
+ const cleanup = () => {
136
+ watcher.off("ready", onReady);
137
+ watcher.off("error", onError);
138
+ };
139
+ const onReady = () => {
140
+ cleanup();
141
+ resolveReady();
142
+ };
143
+ const onError = (error) => {
144
+ cleanup();
145
+ rejectReady(error);
146
+ };
147
+ watcher.once("ready", onReady);
148
+ watcher.once("error", onError);
149
+ });
150
+ this.#watcher.on("all", (event, path) => {
151
+ if (event === "addDir" || event === "unlinkDir") return;
152
+ if (!this.#shouldRefreshForWatchEvent(path)) return;
153
+ this.#scheduleRefresh(path);
154
+ });
155
+ this.#watcher.on("error", (error) => {
156
+ if (this.#closed) return;
157
+ this.#refreshToken += 1;
158
+ this.#publishInvalid(this.#generation + 1, error);
159
+ });
160
+ await watcherReady;
161
+ }
162
+ await this.refresh("initial");
163
+ }
164
+ getState() {
165
+ return this.#state;
166
+ }
167
+ getLastGoodArtifacts() {
168
+ return this.#lastGoodArtifacts;
169
+ }
170
+ subscribe(listener) {
171
+ if (this.#closed) return () => void 0;
172
+ this.#listeners.add(listener);
173
+ return () => this.#listeners.delete(listener);
174
+ }
175
+ async refresh(_reason = "manual") {
176
+ if (this.#closed) return;
177
+ const generation = ++this.#generation;
178
+ const token = ++this.#refreshToken;
179
+ this.#publish({
180
+ generation,
181
+ status: "building",
182
+ ...this.#lastGoodGeneration === void 0 ? {} : { lastGoodGeneration: this.#lastGoodGeneration }
183
+ });
184
+ try {
185
+ const artifacts = await this.#buildArtifacts(this.#buildOptions);
186
+ if (this.#closed || token !== this.#refreshToken) return;
187
+ this.#lastGoodArtifacts = artifacts;
188
+ this.#lastGoodGeneration = generation;
189
+ this.#updateArtifactWatchPaths(artifacts.watchPaths);
190
+ this.#publish({
191
+ artifacts,
192
+ generation,
193
+ lastGoodGeneration: generation,
194
+ status: "ready"
195
+ });
196
+ } catch (error) {
197
+ if (this.#closed || token !== this.#refreshToken) return;
198
+ if (this.#watcher && this.#discoverWatchPaths) {
199
+ try {
200
+ const watchPaths = await this.#discoverWatchPaths();
201
+ if (this.#closed || token !== this.#refreshToken) return;
202
+ this.#updateArtifactWatchPaths(watchPaths);
203
+ } catch {
204
+ }
205
+ }
206
+ this.#publishInvalid(generation, error);
207
+ }
208
+ }
209
+ async close() {
210
+ if (this.#closed) return;
211
+ this.#closed = true;
212
+ this.#refreshToken += 1;
213
+ if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
214
+ this.#listeners.clear();
215
+ await this.#watcher?.close();
216
+ }
217
+ #ignoreWatchPath(path, isDirectory) {
218
+ const resolvedPath = resolve(path);
219
+ const relativePath = relative(dirname(this.#configPath), resolvedPath);
220
+ const segments = relativePath.split(sep).filter(Boolean);
221
+ if (this.#ignoredPaths.some((path2) => isSameOrInside(path2, resolvedPath))) return true;
222
+ if (segments.some((segment) => excludedDirectories.has(segment))) return true;
223
+ if (isDirectory !== false) return false;
224
+ if (resolvedPath === this.#configPath || this.#watchedArtifactPaths.has(resolvedPath))
225
+ return false;
226
+ const extension = extname(resolvedPath).toLowerCase();
227
+ if (configExtensions.has(extension)) return false;
228
+ if (iconExtensions.has(extension) || fontExtensions.has(extension)) return false;
229
+ return true;
230
+ }
231
+ #shouldRefreshForWatchEvent(path) {
232
+ const resolvedPath = resolve(path);
233
+ const extension = extname(resolvedPath).toLowerCase();
234
+ if (!iconExtensions.has(extension) && !fontExtensions.has(extension)) return true;
235
+ return [...this.#watchedArtifactPaths].some((path2) => isSameOrInside(path2, resolvedPath));
236
+ }
237
+ #publish(state) {
238
+ this.#state = state;
239
+ for (const listener of this.#listeners) listener(state);
240
+ }
241
+ #publishInvalid(generation, error) {
242
+ this.#generation = Math.max(this.#generation, generation);
243
+ this.#publish({
244
+ diagnostics: createTileflowArtifactDiagnostics(error, this.#cwd),
245
+ generation,
246
+ status: "invalid",
247
+ ...this.#lastGoodGeneration === void 0 ? {} : { lastGoodGeneration: this.#lastGoodGeneration }
248
+ });
249
+ }
250
+ #scheduleRefresh(_path) {
251
+ if (this.#closed) return;
252
+ if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
253
+ this.#debounceTimer = setTimeout(() => {
254
+ this.#debounceTimer = void 0;
255
+ void this.refresh("filesystem");
256
+ }, this.#debounceMs);
257
+ }
258
+ #updateArtifactWatchPaths(paths) {
259
+ if (!this.#watcher) return;
260
+ const next = new Set(paths.map((path) => resolve(this.#cwd, path)));
261
+ const previous = this.#watchedArtifactPaths;
262
+ this.#watchedArtifactPaths = next;
263
+ for (const path of previous) {
264
+ if (!next.has(path) && !isSameOrInside(dirname(this.#configPath), path)) {
265
+ void this.#watcher.unwatch(path);
266
+ }
267
+ }
268
+ for (const path of next) {
269
+ if (!previous.has(path) && !isSameOrInside(dirname(this.#configPath), path)) {
270
+ this.#watcher.add(path);
271
+ }
272
+ }
273
+ }
274
+ };
275
+ function isSameOrInside(root, candidate) {
276
+ const path = relative(resolve(root), resolve(candidate));
277
+ return path === "" || !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
278
+ }
279
+ function createTileflowArtifactDiagnostics(error, cwd) {
280
+ const inheritedCode = optionalDiagnosticField(error, "code");
281
+ const inheritedPhase = optionalDiagnosticField(error, "phase") ?? optionalNestedDiagnosticField(error, "phase");
282
+ const issueList = getIssueList(error);
283
+ if (issueList.length > 0) {
284
+ return normalizeDiagnostics(
285
+ issueList.map((issue) => ({
286
+ ...issue.code ?? inheritedCode ? { code: issue.code ?? inheritedCode } : {},
287
+ message: sanitizeMessage(issue.message, cwd),
288
+ path: sanitizePath(issue.path, cwd),
289
+ ...issue.phase ?? inheritedPhase ? { phase: issue.phase ?? inheritedPhase } : {}
290
+ }))
291
+ );
292
+ }
293
+ return normalizeDiagnostics([
294
+ {
295
+ ...inheritedCode ? { code: inheritedCode } : {},
296
+ message: sanitizeMessage(error instanceof Error ? error.message : "Unknown build error", cwd),
297
+ path: "",
298
+ ...inheritedPhase ? { phase: inheritedPhase } : {}
299
+ }
300
+ ]);
301
+ }
302
+ function getIssueList(error) {
303
+ if (!error || typeof error !== "object") return [];
304
+ const containers = [];
305
+ for (const key of ["messages", "issues"]) {
306
+ const value = error[key];
307
+ if (Array.isArray(value)) containers.push(value);
308
+ }
309
+ const details = error.details;
310
+ if (details && typeof details === "object") {
311
+ const diagnostics = details.diagnostics;
312
+ if (Array.isArray(diagnostics)) containers.push(diagnostics);
313
+ }
314
+ return containers.flatMap(
315
+ (value) => value.flatMap((entry) => {
316
+ if (!entry || typeof entry !== "object") return [];
317
+ const message = entry.message;
318
+ const path = entry.path;
319
+ const code = optionalDiagnosticField(entry, "code");
320
+ const phase = optionalDiagnosticField(entry, "phase");
321
+ return typeof message === "string" ? [
322
+ {
323
+ ...code ? { code } : {},
324
+ message,
325
+ path: typeof path === "string" ? path : "",
326
+ ...phase ? { phase } : {}
327
+ }
328
+ ] : [];
329
+ })
330
+ );
331
+ }
332
+ function sanitizeMessage(value, cwd) {
333
+ let sanitized = value;
334
+ for (const root of localPathAliases(cwd)) sanitized = sanitized.replaceAll(root, ".");
335
+ sanitized = sanitized.replace(/[\r\n]+/g, " ").trim();
336
+ sanitized = sanitized.replace(/https?:\/\/[^\s'")<>]+/gi, (url) => {
337
+ try {
338
+ return new URL(url.replace(/[),.;]+$/, "")).origin;
339
+ } catch {
340
+ return "(resource URL)";
341
+ }
342
+ });
343
+ sanitized = sanitized.replace(/(^|[\s'"(])[A-Za-z]:[\\/][^\s'")]+/g, "$1(external path)");
344
+ sanitized = sanitized.replace(/(^|[\s'"(])(?:\\\\|\/\/)[^\s'")]+/g, "$1(external path)");
345
+ sanitized = sanitized.replace(/(^|[\s'"(])\/[^\s'")]+/g, "$1(external path)");
346
+ return sanitizeDiagnosticSecrets(sanitized);
347
+ }
348
+ function sanitizePath(value, cwd) {
349
+ if (!value) return "";
350
+ if (win32.isAbsolute(value) && !isAbsolute(value)) return "(external)";
351
+ if (!isAbsolute(value)) return value.replaceAll("\\", "/");
352
+ const local = relative(canonicalPath(cwd), canonicalPath(value)).replaceAll(sep, "/");
353
+ return local.startsWith("../") || local === ".." ? "(external)" : local || ".";
354
+ }
355
+ function localPathAliases(path) {
356
+ return [.../* @__PURE__ */ new Set([resolve(path), canonicalPath(path)])].sort(
357
+ (left, right) => right.length - left.length
358
+ );
359
+ }
360
+ function canonicalPath(path) {
361
+ try {
362
+ return realpathSync.native(resolve(path));
363
+ } catch {
364
+ return resolve(path);
365
+ }
366
+ }
367
+ function normalizeDiagnostics(diagnostics) {
368
+ const bounded = diagnostics.map((diagnostic) => ({
369
+ ...diagnostic.code ? { code: boundDiagnosticField(diagnostic.code) } : {},
370
+ message: boundDiagnosticText(diagnostic.message, 300),
371
+ path: boundDiagnosticText(diagnostic.path, 300),
372
+ ...diagnostic.phase ? { phase: boundDiagnosticField(diagnostic.phase) } : {}
373
+ }));
374
+ bounded.sort((left, right) => {
375
+ const leftKey = `${left.path}\0${left.message}\0${left.code ?? ""}\0${left.phase ?? ""}`;
376
+ const rightKey = `${right.path}\0${right.message}\0${right.code ?? ""}\0${right.phase ?? ""}`;
377
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
378
+ });
379
+ return bounded.filter(
380
+ (diagnostic, index) => index === 0 || JSON.stringify(diagnostic) !== JSON.stringify(bounded[index - 1])
381
+ ).slice(0, 32);
382
+ }
383
+ function optionalDiagnosticField(value, key) {
384
+ if (!value || typeof value !== "object") return void 0;
385
+ const field = value[key];
386
+ return typeof field === "string" && field.trim() ? boundDiagnosticField(field) : void 0;
387
+ }
388
+ function optionalNestedDiagnosticField(value, key) {
389
+ if (!value || typeof value !== "object") return void 0;
390
+ return optionalDiagnosticField(value.details, key);
391
+ }
392
+ function boundDiagnosticField(value) {
393
+ return boundDiagnosticText(value.replace(/[^A-Za-z0-9_-]/g, ""), 64);
394
+ }
395
+ function boundDiagnosticText(value, limit) {
396
+ return value.length <= limit ? value : `${value.slice(0, limit - 1)}\u2026`;
397
+ }
398
+
399
+ // src/artifacts.ts
400
+ var tileflowArtifactPlanSchemaVersion = 1;
401
+ var tileflowArtifactInventoryFileName = ".tileflow-artifacts.json";
402
+ var TileflowHostedManifestOverwriteError = class extends Error {
403
+ code = "HOSTED_MANIFEST_OVERWRITE_REFUSED";
404
+ constructor(path) {
405
+ super(
406
+ `Refusing to overwrite Hosted manifest at ${path}. Remove it, choose another output directory, or explicitly set overwriteHostedManifest: true.`
407
+ );
408
+ this.name = "TileflowHostedManifestOverwriteError";
409
+ }
410
+ };
411
+ function createTileflowManifest(project, options = {}) {
412
+ return createManifest(project, options);
413
+ }
414
+ function createTileflowStyle(project, mapName, options = {}) {
415
+ const style = createStyleFromCatalog(project, mapName, options);
416
+ assertValidTileflowStyle(style, mapName);
417
+ return style;
418
+ }
419
+ function createTileflowStyles(project, options = {}) {
420
+ const { mapAssets, ...styleOptions } = options;
421
+ const styles = createStylesFromCatalog(project, {
422
+ ...styleOptions,
423
+ mapAssets
424
+ });
425
+ assertValidTileflowStyles(project, styles);
426
+ return styles;
427
+ }
428
+ function createTileflowStylesWithInspection(project, options = {}) {
429
+ const { mapAssets, ...styleOptions } = options;
430
+ const inspected = createStylesFromCatalogWithInspection(project, {
431
+ ...styleOptions,
432
+ mapAssets
433
+ });
434
+ const styles = Object.fromEntries(
435
+ Object.entries(inspected).map(([mapName, family]) => [
436
+ mapName,
437
+ Object.fromEntries(
438
+ Object.entries(family).map(([themeName, result]) => [themeName, result.style])
439
+ )
440
+ ])
441
+ );
442
+ const inspections = Object.fromEntries(
443
+ Object.entries(inspected).map(([mapName, family]) => [
444
+ mapName,
445
+ Object.fromEntries(
446
+ Object.entries(family).map(([themeName, result]) => [themeName, result.inspection])
447
+ )
448
+ ])
449
+ );
450
+ assertValidTileflowStyles(project, styles);
451
+ return { inspections, styles };
452
+ }
453
+ function assertValidTileflowStyles(project, styles) {
454
+ const issues = [];
455
+ for (const mapName of getTileflowMapNames(project).sort(compareCodeUnits)) {
456
+ const family = styles[mapName];
457
+ if (!family) continue;
458
+ for (const themeName of Object.keys(family).sort(compareCodeUnits)) {
459
+ const style = family[themeName];
460
+ issues.push(...validateTileflowStyle(style, `${mapName}/${themeName}`));
461
+ }
462
+ }
463
+ if (issues.length > 0) {
464
+ throw new TileflowStyleValidationError(normalizeTileflowStyleValidationIssues(issues));
465
+ }
466
+ }
467
+ async function createTileflowArtifactPlan(prepared, options = {}) {
468
+ const inspected = options.inspection ? createTileflowStylesWithInspection(prepared.project, {
469
+ apiBaseUrl: options.apiBaseUrl,
470
+ mapAssets: prepared.mapAssets
471
+ }) : void 0;
472
+ const compiledStyles = inspected?.styles ?? createTileflowStyles(prepared.project, {
473
+ apiBaseUrl: options.apiBaseUrl,
474
+ mapAssets: prepared.mapAssets
475
+ });
476
+ const preparedFonts = await prepareTileflowStyleFonts(prepared.project, compiledStyles, {
477
+ assetBaseUrl: resolveAssetBaseUrl(options),
478
+ baseDirectory: prepared.baseDirectory,
479
+ cwd: prepared.cwd,
480
+ target: "local"
481
+ });
482
+ const styles = preparedFonts.styles;
483
+ const assets = [...prepared.assets, ...preparedFonts.assets];
484
+ const provenance = await createTileflowBuildProvenance(prepared.cwd);
485
+ const buildManifest = await createTileflowMapBuildManifest(
486
+ Object.fromEntries(
487
+ Object.keys(prepared.project.maps).sort(compareCodeUnits).map((mapName) => {
488
+ const map = prepared.project.maps[mapName];
489
+ const style = styles[mapName];
490
+ return [
491
+ mapName,
492
+ {
493
+ assets: getMapBuildAssets(
494
+ mapName,
495
+ assets,
496
+ preparedFonts.bundles[mapName]?.files ?? []
497
+ ),
498
+ lineage: prepared.project.mapMetadata?.[mapName]?.lineage ?? collectTileflowMapBuildLineage(map),
499
+ map,
500
+ sourceAssets: {
501
+ fonts: preparedFonts.sourceIdentities[mapName] ?? [],
502
+ icons: prepared.mapIconSources[mapName] ?? []
503
+ },
504
+ styles: style
505
+ }
506
+ ];
507
+ })
508
+ ),
509
+ { provenance }
510
+ );
511
+ const stableManifest = createFontAwareManifest(
512
+ createManifest(prepared.project, { styleBaseUrl: options.styleBaseUrl }),
513
+ styles
514
+ );
515
+ const inputs = {
516
+ directories: uniqueStrings(
517
+ [...prepared.watchPaths, ...preparedFonts.watchPaths].map(canonicalInputPath)
518
+ ),
519
+ files: uniqueStrings((options.inputFiles ?? []).map(canonicalInputPath))
520
+ };
521
+ const partial = {
522
+ assets,
523
+ buildManifest,
524
+ manifest: stableManifest,
525
+ project: prepared.project,
526
+ ...inspected ? { styleInspections: inspected.inspections } : {},
527
+ styles,
528
+ watchPaths: uniqueStrings([...inputs.files, ...inputs.directories])
529
+ };
530
+ const generation = hashArtifactGeneration(getStableTileflowArtifactFiles(partial));
531
+ const manifest = createGenerationManifest(stableManifest, generation, assets);
532
+ const generationArtifacts = { ...partial, manifest };
533
+ return {
534
+ ...generationArtifacts,
535
+ files: createGenerationArtifactFiles(generationArtifacts, generation),
536
+ inputs,
537
+ project: prepared.project,
538
+ schemaVersion: tileflowArtifactPlanSchemaVersion,
539
+ sourceProject: prepared.sourceProject
540
+ };
541
+ }
542
+ async function createTileflowBuildArtifacts(options = {}) {
543
+ const loaded = await loadValidTileflowConfigWithInputs(
544
+ options.config ?? defaultTileflowConfigPath,
545
+ {
546
+ cwd: options.cwd,
547
+ fresh: true
548
+ }
549
+ );
550
+ const prepared = await prepareTileflowCatalogIcons(loaded.project, {
551
+ assetBaseUrl: resolveAssetBaseUrl(options),
552
+ baseDirectory: dirname2(loaded.configFile),
553
+ cwd: options.cwd ?? process.cwd()
554
+ });
555
+ return await createTileflowArtifactPlan(prepared, { ...options, inputFiles: loaded.inputFiles });
556
+ }
557
+ function getTileflowArtifactFiles(artifacts) {
558
+ if ("files" in artifacts && Array.isArray(artifacts.files)) {
559
+ return validateArtifactFiles(artifacts.files);
560
+ }
561
+ return getStableTileflowArtifactFiles(artifacts);
562
+ }
563
+ function getStableTileflowArtifactFiles(artifacts) {
564
+ const files = [
565
+ {
566
+ contentType: "application/json; charset=utf-8",
567
+ fileName: tileflowMapBuildManifestFileName,
568
+ source: `${serializeCanonicalJson(artifacts.buildManifest)}
569
+ `
570
+ },
571
+ {
572
+ contentType: "application/json; charset=utf-8",
573
+ fileName: "manifest.json",
574
+ source: `${JSON.stringify(artifacts.manifest, null, 2)}
575
+ `
576
+ },
577
+ ...Object.entries(artifacts.styles).flatMap(
578
+ ([mapName, themes]) => Object.entries(themes).map(([themeName, style]) => ({
579
+ contentType: "application/json; charset=utf-8",
580
+ fileName: `styles/${mapName}/${themeName}.json`,
581
+ source: `${JSON.stringify(style, null, 2)}
582
+ `
583
+ }))
584
+ ),
585
+ ...artifacts.assets
586
+ ];
587
+ return validateArtifactFiles(files);
588
+ }
589
+ function validateArtifactFiles(files) {
590
+ const seen = /* @__PURE__ */ new Set();
591
+ for (const file of files) {
592
+ assertTileflowArtifactFileName(file.fileName);
593
+ if (file.fileName === tileflowArtifactInventoryFileName) {
594
+ throw new Error(`${file.fileName} is reserved for the Tileflow artifact inventory.`);
595
+ }
596
+ if (seen.has(file.fileName)) {
597
+ throw new Error(`Duplicate Tileflow artifact file: ${file.fileName}`);
598
+ }
599
+ seen.add(file.fileName);
600
+ }
601
+ return [...files].sort((left, right) => compareCodeUnits(left.fileName, right.fileName));
602
+ }
603
+ function createGenerationManifest(manifest, generation, assets) {
604
+ return {
605
+ ...manifest,
606
+ maps: Object.fromEntries(
607
+ Object.entries(manifest.maps).map(([mapName, map]) => [
608
+ mapName,
609
+ {
610
+ ...map,
611
+ themes: Object.fromEntries(
612
+ Object.entries(map.themes).map(([themeName, theme]) => [
613
+ themeName,
614
+ {
615
+ ...theme,
616
+ styleUrl: insertGenerationInPublicUrl(
617
+ theme.styleUrl,
618
+ `styles/${mapName}/${themeName}.json`,
619
+ generation
620
+ ),
621
+ ...theme.fontFaces ? {
622
+ fontFaces: theme.fontFaces.map((fontFace) => ({
623
+ ...fontFace,
624
+ source: retargetLocalFontAssetUrl(fontFace.source, assets, generation)
625
+ }))
626
+ } : {}
627
+ }
628
+ ])
629
+ )
630
+ }
631
+ ])
632
+ )
633
+ };
634
+ }
635
+ function createGenerationArtifactFiles(artifacts, generation) {
636
+ const prefix = `generations/${generation}`;
637
+ const stableFiles = getStableTileflowArtifactFiles(artifacts);
638
+ const immutableStyles = Object.entries(artifacts.styles).flatMap(
639
+ ([mapName, themes]) => Object.entries(themes).map(([themeName, style]) => {
640
+ const hasLocalSprite = artifacts.assets.some(
641
+ (asset) => asset.fileName.startsWith(`icons/${mapName}/`)
642
+ );
643
+ const sprite = style.sprite;
644
+ const generationStyleWithSprite = hasLocalSprite && typeof sprite === "string" ? {
645
+ ...style,
646
+ // Every theme in one logical map shares the same immutable sprite closure.
647
+ sprite: isRelativePublicUrl(sprite) ? sprite : insertGenerationInPublicUrl(sprite, `icons/${mapName}/sprite`, generation)
648
+ } : style;
649
+ const generationStyle = replaceTileflowStyleFontSources(
650
+ generationStyleWithSprite,
651
+ (source) => retargetLocalFontAssetUrl(source, artifacts.assets, generation)
652
+ );
653
+ return {
654
+ contentType: "application/json; charset=utf-8",
655
+ fileName: `${prefix}/styles/${mapName}/${themeName}.json`,
656
+ source: `${JSON.stringify(generationStyle, null, 2)}
657
+ `
658
+ };
659
+ })
660
+ );
661
+ const immutableAssets = artifacts.assets.map(
662
+ (asset) => ({ ...asset, fileName: `${prefix}/${asset.fileName}` })
663
+ );
664
+ return validateArtifactFiles([...stableFiles, ...immutableStyles, ...immutableAssets]);
665
+ }
666
+ function createFontAwareManifest(manifest, styles) {
667
+ const maps = Object.fromEntries(
668
+ Object.entries(manifest.maps).map(([mapName, map]) => [
669
+ mapName,
670
+ {
671
+ ...map,
672
+ themes: Object.fromEntries(
673
+ Object.entries(map.themes).map(([themeName, theme]) => {
674
+ const style = styles[mapName]?.[themeName];
675
+ if (!style) {
676
+ throw new Error(`Missing compiled Tileflow style for ${mapName}/${themeName}.`);
677
+ }
678
+ const definitions = getTileflowStyleFontFaces(style);
679
+ return [themeName, { ...theme, fontFaces: definitions }];
680
+ })
681
+ )
682
+ }
683
+ ])
684
+ );
685
+ return parseTileflowRuntimeManifest({ ...manifest, maps });
686
+ }
687
+ function retargetLocalFontAssetUrl(source, assets, generation) {
688
+ if (isRelativePublicUrl(source)) return source;
689
+ const asset = assets.find(
690
+ (candidate) => candidate.fileName.startsWith("fonts/") && (source === candidate.fileName || source.endsWith(`/${candidate.fileName}`))
691
+ );
692
+ if (!asset) throw new Error(`Unable to resolve Tileflow font artifact URL: ${source}`);
693
+ return insertGenerationInPublicUrl(source, asset.fileName, generation);
694
+ }
695
+ function getMapBuildAssets(mapName, assets, fontAssets) {
696
+ const selected = /* @__PURE__ */ new Map();
697
+ for (const asset of assets) {
698
+ if (asset.fileName.startsWith(`icons/${mapName}/`)) selected.set(asset.fileName, asset);
699
+ }
700
+ for (const asset of fontAssets) {
701
+ selected.set(asset.fileName, asset);
702
+ }
703
+ return [...selected.values()].sort(
704
+ (left, right) => compareCodeUnits(left.fileName, right.fileName)
705
+ );
706
+ }
707
+ function hashBytes(source) {
708
+ return createHash("sha256").update(typeof source === "string" ? Buffer.from(source, "utf8") : Buffer.from(source)).digest("hex");
709
+ }
710
+ async function createTileflowBuildProvenance(cwd) {
711
+ const projectDirectory = resolve2(cwd);
712
+ const projectRequire = createRequire(
713
+ resolve2(projectDirectory, "__tileflow_build_provenance__.cjs")
714
+ );
715
+ const toolRequire = createRequire(import.meta.url);
716
+ const packages = {};
717
+ packages["@tileflow/core"] = await readPackageVersion(
718
+ "@tileflow/core",
719
+ resolvePackageJson(projectRequire, "@tileflow/core") ?? resolvePackageJson(toolRequire, "@tileflow/core")
720
+ );
721
+ packages["@tileflow/dev"] = await readPackageVersion(
722
+ "@tileflow/dev",
723
+ new URL("../package.json", import.meta.url)
724
+ );
725
+ const mapsPackage = resolvePackageJson(projectRequire, "@tileflow/maps");
726
+ if (mapsPackage) {
727
+ packages["@tileflow/maps"] = await readPackageVersion("@tileflow/maps", mapsPackage);
728
+ }
729
+ const lockfile = await findNearestLockfile(projectDirectory);
730
+ return {
731
+ ...lockfile ? { lockfile: { format: lockfile.format, sha256: hashBytes(lockfile.source) } } : {},
732
+ packages,
733
+ schemaVersion: 1
734
+ };
735
+ }
736
+ function resolvePackageJson(require2, packageName) {
737
+ try {
738
+ return require2.resolve(`${packageName}/package.json`);
739
+ } catch {
740
+ return void 0;
741
+ }
742
+ }
743
+ async function readPackageVersion(expectedName, path) {
744
+ if (!path) throw new Error(`Unable to resolve build package provenance for ${expectedName}.`);
745
+ let parsed;
746
+ try {
747
+ parsed = JSON.parse(await readFile(path, "utf8"));
748
+ } catch {
749
+ throw new Error(`Unable to read build package provenance for ${expectedName}.`);
750
+ }
751
+ if (!parsed || typeof parsed !== "object" || parsed.name !== expectedName || typeof parsed.version !== "string") {
752
+ throw new Error(`Invalid build package provenance for ${expectedName}.`);
753
+ }
754
+ return parsed.version;
755
+ }
756
+ async function findNearestLockfile(cwd) {
757
+ const candidates = [
758
+ { fileName: "pnpm-lock.yaml", format: "pnpm" },
759
+ { fileName: "package-lock.json", format: "npm" },
760
+ { fileName: "yarn.lock", format: "yarn" },
761
+ { fileName: "bun.lock", format: "bun" },
762
+ { fileName: "bun.lockb", format: "bun" }
763
+ ];
764
+ let directory = cwd;
765
+ while (true) {
766
+ for (const candidate of candidates) {
767
+ try {
768
+ return {
769
+ format: candidate.format,
770
+ source: await readFile(resolve2(directory, candidate.fileName))
771
+ };
772
+ } catch (error) {
773
+ if (!hasErrorCode(error, "ENOENT")) throw error;
774
+ }
775
+ }
776
+ const parent = dirname2(directory);
777
+ if (parent === directory) return void 0;
778
+ directory = parent;
779
+ }
780
+ }
781
+ function insertGenerationInPublicUrl(url, logicalPath, generation) {
782
+ const marker = `/${logicalPath}`;
783
+ const index = url.lastIndexOf(marker);
784
+ if (index >= 0 && index + marker.length === url.length) {
785
+ return `${url.slice(0, index)}/generations/${generation}${marker}`;
786
+ }
787
+ if (url === logicalPath) return `generations/${generation}/${logicalPath}`;
788
+ throw new Error(`Unable to content-address Tileflow artifact URL: ${url}`);
789
+ }
790
+ function createTileflowArtifactSession(options = {}) {
791
+ return createTileflowArtifactSessionWithBuilder(
792
+ options,
793
+ createTileflowBuildArtifacts,
794
+ () => getTileflowWatchPaths(options)
795
+ );
796
+ }
797
+ async function getTileflowWatchPaths(options = {}) {
798
+ const cwd = options.cwd ?? process.cwd();
799
+ const loaded = await loadValidTileflowConfigWithInputs(
800
+ options.config ?? defaultTileflowConfigPath,
801
+ { cwd, fresh: true }
802
+ );
803
+ const [iconPaths, fontPaths] = await Promise.all([
804
+ getTileflowIconWatchPaths(loaded.project, cwd, dirname2(loaded.configFile)),
805
+ getTileflowFontWatchPaths(loaded.project, cwd, dirname2(loaded.configFile))
806
+ ]);
807
+ return uniqueStrings([...loaded.inputFiles, ...iconPaths, ...fontPaths]);
808
+ }
809
+ function isTileflowArtifactInputPath(inputs, path) {
810
+ const candidate = canonicalInputPath(path);
811
+ return inputs.files.some((input) => canonicalInputPath(input) === candidate) || inputs.directories.some((input) => isPathWithin(canonicalInputPath(input), candidate));
812
+ }
813
+ async function refreshTileflowArtifactSession(session, options) {
814
+ const inputs = getArtifactInputGraph(session.getLastGoodArtifacts());
815
+ if (options.inputPath && inputs && !isTileflowArtifactInputPath(inputs, options.inputPath)) {
816
+ return false;
817
+ }
818
+ await session.refresh(options.reason);
819
+ return true;
820
+ }
821
+ function getArtifactInputGraph(artifacts) {
822
+ if (!artifacts || !("inputs" in artifacts)) return void 0;
823
+ const inputs = artifacts.inputs;
824
+ return inputs && Array.isArray(inputs.files) && Array.isArray(inputs.directories) ? inputs : void 0;
825
+ }
826
+ async function writeTileflowBuildArtifacts(options) {
827
+ const artifacts = await createTileflowBuildArtifacts(options);
828
+ await writeTileflowArtifactPlan(artifacts, options);
829
+ return artifacts;
830
+ }
831
+ async function writeTileflowArtifactPlan(artifacts, options) {
832
+ const cwd = options.cwd ?? process.cwd();
833
+ const output = await prepareTileflowOutputDirectory(cwd, options.outDir);
834
+ const files = getTileflowArtifactFiles(artifacts);
835
+ await assertTileflowSelfHostedManifestTarget(resolve2(output.outDir, "manifest.json"), options);
836
+ await commitTileflowArtifactFiles(output, files);
837
+ }
838
+ async function assertTileflowSelfHostedManifestTarget(manifestPath, options = {}) {
839
+ if (options.overwriteHostedManifest) return;
840
+ let source;
841
+ try {
842
+ source = await readFile(resolve2(manifestPath), "utf8");
843
+ } catch (error) {
844
+ if (hasErrorCode(error, "ENOENT")) return;
845
+ throw error;
846
+ }
847
+ try {
848
+ const manifest = parseTileflowRuntimeManifest(JSON.parse(source));
849
+ if (isHostedRuntimeManifest(manifest)) {
850
+ throw new TileflowHostedManifestOverwriteError(resolve2(manifestPath));
851
+ }
852
+ } catch (error) {
853
+ if (error instanceof TileflowHostedManifestOverwriteError) throw error;
854
+ }
855
+ }
856
+ function isHostedRuntimeManifest(manifest) {
857
+ return Boolean(
858
+ manifest.apiUrl || Object.values(manifest.maps).some(
859
+ (map) => map.apiUrl || map.environment || map.mapId || map.usageMode || map.worldGeneration || Object.values(map.themes).some((theme) => theme.styleId)
860
+ )
861
+ );
862
+ }
863
+ function assertValidTileflowStyle(style, mapName) {
864
+ const issues = validateTileflowStyle(style, mapName);
865
+ if (issues.length > 0) {
866
+ throw new TileflowStyleValidationError(normalizeTileflowStyleValidationIssues(issues));
867
+ }
868
+ }
869
+ async function commitTileflowArtifactFiles(output, files) {
870
+ await assertTileflowOutputDirectoryIsSafe(output);
871
+ const { outDir } = output;
872
+ const previous = await readTileflowArtifactInventory(outDir);
873
+ const desiredNames = files.map((file) => file.fileName);
874
+ const generation = getArtifactGeneration(files);
875
+ const retainedGenerations = previous ? generation === previous.generation ? getManagedGenerations(previous.files) : [previous.generation] : [];
876
+ const retainedPrefixes = retainedGenerations.map((item) => `generations/${item}/`);
877
+ const retainedNames = previous ? previous.files.filter((name) => retainedPrefixes.some((prefix) => name.startsWith(prefix))) : [];
878
+ const inventoryNames = uniqueStrings([...desiredNames, ...retainedNames]);
879
+ const inventory = {
880
+ files: inventoryNames,
881
+ generation,
882
+ schemaVersion: 1
883
+ };
884
+ const inventoryFile = {
885
+ contentType: "application/json; charset=utf-8",
886
+ fileName: tileflowArtifactInventoryFileName,
887
+ source: `${JSON.stringify(inventory, null, 2)}
888
+ `
889
+ };
890
+ const stagedFiles = [...files, inventoryFile];
891
+ const staleNames = (previous?.files ?? []).filter((name) => !inventoryNames.includes(name));
892
+ const affectedNames = uniqueStrings([
893
+ ...desiredNames,
894
+ ...staleNames,
895
+ tileflowArtifactInventoryFileName
896
+ ]);
897
+ const stageDir = await mkdtemp(resolve2(outDir, ".tileflow-stage-"));
898
+ const newDir = resolve2(stageDir, "new");
899
+ const backupDir = resolve2(stageDir, "backup");
900
+ const backedUp = /* @__PURE__ */ new Set();
901
+ const installed = /* @__PURE__ */ new Set();
902
+ try {
903
+ for (const file of stagedFiles) {
904
+ const stagePath = managedPath(newDir, file.fileName);
905
+ await mkdir(dirname2(stagePath), { recursive: true });
906
+ await writeFileDurably(stagePath, file.source);
907
+ }
908
+ for (const name of affectedNames) {
909
+ await assertManagedTargetIsSafe(outDir, name);
910
+ const targetPath = managedPath(outDir, name);
911
+ if (!await isFile(targetPath)) continue;
912
+ const backupPath = managedPath(backupDir, name);
913
+ await mkdir(dirname2(backupPath), { recursive: true });
914
+ await copyFile(targetPath, backupPath);
915
+ backedUp.add(name);
916
+ }
917
+ const commitOrder = stagedFiles.map((file) => file.fileName).sort((left, right) => {
918
+ const leftRank = commitRank(left);
919
+ const rightRank = commitRank(right);
920
+ return leftRank - rightRank || compareCodeUnits(left, right);
921
+ });
922
+ for (const name of commitOrder.filter(
923
+ (candidate) => candidate !== tileflowArtifactInventoryFileName
924
+ )) {
925
+ const sourcePath = managedPath(newDir, name);
926
+ const targetPath = managedPath(outDir, name);
927
+ await mkdir(dirname2(targetPath), { recursive: true });
928
+ await rename(sourcePath, targetPath);
929
+ installed.add(name);
930
+ }
931
+ for (const name of staleNames) {
932
+ await removeManagedFile(managedPath(outDir, name));
933
+ }
934
+ const stagedInventoryPath = managedPath(newDir, tileflowArtifactInventoryFileName);
935
+ const inventoryPath = managedPath(outDir, tileflowArtifactInventoryFileName);
936
+ await rename(stagedInventoryPath, inventoryPath);
937
+ installed.add(tileflowArtifactInventoryFileName);
938
+ } catch (error) {
939
+ await rollbackManagedFiles(outDir, backupDir, affectedNames, backedUp, installed);
940
+ throw error;
941
+ } finally {
942
+ await rm(stageDir, { force: true, recursive: true });
943
+ }
944
+ }
945
+ function getArtifactGeneration(files) {
946
+ const generations = getManagedGenerations(files.map((file) => file.fileName));
947
+ if (generations.length !== 1) {
948
+ throw new Error("A Tileflow artifact plan must contain exactly one content generation.");
949
+ }
950
+ return generations[0];
951
+ }
952
+ function getManagedGenerations(fileNames) {
953
+ return uniqueStrings(
954
+ fileNames.flatMap((fileName) => {
955
+ const match = /^generations\/([a-f0-9]{64})\//.exec(fileName);
956
+ return match?.[1] ? [match[1]] : [];
957
+ })
958
+ );
959
+ }
960
+ function commitRank(fileName) {
961
+ if (fileName === tileflowArtifactInventoryFileName) return 2;
962
+ if (fileName === "manifest.json") return 1;
963
+ return 0;
964
+ }
965
+ async function rollbackManagedFiles(outDir, backupDir, affectedNames, backedUp, installed) {
966
+ const rollbackErrors = [];
967
+ for (const name of [...affectedNames].reverse()) {
968
+ try {
969
+ const targetPath = managedPath(outDir, name);
970
+ if (installed.has(name) && !backedUp.has(name)) {
971
+ await removeManagedFile(targetPath);
972
+ }
973
+ if (backedUp.has(name)) {
974
+ await mkdir(dirname2(targetPath), { recursive: true });
975
+ await copyFile(managedPath(backupDir, name), targetPath);
976
+ }
977
+ } catch (error) {
978
+ rollbackErrors.push(error);
979
+ }
980
+ }
981
+ if (rollbackErrors.length > 0) {
982
+ throw new AggregateError(rollbackErrors, "Unable to restore the previous Tileflow generation.");
983
+ }
984
+ }
985
+ async function readTileflowArtifactInventory(outDir) {
986
+ const inventoryPath = managedPath(outDir, tileflowArtifactInventoryFileName);
987
+ let source;
988
+ try {
989
+ source = await readFile(inventoryPath, "utf8");
990
+ } catch (error) {
991
+ if (hasErrorCode(error, "ENOENT")) return void 0;
992
+ throw error;
993
+ }
994
+ let candidate;
995
+ try {
996
+ candidate = JSON.parse(source);
997
+ } catch {
998
+ throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
999
+ }
1000
+ if (!candidate || typeof candidate !== "object" || candidate.schemaVersion !== 1 || typeof candidate.generation !== "string" || !/^[a-f0-9]{64}$/.test(candidate.generation) || !Array.isArray(candidate.files) || candidate.files.length > 1e4) {
1001
+ throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
1002
+ }
1003
+ const files = candidate.files;
1004
+ if (!files.every((name) => typeof name === "string")) {
1005
+ throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
1006
+ }
1007
+ try {
1008
+ for (const name of files) assertTileflowArtifactFileName(name);
1009
+ } catch {
1010
+ throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
1011
+ }
1012
+ if (new Set(files).size !== files.length || files.includes(tileflowArtifactInventoryFileName)) {
1013
+ throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
1014
+ }
1015
+ return {
1016
+ files: [...files],
1017
+ generation: candidate.generation,
1018
+ schemaVersion: 1
1019
+ };
1020
+ }
1021
+ async function assertManagedTargetIsSafe(outDir, fileName) {
1022
+ assertSafeManagedFileName(fileName);
1023
+ const rootStats = await lstat(outDir);
1024
+ if (rootStats.isSymbolicLink()) {
1025
+ throw new Error(`Tileflow refuses to write through a symbolic link: ${outDir}`);
1026
+ }
1027
+ if (!rootStats.isDirectory()) {
1028
+ throw new Error(`Tileflow artifact output is not a directory: ${outDir}`);
1029
+ }
1030
+ const segments = fileName.split("/");
1031
+ let current = outDir;
1032
+ for (let index = 0; index < segments.length; index += 1) {
1033
+ current = resolve2(current, segments[index]);
1034
+ try {
1035
+ const stats = await lstat(current);
1036
+ if (stats.isSymbolicLink()) {
1037
+ throw new Error(`Tileflow refuses to write through a symbolic link: ${fileName}`);
1038
+ }
1039
+ if (index < segments.length - 1 && !stats.isDirectory()) {
1040
+ throw new Error(`Tileflow artifact parent is not a directory: ${fileName}`);
1041
+ }
1042
+ if (index === segments.length - 1 && !stats.isFile()) {
1043
+ throw new Error(`Tileflow artifact target is not a file: ${fileName}`);
1044
+ }
1045
+ } catch (error) {
1046
+ if (hasErrorCode(error, "ENOENT")) return;
1047
+ throw error;
1048
+ }
1049
+ }
1050
+ }
1051
+ async function prepareTileflowOutputDirectory(cwd, requestedOutDir) {
1052
+ const logicalRoot = resolve2(cwd);
1053
+ const logicalOutDir = resolve2(logicalRoot, requestedOutDir);
1054
+ if (!isPathWithin(logicalRoot, logicalOutDir)) {
1055
+ throw new Error(`Tileflow artifact output escapes its working directory: ${logicalOutDir}`);
1056
+ }
1057
+ const rootStats = await lstat(logicalRoot);
1058
+ if (!rootStats.isDirectory()) {
1059
+ throw new Error(`Tileflow artifact working directory is not a directory: ${logicalRoot}`);
1060
+ }
1061
+ const rootDir = await realpath(logicalRoot);
1062
+ const relativeOutDir = relative2(logicalRoot, logicalOutDir);
1063
+ const segments = relativeOutDir ? relativeOutDir.split(sep2) : [];
1064
+ let current = rootDir;
1065
+ for (const segment of segments) {
1066
+ current = resolve2(current, segment);
1067
+ let stats;
1068
+ try {
1069
+ stats = await lstat(current);
1070
+ } catch (error) {
1071
+ if (!hasErrorCode(error, "ENOENT")) throw error;
1072
+ await mkdir(current);
1073
+ stats = await lstat(current);
1074
+ }
1075
+ if (stats.isSymbolicLink()) {
1076
+ throw new Error(`Tileflow refuses to write through a symbolic link: ${current}`);
1077
+ }
1078
+ if (!stats.isDirectory()) {
1079
+ throw new Error(`Tileflow artifact output ancestor is not a directory: ${current}`);
1080
+ }
1081
+ }
1082
+ const outDir = await realpath(current);
1083
+ const output = { outDir, rootDir };
1084
+ await assertTileflowOutputDirectoryIsSafe(output);
1085
+ return output;
1086
+ }
1087
+ async function assertTileflowOutputDirectoryIsSafe(output) {
1088
+ if (!isPathWithin(output.rootDir, output.outDir)) {
1089
+ throw new Error(`Tileflow artifact output escapes its working directory: ${output.outDir}`);
1090
+ }
1091
+ const stats = await lstat(output.outDir);
1092
+ if (stats.isSymbolicLink()) {
1093
+ throw new Error(`Tileflow refuses to write through a symbolic link: ${output.outDir}`);
1094
+ }
1095
+ if (!stats.isDirectory()) {
1096
+ throw new Error(`Tileflow artifact output is not a directory: ${output.outDir}`);
1097
+ }
1098
+ const canonicalOutDir = await realpath(output.outDir);
1099
+ if (canonicalOutDir !== output.outDir || !isPathWithin(output.rootDir, canonicalOutDir)) {
1100
+ throw new Error(`Tileflow artifact output escapes its working directory: ${output.outDir}`);
1101
+ }
1102
+ }
1103
+ function assertSafeManagedFileName(fileName) {
1104
+ if (!fileName || fileName.includes("\\") || fileName.includes("\0") || fileName.startsWith("/") || fileName.endsWith("/") || fileName.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
1105
+ throw new Error(`Unsafe Tileflow artifact file name: ${fileName}`);
1106
+ }
1107
+ }
1108
+ function assertTileflowArtifactFileName(fileName) {
1109
+ assertSafeManagedFileName(fileName);
1110
+ const logicalName = fileName.replace(/^generations\/[a-f0-9]{64}\//, "");
1111
+ const styleMatch = /^styles\/([^/]+)\/([^/]+)\.json$/u.exec(logicalName);
1112
+ const iconMatch = /^icons\/([^/]+)\/sprite(?:@2x)?\.(?:json|png)$/u.exec(logicalName);
1113
+ if (fileName === "manifest.json" || fileName === tileflowMapBuildManifestFileName || styleMatch && tileflowMapIdSchema.safeParse(styleMatch[1]).success && tileflowThemeNameSchema.safeParse(styleMatch[2]).success || iconMatch && tileflowMapIdSchema.safeParse(iconMatch[1]).success || /^fonts\/[a-z0-9]+(?:-[a-z0-9]+)*-[a-f0-9]{64}\.(?:otf|ttf|woff2)$/.test(logicalName) || /^fonts\/licenses\/license-[a-f0-9]{64}\.txt$/.test(logicalName)) {
1114
+ return;
1115
+ }
1116
+ throw new Error(`Unexpected Tileflow managed artifact file name: ${fileName}`);
1117
+ }
1118
+ function managedPath(root, fileName) {
1119
+ assertSafeManagedFileName(fileName);
1120
+ const path = resolve2(root, ...fileName.split("/"));
1121
+ if (!isPathWithin(root, path)) {
1122
+ throw new Error(`Unsafe Tileflow artifact file name: ${fileName}`);
1123
+ }
1124
+ return path;
1125
+ }
1126
+ async function writeFileDurably(path, source) {
1127
+ const handle = await open(path, "wx");
1128
+ try {
1129
+ await handle.writeFile(source);
1130
+ await handle.sync();
1131
+ } finally {
1132
+ await handle.close();
1133
+ }
1134
+ }
1135
+ async function isFile(path) {
1136
+ try {
1137
+ return (await lstat(path)).isFile();
1138
+ } catch (error) {
1139
+ if (hasErrorCode(error, "ENOENT")) return false;
1140
+ throw error;
1141
+ }
1142
+ }
1143
+ async function removeManagedFile(path) {
1144
+ try {
1145
+ const stats = await lstat(path);
1146
+ if (!stats.isFile()) throw new Error(`Refusing to remove non-file artifact target: ${path}`);
1147
+ await unlink(path);
1148
+ } catch (error) {
1149
+ if (!hasErrorCode(error, "ENOENT")) throw error;
1150
+ }
1151
+ }
1152
+ function hashArtifactGeneration(files) {
1153
+ const hash = createHash("sha256");
1154
+ for (const file of files) {
1155
+ const source = typeof file.source === "string" ? Buffer.from(file.source, "utf8") : Buffer.from(file.source);
1156
+ hash.update(`${file.fileName}\0${file.contentType}\0${source.byteLength}\0`, "utf8");
1157
+ hash.update(source);
1158
+ }
1159
+ return hash.digest("hex");
1160
+ }
1161
+ function resolveAssetBaseUrl(options) {
1162
+ if (options.assetBaseUrl !== void 0) return options.assetBaseUrl;
1163
+ if (options.styleBaseUrl && !isRelativePublicUrl(options.styleBaseUrl)) {
1164
+ return options.styleBaseUrl;
1165
+ }
1166
+ return "../..";
1167
+ }
1168
+ function isRelativePublicUrl(value) {
1169
+ return !(value.startsWith("http://") || value.startsWith("https://") || value.startsWith("/") || value.startsWith("data:"));
1170
+ }
1171
+ function uniqueStrings(values) {
1172
+ return [...new Set(values)].sort(compareCodeUnits);
1173
+ }
1174
+ function hasErrorCode(error, code) {
1175
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
1176
+ }
1177
+ function canonicalInputPath(path) {
1178
+ const resolved = resolve2(path);
1179
+ try {
1180
+ return realpathSync2.native(resolved);
1181
+ } catch {
1182
+ try {
1183
+ return resolve2(realpathSync2.native(dirname2(resolved)), resolved.split(/[\\/]/).at(-1));
1184
+ } catch {
1185
+ return resolved;
1186
+ }
1187
+ }
1188
+ }
1189
+
1190
+ export {
1191
+ tileflowArtifactSessionSchemaVersion,
1192
+ createTileflowArtifactDiagnostics,
1193
+ tileflowArtifactPlanSchemaVersion,
1194
+ tileflowArtifactInventoryFileName,
1195
+ TileflowHostedManifestOverwriteError,
1196
+ createTileflowManifest,
1197
+ createTileflowStyle,
1198
+ createTileflowStyles,
1199
+ createTileflowArtifactPlan,
1200
+ createTileflowBuildArtifacts,
1201
+ getTileflowArtifactFiles,
1202
+ createTileflowBuildProvenance,
1203
+ createTileflowArtifactSession,
1204
+ getTileflowWatchPaths,
1205
+ isTileflowArtifactInputPath,
1206
+ refreshTileflowArtifactSession,
1207
+ writeTileflowBuildArtifacts,
1208
+ writeTileflowArtifactPlan,
1209
+ assertTileflowSelfHostedManifestTarget
1210
+ };