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/dist/index.mjs ADDED
@@ -0,0 +1,123 @@
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 { 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-BAU5GYXC.mjs";
3
+ import { n as isSafePath, r as isSafeScope, t as encodePath } from "./path-hNFUjCbA.mjs";
4
+ import { t as assertDeclaredScope } from "./scopes-C1kzqQHz.mjs";
5
+ //#region src/catalog.ts
6
+ /** Extensions of sources that get animated variants, as a path does not tell an animated source from a still one. */
7
+ const ANIMATED_EXTENSIONS = ["gif", "webp"];
8
+ /**
9
+ * Validates a catalog and computes the recipe and hash of every preset density.
10
+ *
11
+ * Preset names are only labels: renaming a preset keeps its hashes and thus its stored renditions.
12
+ *
13
+ * @throws {RangeError} When the catalog is invalid.
14
+ */
15
+ function defineCatalog(definition) {
16
+ const basePath = definition.basePath ?? "/_renditions";
17
+ assertBasePath(basePath);
18
+ const scopes = definition.scopes && Object.freeze([...definition.scopes]);
19
+ if (scopes) {
20
+ if (scopes.length === 0) throw new RangeError("Catalog scopes must not be empty.");
21
+ for (const [index, scope] of scopes.entries()) {
22
+ if (!isSafeScope(scope)) throw new RangeError(`Invalid rendition scope "${scope}".`);
23
+ if (scopes.indexOf(scope) !== index) throw new RangeError(`Duplicate rendition scope "${scope}".`);
24
+ }
25
+ }
26
+ const pipeline = definition.pipelineVersion ?? 1;
27
+ const presets = Object.freeze(Object.keys(definition.presets));
28
+ const presetsByName = /* @__PURE__ */ new Map();
29
+ const variantsByHash = /* @__PURE__ */ new Map();
30
+ for (const preset of presets) {
31
+ if (!/^[\w-]{1,64}$/.test(preset)) throw new RangeError(`Invalid preset name "${preset}".`);
32
+ const { densities = [1], originals = [], animation = "first-frame", ...options } = {
33
+ ...definition.defaults,
34
+ ...omitUndefined(definition.presets[preset])
35
+ };
36
+ if (densities.length === 0 || new Set(densities).size !== densities.length) throw new RangeError(`Preset "${preset}" must declare unique densities.`);
37
+ if (!originals.every((extension) => /^[\da-z]+$/.test(extension))) throw new RangeError(`Preset "${preset}" must list original extensions in lowercase and without the dot.`);
38
+ const sortedDensities = [...densities].sort((a, b) => a - b);
39
+ const createVariants = (variantOptions, animation) => Object.freeze(sortedDensities.map((density) => {
40
+ const recipe = wrapRangeError(`preset "${preset}"`, () => normalizeRecipe({
41
+ options: variantOptions,
42
+ density,
43
+ pipeline,
44
+ policy: definition.sources,
45
+ animation
46
+ }));
47
+ return Object.freeze({
48
+ preset,
49
+ density,
50
+ label: density === 1 ? preset : `${preset}@${density}x`,
51
+ recipe,
52
+ hash: hashRecipe(recipe)
53
+ });
54
+ }));
55
+ const variants = createVariants(options, "first-frame");
56
+ const animatedVariants = animation === "first-frame" ? void 0 : createVariants(options.format === "webp" ? options : {
57
+ ...options,
58
+ format: "webp",
59
+ quality: void 0,
60
+ effort: void 0
61
+ }, animation);
62
+ presetsByName.set(preset, {
63
+ variants,
64
+ originals: Object.freeze([...originals]),
65
+ animatedVariants
66
+ });
67
+ for (const variant of [...variants, ...animatedVariants ?? []]) if (!variantsByHash.has(variant.hash)) variantsByHash.set(variant.hash, variant);
68
+ }
69
+ function getPreset(preset) {
70
+ const found = presetsByName.get(preset);
71
+ if (!found) throw new RangeError(`Unknown rendition preset "${preset}".`);
72
+ return found;
73
+ }
74
+ function variants(preset, path) {
75
+ const { variants, animatedVariants } = getPreset(preset);
76
+ return animatedVariants && path !== void 0 && hasExtension(path, ANIMATED_EXTENSIONS) ? animatedVariants : variants;
77
+ }
78
+ function variant(preset, density = 1, path) {
79
+ const found = variants(preset, path).find((variant) => variant.density === density);
80
+ if (!found) throw new RangeError(`Preset "${preset}" has no density ${density}.`);
81
+ return found;
82
+ }
83
+ function formatVariantUrl({ label, hash, recipe }, { scope, path }) {
84
+ assertDeclaredScope(scopes, scope);
85
+ return formatRenditionPath(basePath, {
86
+ scope,
87
+ label,
88
+ hash,
89
+ format: recipe.format,
90
+ path
91
+ });
92
+ }
93
+ return Object.freeze({
94
+ basePath,
95
+ presets,
96
+ scopes,
97
+ variants,
98
+ variant,
99
+ originals: (preset) => getPreset(preset).originals,
100
+ isOriginal: (preset, path) => hasExtension(path, getPreset(preset).originals),
101
+ allVariants: Object.freeze([...variantsByHash.values()]),
102
+ findVariant: (hash) => variantsByHash.get(hash),
103
+ url: (preset, target, density) => formatVariantUrl(variant(preset, density, target.path), target),
104
+ srcset: (preset, target) => variants(preset, target.path).map((variant) => `${formatVariantUrl(variant, target)} ${variant.density}x`).join(", ")
105
+ });
106
+ }
107
+ /** Whether the extension of a path is in a list of lowercase extensions. */
108
+ function hasExtension(path, extensions) {
109
+ const extension = /\.([^./]+)$/.exec(path)?.[1]?.toLowerCase();
110
+ return extension !== void 0 && extensions.includes(extension);
111
+ }
112
+ function omitUndefined(value) {
113
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
114
+ }
115
+ function wrapRangeError(context, fn) {
116
+ try {
117
+ return fn();
118
+ } catch (error) {
119
+ throw error instanceof RangeError ? new RangeError(`Invalid ${context}: ${error.message}`, { cause: error }) : error;
120
+ }
121
+ }
122
+ //#endregion
123
+ 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,90 @@
1
+ import { a as RenditionTarget, t as Catalog } from "../catalog-D11xkViN.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 an unsafe path. */
7
+ src?: string;
8
+ srcset?: string;
9
+ width?: number;
10
+ height?: number;
11
+ }
12
+ /**
13
+ * Attributes every `<RenditionImg>` renders, such as `{ loading: "lazy", decoding: "async" }`.
14
+ *
15
+ * Attributes of an element override them; `src`, `srcset`, `width` and `height` belong to the rendition alone.
16
+ */
17
+ export type DefaultImgAttrs = Record<string, string | number | boolean> & {
18
+ src?: never;
19
+ srcset?: never;
20
+ width?: never;
21
+ height?: never;
22
+ };
23
+ /** Options of a rendition URL. */
24
+ export interface RenditionUrlOptions<Scope extends string = string> {
25
+ /** Scope of the path; defaults to the module's `defaultScope`. */
26
+ scope?: Scope;
27
+ /** Density of the variant; defaults to `1`. */
28
+ density?: number;
29
+ }
30
+ /** Options of {@link createRenditionHelpers}. */
31
+ export interface RenditionHelpersOptions<Scope extends string = string> {
32
+ /** Scope of calls that set none. */
33
+ defaultScope?: Scope;
34
+ /**
35
+ * Composable returning a function that builds the URL of a source shown as it is; required when a preset lists `originals`.
36
+ *
37
+ * The function receives a safe logical path, which it must encode.
38
+ */
39
+ useOriginalUrl?: () => (target: RenditionTarget<Scope>) => string;
40
+ imgAttrs?: DefaultImgAttrs;
41
+ }
42
+ /**
43
+ * Binds `useRenditions()` and `<RenditionImg>` to a catalog.
44
+ *
45
+ * The Nuxt module calls it in a generated module; applications use the generated helpers.
46
+ *
47
+ * @throws {Error} When a preset lists `originals` and `useOriginalUrl` is missing.
48
+ * @throws {RangeError} When `defaultScope` is not one of the scopes the catalog declares, or `imgAttrs` sets a rendition attribute.
49
+ */
50
+ export declare function createRenditionHelpers<Name extends string, Scope extends string = string>(catalog: Catalog<Name, Scope>, { defaultScope, useOriginalUrl, imgAttrs }?: RenditionHelpersOptions<NoInfer<Scope>>): {
51
+ useRenditions: () => {
52
+ url: (preset: Name, path: string, { scope, density }?: RenditionUrlOptions<Scope>) => string | undefined;
53
+ srcset: (preset: Name, path: string, { scope }?: Pick<RenditionUrlOptions<Scope>, "scope">) => string | undefined;
54
+ attrs: (preset: Name, path: string, { scope }?: Pick<RenditionUrlOptions<Scope>, "scope">) => RenditionImgAttrs;
55
+ };
56
+ RenditionImg: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
57
+ preset: {
58
+ type: PropType<Name>;
59
+ required: true;
60
+ };
61
+ /** Logical source path inside the scope, or a URL shown as is. */
62
+ src: {
63
+ type: StringConstructor;
64
+ required: true;
65
+ };
66
+ scope: {
67
+ type: PropType<Scope>;
68
+ default: undefined;
69
+ };
70
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
71
+ [key: string]: any;
72
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
73
+ preset: {
74
+ type: PropType<Name>;
75
+ required: true;
76
+ };
77
+ /** Logical source path inside the scope, or a URL shown as is. */
78
+ src: {
79
+ type: StringConstructor;
80
+ required: true;
81
+ };
82
+ scope: {
83
+ type: PropType<Scope>;
84
+ default: undefined;
85
+ };
86
+ }>> & Readonly<{}>, {
87
+ scope: unknown extends Scope ? keyof Scope extends never ? undefined : Scope : Scope;
88
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
89
+ };
90
+ //#endregion
@@ -0,0 +1,129 @@
1
+ import { n as isSafePath, r as isSafeScope } from "../path-hNFUjCbA.mjs";
2
+ import { n as isDeclaredScope, r as toDeclaredTarget, t as assertDeclaredScope } from "../scopes-C1kzqQHz.mjs";
3
+ import { t as findRenditionAttr } from "../img-attrs-BoCAHpl5.mjs";
4
+ import { useRuntimeConfig } from "nuxt/app";
5
+ import { defineComponent, h } from "vue";
6
+ //#region src/nuxt/runtime.ts
7
+ /**
8
+ * Binds `useRenditions()` and `<RenditionImg>` to a catalog.
9
+ *
10
+ * The Nuxt module calls it in a generated module; applications use the generated helpers.
11
+ *
12
+ * @throws {Error} When a preset lists `originals` and `useOriginalUrl` is missing.
13
+ * @throws {RangeError} When `defaultScope` is not one of the scopes the catalog declares, or `imgAttrs` sets a rendition attribute.
14
+ */
15
+ function createRenditionHelpers(catalog, { defaultScope, useOriginalUrl, imgAttrs } = {}) {
16
+ if (!useOriginalUrl && catalog.presets.some((preset) => catalog.originals(preset).length > 0)) throw new Error("[image-renditions] Pass `useOriginalUrl`, as a preset of the catalog lists `originals`.");
17
+ if (defaultScope !== void 0 && !isDeclaredScope(catalog.scopes, defaultScope)) throw new RangeError(`Undeclared default rendition scope "${defaultScope}".`);
18
+ const renditionAttr = findRenditionAttr(imgAttrs);
19
+ if (renditionAttr) throw new RangeError(`Default image attribute "${renditionAttr}" is set by the rendition.`);
20
+ /** Builds rendition URLs below the application base URL. */
21
+ function useRenditions() {
22
+ const base = useRuntimeConfig().app.baseURL.replace(/\/$/, "");
23
+ const originalUrl = useOriginalUrl?.();
24
+ /**
25
+ * Target of a path in the scope of a call, or `undefined` for an unsafe path.
26
+ *
27
+ * The scope is checked here because the URL of an original does not go through the rendition URL builder, which checks it too.
28
+ */
29
+ function resolveTarget(path, scope) {
30
+ if (!isSafePath(path)) return;
31
+ const resolved = scope ?? defaultScope;
32
+ if (resolved !== void 0 && !isSafeScope(resolved)) throw new RangeError(`Invalid rendition scope "${resolved}".`);
33
+ assertDeclaredScope(catalog.scopes, resolved);
34
+ return toDeclaredTarget(catalog.scopes, resolved, path);
35
+ }
36
+ function formatUrl(preset, target, density) {
37
+ return base + catalog.url(preset, target, density);
38
+ }
39
+ function formatSrcset(preset, target) {
40
+ return catalog.variants(preset).map(({ density }) => `${formatUrl(preset, target, density)} ${density}x`).join(", ");
41
+ }
42
+ /**
43
+ * URL of a rendition of a logical path, or of the source itself at every declared density when the preset lists its extension in `originals`.
44
+ *
45
+ * Returns `undefined` for an unsafe path, such as one with a `..` segment, so stored data never breaks rendering.
46
+ *
47
+ * @throws {RangeError} When the preset is unknown or does not declare the density, or the scope is invalid or not one the catalog declares.
48
+ */
49
+ function url(preset, path, { scope, density } = {}) {
50
+ const target = resolveTarget(path, scope);
51
+ if (!target) return;
52
+ if (catalog.isOriginal(preset, path)) {
53
+ catalog.variant(preset, density);
54
+ return originalUrl(target);
55
+ }
56
+ return formatUrl(preset, target, density);
57
+ }
58
+ /**
59
+ * `srcset` attribute value listing every density of a preset.
60
+ *
61
+ * Returns `undefined` for an unsafe path, like `url()`, and for a source shown as it is, which has no densities.
62
+ *
63
+ * @throws {RangeError} When the preset is unknown, or the scope is invalid or not one the catalog declares.
64
+ */
65
+ function srcset(preset, path, { scope } = {}) {
66
+ const target = resolveTarget(path, scope);
67
+ return target && !catalog.isOriginal(preset, path) ? formatSrcset(preset, target) : void 0;
68
+ }
69
+ /**
70
+ * Attributes of an `img` element showing every density of a preset.
71
+ *
72
+ * `width` and `height` are set only when the output always has the size of the preset box, so they never distort the image.
73
+ * A source that is already a URL, such as a `blob:`, `data:` or `https:` one, is returned unchanged as `src`.
74
+ * A source whose extension the preset lists in `originals` gets only the `src` of the source itself.
75
+ * An unsafe path, such as one with a `..` segment, yields no `src`, so stored data never breaks rendering.
76
+ */
77
+ function attrs(preset, path, { scope } = {}) {
78
+ if (/^(?:[a-z][\d+.a-z-]*:|\/\/)/i.test(path)) return { src: path };
79
+ const target = resolveTarget(path, scope);
80
+ if (!target) return {};
81
+ if (catalog.isOriginal(preset, path)) return { src: originalUrl(target) };
82
+ const { recipe } = catalog.variant(preset);
83
+ const boxed = recipe.fit === "contain" || (recipe.fit === "cover" || recipe.fit === "fill") && recipe.enlarge;
84
+ return {
85
+ src: formatUrl(preset, target),
86
+ srcset: catalog.variants(preset).length > 1 ? formatSrcset(preset, target) : void 0,
87
+ width: boxed ? recipe.width : void 0,
88
+ height: boxed ? recipe.height : void 0
89
+ };
90
+ }
91
+ return {
92
+ url,
93
+ srcset,
94
+ attrs
95
+ };
96
+ }
97
+ return {
98
+ useRenditions,
99
+ RenditionImg: defineComponent({
100
+ name: "RenditionImg",
101
+ props: {
102
+ preset: {
103
+ type: String,
104
+ required: true
105
+ },
106
+ /** Logical source path inside the scope, or a URL shown as is. */
107
+ src: {
108
+ type: String,
109
+ required: true
110
+ },
111
+ scope: {
112
+ type: String,
113
+ default: void 0
114
+ }
115
+ },
116
+ setup(props) {
117
+ const renditions = useRenditions();
118
+ return () => {
119
+ return h("img", {
120
+ ...imgAttrs,
121
+ ...renditions.attrs(props.preset, props.src, { scope: props.scope })
122
+ });
123
+ };
124
+ }
125
+ })
126
+ };
127
+ }
128
+ //#endregion
129
+ export { createRenditionHelpers };
@@ -0,0 +1,31 @@
1
+ import { DefaultImgAttrs } from "./nuxt/runtime.mjs";
2
+ import { NuxtModule } from "nuxt/schema";
3
+ //#region src/nuxt/module.d.ts
4
+ /** Options of the `image-renditions/nuxt` module, under the `renditions` key of the Nuxt config. */
5
+ export interface ModuleOptions {
6
+ /**
7
+ * Module whose default export is the catalog, such as `#shared/renditions`.
8
+ *
9
+ * It is bundled into both the client and the server, so it must import only the browser-safe `image-renditions` entry.
10
+ */
11
+ catalog?: string;
12
+ /** Module whose default export is the h3 event handler of renditions, mounted at the catalog base path. */
13
+ handler?: string;
14
+ /** Scope for `<RenditionImg>` and `useRenditions()`, one of the catalog scopes if it declares them; their `scope` overrides it. */
15
+ defaultScope?: string;
16
+ /**
17
+ * Module whose default export is a composable returning a function that builds the URL of a source shown as it is, such as `~/renditions/original-url`.
18
+ *
19
+ * Required when a preset lists `originals`.
20
+ * The function receives the scope, after `defaultScope`, and a safe logical path, which it must encode.
21
+ */
22
+ originalUrl?: string;
23
+ /** Attributes every `<RenditionImg>` renders, such as `{ loading: "lazy", decoding: "async" }`; attributes of an element override them. */
24
+ imgAttrs?: DefaultImgAttrs;
25
+ }
26
+ /**
27
+ * Nuxt module that mounts the rendition handler and provides `useRenditions()` and `<RenditionImg>` typed by the catalog.
28
+ */
29
+ declare const module: NuxtModule<ModuleOptions>;
30
+ //#endregion
31
+ export { module as default };
package/dist/nuxt.mjs ADDED
@@ -0,0 +1,60 @@
1
+ import { r as isSafeScope } from "./path-hNFUjCbA.mjs";
2
+ import { n as isDeclaredScope } from "./scopes-C1kzqQHz.mjs";
3
+ import { t as findRenditionAttr } from "./img-attrs-BoCAHpl5.mjs";
4
+ import { createJiti } from "jiti";
5
+ import { addComponent, addImports, addServerHandler, addTemplate, defineNuxtModule, resolvePath } from "nuxt/kit";
6
+ //#region src/nuxt/module.ts
7
+ /**
8
+ * Nuxt module that mounts the rendition handler and provides `useRenditions()` and `<RenditionImg>` typed by the catalog.
9
+ */
10
+ const module = defineNuxtModule({
11
+ meta: {
12
+ name: "image-renditions",
13
+ configKey: "renditions"
14
+ },
15
+ async setup({ catalog: catalogModule, handler, defaultScope, originalUrl, imgAttrs }, nuxt) {
16
+ if (!catalogModule) throw new Error("[image-renditions] Set `renditions.catalog` to the module exporting the catalog.");
17
+ if (defaultScope !== void 0 && !isSafeScope(defaultScope)) throw new Error(`[image-renditions] Invalid default scope "${defaultScope}".`);
18
+ const renditionAttr = findRenditionAttr(imgAttrs);
19
+ if (renditionAttr) throw new Error(`[image-renditions] \`imgAttrs\` cannot set \`${renditionAttr}\`, which the rendition sets.`);
20
+ const catalogPath = await resolvePath(catalogModule);
21
+ const catalog = await createJiti(nuxt.options.rootDir, { alias: nuxt.options.alias }).import(catalogPath, { default: true });
22
+ if (defaultScope !== void 0 && !isDeclaredScope(catalog.scopes, defaultScope)) throw new Error(`[image-renditions] The default scope "${defaultScope}" is not one of the catalog scopes.`);
23
+ if (!originalUrl && catalog.presets.some((preset) => catalog.originals(preset).length > 0)) throw new Error("[image-renditions] Set `renditions.originalUrl` to the module building URLs of originals, as a preset of the catalog lists `originals`.");
24
+ const originalUrlPath = originalUrl && await resolvePath(originalUrl);
25
+ if (handler) addServerHandler({
26
+ route: `${catalog.basePath}/**`,
27
+ handler: await resolvePath(handler)
28
+ });
29
+ const template = addTemplate({
30
+ filename: "image-renditions.ts",
31
+ write: true,
32
+ getContents: () => [
33
+ `import catalog from ${formatImportSpecifier(catalogPath)}`,
34
+ ...originalUrlPath ? [`import useOriginalUrl from ${formatImportSpecifier(originalUrlPath)}`] : [],
35
+ `import { createRenditionHelpers } from "image-renditions/nuxt/runtime"`,
36
+ "",
37
+ `export const { useRenditions, RenditionImg } = createRenditionHelpers(catalog, {`,
38
+ ` defaultScope: ${JSON.stringify(defaultScope)},`,
39
+ ` useOriginalUrl: ${originalUrlPath ? "useOriginalUrl" : "undefined"},`,
40
+ ` imgAttrs: ${JSON.stringify(imgAttrs)},`,
41
+ `})`,
42
+ ""
43
+ ].join("\n")
44
+ });
45
+ addImports({
46
+ name: "useRenditions",
47
+ from: template.dst
48
+ });
49
+ addComponent({
50
+ name: "RenditionImg",
51
+ export: "RenditionImg",
52
+ filePath: template.dst
53
+ });
54
+ }
55
+ });
56
+ function formatImportSpecifier(path) {
57
+ return JSON.stringify(path.replace(/\.[cm]?[jt]s$/, ""));
58
+ }
59
+ //#endregion
60
+ 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,22 @@
1
+ import { r as Recipe } from "./recipe-BT8D60Qb.mjs";
2
+ import { t as EncodedImage } from "./stores-P-TUmax4.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 or processed, and of reason `source-too-large` when it exceeds a size limit.
10
+ */
11
+ process(source: Uint8Array, recipe: Recipe): Promise<EncodedImage>;
12
+ /**
13
+ * Checks that the processor renders a recipe as it describes.
14
+ *
15
+ * `createRenditionServer()` checks every variant of its catalog with it, and `ensure()` every recipe it gets.
16
+ *
17
+ * @throws {RangeError} When the processor cannot render the recipe, such as an animation in a format it cannot encode.
18
+ */
19
+ assertRecipe?(recipe: Recipe): void;
20
+ }
21
+ //#endregion
22
+ export { Processor as t };
@@ -0,0 +1,104 @@
1
+ //#region src/recipe.d.ts
2
+ /** Output encoding of a rendition. */
3
+ type RenditionFormat = "avif" | "webp" | "jpeg" | "png";
4
+ /**
5
+ * How a resized source fills a box of both `width` and `height`.
6
+ *
7
+ * `cover` crops what overflows, `contain` letterboxes, and `fill` stretches.
8
+ * `inside` and `outside` keep the aspect ratio, so one side of the output is smaller or larger than the box.
9
+ */
10
+ type RenditionFit = "cover" | "contain" | "fill" | "inside" | "outside";
11
+ /**
12
+ * Which part of the source stays in view when `cover` crops it or `contain` letterboxes it.
13
+ * `entropy` and `attention` choose the crop by content and apply to `cover` only.
14
+ */
15
+ type RenditionPosition = "center" | "top" | "bottom" | "left" | "right" | "entropy" | "attention";
16
+ /**
17
+ * How a rendition shows an animated source.
18
+ *
19
+ * `first-frame` renders its first frame as a still image, and `preserve` keeps the animation.
20
+ */
21
+ type RenditionAnimation = "first-frame" | "preserve";
22
+ /** How sources of a special kind are turned into a rendition. */
23
+ interface SourcePolicy {
24
+ /** `rasterize` renders vector sources at the resolution the output needs. */
25
+ svg: "rasterize";
26
+ /** `auto` applies the EXIF orientation before resizing; metadata never reaches the output. */
27
+ orientation: "auto";
28
+ }
29
+ /** Transformation and encoding parameters, as written in a preset or in catalog defaults. */
30
+ interface RenditionOptions {
31
+ /** Target width in CSS pixels at density 1. */
32
+ width?: number;
33
+ /** Target height in CSS pixels at density 1. */
34
+ height?: number;
35
+ /** Applies only when both `width` and `height` are set; defaults to `cover`. */
36
+ fit?: RenditionFit;
37
+ /** Applies only to `cover` and `contain`; defaults to `center`. */
38
+ position?: RenditionPosition;
39
+ /** Allows upscaling sources smaller than the target box. */
40
+ enlarge?: boolean;
41
+ /**
42
+ * Hex color `#rgb`, `#rgba`, `#rrggbb` or `#rrggbbaa` for `contain` letterboxing and for flattening transparency into JPEG.
43
+ * Letterboxing defaults to transparent and JPEG flattening to white.
44
+ */
45
+ background?: string;
46
+ /** Defaults to `avif`. */
47
+ format?: RenditionFormat;
48
+ /** Encoder quality from 1 to 100; ignored by `png`. */
49
+ quality?: number;
50
+ /** CPU effort of the `avif` (0–9) and `webp` (0–6) encoders. */
51
+ effort?: number;
52
+ }
53
+ /**
54
+ * Fully explicit, canonical description of one rendition.
55
+ *
56
+ * Fields that cannot affect the output are omitted and applicable defaults are filled in, so equivalent options give equal recipes.
57
+ */
58
+ interface Recipe {
59
+ /** Library-owned version of recipe semantics. */
60
+ readonly engine: number;
61
+ /** Application-owned version that changes every recipe at once. */
62
+ readonly pipeline: number;
63
+ readonly width?: number;
64
+ readonly height?: number;
65
+ readonly fit?: RenditionFit;
66
+ readonly position?: RenditionPosition;
67
+ readonly enlarge?: boolean;
68
+ readonly background?: string;
69
+ readonly format: RenditionFormat;
70
+ readonly quality?: number;
71
+ readonly effort?: number;
72
+ readonly svg: SourcePolicy["svg"];
73
+ readonly animation: RenditionAnimation;
74
+ readonly orientation: SourcePolicy["orientation"];
75
+ }
76
+ /** File extension of each output format, used in rendition URLs and storage keys. */
77
+ declare const formatExtensions: Record<RenditionFormat, string>;
78
+ /** MIME type of each output format. */
79
+ declare const formatContentTypes: Record<RenditionFormat, string>;
80
+ /** Library-owned recipe version; bumping it regenerates every rendition of every application. */
81
+ declare const RECIPE_ENGINE_VERSION = 1;
82
+ /** Upper bound of a rendition side in physical pixels; WebP cannot encode larger sides. */
83
+ declare const MAX_DIMENSION = 16383;
84
+ /** Input of {@link normalizeRecipe}. */
85
+ interface RecipeInput {
86
+ options: RenditionOptions;
87
+ /** Multiplier from CSS pixels to physical pixels. */
88
+ density?: number;
89
+ /** Application-owned pipeline version. */
90
+ pipeline: number;
91
+ policy?: Partial<SourcePolicy>;
92
+ /** Defaults to `first-frame`. */
93
+ animation?: RenditionAnimation;
94
+ }
95
+ /**
96
+ * Builds the canonical recipe for transformation options at a density.
97
+ *
98
+ * @throws {RangeError} When an option is out of range or inconsistent.
99
+ */
100
+ declare function normalizeRecipe({ options, density, pipeline, policy, animation }: RecipeInput): Recipe;
101
+ /** Serializes a recipe deterministically: sorted keys, no undefined fields, no whitespace. */
102
+ declare function serializeRecipe(recipe: Recipe): string;
103
+ //#endregion
104
+ export { RenditionAnimation as a, RenditionOptions as c, formatContentTypes as d, formatExtensions as f, RecipeInput as i, RenditionPosition as l, serializeRecipe as m, RECIPE_ENGINE_VERSION as n, RenditionFit as o, normalizeRecipe as p, Recipe as r, RenditionFormat as s, MAX_DIMENSION as t, SourcePolicy as u };