@mapsight/traffic-style 5.0.1 → 5.0.2
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/meta.json +1 -1
- package/package.json +6 -6
- package/scripts/create-meta-json.d.ts +2 -0
- package/scripts/create-meta-json.d.ts.map +1 -0
- package/scripts/create-meta-json.js +79 -0
- package/scripts/create-meta-json.js.map +1 -0
- package/scripts/create-overview-md.d.ts +2 -0
- package/scripts/create-overview-md.d.ts.map +1 -0
- package/scripts/create-overview-md.js +72 -0
- package/scripts/create-overview-md.js.map +1 -0
- package/scripts/lib/meta.d.ts +32 -0
- package/scripts/lib/meta.d.ts.map +1 -0
- package/scripts/lib/meta.js +23 -0
- package/scripts/lib/meta.js.map +1 -0
- package/scripts/optimize-icons.d.ts +47 -0
- package/scripts/optimize-icons.d.ts.map +1 -0
- package/scripts/optimize-icons.js +477 -0
- package/scripts/optimize-icons.js.map +1 -0
- package/scripts/optimize-icons.test.d.ts +2 -0
- package/scripts/optimize-icons.test.d.ts.map +1 -0
- package/scripts/optimize-icons.test.js +179 -0
- package/scripts/optimize-icons.test.js.map +1 -0
- package/scripts/traffic-icon-sprite.d.ts +3 -0
- package/scripts/traffic-icon-sprite.d.ts.map +1 -0
- package/scripts/traffic-icon-sprite.js +212 -0
- package/scripts/traffic-icon-sprite.js.map +1 -0
- package/scripts/create-meta-json.ts +0 -141
- package/scripts/create-overview-md.ts +0 -110
- package/scripts/lib/meta.ts +0 -40
- package/scripts/optimize-icons.test.ts +0 -234
- package/scripts/optimize-icons.ts +0 -686
- package/scripts/traffic-icon-sprite.ts +0 -292
|
@@ -1,686 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {createHash, randomUUID} from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
glob,
|
|
5
|
-
mkdir,
|
|
6
|
-
readFile,
|
|
7
|
-
rename,
|
|
8
|
-
stat,
|
|
9
|
-
unlink,
|
|
10
|
-
writeFile,
|
|
11
|
-
} from "node:fs/promises";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import {fileURLToPath} from "node:url";
|
|
14
|
-
import {parseArgs} from "node:util";
|
|
15
|
-
|
|
16
|
-
import sharp, {type Sharp} from "sharp";
|
|
17
|
-
import {type Config, optimize as optimizeSvg} from "svgo";
|
|
18
|
-
import {z} from "zod/v4";
|
|
19
|
-
|
|
20
|
-
type ResultStatus = "processed" | "skipped" | "failed";
|
|
21
|
-
type SourceKind = "svg" | "png";
|
|
22
|
-
type TargetFormat = "svg" | "png" | "webp";
|
|
23
|
-
|
|
24
|
-
const ManifestEntrySchema = z.object({
|
|
25
|
-
srcMtimeMs: z.number(),
|
|
26
|
-
srcSize: z.number(),
|
|
27
|
-
outputs: z.array(z.string()),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
type ManifestEntry = z.infer<typeof ManifestEntrySchema>;
|
|
31
|
-
|
|
32
|
-
const ManifestSchema = z.object({
|
|
33
|
-
version: z.number(),
|
|
34
|
-
configHash: z.string(),
|
|
35
|
-
files: z.record(z.string(), ManifestEntrySchema),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
type Manifest = z.infer<typeof ManifestSchema>;
|
|
39
|
-
|
|
40
|
-
interface FileJob {
|
|
41
|
-
absSource: string;
|
|
42
|
-
relSource: string;
|
|
43
|
-
kind: SourceKind;
|
|
44
|
-
outputs: string[];
|
|
45
|
-
absOutSvg?: string;
|
|
46
|
-
absOutPng?: string;
|
|
47
|
-
absOutWebp?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface ProcessResult {
|
|
51
|
-
source: string;
|
|
52
|
-
status: ResultStatus;
|
|
53
|
-
error?: unknown;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const CACHE_VERSION = 2;
|
|
57
|
-
const DEFAULT_CONCURRENCY = 4;
|
|
58
|
-
const createdDirs = new Set<string>();
|
|
59
|
-
|
|
60
|
-
const PNG_OPTIONS = {
|
|
61
|
-
palette: true,
|
|
62
|
-
compressionLevel: 9,
|
|
63
|
-
effort: 8,
|
|
64
|
-
} as const;
|
|
65
|
-
|
|
66
|
-
const WEBP_OPTIONS = {
|
|
67
|
-
lossless: true,
|
|
68
|
-
effort: 6,
|
|
69
|
-
} as const;
|
|
70
|
-
|
|
71
|
-
const SVGO_OPTIONS: Config = {
|
|
72
|
-
multipass: true,
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const scriptName = path.basename(process.argv[1] ?? "optimize-icons.ts");
|
|
76
|
-
|
|
77
|
-
async function main() {
|
|
78
|
-
const {values: cliArgs} = parseArgs({
|
|
79
|
-
args: process.argv.slice(2),
|
|
80
|
-
options: {
|
|
81
|
-
src: {type: "string", short: "s"},
|
|
82
|
-
dest: {type: "string", short: "d"},
|
|
83
|
-
scale: {type: "string", default: "1"},
|
|
84
|
-
concurrency: {
|
|
85
|
-
type: "string",
|
|
86
|
-
short: "c",
|
|
87
|
-
default: String(DEFAULT_CONCURRENCY),
|
|
88
|
-
},
|
|
89
|
-
target: {type: "string", short: "t", default: "svg,png,webp"},
|
|
90
|
-
force: {type: "boolean", default: false},
|
|
91
|
-
verbose: {type: "boolean", default: false},
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
const srcArg = cliArgs.src;
|
|
95
|
-
const destArg = cliArgs.dest;
|
|
96
|
-
const scaleArg = cliArgs.scale;
|
|
97
|
-
const concurrencyArg = cliArgs.concurrency;
|
|
98
|
-
const targetArg = cliArgs.target;
|
|
99
|
-
const force = cliArgs.force;
|
|
100
|
-
const verbose = cliArgs.verbose;
|
|
101
|
-
|
|
102
|
-
if (typeof srcArg !== "string" || typeof destArg !== "string") {
|
|
103
|
-
fail(
|
|
104
|
-
`Usage: node ${scriptName} --src <dir> --dest <dir> ` +
|
|
105
|
-
`[--scale <number>] [--concurrency <number>] [--target <formats>] [--force]`,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const targets = new Set(
|
|
110
|
-
(targetArg ?? "svg,png,webp")
|
|
111
|
-
.split(",")
|
|
112
|
-
.map((s) => s.trim().toLowerCase()) as TargetFormat[],
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
for (const t of targets) {
|
|
116
|
-
if (t !== "svg" && t !== "png" && t !== "webp") {
|
|
117
|
-
fail(
|
|
118
|
-
`Invalid target format: ${t as string}. Must be svg, png, or webp.`,
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const scale = Number(scaleArg ?? "1");
|
|
124
|
-
if (!Number.isFinite(scale) || scale <= 0) {
|
|
125
|
-
fail("--scale must be a positive finite number.");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const concurrency = Number(concurrencyArg ?? String(DEFAULT_CONCURRENCY));
|
|
129
|
-
if (
|
|
130
|
-
!Number.isFinite(concurrency) ||
|
|
131
|
-
!Number.isInteger(concurrency) ||
|
|
132
|
-
concurrency <= 0
|
|
133
|
-
) {
|
|
134
|
-
fail("--concurrency must be a positive integer.");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const absSrc = path.resolve(srcArg);
|
|
138
|
-
const absDest = path.resolve(destArg);
|
|
139
|
-
|
|
140
|
-
const scriptPath = fileURLToPath(import.meta.url);
|
|
141
|
-
const packageRoot = path.resolve(path.dirname(scriptPath), "..");
|
|
142
|
-
const manifestDir = path.join(packageRoot, "tmp");
|
|
143
|
-
const manifestHash = createHash("sha256")
|
|
144
|
-
.update(`${absSrc}:${absDest}`)
|
|
145
|
-
.digest("hex")
|
|
146
|
-
.slice(0, 16);
|
|
147
|
-
const absManifestPath = path.join(
|
|
148
|
-
manifestDir,
|
|
149
|
-
`optimize-icons-manifest-${manifestHash}.json`,
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
if (pathsOverlap(absSrc, absDest)) {
|
|
153
|
-
fail("--src and --dest must not overlap.");
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const configHash = hashConfig({
|
|
157
|
-
cacheVersion: CACHE_VERSION,
|
|
158
|
-
scale,
|
|
159
|
-
png: PNG_OPTIONS,
|
|
160
|
-
webp: WEBP_OPTIONS,
|
|
161
|
-
svgo: SVGO_OPTIONS,
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
if (verbose) {
|
|
165
|
-
console.log(`Starting optimization with scale ${scale}`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
await ensureDir(absDest);
|
|
169
|
-
|
|
170
|
-
const previousManifest = await loadManifest(absManifestPath);
|
|
171
|
-
const cacheUsable =
|
|
172
|
-
!force &&
|
|
173
|
-
previousManifest.version === CACHE_VERSION &&
|
|
174
|
-
previousManifest.configHash === configHash;
|
|
175
|
-
|
|
176
|
-
const jobs = await discoverJobs(absSrc, absDest, targets);
|
|
177
|
-
|
|
178
|
-
if (verbose) {
|
|
179
|
-
console.log(`Found ${jobs.length} source images.`);
|
|
180
|
-
if (force) {
|
|
181
|
-
console.log("Force rebuild enabled.");
|
|
182
|
-
} else if (!cacheUsable) {
|
|
183
|
-
console.log(
|
|
184
|
-
"Manifest/config changed; rebuilding all current sources.",
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const nextManifest: Manifest = {
|
|
190
|
-
version: CACHE_VERSION,
|
|
191
|
-
configHash,
|
|
192
|
-
files: {},
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const currentSourceSet = new Set(jobs.map((job) => job.relSource));
|
|
196
|
-
|
|
197
|
-
const results = await runWithConcurrency(jobs, concurrency, (job) =>
|
|
198
|
-
processJob({
|
|
199
|
-
job,
|
|
200
|
-
previousManifest,
|
|
201
|
-
nextManifest,
|
|
202
|
-
cacheUsable,
|
|
203
|
-
force: force ?? false,
|
|
204
|
-
scale,
|
|
205
|
-
verbose: verbose ?? false,
|
|
206
|
-
absDest,
|
|
207
|
-
}),
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
await cleanupRemovedSourceOutputs({
|
|
211
|
-
previousManifest,
|
|
212
|
-
currentSourceSet,
|
|
213
|
-
absDest,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
await atomicWriteText(
|
|
217
|
-
absManifestPath,
|
|
218
|
-
JSON.stringify(nextManifest, null, 2) + "\n",
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
let processed = 0;
|
|
222
|
-
let skipped = 0;
|
|
223
|
-
let failed = 0;
|
|
224
|
-
|
|
225
|
-
for (const result of results) {
|
|
226
|
-
if (result.status === "processed") processed++;
|
|
227
|
-
if (result.status === "skipped") skipped++;
|
|
228
|
-
if (result.status === "failed") failed++;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (verbose) {
|
|
232
|
-
console.log("");
|
|
233
|
-
console.log(
|
|
234
|
-
`Done. Processed: ${processed} | Skipped: ${skipped} | Failed: ${failed}`,
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (failed > 0) {
|
|
239
|
-
process.exitCode = 1;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async function discoverJobs(
|
|
244
|
-
srcDir: string,
|
|
245
|
-
destDir: string,
|
|
246
|
-
targets: Set<TargetFormat>,
|
|
247
|
-
): Promise<FileJob[]> {
|
|
248
|
-
const jobs: FileJob[] = [];
|
|
249
|
-
const outputOwners = new Map<string, string>();
|
|
250
|
-
|
|
251
|
-
for await (const relGlobPath of glob("**/*", {cwd: srcDir})) {
|
|
252
|
-
const relPath = toPosixPath(relGlobPath);
|
|
253
|
-
const parsed = path.posix.parse(relPath);
|
|
254
|
-
const ext = parsed.ext.toLowerCase();
|
|
255
|
-
|
|
256
|
-
if (ext !== ".svg" && ext !== ".png") {
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const kind: SourceKind = ext === ".svg" ? "svg" : "png";
|
|
261
|
-
const relBase = path.posix.join(parsed.dir, parsed.name);
|
|
262
|
-
|
|
263
|
-
const outputs: string[] = [];
|
|
264
|
-
let absOutSvg: string | undefined;
|
|
265
|
-
let absOutPng: string | undefined;
|
|
266
|
-
let absOutWebp: string | undefined;
|
|
267
|
-
|
|
268
|
-
if (targets.has("svg") && kind === "svg") {
|
|
269
|
-
const relOutSvg = `${relBase}.svg`;
|
|
270
|
-
outputs.push(relOutSvg);
|
|
271
|
-
absOutSvg = path.join(destDir, relOutSvg);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (targets.has("png")) {
|
|
275
|
-
const relOutPng = `${relBase}.png`;
|
|
276
|
-
outputs.push(relOutPng);
|
|
277
|
-
absOutPng = path.join(destDir, relOutPng);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (targets.has("webp")) {
|
|
281
|
-
const relOutWebp = `${relBase}.webp`;
|
|
282
|
-
outputs.push(relOutWebp);
|
|
283
|
-
absOutWebp = path.join(destDir, relOutWebp);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const absSource = path.join(srcDir, relPath);
|
|
287
|
-
|
|
288
|
-
if (outputs.length === 0) {
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const outputCollisionKey = relBase;
|
|
293
|
-
const existingOwner = outputOwners.get(outputCollisionKey);
|
|
294
|
-
|
|
295
|
-
if (existingOwner) {
|
|
296
|
-
throw new Error(
|
|
297
|
-
[
|
|
298
|
-
"Output collision detected.",
|
|
299
|
-
`Both "${existingOwner}" and "${relPath}" map to the same output base "${relBase}".`,
|
|
300
|
-
"Rename one of the source files or change the output naming strategy.",
|
|
301
|
-
].join(" "),
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
outputOwners.set(outputCollisionKey, relPath);
|
|
306
|
-
|
|
307
|
-
jobs.push({
|
|
308
|
-
absSource,
|
|
309
|
-
relSource: relPath,
|
|
310
|
-
kind,
|
|
311
|
-
outputs,
|
|
312
|
-
absOutSvg,
|
|
313
|
-
absOutPng,
|
|
314
|
-
absOutWebp,
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
jobs.sort((a, b) => a.relSource.localeCompare(b.relSource));
|
|
319
|
-
return jobs;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
interface ProcessJobArgs {
|
|
323
|
-
job: FileJob;
|
|
324
|
-
previousManifest: Manifest;
|
|
325
|
-
nextManifest: Manifest;
|
|
326
|
-
cacheUsable: boolean;
|
|
327
|
-
force: boolean;
|
|
328
|
-
scale: number;
|
|
329
|
-
verbose: boolean;
|
|
330
|
-
absDest: string;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
async function processJob(args: ProcessJobArgs): Promise<ProcessResult> {
|
|
334
|
-
const {
|
|
335
|
-
job,
|
|
336
|
-
previousManifest,
|
|
337
|
-
nextManifest,
|
|
338
|
-
cacheUsable,
|
|
339
|
-
force,
|
|
340
|
-
scale,
|
|
341
|
-
verbose,
|
|
342
|
-
absDest,
|
|
343
|
-
} = args;
|
|
344
|
-
|
|
345
|
-
try {
|
|
346
|
-
const srcStat = await stat(job.absSource);
|
|
347
|
-
|
|
348
|
-
const nextEntry: ManifestEntry = {
|
|
349
|
-
srcMtimeMs: srcStat.mtimeMs,
|
|
350
|
-
srcSize: srcStat.size,
|
|
351
|
-
outputs: job.outputs,
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
const previousEntry = previousManifest.files[job.relSource]!;
|
|
355
|
-
const shouldProcess = await needsProcessing({
|
|
356
|
-
previousEntry,
|
|
357
|
-
nextEntry,
|
|
358
|
-
cacheUsable,
|
|
359
|
-
force,
|
|
360
|
-
absDest,
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
if (!shouldProcess) {
|
|
364
|
-
nextManifest.files[job.relSource] = previousEntry;
|
|
365
|
-
return {source: job.relSource, status: "skipped"};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (verbose) {
|
|
369
|
-
console.log(`Processing: ${job.relSource}`);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (job.kind === "svg") {
|
|
373
|
-
await processSvgJob(job, scale);
|
|
374
|
-
} else {
|
|
375
|
-
await processPngJob(job, scale);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
nextManifest.files[job.relSource] = nextEntry;
|
|
379
|
-
return {source: job.relSource, status: "processed"};
|
|
380
|
-
} catch (error) {
|
|
381
|
-
console.error(`Error processing ${job.relSource}:`, error);
|
|
382
|
-
return {source: job.relSource, status: "failed", error};
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
async function needsProcessing(args: {
|
|
387
|
-
previousEntry?: ManifestEntry;
|
|
388
|
-
nextEntry: ManifestEntry;
|
|
389
|
-
cacheUsable: boolean;
|
|
390
|
-
force: boolean;
|
|
391
|
-
absDest: string;
|
|
392
|
-
}): Promise<boolean> {
|
|
393
|
-
const {previousEntry, nextEntry, cacheUsable, force, absDest} = args;
|
|
394
|
-
|
|
395
|
-
if (force) return true;
|
|
396
|
-
if (!cacheUsable) return true;
|
|
397
|
-
if (!previousEntry) return true;
|
|
398
|
-
|
|
399
|
-
if (previousEntry.srcMtimeMs !== nextEntry.srcMtimeMs) return true;
|
|
400
|
-
if (previousEntry.srcSize !== nextEntry.srcSize) return true;
|
|
401
|
-
|
|
402
|
-
if (!sameStringArray(previousEntry.outputs, nextEntry.outputs)) {
|
|
403
|
-
return true;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
for (const relOutput of nextEntry.outputs) {
|
|
407
|
-
const absOutput = path.join(absDest, relOutput);
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
await stat(absOutput);
|
|
411
|
-
} catch {
|
|
412
|
-
return true;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
async function processSvgJob(job: FileJob, scale: number) {
|
|
420
|
-
const svgContent = await readFile(job.absSource, "utf8");
|
|
421
|
-
const optimized = optimizeSvg(svgContent, {
|
|
422
|
-
path: job.absSource,
|
|
423
|
-
...SVGO_OPTIONS,
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
const optimizedSvg = optimized.data;
|
|
427
|
-
const svgBuffer = Buffer.from(optimizedSvg);
|
|
428
|
-
|
|
429
|
-
const promises: Promise<void>[] = [];
|
|
430
|
-
|
|
431
|
-
if (job.absOutSvg) {
|
|
432
|
-
promises.push(atomicWriteText(job.absOutSvg, optimizedSvg));
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (job.absOutPng || job.absOutWebp) {
|
|
436
|
-
const metadata = await sharp(svgBuffer).metadata();
|
|
437
|
-
const targetWidth = getScaledDimension(metadata.width, scale);
|
|
438
|
-
|
|
439
|
-
promises.push(
|
|
440
|
-
writeRasterVariants({
|
|
441
|
-
input: sharp(
|
|
442
|
-
svgBuffer,
|
|
443
|
-
scale > 1 ? {density: Math.ceil(72 * scale)} : undefined,
|
|
444
|
-
),
|
|
445
|
-
outPng: job.absOutPng,
|
|
446
|
-
outWebp: job.absOutWebp,
|
|
447
|
-
targetWidth,
|
|
448
|
-
scale,
|
|
449
|
-
}),
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
await Promise.all(promises);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
async function processPngJob(job: FileJob, scale: number) {
|
|
457
|
-
if (!job.absOutPng && !job.absOutWebp) {
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const input = sharp(job.absSource);
|
|
462
|
-
const metadata = await input.metadata();
|
|
463
|
-
const targetWidth = getScaledDimension(metadata.width, scale);
|
|
464
|
-
|
|
465
|
-
await writeRasterVariants({
|
|
466
|
-
input,
|
|
467
|
-
outPng: job.absOutPng,
|
|
468
|
-
outWebp: job.absOutWebp,
|
|
469
|
-
targetWidth,
|
|
470
|
-
scale,
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
async function writeRasterVariants(args: {
|
|
475
|
-
input: Sharp;
|
|
476
|
-
outPng?: string;
|
|
477
|
-
outWebp?: string;
|
|
478
|
-
targetWidth?: number;
|
|
479
|
-
scale: number;
|
|
480
|
-
}) {
|
|
481
|
-
const {input, outPng, outWebp, targetWidth} = args;
|
|
482
|
-
|
|
483
|
-
let base = input.clone();
|
|
484
|
-
|
|
485
|
-
if (targetWidth) {
|
|
486
|
-
base = base.resize({width: targetWidth});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const promises: Promise<void>[] = [];
|
|
490
|
-
|
|
491
|
-
if (outPng) {
|
|
492
|
-
promises.push(atomicToFile(base.clone().png(PNG_OPTIONS), outPng));
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (outWebp) {
|
|
496
|
-
promises.push(atomicToFile(base.clone().webp(WEBP_OPTIONS), outWebp));
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
await Promise.all(promises);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
async function cleanupRemovedSourceOutputs(args: {
|
|
503
|
-
previousManifest: Manifest;
|
|
504
|
-
currentSourceSet: Set<string>;
|
|
505
|
-
absDest: string;
|
|
506
|
-
}) {
|
|
507
|
-
const {previousManifest, currentSourceSet, absDest} = args;
|
|
508
|
-
|
|
509
|
-
for (const [relSource, entry] of Object.entries(previousManifest.files)) {
|
|
510
|
-
if (currentSourceSet.has(relSource)) {
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
for (const relOutput of entry.outputs) {
|
|
515
|
-
const absOutput = path.join(absDest, relOutput);
|
|
516
|
-
await safeUnlink(absOutput);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
async function loadManifest(manifestPath: string): Promise<Manifest> {
|
|
522
|
-
try {
|
|
523
|
-
const raw = await readFile(manifestPath, "utf8");
|
|
524
|
-
return ManifestSchema.parse(JSON.parse(raw));
|
|
525
|
-
} catch {
|
|
526
|
-
return emptyManifest();
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function emptyManifest(): Manifest {
|
|
531
|
-
return {
|
|
532
|
-
version: 0,
|
|
533
|
-
configHash: "",
|
|
534
|
-
files: {},
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
async function atomicToFile(pipeline: Sharp, outPath: string) {
|
|
539
|
-
await ensureDir(path.dirname(outPath));
|
|
540
|
-
|
|
541
|
-
const tempPath = makeTempPath(outPath);
|
|
542
|
-
|
|
543
|
-
try {
|
|
544
|
-
await pipeline.toFile(tempPath);
|
|
545
|
-
await rename(tempPath, outPath);
|
|
546
|
-
} catch (error) {
|
|
547
|
-
await safeUnlink(tempPath);
|
|
548
|
-
throw error;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
async function atomicWriteText(outPath: string, content: string) {
|
|
553
|
-
await ensureDir(path.dirname(outPath));
|
|
554
|
-
|
|
555
|
-
const tempPath = makeTempPath(outPath);
|
|
556
|
-
|
|
557
|
-
try {
|
|
558
|
-
await writeFile(tempPath, content, "utf8");
|
|
559
|
-
await rename(tempPath, outPath);
|
|
560
|
-
} catch (error) {
|
|
561
|
-
await safeUnlink(tempPath);
|
|
562
|
-
throw error;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
async function ensureDir(dirPath: string) {
|
|
567
|
-
const absDir = path.resolve(dirPath);
|
|
568
|
-
|
|
569
|
-
if (createdDirs.has(absDir)) {
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
await mkdir(absDir, {recursive: true});
|
|
574
|
-
createdDirs.add(absDir);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
async function safeUnlink(filePath: string) {
|
|
578
|
-
try {
|
|
579
|
-
await unlink(filePath);
|
|
580
|
-
} catch {
|
|
581
|
-
// ignore missing files and cleanup failures
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function makeTempPath(finalPath: string) {
|
|
586
|
-
return `${finalPath}.tmp-${process.pid}-${randomUUID()}`;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function hashConfig(value: unknown): string {
|
|
590
|
-
return createHash("sha256")
|
|
591
|
-
.update(JSON.stringify(value))
|
|
592
|
-
.digest("hex")
|
|
593
|
-
.slice(0, 16);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function getScaledDimension(
|
|
597
|
-
original: number | undefined,
|
|
598
|
-
scaleValue: number,
|
|
599
|
-
): number | undefined {
|
|
600
|
-
if (!original || scaleValue === 1) {
|
|
601
|
-
return undefined;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const scaled = Math.max(1, Math.round(original * scaleValue));
|
|
605
|
-
|
|
606
|
-
if (scaled === original) {
|
|
607
|
-
return undefined;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
return scaled;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function sameStringArray(a: string[], b: string[]) {
|
|
614
|
-
if (a.length !== b.length) return false;
|
|
615
|
-
|
|
616
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
617
|
-
if (a[i] !== b[i]) return false;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
return true;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function toPosixPath(filePath: string) {
|
|
624
|
-
return filePath.split(path.sep).join(path.posix.sep);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function isSameOrSubpath(parent: string, child: string) {
|
|
628
|
-
const rel = path.relative(parent, child);
|
|
629
|
-
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function pathsOverlap(a: string, b: string) {
|
|
633
|
-
return isSameOrSubpath(a, b) || isSameOrSubpath(b, a);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
async function runWithConcurrency<T, R>(
|
|
637
|
-
items: T[],
|
|
638
|
-
limit: number,
|
|
639
|
-
worker: (item: T, index: number) => Promise<R>,
|
|
640
|
-
): Promise<R[]> {
|
|
641
|
-
const results = new Array<R>(items.length);
|
|
642
|
-
let nextIndex = 0;
|
|
643
|
-
|
|
644
|
-
async function runner() {
|
|
645
|
-
while (true) {
|
|
646
|
-
const currentIndex = nextIndex;
|
|
647
|
-
nextIndex += 1;
|
|
648
|
-
|
|
649
|
-
if (currentIndex >= items.length) {
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
results[currentIndex] = await worker(
|
|
654
|
-
items[currentIndex]!,
|
|
655
|
-
currentIndex,
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
const workerCount = Math.min(limit, items.length);
|
|
661
|
-
await Promise.all(
|
|
662
|
-
Array.from({length: workerCount}, async () => {
|
|
663
|
-
await runner();
|
|
664
|
-
}),
|
|
665
|
-
);
|
|
666
|
-
|
|
667
|
-
return results;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function fail(message: string): never {
|
|
671
|
-
console.error(message);
|
|
672
|
-
throw new Error(message);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
export function runMain() {
|
|
676
|
-
return main();
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if (process.env.NODE_ENV !== "test") {
|
|
680
|
-
runMain().catch((error) => {
|
|
681
|
-
console.error("Fatal error:", error);
|
|
682
|
-
throw error;
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
export {main, discoverJobs, processJob, hashConfig, getScaledDimension};
|