@webiny/website-builder-sdk 0.0.0-unstable.7be00a75a9 → 0.0.0-unstable.7c2def32a7
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/BindingsResolver.d.ts +4 -1
- package/BindingsResolver.js.map +1 -1
- package/ComponentInputTraverser.js +4 -0
- package/ComponentInputTraverser.js.map +1 -1
- package/ComponentResolver.d.ts +6 -3
- package/ComponentResolver.js +9 -4
- package/ComponentResolver.js.map +1 -1
- package/ContentSdk.d.ts +67 -1
- package/ContentSdk.js +90 -8
- package/ContentSdk.js.map +1 -1
- package/DocumentStoreManager.d.ts +9 -2
- package/DocumentStoreManager.js +24 -5
- package/DocumentStoreManager.js.map +1 -1
- package/EditingSdk.d.ts +7 -1
- package/EditingSdk.js +5 -3
- package/EditingSdk.js.map +1 -1
- package/ElementFactory.js +7 -2
- package/ElementFactory.js.map +1 -1
- package/ElementFactory.test.js +102 -0
- package/ElementFactory.test.js.map +1 -1
- package/LiveSdk.d.ts +3 -1
- package/LiveSdk.js +3 -2
- package/LiveSdk.js.map +1 -1
- package/PreviewDocument.d.ts +3 -1
- package/PreviewDocument.js +5 -2
- package/PreviewDocument.js.map +1 -1
- package/PreviewSdk.d.ts +4 -1
- package/PreviewSdk.js +6 -2
- package/PreviewSdk.js.map +1 -1
- package/ResizeObserver.js +1 -1
- package/ResizeObserver.js.map +1 -1
- package/StylesBindingsProcessor.js +3 -2
- package/StylesBindingsProcessor.js.map +1 -1
- package/Theme.js +2 -1
- package/Theme.js.map +1 -1
- package/asset/deliveryUrl.d.ts +41 -0
- package/asset/deliveryUrl.js +34 -0
- package/asset/deliveryUrl.js.map +1 -0
- package/asset/deliveryUrl.test.d.ts +1 -0
- package/asset/deliveryUrl.test.js +127 -0
- package/asset/deliveryUrl.test.js.map +1 -0
- package/asset/geometry.d.ts +7 -0
- package/asset/geometry.js +10 -0
- package/asset/geometry.js.map +1 -0
- package/asset/imageSrcSet.d.ts +63 -0
- package/asset/imageSrcSet.js +98 -0
- package/asset/imageSrcSet.js.map +1 -0
- package/asset/imageSrcSet.test.d.ts +1 -0
- package/asset/imageSrcSet.test.js +103 -0
- package/asset/imageSrcSet.test.js.map +1 -0
- package/asset/index.d.ts +5 -0
- package/asset/index.js +5 -0
- package/asset/normalize.d.ts +24 -0
- package/asset/normalize.js +106 -0
- package/asset/normalize.js.map +1 -0
- package/asset/normalize.test.d.ts +1 -0
- package/asset/normalize.test.js +263 -0
- package/asset/normalize.test.js.map +1 -0
- package/asset/types.d.ts +2 -0
- package/asset/types.js +0 -0
- package/componentLoader/ComponentLoader.d.ts +16 -0
- package/componentLoader/ComponentLoader.js +0 -0
- package/componentLoader/RegistryOnlyComponentLoader.d.ts +9 -0
- package/componentLoader/RegistryOnlyComponentLoader.js +8 -0
- package/componentLoader/RegistryOnlyComponentLoader.js.map +1 -0
- package/componentLoader/createComponentLoader.d.ts +7 -0
- package/componentLoader/createComponentLoader.js +10 -0
- package/componentLoader/createComponentLoader.js.map +1 -0
- package/componentLoader/defaultComponentLoader.d.ts +7 -0
- package/componentLoader/defaultComponentLoader.js +8 -0
- package/componentLoader/defaultComponentLoader.js.map +1 -0
- package/componentLoader/index.d.ts +3 -0
- package/componentLoader/index.js +3 -0
- package/contentClientState.test.d.ts +1 -0
- package/contentClientState.test.js +384 -0
- package/contentClientState.test.js.map +1 -0
- package/contentEntry/ContentEntryBindingsResolver.d.ts +33 -0
- package/contentEntry/ContentEntryBindingsResolver.js +52 -0
- package/contentEntry/ContentEntryBindingsResolver.js.map +1 -0
- package/contentEntry/ContentEntryCache.d.ts +49 -0
- package/contentEntry/ContentEntryCache.js +51 -0
- package/contentEntry/ContentEntryCache.js.map +1 -0
- package/contentEntry/resolveContentEntry.d.ts +74 -0
- package/contentEntry/resolveContentEntry.js +53 -0
- package/contentEntry/resolveContentEntry.js.map +1 -0
- package/contentEntry/resolveContentEntry.test.d.ts +1 -0
- package/contentEntry/resolveContentEntry.test.js +213 -0
- package/contentEntry/resolveContentEntry.test.js.map +1 -0
- package/createComponent.d.ts +5 -0
- package/createComponent.js +32 -0
- package/createComponent.js.map +1 -0
- package/createContentClient.d.ts +17 -0
- package/createContentClient.js +16 -0
- package/createContentClient.js.map +1 -0
- package/createContentClient.test.d.ts +1 -0
- package/createContentClient.test.js +165 -0
- package/createContentClient.test.js.map +1 -0
- package/createInput.d.ts +10 -2
- package/createInput.js +8 -1
- package/createInput.js.map +1 -1
- package/dataProviders/ApiClient.d.ts +10 -5
- package/dataProviders/ApiClient.js +10 -10
- package/dataProviders/ApiClient.js.map +1 -1
- package/dataProviders/DefaultDataProvider.d.ts +4 -0
- package/dataProviders/DefaultDataProvider.js +35 -0
- package/dataProviders/DefaultDataProvider.js.map +1 -1
- package/dataProviders/GET_EXPERIMENT_PAUSED.d.ts +1 -0
- package/dataProviders/GET_EXPERIMENT_PAUSED.js +17 -0
- package/dataProviders/GET_EXPERIMENT_PAUSED.js.map +1 -0
- package/dataProviders/GET_PAGE_BY_ID.d.ts +1 -1
- package/dataProviders/GET_PAGE_BY_ID.js +2 -0
- package/dataProviders/GET_PAGE_BY_ID.js.map +1 -1
- package/dataProviders/GET_PAGE_BY_PATH.d.ts +1 -1
- package/dataProviders/GET_PAGE_BY_PATH.js +2 -0
- package/dataProviders/GET_PAGE_BY_PATH.js.map +1 -1
- package/dataProviders/GET_PAGE_EXPERIMENT.d.ts +1 -0
- package/dataProviders/GET_PAGE_EXPERIMENT.js +32 -0
- package/dataProviders/GET_PAGE_EXPERIMENT.js.map +1 -0
- package/dataProviders/GET_VARIANT_CONTENT.d.ts +1 -0
- package/dataProviders/GET_VARIANT_CONTENT.js +24 -0
- package/dataProviders/GET_VARIANT_CONTENT.js.map +1 -0
- package/dataProviders/NullDataProvider.d.ts +4 -0
- package/dataProviders/NullDataProvider.js +9 -0
- package/dataProviders/NullDataProvider.js.map +1 -1
- package/dataProviders/queries.test.d.ts +1 -0
- package/dataProviders/queries.test.js +45 -0
- package/dataProviders/queries.test.js.map +1 -0
- package/defaultBreakpoints.js +23 -3
- package/defaultBreakpoints.js.map +1 -1
- package/experiments/PostHogAnalyticsProvider.d.ts +31 -0
- package/experiments/PostHogAnalyticsProvider.js +28 -0
- package/experiments/PostHogAnalyticsProvider.js.map +1 -0
- package/experiments/analyticsProvider.d.ts +4 -0
- package/experiments/analyticsProvider.js +11 -0
- package/experiments/analyticsProvider.js.map +1 -0
- package/experiments/bucketing.d.ts +27 -0
- package/experiments/bucketing.js +91 -0
- package/experiments/bucketing.js.map +1 -0
- package/experiments/bucketing.test.d.ts +1 -0
- package/experiments/bucketing.test.js +183 -0
- package/experiments/bucketing.test.js.map +1 -0
- package/experiments/index.d.ts +5 -0
- package/experiments/index.js +5 -0
- package/experiments/render.d.ts +82 -0
- package/experiments/render.js +149 -0
- package/experiments/render.js.map +1 -0
- package/experiments/types.d.ts +108 -0
- package/experiments/types.js +4 -0
- package/experiments/types.js.map +1 -0
- package/headersProvider.d.ts +1 -2
- package/headersProvider.js.map +1 -1
- package/image/geometry.d.ts +54 -0
- package/image/geometry.js +112 -0
- package/image/geometry.js.map +1 -0
- package/image/geometry.test.d.ts +1 -0
- package/image/geometry.test.js +190 -0
- package/image/geometry.test.js.map +1 -0
- package/image/index.d.ts +3 -0
- package/image/index.js +3 -0
- package/image/presets.d.ts +6 -0
- package/image/presets.js +20 -0
- package/image/presets.js.map +1 -0
- package/image/types.d.ts +10 -0
- package/image/types.js +0 -0
- package/index.d.ts +12 -0
- package/index.js +12 -0
- package/jsonPatch.js +3 -3
- package/package.json +7 -6
- package/types/WebsiteBuilderTheme.d.ts +5 -0
- package/types.d.ts +75 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build asset **delivery URLs** that frame the image server-side. The delivery
|
|
3
|
+
* pipeline applies the crop (`?crop`), an optional target aspect ratio + focal point
|
|
4
|
+
* (`?aspectRatio`, `?focal`), resizing (`?width`), and format (`?format`) — so the
|
|
5
|
+
* returned URL's pixels are already framed. Use with any renderer that loads a URL:
|
|
6
|
+
* `next/image`, a plain `<img>`, Angular, React Native, Open Graph tags, email, etc.
|
|
7
|
+
* Each distinct framing yields its own cacheable URL.
|
|
8
|
+
*/
|
|
9
|
+
import type { Asset } from "./types.js";
|
|
10
|
+
export interface AssetUrlOptions {
|
|
11
|
+
/** Delivery width (snapped to the configured width ladder server-side). */
|
|
12
|
+
width?: number;
|
|
13
|
+
/** Output format, e.g. `"auto"` (negotiated via Accept), `"webp"`, `"avif"`. */
|
|
14
|
+
format?: string;
|
|
15
|
+
/** Encoder quality (1–100). */
|
|
16
|
+
quality?: number;
|
|
17
|
+
/** Include the per-usage crop (`image.crop`) as a server-side crop. Default `true`. */
|
|
18
|
+
crop?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Frame to a target aspect ratio server-side (`"16:9"` or a `w/h` number),
|
|
21
|
+
* centered on the asset's focal point. Omit to deliver at the crop's own ratio.
|
|
22
|
+
*/
|
|
23
|
+
aspectRatio?: number | string;
|
|
24
|
+
/**
|
|
25
|
+
* Include the asset's focal point (`image.focalPoint`) when `aspectRatio` is set.
|
|
26
|
+
* Default `true`.
|
|
27
|
+
*/
|
|
28
|
+
focal?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The `crop=top,left,bottom,right` query value for an asset's per-usage crop, or
|
|
32
|
+
* `undefined` when there is no crop (or a full-image, no-op crop).
|
|
33
|
+
*/
|
|
34
|
+
export declare const getAssetCropParam: (asset: Pick<Asset, "image"> | null | undefined) => string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Build a delivery URL for an asset, baking the per-usage crop into the delivered
|
|
37
|
+
* image (plus optional width/format/quality). Returns `""` for a missing asset.
|
|
38
|
+
*/
|
|
39
|
+
export declare const getAssetUrl: (asset: (Pick<Asset, "src" | "image"> & {
|
|
40
|
+
src?: string;
|
|
41
|
+
}) | null | undefined, options?: AssetUrlOptions) => string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const round = (n)=>Math.round(10000 * n) / 10000;
|
|
2
|
+
const getAssetCropParam = (asset)=>{
|
|
3
|
+
const c = asset?.image?.crop;
|
|
4
|
+
if (!c || 0 === c.top && 0 === c.left && 0 === c.bottom && 0 === c.right) return;
|
|
5
|
+
return [
|
|
6
|
+
c.top,
|
|
7
|
+
c.left,
|
|
8
|
+
c.bottom,
|
|
9
|
+
c.right
|
|
10
|
+
].map(round).join(",");
|
|
11
|
+
};
|
|
12
|
+
const getAssetUrl = (asset, options = {})=>{
|
|
13
|
+
const src = asset?.src;
|
|
14
|
+
if (!src) return "";
|
|
15
|
+
const params = [];
|
|
16
|
+
if (false !== options.crop) {
|
|
17
|
+
const crop = getAssetCropParam(asset);
|
|
18
|
+
if (crop) params.push(`crop=${crop}`);
|
|
19
|
+
}
|
|
20
|
+
if (void 0 !== options.aspectRatio) {
|
|
21
|
+
params.push(`aspectRatio=${options.aspectRatio}`);
|
|
22
|
+
const fp = asset?.image?.focalPoint;
|
|
23
|
+
if (false !== options.focal && fp && !(0.5 === fp.x && 0.5 === fp.y)) params.push(`focal=${round(fp.x)},${round(fp.y)}`);
|
|
24
|
+
}
|
|
25
|
+
if (options.width) params.push(`width=${options.width}`);
|
|
26
|
+
if (options.format) params.push(`format=${options.format}`);
|
|
27
|
+
if (options.quality) params.push(`quality=${options.quality}`);
|
|
28
|
+
if (0 === params.length) return src;
|
|
29
|
+
const separator = src.includes("?") ? "&" : "?";
|
|
30
|
+
return `${src}${separator}${params.join("&")}`;
|
|
31
|
+
};
|
|
32
|
+
export { getAssetCropParam, getAssetUrl };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=deliveryUrl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset/deliveryUrl.js","sources":["../../src/asset/deliveryUrl.ts"],"sourcesContent":["/**\n * Build asset **delivery URLs** that frame the image server-side. The delivery\n * pipeline applies the crop (`?crop`), an optional target aspect ratio + focal point\n * (`?aspectRatio`, `?focal`), resizing (`?width`), and format (`?format`) — so the\n * returned URL's pixels are already framed. Use with any renderer that loads a URL:\n * `next/image`, a plain `<img>`, Angular, React Native, Open Graph tags, email, etc.\n * Each distinct framing yields its own cacheable URL.\n */\nimport type { Asset } from \"./types.js\";\n\nexport interface AssetUrlOptions {\n /** Delivery width (snapped to the configured width ladder server-side). */\n width?: number;\n /** Output format, e.g. `\"auto\"` (negotiated via Accept), `\"webp\"`, `\"avif\"`. */\n format?: string;\n /** Encoder quality (1–100). */\n quality?: number;\n /** Include the per-usage crop (`image.crop`) as a server-side crop. Default `true`. */\n crop?: boolean;\n /**\n * Frame to a target aspect ratio server-side (`\"16:9\"` or a `w/h` number),\n * centered on the asset's focal point. Omit to deliver at the crop's own ratio.\n */\n aspectRatio?: number | string;\n /**\n * Include the asset's focal point (`image.focalPoint`) when `aspectRatio` is set.\n * Default `true`.\n */\n focal?: boolean;\n}\n\nconst round = (n: number): number => Math.round(n * 10000) / 10000;\n\n/**\n * The `crop=top,left,bottom,right` query value for an asset's per-usage crop, or\n * `undefined` when there is no crop (or a full-image, no-op crop).\n */\nexport const getAssetCropParam = (\n asset: Pick<Asset, \"image\"> | null | undefined\n): string | undefined => {\n const c = asset?.image?.crop;\n if (!c || (c.top === 0 && c.left === 0 && c.bottom === 0 && c.right === 0)) {\n return undefined;\n }\n return [c.top, c.left, c.bottom, c.right].map(round).join(\",\");\n};\n\n/**\n * Build a delivery URL for an asset, baking the per-usage crop into the delivered\n * image (plus optional width/format/quality). Returns `\"\"` for a missing asset.\n */\nexport const getAssetUrl = (\n asset: (Pick<Asset, \"src\" | \"image\"> & { src?: string }) | null | undefined,\n options: AssetUrlOptions = {}\n): string => {\n const src = asset?.src;\n if (!src) {\n return \"\";\n }\n\n const params: string[] = [];\n if (options.crop !== false) {\n const crop = getAssetCropParam(asset);\n if (crop) {\n params.push(`crop=${crop}`);\n }\n }\n if (options.aspectRatio !== undefined) {\n params.push(`aspectRatio=${options.aspectRatio}`);\n const fp = asset?.image?.focalPoint;\n if (options.focal !== false && fp && !(fp.x === 0.5 && fp.y === 0.5)) {\n params.push(`focal=${round(fp.x)},${round(fp.y)}`);\n }\n }\n if (options.width) {\n params.push(`width=${options.width}`);\n }\n if (options.format) {\n params.push(`format=${options.format}`);\n }\n if (options.quality) {\n params.push(`quality=${options.quality}`);\n }\n\n if (params.length === 0) {\n return src;\n }\n const separator = src.includes(\"?\") ? \"&\" : \"?\";\n return `${src}${separator}${params.join(\"&\")}`;\n};\n"],"names":["round","n","Math","getAssetCropParam","asset","c","getAssetUrl","options","src","params","crop","undefined","fp","separator"],"mappings":"AA+BA,MAAMA,QAAQ,CAACC,IAAsBC,KAAK,KAAK,CAACD,AAAI,QAAJA,KAAa;AAMtD,MAAME,oBAAoB,CAC7BC;IAEA,MAAMC,IAAID,OAAO,OAAO;IACxB,IAAI,CAACC,KAAMA,AAAU,MAAVA,EAAE,GAAG,IAAUA,AAAW,MAAXA,EAAE,IAAI,IAAUA,AAAa,MAAbA,EAAE,MAAM,IAAUA,AAAY,MAAZA,EAAE,KAAK,EAC/D;IAEJ,OAAO;QAACA,EAAE,GAAG;QAAEA,EAAE,IAAI;QAAEA,EAAE,MAAM;QAAEA,EAAE,KAAK;KAAC,CAAC,GAAG,CAACL,OAAO,IAAI,CAAC;AAC9D;AAMO,MAAMM,cAAc,CACvBF,OACAG,UAA2B,CAAC,CAAC;IAE7B,MAAMC,MAAMJ,OAAO;IACnB,IAAI,CAACI,KACD,OAAO;IAGX,MAAMC,SAAmB,EAAE;IAC3B,IAAIF,AAAiB,UAAjBA,QAAQ,IAAI,EAAY;QACxB,MAAMG,OAAOP,kBAAkBC;QAC/B,IAAIM,MACAD,OAAO,IAAI,CAAC,CAAC,KAAK,EAAEC,MAAM;IAElC;IACA,IAAIH,AAAwBI,WAAxBJ,QAAQ,WAAW,EAAgB;QACnCE,OAAO,IAAI,CAAC,CAAC,YAAY,EAAEF,QAAQ,WAAW,EAAE;QAChD,MAAMK,KAAKR,OAAO,OAAO;QACzB,IAAIG,AAAkB,UAAlBA,QAAQ,KAAK,IAAcK,MAAM,CAAEA,CAAAA,AAAS,QAATA,GAAG,CAAC,IAAYA,AAAS,QAATA,GAAG,CAAC,AAAO,GAC9DH,OAAO,IAAI,CAAC,CAAC,MAAM,EAAET,MAAMY,GAAG,CAAC,EAAE,CAAC,EAAEZ,MAAMY,GAAG,CAAC,GAAG;IAEzD;IACA,IAAIL,QAAQ,KAAK,EACbE,OAAO,IAAI,CAAC,CAAC,MAAM,EAAEF,QAAQ,KAAK,EAAE;IAExC,IAAIA,QAAQ,MAAM,EACdE,OAAO,IAAI,CAAC,CAAC,OAAO,EAAEF,QAAQ,MAAM,EAAE;IAE1C,IAAIA,QAAQ,OAAO,EACfE,OAAO,IAAI,CAAC,CAAC,QAAQ,EAAEF,QAAQ,OAAO,EAAE;IAG5C,IAAIE,AAAkB,MAAlBA,OAAO,MAAM,EACb,OAAOD;IAEX,MAAMK,YAAYL,IAAI,QAAQ,CAAC,OAAO,MAAM;IAC5C,OAAO,GAAGA,MAAMK,YAAYJ,OAAO,IAAI,CAAC,MAAM;AAClD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getAssetCropParam, getAssetUrl } from "./deliveryUrl.js";
|
|
3
|
+
const asset = (crop)=>({
|
|
4
|
+
id: "f",
|
|
5
|
+
src: "https://cdn/x/pic.jpg",
|
|
6
|
+
name: "pic.jpg",
|
|
7
|
+
mimeType: "image/jpeg",
|
|
8
|
+
size: 1,
|
|
9
|
+
image: {
|
|
10
|
+
width: 800,
|
|
11
|
+
height: 600,
|
|
12
|
+
...crop ? {
|
|
13
|
+
crop
|
|
14
|
+
} : {}
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
describe("getAssetCropParam", ()=>{
|
|
18
|
+
it("serializes a crop as top,left,bottom,right", ()=>{
|
|
19
|
+
expect(getAssetCropParam(asset({
|
|
20
|
+
top: 0.1,
|
|
21
|
+
left: 0.2,
|
|
22
|
+
bottom: 0.1,
|
|
23
|
+
right: 0.05
|
|
24
|
+
}))).toBe("0.1,0.2,0.1,0.05");
|
|
25
|
+
});
|
|
26
|
+
it("is undefined for no crop or a full crop", ()=>{
|
|
27
|
+
expect(getAssetCropParam(asset())).toBeUndefined();
|
|
28
|
+
expect(getAssetCropParam(asset({
|
|
29
|
+
top: 0,
|
|
30
|
+
left: 0,
|
|
31
|
+
bottom: 0,
|
|
32
|
+
right: 0
|
|
33
|
+
}))).toBeUndefined();
|
|
34
|
+
expect(getAssetCropParam(null)).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("getAssetUrl", ()=>{
|
|
38
|
+
it("bakes the crop + width + format into the URL", ()=>{
|
|
39
|
+
const url = getAssetUrl(asset({
|
|
40
|
+
top: 0.1,
|
|
41
|
+
left: 0.2,
|
|
42
|
+
bottom: 0.1,
|
|
43
|
+
right: 0.05
|
|
44
|
+
}), {
|
|
45
|
+
width: 1200,
|
|
46
|
+
format: "auto"
|
|
47
|
+
});
|
|
48
|
+
expect(url).toBe("https://cdn/x/pic.jpg?crop=0.1,0.2,0.1,0.05&width=1200&format=auto");
|
|
49
|
+
});
|
|
50
|
+
it("omits the crop when there is none", ()=>{
|
|
51
|
+
expect(getAssetUrl(asset(), {
|
|
52
|
+
width: 640
|
|
53
|
+
})).toBe("https://cdn/x/pic.jpg?width=640");
|
|
54
|
+
});
|
|
55
|
+
it("can opt out of the crop", ()=>{
|
|
56
|
+
expect(getAssetUrl(asset({
|
|
57
|
+
top: 0.1,
|
|
58
|
+
left: 0,
|
|
59
|
+
bottom: 0,
|
|
60
|
+
right: 0
|
|
61
|
+
}), {
|
|
62
|
+
crop: false
|
|
63
|
+
})).toBe("https://cdn/x/pic.jpg");
|
|
64
|
+
});
|
|
65
|
+
it("returns the bare src when no options apply", ()=>{
|
|
66
|
+
expect(getAssetUrl(asset())).toBe("https://cdn/x/pic.jpg");
|
|
67
|
+
});
|
|
68
|
+
it("uses & when the src already has a query string", ()=>{
|
|
69
|
+
const url = getAssetUrl({
|
|
70
|
+
src: "https://cdn/x/pic.jpg?v=2",
|
|
71
|
+
image: {}
|
|
72
|
+
}, {
|
|
73
|
+
width: 640
|
|
74
|
+
});
|
|
75
|
+
expect(url).toBe("https://cdn/x/pic.jpg?v=2&width=640");
|
|
76
|
+
});
|
|
77
|
+
it("returns empty string for a missing asset", ()=>{
|
|
78
|
+
expect(getAssetUrl(null)).toBe("");
|
|
79
|
+
expect(getAssetUrl(void 0)).toBe("");
|
|
80
|
+
});
|
|
81
|
+
it("emits aspectRatio + focal (from the asset) when framing to a ratio", ()=>{
|
|
82
|
+
const a = {
|
|
83
|
+
...asset(),
|
|
84
|
+
image: {
|
|
85
|
+
width: 800,
|
|
86
|
+
height: 600,
|
|
87
|
+
focalPoint: {
|
|
88
|
+
x: 0.7,
|
|
89
|
+
y: 0.4
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
expect(getAssetUrl(a, {
|
|
94
|
+
aspectRatio: "16:9",
|
|
95
|
+
width: 1200
|
|
96
|
+
})).toBe("https://cdn/x/pic.jpg?aspectRatio=16:9&focal=0.7,0.4&width=1200");
|
|
97
|
+
});
|
|
98
|
+
it("omits focal when it is centered or opted out", ()=>{
|
|
99
|
+
const centered = {
|
|
100
|
+
...asset(),
|
|
101
|
+
image: {
|
|
102
|
+
focalPoint: {
|
|
103
|
+
x: 0.5,
|
|
104
|
+
y: 0.5
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
expect(getAssetUrl(centered, {
|
|
109
|
+
aspectRatio: 1
|
|
110
|
+
})).toBe("https://cdn/x/pic.jpg?aspectRatio=1");
|
|
111
|
+
const offCenter = {
|
|
112
|
+
...asset(),
|
|
113
|
+
image: {
|
|
114
|
+
focalPoint: {
|
|
115
|
+
x: 0.7,
|
|
116
|
+
y: 0.4
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
expect(getAssetUrl(offCenter, {
|
|
121
|
+
aspectRatio: 1,
|
|
122
|
+
focal: false
|
|
123
|
+
})).toBe("https://cdn/x/pic.jpg?aspectRatio=1");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
//# sourceMappingURL=deliveryUrl.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset/deliveryUrl.test.js","sources":["../../src/asset/deliveryUrl.test.ts"],"sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { getAssetCropParam, getAssetUrl } from \"./deliveryUrl.js\";\n\nconst asset = (crop?: any) => ({\n id: \"f\",\n src: \"https://cdn/x/pic.jpg\",\n name: \"pic.jpg\",\n mimeType: \"image/jpeg\",\n size: 1,\n image: { width: 800, height: 600, ...(crop ? { crop } : {}) }\n});\n\ndescribe(\"getAssetCropParam\", () => {\n it(\"serializes a crop as top,left,bottom,right\", () => {\n expect(getAssetCropParam(asset({ top: 0.1, left: 0.2, bottom: 0.1, right: 0.05 }))).toBe(\n \"0.1,0.2,0.1,0.05\"\n );\n });\n\n it(\"is undefined for no crop or a full crop\", () => {\n expect(getAssetCropParam(asset())).toBeUndefined();\n expect(getAssetCropParam(asset({ top: 0, left: 0, bottom: 0, right: 0 }))).toBeUndefined();\n expect(getAssetCropParam(null)).toBeUndefined();\n });\n});\n\ndescribe(\"getAssetUrl\", () => {\n it(\"bakes the crop + width + format into the URL\", () => {\n const url = getAssetUrl(asset({ top: 0.1, left: 0.2, bottom: 0.1, right: 0.05 }), {\n width: 1200,\n format: \"auto\"\n });\n expect(url).toBe(\"https://cdn/x/pic.jpg?crop=0.1,0.2,0.1,0.05&width=1200&format=auto\");\n });\n\n it(\"omits the crop when there is none\", () => {\n expect(getAssetUrl(asset(), { width: 640 })).toBe(\"https://cdn/x/pic.jpg?width=640\");\n });\n\n it(\"can opt out of the crop\", () => {\n expect(\n getAssetUrl(asset({ top: 0.1, left: 0, bottom: 0, right: 0 }), { crop: false })\n ).toBe(\"https://cdn/x/pic.jpg\");\n });\n\n it(\"returns the bare src when no options apply\", () => {\n expect(getAssetUrl(asset())).toBe(\"https://cdn/x/pic.jpg\");\n });\n\n it(\"uses & when the src already has a query string\", () => {\n const url = getAssetUrl(\n { src: \"https://cdn/x/pic.jpg?v=2\", image: {} },\n {\n width: 640\n }\n );\n expect(url).toBe(\"https://cdn/x/pic.jpg?v=2&width=640\");\n });\n\n it(\"returns empty string for a missing asset\", () => {\n expect(getAssetUrl(null)).toBe(\"\");\n expect(getAssetUrl(undefined)).toBe(\"\");\n });\n\n it(\"emits aspectRatio + focal (from the asset) when framing to a ratio\", () => {\n const a = {\n ...asset(),\n image: { width: 800, height: 600, focalPoint: { x: 0.7, y: 0.4 } }\n };\n expect(getAssetUrl(a, { aspectRatio: \"16:9\", width: 1200 })).toBe(\n \"https://cdn/x/pic.jpg?aspectRatio=16:9&focal=0.7,0.4&width=1200\"\n );\n });\n\n it(\"omits focal when it is centered or opted out\", () => {\n const centered = { ...asset(), image: { focalPoint: { x: 0.5, y: 0.5 } } };\n expect(getAssetUrl(centered, { aspectRatio: 1 })).toBe(\n \"https://cdn/x/pic.jpg?aspectRatio=1\"\n );\n const offCenter = { ...asset(), image: { focalPoint: { x: 0.7, y: 0.4 } } };\n expect(getAssetUrl(offCenter, { aspectRatio: 1, focal: false })).toBe(\n \"https://cdn/x/pic.jpg?aspectRatio=1\"\n );\n });\n});\n"],"names":["asset","crop","describe","it","expect","getAssetCropParam","url","getAssetUrl","undefined","a","centered","offCenter"],"mappings":";;AAGA,MAAMA,QAAQ,CAACC,OAAgB;QAC3B,IAAI;QACJ,KAAK;QACL,MAAM;QACN,UAAU;QACV,MAAM;QACN,OAAO;YAAE,OAAO;YAAK,QAAQ;YAAK,GAAIA,OAAO;gBAAEA;YAAK,IAAI,CAAC,CAAC;QAAE;IAChE;AAEAC,SAAS,qBAAqB;IAC1BC,GAAG,8CAA8C;QAC7CC,OAAOC,kBAAkBL,MAAM;YAAE,KAAK;YAAK,MAAM;YAAK,QAAQ;YAAK,OAAO;QAAK,KAAK,IAAI,CACpF;IAER;IAEAG,GAAG,2CAA2C;QAC1CC,OAAOC,kBAAkBL,UAAU,aAAa;QAChDI,OAAOC,kBAAkBL,MAAM;YAAE,KAAK;YAAG,MAAM;YAAG,QAAQ;YAAG,OAAO;QAAE,KAAK,aAAa;QACxFI,OAAOC,kBAAkB,OAAO,aAAa;IACjD;AACJ;AAEAH,SAAS,eAAe;IACpBC,GAAG,gDAAgD;QAC/C,MAAMG,MAAMC,YAAYP,MAAM;YAAE,KAAK;YAAK,MAAM;YAAK,QAAQ;YAAK,OAAO;QAAK,IAAI;YAC9E,OAAO;YACP,QAAQ;QACZ;QACAI,OAAOE,KAAK,IAAI,CAAC;IACrB;IAEAH,GAAG,qCAAqC;QACpCC,OAAOG,YAAYP,SAAS;YAAE,OAAO;QAAI,IAAI,IAAI,CAAC;IACtD;IAEAG,GAAG,2BAA2B;QAC1BC,OACIG,YAAYP,MAAM;YAAE,KAAK;YAAK,MAAM;YAAG,QAAQ;YAAG,OAAO;QAAE,IAAI;YAAE,MAAM;QAAM,IAC/E,IAAI,CAAC;IACX;IAEAG,GAAG,8CAA8C;QAC7CC,OAAOG,YAAYP,UAAU,IAAI,CAAC;IACtC;IAEAG,GAAG,kDAAkD;QACjD,MAAMG,MAAMC,YACR;YAAE,KAAK;YAA6B,OAAO,CAAC;QAAE,GAC9C;YACI,OAAO;QACX;QAEJH,OAAOE,KAAK,IAAI,CAAC;IACrB;IAEAH,GAAG,4CAA4C;QAC3CC,OAAOG,YAAY,OAAO,IAAI,CAAC;QAC/BH,OAAOG,YAAYC,SAAY,IAAI,CAAC;IACxC;IAEAL,GAAG,sEAAsE;QACrE,MAAMM,IAAI;YACN,GAAGT,OAAO;YACV,OAAO;gBAAE,OAAO;gBAAK,QAAQ;gBAAK,YAAY;oBAAE,GAAG;oBAAK,GAAG;gBAAI;YAAE;QACrE;QACAI,OAAOG,YAAYE,GAAG;YAAE,aAAa;YAAQ,OAAO;QAAK,IAAI,IAAI,CAC7D;IAER;IAEAN,GAAG,gDAAgD;QAC/C,MAAMO,WAAW;YAAE,GAAGV,OAAO;YAAE,OAAO;gBAAE,YAAY;oBAAE,GAAG;oBAAK,GAAG;gBAAI;YAAE;QAAE;QACzEI,OAAOG,YAAYG,UAAU;YAAE,aAAa;QAAE,IAAI,IAAI,CAClD;QAEJ,MAAMC,YAAY;YAAE,GAAGX,OAAO;YAAE,OAAO;gBAAE,YAAY;oBAAE,GAAG;oBAAK,GAAG;gBAAI;YAAE;QAAE;QAC1EI,OAAOG,YAAYI,WAAW;YAAE,aAAa;YAAG,OAAO;QAAM,IAAI,IAAI,CACjE;IAER;AACJ"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ImageRenderData, type NormalizedRect } from "../image/geometry.js";
|
|
2
|
+
import type { AspectRatioInput } from "../image/types.js";
|
|
3
|
+
import type { Asset } from "./types.js";
|
|
4
|
+
type AssetLike = Pick<Asset, "image">;
|
|
5
|
+
export declare function getAssetVisibleRect(asset: AssetLike, aspectRatio?: AspectRatioInput): NormalizedRect;
|
|
6
|
+
export declare function getAssetImageRenderData(asset: AssetLike, aspectRatio?: AspectRatioInput): ImageRenderData;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getImageRenderData, getVisibleRect } from "../image/geometry.js";
|
|
2
|
+
function getAssetVisibleRect(asset, aspectRatio) {
|
|
3
|
+
return getVisibleRect(asset.image ?? {}, aspectRatio);
|
|
4
|
+
}
|
|
5
|
+
function getAssetImageRenderData(asset, aspectRatio) {
|
|
6
|
+
return getImageRenderData(asset.image ?? {}, aspectRatio);
|
|
7
|
+
}
|
|
8
|
+
export { getAssetImageRenderData, getAssetVisibleRect };
|
|
9
|
+
|
|
10
|
+
//# sourceMappingURL=geometry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset/geometry.js","sources":["../../src/asset/geometry.ts"],"sourcesContent":["import {\n getImageRenderData,\n getVisibleRect,\n type ImageRenderData,\n type NormalizedRect\n} from \"../image/geometry.js\";\nimport type { AspectRatioInput } from \"../image/types.js\";\nimport type { Asset } from \"./types.js\";\n\ntype AssetLike = Pick<Asset, \"image\">;\n\nexport function getAssetVisibleRect(\n asset: AssetLike,\n aspectRatio?: AspectRatioInput\n): NormalizedRect {\n return getVisibleRect(asset.image ?? {}, aspectRatio);\n}\n\nexport function getAssetImageRenderData(\n asset: AssetLike,\n aspectRatio?: AspectRatioInput\n): ImageRenderData {\n return getImageRenderData(asset.image ?? {}, aspectRatio);\n}\n"],"names":["getAssetVisibleRect","asset","aspectRatio","getVisibleRect","getAssetImageRenderData","getImageRenderData"],"mappings":";AAWO,SAASA,oBACZC,KAAgB,EAChBC,WAA8B;IAE9B,OAAOC,eAAeF,MAAM,KAAK,IAAI,CAAC,GAAGC;AAC7C;AAEO,SAASE,wBACZH,KAAgB,EAChBC,WAA8B;IAE9B,OAAOG,mBAAmBJ,MAAM,KAAK,IAAI,CAAC,GAAGC;AACjD"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic responsive-image helpers built on {@link getAssetUrl}.
|
|
3
|
+
*
|
|
4
|
+
* Every renderer (Next.js, React, Vue, Nuxt — and any future Angular / React Native
|
|
5
|
+
* / vanilla adapter) needs the same three things from a `Asset`: a width
|
|
6
|
+
* ladder, a `srcSet` with the per-usage crop/focal/format baked into each width, and
|
|
7
|
+
* the delivered image's intrinsic (cropped) dimensions for layout-shift-free
|
|
8
|
+
* rendering. Keeping that logic here — instead of re-deriving it per framework —
|
|
9
|
+
* makes each renderer a thin adapter over the one delivery-URL contract.
|
|
10
|
+
*/
|
|
11
|
+
import type { Asset } from "./types.js";
|
|
12
|
+
import { type AssetUrlOptions } from "./deliveryUrl.js";
|
|
13
|
+
/**
|
|
14
|
+
* Canonical delivery-width ladder. Kept in lockstep with the server's resize ladder
|
|
15
|
+
* (`imageResizeWidths` in `@webiny/api-file-manager`) so every width a renderer
|
|
16
|
+
* requests snaps to a cached variant instead of forcing a fresh transform.
|
|
17
|
+
*/
|
|
18
|
+
export declare const IMAGE_RESIZE_WIDTHS: readonly [128, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
19
|
+
export interface ImageDimensions {
|
|
20
|
+
/** Delivered (cropped) width in pixels, or `0` when the intrinsic size is unknown. */
|
|
21
|
+
width: number;
|
|
22
|
+
/** Delivered (cropped) height in pixels, or `0` when the intrinsic size is unknown. */
|
|
23
|
+
height: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The intrinsic dimensions of the *delivered* image — i.e. after the per-usage crop
|
|
27
|
+
* (and an optional target aspect ratio) are applied. Renderers pass these to their
|
|
28
|
+
* layout primitive (e.g. `next/image`'s `width`/`height`) so the box reserves the
|
|
29
|
+
* right space and the page doesn't shift as the image loads. Returns `0`/`0` when the
|
|
30
|
+
* asset's intrinsic size is unknown.
|
|
31
|
+
*/
|
|
32
|
+
export declare const getImageDimensions: (asset: Pick<Asset, "image"> | null | undefined, options?: {
|
|
33
|
+
aspectRatio?: number | string;
|
|
34
|
+
}) => ImageDimensions;
|
|
35
|
+
export interface ImageSrcSetOptions extends Omit<AssetUrlOptions, "width"> {
|
|
36
|
+
/** Widths to emit. Defaults to {@link IMAGE_RESIZE_WIDTHS}, trimmed by `cssWidth`. */
|
|
37
|
+
widths?: readonly number[];
|
|
38
|
+
/**
|
|
39
|
+
* A fixed CSS width for the rendered image (`"320px"` or `320`). When set, the
|
|
40
|
+
* ladder is trimmed to the smallest set that still covers it (incl. high-DPR);
|
|
41
|
+
* fluid widths (`%`, `vw`, unset) use the full ladder.
|
|
42
|
+
*/
|
|
43
|
+
cssWidth?: number | string;
|
|
44
|
+
}
|
|
45
|
+
export interface ImageSrcSet {
|
|
46
|
+
/** Single-URL fallback for clients that ignore `srcSet` (the largest emitted width). */
|
|
47
|
+
src: string;
|
|
48
|
+
/** `"<url> 128w, <url> 384w, …"` — crop/focal/format baked into every width. */
|
|
49
|
+
srcSet: string;
|
|
50
|
+
/** Delivered (cropped) intrinsic dimensions; `0` when unknown. */
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build the responsive descriptor for an asset: a `srcSet` across the width ladder
|
|
56
|
+
* (each entry a delivery URL with the per-usage crop/focal/format baked in), a
|
|
57
|
+
* single-URL `src` fallback, and the delivered dimensions. Framework-agnostic — hand
|
|
58
|
+
* `src`/`srcSet` to any `<img>`-like primitive. Returns empty strings for a missing
|
|
59
|
+
* asset.
|
|
60
|
+
*/
|
|
61
|
+
export declare const getImageSrcSet: (asset: (Pick<Asset, "src" | "image"> & {
|
|
62
|
+
src?: string;
|
|
63
|
+
}) | null | undefined, options?: ImageSrcSetOptions) => ImageSrcSet;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { getAssetUrl } from "./deliveryUrl.js";
|
|
2
|
+
const IMAGE_RESIZE_WIDTHS = [
|
|
3
|
+
128,
|
|
4
|
+
384,
|
|
5
|
+
640,
|
|
6
|
+
750,
|
|
7
|
+
828,
|
|
8
|
+
1080,
|
|
9
|
+
1200,
|
|
10
|
+
1920,
|
|
11
|
+
2048,
|
|
12
|
+
3840
|
|
13
|
+
];
|
|
14
|
+
const parseAspectRatio = (input)=>{
|
|
15
|
+
if (void 0 === input) return;
|
|
16
|
+
if ("number" == typeof input) return input > 0 ? input : void 0;
|
|
17
|
+
const trimmed = input.trim();
|
|
18
|
+
if (trimmed.includes(":")) {
|
|
19
|
+
const [w, h] = trimmed.split(":").map(Number);
|
|
20
|
+
return w > 0 && h > 0 ? w / h : void 0;
|
|
21
|
+
}
|
|
22
|
+
const n = Number(trimmed);
|
|
23
|
+
return n > 0 ? n : void 0;
|
|
24
|
+
};
|
|
25
|
+
const toPx = (cssWidth)=>{
|
|
26
|
+
if (void 0 === cssWidth) return;
|
|
27
|
+
if ("number" == typeof cssWidth) return cssWidth > 0 ? cssWidth : void 0;
|
|
28
|
+
const trimmed = cssWidth.trim();
|
|
29
|
+
if (trimmed.endsWith("px")) {
|
|
30
|
+
const n = parseInt(trimmed, 10);
|
|
31
|
+
return n > 0 ? n : void 0;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const resolveWidths = (widths, cssWidth)=>{
|
|
35
|
+
const px = toPx(cssWidth);
|
|
36
|
+
if (!px) return [
|
|
37
|
+
...widths
|
|
38
|
+
];
|
|
39
|
+
const result = [];
|
|
40
|
+
for (const w of widths){
|
|
41
|
+
result.push(w);
|
|
42
|
+
if (w >= 2 * px) break;
|
|
43
|
+
}
|
|
44
|
+
return result.length ? result : [
|
|
45
|
+
widths[widths.length - 1]
|
|
46
|
+
];
|
|
47
|
+
};
|
|
48
|
+
const getImageDimensions = (asset, options = {})=>{
|
|
49
|
+
const img = asset?.image;
|
|
50
|
+
const iw = img?.width ?? 0;
|
|
51
|
+
const ih = img?.height ?? 0;
|
|
52
|
+
if (!iw || !ih) return {
|
|
53
|
+
width: 0,
|
|
54
|
+
height: 0
|
|
55
|
+
};
|
|
56
|
+
const crop = img?.crop;
|
|
57
|
+
const width = crop ? Math.round((1 - (crop.left ?? 0) - (crop.right ?? 0)) * iw) : iw;
|
|
58
|
+
const height = crop ? Math.round((1 - (crop.top ?? 0) - (crop.bottom ?? 0)) * ih) : ih;
|
|
59
|
+
const ar = parseAspectRatio(options.aspectRatio);
|
|
60
|
+
if (ar) return {
|
|
61
|
+
width,
|
|
62
|
+
height: Math.round(width / ar)
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
width,
|
|
66
|
+
height
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
const getImageSrcSet = (asset, options = {})=>{
|
|
70
|
+
const { widths = IMAGE_RESIZE_WIDTHS, cssWidth, ...urlOptions } = options;
|
|
71
|
+
const dims = getImageDimensions(asset, {
|
|
72
|
+
aspectRatio: urlOptions.aspectRatio
|
|
73
|
+
});
|
|
74
|
+
if (!asset?.src) return {
|
|
75
|
+
src: "",
|
|
76
|
+
srcSet: "",
|
|
77
|
+
width: dims.width,
|
|
78
|
+
height: dims.height
|
|
79
|
+
};
|
|
80
|
+
const selected = resolveWidths(widths, cssWidth);
|
|
81
|
+
const srcSet = selected.map((w)=>`${getAssetUrl(asset, {
|
|
82
|
+
...urlOptions,
|
|
83
|
+
width: w
|
|
84
|
+
})} ${w}w`).join(", ");
|
|
85
|
+
const src = getAssetUrl(asset, {
|
|
86
|
+
...urlOptions,
|
|
87
|
+
width: selected[selected.length - 1]
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
src,
|
|
91
|
+
srcSet,
|
|
92
|
+
width: dims.width,
|
|
93
|
+
height: dims.height
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
export { IMAGE_RESIZE_WIDTHS, getImageDimensions, getImageSrcSet };
|
|
97
|
+
|
|
98
|
+
//# sourceMappingURL=imageSrcSet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset/imageSrcSet.js","sources":["../../src/asset/imageSrcSet.ts"],"sourcesContent":["/**\n * Framework-agnostic responsive-image helpers built on {@link getAssetUrl}.\n *\n * Every renderer (Next.js, React, Vue, Nuxt — and any future Angular / React Native\n * / vanilla adapter) needs the same three things from a `Asset`: a width\n * ladder, a `srcSet` with the per-usage crop/focal/format baked into each width, and\n * the delivered image's intrinsic (cropped) dimensions for layout-shift-free\n * rendering. Keeping that logic here — instead of re-deriving it per framework —\n * makes each renderer a thin adapter over the one delivery-URL contract.\n */\nimport type { Asset } from \"./types.js\";\nimport { getAssetUrl, type AssetUrlOptions } from \"./deliveryUrl.js\";\n\n/**\n * Canonical delivery-width ladder. Kept in lockstep with the server's resize ladder\n * (`imageResizeWidths` in `@webiny/api-file-manager`) so every width a renderer\n * requests snaps to a cached variant instead of forcing a fresh transform.\n */\nexport const IMAGE_RESIZE_WIDTHS = [128, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840] as const;\n\n/** Parse an aspect ratio given as a number, a `\"w:h\"` string, or a decimal string. */\nconst parseAspectRatio = (input: number | string | undefined): number | undefined => {\n if (input === undefined) {\n return undefined;\n }\n if (typeof input === \"number\") {\n return input > 0 ? input : undefined;\n }\n const trimmed = input.trim();\n if (trimmed.includes(\":\")) {\n const [w, h] = trimmed.split(\":\").map(Number);\n return w > 0 && h > 0 ? w / h : undefined;\n }\n const n = Number(trimmed);\n return n > 0 ? n : undefined;\n};\n\n/** Resolve a fixed CSS width to pixels, or `undefined` for fluid widths (`%`, `vw`, …). */\nconst toPx = (cssWidth: number | string | undefined): number | undefined => {\n if (cssWidth === undefined) {\n return undefined;\n }\n if (typeof cssWidth === \"number\") {\n return cssWidth > 0 ? cssWidth : undefined;\n }\n const trimmed = cssWidth.trim();\n if (trimmed.endsWith(\"px\")) {\n const n = parseInt(trimmed, 10);\n return n > 0 ? n : undefined;\n }\n return undefined;\n};\n\n/**\n * Trim the ladder to the smallest set that still covers a fixed CSS width, including\n * one step past `2×` for high-DPR screens. Fluid widths use the full ladder.\n */\nconst resolveWidths = (\n widths: readonly number[],\n cssWidth: number | string | undefined\n): number[] => {\n const px = toPx(cssWidth);\n if (!px) {\n return [...widths];\n }\n const result: number[] = [];\n for (const w of widths) {\n result.push(w);\n if (w >= px * 2) {\n break;\n }\n }\n return result.length ? result : [widths[widths.length - 1]];\n};\n\nexport interface ImageDimensions {\n /** Delivered (cropped) width in pixels, or `0` when the intrinsic size is unknown. */\n width: number;\n /** Delivered (cropped) height in pixels, or `0` when the intrinsic size is unknown. */\n height: number;\n}\n\n/**\n * The intrinsic dimensions of the *delivered* image — i.e. after the per-usage crop\n * (and an optional target aspect ratio) are applied. Renderers pass these to their\n * layout primitive (e.g. `next/image`'s `width`/`height`) so the box reserves the\n * right space and the page doesn't shift as the image loads. Returns `0`/`0` when the\n * asset's intrinsic size is unknown.\n */\nexport const getImageDimensions = (\n asset: Pick<Asset, \"image\"> | null | undefined,\n options: { aspectRatio?: number | string } = {}\n): ImageDimensions => {\n const img = asset?.image;\n const iw = img?.width ?? 0;\n const ih = img?.height ?? 0;\n if (!iw || !ih) {\n return { width: 0, height: 0 };\n }\n const crop = img?.crop;\n const width = crop ? Math.round((1 - (crop.left ?? 0) - (crop.right ?? 0)) * iw) : iw;\n const height = crop ? Math.round((1 - (crop.top ?? 0) - (crop.bottom ?? 0)) * ih) : ih;\n\n const ar = parseAspectRatio(options.aspectRatio);\n if (ar) {\n // Framed to a fixed ratio server-side: keep the cropped width, derive height.\n return { width, height: Math.round(width / ar) };\n }\n return { width, height };\n};\n\nexport interface ImageSrcSetOptions extends Omit<AssetUrlOptions, \"width\"> {\n /** Widths to emit. Defaults to {@link IMAGE_RESIZE_WIDTHS}, trimmed by `cssWidth`. */\n widths?: readonly number[];\n /**\n * A fixed CSS width for the rendered image (`\"320px\"` or `320`). When set, the\n * ladder is trimmed to the smallest set that still covers it (incl. high-DPR);\n * fluid widths (`%`, `vw`, unset) use the full ladder.\n */\n cssWidth?: number | string;\n}\n\nexport interface ImageSrcSet {\n /** Single-URL fallback for clients that ignore `srcSet` (the largest emitted width). */\n src: string;\n /** `\"<url> 128w, <url> 384w, …\"` — crop/focal/format baked into every width. */\n srcSet: string;\n /** Delivered (cropped) intrinsic dimensions; `0` when unknown. */\n width: number;\n height: number;\n}\n\n/**\n * Build the responsive descriptor for an asset: a `srcSet` across the width ladder\n * (each entry a delivery URL with the per-usage crop/focal/format baked in), a\n * single-URL `src` fallback, and the delivered dimensions. Framework-agnostic — hand\n * `src`/`srcSet` to any `<img>`-like primitive. Returns empty strings for a missing\n * asset.\n */\nexport const getImageSrcSet = (\n asset: (Pick<Asset, \"src\" | \"image\"> & { src?: string }) | null | undefined,\n options: ImageSrcSetOptions = {}\n): ImageSrcSet => {\n const { widths = IMAGE_RESIZE_WIDTHS, cssWidth, ...urlOptions } = options;\n const dims = getImageDimensions(asset, { aspectRatio: urlOptions.aspectRatio });\n\n if (!asset?.src) {\n return { src: \"\", srcSet: \"\", width: dims.width, height: dims.height };\n }\n\n const selected = resolveWidths(widths, cssWidth);\n const srcSet = selected\n .map(w => `${getAssetUrl(asset, { ...urlOptions, width: w })} ${w}w`)\n .join(\", \");\n const src = getAssetUrl(asset, { ...urlOptions, width: selected[selected.length - 1] });\n\n return { src, srcSet, width: dims.width, height: dims.height };\n};\n"],"names":["IMAGE_RESIZE_WIDTHS","parseAspectRatio","input","undefined","trimmed","w","h","Number","n","toPx","cssWidth","parseInt","resolveWidths","widths","px","result","getImageDimensions","asset","options","img","iw","ih","crop","width","Math","height","ar","getImageSrcSet","urlOptions","dims","selected","srcSet","getAssetUrl","src"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { IMAGE_RESIZE_WIDTHS, getImageDimensions, getImageSrcSet } from "./imageSrcSet.js";
|
|
3
|
+
const asset = (image)=>({
|
|
4
|
+
id: "f",
|
|
5
|
+
src: "https://cdn/x/pic.jpg",
|
|
6
|
+
name: "pic.jpg",
|
|
7
|
+
mimeType: "image/jpeg",
|
|
8
|
+
size: 1,
|
|
9
|
+
image: {
|
|
10
|
+
width: 800,
|
|
11
|
+
height: 600,
|
|
12
|
+
...image
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
describe("getImageDimensions", ()=>{
|
|
16
|
+
it("returns intrinsic dimensions when there is no crop", ()=>{
|
|
17
|
+
expect(getImageDimensions(asset())).toEqual({
|
|
18
|
+
width: 800,
|
|
19
|
+
height: 600
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
it("shrinks by the crop insets", ()=>{
|
|
23
|
+
expect(getImageDimensions(asset({
|
|
24
|
+
crop: {
|
|
25
|
+
top: 0.1,
|
|
26
|
+
left: 0.25,
|
|
27
|
+
bottom: 0.1,
|
|
28
|
+
right: 0.25
|
|
29
|
+
}
|
|
30
|
+
}))).toEqual({
|
|
31
|
+
width: 400,
|
|
32
|
+
height: 480
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
it("derives height from a target aspect ratio", ()=>{
|
|
36
|
+
expect(getImageDimensions(asset({
|
|
37
|
+
crop: {
|
|
38
|
+
top: 0,
|
|
39
|
+
left: 0.25,
|
|
40
|
+
bottom: 0,
|
|
41
|
+
right: 0.25
|
|
42
|
+
}
|
|
43
|
+
}), {
|
|
44
|
+
aspectRatio: "16:9"
|
|
45
|
+
})).toEqual({
|
|
46
|
+
width: 400,
|
|
47
|
+
height: 225
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
it("returns 0/0 when the intrinsic size is unknown", ()=>{
|
|
51
|
+
expect(getImageDimensions({
|
|
52
|
+
image: {}
|
|
53
|
+
})).toEqual({
|
|
54
|
+
width: 0,
|
|
55
|
+
height: 0
|
|
56
|
+
});
|
|
57
|
+
expect(getImageDimensions(null)).toEqual({
|
|
58
|
+
width: 0,
|
|
59
|
+
height: 0
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("getImageSrcSet", ()=>{
|
|
64
|
+
it("emits the full ladder with crop + format baked into every width", ()=>{
|
|
65
|
+
const { srcSet, width, height } = getImageSrcSet(asset({
|
|
66
|
+
crop: {
|
|
67
|
+
top: 0.1,
|
|
68
|
+
left: 0,
|
|
69
|
+
bottom: 0,
|
|
70
|
+
right: 0
|
|
71
|
+
}
|
|
72
|
+
}), {
|
|
73
|
+
format: "auto"
|
|
74
|
+
});
|
|
75
|
+
const entries = srcSet.split(", ");
|
|
76
|
+
expect(entries).toHaveLength(IMAGE_RESIZE_WIDTHS.length);
|
|
77
|
+
expect(entries[0]).toBe("https://cdn/x/pic.jpg?crop=0.1,0,0,0&width=128&format=auto 128w");
|
|
78
|
+
expect(width).toBe(800);
|
|
79
|
+
expect(height).toBe(540);
|
|
80
|
+
});
|
|
81
|
+
it("trims the ladder to a fixed CSS width (incl. 2x DPR)", ()=>{
|
|
82
|
+
const { srcSet, src } = getImageSrcSet(asset(), {
|
|
83
|
+
cssWidth: "300px"
|
|
84
|
+
});
|
|
85
|
+
const widths = srcSet.split(", ").map((e)=>Number(e.match(/width=(\d+)/)[1]));
|
|
86
|
+
expect(widths).toEqual([
|
|
87
|
+
128,
|
|
88
|
+
384,
|
|
89
|
+
640
|
|
90
|
+
]);
|
|
91
|
+
expect(src).toBe("https://cdn/x/pic.jpg?width=640");
|
|
92
|
+
});
|
|
93
|
+
it("returns empty strings for a missing asset", ()=>{
|
|
94
|
+
expect(getImageSrcSet(null)).toEqual({
|
|
95
|
+
src: "",
|
|
96
|
+
srcSet: "",
|
|
97
|
+
width: 0,
|
|
98
|
+
height: 0
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=imageSrcSet.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset/imageSrcSet.test.js","sources":["../../src/asset/imageSrcSet.test.ts"],"sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { IMAGE_RESIZE_WIDTHS, getImageDimensions, getImageSrcSet } from \"./imageSrcSet.js\";\n\nconst asset = (image?: any) => ({\n id: \"f\",\n src: \"https://cdn/x/pic.jpg\",\n name: \"pic.jpg\",\n mimeType: \"image/jpeg\",\n size: 1,\n image: { width: 800, height: 600, ...image }\n});\n\ndescribe(\"getImageDimensions\", () => {\n it(\"returns intrinsic dimensions when there is no crop\", () => {\n expect(getImageDimensions(asset())).toEqual({ width: 800, height: 600 });\n });\n\n it(\"shrinks by the crop insets\", () => {\n expect(\n getImageDimensions(asset({ crop: { top: 0.1, left: 0.25, bottom: 0.1, right: 0.25 } }))\n ).toEqual({ width: 400, height: 480 });\n });\n\n it(\"derives height from a target aspect ratio\", () => {\n // Cropped width 400 @ 16:9 -> height 225.\n expect(\n getImageDimensions(asset({ crop: { top: 0, left: 0.25, bottom: 0, right: 0.25 } }), {\n aspectRatio: \"16:9\"\n })\n ).toEqual({ width: 400, height: 225 });\n });\n\n it(\"returns 0/0 when the intrinsic size is unknown\", () => {\n expect(getImageDimensions({ image: {} })).toEqual({ width: 0, height: 0 });\n expect(getImageDimensions(null)).toEqual({ width: 0, height: 0 });\n });\n});\n\ndescribe(\"getImageSrcSet\", () => {\n it(\"emits the full ladder with crop + format baked into every width\", () => {\n const { srcSet, width, height } = getImageSrcSet(\n asset({ crop: { top: 0.1, left: 0, bottom: 0, right: 0 } }),\n { format: \"auto\" }\n );\n const entries = srcSet.split(\", \");\n expect(entries).toHaveLength(IMAGE_RESIZE_WIDTHS.length);\n expect(entries[0]).toBe(\"https://cdn/x/pic.jpg?crop=0.1,0,0,0&width=128&format=auto 128w\");\n expect(width).toBe(800);\n expect(height).toBe(540);\n });\n\n it(\"trims the ladder to a fixed CSS width (incl. 2x DPR)\", () => {\n const { srcSet, src } = getImageSrcSet(asset(), { cssWidth: \"300px\" });\n const widths = srcSet.split(\", \").map(e => Number(e.match(/width=(\\d+)/)![1]));\n // Smallest set covering 300px up to 2x (600) -> 128, 384, 640.\n expect(widths).toEqual([128, 384, 640]);\n // `src` fallback is the largest emitted width.\n expect(src).toBe(\"https://cdn/x/pic.jpg?width=640\");\n });\n\n it(\"returns empty strings for a missing asset\", () => {\n expect(getImageSrcSet(null)).toEqual({ src: \"\", srcSet: \"\", width: 0, height: 0 });\n });\n});\n"],"names":["asset","image","describe","it","expect","getImageDimensions","srcSet","width","height","getImageSrcSet","entries","IMAGE_RESIZE_WIDTHS","src","widths","e","Number"],"mappings":";;AAGA,MAAMA,QAAQ,CAACC,QAAiB;QAC5B,IAAI;QACJ,KAAK;QACL,MAAM;QACN,UAAU;QACV,MAAM;QACN,OAAO;YAAE,OAAO;YAAK,QAAQ;YAAK,GAAGA,KAAK;QAAC;IAC/C;AAEAC,SAAS,sBAAsB;IAC3BC,GAAG,sDAAsD;QACrDC,OAAOC,mBAAmBL,UAAU,OAAO,CAAC;YAAE,OAAO;YAAK,QAAQ;QAAI;IAC1E;IAEAG,GAAG,8BAA8B;QAC7BC,OACIC,mBAAmBL,MAAM;YAAE,MAAM;gBAAE,KAAK;gBAAK,MAAM;gBAAM,QAAQ;gBAAK,OAAO;YAAK;QAAE,KACtF,OAAO,CAAC;YAAE,OAAO;YAAK,QAAQ;QAAI;IACxC;IAEAG,GAAG,6CAA6C;QAE5CC,OACIC,mBAAmBL,MAAM;YAAE,MAAM;gBAAE,KAAK;gBAAG,MAAM;gBAAM,QAAQ;gBAAG,OAAO;YAAK;QAAE,IAAI;YAChF,aAAa;QACjB,IACF,OAAO,CAAC;YAAE,OAAO;YAAK,QAAQ;QAAI;IACxC;IAEAG,GAAG,kDAAkD;QACjDC,OAAOC,mBAAmB;YAAE,OAAO,CAAC;QAAE,IAAI,OAAO,CAAC;YAAE,OAAO;YAAG,QAAQ;QAAE;QACxED,OAAOC,mBAAmB,OAAO,OAAO,CAAC;YAAE,OAAO;YAAG,QAAQ;QAAE;IACnE;AACJ;AAEAH,SAAS,kBAAkB;IACvBC,GAAG,mEAAmE;QAClE,MAAM,EAAEG,MAAM,EAAEC,KAAK,EAAEC,MAAM,EAAE,GAAGC,eAC9BT,MAAM;YAAE,MAAM;gBAAE,KAAK;gBAAK,MAAM;gBAAG,QAAQ;gBAAG,OAAO;YAAE;QAAE,IACzD;YAAE,QAAQ;QAAO;QAErB,MAAMU,UAAUJ,OAAO,KAAK,CAAC;QAC7BF,OAAOM,SAAS,YAAY,CAACC,oBAAoB,MAAM;QACvDP,OAAOM,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC;QACxBN,OAAOG,OAAO,IAAI,CAAC;QACnBH,OAAOI,QAAQ,IAAI,CAAC;IACxB;IAEAL,GAAG,wDAAwD;QACvD,MAAM,EAAEG,MAAM,EAAEM,GAAG,EAAE,GAAGH,eAAeT,SAAS;YAAE,UAAU;QAAQ;QACpE,MAAMa,SAASP,OAAO,KAAK,CAAC,MAAM,GAAG,CAACQ,CAAAA,IAAKC,OAAOD,EAAE,KAAK,CAAC,cAAe,CAAC,EAAE;QAE5EV,OAAOS,QAAQ,OAAO,CAAC;YAAC;YAAK;YAAK;SAAI;QAEtCT,OAAOQ,KAAK,IAAI,CAAC;IACrB;IAEAT,GAAG,6CAA6C;QAC5CC,OAAOK,eAAe,OAAO,OAAO,CAAC;YAAE,KAAK;YAAI,QAAQ;YAAI,OAAO;YAAG,QAAQ;QAAE;IACpF;AACJ"}
|
package/asset/index.d.ts
ADDED
package/asset/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Asset, AssetCategory, AssetImage } from "./types.js";
|
|
2
|
+
export declare function getAssetCategory(mimeType?: string | null): AssetCategory;
|
|
3
|
+
interface LegacyImageEdit {
|
|
4
|
+
crop?: {
|
|
5
|
+
top: number;
|
|
6
|
+
left: number;
|
|
7
|
+
bottom: number;
|
|
8
|
+
right: number;
|
|
9
|
+
};
|
|
10
|
+
hotspot?: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
width?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
};
|
|
16
|
+
alt?: string;
|
|
17
|
+
caption?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function assetImageFromLegacyEdit(edit?: LegacyImageEdit | null, dimensions?: {
|
|
20
|
+
width?: number | null;
|
|
21
|
+
height?: number | null;
|
|
22
|
+
}): AssetImage | undefined;
|
|
23
|
+
export declare function normalizeToAsset(input: unknown): Asset | null;
|
|
24
|
+
export {};
|