image-renditions 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,10 +33,11 @@ The library is modular, with pluggable parts:
33
33
 
34
34
  ## When not to use it
35
35
 
36
- - The URL must choose the size, such as `?w=300`.
37
- Here every size is a preset in your code.
38
- - Image processing must not share CPU with your app.
39
- Here images are processed inside the app process; a separate image service, such as imgproxy, or a hosted one, such as Cloudflare Images, takes that load elsewhere.
36
+ Use an image service such as [imgproxy](https://imgproxy.net) or [Thumbor](https://thumbor.readthedocs.io) when the URL must choose the size, such as `?w=300`.
37
+ Such a service takes the size and other options from the URL, and to keep clients from changing them it can require URLs signed with a secret key, which must stay out of browser code.
38
+
39
+ image-renditions renders only the presets of its catalog: a URL carries a hash of the preset options, and a request for any other hash renders nothing.
40
+ Its URLs need no secret, so they can be built in the browser too, but every size must be a preset in your code.
40
41
 
41
42
  ## Quick start
42
43
 
@@ -135,7 +136,7 @@ Options of a preset, or of `defaults`, which every preset overrides field by fie
135
136
  - `format` — `avif` (default), `webp`, `jpeg` or `png`.
136
137
  - `quality` — encoder quality from 1 to 100, by default 50 for `avif` and 80 for `webp` and `jpeg`; `png` is lossless.
137
138
  - `effort` — CPU effort of the `avif` (0–9) and `webp` (0–6) encoders, 4 by default; other formats ignore it.
138
- - `densities` — pixel densities to render, `[1]` by default.
139
+ - `densities` — pixel densities to render, which include `1`; `[1]` by default.
139
140
  - `animation` — `first-frame` (default) renders the first frame of animated sources, and `preserve` keeps the animation of GIF and WebP sources; see [Animation](#animation).
140
141
  - `originals` — extensions of sources shown as they are, so no option above applies to them; see [Originals](#originals).
141
142
 
@@ -167,6 +168,7 @@ renditions.url("cover", post.coverPath)
167
168
  renditions.url("avatar", path, { density: 2 })
168
169
  renditions.srcset("avatar", path)
169
170
  renditions.attrs("avatar", path) // src, srcset and size of <RenditionImg>, for your own img element
171
+ renditions.attrs("avatar", path, { size: false }) // src and srcset, for a component that sizes the image itself
170
172
  ```
171
173
 
172
174
  `url()` and `srcset()` return `undefined` for an unsafe path.
@@ -276,16 +278,17 @@ For such a path, `<RenditionImg>` and `useRenditions()` give the URL of the sour
276
278
  - Changing `originals` keeps the hashes of the preset.
277
279
 
278
280
  Only the app knows where its sources are served from, so it builds their URLs.
279
- Set the module option `originalUrl` to a module whose default export is a composable returning a function from the scope and the logical path to the URL:
281
+ Set the module option `originalUrl` to a module whose default export is a composable returning a function from the scope and the logical path to the URL.
282
+ `defineRenditionOriginalUrl()`, which the module auto-imports, types them by the catalog:
280
283
 
281
284
  ```ts
282
285
  // app/renditions/original-url.ts
283
- import { encodePath, type RenditionTarget } from "image-renditions"
286
+ import { encodePath } from "image-renditions"
284
287
 
285
- export default function useOriginalUrl() {
288
+ export default defineRenditionOriginalUrl(() => {
286
289
  const { uploadsUrl } = useRuntimeConfig().public
287
- return ({ path }: RenditionTarget) => `${uploadsUrl}/${encodePath(path)}`
288
- }
290
+ return ({ path }) => `${uploadsUrl}/${encodePath(path)}`
291
+ })
289
292
  ```
290
293
 
291
294
  ```ts
@@ -518,7 +521,7 @@ If `publicUrl` never changes, pass a longer `redirectCacheControl` to `defineRen
518
521
 
519
522
  ## Concurrency and timeouts
520
523
 
521
- Rendering takes the CPU and memory of the app process, so the `limits` option of `createRenditionServer()` bounds it per process:
524
+ The `limits` option of `createRenditionServer()` bounds rendering per process:
522
525
 
523
526
  - `concurrency` — renditions processed at once, 2 by default.
524
527
  - `queueSize` — renditions waiting for a slot, 100 by default.
@@ -2,7 +2,7 @@ import { a as RenditionAnimation, c as RenditionOptions, r as Recipe, u as Sourc
2
2
  //#region src/catalog.d.ts
3
3
  /** Named transformation in a catalog. */
4
4
  interface PresetDefinition extends RenditionOptions {
5
- /** Pixel densities to render, as multipliers of `width` and `height`; defaults to `[1]`. */
5
+ /** Pixel densities to render, as multipliers of `width` and `height`, which include `1`; defaults to `[1]`. */
6
6
  densities?: readonly number[];
7
7
  /**
8
8
  * Extensions of source files shown as they are instead of rendered, such as `svg`, in lowercase and without the dot.
package/dist/h3.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as RenditionHandlerOptions } from "./handler-P49xsp3S.mjs";
1
+ import { r as RenditionHandlerOptions } from "./handler-BhkqhlYb.mjs";
2
2
  import { EventHandler, H3Event } from "h3";
3
3
  //#region src/h3.d.ts
4
4
  /** Source of the application context of a request. */
package/dist/h3.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createRenditionHandler } from "./handler-D5toFJ55.mjs";
1
+ import { t as createRenditionHandler } from "./handler-BZx2-0oV.mjs";
2
2
  import { defineEventHandler } from "h3";
3
3
  //#region src/h3.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
1
  import { r as toDeclaredTarget } from "./scopes-C1kzqQHz.mjs";
2
- import { i as parseRenditionPath } from "./url-C6PtYIEf.mjs";
2
+ import { s as parseRenditionPath } from "./url-ookAu7xz.mjs";
3
3
  import { t as RenditionError } from "./error-DViVpVmH.mjs";
4
4
  //#region src/server/handler.ts
5
5
  const failureStatus = {
@@ -1,5 +1,5 @@
1
1
  import { r as Recipe, s as RenditionFormat } from "./recipe-BT8D60Qb.mjs";
2
- import { a as RenditionTarget, t as Catalog } from "./catalog-D11xkViN.mjs";
2
+ import { a as RenditionTarget, t as Catalog } from "./catalog-6wdmwTO7.mjs";
3
3
  import { n as RenditionAddress } from "./url-DoJdUx9J.mjs";
4
4
  import { i as StoreCallOptions, n as ResultStore, r as SourceStore } from "./stores-P-TUmax4.mjs";
5
5
  import { r as Processor, t as Degradation } from "./processor-Dd3jqaOW.mjs";
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as RenditionAnimation, c as RenditionOptions, d as formatContentTypes, f as formatExtensions, i as RecipeInput, l as RenditionPosition, m as serializeRecipe, n as RECIPE_ENGINE_VERSION, o as RenditionFit, p as normalizeRecipe, r as Recipe, s as RenditionFormat, t as MAX_DIMENSION, u as SourcePolicy } from "./recipe-BT8D60Qb.mjs";
2
- 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";
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-6wdmwTO7.mjs";
3
3
  import { i as parseRenditionPath, n as RenditionAddress, r as formatRenditionPath, t as DEFAULT_BASE_PATH } from "./url-DoJdUx9J.mjs";
4
4
  //#region src/hash.d.ts
5
5
  /** Identifies a recipe by the truncated SHA-256 of its canonical serialization. */
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { a as normalizeRecipe, i as formatExtensions, n as RECIPE_ENGINE_VERSION
2
2
  import { t as hashRecipe } from "./hash-CHipV1N6.mjs";
3
3
  import { n as isSafePath, r as isSafeScope, t as encodePath } from "./path-hNFUjCbA.mjs";
4
4
  import { t as assertDeclaredScope } from "./scopes-C1kzqQHz.mjs";
5
- import { i as parseRenditionPath, n as assertBasePath, r as formatRenditionPath, t as DEFAULT_BASE_PATH } from "./url-C6PtYIEf.mjs";
5
+ import { a as isSafeLabel, i as formatRenditionPath, n as assertBasePath, o as joinRenditionPath, r as assertSafeTarget, s as parseRenditionPath, t as DEFAULT_BASE_PATH } from "./url-ookAu7xz.mjs";
6
6
  //#region src/catalog.ts
7
7
  /** Extensions of sources that get animated variants, as a path does not tell an animated source from a still one. */
8
8
  const ANIMATED_EXTENSIONS = ["gif", "webp"];
@@ -35,6 +35,7 @@ function defineCatalog(definition) {
35
35
  ...omitUndefined(definition.presets[preset])
36
36
  };
37
37
  if (densities.length === 0 || new Set(densities).size !== densities.length) throw new RangeError(`Preset "${preset}" must declare unique densities.`);
38
+ if (!densities.includes(1)) throw new RangeError(`Preset "${preset}" must include density 1.`);
38
39
  if (!originals.every((extension) => /^[\da-z]+$/.test(extension))) throw new RangeError(`Preset "${preset}" must list original extensions in lowercase and without the dot.`);
39
40
  const sortedDensities = [...densities].sort((a, b) => a - b);
40
41
  const createVariants = (variantOptions, animation) => Object.freeze(sortedDensities.map((density) => {
@@ -45,10 +46,12 @@ function defineCatalog(definition) {
45
46
  policy: definition.sources,
46
47
  animation
47
48
  }));
49
+ const label = density === 1 ? preset : `${preset}@${density}x`;
50
+ if (!isSafeLabel(label)) throw new RangeError(`Invalid variant label "${label}" of preset "${preset}".`);
48
51
  return Object.freeze({
49
52
  preset,
50
53
  density,
51
- label: density === 1 ? preset : `${preset}@${density}x`,
54
+ label,
52
55
  recipe,
53
56
  hash: hashRecipe(recipe)
54
57
  });
@@ -85,15 +88,24 @@ function defineCatalog(definition) {
85
88
  if (!found) throw new RangeError(`Preset "${preset}" has no density ${density}.`);
86
89
  return found;
87
90
  }
88
- function formatVariantUrl({ label, hash, recipe }, { scope, path }) {
91
+ /** Checks a target and encodes its path, once for all the variant URLs of a call. */
92
+ function encodeTarget({ scope, path }) {
89
93
  assertDeclaredScope(scopes, scope);
90
- return formatRenditionPath(basePath, {
94
+ assertSafeTarget(scope, path);
95
+ return encodePath(path);
96
+ }
97
+ /**
98
+ * URL of a variant for a target that `encodeTarget()` has checked and encoded.
99
+ *
100
+ * The base path and the labels are checked when the catalog is defined, and hashes are always safe.
101
+ */
102
+ function joinVariantUrl({ label, hash, recipe }, scope, encodedPath) {
103
+ return joinRenditionPath(basePath, {
91
104
  scope,
92
105
  label,
93
106
  hash,
94
- format: recipe.format,
95
- path
96
- });
107
+ format: recipe.format
108
+ }, encodedPath);
97
109
  }
98
110
  return Object.freeze({
99
111
  basePath,
@@ -105,8 +117,12 @@ function defineCatalog(definition) {
105
117
  isOriginal: (preset, path) => hasExtension(path, getPreset(preset).originals),
106
118
  allVariants: Object.freeze([...variantsByHash.values()]),
107
119
  findVariant: (hash) => variantsByHash.get(hash),
108
- url: (preset, target, density) => formatVariantUrl(variant(preset, density, target.path), target),
109
- srcset: (preset, target) => variants(preset, target.path).map((variant) => `${formatVariantUrl(variant, target)} ${variant.density}x`).join(", ")
120
+ url: (preset, target, density) => joinVariantUrl(variant(preset, density, target.path), target.scope, encodeTarget(target)),
121
+ srcset: (preset, target) => {
122
+ const found = variants(preset, target.path);
123
+ const encodedPath = encodeTarget(target);
124
+ return found.map((entry) => `${joinVariantUrl(entry, target.scope, encodedPath)} ${entry.density}x`).join(", ");
125
+ }
110
126
  });
111
127
  }
112
128
  /** Whether the extension of a path is in a list of lowercase extensions. */
@@ -1,4 +1,4 @@
1
- import { a as RenditionTarget, t as Catalog } from "../catalog-D11xkViN.mjs";
1
+ import { a as RenditionTarget, s as ScopeName, t as Catalog } from "../catalog-6wdmwTO7.mjs";
2
2
  import { PropType } from "vue";
3
3
  //#region src/nuxt/runtime.d.ts
4
4
  /** Attributes of an `img` element showing a rendition. */
@@ -27,6 +27,29 @@ export interface RenditionUrlOptions<Scope extends string = string> {
27
27
  /** Density of the variant; defaults to `1`. */
28
28
  density?: number;
29
29
  }
30
+ /** Options of the `img` attributes of a rendition. */
31
+ export interface RenditionAttrsOptions<Scope extends string = string> {
32
+ /** Scope of the path; defaults to the module's `defaultScope`. */
33
+ scope?: Scope;
34
+ /** Whether to set `width` and `height`; `false` leaves the size to a component that sizes the image itself. */
35
+ size?: boolean;
36
+ }
37
+ /**
38
+ * Registry of the catalog the Nuxt module serves, which the module fills with the type of its catalog.
39
+ *
40
+ * It types helpers defined in files of the application, such as {@link defineRenditionOriginalUrl}.
41
+ */
42
+ export interface RenditionRegister {}
43
+ /** Scope of the catalog in {@link RenditionRegister}, or any string without one. */
44
+ type RegisteredScope = RenditionRegister extends {
45
+ catalog: infer C extends Pick<Catalog, "scopes">;
46
+ } ? ScopeName<C> : string;
47
+ /**
48
+ * Defines the composable of the `originalUrl` module option, typing its targets by the catalog of the Nuxt module.
49
+ *
50
+ * The Nuxt module auto-imports it.
51
+ */
52
+ export declare function defineRenditionOriginalUrl(useOriginalUrl: () => (target: RenditionTarget<RegisteredScope>) => string): () => (target: RenditionTarget<RegisteredScope>) => string;
30
53
  /** Options of {@link createRenditionHelpers}. */
31
54
  export interface RenditionHelpersOptions<Scope extends string = string> {
32
55
  /** Scope of calls that set none. */
@@ -51,7 +74,7 @@ export declare function createRenditionHelpers<Name extends string, Scope extend
51
74
  useRenditions: () => {
52
75
  url: (preset: Name, path: string, { scope, density }?: RenditionUrlOptions<Scope>) => string | undefined;
53
76
  srcset: (preset: Name, path: string, { scope }?: Pick<RenditionUrlOptions<Scope>, "scope">) => string | undefined;
54
- attrs: (preset: Name, path: string, { scope }?: Pick<RenditionUrlOptions<Scope>, "scope">) => RenditionImgAttrs;
77
+ attrs: (preset: Name, path: string, { scope, size }?: RenditionAttrsOptions<Scope>) => RenditionImgAttrs;
55
78
  };
56
79
  RenditionImg: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
57
80
  preset: {
@@ -87,4 +110,5 @@ export declare function createRenditionHelpers<Name extends string, Scope extend
87
110
  scope: unknown extends Scope ? keyof Scope extends never ? undefined : Scope : Scope;
88
111
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
89
112
  };
90
- //#endregion
113
+ //#endregion
114
+ export {};
@@ -1,10 +1,19 @@
1
- import { n as isSafePath, r as isSafeScope } from "../path-hNFUjCbA.mjs";
1
+ import { n as isSafePath, r as isSafeScope, t as encodePath } from "../path-hNFUjCbA.mjs";
2
2
  import { n as isDeclaredScope, r as toDeclaredTarget, t as assertDeclaredScope } from "../scopes-C1kzqQHz.mjs";
3
+ import { o as joinRenditionPath } from "../url-ookAu7xz.mjs";
3
4
  import { t as findRenditionAttr } from "../img-attrs-BoCAHpl5.mjs";
4
5
  import { useRuntimeConfig } from "nuxt/app";
5
6
  import { defineComponent, h } from "vue";
6
7
  //#region src/nuxt/runtime.ts
7
8
  /**
9
+ * Defines the composable of the `originalUrl` module option, typing its targets by the catalog of the Nuxt module.
10
+ *
11
+ * The Nuxt module auto-imports it.
12
+ */
13
+ function defineRenditionOriginalUrl(useOriginalUrl) {
14
+ return useOriginalUrl;
15
+ }
16
+ /**
8
17
  * Binds `useRenditions()` and `<RenditionImg>` to a catalog.
9
18
  *
10
19
  * The Nuxt module calls it in a generated module; applications use the generated helpers.
@@ -24,20 +33,37 @@ function createRenditionHelpers(catalog, { defaultScope, useOriginalUrl, imgAttr
24
33
  /**
25
34
  * Target of a path in the scope of a call, or `undefined` for an unsafe path.
26
35
  *
27
- * The scope is checked here because the URL of an original does not go through the rendition URL builder, which checks it too.
36
+ * The path and the scope are checked here once, since the URLs below are joined from them or built by `useOriginalUrl` rather than by `catalog.url()`, which would check them.
28
37
  */
29
38
  function resolveTarget(path, scope) {
30
39
  if (!isSafePath(path)) return;
31
40
  const resolved = scope ?? defaultScope;
32
41
  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);
42
+ const target = toDeclaredTarget(catalog.scopes, resolved, path);
43
+ if (!target) assertDeclaredScope(catalog.scopes, resolved);
44
+ return target;
35
45
  }
36
- function formatUrl(preset, target, density) {
37
- return base + catalog.url(preset, target, density);
46
+ /**
47
+ * URL of a variant below the application base URL, for a target with an encoded path.
48
+ *
49
+ * `resolveTarget()` has checked the target and the catalog its variants, so the URL is joined without checks.
50
+ */
51
+ function joinVariantUrl({ label, hash, recipe }, scope, encodedPath) {
52
+ return base + joinRenditionPath(catalog.basePath, {
53
+ scope,
54
+ label,
55
+ hash,
56
+ format: recipe.format
57
+ }, encodedPath);
58
+ }
59
+ /** URLs of variants of a target, which encode its path once for all of them. */
60
+ function formatVariantUrls(variants, { scope, path }) {
61
+ const encodedPath = encodePath(path);
62
+ return variants.map((variant) => joinVariantUrl(variant, scope, encodedPath));
38
63
  }
39
- function formatSrcset(preset, target) {
40
- return catalog.variants(preset).map(({ density }) => `${formatUrl(preset, target, density)} ${density}x`).join(", ");
64
+ /** `srcset` value of variants and their URLs. */
65
+ function formatSrcset(variants, urls) {
66
+ return variants.map(({ density }, index) => `${urls[index]} ${density}x`).join(", ");
41
67
  }
42
68
  /**
43
69
  * 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`.
@@ -49,11 +75,8 @@ function createRenditionHelpers(catalog, { defaultScope, useOriginalUrl, imgAttr
49
75
  function url(preset, path, { scope, density } = {}) {
50
76
  const target = resolveTarget(path, scope);
51
77
  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);
78
+ const variant = catalog.variant(preset, density, path);
79
+ return catalog.isOriginal(preset, path) ? originalUrl(target) : joinVariantUrl(variant, target.scope, encodePath(path));
57
80
  }
58
81
  /**
59
82
  * `srcset` attribute value listing every density of a preset.
@@ -64,28 +87,37 @@ function createRenditionHelpers(catalog, { defaultScope, useOriginalUrl, imgAttr
64
87
  */
65
88
  function srcset(preset, path, { scope } = {}) {
66
89
  const target = resolveTarget(path, scope);
67
- return target && !catalog.isOriginal(preset, path) ? formatSrcset(preset, target) : void 0;
90
+ if (!target || catalog.isOriginal(preset, path)) return;
91
+ const variants = catalog.variants(preset, path);
92
+ return formatSrcset(variants, formatVariantUrls(variants, target));
68
93
  }
69
94
  /**
70
95
  * Attributes of an `img` element showing every density of a preset.
71
96
  *
72
- * `width` and `height` are set only when the output always has the size of the preset box, so they never distort the image.
97
+ * `width` and `height` are set only when the output always has the size of the preset box, so they never distort the image, and `size: false` leaves them out.
73
98
  * A source that is already a URL, such as a `blob:`, `data:` or `https:` one, is returned unchanged as `src`.
74
99
  * A source whose extension the preset lists in `originals` gets only the `src` of the source itself.
75
100
  * An unsafe path, such as one with a `..` segment, yields no `src`, so stored data never breaks rendering.
101
+ *
102
+ * @throws {RangeError} When the preset is unknown, or the scope is invalid or not one the catalog declares.
76
103
  */
77
- function attrs(preset, path, { scope } = {}) {
104
+ function attrs(preset, path, { scope, size = true } = {}) {
78
105
  if (/^(?:[a-z][\d+.a-z-]*:|\/\/)/i.test(path)) return { src: path };
79
106
  const target = resolveTarget(path, scope);
80
107
  if (!target) return {};
81
108
  if (catalog.isOriginal(preset, path)) return { src: originalUrl(target) };
82
- const { recipe } = catalog.variant(preset);
109
+ const variants = catalog.variants(preset, path);
110
+ const main = variants.findIndex(({ density }) => density === 1);
111
+ const { recipe } = variants[main];
112
+ const urls = formatVariantUrls(variants, target);
83
113
  const boxed = recipe.fit === "contain" || (recipe.fit === "cover" || recipe.fit === "fill") && recipe.enlarge;
84
114
  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
115
+ src: urls[main],
116
+ ...variants.length > 1 ? { srcset: formatSrcset(variants, urls) } : void 0,
117
+ ...size && boxed ? {
118
+ width: recipe.width,
119
+ height: recipe.height
120
+ } : void 0
89
121
  };
90
122
  }
91
123
  return {
@@ -126,4 +158,4 @@ function createRenditionHelpers(catalog, { defaultScope, useOriginalUrl, imgAttr
126
158
  };
127
159
  }
128
160
  //#endregion
129
- export { createRenditionHelpers };
161
+ export { createRenditionHelpers, defineRenditionOriginalUrl };
package/dist/nuxt.mjs CHANGED
@@ -2,7 +2,7 @@ import { r as isSafeScope } from "./path-hNFUjCbA.mjs";
2
2
  import { n as isDeclaredScope } from "./scopes-C1kzqQHz.mjs";
3
3
  import { t as findRenditionAttr } from "./img-attrs-BoCAHpl5.mjs";
4
4
  import { createJiti } from "jiti";
5
- import { addComponent, addImports, addServerHandler, addTemplate, defineNuxtModule, resolvePath } from "nuxt/kit";
5
+ import { addComponent, addImports, addServerHandler, addTemplate, addTypeTemplate, defineNuxtModule, resolvePath } from "nuxt/kit";
6
6
  //#region src/nuxt/module.ts
7
7
  /**
8
8
  * Nuxt module that mounts the rendition handler and provides `useRenditions()` and `<RenditionImg>` typed by the catalog.
@@ -46,6 +46,28 @@ const module = defineNuxtModule({
46
46
  name: "useRenditions",
47
47
  from: template.dst
48
48
  });
49
+ addTypeTemplate({
50
+ filename: "types/image-renditions.d.ts",
51
+ getContents: () => [
52
+ `declare module "image-renditions/nuxt/runtime" {`,
53
+ ` interface RenditionRegister {`,
54
+ ` catalog: typeof import(${formatImportSpecifier(catalogPath)}).default`,
55
+ ` }`,
56
+ `}`,
57
+ "",
58
+ "export {}",
59
+ ""
60
+ ].join("\n")
61
+ });
62
+ const defineTemplate = addTemplate({
63
+ filename: "image-renditions-define.ts",
64
+ write: true,
65
+ getContents: () => `export { defineRenditionOriginalUrl } from "image-renditions/nuxt/runtime"\n`
66
+ });
67
+ addImports({
68
+ name: "defineRenditionOriginalUrl",
69
+ from: defineTemplate.dst
70
+ });
49
71
  addComponent({
50
72
  name: "RenditionImg",
51
73
  export: "RenditionImg",
package/dist/server.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as RenditionError, a as createRenditionHandler, c as RenditionServer, d as createRenditionServer, f as MemoryReadinessIndexOptions, g as createLocalCoordinator, h as Coordinator, i as RenditionRequest, l as RenditionServerOptions, m as createMemoryReadinessIndex, n as RenditionHandler, o as Rendition, p as ReadinessIndex, r as RenditionHandlerOptions, s as RenditionDegradation, t as RenditionFailure, u as SourceResolution, v as RenditionFailureReason } from "./handler-P49xsp3S.mjs";
1
+ import { _ as RenditionError, a as createRenditionHandler, c as RenditionServer, d as createRenditionServer, f as MemoryReadinessIndexOptions, g as createLocalCoordinator, h as Coordinator, i as RenditionRequest, l as RenditionServerOptions, m as createMemoryReadinessIndex, n as RenditionHandler, o as Rendition, p as ReadinessIndex, r as RenditionHandlerOptions, s as RenditionDegradation, t as RenditionFailure, u as SourceResolution, v as RenditionFailureReason } from "./handler-BhkqhlYb.mjs";
2
2
  import { i as StoreCallOptions, n as ResultStore, r as SourceStore, t as EncodedImage } from "./stores-P-TUmax4.mjs";
3
3
  import { n as ProcessedImage, r as Processor, t as Degradation } from "./processor-Dd3jqaOW.mjs";
4
4
  export { Coordinator, Degradation, EncodedImage, MemoryReadinessIndexOptions, ProcessedImage, Processor, ReadinessIndex, Rendition, RenditionDegradation, RenditionError, RenditionFailure, RenditionFailureReason, RenditionHandler, RenditionHandlerOptions, RenditionRequest, RenditionServer, RenditionServerOptions, ResultStore, SourceResolution, SourceStore, StoreCallOptions, createLocalCoordinator, createMemoryReadinessIndex, createRenditionHandler, createRenditionServer };
package/dist/server.mjs CHANGED
@@ -3,7 +3,7 @@ import { t as hashRecipe } from "./hash-CHipV1N6.mjs";
3
3
  import { n as isSafePath } from "./path-hNFUjCbA.mjs";
4
4
  import { n as isDeclaredScope } from "./scopes-C1kzqQHz.mjs";
5
5
  import { t as RenditionError } from "./error-DViVpVmH.mjs";
6
- import { t as createRenditionHandler } from "./handler-D5toFJ55.mjs";
6
+ import { t as createRenditionHandler } from "./handler-BZx2-0oV.mjs";
7
7
  //#region src/server/coordinator.ts
8
8
  /** Creates a coordinator that merges concurrent equal requests within the process. */
9
9
  function createLocalCoordinator() {
@@ -12,12 +12,33 @@ const DEFAULT_BASE_PATH = "/_renditions";
12
12
  */
13
13
  function formatRenditionPath(basePath, { scope, label, hash, format, path }) {
14
14
  assertBasePath(basePath);
15
- if (scope !== void 0 && !isSafeScope(scope)) throw new RangeError(`Invalid rendition scope "${scope}".`);
15
+ assertSafeTarget(scope, path);
16
16
  const extension = formatExtensions[format];
17
17
  if (!isSafeLabel(label) || !isHash(hash) || !extension) throw new RangeError(`Invalid rendition variant "${label}.${hash}.${extension ?? format}".`);
18
+ return joinRenditionPath(basePath, {
19
+ scope,
20
+ label,
21
+ hash,
22
+ format
23
+ }, encodePath(path));
24
+ }
25
+ /**
26
+ * Checks that a scope and a source path are safe to put into a rendition URL.
27
+ *
28
+ * @throws {RangeError} When the scope or the path is unsafe.
29
+ */
30
+ function assertSafeTarget(scope, path) {
31
+ if (scope !== void 0 && !isSafeScope(scope)) throw new RangeError(`Invalid rendition scope "${scope}".`);
18
32
  if (!isSafePath(path)) throw new RangeError(`Unsafe rendition source path "${path}".`);
33
+ }
34
+ /**
35
+ * Joins the parts of a rendition URL path without checking them, with the source path already encoded.
36
+ *
37
+ * It serves parts checked once for several URLs, such as the variants of a catalog for one target.
38
+ */
39
+ function joinRenditionPath(basePath, { scope, label, hash, format }, encodedPath) {
19
40
  const scopePrefix = scope === void 0 ? "" : `${scope}/`;
20
- return `${basePath}/${URL_FORMAT_VERSION}/${scopePrefix}${label}.${hash}.${extension}/${encodePath(path)}`;
41
+ return `${basePath}/${URL_FORMAT_VERSION}/${scopePrefix}${label}.${hash}.${formatExtensions[format]}/${encodedPath}`;
21
42
  }
22
43
  /**
23
44
  * Parses the path of a rendition URL, ignoring any query string.
@@ -55,6 +76,7 @@ function assertBasePath(basePath) {
55
76
  if (!/^(?:\/[\w.~-]+)+$/.test(basePath)) throw new RangeError(`Invalid rendition base path "${basePath}".`);
56
77
  }
57
78
  const formats = Object.keys(formatExtensions);
79
+ /** Checks that a variant label is 1 to 64 characters that need no encoding in a URL. */
58
80
  function isSafeLabel(label) {
59
81
  return /^[\w@.-]{1,64}$/.test(label);
60
82
  }
@@ -74,4 +96,4 @@ function decodeSegment(segment) {
74
96
  }
75
97
  }
76
98
  //#endregion
77
- export { parseRenditionPath as i, assertBasePath as n, formatRenditionPath as r, DEFAULT_BASE_PATH as t };
99
+ export { isSafeLabel as a, formatRenditionPath as i, assertBasePath as n, joinRenditionPath as o, assertSafeTarget as r, parseRenditionPath as s, DEFAULT_BASE_PATH as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-renditions",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "On-demand image transformations that persist every result to durable storage and keep app servers stateless.",
5
5
  "homepage": "https://github.com/IlyaSemenov/image-renditions#readme",
6
6
  "bugs": "https://github.com/IlyaSemenov/image-renditions/issues",