image-renditions 0.0.0-stage → 0.2.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 +588 -2
- package/dist/catalog-D11xkViN.d.mts +130 -0
- package/dist/error-DViVpVmH.mjs +12 -0
- package/dist/fs.d.mts +25 -0
- package/dist/fs.mjs +50 -0
- package/dist/h3.d.mts +27 -0
- package/dist/h3.mjs +25 -0
- package/dist/handler-9XP5SZhg.d.mts +227 -0
- package/dist/handler-CPJllrYi.mjs +78 -0
- package/dist/img-attrs-BoCAHpl5.mjs +14 -0
- package/dist/index.d.mts +23 -0
- package/dist/index.mjs +123 -0
- package/dist/nuxt/runtime.d.mts +90 -0
- package/dist/nuxt/runtime.mjs +129 -0
- package/dist/nuxt.d.mts +31 -0
- package/dist/nuxt.mjs +60 -0
- package/dist/path-hNFUjCbA.mjs +22 -0
- package/dist/processor-DvyPs-OZ.d.mts +22 -0
- package/dist/recipe-BT8D60Qb.d.mts +104 -0
- package/dist/recipe-BavgOq0b.mjs +142 -0
- package/dist/s3.d.mts +26 -0
- package/dist/s3.mjs +81 -0
- package/dist/scopes-C1kzqQHz.mjs +22 -0
- package/dist/server.d.mts +4 -0
- package/dist/server.mjs +218 -0
- package/dist/sharp.d.mts +22 -0
- package/dist/sharp.mjs +205 -0
- package/dist/stores-P-TUmax4.d.mts +42 -0
- package/dist/url-BAU5GYXC.mjs +81 -0
- package/dist/url-BVH3ZB0F.d.mts +38 -0
- package/package.json +89 -4
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { a as RenditionAnimation, c as RenditionOptions, r as Recipe, u as SourcePolicy } from "./recipe-BT8D60Qb.mjs";
|
|
2
|
+
//#region src/catalog.d.ts
|
|
3
|
+
/** Named transformation in a catalog. */
|
|
4
|
+
interface PresetDefinition extends RenditionOptions {
|
|
5
|
+
/** Pixel densities to render, as multipliers of `width` and `height`; defaults to `[1]`. */
|
|
6
|
+
densities?: readonly number[];
|
|
7
|
+
/**
|
|
8
|
+
* Extensions of source files shown as they are instead of rendered, such as `svg`, in lowercase and without the dot.
|
|
9
|
+
*
|
|
10
|
+
* Such sources are shown as they are whatever the other options, including `animation`.
|
|
11
|
+
* They do not affect hashes, and the server still renders such a source when its rendition URL is requested.
|
|
12
|
+
*/
|
|
13
|
+
originals?: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* How renditions show animated sources; `first-frame` by default.
|
|
16
|
+
*
|
|
17
|
+
* With `preserve`, sources with the `gif` or `webp` extension get variants of their own in `webp` that keep the animation, which take `quality` and `effort` only from a `webp` preset.
|
|
18
|
+
* The variants of other sources keep their hashes.
|
|
19
|
+
*/
|
|
20
|
+
animation?: RenditionAnimation;
|
|
21
|
+
}
|
|
22
|
+
/** Shared configuration of renditions, imported by both the client and the server. */
|
|
23
|
+
interface CatalogDefinition<Name extends string = string, Scope extends string = string> {
|
|
24
|
+
/** Path the rendition handler is mounted at, `/_renditions` by default. */
|
|
25
|
+
basePath?: string;
|
|
26
|
+
/** Bumping it gives every variant a new hash, so every rendition is generated again. */
|
|
27
|
+
pipelineVersion?: number;
|
|
28
|
+
/** Options of every preset; a preset overrides them field by field. */
|
|
29
|
+
defaults?: PresetDefinition;
|
|
30
|
+
/** How special kinds of sources are rendered. */
|
|
31
|
+
sources?: Partial<SourcePolicy>;
|
|
32
|
+
/**
|
|
33
|
+
* Scopes a logical path can belong to, such as `tenant`; every target then has one of them.
|
|
34
|
+
*
|
|
35
|
+
* Without them, a target may have any scope or none.
|
|
36
|
+
*/
|
|
37
|
+
scopes?: readonly Scope[];
|
|
38
|
+
presets: Record<Name, PresetDefinition>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One density of a preset, which is a separate recipe with its own hash.
|
|
42
|
+
*
|
|
43
|
+
* A preset that preserves animation has a second variant at each density for GIF and WebP sources.
|
|
44
|
+
*/
|
|
45
|
+
interface RenditionVariant {
|
|
46
|
+
readonly preset: string;
|
|
47
|
+
readonly density: number;
|
|
48
|
+
/** Readable URL label, such as `avatar` or `avatar@2x`. */
|
|
49
|
+
readonly label: string;
|
|
50
|
+
readonly recipe: Recipe;
|
|
51
|
+
readonly hash: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Source a rendition is made from.
|
|
55
|
+
*
|
|
56
|
+
* `Scope` is the union of the scopes a catalog declares, which makes `scope` required, or `string` for a catalog without them.
|
|
57
|
+
*/
|
|
58
|
+
type RenditionTarget<Scope extends string = string> = {
|
|
59
|
+
/** Logical source path inside the scope. */
|
|
60
|
+
path: string;
|
|
61
|
+
} & (string extends Scope ? {
|
|
62
|
+
/** Application-defined space of the path, such as a tenant; omitted when the application has one space. */
|
|
63
|
+
scope?: string;
|
|
64
|
+
} : {
|
|
65
|
+
/** Space of the path, one of the scopes of the catalog. */
|
|
66
|
+
scope: Scope;
|
|
67
|
+
});
|
|
68
|
+
/** Validated catalog with its variants and URL builders. */
|
|
69
|
+
interface Catalog<Name extends string = string, Scope extends string = string> {
|
|
70
|
+
readonly basePath: string;
|
|
71
|
+
readonly presets: readonly Name[];
|
|
72
|
+
/** Scopes every target has one of, if the catalog declares them. */
|
|
73
|
+
readonly scopes?: readonly Scope[];
|
|
74
|
+
/**
|
|
75
|
+
* Variants of a preset ordered by density.
|
|
76
|
+
*
|
|
77
|
+
* With the path of a GIF or WebP source of a preset that preserves animation, they are the variants that keep it.
|
|
78
|
+
*/
|
|
79
|
+
variants(preset: Name, path?: string): readonly RenditionVariant[];
|
|
80
|
+
/**
|
|
81
|
+
* Variant of a preset at a density, `1` by default, chosen by the source path like in `variants()`.
|
|
82
|
+
*
|
|
83
|
+
* @throws {RangeError} When the preset does not declare the density.
|
|
84
|
+
*/
|
|
85
|
+
variant(preset: Name, density?: number, path?: string): RenditionVariant;
|
|
86
|
+
/**
|
|
87
|
+
* Extensions of source files a preset shows as they are instead of rendered.
|
|
88
|
+
*
|
|
89
|
+
* @throws {RangeError} When the preset is unknown.
|
|
90
|
+
*/
|
|
91
|
+
originals(preset: Name): readonly string[];
|
|
92
|
+
/**
|
|
93
|
+
* Whether a preset shows a source as it is instead of rendered, judged by the extension of its path.
|
|
94
|
+
*
|
|
95
|
+
* @throws {RangeError} When the preset is unknown.
|
|
96
|
+
*/
|
|
97
|
+
isOriginal(preset: Name, path: string): boolean;
|
|
98
|
+
/** Every variant of every preset, one per hash. */
|
|
99
|
+
readonly allVariants: readonly RenditionVariant[];
|
|
100
|
+
/** Variant with a recipe hash, if the catalog has one. */
|
|
101
|
+
findVariant(hash: string): RenditionVariant | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Root-relative URL of a rendition.
|
|
104
|
+
*
|
|
105
|
+
* @throws {RangeError} When the preset is unknown or does not declare the density, the scope is not one the catalog declares, or the scope or path is unsafe to put into the URL.
|
|
106
|
+
*/
|
|
107
|
+
url(preset: Name, target: RenditionTarget<Scope>, density?: number): string;
|
|
108
|
+
/**
|
|
109
|
+
* `srcset` attribute value listing every density of a preset.
|
|
110
|
+
*
|
|
111
|
+
* @throws {RangeError} When the preset is unknown, the scope is not one the catalog declares, or the scope or path is unsafe to put into the URL.
|
|
112
|
+
*/
|
|
113
|
+
srcset(preset: Name, target: RenditionTarget<Scope>): string;
|
|
114
|
+
}
|
|
115
|
+
/** Preset names of a catalog, such as `PresetName<typeof catalog>`. */
|
|
116
|
+
type PresetName<C extends Pick<Catalog, "presets">> = C["presets"][number];
|
|
117
|
+
/** Scopes a catalog declares, or `string` when it declares none. */
|
|
118
|
+
type ScopeName<C extends Pick<Catalog, "scopes">> = NonNullable<C["scopes"]>[number];
|
|
119
|
+
/**
|
|
120
|
+
* Validates a catalog and computes the recipe and hash of every preset density.
|
|
121
|
+
*
|
|
122
|
+
* Preset names are only labels: renaming a preset keeps its hashes and thus its stored renditions.
|
|
123
|
+
*
|
|
124
|
+
* @throws {RangeError} When the catalog is invalid.
|
|
125
|
+
*/
|
|
126
|
+
declare function defineCatalog<const Name extends string, const Scope extends string = string>(definition: CatalogDefinition<Name, Scope> & (string extends Scope ? unknown : {
|
|
127
|
+
scopes: readonly Scope[];
|
|
128
|
+
})): Catalog<Name, Scope>;
|
|
129
|
+
//#endregion
|
|
130
|
+
export { RenditionTarget as a, defineCatalog as c, PresetName as i, CatalogDefinition as n, RenditionVariant as o, PresetDefinition as r, ScopeName as s, Catalog as t };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/server/error.ts
|
|
2
|
+
/** Failure of a rendition operation, with a reason a failure policy can tell apart. */
|
|
3
|
+
var RenditionError = class extends Error {
|
|
4
|
+
reason;
|
|
5
|
+
name = "RenditionError";
|
|
6
|
+
constructor(reason, message = reason, options) {
|
|
7
|
+
super(message, options);
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { RenditionError as t };
|
package/dist/fs.d.mts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { n as ResultStore, r as SourceStore } from "./stores-P-TUmax4.mjs";
|
|
2
|
+
//#region src/stores/fs.d.ts
|
|
3
|
+
/** Options of {@link createFileSystemSource}. */
|
|
4
|
+
export interface FileSystemSourceOptions {
|
|
5
|
+
/** Directory source keys are resolved in. */
|
|
6
|
+
root: string;
|
|
7
|
+
/** Largest source file, in bytes; larger sources fail as too large. */
|
|
8
|
+
maxBytes?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Options of {@link createFileSystemResults}. */
|
|
11
|
+
export interface FileSystemResultsOptions {
|
|
12
|
+
/** Directory renditions are written to. */
|
|
13
|
+
root: string;
|
|
14
|
+
/**
|
|
15
|
+
* URL the directory is served at, such as `https://cdn.example.com/renditions` or `/renditions`.
|
|
16
|
+
*
|
|
17
|
+
* The web server in front of it sets `Cache-Control`; renditions never change, so it can mark them immutable.
|
|
18
|
+
*/
|
|
19
|
+
publicUrl: string;
|
|
20
|
+
}
|
|
21
|
+
/** Creates a source store reading files from a directory. */
|
|
22
|
+
export declare function createFileSystemSource({ root, maxBytes }: FileSystemSourceOptions): SourceStore;
|
|
23
|
+
/** Creates a result store writing files to a directory that a web server serves. */
|
|
24
|
+
export declare function createFileSystemResults({ root, publicUrl }: FileSystemResultsOptions): ResultStore;
|
|
25
|
+
//#endregion
|
package/dist/fs.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { n as isSafePath, t as encodePath } from "./path-hNFUjCbA.mjs";
|
|
2
|
+
import { t as RenditionError } from "./error-DViVpVmH.mjs";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, resolve, sep } from "node:path";
|
|
6
|
+
//#region src/stores/fs.ts
|
|
7
|
+
/** Creates a source store reading files from a directory. */
|
|
8
|
+
function createFileSystemSource({ root, maxBytes = 52428800 }) {
|
|
9
|
+
return { async read(key, { signal }) {
|
|
10
|
+
const path = resolvePath(root, key);
|
|
11
|
+
const stats = await stat(path).catch(ignoreMissing);
|
|
12
|
+
if (!stats?.isFile()) return;
|
|
13
|
+
if (stats.size > maxBytes) throw new RenditionError("source-too-large", `The source ${key} exceeds ${maxBytes} bytes.`);
|
|
14
|
+
return new Uint8Array(await readFile(path, { signal }));
|
|
15
|
+
} };
|
|
16
|
+
}
|
|
17
|
+
/** Creates a result store writing files to a directory that a web server serves. */
|
|
18
|
+
function createFileSystemResults({ root, publicUrl }) {
|
|
19
|
+
return {
|
|
20
|
+
async exists(key) {
|
|
21
|
+
return (await stat(resolvePath(root, key)).catch(ignoreMissing))?.isFile() ?? false;
|
|
22
|
+
},
|
|
23
|
+
async write(key, { data }, { signal }) {
|
|
24
|
+
const path = resolvePath(root, key);
|
|
25
|
+
await mkdir(dirname(path), { recursive: true });
|
|
26
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
27
|
+
try {
|
|
28
|
+
await writeFile(temporary, data, { signal });
|
|
29
|
+
await rename(temporary, path);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
await rm(temporary, { force: true });
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
url: (key) => `${publicUrl.replace(/\/$/, "")}/${encodePath(key)}`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function resolvePath(root, key) {
|
|
39
|
+
const base = resolve(root);
|
|
40
|
+
const path = resolve(base, key);
|
|
41
|
+
const prefix = base.endsWith(sep) ? base : base + sep;
|
|
42
|
+
if (!isSafePath(key) || !path.startsWith(prefix)) throw new RenditionError("invalid-request", `Unsafe storage key ${key}.`);
|
|
43
|
+
return path;
|
|
44
|
+
}
|
|
45
|
+
function ignoreMissing(error) {
|
|
46
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") return;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { createFileSystemResults, createFileSystemSource };
|
package/dist/h3.d.mts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { r as RenditionHandlerOptions } from "./handler-9XP5SZhg.mjs";
|
|
2
|
+
import { EventHandler, H3Event } from "h3";
|
|
3
|
+
//#region src/h3.d.ts
|
|
4
|
+
/** Source of the application context of a request. */
|
|
5
|
+
interface ContextOptions<Context> {
|
|
6
|
+
/**
|
|
7
|
+
* Derives the application context, such as a tenant, from the request event.
|
|
8
|
+
*
|
|
9
|
+
* Caches reuse a redirect for every request of its URL, so derive the context from the URL and host only, or set `redirectCacheControl` to `no-store`.
|
|
10
|
+
* It is skipped for unsupported methods and for malformed URLs unless `onFailure` needs the context.
|
|
11
|
+
*/
|
|
12
|
+
getContext(event: H3Event): Context | Promise<Context>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Options of {@link defineRenditionEventHandler}.
|
|
16
|
+
*
|
|
17
|
+
* `getContext` may be omitted when the server accepts an `undefined` context.
|
|
18
|
+
*/
|
|
19
|
+
export type RenditionEventHandlerOptions<Context, Scope extends string = string> = RenditionHandlerOptions<Context, Scope> & (undefined extends Context ? Partial<ContextOptions<Context>> : ContextOptions<Context>);
|
|
20
|
+
/**
|
|
21
|
+
* Defines an h3 event handler serving rendition URLs, for h3 v1 and v2.
|
|
22
|
+
*
|
|
23
|
+
* Mount it at the catalog base path with a catch-all route, such as `/_renditions/**`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function defineRenditionEventHandler<Context, Scope extends string = string>(options: RenditionEventHandlerOptions<Context, Scope>): EventHandler;
|
|
26
|
+
//#endregion
|
|
27
|
+
export {};
|
package/dist/h3.mjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { t as createRenditionHandler } from "./handler-CPJllrYi.mjs";
|
|
2
|
+
import { defineEventHandler } from "h3";
|
|
3
|
+
//#region src/h3.ts
|
|
4
|
+
/**
|
|
5
|
+
* Defines an h3 event handler serving rendition URLs, for h3 v1 and v2.
|
|
6
|
+
*
|
|
7
|
+
* Mount it at the catalog base path with a catch-all route, such as `/_renditions/**`.
|
|
8
|
+
*/
|
|
9
|
+
function defineRenditionEventHandler(options) {
|
|
10
|
+
const handler = createRenditionHandler(options);
|
|
11
|
+
const getContext = options.getContext;
|
|
12
|
+
return defineEventHandler((event) => handler(readRequest(event), () => getContext?.(event)));
|
|
13
|
+
}
|
|
14
|
+
function readRequest(event) {
|
|
15
|
+
if (event.url instanceof URL) return {
|
|
16
|
+
method: event.req.method,
|
|
17
|
+
path: event.url.pathname + event.url.search
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
method: event.method,
|
|
21
|
+
path: event.path
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { defineRenditionEventHandler };
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { r as Recipe, s as RenditionFormat } from "./recipe-BT8D60Qb.mjs";
|
|
2
|
+
import { a as RenditionTarget, t as Catalog } from "./catalog-D11xkViN.mjs";
|
|
3
|
+
import { n as RenditionAddress } from "./url-BVH3ZB0F.mjs";
|
|
4
|
+
import { i as StoreCallOptions, n as ResultStore, r as SourceStore } from "./stores-P-TUmax4.mjs";
|
|
5
|
+
import { t as Processor } from "./processor-DvyPs-OZ.mjs";
|
|
6
|
+
//#region src/server/error.d.ts
|
|
7
|
+
/** Why a rendition could not be served. */
|
|
8
|
+
type RenditionFailureReason =
|
|
9
|
+
/** The URL is malformed, the source resolver rejected its target, or a key is unsafe or too long for the store. */
|
|
10
|
+
"invalid-request" |
|
|
11
|
+
/** The recipe hash is not in the catalog, and no rendition with it is known to be published. */
|
|
12
|
+
"unknown-recipe" | "source-not-found" |
|
|
13
|
+
/** The source cannot be decoded or processed. */
|
|
14
|
+
"unsupported-source" |
|
|
15
|
+
/** The source exceeds a byte or pixel limit. */
|
|
16
|
+
"source-too-large" |
|
|
17
|
+
/** Reading the source failed for a reason other than its absence. */
|
|
18
|
+
"source-error" |
|
|
19
|
+
/** Processing failed for a reason other than the source. */
|
|
20
|
+
"processing-failed" |
|
|
21
|
+
/** Checking for or writing the result in storage failed. */
|
|
22
|
+
"storage-error" |
|
|
23
|
+
/** The processing queue is full, or a request waited in it for too long. */
|
|
24
|
+
"overloaded";
|
|
25
|
+
/** Failure of a rendition operation, with a reason a failure policy can tell apart. */
|
|
26
|
+
declare class RenditionError extends Error {
|
|
27
|
+
readonly reason: RenditionFailureReason;
|
|
28
|
+
name: string;
|
|
29
|
+
constructor(reason: RenditionFailureReason, message?: string, options?: ErrorOptions);
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/server/coordinator.d.ts
|
|
33
|
+
/**
|
|
34
|
+
* Serializes work on the same rendition.
|
|
35
|
+
*
|
|
36
|
+
* Callers with an equal key share the outcome of one task run.
|
|
37
|
+
* A cross-replica implementation can hold a distributed lock instead, because the task rechecks whether the rendition is already published.
|
|
38
|
+
* The server sets no timeout on `run`, so a distributed lock must limit how long it waits.
|
|
39
|
+
*/
|
|
40
|
+
interface Coordinator {
|
|
41
|
+
run<T>(key: string, task: () => Promise<T>): Promise<T>;
|
|
42
|
+
}
|
|
43
|
+
/** Creates a coordinator that merges concurrent equal requests within the process. */
|
|
44
|
+
declare function createLocalCoordinator(): Coordinator;
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/server/readiness.d.ts
|
|
47
|
+
/**
|
|
48
|
+
* Positive knowledge that renditions are published, keyed by their public URL.
|
|
49
|
+
*
|
|
50
|
+
* An index never records failures or absence.
|
|
51
|
+
* A lost entry costs one storage check for a recipe in the catalog, but leaves a recipe missing from the catalog unservable.
|
|
52
|
+
*/
|
|
53
|
+
interface ReadinessIndex {
|
|
54
|
+
has(url: string, options?: StoreCallOptions): boolean | Promise<boolean>;
|
|
55
|
+
add(url: string, options?: StoreCallOptions): void | Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/** Options of {@link createMemoryReadinessIndex}. */
|
|
58
|
+
interface MemoryReadinessIndexOptions {
|
|
59
|
+
/** Entries kept before the least recently used ones are evicted. */
|
|
60
|
+
maxEntries?: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Creates a process-local readiness index with least-recently-used eviction.
|
|
64
|
+
*
|
|
65
|
+
* @throws {RangeError} When `maxEntries` is not a non-negative integer.
|
|
66
|
+
*/
|
|
67
|
+
declare function createMemoryReadinessIndex({ maxEntries }?: MemoryReadinessIndexOptions): ReadinessIndex;
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/server/server.d.ts
|
|
70
|
+
/** Storage locations of a source and its renditions. */
|
|
71
|
+
interface SourceResolution {
|
|
72
|
+
/** Key of the source object in the source store. */
|
|
73
|
+
sourceKey: string;
|
|
74
|
+
/**
|
|
75
|
+
* Key prefix of every rendition of the source; a rendition is stored at `<resultPrefix>/<hash>.<extension>`.
|
|
76
|
+
*
|
|
77
|
+
* Defaults to `sourceKey`, which suits a result store separate from the source store.
|
|
78
|
+
* Different sources must have different prefixes, and in a store shared with sources the prefixes must stay apart from source keys, such as under `renditions/`.
|
|
79
|
+
*/
|
|
80
|
+
resultPrefix?: string;
|
|
81
|
+
}
|
|
82
|
+
/** Options of {@link createRenditionServer}. */
|
|
83
|
+
interface RenditionServerOptions<Context, Scope extends string = string> {
|
|
84
|
+
/** Catalog of the renditions the server serves, whose scopes type and limit its targets. */
|
|
85
|
+
catalog: Catalog<string, Scope>;
|
|
86
|
+
source: SourceStore;
|
|
87
|
+
results: ResultStore;
|
|
88
|
+
processor: Processor;
|
|
89
|
+
/**
|
|
90
|
+
* Maps a target to storage locations within the application context, such as a tenant.
|
|
91
|
+
*
|
|
92
|
+
* Returns `undefined` when the target must not be served, for example an unknown scope.
|
|
93
|
+
* Targets of different scopes must map to different locations even when their paths are equal.
|
|
94
|
+
* By default an unscoped path is the source key, and scoped targets are not served.
|
|
95
|
+
*/
|
|
96
|
+
resolveSource?: (target: RenditionTarget<NoInfer<Scope>>, context: Context) => SourceResolution | undefined | Promise<SourceResolution | undefined>;
|
|
97
|
+
readiness?: {
|
|
98
|
+
/** Process-local index consulted first. */
|
|
99
|
+
memory?: MemoryReadinessIndexOptions;
|
|
100
|
+
/**
|
|
101
|
+
* Index shared between replicas, consulted after the memory index.
|
|
102
|
+
*
|
|
103
|
+
* Each call gets an abort signal that fires after `limits.indexTimeout`; the server stops waiting then, even if the call goes on.
|
|
104
|
+
*/
|
|
105
|
+
shared?: ReadinessIndex;
|
|
106
|
+
};
|
|
107
|
+
/** Merges work on the same rendition; process-local by default. */
|
|
108
|
+
coordinator?: Coordinator;
|
|
109
|
+
limits?: {
|
|
110
|
+
/** Renditions processed at once, including their source download. */
|
|
111
|
+
concurrency?: number;
|
|
112
|
+
/** Renditions waiting for a processing slot; more fail as `overloaded`. */
|
|
113
|
+
queueSize?: number;
|
|
114
|
+
/** Milliseconds a rendition may wait for a processing slot before it fails as `overloaded`. */
|
|
115
|
+
queueTimeout?: number;
|
|
116
|
+
/** Milliseconds each storage call may take before it fails; the store receives an abort signal to cancel its work. */
|
|
117
|
+
storageTimeout?: number;
|
|
118
|
+
/** Milliseconds each shared readiness index call may take before it counts as failed; the index receives an abort signal to cancel its work. */
|
|
119
|
+
indexTimeout?: number;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Receives errors that do not fail the operation, such as an unavailable shared readiness index.
|
|
123
|
+
*
|
|
124
|
+
* It is not awaited, and its own failures are ignored.
|
|
125
|
+
*/
|
|
126
|
+
onError?(error: unknown): void;
|
|
127
|
+
}
|
|
128
|
+
/** Published rendition. */
|
|
129
|
+
interface Rendition {
|
|
130
|
+
/** Key in the result store. */
|
|
131
|
+
key: string;
|
|
132
|
+
/** Public URL. */
|
|
133
|
+
url: string;
|
|
134
|
+
}
|
|
135
|
+
/** Generates renditions and finds published ones. */
|
|
136
|
+
interface RenditionServer<Context, Scope extends string = string> {
|
|
137
|
+
readonly catalog: Catalog<string, Scope>;
|
|
138
|
+
/**
|
|
139
|
+
* Makes sure the rendition of a target is published and returns it.
|
|
140
|
+
*
|
|
141
|
+
* Resolves only after the rendition is written or confirmed to exist.
|
|
142
|
+
*
|
|
143
|
+
* @throws {RangeError} When the processor cannot render the recipe.
|
|
144
|
+
* @throws {RenditionError} When the rendition cannot be published.
|
|
145
|
+
*/
|
|
146
|
+
ensure: (recipe: Recipe, target: RenditionTarget<Scope>, context: Context) => Promise<Rendition>;
|
|
147
|
+
/**
|
|
148
|
+
* Finds a rendition by recipe hash in the readiness indexes, without any storage call.
|
|
149
|
+
*
|
|
150
|
+
* Serves renditions of recipes the catalog no longer contains.
|
|
151
|
+
*
|
|
152
|
+
* @throws {RenditionError} When the target is rejected, with reason `invalid-request`.
|
|
153
|
+
*/
|
|
154
|
+
findPublished: (hash: string, format: RenditionFormat, target: RenditionTarget<Scope>, context: Context) => Promise<Rendition | undefined>;
|
|
155
|
+
/**
|
|
156
|
+
* Storage locations of a target as `ensure()` uses them, with `resultPrefix` defaulting to `sourceKey`.
|
|
157
|
+
*
|
|
158
|
+
* It checks that the catalog declares the scope, calls `resolveSource` and checks that the keys are safe, but does not check that the source exists.
|
|
159
|
+
* Full rendition keys and the limits of a store are checked only when a rendition is located or stored.
|
|
160
|
+
*
|
|
161
|
+
* @throws {RenditionError} When the scope is not declared, or the target is rejected or resolves to an unsafe key, with reason `invalid-request`.
|
|
162
|
+
*/
|
|
163
|
+
resolve: (target: RenditionTarget<Scope>, context: Context) => Promise<Required<SourceResolution>>;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Creates a rendition server over source and result stores.
|
|
167
|
+
*
|
|
168
|
+
* @throws {RangeError} When a limit is not an integer in its range, or the processor cannot render a variant of the catalog.
|
|
169
|
+
*/
|
|
170
|
+
declare function createRenditionServer<Context, Scope extends string = string>(options: RenditionServerOptions<Context, Scope>): RenditionServer<Context, Scope>;
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/server/handler.d.ts
|
|
173
|
+
/** HTTP request fields a rendition handler reads. */
|
|
174
|
+
interface RenditionRequest {
|
|
175
|
+
method: string;
|
|
176
|
+
/** Request path in its wire encoding, with an optional query string. */
|
|
177
|
+
path: string;
|
|
178
|
+
}
|
|
179
|
+
/** Failed rendition request, as passed to a failure policy. */
|
|
180
|
+
interface RenditionFailure<Context, Scope extends string = string> {
|
|
181
|
+
reason: RenditionFailureReason;
|
|
182
|
+
error: RenditionError;
|
|
183
|
+
/** Parsed rendition URL, absent when the URL is malformed. */
|
|
184
|
+
address?: RenditionAddress;
|
|
185
|
+
/**
|
|
186
|
+
* Target of the URL, absent when the URL is malformed or its scope is not one the catalog declares.
|
|
187
|
+
*
|
|
188
|
+
* It does not mean that `resolveSource` accepts the target or that the source exists.
|
|
189
|
+
*/
|
|
190
|
+
target?: RenditionTarget<Scope>;
|
|
191
|
+
context: Context;
|
|
192
|
+
}
|
|
193
|
+
/** Options of {@link createRenditionHandler}. */
|
|
194
|
+
interface RenditionHandlerOptions<Context, Scope extends string = string> {
|
|
195
|
+
/** Server whose catalog the handler serves. */
|
|
196
|
+
server: RenditionServer<Context, Scope>;
|
|
197
|
+
/**
|
|
198
|
+
* Builds the response to a failure; `undefined` keeps the default response with a status of the reason.
|
|
199
|
+
*
|
|
200
|
+
* The handler sets `Cache-Control: no-store` on every failure response, including fallbacks such as redirects to the original.
|
|
201
|
+
*/
|
|
202
|
+
onFailure?(failure: RenditionFailure<Context, Scope>): Response | undefined | Promise<Response | undefined>;
|
|
203
|
+
/**
|
|
204
|
+
* `Cache-Control` of redirects to published renditions; `public, max-age=86400` by default.
|
|
205
|
+
*
|
|
206
|
+
* Keep it moderate: the target URL depends on storage configuration, and a cached redirect cannot be revoked.
|
|
207
|
+
* Use `no-store` when the context depends on cookies or a session, because caches reuse a redirect for every request of its URL.
|
|
208
|
+
*/
|
|
209
|
+
redirectCacheControl?: string;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Serves rendition URLs by redirecting to published renditions.
|
|
213
|
+
*
|
|
214
|
+
* `getContext` returns the application context, such as a tenant.
|
|
215
|
+
* The handler calls it at most once, and only for a request it passes to the server or a failure it passes to `onFailure`, so unsupported methods and malformed URLs skip a lookup.
|
|
216
|
+
* Its failures propagate unchanged, without `onFailure`.
|
|
217
|
+
*/
|
|
218
|
+
type RenditionHandler<Context> = (request: RenditionRequest, getContext: () => Context | Promise<Context>) => Promise<Response>;
|
|
219
|
+
/**
|
|
220
|
+
* Creates a framework-independent rendition handler.
|
|
221
|
+
*
|
|
222
|
+
* A hash from the catalog is rendered on demand.
|
|
223
|
+
* An unknown hash is served only when the readiness indexes know its rendition, so arbitrary hashes cost no storage calls.
|
|
224
|
+
*/
|
|
225
|
+
declare function createRenditionHandler<Context, Scope extends string = string>({ server, onFailure, redirectCacheControl }: RenditionHandlerOptions<Context, Scope>): RenditionHandler<Context>;
|
|
226
|
+
//#endregion
|
|
227
|
+
export { RenditionFailureReason as _, createRenditionHandler as a, RenditionServerOptions as c, MemoryReadinessIndexOptions as d, ReadinessIndex as f, RenditionError as g, createLocalCoordinator as h, RenditionRequest as i, SourceResolution as l, Coordinator as m, RenditionHandler as n, Rendition as o, createMemoryReadinessIndex as p, RenditionHandlerOptions as r, RenditionServer as s, RenditionFailure as t, createRenditionServer as u };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { a as parseRenditionPath } from "./url-BAU5GYXC.mjs";
|
|
2
|
+
import { r as toDeclaredTarget } from "./scopes-C1kzqQHz.mjs";
|
|
3
|
+
import { t as RenditionError } from "./error-DViVpVmH.mjs";
|
|
4
|
+
//#region src/server/handler.ts
|
|
5
|
+
const failureStatus = {
|
|
6
|
+
"invalid-request": 404,
|
|
7
|
+
"unknown-recipe": 404,
|
|
8
|
+
"source-not-found": 404,
|
|
9
|
+
"unsupported-source": 422,
|
|
10
|
+
"source-too-large": 422,
|
|
11
|
+
"source-error": 502,
|
|
12
|
+
"processing-failed": 500,
|
|
13
|
+
"storage-error": 502,
|
|
14
|
+
overloaded: 503
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Creates a framework-independent rendition handler.
|
|
18
|
+
*
|
|
19
|
+
* A hash from the catalog is rendered on demand.
|
|
20
|
+
* An unknown hash is served only when the readiness indexes know its rendition, so arbitrary hashes cost no storage calls.
|
|
21
|
+
*/
|
|
22
|
+
function createRenditionHandler({ server, onFailure, redirectCacheControl = "public, max-age=86400" }) {
|
|
23
|
+
const { catalog } = server;
|
|
24
|
+
async function fail(error, address, target, getContext) {
|
|
25
|
+
const response = (onFailure && await onFailure({
|
|
26
|
+
reason: error.reason,
|
|
27
|
+
error,
|
|
28
|
+
address,
|
|
29
|
+
target,
|
|
30
|
+
context: await getContext()
|
|
31
|
+
})) ?? new Response(error.reason, {
|
|
32
|
+
status: failureStatus[error.reason],
|
|
33
|
+
headers: {
|
|
34
|
+
"content-type": "text/plain",
|
|
35
|
+
...error.reason === "overloaded" && { "retry-after": "1" }
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const headers = new Headers(response.headers);
|
|
39
|
+
headers.set("cache-control", "no-store");
|
|
40
|
+
return new Response(response.body, {
|
|
41
|
+
status: response.status,
|
|
42
|
+
statusText: response.statusText,
|
|
43
|
+
headers
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return async ({ method, path }, getContext) => {
|
|
47
|
+
if (method !== "GET" && method !== "HEAD") return new Response(null, {
|
|
48
|
+
status: 405,
|
|
49
|
+
headers: {
|
|
50
|
+
allow: "GET, HEAD",
|
|
51
|
+
"cache-control": "no-store"
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const address = parseRenditionPath(catalog.basePath, path);
|
|
55
|
+
if (!address) return fail(new RenditionError("invalid-request", "Malformed rendition URL."), void 0, void 0, getContext);
|
|
56
|
+
const target = toDeclaredTarget(catalog.scopes, address.scope, address.path);
|
|
57
|
+
if (!target) return fail(new RenditionError("invalid-request", address.scope === void 0 ? "The URL has no scope." : `The scope ${address.scope} is not declared.`), address, void 0, getContext);
|
|
58
|
+
const variant = catalog.findVariant(address.hash);
|
|
59
|
+
if (variant && variant.recipe.format !== address.format) return fail(new RenditionError("invalid-request", "The extension does not match the recipe format."), address, target, getContext);
|
|
60
|
+
const context = await getContext();
|
|
61
|
+
try {
|
|
62
|
+
const rendition = variant ? await server.ensure(variant.recipe, target, context) : await server.findPublished(address.hash, address.format, target, context);
|
|
63
|
+
if (!rendition) throw new RenditionError("unknown-recipe", `Unknown recipe ${address.hash}.`);
|
|
64
|
+
return new Response(null, {
|
|
65
|
+
status: 307,
|
|
66
|
+
headers: {
|
|
67
|
+
location: rendition.url,
|
|
68
|
+
"cache-control": redirectCacheControl
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof RenditionError) return fail(error, address, target, () => context);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { createRenditionHandler as t };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/nuxt/img-attrs.ts
|
|
2
|
+
/** Attributes of an `img` element that belong to the rendition alone. */
|
|
3
|
+
const RENDITION_ATTRS = [
|
|
4
|
+
"src",
|
|
5
|
+
"srcset",
|
|
6
|
+
"width",
|
|
7
|
+
"height"
|
|
8
|
+
];
|
|
9
|
+
/** Name of a default attribute that would set a rendition attribute, in any letter case as HTML ignores it, if there is one. */
|
|
10
|
+
function findRenditionAttr(imgAttrs) {
|
|
11
|
+
return Object.keys(imgAttrs ?? {}).find((name) => RENDITION_ATTRS.includes(name.toLowerCase()));
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { findRenditionAttr as t };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
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 { a as formatRenditionPath, i as assertBasePath, n as RenditionAddress, o as parseRenditionPath, r as URL_FORMAT_VERSION, t as DEFAULT_BASE_PATH } from "./url-BVH3ZB0F.mjs";
|
|
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
|
+
/** Identifies a recipe by the truncated SHA-256 of its canonical serialization. */
|
|
8
|
+
export declare function hashRecipe(recipe: Recipe): string;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/path.d.ts
|
|
11
|
+
/**
|
|
12
|
+
* Checks that a slash-separated path addresses exactly one storage object.
|
|
13
|
+
*
|
|
14
|
+
* Unsafe or ambiguous paths are rejected rather than normalized, so one object never gets several URLs:
|
|
15
|
+
* no leading, trailing or repeated slashes, no `.` or `..` segments, no backslashes, which Windows file systems treat as separators, no NUL characters, which end strings in file system APIs, and no unpaired surrogates, which have no URL encoding.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isSafePath(path: string): boolean;
|
|
18
|
+
/** Percent-encodes each segment of a slash-separated path for use in a URL path. */
|
|
19
|
+
export declare function encodePath(path: string): string;
|
|
20
|
+
/** Checks that a scope is a short token that needs no URL encoding. */
|
|
21
|
+
export declare function isSafeScope(scope: string): boolean;
|
|
22
|
+
//#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, URL_FORMAT_VERSION, assertBasePath, defineCatalog, formatContentTypes, formatExtensions, formatRenditionPath, normalizeRecipe, parseRenditionPath, serializeRecipe };
|