@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 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":[]}
@@ -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 };
@@ -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
+ }