image-renditions 0.2.0 → 0.3.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/README.md +12 -6
- package/dist/h3.d.mts +1 -1
- package/dist/h3.mjs +1 -1
- package/dist/{handler-CPJllrYi.mjs → handler-D5toFJ55.mjs} +1 -1
- package/dist/{handler-9XP5SZhg.d.mts → handler-RAQFWtrg.d.mts} +1 -1
- package/dist/hash-CHipV1N6.mjs +16 -0
- package/dist/index.d.mts +2 -4
- package/dist/index.mjs +8 -3
- package/dist/server.d.mts +1 -1
- package/dist/server.mjs +2 -2
- package/dist/{url-BAU5GYXC.mjs → url-C6PtYIEf.mjs} +11 -15
- package/dist/{url-BVH3ZB0F.d.mts → url-DoJdUx9J.d.mts} +1 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ Resize and re-encode uploaded images when a page first shows them, store each re
|
|
|
8
8
|
|
|
9
9
|
```html
|
|
10
10
|
<img
|
|
11
|
-
src="/_renditions/v1/avatar.
|
|
11
|
+
src="/_renditions/v1/avatar.faf7e221f65d.avif/avatars/42.jpg"
|
|
12
12
|
srcset="
|
|
13
|
-
/_renditions/v1/avatar.
|
|
14
|
-
/_renditions/v1/avatar@2x.
|
|
13
|
+
/_renditions/v1/avatar.faf7e221f65d.avif/avatars/42.jpg 1x,
|
|
14
|
+
/_renditions/v1/avatar@2x.b62b90cf6c3e.avif/avatars/42.jpg 2x
|
|
15
15
|
"
|
|
16
16
|
alt=""
|
|
17
17
|
/>
|
|
@@ -23,7 +23,13 @@ Resize and re-encode uploaded images when a page first shows them, store each re
|
|
|
23
23
|
|
|
24
24
|
With an S3 bucket, or a directory that outlives deploys and is shared by every replica, app servers keep no image files: renditions survive redeploys, and any replica serves any of them.
|
|
25
25
|
|
|
26
|
-
The
|
|
26
|
+
The library is modular, with pluggable parts:
|
|
27
|
+
|
|
28
|
+
- [HTTP](#h3-and-other-frameworks): an event handler for h3 1.15 or newer and h3 2, and a framework-neutral handler for other servers.
|
|
29
|
+
- [Processing](#processing): a processor built on sharp.
|
|
30
|
+
- [Storage](#storage): an S3 bucket or a file system directory, chosen separately for uploads and renditions.
|
|
31
|
+
- [Nuxt](#quick-start): a Nuxt 4 module that registers the handler and the `<RenditionImg>` component.
|
|
32
|
+
- [Extending](#extending): other storage, image libraries, and state shared between replicas plug in through the interfaces of `image-renditions/server`.
|
|
27
33
|
|
|
28
34
|
## When not to use it
|
|
29
35
|
|
|
@@ -232,7 +238,7 @@ export default defineRenditionEventHandler({
|
|
|
232
238
|
- Map different scopes to different keys even for equal paths.
|
|
233
239
|
- A rendition is stored in the result store at `<sourceKey>/<hash>.<extension>`.
|
|
234
240
|
To keep renditions in the bucket of uploads, also return a `resultPrefix` that no source key starts with, such as `` `renditions/${sourceKey}` ``, to use instead of `sourceKey`.
|
|
235
|
-
- S3 limits a key to 1024 bytes in UTF-8, and a rendition key
|
|
241
|
+
- S3 limits a key to 1024 bytes in UTF-8, and the library writes a rendition under a key up to 18 bytes longer than its result prefix.
|
|
236
242
|
If source keys can come that close to the limit, return a shorter `resultPrefix`, or their renditions fail as `invalid-request`.
|
|
237
243
|
|
|
238
244
|
Derive the context from the URL only, host included, for example from the tenant's domain: browsers and CDNs cache the redirect to a rendition by its URL for a day.
|
|
@@ -443,7 +449,7 @@ Build URLs in the app with the catalog, which is safe to import in browsers:
|
|
|
443
449
|
|
|
444
450
|
```ts
|
|
445
451
|
catalog.url("avatar", { path: "avatars/42.jpg" })
|
|
446
|
-
// /_renditions/v1/avatar.
|
|
452
|
+
// /_renditions/v1/avatar.faf7e221f65d.avif/avatars/42.jpg
|
|
447
453
|
|
|
448
454
|
catalog.srcset("avatar", { scope: "tenant", path: "avatars/42.jpg" })
|
|
449
455
|
```
|
package/dist/h3.d.mts
CHANGED
package/dist/h3.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as parseRenditionPath } from "./url-BAU5GYXC.mjs";
|
|
2
1
|
import { r as toDeclaredTarget } from "./scopes-C1kzqQHz.mjs";
|
|
2
|
+
import { i as parseRenditionPath } from "./url-C6PtYIEf.mjs";
|
|
3
3
|
import { t as RenditionError } from "./error-DViVpVmH.mjs";
|
|
4
4
|
//#region src/server/handler.ts
|
|
5
5
|
const failureStatus = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as Recipe, s as RenditionFormat } from "./recipe-BT8D60Qb.mjs";
|
|
2
2
|
import { a as RenditionTarget, t as Catalog } from "./catalog-D11xkViN.mjs";
|
|
3
|
-
import { n as RenditionAddress } from "./url-
|
|
3
|
+
import { n as RenditionAddress } from "./url-DoJdUx9J.mjs";
|
|
4
4
|
import { i as StoreCallOptions, n as ResultStore, r as SourceStore } from "./stores-P-TUmax4.mjs";
|
|
5
5
|
import { t as Processor } from "./processor-DvyPs-OZ.mjs";
|
|
6
6
|
//#region src/server/error.d.ts
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { o as serializeRecipe } from "./recipe-BavgOq0b.mjs";
|
|
2
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
|
+
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
4
|
+
//#region src/hash.ts
|
|
5
|
+
/**
|
|
6
|
+
* Length of a recipe hash in hex digits, which keeps 48 bits of SHA-256.
|
|
7
|
+
*
|
|
8
|
+
* It tells apart the few thousand recipes an application may have over its lifetime, and a catalog rejects a collision among its own recipes.
|
|
9
|
+
*/
|
|
10
|
+
const RECIPE_HASH_LENGTH = 12;
|
|
11
|
+
/** Identifies a recipe by the truncated SHA-256 of its canonical serialization. */
|
|
12
|
+
function hashRecipe(recipe) {
|
|
13
|
+
return bytesToHex(sha256(utf8ToBytes(serializeRecipe(recipe)))).slice(0, RECIPE_HASH_LENGTH);
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { hashRecipe as t };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { a as RenditionAnimation, c as RenditionOptions, d as formatContentTypes, f as formatExtensions, i as RecipeInput, l as RenditionPosition, m as serializeRecipe, n as RECIPE_ENGINE_VERSION, o as RenditionFit, p as normalizeRecipe, r as Recipe, s as RenditionFormat, t as MAX_DIMENSION, u as SourcePolicy } from "./recipe-BT8D60Qb.mjs";
|
|
2
2
|
import { a as RenditionTarget, c as defineCatalog, i as PresetName, n as CatalogDefinition, o as RenditionVariant, r as PresetDefinition, s as ScopeName, t as Catalog } from "./catalog-D11xkViN.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { i as parseRenditionPath, n as RenditionAddress, r as formatRenditionPath, t as DEFAULT_BASE_PATH } from "./url-DoJdUx9J.mjs";
|
|
4
4
|
//#region src/hash.d.ts
|
|
5
|
-
/** Length of a recipe hash in hex digits, which keeps 128 bits of SHA-256. */
|
|
6
|
-
export declare const RECIPE_HASH_LENGTH = 32;
|
|
7
5
|
/** Identifies a recipe by the truncated SHA-256 of its canonical serialization. */
|
|
8
6
|
export declare function hashRecipe(recipe: Recipe): string;
|
|
9
7
|
//#endregion
|
|
@@ -20,4 +18,4 @@ export declare function encodePath(path: string): string;
|
|
|
20
18
|
/** Checks that a scope is a short token that needs no URL encoding. */
|
|
21
19
|
export declare function isSafeScope(scope: string): boolean;
|
|
22
20
|
//#endregion
|
|
23
|
-
export { Catalog, CatalogDefinition, DEFAULT_BASE_PATH, MAX_DIMENSION, PresetDefinition, PresetName, RECIPE_ENGINE_VERSION, Recipe, RecipeInput, RenditionAddress, RenditionAnimation, RenditionFit, RenditionFormat, RenditionOptions, RenditionPosition, RenditionTarget, RenditionVariant, ScopeName, SourcePolicy,
|
|
21
|
+
export { Catalog, CatalogDefinition, DEFAULT_BASE_PATH, MAX_DIMENSION, PresetDefinition, PresetName, RECIPE_ENGINE_VERSION, Recipe, RecipeInput, type RenditionAddress, RenditionAnimation, RenditionFit, RenditionFormat, RenditionOptions, RenditionPosition, RenditionTarget, RenditionVariant, ScopeName, SourcePolicy, defineCatalog, formatContentTypes, formatExtensions, formatRenditionPath, normalizeRecipe, parseRenditionPath, serializeRecipe };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { a as normalizeRecipe, i as formatExtensions, n as RECIPE_ENGINE_VERSION, o as serializeRecipe, r as formatContentTypes, t as MAX_DIMENSION } from "./recipe-BavgOq0b.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { t as hashRecipe } from "./hash-CHipV1N6.mjs";
|
|
3
3
|
import { n as isSafePath, r as isSafeScope, t as encodePath } from "./path-hNFUjCbA.mjs";
|
|
4
4
|
import { t as assertDeclaredScope } from "./scopes-C1kzqQHz.mjs";
|
|
5
|
+
import { i as parseRenditionPath, n as assertBasePath, r as formatRenditionPath, t as DEFAULT_BASE_PATH } from "./url-C6PtYIEf.mjs";
|
|
5
6
|
//#region src/catalog.ts
|
|
6
7
|
/** Extensions of sources that get animated variants, as a path does not tell an animated source from a still one. */
|
|
7
8
|
const ANIMATED_EXTENSIONS = ["gif", "webp"];
|
|
@@ -64,7 +65,11 @@ function defineCatalog(definition) {
|
|
|
64
65
|
originals: Object.freeze([...originals]),
|
|
65
66
|
animatedVariants
|
|
66
67
|
});
|
|
67
|
-
for (const variant of [...variants, ...animatedVariants ?? []])
|
|
68
|
+
for (const variant of [...variants, ...animatedVariants ?? []]) {
|
|
69
|
+
const known = variantsByHash.get(variant.hash);
|
|
70
|
+
if (known && serializeRecipe(known.recipe) !== serializeRecipe(variant.recipe)) throw new RangeError(`Variants "${known.label}" and "${variant.label}" have different recipes of the same hash; change pipelineVersion to change every hash.`);
|
|
71
|
+
if (!known) variantsByHash.set(variant.hash, variant);
|
|
72
|
+
}
|
|
68
73
|
}
|
|
69
74
|
function getPreset(preset) {
|
|
70
75
|
const found = presetsByName.get(preset);
|
|
@@ -120,4 +125,4 @@ function wrapRangeError(context, fn) {
|
|
|
120
125
|
}
|
|
121
126
|
}
|
|
122
127
|
//#endregion
|
|
123
|
-
export { DEFAULT_BASE_PATH, MAX_DIMENSION, RECIPE_ENGINE_VERSION,
|
|
128
|
+
export { DEFAULT_BASE_PATH, MAX_DIMENSION, RECIPE_ENGINE_VERSION, defineCatalog, encodePath, formatContentTypes, formatExtensions, formatRenditionPath, hashRecipe, isSafePath, isSafeScope, normalizeRecipe, parseRenditionPath, serializeRecipe };
|
package/dist/server.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as RenditionFailureReason, a as createRenditionHandler, c as RenditionServerOptions, d as MemoryReadinessIndexOptions, f as ReadinessIndex, g as RenditionError, h as createLocalCoordinator, i as RenditionRequest, l as SourceResolution, m as Coordinator, n as RenditionHandler, o as Rendition, p as createMemoryReadinessIndex, r as RenditionHandlerOptions, s as RenditionServer, t as RenditionFailure, u as createRenditionServer } from "./handler-
|
|
1
|
+
import { _ as RenditionFailureReason, a as createRenditionHandler, c as RenditionServerOptions, d as MemoryReadinessIndexOptions, f as ReadinessIndex, g as RenditionError, h as createLocalCoordinator, i as RenditionRequest, l as SourceResolution, m as Coordinator, n as RenditionHandler, o as Rendition, p as createMemoryReadinessIndex, r as RenditionHandlerOptions, s as RenditionServer, t as RenditionFailure, u as createRenditionServer } from "./handler-RAQFWtrg.mjs";
|
|
2
2
|
import { i as StoreCallOptions, n as ResultStore, r as SourceStore, t as EncodedImage } from "./stores-P-TUmax4.mjs";
|
|
3
3
|
import { t as Processor } from "./processor-DvyPs-OZ.mjs";
|
|
4
4
|
export { Coordinator, EncodedImage, MemoryReadinessIndexOptions, Processor, ReadinessIndex, Rendition, RenditionError, RenditionFailure, RenditionFailureReason, RenditionHandler, RenditionHandlerOptions, RenditionRequest, RenditionServer, RenditionServerOptions, ResultStore, SourceResolution, SourceStore, StoreCallOptions, createLocalCoordinator, createMemoryReadinessIndex, createRenditionHandler, createRenditionServer };
|
package/dist/server.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { i as formatExtensions, s as assertInteger } from "./recipe-BavgOq0b.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { t as hashRecipe } from "./hash-CHipV1N6.mjs";
|
|
3
3
|
import { n as isSafePath } from "./path-hNFUjCbA.mjs";
|
|
4
4
|
import { n as isDeclaredScope } from "./scopes-C1kzqQHz.mjs";
|
|
5
5
|
import { t as RenditionError } from "./error-DViVpVmH.mjs";
|
|
6
|
-
import { t as createRenditionHandler } from "./handler-
|
|
6
|
+
import { t as createRenditionHandler } from "./handler-D5toFJ55.mjs";
|
|
7
7
|
//#region src/server/coordinator.ts
|
|
8
8
|
/** Creates a coordinator that merges concurrent equal requests within the process. */
|
|
9
9
|
function createLocalCoordinator() {
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
import { i as formatExtensions
|
|
1
|
+
import { i as formatExtensions } from "./recipe-BavgOq0b.mjs";
|
|
2
2
|
import { n as isSafePath, r as isSafeScope, t as encodePath } from "./path-hNFUjCbA.mjs";
|
|
3
|
-
import { sha256 } from "@noble/hashes/sha2.js";
|
|
4
|
-
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
5
|
-
//#region src/hash.ts
|
|
6
|
-
/** Length of a recipe hash in hex digits, which keeps 128 bits of SHA-256. */
|
|
7
|
-
const RECIPE_HASH_LENGTH = 32;
|
|
8
|
-
/** Identifies a recipe by the truncated SHA-256 of its canonical serialization. */
|
|
9
|
-
function hashRecipe(recipe) {
|
|
10
|
-
return bytesToHex(sha256(utf8ToBytes(serializeRecipe(recipe)))).slice(0, 32);
|
|
11
|
-
}
|
|
12
|
-
//#endregion
|
|
13
3
|
//#region src/url.ts
|
|
14
4
|
/** Version segment of the rendition URL format this library builds and parses. */
|
|
15
5
|
const URL_FORMAT_VERSION = "v1";
|
|
@@ -26,7 +16,8 @@ function formatRenditionPath(basePath, { scope, label, hash, format, path }) {
|
|
|
26
16
|
const extension = formatExtensions[format];
|
|
27
17
|
if (!isSafeLabel(label) || !isHash(hash) || !extension) throw new RangeError(`Invalid rendition variant "${label}.${hash}.${extension ?? format}".`);
|
|
28
18
|
if (!isSafePath(path)) throw new RangeError(`Unsafe rendition source path "${path}".`);
|
|
29
|
-
|
|
19
|
+
const scopePrefix = scope === void 0 ? "" : `${scope}/`;
|
|
20
|
+
return `${basePath}/${URL_FORMAT_VERSION}/${scopePrefix}${label}.${hash}.${extension}/${encodePath(path)}`;
|
|
30
21
|
}
|
|
31
22
|
/**
|
|
32
23
|
* Parses the path of a rendition URL, ignoring any query string.
|
|
@@ -34,7 +25,7 @@ function formatRenditionPath(basePath, { scope, label, hash, format, path }) {
|
|
|
34
25
|
* Returns `undefined` for a path outside `basePath`, another URL format version, or malformed or unsafe parts.
|
|
35
26
|
*/
|
|
36
27
|
function parseRenditionPath(basePath, url) {
|
|
37
|
-
const prefix = `${basePath}
|
|
28
|
+
const prefix = `${basePath}/${URL_FORMAT_VERSION}/`;
|
|
38
29
|
const pathname = url.split("?", 1)[0];
|
|
39
30
|
if (!pathname.startsWith(prefix)) return;
|
|
40
31
|
const parts = pathname.slice(prefix.length).split("/");
|
|
@@ -67,8 +58,13 @@ const formats = Object.keys(formatExtensions);
|
|
|
67
58
|
function isSafeLabel(label) {
|
|
68
59
|
return /^[\w@.-]{1,64}$/.test(label);
|
|
69
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Checks that a recipe hash is 1 to 64 ASCII letters, digits, `_` or `-`, which need no encoding in a URL or a storage key.
|
|
63
|
+
*
|
|
64
|
+
* The grammar does not fix the format of hashes, so a hash the catalog cannot produce is looked up as a published rendition instead of making the URL malformed.
|
|
65
|
+
*/
|
|
70
66
|
function isHash(hash) {
|
|
71
|
-
return
|
|
67
|
+
return /^[\w-]{1,64}$/.test(hash);
|
|
72
68
|
}
|
|
73
69
|
function decodeSegment(segment) {
|
|
74
70
|
try {
|
|
@@ -78,4 +74,4 @@ function decodeSegment(segment) {
|
|
|
78
74
|
}
|
|
79
75
|
}
|
|
80
76
|
//#endregion
|
|
81
|
-
export { parseRenditionPath as
|
|
77
|
+
export { parseRenditionPath as i, assertBasePath as n, formatRenditionPath as r, DEFAULT_BASE_PATH as t };
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { s as RenditionFormat } from "./recipe-BT8D60Qb.mjs";
|
|
2
2
|
//#region src/url.d.ts
|
|
3
|
-
/** Version segment of the rendition URL format this library builds and parses. */
|
|
4
|
-
declare const URL_FORMAT_VERSION = "v1";
|
|
5
3
|
/** Mount path of the rendition handler unless a catalog sets another one. */
|
|
6
4
|
declare const DEFAULT_BASE_PATH = "/_renditions";
|
|
7
5
|
/** Parts of a rendition URL. */
|
|
@@ -28,11 +26,5 @@ declare function formatRenditionPath(basePath: string, { scope, label, hash, for
|
|
|
28
26
|
* Returns `undefined` for a path outside `basePath`, another URL format version, or malformed or unsafe parts.
|
|
29
27
|
*/
|
|
30
28
|
declare function parseRenditionPath(basePath: string, url: string): RenditionAddress | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* Checks that a base path is absolute, has no trailing slash and needs no URL encoding.
|
|
33
|
-
*
|
|
34
|
-
* @throws {RangeError} When the base path is invalid.
|
|
35
|
-
*/
|
|
36
|
-
declare function assertBasePath(basePath: string): void;
|
|
37
29
|
//#endregion
|
|
38
|
-
export {
|
|
30
|
+
export { parseRenditionPath as i, RenditionAddress as n, formatRenditionPath as r, DEFAULT_BASE_PATH as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "image-renditions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "On-demand image transformations that persist every result to durable storage and keep app servers stateless.",
|
|
5
5
|
"homepage": "https://github.com/IlyaSemenov/image-renditions#readme",
|
|
6
6
|
"bugs": "https://github.com/IlyaSemenov/image-renditions/issues",
|