@sproux/media-sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @sproux/media-sdk
2
2
 
3
- Media URL builder library for Sproux — build image variant URLs, video HLS playlist URLs, and thumbnails from original media URLs.
3
+ Media URL builder library for Sproux — build image variant URLs, video HLS playlist URLs, and thumbnails from object keys using a singleton pattern.
4
4
 
5
5
  ## Installation
6
6
 
@@ -18,138 +18,145 @@ yarn add @sproux/media-sdk
18
18
  ## Quick Start
19
19
 
20
20
  ```typescript
21
- import {
22
- buildImageVariantUrl,
23
- buildVideoHlsUrl,
24
- buildVideoThumbnailUrl,
25
- parseMediaUrl,
26
- } from '@sproux/media-sdk';
21
+ import { SprouxMedia, IMAGE_FORMAT, IMAGE_RESIZE_TYPE } from '@sproux/media-sdk';
27
22
 
28
- // Build an image variant URL
29
- const imageUrl = buildImageVariantUrl(
30
- 'https://cdn.example.com/avatar/image/usr-1/profile.jpg',
31
- { width: 200, height: 200, resizeType: 'fit', quality: 80 }
32
- );
33
- // → "https://cdn.example.com/avatar/image/usr-1/profile-200x200-fit-q80.webp"
23
+ // Initialize once (e.g. at app startup)
24
+ const media = SprouxMedia.init({ cdnUrl: 'https://cdn.example.com' });
25
+
26
+ // Build an image variant URL using enums
27
+ const imageUrl = media.getImageUrl('avatar/image/usr-1/abc', {
28
+ extension: IMAGE_FORMAT.WEBP,
29
+ width: 200,
30
+ height: 200,
31
+ resizeType: IMAGE_RESIZE_TYPE.FIT,
32
+ quality: 80,
33
+ });
34
+ // → "https://cdn.example.com/avatar/image/usr-1/abc-200x200-fit-q80.webp"
34
35
 
35
36
  // Build a video HLS playlist URL
36
- const hlsUrl = buildVideoHlsUrl(
37
- 'https://cdn.example.com/gallery/video/usr-1/intro.mp4'
38
- );
37
+ const hlsUrl = media.getVideoHlsUrl('gallery/video/usr-1/intro');
39
38
  // → "https://cdn.example.com/gallery/video/usr-1/intro/playlist.m3u8"
40
39
 
41
40
  // Build a video thumbnail URL
42
- const thumbnailUrl = buildVideoThumbnailUrl(
43
- 'https://cdn.example.com/gallery/video/usr-1/intro.mp4'
44
- );
41
+ const thumbUrl = media.getVideoThumbnailUrl('gallery/video/usr-1/intro');
45
42
  // → "https://cdn.example.com/gallery/video/usr-1/intro/thumbnail.webp"
46
43
  ```
47
44
 
48
- ## API Reference
45
+ ## Singleton Usage
49
46
 
50
- ### `parseMediaUrl(url: string): ParsedMediaUrl`
47
+ ```typescript
48
+ import { SprouxMedia, IMAGE_FORMAT, IMAGE_RESIZE_TYPE } from '@sproux/media-sdk';
51
49
 
52
- Parse a media URL into its structured components.
50
+ // Initialize once at startup
51
+ SprouxMedia.init({ cdnUrl: 'https://cdn.example.com' });
53
52
 
54
- **Expected URL pattern:** `{cdnBase}/{purpose}/{type}/{userId}/{name}.{extension}`
53
+ // Access from anywhere via getInstance()
54
+ const media = SprouxMedia.getInstance();
55
+ const url = media.getImageUrl('avatar/image/usr-1/photo', {
56
+ extension: IMAGE_FORMAT.WEBP,
57
+ width: 400,
58
+ height: 300,
59
+ resizeType: IMAGE_RESIZE_TYPE.FILL,
60
+ });
61
+ ```
55
62
 
56
- ```typescript
57
- import { parseMediaUrl } from '@sproux/media-sdk';
63
+ > Calling `getInstance()` before `init()` will throw an error.
58
64
 
59
- const parsed = parseMediaUrl('https://cdn.example.com/avatar/image/usr-1/abc.jpg');
60
- // {
61
- // originalUrl: "https://cdn.example.com/avatar/image/usr-1/abc.jpg",
62
- // cdnBase: "https://cdn.example.com",
63
- // objectKey: "avatar/image/usr-1/abc.jpg",
64
- // purpose: "avatar",
65
- // type: "image",
66
- // userId: "usr-1",
67
- // name: "abc",
68
- // extension: "jpg"
69
- // }
65
+ ## API Reference
66
+
67
+ ### `SprouxMedia.init(config: SprouxMediaConfig): SprouxMedia`
68
+
69
+ Initialize the singleton with CDN configuration. Returns the singleton instance.
70
+
71
+ ```typescript
72
+ const media = SprouxMedia.init({ cdnUrl: 'https://cdn.example.com' });
70
73
  ```
71
74
 
72
- ### `buildImageVariantUrl(originalUrl: string, options: ImageVariantOptions): string`
75
+ ### `SprouxMedia.getInstance(): SprouxMedia`
76
+
77
+ Returns the existing singleton instance. Throws if `init()` has not been called.
78
+
79
+ ### `media.getImageUrl(objectKey: string, options: ImageUrlOptions): string`
73
80
 
74
81
  Build a CDN URL for an image variant with specific dimensions, resize type, and quality.
75
82
 
83
+ - `objectKey` — Object key **without** extension (e.g. `"avatar/image/usr-1/abc"`)
84
+
76
85
  ```typescript
