@tileflow/dev 0.1.0-alpha.23 → 0.1.0-alpha.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -515
- package/dist/artifacts.d.ts +9 -2
- package/dist/artifacts.js +4 -4
- package/dist/{chunk-G5FUSWNN.js → chunk-3WCEHA27.js} +2 -2
- package/dist/{chunk-BL3FK4GO.js → chunk-7C4F6GAX.js} +1 -1
- package/dist/chunk-D2QWYYIF.js +1605 -0
- package/dist/{chunk-RSBV4L2Y.js → chunk-UTRO4IBQ.js} +1 -1
- package/dist/{chunk-XTRHREXO.js → chunk-W3X2JXYS.js} +11 -5
- package/dist/fonts.js +2 -2
- package/dist/icons.d.ts +213 -20
- package/dist/icons.js +24 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +26 -6
- package/dist/server.js +5 -5
- package/docs/artifact-lifecycle.md +153 -0
- package/docs/bounded-pmtiles-resource-inspection.md +21 -0
- package/docs/bounded-vector-feature-inspection.md +101 -0
- package/docs/local-comparison-workbench-and-compiler-inspection.md +250 -0
- package/docs/structured-config-inspection-and-diagnostics.md +39 -0
- package/package.json +4 -3
- package/dist/chunk-CENQR5Q2.js +0 -844
package/dist/chunk-CENQR5Q2.js
DELETED
|
@@ -1,844 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
TileflowAssetDirectoryError,
|
|
3
|
-
resolveTileflowAssetDirectories
|
|
4
|
-
} from "./chunk-BL3FK4GO.js";
|
|
5
|
-
|
|
6
|
-
// src/icons.ts
|
|
7
|
-
import { readdir, readFile, realpath, stat } from "fs/promises";
|
|
8
|
-
import { extname, isAbsolute, relative, resolve, sep } from "path";
|
|
9
|
-
import { SaxesParser } from "saxes";
|
|
10
|
-
import {
|
|
11
|
-
compareCodeUnits,
|
|
12
|
-
hashTileflowIconPackageManifest,
|
|
13
|
-
hashTileflowRenderedIconPixels,
|
|
14
|
-
parseResolvedTileflowMap,
|
|
15
|
-
serializeCanonicalJson,
|
|
16
|
-
sha256Hex,
|
|
17
|
-
tileflowIconIdPattern,
|
|
18
|
-
tileflowIconPackageLimits,
|
|
19
|
-
tileflowIconPackageManifestSchema
|
|
20
|
-
} from "@tileflow/core";
|
|
21
|
-
var TileflowIconCompilationError = class extends Error {
|
|
22
|
-
code = "ICON_INVALID";
|
|
23
|
-
issues;
|
|
24
|
-
phase = "icon-compilation";
|
|
25
|
-
constructor(issues) {
|
|
26
|
-
super(
|
|
27
|
-
[
|
|
28
|
-
"Invalid Tileflow icon package",
|
|
29
|
-
...issues.map((issue) => `- ${issue.path}: ${issue.message}`)
|
|
30
|
-
].join("\n")
|
|
31
|
-
);
|
|
32
|
-
this.name = "TileflowIconCompilationError";
|
|
33
|
-
this.issues = issues;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
var iconFileExtensions = /* @__PURE__ */ new Set([".svg", ".png", ".jpg", ".jpeg", ".webp"]);
|
|
37
|
-
var iconSpriteSize = 24;
|
|
38
|
-
async function compileTileflowIconPackages(project, options) {
|
|
39
|
-
const result = await compileIconSources(project, options);
|
|
40
|
-
const packagesByHash = /* @__PURE__ */ new Map();
|
|
41
|
-
for (const compiled of result.compiledBySequence.values()) {
|
|
42
|
-
packagesByHash.set(compiled.package.contentHash, compiled.package);
|
|
43
|
-
}
|
|
44
|
-
const sourceIdentities = {};
|
|
45
|
-
for (const request of result.mapRequests) {
|
|
46
|
-
const compiled = result.compiledBySequence.get(request.sequenceKey);
|
|
47
|
-
sourceIdentities[request.mapName] = compiled?.icons.map((icon) => ({
|
|
48
|
-
format: icon.input.format,
|
|
49
|
-
id: icon.input.name,
|
|
50
|
-
kind: icon.input.kind,
|
|
51
|
-
sha256: icon.sourceSha256
|
|
52
|
-
})) ?? [];
|
|
53
|
-
}
|
|
54
|
-
const bindings = result.mapRequests.flatMap((request) => {
|
|
55
|
-
if (request.directories.length === 0) return [];
|
|
56
|
-
const compiled = result.compiledBySequence.get(request.sequenceKey);
|
|
57
|
-
if (!compiled) {
|
|
58
|
-
throw new Error(`Missing compiled icon package for map ${request.mapName}`);
|
|
59
|
-
}
|
|
60
|
-
return [
|
|
61
|
-
{
|
|
62
|
-
iconIds: compiled.package.manifest.iconNames,
|
|
63
|
-
label: request.mapName,
|
|
64
|
-
mapName: request.mapName,
|
|
65
|
-
packageHash: compiled.package.contentHash
|
|
66
|
-
}
|
|
67
|
-
];
|
|
68
|
-
});
|
|
69
|
-
return {
|
|
70
|
-
bindings,
|
|
71
|
-
packages: [...packagesByHash.values()].sort(
|
|
72
|
-
(left, right) => compareCodeUnits(left.contentHash, right.contentHash)
|
|
73
|
-
),
|
|
74
|
-
sourceIdentities,
|
|
75
|
-
watchPaths: uniqueStrings(
|
|
76
|
-
result.mapRequests.flatMap(
|
|
77
|
-
(request) => request.directories.filter((directory) => directory.watch).map((directory) => directory.realPath)
|
|
78
|
-
)
|
|
79
|
-
).sort(compareCodeUnits)
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
async function inspectTileflowIconCatalogs(project, options) {
|
|
83
|
-
const result = await compileIconSources(project, {
|
|
84
|
-
baseDirectory: options.baseDirectory,
|
|
85
|
-
cwd: options.cwd,
|
|
86
|
-
mapNames: options.mapNames,
|
|
87
|
-
target: "local"
|
|
88
|
-
});
|
|
89
|
-
const maps = result.mapRequests.map((request) => {
|
|
90
|
-
if (request.directories.length === 0) return { name: request.mapName, icons: { kind: "none" } };
|
|
91
|
-
const compiled = result.compiledBySequence.get(request.sequenceKey);
|
|
92
|
-
if (!compiled) throw new Error(`Missing compiled icon catalog for map ${request.mapName}`);
|
|
93
|
-
return {
|
|
94
|
-
name: request.mapName,
|
|
95
|
-
icons: {
|
|
96
|
-
directories: request.directories.map(describeAssetDirectory),
|
|
97
|
-
iconIds: [...compiled.package.manifest.iconNames],
|
|
98
|
-
kind: "directories",
|
|
99
|
-
label: request.mapName,
|
|
100
|
-
packageHash: compiled.package.contentHash
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
});
|
|
104
|
-
const requestBySequence = new Map(
|
|
105
|
-
result.mapRequests.filter((request) => request.directories.length > 0).map((request) => [request.sequenceKey, request])
|
|
106
|
-
);
|
|
107
|
-
const realCwd = await realpath(options.cwd);
|
|
108
|
-
const catalogs = [...result.compiledBySequence.entries()].map(([sequenceKey, compiled]) => {
|
|
109
|
-
const request = requestBySequence.get(sequenceKey);
|
|
110
|
-
if (!request) throw new Error(`Missing icon directory sequence ${sequenceKey}`);
|
|
111
|
-
return {
|
|
112
|
-
compiledPackage: compiled.package,
|
|
113
|
-
directories: request.directories.map(describeAssetDirectory),
|
|
114
|
-
icons: compiled.icons.map((icon, index) => {
|
|
115
|
-
const manifestIcon = compiled.package.manifest.renderedIcons[index];
|
|
116
|
-
const oneXAtlas = compiled.layoutOneX.index[icon.input.name];
|
|
117
|
-
const twoXAtlas = compiled.layoutTwoX.index[icon.input.name];
|
|
118
|
-
if (!manifestIcon || manifestIcon.name !== icon.input.name || !oneXAtlas || !twoXAtlas) {
|
|
119
|
-
throw new Error(`Missing compiled catalog metadata for icon ${icon.input.name}`);
|
|
120
|
-
}
|
|
121
|
-
return {
|
|
122
|
-
id: icon.input.name,
|
|
123
|
-
rendered: {
|
|
124
|
-
oneX: {
|
|
125
|
-
atlas: atlasRectangle(oneXAtlas),
|
|
126
|
-
height: icon.oneX.height,
|
|
127
|
-
pixelRatio: 1,
|
|
128
|
-
pixelSha256: manifestIcon.pixelSha256.oneX,
|
|
129
|
-
width: icon.oneX.width
|
|
130
|
-
},
|
|
131
|
-
twoX: {
|
|
132
|
-
atlas: atlasRectangle(twoXAtlas),
|
|
133
|
-
height: icon.twoX.height,
|
|
134
|
-
pixelRatio: 2,
|
|
135
|
-
pixelSha256: manifestIcon.pixelSha256.twoX,
|
|
136
|
-
width: icon.twoX.width
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
source: {
|
|
140
|
-
byteLength: icon.input.source.byteLength,
|
|
141
|
-
dimensions: { ...icon.dimensions },
|
|
142
|
-
format: icon.input.format,
|
|
143
|
-
path: icon.input.displayPath
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
}),
|
|
147
|
-
insideWorkingTree: request.directories.every(
|
|
148
|
-
(directory) => isPathInside(realCwd, directory.realPath)
|
|
149
|
-
),
|
|
150
|
-
replacements: [...compiled.sourceReplacements]
|
|
151
|
-
};
|
|
152
|
-
}).sort(
|
|
153
|
-
(left, right) => compareCodeUnits(left.directories.join("\0"), right.directories.join("\0"))
|
|
154
|
-
);
|
|
155
|
-
return { catalogs, maps };
|
|
156
|
-
}
|
|
157
|
-
async function compileIconSources(project, options) {
|
|
158
|
-
const mapRequests = await getMapIconRequests(
|
|
159
|
-
project,
|
|
160
|
-
options.cwd,
|
|
161
|
-
options.baseDirectory ?? options.cwd,
|
|
162
|
-
options.target,
|
|
163
|
-
options.mapNames
|
|
164
|
-
);
|
|
165
|
-
const issues = [];
|
|
166
|
-
const inspectedBySource = /* @__PURE__ */ new Map();
|
|
167
|
-
for (const directory of uniqueDirectories(mapRequests)) {
|
|
168
|
-
const inspected = await inspectIconSource(directory, options.target, issues);
|
|
169
|
-
if (inspected) inspectedBySource.set(directory.realPath, inspected);
|
|
170
|
-
}
|
|
171
|
-
if (issues.length > 0) throw new TileflowIconCompilationError(issues);
|
|
172
|
-
const compiledBySequence = /* @__PURE__ */ new Map();
|
|
173
|
-
for (const request of uniqueSequences(mapRequests)) {
|
|
174
|
-
if (request.directories.length === 0) continue;
|
|
175
|
-
try {
|
|
176
|
-
const composed = composeIconDirectories(request, inspectedBySource);
|
|
177
|
-
compiledBySequence.set(request.sequenceKey, await compileInspectedIconSource(composed));
|
|
178
|
-
} catch (error) {
|
|
179
|
-
issues.push({
|
|
180
|
-
message: error instanceof Error ? error.message : "Icon compilation failed",
|
|
181
|
-
path: `maps.${request.mapName}.icons`
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (issues.length > 0) throw new TileflowIconCompilationError(issues);
|
|
186
|
-
return { compiledBySequence, mapRequests };
|
|
187
|
-
}
|
|
188
|
-
async function prepareTileflowCatalogIcons(project, options) {
|
|
189
|
-
const compiled = await compileTileflowIconPackages(project, {
|
|
190
|
-
baseDirectory: options.baseDirectory,
|
|
191
|
-
cwd: options.cwd,
|
|
192
|
-
target: "local"
|
|
193
|
-
});
|
|
194
|
-
const packagesByHash = new Map(
|
|
195
|
-
compiled.packages.map((iconPackage) => [iconPackage.contentHash, iconPackage])
|
|
196
|
-
);
|
|
197
|
-
const assets = [];
|
|
198
|
-
const mapAssets = {};
|
|
199
|
-
const mapIconSources = {};
|
|
200
|
-
for (const mapName of Object.keys(project.maps)) mapIconSources[mapName] = [];
|
|
201
|
-
for (const binding of compiled.bindings) {
|
|
202
|
-
const iconPackage = packagesByHash.get(binding.packageHash);
|
|
203
|
-
if (!iconPackage) throw new Error(`Missing compiled icon package for map ${binding.mapName}`);
|
|
204
|
-
const spriteUrl = joinUrl(options.assetBaseUrl, `icons/${binding.mapName}/sprite`);
|
|
205
|
-
mapAssets[binding.mapName] = { icons: { ids: binding.iconIds, sprite: spriteUrl } };
|
|
206
|
-
mapIconSources[binding.mapName] = compiled.sourceIdentities[binding.mapName] ?? [];
|
|
207
|
-
for (const file of iconPackage.files) {
|
|
208
|
-
assets.push({
|
|
209
|
-
contentType: file.contentType === "application/json" ? "application/json; charset=utf-8" : file.contentType,
|
|
210
|
-
fileName: `icons/${binding.mapName}/${file.fileName}`,
|
|
211
|
-
source: file.contentType === "application/json" ? new TextDecoder().decode(file.source) : file.source
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return {
|
|
216
|
-
assets,
|
|
217
|
-
baseDirectory: await realpath(options.baseDirectory ?? options.cwd),
|
|
218
|
-
cwd: await realpath(options.cwd),
|
|
219
|
-
mapAssets,
|
|
220
|
-
mapIconSources,
|
|
221
|
-
project,
|
|
222
|
-
sourceProject: project,
|
|
223
|
-
watchPaths: compiled.watchPaths
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
async function getTileflowIconWatchPaths(project, cwd, baseDirectory = cwd) {
|
|
227
|
-
const requests = await getMapIconRequests(project, cwd, baseDirectory, "local");
|
|
228
|
-
return uniqueStrings(
|
|
229
|
-
requests.flatMap(
|
|
230
|
-
(request) => request.directories.filter((directory) => directory.watch).map((directory) => directory.realPath)
|
|
231
|
-
)
|
|
232
|
-
).sort(compareCodeUnits);
|
|
233
|
-
}
|
|
234
|
-
async function getMapIconRequests(project, cwd, baseDirectory, target, mapNames) {
|
|
235
|
-
const selectedNames = (mapNames === void 0 ? Object.keys(project.maps) : [...new Set(mapNames)]).sort(compareCodeUnits);
|
|
236
|
-
const requests = [];
|
|
237
|
-
for (const mapName of selectedNames) {
|
|
238
|
-
const mapConfig = project.maps[mapName];
|
|
239
|
-
if (!mapConfig) throw new Error(`Unknown Tileflow map: ${mapName}`);
|
|
240
|
-
const resolvedMap = parseResolvedTileflowMap(mapConfig);
|
|
241
|
-
try {
|
|
242
|
-
const directories = await resolveTileflowAssetDirectories(resolvedMap.icons ?? [], {
|
|
243
|
-
baseDirectory,
|
|
244
|
-
configPath: `maps.${mapName}.icons`,
|
|
245
|
-
cwd,
|
|
246
|
-
kind: "icons",
|
|
247
|
-
target
|
|
248
|
-
});
|
|
249
|
-
requests.push({
|
|
250
|
-
directories,
|
|
251
|
-
mapName,
|
|
252
|
-
sequenceKey: directories.map((directory) => directory.realPath).join("\0")
|
|
253
|
-
});
|
|
254
|
-
} catch (error) {
|
|
255
|
-
if (error instanceof TileflowAssetDirectoryError) {
|
|
256
|
-
throw new TileflowIconCompilationError([...error.issues]);
|
|
257
|
-
}
|
|
258
|
-
throw error;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return requests;
|
|
262
|
-
}
|
|
263
|
-
function uniqueDirectories(requests) {
|
|
264
|
-
return [
|
|
265
|
-
...new Map(
|
|
266
|
-
requests.flatMap(
|
|
267
|
-
(request) => request.directories.map((directory) => [directory.realPath, directory])
|
|
268
|
-
)
|
|
269
|
-
).values()
|
|
270
|
-
].sort((left, right) => compareCodeUnits(left.realPath, right.realPath));
|
|
271
|
-
}
|
|
272
|
-
function uniqueSequences(requests) {
|
|
273
|
-
return [...new Map(requests.map((request) => [request.sequenceKey, request])).values()].sort(
|
|
274
|
-
(left, right) => compareCodeUnits(left.sequenceKey, right.sequenceKey)
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
function composeIconDirectories(request, inspectedBySource) {
|
|
278
|
-
const iconsById = /* @__PURE__ */ new Map();
|
|
279
|
-
const idsByCaseFold = /* @__PURE__ */ new Map();
|
|
280
|
-
const replacements = [];
|
|
281
|
-
for (const directory of request.directories) {
|
|
282
|
-
const inspected = inspectedBySource.get(directory.realPath);
|
|
283
|
-
if (!inspected)
|
|
284
|
-
throw new Error(`Missing inspected icon directory ${describeAssetDirectory(directory)}`);
|
|
285
|
-
for (const icon of inspected.icons) {
|
|
286
|
-
const folded = icon.name.toLocaleLowerCase("en-US");
|
|
287
|
-
const existing = idsByCaseFold.get(folded);
|
|
288
|
-
if (existing !== void 0 && existing !== icon.name) {
|
|
289
|
-
throw new Error(`Icon ID "${icon.name}" collides case-insensitively with "${existing}".`);
|
|
290
|
-
}
|
|
291
|
-
idsByCaseFold.set(folded, icon.name);
|
|
292
|
-
const replaced = iconsById.get(icon.name);
|
|
293
|
-
if (replaced) {
|
|
294
|
-
replacements.push({
|
|
295
|
-
id: icon.name,
|
|
296
|
-
replaced: replaced.displayPath,
|
|
297
|
-
winner: icon.displayPath
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
iconsById.set(icon.name, icon);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
const icons = [...iconsById.values()].sort(
|
|
304
|
-
(left, right) => compareCodeUnits(left.name, right.name)
|
|
305
|
-
);
|
|
306
|
-
const aggregateBytes = icons.reduce((total, icon) => total + icon.source.byteLength, 0);
|
|
307
|
-
if (icons.length > tileflowIconPackageLimits.maxIconCount) {
|
|
308
|
-
throw new Error(
|
|
309
|
-
`Composed icon set contains more than ${tileflowIconPackageLimits.maxIconCount} icons`
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
if (aggregateBytes > tileflowIconPackageLimits.maxSourceBytes) {
|
|
313
|
-
throw new Error(
|
|
314
|
-
`Composed icon set exceeds ${tileflowIconPackageLimits.maxSourceBytes} aggregate bytes`
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
return { icons, replacements };
|
|
318
|
-
}
|
|
319
|
-
function describeAssetDirectory(directory) {
|
|
320
|
-
return typeof directory.authoring === "string" ? directory.authoring : `npm:${directory.authoring.package}/${directory.authoring.path}`;
|
|
321
|
-
}
|
|
322
|
-
function describeAssetFile(directory, fileName) {
|
|
323
|
-
return `${describeAssetDirectory(directory).replace(/\/+$/u, "")}/${fileName}`;
|
|
324
|
-
}
|
|
325
|
-
async function inspectIconSource(directory, target, issues) {
|
|
326
|
-
let entries;
|
|
327
|
-
const issueCount = issues.length;
|
|
328
|
-
try {
|
|
329
|
-
entries = await readdir(directory.realPath, { withFileTypes: true });
|
|
330
|
-
} catch (error) {
|
|
331
|
-
issues.push({
|
|
332
|
-
message: describeFileSystemError(error, "Icon source could not be read"),
|
|
333
|
-
path: directory.configPath
|
|
334
|
-
});
|
|
335
|
-
return null;
|
|
336
|
-
}
|
|
337
|
-
entries.sort((left, right) => compareCodeUnits(left.name, right.name));
|
|
338
|
-
const candidates = [];
|
|
339
|
-
const names = /* @__PURE__ */ new Set();
|
|
340
|
-
for (const entry of entries) {
|
|
341
|
-
const entryPath = resolve(directory.realPath, entry.name);
|
|
342
|
-
const path = `${directory.configPath}/${entry.name}`;
|
|
343
|
-
if (entry.isDirectory()) {
|
|
344
|
-
issues.push({ message: "Nested directories are not supported", path });
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
let entryStat;
|
|
348
|
-
let realEntryPath = entryPath;
|
|
349
|
-
try {
|
|
350
|
-
if (entry.isSymbolicLink()) {
|
|
351
|
-
realEntryPath = await realpath(entryPath);
|
|
352
|
-
if (!isPathInside(directory.containmentRoot, realEntryPath)) {
|
|
353
|
-
issues.push({ message: "Icon symlink escapes its owning directory boundary", path });
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
entryStat = await stat(realEntryPath);
|
|
358
|
-
} catch (error) {
|
|
359
|
-
issues.push({ message: describeFileSystemError(error, "Icon entry could not be read"), path });
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
if (!entryStat.isFile()) {
|
|
363
|
-
issues.push({ message: "Icon entries must be regular files", path });
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
const extension = extname(entry.name).toLowerCase();
|
|
367
|
-
if (!iconFileExtensions.has(extension)) {
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
const sourceName = entry.name.slice(0, -extname(entry.name).length);
|
|
371
|
-
const kind = sourceName.endsWith(".pattern") ? "pattern" : "icon";
|
|
372
|
-
const name = kind === "pattern" ? sourceName.slice(0, -".pattern".length) : sourceName;
|
|
373
|
-
if (names.has(name)) {
|
|
374
|
-
issues.push({
|
|
375
|
-
message: `Duplicate icon basename "${name}"`,
|
|
376
|
-
path
|
|
377
|
-
});
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
names.add(name);
|
|
381
|
-
if (!tileflowIconIdPattern.test(name) || name.length > 64) {
|
|
382
|
-
issues.push({
|
|
383
|
-
message: "Icon filenames must produce a lower-kebab ID of at most 64 characters",
|
|
384
|
-
path
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
if (entryStat.size > tileflowIconPackageLimits.maxSourceFileBytes) {
|
|
388
|
-
issues.push({
|
|
389
|
-
message: `Icon source exceeds ${tileflowIconPackageLimits.maxSourceFileBytes} bytes`,
|
|
390
|
-
path
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
candidates.push({
|
|
394
|
-
displayPath: describeAssetFile(directory, entry.name),
|
|
395
|
-
fileName: entry.name,
|
|
396
|
-
format: iconSourceFormat(extension),
|
|
397
|
-
kind,
|
|
398
|
-
name,
|
|
399
|
-
path: realEntryPath,
|
|
400
|
-
size: entryStat.size
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
candidates.sort((left, right) => compareCodeUnits(left.name, right.name));
|
|
404
|
-
if (candidates.length === 0) {
|
|
405
|
-
issues.push({
|
|
406
|
-
message: "No supported .svg, .png, .jpg, .jpeg, or .webp icon files were found",
|
|
407
|
-
path: directory.configPath
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
if (issues.length > issueCount) {
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
const icons = [];
|
|
414
|
-
for (const candidate of candidates) {
|
|
415
|
-
const displayPath = candidate.displayPath;
|
|
416
|
-
try {
|
|
417
|
-
const source = await readFile(candidate.path);
|
|
418
|
-
if (source.byteLength !== candidate.size || source.byteLength > tileflowIconPackageLimits.maxSourceFileBytes) {
|
|
419
|
-
throw new Error("Icon changed while it was being validated");
|
|
420
|
-
}
|
|
421
|
-
if (target === "hosted" && extname(candidate.fileName).toLowerCase() === ".svg") {
|
|
422
|
-
validateHostedSvg(source, displayPath);
|
|
423
|
-
}
|
|
424
|
-
icons.push({
|
|
425
|
-
displayPath,
|
|
426
|
-
fileName: candidate.fileName,
|
|
427
|
-
format: candidate.format,
|
|
428
|
-
kind: candidate.kind,
|
|
429
|
-
name: candidate.name,
|
|
430
|
-
path: candidate.path,
|
|
431
|
-
source
|
|
432
|
-
});
|
|
433
|
-
} catch (error) {
|
|
434
|
-
issues.push({
|
|
435
|
-
message: error instanceof Error ? error.message : "Icon could not be read",
|
|
436
|
-
path: displayPath
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
if (issues.length > issueCount) {
|
|
441
|
-
return null;
|
|
442
|
-
}
|
|
443
|
-
return { icons };
|
|
444
|
-
}
|
|
445
|
-
async function compileInspectedIconSource(inspected) {
|
|
446
|
-
const rendered = await mapWithConcurrency(
|
|
447
|
-
inspected.icons,
|
|
448
|
-
tileflowIconPackageLimits.decodeConcurrency,
|
|
449
|
-
async (icon) => {
|
|
450
|
-
const dimensions = await validateDecodedDimensions(icon);
|
|
451
|
-
if (icon.kind === "pattern" && (dimensions.width < 2 || dimensions.width > 512 || (dimensions.width & dimensions.width - 1) !== 0)) {
|
|
452
|
-
throw new Error(
|
|
453
|
-
`${icon.fileName} pattern width must be a power of two from 2 through 512 pixels`
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
const oneX = await renderIcon(icon, dimensions, 1);
|
|
457
|
-
const twoX = await renderIcon(icon, dimensions, 2);
|
|
458
|
-
return {
|
|
459
|
-
dimensions,
|
|
460
|
-
input: icon,
|
|
461
|
-
oneX,
|
|
462
|
-
pixelSha256: {
|
|
463
|
-
oneX: await hashTileflowRenderedIconPixels({
|
|
464
|
-
height: oneX.height,
|
|
465
|
-
pixelRatio: 1,
|
|
466
|
-
rgba: oneX.rgba,
|
|
467
|
-
width: oneX.width
|
|
468
|
-
}),
|
|
469
|
-
twoX: await hashTileflowRenderedIconPixels({
|
|
470
|
-
height: twoX.height,
|
|
471
|
-
pixelRatio: 2,
|
|
472
|
-
rgba: twoX.rgba,
|
|
473
|
-
width: twoX.width
|
|
474
|
-
})
|
|
475
|
-
},
|
|
476
|
-
sourceSha256: await sha256Hex(icon.source),
|
|
477
|
-
twoX
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
);
|
|
481
|
-
const layoutOneX = createSpriteLayout(
|
|
482
|
-
rendered.map(({ input, oneX }) => ({ ...oneX, name: input.name })),
|
|
483
|
-
1
|
|
484
|
-
);
|
|
485
|
-
const layoutTwoX = createSpriteLayout(
|
|
486
|
-
rendered.map(({ input, twoX }) => ({ ...twoX, name: input.name })),
|
|
487
|
-
2
|
|
488
|
-
);
|
|
489
|
-
const [oneXImage, twoXImage] = await Promise.all([
|
|
490
|
-
createSpriteImage(
|
|
491
|
-
rendered.map(({ input, oneX }) => ({ ...oneX, name: input.name })),
|
|
492
|
-
layoutOneX
|
|
493
|
-
),
|
|
494
|
-
createSpriteImage(
|
|
495
|
-
rendered.map(({ input, twoX }) => ({ ...twoX, name: input.name })),
|
|
496
|
-
layoutTwoX
|
|
497
|
-
)
|
|
498
|
-
]);
|
|
499
|
-
const oneXJson = new TextEncoder().encode(`${serializeCanonicalJson(layoutOneX.index)}
|
|
500
|
-
`);
|
|
501
|
-
const twoXJson = new TextEncoder().encode(`${serializeCanonicalJson(layoutTwoX.index)}
|
|
502
|
-
`);
|
|
503
|
-
const files = [
|
|
504
|
-
{ contentType: "application/json", fileName: "sprite.json", source: oneXJson },
|
|
505
|
-
{ contentType: "image/png", fileName: "sprite.png", source: oneXImage },
|
|
506
|
-
{ contentType: "application/json", fileName: "sprite@2x.json", source: twoXJson },
|
|
507
|
-
{ contentType: "image/png", fileName: "sprite@2x.png", source: twoXImage }
|
|
508
|
-
];
|
|
509
|
-
assertGeneratedFileLimits(files);
|
|
510
|
-
const fileDigests = await Promise.all(files.map((file) => sha256Hex(file.source)));
|
|
511
|
-
const manifest = tileflowIconPackageManifestSchema.parse({
|
|
512
|
-
files: files.map((file, index) => ({
|
|
513
|
-
byteLength: file.source.byteLength,
|
|
514
|
-
contentType: file.contentType,
|
|
515
|
-
name: file.fileName,
|
|
516
|
-
sha256: fileDigests[index]
|
|
517
|
-
})),
|
|
518
|
-
format: "tileflow-icon-package-v1",
|
|
519
|
-
iconNames: inspected.icons.map((icon) => icon.name),
|
|
520
|
-
renderedIcons: inspected.icons.map((icon, index) => {
|
|
521
|
-
const pixels = rendered[index];
|
|
522
|
-
if (!pixels) {
|
|
523
|
-
throw new Error(`Missing rendered pixels for ${icon.name}`);
|
|
524
|
-
}
|
|
525
|
-
return { name: icon.name, pixelSha256: pixels.pixelSha256 };
|
|
526
|
-
}),
|
|
527
|
-
sprites: {
|
|
528
|
-
oneX: { height: layoutOneX.height, pixelRatio: 1, width: layoutOneX.width },
|
|
529
|
-
twoX: { height: layoutTwoX.height, pixelRatio: 2, width: layoutTwoX.width }
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
return {
|
|
533
|
-
icons: rendered,
|
|
534
|
-
layoutOneX,
|
|
535
|
-
layoutTwoX,
|
|
536
|
-
package: {
|
|
537
|
-
contentHash: await hashTileflowIconPackageManifest(manifest),
|
|
538
|
-
files,
|
|
539
|
-
manifest
|
|
540
|
-
},
|
|
541
|
-
sourceReplacements: inspected.replacements ?? []
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
function createSpriteLayout(icons, pixelRatio) {
|
|
545
|
-
const columns = Math.ceil(Math.sqrt(icons.length));
|
|
546
|
-
const widest = Math.max(...icons.map((icon) => icon.width));
|
|
547
|
-
const targetWidth = Math.min(tileflowIconPackageLimits.maxAtlasDimension, columns * widest);
|
|
548
|
-
const placements = [];
|
|
549
|
-
let left = 0;
|
|
550
|
-
let rowHeight = 0;
|
|
551
|
-
let top = 0;
|
|
552
|
-
for (const icon of icons) {
|
|
553
|
-
if (icon.width > tileflowIconPackageLimits.maxAtlasDimension) {
|
|
554
|
-
throw new Error(
|
|
555
|
-
`Generated sprite exceeds ${tileflowIconPackageLimits.maxAtlasDimension} pixels per dimension`
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
if (left > 0 && left + icon.width > targetWidth) {
|
|
559
|
-
top += rowHeight;
|
|
560
|
-
left = 0;
|
|
561
|
-
rowHeight = 0;
|
|
562
|
-
}
|
|
563
|
-
placements.push({ left, top });
|
|
564
|
-
left += icon.width;
|
|
565
|
-
rowHeight = Math.max(rowHeight, icon.height);
|
|
566
|
-
}
|
|
567
|
-
const width = targetWidth;
|
|
568
|
-
const height = top + rowHeight;
|
|
569
|
-
if (width > tileflowIconPackageLimits.maxAtlasDimension || height > tileflowIconPackageLimits.maxAtlasDimension) {
|
|
570
|
-
throw new Error(
|
|
571
|
-
`Generated sprite exceeds ${tileflowIconPackageLimits.maxAtlasDimension} pixels per dimension`
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
const index = Object.fromEntries(
|
|
575
|
-
icons.map((icon, indexNumber) => {
|
|
576
|
-
const placement = placements[indexNumber];
|
|
577
|
-
return [
|
|
578
|
-
icon.name,
|
|
579
|
-
{
|
|
580
|
-
height: icon.height,
|
|
581
|
-
pixelRatio,
|
|
582
|
-
width: icon.width,
|
|
583
|
-
x: placement.left,
|
|
584
|
-
y: placement.top
|
|
585
|
-
}
|
|
586
|
-
];
|
|
587
|
-
})
|
|
588
|
-
);
|
|
589
|
-
return { height, index, width };
|
|
590
|
-
}
|
|
591
|
-
async function validateDecodedDimensions(icon) {
|
|
592
|
-
const sharp = await loadSharp();
|
|
593
|
-
const metadata = await sharp(icon.source, {
|
|
594
|
-
density: 72,
|
|
595
|
-
failOn: "error",
|
|
596
|
-
limitInputPixels: tileflowIconPackageLimits.maxDecodedPixelsPerIcon
|
|
597
|
-
}).metadata();
|
|
598
|
-
const width = metadata.width ?? 0;
|
|
599
|
-
const height = metadata.height ?? 0;
|
|
600
|
-
if (width <= 0 || height <= 0) {
|
|
601
|
-
throw new Error(`Unable to determine decoded dimensions for ${icon.fileName}`);
|
|
602
|
-
}
|
|
603
|
-
if (width * height > tileflowIconPackageLimits.maxDecodedPixelsPerIcon) {
|
|
604
|
-
throw new Error(
|
|
605
|
-
`${icon.fileName} exceeds ${tileflowIconPackageLimits.maxDecodedPixelsPerIcon} decoded pixels`
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
return { height, width };
|
|
609
|
-
}
|
|
610
|
-
async function renderIcon(icon, dimensions, pixelRatio) {
|
|
611
|
-
const sharp = await loadSharp();
|
|
612
|
-
const targetWidth = (icon.kind === "pattern" ? dimensions.width : iconSpriteSize) * pixelRatio;
|
|
613
|
-
const targetHeight = (icon.kind === "pattern" ? dimensions.height : iconSpriteSize) * pixelRatio;
|
|
614
|
-
const { data, info } = await sharp(icon.source, {
|
|
615
|
-
density: 72 * pixelRatio,
|
|
616
|
-
failOn: "error",
|
|
617
|
-
limitInputPixels: tileflowIconPackageLimits.maxDecodedPixelsPerIcon
|
|
618
|
-
}).resize(targetWidth, targetHeight, {
|
|
619
|
-
background: { alpha: 0, b: 0, g: 0, r: 0 },
|
|
620
|
-
fit: "contain"
|
|
621
|
-
}).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
622
|
-
if (info.channels !== 4 || info.width !== targetWidth || info.height !== targetHeight) {
|
|
623
|
-
throw new Error(`Rendered icon ${icon.fileName} did not produce canonical RGBA pixels`);
|
|
624
|
-
}
|
|
625
|
-
const rgba = new Uint8Array(data);
|
|
626
|
-
const png = await sharp(data, {
|
|
627
|
-
raw: { channels: 4, height: info.height, width: info.width }
|
|
628
|
-
}).png({ adaptiveFiltering: false, compressionLevel: 9, palette: false }).toBuffer();
|
|
629
|
-
return { height: info.height, png, rgba, width: info.width };
|
|
630
|
-
}
|
|
631
|
-
async function createSpriteImage(icons, layout) {
|
|
632
|
-
const sharp = await loadSharp();
|
|
633
|
-
const atlas = new Uint8Array(layout.width * layout.height * 4);
|
|
634
|
-
for (const icon of icons) {
|
|
635
|
-
const placement = layout.index[icon.name];
|
|
636
|
-
if (!placement || icon.width !== placement.width || icon.height !== placement.height) {
|
|
637
|
-
throw new Error("Rendered icon dimensions do not match the sprite layout");
|
|
638
|
-
}
|
|
639
|
-
const rowBytes = icon.width * 4;
|
|
640
|
-
for (let row = 0; row < icon.height; row += 1) {
|
|
641
|
-
const sourceStart = row * rowBytes;
|
|
642
|
-
const targetStart = ((placement.y + row) * layout.width + placement.x) * 4;
|
|
643
|
-
atlas.set(icon.rgba.subarray(sourceStart, sourceStart + rowBytes), targetStart);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
return sharp(atlas, {
|
|
647
|
-
raw: {
|
|
648
|
-
channels: 4,
|
|
649
|
-
height: layout.height,
|
|
650
|
-
width: layout.width
|
|
651
|
-
}
|
|
652
|
-
}).png({ adaptiveFiltering: false, compressionLevel: 9, palette: false }).toBuffer();
|
|
653
|
-
}
|
|
654
|
-
function assertGeneratedFileLimits(files) {
|
|
655
|
-
let totalBytes = 0;
|
|
656
|
-
for (const file of files) {
|
|
657
|
-
if (file.source.byteLength > tileflowIconPackageLimits.maxGeneratedFileBytes) {
|
|
658
|
-
throw new Error(
|
|
659
|
-
`${file.fileName} exceeds ${tileflowIconPackageLimits.maxGeneratedFileBytes} generated bytes`
|
|
660
|
-
);
|
|
661
|
-
}
|
|
662
|
-
totalBytes += file.source.byteLength;
|
|
663
|
-
}
|
|
664
|
-
if (totalBytes > tileflowIconPackageLimits.maxGeneratedPackageBytes) {
|
|
665
|
-
throw new Error(
|
|
666
|
-
`Generated package exceeds ${tileflowIconPackageLimits.maxGeneratedPackageBytes} bytes`
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
function validateHostedSvg(source, displayPath) {
|
|
671
|
-
const parser = new SaxesParser({ fileName: displayPath, xmlns: true });
|
|
672
|
-
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
673
|
-
let styleDepth = 0;
|
|
674
|
-
let styleText = "";
|
|
675
|
-
parser.on("doctype", () => {
|
|
676
|
-
throw new Error("Hosted SVGs cannot contain a document type or entity declarations");
|
|
677
|
-
});
|
|
678
|
-
parser.on("processinginstruction", (instruction) => {
|
|
679
|
-
if (instruction.target.toLowerCase() === "xml-stylesheet") {
|
|
680
|
-
throw new Error("Hosted SVGs cannot load external stylesheets");
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
parser.on("opentag", (tag) => {
|
|
684
|
-
const localName = tag.local.toLowerCase();
|
|
685
|
-
if (localName === "script") {
|
|
686
|
-
throw new Error("Hosted SVGs cannot contain scripts");
|
|
687
|
-
}
|
|
688
|
-
if (localName === "style") {
|
|
689
|
-
styleDepth += 1;
|
|
690
|
-
}
|
|
691
|
-
for (const attribute of Object.values(tag.attributes)) {
|
|
692
|
-
const attributeName = attribute.local.toLowerCase();
|
|
693
|
-
const value = attribute.value.trim();
|
|
694
|
-
if (attributeName.startsWith("on")) {
|
|
695
|
-
throw new Error("Hosted SVGs cannot contain script event handlers");
|
|
696
|
-
}
|
|
697
|
-
if (attributeName === "href" && value.length > 0 && !value.startsWith("#")) {
|
|
698
|
-
throw new Error("Hosted SVG resource references must use local fragments");
|
|
699
|
-
}
|
|
700
|
-
if (containsExternalCssUrl(value)) {
|
|
701
|
-
throw new Error("Hosted SVG resource references must use local fragments");
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
});
|
|
705
|
-
parser.on("text", (text) => {
|
|
706
|
-
if (styleDepth > 0) {
|
|
707
|
-
styleText += text;
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
parser.on("cdata", (text) => {
|
|
711
|
-
if (styleDepth > 0) {
|
|
712
|
-
styleText += text;
|
|
713
|
-
}
|
|
714
|
-
});
|
|
715
|
-
parser.on("closetag", (tag) => {
|
|
716
|
-
if (tag.local.toLowerCase() !== "style") {
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
styleDepth -= 1;
|
|
720
|
-
if (styleDepth === 0) {
|
|
721
|
-
if (styleText.toLowerCase().includes("@import") || containsExternalCssUrl(styleText)) {
|
|
722
|
-
throw new Error("Hosted SVGs cannot load external stylesheets or resources");
|
|
723
|
-
}
|
|
724
|
-
styleText = "";
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
parser.on("error", (error) => {
|
|
728
|
-
throw error;
|
|
729
|
-
});
|
|
730
|
-
const chunkBytes = 64 * 1024;
|
|
731
|
-
for (let offset = 0; offset < source.byteLength; offset += chunkBytes) {
|
|
732
|
-
parser.write(decoder.decode(source.subarray(offset, offset + chunkBytes), { stream: true }));
|
|
733
|
-
}
|
|
734
|
-
parser.write(decoder.decode());
|
|
735
|
-
parser.close();
|
|
736
|
-
}
|
|
737
|
-
function containsExternalCssUrl(value) {
|
|
738
|
-
const lowerValue = value.toLowerCase();
|
|
739
|
-
let cursor = 0;
|
|
740
|
-
while (cursor < value.length) {
|
|
741
|
-
const start = lowerValue.indexOf("url(", cursor);
|
|
742
|
-
if (start === -1) {
|
|
743
|
-
return false;
|
|
744
|
-
}
|
|
745
|
-
const end = value.indexOf(")", start + 4);
|
|
746
|
-
if (end === -1) {
|
|
747
|
-
return true;
|
|
748
|
-
}
|
|
749
|
-
const rawTarget = value.slice(start + 4, end).trim();
|
|
750
|
-
const target = stripMatchingQuotes(rawTarget);
|
|
751
|
-
if (!target.startsWith("#")) {
|
|
752
|
-
return true;
|
|
753
|
-
}
|
|
754
|
-
cursor = end + 1;
|
|
755
|
-
}
|
|
756
|
-
return false;
|
|
757
|
-
}
|
|
758
|
-
function stripMatchingQuotes(value) {
|
|
759
|
-
if (value.length < 2) {
|
|
760
|
-
return value;
|
|
761
|
-
}
|
|
762
|
-
const first = value[0];
|
|
763
|
-
const last = value[value.length - 1];
|
|
764
|
-
return first === '"' && last === '"' || first === "'" && last === "'" ? value.slice(1, -1).trim() : value;
|
|
765
|
-
}
|
|
766
|
-
async function mapWithConcurrency(values, concurrency, map) {
|
|
767
|
-
const results = new Array(values.length);
|
|
768
|
-
let nextIndex = 0;
|
|
769
|
-
async function worker() {
|
|
770
|
-
while (nextIndex < values.length) {
|
|
771
|
-
const index = nextIndex;
|
|
772
|
-
nextIndex += 1;
|
|
773
|
-
const value = values[index];
|
|
774
|
-
if (value !== void 0) {
|
|
775
|
-
results[index] = await map(value, index);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
await Promise.all(
|
|
780
|
-
Array.from({ length: Math.min(concurrency, values.length) }, async () => worker())
|
|
781
|
-
);
|
|
782
|
-
return results;
|
|
783
|
-
}
|
|
784
|
-
async function loadSharp() {
|
|
785
|
-
try {
|
|
786
|
-
return (await import("sharp")).default;
|
|
787
|
-
} catch (error) {
|
|
788
|
-
throw new Error(
|
|
789
|
-
'Local icon sprites require the optional "sharp" package. Install sharp or disable local icons.',
|
|
790
|
-
{ cause: error }
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
function atlasRectangle(entry) {
|
|
795
|
-
return { height: entry.height, width: entry.width, x: entry.x, y: entry.y };
|
|
796
|
-
}
|
|
797
|
-
function iconSourceFormat(extension) {
|
|
798
|
-
switch (extension) {
|
|
799
|
-
case ".svg":
|
|
800
|
-
return "svg";
|
|
801
|
-
case ".png":
|
|
802
|
-
return "png";
|
|
803
|
-
case ".jpg":
|
|
804
|
-
case ".jpeg":
|
|
805
|
-
return "jpeg";
|
|
806
|
-
case ".webp":
|
|
807
|
-
return "webp";
|
|
808
|
-
default:
|
|
809
|
-
throw new Error(`Unsupported icon source format: ${extension}`);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
function isPathInside(root, candidate) {
|
|
813
|
-
const pathFromRoot = relative(root, candidate);
|
|
814
|
-
return pathFromRoot === "" || !isAbsolute(pathFromRoot) && pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`);
|
|
815
|
-
}
|
|
816
|
-
function describeFileSystemError(error, fallback) {
|
|
817
|
-
if (error && typeof error === "object" && "code" in error) {
|
|
818
|
-
const code = String(error.code ?? "");
|
|
819
|
-
if (code === "ENOENT") {
|
|
820
|
-
return fallback;
|
|
821
|
-
}
|
|
822
|
-
if (code === "EACCES" || code === "EPERM") {
|
|
823
|
-
return "Icon source is not readable";
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
return error instanceof Error && error.message ? error.message : fallback;
|
|
827
|
-
}
|
|
828
|
-
function joinUrl(base, path) {
|
|
829
|
-
const trimmedBase = base.replace(/\/+$/g, "");
|
|
830
|
-
const trimmedPath = path.replace(/^\/+/g, "");
|
|
831
|
-
return trimmedBase ? `${trimmedBase}/${trimmedPath}` : trimmedPath;
|
|
832
|
-
}
|
|
833
|
-
function uniqueStrings(values) {
|
|
834
|
-
return [...new Set(values)];
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
export {
|
|
838
|
-
TileflowIconCompilationError,
|
|
839
|
-
iconFileExtensions,
|
|
840
|
-
compileTileflowIconPackages,
|
|
841
|
-
inspectTileflowIconCatalogs,
|
|
842
|
-
prepareTileflowCatalogIcons,
|
|
843
|
-
getTileflowIconWatchPaths
|
|
844
|
-
};
|