ipx 3.1.1 → 4.0.0-alpha.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/README.md +69 -42
- package/dist/THIRD-PARTY-LICENSES.md +539 -0
- package/dist/_chunks/libs/@fastify/accept-negotiator.mjs +71 -0
- package/dist/_chunks/libs/boolbase.mjs +12 -0
- package/dist/_chunks/libs/css-select.mjs +4139 -0
- package/dist/_chunks/libs/css-tree.mjs +31158 -0
- package/dist/_chunks/libs/csso.mjs +1834 -0
- package/dist/_chunks/libs/etag.mjs +25 -0
- package/dist/_chunks/libs/h3.mjs +267 -0
- package/dist/_chunks/libs/image-meta.d.mts +12 -0
- package/dist/_chunks/libs/image-meta.mjs +558 -0
- package/dist/_chunks/libs/sax.mjs +836 -0
- package/dist/_chunks/libs/svgo.mjs +5918 -0
- package/dist/_chunks/libs/ufo.mjs +41 -0
- package/dist/_chunks/node-fs.mjs +690 -0
- package/dist/_chunks/rolldown-runtime.mjs +42 -0
- package/dist/_chunks/svgo-node.mjs +3 -0
- package/dist/cli.d.mts +1 -2
- package/dist/cli.mjs +67 -59
- package/dist/index.d.mts +214 -321
- package/dist/index.mjs +30 -50
- package/package.json +44 -49
- package/bin/ipx.mjs +0 -2
- package/dist/cli.cjs +0 -63
- package/dist/cli.d.cts +0 -2
- package/dist/cli.d.ts +0 -2
- package/dist/index.cjs +0 -62
- package/dist/index.d.cts +0 -397
- package/dist/index.d.ts +0 -397
- package/dist/shared/ipx.CXJeaylD.mjs +0 -764
- package/dist/shared/ipx.GUc23orS.cjs +0 -778
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
import { __exportAll, __toESM } from "./rolldown-runtime.mjs";
|
|
2
|
+
import { decode, hasProtocol, joinURL, withLeadingSlash } from "./libs/ufo.mjs";
|
|
3
|
+
import { HTTPError, defineEventHandler } from "./libs/h3.mjs";
|
|
4
|
+
import { imageMeta } from "./libs/image-meta.mjs";
|
|
5
|
+
import { require_etag } from "./libs/etag.mjs";
|
|
6
|
+
import { require_accept_negotiator } from "./libs/@fastify/accept-negotiator.mjs";
|
|
7
|
+
import { join, parse, resolve } from "pathe";
|
|
8
|
+
function VArg(argument) {
|
|
9
|
+
if (argument === "Infinity") return Infinity;
|
|
10
|
+
if (argument === "undefined") return;
|
|
11
|
+
try {
|
|
12
|
+
const val = JSON.parse(argument);
|
|
13
|
+
const t = typeof val;
|
|
14
|
+
if (t === "boolean" || t === "number" || t === "string" || val === null) return val;
|
|
15
|
+
} catch {}
|
|
16
|
+
return argument;
|
|
17
|
+
}
|
|
18
|
+
function parseArgs(arguments_, mappers) {
|
|
19
|
+
const vargs = arguments_.split("_");
|
|
20
|
+
return mappers.map((v, index) => v(vargs[index]));
|
|
21
|
+
}
|
|
22
|
+
function getHandler(key) {
|
|
23
|
+
return handlers_exports[key];
|
|
24
|
+
}
|
|
25
|
+
function applyHandler(context, pipe, handler, argumentsString) {
|
|
26
|
+
const arguments_ = handler.args ? parseArgs(argumentsString, handler.args) : [];
|
|
27
|
+
return handler.apply(context, pipe, ...arguments_);
|
|
28
|
+
}
|
|
29
|
+
function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
|
|
30
|
+
const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
|
|
31
|
+
let { width, height } = desiredDimensions;
|
|
32
|
+
if (sourceDimensions.width && width > sourceDimensions.width) {
|
|
33
|
+
width = sourceDimensions.width;
|
|
34
|
+
height = Math.round(sourceDimensions.width / desiredAspectRatio);
|
|
35
|
+
}
|
|
36
|
+
if (sourceDimensions.height && height > sourceDimensions.height) {
|
|
37
|
+
height = sourceDimensions.height;
|
|
38
|
+
width = Math.round(sourceDimensions.height * desiredAspectRatio);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
width,
|
|
42
|
+
height
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
var handlers_exports = /* @__PURE__ */ __exportAll({
|
|
46
|
+
b: () => b,
|
|
47
|
+
background: () => background,
|
|
48
|
+
blur: () => blur,
|
|
49
|
+
crop: () => crop,
|
|
50
|
+
enlarge: () => enlarge,
|
|
51
|
+
extend: () => extend,
|
|
52
|
+
extract: () => extract,
|
|
53
|
+
fit: () => fit,
|
|
54
|
+
flatten: () => flatten,
|
|
55
|
+
flip: () => flip,
|
|
56
|
+
flop: () => flop,
|
|
57
|
+
gamma: () => gamma,
|
|
58
|
+
grayscale: () => grayscale,
|
|
59
|
+
h: () => h,
|
|
60
|
+
height: () => height,
|
|
61
|
+
kernel: () => kernel,
|
|
62
|
+
median: () => median,
|
|
63
|
+
modulate: () => modulate,
|
|
64
|
+
negate: () => negate,
|
|
65
|
+
normalize: () => normalize,
|
|
66
|
+
pos: () => pos,
|
|
67
|
+
position: () => position,
|
|
68
|
+
q: () => q,
|
|
69
|
+
quality: () => quality,
|
|
70
|
+
resize: () => resize,
|
|
71
|
+
rotate: () => rotate,
|
|
72
|
+
s: () => s,
|
|
73
|
+
sharpen: () => sharpen,
|
|
74
|
+
threshold: () => threshold,
|
|
75
|
+
tint: () => tint,
|
|
76
|
+
trim: () => trim,
|
|
77
|
+
w: () => w,
|
|
78
|
+
width: () => width
|
|
79
|
+
});
|
|
80
|
+
const quality = {
|
|
81
|
+
args: [VArg],
|
|
82
|
+
order: -1,
|
|
83
|
+
apply: (context, _pipe, quality) => {
|
|
84
|
+
context.quality = quality;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const fit = {
|
|
88
|
+
args: [VArg],
|
|
89
|
+
order: -1,
|
|
90
|
+
apply: (context, _pipe, fit) => {
|
|
91
|
+
context.fit = fit;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const position = {
|
|
95
|
+
args: [VArg],
|
|
96
|
+
order: -1,
|
|
97
|
+
apply: (context, _pipe, position) => {
|
|
98
|
+
context.position = position;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const HEX_RE = /^([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i;
|
|
102
|
+
const SHORTHEX_RE = /^([\da-f])([\da-f])([\da-f])$/i;
|
|
103
|
+
const background = {
|
|
104
|
+
args: [VArg],
|
|
105
|
+
order: -1,
|
|
106
|
+
apply: (context, _pipe, background) => {
|
|
107
|
+
background = String(background);
|
|
108
|
+
if (!background.startsWith("#") && (HEX_RE.test(background) || SHORTHEX_RE.test(background))) background = "#" + background;
|
|
109
|
+
context.background = background;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const enlarge = {
|
|
113
|
+
args: [],
|
|
114
|
+
apply: (context) => {
|
|
115
|
+
context.enlarge = true;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const kernel = {
|
|
119
|
+
args: [VArg],
|
|
120
|
+
apply: (context, _pipe, kernel) => {
|
|
121
|
+
context.kernel = kernel;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const width = {
|
|
125
|
+
args: [VArg],
|
|
126
|
+
apply: (context, pipe, width) => {
|
|
127
|
+
return pipe.resize(width, void 0, { withoutEnlargement: !context.enlarge });
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const height = {
|
|
131
|
+
args: [VArg],
|
|
132
|
+
apply: (context, pipe, height) => {
|
|
133
|
+
return pipe.resize(void 0, height, { withoutEnlargement: !context.enlarge });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const resize = {
|
|
137
|
+
args: [
|
|
138
|
+
VArg,
|
|
139
|
+
VArg,
|
|
140
|
+
VArg
|
|
141
|
+
],
|
|
142
|
+
apply: (context, pipe, size) => {
|
|
143
|
+
let [width, height] = String(size).split("x").map(Number);
|
|
144
|
+
if (!width) return;
|
|
145
|
+
if (!height) height = width;
|
|
146
|
+
if (!context.enlarge) {
|
|
147
|
+
const clamped = clampDimensionsPreservingAspectRatio(context.meta, {
|
|
148
|
+
width,
|
|
149
|
+
height
|
|
150
|
+
});
|
|
151
|
+
width = clamped.width;
|
|
152
|
+
height = clamped.height;
|
|
153
|
+
}
|
|
154
|
+
return pipe.resize(width, height, {
|
|
155
|
+
fit: context.fit,
|
|
156
|
+
position: context.position,
|
|
157
|
+
background: context.background,
|
|
158
|
+
kernel: context.kernel
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const trim = {
|
|
163
|
+
args: [VArg],
|
|
164
|
+
apply: (_context, pipe, threshold) => {
|
|
165
|
+
return pipe.trim(threshold);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const extend = {
|
|
169
|
+
args: [
|
|
170
|
+
VArg,
|
|
171
|
+
VArg,
|
|
172
|
+
VArg,
|
|
173
|
+
VArg
|
|
174
|
+
],
|
|
175
|
+
apply: (context, pipe, top, right, bottom, left) => {
|
|
176
|
+
return pipe.extend({
|
|
177
|
+
top,
|
|
178
|
+
left,
|
|
179
|
+
bottom,
|
|
180
|
+
right,
|
|
181
|
+
background: context.background
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const extract = {
|
|
186
|
+
args: [
|
|
187
|
+
VArg,
|
|
188
|
+
VArg,
|
|
189
|
+
VArg,
|
|
190
|
+
VArg
|
|
191
|
+
],
|
|
192
|
+
apply: (_context, pipe, left, top, width, height) => {
|
|
193
|
+
return pipe.extract({
|
|
194
|
+
left,
|
|
195
|
+
top,
|
|
196
|
+
width,
|
|
197
|
+
height
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
const rotate = {
|
|
202
|
+
args: [VArg],
|
|
203
|
+
apply: (context, pipe, angel) => {
|
|
204
|
+
return pipe.rotate(angel, { background: context.background });
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const flip = {
|
|
208
|
+
args: [],
|
|
209
|
+
apply: (_context, pipe) => {
|
|
210
|
+
return pipe.flip();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const flop = {
|
|
214
|
+
args: [],
|
|
215
|
+
apply: (_context, pipe) => {
|
|
216
|
+
return pipe.flop();
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const sharpen = {
|
|
220
|
+
args: [
|
|
221
|
+
VArg,
|
|
222
|
+
VArg,
|
|
223
|
+
VArg
|
|
224
|
+
],
|
|
225
|
+
apply: (_context, pipe, sigma, flat, jagged) => {
|
|
226
|
+
return pipe.sharpen({
|
|
227
|
+
sigma,
|
|
228
|
+
m1: flat,
|
|
229
|
+
m2: jagged
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const median = {
|
|
234
|
+
args: [
|
|
235
|
+
VArg,
|
|
236
|
+
VArg,
|
|
237
|
+
VArg
|
|
238
|
+
],
|
|
239
|
+
apply: (_context, pipe, size) => {
|
|
240
|
+
return pipe.median(size);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const blur = {
|
|
244
|
+
args: [
|
|
245
|
+
VArg,
|
|
246
|
+
VArg,
|
|
247
|
+
VArg
|
|
248
|
+
],
|
|
249
|
+
apply: (_context, pipe, sigma) => {
|
|
250
|
+
return pipe.blur(sigma);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const flatten = {
|
|
254
|
+
args: [
|
|
255
|
+
VArg,
|
|
256
|
+
VArg,
|
|
257
|
+
VArg
|
|
258
|
+
],
|
|
259
|
+
apply: (context, pipe) => {
|
|
260
|
+
return pipe.flatten({ background: context.background });
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const gamma = {
|
|
264
|
+
args: [
|
|
265
|
+
VArg,
|
|
266
|
+
VArg,
|
|
267
|
+
VArg
|
|
268
|
+
],
|
|
269
|
+
apply: (_context, pipe, gamma, gammaOut) => {
|
|
270
|
+
return pipe.gamma(gamma, gammaOut);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const negate = {
|
|
274
|
+
args: [
|
|
275
|
+
VArg,
|
|
276
|
+
VArg,
|
|
277
|
+
VArg
|
|
278
|
+
],
|
|
279
|
+
apply: (_context, pipe) => {
|
|
280
|
+
return pipe.negate();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const normalize = {
|
|
284
|
+
args: [
|
|
285
|
+
VArg,
|
|
286
|
+
VArg,
|
|
287
|
+
VArg
|
|
288
|
+
],
|
|
289
|
+
apply: (_context, pipe) => {
|
|
290
|
+
return pipe.normalize();
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const threshold = {
|
|
294
|
+
args: [VArg],
|
|
295
|
+
apply: (_context, pipe, threshold) => {
|
|
296
|
+
return pipe.threshold(threshold);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const modulate = {
|
|
300
|
+
args: [VArg],
|
|
301
|
+
apply: (_context, pipe, brightness, saturation, hue) => {
|
|
302
|
+
return pipe.modulate({
|
|
303
|
+
brightness,
|
|
304
|
+
saturation,
|
|
305
|
+
hue
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const tint = {
|
|
310
|
+
args: [VArg],
|
|
311
|
+
apply: (_context, pipe, rgb) => {
|
|
312
|
+
return pipe.tint(rgb);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const grayscale = {
|
|
316
|
+
args: [VArg],
|
|
317
|
+
apply: (_context, pipe) => {
|
|
318
|
+
return pipe.grayscale();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const crop = extract;
|
|
322
|
+
const q = quality;
|
|
323
|
+
const b = background;
|
|
324
|
+
const w = width;
|
|
325
|
+
const h = height;
|
|
326
|
+
const s = resize;
|
|
327
|
+
const pos = position;
|
|
328
|
+
function getEnv(name) {
|
|
329
|
+
const value = globalThis.process?.env?.[name];
|
|
330
|
+
if (value !== void 0) return JSON.parse(value);
|
|
331
|
+
}
|
|
332
|
+
function requireModule(id) {
|
|
333
|
+
const { createRequire } = globalThis.process.getBuiltinModule("node:module");
|
|
334
|
+
return createRequire(import.meta.url)(id);
|
|
335
|
+
}
|
|
336
|
+
function cachedPromise(function_) {
|
|
337
|
+
let p;
|
|
338
|
+
return (...arguments_) => {
|
|
339
|
+
if (p) return p;
|
|
340
|
+
p = Promise.resolve(function_(...arguments_));
|
|
341
|
+
return p;
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
|
|
345
|
+
"jpeg",
|
|
346
|
+
"png",
|
|
347
|
+
"webp",
|
|
348
|
+
"avif",
|
|
349
|
+
"tiff",
|
|
350
|
+
"heif",
|
|
351
|
+
"gif",
|
|
352
|
+
"heic"
|
|
353
|
+
]);
|
|
354
|
+
function createIPX(userOptions) {
|
|
355
|
+
const options = {
|
|
356
|
+
...userOptions,
|
|
357
|
+
alias: userOptions.alias || getEnv("IPX_ALIAS") || {},
|
|
358
|
+
maxAge: userOptions.maxAge ?? getEnv("IPX_MAX_AGE") ?? 60,
|
|
359
|
+
sharpOptions: {
|
|
360
|
+
jpegProgressive: true,
|
|
361
|
+
...userOptions.sharpOptions
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
options.alias = Object.fromEntries(Object.entries(options.alias || {}).map((e) => [withLeadingSlash(e[0]), e[1]]));
|
|
365
|
+
const getSharp = cachedPromise(async () => {
|
|
366
|
+
return await import("sharp").then((r) => r.default || r);
|
|
367
|
+
});
|
|
368
|
+
const getSVGO = cachedPromise(async () => {
|
|
369
|
+
const { optimize } = await import("./svgo-node.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
|
|
370
|
+
return { optimize };
|
|
371
|
+
});
|
|
372
|
+
return function ipx(id, modifiers = {}, opts = {}) {
|
|
373
|
+
if (!id) throw new HTTPError({
|
|
374
|
+
statusCode: 400,
|
|
375
|
+
statusText: `IPX_MISSING_ID`,
|
|
376
|
+
message: `Resource id is missing`
|
|
377
|
+
});
|
|
378
|
+
id = hasProtocol(id) ? id : withLeadingSlash(id);
|
|
379
|
+
for (const base in options.alias) if (id.startsWith(base)) id = joinURL(options.alias[base], id.slice(base.length));
|
|
380
|
+
const storage = hasProtocol(id) ? options.httpStorage || options.storage : options.storage || options.httpStorage;
|
|
381
|
+
if (!storage) throw new HTTPError({
|
|
382
|
+
statusCode: 500,
|
|
383
|
+
statusText: `IPX_NO_STORAGE`,
|
|
384
|
+
message: "No storage configured!"
|
|
385
|
+
});
|
|
386
|
+
const getSourceMeta = cachedPromise(async () => {
|
|
387
|
+
const sourceMeta = await storage.getMeta(id, opts);
|
|
388
|
+
if (!sourceMeta) throw new HTTPError({
|
|
389
|
+
statusCode: 404,
|
|
390
|
+
statusText: `IPX_RESOURCE_NOT_FOUND`,
|
|
391
|
+
message: `Resource not found: ${id}`
|
|
392
|
+
});
|
|
393
|
+
const _maxAge = sourceMeta.maxAge ?? options.maxAge;
|
|
394
|
+
return {
|
|
395
|
+
maxAge: typeof _maxAge === "string" ? Number.parseInt(_maxAge) : _maxAge,
|
|
396
|
+
mtime: sourceMeta.mtime ? new Date(sourceMeta.mtime) : void 0
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
const getSourceData = cachedPromise(async () => {
|
|
400
|
+
const sourceData = await storage.getData(id, opts);
|
|
401
|
+
if (!sourceData) throw new HTTPError({
|
|
402
|
+
statusCode: 404,
|
|
403
|
+
statusText: `IPX_RESOURCE_NOT_FOUND`,
|
|
404
|
+
message: `Resource not found: ${id}`
|
|
405
|
+
});
|
|
406
|
+
return Buffer.from(sourceData);
|
|
407
|
+
});
|
|
408
|
+
return {
|
|
409
|
+
getSourceMeta,
|
|
410
|
+
process: cachedPromise(async () => {
|
|
411
|
+
const sourceData = await getSourceData();
|
|
412
|
+
let imageMeta$1;
|
|
413
|
+
try {
|
|
414
|
+
imageMeta$1 = imageMeta(sourceData);
|
|
415
|
+
} catch {
|
|
416
|
+
throw new HTTPError({
|
|
417
|
+
statusCode: 400,
|
|
418
|
+
statusText: `IPX_INVALID_IMAGE`,
|
|
419
|
+
message: `Cannot parse image metadata: ${id}`
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
let mFormat = modifiers.f || modifiers.format;
|
|
423
|
+
if (mFormat === "jpg") mFormat = "jpeg";
|
|
424
|
+
const format = mFormat && SUPPORTED_FORMATS.has(mFormat) ? mFormat : SUPPORTED_FORMATS.has(imageMeta$1.type || "") ? imageMeta$1.type : "jpeg";
|
|
425
|
+
if (imageMeta$1.type === "svg" && !mFormat) if (options.svgo === false) return {
|
|
426
|
+
data: sourceData,
|
|
427
|
+
format: "svg+xml",
|
|
428
|
+
meta: imageMeta$1
|
|
429
|
+
};
|
|
430
|
+
else {
|
|
431
|
+
const { optimize } = await getSVGO();
|
|
432
|
+
return {
|
|
433
|
+
data: optimize(sourceData.toString("utf8"), {
|
|
434
|
+
...options.svgo,
|
|
435
|
+
plugins: ["removeScripts", ...options.svgo?.plugins || []]
|
|
436
|
+
}).data,
|
|
437
|
+
format: "svg+xml",
|
|
438
|
+
meta: imageMeta$1
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
|
|
442
|
+
let sharp = (await getSharp())(sourceData, {
|
|
443
|
+
animated,
|
|
444
|
+
...options.sharpOptions
|
|
445
|
+
});
|
|
446
|
+
Object.assign(sharp.options, options.sharpOptions);
|
|
447
|
+
const handlers = Object.entries(modifiers).map(([name, arguments_]) => ({
|
|
448
|
+
handler: getHandler(name),
|
|
449
|
+
name,
|
|
450
|
+
args: arguments_
|
|
451
|
+
})).filter((h) => h.handler).sort((a, b) => {
|
|
452
|
+
const aKey = (a.handler.order || a.name || "").toString();
|
|
453
|
+
const bKey = (b.handler.order || b.name || "").toString();
|
|
454
|
+
return aKey.localeCompare(bKey);
|
|
455
|
+
});
|
|
456
|
+
const handlerContext = { meta: imageMeta$1 };
|
|
457
|
+
for (const h of handlers) sharp = applyHandler(handlerContext, sharp, h.handler, h.args.toString()) || sharp;
|
|
458
|
+
if (SUPPORTED_FORMATS.has(format || "")) sharp = sharp.toFormat(format, { quality: handlerContext.quality });
|
|
459
|
+
return {
|
|
460
|
+
data: await sharp.toBuffer(),
|
|
461
|
+
format,
|
|
462
|
+
meta: imageMeta$1
|
|
463
|
+
};
|
|
464
|
+
})
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
var import_etag = /* @__PURE__ */ __toESM(require_etag(), 1);
|
|
469
|
+
var import_accept_negotiator = require_accept_negotiator();
|
|
470
|
+
function createIPXFetchHandler(ipx) {
|
|
471
|
+
return createIPXHandler(ipx).fetch;
|
|
472
|
+
}
|
|
473
|
+
function createIPXNodeHandler(ipx) {
|
|
474
|
+
const { toNodeHandler } = requireModule("srvx/node");
|
|
475
|
+
return toNodeHandler(createIPXFetchHandler(ipx));
|
|
476
|
+
}
|
|
477
|
+
function serveIPX(ipx, opts) {
|
|
478
|
+
const { serve } = requireModule("srvx");
|
|
479
|
+
const fetch = createIPXFetchHandler(ipx);
|
|
480
|
+
return serve({
|
|
481
|
+
...opts,
|
|
482
|
+
fetch
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
const MODIFIER_SEP = /[&,]/g;
|
|
486
|
+
const MODIFIER_VAL_SEP = /[:=_]/;
|
|
487
|
+
function createIPXHandler(ipx) {
|
|
488
|
+
return defineEventHandler(async (event) => {
|
|
489
|
+
const [modifiersString = "", ...idSegments] = event.url.pathname.slice(1).split("/");
|
|
490
|
+
const id = safeString(decode(idSegments.join("/")));
|
|
491
|
+
if (!modifiersString) throw new HTTPError({
|
|
492
|
+
statusCode: 400,
|
|
493
|
+
statusText: "IPX_MISSING_MODIFIERS",
|
|
494
|
+
message: `Modifiers are missing: ${id}`
|
|
495
|
+
});
|
|
496
|
+
if (!id || id === "/") throw new HTTPError({
|
|
497
|
+
statusCode: 400,
|
|
498
|
+
statusText: "IPX_MISSING_ID",
|
|
499
|
+
message: `Resource id is missing: ${event.path}`
|
|
500
|
+
});
|
|
501
|
+
const modifiers = Object.create(null);
|
|
502
|
+
if (modifiersString !== "_") for (const p of modifiersString.split(MODIFIER_SEP)) {
|
|
503
|
+
const [key, ...values] = p.split(MODIFIER_VAL_SEP);
|
|
504
|
+
modifiers[safeString(key)] = values.map((v) => safeString(decode(v))).join("_");
|
|
505
|
+
}
|
|
506
|
+
if ((modifiers.f || modifiers.format) === "auto") {
|
|
507
|
+
const acceptHeader = event.req.headers.get("accept") || "";
|
|
508
|
+
const animated = modifiers.animated ?? modifiers.a;
|
|
509
|
+
const autoFormat = autoDetectFormat(acceptHeader, !!animated || animated === "");
|
|
510
|
+
delete modifiers.f;
|
|
511
|
+
delete modifiers.format;
|
|
512
|
+
if (autoFormat) {
|
|
513
|
+
modifiers.format = autoFormat;
|
|
514
|
+
event.res.headers.append("vary", "Accept");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const img = ipx(id, modifiers);
|
|
518
|
+
const sourceMeta = await img.getSourceMeta();
|
|
519
|
+
sendResponseHeaderIfNotSet(event, "content-security-policy", "default-src 'none'");
|
|
520
|
+
if (sourceMeta.mtime) {
|
|
521
|
+
sendResponseHeaderIfNotSet(event, "last-modified", sourceMeta.mtime.toUTCString());
|
|
522
|
+
const _ifModifiedSince = event.req.headers.get("if-modified-since");
|
|
523
|
+
if (_ifModifiedSince && new Date(_ifModifiedSince) >= sourceMeta.mtime) {
|
|
524
|
+
event.res.status = 304;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const { data, format } = await img.process();
|
|
529
|
+
if (typeof sourceMeta.maxAge === "number") sendResponseHeaderIfNotSet(event, "cache-control", `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`);
|
|
530
|
+
const etag = (0, import_etag.default)(data);
|
|
531
|
+
sendResponseHeaderIfNotSet(event, "etag", etag);
|
|
532
|
+
if (etag && event.req.headers.get("if-none-match") === etag) {
|
|
533
|
+
event.res.status = 304;
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (format) sendResponseHeaderIfNotSet(event, "content-type", `image/${format}`);
|
|
537
|
+
return data;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
function sendResponseHeaderIfNotSet(event, name, value) {
|
|
541
|
+
if (!event.res.headers.has(name)) event.res.headers.set(name, value);
|
|
542
|
+
}
|
|
543
|
+
function autoDetectFormat(acceptHeader, animated) {
|
|
544
|
+
if (animated) return (0, import_accept_negotiator.negotiate)(acceptHeader, ["image/webp", "image/gif"])?.split("/")[1] || "gif";
|
|
545
|
+
return (0, import_accept_negotiator.negotiate)(acceptHeader, [
|
|
546
|
+
"image/avif",
|
|
547
|
+
"image/webp",
|
|
548
|
+
"image/jpeg",
|
|
549
|
+
"image/png",
|
|
550
|
+
"image/tiff",
|
|
551
|
+
"image/heif",
|
|
552
|
+
"image/gif"
|
|
553
|
+
])?.split("/")[1] || "jpeg";
|
|
554
|
+
}
|
|
555
|
+
function safeString(input) {
|
|
556
|
+
return JSON.stringify(input).replace(/^"|"$/g, "").replace(/\\+/g, "\\").replace(/\\"/g, "\"");
|
|
557
|
+
}
|
|
558
|
+
const HTTP_RE = /^https?:\/\//;
|
|
559
|
+
function ipxHttpStorage(_options = {}) {
|
|
560
|
+
const allowAllDomains = _options.allowAllDomains ?? getEnv("IPX_HTTP_ALLOW_ALL_DOMAINS") ?? false;
|
|
561
|
+
let _domains = _options.domains || getEnv("IPX_HTTP_DOMAINS") || [];
|
|
562
|
+
const defaultMaxAge = _options.maxAge || getEnv("IPX_HTTP_MAX_AGE") || 300;
|
|
563
|
+
const fetchOptions = _options.fetchOptions || getEnv("IPX_HTTP_FETCH_OPTIONS") || {};
|
|
564
|
+
if (typeof _domains === "string") _domains = _domains.split(",").map((s) => s.trim());
|
|
565
|
+
const domains = new Set(_domains.map((d) => {
|
|
566
|
+
if (!HTTP_RE.test(d)) d = "http://" + d;
|
|
567
|
+
return new URL(d).hostname;
|
|
568
|
+
}).filter(Boolean));
|
|
569
|
+
function validateId(id) {
|
|
570
|
+
const url = new URL(decodeURIComponent(id));
|
|
571
|
+
if (!url.hostname) throw new HTTPError({
|
|
572
|
+
statusCode: 403,
|
|
573
|
+
statusText: `IPX_MISSING_HOSTNAME`,
|
|
574
|
+
message: `Hostname is missing: ${id}`
|
|
575
|
+
});
|
|
576
|
+
if (!allowAllDomains && !domains.has(url.hostname)) throw new HTTPError({
|
|
577
|
+
statusCode: 403,
|
|
578
|
+
statusText: `IPX_FORBIDDEN_HOST`,
|
|
579
|
+
message: `Forbidden host: ${url.hostname}`
|
|
580
|
+
});
|
|
581
|
+
return url.toString();
|
|
582
|
+
}
|
|
583
|
+
function parseResponse(response) {
|
|
584
|
+
let maxAge = defaultMaxAge;
|
|
585
|
+
if (_options.ignoreCacheControl !== true) {
|
|
586
|
+
const _cacheControl = response.headers.get("cache-control");
|
|
587
|
+
if (_cacheControl) {
|
|
588
|
+
const m = _cacheControl.match(/max-age=(\d+)/);
|
|
589
|
+
if (m && m[1]) maxAge = Number.parseInt(m[1]);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
let mtime;
|
|
593
|
+
const _lastModified = response.headers.get("last-modified");
|
|
594
|
+
if (_lastModified) mtime = new Date(_lastModified);
|
|
595
|
+
return {
|
|
596
|
+
maxAge,
|
|
597
|
+
mtime
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
name: "ipx:http",
|
|
602
|
+
async getMeta(id) {
|
|
603
|
+
const url = validateId(id);
|
|
604
|
+
try {
|
|
605
|
+
const response = await fetch(url, {
|
|
606
|
+
...fetchOptions,
|
|
607
|
+
method: "HEAD"
|
|
608
|
+
});
|
|
609
|
+
if (!response.ok) return {};
|
|
610
|
+
const { maxAge, mtime } = parseResponse(response);
|
|
611
|
+
return {
|
|
612
|
+
mtime,
|
|
613
|
+
maxAge
|
|
614
|
+
};
|
|
615
|
+
} catch {
|
|
616
|
+
return {};
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
async getData(id) {
|
|
620
|
+
const url = validateId(id);
|
|
621
|
+
const response = await fetch(url, fetchOptions);
|
|
622
|
+
if (!response.ok) throw new HTTPError({
|
|
623
|
+
statusCode: response.status,
|
|
624
|
+
statusText: response.statusText,
|
|
625
|
+
message: `Failed to fetch ${id}: ${response.status} ${response.statusText}`
|
|
626
|
+
});
|
|
627
|
+
return await response.arrayBuffer();
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function ipxFSStorage(_options = {}) {
|
|
632
|
+
const dirs = resolveDirs(_options.dir);
|
|
633
|
+
const maxAge = _options.maxAge || getEnv("IPX_FS_MAX_AGE");
|
|
634
|
+
const fs = globalThis.process.getBuiltinModule("node:fs/promises");
|
|
635
|
+
const resolveFile = async (id) => {
|
|
636
|
+
for (const dir of dirs) {
|
|
637
|
+
const filePath = join(dir, id);
|
|
638
|
+
if (!isValidPath(filePath) || !filePath.startsWith(dir + "/")) throw new HTTPError({
|
|
639
|
+
statusCode: 403,
|
|
640
|
+
statusText: `IPX_FORBIDDEN_PATH`,
|
|
641
|
+
message: `Forbidden path: ${id}`
|
|
642
|
+
});
|
|
643
|
+
try {
|
|
644
|
+
const stats = await fs.stat(filePath);
|
|
645
|
+
if (!stats.isFile()) continue;
|
|
646
|
+
return {
|
|
647
|
+
stats,
|
|
648
|
+
read: () => fs.readFile(filePath)
|
|
649
|
+
};
|
|
650
|
+
} catch (error) {
|
|
651
|
+
if (error.code === "ENOENT") continue;
|
|
652
|
+
throw new HTTPError({
|
|
653
|
+
statusCode: 403,
|
|
654
|
+
statusText: `IPX_FORBIDDEN_FILE`,
|
|
655
|
+
message: `Cannot access file: ${id}`
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
throw new HTTPError({
|
|
660
|
+
statusCode: 404,
|
|
661
|
+
statusText: `IPX_FILE_NOT_FOUND`,
|
|
662
|
+
message: `File not found: ${id}`
|
|
663
|
+
});
|
|
664
|
+
};
|
|
665
|
+
return {
|
|
666
|
+
name: "ipx:node-fs",
|
|
667
|
+
async getMeta(id) {
|
|
668
|
+
const { stats } = await resolveFile(id);
|
|
669
|
+
return {
|
|
670
|
+
mtime: stats.mtime,
|
|
671
|
+
maxAge
|
|
672
|
+
};
|
|
673
|
+
},
|
|
674
|
+
async getData(id) {
|
|
675
|
+
const { read } = await resolveFile(id);
|
|
676
|
+
return read();
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
const isWindows = process.platform === "win32";
|
|
681
|
+
function isValidPath(fp) {
|
|
682
|
+
if (isWindows) fp = fp.slice(parse(fp).root.length);
|
|
683
|
+
if (/["*:<>?|]/.test(fp)) return false;
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
function resolveDirs(dirs) {
|
|
687
|
+
if (!dirs || !Array.isArray(dirs)) return [resolve(dirs || getEnv("IPX_FS_DIR") || ".")];
|
|
688
|
+
return dirs.map((dirs) => resolve(dirs));
|
|
689
|
+
}
|
|
690
|
+
export { createIPX, createIPXFetchHandler, createIPXNodeHandler, ipxFSStorage, ipxHttpStorage, serveIPX };
|