77
- import { buildImageVariantUrl } from '@sproux/media-sdk';
78
-
79
- const url = buildImageVariantUrl(
80
- 'https://cdn.example.com/avatar/image/usr-1/photo.jpg',
81
- {
82
- width: 400,
83
- height: 300,
84
- resizeType: 'fill',
85
- format: 'webp', // optional, defaults to 'webp'
86
- quality: 85, // optional
87
- }
88
- );
86
+ import { IMAGE_FORMAT, IMAGE_RESIZE_TYPE } from '@sproux/media-sdk';
87
+
88
+ media.getImageUrl('avatar/image/usr-1/photo', {
89
+ extension: IMAGE_FORMAT.WEBP,
90
+ width: 400,
91
+ height: 300,
92
+ resizeType: IMAGE_RESIZE_TYPE.FILL,
93
+ quality: 85,
94
+ });
89
95
  // → "https://cdn.example.com/avatar/image/usr-1/photo-400x300-fill-q85.webp"
90
96
  ```
91
97
 
92
- #### ImageVariantOptions
98
+ #### ImageUrlOptions
93
99
 
94
- | Property | Type | Required | Description |
95
- | ------------ | ----------------- | -------- | ------------------------------------- |
96
- | `width` | `number` | Yes | Target width in pixels (1-4096) |
97
- | `height` | `number` | Yes | Target height in pixels (1-4096) |
98
- | `resizeType` | `ImageResizeType` | Yes | Resize strategy: `fit`, `fill`, `auto` |
99
- | `format` | `ImageFormat` | No | Output format (default: `webp`) |
100
- | `quality` | `number` | No | Quality 1-100 |
100
+ | Property | Type | Required | Description |
101
+ | ------------ | ----------------- | -------- | -------------------------------------- |
102
+ | `extension` | `ImageFormat` | Yes | Output format (`webp`, `avif`, `jpeg`, `png`, `gif`, `ico`, `svg`, `jpg`) |
103
+ | `width` | `number` | Yes | Target width in pixels (14096) |
104
+ | `height` | `number` | Yes | Target height in pixels (1–4096) |
105
+ | `resizeType` | `ImageResizeType` | Yes | Resize strategy: `fit`, `fill`, `force`, `fill-down`, `auto` |
106
+ | `quality` | `number` | No | Quality 1100 |
101
107
 
102
- ### `buildVideoHlsUrl(originalUrl: string): string`
108
+ ### `media.getVideoHlsUrl(objectKey: string): string`
103
109
 
104
- Build an HLS playlist URL from an original video URL.
110
+ Build an HLS playlist URL from an object key (without extension).
105
111
 
106
112
  ```typescript
107
- import { buildVideoHlsUrl } from '@sproux/media-sdk';
108
-
109
- const hlsUrl = buildVideoHlsUrl(
110
- 'https://cdn.example.com/gallery/video/usr-1/video.mp4'
111
- );
113
+ media.getVideoHlsUrl('gallery/video/usr-1/video');
112
114
  // → "https://cdn.example.com/gallery/video/usr-1/video/playlist.m3u8"
113
115
  ```
114
116
 
115
- ### `buildVideoThumbnailUrl(originalUrl: string): string`
117
+ ### `media.getVideoThumbnailUrl(objectKey: string): string`
116
118
 
117
- Build a thumbnail URL from an original video URL.
119
+ Build a thumbnail URL from an object key (without extension).
118
120
 
119
121
  ```typescript
120
- import { buildVideoThumbnailUrl } from '@sproux/media-sdk';
121
-
122
- const thumbnailUrl = buildVideoThumbnailUrl(
123
- 'https://cdn.example.com/gallery/video/usr-1/video.mp4'
124
- );
122
+ media.getVideoThumbnailUrl('gallery/video/usr-1/video');
125
123
  // → "https://cdn.example.com/gallery/video/usr-1/video/thumbnail.webp"
126
124
  ```
127
125
 
128
- ### `buildVariantString(options: ImageVariantOptions): string`
126
+ ## Enums
129
127
 
130
- Build a deterministic variant string from image variant options. This is used internally by `buildImageVariantUrl`.
128
+ Use the provided enums for type-safe image options:
131
129
 
132
130
  ```typescript
133
- import { buildVariantString } from '@sproux/media-sdk';
134
-
135
- buildVariantString({ width: 200, height: 200, resizeType: 'fit' });
136
- // → "200x200-fit"
137
-
138
- buildVariantString({ width: 200, height: 200, resizeType: 'fit', quality: 80 });
139
- // → "200x200-fit-q80"
131
+ import { IMAGE_FORMAT, IMAGE_RESIZE_TYPE } from '@sproux/media-sdk';
132
+
133
+ // IMAGE_FORMAT
134
+ IMAGE_FORMAT.WEBP // 'webp'
135
+ IMAGE_FORMAT.AVIF // 'avif'
136
+ IMAGE_FORMAT.JPEG // 'jpeg'
137
+ IMAGE_FORMAT.PNG // 'png'
138
+ IMAGE_FORMAT.GIF // 'gif'
139
+ IMAGE_FORMAT.ICO // 'ico'
140
+ IMAGE_FORMAT.SVG // 'svg'
141
+ IMAGE_FORMAT.JPG // 'jpg'
142
+
143
+ // IMAGE_RESIZE_TYPE
144
+ IMAGE_RESIZE_TYPE.FIT // 'fit'
145
+ IMAGE_RESIZE_TYPE.FILL // 'fill'
146
+ IMAGE_RESIZE_TYPE.FORCE // 'force'
147
+ IMAGE_RESIZE_TYPE.FILL_DOWN // 'fill-down'
148
+ IMAGE_RESIZE_TYPE.AUTO // 'auto'
140
149
  ```
141
150
 
142
151
  ## Constants
143
152
 
144
- The SDK exports constants for type-safe usage:
145
-
146
153
  ```typescript
