musora-content-services 1.4.2 → 1.4.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [1.4.3](https://github.com/railroadmedia/musora-content-services/compare/v1.4.2...v1.4.3) (2025-03-31)
6
+
5
7
  ### [1.4.2](https://github.com/railroadmedia/musora-content-services/compare/v1.4.1...v1.4.2) (2025-03-28)
6
8
 
7
9
  ### [1.4.1](https://github.com/railroadmedia/musora-content-services/compare/v1.4.0...v1.4.1) (2025-03-25)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
File without changes
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Builds an optimized image URL using both Sanity and Cloudflare Image Resizing services
3
+ *
4
+ * This function takes a Sanity CDN URL and image transformation parameters, then:
5
+ * 1. Adds appropriate transformation parameters to the Sanity URL
6
+ * 2. Wraps the resulting URL with Cloudflare's Image Resizing service
7
+ * 3. Maps compatible parameters between the two services for optimal results
8
+ *
9
+ * @param {string} url - The original image URL, typically from Sanity CDN
10
+ * @param {Object} options - Image transformation options
11
+ * @param {number} [options.width] - Desired image width in pixels
12
+ * @param {number} [options.height] - Desired image height in pixels
13
+ * @param {number} [options.quality] - Image quality (1-100)
14
+ * @param {string} [options.fit] - Resize strategy: 'cover', 'contain', 'scale-down', 'none'
15
+ * @param {string} [options.gravity] - Content positioning: 'auto', 'top', 'bottom', 'left', 'right', etc.
16
+ * @param {string} [options.crop] - Sanity-specific crop mode
17
+ * @param {string} [options.format] - Image format: 'auto', 'webp', 'jpeg', 'png', etc.
18
+ * @param {number} [options.blur] - Blur amount
19
+ * @param {number} [options.sharpen] - Sharpen amount
20
+ * @param {number} [options.saturation] - Saturation adjustment
21
+ * @param {number} [options.brightness] - Brightness adjustment
22
+ * @param {number} [options.contrast] - Contrast adjustment
23
+ * @param {number} [options.gamma] - Gamma adjustment
24
+ * @param {number} [options.rotate] - Rotation angle in degrees
25
+ * @param {number} [options.dpr] - Device pixel ratio for responsive images
26
+ * @param {string} [options.background] - Background color (e.g., '#FFFFFF')
27
+ * @param {number} [options.padding] - Padding to add around the image
28
+ * @param {boolean} [options.anim] - Whether to preserve animation in GIFs
29
+ * @param {string} [options.onerror] - Error handling strategy
30
+ * @param {string} [options.compression] - Compression strategy
31
+ * @param {string} [options.fetchFormat] - Format to request from origin
32
+ * @param {string} [options.sampling] - Chroma subsampling strategy
33
+ * @param {string} [options.metadata] - Metadata to preserve
34
+ *
35
+ * @returns {string} The fully constructed image URL with transformations
36
+ */
37
+ export function buildImageSRC(url, options = {}) {
38
+ // Process Sanity URL first if applicable
39
+ if (url.includes('cdn.sanity.io')) {
40
+ url = applySanityTransformations(url, options)
41
+ }
42
+
43
+ // Then apply Cloudflare transformations
44
+ return applyCloudflareWrapper(url, options)
45
+ }
46
+
47
+ /**
48
+ * Applies Sanity-specific image transformations to a Sanity CDN URL
49
+ *
50
+ * @param {string} url - The Sanity CDN URL
51
+ * @param {Object} options - Image transformation options
52
+ * @returns {string} URL with Sanity transformations applied
53
+ * @private
54
+ */
55
+ export function applySanityTransformations(url, options) {
56
+ const { width, height, quality } = options
57
+
58
+ const sanityOptions = []
59
+
60
+ // Dimensions
61
+ if (width) sanityOptions.push(`w=${width}`)
62
+ if (height) sanityOptions.push(`h=${height}`)
63
+ if (quality) sanityOptions.push(`q=${quality}`)
64
+
65
+ // Add parameters to Sanity URL
66
+ const sanityQuery = sanityOptions.length > 0 ? `?${sanityOptions.join('&')}` : ''
67
+ return `${url}${sanityQuery}`
68
+ }
69
+
70
+ /**
71
+ * Wraps a URL with Cloudflare's Image Resizing service and applies transformations
72
+ *
73
+ * @param {string} url - The source URL (can be any image URL)
74
+ * @param {Object} options - Image transformation options
75
+ * @returns {string} URL with Cloudflare transformations applied
76
+ * @private
77
+ */
78
+ export function applyCloudflareWrapper(url, options) {
79
+ const {
80
+ width,
81
+ height,
82
+ quality,
83
+ fit,
84
+ gravity,
85
+ format,
86
+ blur,
87
+ sharpen,
88
+ brightness,
89
+ contrast,
90
+ gamma,
91
+ rotate,
92
+ dpr,
93
+ background,
94
+ padding,
95
+ anim,
96
+ onerror,
97
+ compression,
98
+ fetchFormat,
99
+ sampling,
100
+ metadata,
101
+ } = options
102
+
103
+ const cloudflareOptions = []
104
+
105
+ // Build Cloudflare options - required parameters
106
+ if (width) cloudflareOptions.push(`width=${width}`)
107
+ if (height) cloudflareOptions.push(`height=${height}`)
108
+ if (quality) cloudflareOptions.push(`quality=${quality}`)
109
+
110
+ // Add optional Cloudflare parameters
111
+ if (format) cloudflareOptions.push(`format=${format}`)
112
+ if (fit) cloudflareOptions.push(`fit=${fit}`)
113
+ if (gravity) cloudflareOptions.push(`gravity=${gravity}`)
114
+
115
+ if (sharpen !== null && sharpen !== undefined) cloudflareOptions.push(`sharpen=${sharpen}`)
116
+ if (blur !== null && blur !== undefined) cloudflareOptions.push(`blur=${blur}`)
117
+ if (brightness !== null && brightness !== undefined)
118
+ cloudflareOptions.push(`brightness=${brightness}`)
119
+ if (contrast !== null && contrast !== undefined) cloudflareOptions.push(`contrast=${contrast}`)
120
+ if (gamma !== null && gamma !== undefined) cloudflareOptions.push(`gamma=${gamma}`)
121
+ if (rotate !== null && rotate !== undefined) cloudflareOptions.push(`rotate=${rotate}`)
122
+ if (dpr !== null && dpr !== undefined) cloudflareOptions.push(`dpr=${dpr}`)
123
+
124
+ if (metadata !== null && metadata !== undefined) cloudflareOptions.push(`metadata=${metadata}`)
125
+ if (onerror !== null && onerror !== undefined) cloudflareOptions.push(`onerror=${onerror}`)
126
+ if (anim === false) cloudflareOptions.push('anim=false')
127
+ if (background !== null && background !== undefined)
128
+ cloudflareOptions.push(`background=${background}`)
129
+ if (fetchFormat !== null && fetchFormat !== undefined)
130
+ cloudflareOptions.push(`fetchFormat=${fetchFormat}`)
131
+ if (compression !== null && compression !== undefined)
132
+ cloudflareOptions.push(`compression=${compression}`)
133
+ if (sampling !== null && sampling !== undefined) cloudflareOptions.push(`sampling=${sampling}`)
134
+ if (padding !== null && padding !== undefined) cloudflareOptions.push(`padding=${padding}`)
135
+
136
+ const optionsString = cloudflareOptions.length > 0 ? cloudflareOptions.join(',') : ''
137
+
138
+ return `https://www.musora.com/cdn-cgi/image/${optionsString}/${url}`
139
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Verifies if an image URL follows best practices for optimization
3
+ *
4
+ * This function checks whether:
5
+ * 1. Sanity CDN images include query parameters for optimization
6
+ * 2. Direct S3 images are avoided (should use CDN instead)
7
+ *
8
+ * @param {string} src - The image source URL to verify
9
+ * @returns {void}
10
+ *
11
+ * @example
12
+ * // Check a direct Sanity URL
13
+ * verifyImageSRC('https://cdn.sanity.io/images/4032r8py/staging/504c4e3393170f937a579de6f3c75c457b0c9e65-640x360.jpg');
14
+ *
15
+ * @example
16
+ * // Check a Sanity URL inside a Cloudflare URL
17
+ * verifyImageSRC('https://www.musora.com/cdn-cgi/image/width=500,quality=95/https://cdn.sanity.io/images/4032r8py/staging/504c4e3393170f937a579de6f3c75c457b0c9e65-640x360.jpg');
18
+ */
19
+ export function verifyImageSRC(src) {
20
+ // Exit early if the URL is empty
21
+ if (!src) return
22
+
23
+ // Check for S3 direct URLs
24
+ if (isBucketUrl(src)) {
25
+ warnAboutDirectS3Url(src)
26
+ return
27
+ }
28
+
29
+ // Check for Sanity URLs
30
+ if (src.includes('cdn.sanity.io')) {
31
+ verifySanityUrl(src)
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Checks if a URL is a direct link to an S3 bucket
37
+ *
38
+ * @param {string} url - The URL to check
39
+ * @returns {boolean} True if the URL is a direct S3 bucket URL
40
+ * @private
41
+ */
42
+ export function isBucketUrl(url) {
43
+ // Check for common S3 patterns
44
+ return (
45
+ url.includes('.s3.amazonaws.com') ||
46
+ url.includes('s3.us-') ||
47
+ url.includes('amazonaws.com/') ||
48
+ (url.includes('musora-') && url.includes('.s3.'))
49
+ )
50
+ }
51
+
52
+ /**
53
+ * Issues a warning about using direct S3 URLs instead of a CDN
54
+ *
55
+ * @param {string} url - The S3 URL that triggered the warning
56
+ * @private
57
+ */
58
+ function warnAboutDirectS3Url(url) {
59
+ // Only warn in development mode
60
+ if (process.env.NODE_ENV !== 'production') {
61
+ console.warn(`WARNING: Direct S3 bucket URL detected: ${url}
62
+ This is not recommended. Use Cloudfront or another CDN for better performance.`)
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Extracts a Sanity URL from a potentially Cloudflare-wrapped URL
68
+ *
69
+ * @param {string} url - The URL to process
70
+ * @returns {string} The extracted Sanity URL
71
+ * @private
72
+ */
73
+ export function extractSanityUrl(url) {
74
+ // If this is a Cloudflare URL, extract the Sanity portion
75
+ if (url.includes('/cdn-cgi/image/')) {
76
+ // Split the URL to get the portion after /cdn-cgi/image/[parameters]/
77
+ const parts = url.split('/cdn-cgi/image/')
78
+ if (parts.length > 1) {
79
+ // Get everything after the first / that follows the Cloudflare parameters
80
+ const cfParamsAndSanityUrl = parts[1]
81
+ const slashIndex = cfParamsAndSanityUrl.indexOf('/', 0)
82
+ if (slashIndex !== -1) {
83
+ let sanityUrl = cfParamsAndSanityUrl.substring(slashIndex + 1)
84
+ return sanityUrl
85
+ }
86
+ }
87
+ }
88
+
89
+ // If not a Cloudflare URL or extraction failed, return the original URL
90
+ return url
91
+ }
92
+
93
+ /**
94
+ * Verifies if a Sanity CDN image URL includes optimization parameters
95
+ *
96
+ * @param {string} src - The Sanity image URL to verify
97
+ * @private
98
+ */
99
+ function verifySanityUrl(src) {
100
+ // Extract the Sanity URL if it's wrapped in a Cloudflare URL
101
+ const sanityUrl = extractSanityUrl(src)
102
+
103
+ // Check if the Sanity URL has parameters (any query string)
104
+ const hasParameters = sanityUrl.includes('?')
105
+
106
+ // Warn if no parameters are found
107
+ if (!hasParameters) {
108
+ // Only warn in development mode
109
+ if (process.env.NODE_ENV !== 'production') {
110
+ console.warn(`WARNING: Sanity CDN URL without parameters detected: ${src}
111
+ This may cause performance issues. Consider adding image transformations using the buildImageSRC MCS service.`)
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,37 @@
1
+ const { buildImageSRC, applySanityTransformations } = require('../src/services/imageSRCBuilder.js')
2
+
3
+ describe('imageSRCBuilder', function () {
4
+ beforeEach(() => {})
5
+
6
+ test('applySanityTransformations', async () => {
7
+ const url =
8
+ 'https://cdn.sanity.io/images/4032r8py/production/83c3b6e7354a46c605804c093f707daa4e3f8f25-8000x4500.png'
9
+ const width = 500
10
+ const height = 100
11
+ const quality = 95
12
+ const resultingURL = applySanityTransformations(url, {
13
+ width: width,
14
+ quality: quality,
15
+ height: height,
16
+ })
17
+ const expected = `${url}?w=${width}&h=${height}&q=${quality}`
18
+
19
+ expect(resultingURL).toStrictEqual(expected)
20
+ })
21
+
22
+ test('buildImageSRC', async () => {
23
+ const url = 'https://d2vyvo0tyx8ig5.cloudfront.net/books/foundations/level-2.jpg'
24
+ const width = 500
25
+ const height = 100
26
+ const quality = 95
27
+ const resultingURL = buildImageSRC(url, {
28
+ width: width,
29
+ quality: quality,
30
+ height: height,
31
+ })
32
+
33
+ const expected = `https://www.musora.com/cdn-cgi/image/width=${width},height=${height},quality=${quality}/${url}`
34
+
35
+ expect(resultingURL).toStrictEqual(expected)
36
+ })
37
+ })
@@ -0,0 +1,160 @@
1
+ // Mock console.warn to avoid cluttering test output and to verify warnings
2
+ import { extractSanityUrl, isBucketUrl, verifyImageSRC } from '../src/services/imageSRCVerify.js'
3
+
4
+ const originalConsoleWarn = console.warn
5
+ const originalConsoleError = console.error
6
+ const originalNodeEnv = process.env.NODE_ENV
7
+
8
+ describe('Image URL Verification', () => {
9
+ let consoleWarnMock
10
+
11
+ beforeEach(() => {
12
+ // Mock console.warn and console.error
13
+ consoleWarnMock = jest.fn()
14
+ console.warn = consoleWarnMock
15
+ console.error = jest.fn()
16
+
17
+ // Set NODE_ENV to development for all tests
18
+ process.env.NODE_ENV = 'development'
19
+ })
20
+
21
+ afterEach(() => {
22
+ // Restore the original console methods and NODE_ENV
23
+ console.warn = originalConsoleWarn
24
+ console.error = originalConsoleError
25
+ process.env.NODE_ENV = originalNodeEnv
26
+ })
27
+
28
+ describe('verifyImageSRC', () => {
29
+ test('should not warn for Sanity URL with parameters', () => {
30
+ // Arrange
31
+ const url =
32
+ 'https://cdn.sanity.io/images/4032r8py/staging/504c4e3393170f937a579de6f3c75c457b0c9e65-640x360.jpg?w=500&q=95'
33
+
34
+ // Act
35
+ verifyImageSRC(url)
36
+
37
+ // Assert
38
+ expect(consoleWarnMock).not.toHaveBeenCalled()
39
+ })
40
+
41
+ test('should warn for Sanity URL without parameters', () => {
42
+ // Arrange
43
+ const url =
44
+ 'https://cdn.sanity.io/images/4032r8py/staging/504c4e3393170f937a579de6f3c75c457b0c9e65-640x360.jpg'
45
+
46
+ // Act
47
+ verifyImageSRC(url)
48
+
49
+ // Assert
50
+ expect(consoleWarnMock).toHaveBeenCalled()
51
+ expect(consoleWarnMock.mock.calls[0][0]).toContain(
52
+ 'WARNING: Sanity CDN URL without parameters detected'
53
+ )
54
+ })
55
+
56
+ test('should warn for direct S3 bucket URL', () => {
57
+ // Arrange
58
+ const url = 'https://musora-images.s3.amazonaws.com/drumeo/images/some-image.jpg'
59
+
60
+ // Act
61
+ verifyImageSRC(url)
62
+
63
+ // Assert
64
+ expect(consoleWarnMock).toHaveBeenCalled()
65
+ expect(consoleWarnMock.mock.calls[0][0]).toContain('Direct S3 bucket URL detected')
66
+ })
67
+
68
+ test('should not warn when URL is empty', () => {
69
+ // Arrange
70
+ const url = ''
71
+
72
+ // Act
73
+ verifyImageSRC(url)
74
+
75
+ // Assert
76
+ expect(consoleWarnMock).not.toHaveBeenCalled()
77
+ })
78
+ })
79
+
80
+ describe('Cloudflare wrapped Sanity URLs', () => {
81
+ test('should extract and validate Sanity URL from Cloudflare wrapper', () => {
82
+ // Arrange
83
+ const url =
84
+ 'https://www.musora.com/cdn-cgi/image/width=500,quality=95/https://cdn.sanity.io/images/4032r8py/staging/504c4e3393170f937a579de6f3c75c457b0c9e65-640x360.jpg'
85
+
86
+ // Act
87
+ verifyImageSRC(url)
88
+
89
+ // Assert
90
+ expect(consoleWarnMock).toHaveBeenCalled()
91
+ expect(consoleWarnMock.mock.calls[0][0]).toContain(
92
+ 'WARNING: Sanity CDN URL without parameters detected'
93
+ )
94
+ })
95
+
96
+ test('should not warn for Cloudflare wrapped Sanity URL with parameters', () => {
97
+ // Arrange
98
+ const url =
99
+ 'https://www.musora.com/cdn-cgi/image/width=500,quality=95/https://cdn.sanity.io/images/4032r8py/staging/504c4e3393170f937a579de6f3c75c457b0c9e65-640x360.jpg?w=500&q=95'
100
+
101
+ // Act
102
+ verifyImageSRC(url)
103
+
104
+ // Assert
105
+ expect(consoleWarnMock).not.toHaveBeenCalled()
106
+ })
107
+ })
108
+
109
+ describe('isBucketUrl', () => {
110
+ test('should detect standard S3 URLs', () => {
111
+ // Arrange & Act & Assert
112
+ expect(isBucketUrl('https://my-bucket.s3.amazonaws.com/image.jpg')).toBe(true)
113
+ expect(isBucketUrl('https://s3.us-east-1.amazonaws.com/my-bucket/image.jpg')).toBe(true)
114
+ expect(isBucketUrl('https://musora-images.s3.amazonaws.com/path/to/image.jpg')).toBe(true)
115
+ })
116
+
117
+ test('should not flag non-S3 URLs', () => {
118
+ // Arrange & Act & Assert
119
+ expect(isBucketUrl('https://cdn.sanity.io/images/123/production/image.jpg')).toBe(false)
120
+ expect(isBucketUrl('https://www.musora.com/image.jpg')).toBe(false)
121
+ })
122
+ })
123
+
124
+ describe('extractSanityUrl', () => {
125
+ test('should extract Sanity URL from Cloudflare wrapper', () => {
126
+ // Arrange
127
+ const wrappedUrl =
128
+ 'https://www.musora.com/cdn-cgi/image/width=500,quality=95/https://cdn.sanity.io/images/123/production/image.jpg'
129
+ const expectedExtracted = 'https://cdn.sanity.io/images/123/production/image.jpg'
130
+
131
+ // Act
132
+ const result = extractSanityUrl(wrappedUrl)
133
+
134
+ // Assert
135
+ expect(result).toBe(expectedExtracted)
136
+ })
137
+
138
+ test('should return original URL if not Cloudflare wrapped', () => {
139
+ // Arrange
140
+ const url = 'https://cdn.sanity.io/images/123/production/image.jpg'
141
+
142
+ // Act
143
+ const result = extractSanityUrl(url)
144
+
145
+ // Assert
146
+ expect(result).toBe(url)
147
+ })
148
+
149
+ test('should handle malformed Cloudflare URLs gracefully', () => {
150
+ // Arrange
151
+ const malformedUrl = 'https://www.musora.com/cdn-cgi/image/width=500'
152
+
153
+ // Act
154
+ const result = extractSanityUrl(malformedUrl)
155
+
156
+ // Assert
157
+ expect(result).toBe(malformedUrl)
158
+ })
159
+ })
160
+ })