@tileflow/dev 0.1.0-alpha.2 → 0.1.0-alpha.21
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/GENERATED_OUTPUT_LICENSE.md +21 -0
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +547 -2
- package/THIRD_PARTY_NOTICES.md +69 -0
- package/TRADEMARKS.md +10 -0
- package/dist/artifacts.d.ts +191 -0
- package/dist/artifacts.js +71 -0
- package/dist/chunk-6MGFMKG3.js +284 -0
- package/dist/chunk-BL3FK4GO.js +170 -0
- package/dist/chunk-CENQR5Q2.js +844 -0
- package/dist/chunk-D4OE4ITL.js +13 -0
- package/dist/chunk-KDFTE4Q5.js +156 -0
- package/dist/chunk-KH7CMHT3.js +2069 -0
- package/dist/chunk-LNCTVFUD.js +236 -0
- package/dist/chunk-MIGBHCXX.js +4222 -0
- package/dist/chunk-MKPSQSMX.js +805 -0
- package/dist/chunk-ONYQNLNA.js +1108 -0
- package/dist/chunk-RSBV4L2Y.js +671 -0
- package/dist/chunk-XTRHREXO.js +1688 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.js +27 -0
- package/dist/fonts.d.ts +47 -0
- package/dist/fonts.js +16 -0
- package/dist/icons.d.ts +136 -0
- package/dist/icons.js +17 -0
- package/dist/index.d.ts +13 -54
- package/dist/index.js +171 -297
- package/dist/inspect.d.ts +149 -0
- package/dist/inspect.js +18 -0
- package/dist/preview-assets-6LYL7GP7.js +145 -0
- package/dist/preview.d.ts +69 -0
- package/dist/preview.js +18 -0
- package/dist/server.d.ts +81 -0
- package/dist/server.js +48 -0
- package/dist/tilesets-C0tGeenP.d.ts +94 -0
- package/dist/tilesets.d.ts +1 -0
- package/dist/tilesets.js +6 -0
- package/dist/validation.d.ts +63 -0
- package/dist/validation.js +24 -0
- package/package.json +80 -7
|
@@ -0,0 +1,1688 @@
|
|
|
1
|
+
import {
|
|
2
|
+
inspectTileflowPmtilesForLocalUse
|
|
3
|
+
} from "./chunk-ONYQNLNA.js";
|
|
4
|
+
import {
|
|
5
|
+
getTileflowFontWatchPaths,
|
|
6
|
+
joinTileflowPublicUrl,
|
|
7
|
+
prepareTileflowStyleFonts,
|
|
8
|
+
replaceTileflowStyleFontSources
|
|
9
|
+
} from "./chunk-RSBV4L2Y.js";
|
|
10
|
+
import {
|
|
11
|
+
getTileflowIconWatchPaths,
|
|
12
|
+
prepareTileflowCatalogIcons
|
|
13
|
+
} from "./chunk-CENQR5Q2.js";
|
|
14
|
+
import {
|
|
15
|
+
TileflowStyleValidationError,
|
|
16
|
+
normalizeTileflowStyleValidationIssues,
|
|
17
|
+
sanitizeDiagnosticSecrets,
|
|
18
|
+
validateTileflowStyle
|
|
19
|
+
} from "./chunk-KDFTE4Q5.js";
|
|
20
|
+
import {
|
|
21
|
+
defaultTileflowConfigPath,
|
|
22
|
+
getTileflowMapNames,
|
|
23
|
+
loadValidTileflowConfigWithInputs
|
|
24
|
+
} from "./chunk-6MGFMKG3.js";
|
|
25
|
+
import {
|
|
26
|
+
isPathWithin
|
|
27
|
+
} from "./chunk-D4OE4ITL.js";
|
|
28
|
+
|
|
29
|
+
// src/artifacts.ts
|
|
30
|
+
import { createHash } from "crypto";
|
|
31
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
32
|
+
import {
|
|
33
|
+
copyFile as copyFile2,
|
|
34
|
+
lstat as lstat2,
|
|
35
|
+
mkdir as mkdir2,
|
|
36
|
+
mkdtemp,
|
|
37
|
+
open as open2,
|
|
38
|
+
readFile,
|
|
39
|
+
realpath as realpath2,
|
|
40
|
+
rename,
|
|
41
|
+
rm as rm2,
|
|
42
|
+
unlink
|
|
43
|
+
} from "fs/promises";
|
|
44
|
+
import { createRequire } from "module";
|
|
45
|
+
import { dirname as dirname2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
46
|
+
import {
|
|
47
|
+
compareCodeUnits as compareCodeUnits2,
|
|
48
|
+
getTileflowStyleFontFaces,
|
|
49
|
+
serializeCanonicalJson,
|
|
50
|
+
tileflowMapIdSchema,
|
|
51
|
+
tileflowThemeNameSchema
|
|
52
|
+
} from "@tileflow/core";
|
|
53
|
+
import {
|
|
54
|
+
createManifest,
|
|
55
|
+
createStyleFromCatalog,
|
|
56
|
+
createStylesFromCatalog,
|
|
57
|
+
createStylesFromCatalogWithInspection,
|
|
58
|
+
createTileflowMapBuildManifest,
|
|
59
|
+
tileflowMapBuildManifestFileName
|
|
60
|
+
} from "@tileflow/core/build";
|
|
61
|
+
import { parseTileflowRuntimeManifest } from "@tileflow/core/manifest";
|
|
62
|
+
|
|
63
|
+
// src/local-tilesets.ts
|
|
64
|
+
import { randomUUID } from "crypto";
|
|
65
|
+
import { constants } from "fs";
|
|
66
|
+
import { chmod, copyFile, lstat, mkdir, open, readdir, realpath, rm, stat } from "fs/promises";
|
|
67
|
+
import { basename, resolve } from "path";
|
|
68
|
+
import { compareCodeUnits } from "@tileflow/core";
|
|
69
|
+
var minimumLocalPmtilesBytes = 127n;
|
|
70
|
+
var maximumLocalPmtilesBytes = 8000000000n;
|
|
71
|
+
async function prepareTileflowLocalTilesets(project, styles, options) {
|
|
72
|
+
const hasSources = Object.values(project.maps).some(
|
|
73
|
+
(map) => Object.keys(map.sources ?? {}).length > 0
|
|
74
|
+
);
|
|
75
|
+
if (!hasSources) {
|
|
76
|
+
return { dispose: async () => void 0, files: [], styles, watchPaths: [] };
|
|
77
|
+
}
|
|
78
|
+
const logicalProjectRoot = resolve(options.cwd);
|
|
79
|
+
const projectRoot = await realpath(logicalProjectRoot).catch(() => logicalProjectRoot);
|
|
80
|
+
const snapshotStore = resolve(logicalProjectRoot, ".tileflow/cache/pmtiles-snapshots/v1");
|
|
81
|
+
await assertSafeSnapshotStore(logicalProjectRoot, snapshotStore);
|
|
82
|
+
await collectAbandonedSnapshotGenerations(snapshotStore);
|
|
83
|
+
const generationId = `snapshot-${process.pid}-${randomUUID().replaceAll("-", "")}`;
|
|
84
|
+
const generationDirectory = resolve(snapshotStore, generationId);
|
|
85
|
+
await mkdir(generationDirectory);
|
|
86
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
87
|
+
const files = /* @__PURE__ */ new Map();
|
|
88
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
89
|
+
const output = Object.fromEntries(
|
|
90
|
+
Object.entries(styles).map(([mapName, family]) => [
|
|
91
|
+
mapName,
|
|
92
|
+
Object.fromEntries(
|
|
93
|
+
Object.entries(family).map(([themeName, style]) => [themeName, cloneStyle(style)])
|
|
94
|
+
)
|
|
95
|
+
])
|
|
96
|
+
);
|
|
97
|
+
let disposed = false;
|
|
98
|
+
const dispose = async () => {
|
|
99
|
+
if (disposed) return;
|
|
100
|
+
disposed = true;
|
|
101
|
+
if (!isPathWithin(snapshotStore, generationDirectory)) {
|
|
102
|
+
throw new Error("Local PMTiles snapshot generation escaped its store.");
|
|
103
|
+
}
|
|
104
|
+
await rm(generationDirectory, { force: true, recursive: true });
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
for (const mapName of Object.keys(project.maps).sort(compareCodeUnits)) {
|
|
108
|
+
const map = project.maps[mapName];
|
|
109
|
+
for (const [sourceId, source] of Object.entries(map.sources ?? {}).sort(
|
|
110
|
+
([left], [right]) => compareCodeUnits(left, right)
|
|
111
|
+
)) {
|
|
112
|
+
const requestedPath = resolve(options.baseDirectory, source.local);
|
|
113
|
+
const sourcePath = await realpath(requestedPath).catch(() => requestedPath);
|
|
114
|
+
if (!isPathWithin(projectRoot, sourcePath)) {
|
|
115
|
+
throw localTilesetError(
|
|
116
|
+
"TF_LOCAL_TILESET_OUTSIDE_ROOT",
|
|
117
|
+
`maps.${mapName}.sources.${sourceId}.local`,
|
|
118
|
+
"Local hosted tileset must stay inside the project root."
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const sourceStat = await lstat(sourcePath).catch(() => null);
|
|
122
|
+
if (!sourceStat?.isFile() || sourceStat.isSymbolicLink()) {
|
|
123
|
+
throw localTilesetError(
|
|
124
|
+
"TF_LOCAL_TILESET_NOT_FOUND",
|
|
125
|
+
`maps.${mapName}.sources.${sourceId}.local`,
|
|
126
|
+
`Local hosted tileset "${sourceId}" was not found.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
let snapshot = snapshots.get(sourcePath);
|
|
130
|
+
if (!snapshot) {
|
|
131
|
+
snapshot = await createLocalTilesetSnapshot(
|
|
132
|
+
sourcePath,
|
|
133
|
+
generationDirectory,
|
|
134
|
+
snapshots.size
|
|
135
|
+
);
|
|
136
|
+
snapshots.set(sourcePath, snapshot);
|
|
137
|
+
}
|
|
138
|
+
const file = {
|
|
139
|
+
byteLength: snapshot.byteLength,
|
|
140
|
+
etag: snapshot.etag,
|
|
141
|
+
fileName: `tilesets/${source.tileset}.pmtiles`,
|
|
142
|
+
logicalId: source.tileset,
|
|
143
|
+
sourcePath: snapshot.sourcePath
|
|
144
|
+
};
|
|
145
|
+
const existing = bindings.get(source.tileset);
|
|
146
|
+
if (existing && existing.originalSourcePath !== sourcePath) {
|
|
147
|
+
throw localTilesetError(
|
|
148
|
+
"TF_LOCAL_TILESET_BINDING_CONFLICT",
|
|
149
|
+
`maps.${mapName}.sources.${sourceId}.local`,
|
|
150
|
+
`Logical tileset "${source.tileset}" resolves to different local archives in one generation.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
bindings.set(source.tileset, { file, originalSourcePath: sourcePath });
|
|
154
|
+
files.set(file.fileName, files.get(file.fileName) ?? file);
|
|
155
|
+
validateSourceContract(
|
|
156
|
+
mapName,
|
|
157
|
+
sourceId,
|
|
158
|
+
source.type,
|
|
159
|
+
output[mapName] ?? {},
|
|
160
|
+
snapshot.inspection
|
|
161
|
+
);
|
|
162
|
+
const publicUrl = joinTileflowPublicUrl(options.assetBaseUrl, file.fileName);
|
|
163
|
+
for (const style of Object.values(output[mapName] ?? {})) {
|
|
164
|
+
const definition = style.sources[sourceId];
|
|
165
|
+
if (!definition) {
|
|
166
|
+
throw localTilesetError(
|
|
167
|
+
"TF_LOCAL_TILESET_SOURCE_MISSING",
|
|
168
|
+
`maps.${mapName}.sources.${sourceId}`,
|
|
169
|
+
`Compiled style is missing hosted source "${sourceId}".`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
definition.url = `tileflow-pmtiles://${publicUrl}`;
|
|
173
|
+
delete definition.tiles;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
dispose,
|
|
179
|
+
files: [...files.values()].sort(
|
|
180
|
+
(left, right) => compareCodeUnits(left.fileName, right.fileName)
|
|
181
|
+
),
|
|
182
|
+
styles: output,
|
|
183
|
+
watchPaths: [...snapshots.keys()].sort(compareCodeUnits)
|
|
184
|
+
};
|
|
185
|
+
} catch (error) {
|
|
186
|
+
await dispose().catch(() => void 0);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function createLocalTilesetSnapshot(sourcePath, generationDirectory, index) {
|
|
191
|
+
const sourceHandle = await open(sourcePath, "r");
|
|
192
|
+
const snapshotPath = resolve(generationDirectory, `${index}.pmtiles`);
|
|
193
|
+
try {
|
|
194
|
+
const before = sourceIdentity(await sourceHandle.stat({ bigint: true }));
|
|
195
|
+
if (before.size < minimumLocalPmtilesBytes || before.size > maximumLocalPmtilesBytes) {
|
|
196
|
+
throw localTilesetError(
|
|
197
|
+
"TF_TILESET_ARCHIVE_INVALID",
|
|
198
|
+
"archive",
|
|
199
|
+
"Expected one bounded PMTiles file."
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
await copyFile(sourcePath, snapshotPath, constants.COPYFILE_FICLONE);
|
|
203
|
+
await syncFile(snapshotPath);
|
|
204
|
+
const [afterHandle, afterPath, snapshotStat] = await Promise.all([
|
|
205
|
+
sourceHandle.stat({ bigint: true }).then(sourceIdentity),
|
|
206
|
+
stat(sourcePath, { bigint: true }).then(sourceIdentity),
|
|
207
|
+
stat(snapshotPath, { bigint: true })
|
|
208
|
+
]);
|
|
209
|
+
assertSourceIdentity(before, afterHandle);
|
|
210
|
+
assertSourceIdentity(before, afterPath);
|
|
211
|
+
if (!snapshotStat.isFile() || snapshotStat.size !== before.size) {
|
|
212
|
+
throw localTilesetError(
|
|
213
|
+
"TF_LOCAL_TILESET_CHANGED",
|
|
214
|
+
"local",
|
|
215
|
+
"Local hosted tileset changed while its immutable snapshot was prepared."
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const inspection = await inspectTileflowPmtilesForLocalUse(snapshotPath);
|
|
219
|
+
await chmod(snapshotPath, 292);
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
byteLength: Number(before.size),
|
|
222
|
+
etag: `"${generationIdFromDirectory(generationDirectory)}-${index}"`,
|
|
223
|
+
inspection,
|
|
224
|
+
sourcePath: snapshotPath
|
|
225
|
+
});
|
|
226
|
+
} finally {
|
|
227
|
+
await sourceHandle.close();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function generationIdFromDirectory(path) {
|
|
231
|
+
const generationId = basename(path);
|
|
232
|
+
if (!/^snapshot-\d+-[a-f0-9]{32}$/u.test(generationId)) {
|
|
233
|
+
throw new Error("Local PMTiles snapshot generation is invalid.");
|
|
234
|
+
}
|
|
235
|
+
return generationId;
|
|
236
|
+
}
|
|
237
|
+
function sourceIdentity(value) {
|
|
238
|
+
if (!value.isFile()) throw new Error("Local PMTiles source is not a file.");
|
|
239
|
+
return Object.freeze({
|
|
240
|
+
device: BigInt(value.dev),
|
|
241
|
+
inode: BigInt(value.ino),
|
|
242
|
+
mtimeNanoseconds: "mtimeNs" in value ? BigInt(value.mtimeNs) : BigInt(Math.round(value.mtimeMs * 1e6)),
|
|
243
|
+
size: BigInt(value.size)
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function assertSourceIdentity(before, after) {
|
|
247
|
+
if (before.device !== after.device || before.inode !== after.inode || before.mtimeNanoseconds !== after.mtimeNanoseconds || before.size !== after.size) {
|
|
248
|
+
throw localTilesetError(
|
|
249
|
+
"TF_LOCAL_TILESET_CHANGED",
|
|
250
|
+
"local",
|
|
251
|
+
"Local hosted tileset changed while its immutable snapshot was prepared."
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function validateSourceContract(mapName, sourceId, sourceType, styles, inspection) {
|
|
256
|
+
const isVectorArchive = inspection.contract.tileType === "mlt" || inspection.contract.tileType === "mvt";
|
|
257
|
+
if (sourceType === "vector" !== isVectorArchive) {
|
|
258
|
+
throw localTilesetError(
|
|
259
|
+
"TF_SOURCE_TILE_TYPE_MISMATCH",
|
|
260
|
+
`maps.${mapName}.sources.${sourceId}.type`,
|
|
261
|
+
`Source "${sourceId}" declares ${sourceType} but the PMTiles archive contains ${inspection.contract.tileType}.`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (!isVectorArchive || !inspection.contract.sourceLayersDeclared) return;
|
|
265
|
+
const availableLayers = new Map(
|
|
266
|
+
inspection.contract.sourceLayers.map((layer) => [layer.id, layer])
|
|
267
|
+
);
|
|
268
|
+
for (const style of Object.values(styles)) {
|
|
269
|
+
const requirements = asRecord(style.metadata?.["tileflow:sourceRequirements"]);
|
|
270
|
+
const sources = asRecord(requirements?.sources);
|
|
271
|
+
const source = asRecord(sources?.[sourceId]);
|
|
272
|
+
const sourceLayers = Array.isArray(source?.sourceLayers) ? source.sourceLayers : [];
|
|
273
|
+
for (const candidate of sourceLayers) {
|
|
274
|
+
const required = asRecord(candidate);
|
|
275
|
+
const layerId = required?.id;
|
|
276
|
+
if (typeof layerId !== "string") continue;
|
|
277
|
+
const available = availableLayers.get(layerId);
|
|
278
|
+
if (!available) {
|
|
279
|
+
const candidates = [...availableLayers.keys()].sort(compareCodeUnits);
|
|
280
|
+
throw localTilesetError(
|
|
281
|
+
"TF_SOURCE_LAYER_NOT_FOUND",
|
|
282
|
+
`maps.${mapName}.sources.${sourceId}.sourceLayers.${layerId}`,
|
|
283
|
+
`Source "${sourceId}" has no source-layer "${layerId}". Available: ${candidates.join(", ") || "(none)"}.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (!available.fieldsDeclared) continue;
|
|
287
|
+
const availableFields = new Map(available.fields.map((field) => [field.name, field.type]));
|
|
288
|
+
const requiredFields = Array.isArray(required?.fields) ? required.fields : [];
|
|
289
|
+
for (const fieldCandidate of requiredFields) {
|
|
290
|
+
const field = asRecord(fieldCandidate);
|
|
291
|
+
const fieldName = field?.name;
|
|
292
|
+
if (typeof fieldName !== "string") continue;
|
|
293
|
+
if (!availableFields.has(fieldName)) {
|
|
294
|
+
const candidates = [...availableFields.keys()].sort(compareCodeUnits);
|
|
295
|
+
throw localTilesetError(
|
|
296
|
+
"TF_SOURCE_FIELD_NOT_FOUND",
|
|
297
|
+
`maps.${mapName}.sources.${sourceId}.sourceLayers.${layerId}.fields.${fieldName}`,
|
|
298
|
+
`Source-layer "${layerId}" has no field "${fieldName}". Available: ${candidates.join(", ") || "(none)"}.`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const requiredType = field?.type;
|
|
302
|
+
const actualType = availableFields.get(fieldName);
|
|
303
|
+
if (typeof requiredType === "string" && actualType !== void 0 && requiredType !== actualType) {
|
|
304
|
+
throw localTilesetError(
|
|
305
|
+
"TF_SOURCE_FIELD_TYPE_MISMATCH",
|
|
306
|
+
`maps.${mapName}.sources.${sourceId}.sourceLayers.${layerId}.fields.${fieldName}`,
|
|
307
|
+
`Field "${layerId}.${fieldName}" is ${actualType}; ${requiredType} is required.`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function collectAbandonedSnapshotGenerations(store) {
|
|
315
|
+
const entries = await readdir(store, { withFileTypes: true }).catch(() => []);
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
318
|
+
const match = /^snapshot-(\d+)-[a-f0-9]{32}$/u.exec(entry.name);
|
|
319
|
+
if (!match?.[1] || processIsAlive(Number(match[1]))) continue;
|
|
320
|
+
const path = resolve(store, entry.name);
|
|
321
|
+
if (isPathWithin(store, path)) await rm(path, { force: true, recursive: true });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function assertSafeSnapshotStore(projectRoot, store) {
|
|
325
|
+
const segments = [".tileflow", "cache", "pmtiles-snapshots", "v1"];
|
|
326
|
+
let current = projectRoot;
|
|
327
|
+
for (const segment of segments) {
|
|
328
|
+
current = resolve(current, segment);
|
|
329
|
+
try {
|
|
330
|
+
const value = await lstat(current);
|
|
331
|
+
if (!value.isDirectory() || value.isSymbolicLink()) {
|
|
332
|
+
throw new Error(`Local PMTiles snapshot path is unsafe: ${current}`);
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (!hasErrorCode(error, "ENOENT")) throw error;
|
|
336
|
+
await mkdir(current).catch((mkdirError) => {
|
|
337
|
+
if (!hasErrorCode(mkdirError, "EEXIST")) throw mkdirError;
|
|
338
|
+
});
|
|
339
|
+
const created = await lstat(current);
|
|
340
|
+
if (!created.isDirectory() || created.isSymbolicLink()) {
|
|
341
|
+
throw new Error(`Local PMTiles snapshot path is unsafe: ${current}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (resolve(current) !== resolve(store)) {
|
|
346
|
+
throw new Error("Local PMTiles snapshot path escaped its project root.");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function processIsAlive(pid) {
|
|
350
|
+
try {
|
|
351
|
+
process.kill(pid, 0);
|
|
352
|
+
return true;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
return !hasErrorCode(error, "ESRCH");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async function syncFile(path) {
|
|
358
|
+
const handle = await open(path, "r");
|
|
359
|
+
try {
|
|
360
|
+
await handle.sync();
|
|
361
|
+
} finally {
|
|
362
|
+
await handle.close();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function cloneStyle(style) {
|
|
366
|
+
return JSON.parse(JSON.stringify(style));
|
|
367
|
+
}
|
|
368
|
+
function localTilesetError(code, path, message) {
|
|
369
|
+
return Object.assign(new Error(message), { code, path, phase: "local-tileset-resolution" });
|
|
370
|
+
}
|
|
371
|
+
function asRecord(value) {
|
|
372
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
373
|
+
}
|
|
374
|
+
function hasErrorCode(error, code) {
|
|
375
|
+
return Boolean(error && typeof error === "object" && error.code === code);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/session.ts
|
|
379
|
+
import { watch } from "chokidar";
|
|
380
|
+
import { realpathSync } from "fs";
|
|
381
|
+
import { dirname, extname, isAbsolute, relative, resolve as resolve2, sep, win32 } from "path";
|
|
382
|
+
var tileflowArtifactSessionSchemaVersion = 1;
|
|
383
|
+
var configExtensions = /* @__PURE__ */ new Set([".cjs", ".cts", ".js", ".json", ".mjs", ".mts", ".ts", ".tsx"]);
|
|
384
|
+
var iconExtensions = /* @__PURE__ */ new Set([".jpeg", ".jpg", ".png", ".svg", ".webp"]);
|
|
385
|
+
var fontExtensions = /* @__PURE__ */ new Set([".otf", ".ttf", ".woff2"]);
|
|
386
|
+
var excludedDirectories = /* @__PURE__ */ new Set([
|
|
387
|
+
".cache",
|
|
388
|
+
".git",
|
|
389
|
+
".hg",
|
|
390
|
+
".next",
|
|
391
|
+
".nuxt",
|
|
392
|
+
".svn",
|
|
393
|
+
".svelte-kit",
|
|
394
|
+
".tileflow",
|
|
395
|
+
".turbo",
|
|
396
|
+
"build",
|
|
397
|
+
"coverage",
|
|
398
|
+
"dist",
|
|
399
|
+
"node_modules"
|
|
400
|
+
]);
|
|
401
|
+
async function createTileflowArtifactSessionWithBuilder(options, buildArtifacts, discoverWatchPaths) {
|
|
402
|
+
const session = new TileflowArtifactSessionImpl(options, buildArtifacts, discoverWatchPaths);
|
|
403
|
+
await session.start();
|
|
404
|
+
return session;
|
|
405
|
+
}
|
|
406
|
+
var TileflowArtifactSessionImpl = class {
|
|
407
|
+
#buildArtifacts;
|
|
408
|
+
#buildOptions;
|
|
409
|
+
#configPath;
|
|
410
|
+
#cwd;
|
|
411
|
+
#debounceMs;
|
|
412
|
+
#discoverWatchPaths;
|
|
413
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
414
|
+
#ignoredPaths;
|
|
415
|
+
#watchEnabled;
|
|
416
|
+
#closed = false;
|
|
417
|
+
#closing;
|
|
418
|
+
#debounceTimer;
|
|
419
|
+
#generation = 0;
|
|
420
|
+
#current;
|
|
421
|
+
#artifactGenerations = /* @__PURE__ */ new Set();
|
|
422
|
+
#lastGoodGeneration;
|
|
423
|
+
#refreshes = /* @__PURE__ */ new Set();
|
|
424
|
+
#refreshToken = 0;
|
|
425
|
+
#state = { generation: 0, status: "building" };
|
|
426
|
+
#watcher;
|
|
427
|
+
#watchedArtifactPaths = /* @__PURE__ */ new Set();
|
|
428
|
+
constructor(options, buildArtifacts, discoverWatchPaths) {
|
|
429
|
+
this.#cwd = resolve2(options.cwd ?? process.cwd());
|
|
430
|
+
this.#configPath = resolve2(this.#cwd, options.config ?? "tileflow.config.ts");
|
|
431
|
+
this.#debounceMs = Math.max(0, Math.min(options.debounceMs ?? 75, 1e3));
|
|
432
|
+
this.#watchEnabled = options.watch ?? false;
|
|
433
|
+
this.#ignoredPaths = (options.ignoredPaths ?? []).map((path) => resolve2(this.#cwd, path));
|
|
434
|
+
this.#buildArtifacts = buildArtifacts;
|
|
435
|
+
this.#discoverWatchPaths = discoverWatchPaths;
|
|
436
|
+
this.#buildOptions = {
|
|
437
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
438
|
+
assetBaseUrl: options.assetBaseUrl,
|
|
439
|
+
config: options.config,
|
|
440
|
+
cwd: this.#cwd,
|
|
441
|
+
inspection: options.inspection,
|
|
442
|
+
styleBaseUrl: options.styleBaseUrl,
|
|
443
|
+
target: options.target
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
async start() {
|
|
447
|
+
if (this.#watchEnabled) {
|
|
448
|
+
this.#watcher = watch(dirname(this.#configPath), {
|
|
449
|
+
awaitWriteFinish: { pollInterval: 20, stabilityThreshold: 50 },
|
|
450
|
+
followSymlinks: false,
|
|
451
|
+
ignoreInitial: true,
|
|
452
|
+
ignored: (path, stats) => this.#ignoreWatchPath(path, stats?.isDirectory()),
|
|
453
|
+
usePolling: process.platform === "win32"
|
|
454
|
+
});
|
|
455
|
+
const watcherReady = new Promise((resolveReady, rejectReady) => {
|
|
456
|
+
const watcher = this.#watcher;
|
|
457
|
+
const cleanup = () => {
|
|
458
|
+
watcher.off("ready", onReady);
|
|
459
|
+
watcher.off("error", onError);
|
|
460
|
+
};
|
|
461
|
+
const onReady = () => {
|
|
462
|
+
cleanup();
|
|
463
|
+
resolveReady();
|
|
464
|
+
};
|
|
465
|
+
const onError = (error) => {
|
|
466
|
+
cleanup();
|
|
467
|
+
rejectReady(error);
|
|
468
|
+
};
|
|
469
|
+
watcher.once("ready", onReady);
|
|
470
|
+
watcher.once("error", onError);
|
|
471
|
+
});
|
|
472
|
+
this.#watcher.on("all", (event, path) => {
|
|
473
|
+
if (event === "addDir" || event === "unlinkDir") return;
|
|
474
|
+
if (!this.#shouldRefreshForWatchEvent(path)) return;
|
|
475
|
+
this.#scheduleRefresh(path);
|
|
476
|
+
});
|
|
477
|
+
this.#watcher.on("error", (error) => {
|
|
478
|
+
if (this.#closed) return;
|
|
479
|
+
this.#refreshToken += 1;
|
|
480
|
+
this.#publishInvalid(this.#generation + 1, error);
|
|
481
|
+
});
|
|
482
|
+
await watcherReady;
|
|
483
|
+
}
|
|
484
|
+
await this.refresh("initial");
|
|
485
|
+
}
|
|
486
|
+
getState() {
|
|
487
|
+
return this.#state;
|
|
488
|
+
}
|
|
489
|
+
getLastGoodArtifacts() {
|
|
490
|
+
return this.#current?.artifacts;
|
|
491
|
+
}
|
|
492
|
+
acquireArtifacts(generation) {
|
|
493
|
+
const current = this.#current;
|
|
494
|
+
if (this.#closed || !current?.accepting || generation !== void 0 && generation !== current.generation) {
|
|
495
|
+
return void 0;
|
|
496
|
+
}
|
|
497
|
+
current.references += 1;
|
|
498
|
+
let released = false;
|
|
499
|
+
return Object.freeze({
|
|
500
|
+
artifacts: current.artifacts,
|
|
501
|
+
generation: current.generation,
|
|
502
|
+
release: async () => {
|
|
503
|
+
if (released) return;
|
|
504
|
+
released = true;
|
|
505
|
+
current.references -= 1;
|
|
506
|
+
if (!current.accepting && current.references === 0) {
|
|
507
|
+
await this.#disposeGeneration(current);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
subscribe(listener) {
|
|
513
|
+
if (this.#closed) return () => void 0;
|
|
514
|
+
this.#listeners.add(listener);
|
|
515
|
+
return () => this.#listeners.delete(listener);
|
|
516
|
+
}
|
|
517
|
+
async refresh(_reason = "manual") {
|
|
518
|
+
const refresh = this.#refresh();
|
|
519
|
+
this.#refreshes.add(refresh);
|
|
520
|
+
try {
|
|
521
|
+
await refresh;
|
|
522
|
+
} finally {
|
|
523
|
+
this.#refreshes.delete(refresh);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async #refresh() {
|
|
527
|
+
if (this.#closed) return;
|
|
528
|
+
const generation = ++this.#generation;
|
|
529
|
+
const token = ++this.#refreshToken;
|
|
530
|
+
this.#publish({
|
|
531
|
+
generation,
|
|
532
|
+
status: "building",
|
|
533
|
+
...this.#lastGoodGeneration === void 0 ? {} : { lastGoodGeneration: this.#lastGoodGeneration }
|
|
534
|
+
});
|
|
535
|
+
try {
|
|
536
|
+
const artifacts = await this.#buildArtifacts(this.#buildOptions);
|
|
537
|
+
if (this.#closed || token !== this.#refreshToken) {
|
|
538
|
+
await disposeArtifacts(artifacts);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const previous = this.#current;
|
|
542
|
+
const current = createArtifactGeneration(generation, artifacts);
|
|
543
|
+
this.#artifactGenerations.add(current);
|
|
544
|
+
this.#current = current;
|
|
545
|
+
this.#lastGoodGeneration = generation;
|
|
546
|
+
this.#updateArtifactWatchPaths(artifacts.watchPaths);
|
|
547
|
+
if (previous) {
|
|
548
|
+
previous.accepting = false;
|
|
549
|
+
if (previous.references === 0) await this.#disposeGeneration(previous);
|
|
550
|
+
}
|
|
551
|
+
this.#publish({
|
|
552
|
+
artifacts,
|
|
553
|
+
generation,
|
|
554
|
+
lastGoodGeneration: generation,
|
|
555
|
+
status: "ready"
|
|
556
|
+
});
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (this.#closed || token !== this.#refreshToken) return;
|
|
559
|
+
if (this.#watcher && this.#discoverWatchPaths) {
|
|
560
|
+
try {
|
|
561
|
+
const watchPaths = await this.#discoverWatchPaths();
|
|
562
|
+
if (this.#closed || token !== this.#refreshToken) return;
|
|
563
|
+
this.#updateArtifactWatchPaths(watchPaths);
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
this.#publishInvalid(generation, error);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
close() {
|
|
571
|
+
this.#closing ??= this.#close();
|
|
572
|
+
return this.#closing;
|
|
573
|
+
}
|
|
574
|
+
async #close() {
|
|
575
|
+
this.#closed = true;
|
|
576
|
+
this.#refreshToken += 1;
|
|
577
|
+
if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
|
|
578
|
+
this.#listeners.clear();
|
|
579
|
+
await this.#watcher?.close();
|
|
580
|
+
const retained = [...this.#artifactGenerations];
|
|
581
|
+
this.#current = void 0;
|
|
582
|
+
for (const generation of retained) {
|
|
583
|
+
generation.accepting = false;
|
|
584
|
+
if (generation.references === 0) void this.#disposeGeneration(generation);
|
|
585
|
+
}
|
|
586
|
+
await Promise.allSettled([...this.#refreshes]);
|
|
587
|
+
await Promise.all(retained.map((generation) => generation.completion));
|
|
588
|
+
}
|
|
589
|
+
#ignoreWatchPath(path, isDirectory) {
|
|
590
|
+
const resolvedPath = resolve2(path);
|
|
591
|
+
const relativePath = relative(dirname(this.#configPath), resolvedPath);
|
|
592
|
+
const segments = relativePath.split(sep).filter(Boolean);
|
|
593
|
+
if (this.#ignoredPaths.some((path2) => isSameOrInside(path2, resolvedPath))) return true;
|
|
594
|
+
if (segments.some((segment) => excludedDirectories.has(segment))) return true;
|
|
595
|
+
if (isDirectory !== false) return false;
|
|
596
|
+
if (resolvedPath === this.#configPath || this.#watchedArtifactPaths.has(resolvedPath))
|
|
597
|
+
return false;
|
|
598
|
+
const extension = extname(resolvedPath).toLowerCase();
|
|
599
|
+
if (configExtensions.has(extension)) return false;
|
|
600
|
+
if (iconExtensions.has(extension) || fontExtensions.has(extension)) return false;
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
#shouldRefreshForWatchEvent(path) {
|
|
604
|
+
const resolvedPath = resolve2(path);
|
|
605
|
+
const extension = extname(resolvedPath).toLowerCase();
|
|
606
|
+
if (!iconExtensions.has(extension) && !fontExtensions.has(extension)) return true;
|
|
607
|
+
return [...this.#watchedArtifactPaths].some((path2) => isSameOrInside(path2, resolvedPath));
|
|
608
|
+
}
|
|
609
|
+
#publish(state) {
|
|
610
|
+
this.#state = state;
|
|
611
|
+
for (const listener of this.#listeners) listener(state);
|
|
612
|
+
}
|
|
613
|
+
#publishInvalid(generation, error) {
|
|
614
|
+
this.#generation = Math.max(this.#generation, generation);
|
|
615
|
+
this.#publish({
|
|
616
|
+
diagnostics: createTileflowArtifactDiagnostics(error, this.#cwd),
|
|
617
|
+
generation,
|
|
618
|
+
status: "invalid",
|
|
619
|
+
...this.#lastGoodGeneration === void 0 ? {} : { lastGoodGeneration: this.#lastGoodGeneration }
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
#scheduleRefresh(_path) {
|
|
623
|
+
if (this.#closed) return;
|
|
624
|
+
if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
|
|
625
|
+
this.#debounceTimer = setTimeout(() => {
|
|
626
|
+
this.#debounceTimer = void 0;
|
|
627
|
+
void this.refresh("filesystem");
|
|
628
|
+
}, this.#debounceMs);
|
|
629
|
+
}
|
|
630
|
+
#updateArtifactWatchPaths(paths) {
|
|
631
|
+
if (!this.#watcher) return;
|
|
632
|
+
const next = new Set(paths.map((path) => resolve2(this.#cwd, path)));
|
|
633
|
+
const previous = this.#watchedArtifactPaths;
|
|
634
|
+
this.#watchedArtifactPaths = next;
|
|
635
|
+
for (const path of previous) {
|
|
636
|
+
if (!next.has(path) && !isSameOrInside(dirname(this.#configPath), path)) {
|
|
637
|
+
void this.#watcher.unwatch(path);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
for (const path of next) {
|
|
641
|
+
if (!previous.has(path) && !isSameOrInside(dirname(this.#configPath), path)) {
|
|
642
|
+
this.#watcher.add(path);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
#disposeGeneration(generation) {
|
|
647
|
+
if (generation.disposeStarted) return generation.completion;
|
|
648
|
+
generation.disposeStarted = true;
|
|
649
|
+
void disposeArtifacts(generation.artifacts).then(
|
|
650
|
+
() => {
|
|
651
|
+
this.#artifactGenerations.delete(generation);
|
|
652
|
+
generation.resolveCompletion();
|
|
653
|
+
},
|
|
654
|
+
(error) => {
|
|
655
|
+
this.#artifactGenerations.delete(generation);
|
|
656
|
+
generation.rejectCompletion(error);
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
return generation.completion;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
function createArtifactGeneration(generation, artifacts) {
|
|
663
|
+
let rejectCompletion;
|
|
664
|
+
let resolveCompletion;
|
|
665
|
+
const completion = new Promise((resolvePromise, rejectPromise) => {
|
|
666
|
+
rejectCompletion = rejectPromise;
|
|
667
|
+
resolveCompletion = resolvePromise;
|
|
668
|
+
});
|
|
669
|
+
return {
|
|
670
|
+
accepting: true,
|
|
671
|
+
artifacts,
|
|
672
|
+
completion,
|
|
673
|
+
disposeStarted: false,
|
|
674
|
+
generation,
|
|
675
|
+
references: 0,
|
|
676
|
+
rejectCompletion,
|
|
677
|
+
resolveCompletion
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
async function disposeArtifacts(artifacts) {
|
|
681
|
+
const dispose = artifacts.dispose;
|
|
682
|
+
artifacts.dispose = void 0;
|
|
683
|
+
await dispose?.();
|
|
684
|
+
}
|
|
685
|
+
function isSameOrInside(root, candidate) {
|
|
686
|
+
const path = relative(resolve2(root), resolve2(candidate));
|
|
687
|
+
return path === "" || !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
|
|
688
|
+
}
|
|
689
|
+
function createTileflowArtifactDiagnostics(error, cwd) {
|
|
690
|
+
const inheritedCode = optionalDiagnosticField(error, "code");
|
|
691
|
+
const inheritedPath = optionalDiagnosticPath(error);
|
|
692
|
+
const inheritedPhase = optionalDiagnosticField(error, "phase") ?? optionalNestedDiagnosticField(error, "phase");
|
|
693
|
+
const semanticResolutionPath = isTileflowMapResolutionDiagnostic(error, inheritedPath) ? sanitizeDiagnosticSecrets(inheritedPath) : void 0;
|
|
694
|
+
const issueList = getIssueList(error);
|
|
695
|
+
if (issueList.length > 0) {
|
|
696
|
+
return normalizeDiagnostics(
|
|
697
|
+
issueList.map((issue) => ({
|
|
698
|
+
...issue.code ?? inheritedCode ? { code: issue.code ?? inheritedCode } : {},
|
|
699
|
+
message: sanitizeMessage(issue.message, cwd),
|
|
700
|
+
path: sanitizePath(issue.path, cwd),
|
|
701
|
+
...issue.phase ?? inheritedPhase ? { phase: issue.phase ?? inheritedPhase } : {}
|
|
702
|
+
}))
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
return normalizeDiagnostics([
|
|
706
|
+
{
|
|
707
|
+
...inheritedCode ? { code: inheritedCode } : {},
|
|
708
|
+
message: sanitizeMessage(
|
|
709
|
+
error instanceof Error ? removeSemanticResolutionPath(error.message, semanticResolutionPath) : "Unknown build error",
|
|
710
|
+
cwd
|
|
711
|
+
),
|
|
712
|
+
path: semanticResolutionPath ?? sanitizePath(inheritedPath ?? "", cwd),
|
|
713
|
+
...inheritedPhase ? { phase: inheritedPhase } : {}
|
|
714
|
+
}
|
|
715
|
+
]);
|
|
716
|
+
}
|
|
717
|
+
function getIssueList(error) {
|
|
718
|
+
if (!error || typeof error !== "object") return [];
|
|
719
|
+
const containers = [];
|
|
720
|
+
for (const key of ["messages", "issues"]) {
|
|
721
|
+
const value = error[key];
|
|
722
|
+
if (Array.isArray(value)) containers.push(value);
|
|
723
|
+
}
|
|
724
|
+
const details = error.details;
|
|
725
|
+
if (details && typeof details === "object") {
|
|
726
|
+
const diagnostics = details.diagnostics;
|
|
727
|
+
if (Array.isArray(diagnostics)) containers.push(diagnostics);
|
|
728
|
+
}
|
|
729
|
+
return containers.flatMap(
|
|
730
|
+
(value) => value.flatMap((entry) => {
|
|
731
|
+
if (!entry || typeof entry !== "object") return [];
|
|
732
|
+
const message = entry.message;
|
|
733
|
+
const path = entry.path;
|
|
734
|
+
const code = optionalDiagnosticField(entry, "code");
|
|
735
|
+
const phase = optionalDiagnosticField(entry, "phase");
|
|
736
|
+
return typeof message === "string" ? [
|
|
737
|
+
{
|
|
738
|
+
...code ? { code } : {},
|
|
739
|
+
message,
|
|
740
|
+
path: typeof path === "string" ? path : "",
|
|
741
|
+
...phase ? { phase } : {}
|
|
742
|
+
}
|
|
743
|
+
] : [];
|
|
744
|
+
})
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
function sanitizeMessage(value, cwd) {
|
|
748
|
+
let sanitized = value;
|
|
749
|
+
for (const root of localPathAliases(cwd)) sanitized = sanitized.replaceAll(root, ".");
|
|
750
|
+
sanitized = sanitized.replace(/[\r\n]+/g, " ").trim();
|
|
751
|
+
sanitized = sanitized.replace(/https?:\/\/[^\s'")<>]+/gi, (url) => {
|
|
752
|
+
try {
|
|
753
|
+
return new URL(url.replace(/[),.;]+$/, "")).origin;
|
|
754
|
+
} catch {
|
|
755
|
+
return "(resource URL)";
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
sanitized = sanitized.replace(/(^|[\s'"(])[A-Za-z]:[\\/][^\s'")]+/g, "$1(external path)");
|
|
759
|
+
sanitized = sanitized.replace(/(^|[\s'"(])(?:\\\\|\/\/)[^\s'")]+/g, "$1(external path)");
|
|
760
|
+
sanitized = sanitized.replace(/(^|[\s'"(])\/[^\s'")]+/g, "$1(external path)");
|
|
761
|
+
return sanitizeDiagnosticSecrets(sanitized);
|
|
762
|
+
}
|
|
763
|
+
function sanitizePath(value, cwd) {
|
|
764
|
+
if (!value) return "";
|
|
765
|
+
if (win32.isAbsolute(value) && !isAbsolute(value)) return "(external)";
|
|
766
|
+
if (!isAbsolute(value)) return sanitizeDiagnosticSecrets(value.replaceAll("\\", "/"));
|
|
767
|
+
const local = relative(canonicalPath(cwd), canonicalPath(value)).replaceAll(sep, "/");
|
|
768
|
+
return local.startsWith("../") || local === ".." ? "(external)" : sanitizeDiagnosticSecrets(local || ".");
|
|
769
|
+
}
|
|
770
|
+
function optionalDiagnosticPath(value) {
|
|
771
|
+
if (!value || typeof value !== "object") return void 0;
|
|
772
|
+
const path = value.path;
|
|
773
|
+
return typeof path === "string" && path ? path : void 0;
|
|
774
|
+
}
|
|
775
|
+
function isTileflowMapResolutionDiagnostic(value, path) {
|
|
776
|
+
if (!value || typeof value !== "object" || !path) return false;
|
|
777
|
+
const record = value;
|
|
778
|
+
return record.name === "TileflowMapResolutionError" && typeof record.code === "string" && typeof record.mapId === "string" && path.startsWith("/modules/") && !/[\p{Cc}]/u.test(path) && !/~(?:[^01]|$)/u.test(path);
|
|
779
|
+
}
|
|
780
|
+
function removeSemanticResolutionPath(message, path) {
|
|
781
|
+
if (!path) return message;
|
|
782
|
+
return message.replace(` at ${path}:`, ":");
|
|
783
|
+
}
|
|
784
|
+
function localPathAliases(path) {
|
|
785
|
+
return [.../* @__PURE__ */ new Set([resolve2(path), canonicalPath(path)])].sort(
|
|
786
|
+
(left, right) => right.length - left.length
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
function canonicalPath(path) {
|
|
790
|
+
try {
|
|
791
|
+
return realpathSync.native(resolve2(path));
|
|
792
|
+
} catch {
|
|
793
|
+
return resolve2(path);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function normalizeDiagnostics(diagnostics) {
|
|
797
|
+
const bounded = diagnostics.map((diagnostic) => ({
|
|
798
|
+
...diagnostic.code ? { code: boundDiagnosticField(diagnostic.code) } : {},
|
|
799
|
+
message: boundDiagnosticText(diagnostic.message, 300),
|
|
800
|
+
path: boundDiagnosticText(diagnostic.path, 300),
|
|
801
|
+
...diagnostic.phase ? { phase: boundDiagnosticField(diagnostic.phase) } : {}
|
|
802
|
+
}));
|
|
803
|
+
bounded.sort((left, right) => {
|
|
804
|
+
const leftKey = `${left.path}\0${left.message}\0${left.code ?? ""}\0${left.phase ?? ""}`;
|
|
805
|
+
const rightKey = `${right.path}\0${right.message}\0${right.code ?? ""}\0${right.phase ?? ""}`;
|
|
806
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
807
|
+
});
|
|
808
|
+
return bounded.filter(
|
|
809
|
+
(diagnostic, index) => index === 0 || JSON.stringify(diagnostic) !== JSON.stringify(bounded[index - 1])
|
|
810
|
+
).slice(0, 32);
|
|
811
|
+
}
|
|
812
|
+
function optionalDiagnosticField(value, key) {
|
|
813
|
+
if (!value || typeof value !== "object") return void 0;
|
|
814
|
+
const field = value[key];
|
|
815
|
+
return typeof field === "string" && field.trim() ? boundDiagnosticField(field) : void 0;
|
|
816
|
+
}
|
|
817
|
+
function optionalNestedDiagnosticField(value, key) {
|
|
818
|
+
if (!value || typeof value !== "object") return void 0;
|
|
819
|
+
return optionalDiagnosticField(value.details, key);
|
|
820
|
+
}
|
|
821
|
+
function boundDiagnosticField(value) {
|
|
822
|
+
return boundDiagnosticText(value.replace(/[^A-Za-z0-9_-]/g, ""), 64);
|
|
823
|
+
}
|
|
824
|
+
function boundDiagnosticText(value, limit) {
|
|
825
|
+
return value.length <= limit ? value : `${value.slice(0, limit - 1)}\u2026`;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/artifacts.ts
|
|
829
|
+
var tileflowArtifactPlanSchemaVersion = 1;
|
|
830
|
+
var tileflowArtifactInventoryFileName = ".tileflow-artifacts.json";
|
|
831
|
+
var TileflowHostedManifestOverwriteError = class extends Error {
|
|
832
|
+
code = "HOSTED_MANIFEST_OVERWRITE_REFUSED";
|
|
833
|
+
constructor(path) {
|
|
834
|
+
super(
|
|
835
|
+
`Refusing to overwrite Hosted manifest at ${path}. Remove it, choose another output directory, or explicitly set overwriteHostedManifest: true.`
|
|
836
|
+
);
|
|
837
|
+
this.name = "TileflowHostedManifestOverwriteError";
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
var TileflowLocalTilesetProductionError = class extends Error {
|
|
841
|
+
code = "TF_LOCAL_TILESET_PRODUCTION_UNRESOLVED";
|
|
842
|
+
constructor() {
|
|
843
|
+
super(
|
|
844
|
+
"Production builds cannot publish local PMTiles sources. Publish the tileset explicitly or provide an application-owned production source."
|
|
845
|
+
);
|
|
846
|
+
this.name = "TileflowLocalTilesetProductionError";
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
function createTileflowManifest(project, options = {}) {
|
|
850
|
+
return createManifest(project, options);
|
|
851
|
+
}
|
|
852
|
+
function createTileflowStyle(project, mapName, options = {}) {
|
|
853
|
+
const style = createStyleFromCatalog(project, mapName, options);
|
|
854
|
+
assertValidTileflowStyle(style, mapName);
|
|
855
|
+
return style;
|
|
856
|
+
}
|
|
857
|
+
function createTileflowStyles(project, options = {}) {
|
|
858
|
+
const { mapAssets, ...styleOptions } = options;
|
|
859
|
+
const styles = createStylesFromCatalog(project, {
|
|
860
|
+
...styleOptions,
|
|
861
|
+
mapAssets
|
|
862
|
+
});
|
|
863
|
+
assertValidTileflowStyles(project, styles);
|
|
864
|
+
return styles;
|
|
865
|
+
}
|
|
866
|
+
function createTileflowStylesWithInspection(project, options = {}) {
|
|
867
|
+
const { mapAssets, ...styleOptions } = options;
|
|
868
|
+
const inspected = createStylesFromCatalogWithInspection(project, {
|
|
869
|
+
...styleOptions,
|
|
870
|
+
mapAssets
|
|
871
|
+
});
|
|
872
|
+
const styles = Object.fromEntries(
|
|
873
|
+
Object.entries(inspected).map(([mapName, family]) => [
|
|
874
|
+
mapName,
|
|
875
|
+
Object.fromEntries(
|
|
876
|
+
Object.entries(family).map(([themeName, result]) => [themeName, result.style])
|
|
877
|
+
)
|
|
878
|
+
])
|
|
879
|
+
);
|
|
880
|
+
const inspections = Object.fromEntries(
|
|
881
|
+
Object.entries(inspected).map(([mapName, family]) => [
|
|
882
|
+
mapName,
|
|
883
|
+
Object.fromEntries(
|
|
884
|
+
Object.entries(family).map(([themeName, result]) => [themeName, result.inspection])
|
|
885
|
+
)
|
|
886
|
+
])
|
|
887
|
+
);
|
|
888
|
+
assertValidTileflowStyles(project, styles);
|
|
889
|
+
return { inspections, styles };
|
|
890
|
+
}
|
|
891
|
+
function assertValidTileflowStyles(project, styles) {
|
|
892
|
+
const issues = [];
|
|
893
|
+
for (const mapName of getTileflowMapNames(project).sort(compareCodeUnits2)) {
|
|
894
|
+
const family = styles[mapName];
|
|
895
|
+
if (!family) continue;
|
|
896
|
+
for (const themeName of Object.keys(family).sort(compareCodeUnits2)) {
|
|
897
|
+
const style = family[themeName];
|
|
898
|
+
issues.push(...validateTileflowStyle(style, `${mapName}/${themeName}`));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (issues.length > 0) {
|
|
902
|
+
throw new TileflowStyleValidationError(normalizeTileflowStyleValidationIssues(issues));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async function createTileflowArtifactPlan(prepared, options = {}) {
|
|
906
|
+
if (options.target === "production" && hasLocalTilesetSources(prepared.project)) {
|
|
907
|
+
throw new TileflowLocalTilesetProductionError();
|
|
908
|
+
}
|
|
909
|
+
const inspected = options.inspection ? createTileflowStylesWithInspection(prepared.project, {
|
|
910
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
911
|
+
mapAssets: prepared.mapAssets
|
|
912
|
+
}) : void 0;
|
|
913
|
+
const compiledStyles = inspected?.styles ?? createTileflowStyles(prepared.project, {
|
|
914
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
915
|
+
mapAssets: prepared.mapAssets
|
|
916
|
+
});
|
|
917
|
+
const localTilesets = await prepareTileflowLocalTilesets(prepared.project, compiledStyles, {
|
|
918
|
+
assetBaseUrl: resolveAssetBaseUrl(options),
|
|
919
|
+
baseDirectory: prepared.baseDirectory,
|
|
920
|
+
cwd: prepared.cwd
|
|
921
|
+
});
|
|
922
|
+
try {
|
|
923
|
+
const preparedFonts = await prepareTileflowStyleFonts(prepared.project, localTilesets.styles, {
|
|
924
|
+
assetBaseUrl: resolveAssetBaseUrl(options),
|
|
925
|
+
baseDirectory: prepared.baseDirectory,
|
|
926
|
+
cwd: prepared.cwd,
|
|
927
|
+
target: "local"
|
|
928
|
+
});
|
|
929
|
+
const styles = preparedFonts.styles;
|
|
930
|
+
const assets = [...prepared.assets, ...preparedFonts.assets];
|
|
931
|
+
const provenance = await createTileflowBuildProvenance(prepared.cwd);
|
|
932
|
+
const buildManifest = await createTileflowMapBuildManifest(
|
|
933
|
+
Object.fromEntries(
|
|
934
|
+
Object.keys(prepared.project.maps).sort(compareCodeUnits2).map((mapName) => {
|
|
935
|
+
const map = prepared.project.maps[mapName];
|
|
936
|
+
const style = styles[mapName];
|
|
937
|
+
return [
|
|
938
|
+
mapName,
|
|
939
|
+
{
|
|
940
|
+
assets: getMapBuildAssets(
|
|
941
|
+
mapName,
|
|
942
|
+
assets,
|
|
943
|
+
preparedFonts.bundles[mapName]?.files ?? []
|
|
944
|
+
),
|
|
945
|
+
lineage: prepared.project.mapMetadata?.[mapName]?.lineage ?? [
|
|
946
|
+
{ id: map.id, mapVersion: map.version }
|
|
947
|
+
],
|
|
948
|
+
map,
|
|
949
|
+
sourceAssets: {
|
|
950
|
+
fonts: preparedFonts.sourceIdentities[mapName] ?? [],
|
|
951
|
+
icons: prepared.mapIconSources[mapName] ?? []
|
|
952
|
+
},
|
|
953
|
+
styles: style
|
|
954
|
+
}
|
|
955
|
+
];
|
|
956
|
+
})
|
|
957
|
+
),
|
|
958
|
+
{ provenance }
|
|
959
|
+
);
|
|
960
|
+
const stableManifest = createFontAwareManifest(
|
|
961
|
+
createManifest(prepared.project, { styleBaseUrl: options.styleBaseUrl }),
|
|
962
|
+
styles
|
|
963
|
+
);
|
|
964
|
+
const inputs = {
|
|
965
|
+
directories: uniqueStrings(
|
|
966
|
+
[...prepared.watchPaths, ...preparedFonts.watchPaths].map(canonicalInputPath)
|
|
967
|
+
),
|
|
968
|
+
files: uniqueStrings(
|
|
969
|
+
[...options.inputFiles ?? [], ...localTilesets.watchPaths].map(canonicalInputPath)
|
|
970
|
+
)
|
|
971
|
+
};
|
|
972
|
+
const partial = {
|
|
973
|
+
assets,
|
|
974
|
+
buildManifest,
|
|
975
|
+
dispose: localTilesets.dispose,
|
|
976
|
+
manifest: stableManifest,
|
|
977
|
+
project: prepared.project,
|
|
978
|
+
...localTilesets.files.length > 0 ? { localTilesets: localTilesets.files } : {},
|
|
979
|
+
...inspected ? { styleInspections: inspected.inspections } : {},
|
|
980
|
+
styles,
|
|
981
|
+
watchPaths: uniqueStrings([...inputs.files, ...inputs.directories])
|
|
982
|
+
};
|
|
983
|
+
const generation = hashArtifactGeneration(getStableTileflowArtifactFiles(partial));
|
|
984
|
+
const manifest = createGenerationManifest(stableManifest, generation, assets);
|
|
985
|
+
const generationArtifacts = { ...partial, manifest };
|
|
986
|
+
return {
|
|
987
|
+
...generationArtifacts,
|
|
988
|
+
files: createGenerationArtifactFiles(generationArtifacts, generation),
|
|
989
|
+
inputs,
|
|
990
|
+
project: prepared.project,
|
|
991
|
+
schemaVersion: tileflowArtifactPlanSchemaVersion,
|
|
992
|
+
sourceProject: prepared.sourceProject
|
|
993
|
+
};
|
|
994
|
+
} catch (error) {
|
|
995
|
+
await localTilesets.dispose().catch(() => void 0);
|
|
996
|
+
throw error;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async function createTileflowBuildArtifacts(options = {}) {
|
|
1000
|
+
const loaded = await loadValidTileflowConfigWithInputs(
|
|
1001
|
+
options.config ?? defaultTileflowConfigPath,
|
|
1002
|
+
{
|
|
1003
|
+
cwd: options.cwd,
|
|
1004
|
+
fresh: true
|
|
1005
|
+
}
|
|
1006
|
+
);
|
|
1007
|
+
const prepared = await prepareTileflowCatalogIcons(loaded.project, {
|
|
1008
|
+
assetBaseUrl: resolveAssetBaseUrl(options),
|
|
1009
|
+
baseDirectory: dirname2(loaded.configFile),
|
|
1010
|
+
cwd: options.cwd ?? process.cwd()
|
|
1011
|
+
});
|
|
1012
|
+
return await createTileflowArtifactPlan(prepared, { ...options, inputFiles: loaded.inputFiles });
|
|
1013
|
+
}
|
|
1014
|
+
function getTileflowArtifactFiles(artifacts) {
|
|
1015
|
+
if ("files" in artifacts && Array.isArray(artifacts.files)) {
|
|
1016
|
+
return validateArtifactFiles(artifacts.files);
|
|
1017
|
+
}
|
|
1018
|
+
return getStableTileflowArtifactFiles(artifacts);
|
|
1019
|
+
}
|
|
1020
|
+
function getStableTileflowArtifactFiles(artifacts) {
|
|
1021
|
+
const files = [
|
|
1022
|
+
{
|
|
1023
|
+
contentType: "application/json; charset=utf-8",
|
|
1024
|
+
fileName: tileflowMapBuildManifestFileName,
|
|
1025
|
+
source: `${serializeCanonicalJson(artifacts.buildManifest)}
|
|
1026
|
+
`
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
contentType: "application/json; charset=utf-8",
|
|
1030
|
+
fileName: "manifest.json",
|
|
1031
|
+
source: `${JSON.stringify(artifacts.manifest, null, 2)}
|
|
1032
|
+
`
|
|
1033
|
+
},
|
|
1034
|
+
...Object.entries(artifacts.styles).flatMap(
|
|
1035
|
+
([mapName, themes]) => Object.entries(themes).map(([themeName, style]) => ({
|
|
1036
|
+
contentType: "application/json; charset=utf-8",
|
|
1037
|
+
fileName: `styles/${mapName}/${themeName}.json`,
|
|
1038
|
+
source: `${JSON.stringify(style, null, 2)}
|
|
1039
|
+
`
|
|
1040
|
+
}))
|
|
1041
|
+
),
|
|
1042
|
+
...artifacts.assets
|
|
1043
|
+
];
|
|
1044
|
+
return validateArtifactFiles(files);
|
|
1045
|
+
}
|
|
1046
|
+
function validateArtifactFiles(files) {
|
|
1047
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1048
|
+
for (const file of files) {
|
|
1049
|
+
assertTileflowArtifactFileName(file.fileName);
|
|
1050
|
+
if (file.fileName === tileflowArtifactInventoryFileName) {
|
|
1051
|
+
throw new Error(`${file.fileName} is reserved for the Tileflow artifact inventory.`);
|
|
1052
|
+
}
|
|
1053
|
+
if (seen.has(file.fileName)) {
|
|
1054
|
+
throw new Error(`Duplicate Tileflow artifact file: ${file.fileName}`);
|
|
1055
|
+
}
|
|
1056
|
+
seen.add(file.fileName);
|
|
1057
|
+
}
|
|
1058
|
+
return [...files].sort((left, right) => compareCodeUnits2(left.fileName, right.fileName));
|
|
1059
|
+
}
|
|
1060
|
+
function createGenerationManifest(manifest, generation, assets) {
|
|
1061
|
+
return {
|
|
1062
|
+
...manifest,
|
|
1063
|
+
maps: Object.fromEntries(
|
|
1064
|
+
Object.entries(manifest.maps).map(([mapName, map]) => [
|
|
1065
|
+
mapName,
|
|
1066
|
+
{
|
|
1067
|
+
...map,
|
|
1068
|
+
themes: Object.fromEntries(
|
|
1069
|
+
Object.entries(map.themes).map(([themeName, theme]) => [
|
|
1070
|
+
themeName,
|
|
1071
|
+
{
|
|
1072
|
+
...theme,
|
|
1073
|
+
styleUrl: insertGenerationInPublicUrl(
|
|
1074
|
+
theme.styleUrl,
|
|
1075
|
+
`styles/${mapName}/${themeName}.json`,
|
|
1076
|
+
generation
|
|
1077
|
+
),
|
|
1078
|
+
...theme.fontFaces ? {
|
|
1079
|
+
fontFaces: theme.fontFaces.map((fontFace) => ({
|
|
1080
|
+
...fontFace,
|
|
1081
|
+
source: retargetLocalFontAssetUrl(fontFace.source, assets, generation)
|
|
1082
|
+
}))
|
|
1083
|
+
} : {}
|
|
1084
|
+
}
|
|
1085
|
+
])
|
|
1086
|
+
)
|
|
1087
|
+
}
|
|
1088
|
+
])
|
|
1089
|
+
)
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function createGenerationArtifactFiles(artifacts, generation) {
|
|
1093
|
+
const prefix = `generations/${generation}`;
|
|
1094
|
+
const stableFiles = getStableTileflowArtifactFiles(artifacts);
|
|
1095
|
+
const immutableStyles = Object.entries(artifacts.styles).flatMap(
|
|
1096
|
+
([mapName, themes]) => Object.entries(themes).map(([themeName, style]) => {
|
|
1097
|
+
const hasLocalSprite = artifacts.assets.some(
|
|
1098
|
+
(asset) => asset.fileName.startsWith(`icons/${mapName}/`)
|
|
1099
|
+
);
|
|
1100
|
+
const sprite = style.sprite;
|
|
1101
|
+
const generationStyleWithSprite = hasLocalSprite && typeof sprite === "string" ? {
|
|
1102
|
+
...style,
|
|
1103
|
+
// Every theme in one logical map shares the same immutable sprite closure.
|
|
1104
|
+
sprite: isRelativePublicUrl(sprite) ? sprite : insertGenerationInPublicUrl(sprite, `icons/${mapName}/sprite`, generation)
|
|
1105
|
+
} : style;
|
|
1106
|
+
const generationStyle = replaceTileflowStyleFontSources(
|
|
1107
|
+
generationStyleWithSprite,
|
|
1108
|
+
(source) => retargetLocalFontAssetUrl(source, artifacts.assets, generation)
|
|
1109
|
+
);
|
|
1110
|
+
return {
|
|
1111
|
+
contentType: "application/json; charset=utf-8",
|
|
1112
|
+
fileName: `${prefix}/styles/${mapName}/${themeName}.json`,
|
|
1113
|
+
source: `${JSON.stringify(generationStyle, null, 2)}
|
|
1114
|
+
`
|
|
1115
|
+
};
|
|
1116
|
+
})
|
|
1117
|
+
);
|
|
1118
|
+
const immutableAssets = artifacts.assets.map(
|
|
1119
|
+
(asset) => ({ ...asset, fileName: `${prefix}/${asset.fileName}` })
|
|
1120
|
+
);
|
|
1121
|
+
return validateArtifactFiles([...stableFiles, ...immutableStyles, ...immutableAssets]);
|
|
1122
|
+
}
|
|
1123
|
+
function createFontAwareManifest(manifest, styles) {
|
|
1124
|
+
const maps = Object.fromEntries(
|
|
1125
|
+
Object.entries(manifest.maps).map(([mapName, map]) => [
|
|
1126
|
+
mapName,
|
|
1127
|
+
{
|
|
1128
|
+
...map,
|
|
1129
|
+
themes: Object.fromEntries(
|
|
1130
|
+
Object.entries(map.themes).map(([themeName, theme]) => {
|
|
1131
|
+
const style = styles[mapName]?.[themeName];
|
|
1132
|
+
if (!style) {
|
|
1133
|
+
throw new Error(`Missing compiled Tileflow style for ${mapName}/${themeName}.`);
|
|
1134
|
+
}
|
|
1135
|
+
const definitions = getTileflowStyleFontFaces(style);
|
|
1136
|
+
return [themeName, { ...theme, fontFaces: definitions }];
|
|
1137
|
+
})
|
|
1138
|
+
)
|
|
1139
|
+
}
|
|
1140
|
+
])
|
|
1141
|
+
);
|
|
1142
|
+
return parseTileflowRuntimeManifest({ ...manifest, maps });
|
|
1143
|
+
}
|
|
1144
|
+
function retargetLocalFontAssetUrl(source, assets, generation) {
|
|
1145
|
+
if (isRelativePublicUrl(source)) return source;
|
|
1146
|
+
const asset = assets.find(
|
|
1147
|
+
(candidate) => candidate.fileName.startsWith("fonts/") && (source === candidate.fileName || source.endsWith(`/${candidate.fileName}`))
|
|
1148
|
+
);
|
|
1149
|
+
if (!asset) throw new Error(`Unable to resolve Tileflow font artifact URL: ${source}`);
|
|
1150
|
+
return insertGenerationInPublicUrl(source, asset.fileName, generation);
|
|
1151
|
+
}
|
|
1152
|
+
function getMapBuildAssets(mapName, assets, fontAssets) {
|
|
1153
|
+
const selected = /* @__PURE__ */ new Map();
|
|
1154
|
+
for (const asset of assets) {
|
|
1155
|
+
if (asset.fileName.startsWith(`icons/${mapName}/`)) selected.set(asset.fileName, asset);
|
|
1156
|
+
}
|
|
1157
|
+
for (const asset of fontAssets) {
|
|
1158
|
+
selected.set(asset.fileName, asset);
|
|
1159
|
+
}
|
|
1160
|
+
return [...selected.values()].sort(
|
|
1161
|
+
(left, right) => compareCodeUnits2(left.fileName, right.fileName)
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
function hashBytes(source) {
|
|
1165
|
+
return createHash("sha256").update(typeof source === "string" ? Buffer.from(source, "utf8") : Buffer.from(source)).digest("hex");
|
|
1166
|
+
}
|
|
1167
|
+
async function createTileflowBuildProvenance(cwd) {
|
|
1168
|
+
const projectDirectory = resolve3(cwd);
|
|
1169
|
+
const projectRequire = createRequire(
|
|
1170
|
+
resolve3(projectDirectory, "__tileflow_build_provenance__.cjs")
|
|
1171
|
+
);
|
|
1172
|
+
const toolRequire = createRequire(import.meta.url);
|
|
1173
|
+
const packages = {};
|
|
1174
|
+
packages["@tileflow/core"] = await readPackageVersion(
|
|
1175
|
+
"@tileflow/core",
|
|
1176
|
+
resolvePackageJson(projectRequire, "@tileflow/core") ?? resolvePackageJson(toolRequire, "@tileflow/core")
|
|
1177
|
+
);
|
|
1178
|
+
packages["@tileflow/dev"] = await readPackageVersion(
|
|
1179
|
+
"@tileflow/dev",
|
|
1180
|
+
new URL("../package.json", import.meta.url)
|
|
1181
|
+
);
|
|
1182
|
+
const mapsPackage = resolvePackageJson(projectRequire, "@tileflow/maps");
|
|
1183
|
+
if (mapsPackage) {
|
|
1184
|
+
packages["@tileflow/maps"] = await readPackageVersion("@tileflow/maps", mapsPackage);
|
|
1185
|
+
}
|
|
1186
|
+
const lockfile = await findNearestLockfile(projectDirectory);
|
|
1187
|
+
return {
|
|
1188
|
+
...lockfile ? { lockfile: { format: lockfile.format, sha256: hashBytes(lockfile.source) } } : {},
|
|
1189
|
+
packages,
|
|
1190
|
+
schemaVersion: 1
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function resolvePackageJson(require2, packageName) {
|
|
1194
|
+
try {
|
|
1195
|
+
return require2.resolve(`${packageName}/package.json`);
|
|
1196
|
+
} catch {
|
|
1197
|
+
return void 0;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
async function readPackageVersion(expectedName, path) {
|
|
1201
|
+
if (!path) throw new Error(`Unable to resolve build package provenance for ${expectedName}.`);
|
|
1202
|
+
let parsed;
|
|
1203
|
+
try {
|
|
1204
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1205
|
+
} catch {
|
|
1206
|
+
throw new Error(`Unable to read build package provenance for ${expectedName}.`);
|
|
1207
|
+
}
|
|
1208
|
+
if (!parsed || typeof parsed !== "object" || parsed.name !== expectedName || typeof parsed.version !== "string") {
|
|
1209
|
+
throw new Error(`Invalid build package provenance for ${expectedName}.`);
|
|
1210
|
+
}
|
|
1211
|
+
return parsed.version;
|
|
1212
|
+
}
|
|
1213
|
+
async function findNearestLockfile(cwd) {
|
|
1214
|
+
const candidates = [
|
|
1215
|
+
{ fileName: "pnpm-lock.yaml", format: "pnpm" },
|
|
1216
|
+
{ fileName: "package-lock.json", format: "npm" },
|
|
1217
|
+
{ fileName: "yarn.lock", format: "yarn" },
|
|
1218
|
+
{ fileName: "bun.lock", format: "bun" },
|
|
1219
|
+
{ fileName: "bun.lockb", format: "bun" }
|
|
1220
|
+
];
|
|
1221
|
+
let directory = cwd;
|
|
1222
|
+
while (true) {
|
|
1223
|
+
for (const candidate of candidates) {
|
|
1224
|
+
try {
|
|
1225
|
+
return {
|
|
1226
|
+
format: candidate.format,
|
|
1227
|
+
source: await readFile(resolve3(directory, candidate.fileName))
|
|
1228
|
+
};
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
if (!hasErrorCode2(error, "ENOENT")) throw error;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const parent = dirname2(directory);
|
|
1234
|
+
if (parent === directory) return void 0;
|
|
1235
|
+
directory = parent;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function insertGenerationInPublicUrl(url, logicalPath, generation) {
|
|
1239
|
+
const marker = `/${logicalPath}`;
|
|
1240
|
+
const index = url.lastIndexOf(marker);
|
|
1241
|
+
if (index >= 0 && index + marker.length === url.length) {
|
|
1242
|
+
return `${url.slice(0, index)}/generations/${generation}${marker}`;
|
|
1243
|
+
}
|
|
1244
|
+
if (url === logicalPath) return `generations/${generation}/${logicalPath}`;
|
|
1245
|
+
throw new Error(`Unable to content-address Tileflow artifact URL: ${url}`);
|
|
1246
|
+
}
|
|
1247
|
+
function createTileflowArtifactSession(options = {}) {
|
|
1248
|
+
return createTileflowArtifactSessionWithBuilder(
|
|
1249
|
+
options,
|
|
1250
|
+
createTileflowBuildArtifacts,
|
|
1251
|
+
() => getTileflowWatchPaths(options)
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
async function getTileflowWatchPaths(options = {}) {
|
|
1255
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1256
|
+
const loaded = await loadValidTileflowConfigWithInputs(
|
|
1257
|
+
options.config ?? defaultTileflowConfigPath,
|
|
1258
|
+
{ cwd, fresh: true }
|
|
1259
|
+
);
|
|
1260
|
+
const [iconPaths, fontPaths] = await Promise.all([
|
|
1261
|
+
getTileflowIconWatchPaths(loaded.project, cwd, dirname2(loaded.configFile)),
|
|
1262
|
+
getTileflowFontWatchPaths(loaded.project, cwd, dirname2(loaded.configFile))
|
|
1263
|
+
]);
|
|
1264
|
+
return uniqueStrings([...loaded.inputFiles, ...iconPaths, ...fontPaths]);
|
|
1265
|
+
}
|
|
1266
|
+
function isTileflowArtifactInputPath(inputs, path) {
|
|
1267
|
+
const candidate = canonicalInputPath(path);
|
|
1268
|
+
return inputs.files.some((input) => canonicalInputPath(input) === candidate) || inputs.directories.some((input) => isPathWithin(canonicalInputPath(input), candidate));
|
|
1269
|
+
}
|
|
1270
|
+
async function refreshTileflowArtifactSession(session, options) {
|
|
1271
|
+
const inputs = getArtifactInputGraph(session.getLastGoodArtifacts());
|
|
1272
|
+
if (options.inputPath && inputs && !isTileflowArtifactInputPath(inputs, options.inputPath)) {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
await session.refresh(options.reason);
|
|
1276
|
+
return true;
|
|
1277
|
+
}
|
|
1278
|
+
function getArtifactInputGraph(artifacts) {
|
|
1279
|
+
if (!artifacts || !("inputs" in artifacts)) return void 0;
|
|
1280
|
+
const inputs = artifacts.inputs;
|
|
1281
|
+
return inputs && Array.isArray(inputs.files) && Array.isArray(inputs.directories) ? inputs : void 0;
|
|
1282
|
+
}
|
|
1283
|
+
async function writeTileflowBuildArtifacts(options) {
|
|
1284
|
+
const artifacts = await createTileflowBuildArtifacts({ ...options, target: "production" });
|
|
1285
|
+
try {
|
|
1286
|
+
await writeTileflowArtifactPlan(artifacts, options);
|
|
1287
|
+
return { ...artifacts, dispose: void 0, localTilesets: void 0 };
|
|
1288
|
+
} finally {
|
|
1289
|
+
await disposeTileflowBuildArtifacts(artifacts);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
async function disposeTileflowBuildArtifacts(artifacts) {
|
|
1293
|
+
const dispose = artifacts.dispose;
|
|
1294
|
+
artifacts.dispose = void 0;
|
|
1295
|
+
await dispose?.();
|
|
1296
|
+
}
|
|
1297
|
+
async function writeTileflowArtifactPlan(artifacts, options) {
|
|
1298
|
+
assertNoLocalTilesetsForProduction(artifacts);
|
|
1299
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1300
|
+
const output = await prepareTileflowOutputDirectory(cwd, options.outDir);
|
|
1301
|
+
const files = getTileflowArtifactFiles(artifacts);
|
|
1302
|
+
await assertTileflowSelfHostedManifestTarget(resolve3(output.outDir, "manifest.json"), options);
|
|
1303
|
+
await commitTileflowArtifactFiles(output, files);
|
|
1304
|
+
}
|
|
1305
|
+
function assertNoLocalTilesetsForProduction(artifacts) {
|
|
1306
|
+
if (artifacts.localTilesets?.length) throw new TileflowLocalTilesetProductionError();
|
|
1307
|
+
}
|
|
1308
|
+
function hasLocalTilesetSources(project) {
|
|
1309
|
+
return Object.values(project.maps).some((map) => Object.keys(map.sources ?? {}).length > 0);
|
|
1310
|
+
}
|
|
1311
|
+
async function assertTileflowSelfHostedManifestTarget(manifestPath, options = {}) {
|
|
1312
|
+
if (options.overwriteHostedManifest) return;
|
|
1313
|
+
let source;
|
|
1314
|
+
try {
|
|
1315
|
+
source = await readFile(resolve3(manifestPath), "utf8");
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
if (hasErrorCode2(error, "ENOENT")) return;
|
|
1318
|
+
throw error;
|
|
1319
|
+
}
|
|
1320
|
+
try {
|
|
1321
|
+
const manifest = parseTileflowRuntimeManifest(JSON.parse(source));
|
|
1322
|
+
if (isHostedRuntimeManifest(manifest)) {
|
|
1323
|
+
throw new TileflowHostedManifestOverwriteError(resolve3(manifestPath));
|
|
1324
|
+
}
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
if (error instanceof TileflowHostedManifestOverwriteError) throw error;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
function isHostedRuntimeManifest(manifest) {
|
|
1330
|
+
return Boolean(
|
|
1331
|
+
manifest.apiUrl || Object.values(manifest.maps).some(
|
|
1332
|
+
(map) => map.apiUrl || map.environment || map.mapId || map.usageMode || map.worldGeneration || Object.values(map.themes).some((theme) => theme.styleId)
|
|
1333
|
+
)
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
function assertValidTileflowStyle(style, mapName) {
|
|
1337
|
+
const issues = validateTileflowStyle(style, mapName);
|
|
1338
|
+
if (issues.length > 0) {
|
|
1339
|
+
throw new TileflowStyleValidationError(normalizeTileflowStyleValidationIssues(issues));
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
async function commitTileflowArtifactFiles(output, files) {
|
|
1343
|
+
await assertTileflowOutputDirectoryIsSafe(output);
|
|
1344
|
+
const { outDir } = output;
|
|
1345
|
+
const previous = await readTileflowArtifactInventory(outDir);
|
|
1346
|
+
const desiredNames = files.map((file) => file.fileName);
|
|
1347
|
+
const generation = getArtifactGeneration(files);
|
|
1348
|
+
const retainedGenerations = previous ? generation === previous.generation ? getManagedGenerations(previous.files) : [previous.generation] : [];
|
|
1349
|
+
const retainedPrefixes = retainedGenerations.map((item) => `generations/${item}/`);
|
|
1350
|
+
const retainedGenerationNames = previous ? previous.files.filter((name) => retainedPrefixes.some((prefix) => name.startsWith(prefix))) : [];
|
|
1351
|
+
const retainedNames = uniqueStrings(retainedGenerationNames);
|
|
1352
|
+
const inventoryNames = uniqueStrings([...desiredNames, ...retainedNames]);
|
|
1353
|
+
const inventory = {
|
|
1354
|
+
files: inventoryNames,
|
|
1355
|
+
generation,
|
|
1356
|
+
schemaVersion: 1
|
|
1357
|
+
};
|
|
1358
|
+
const inventoryFile = {
|
|
1359
|
+
contentType: "application/json; charset=utf-8",
|
|
1360
|
+
fileName: tileflowArtifactInventoryFileName,
|
|
1361
|
+
source: `${JSON.stringify(inventory, null, 2)}
|
|
1362
|
+
`
|
|
1363
|
+
};
|
|
1364
|
+
const stagedFiles = [...files, inventoryFile];
|
|
1365
|
+
const staleNames = (previous?.files ?? []).filter((name) => !inventoryNames.includes(name));
|
|
1366
|
+
const affectedNames = uniqueStrings([
|
|
1367
|
+
...desiredNames,
|
|
1368
|
+
...staleNames,
|
|
1369
|
+
tileflowArtifactInventoryFileName
|
|
1370
|
+
]);
|
|
1371
|
+
const stageDir = await mkdtemp(resolve3(outDir, ".tileflow-stage-"));
|
|
1372
|
+
const newDir = resolve3(stageDir, "new");
|
|
1373
|
+
const backupDir = resolve3(stageDir, "backup");
|
|
1374
|
+
const backedUp = /* @__PURE__ */ new Set();
|
|
1375
|
+
const installed = /* @__PURE__ */ new Set();
|
|
1376
|
+
try {
|
|
1377
|
+
for (const file of stagedFiles) {
|
|
1378
|
+
const stagePath = managedPath(newDir, file.fileName);
|
|
1379
|
+
await mkdir2(dirname2(stagePath), { recursive: true });
|
|
1380
|
+
await writeFileDurably(stagePath, file.source);
|
|
1381
|
+
}
|
|
1382
|
+
for (const name of affectedNames) {
|
|
1383
|
+
await assertManagedTargetIsSafe(outDir, name);
|
|
1384
|
+
const targetPath = managedPath(outDir, name);
|
|
1385
|
+
if (!await isFile(targetPath)) continue;
|
|
1386
|
+
const backupPath = managedPath(backupDir, name);
|
|
1387
|
+
await mkdir2(dirname2(backupPath), { recursive: true });
|
|
1388
|
+
await copyFile2(targetPath, backupPath);
|
|
1389
|
+
backedUp.add(name);
|
|
1390
|
+
}
|
|
1391
|
+
const commitOrder = stagedFiles.map((file) => file.fileName).sort((left, right) => {
|
|
1392
|
+
const leftRank = commitRank(left);
|
|
1393
|
+
const rightRank = commitRank(right);
|
|
1394
|
+
return leftRank - rightRank || compareCodeUnits2(left, right);
|
|
1395
|
+
});
|
|
1396
|
+
for (const name of commitOrder.filter(
|
|
1397
|
+
(candidate) => candidate !== tileflowArtifactInventoryFileName
|
|
1398
|
+
)) {
|
|
1399
|
+
const sourcePath = managedPath(newDir, name);
|
|
1400
|
+
const targetPath = managedPath(outDir, name);
|
|
1401
|
+
await mkdir2(dirname2(targetPath), { recursive: true });
|
|
1402
|
+
await rename(sourcePath, targetPath);
|
|
1403
|
+
installed.add(name);
|
|
1404
|
+
}
|
|
1405
|
+
for (const name of staleNames) {
|
|
1406
|
+
await removeManagedFile(managedPath(outDir, name));
|
|
1407
|
+
}
|
|
1408
|
+
const stagedInventoryPath = managedPath(newDir, tileflowArtifactInventoryFileName);
|
|
1409
|
+
const inventoryPath = managedPath(outDir, tileflowArtifactInventoryFileName);
|
|
1410
|
+
await rename(stagedInventoryPath, inventoryPath);
|
|
1411
|
+
installed.add(tileflowArtifactInventoryFileName);
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
await rollbackManagedFiles(outDir, backupDir, affectedNames, backedUp, installed);
|
|
1414
|
+
throw error;
|
|
1415
|
+
} finally {
|
|
1416
|
+
await rm2(stageDir, { force: true, recursive: true });
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
function getArtifactGeneration(files) {
|
|
1420
|
+
const generations = getManagedGenerations(files.map((file) => file.fileName));
|
|
1421
|
+
if (generations.length !== 1) {
|
|
1422
|
+
throw new Error("A Tileflow artifact plan must contain exactly one content generation.");
|
|
1423
|
+
}
|
|
1424
|
+
return generations[0];
|
|
1425
|
+
}
|
|
1426
|
+
function getManagedGenerations(fileNames) {
|
|
1427
|
+
return uniqueStrings(
|
|
1428
|
+
fileNames.flatMap((fileName) => {
|
|
1429
|
+
const match = /^generations\/([a-f0-9]{64})\//.exec(fileName);
|
|
1430
|
+
return match?.[1] ? [match[1]] : [];
|
|
1431
|
+
})
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
function commitRank(fileName) {
|
|
1435
|
+
if (fileName === tileflowArtifactInventoryFileName) return 2;
|
|
1436
|
+
if (fileName === "manifest.json") return 1;
|
|
1437
|
+
return 0;
|
|
1438
|
+
}
|
|
1439
|
+
async function rollbackManagedFiles(outDir, backupDir, affectedNames, backedUp, installed) {
|
|
1440
|
+
const rollbackErrors = [];
|
|
1441
|
+
for (const name of [...affectedNames].reverse()) {
|
|
1442
|
+
try {
|
|
1443
|
+
const targetPath = managedPath(outDir, name);
|
|
1444
|
+
if (installed.has(name) && !backedUp.has(name)) {
|
|
1445
|
+
await removeManagedFile(targetPath);
|
|
1446
|
+
}
|
|
1447
|
+
if (backedUp.has(name)) {
|
|
1448
|
+
await mkdir2(dirname2(targetPath), { recursive: true });
|
|
1449
|
+
await copyFile2(managedPath(backupDir, name), targetPath);
|
|
1450
|
+
}
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
rollbackErrors.push(error);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (rollbackErrors.length > 0) {
|
|
1456
|
+
throw new AggregateError(rollbackErrors, "Unable to restore the previous Tileflow generation.");
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
async function readTileflowArtifactInventory(outDir) {
|
|
1460
|
+
const inventoryPath = managedPath(outDir, tileflowArtifactInventoryFileName);
|
|
1461
|
+
let source;
|
|
1462
|
+
try {
|
|
1463
|
+
source = await readFile(inventoryPath, "utf8");
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
if (hasErrorCode2(error, "ENOENT")) return void 0;
|
|
1466
|
+
throw error;
|
|
1467
|
+
}
|
|
1468
|
+
let candidate;
|
|
1469
|
+
try {
|
|
1470
|
+
candidate = JSON.parse(source);
|
|
1471
|
+
} catch {
|
|
1472
|
+
throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
|
|
1473
|
+
}
|
|
1474
|
+
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) {
|
|
1475
|
+
throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
|
|
1476
|
+
}
|
|
1477
|
+
const files = candidate.files;
|
|
1478
|
+
if (!files.every((name) => typeof name === "string")) {
|
|
1479
|
+
throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
|
|
1480
|
+
}
|
|
1481
|
+
try {
|
|
1482
|
+
for (const name of files) assertTileflowArtifactFileName(name);
|
|
1483
|
+
} catch {
|
|
1484
|
+
throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
|
|
1485
|
+
}
|
|
1486
|
+
if (new Set(files).size !== files.length || files.includes(tileflowArtifactInventoryFileName)) {
|
|
1487
|
+
throw new Error(`Invalid Tileflow artifact inventory: ${inventoryPath}`);
|
|
1488
|
+
}
|
|
1489
|
+
return {
|
|
1490
|
+
files: [...files],
|
|
1491
|
+
generation: candidate.generation,
|
|
1492
|
+
schemaVersion: 1
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
async function assertManagedTargetIsSafe(outDir, fileName) {
|
|
1496
|
+
assertSafeManagedFileName(fileName);
|
|
1497
|
+
const rootStats = await lstat2(outDir);
|
|
1498
|
+
if (rootStats.isSymbolicLink()) {
|
|
1499
|
+
throw new Error(`Tileflow refuses to write through a symbolic link: ${outDir}`);
|
|
1500
|
+
}
|
|
1501
|
+
if (!rootStats.isDirectory()) {
|
|
1502
|
+
throw new Error(`Tileflow artifact output is not a directory: ${outDir}`);
|
|
1503
|
+
}
|
|
1504
|
+
const segments = fileName.split("/");
|
|
1505
|
+
let current = outDir;
|
|
1506
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
1507
|
+
current = resolve3(current, segments[index]);
|
|
1508
|
+
try {
|
|
1509
|
+
const stats = await lstat2(current);
|
|
1510
|
+
if (stats.isSymbolicLink()) {
|
|
1511
|
+
throw new Error(`Tileflow refuses to write through a symbolic link: ${fileName}`);
|
|
1512
|
+
}
|
|
1513
|
+
if (index < segments.length - 1 && !stats.isDirectory()) {
|
|
1514
|
+
throw new Error(`Tileflow artifact parent is not a directory: ${fileName}`);
|
|
1515
|
+
}
|
|
1516
|
+
if (index === segments.length - 1 && !stats.isFile()) {
|
|
1517
|
+
throw new Error(`Tileflow artifact target is not a file: ${fileName}`);
|
|
1518
|
+
}
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
if (hasErrorCode2(error, "ENOENT")) return;
|
|
1521
|
+
throw error;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
async function prepareTileflowOutputDirectory(cwd, requestedOutDir) {
|
|
1526
|
+
const logicalRoot = resolve3(cwd);
|
|
1527
|
+
const logicalOutDir = resolve3(logicalRoot, requestedOutDir);
|
|
1528
|
+
if (!isPathWithin(logicalRoot, logicalOutDir)) {
|
|
1529
|
+
throw new Error(`Tileflow artifact output escapes its working directory: ${logicalOutDir}`);
|
|
1530
|
+
}
|
|
1531
|
+
const rootStats = await lstat2(logicalRoot);
|
|
1532
|
+
if (!rootStats.isDirectory()) {
|
|
1533
|
+
throw new Error(`Tileflow artifact working directory is not a directory: ${logicalRoot}`);
|
|
1534
|
+
}
|
|
1535
|
+
const rootDir = await realpath2(logicalRoot);
|
|
1536
|
+
const relativeOutDir = relative2(logicalRoot, logicalOutDir);
|
|
1537
|
+
const segments = relativeOutDir ? relativeOutDir.split(sep2) : [];
|
|
1538
|
+
let current = rootDir;
|
|
1539
|
+
for (const segment of segments) {
|
|
1540
|
+
current = resolve3(current, segment);
|
|
1541
|
+
let stats;
|
|
1542
|
+
try {
|
|
1543
|
+
stats = await lstat2(current);
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
if (!hasErrorCode2(error, "ENOENT")) throw error;
|
|
1546
|
+
await mkdir2(current);
|
|
1547
|
+
stats = await lstat2(current);
|
|
1548
|
+
}
|
|
1549
|
+
if (stats.isSymbolicLink()) {
|
|
1550
|
+
throw new Error(`Tileflow refuses to write through a symbolic link: ${current}`);
|
|
1551
|
+
}
|
|
1552
|
+
if (!stats.isDirectory()) {
|
|
1553
|
+
throw new Error(`Tileflow artifact output ancestor is not a directory: ${current}`);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const outDir = await realpath2(current);
|
|
1557
|
+
const output = { outDir, rootDir };
|
|
1558
|
+
await assertTileflowOutputDirectoryIsSafe(output);
|
|
1559
|
+
return output;
|
|
1560
|
+
}
|
|
1561
|
+
async function assertTileflowOutputDirectoryIsSafe(output) {
|
|
1562
|
+
if (!isPathWithin(output.rootDir, output.outDir)) {
|
|
1563
|
+
throw new Error(`Tileflow artifact output escapes its working directory: ${output.outDir}`);
|
|
1564
|
+
}
|
|
1565
|
+
const stats = await lstat2(output.outDir);
|
|
1566
|
+
if (stats.isSymbolicLink()) {
|
|
1567
|
+
throw new Error(`Tileflow refuses to write through a symbolic link: ${output.outDir}`);
|
|
1568
|
+
}
|
|
1569
|
+
if (!stats.isDirectory()) {
|
|
1570
|
+
throw new Error(`Tileflow artifact output is not a directory: ${output.outDir}`);
|
|
1571
|
+
}
|
|
1572
|
+
const canonicalOutDir = await realpath2(output.outDir);
|
|
1573
|
+
if (canonicalOutDir !== output.outDir || !isPathWithin(output.rootDir, canonicalOutDir)) {
|
|
1574
|
+
throw new Error(`Tileflow artifact output escapes its working directory: ${output.outDir}`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function assertSafeManagedFileName(fileName) {
|
|
1578
|
+
if (!fileName || fileName.includes("\\") || fileName.includes("\0") || fileName.startsWith("/") || fileName.endsWith("/") || fileName.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
|
|
1579
|
+
throw new Error(`Unsafe Tileflow artifact file name: ${fileName}`);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
function assertTileflowArtifactFileName(fileName) {
|
|
1583
|
+
assertSafeManagedFileName(fileName);
|
|
1584
|
+
const logicalName = fileName.replace(/^generations\/[a-f0-9]{64}\//, "");
|
|
1585
|
+
const styleMatch = /^styles\/([^/]+)\/([^/]+)\.json$/u.exec(logicalName);
|
|
1586
|
+
const iconMatch = /^icons\/([^/]+)\/sprite(?:@2x)?\.(?:json|png)$/u.exec(logicalName);
|
|
1587
|
+
if (fileName === "manifest.json" || fileName === tileflowMapBuildManifestFileName || styleMatch && tileflowMapIdSchema.safeParse(styleMatch[1]).success && tileflowThemeNameSchema.safeParse(styleMatch[2]).success || iconMatch && tileflowMapIdSchema.safeParse(iconMatch[1]).success || /^tilesets\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.pmtiles$/u.test(logicalName) || /^fonts\/[a-z0-9]+(?:-[a-z0-9]+)*-[a-f0-9]{64}\.(?:otf|ttf|woff2)$/.test(logicalName) || /^fonts\/licenses\/license-[a-f0-9]{64}\.txt$/.test(logicalName)) {
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
throw new Error(`Unexpected Tileflow managed artifact file name: ${fileName}`);
|
|
1591
|
+
}
|
|
1592
|
+
function managedPath(root, fileName) {
|
|
1593
|
+
assertSafeManagedFileName(fileName);
|
|
1594
|
+
const path = resolve3(root, ...fileName.split("/"));
|
|
1595
|
+
if (!isPathWithin(root, path)) {
|
|
1596
|
+
throw new Error(`Unsafe Tileflow artifact file name: ${fileName}`);
|
|
1597
|
+
}
|
|
1598
|
+
return path;
|
|
1599
|
+
}
|
|
1600
|
+
async function writeFileDurably(path, source) {
|
|
1601
|
+
const handle = await open2(path, "wx");
|
|
1602
|
+
try {
|
|
1603
|
+
await handle.writeFile(source);
|
|
1604
|
+
await handle.sync();
|
|
1605
|
+
} finally {
|
|
1606
|
+
await handle.close();
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
async function isFile(path) {
|
|
1610
|
+
try {
|
|
1611
|
+
return (await lstat2(path)).isFile();
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
if (hasErrorCode2(error, "ENOENT")) return false;
|
|
1614
|
+
throw error;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
async function removeManagedFile(path) {
|
|
1618
|
+
try {
|
|
1619
|
+
const stats = await lstat2(path);
|
|
1620
|
+
if (!stats.isFile()) throw new Error(`Refusing to remove non-file artifact target: ${path}`);
|
|
1621
|
+
await unlink(path);
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
if (!hasErrorCode2(error, "ENOENT")) throw error;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function hashArtifactGeneration(files) {
|
|
1627
|
+
const hash = createHash("sha256");
|
|
1628
|
+
for (const file of files) {
|
|
1629
|
+
const source = typeof file.source === "string" ? Buffer.from(file.source, "utf8") : Buffer.from(file.source);
|
|
1630
|
+
hash.update(`${file.fileName}\0${file.contentType}\0${source.byteLength}\0`, "utf8");
|
|
1631
|
+
hash.update(source);
|
|
1632
|
+
}
|
|
1633
|
+
return hash.digest("hex");
|
|
1634
|
+
}
|
|
1635
|
+
function resolveAssetBaseUrl(options) {
|
|
1636
|
+
if (options.assetBaseUrl !== void 0) return options.assetBaseUrl;
|
|
1637
|
+
if (options.styleBaseUrl && !isRelativePublicUrl(options.styleBaseUrl)) {
|
|
1638
|
+
return options.styleBaseUrl;
|
|
1639
|
+
}
|
|
1640
|
+
return "../..";
|
|
1641
|
+
}
|
|
1642
|
+
function isRelativePublicUrl(value) {
|
|
1643
|
+
return !(value.startsWith("http://") || value.startsWith("https://") || value.startsWith("/") || value.startsWith("data:"));
|
|
1644
|
+
}
|
|
1645
|
+
function uniqueStrings(values) {
|
|
1646
|
+
return [...new Set(values)].sort(compareCodeUnits2);
|
|
1647
|
+
}
|
|
1648
|
+
function hasErrorCode2(error, code) {
|
|
1649
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
|
|
1650
|
+
}
|
|
1651
|
+
function canonicalInputPath(path) {
|
|
1652
|
+
const resolved = resolve3(path);
|
|
1653
|
+
try {
|
|
1654
|
+
return realpathSync2.native(resolved);
|
|
1655
|
+
} catch {
|
|
1656
|
+
try {
|
|
1657
|
+
return resolve3(realpathSync2.native(dirname2(resolved)), resolved.split(/[\\/]/).at(-1));
|
|
1658
|
+
} catch {
|
|
1659
|
+
return resolved;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
export {
|
|
1665
|
+
prepareTileflowLocalTilesets,
|
|
1666
|
+
tileflowArtifactSessionSchemaVersion,
|
|
1667
|
+
createTileflowArtifactDiagnostics,
|
|
1668
|
+
tileflowArtifactPlanSchemaVersion,
|
|
1669
|
+
tileflowArtifactInventoryFileName,
|
|
1670
|
+
TileflowHostedManifestOverwriteError,
|
|
1671
|
+
TileflowLocalTilesetProductionError,
|
|
1672
|
+
createTileflowManifest,
|
|
1673
|
+
createTileflowStyle,
|
|
1674
|
+
createTileflowStyles,
|
|
1675
|
+
createTileflowArtifactPlan,
|
|
1676
|
+
createTileflowBuildArtifacts,
|
|
1677
|
+
getTileflowArtifactFiles,
|
|
1678
|
+
createTileflowBuildProvenance,
|
|
1679
|
+
createTileflowArtifactSession,
|
|
1680
|
+
getTileflowWatchPaths,
|
|
1681
|
+
isTileflowArtifactInputPath,
|
|
1682
|
+
refreshTileflowArtifactSession,
|
|
1683
|
+
writeTileflowBuildArtifacts,
|
|
1684
|
+
disposeTileflowBuildArtifacts,
|
|
1685
|
+
writeTileflowArtifactPlan,
|
|
1686
|
+
assertNoLocalTilesetsForProduction,
|
|
1687
|
+
assertTileflowSelfHostedManifestTarget
|
|
1688
|
+
};
|