147
154
  import {
148
- MEDIA_TYPE, // { IMAGE: 'image', VIDEO: 'video' }
149
- MEDIA_PURPOSE, // { AVATAR: 'avatar', HERO: 'hero', GALLERY: 'gallery' }
150
- MEDIA_STATUS, // { PENDING, UPLOADED, PROCESSING, READY, ERROR }
151
- IMAGE_FORMAT, // { WEBP, AVIF, JPEG, PNG, JPG }
152
- IMAGE_RESIZE_TYPE // { FIT, FILL, AUTO }
155
+ MEDIA_TYPE, // { IMAGE: 'image', VIDEO: 'video' }
156
+ MEDIA_PURPOSE, // { AVATAR: 'avatar', HERO: 'hero', GALLERY: 'gallery' }
157
+ MEDIA_STATUS, // { PENDING, UPLOADED, PROCESSING, READY, ERROR }
158
+ R2_PATH, // { HLS_PLAYLIST: 'playlist.m3u8', THUMBNAIL: 'thumbnail.webp' }
159
+ MEDIA_DEFAULTS, // { IMAGE_FORMAT: 'webp' }
153
160
  } from '@sproux/media-sdk';
154
161
  ```
155
162
 
@@ -157,50 +164,35 @@ import {
157
164
 
158
165
  ```typescript
159
166
  import type {
160
- MediaType, // 'image' | 'video'
161
- MediaPurpose, // 'avatar' | 'hero' | 'gallery'
162
- MediaStatus, // 'pending' | 'uploaded' | 'processing' | 'ready' | 'error'
163
- ImageFormat, // 'webp' | 'avif' | 'jpeg' | 'png' | 'jpg'
164
- ImageResizeType, // 'fit' | 'fill' | 'auto'
165
- ParsedMediaUrl,
166
- ImageVariantOptions,
167
+ ImageFormat, // 'webp' | 'avif' | 'jpeg' | 'png' | 'gif' | 'ico' | 'svg' | 'jpg'
168
+ ImageResizeType, // 'fit' | 'fill' | 'force' | 'fill-down' | 'auto'
169
+ SprouxMediaConfig, // { cdnUrl: string }
170
+ ImageUrlOptions, // { extension, width, height, resizeType, quality? }
167
171
  } from '@sproux/media-sdk';
168
172
  ```
169
173
 
170
- ## URL Structure
171
-
172
- This SDK expects media URLs to follow this structure:
173
-
174
- ```
175
- {cdnBase}/{purpose}/{type}/{userId}/{filename}
176
- ```
174
+ > `ImageFormat` and `ImageResizeType` are string union types derived from their respective enums. You can use either the enum values (`IMAGE_FORMAT.WEBP`) or raw strings (`'webp'`).
177
175
 
178
- | Segment | Description | Valid Values |
179
- | ---------- | ----------------------------------------- | --------------------------------- |
180
- | `cdnBase` | CDN origin including protocol | Any valid URL origin |
181
- | `purpose` | Media use case | `avatar`, `hero`, `gallery` |
182
- | `type` | Media type | `image`, `video` |
183
- | `userId` | User identifier | Any string |
184
- | `filename` | File name with extension | `{name}.{extension}` |
176
+ ## URL Structure
185
177
 
186
- ### Image Variant URL Structure
178
+ ### Image Variant URL
187
179
 
188
180
  ```
189
- {cdnBase}/{purpose}/image/{userId}/{name}-{width}x{height}-{resizeType}[-q{quality}].{format}
181
+ {cdnUrl}/{objectKey}-{width}x{height}-{resizeType}[-q{quality}].{extension}
190
182
  ```
191
183
 
192
184
  Example: `https://cdn.example.com/avatar/image/usr-1/photo-200x200-fit-q80.webp`
193
185
 
194
- ### Video HLS URL Structure
186
+ ### Video HLS URL
195
187
 
196
188
  ```
197
- {cdnBase}/{purpose}/video/{userId}/{name}/playlist.m3u8
189
+ {cdnUrl}/{objectKey}/playlist.m3u8
198
190
  ```
199
191
 
200
- ### Video Thumbnail URL Structure
192
+ ### Video Thumbnail URL
201
193
 
202
194
  ```
203
- {cdnBase}/{purpose}/video/{userId}/{name}/thumbnail.webp
195
+ {cdnUrl}/{objectKey}/thumbnail.webp
204
196
  ```
205
197
 
206
198
  ## License
package/dist/index.cjs CHANGED
@@ -27,11 +27,7 @@ __export(index_exports, {
27
27
  MEDIA_STATUS: () => MEDIA_STATUS,
28
28
  MEDIA_TYPE: () => MEDIA_TYPE,
29
29
  R2_PATH: () => R2_PATH,
30
- buildImageVariantUrl: () => buildImageVariantUrl,
31
- buildVariantString: () => buildVariantString,
32
- buildVideoHlsUrl: () => buildVideoHlsUrl,
33
- buildVideoThumbnailUrl: () => buildVideoThumbnailUrl,
34
- parseMediaUrl: () => parseMediaUrl
30
+ SprouxMedia: () => SprouxMedia
35
31
  });
36
32
  module.exports = __toCommonJS(index_exports);
37
33
 
@@ -52,136 +48,126 @@ var MEDIA_STATUS = {
52
48
  READY: "ready",
53
49
  ERROR: "error"
54
50
  };
55
- var IMAGE_FORMAT = {
56
- WEBP: "webp",
57
- AVIF: "avif",
58
- JPEG: "jpeg",
59
- PNG: "png",
60
- JPG: "jpg"
61
- };
62
- var IMAGE_RESIZE_TYPE = {
63
- FIT: "fit",
64
- FILL: "fill",
65
- AUTO: "auto"
66
- };
51
+ var IMAGE_FORMAT = /* @__PURE__ */ ((IMAGE_FORMAT2) => {
52
+ IMAGE_FORMAT2["WEBP"] = "webp";
53
+ IMAGE_FORMAT2["AVIF"] = "avif";
54
+ IMAGE_FORMAT2["JPEG"] = "jpeg";
55
+ IMAGE_FORMAT2["PNG"] = "png";
56
+ IMAGE_FORMAT2["GIF"] = "gif";
57
+ IMAGE_FORMAT2["ICO"] = "ico";
58
+ IMAGE_FORMAT2["SVG"] = "svg";
59
+ IMAGE_FORMAT2["JPG"] = "jpg";
60
+ return IMAGE_FORMAT2;
61
+ })(IMAGE_FORMAT || {});
62
+ var IMAGE_RESIZE_TYPE = /* @__PURE__ */ ((IMAGE_RESIZE_TYPE2) => {
63
+ IMAGE_RESIZE_TYPE2["FIT"] = "fit";
64
+ IMAGE_RESIZE_TYPE2["FILL"] = "fill";
65
+ IMAGE_RESIZE_TYPE2["FORCE"] = "force";
66
+ IMAGE_RESIZE_TYPE2["FILL_DOWN"] = "fill-down";
67
+ IMAGE_RESIZE_TYPE2["AUTO"] = "auto";
68
+ return IMAGE_RESIZE_TYPE2;
69
+ })(IMAGE_RESIZE_TYPE || {});
67
70
  var R2_PATH = {
68
71
  HLS_PLAYLIST: "playlist.m3u8",
69
72
  THUMBNAIL: "thumbnail.webp"
70
73
  };
