diff --git a/README.md b/README.md index 93ce1db9..698966dc 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ The following options can be used when creating an instance of `ImgixClient`: - **`includeLibraryParam`:** Boolean. Specifies whether the constructed URLs will include an [`ixlib` parameter](#what-is-the-ixlib-param-on-every-request). Defaults to `true`. - **`secureURLToken`:** String. When specified, this token will be used to sign images. Read more about securing images [on the imgix Docs site](https://docs.imgix.com/setup/securing-images). Defaults to `null`. - :warning: *The `secureURLToken` option should only be used in server-side applications to prevent exposing your secure token.* :warning: +- **`sortParams`:** Boolean. Specifies whether the constructed URLs parameters should be sorted alphabetically. Defaults to `false`. ## API diff --git a/src/constants.js b/src/constants.js index 2284c3a1..f03d23ad 100644 --- a/src/constants.js +++ b/src/constants.js @@ -26,4 +26,5 @@ export const DEFAULT_OPTIONS = { includeLibraryParam: true, urlPrefix: 'https://', secureURLToken: null, + sortParams: false, }; diff --git a/src/index.js b/src/index.js index 6b4376fa..9c2b5f7e 100644 --- a/src/index.js +++ b/src/index.js @@ -108,27 +108,39 @@ export default class ImgixClient { const useCustomEncoder = !!options.encoder; const customEncoder = options.encoder; - const queryParams = [ + let queryParams = [ // Set the libraryParam if applicable. ...(this.settings.libraryParam - ? [`ixlib=${this.settings.libraryParam}`] + ? [['ixlib', this.settings.libraryParam]] : []), + ...Object.entries(params), + ]; - // Map over the key-value pairs in params while applying applicable encoding. - ...Object.entries(params).reduce((prev, [key, value]) => { - if (value == null) { - return prev; - } - const encodedKey = useCustomEncoder ? customEncoder(key, value) : encodeURIComponent(key); - const encodedValue = - key.substr(-2) === '64' - ? useCustomEncoder ? customEncoder(value, key) : Base64.encodeURI(value) - : useCustomEncoder ? customEncoder(value, key) : encodeURIComponent(value); - prev.push(`${encodedKey}=${encodedValue}`); + if (this.settings.sortParams) { + queryParams = queryParams.sort(([a], [b]) => + a > b ? 1 : a < b ? -1 : 0, + ); + } + // Map over the key-value pairs in params while applying applicable encoding. + queryParams = queryParams.reduce((prev, [key, value]) => { + if (value == null) { return prev; - }, []), - ]; + } + + const encodedKey = useCustomEncoder + ? customEncoder(key, value) + : encodeURIComponent(key); + const encodedValue = useCustomEncoder + ? customEncoder(value, key) + : key.substr(-2) === '64' + ? Base64.encodeURI(value) + : encodeURIComponent(value); + + prev.push(`${encodedKey}=${encodedValue}`); + + return prev; + }, []); return `${queryParams.length > 0 ? '?' : ''}${queryParams.join('&')}`; } @@ -152,7 +164,7 @@ export default class ImgixClient { * @param {boolean} options.encode Whether to encode the path, default true * @returns {string} The sanitized path */ - _sanitizePath(path, options = {}) { + _sanitizePath(path, options = {}) { // Strip leading slash first (we'll re-add after encoding) let _path = path.replace(/^\//, ''); @@ -289,12 +301,7 @@ export default class ImgixClient { } const srcset = targetWidthValues.map( - (w) => - `${this.buildURL( - path, - { ...params, w }, - options, - )} ${w}w`, + (w) => `${this.buildURL(path, { ...params, w }, options)} ${w}w`, ); return srcset.join(',\n'); @@ -334,11 +341,7 @@ export default class ImgixClient { const srcset = disableVariableQuality ? targetRatios.map( (dpr) => - `${this.buildURL( - path, - { ...params, dpr }, - options, - )} ${dpr}x`, + `${this.buildURL(path, { ...params, dpr }, options)} ${dpr}x`, ) : targetRatios.map((dpr) => withQuality(path, params, dpr)); diff --git a/test/test-buildURL.js b/test/test-buildURL.js index b273126b..77a76c7d 100644 --- a/test/test-buildURL.js +++ b/test/test-buildURL.js @@ -396,11 +396,11 @@ describe('URL Builder:', function describeSuite() { { encoder: (path) => encodeURI(path).replace("'", "%27") } - ) + ); - const expected = 'https://test.imgix.net/unsplash/walrus.jpg?txt=test!(%27)&txt-color=000&txt-size=400&txt-font=Avenir-Black&txt-x=800&txt-y=600' + const expected = 'https://test.imgix.net/unsplash/walrus.jpg?txt=test!(%27)&txt-color=000&txt-size=400&txt-font=Avenir-Black&txt-x=800&txt-y=600'; - assert.strictEqual(actual, expected) + assert.strictEqual(actual, expected); }); it('can custom encode the parameter value based on the parameter key', () => { @@ -410,5 +410,26 @@ describe('URL Builder:', function describeSuite() { assert.strictEqual(result, expectation); }); + + it('sorts parameters if the `sortParams` setting is truthy', () => { + client.settings.sortParams = true; + + const actual = client.buildURL( + "unsplash/walrus.jpg", + { + txt64: 'base64', + txt: 'text', + 'txt-color': '000', + 'txt-size': 400, + 'txt-x': 800, + 'txt-y': 600, + 'txt-font': 'Avenir-Black', + } + ); + + const expected = 'https://test.imgix.net/unsplash/walrus.jpg?txt=text&txt-color=000&txt-font=Avenir-Black&txt-size=400&txt-x=800&txt-y=600&txt64=YmFzZTY0'; + + assert.strictEqual(actual, expected); + }); }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 01f1cd10..ecdd185a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -9,6 +9,7 @@ declare class ImgixClient { secureURLToken?: string; useHTTPS?: boolean; includeLibraryParam?: boolean; + sortParams?: boolean; }); buildURL( @@ -53,12 +54,12 @@ export interface SrcSetOptions { } export interface _sanitizePathOptions { - disablePathEncoding?: boolean, - encoder?: (path: string) => string + disablePathEncoding?: boolean; + encoder?: (path: string) => string; } export interface _buildParamsOptions { - encoder?: (value: string, key?: string) => string + encoder?: (value: string, key?: string) => string; } export default ImgixClient;