@betttercms/image-url 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/dist/index.cjs +81 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +50 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @betttercms/image-url
|
|
2
|
+
|
|
3
|
+
Chainable, immutable builder for optimized BetterCMS image URLs — the
|
|
4
|
+
`@sanity/image-url` of BetterCMS. Zero runtime dependencies; assembles URLs
|
|
5
|
+
against the media transform endpoint (`GET /media/:id?w&h&format&quality&fit&dpr`).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @betttercms/image-url
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import imageUrlBuilder from "@betttercms/image-url";
|
|
17
|
+
|
|
18
|
+
const urlFor = imageUrlBuilder({ baseUrl: "https://media.bettercms.ai" });
|
|
19
|
+
|
|
20
|
+
// From an asset id, a MediaAsset object, or an existing media URL:
|
|
21
|
+
urlFor(asset).width(800).format("webp").quality(80).url();
|
|
22
|
+
// → https://media.bettercms.ai/media/<id>?w=800&format=webp&quality=80
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
One-shot helper:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { imageUrl } from "@betttercms/image-url";
|
|
29
|
+
imageUrl(asset).size(400, 300).fit("cover").dpr(2).url();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Builder methods
|
|
33
|
+
|
|
34
|
+
| Method | Param → query |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `.width(n)` / `.height(n)` | `w` / `h` (rounded) |
|
|
37
|
+
| `.size(w, h)` | `w` + `h` |
|
|
38
|
+
| `.format(f)` | `format` (`webp`\|`jpeg`\|`png`\|`avif`; `jpg`→`jpeg`) |
|
|
39
|
+
| `.quality(q)` | `quality` (clamped 1–100) |
|
|
40
|
+
| `.fit(f)` | `fit` (`cover`\|`contain`\|`fill`\|`inside`\|`outside`) |
|
|
41
|
+
| `.dpr(d)` | `dpr` (clamped 1–3; omitted when 1) |
|
|
42
|
+
| `.url()` / `.toString()` | render |
|
|
43
|
+
|
|
44
|
+
`baseUrl` defaults to `https://media.bettercms.ai`. Every method returns a new
|
|
45
|
+
builder, so chains are safe to fork.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
default: () => index_default,
|
|
24
|
+
imageUrl: () => imageUrl,
|
|
25
|
+
imageUrlBuilder: () => imageUrlBuilder
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var DEFAULT_BASE_URL = "https://media.bettercms.ai";
|
|
29
|
+
function resolveId(source) {
|
|
30
|
+
const raw = typeof source === "string" ? source : source.id ?? source._id ?? source.url ?? source.src ?? "";
|
|
31
|
+
if (!raw) throw new Error("[image-url] cannot resolve an asset id from source");
|
|
32
|
+
if (!raw.includes("/")) return raw;
|
|
33
|
+
const path = raw.split("?")[0].split("#")[0];
|
|
34
|
+
const segments = path.split("/").filter(Boolean);
|
|
35
|
+
const mediaIdx = segments.indexOf("media");
|
|
36
|
+
const id = mediaIdx >= 0 ? segments[mediaIdx + 1] : segments[segments.length - 1];
|
|
37
|
+
if (!id) throw new Error(`[image-url] cannot resolve an asset id from "${raw}"`);
|
|
38
|
+
return id;
|
|
39
|
+
}
|
|
40
|
+
function clamp(value, min, max) {
|
|
41
|
+
return Math.min(max, Math.max(min, value));
|
|
42
|
+
}
|
|
43
|
+
function makeBuilder(baseUrl, id, t) {
|
|
44
|
+
const next = (patch) => makeBuilder(baseUrl, id, { ...t, ...patch });
|
|
45
|
+
const build = () => {
|
|
46
|
+
const params = new URLSearchParams();
|
|
47
|
+
if (t.w != null) params.set("w", String(Math.round(t.w)));
|
|
48
|
+
if (t.h != null) params.set("h", String(Math.round(t.h)));
|
|
49
|
+
if (t.format) params.set("format", t.format === "jpg" ? "jpeg" : t.format);
|
|
50
|
+
if (t.quality != null) params.set("quality", String(clamp(t.quality, 1, 100)));
|
|
51
|
+
if (t.fit) params.set("fit", t.fit);
|
|
52
|
+
if (t.dpr != null && t.dpr !== 1) params.set("dpr", String(clamp(t.dpr, 1, 3)));
|
|
53
|
+
const qs = params.toString();
|
|
54
|
+
return `${baseUrl}/media/${id}${qs ? `?${qs}` : ""}`;
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
width: (v) => next({ w: v }),
|
|
58
|
+
height: (v) => next({ h: v }),
|
|
59
|
+
size: (w, h) => next({ w, h }),
|
|
60
|
+
format: (v) => next({ format: v }),
|
|
61
|
+
quality: (v) => next({ quality: v }),
|
|
62
|
+
fit: (v) => next({ fit: v }),
|
|
63
|
+
dpr: (v) => next({ dpr: v }),
|
|
64
|
+
url: build,
|
|
65
|
+
toString: build
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function imageUrlBuilder(options = {}) {
|
|
69
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
70
|
+
return (source) => makeBuilder(baseUrl, resolveId(source), {});
|
|
71
|
+
}
|
|
72
|
+
function imageUrl(source, options) {
|
|
73
|
+
return imageUrlBuilder(options)(source);
|
|
74
|
+
}
|
|
75
|
+
var index_default = imageUrlBuilder;
|
|
76
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
77
|
+
0 && (module.exports = {
|
|
78
|
+
imageUrl,
|
|
79
|
+
imageUrlBuilder
|
|
80
|
+
});
|
|
81
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @betttercms/image-url — chainable, immutable builder for optimized\n * BetterCMS image URLs. Mirrors the ergonomics of `@sanity/image-url`.\n *\n * import imageUrlBuilder from \"@betttercms/image-url\";\n * const urlFor = imageUrlBuilder({ baseUrl: \"https://media.bettercms.ai\" });\n * urlFor(asset).width(800).format(\"webp\").url();\n *\n * The builder only assembles URLs against the backend media transform\n * endpoint (`GET /media/:id?w&h&format&quality&fit&dpr`). It performs no I/O.\n */\n\n/** A value the builder can resolve an asset id from. */\nexport type ImageSource =\n | string\n | { id?: string; _id?: string; url?: string; src?: string };\n\nexport type ImageFormat = \"webp\" | \"jpeg\" | \"jpg\" | \"png\" | \"avif\";\n\n/** Resize behaviour, passed through to the server-side sharp pipeline. */\nexport type ImageFit = \"cover\" | \"contain\" | \"fill\" | \"inside\" | \"outside\";\n\nexport interface ImageUrlOptions {\n /** Media host base, no trailing slash. Defaults to the prod media host. */\n baseUrl?: string;\n}\n\nconst DEFAULT_BASE_URL = \"https://media.bettercms.ai\";\n\ninterface Transform {\n w?: number;\n h?: number;\n format?: ImageFormat;\n quality?: number;\n fit?: ImageFit;\n dpr?: number;\n}\n\nexport interface ImageUrlBuilder {\n width(value: number): ImageUrlBuilder;\n height(value: number): ImageUrlBuilder;\n /** Set width and height together. */\n size(width: number, height: number): ImageUrlBuilder;\n format(value: ImageFormat): ImageUrlBuilder;\n /** Output quality, 1–100. */\n quality(value: number): ImageUrlBuilder;\n fit(value: ImageFit): ImageUrlBuilder;\n /** Device pixel ratio multiplier, 1–3. */\n dpr(value: number): ImageUrlBuilder;\n /** Render the final URL string. */\n url(): string;\n /** Alias for {@link url}. */\n toString(): string;\n}\n\n/** Resolve the asset id from any accepted source shape. */\nfunction resolveId(source: ImageSource): string {\n const raw =\n typeof source === \"string\"\n ? source\n : source.id ?? source._id ?? source.url ?? source.src ?? \"\";\n if (!raw) throw new Error(\"[image-url] cannot resolve an asset id from source\");\n\n // A bare id (no slashes) is used as-is.\n if (!raw.includes(\"/\")) return raw;\n\n // Otherwise treat it as a URL/path: prefer the segment after `/media/`,\n // falling back to the last non-empty path segment.\n const path = raw.split(\"?\")[0]!.split(\"#\")[0]!;\n const segments = path.split(\"/\").filter(Boolean);\n const mediaIdx = segments.indexOf(\"media\");\n const id = mediaIdx >= 0 ? segments[mediaIdx + 1] : segments[segments.length - 1];\n if (!id) throw new Error(`[image-url] cannot resolve an asset id from \"${raw}\"`);\n return id;\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\nfunction makeBuilder(baseUrl: string, id: string, t: Transform): ImageUrlBuilder {\n const next = (patch: Partial<Transform>): ImageUrlBuilder =>\n makeBuilder(baseUrl, id, { ...t, ...patch });\n\n const build = (): string => {\n const params = new URLSearchParams();\n if (t.w != null) params.set(\"w\", String(Math.round(t.w)));\n if (t.h != null) params.set(\"h\", String(Math.round(t.h)));\n if (t.format) params.set(\"format\", t.format === \"jpg\" ? \"jpeg\" : t.format);\n if (t.quality != null) params.set(\"quality\", String(clamp(t.quality, 1, 100)));\n if (t.fit) params.set(\"fit\", t.fit);\n if (t.dpr != null && t.dpr !== 1) params.set(\"dpr\", String(clamp(t.dpr, 1, 3)));\n const qs = params.toString();\n return `${baseUrl}/media/${id}${qs ? `?${qs}` : \"\"}`;\n };\n\n return {\n width: (v) => next({ w: v }),\n height: (v) => next({ h: v }),\n size: (w, h) => next({ w, h }),\n format: (v) => next({ format: v }),\n quality: (v) => next({ quality: v }),\n fit: (v) => next({ fit: v }),\n dpr: (v) => next({ dpr: v }),\n url: build,\n toString: build,\n };\n}\n\n/**\n * Create a builder factory bound to a media host. The returned function\n * starts a fresh, immutable chain for a given image source.\n */\nexport function imageUrlBuilder(\n options: ImageUrlOptions = {},\n): (source: ImageSource) => ImageUrlBuilder {\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n return (source: ImageSource) => makeBuilder(baseUrl, resolveId(source), {});\n}\n\n/** One-shot convenience: `imageUrl(asset, { baseUrl }).width(800).url()`. */\nexport function imageUrl(\n source: ImageSource,\n options?: ImageUrlOptions,\n): ImageUrlBuilder {\n return imageUrlBuilder(options)(source);\n}\n\nexport default imageUrlBuilder;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2BA,IAAM,mBAAmB;AA6BzB,SAAS,UAAU,QAA6B;AAC9C,QAAM,MACJ,OAAO,WAAW,WACd,SACA,OAAO,MAAM,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO;AAC7D,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,oDAAoD;AAG9E,MAAI,CAAC,IAAI,SAAS,GAAG,EAAG,QAAO;AAI/B,QAAM,OAAO,IAAI,MAAM,GAAG,EAAE,CAAC,EAAG,MAAM,GAAG,EAAE,CAAC;AAC5C,QAAM,WAAW,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAC/C,QAAM,WAAW,SAAS,QAAQ,OAAO;AACzC,QAAM,KAAK,YAAY,IAAI,SAAS,WAAW,CAAC,IAAI,SAAS,SAAS,SAAS,CAAC;AAChF,MAAI,CAAC,GAAI,OAAM,IAAI,MAAM,gDAAgD,GAAG,GAAG;AAC/E,SAAO;AACT;AAEA,SAAS,MAAM,OAAe,KAAa,KAAqB;AAC9D,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC3C;AAEA,SAAS,YAAY,SAAiB,IAAY,GAA+B;AAC/E,QAAM,OAAO,CAAC,UACZ,YAAY,SAAS,IAAI,EAAE,GAAG,GAAG,GAAG,MAAM,CAAC;AAE7C,QAAM,QAAQ,MAAc;AAC1B,UAAM,SAAS,IAAI,gBAAgB;AACnC,QAAI,EAAE,KAAK,KAAM,QAAO,IAAI,KAAK,OAAO,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC;AACxD,QAAI,EAAE,KAAK,KAAM,QAAO,IAAI,KAAK,OAAO,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC;AACxD,QAAI,EAAE,OAAQ,QAAO,IAAI,UAAU,EAAE,WAAW,QAAQ,SAAS,EAAE,MAAM;AACzE,QAAI,EAAE,WAAW,KAAM,QAAO,IAAI,WAAW,OAAO,MAAM,EAAE,SAAS,GAAG,GAAG,CAAC,CAAC;AAC7E,QAAI,EAAE,IAAK,QAAO,IAAI,OAAO,EAAE,GAAG;AAClC,QAAI,EAAE,OAAO,QAAQ,EAAE,QAAQ,EAAG,QAAO,IAAI,OAAO,OAAO,MAAM,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;AAC9E,UAAM,KAAK,OAAO,SAAS;AAC3B,WAAO,GAAG,OAAO,UAAU,EAAE,GAAG,KAAK,IAAI,EAAE,KAAK,EAAE;AAAA,EACpD;AAEA,SAAO;AAAA,IACL,OAAO,CAAC,MAAM,KAAK,EAAE,GAAG,EAAE,CAAC;AAAA,IAC3B,QAAQ,CAAC,MAAM,KAAK,EAAE,GAAG,EAAE,CAAC;AAAA,IAC5B,MAAM,CAAC,GAAG,MAAM,KAAK,EAAE,GAAG,EAAE,CAAC;AAAA,IAC7B,QAAQ,CAAC,MAAM,KAAK,EAAE,QAAQ,EAAE,CAAC;AAAA,IACjC,SAAS,CAAC,MAAM,KAAK,EAAE,SAAS,EAAE,CAAC;AAAA,IACnC,KAAK,CAAC,MAAM,KAAK,EAAE,KAAK,EAAE,CAAC;AAAA,IAC3B,KAAK,CAAC,MAAM,KAAK,EAAE,KAAK,EAAE,CAAC;AAAA,IAC3B,KAAK;AAAA,IACL,UAAU;AAAA,EACZ;AACF;AAMO,SAAS,gBACd,UAA2B,CAAC,GACc;AAC1C,QAAM,WAAW,QAAQ,WAAW,kBAAkB,QAAQ,QAAQ,EAAE;AACxE,SAAO,CAAC,WAAwB,YAAY,SAAS,UAAU,MAAM,GAAG,CAAC,CAAC;AAC5E;AAGO,SAAS,SACd,QACA,SACiB;AACjB,SAAO,gBAAgB,OAAO,EAAE,MAAM;AACxC;AAEA,IAAO,gBAAQ;","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @betttercms/image-url — chainable, immutable builder for optimized
|
|
3
|
+
* BetterCMS image URLs. Mirrors the ergonomics of `@sanity/image-url`.
|
|
4
|
+
*
|
|
5
|
+
* import imageUrlBuilder from "@betttercms/image-url";
|
|
6
|
+
* const urlFor = imageUrlBuilder({ baseUrl: "https://media.bettercms.ai" });
|
|
7
|
+
* urlFor(asset).width(800).format("webp").url();
|
|
8
|
+
*
|
|
9
|
+
* The builder only assembles URLs against the backend media transform
|
|
10
|
+
* endpoint (`GET /media/:id?w&h&format&quality&fit&dpr`). It performs no I/O.
|
|
11
|
+
*/
|
|
12
|
+
/** A value the builder can resolve an asset id from. */
|
|
13
|
+
type ImageSource = string | {
|
|
14
|
+
id?: string;
|
|
15
|
+
_id?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
src?: string;
|
|
18
|
+
};
|
|
19
|
+
type ImageFormat = "webp" | "jpeg" | "jpg" | "png" | "avif";
|
|
20
|
+
/** Resize behaviour, passed through to the server-side sharp pipeline. */
|
|
21
|
+
type ImageFit = "cover" | "contain" | "fill" | "inside" | "outside";
|
|
22
|
+
interface ImageUrlOptions {
|
|
23
|
+
/** Media host base, no trailing slash. Defaults to the prod media host. */
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
interface ImageUrlBuilder {
|
|
27
|
+
width(value: number): ImageUrlBuilder;
|
|
28
|
+
height(value: number): ImageUrlBuilder;
|
|
29
|
+
/** Set width and height together. */
|
|
30
|
+
size(width: number, height: number): ImageUrlBuilder;
|
|
31
|
+
format(value: ImageFormat): ImageUrlBuilder;
|
|
32
|
+
/** Output quality, 1–100. */
|
|
33
|
+
quality(value: number): ImageUrlBuilder;
|
|
34
|
+
fit(value: ImageFit): ImageUrlBuilder;
|
|
35
|
+
/** Device pixel ratio multiplier, 1–3. */
|
|
36
|
+
dpr(value: number): ImageUrlBuilder;
|
|
37
|
+
/** Render the final URL string. */
|
|
38
|
+
url(): string;
|
|
39
|
+
/** Alias for {@link url}. */
|
|
40
|
+
toString(): string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a builder factory bound to a media host. The returned function
|
|
44
|
+
* starts a fresh, immutable chain for a given image source.
|
|
45
|
+
*/
|
|
46
|
+
declare function imageUrlBuilder(options?: ImageUrlOptions): (source: ImageSource) => ImageUrlBuilder;
|
|
47
|
+
/** One-shot convenience: `imageUrl(asset, { baseUrl }).width(800).url()`. */
|
|
48
|
+
declare function imageUrl(source: ImageSource, options?: ImageUrlOptions): ImageUrlBuilder;
|
|
49
|
+
|
|
50
|
+
export { type ImageFit, type ImageFormat, type ImageSource, type ImageUrlBuilder, type ImageUrlOptions, imageUrlBuilder as default, imageUrl, imageUrlBuilder };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @betttercms/image-url — chainable, immutable builder for optimized
|
|
3
|
+
* BetterCMS image URLs. Mirrors the ergonomics of `@sanity/image-url`.
|
|
4
|
+
*
|
|
5
|
+
* import imageUrlBuilder from "@betttercms/image-url";
|
|
6
|
+
* const urlFor = imageUrlBuilder({ baseUrl: "https://media.bettercms.ai" });
|
|
7
|
+
* urlFor(asset).width(800).format("webp").url();
|
|
8
|
+
*
|
|
9
|
+
* The builder only assembles URLs against the backend media transform
|
|
10
|
+
* endpoint (`GET /media/:id?w&h&format&quality&fit&dpr`). It performs no I/O.
|
|
11
|
+
*/
|
|
12
|
+
/** A value the builder can resolve an asset id from. */
|
|
13
|
+
type ImageSource = string | {
|
|
14
|
+
id?: string;
|
|
15
|
+
_id?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
src?: string;
|
|
18
|
+
};
|
|
19
|
+
type ImageFormat = "webp" | "jpeg" | "jpg" | "png" | "avif";
|
|
20
|
+
/** Resize behaviour, passed through to the server-side sharp pipeline. */
|
|
21
|
+
type ImageFit = "cover" | "contain" | "fill" | "inside" | "outside";
|
|
22
|
+
interface ImageUrlOptions {
|
|
23
|
+
/** Media host base, no trailing slash. Defaults to the prod media host. */
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
interface ImageUrlBuilder {
|
|
27
|
+
width(value: number): ImageUrlBuilder;
|
|
28
|
+
height(value: number): ImageUrlBuilder;
|
|
29
|
+
/** Set width and height together. */
|
|
30
|
+
size(width: number, height: number): ImageUrlBuilder;
|
|
31
|
+
format(value: ImageFormat): ImageUrlBuilder;
|
|
32
|
+
/** Output quality, 1–100. */
|
|
33
|
+
quality(value: number): ImageUrlBuilder;
|
|
34
|
+
fit(value: ImageFit): ImageUrlBuilder;
|
|
35
|
+
/** Device pixel ratio multiplier, 1–3. */
|
|
36
|
+
dpr(value: number): ImageUrlBuilder;
|
|
37
|
+
/** Render the final URL string. */
|
|
38
|
+
url(): string;
|
|
39
|
+
/** Alias for {@link url}. */
|
|
40
|
+
toString(): string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a builder factory bound to a media host. The returned function
|
|
44
|
+
* starts a fresh, immutable chain for a given image source.
|
|
45
|
+
*/
|
|
46
|
+
declare function imageUrlBuilder(options?: ImageUrlOptions): (source: ImageSource) => ImageUrlBuilder;
|
|
47
|
+
/** One-shot convenience: `imageUrl(asset, { baseUrl }).width(800).url()`. */
|
|
48
|
+
declare function imageUrl(source: ImageSource, options?: ImageUrlOptions): ImageUrlBuilder;
|
|
49
|
+
|
|
50
|
+
export { type ImageFit, type ImageFormat, type ImageSource, type ImageUrlBuilder, type ImageUrlOptions, imageUrlBuilder as default, imageUrl, imageUrlBuilder };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://media.bettercms.ai";
|
|
3
|
+
function resolveId(source) {
|
|
4
|
+
const raw = typeof source === "string" ? source : source.id ?? source._id ?? source.url ?? source.src ?? "";
|
|
5
|
+
if (!raw) throw new Error("[image-url] cannot resolve an asset id from source");
|
|
6
|
+
if (!raw.includes("/")) return raw;
|
|
7
|
+
const path = raw.split("?")[0].split("#")[0];
|
|
8
|
+
const segments = path.split("/").filter(Boolean);
|
|
9
|
+
const mediaIdx = segments.indexOf("media");
|
|
10
|
+
const id = mediaIdx >= 0 ? segments[mediaIdx + 1] : segments[segments.length - 1];
|
|
11
|
+
if (!id) throw new Error(`[image-url] cannot resolve an asset id from "${raw}"`);
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
function clamp(value, min, max) {
|
|
15
|
+
return Math.min(max, Math.max(min, value));
|
|
16
|
+
}
|
|
17
|
+
function makeBuilder(baseUrl, id, t) {
|
|
18
|
+
const next = (patch) => makeBuilder(baseUrl, id, { ...t, ...patch });
|
|
19
|
+
const build = () => {
|
|
20
|
+
const params = new URLSearchParams();
|
|
21
|
+
if (t.w != null) params.set("w", String(Math.round(t.w)));
|
|
22
|
+
if (t.h != null) params.set("h", String(Math.round(t.h)));
|
|
23
|
+
if (t.format) params.set("format", t.format === "jpg" ? "jpeg" : t.format);
|
|
24
|
+
if (t.quality != null) params.set("quality", String(clamp(t.quality, 1, 100)));
|
|
25
|
+
if (t.fit) params.set("fit", t.fit);
|
|
26
|
+
if (t.dpr != null && t.dpr !== 1) params.set("dpr", String(clamp(t.dpr, 1, 3)));
|
|
27
|
+
const qs = params.toString();
|
|
28
|
+
return `${baseUrl}/media/${id}${qs ? `?${qs}` : ""}`;
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
width: (v) => next({ w: v }),
|
|
32
|
+
height: (v) => next({ h: v }),
|
|
33
|
+
size: (w, h) => next({ w, h }),
|
|
34
|
+
format: (v) => next({ format: v }),
|
|
35
|
+
quality: (v) => next({ quality: v }),
|
|
36
|
+
fit: (v) => next({ fit: v }),
|
|
37
|
+
dpr: (v) => next({ dpr: v }),
|
|
38
|
+
url: build,
|
|
39
|
+
toString: build
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function imageUrlBuilder(options = {}) {
|
|
43
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
44
|
+
return (source) => makeBuilder(baseUrl, resolveId(source), {});
|
|
45
|
+
}
|
|
46
|
+
function imageUrl(source, options) {
|
|
47
|
+
return imageUrlBuilder(options)(source);
|
|
48
|
+
}
|
|
49
|
+
var index_default = imageUrlBuilder;
|
|
50
|
+
export {
|
|
51
|
+
index_default as default,
|
|
52
|
+
imageUrl,
|
|
53
|
+
imageUrlBuilder
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @betttercms/image-url — chainable, immutable builder for optimized\n * BetterCMS image URLs. Mirrors the ergonomics of `@sanity/image-url`.\n *\n * import imageUrlBuilder from \"@betttercms/image-url\";\n * const urlFor = imageUrlBuilder({ baseUrl: \"https://media.bettercms.ai\" });\n * urlFor(asset).width(800).format(\"webp\").url();\n *\n * The builder only assembles URLs against the backend media transform\n * endpoint (`GET /media/:id?w&h&format&quality&fit&dpr`). It performs no I/O.\n */\n\n/** A value the builder can resolve an asset id from. */\nexport type ImageSource =\n | string\n | { id?: string; _id?: string; url?: string; src?: string };\n\nexport type ImageFormat = \"webp\" | \"jpeg\" | \"jpg\" | \"png\" | \"avif\";\n\n/** Resize behaviour, passed through to the server-side sharp pipeline. */\nexport type ImageFit = \"cover\" | \"contain\" | \"fill\" | \"inside\" | \"outside\";\n\nexport interface ImageUrlOptions {\n /** Media host base, no trailing slash. Defaults to the prod media host. */\n baseUrl?: string;\n}\n\nconst DEFAULT_BASE_URL = \"https://media.bettercms.ai\";\n\ninterface Transform {\n w?: number;\n h?: number;\n format?: ImageFormat;\n quality?: number;\n fit?: ImageFit;\n dpr?: number;\n}\n\nexport interface ImageUrlBuilder {\n width(value: number): ImageUrlBuilder;\n height(value: number): ImageUrlBuilder;\n /** Set width and height together. */\n size(width: number, height: number): ImageUrlBuilder;\n format(value: ImageFormat): ImageUrlBuilder;\n /** Output quality, 1–100. */\n quality(value: number): ImageUrlBuilder;\n fit(value: ImageFit): ImageUrlBuilder;\n /** Device pixel ratio multiplier, 1–3. */\n dpr(value: number): ImageUrlBuilder;\n /** Render the final URL string. */\n url(): string;\n /** Alias for {@link url}. */\n toString(): string;\n}\n\n/** Resolve the asset id from any accepted source shape. */\nfunction resolveId(source: ImageSource): string {\n const raw =\n typeof source === \"string\"\n ? source\n : source.id ?? source._id ?? source.url ?? source.src ?? \"\";\n if (!raw) throw new Error(\"[image-url] cannot resolve an asset id from source\");\n\n // A bare id (no slashes) is used as-is.\n if (!raw.includes(\"/\")) return raw;\n\n // Otherwise treat it as a URL/path: prefer the segment after `/media/`,\n // falling back to the last non-empty path segment.\n const path = raw.split(\"?\")[0]!.split(\"#\")[0]!;\n const segments = path.split(\"/\").filter(Boolean);\n const mediaIdx = segments.indexOf(\"media\");\n const id = mediaIdx >= 0 ? segments[mediaIdx + 1] : segments[segments.length - 1];\n if (!id) throw new Error(`[image-url] cannot resolve an asset id from \"${raw}\"`);\n return id;\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\nfunction makeBuilder(baseUrl: string, id: string, t: Transform): ImageUrlBuilder {\n const next = (patch: Partial<Transform>): ImageUrlBuilder =>\n makeBuilder(baseUrl, id, { ...t, ...patch });\n\n const build = (): string => {\n const params = new URLSearchParams();\n if (t.w != null) params.set(\"w\", String(Math.round(t.w)));\n if (t.h != null) params.set(\"h\", String(Math.round(t.h)));\n if (t.format) params.set(\"format\", t.format === \"jpg\" ? \"jpeg\" : t.format);\n if (t.quality != null) params.set(\"quality\", String(clamp(t.quality, 1, 100)));\n if (t.fit) params.set(\"fit\", t.fit);\n if (t.dpr != null && t.dpr !== 1) params.set(\"dpr\", String(clamp(t.dpr, 1, 3)));\n const qs = params.toString();\n return `${baseUrl}/media/${id}${qs ? `?${qs}` : \"\"}`;\n };\n\n return {\n width: (v) => next({ w: v }),\n height: (v) => next({ h: v }),\n size: (w, h) => next({ w, h }),\n format: (v) => next({ format: v }),\n quality: (v) => next({ quality: v }),\n fit: (v) => next({ fit: v }),\n dpr: (v) => next({ dpr: v }),\n url: build,\n toString: build,\n };\n}\n\n/**\n * Create a builder factory bound to a media host. The returned function\n * starts a fresh, immutable chain for a given image source.\n */\nexport function imageUrlBuilder(\n options: ImageUrlOptions = {},\n): (source: ImageSource) => ImageUrlBuilder {\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n return (source: ImageSource) => makeBuilder(baseUrl, resolveId(source), {});\n}\n\n/** One-shot convenience: `imageUrl(asset, { baseUrl }).width(800).url()`. */\nexport function imageUrl(\n source: ImageSource,\n options?: ImageUrlOptions,\n): ImageUrlBuilder {\n return imageUrlBuilder(options)(source);\n}\n\nexport default imageUrlBuilder;\n"],"mappings":";AA2BA,IAAM,mBAAmB;AA6BzB,SAAS,UAAU,QAA6B;AAC9C,QAAM,MACJ,OAAO,WAAW,WACd,SACA,OAAO,MAAM,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO;AAC7D,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,oDAAoD;AAG9E,MAAI,CAAC,IAAI,SAAS,GAAG,EAAG,QAAO;AAI/B,QAAM,OAAO,IAAI,MAAM,GAAG,EAAE,CAAC,EAAG,MAAM,GAAG,EAAE,CAAC;AAC5C,QAAM,WAAW,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAC/C,QAAM,WAAW,SAAS,QAAQ,OAAO;AACzC,QAAM,KAAK,YAAY,IAAI,SAAS,WAAW,CAAC,IAAI,SAAS,SAAS,SAAS,CAAC;AAChF,MAAI,CAAC,GAAI,OAAM,IAAI,MAAM,gDAAgD,GAAG,GAAG;AAC/E,SAAO;AACT;AAEA,SAAS,MAAM,OAAe,KAAa,KAAqB;AAC9D,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC3C;AAEA,SAAS,YAAY,SAAiB,IAAY,GAA+B;AAC/E,QAAM,OAAO,CAAC,UACZ,YAAY,SAAS,IAAI,EAAE,GAAG,GAAG,GAAG,MAAM,CAAC;AAE7C,QAAM,QAAQ,MAAc;AAC1B,UAAM,SAAS,IAAI,gBAAgB;AACnC,QAAI,EAAE,KAAK,KAAM,QAAO,IAAI,KAAK,OAAO,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC;AACxD,QAAI,EAAE,KAAK,KAAM,QAAO,IAAI,KAAK,OAAO,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC;AACxD,QAAI,EAAE,OAAQ,QAAO,IAAI,UAAU,EAAE,WAAW,QAAQ,SAAS,EAAE,MAAM;AACzE,QAAI,EAAE,WAAW,KAAM,QAAO,IAAI,WAAW,OAAO,MAAM,EAAE,SAAS,GAAG,GAAG,CAAC,CAAC;AAC7E,QAAI,EAAE,IAAK,QAAO,IAAI,OAAO,EAAE,GAAG;AAClC,QAAI,EAAE,OAAO,QAAQ,EAAE,QAAQ,EAAG,QAAO,IAAI,OAAO,OAAO,MAAM,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;AAC9E,UAAM,KAAK,OAAO,SAAS;AAC3B,WAAO,GAAG,OAAO,UAAU,EAAE,GAAG,KAAK,IAAI,EAAE,KAAK,EAAE;AAAA,EACpD;AAEA,SAAO;AAAA,IACL,OAAO,CAAC,MAAM,KAAK,EAAE,GAAG,EAAE,CAAC;AAAA,IAC3B,QAAQ,CAAC,MAAM,KAAK,EAAE,GAAG,EAAE,CAAC;AAAA,IAC5B,MAAM,CAAC,GAAG,MAAM,KAAK,EAAE,GAAG,EAAE,CAAC;AAAA,IAC7B,QAAQ,CAAC,MAAM,KAAK,EAAE,QAAQ,EAAE,CAAC;AAAA,IACjC,SAAS,CAAC,MAAM,KAAK,EAAE,SAAS,EAAE,CAAC;AAAA,IACnC,KAAK,CAAC,MAAM,KAAK,EAAE,KAAK,EAAE,CAAC;AAAA,IAC3B,KAAK,CAAC,MAAM,KAAK,EAAE,KAAK,EAAE,CAAC;AAAA,IAC3B,KAAK;AAAA,IACL,UAAU;AAAA,EACZ;AACF;AAMO,SAAS,gBACd,UAA2B,CAAC,GACc;AAC1C,QAAM,WAAW,QAAQ,WAAW,kBAAkB,QAAQ,QAAQ,EAAE;AACxE,SAAO,CAAC,WAAwB,YAAY,SAAS,UAAU,MAAM,GAAG,CAAC,CAAC;AAC5E;AAGO,SAAS,SACd,QACA,SACiB;AACjB,SAAO,gBAAgB,OAAO,EAAE,MAAM;AACxC;AAEA,IAAO,gBAAQ;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@betttercms/image-url",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"description": "Build optimized BetterCMS image URLs with a chainable, immutable builder.",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"require": "./dist/index.cjs",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@betttercms/types": "^1.2.0",
|
|
30
|
+
"tsup": "^8.5.1",
|
|
31
|
+
"vitest": "^4.1.4"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public",
|
|
35
|
+
"registry": "https://registry.npmjs.org/"
|
|
36
|
+
}
|
|
37
|
+
}
|