71
74
  var MEDIA_DEFAULTS = {
72
- IMAGE_FORMAT: IMAGE_FORMAT.WEBP
75
+ IMAGE_FORMAT: "webp" /* WEBP */
73
76
  };
74
77
 
75
- // src/parse-media-url.ts
76
- var VALID_PURPOSES = new Set(Object.values(MEDIA_PURPOSE));
77
- var VALID_TYPES = new Set(Object.values(MEDIA_TYPE));
78
- function parseMediaUrl(originalUrl) {
79
- let url;
80
- try {
81
- url = new URL(originalUrl);
82
- } catch {
83
- throw new Error(`Invalid media URL: "${originalUrl}"`);
84
- }
85
- const pathname = url.pathname.startsWith("/") ? url.pathname.slice(1) : url.pathname;
86
- const segments = pathname.split("/");
87
- if (segments.length < 4) {
88
- throw new Error(
89
- `Invalid media URL path: expected at least 4 segments (purpose/type/userId/filename), got ${segments.length} in "${pathname}"`
90
- );
91
- }
92
- const [purpose, type, userId, filename] = segments;
93
- if (!VALID_PURPOSES.has(purpose)) {
94
- throw new Error(
95
- `Invalid media purpose: "${purpose}". Expected one of: ${[...VALID_PURPOSES].join(", ")}`
96
- );
97
- }
98
- if (!VALID_TYPES.has(type)) {
99
- throw new Error(
100
- `Invalid media type: "${type}". Expected one of: ${[...VALID_TYPES].join(", ")}`
101
- );
102
- }
103
- if (!userId) {
104
- throw new Error("Missing userId segment in media URL");
105
- }
106
- if (!filename) {
107
- throw new Error("Missing filename segment in media URL");
108
- }
109
- const dotIndex = filename.lastIndexOf(".");
110
- if (dotIndex <= 0) {
111
- throw new Error(`Invalid filename: "${filename}". Expected format: "name.extension"`);
112
- }
113
- const name = filename.slice(0, dotIndex);
114
- const extension = filename.slice(dotIndex + 1);
115
- if (!extension) {
116
- throw new Error(`Missing file extension in filename: "${filename}"`);
117
- }
118
- const cdnBase = `${url.protocol}//${url.host}`;
119
- const objectKey = pathname;
120
- return {
121
- originalUrl,
122
- cdnBase,
123
- objectKey,
124
- purpose,
125
- type,
126
- userId,
127
- name,
128
- extension
129
- };
130
- }
131
-
132
- // src/build-variant-string.ts
133
- function buildVariantString(options) {
134
- validateVariantOptions(options);
135
- const parts = [`${options.width}x${options.height}`, options.resizeType];
136
- if (options.quality != null) {
137
- parts.push(`q${options.quality}`);
138
- }
139
- return parts.join("-");
140
- }
141
- function validateVariantOptions(options) {
142
- if (!Number.isInteger(options.width) || options.width < 1 || options.width > 4096) {
143
- throw new Error(`Invalid width: ${options.width}. Must be an integer between 1 and 4096.`);
144
- }
145
- if (!Number.isInteger(options.height) || options.height < 1 || options.height > 4096) {
146
- throw new Error(`Invalid height: ${options.height}. Must be an integer between 1 and 4096.`);
78
+ // src/sproux-media.ts
79
+ var SprouxMedia = class _SprouxMedia {
80
+ static instance = null;
81
+ cdnUrl;
82
+ constructor(config) {
83
+ this.cdnUrl = config.cdnUrl.replace(/\/+$/, "");
84
+ }
85
+ /**
86
+ * Initialize the singleton with CDN configuration.
87
+ * Returns the singleton instance.
88
+ */
89
+ static init(config) {
90
+ if (_SprouxMedia.instance) {
91
+ return _SprouxMedia.instance;
92
+ }
93
+ return _SprouxMedia.instance = new _SprouxMedia(config);
94
+ }
95
+ /**
96
+ * Returns the existing singleton instance.
97
+ * Throws if `init()` has not been called.
98
+ */
99
+ static getInstance() {
100
+ if (!_SprouxMedia.instance) {
101
+ throw new Error("SprouxMedia has not been initialized. Call SprouxMedia.init() first.");
102
+ }
103
+ return _SprouxMedia.instance;
104
+ }
105
+ /**
106
+ * Build a CDN URL for an image variant.
107
+ *
108
+ * @param objectKey - Object key without extension (e.g. "avatar/image/usr-1/abc")
109
+ * @param options - Image variant options including extension, dimensions, resize type, and optional quality
110
+ * @returns Full CDN URL for the image variant
111
+ *
112
+ * @example
113
+ * media.getImageUrl('avatar/image/usr-1/abc', {
114
+ * extension: 'webp', width: 200, height: 200, resizeType: 'fit', quality: 80,
115
+ * });
116
+ * // "https://cdn.example.com/avatar/image/usr-1/abc-200x200-fit-q80.webp"
117
+ */
118
+ getImageUrl(objectKey, options) {
119
+ this.validateImageOptions(options);
120
+ const variant = this.buildVariantString(options);
121
+ return `${this.cdnUrl}/${objectKey}-${variant}.${options.extension}`;
122
+ }
123
+ /**
124
+ * Build a CDN URL for a video HLS playlist.
125
+ *
126
+ * @param objectKey - Object key without extension (e.g. "gallery/video/usr-1/xyz")
127
+ * @returns Full CDN URL for the HLS playlist
128
+ */
129
+ getVideoHlsUrl(objectKey) {
130
+ return `${this.cdnUrl}/${objectKey}/${R2_PATH.HLS_PLAYLIST}`;
131
+ }
132
+ /**
133
+ * Build a CDN URL for a video thumbnail.
134
+ *
135
+ * @param objectKey - Object key without extension (e.g. "gallery/video/usr-1/xyz")
136
+ * @returns Full CDN URL for the thumbnail
137
+ */
138
+ getVideoThumbnailUrl(objectKey) {
139
+ return `${this.cdnUrl}/${objectKey}/${R2_PATH.THUMBNAIL}`;
140
+ }
141
+ /**
142
+ * Build a deterministic variant string from image options.
143
+ *
144
+ * Format: {width}x{height}-{resizeType}[-q{quality}]
145
+ */
146
+ buildVariantString(options) {
147
+ const parts = [`${options.width}x${options.height}`, options.resizeType];
148
+ if (options.quality != null) {
149
+ parts.push(`q${options.quality}`);
150
+ }
151
+ return parts.join("-");
147
152
  }
148
- if (options.quality != null) {
149
- if (!Number.isInteger(options.quality) || options.quality < 1 || options.quality > 100) {
153
+ validateImageOptions(options) {
154
+ if (!Number.isInteger(options.width) || options.width < 1 || options.width > 4096) {
155
+ throw new Error(`Invalid width: ${options.width}. Must be an integer between 1 and 4096.`);
156
+ }
157
+ if (!Number.isInteger(options.height) || options.height < 1 || options.height > 4096) {
150
158
  throw new Error(
151
- `Invalid quality: ${options.quality}. Must be an integer between 1 and 100.`
159
+ `Invalid height: ${options.height}. Must be an integer between 1 and 4096.`
152
160
  );
153
161
  }
162
+ if (options.quality != null) {
163
+ if (!Number.isInteger(options.quality) || options.quality < 1 || options.quality > 100) {
164
+ throw new Error(
165
+ `Invalid quality: ${options.quality}. Must be an integer between 1 and 100.`
166
+ );
167
+ }
168
+ }
154
169
  }
155
- }
156
-
157
- // src/build-image-variant-url.ts
158
- function buildImageVariantUrl(originalUrl, options) {
159
- const parsed = parseMediaUrl(originalUrl);
160
- if (parsed.type !== "image") {
161
- throw new Error(`Expected image URL, got type "${parsed.type}" in "${originalUrl}"`);
162
- }
163
- const variant = buildVariantString(options);
164
- const format = options.format ?? MEDIA_DEFAULTS.IMAGE_FORMAT;
165
- return `${parsed.cdnBase}/${parsed.purpose}/${parsed.type}/${parsed.userId}/${parsed.name}-${variant}.${format}`;
166
- }
167
-
168
- // src/build-video-hls-url.ts
169
- function buildVideoHlsUrl(originalUrl) {
170
- const parsed = parseMediaUrl(originalUrl);
171
- if (parsed.type !== "video") {
172
- throw new Error(`Expected video URL, got type "${parsed.type}" in "${originalUrl}"`);
173
- }
174
- return `${parsed.cdnBase}/${parsed.purpose}/video/${parsed.userId}/${parsed.name}/${R2_PATH.HLS_PLAYLIST}`;
175
- }
176
-
177
- // src/build-video-thumbnail-url.ts
178
- function buildVideoThumbnailUrl(originalUrl) {
179
- const parsed = parseMediaUrl(originalUrl);
180
- if (parsed.type !== "video") {
181
- throw new Error(`Expected video URL, got type "${parsed.type}" in "${originalUrl}"`);
182
- }
183
- return `${parsed.cdnBase}/${parsed.purpose}/video/${parsed.userId}/${parsed.name}/${R2_PATH.THUMBNAIL}`;
184
- }
170
+ };
185
171
  // Annotate the CommonJS export names for ESM import in node:
186
172
  0 && (module.exports = {
187
173
  IMAGE_FORMAT,
@@ -191,10 +177,6 @@ function buildVideoThumbnailUrl(originalUrl) {
191
177
  MEDIA_STATUS,
192
178
  MEDIA_TYPE,
193
179
  R2_PATH,
194
- buildImageVariantUrl,
195
- buildVariantString,
196
- buildVideoHlsUrl,
197
- buildVideoThumbnailUrl,
198
- parseMediaUrl
180
+ SprouxMedia
199
181
  });
200
182
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/parse-media-url.ts","../src/build-variant-string.ts","../src/build-image-variant-url.ts","../src/build-video-hls-url.ts","../src/build-video-thumbnail-url.ts"],"sourcesContent":["// Constants\r\nexport {\r\n MEDIA_TYPE,\r\n MEDIA_PURPOSE,\r\n MEDIA_STATUS,\r\n IMAGE_FORMAT,\r\n IMAGE_RESIZE_TYPE,\r\n R2_PATH,\r\n MEDIA_DEFAULTS,\r\n} from './constants';\r\n\r\n// Types\r\nexport type {\r\n MediaType,\r\n MediaPurpose,\r\n MediaStatus,\r\n ImageFormat,\r\n ImageResizeType,\r\n ParsedMediaUrl,\r\n ImageVariantOptions,\r\n} from './types';\r\n\r\n// Functions\r\nexport { parseMediaUrl } from './parse-media-url';\r\nexport { buildVariantString } from './build-variant-string';\r\nexport { buildImageVariantUrl } from './build-image-variant-url';\r\nexport { buildVideoHlsUrl } from './build-video-hls-url';\r\nexport { buildVideoThumbnailUrl } from './build-video-thumbnail-url';\r\n","// ============================================================================\r\n// MEDIA TYPE\r\n// ============================================================================\r\n\r\nexport const MEDIA_TYPE = {\r\n IMAGE: 'image',\r\n VIDEO: 'video',\r\n} as const;\r\n\r\n// ============================================================================\r\n// MEDIA PURPOSE\r\n// ============================================================================\r\n\r\nexport const MEDIA_PURPOSE = {\r\n AVATAR: 'avatar',\r\n HERO: 'hero',\r\n GALLERY: 'gallery',\r\n} as const;\r\n\r\n// ============================================================================\r\n// MEDIA STATUS\r\n// ============================================================================\r\n\r\nexport const MEDIA_STATUS = {\r\n PENDING: 'pending',\r\n UPLOADED: 'uploaded',\r\n PROCESSING: 'processing',\r\n READY: 'ready',\r\n ERROR: 'error',\r\n} as const;\r\n\r\n// ============================================================================\r\n// IMAGE FORMAT\r\n// ============================================================================\r\n\r\nexport const IMAGE_FORMAT = {\r\n WEBP: 'webp',\r\n AVIF: 'avif',\r\n JPEG: 'jpeg',\r\n PNG: 'png',\r\n JPG: 'jpg',\r\n} as const;\r\n\r\n// ============================================================================\r\n// IMAGE RESIZE TYPE\r\n// ============================================================================\r\n\r\nexport const IMAGE_RESIZE_TYPE = {\r\n FIT: 'fit',\r\n FILL: 'fill',\r\n AUTO: 'auto',\r\n} as const;\r\n\r\n// ============================================================================\r\n// R2/CDN PATH CONSTANTS\r\n// ============================================================================\r\n\r\nexport const R2_PATH = {\r\n HLS_PLAYLIST: 'playlist.m3u8',\r\n THUMBNAIL: 'thumbnail.webp',\r\n} as const;\r\n\r\n// ============================================================================\r\n// DEFAULTS\r\n// ============================================================================\r\n\r\nexport const MEDIA_DEFAULTS = {\r\n IMAGE_FORMAT: IMAGE_FORMAT.WEBP,\r\n} as const;\r\n","import { MEDIA_PURPOSE, MEDIA_TYPE } from './constants';\r\nimport type { ParsedMediaUrl, MediaPurpose, MediaType } from './types';\r\n\r\nconst VALID_PURPOSES = new Set<string>(Object.values(MEDIA_PURPOSE));\r\nconst VALID_TYPES = new Set<string>(Object.values(MEDIA_TYPE));\r\n\r\n/**\r\n * Parse a full media URL into structured components.\r\n *\r\n * Expected URL pattern:\r\n * {cdnBase}/{purpose}/{type}/{userId}/{name}.{extension}\r\n *\r\n * Example:\r\n * https://cdn.example.com/avatar/image/usr-1/abc.jpg\r\n * → { cdnBase: \"https://cdn.example.com\", purpose: \"avatar\", type: \"image\", userId: \"usr-1\", name: \"abc\", extension: \"jpg\" }\r\n */\r\nexport function parseMediaUrl(originalUrl: string): ParsedMediaUrl {\r\n let url: URL;\r\n try {\r\n url = new URL(originalUrl);\r\n } catch {\r\n throw new Error(`Invalid media URL: \"${originalUrl}\"`);\r\n }\r\n\r\n // Remove leading slash and split into segments\r\n const pathname = url.pathname.startsWith('/') ? url.pathname.slice(1) : url.pathname;\r\n const segments = pathname.split('/');\r\n\r\n if (segments.length < 4) {\r\n throw new Error(\r\n `Invalid media URL path: expected at least 4 segments (purpose/type/userId/filename), got ${segments.length} in \"${pathname}\"`,\r\n );\r\n }\r\n\r\n const [purpose, type, userId, filename] = segments as [string, string, string, string];\r\n\r\n if (!VALID_PURPOSES.has(purpose)) {\r\n throw new Error(\r\n `Invalid media purpose: \"${purpose}\". Expected one of: ${[...VALID_PURPOSES].join(', ')}`,\r\n );\r\n }\r\n\r\n if (!VALID_TYPES.has(type)) {\r\n throw new Error(\r\n `Invalid media type: \"${type}\". Expected one of: ${[...VALID_TYPES].join(', ')}`,\r\n );\r\n }\r\n\r\n if (!userId) {\r\n throw new Error('Missing userId segment in media URL');\r\n }\r\n\r\n if (!filename) {\r\n throw new Error('Missing filename segment in media URL');\r\n }\r\n\r\n const dotIndex = filename.lastIndexOf('.');\r\n if (dotIndex <= 0) {\r\n throw new Error(`Invalid filename: \"${filename}\". Expected format: \"name.extension\"`);\r\n }\r\n\r\n const name = filename.slice(0, dotIndex);\r\n const extension = filename.slice(dotIndex + 1);\r\n\r\n if (!extension) {\r\n throw new Error(`Missing file extension in filename: \"${filename}\"`);\r\n }\r\n\r\n const cdnBase = `${url.protocol}//${url.host}`;\r\n const objectKey = pathname;\r\n\r\n return {\r\n originalUrl,\r\n cdnBase,\r\n objectKey,\r\n purpose: purpose as MediaPurpose,\r\n type: type as MediaType,\r\n userId,\r\n name,\r\n extension,\r\n };\r\n}\r\n","import type { ImageVariantOptions } from './types';\r\n\r\n/**\r\n * Build a deterministic variant string from image variant options.\r\n *\r\n * Format: {width}x{height}-{resizeType}[-q{quality}]\r\n *\r\n * Examples:\r\n * { width: 200, height: 200, resizeType: 'fit' } → \"200x200-fit\"\r\n * { width: 200, height: 200, resizeType: 'fit', quality: 80 } → \"200x200-fit-q80\"\r\n */\r\nexport function buildVariantString(options: ImageVariantOptions): string {\r\n validateVariantOptions(options);\r\n\r\n const parts: string[] = [`${options.width}x${options.height}`, options.resizeType];\r\n\r\n if (options.quality != null) {\r\n parts.push(`q${options.quality}`);\r\n }\r\n\r\n return parts.join('-');\r\n}\r\n\r\nfunction validateVariantOptions(options: ImageVariantOptions): void {\r\n if (!Number.isInteger(options.width) || options.width < 1 || options.width > 4096) {\r\n throw new Error(`Invalid width: ${options.width}. Must be an integer between 1 and 4096.`);\r\n }\r\n\r\n if (!Number.isInteger(options.height) || options.height < 1 || options.height > 4096) {\r\n throw new Error(`Invalid height: ${options.height}. Must be an integer between 1 and 4096.`);\r\n }\r\n\r\n if (options.quality != null) {\r\n if (!Number.isInteger(options.quality) || options.quality < 1 || options.quality > 100) {\r\n throw new Error(\r\n `Invalid quality: ${options.quality}. Must be an integer between 1 and 100.`,\r\n );\r\n }\r\n }\r\n}\r\n","import { MEDIA_DEFAULTS } from './constants';\r\nimport { parseMediaUrl } from './parse-media-url';\r\nimport { buildVariantString } from './build-variant-string';\r\nimport type { ImageVariantOptions } from './types';\r\n\r\n/**\r\n * Build a full CDN URL for an image variant.\r\n *\r\n * Takes an original image URL and variant options, returns the CDN URL\r\n * for that specific variant.\r\n *\r\n * Example:\r\n * buildImageVariantUrl(\"https://cdn.example.com/avatar/image/usr-1/abc.jpg\", {\r\n * width: 200, height: 200, resizeType: 'fit', quality: 80\r\n * })\r\n * → \"https://cdn.example.com/avatar/image/usr-1/abc-200x200-fit-q80.webp\"\r\n */\r\nexport function buildImageVariantUrl(originalUrl: string, options: ImageVariantOptions): string {\r\n const parsed = parseMediaUrl(originalUrl);\r\n\r\n if (parsed.type !== 'image') {\r\n throw new Error(`Expected image URL, got type \"${parsed.type}\" in \"${originalUrl}\"`);\r\n }\r\n\r\n const variant = buildVariantString(options);\r\n const format = options.format ?? MEDIA_DEFAULTS.IMAGE_FORMAT;\r\n\r\n return `${parsed.cdnBase}/${parsed.purpose}/${parsed.type}/${parsed.userId}/${parsed.name}-${variant}.${format}`;\r\n}\r\n","import { R2_PATH } from './constants';\r\nimport { parseMediaUrl } from './parse-media-url';\r\n\r\n/**\r\n * Build HLS playlist URL from an original video URL.\r\n *\r\n * Example:\r\n * buildVideoHlsUrl(\"https://cdn.example.com/gallery/video/usr-1/xyz.mp4\")\r\n * → \"https://cdn.example.com/gallery/video/usr-1/xyz/playlist.m3u8\"\r\n */\r\nexport function buildVideoHlsUrl(originalUrl: string): string {\r\n const parsed = parseMediaUrl(originalUrl);\r\n\r\n if (parsed.type !== 'video') {\r\n throw new Error(`Expected video URL, got type \"${parsed.type}\" in \"${originalUrl}\"`);\r\n }\r\n\r\n return `${parsed.cdnBase}/${parsed.purpose}/video/${parsed.userId}/${parsed.name}/${R2_PATH.HLS_PLAYLIST}`;\r\n}\r\n","import { R2_PATH } from './constants';\r\nimport { parseMediaUrl } from './parse-media-url';\r\n\r\n/**\r\n * Build thumbnail URL from an original video URL.\r\n *\r\n * Example:\r\n * buildVideoThumbnailUrl(\"https://cdn.example.com/gallery/video/usr-1/xyz.mp4\")\r\n * → \"https://cdn.example.com/gallery/video/usr-1/xyz/thumbnail.webp\"\r\n */\r\nexport function buildVideoThumbnailUrl(originalUrl: string): string {\r\n const parsed = parseMediaUrl(originalUrl);\r\n\r\n if (parsed.type !== 'video') {\r\n throw new Error(`Expected video URL, got type \"${parsed.type}\" in \"${originalUrl}\"`);\r\n }\r\n\r\n return `${parsed.cdnBase}/${parsed.purpose}/video/${parsed.userId}/${parsed.name}/${R2_PATH.THUMBNAIL}`;\r\n}\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/sproux-media.ts"],"sourcesContent":["// Constants\r\nexport {\r\n MEDIA_TYPE,\r\n MEDIA_PURPOSE,\r\n MEDIA_STATUS,\r\n IMAGE_FORMAT,\r\n IMAGE_RESIZE_TYPE,\r\n R2_PATH,\r\n MEDIA_DEFAULTS,\r\n} from './constants';\r\n\r\n// Types\r\nexport type { ImageFormat, ImageResizeType, SprouxMediaConfig, ImageUrlOptions } from './types';\r\n\r\n// Singleton class\r\nexport { SprouxMedia } from './sproux-media';\r\n","// ============================================================================\r\n// MEDIA TYPE\r\n// ============================================================================\r\n\r\nexport const MEDIA_TYPE = {\r\n IMAGE: 'image',\r\n VIDEO: 'video',\r\n} as const;\r\n\r\n// ============================================================================\r\n// MEDIA PURPOSE\r\n// ============================================================================\r\n\r\nexport const MEDIA_PURPOSE = {\r\n AVATAR: 'avatar',\r\n HERO: 'hero',\r\n GALLERY: 'gallery',\r\n} as const;\r\n\r\n// ============================================================================\r\n// MEDIA STATUS\r\n// ============================================================================\r\n\r\nexport const MEDIA_STATUS = {\r\n PENDING: 'pending',\r\n UPLOADED: 'uploaded',\r\n PROCESSING: 'processing',\r\n READY: 'ready',\r\n ERROR: 'error',\r\n} as const;\r\n\r\n// ============================================================================\r\n// IMAGE FORMAT\r\n// ============================================================================\r\n\r\nexport enum IMAGE_FORMAT {\r\n WEBP = 'webp',\r\n AVIF = 'avif',\r\n JPEG = 'jpeg',\r\n PNG = 'png',\r\n GIF = 'gif',\r\n ICO = 'ico',\r\n SVG = 'svg',\r\n JPG = 'jpg',\r\n}\r\n\r\n// ============================================================================\r\n// IMAGE RESIZE TYPE\r\n// ============================================================================\r\n\r\nexport enum IMAGE_RESIZE_TYPE {\r\n FIT = 'fit',\r\n FILL = 'fill',\r\n FORCE = 'force',\r\n FILL_DOWN = 'fill-down',\r\n AUTO = 'auto',\r\n}\r\n\r\n// ============================================================================\r\n// R2/CDN PATH CONSTANTS\r\n// ============================================================================\r\n\r\nexport const R2_PATH = {\r\n HLS_PLAYLIST: 'playlist.m3u8',\r\n THUMBNAIL: 'thumbnail.webp',\r\n} as const;\r\n\r\n// ============================================================================\r\n// DEFAULTS\r\n// ============================================================================\r\n\r\nexport const MEDIA_DEFAULTS = {\r\n IMAGE_FORMAT: IMAGE_FORMAT.WEBP,\r\n} as const;\r\n","import { R2_PATH } from './constants';\r\nimport type { SprouxMediaConfig, ImageUrlOptions } from './types';\r\n\r\nexport class SprouxMedia {\r\n private static instance: SprouxMedia | null = null;\r\n\r\n private readonly cdnUrl: string;\r\n\r\n private constructor(config: SprouxMediaConfig) {\r\n this.cdnUrl = config.cdnUrl.replace(/\\/+$/, '');\r\n }\r\n\r\n /**\r\n * Initialize the singleton with CDN configuration.\r\n * Returns the singleton instance.\r\n */\r\n static init(config: SprouxMediaConfig): SprouxMedia {\r\n if (SprouxMedia.instance) {\r\n return SprouxMedia.instance;\r\n }\r\n \r\n return (SprouxMedia.instance = new SprouxMedia(config));\r\n }\r\n\r\n /**\r\n * Returns the existing singleton instance.\r\n * Throws if `init()` has not been called.\r\n */\r\n static getInstance(): SprouxMedia {\r\n if (!SprouxMedia.instance) {\r\n throw new Error('SprouxMedia has not been initialized. Call SprouxMedia.init() first.');\r\n }\r\n return SprouxMedia.instance;\r\n }\r\n\r\n /**\r\n * Build a CDN URL for an image variant.\r\n *\r\n * @param objectKey - Object key without extension (e.g. \"avatar/image/usr-1/abc\")\r\n * @param options - Image variant options including extension, dimensions, resize type, and optional quality\r\n * @returns Full CDN URL for the image variant\r\n *\r\n * @example\r\n * media.getImageUrl('avatar/image/usr-1/abc', {\r\n * extension: 'webp', width: 200, height: 200, resizeType: 'fit', quality: 80,\r\n * });\r\n * // → \"https://cdn.example.com/avatar/image/usr-1/abc-200x200-fit-q80.webp\"\r\n */\r\n getImageUrl(objectKey: string, options: ImageUrlOptions): string {\r\n this.validateImageOptions(options);\r\n const variant = this.buildVariantString(options);\r\n return `${this.cdnUrl}/${objectKey}-${variant}.${options.extension}`;\r\n }\r\n\r\n /**\r\n * Build a CDN URL for a video HLS playlist.\r\n *\r\n * @param objectKey - Object key without extension (e.g. \"gallery/video/usr-1/xyz\")\r\n * @returns Full CDN URL for the HLS playlist\r\n */\r\n getVideoHlsUrl(objectKey: string): string {\r\n return `${this.cdnUrl}/${objectKey}/${R2_PATH.HLS_PLAYLIST}`;\r\n }\r\n\r\n /**\r\n * Build a CDN URL for a video thumbnail.\r\n *\r\n * @param objectKey - Object key without extension (e.g. \"gallery/video/usr-1/xyz\")\r\n * @returns Full CDN URL for the thumbnail\r\n */\r\n getVideoThumbnailUrl(objectKey: string): string {\r\n return `${this.cdnUrl}/${objectKey}/${R2_PATH.THUMBNAIL}`;\r\n }\r\n\r\n /**\r\n * Build a deterministic variant string from image options.\r\n *\r\n * Format: {width}x{height}-{resizeType}[-q{quality}]\r\n */\r\n private buildVariantString(options: ImageUrlOptions): string {\r\n const parts: string[] = [`${options.width}x${options.height}`, options.resizeType];\r\n\r\n if (options.quality != null) {\r\n parts.push(`q${options.quality}`);\r\n }\r\n\r\n return parts.join('-');\r\n }\r\n\r\n private validateImageOptions(options: ImageUrlOptions): void {\r\n if (!Number.isInteger(options.width) || options.width < 1 || options.width > 4096) {\r\n throw new Error(`Invalid width: ${options.width}. Must be an integer between 1 and 4096.`);\r\n }\r\n\r\n if (!Number.isInteger(options.height) || options.height < 1 || options.height > 4096) {\r\n throw new Error(\r\n `Invalid height: ${options.height}. Must be an integer between 1 and 4096.`,\r\n );\r\n }\r\n\r\n if (options.quality != null) {\r\n if (!Number.isInteger(options.quality) || options.quality < 1 || options.quality > 100) {\r\n throw new Error(\r\n `Invalid quality: ${options.quality}. Must be an integer between 1 and 100.`,\r\n );\r\n }\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":["IMAGE_FORMAT","IMAGE_RESIZE_TYPE"]}