@xsynaptic/astro-image-pipeline 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +42 -2
- package/dist/image-encoding-CPYBEK0T.d.mts +13 -0
- package/dist/image-presets-CS7TnTsE.d.mts +83 -0
- package/dist/image-presets-ZN7_n2p8.mjs +184 -0
- package/dist/image-presets-ZN7_n2p8.mjs.map +1 -0
- package/dist/image-state-Bdi_-yWQ.mjs +110 -0
- package/dist/image-state-Bdi_-yWQ.mjs.map +1 -0
- package/dist/index.d.mts +22 -0
- package/dist/index.mjs +296 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node.d.mts +148 -0
- package/dist/node.mjs +412 -0
- package/dist/node.mjs.map +1 -0
- package/dist/presets.d.mts +3 -0
- package/dist/presets.mjs +45 -0
- package/dist/presets.mjs.map +1 -0
- package/package.json +62 -3
package/dist/node.mjs
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { a as serializeEncoding, d as parseImageName, n as isWithinSource } from "./image-presets-ZN7_n2p8.mjs";
|
|
2
|
+
import { c as readJsonFile, d as engineId, f as engineVersions, h as renderImage, l as writeJsonFile, m as readSourceSize, n as readCurrentManifest, o as UserError, p as openImageSource, r as readManifest, s as readFileIfExists, t as getImageCachePaths, u as formatList } from "./image-state-Bdi_-yWQ.mjs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { createReadStream, existsSync, mkdirSync, promises } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import sharp from "sharp";
|
|
7
|
+
import pLimit from "p-limit";
|
|
8
|
+
import { availableParallelism } from "node:os";
|
|
9
|
+
import { DatabaseSync } from "node:sqlite";
|
|
10
|
+
import { pipeline } from "node:stream/promises";
|
|
11
|
+
//#region src/render-key.ts
|
|
12
|
+
function hashRenderKey(parts) {
|
|
13
|
+
return createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 16);
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/file-cache.ts
|
|
17
|
+
async function createFileCache({ directory, onReplace }) {
|
|
18
|
+
const keysPath = `${directory}-keys.json`;
|
|
19
|
+
const stored = await readJsonFile(keysPath);
|
|
20
|
+
const records = new Map(Object.entries(stored?.files ?? {}));
|
|
21
|
+
function filePath(name) {
|
|
22
|
+
return path.join(directory, name);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
isFresh(name, key) {
|
|
26
|
+
return records.get(name)?.key === key && existsSync(filePath(name));
|
|
27
|
+
},
|
|
28
|
+
async save() {
|
|
29
|
+
await writeJsonFile(keysPath, { files: Object.fromEntries([...records].toSorted(([first], [second]) => first.localeCompare(second))) });
|
|
30
|
+
},
|
|
31
|
+
async write(name, key, data) {
|
|
32
|
+
const target = filePath(name);
|
|
33
|
+
const existing = await readFileIfExists(target);
|
|
34
|
+
const isChanged = !existing?.equals(data);
|
|
35
|
+
if (isChanged) {
|
|
36
|
+
const temporary = `${target}.tmp`;
|
|
37
|
+
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
38
|
+
await promises.writeFile(temporary, data);
|
|
39
|
+
if (existing) await onReplace?.(name);
|
|
40
|
+
await promises.rename(temporary, target);
|
|
41
|
+
}
|
|
42
|
+
records.set(name, { key });
|
|
43
|
+
return isChanged;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/render-lock.ts
|
|
49
|
+
async function didClaimLock(lockPath) {
|
|
50
|
+
try {
|
|
51
|
+
const handle = await promises.open(lockPath, "wx");
|
|
52
|
+
await handle.writeFile(String(process.pid));
|
|
53
|
+
await handle.close();
|
|
54
|
+
return true;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error.code === "EEXIST") return false;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isProcessAlive(pid) {
|
|
61
|
+
try {
|
|
62
|
+
process.kill(pid, 0);
|
|
63
|
+
return true;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return error.code !== "ESRCH";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function readLockHolder(lockPath) {
|
|
69
|
+
const data = await readFileIfExists(lockPath);
|
|
70
|
+
const pid = Number(data?.toString("utf8").trim());
|
|
71
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
|
|
72
|
+
}
|
|
73
|
+
async function acquireRenderLock(directory) {
|
|
74
|
+
const lockPath = `${directory}.lock`;
|
|
75
|
+
await promises.mkdir(path.dirname(lockPath), { recursive: true });
|
|
76
|
+
if (await didClaimLock(lockPath)) return lockPath;
|
|
77
|
+
const holder = await readLockHolder(lockPath);
|
|
78
|
+
if (holder !== void 0 && isProcessAlive(holder)) throw new UserError(`Another render (pid ${String(holder)}) holds ${lockPath}`);
|
|
79
|
+
await promises.rm(lockPath, { force: true });
|
|
80
|
+
if (await didClaimLock(lockPath)) return lockPath;
|
|
81
|
+
throw new UserError(`Another render took over the stale lock at ${lockPath}`);
|
|
82
|
+
}
|
|
83
|
+
async function withRenderLock(directory, run) {
|
|
84
|
+
const lockPath = await acquireRenderLock(directory);
|
|
85
|
+
try {
|
|
86
|
+
return await run();
|
|
87
|
+
} finally {
|
|
88
|
+
await promises.rm(lockPath, { force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/run-render-jobs.ts
|
|
93
|
+
const saveInterval = 1e4;
|
|
94
|
+
function getDefaultConcurrency() {
|
|
95
|
+
return Math.min(availableParallelism(), Number(process.env.UV_THREADPOOL_SIZE) || 4);
|
|
96
|
+
}
|
|
97
|
+
function runRenderJobs(options) {
|
|
98
|
+
const directory = path.resolve(options.directory);
|
|
99
|
+
return withRenderLock(directory, () => runJobs({
|
|
100
|
+
...options,
|
|
101
|
+
directory
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
async function runJobs({ concurrency = getDefaultConcurrency(), directory, jobs, onFailure = "throw", onProgress, onReplace, signal }) {
|
|
105
|
+
const counts = {
|
|
106
|
+
fresh: 0,
|
|
107
|
+
overwritten: 0,
|
|
108
|
+
rendered: 0,
|
|
109
|
+
written: 0
|
|
110
|
+
};
|
|
111
|
+
const cache = await createFileCache({
|
|
112
|
+
directory,
|
|
113
|
+
onReplace: async (name) => {
|
|
114
|
+
counts.overwritten++;
|
|
115
|
+
await onReplace?.(name);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
const limit = pLimit(concurrency);
|
|
119
|
+
const saveLimit = pLimit(1);
|
|
120
|
+
const save = () => saveLimit(() => cache.save());
|
|
121
|
+
const failures = [];
|
|
122
|
+
let done = 0;
|
|
123
|
+
let lastSave = Date.now();
|
|
124
|
+
async function runJob(job) {
|
|
125
|
+
if (cache.isFresh(job.name, job.key)) {
|
|
126
|
+
counts.fresh++;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const data = await job.render();
|
|
130
|
+
counts.rendered++;
|
|
131
|
+
if (await cache.write(job.name, job.key, data)) counts.written++;
|
|
132
|
+
}
|
|
133
|
+
await Promise.all(jobs.map((job) => limit(async () => {
|
|
134
|
+
if (signal?.aborted) return;
|
|
135
|
+
try {
|
|
136
|
+
await runJob(job);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
failures.push({
|
|
139
|
+
message: error instanceof Error ? error.message : String(error),
|
|
140
|
+
name: job.name
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
done++;
|
|
144
|
+
onProgress?.({
|
|
145
|
+
...counts,
|
|
146
|
+
done,
|
|
147
|
+
total: jobs.length
|
|
148
|
+
});
|
|
149
|
+
if (Date.now() - lastSave < saveInterval) return;
|
|
150
|
+
lastSave = Date.now();
|
|
151
|
+
await save();
|
|
152
|
+
})));
|
|
153
|
+
await save();
|
|
154
|
+
signal?.throwIfAborted();
|
|
155
|
+
const sortedFailures = failures.toSorted((first, second) => first.name.localeCompare(second.name));
|
|
156
|
+
if (onFailure === "throw" && sortedFailures.length > 0) throw new Error(`${String(sortedFailures.length)} files failed to render:\n${formatList(sortedFailures.map(({ message, name }) => `${name}: ${message}`))}`);
|
|
157
|
+
return {
|
|
158
|
+
...counts,
|
|
159
|
+
failures: sortedFailures,
|
|
160
|
+
total: jobs.length
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/source-fingerprints.ts
|
|
165
|
+
async function getFileStats(filePath) {
|
|
166
|
+
try {
|
|
167
|
+
return await promises.stat(filePath);
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function openDatabase(cachePath) {
|
|
173
|
+
mkdirSync(cachePath, { recursive: true });
|
|
174
|
+
const database = new DatabaseSync(path.join(cachePath, "source-fingerprints.sqlite"));
|
|
175
|
+
database.exec("PRAGMA busy_timeout = 10000; PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL");
|
|
176
|
+
database.exec("CREATE TABLE IF NOT EXISTS fingerprints (path TEXT PRIMARY KEY, version TEXT NOT NULL, sha256 TEXT NOT NULL, width INTEGER NOT NULL, height INTEGER NOT NULL)");
|
|
177
|
+
return database;
|
|
178
|
+
}
|
|
179
|
+
function createSourceFingerprints(cachePath) {
|
|
180
|
+
const database = openDatabase(cachePath);
|
|
181
|
+
const selectStatement = database.prepare("SELECT sha256, width, height FROM fingerprints WHERE path = ? AND version = ?");
|
|
182
|
+
const upsertStatement = database.prepare("INSERT INTO fingerprints (path, version, sha256, width, height) VALUES (?, ?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET version = excluded.version, sha256 = excluded.sha256, width = excluded.width, height = excluded.height");
|
|
183
|
+
return {
|
|
184
|
+
close() {
|
|
185
|
+
database.close();
|
|
186
|
+
},
|
|
187
|
+
async get(filePath) {
|
|
188
|
+
const absolutePath = path.resolve(filePath);
|
|
189
|
+
const stats = await getFileStats(absolutePath);
|
|
190
|
+
if (!stats) return;
|
|
191
|
+
const version = `${String(stats.mtimeMs)}:${String(stats.size)}`;
|
|
192
|
+
const cached = selectStatement.get(absolutePath, version);
|
|
193
|
+
if (cached) return {
|
|
194
|
+
height: cached.height,
|
|
195
|
+
sha256: cached.sha256,
|
|
196
|
+
width: cached.width
|
|
197
|
+
};
|
|
198
|
+
const digest = createHash("sha256");
|
|
199
|
+
await pipeline(createReadStream(absolutePath), digest);
|
|
200
|
+
const { height, width } = await readSourceSize(absolutePath);
|
|
201
|
+
const fingerprint = {
|
|
202
|
+
height,
|
|
203
|
+
sha256: digest.digest("hex"),
|
|
204
|
+
width
|
|
205
|
+
};
|
|
206
|
+
upsertStatement.run(absolutePath, version, fingerprint.sha256, width, height);
|
|
207
|
+
return fingerprint;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/image-build.ts
|
|
213
|
+
function sortNames(names) {
|
|
214
|
+
return names.toSorted((first, second) => first.localeCompare(second));
|
|
215
|
+
}
|
|
216
|
+
function getRenditionKey({ digest, encoding, engine, format, height, width }) {
|
|
217
|
+
return hashRenderKey([
|
|
218
|
+
digest,
|
|
219
|
+
format,
|
|
220
|
+
serializeEncoding(encoding),
|
|
221
|
+
width,
|
|
222
|
+
height ?? "",
|
|
223
|
+
3,
|
|
224
|
+
engine
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
function getManifestRenditions(manifest) {
|
|
228
|
+
const renditions = [];
|
|
229
|
+
const invalid = [];
|
|
230
|
+
for (const [name, sourceId] of Object.entries(manifest.renditions)) {
|
|
231
|
+
const parsed = parseImageName(name);
|
|
232
|
+
if (parsed) renditions.push({
|
|
233
|
+
...parsed,
|
|
234
|
+
name,
|
|
235
|
+
sourceId
|
|
236
|
+
});
|
|
237
|
+
else invalid.push(name);
|
|
238
|
+
}
|
|
239
|
+
if (invalid.length > 0) throw new UserError(`${String(invalid.length)} manifest names are outside the grammar; rebuild first:\n${formatList(sortNames(invalid))}`);
|
|
240
|
+
return renditions;
|
|
241
|
+
}
|
|
242
|
+
async function readSourceDigests(fingerprints, { mediaPath }, { concurrency, renditions }) {
|
|
243
|
+
const limit = pLimit(concurrency);
|
|
244
|
+
const digests = /* @__PURE__ */ new Map();
|
|
245
|
+
const vanished = [];
|
|
246
|
+
const upscaled = [];
|
|
247
|
+
const renditionsBySource = Map.groupBy(renditions, (rendition) => rendition.sourceId);
|
|
248
|
+
await Promise.all([...renditionsBySource].map(([sourceId, sourceRenditions]) => limit(async () => {
|
|
249
|
+
const fingerprint = await fingerprints.get(path.resolve(mediaPath, sourceId));
|
|
250
|
+
if (!fingerprint) {
|
|
251
|
+
vanished.push(sourceId);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
for (const rendition of sourceRenditions) if (!isWithinSource(rendition, fingerprint)) upscaled.push(`${rendition.name} (source ${String(fingerprint.width)}x${String(fingerprint.height)})`);
|
|
255
|
+
digests.set(sourceId, fingerprint.sha256);
|
|
256
|
+
})));
|
|
257
|
+
if (vanished.length > 0) throw new UserError(`${String(vanished.length)} sources vanished since the build; rebuild first:\n${formatList(sortNames(vanished))}`);
|
|
258
|
+
if (upscaled.length > 0) throw new UserError(`${String(upscaled.length)} renditions are larger than their source; rebuild first:\n${formatList(sortNames(upscaled))}`);
|
|
259
|
+
return digests;
|
|
260
|
+
}
|
|
261
|
+
function assertEncodedSize(rendition, info) {
|
|
262
|
+
if (info.width === rendition.width && (rendition.height ?? info.height) === info.height) return;
|
|
263
|
+
throw new Error(`encoded ${String(info.width)}x${String(info.height)}`);
|
|
264
|
+
}
|
|
265
|
+
function getManifestRenditionKey(rendition, manifest, digests) {
|
|
266
|
+
const digest = digests.get(rendition.sourceId);
|
|
267
|
+
if (!digest) throw new Error(`no digest was read for source ${rendition.sourceId}`);
|
|
268
|
+
return getRenditionKey({
|
|
269
|
+
digest,
|
|
270
|
+
encoding: manifest.encoding[rendition.format],
|
|
271
|
+
engine: engineId,
|
|
272
|
+
format: rendition.format,
|
|
273
|
+
height: rendition.height,
|
|
274
|
+
width: rendition.width
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async function encodeRendition(rendition, manifest) {
|
|
278
|
+
const { data, info } = await renderImage(path.resolve(manifest.mediaPath, rendition.sourceId), {
|
|
279
|
+
encoding: manifest.encoding,
|
|
280
|
+
format: rendition.format,
|
|
281
|
+
height: rendition.height,
|
|
282
|
+
limitInputPixels: manifest.limitInputPixels,
|
|
283
|
+
width: rendition.width
|
|
284
|
+
});
|
|
285
|
+
assertEncodedSize(rendition, info);
|
|
286
|
+
return data;
|
|
287
|
+
}
|
|
288
|
+
async function readImageJobs(cachePath, concurrency) {
|
|
289
|
+
const { renditionsPath } = getImageCachePaths(cachePath);
|
|
290
|
+
const manifest = await readCurrentManifest(cachePath);
|
|
291
|
+
const renditions = getManifestRenditions(manifest);
|
|
292
|
+
const fingerprints = createSourceFingerprints(cachePath);
|
|
293
|
+
try {
|
|
294
|
+
const digests = await readSourceDigests(fingerprints, manifest, {
|
|
295
|
+
concurrency,
|
|
296
|
+
renditions
|
|
297
|
+
});
|
|
298
|
+
return {
|
|
299
|
+
directory: renditionsPath,
|
|
300
|
+
jobs: renditions.map((rendition) => ({
|
|
301
|
+
key: getManifestRenditionKey(rendition, manifest, digests),
|
|
302
|
+
name: rendition.name,
|
|
303
|
+
render: () => encodeRendition(rendition, manifest)
|
|
304
|
+
}))
|
|
305
|
+
};
|
|
306
|
+
} finally {
|
|
307
|
+
fingerprints.close();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function renderImages(options) {
|
|
311
|
+
const cachePath = path.resolve(options.cachePath);
|
|
312
|
+
const concurrency = getDefaultConcurrency();
|
|
313
|
+
const run = await readImageJobs(cachePath, concurrency);
|
|
314
|
+
sharp.concurrency(1);
|
|
315
|
+
sharp.cache(false);
|
|
316
|
+
const counts = await runRenderJobs({
|
|
317
|
+
...options,
|
|
318
|
+
...run,
|
|
319
|
+
concurrency
|
|
320
|
+
});
|
|
321
|
+
await promises.rm(getImageCachePaths(cachePath).devPath, {
|
|
322
|
+
force: true,
|
|
323
|
+
recursive: true
|
|
324
|
+
});
|
|
325
|
+
return counts;
|
|
326
|
+
}
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/open-graph-build.ts
|
|
329
|
+
function groupByImage(cards) {
|
|
330
|
+
const groups = /* @__PURE__ */ new Map();
|
|
331
|
+
for (const [name, { image, input }] of Object.entries(cards)) {
|
|
332
|
+
const groupKey = image ? JSON.stringify(image) : "";
|
|
333
|
+
const group = groups.get(groupKey) ?? (image ? {
|
|
334
|
+
cards: [],
|
|
335
|
+
image
|
|
336
|
+
} : { cards: [] });
|
|
337
|
+
group.cards.push([name, input]);
|
|
338
|
+
groups.set(groupKey, group);
|
|
339
|
+
}
|
|
340
|
+
return [...groups.values()];
|
|
341
|
+
}
|
|
342
|
+
async function readSourceState(fingerprints, mediaPath, image) {
|
|
343
|
+
const source = {
|
|
344
|
+
...image,
|
|
345
|
+
path: path.resolve(mediaPath, image.id)
|
|
346
|
+
};
|
|
347
|
+
const fingerprint = await fingerprints.get(source.path);
|
|
348
|
+
return fingerprint ? {
|
|
349
|
+
digest: fingerprint.sha256,
|
|
350
|
+
source
|
|
351
|
+
} : { source };
|
|
352
|
+
}
|
|
353
|
+
function createGroupDecoder(decode) {
|
|
354
|
+
let current;
|
|
355
|
+
return function getDecoded(state) {
|
|
356
|
+
if (current?.state !== state) current = {
|
|
357
|
+
decoded: state.digest ? decode(state.source) : Promise.reject(/* @__PURE__ */ new Error(`Missing image: ${state.source.id}`)),
|
|
358
|
+
state
|
|
359
|
+
};
|
|
360
|
+
return current.decoded;
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async function createCardJobs({ cachePath, decode, groups, mediaPath, render, templateVersion }) {
|
|
364
|
+
const fingerprints = createSourceFingerprints(cachePath);
|
|
365
|
+
const getDecoded = createGroupDecoder(decode);
|
|
366
|
+
const jobs = [];
|
|
367
|
+
try {
|
|
368
|
+
for (const { cards, image } of groups) {
|
|
369
|
+
const state = image ? await readSourceState(fingerprints, mediaPath, image) : void 0;
|
|
370
|
+
for (const [name, input] of cards) jobs.push({
|
|
371
|
+
key: hashRenderKey([
|
|
372
|
+
engineId,
|
|
373
|
+
templateVersion,
|
|
374
|
+
JSON.stringify(input),
|
|
375
|
+
image ? JSON.stringify(image) : "",
|
|
376
|
+
state?.digest ?? ""
|
|
377
|
+
]),
|
|
378
|
+
name,
|
|
379
|
+
render: async () => render(input, state ? await getDecoded(state) : void 0)
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
} finally {
|
|
383
|
+
fingerprints.close();
|
|
384
|
+
}
|
|
385
|
+
return jobs;
|
|
386
|
+
}
|
|
387
|
+
async function renderOpenGraphCards({ cachePath, decode, render, templateVersion, ...options }) {
|
|
388
|
+
const resolvedCachePath = path.resolve(cachePath);
|
|
389
|
+
const { openGraphCardsPath } = getImageCachePaths(resolvedCachePath);
|
|
390
|
+
const manifest = await readCurrentManifest(resolvedCachePath);
|
|
391
|
+
const groups = groupByImage(manifest.openGraphCards);
|
|
392
|
+
const jobs = await createCardJobs({
|
|
393
|
+
cachePath: resolvedCachePath,
|
|
394
|
+
decode,
|
|
395
|
+
groups,
|
|
396
|
+
mediaPath: manifest.mediaPath,
|
|
397
|
+
render,
|
|
398
|
+
templateVersion
|
|
399
|
+
});
|
|
400
|
+
return {
|
|
401
|
+
...await runRenderJobs({
|
|
402
|
+
...options,
|
|
403
|
+
directory: openGraphCardsPath,
|
|
404
|
+
jobs
|
|
405
|
+
}),
|
|
406
|
+
sources: groups.filter((group) => group.image).length
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
410
|
+
export { UserError, createSourceFingerprints, engineVersions, getImageCachePaths, openImageSource, readManifest, readSourceSize, renderImages, renderOpenGraphCards, runRenderJobs };
|
|
411
|
+
|
|
412
|
+
//# sourceMappingURL=node.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.mjs","names":["fs","fs","fs","fs"],"sources":["../src/render-key.ts","../src/file-cache.ts","../src/render-lock.ts","../src/run-render-jobs.ts","../src/source-fingerprints.ts","../src/image-build.ts","../src/open-graph-build.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\nexport function hashRenderKey(parts: ReadonlyArray<number | string>) {\n\treturn createHash('sha256').update(parts.join(':')).digest('hex').slice(0, 16);\n}\n","import { existsSync, promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nimport { readFileIfExists, readJsonFile, writeJsonFile } from './files.js';\n\ninterface FileCacheRecord {\n\tkey: string;\n}\n\n// Renaming `files` would orphan every recorded key and re-render the whole library\ninterface RenderKeys {\n\tfiles: Record<string, FileCacheRecord>;\n}\n\nexport async function createFileCache({\n\tdirectory,\n\tonReplace,\n}: {\n\tdirectory: string;\n\tonReplace?: (name: string) => Promise<void>;\n}) {\n\tconst keysPath = `${directory}-keys.json`;\n\tconst stored = await readJsonFile<RenderKeys>(keysPath);\n\tconst records = new Map(Object.entries(stored?.files ?? {}));\n\n\tfunction filePath(name: string) {\n\t\treturn path.join(directory, name);\n\t}\n\n\treturn {\n\t\tisFresh(name: string, key: string) {\n\t\t\treturn records.get(name)?.key === key && existsSync(filePath(name));\n\t\t},\n\n\t\tasync save() {\n\t\t\tawait writeJsonFile(keysPath, {\n\t\t\t\tfiles: Object.fromEntries(\n\t\t\t\t\t[...records].toSorted(([first], [second]) => first.localeCompare(second)),\n\t\t\t\t),\n\t\t\t} satisfies RenderKeys);\n\t\t},\n\n\t\t// Identical bytes keep the old file and its mtime, so rsync has nothing to send\n\t\tasync write(name: string, key: string, data: Uint8Array) {\n\t\t\tconst target = filePath(name);\n\t\t\tconst existing = await readFileIfExists(target);\n\t\t\tconst isChanged = !existing?.equals(data);\n\n\t\t\tif (isChanged) {\n\t\t\t\tconst temporary = `${target}.tmp`;\n\n\t\t\t\tawait fs.mkdir(path.dirname(target), { recursive: true });\n\t\t\t\tawait fs.writeFile(temporary, data);\n\t\t\t\tif (existing) await onReplace?.(name);\n\t\t\t\tawait fs.rename(temporary, target);\n\t\t\t}\n\n\t\t\trecords.set(name, { key });\n\n\t\t\treturn isChanged;\n\t\t},\n\t};\n}\n","import { promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nimport { readFileIfExists } from './files.js';\nimport { UserError } from './user-error.js';\n\nasync function didClaimLock(lockPath: string) {\n\ttry {\n\t\tconst handle = await fs.open(lockPath, 'wx');\n\n\t\tawait handle.writeFile(String(process.pid));\n\t\tawait handle.close();\n\n\t\treturn true;\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === 'EEXIST') return false;\n\n\t\tthrow error;\n\t}\n}\n\nfunction isProcessAlive(pid: number) {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\n\t\treturn true;\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code !== 'ESRCH';\n\t}\n}\n\nasync function readLockHolder(lockPath: string) {\n\tconst data = await readFileIfExists(lockPath);\n\tconst pid = Number(data?.toString('utf8').trim());\n\n\treturn Number.isSafeInteger(pid) && pid > 0 ? pid : undefined;\n}\n\nasync function acquireRenderLock(directory: string) {\n\tconst lockPath = `${directory}.lock`;\n\n\tawait fs.mkdir(path.dirname(lockPath), { recursive: true });\n\n\tif (await didClaimLock(lockPath)) return lockPath;\n\n\tconst holder = await readLockHolder(lockPath);\n\n\tif (holder !== undefined && isProcessAlive(holder)) {\n\t\tthrow new UserError(`Another render (pid ${String(holder)}) holds ${lockPath}`);\n\t}\n\n\tawait fs.rm(lockPath, { force: true });\n\n\tif (await didClaimLock(lockPath)) return lockPath;\n\n\tthrow new UserError(`Another render took over the stale lock at ${lockPath}`);\n}\n\n// The lock sits beside the output directory, which is rsynced whole\nexport async function withRenderLock<Result>(directory: string, run: () => Promise<Result>) {\n\tconst lockPath = await acquireRenderLock(directory);\n\n\ttry {\n\t\treturn await run();\n\t} finally {\n\t\tawait fs.rm(lockPath, { force: true });\n\t}\n}\n","import { availableParallelism } from 'node:os';\nimport path from 'node:path';\nimport pLimit from 'p-limit';\n\nimport { createFileCache } from './file-cache.js';\nimport { formatList } from './format.js';\nimport { withRenderLock } from './render-lock.js';\n\nconst saveInterval = 10_000;\n\nexport interface RenderJob {\n\tkey: string;\n\tname: string;\n\trender: () => Promise<Uint8Array>;\n}\n\nexport interface RenderCounts {\n\tfresh: number;\n\toverwritten: number;\n\trendered: number;\n\twritten: number;\n}\n\nexport interface RenderFailure {\n\tmessage: string;\n\tname: string;\n}\n\nexport interface RenderJobOptions {\n\tconcurrency?: number;\n\tdirectory: string;\n\tjobs: ReadonlyArray<RenderJob>;\n\tonFailure?: 'report' | 'throw';\n\tonProgress?: (progress: RenderCounts & { done: number; total: number }) => void;\n\tonReplace?: (name: string) => Promise<void>;\n\tsignal?: AbortSignal;\n}\n\n// sharp runs each job on a libuv thread, so jobs beyond the pool size would only queue\nexport function getDefaultConcurrency() {\n\treturn Math.min(availableParallelism(), Number(process.env.UV_THREADPOOL_SIZE) || 4);\n}\n\nexport function runRenderJobs(options: RenderJobOptions) {\n\tconst directory = path.resolve(options.directory);\n\n\treturn withRenderLock(directory, () => runJobs({ ...options, directory }));\n}\n\nasync function runJobs({\n\tconcurrency = getDefaultConcurrency(),\n\tdirectory,\n\tjobs,\n\tonFailure = 'throw',\n\tonProgress,\n\tonReplace,\n\tsignal,\n}: RenderJobOptions) {\n\tconst counts: RenderCounts = { fresh: 0, overwritten: 0, rendered: 0, written: 0 };\n\tconst cache = await createFileCache({\n\t\tdirectory,\n\t\tonReplace: async (name) => {\n\t\t\tcounts.overwritten++;\n\t\t\tawait onReplace?.(name);\n\t\t},\n\t});\n\tconst limit = pLimit(concurrency);\n\tconst saveLimit = pLimit(1);\n\tconst save = () => saveLimit(() => cache.save());\n\tconst failures: Array<RenderFailure> = [];\n\tlet done = 0;\n\tlet lastSave = Date.now();\n\n\tasync function runJob(job: RenderJob) {\n\t\tif (cache.isFresh(job.name, job.key)) {\n\t\t\tcounts.fresh++;\n\t\t\treturn;\n\t\t}\n\n\t\tconst data = await job.render();\n\n\t\tcounts.rendered++;\n\t\tif (await cache.write(job.name, job.key, data)) counts.written++;\n\t}\n\n\tawait Promise.all(\n\t\tjobs.map((job) =>\n\t\t\tlimit(async () => {\n\t\t\t\tif (signal?.aborted) return;\n\n\t\t\t\ttry {\n\t\t\t\t\tawait runJob(job);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailures.push({\n\t\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t\tname: job.name,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tdone++;\n\t\t\t\tonProgress?.({ ...counts, done, total: jobs.length });\n\n\t\t\t\tif (Date.now() - lastSave < saveInterval) return;\n\n\t\t\t\tlastSave = Date.now();\n\t\t\t\tawait save();\n\t\t\t}),\n\t\t),\n\t);\n\n\tawait save();\n\tsignal?.throwIfAborted();\n\n\tconst sortedFailures = failures.toSorted((first, second) =>\n\t\tfirst.name.localeCompare(second.name),\n\t);\n\n\tif (onFailure === 'throw' && sortedFailures.length > 0) {\n\t\tthrow new Error(\n\t\t\t`${String(sortedFailures.length)} files failed to render:\\n${formatList(sortedFailures.map(({ message, name }) => `${name}: ${message}`))}`,\n\t\t);\n\t}\n\n\treturn { ...counts, failures: sortedFailures, total: jobs.length };\n}\n","import { createHash } from 'node:crypto';\nimport { createReadStream, promises as fs, mkdirSync } from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport { pipeline } from 'node:stream/promises';\n\nimport type { ImageDimensions } from './image-presets.js';\n\nimport { readSourceSize } from './image-render.js';\n\ninterface SourceFingerprint extends ImageDimensions {\n\tsha256: string;\n}\n\nexport interface SourceFingerprints {\n\tclose(): void;\n\tget(filePath: string): Promise<SourceFingerprint | undefined>;\n}\n\nasync function getFileStats(filePath: string) {\n\ttry {\n\t\treturn await fs.stat(filePath);\n\t} catch {\n\t\treturn;\n\t}\n}\n\nfunction openDatabase(cachePath: string) {\n\tmkdirSync(cachePath, { recursive: true });\n\n\tconst database = new DatabaseSync(path.join(cachePath, 'source-fingerprints.sqlite'));\n\n\tdatabase.exec(\n\t\t'PRAGMA busy_timeout = 10000; PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL',\n\t);\n\tdatabase.exec(\n\t\t'CREATE TABLE IF NOT EXISTS fingerprints (path TEXT PRIMARY KEY, version TEXT NOT NULL, sha256 TEXT NOT NULL, width INTEGER NOT NULL, height INTEGER NOT NULL)',\n\t);\n\n\treturn database;\n}\n\nexport function createSourceFingerprints(cachePath: string): SourceFingerprints {\n\tconst database = openDatabase(cachePath);\n\tconst selectStatement = database.prepare(\n\t\t'SELECT sha256, width, height FROM fingerprints WHERE path = ? AND version = ?',\n\t);\n\tconst upsertStatement = database.prepare(\n\t\t'INSERT INTO fingerprints (path, version, sha256, width, height) VALUES (?, ?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET version = excluded.version, sha256 = excluded.sha256, width = excluded.width, height = excluded.height',\n\t);\n\n\treturn {\n\t\tclose() {\n\t\t\tdatabase.close();\n\t\t},\n\n\t\tasync get(filePath) {\n\t\t\tconst absolutePath = path.resolve(filePath);\n\t\t\tconst stats = await getFileStats(absolutePath);\n\n\t\t\tif (!stats) return;\n\n\t\t\tconst version = `${String(stats.mtimeMs)}:${String(stats.size)}`;\n\t\t\tconst cached = selectStatement.get(absolutePath, version) as SourceFingerprint | undefined;\n\n\t\t\tif (cached) return { height: cached.height, sha256: cached.sha256, width: cached.width };\n\n\t\t\tconst digest = createHash('sha256');\n\n\t\t\tawait pipeline(createReadStream(absolutePath), digest);\n\n\t\t\tconst { height, width } = await readSourceSize(absolutePath);\n\t\t\tconst fingerprint = { height, sha256: digest.digest('hex'), width };\n\n\t\t\tupsertStatement.run(absolutePath, version, fingerprint.sha256, width, height);\n\n\t\t\treturn fingerprint;\n\t\t},\n\t};\n}\n","import type { OutputInfo } from 'sharp';\n\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport pLimit from 'p-limit';\nimport sharp from 'sharp';\n\nimport type { ImageFormat, ImageName } from './image-name.js';\nimport type { ImageManifest } from './image-state.js';\nimport type { RenderJobOptions } from './run-render-jobs.js';\nimport type { SourceFingerprints } from './source-fingerprints.js';\n\nimport { formatList } from './format.js';\nimport { imageRenderRevision, serializeEncoding } from './image-encoding.js';\nimport { parseImageName } from './image-name.js';\nimport { isWithinSource } from './image-presets.js';\nimport { engineId, renderImage } from './image-render.js';\nimport { getImageCachePaths, readCurrentManifest } from './image-state.js';\nimport { hashRenderKey } from './render-key.js';\nimport { getDefaultConcurrency, runRenderJobs } from './run-render-jobs.js';\nimport { createSourceFingerprints } from './source-fingerprints.js';\nimport { UserError } from './user-error.js';\n\nexport interface ImageRenderOptions extends Pick<\n\tRenderJobOptions,\n\t'onFailure' | 'onProgress' | 'onReplace' | 'signal'\n> {\n\tcachePath: string;\n}\n\ninterface ImageRendition extends ImageName {\n\tname: string;\n\tsourceId: string;\n}\n\nfunction sortNames(names: ReadonlyArray<string>) {\n\treturn names.toSorted((first, second) => first.localeCompare(second));\n}\n\nexport function getRenditionKey({\n\tdigest,\n\tencoding,\n\tengine,\n\tformat,\n\theight,\n\twidth,\n}: {\n\tdigest: string;\n\tencoding: ImageManifest['encoding'][ImageFormat];\n\tengine: string;\n\tformat: ImageFormat;\n\theight?: number | undefined;\n\twidth: number;\n}) {\n\treturn hashRenderKey([\n\t\tdigest,\n\t\tformat,\n\t\tserializeEncoding(encoding),\n\t\twidth,\n\t\theight ?? '',\n\t\timageRenderRevision,\n\t\tengine,\n\t]);\n}\n\nfunction getManifestRenditions(manifest: ImageManifest) {\n\tconst renditions: Array<ImageRendition> = [];\n\tconst invalid: Array<string> = [];\n\n\tfor (const [name, sourceId] of Object.entries(manifest.renditions)) {\n\t\tconst parsed = parseImageName(name);\n\n\t\tif (parsed) {\n\t\t\trenditions.push({ ...parsed, name, sourceId });\n\t\t} else {\n\t\t\tinvalid.push(name);\n\t\t}\n\t}\n\n\tif (invalid.length > 0) {\n\t\tthrow new UserError(\n\t\t\t`${String(invalid.length)} manifest names are outside the grammar; rebuild first:\\n${formatList(sortNames(invalid))}`,\n\t\t);\n\t}\n\n\treturn renditions;\n}\n\nasync function readSourceDigests(\n\tfingerprints: SourceFingerprints,\n\t{ mediaPath }: ImageManifest,\n\t{ concurrency, renditions }: { concurrency: number; renditions: ReadonlyArray<ImageRendition> },\n) {\n\tconst limit = pLimit(concurrency);\n\tconst digests = new Map<string, string>();\n\tconst vanished: Array<string> = [];\n\tconst upscaled: Array<string> = [];\n\tconst renditionsBySource = Map.groupBy(renditions, (rendition) => rendition.sourceId);\n\n\tawait Promise.all(\n\t\t[...renditionsBySource].map(([sourceId, sourceRenditions]) =>\n\t\t\tlimit(async () => {\n\t\t\t\tconst fingerprint = await fingerprints.get(path.resolve(mediaPath, sourceId));\n\n\t\t\t\tif (!fingerprint) {\n\t\t\t\t\tvanished.push(sourceId);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tfor (const rendition of sourceRenditions) {\n\t\t\t\t\tif (!isWithinSource(rendition, fingerprint)) {\n\t\t\t\t\t\tupscaled.push(\n\t\t\t\t\t\t\t`${rendition.name} (source ${String(fingerprint.width)}x${String(fingerprint.height)})`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tdigests.set(sourceId, fingerprint.sha256);\n\t\t\t}),\n\t\t),\n\t);\n\n\tif (vanished.length > 0) {\n\t\tthrow new UserError(\n\t\t\t`${String(vanished.length)} sources vanished since the build; rebuild first:\\n${formatList(sortNames(vanished))}`,\n\t\t);\n\t}\n\n\tif (upscaled.length > 0) {\n\t\tthrow new UserError(\n\t\t\t`${String(upscaled.length)} renditions are larger than their source; rebuild first:\\n${formatList(sortNames(upscaled))}`,\n\t\t);\n\t}\n\n\treturn digests;\n}\n\nfunction assertEncodedSize(rendition: ImageRendition, info: OutputInfo) {\n\tif (info.width === rendition.width && (rendition.height ?? info.height) === info.height) return;\n\n\tthrow new Error(`encoded ${String(info.width)}x${String(info.height)}`);\n}\n\nfunction getManifestRenditionKey(\n\trendition: ImageRendition,\n\tmanifest: ImageManifest,\n\tdigests: ReadonlyMap<string, string>,\n) {\n\tconst digest = digests.get(rendition.sourceId);\n\n\tif (!digest) throw new Error(`no digest was read for source ${rendition.sourceId}`);\n\n\treturn getRenditionKey({\n\t\tdigest,\n\t\tencoding: manifest.encoding[rendition.format],\n\t\tengine: engineId,\n\t\tformat: rendition.format,\n\t\theight: rendition.height,\n\t\twidth: rendition.width,\n\t});\n}\n\nasync function encodeRendition(rendition: ImageRendition, manifest: ImageManifest) {\n\tconst { data, info } = await renderImage(path.resolve(manifest.mediaPath, rendition.sourceId), {\n\t\tencoding: manifest.encoding,\n\t\tformat: rendition.format,\n\t\theight: rendition.height,\n\t\tlimitInputPixels: manifest.limitInputPixels,\n\t\twidth: rendition.width,\n\t});\n\n\tassertEncodedSize(rendition, info);\n\n\treturn data;\n}\n\nasync function readImageJobs(cachePath: string, concurrency: number) {\n\tconst { renditionsPath } = getImageCachePaths(cachePath);\n\tconst manifest = await readCurrentManifest(cachePath);\n\tconst renditions = getManifestRenditions(manifest);\n\tconst fingerprints = createSourceFingerprints(cachePath);\n\n\ttry {\n\t\tconst digests = await readSourceDigests(fingerprints, manifest, { concurrency, renditions });\n\t\tconst jobs = renditions.map((rendition) => ({\n\t\t\tkey: getManifestRenditionKey(rendition, manifest, digests),\n\t\t\tname: rendition.name,\n\t\t\trender: () => encodeRendition(rendition, manifest),\n\t\t}));\n\n\t\treturn { directory: renditionsPath, jobs };\n\t} finally {\n\t\tfingerprints.close();\n\t}\n}\n\nexport async function renderImages(options: ImageRenderOptions) {\n\tconst cachePath = path.resolve(options.cachePath);\n\tconst concurrency = getDefaultConcurrency();\n\tconst run = await readImageJobs(cachePath, concurrency);\n\n\t// Process-wide; the job pool supplies the parallelism instead of libvips threads\n\tsharp.concurrency(1);\n\tsharp.cache(false);\n\n\tconst counts = await runRenderJobs({ ...options, ...run, concurrency });\n\n\t// Dev serves built renditions first, so its own renders are redundant now\n\tawait fs.rm(getImageCachePaths(cachePath).devPath, { force: true, recursive: true });\n\n\treturn counts;\n}\n","import path from 'node:path';\n\nimport type { OpenGraphCard, OpenGraphCardImage, OpenGraphCardInput } from './open-graph-cards.js';\nimport type { RenderJob, RenderJobOptions } from './run-render-jobs.js';\nimport type { SourceFingerprints } from './source-fingerprints.js';\n\nimport { engineId } from './image-render.js';\nimport { getImageCachePaths, readCurrentManifest } from './image-state.js';\nimport { hashRenderKey } from './render-key.js';\nimport { runRenderJobs } from './run-render-jobs.js';\nimport { createSourceFingerprints } from './source-fingerprints.js';\n\nexport interface OpenGraphCardSource extends OpenGraphCardImage {\n\tpath: string;\n}\n\nexport interface OpenGraphCardRenderOptions<Decoded> extends Pick<\n\tRenderJobOptions,\n\t'concurrency' | 'onFailure' | 'onProgress' | 'onReplace' | 'signal'\n> {\n\tcachePath: string;\n\tdecode: (source: OpenGraphCardSource) => Promise<Decoded>;\n\trender: (input: OpenGraphCardInput, decoded?: Decoded) => Promise<Uint8Array>;\n\ttemplateVersion: string;\n}\n\ninterface OpenGraphCardGroup {\n\tcards: Array<[name: string, input: OpenGraphCardInput]>;\n\timage?: OpenGraphCardImage;\n}\n\ninterface OpenGraphCardSourceState {\n\tdigest?: string;\n\tsource: OpenGraphCardSource;\n}\n\nfunction groupByImage(cards: Record<string, OpenGraphCard>) {\n\tconst groups = new Map<string, OpenGraphCardGroup>();\n\n\tfor (const [name, { image, input }] of Object.entries(cards)) {\n\t\tconst groupKey = image ? JSON.stringify(image) : '';\n\t\tconst group = groups.get(groupKey) ?? (image ? { cards: [], image } : { cards: [] });\n\n\t\tgroup.cards.push([name, input]);\n\t\tgroups.set(groupKey, group);\n\t}\n\n\treturn [...groups.values()];\n}\n\nasync function readSourceState(\n\tfingerprints: SourceFingerprints,\n\tmediaPath: string,\n\timage: OpenGraphCardImage,\n): Promise<OpenGraphCardSourceState> {\n\tconst source = { ...image, path: path.resolve(mediaPath, image.id) };\n\tconst fingerprint = await fingerprints.get(source.path);\n\n\treturn fingerprint ? { digest: fingerprint.sha256, source } : { source };\n}\n\nfunction createGroupDecoder<Decoded>(decode: OpenGraphCardRenderOptions<Decoded>['decode']) {\n\tlet current: undefined | { decoded: Promise<Decoded>; state: OpenGraphCardSourceState };\n\n\treturn function getDecoded(state: OpenGraphCardSourceState) {\n\t\tif (current?.state !== state) {\n\t\t\tcurrent = {\n\t\t\t\tdecoded: state.digest\n\t\t\t\t\t? decode(state.source)\n\t\t\t\t\t: Promise.reject(new Error(`Missing image: ${state.source.id}`)),\n\t\t\t\tstate,\n\t\t\t};\n\t\t}\n\n\t\treturn current.decoded;\n\t};\n}\n\nasync function createCardJobs<Decoded>({\n\tcachePath,\n\tdecode,\n\tgroups,\n\tmediaPath,\n\trender,\n\ttemplateVersion,\n}: Pick<OpenGraphCardRenderOptions<Decoded>, 'decode' | 'render' | 'templateVersion'> & {\n\tcachePath: string;\n\tgroups: ReadonlyArray<OpenGraphCardGroup>;\n\tmediaPath: string;\n}) {\n\tconst fingerprints = createSourceFingerprints(cachePath);\n\tconst getDecoded = createGroupDecoder(decode);\n\tconst jobs: Array<RenderJob> = [];\n\n\ttry {\n\t\tfor (const { cards, image } of groups) {\n\t\t\tconst state = image ? await readSourceState(fingerprints, mediaPath, image) : undefined;\n\n\t\t\tfor (const [name, input] of cards) {\n\t\t\t\tjobs.push({\n\t\t\t\t\tkey: hashRenderKey([\n\t\t\t\t\t\tengineId,\n\t\t\t\t\t\ttemplateVersion,\n\t\t\t\t\t\tJSON.stringify(input),\n\t\t\t\t\t\timage ? JSON.stringify(image) : '',\n\t\t\t\t\t\tstate?.digest ?? '',\n\t\t\t\t\t]),\n\t\t\t\t\tname,\n\t\t\t\t\trender: async () => render(input, state ? await getDecoded(state) : undefined),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tfingerprints.close();\n\t}\n\n\treturn jobs;\n}\n\nexport async function renderOpenGraphCards<Decoded>({\n\tcachePath,\n\tdecode,\n\trender,\n\ttemplateVersion,\n\t...options\n}: OpenGraphCardRenderOptions<Decoded>) {\n\tconst resolvedCachePath = path.resolve(cachePath);\n\tconst { openGraphCardsPath } = getImageCachePaths(resolvedCachePath);\n\tconst manifest = await readCurrentManifest(resolvedCachePath);\n\tconst groups = groupByImage(manifest.openGraphCards);\n\tconst jobs = await createCardJobs({\n\t\tcachePath: resolvedCachePath,\n\t\tdecode,\n\t\tgroups,\n\t\tmediaPath: manifest.mediaPath,\n\t\trender,\n\t\ttemplateVersion,\n\t});\n\tconst counts = await runRenderJobs({\n\t\t...options,\n\t\tdirectory: openGraphCardsPath,\n\t\tjobs,\n\t});\n\n\treturn { ...counts, sources: groups.filter((group) => group.image).length };\n}\n"],"mappings":";;;;;;;;;;;AAEA,SAAgB,cAAc,OAAuC;CACpE,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,EAAE;AAC9E;;;ACUA,eAAsB,gBAAgB,EACrC,WACA,aAIE;CACF,MAAM,WAAW,GAAG,UAAU;CAC9B,MAAM,SAAS,MAAM,aAAyB,QAAQ;CACtD,MAAM,UAAU,IAAI,IAAI,OAAO,QAAQ,QAAQ,SAAS,CAAC,CAAC,CAAC;CAE3D,SAAS,SAAS,MAAc;EAC/B,OAAO,KAAK,KAAK,WAAW,IAAI;CACjC;CAEA,OAAO;EACN,QAAQ,MAAc,KAAa;GAClC,OAAO,QAAQ,IAAI,IAAI,CAAC,EAAE,QAAQ,OAAO,WAAW,SAAS,IAAI,CAAC;EACnE;EAEA,MAAM,OAAO;GACZ,MAAM,cAAc,UAAU,EAC7B,OAAO,OAAO,YACb,CAAC,GAAG,OAAO,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,YAAY,MAAM,cAAc,MAAM,CAAC,CACzE,EACD,CAAsB;EACvB;EAGA,MAAM,MAAM,MAAc,KAAa,MAAkB;GACxD,MAAM,SAAS,SAAS,IAAI;GAC5B,MAAM,WAAW,MAAM,iBAAiB,MAAM;GAC9C,MAAM,YAAY,CAAC,UAAU,OAAO,IAAI;GAExC,IAAI,WAAW;IACd,MAAM,YAAY,GAAG,OAAO;IAE5B,MAAMA,SAAG,MAAM,KAAK,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;IACxD,MAAMA,SAAG,UAAU,WAAW,IAAI;IAClC,IAAI,UAAU,MAAM,YAAY,IAAI;IACpC,MAAMA,SAAG,OAAO,WAAW,MAAM;GAClC;GAEA,QAAQ,IAAI,MAAM,EAAE,IAAI,CAAC;GAEzB,OAAO;EACR;CACD;AACD;;;ACxDA,eAAe,aAAa,UAAkB;CAC7C,IAAI;EACH,MAAM,SAAS,MAAMC,SAAG,KAAK,UAAU,IAAI;EAE3C,MAAM,OAAO,UAAU,OAAO,QAAQ,GAAG,CAAC;EAC1C,MAAM,OAAO,MAAM;EAEnB,OAAO;CACR,SAAS,OAAO;EACf,IAAK,MAAgC,SAAS,UAAU,OAAO;EAE/D,MAAM;CACP;AACD;AAEA,SAAS,eAAe,KAAa;CACpC,IAAI;EACH,QAAQ,KAAK,KAAK,CAAC;EAEnB,OAAO;CACR,SAAS,OAAO;EACf,OAAQ,MAAgC,SAAS;CAClD;AACD;AAEA,eAAe,eAAe,UAAkB;CAC/C,MAAM,OAAO,MAAM,iBAAiB,QAAQ;CAC5C,MAAM,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC,CAAC,KAAK,CAAC;CAEhD,OAAO,OAAO,cAAc,GAAG,KAAK,MAAM,IAAI,MAAM,KAAA;AACrD;AAEA,eAAe,kBAAkB,WAAmB;CACnD,MAAM,WAAW,GAAG,UAAU;CAE9B,MAAMA,SAAG,MAAM,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;CAE1D,IAAI,MAAM,aAAa,QAAQ,GAAG,OAAO;CAEzC,MAAM,SAAS,MAAM,eAAe,QAAQ;CAE5C,IAAI,WAAW,KAAA,KAAa,eAAe,MAAM,GAChD,MAAM,IAAI,UAAU,uBAAuB,OAAO,MAAM,EAAE,UAAU,UAAU;CAG/E,MAAMA,SAAG,GAAG,UAAU,EAAE,OAAO,KAAK,CAAC;CAErC,IAAI,MAAM,aAAa,QAAQ,GAAG,OAAO;CAEzC,MAAM,IAAI,UAAU,8CAA8C,UAAU;AAC7E;AAGA,eAAsB,eAAuB,WAAmB,KAA4B;CAC3F,MAAM,WAAW,MAAM,kBAAkB,SAAS;CAElD,IAAI;EACH,OAAO,MAAM,IAAI;CAClB,UAAU;EACT,MAAMA,SAAG,GAAG,UAAU,EAAE,OAAO,KAAK,CAAC;CACtC;AACD;;;AC3DA,MAAM,eAAe;AA+BrB,SAAgB,wBAAwB;CACvC,OAAO,KAAK,IAAI,qBAAqB,GAAG,OAAO,QAAQ,IAAI,kBAAkB,KAAK,CAAC;AACpF;AAEA,SAAgB,cAAc,SAA2B;CACxD,MAAM,YAAY,KAAK,QAAQ,QAAQ,SAAS;CAEhD,OAAO,eAAe,iBAAiB,QAAQ;EAAE,GAAG;EAAS;CAAU,CAAC,CAAC;AAC1E;AAEA,eAAe,QAAQ,EACtB,cAAc,sBAAsB,GACpC,WACA,MACA,YAAY,SACZ,YACA,WACA,UACoB;CACpB,MAAM,SAAuB;EAAE,OAAO;EAAG,aAAa;EAAG,UAAU;EAAG,SAAS;CAAE;CACjF,MAAM,QAAQ,MAAM,gBAAgB;EACnC;EACA,WAAW,OAAO,SAAS;GAC1B,OAAO;GACP,MAAM,YAAY,IAAI;EACvB;CACD,CAAC;CACD,MAAM,QAAQ,OAAO,WAAW;CAChC,MAAM,YAAY,OAAO,CAAC;CAC1B,MAAM,aAAa,gBAAgB,MAAM,KAAK,CAAC;CAC/C,MAAM,WAAiC,CAAC;CACxC,IAAI,OAAO;CACX,IAAI,WAAW,KAAK,IAAI;CAExB,eAAe,OAAO,KAAgB;EACrC,IAAI,MAAM,QAAQ,IAAI,MAAM,IAAI,GAAG,GAAG;GACrC,OAAO;GACP;EACD;EAEA,MAAM,OAAO,MAAM,IAAI,OAAO;EAE9B,OAAO;EACP,IAAI,MAAM,MAAM,MAAM,IAAI,MAAM,IAAI,KAAK,IAAI,GAAG,OAAO;CACxD;CAEA,MAAM,QAAQ,IACb,KAAK,KAAK,QACT,MAAM,YAAY;EACjB,IAAI,QAAQ,SAAS;EAErB,IAAI;GACH,MAAM,OAAO,GAAG;EACjB,SAAS,OAAO;GACf,SAAS,KAAK;IACb,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAC9D,MAAM,IAAI;GACX,CAAC;EACF;EAEA;EACA,aAAa;GAAE,GAAG;GAAQ;GAAM,OAAO,KAAK;EAAO,CAAC;EAEpD,IAAI,KAAK,IAAI,IAAI,WAAW,cAAc;EAE1C,WAAW,KAAK,IAAI;EACpB,MAAM,KAAK;CACZ,CAAC,CACF,CACD;CAEA,MAAM,KAAK;CACX,QAAQ,eAAe;CAEvB,MAAM,iBAAiB,SAAS,UAAU,OAAO,WAChD,MAAM,KAAK,cAAc,OAAO,IAAI,CACrC;CAEA,IAAI,cAAc,WAAW,eAAe,SAAS,GACpD,MAAM,IAAI,MACT,GAAG,OAAO,eAAe,MAAM,EAAE,4BAA4B,WAAW,eAAe,KAAK,EAAE,SAAS,WAAW,GAAG,KAAK,IAAI,SAAS,CAAC,GACzI;CAGD,OAAO;EAAE,GAAG;EAAQ,UAAU;EAAgB,OAAO,KAAK;CAAO;AAClE;;;ACzGA,eAAe,aAAa,UAAkB;CAC7C,IAAI;EACH,OAAO,MAAMC,SAAG,KAAK,QAAQ;CAC9B,QAAQ;EACP;CACD;AACD;AAEA,SAAS,aAAa,WAAmB;CACxC,UAAU,WAAW,EAAE,WAAW,KAAK,CAAC;CAExC,MAAM,WAAW,IAAI,aAAa,KAAK,KAAK,WAAW,4BAA4B,CAAC;CAEpF,SAAS,KACR,qFACD;CACA,SAAS,KACR,+JACD;CAEA,OAAO;AACR;AAEA,SAAgB,yBAAyB,WAAuC;CAC/E,MAAM,WAAW,aAAa,SAAS;CACvC,MAAM,kBAAkB,SAAS,QAChC,+EACD;CACA,MAAM,kBAAkB,SAAS,QAChC,+NACD;CAEA,OAAO;EACN,QAAQ;GACP,SAAS,MAAM;EAChB;EAEA,MAAM,IAAI,UAAU;GACnB,MAAM,eAAe,KAAK,QAAQ,QAAQ;GAC1C,MAAM,QAAQ,MAAM,aAAa,YAAY;GAE7C,IAAI,CAAC,OAAO;GAEZ,MAAM,UAAU,GAAG,OAAO,MAAM,OAAO,EAAE,GAAG,OAAO,MAAM,IAAI;GAC7D,MAAM,SAAS,gBAAgB,IAAI,cAAc,OAAO;GAExD,IAAI,QAAQ,OAAO;IAAE,QAAQ,OAAO;IAAQ,QAAQ,OAAO;IAAQ,OAAO,OAAO;GAAM;GAEvF,MAAM,SAAS,WAAW,QAAQ;GAElC,MAAM,SAAS,iBAAiB,YAAY,GAAG,MAAM;GAErD,MAAM,EAAE,QAAQ,UAAU,MAAM,eAAe,YAAY;GAC3D,MAAM,cAAc;IAAE;IAAQ,QAAQ,OAAO,OAAO,KAAK;IAAG;GAAM;GAElE,gBAAgB,IAAI,cAAc,SAAS,YAAY,QAAQ,OAAO,MAAM;GAE5E,OAAO;EACR;CACD;AACD;;;AC5CA,SAAS,UAAU,OAA8B;CAChD,OAAO,MAAM,UAAU,OAAO,WAAW,MAAM,cAAc,MAAM,CAAC;AACrE;AAEA,SAAgB,gBAAgB,EAC/B,QACA,UACA,QACA,QACA,QACA,SAQE;CACF,OAAO,cAAc;EACpB;EACA;EACA,kBAAkB,QAAQ;EAC1B;EACA,UAAU;;EAEV;CACD,CAAC;AACF;AAEA,SAAS,sBAAsB,UAAyB;CACvD,MAAM,aAAoC,CAAC;CAC3C,MAAM,UAAyB,CAAC;CAEhC,KAAK,MAAM,CAAC,MAAM,aAAa,OAAO,QAAQ,SAAS,UAAU,GAAG;EACnE,MAAM,SAAS,eAAe,IAAI;EAElC,IAAI,QACH,WAAW,KAAK;GAAE,GAAG;GAAQ;GAAM;EAAS,CAAC;OAE7C,QAAQ,KAAK,IAAI;CAEnB;CAEA,IAAI,QAAQ,SAAS,GACpB,MAAM,IAAI,UACT,GAAG,OAAO,QAAQ,MAAM,EAAE,2DAA2D,WAAW,UAAU,OAAO,CAAC,GACnH;CAGD,OAAO;AACR;AAEA,eAAe,kBACd,cACA,EAAE,aACF,EAAE,aAAa,cACd;CACD,MAAM,QAAQ,OAAO,WAAW;CAChC,MAAM,0BAAU,IAAI,IAAoB;CACxC,MAAM,WAA0B,CAAC;CACjC,MAAM,WAA0B,CAAC;CACjC,MAAM,qBAAqB,IAAI,QAAQ,aAAa,cAAc,UAAU,QAAQ;CAEpF,MAAM,QAAQ,IACb,CAAC,GAAG,kBAAkB,CAAC,CAAC,KAAK,CAAC,UAAU,sBACvC,MAAM,YAAY;EACjB,MAAM,cAAc,MAAM,aAAa,IAAI,KAAK,QAAQ,WAAW,QAAQ,CAAC;EAE5E,IAAI,CAAC,aAAa;GACjB,SAAS,KAAK,QAAQ;GACtB;EACD;EAEA,KAAK,MAAM,aAAa,kBACvB,IAAI,CAAC,eAAe,WAAW,WAAW,GACzC,SAAS,KACR,GAAG,UAAU,KAAK,WAAW,OAAO,YAAY,KAAK,EAAE,GAAG,OAAO,YAAY,MAAM,EAAE,EACtF;EAIF,QAAQ,IAAI,UAAU,YAAY,MAAM;CACzC,CAAC,CACF,CACD;CAEA,IAAI,SAAS,SAAS,GACrB,MAAM,IAAI,UACT,GAAG,OAAO,SAAS,MAAM,EAAE,qDAAqD,WAAW,UAAU,QAAQ,CAAC,GAC/G;CAGD,IAAI,SAAS,SAAS,GACrB,MAAM,IAAI,UACT,GAAG,OAAO,SAAS,MAAM,EAAE,4DAA4D,WAAW,UAAU,QAAQ,CAAC,GACtH;CAGD,OAAO;AACR;AAEA,SAAS,kBAAkB,WAA2B,MAAkB;CACvE,IAAI,KAAK,UAAU,UAAU,UAAU,UAAU,UAAU,KAAK,YAAY,KAAK,QAAQ;CAEzF,MAAM,IAAI,MAAM,WAAW,OAAO,KAAK,KAAK,EAAE,GAAG,OAAO,KAAK,MAAM,GAAG;AACvE;AAEA,SAAS,wBACR,WACA,UACA,SACC;CACD,MAAM,SAAS,QAAQ,IAAI,UAAU,QAAQ;CAE7C,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,iCAAiC,UAAU,UAAU;CAElF,OAAO,gBAAgB;EACtB;EACA,UAAU,SAAS,SAAS,UAAU;EACtC,QAAQ;EACR,QAAQ,UAAU;EAClB,QAAQ,UAAU;EAClB,OAAO,UAAU;CAClB,CAAC;AACF;AAEA,eAAe,gBAAgB,WAA2B,UAAyB;CAClF,MAAM,EAAE,MAAM,SAAS,MAAM,YAAY,KAAK,QAAQ,SAAS,WAAW,UAAU,QAAQ,GAAG;EAC9F,UAAU,SAAS;EACnB,QAAQ,UAAU;EAClB,QAAQ,UAAU;EAClB,kBAAkB,SAAS;EAC3B,OAAO,UAAU;CAClB,CAAC;CAED,kBAAkB,WAAW,IAAI;CAEjC,OAAO;AACR;AAEA,eAAe,cAAc,WAAmB,aAAqB;CACpE,MAAM,EAAE,mBAAmB,mBAAmB,SAAS;CACvD,MAAM,WAAW,MAAM,oBAAoB,SAAS;CACpD,MAAM,aAAa,sBAAsB,QAAQ;CACjD,MAAM,eAAe,yBAAyB,SAAS;CAEvD,IAAI;EACH,MAAM,UAAU,MAAM,kBAAkB,cAAc,UAAU;GAAE;GAAa;EAAW,CAAC;EAO3F,OAAO;GAAE,WAAW;GAAgB,MANvB,WAAW,KAAK,eAAe;IAC3C,KAAK,wBAAwB,WAAW,UAAU,OAAO;IACzD,MAAM,UAAU;IAChB,cAAc,gBAAgB,WAAW,QAAQ;GAClD,EAEuC;EAAE;CAC1C,UAAU;EACT,aAAa,MAAM;CACpB;AACD;AAEA,eAAsB,aAAa,SAA6B;CAC/D,MAAM,YAAY,KAAK,QAAQ,QAAQ,SAAS;CAChD,MAAM,cAAc,sBAAsB;CAC1C,MAAM,MAAM,MAAM,cAAc,WAAW,WAAW;CAGtD,MAAM,YAAY,CAAC;CACnB,MAAM,MAAM,KAAK;CAEjB,MAAM,SAAS,MAAM,cAAc;EAAE,GAAG;EAAS,GAAG;EAAK;CAAY,CAAC;CAGtE,MAAMC,SAAG,GAAG,mBAAmB,SAAS,CAAC,CAAC,SAAS;EAAE,OAAO;EAAM,WAAW;CAAK,CAAC;CAEnF,OAAO;AACR;;;AC/KA,SAAS,aAAa,OAAsC;CAC3D,MAAM,yBAAS,IAAI,IAAgC;CAEnD,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,YAAY,OAAO,QAAQ,KAAK,GAAG;EAC7D,MAAM,WAAW,QAAQ,KAAK,UAAU,KAAK,IAAI;EACjD,MAAM,QAAQ,OAAO,IAAI,QAAQ,MAAM,QAAQ;GAAE,OAAO,CAAC;GAAG;EAAM,IAAI,EAAE,OAAO,CAAC,EAAE;EAElF,MAAM,MAAM,KAAK,CAAC,MAAM,KAAK,CAAC;EAC9B,OAAO,IAAI,UAAU,KAAK;CAC3B;CAEA,OAAO,CAAC,GAAG,OAAO,OAAO,CAAC;AAC3B;AAEA,eAAe,gBACd,cACA,WACA,OACoC;CACpC,MAAM,SAAS;EAAE,GAAG;EAAO,MAAM,KAAK,QAAQ,WAAW,MAAM,EAAE;CAAE;CACnE,MAAM,cAAc,MAAM,aAAa,IAAI,OAAO,IAAI;CAEtD,OAAO,cAAc;EAAE,QAAQ,YAAY;EAAQ;CAAO,IAAI,EAAE,OAAO;AACxE;AAEA,SAAS,mBAA4B,QAAuD;CAC3F,IAAI;CAEJ,OAAO,SAAS,WAAW,OAAiC;EAC3D,IAAI,SAAS,UAAU,OACtB,UAAU;GACT,SAAS,MAAM,SACZ,OAAO,MAAM,MAAM,IACnB,QAAQ,uBAAO,IAAI,MAAM,kBAAkB,MAAM,OAAO,IAAI,CAAC;GAChE;EACD;EAGD,OAAO,QAAQ;CAChB;AACD;AAEA,eAAe,eAAwB,EACtC,WACA,QACA,QACA,WACA,QACA,mBAKE;CACF,MAAM,eAAe,yBAAyB,SAAS;CACvD,MAAM,aAAa,mBAAmB,MAAM;CAC5C,MAAM,OAAyB,CAAC;CAEhC,IAAI;EACH,KAAK,MAAM,EAAE,OAAO,WAAW,QAAQ;GACtC,MAAM,QAAQ,QAAQ,MAAM,gBAAgB,cAAc,WAAW,KAAK,IAAI,KAAA;GAE9E,KAAK,MAAM,CAAC,MAAM,UAAU,OAC3B,KAAK,KAAK;IACT,KAAK,cAAc;KAClB;KACA;KACA,KAAK,UAAU,KAAK;KACpB,QAAQ,KAAK,UAAU,KAAK,IAAI;KAChC,OAAO,UAAU;IAClB,CAAC;IACD;IACA,QAAQ,YAAY,OAAO,OAAO,QAAQ,MAAM,WAAW,KAAK,IAAI,KAAA,CAAS;GAC9E,CAAC;EAEH;CACD,UAAU;EACT,aAAa,MAAM;CACpB;CAEA,OAAO;AACR;AAEA,eAAsB,qBAA8B,EACnD,WACA,QACA,QACA,iBACA,GAAG,WACoC;CACvC,MAAM,oBAAoB,KAAK,QAAQ,SAAS;CAChD,MAAM,EAAE,uBAAuB,mBAAmB,iBAAiB;CACnE,MAAM,WAAW,MAAM,oBAAoB,iBAAiB;CAC5D,MAAM,SAAS,aAAa,SAAS,cAAc;CACnD,MAAM,OAAO,MAAM,eAAe;EACjC,WAAW;EACX;EACA;EACA,WAAW,SAAS;EACpB;EACA;CACD,CAAC;CAOD,OAAO;EAAE,GAAG,MANS,cAAc;GAClC,GAAG;GACH,WAAW;GACX;EACD,CAAC;EAEmB,SAAS,OAAO,QAAQ,UAAU,MAAM,KAAK,CAAC,CAAC;CAAO;AAC3E"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { t as ImageEncoding } from "./image-encoding-CPYBEK0T.mjs";
|
|
2
|
+
import { a as defineImagePipeline, c as OpenGraphCardInput, d as ImageFormat, f as findStemCollisions, g as parseImageName, h as isImageSource, i as ImageSource, l as OpenGraphCardValue, m as imageFormats, o as OpenGraphCard, p as getImageStem, r as ImagePreset, s as OpenGraphCardImage, t as CoverPresetName, u as defineOpenGraphCards } from "./image-presets-CS7TnTsE.mjs";
|
|
3
|
+
export { type CoverPresetName, type ImageEncoding, type ImageFormat, type ImagePreset, type ImageSource, type OpenGraphCard, type OpenGraphCardImage, type OpenGraphCardInput, type OpenGraphCardValue, defineImagePipeline, defineOpenGraphCards, findStemCollisions, getImageStem, imageFormats, isImageSource, parseImageName };
|
package/dist/presets.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { c as imageFormats, d as parseImageName, i as recordOpenGraphCard, l as isImageSource, o as findStemCollisions, s as getImageStem, t as defineImagePipeline, u as isImageStem } from "./image-presets-ZN7_n2p8.mjs";
|
|
2
|
+
//#region src/open-graph-cards.ts
|
|
3
|
+
function canonicalizeValue(value) {
|
|
4
|
+
if (Array.isArray(value)) return value.map((item) => canonicalizeValue(item));
|
|
5
|
+
if (typeof value === "object") return canonicalizeObject(value);
|
|
6
|
+
if (typeof value === "number" && !Number.isFinite(value)) throw new TypeError(`OpenGraph card input can't hold ${String(value)}; JSON has no such number`);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function compareCodeUnits(first, second) {
|
|
10
|
+
if (first < second) return -1;
|
|
11
|
+
if (first > second) return 1;
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
function canonicalizeObject(value) {
|
|
15
|
+
const sorted = {};
|
|
16
|
+
for (const key of Object.keys(value).toSorted(compareCodeUnits)) {
|
|
17
|
+
const entry = value[key];
|
|
18
|
+
if (entry !== void 0) sorted[key] = canonicalizeValue(entry);
|
|
19
|
+
}
|
|
20
|
+
return sorted;
|
|
21
|
+
}
|
|
22
|
+
function canonicalizeOpenGraphCard({ image, input }) {
|
|
23
|
+
const canonicalInput = canonicalizeObject(input);
|
|
24
|
+
if (!image) return { input: canonicalInput };
|
|
25
|
+
return {
|
|
26
|
+
image: image.treatment === void 0 ? { id: image.id } : {
|
|
27
|
+
id: image.id,
|
|
28
|
+
treatment: image.treatment
|
|
29
|
+
},
|
|
30
|
+
input: canonicalInput
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function defineOpenGraphCards({ format }) {
|
|
34
|
+
function getCardName({ id, ...card }) {
|
|
35
|
+
if (!isImageStem(id)) throw new Error(`OpenGraph card ${id} can't be named: whitespace, quotes, &, <, >, ), commas and "." or ".." segments don't fit in a URL`);
|
|
36
|
+
const name = `${id}.${format}`;
|
|
37
|
+
recordOpenGraphCard(name, canonicalizeOpenGraphCard(card));
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
return { getCardName };
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { defineImagePipeline, defineOpenGraphCards, findStemCollisions, getImageStem, imageFormats, isImageSource, parseImageName };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=presets.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presets.mjs","names":[],"sources":["../src/open-graph-cards.ts"],"sourcesContent":["import type { ImageFormat } from './image-name.js';\n\nimport { isImageStem } from './image-name.js';\nimport { recordOpenGraphCard } from './image-recorder.js';\n\nexport type OpenGraphCardValue =\n\tArray<OpenGraphCardValue> | boolean | number | OpenGraphCardObject | string;\n\ninterface OpenGraphCardObject {\n\t[key: string]: OpenGraphCardValue | undefined;\n}\n\nexport type OpenGraphCardInput = OpenGraphCardObject;\n\nexport interface OpenGraphCardImage {\n\tid: string;\n\ttreatment?: string;\n}\n\nexport interface OpenGraphCard {\n\timage?: OpenGraphCardImage;\n\tinput: OpenGraphCardInput;\n}\n\nfunction canonicalizeValue(value: OpenGraphCardValue): OpenGraphCardValue {\n\tif (Array.isArray(value)) return value.map((item) => canonicalizeValue(item));\n\n\tif (typeof value === 'object') return canonicalizeObject(value);\n\n\tif (typeof value === 'number' && !Number.isFinite(value)) {\n\t\tthrow new TypeError(\n\t\t\t`OpenGraph card input can't hold ${String(value)}; JSON has no such number`,\n\t\t);\n\t}\n\n\treturn value;\n}\n\nfunction compareCodeUnits(first: string, second: string) {\n\tif (first < second) return -1;\n\n\tif (first > second) return 1;\n\n\treturn 0;\n}\n\nfunction canonicalizeObject(value: OpenGraphCardObject) {\n\tconst sorted: OpenGraphCardObject = {};\n\n\tfor (const key of Object.keys(value).toSorted(compareCodeUnits)) {\n\t\tconst entry = value[key];\n\n\t\tif (entry !== undefined) sorted[key] = canonicalizeValue(entry);\n\t}\n\n\treturn sorted;\n}\n\nexport function canonicalizeOpenGraphCard({ image, input }: OpenGraphCard): OpenGraphCard {\n\tconst canonicalInput = canonicalizeObject(input);\n\n\tif (!image) return { input: canonicalInput };\n\n\tconst canonicalImage: OpenGraphCardImage =\n\t\timage.treatment === undefined ? { id: image.id } : { id: image.id, treatment: image.treatment };\n\n\treturn { image: canonicalImage, input: canonicalInput };\n}\n\nexport function defineOpenGraphCards({ format }: { format: ImageFormat }) {\n\tfunction getCardName({ id, ...card }: OpenGraphCard & { id: string }) {\n\t\tif (!isImageStem(id)) {\n\t\t\tthrow new Error(\n\t\t\t\t`OpenGraph card ${id} can't be named: whitespace, quotes, &, <, >, ), commas and \".\" or \"..\" segments don't fit in a URL`,\n\t\t\t);\n\t\t}\n\n\t\tconst name = `${id}.${format}`;\n\n\t\trecordOpenGraphCard(name, canonicalizeOpenGraphCard(card));\n\n\t\treturn name;\n\t}\n\n\treturn { getCardName };\n}\n"],"mappings":";;AAwBA,SAAS,kBAAkB,OAA+C;CACzE,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,KAAK,SAAS,kBAAkB,IAAI,CAAC;CAE5E,IAAI,OAAO,UAAU,UAAU,OAAO,mBAAmB,KAAK;CAE9D,IAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,GACtD,MAAM,IAAI,UACT,mCAAmC,OAAO,KAAK,EAAE,0BAClD;CAGD,OAAO;AACR;AAEA,SAAS,iBAAiB,OAAe,QAAgB;CACxD,IAAI,QAAQ,QAAQ,OAAO;CAE3B,IAAI,QAAQ,QAAQ,OAAO;CAE3B,OAAO;AACR;AAEA,SAAS,mBAAmB,OAA4B;CACvD,MAAM,SAA8B,CAAC;CAErC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,gBAAgB,GAAG;EAChE,MAAM,QAAQ,MAAM;EAEpB,IAAI,UAAU,KAAA,GAAW,OAAO,OAAO,kBAAkB,KAAK;CAC/D;CAEA,OAAO;AACR;AAEA,SAAgB,0BAA0B,EAAE,OAAO,SAAuC;CACzF,MAAM,iBAAiB,mBAAmB,KAAK;CAE/C,IAAI,CAAC,OAAO,OAAO,EAAE,OAAO,eAAe;CAK3C,OAAO;EAAE,OAFR,MAAM,cAAc,KAAA,IAAY,EAAE,IAAI,MAAM,GAAG,IAAI;GAAE,IAAI,MAAM;GAAI,WAAW,MAAM;EAAU;EAE/D,OAAO;CAAe;AACvD;AAEA,SAAgB,qBAAqB,EAAE,UAAmC;CACzE,SAAS,YAAY,EAAE,IAAI,GAAG,QAAwC;EACrE,IAAI,CAAC,YAAY,EAAE,GAClB,MAAM,IAAI,MACT,kBAAkB,GAAG,oGACtB;EAGD,MAAM,OAAO,GAAG,GAAG,GAAG;EAEtB,oBAAoB,MAAM,0BAA0B,IAAI,CAAC;EAEzD,OAAO;CACR;CAEA,OAAO,EAAE,YAAY;AACtB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xsynaptic/astro-image-pipeline",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stable-name image renditions for Astro sites, rendered outside the bundler",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Alexander Synaptic <x@synapticism.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/xsynaptic/astro-lab.git",
|
|
11
|
+
"directory": "packages/astro-image-pipeline"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/xsynaptic/astro-lab/tree/main/packages/astro-image-pipeline#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/xsynaptic/astro-lab/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"astro",
|
|
19
|
+
"astro-integration",
|
|
20
|
+
"withastro",
|
|
21
|
+
"images",
|
|
22
|
+
"sharp",
|
|
23
|
+
"responsive-images"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=24.0.0"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.mjs",
|
|
29
|
+
"types": "./dist/index.d.mts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.mts",
|
|
33
|
+
"import": "./dist/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./presets": {
|
|
36
|
+
"types": "./dist/presets.d.mts",
|
|
37
|
+
"import": "./dist/presets.mjs"
|
|
38
|
+
},
|
|
39
|
+
"./node": {
|
|
40
|
+
"types": "./dist/node.d.mts",
|
|
41
|
+
"import": "./dist/node.mjs"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist"
|
|
46
|
+
],
|
|
47
|
+
"sideEffects": false,
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"p-limit": "^7.3.3"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"astro": "^7.4.2",
|
|
53
|
+
"sharp": "^0.35"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^24.19.2",
|
|
57
|
+
"astro": "^7.4.2",
|
|
58
|
+
"sharp": "^0.35.5"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsdown",
|
|
62
|
+
"dev": "tsdown --watch",
|
|
63
|
+
"check-types": "tsc --noEmit"
|
|
64
|
+
}
|
|
6
65
|
}
|