image-renditions 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 +443 -2
- package/dist/catalog-CSnosQ1L.d.mts +64 -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-BFV_vIr4.d.mts +207 -0
- package/dist/handler-BHMwNZnI.mjs +76 -0
- package/dist/index.d.mts +23 -0
- package/dist/index.mjs +84 -0
- package/dist/nuxt/runtime.d.mts +64 -0
- package/dist/nuxt/runtime.mjs +78 -0
- package/dist/nuxt.d.mts +21 -0
- package/dist/nuxt.mjs +45 -0
- package/dist/path-hNFUjCbA.mjs +22 -0
- package/dist/processor-_xuwB7Aa.d.mts +14 -0
- package/dist/recipe-DmOhQJWE.mjs +141 -0
- package/dist/recipe-Je9DxqO1.d.mts +98 -0
- package/dist/s3.d.mts +52 -0
- package/dist/s3.mjs +80 -0
- package/dist/server.d.mts +4 -0
- package/dist/server.mjs +195 -0
- package/dist/sharp.d.mts +10 -0
- package/dist/sharp.mjs +143 -0
- package/dist/stores-DFDRMZcC.d.mts +42 -0
- package/dist/url-BByyAbmB.d.mts +38 -0
- package/dist/url-BIvAw2BR.mjs +81 -0
- package/package.json +85 -4
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { o as RenditionFormat, r as Recipe } from "./recipe-Je9DxqO1.mjs";
|
|
2
|
+
import { i as RenditionTarget, t as Catalog } from "./catalog-CSnosQ1L.mjs";
|
|
3
|
+
import { n as RenditionAddress } from "./url-BByyAbmB.mjs";
|
|
4
|
+
import { i as StoreCallOptions, n as ResultStore, r as SourceStore } from "./stores-DFDRMZcC.mjs";
|
|
5
|
+
import { t as Processor } from "./processor-_xuwB7Aa.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 rendered, or exceeds a size limit. */
|
|
14
|
+
"unsupported-source" |
|
|
15
|
+
/** Reading the source failed for a reason other than its absence. */
|
|
16
|
+
"source-error" |
|
|
17
|
+
/** Processing failed for a reason other than the source. */
|
|
18
|
+
"processing-failed" |
|
|
19
|
+
/** Checking for or writing the result in storage failed. */
|
|
20
|
+
"storage-error" |
|
|
21
|
+
/** The processing queue is full, or a request waited in it for too long. */
|
|
22
|
+
"overloaded";
|
|
23
|
+
/** Failure of a rendition operation, with a reason a failure policy can tell apart. */
|
|
24
|
+
declare class RenditionError extends Error {
|
|
25
|
+
readonly reason: RenditionFailureReason;
|
|
26
|
+
name: string;
|
|
27
|
+
constructor(reason: RenditionFailureReason, message?: string, options?: ErrorOptions);
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/server/coordinator.d.ts
|
|
31
|
+
/**
|
|
32
|
+
* Serializes work on the same rendition.
|
|
33
|
+
*
|
|
34
|
+
* Callers with an equal key share the outcome of one task run.
|
|
35
|
+
* A cross-replica implementation can hold a distributed lock instead, because the task rechecks whether the rendition is already published.
|
|
36
|
+
* The server sets no timeout on `run`, so a distributed lock must limit how long it waits.
|
|
37
|
+
*/
|
|
38
|
+
interface Coordinator {
|
|
39
|
+
run<T>(key: string, task: () => Promise<T>): Promise<T>;
|
|
40
|
+
}
|
|
41
|
+
/** Creates a coordinator that merges concurrent equal requests within the process. */
|
|
42
|
+
declare function createLocalCoordinator(): Coordinator;
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/server/readiness.d.ts
|
|
45
|
+
/**
|
|
46
|
+
* Positive knowledge that renditions are published, keyed by their public URL.
|
|
47
|
+
*
|
|
48
|
+
* An index never records failures or absence.
|
|
49
|
+
* A lost entry costs one storage check for a recipe in the catalog, but leaves a recipe missing from the catalog unservable.
|
|
50
|
+
*/
|
|
51
|
+
interface ReadinessIndex {
|
|
52
|
+
has(url: string, options?: StoreCallOptions): boolean | Promise<boolean>;
|
|
53
|
+
add(url: string, options?: StoreCallOptions): void | Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
/** Options of {@link createMemoryReadinessIndex}. */
|
|
56
|
+
interface MemoryReadinessIndexOptions {
|
|
57
|
+
/** Entries kept before the least recently used ones are evicted. */
|
|
58
|
+
maxEntries?: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Creates a process-local readiness index with least-recently-used eviction.
|
|
62
|
+
*
|
|
63
|
+
* @throws {RangeError} When `maxEntries` is not a non-negative integer.
|
|
64
|
+
*/
|
|
65
|
+
declare function createMemoryReadinessIndex({ maxEntries }?: MemoryReadinessIndexOptions): ReadinessIndex;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/server/server.d.ts
|
|
68
|
+
/** Storage locations of a source and its renditions. */
|
|
69
|
+
interface SourceResolution {
|
|
70
|
+
/** Key of the source object in the source store. */
|
|
71
|
+
sourceKey: string;
|
|
72
|
+
/**
|
|
73
|
+
* Key prefix of every rendition of the source; a rendition is stored at `<resultPrefix>/<hash>.<extension>`.
|
|
74
|
+
*
|
|
75
|
+
* Defaults to `sourceKey`, which suits a result store separate from the source store.
|
|
76
|
+
* 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/`.
|
|
77
|
+
*/
|
|
78
|
+
resultPrefix?: string;
|
|
79
|
+
}
|
|
80
|
+
/** Options of {@link createRenditionServer}. */
|
|
81
|
+
interface RenditionServerOptions<Context> {
|
|
82
|
+
source: SourceStore;
|
|
83
|
+
results: ResultStore;
|
|
84
|
+
processor: Processor;
|
|
85
|
+
/**
|
|
86
|
+
* Maps a target to storage locations within the application context, such as a tenant.
|
|
87
|
+
*
|
|
88
|
+
* Returns `undefined` when the target must not be served, for example an unknown scope.
|
|
89
|
+
* Targets of different scopes must map to different locations even when their paths are equal.
|
|
90
|
+
* By default an unscoped path is the source key, and scoped targets are not served.
|
|
91
|
+
*/
|
|
92
|
+
resolveSource?(target: RenditionTarget, context: Context): SourceResolution | undefined | Promise<SourceResolution | undefined>;
|
|
93
|
+
readiness?: {
|
|
94
|
+
/** Process-local index consulted first. */
|
|
95
|
+
memory?: MemoryReadinessIndexOptions;
|
|
96
|
+
/**
|
|
97
|
+
* Index shared between replicas, consulted after the memory index.
|
|
98
|
+
*
|
|
99
|
+
* Each call gets an abort signal that fires after `limits.indexTimeout`; the server stops waiting then, even if the call goes on.
|
|
100
|
+
*/
|
|
101
|
+
shared?: ReadinessIndex;
|
|
102
|
+
};
|
|
103
|
+
/** Merges work on the same rendition; process-local by default. */
|
|
104
|
+
coordinator?: Coordinator;
|
|
105
|
+
limits?: {
|
|
106
|
+
/** Renditions processed at once, including their source download. */
|
|
107
|
+
concurrency?: number;
|
|
108
|
+
/** Renditions waiting for a processing slot; more fail as `overloaded`. */
|
|
109
|
+
queueSize?: number;
|
|
110
|
+
/** Milliseconds a rendition may wait for a processing slot before it fails as `overloaded`. */
|
|
111
|
+
queueTimeout?: number;
|
|
112
|
+
/** Milliseconds each storage call may take before it fails; the store receives an abort signal to cancel its work. */
|
|
113
|
+
storageTimeout?: number;
|
|
114
|
+
/** Milliseconds each shared readiness index call may take before it counts as failed; the index receives an abort signal to cancel its work. */
|
|
115
|
+
indexTimeout?: number;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Receives errors that do not fail the operation, such as an unavailable shared readiness index.
|
|
119
|
+
*
|
|
120
|
+
* It is not awaited, and its own failures are ignored.
|
|
121
|
+
*/
|
|
122
|
+
onError?(error: unknown): void;
|
|
123
|
+
}
|
|
124
|
+
/** Published rendition. */
|
|
125
|
+
interface Rendition {
|
|
126
|
+
/** Key in the result store. */
|
|
127
|
+
key: string;
|
|
128
|
+
/** Public URL. */
|
|
129
|
+
url: string;
|
|
130
|
+
}
|
|
131
|
+
/** Generates renditions and finds published ones. */
|
|
132
|
+
interface RenditionServer<Context> {
|
|
133
|
+
/**
|
|
134
|
+
* Makes sure the rendition of a target is published and returns it.
|
|
135
|
+
*
|
|
136
|
+
* Resolves only after the rendition is written or confirmed to exist.
|
|
137
|
+
*
|
|
138
|
+
* @throws {RenditionError} When the rendition cannot be published.
|
|
139
|
+
*/
|
|
140
|
+
ensure(recipe: Recipe, target: RenditionTarget, context: Context): Promise<Rendition>;
|
|
141
|
+
/**
|
|
142
|
+
* Finds a rendition by recipe hash in the readiness indexes, without any storage call.
|
|
143
|
+
*
|
|
144
|
+
* Serves renditions of recipes the catalog no longer contains.
|
|
145
|
+
*
|
|
146
|
+
* @throws {RenditionError} Of reason `invalid-request` when the target is rejected.
|
|
147
|
+
*/
|
|
148
|
+
findPublished(hash: string, format: RenditionFormat, target: RenditionTarget, context: Context): Promise<Rendition | undefined>;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Creates a rendition server over source and result stores.
|
|
152
|
+
*
|
|
153
|
+
* @throws {RangeError} When a limit is not an integer in its range.
|
|
154
|
+
*/
|
|
155
|
+
declare function createRenditionServer<Context>(options: RenditionServerOptions<Context>): RenditionServer<Context>;
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/server/handler.d.ts
|
|
158
|
+
/** HTTP request fields a rendition handler reads. */
|
|
159
|
+
interface RenditionRequest {
|
|
160
|
+
method: string;
|
|
161
|
+
/** Request path in its wire encoding, with an optional query string. */
|
|
162
|
+
path: string;
|
|
163
|
+
}
|
|
164
|
+
/** Failed rendition request, as passed to a failure policy. */
|
|
165
|
+
interface RenditionFailure<Context> {
|
|
166
|
+
reason: RenditionFailureReason;
|
|
167
|
+
error: RenditionError;
|
|
168
|
+
/** Parsed rendition URL, absent when the URL is malformed. */
|
|
169
|
+
address?: RenditionAddress;
|
|
170
|
+
context: Context;
|
|
171
|
+
}
|
|
172
|
+
/** Options of {@link createRenditionHandler}. */
|
|
173
|
+
interface RenditionHandlerOptions<Context> {
|
|
174
|
+
/** Catalog whose recipes are generated on demand. */
|
|
175
|
+
catalog: Catalog;
|
|
176
|
+
server: RenditionServer<Context>;
|
|
177
|
+
/**
|
|
178
|
+
* Builds the response to a failure; `undefined` keeps the default response with a status of the reason.
|
|
179
|
+
*
|
|
180
|
+
* The handler sets `Cache-Control: no-store` on every failure response, including fallbacks such as redirects to the original.
|
|
181
|
+
*/
|
|
182
|
+
onFailure?(failure: RenditionFailure<Context>): Response | undefined | Promise<Response | undefined>;
|
|
183
|
+
/**
|
|
184
|
+
* `Cache-Control` of redirects to published renditions; `public, max-age=86400` by default.
|
|
185
|
+
*
|
|
186
|
+
* Keep it moderate: the target URL depends on storage configuration, and a cached redirect cannot be revoked.
|
|
187
|
+
* Use `no-store` when the context depends on cookies or a session, because caches reuse a redirect for every request of its URL.
|
|
188
|
+
*/
|
|
189
|
+
redirectCacheControl?: string;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Serves rendition URLs by redirecting to published renditions.
|
|
193
|
+
*
|
|
194
|
+
* `getContext` returns the application context, such as a tenant.
|
|
195
|
+
* 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.
|
|
196
|
+
* Its failures propagate unchanged, without `onFailure`.
|
|
197
|
+
*/
|
|
198
|
+
type RenditionHandler<Context> = (request: RenditionRequest, getContext: () => Context | Promise<Context>) => Promise<Response>;
|
|
199
|
+
/**
|
|
200
|
+
* Creates a framework-independent rendition handler.
|
|
201
|
+
*
|
|
202
|
+
* A hash from the catalog is rendered on demand.
|
|
203
|
+
* An unknown hash is served only when the readiness indexes know its rendition, so arbitrary hashes cost no storage calls.
|
|
204
|
+
*/
|
|
205
|
+
declare function createRenditionHandler<Context>({ catalog, server, onFailure, redirectCacheControl }: RenditionHandlerOptions<Context>): RenditionHandler<Context>;
|
|
206
|
+
//#endregion
|
|
207
|
+
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,76 @@
|
|
|
1
|
+
import { a as parseRenditionPath } from "./url-BIvAw2BR.mjs";
|
|
2
|
+
import { t as RenditionError } from "./error-DViVpVmH.mjs";
|
|
3
|
+
//#region src/server/handler.ts
|
|
4
|
+
const failureStatus = {
|
|
5
|
+
"invalid-request": 404,
|
|
6
|
+
"unknown-recipe": 404,
|
|
7
|
+
"source-not-found": 404,
|
|
8
|
+
"unsupported-source": 422,
|
|
9
|
+
"source-error": 502,
|
|
10
|
+
"processing-failed": 500,
|
|
11
|
+
"storage-error": 502,
|
|
12
|
+
overloaded: 503
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Creates a framework-independent rendition handler.
|
|
16
|
+
*
|
|
17
|
+
* A hash from the catalog is rendered on demand.
|
|
18
|
+
* An unknown hash is served only when the readiness indexes know its rendition, so arbitrary hashes cost no storage calls.
|
|
19
|
+
*/
|
|
20
|
+
function createRenditionHandler({ catalog, server, onFailure, redirectCacheControl = "public, max-age=86400" }) {
|
|
21
|
+
async function fail(error, address, getContext) {
|
|
22
|
+
const response = (onFailure && await onFailure({
|
|
23
|
+
reason: error.reason,
|
|
24
|
+
error,
|
|
25
|
+
address,
|
|
26
|
+
context: await getContext()
|
|
27
|
+
})) ?? new Response(error.reason, {
|
|
28
|
+
status: failureStatus[error.reason],
|
|
29
|
+
headers: {
|
|
30
|
+
"content-type": "text/plain",
|
|
31
|
+
...error.reason === "overloaded" && { "retry-after": "1" }
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const headers = new Headers(response.headers);
|
|
35
|
+
headers.set("cache-control", "no-store");
|
|
36
|
+
return new Response(response.body, {
|
|
37
|
+
status: response.status,
|
|
38
|
+
statusText: response.statusText,
|
|
39
|
+
headers
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return async ({ method, path }, getContext) => {
|
|
43
|
+
if (method !== "GET" && method !== "HEAD") return new Response(null, {
|
|
44
|
+
status: 405,
|
|
45
|
+
headers: {
|
|
46
|
+
allow: "GET, HEAD",
|
|
47
|
+
"cache-control": "no-store"
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const address = parseRenditionPath(catalog.basePath, path);
|
|
51
|
+
if (!address) return fail(new RenditionError("invalid-request", "Malformed rendition URL."), void 0, getContext);
|
|
52
|
+
const variant = catalog.findVariant(address.hash);
|
|
53
|
+
if (variant && variant.recipe.format !== address.format) return fail(new RenditionError("invalid-request", "The extension does not match the recipe format."), address, getContext);
|
|
54
|
+
const context = await getContext();
|
|
55
|
+
const target = {
|
|
56
|
+
scope: address.scope,
|
|
57
|
+
path: address.path
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
const rendition = variant ? await server.ensure(variant.recipe, target, context) : await server.findPublished(address.hash, address.format, target, context);
|
|
61
|
+
if (!rendition) throw new RenditionError("unknown-recipe", `Unknown recipe ${address.hash}.`);
|
|
62
|
+
return new Response(null, {
|
|
63
|
+
status: 307,
|
|
64
|
+
headers: {
|
|
65
|
+
location: rendition.url,
|
|
66
|
+
"cache-control": redirectCacheControl
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error instanceof RenditionError) return fail(error, address, () => context);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { createRenditionHandler as t };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { a as RenditionFit, c as RenditionPosition, d as formatExtensions, f as normalizeRecipe, i as RecipeInput, l as SourcePolicy, n as RECIPE_ENGINE_VERSION, o as RenditionFormat, p as serializeRecipe, r as Recipe, s as RenditionOptions, t as MAX_DIMENSION, u as formatContentTypes } from "./recipe-Je9DxqO1.mjs";
|
|
2
|
+
import { a as RenditionVariant, i as RenditionTarget, n as CatalogDefinition, o as defineCatalog, r as PresetDefinition, t as Catalog } from "./catalog-CSnosQ1L.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-BByyAbmB.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, RECIPE_ENGINE_VERSION, Recipe, RecipeInput, RenditionAddress, RenditionFit, RenditionFormat, RenditionOptions, RenditionPosition, RenditionTarget, RenditionVariant, SourcePolicy, URL_FORMAT_VERSION, assertBasePath, defineCatalog, formatContentTypes, formatExtensions, formatRenditionPath, normalizeRecipe, parseRenditionPath, serializeRecipe };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
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-DmOhQJWE.mjs";
|
|
2
|
+
import { a as parseRenditionPath, i as formatRenditionPath, n as URL_FORMAT_VERSION, o as RECIPE_HASH_LENGTH, r as assertBasePath, s as hashRecipe, t as DEFAULT_BASE_PATH } from "./url-BIvAw2BR.mjs";
|
|
3
|
+
import { n as isSafePath, r as isSafeScope, t as encodePath } from "./path-hNFUjCbA.mjs";
|
|
4
|
+
//#region src/catalog.ts
|
|
5
|
+
/**
|
|
6
|
+
* Validates a catalog and computes the recipe and hash of every preset density.
|
|
7
|
+
*
|
|
8
|
+
* Preset names are only labels: renaming a preset keeps its hashes and thus its stored renditions.
|
|
9
|
+
*
|
|
10
|
+
* @throws {RangeError} When the catalog is invalid.
|
|
11
|
+
*/
|
|
12
|
+
function defineCatalog(definition) {
|
|
13
|
+
const basePath = definition.basePath ?? "/_renditions";
|
|
14
|
+
assertBasePath(basePath);
|
|
15
|
+
const pipeline = definition.pipelineVersion ?? 1;
|
|
16
|
+
const presets = Object.freeze(Object.keys(definition.presets));
|
|
17
|
+
const variantsByPreset = /* @__PURE__ */ new Map();
|
|
18
|
+
const variantsByHash = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const preset of presets) {
|
|
20
|
+
if (!/^[\w-]{1,64}$/.test(preset)) throw new RangeError(`Invalid preset name "${preset}".`);
|
|
21
|
+
const { densities = [1], ...options } = {
|
|
22
|
+
...definition.defaults,
|
|
23
|
+
...withoutUndefined(definition.presets[preset])
|
|
24
|
+
};
|
|
25
|
+
if (densities.length === 0 || new Set(densities).size !== densities.length) throw new RangeError(`Preset "${preset}" must declare unique densities.`);
|
|
26
|
+
const variants = Object.freeze([...densities].sort((a, b) => a - b).map((density) => {
|
|
27
|
+
const recipe = wrapRangeError(`preset "${preset}"`, () => normalizeRecipe({
|
|
28
|
+
options,
|
|
29
|
+
density,
|
|
30
|
+
pipeline,
|
|
31
|
+
policy: definition.sources
|
|
32
|
+
}));
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
preset,
|
|
35
|
+
density,
|
|
36
|
+
label: density === 1 ? preset : `${preset}@${density}x`,
|
|
37
|
+
recipe,
|
|
38
|
+
hash: hashRecipe(recipe)
|
|
39
|
+
});
|
|
40
|
+
}));
|
|
41
|
+
variantsByPreset.set(preset, variants);
|
|
42
|
+
for (const variant of variants) if (!variantsByHash.has(variant.hash)) variantsByHash.set(variant.hash, variant);
|
|
43
|
+
}
|
|
44
|
+
function variants(preset) {
|
|
45
|
+
const found = variantsByPreset.get(preset);
|
|
46
|
+
if (!found) throw new RangeError(`Unknown rendition preset "${preset}".`);
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
function variant(preset, density = 1) {
|
|
50
|
+
const found = variants(preset).find((variant) => variant.density === density);
|
|
51
|
+
if (!found) throw new RangeError(`Preset "${preset}" has no density ${density}.`);
|
|
52
|
+
return found;
|
|
53
|
+
}
|
|
54
|
+
function variantUrl({ label, hash, recipe }, { scope, path }) {
|
|
55
|
+
return formatRenditionPath(basePath, {
|
|
56
|
+
scope,
|
|
57
|
+
label,
|
|
58
|
+
hash,
|
|
59
|
+
format: recipe.format,
|
|
60
|
+
path
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
basePath,
|
|
65
|
+
presets,
|
|
66
|
+
variants,
|
|
67
|
+
variant,
|
|
68
|
+
findVariant: (hash) => variantsByHash.get(hash),
|
|
69
|
+
url: (preset, target, density) => variantUrl(variant(preset, density), target),
|
|
70
|
+
srcset: (preset, target) => variants(preset).map((variant) => `${variantUrl(variant, target)} ${variant.density}x`).join(", ")
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function withoutUndefined(value) {
|
|
74
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
75
|
+
}
|
|
76
|
+
function wrapRangeError(context, fn) {
|
|
77
|
+
try {
|
|
78
|
+
return fn();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw error instanceof RangeError ? new RangeError(`Invalid ${context}: ${error.message}`, { cause: error }) : error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { DEFAULT_BASE_PATH, MAX_DIMENSION, RECIPE_ENGINE_VERSION, RECIPE_HASH_LENGTH, URL_FORMAT_VERSION, assertBasePath, defineCatalog, encodePath, formatContentTypes, formatExtensions, formatRenditionPath, hashRecipe, isSafePath, isSafeScope, normalizeRecipe, parseRenditionPath, serializeRecipe };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { t as Catalog } from "../catalog-CSnosQ1L.mjs";
|
|
2
|
+
import { PropType } from "vue";
|
|
3
|
+
//#region src/nuxt/runtime.d.ts
|
|
4
|
+
/** Attributes of an `img` element showing a rendition. */
|
|
5
|
+
export interface RenditionImgAttrs {
|
|
6
|
+
/** Absent for a path no rendition URL can address. */
|
|
7
|
+
src?: string;
|
|
8
|
+
srcset?: string;
|
|
9
|
+
width?: number;
|
|
10
|
+
height?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Options of a rendition URL. */
|
|
13
|
+
export interface RenditionUrlOptions {
|
|
14
|
+
/** Scope of the path; defaults to the module's `defaultScope`. */
|
|
15
|
+
scope?: string;
|
|
16
|
+
/** Density of the variant; defaults to `1`. */
|
|
17
|
+
density?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Binds `useRenditions()` and `<RenditionImg>` to a catalog.
|
|
21
|
+
*
|
|
22
|
+
* The Nuxt module calls it in a generated module; applications use the generated helpers.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createRenditionHelpers<Name extends string>(catalog: Catalog<Name>, defaultScope?: string): {
|
|
25
|
+
useRenditions: () => {
|
|
26
|
+
url: (preset: Name, path: string, { scope, density }?: RenditionUrlOptions) => string;
|
|
27
|
+
srcset: (preset: Name, path: string, { scope }?: Pick<RenditionUrlOptions, "scope">) => string;
|
|
28
|
+
attrs: (preset: Name, path: string, options?: Pick<RenditionUrlOptions, "scope">) => RenditionImgAttrs;
|
|
29
|
+
};
|
|
30
|
+
RenditionImg: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
31
|
+
preset: {
|
|
32
|
+
type: PropType<Name>;
|
|
33
|
+
required: true;
|
|
34
|
+
};
|
|
35
|
+
/** Logical source path inside the scope, or a URL shown as is. */
|
|
36
|
+
src: {
|
|
37
|
+
type: StringConstructor;
|
|
38
|
+
required: true;
|
|
39
|
+
};
|
|
40
|
+
scope: {
|
|
41
|
+
type: StringConstructor;
|
|
42
|
+
default: undefined;
|
|
43
|
+
};
|
|
44
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
45
|
+
[key: string]: any;
|
|
46
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
47
|
+
preset: {
|
|
48
|
+
type: PropType<Name>;
|
|
49
|
+
required: true;
|
|
50
|
+
};
|
|
51
|
+
/** Logical source path inside the scope, or a URL shown as is. */
|
|
52
|
+
src: {
|
|
53
|
+
type: StringConstructor;
|
|
54
|
+
required: true;
|
|
55
|
+
};
|
|
56
|
+
scope: {
|
|
57
|
+
type: StringConstructor;
|
|
58
|
+
default: undefined;
|
|
59
|
+
};
|
|
60
|
+
}>> & Readonly<{}>, {
|
|
61
|
+
scope: string;
|
|
62
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { n as isSafePath } from "../path-hNFUjCbA.mjs";
|
|
2
|
+
import { useRuntimeConfig } from "nuxt/app";
|
|
3
|
+
import { defineComponent, h } from "vue";
|
|
4
|
+
//#region src/nuxt/runtime.ts
|
|
5
|
+
/**
|
|
6
|
+
* Binds `useRenditions()` and `<RenditionImg>` to a catalog.
|
|
7
|
+
*
|
|
8
|
+
* The Nuxt module calls it in a generated module; applications use the generated helpers.
|
|
9
|
+
*/
|
|
10
|
+
function createRenditionHelpers(catalog, defaultScope) {
|
|
11
|
+
/** Builds rendition URLs below the application base URL. */
|
|
12
|
+
function useRenditions() {
|
|
13
|
+
const base = useRuntimeConfig().app.baseURL.replace(/\/$/, "");
|
|
14
|
+
function url(preset, path, { scope, density } = {}) {
|
|
15
|
+
return base + catalog.url(preset, {
|
|
16
|
+
scope: scope ?? defaultScope,
|
|
17
|
+
path
|
|
18
|
+
}, density);
|
|
19
|
+
}
|
|
20
|
+
function srcset(preset, path, { scope } = {}) {
|
|
21
|
+
return catalog.variants(preset).map(({ density }) => `${url(preset, path, {
|
|
22
|
+
scope,
|
|
23
|
+
density
|
|
24
|
+
})} ${density}x`).join(", ");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Attributes of an `img` element showing every density of a preset.
|
|
28
|
+
*
|
|
29
|
+
* `width` and `height` are set only when the output always has the size of the preset box, so they never distort the image.
|
|
30
|
+
* A source that is already a URL, such as a `blob:`, `data:` or `https:` one, is returned unchanged as `src`.
|
|
31
|
+
* A path no rendition URL can address, such as one with a `..` segment, yields no `src`, so stored data never breaks rendering.
|
|
32
|
+
*/
|
|
33
|
+
function attrs(preset, path, options = {}) {
|
|
34
|
+
if (/^(?:[a-z][\d+.a-z-]*:|\/\/)/i.test(path)) return { src: path };
|
|
35
|
+
if (!isSafePath(path)) return {};
|
|
36
|
+
const { recipe } = catalog.variant(preset);
|
|
37
|
+
const boxed = recipe.fit === "contain" || (recipe.fit === "cover" || recipe.fit === "fill") && recipe.enlarge;
|
|
38
|
+
return {
|
|
39
|
+
src: url(preset, path, options),
|
|
40
|
+
srcset: catalog.variants(preset).length > 1 ? srcset(preset, path, options) : void 0,
|
|
41
|
+
width: boxed ? recipe.width : void 0,
|
|
42
|
+
height: boxed ? recipe.height : void 0
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
url,
|
|
47
|
+
srcset,
|
|
48
|
+
attrs
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
useRenditions,
|
|
53
|
+
RenditionImg: defineComponent({
|
|
54
|
+
name: "RenditionImg",
|
|
55
|
+
props: {
|
|
56
|
+
preset: {
|
|
57
|
+
type: String,
|
|
58
|
+
required: true
|
|
59
|
+
},
|
|
60
|
+
/** Logical source path inside the scope, or a URL shown as is. */
|
|
61
|
+
src: {
|
|
62
|
+
type: String,
|
|
63
|
+
required: true
|
|
64
|
+
},
|
|
65
|
+
scope: {
|
|
66
|
+
type: String,
|
|
67
|
+
default: void 0
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
setup(props) {
|
|
71
|
+
const renditions = useRenditions();
|
|
72
|
+
return () => h("img", renditions.attrs(props.preset, props.src, { scope: props.scope }));
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { createRenditionHelpers };
|
package/dist/nuxt.d.mts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NuxtModule } from "nuxt/schema";
|
|
2
|
+
//#region src/nuxt/module.d.ts
|
|
3
|
+
/** Options of the `image-renditions/nuxt` module, under the `renditions` key of the Nuxt config. */
|
|
4
|
+
export interface ModuleOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Module whose default export is the catalog, such as `#shared/renditions`.
|
|
7
|
+
*
|
|
8
|
+
* It is bundled into both the client and the server, so it must import only the browser-safe `image-renditions` entry.
|
|
9
|
+
*/
|
|
10
|
+
catalog?: string;
|
|
11
|
+
/** Module whose default export is the h3 event handler of renditions, mounted at the catalog base path. */
|
|
12
|
+
handler?: string;
|
|
13
|
+
/** Scope for `<RenditionImg>` and `useRenditions()`; their `scope` overrides it. */
|
|
14
|
+
defaultScope?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Nuxt module that mounts the rendition handler and provides `useRenditions()` and `<RenditionImg>` typed by the catalog.
|
|
18
|
+
*/
|
|
19
|
+
declare const module: NuxtModule<ModuleOptions>;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { module as default };
|
package/dist/nuxt.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { r as isSafeScope } from "./path-hNFUjCbA.mjs";
|
|
2
|
+
import { createJiti } from "jiti";
|
|
3
|
+
import { addComponent, addImports, addServerHandler, addTemplate, defineNuxtModule, resolvePath } from "nuxt/kit";
|
|
4
|
+
//#region src/nuxt/module.ts
|
|
5
|
+
/**
|
|
6
|
+
* Nuxt module that mounts the rendition handler and provides `useRenditions()` and `<RenditionImg>` typed by the catalog.
|
|
7
|
+
*/
|
|
8
|
+
const module = defineNuxtModule({
|
|
9
|
+
meta: {
|
|
10
|
+
name: "image-renditions",
|
|
11
|
+
configKey: "renditions"
|
|
12
|
+
},
|
|
13
|
+
async setup({ catalog: catalogModule, handler, defaultScope }, nuxt) {
|
|
14
|
+
if (!catalogModule) throw new Error("[image-renditions] Set `renditions.catalog` to the module exporting the catalog.");
|
|
15
|
+
if (defaultScope !== void 0 && !isSafeScope(defaultScope)) throw new Error(`[image-renditions] Invalid default scope "${defaultScope}".`);
|
|
16
|
+
const catalogPath = await resolvePath(catalogModule);
|
|
17
|
+
const catalog = await createJiti(nuxt.options.rootDir, { alias: nuxt.options.alias }).import(catalogPath, { default: true });
|
|
18
|
+
if (handler) addServerHandler({
|
|
19
|
+
route: `${catalog.basePath}/**`,
|
|
20
|
+
handler: await resolvePath(handler)
|
|
21
|
+
});
|
|
22
|
+
const template = addTemplate({
|
|
23
|
+
filename: "image-renditions.ts",
|
|
24
|
+
write: true,
|
|
25
|
+
getContents: () => [
|
|
26
|
+
`import catalog from ${JSON.stringify(catalogPath.replace(/\.[cm]?[jt]s$/, ""))}`,
|
|
27
|
+
`import { createRenditionHelpers } from "image-renditions/nuxt/runtime"`,
|
|
28
|
+
"",
|
|
29
|
+
`export const { useRenditions, RenditionImg } = createRenditionHelpers(catalog, ${JSON.stringify(defaultScope)})`,
|
|
30
|
+
""
|
|
31
|
+
].join("\n")
|
|
32
|
+
});
|
|
33
|
+
addImports({
|
|
34
|
+
name: "useRenditions",
|
|
35
|
+
from: template.dst
|
|
36
|
+
});
|
|
37
|
+
addComponent({
|
|
38
|
+
name: "RenditionImg",
|
|
39
|
+
export: "RenditionImg",
|
|
40
|
+
filePath: template.dst
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
//#endregion
|
|
45
|
+
export { module as default };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/path.ts
|
|
2
|
+
/** Longest accepted logical path or storage key, in UTF-16 code units. */
|
|
3
|
+
const MAX_PATH_LENGTH = 1024;
|
|
4
|
+
/**
|
|
5
|
+
* Checks that a slash-separated path addresses exactly one storage object.
|
|
6
|
+
*
|
|
7
|
+
* Unsafe or ambiguous paths are rejected rather than normalized, so one object never gets several URLs:
|
|
8
|
+
* 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.
|
|
9
|
+
*/
|
|
10
|
+
function isSafePath(path) {
|
|
11
|
+
return path.length > 0 && path.length <= MAX_PATH_LENGTH && !/[\\\u0000]|\p{Cs}/u.test(path) && path.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
|
12
|
+
}
|
|
13
|
+
/** Percent-encodes each segment of a slash-separated path for use in a URL path. */
|
|
14
|
+
function encodePath(path) {
|
|
15
|
+
return path.split("/").map(encodeURIComponent).join("/");
|
|
16
|
+
}
|
|
17
|
+
/** Checks that a scope is a short token that needs no URL encoding. */
|
|
18
|
+
function isSafeScope(scope) {
|
|
19
|
+
return /^[\w-]{1,64}$/.test(scope);
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { isSafePath as n, isSafeScope as r, encodePath as t };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { r as Recipe } from "./recipe-Je9DxqO1.mjs";
|
|
2
|
+
import { t as EncodedImage } from "./stores-DFDRMZcC.mjs";
|
|
3
|
+
//#region src/server/processor.d.ts
|
|
4
|
+
/** Renders a source image according to a recipe. */
|
|
5
|
+
interface Processor {
|
|
6
|
+
/**
|
|
7
|
+
* Renders the source.
|
|
8
|
+
*
|
|
9
|
+
* Rejects with a `RenditionError` of reason `unsupported-source` when the source cannot be decoded.
|
|
10
|
+
*/
|
|
11
|
+
process(source: Uint8Array, recipe: Recipe): Promise<EncodedImage>;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { Processor as t };
|