├── .babelrc ├── .circleci └── config.yml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── pull_request_template.md └── workflows │ ├── add-issue-to-project.yml │ └── add-pr-to-project.yml ├── .gitignore ├── .jshintrc ├── .npmignore ├── .prettierignore ├── .prettierrc.cjs ├── .renovaterc.json ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── assert_version.cjs ├── dist ├── imgix-js-core.umd.js ├── index.cjs.js ├── index.d.ts └── index.esm.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── constants.js ├── helpers.js ├── index.js └── validators.js ├── test ├── performance │ └── test-benchmarks.js ├── test-_buildSrcSet.js ├── test-_buildURL.js ├── test-buildSrcSet.js ├── test-buildURL.js ├── test-client.js ├── test-extractURL.js ├── test-pathEncoding.js └── test-validators.js ├── tsconfig.json └── types ├── index.d.ts ├── index.test-d.ts ├── tsconfig.json └── tslint.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "browsers": ["last 2 versions", "safari >= 7"] 6 | }, 7 | "modules": false 8 | }] 9 | ] 10 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@5.1.0 5 | jobs: 6 | test: 7 | parameters: 8 | version: 9 | default: "stable" 10 | description: Node.JS version to install 11 | type: string 12 | docker: 13 | - image: cimg/node:<> 14 | steps: 15 | - checkout 16 | - node/install-packages 17 | - run: npm run test 18 | - run: 19 | name: "Test that build is successful" 20 | command: npm run build 21 | - run: 22 | name: "Test that compile is successful" 23 | command: npm run compile 24 | deploy: 25 | docker: 26 | - image: cimg/node:20.2 27 | steps: 28 | - checkout 29 | - node/install-packages 30 | - run: npx semantic-release 31 | workflows: 32 | test: 33 | jobs: 34 | - test: 35 | matrix: 36 | parameters: 37 | version: 38 | - "20.2" 39 | - "lts" 40 | - deploy: 41 | requires: 42 | - test -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default owners for repo 2 | * @imgix/imgix-sdk-team 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Before you submit:** 7 | 8 | - [ ] Please read the [contributing guidelines](CONTRIBUTING.md) 9 | - [ ] Please search through the existing issues (both open AND closed) to see if your issue has been discussed before. Github issue search can be used for this: https://github.com/imgix/js-core/issues?utf8=%E2%9C%93&q=is%3Aissue 10 | - [ ] Please ensure the problem has been isolated and reduced. This link explains more: http://css-tricks.com/6263-reduced-test-cases/ 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. Please strive to reach the **root problem** of your issue to avoid the XY problem. See more: https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem 14 | 15 | **To Reproduce** 16 | A bug is a _demonstrable problem_ that is caused by the code in the repository. Thus, the contributors need a way to reproduce your issue - if we can't reproduce your issue, we can't help you! Also, please be as detailed as possible. 17 | 18 | [a link to a codesandox or repl.it] 19 | 20 | [alternatively, please provide a code example] 21 | 22 | ```js 23 | // A *self-contained* demonstration of the problem follows... 24 | // This should be able to be dropped into a file with @imgix/js-core installed and just work 25 | ``` 26 | 27 | Steps to reproduce the behaviour: 28 | 29 | 1. Go to '...' 30 | 2. Click on '....' 31 | 3. Scroll down to '....' 32 | 4. See error 33 | 34 | **Expected behaviour** 35 | A clear and concise description of what you expected to happen. 36 | 37 | **Screenshots** 38 | If applicable, add screenshots to help explain your problem. 39 | 40 | **Information:** 41 | 42 | - @imgix/js-core version: [e.g. v1.0] 43 | - browser version: [include link from [https://www.whatsmybrowser.org/](https://www.whatsmybrowser.org/) or details about the OS used and browser version] 44 | 45 | **Additional context** 46 | Add any other context about the problem here. 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Before you submit:** 7 | 8 | - [ ] Please read the [contributing guidelines](CONTRIBUTING.md) 9 | - [ ] Please search through the existing issues (both open AND closed) to see if your feature has already been discussed. Github issue search can be used for this: https://github.com/imgix/js-core/issues?utf8=%E2%9C%93&q=is%3Aissue 10 | - [ ] Please take a moment to find out whether your idea fits with the scope and aims of the project 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of how this feature would function. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about the project 4 | --- 5 | 6 | **Before you submit:** 7 | 8 | - [ ] Please read the [contributing guidelines](CONTRIBUTING.md) 9 | - [ ] Please search through the existing issues (both open AND closed) to see if your question has already been discussed. Github issue search can be used for this: https://github.com/imgix/js-core/issues?utf8=%E2%9C%93&q=is%3Aissue 10 | 11 | **Question** 12 | A clear and concise description of your question 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Description 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ## Checklist 18 | 19 | 22 | 23 | 24 | 25 | - [ ] Read the [contributing guidelines](CONTRIBUTING.md). 26 | - [ ] Each commit follows the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) spec format. 27 | - [ ] Update the readme (if applicable). 28 | - [ ] Update or add any necessary API documentation (if applicable) 29 | - [ ] All existing unit tests are still passing (if applicable). 30 | 31 | 32 | 33 | - [ ] Add some [steps](#steps-to-test) so we can test your bug fix or feature (if applicable). 34 | - [ ] Add new passing unit tests to cover the code introduced by your PR (if applicable). 35 | - [ ] Any breaking changes are specified on the commit on which they are introduced with `BREAKING CHANGE` in the body of the commit. 36 | - [ ] If this is a big feature with breaking changes, consider opening an issue to discuss first. This is completely up to you, but please keep in mind that your PR might not be accepted. 37 | -------------------------------------------------------------------------------- /.github/workflows/add-issue-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add issues to project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.5.0 14 | with: 15 | project-url: https://github.com/orgs/imgix/projects/4 16 | github-token: ${{ secrets.GH_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/add-pr-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add needs-review and size/XL pull requests to projects 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.5.0 14 | with: 15 | project-url: https://github.com/orgs/imgix/projects/4 16 | github-token: ${{ secrets.GH_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | npm-debug.log 4 | .DS_Store 5 | .env -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esnext": true, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # npm ignore file is to override the default of reading the .gitignore file 2 | # http://mammal.io/articles/using-es6-today/ 3 | src/ 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | CHANGELOG.md 3 | package.json 4 | dist/imgix-js-core.umd.js -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | singleQuote: true, 4 | tabWidth: 2, 5 | useTabs: false, 6 | trailingComma: 'all', 7 | }; 8 | -------------------------------------------------------------------------------- /.renovaterc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>imgix/renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 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 | 5 | ## [v3.4.0](https://github.com/imgix/js-core/compare/v3.2.2...v3.4.0) (2021-12-16) 6 | 7 | ### Features 8 | 9 | * dpr srcset options ([#307](https://github.com/imgix/js-core/issues/307)) ([380abf0](https://github.com/imgix/js-core/commit/380abf094ce617d6f23208e57b46169ca9949609)) 10 | 11 | ## [v3.2.1](https://github.com/imgix/js-core/compare/v3.2.0...v3.2.1) (2021-06-28) 12 | 13 | * build: remove stale `.d.ts` file from `dist` ([#293](https://github.com/imgix/js-core/pull/293)) 14 | 15 | ## [v3.2.0](https://github.com/imgix/js-core/compare/v3.1.3...v3.2.0) (2021-06-22) 16 | 17 | * feat: export srcset TypeScript interface ([#283](https://github.com/imgix/js-core/pull/283)) 18 | * fix: remove "v" prefix from `VERSION` constant ([#289](https://github.com/imgix/js-core/pull/289)) 19 | * fix: ensure `undefined` parameters not added to url ([#286](https://github.com/imgix/js-core/pull/286)) 20 | 21 | ## [v3.1.3](https://github.com/imgix/js-core/compare/v3.1.2...v3.1.3) (2021-03-25) 22 | 23 | * build: declare esm as devDep ([#273](https://github.com/imgix/js-core/pull/273)) 24 | 25 | ## [v3.1.2](https://github.com/imgix/js-core/compare/v3.1.1...v3.1.2) (2021-03-25) 26 | 27 | * fix: improve error messages for target width validator ([#267](https://github.com/imgix/js-core/pull/267)) 28 | * build: use js extensions ([#269](https://github.com/imgix/js-core/pull/269)) 29 | 30 | ## [v3.1.1](https://github.com/imgix/js-core/compare/v3.1.0...v3.1.1) (2021-03-23) 31 | 32 | * fix: validate minWidth, maxWidth, widthTolerance ([#257](https://github.com/imgix/js-core/pull/257)) 33 | * fix: remove type and browser attributes from package.json ([#260](https://github.com/imgix/js-core/pull/260)) 34 | * build: tell rollup we want to explicitly export default ([#262](https://github.com/imgix/js-core/pull/262)) 35 | 36 | ## [v3.1.0](https://github.com/imgix/js-core/compare/v3.0.0...v3.1.0) (2021-03-09) 37 | 38 | * fix: typings for ImgixClient.targetWidths ([47658bc](https://github.com/imgix/js-core/commit/47658bc4869a156db6541cd97dfb41f6cc23351f)) 39 | 40 | ## [v3.0.0](https://github.com/imgix/js-core/compare/v2.3.2...v3.0.0) (2021-03-08) 41 | 42 | * feat: esm rewrite ([#188](https://github.com/imgix/js-core/pull/188)) 43 | * feat: remove ensureEven requirement ([#206](https://github.com/imgix/js-core/pull/206)) 44 | * feat: use mjs file extensions with type module ([#209](https://github.com/imgix/js-core/pull/209)) 45 | * feat: enforce 0.01 lower bound for widthTolerance ([#211](https://github.com/imgix/js-core/pull/211)) 46 | * feat: create a DPR srcset when a fixed height is specified ([#215](https://github.com/imgix/js-core/pull/215)) 47 | * feat: drop bower.json ([#222](https://github.com/imgix/js-core/pull/222)) 48 | * fix: percent encode plus signs in path components ([#223](https://github.com/imgix/js-core/pull/223)) 49 | * feat: static targetWidths functionality ([#248](https://github.com/imgix/js-core/pull/248)) 50 | 51 | ## [v3.0.0-beta.4](https://github.com/imgix/js-core/compare/v3.0.0-beta.2...v3.0.0-beta.4) (2021-03-04) 52 | 53 | * feat: static targetWidths functionality ([#248](https://github.com/imgix/js-core/pull/248)) 54 | 55 | ## [v3.0.0-beta.2](https://github.com/imgix/js-core/compare/2.3.2...v3.0.0-beta.2) (2021-02-24) 56 | 57 | * feat: esm rewrite ([#188](https://github.com/imgix/js-core/pull/188)) 58 | * feat: remove ensureEven requirement ([#206](https://github.com/imgix/js-core/pull/206)) 59 | * feat: use mjs file extensions with type module ([#209](https://github.com/imgix/js-core/pull/209)) 60 | * feat: enforce 0.01 lower bound for widthTolerance ([#211](https://github.com/imgix/js-core/pull/211)) 61 | * feat: create a DPR srcset when a fixed height is specified ([#215](https://github.com/imgix/js-core/pull/215)) 62 | * feat: drop bower.json ([#222](https://github.com/imgix/js-core/pull/222)) 63 | * fix: percent encode plus signs in path components ([#223](https://github.com/imgix/js-core/pull/223)) 64 | 65 | ## [2.3.2](https://github.com/imgix/js-core/compare/2.3.1...2.3.2) (2020-10-12) 66 | 67 | * fix(buildURL): ensure operation is idempotent ([#168](https://github.com/imgix/js-core/pull/168)) 68 | 69 | ## [2.3.1](https://github.com/imgix/js-core/compare/2.3.0...2.3.1) (2019-03-10) 70 | 71 | * fix: add missing variable declarations ([#121](https://github.com/imgix/js-core/pull/121)) 72 | 73 | ## [2.3.0](https://github.com/imgix/js-core/compare/2.2.1...2.3.0) (2019-03-04) 74 | 75 | * feat: add srcset option parameter to buildSrcSet() method signature ([#118](https://github.com/imgix/js-core/pull/118)) 76 | * perf(srcset): memoize generated srcset width-pairs ([#115](https://github.com/imgix/js-core/pull/115)) 77 | * fix: throw error when certain srcset modifiers are passed zero ([#114](https://github.com/imgix/js-core/pull/114)) 78 | * feat: append variable qualities to dpr srcsets ([#111](https://github.com/imgix/js-core/pull/111)) 79 | * feat: add support for defining a custom srcset width array ([#110](https://github.com/imgix/js-core/pull/110)) 80 | * feat: add support for defining a custom srcset width tolerance ([#109](https://github.com/imgix/js-core/pull/109)) 81 | * feat: add support for defining a min and max srcset width ([#108](https://github.com/imgix/js-core/pull/108)) 82 | 83 | ## [2.2.1](https://github.com/imgix/js-core/compare/2.2.0...2.2.1) (2019-11-27) 84 | 85 | * build(deps): remove typescript as runtime dependency ([#77](https://github.com/imgix/js-core/pull/77)) 86 | 87 | ## [2.2.1](https://github.com/imgix/js-core/compare/2.2.0...2.2.1) (2019-11-27) 88 | 89 | * build(deps): remove typescript as runtime dependency ([#77](https://github.com/imgix/js-core/pull/77)) 90 | 91 | ## [2.2.0](https://github.com/imgix/js-core/compare/2.1.2...2.2.0) (2019-10-22) 92 | 93 | * feat: add typescript declaration file for `ImgixClient` ([#64](https://github.com/imgix/js-core/pull/64)) 94 | 95 | ## [2.1.2](https://github.com/imgix/js-core/compare/2.1.1...2.1.2) (2019-09-17) 96 | 97 | * fix: ensure URL-legal, path-illegal characters are encoded ([#61](https://github.com/imgix/js-core/pull/61)) 98 | 99 | ## [2.1.1](https://github.com/imgix/js-core/compare/2.1.0...2.1.1) (2019-07-28) 100 | 101 | * fix: include dpr parameter when generating fixed-width srcset ([#59](https://github.com/imgix/js-core/pull/59)) 102 | 103 | ## [2.1.0](https://github.com/imgix/js-core/compare/1.2.1...2.1.0) (2019-07-28) 104 | 105 | * feat: add srcset generation ([#53](https://github.com/imgix/js-core/pull/53)) 106 | 107 | # [2.0.0](https://github.com/imgix/js-core/compare/1.4.0...2.0.0) (2019-06-06) 108 | 109 | * fix: remove deprecated domain sharding functionality ([#42](https://github.com/imgix/js-core/pull/42)) 110 | * fix: remove deprecated settings.host ([#45](https://github.com/imgix/js-core/pull/45)) 111 | 112 | ## [1.4.0](https://github.com/imgix/js-core/compare/1.3.0...1.4.0) (2019-06-05) 113 | 114 | * docs: deprecate settings.domains ([#43](https://github.com/imgix/js-core/pull/43)) 115 | * feat: add settings.domain argument ([#44](https://github.com/imgix/js-core/pull/44)) 116 | 117 | ## [1.3.0](https://github.com/imgix/js-core/compare/1.2.1...1.3.0) (2019-05-07) 118 | 119 | * deprecate domain sharding ([#39](https://github.com/imgix/js-core/pull/39)) 120 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Please read the imgix [Code of Conduct](https://github.com/imgix/code-of-conduct). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Thank you for investing your time in contributing to this project! Please take a moment to review this document in order to streamline the contribution process for you and any reviewers involved. 4 | 5 | Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. 6 | 7 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 8 | 9 | ## New contributor guide 10 | 11 | To get an overview of the project, read the [README](README.md). Here are some resources to help you get started with open source contributions: 12 | 13 | - [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github) 14 | - [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git) 15 | - [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) 16 | - [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests) 17 | 18 | ## Opening a Pull Request 19 | 20 | _To help the project's maintainers and community quickly understand the nature of your pull request, please be sure to do the following:_ 21 | 22 | 1. Include a descriptive Pull Request title. 23 | 2. Provide a detailed description that explains the nature of the change(s) introduced. This is not only helpful for your reviewer, but also for future users who may need to revisit your Pull Request for context purposes. Screenshots/video captures are helpful here! 24 | 3. Make incremental, modular changes, with a clean commit history. This helps reviewers understand your contribution more easily and maintain project quality. 25 | 26 | ### Checklist 27 | 28 | Check to see that you have completed each of the following before requesting a review of your Pull Request: 29 | 30 | - [ ] All existing unit tests are still passing (if applicable) 31 | - [ ] Add new passing unit tests to cover the code introduced by your PR 32 | - [ ] Update the README 33 | - [ ] Update or add any necessary API documentation 34 | - [ ] All commits in the branch adhere to the [conventional commit](#conventional-commit-spec) format: e.g. `fix: bug #issue-number` 35 | 36 | ## Conventional Commit Spec 37 | 38 | Commits should be in the format `(): `. This allows our team to leverage tooling for automatic releases and changelog generation. An example of a commit in this format might be: `docs(readme): fix typo in documentation` 39 | 40 | `type` can be any of the follow: 41 | 42 | - `feat`: a feature, or breaking change 43 | - `fix`: a bug-fix 44 | - `test`: Adding missing tests or correcting existing tests 45 | - `docs`: documentation only changes (readme, changelog, contributing guide) 46 | - `refactor`: a code change that neither fixes a bug nor adds a feature 47 | - `chore`: reoccurring tasks for project maintainability (example scopes: release, deps) 48 | - `config`: changes to tooling configurations used in the project 49 | - `build`: changes that affect the build system or external dependencies (example scopes: npm, bundler, gradle) 50 | - `ci`: changes to CI configuration files and scripts (example scopes: travis) 51 | - `perf`: a code change that improves performance 52 | - `style`: changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 53 | 54 | `scope` is optional, and can be anything. 55 | `description` should be a short description of the change, written in the imperative-mood. 56 | 57 | ### Example workflow 58 | 59 | Follow this process if you'd like your work considered for inclusion in the 60 | project: 61 | 62 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 63 | and configure the remotes: 64 | 65 | ```bash 66 | # Clone your fork of the repo into the current directory 67 | git clone git@github.com:/js-core.git 68 | # Navigate to the newly cloned directory 69 | cd js-core 70 | # Assign the original repo to a remote called "upstream" 71 | git remote add upstream https://github.com/imgix/js-core 72 | ``` 73 | 74 | 2. If you cloned a while ago, get the latest changes from upstream: 75 | 76 | ```bash 77 | git checkout 78 | git pull upstream 79 | ``` 80 | 81 | 3. Create a new topic branch (off the main project development branch) to 82 | contain your feature, change, or fix: 83 | 84 | ```bash 85 | git checkout -b 86 | ``` 87 | 88 | 4. Commit your changes in logical chunks. Use Git's 89 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 90 | feature to tidy up your commits before making them public. 91 | 92 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 93 | 94 | ```bash 95 | git pull [--rebase] upstream 96 | ``` 97 | 98 | 6. Push your topic branch up to your fork: 99 | 100 | ```bash 101 | git push origin 102 | ``` 103 | 104 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 105 | with a clear title and description. 106 | 107 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to 108 | license your work under the same license as that used by the project. 109 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Zebrafish Labs 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ![imgix logo](https://assets.imgix.net/sdk-imgix-logo.svg) 4 | 5 | `@imgix/js-core` is a JavaScript library for generating image URLs with [imgix](https://www.imgix.com/) that can be used in browser or server-side settings. 6 | 7 | [![NPM Version](https://img.shields.io/npm/v/imgix-core-js.svg)](https://www.npmjs.com/package/@imgix/js-core) 8 | [![Build Status](https://circleci.com/gh/imgix/js-core.svg?style=shield)](https://circleci.com/gh/imgix/js-core) 9 | [![Monthly Downloads](https://img.shields.io/npm/dm/imgix-core-js.svg)](https://www.npmjs.com/package/@imgix/js-core) 10 | [![Minified Size](https://img.shields.io/bundlephobia/min/imgix-core-js)](https://bundlephobia.com/result?p=imgix-core-js) 11 | [![License](https://img.shields.io/github/license/imgix/js-core)](https://github.com/imgix/js-core/blob/main/LICENSE.md) 12 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimgix%2Fimgix-core-js.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fimgix%2Fimgix-core-js?ref=badge_shield) 13 | 14 | --- 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | - [Installing](#installing) 23 | - [Usage](#usage) 24 | * [CommonJS](#commonjs) 25 | * [ES6 Modules](#es6-modules) 26 | * [In-browser](#in-browser) 27 | - [Configuration](#configuration) 28 | - [API](#api) 29 | * [`ImgixClient.buildURL(path, params, options)`](#imgixclientbuildurlpath-params-options) 30 | * [`ImgixClient.buildSrcSet(path, params, options)`](#imgixclientbuildsrcsetpath-params-options) 31 | + [Fixed Image Rendering](#fixed-image-rendering) 32 | + [Custom Widths](#custom-widths) 33 | + [Width Tolerance](#width-tolerance) 34 | + [Minimum and Maximum Width Ranges](#minimum-and-maximum-width-ranges) 35 | + [Variable Qualities](#variable-qualities) 36 | + [Disable Path Encoding](#disable-path-encoding) 37 | * [Web Proxy Sources](#web-proxy-sources) 38 | - [What is the `Ixlib` Param on Every Request?](#what-is-the-ixlib-param-on-every-request) 39 | - [Support for Management API](#support-for-management-api) 40 | - [Testing](#testing) 41 | - [License](#license) 42 | 43 | 44 | 45 | ## Installing 46 | 47 | @imgix/js-core can be installed via npm: 48 | 49 | ```bash 50 | npm install @imgix/js-core 51 | ``` 52 | 53 | ## Usage 54 | 55 | Depending on your module system, using @imgix/js-core is done a few different ways. The most common entry point will be the `ImgixClient` class. Whenever you provide data to `ImgixClient`, make sure it is not already URL-encoded, as the library handles proper encoding internally. 56 | 57 | ### CommonJS 58 | 59 | ```js 60 | const ImgixClient = require('@imgix/js-core'); 61 | 62 | const client = new ImgixClient({ 63 | domain: 'testing.imgix.net', 64 | secureURLToken: '', 65 | }); 66 | 67 | const url = client.buildURL('/path/to/image.png', { 68 | w: 400, 69 | h: 300, 70 | }); 71 | 72 | console.log(url); // => "https://testing.imgix.net/users/1.png?w=400&h=300&s=…" 73 | ``` 74 | 75 | ### ES6 Modules 76 | 77 | ```js 78 | import ImgixClient from '@imgix/js-core'; 79 | 80 | const client = new ImgixClient({ 81 | domain: 'testing.imgix.net', 82 | secureURLToken: '', 83 | }); 84 | 85 | const url = client.buildURL('/path/to/image.png', { w: 400, h: 300 }); 86 | console.log(url); // => 'https://testing.imgix.net/users/1.png?w=400&h=300&s=…' 87 | ``` 88 | 89 | ### In-browser 90 | 91 | ```js 92 | var client = new ImgixClient({ 93 | domain: 'testing.imgix.net', 94 | // Do not use signed URLs with `secureURLToken` on the client side, 95 | // as this would leak your token to the world. Signed URLs should 96 | // be generated on the server. 97 | }); 98 | 99 | var url = client.buildURL('/path/to/image.png', { w: 400, h: 300 }); 100 | console.log(url); // => "https://testing.imgix.net/users/1.png?w=400&h=300" 101 | ``` 102 | 103 | ## Configuration 104 | 105 | The following options can be used when creating an instance of `ImgixClient`: 106 | 107 | - **`domain`:** String, required. The imgix domain that will be used when constructing URLs. Defaults to `null`. 108 | - **`useHTTPS`:** Boolean. Specifies whether constructed URLs should use the HTTPS protocol. Defaults to `true`. 109 | - **`includeLibraryParam`:** Boolean. Specifies whether the constructed URLs will include an [`ixlib` parameter](#what-is-the-ixlib-param-on-every-request). Defaults to `true`. 110 | - **`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`. 111 | - :warning: *The `secureURLToken` option should only be used in server-side applications to prevent exposing your secure token.* :warning: 112 | 113 | 114 | ## API 115 | 116 | ### `ImgixClient.buildURL(path, params, options)` 117 | 118 | - **`path`:** String, required. A full, unencoded path to the image. This includes any additional directory information required to [locate the image](https://docs.imgix.com/setup/serving-images) within a source. 119 | - **`params`:** Object. Any number of imgix rendering API [parameters](https://docs.imgix.com/apis/url). 120 | - **`options`:** Object. Any number of modifiers, described below: 121 | - [**`disablePathEncoding`**](#disable-path-encoding): Boolean. Disables encoding logic applied to the image path. 122 | - [**`encoder`**](#encoder): Function. Applies custom logic to encode the image path and query parameters. 123 | 124 | Construct a single image URL by passing in the image `path` and any rendering API parameters. 125 | 126 | ```js 127 | const client = new ImgixClient({ 128 | domain: 'testing.imgix.net', 129 | }); 130 | 131 | const url = client.buildURL('folder/image.jpg', { 132 | w: 1000, 133 | }); 134 | ``` 135 | 136 | **Returns**: an image URL as a string. 137 | 138 | ```html 139 | https://testing.imgix.net/folder/image.jpg?w=1000&ixlib=js-... 140 | ``` 141 | 142 | ### `ImgixClient.buildSrcSet(path, params, options)` 143 | 144 | 145 | 146 | - **`path`:** String, required. A full, unencoded path to the image. This includes any additional directory information required to [locate the image](https://docs.imgix.com/setup/serving-images) within a source. 147 | - **`params`:** Object. Any number of imgix rendering API [parameters](https://docs.imgix.com/apis/url). 148 | - **`options`:** Object. Any number of srcset modifiers, described below: 149 | - [**`widths`**](#custom-widths) 150 | - [**`widthTolerance`**](#width-tolerance) 151 | - [**`minWidth`**](#minimum-and-maximum-width-ranges) 152 | - [**`maxWidth`**](#minimum-and-maximum-width-ranges) 153 | - [**`disableVariableQuality`**](#variable-qualities) 154 | - [**`devicePixelRatios`**](#fixed-image-rendering) 155 | - [**`variableQualities`**](#variable-qualities) 156 | - [**`disablePathEncoding`**](#disable-path-encoding) 157 | 158 | 159 | 160 | The @imgix/js-core module allows for generation of custom `srcset` attributes, which can be invoked through `buildSrcSet()`. By default, the `srcset` generated will allow for responsive size switching by building a list of image-width mappings. 161 | 162 | ```js 163 | const client = new ImgixClient({ 164 | domain: 'testing.imgix.net', 165 | secureURLToken: 'my-token', 166 | }); 167 | 168 | const srcset = client.buildSrcSet('image.jpg'); 169 | 170 | console.log(srcset); 171 | ``` 172 | 173 | **Returns**: A `srcset` attribute value as a string. 174 | 175 | 176 | 177 | ```html 178 | https://testing.imgix.net/image.jpg?w=100&s=e2e581a39c917bdee50b2f8689c30893 100w, 179 | https://testing.imgix.net/image.jpg?w=116&s=836e0bc15da2ad74af8130d93a0ebda6 116w, 180 | https://testing.imgix.net/image.jpg?w=134&s=688416d933381acda1f57068709aab79 134w, 181 | ... 182 | https://testing.imgix.net/image.jpg?w=7400&s=91779d82a0e1ac16db04c522fa4017e5 7400w, 183 | https://testing.imgix.net/image.jpg?w=8192&s=59eb881b618fed314fe30cf9e3ec7b00 8192w 184 | ``` 185 | 186 | 187 | 188 | #### Fixed Image Rendering 189 | 190 | Specifying either a `w` or a `h` parameter to `buildSrcSet()` will create a DPR-based srcset. This DPR-based srcset allows for the fixed-sized image to be served at different resolutions (i.e. at different pixel densities). 191 | 192 | ```js 193 | const client = new ImgixClient({ 194 | domain: 'testing.imgix.net', 195 | secureURLToken: 'my-token', 196 | }); 197 | 198 | const srcset = client.buildSrcSet('image.jpg', { 199 | h: 800, 200 | ar: '3:2', 201 | fit: 'crop', 202 | }); 203 | 204 | console.log(srcset); 205 | ``` 206 | 207 | Will produce the following attribute value: 208 | 209 | 210 | 211 | ```html 212 | https://testing.imgix.net/image.jpg?h=800&ar=3%3A2&fit=crop&dpr=1&s=3d754a157458402fd3e26977107ade74 1x, 213 | https://testing.imgix.net/image.jpg?h=800&ar=3%3A2&fit=crop&dpr=2&s=a984ad1a81d24d9dd7d18195d5262c82 2x, 214 | https://testing.imgix.net/image.jpg?h=800&ar=3%3A2&fit=crop&dpr=3&s=8b93ab83d3f1ede4887e6826112d60d1 3x, 215 | https://testing.imgix.net/image.jpg?h=800&ar=3%3A2&fit=crop&dpr=4&s=df7b67aa0439588edbfc1c249b3965d6 4x, 216 | https://testing.imgix.net/image.jpg?h=800&ar=3%3A2&fit=crop&dpr=5&s=7c4b8adb733db37d00240da4ca65d410 5x 217 | ``` 218 | 219 | 220 | 221 | By default, this library generates a `srcset` with pixel density values of `1` through `5`. 222 | These target ratios can be controlled by using the `devicePixelRatios` parameters. 223 | 224 | ```js 225 | const client = new ImgixClient({ 226 | domain: 'testing.imgix.net', 227 | secureURLToken: 'my-token', 228 | }); 229 | 230 | const srcset = client.buildSrcSet( 231 | 'image.jpg', 232 | { 233 | h: 800, 234 | ar: '3:2', 235 | fit: 'crop', 236 | }, 237 | { 238 | devicePixelRatios: [1, 2], 239 | }, 240 | ); 241 | 242 | console.log(srcset); 243 | ``` 244 | 245 | Will result in a smaller srcset. 246 | 247 | ```html 248 | https://testing.imgix.net/image.jpg?h=800&ar=3%3A2&fit=crop&dpr=1&s=3d754a157458402fd3e26977107ade74 249 | 1x, 250 | https://testing.imgix.net/image.jpg?h=800&ar=3%3A2&fit=crop&dpr=2&s=a984ad1a81d24d9dd7d18195d5262c82 251 | 2x 252 | ``` 253 | 254 | For more information to better understand `srcset`, we highly recommend [Eric Portis' "Srcset and sizes" article](https://ericportis.com/posts/2014/srcset-sizes/) which goes into depth about the subject. 255 | 256 | #### Custom Widths 257 | 258 | In situations where specific widths are desired when generating `srcset` pairs, a user can specify them by passing an array of positive integers as `widths` to the third options object: 259 | 260 | ```js 261 | const client = new ImgixClient({ 262 | domain: 'testing.imgix.net', 263 | }); 264 | 265 | const srcset = client.buildSrcSet( 266 | 'image.jpg', 267 | {}, 268 | { widths: [100, 500, 1000, 1800] }, 269 | ); 270 | 271 | console.log(srcset); 272 | ``` 273 | 274 | Will generate the following `srcset` of width pairs: 275 | 276 | ```html 277 | https://testing.imgix.net/image.jpg?w=100 100w, 278 | https://testing.imgix.net/image.jpg?w=500 500w, 279 | https://testing.imgix.net/image.jpg?w=1000 1000w, 280 | https://testing.imgix.net/image.jpg?w=1800 1800w 281 | ``` 282 | 283 | **Note:** that in situations where a `srcset` is being rendered as a [fixed image](#fixed-image-rendering), any custom `widths` passed in will be ignored. Additionally, if both `widths` and a `widthTolerance` are passed to the `buildSrcSet` method, the custom widths list will take precedence. 284 | 285 | #### Width Tolerance 286 | 287 | The `srcset` width tolerance dictates the maximum tolerated size difference between an image's downloaded size and its rendered size. For example: setting this value to 0.1 means that an image will not render more than 10% larger or smaller than its native size. In practice, the image URLs generated for a width-based srcset attribute will grow by twice this rate. A lower tolerance means images will render closer to their native size (thereby increasing perceived image quality), but a large srcset list will be generated and consequently users may experience lower rates of cache-hit for pre-rendered images on your site. 288 | 289 | By default this rate is set to 8 percent, which we consider to be the ideal rate for maximizing cache hits without sacrificing visual quality. Users can specify their own width tolerance by providing a positive scalar value as `widthTolerance` to the third options object: 290 | 291 | ```js 292 | const client = new ImgixClient({ 293 | domain: 'testing.imgix.net', 294 | }); 295 | 296 | const srcset = client.buildSrcSet('image.jpg', {}, { widthTolerance: 0.2 }); 297 | 298 | console.log(srcset); 299 | ``` 300 | 301 | In this case, the `width_tolerance` is set to 20 percent, which will be reflected in the difference between subsequent widths in a srcset pair: 302 | 303 | 304 | 305 | ```html 306 | https://testing.imgix.net/image.jpg?w=100 100w, 307 | https://testing.imgix.net/image.jpg?w=140 140w, 308 | https://testing.imgix.net/image.jpg?w=196 196w, 309 | ... 310 | https://testing.imgix.net/image.jpg?w=8192 8192w 311 | ``` 312 | 313 | 314 | 315 | #### Minimum and Maximum Width Ranges 316 | 317 | In certain circumstances, you may want to limit the minimum or maximum value of the non-fixed `srcset` generated by the `buildSrcSet()` method. To do this, you can pass in an options object as a third argument, providing positive integers as `minWidth` and/or `maxWidth` attributes: 318 | 319 | ```js 320 | const client = new ImgixClient({ 321 | domain: 'testing.imgix.net', 322 | }); 323 | 324 | const srcset = client.buildSrcSet( 325 | 'image.jpg', 326 | {}, 327 | { minWidth: 500, maxWidth: 2000 }, 328 | ); 329 | 330 | console.log(srcset); 331 | ``` 332 | 333 | Will result in a smaller, more tailored srcset. 334 | 335 | ```html 336 | https://testing.imgix.net/image.jpg?w=500 500w, 337 | https://testing.imgix.net/image.jpg?w=580 580w, 338 | https://testing.imgix.net/image.jpg?w=672 672w, 339 | https://testing.imgix.net/image.jpg?w=780 780w, 340 | https://testing.imgix.net/image.jpg?w=906 906w, 341 | https://testing.imgix.net/image.jpg?w=1050 1050w, 342 | https://testing.imgix.net/image.jpg?w=1218 1218w, 343 | https://testing.imgix.net/image.jpg?w=1414 1414w, 344 | https://testing.imgix.net/image.jpg?w=1640 1640w, 345 | https://testing.imgix.net/image.jpg?w=1902 1902w, 346 | https://testing.imgix.net/image.jpg?w=2000 2000w 347 | ``` 348 | 349 | Remember that browsers will apply a device pixel ratio as a multiplier when selecting which image to download from a `srcset`. For example, even if you know your image will render no larger than 1000px, specifying `options: { max_srcset: 1000 }` will give your users with DPR higher than 1 no choice but to download and render a low-resolution version of the image. Therefore, it is vital to factor in any potential differences when choosing a minimum or maximum range. 350 | 351 | **Note:** that according to the [imgix API](https://docs.imgix.com/apis/url/size/w), the maximum renderable image width is 8192 pixels. 352 | 353 | #### Variable Qualities 354 | 355 | This library will automatically append a variable `q` parameter mapped to each `dpr` parameter when generating a [fixed-image](https://github.com/imgix/js-core#fixed-image-rendering) srcset. This technique is commonly used to compensate for the increased filesize of high-DPR images. Since high-DPR images are displayed at a higher pixel density on devices, image quality can be lowered to reduce overall filesize without sacrificing perceived visual quality. For more information and examples of this technique in action, see [this blog post](https://blog.imgix.com/2016/03/30/dpr-quality). 356 | 357 | This behavior will respect any overriding `q` value passed in as a parameter. Additionally, it can be disabled altogether by passing `{ disableVariableQuality: true }` to the third argument of `buildSrcSet()`. 358 | 359 | This behavior specifically occurs when a [fixed-size image](https://github.com/imgix/js-core#fixed-image-rendering) is rendered, for example: 360 | 361 | ```js 362 | const client = new ImgixClient({ 363 | domain: 'testing.imgix.net', 364 | }); 365 | 366 | const srcset = client.buildSrcSet('image.jpg', { w: 100 }); 367 | 368 | console.log(srcset); 369 | ``` 370 | 371 | Will generate a srcset with the following `q` to `dpr` mapping: 372 | 373 | ```html 374 | https://testing.imgix.net/image.jpg?w=100&dpr=1&q=75 1x, 375 | https://testing.imgix.net/image.jpg?w=100&dpr=2&q=50 2x, 376 | https://testing.imgix.net/image.jpg?w=100&dpr=3&q=35 3x, 377 | https://testing.imgix.net/image.jpg?w=100&dpr=4&q=23 4x, 378 | https://testing.imgix.net/image.jpg?w=100&dpr=5&q=20 5x 379 | ``` 380 | 381 | Quality parameters is overridable for each `dpr` by passing `variableQualities` parameters. 382 | 383 | ```js 384 | const client = new ImgixClient({ 385 | domain: 'testing.imgix.net', 386 | }); 387 | 388 | const srcset = client.buildSrcSet( 389 | 'image.jpg', 390 | { w: 100 }, 391 | { variableQualities: { 1: 45, 2: 30, 3: 20, 4: 15, 5: 10 } }, 392 | ); 393 | 394 | console.log(srcset); 395 | ``` 396 | 397 | Will generate the following custom `q` to `dpr` mapping: 398 | 399 | ```html 400 | https://testing.imgix.net/image.jpg?w=100&dpr=1&q=45 1x, 401 | https://testing.imgix.net/image.jpg?w=100&dpr=2&q=30 2x, 402 | https://testing.imgix.net/image.jpg?w=100&dpr=3&q=20 3x, 403 | https://testing.imgix.net/image.jpg?w=100&dpr=4&q=15 4x, 404 | https://testing.imgix.net/image.jpg?w=100&dpr=5&q=10 5x 405 | ``` 406 | 407 | #### Disable Path Encoding 408 | 409 | This library will encode by default all paths passed to both `buildURL` and `buildSrcSet` methods. To disable path encoding, pass `{ disablePathEncoding: true }` to the third argument `options` of `buildURL()` or `buildSrcSet()`. 410 | 411 | ```js 412 | const client = new ImgixClient({ 413 | domain: 'testing.imgix.net', 414 | }); 415 | 416 | const src = client.buildURL( 417 | 'file+with%20some+crazy?things.jpg', 418 | {}, 419 | { disablePathEncoding: true }, 420 | ); 421 | console.log(src); 422 | 423 | const srcset = client.buildSrcSet( 424 | 'file+with%20some+crazy?things.jpg', 425 | {}, 426 | { disablePathEncoding: true }, 427 | ); 428 | console.log(srcset); 429 | ``` 430 | 431 | Normally this would output a src of `https://testing.imgix.net/file%2Bwith%2520some%2Bcrazy%3Fthings.jpg`, but since path encoding is disabled, it will output a src of `https://testing.imgix.net/file+with%20some+crazy?things.jpg`. 432 | 433 | ### Custom URL encoding 434 | 435 | This library will encode by default using `encodeURI()`, `encodeURIComponent()`, or a combination of the two depending on the image path and parameters. 436 | You can define a custom encoding function in `buildURL's `options` object **if** you wish to override this behavior. Note that encoding your own URL can result in a URL that is **not** recognized by the imgix rendering API. 437 | 438 | ```js 439 | const ImgixClient = require("@imgix/js-core"); 440 | const client = new ImgixClient({ 441 | domain: 'test.imgix.com', 442 | secureURLToken: 'xxxxxxxx', 443 | }); 444 | 445 | client.buildURL( 446 | "https://proxy.imgix.net/image.jpg", 447 | { 448 | "txt": "test!(')*" 449 | }, 450 | { 451 | encoder: (path) => encodeURI(path).replace("'", "%27") 452 | } 453 | ) 454 | 455 | /* 456 | output: 457 | https://proxy.imgix.net/image.jpg?txt=test!(%27) 458 | */ 459 | 460 | ``` 461 | 462 | The custom encoder also accepts a second optional parameter `key` which allows users to modify how query parameters are encoded. This parameter does not affect the custom encoding logic of the image path. 463 | 464 | ```js 465 | const ImgixClient = require("@imgix/js-core"); 466 | const client = new ImgixClient({ 467 | domain: 'test.imgix.com', 468 | secureURLToken: 'xxxxxxxx', 469 | }); 470 | 471 | client.buildURL( 472 | "https://proxy.imgix.net/image.jpg", 473 | { 474 | "txt": "test!(')*" 475 | }, 476 | { 477 | encoder: (value, key) => key?.substr(-2) === '64' ? Base64.encodeURI(value) : value.replace(' ', "+") 478 | } 479 | ) 480 | ``` 481 | 482 | ### Web Proxy Sources 483 | 484 | If you are using a [Web Proxy Source](https://docs.imgix.com/setup/creating-sources/web-proxy), all you need to do is pass the full image URL you would like to proxy to `@imgix/js-core` as the path, and include a `secureURLToken` when creating the client. `@imgix/js-core` will then encode this full URL into a format that imgix will understand, thus creating a proxy URL for you. 485 | 486 | ```js 487 | import ImgixClient from '@imgix/js-core'; 488 | 489 | const client = new ImgixClient({ 490 | domain: 'my-proxy-domain.imgix.net', 491 | secureURLToken: '', 492 | }); 493 | 494 | client.buildURL('https://example.com/image-to-proxy.jpg', {}); 495 | client.buildSrcSet('https://example.com/image-to-proxy.jpg', {}); 496 | ``` 497 | 498 | ## What is the `Ixlib` Param on Every Request? 499 | 500 | For security and diagnostic purposes, we sign all requests with the language and version of library used to generate the URL. 501 | 502 | This can be disabled by passing a falsy value for the `includeLibraryParam` option to `new ImgixClient`: 503 | 504 | ```js 505 | new ImgixClient({ 506 | domain: 'my-source.imgix.net', 507 | includeLibraryParam: false, 508 | }); 509 | ``` 510 | 511 | ## Support for Management API 512 | 513 | Users looking for client library support for the imgix [management API](https://docs.imgix.com/apis/management) should use the [imgix-management-js](https://github.com/imgix/imgix-management-js) library. These two projects may be merged at a future date. 514 | 515 | ## Testing 516 | 517 | @imgix/js-core uses mocha for testing. Here’s how to run those tests: 518 | 519 | ```bash 520 | npm test 521 | ``` 522 | 523 | ## License 524 | 525 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimgix%2Fimgix-core-js.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fimgix%2Fimgix-core-js?ref=badge_large) 526 | -------------------------------------------------------------------------------- /assert_version.cjs: -------------------------------------------------------------------------------- 1 | var packageVersion = require('./package').version; 2 | var ImgixClient = require('./dist/index.cjs.js'); 3 | 4 | if (packageVersion.includes(ImgixClient.version())) { 5 | return 0; 6 | } else { 7 | process.stdout.write( 8 | 'FAIL: package.json and src/constants.mjs versions do not match!\n', 9 | ); 10 | return 1; 11 | } -------------------------------------------------------------------------------- /dist/imgix-js-core.umd.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).ImgixClient=e()}(this,function(){"use strict";function e(e,t){var r,n=Object.keys(e);return Object.getOwnPropertySymbols&&(r=Object.getOwnPropertySymbols(e),t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)),n}function s(n){for(var t=1;tt.length)&&(e=t.length);for(var r=0,n=new Array(e);r{let r={};return t.forEach((t,e)=>r[t]=e),r})(d),F=/^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/,p=String.fromCharCode.bind(String),g="function"==typeof Uint8Array.from?Uint8Array.from.bind(Uint8Array):(t,e=t=>t)=>new Uint8Array(Array.prototype.slice.call(t,0).map(e)),b=t=>t.replace(/=/g,"").replace(/[+\/]/g,t=>"+"==t?"-":"_"),v=t=>t.replace(/[^A-Za-z0-9\+\/]/g,"");var m=e=>{let r,n,o,i,a="";var t=e.length%3;for(let t=0;t>18&63]+d[r>>12&63]+d[r>>6&63]+d[63&r]}return t?a.slice(0,t-3)+"===".substring(t):a};const w=t?t=>btoa(t):a?t=>Buffer.from(t,"binary").toString("base64"):m,A=a?t=>Buffer.from(t).toString("base64"):r=>{var n=[];for(let t=0,e=r.length;te?b(A(t)):A(t),D=t=>{var e;return t.length<2?(e=t.charCodeAt(0))<128?t:e<2048?p(192|e>>>6)+p(128|63&e):p(224|e>>>12&15)+p(128|e>>>6&63)+p(128|63&e):(e=65536+1024*(t.charCodeAt(0)-55296)+(t.charCodeAt(1)-56320),p(240|e>>>18&7)+p(128|e>>>12&63)+p(128|e>>>6&63)+p(128|63&e))},W=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g,x=t=>t.replace(W,D),T=a?t=>Buffer.from(t,"utf8").toString("base64"):c?t=>A(c.encode(t)):t=>w(x(t)),P=(t,e=!1)=>e?b(T(t)):T(t);t=t=>P(t,!0);const H=/[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g,M=t=>{switch(t.length){case 4:var e=((7&t.charCodeAt(0))<<18|(63&t.charCodeAt(1))<<12|(63&t.charCodeAt(2))<<6|63&t.charCodeAt(3))-65536;return p(55296+(e>>>10))+p(56320+(1023&e));case 3:return p((15&t.charCodeAt(0))<<12|(63&t.charCodeAt(1))<<6|63&t.charCodeAt(2));default:return p((31&t.charCodeAt(0))<<6|63&t.charCodeAt(1))}},U=t=>t.replace(H,M);var j=e=>{if(e=e.replace(/\s+/g,""),!F.test(e))throw new TypeError("malformed base64.");e+="==".slice(2-(3&e.length));let r,n="",o,i;for(let t=0;t>16&255):64===i?p(r>>16&255,r>>8&255):p(r>>16&255,r>>8&255,255&r);return n};const B=z?t=>atob(v(t)):a?t=>Buffer.from(t,"base64").toString("binary"):j,C=a?t=>g(Buffer.from(t,"base64")):t=>g(B(t),t=>t.charCodeAt(0)),R=t=>C(O(t)),Z=a?t=>Buffer.from(t,"base64").toString("utf8"):u?t=>u.decode(C(t)):t=>U(B(t)),O=t=>v(t.replace(/[-_]/g,t=>"-"==t?"+":"/")),_=t=>Z(O(t));function N(){var t=(t,e)=>Object.defineProperty(String.prototype,t,Q(e));t("fromBase64",function(){return _(this)}),t("toBase64",function(t){return P(this,t)}),t("toBase64URI",function(){return P(this,!0)}),t("toBase64URL",function(){return P(this,!0)}),t("toUint8Array",function(){return R(this)})}function q(){var t=(t,e)=>Object.defineProperty(Uint8Array.prototype,t,Q(e));t("toBase64",function(t){return S(this,t)}),t("toBase64URI",function(){return S(this,!0)}),t("toBase64URL",function(){return S(this,!0)})}const Q=t=>({value:t,enumerable:!1,writable:!0,configurable:!0});const V={version:k,VERSION:"3.7.2",atob:B,atobPolyfill:j,btoa:w,btoaPolyfill:m,fromBase64:_,toBase64:P,encode:P,encodeURI:t,encodeURL:t,utob:x,btou:U,decode:_,isValid:t=>{return"string"==typeof t&&(t=t.replace(/\s+/g,"").replace(/={0,2}$/,""),!/[^\s0-9a-zA-Z\+/]/.test(t)||!/[^\s0-9a-zA-Z\-_]/.test(t))},fromUint8Array:S,toUint8Array:R,extendString:N,extendUint8Array:q,extendBuiltins:()=>{N(),q()}};function $(t,e){return t(e={exports:{}},e.exports),e.exports}function G(t){return null!=t&&(X(t)||"function"==typeof(e=t).readFloatLE&&"function"==typeof e.slice&&X(e.slice(0,0))||!!t._isBuffer);var e}var J=$(function(t){var i,r;i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r={rotl:function(t,e){return t<>>32-e},rotr:function(t,e){return t<<32-e|t>>>e},endian:function(t){if(t.constructor==Number)return 16711935&r.rotl(t,8)|4278255360&r.rotl(t,24);for(var e=0;e>>5]|=t[r]<<24-n%32;return e},wordsToBytes:function(t){for(var e=[],r=0;r<32*t.length;r+=8)e.push(t[r>>>5]>>>24-r%32&255);return e},bytesToHex:function(t){for(var e=[],r=0;r>>4).toString(16)),e.push((15&t[r]).toString(16));return e.join("")},hexToBytes:function(t){for(var e=[],r=0;r>>6*(3-o)&63)):e.push("=");return e.join("")},base64ToBytes:function(t){t=t.replace(/[^A-Z0-9+\/]/gi,"");for(var e=[],r=0,n=0;r>>6-2*n);return e}},t.exports=r}),E={utf8:{stringToBytes:function(t){return E.bin.stringToBytes(unescape(encodeURIComponent(t)))},bytesToString:function(t){return decodeURIComponent(escape(E.bin.bytesToString(t)))}},bin:{stringToBytes:function(t){for(var e=[],r=0;r>>24)|4278255360&(r[u]<<24|r[u]>>>8);r[e>>>5]|=128<>>9<<4)]=e;for(var c=g._ff,s=g._gg,l=g._hh,f=g._ii,u=0;u>>0,o=o+d>>>0,i=i+y>>>0,a=a+p>>>0}return b.endian([n,o,i,a])}var b,v,m,w;b=J,v=K.utf8,m=G,w=K.bin,g._ff=function(t,e,r,n,o,i,a){t=t+(e&r|~e&n)+(o>>>0)+a;return(t<>>32-i)+e},g._gg=function(t,e,r,n,o,i,a){t=t+(e&n|r&~n)+(o>>>0)+a;return(t<>>32-i)+e},g._hh=function(t,e,r,n,o,i,a){t=t+(e^r^n)+(o>>>0)+a;return(t<>>32-i)+e},g._ii=function(t,e,r,n,o,i,a){t=t+(r^(e|~n))+(o>>>0)+a;return(t<>>32-i)+e},g._blocksize=16,g._digestsize=16,t.exports=function(t,e){if(null==t)throw new Error("Illegal argument "+t);t=b.wordsToBytes(g(t,e));return e&&e.asBytes?t:e&&e.asString?w.bytesToString(t):b.bytesToHex(t)}});const tt=/\+/g;function et(t=""){try{return decodeURIComponent(""+t)}catch{return""+t}}function rt(t=""){var e={};for(const o of(t="?"===t[0]?t.slice(1):t).split("&")){var r,n=o.match(/([^=]+)=?(.*)/)||[];n.length<2||"__proto__"!==(r=et(n[1]))&&"constructor"!==r&&(n=et((n[2]||"").replace(tt," ")),void 0!==e[r]?Array.isArray(e[r])?e[r].push(n):e[r]=[e[r],n]:e[r]=n)}return e}const nt=/^\w{2,}:(\/\/)?/,ot=/^\/\/[^/]+/;function it(t,e=!1){return nt.test(t)||e&&ot.test(t)}function at(t){return rt(I(t).search)}function I(t="",e){var r,n,o,i,a,u;return it(t,!0)?([r="",n,o=""]=(t.replace(/\\/g,"/").match(/([^/:]+:)?\/\/([^/@]+@)?(.*)/)||[]).splice(1),[o="",i=""]=(o.match(/([^#/?]*)(.*)?/)||[]).splice(1),{pathname:i,search:a,hash:u}=ut(i.replace(/\/(?=[A-Za-z]:)/,"")),{protocol:r,auth:n?n.slice(0,Math.max(0,n.length-1)):"",host:o,pathname:i,search:a,hash:u}):e?I(e+t):ut(t)}function ut(t=""){var[t="",e="",r=""]=(t.match(/([^#?]*)(\?[^#]*)?(#.*)?/)||[]).splice(1);return{pathname:t,search:e,hash:r}}var ct=/^(?:[a-z\d\-_]{1,62}\.){0,125}(?:[a-z\d](?:\-(?=\-*[a-z\d])|[a-z]|\d){0,62}\.)[a-z\d]{1,63}$/i,st={1:75,2:50,3:35,4:23,5:20},lt=[1,2,3,4,5],ft={domain:null,useHTTPS:!0,includeLibraryParam:!0,urlPrefix:"https://",secureURLToken:null};function L(t){var e=t.url,e=void 0===e?"":e,t=t.useHttps,t=void 0!==t&&t?"https://":"http://";return it(e,!0)?I(e):L({url:t+e})}function ht(t,e){if(!Number.isInteger(t)||!Number.isInteger(e)||t<=0||e<=0||e arr.length) len = arr.length; 129 | for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; 130 | return arr2; 131 | } 132 | function _nonIterableSpread() { 133 | throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); 134 | } 135 | function _nonIterableRest() { 136 | throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); 137 | } 138 | function _toPrimitive(input, hint) { 139 | if (typeof input !== "object" || input === null) return input; 140 | var prim = input[Symbol.toPrimitive]; 141 | if (prim !== undefined) { 142 | var res = prim.call(input, hint || "default"); 143 | if (typeof res !== "object") return res; 144 | throw new TypeError("@@toPrimitive must return a primitive value."); 145 | } 146 | return (hint === "string" ? String : Number)(input); 147 | } 148 | function _toPropertyKey(arg) { 149 | var key = _toPrimitive(arg, "string"); 150 | return typeof key === "symbol" ? key : String(key); 151 | } 152 | 153 | // package version used in the ix-lib parameter 154 | var VERSION = '3.7.1'; 155 | // regex pattern used to determine if a domain is valid 156 | var DOMAIN_REGEX = /^(?:[a-z\d\-_]{1,62}\.){0,125}(?:[a-z\d](?:\-(?=\-*[a-z\d])|[a-z]|\d){0,62}\.)[a-z\d]{1,63}$/i; 157 | // minimum generated srcset width 158 | var MIN_SRCSET_WIDTH = 100; 159 | // maximum generated srcset width 160 | var MAX_SRCSET_WIDTH = 8192; 161 | // default tolerable percent difference between srcset pair widths 162 | var DEFAULT_SRCSET_WIDTH_TOLERANCE = 0.08; 163 | 164 | // default quality parameter values mapped by each dpr srcset entry 165 | var DPR_QUALITIES = { 166 | 1: 75, 167 | 2: 50, 168 | 3: 35, 169 | 4: 23, 170 | 5: 20 171 | }; 172 | var DEFAULT_DPR = [1, 2, 3, 4, 5]; 173 | var DEFAULT_OPTIONS = { 174 | domain: null, 175 | useHTTPS: true, 176 | includeLibraryParam: true, 177 | urlPrefix: 'https://', 178 | secureURLToken: null 179 | }; 180 | 181 | /** 182 | * `extractUrl()` extracts URL components from a source URL string. 183 | * It does this by matching the URL against regular expressions. The irrelevant 184 | * (entire URL) matches are removed and the rest stored as their corresponding 185 | * URL components. 186 | * 187 | * `url` can be a partial, full URL, or full proxy URL. `useHttps` boolean 188 | * defaults to false. 189 | * 190 | * @returns {Object} `{ protocol, auth, host, pathname, search, hash }` 191 | * extracted from the URL. 192 | */ 193 | function extractUrl(_ref) { 194 | var _ref$url = _ref.url, 195 | url = _ref$url === void 0 ? '' : _ref$url, 196 | _ref$useHttps = _ref.useHttps, 197 | useHttps = _ref$useHttps === void 0 ? false : _ref$useHttps; 198 | var defaultProto = useHttps ? 'https://' : 'http://'; 199 | if (!ufo.hasProtocol(url, true)) { 200 | return extractUrl({ 201 | url: defaultProto + url 202 | }); 203 | } 204 | /** 205 | * Regex are hard to parse. Leaving this breakdown here for reference. 206 | * - `protocol`: ([^:/]+:)? - all not `:` or `/` & preceded by `:`, 0-1 times 207 | * - `auth`: ([^/@]+@)? - all not `/` or `@` & preceded by `@`, 0-1 times 208 | * - `domainAndPath`: (.*) /) - all except line breaks 209 | * - `domain`: `([^/]*)` - all before a `/` token 210 | */ 211 | return ufo.parseURL(url); 212 | } 213 | 214 | function validateAndDestructureOptions(options) { 215 | var widthTolerance; 216 | if (options.widthTolerance !== undefined) { 217 | validateWidthTolerance(options.widthTolerance); 218 | widthTolerance = options.widthTolerance; 219 | } else { 220 | widthTolerance = DEFAULT_SRCSET_WIDTH_TOLERANCE; 221 | } 222 | var minWidth = options.minWidth === undefined ? MIN_SRCSET_WIDTH : options.minWidth; 223 | var maxWidth = options.maxWidth === undefined ? MAX_SRCSET_WIDTH : options.maxWidth; 224 | 225 | // Validate the range unless we're using defaults for both 226 | if (minWidth != MIN_SRCSET_WIDTH || maxWidth != MAX_SRCSET_WIDTH) { 227 | validateRange(minWidth, maxWidth); 228 | } 229 | return [widthTolerance, minWidth, maxWidth]; 230 | } 231 | function validateRange(min, max) { 232 | if (!(Number.isInteger(min) && Number.isInteger(max)) || min <= 0 || max <= 0 || min > max) { 233 | throw new Error("The min and max srcset widths can only be passed positive Number values, and min must be less than max. Found min: ".concat(min, " and max: ").concat(max, ".")); 234 | } 235 | } 236 | function validateWidthTolerance(widthTolerance) { 237 | if (typeof widthTolerance != 'number' || widthTolerance < 0.01) { 238 | throw new Error('The srcset widthTolerance must be a number greater than or equal to 0.01'); 239 | } 240 | } 241 | function validateWidths(customWidths) { 242 | if (!Array.isArray(customWidths) || !customWidths.length) { 243 | throw new Error('The widths argument can only be passed a valid non-empty array of integers'); 244 | } else { 245 | var allPositiveIntegers = customWidths.every(function (width) { 246 | return Number.isInteger(width) && width > 0; 247 | }); 248 | if (!allPositiveIntegers) { 249 | throw new Error('A custom widths argument can only contain positive integer values'); 250 | } 251 | } 252 | } 253 | function validateVariableQuality(disableVariableQuality) { 254 | if (typeof disableVariableQuality != 'boolean') { 255 | throw new Error('The disableVariableQuality argument can only be passed a Boolean value'); 256 | } 257 | } 258 | function validateDevicePixelRatios(devicePixelRatios) { 259 | if (!Array.isArray(devicePixelRatios) || !devicePixelRatios.length) { 260 | throw new Error('The devicePixelRatios argument can only be passed a valid non-empty array of integers'); 261 | } else { 262 | var allValidDPR = devicePixelRatios.every(function (dpr) { 263 | return typeof dpr === 'number' && dpr >= 1 && dpr <= 5; 264 | }); 265 | if (!allValidDPR) { 266 | throw new Error('The devicePixelRatios argument can only contain positive integer values between 1 and 5'); 267 | } 268 | } 269 | } 270 | function validateVariableQualities(variableQualities) { 271 | if (_typeof(variableQualities) !== 'object') { 272 | throw new Error('The variableQualities argument can only be an object'); 273 | } 274 | } 275 | 276 | var ImgixClient = /*#__PURE__*/function () { 277 | function ImgixClient() { 278 | var opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 279 | _classCallCheck(this, ImgixClient); 280 | this.settings = _objectSpread2(_objectSpread2({}, DEFAULT_OPTIONS), opts); 281 | // a cache to store memoized srcset width-pairs 282 | this.targetWidthsCache = {}; 283 | if (typeof this.settings.domain != 'string') { 284 | throw new Error('ImgixClient must be passed a valid string domain'); 285 | } 286 | if (DOMAIN_REGEX.exec(this.settings.domain) == null) { 287 | throw new Error('Domain must be passed in as fully-qualified ' + 'domain name and should not include a protocol or any path ' + 'element, i.e. "example.imgix.net".'); 288 | } 289 | if (this.settings.includeLibraryParam) { 290 | this.settings.libraryParam = 'js-' + ImgixClient.version(); 291 | } 292 | this.settings.urlPrefix = this.settings.useHTTPS ? 'https://' : 'http://'; 293 | } 294 | _createClass(ImgixClient, [{ 295 | key: "buildURL", 296 | value: function buildURL() { 297 | var rawPath = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 298 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 299 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 300 | var path = this._sanitizePath(rawPath, options); 301 | var finalParams = this._buildParams(params, options); 302 | if (!!this.settings.secureURLToken) { 303 | finalParams = this._signParams(path, finalParams); 304 | } 305 | return this.settings.urlPrefix + this.settings.domain + path + finalParams; 306 | } 307 | 308 | /** 309 | *`_buildURL` static method allows full URLs to be formatted for use with 310 | * imgix. 311 | * 312 | * - If the source URL has included parameters, they are merged with 313 | * the `params` passed in as an argument. 314 | * - URL must match `{host}/{pathname}?{query}` otherwise an error is thrown. 315 | * 316 | * @param {String} url - full source URL path string, required 317 | * @param {Object} params - imgix params object, optional 318 | * @param {Object} options - imgix client options, optional 319 | * 320 | * @returns URL string formatted to imgix specifications. 321 | * 322 | * @example 323 | * const client = ImgixClient 324 | * const params = { w: 100 } 325 | * const opts = { useHttps: true } 326 | * const src = "sdk-test.imgix.net/amsterdam.jpg?h=100" 327 | * const url = client._buildURL(src, params, opts) 328 | * console.log(url) 329 | * // => "https://sdk-test.imgix.net/amsterdam.jpg?h=100&w=100" 330 | */ 331 | }, { 332 | key: "_buildParams", 333 | value: function _buildParams() { 334 | var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 335 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 336 | // If a custom encoder is present, use it 337 | // Otherwise just use the encodeURIComponent 338 | var useCustomEncoder = !!options.encoder; 339 | var customEncoder = options.encoder; 340 | var queryParams = [].concat(_toConsumableArray(this.settings.libraryParam ? ["ixlib=".concat(this.settings.libraryParam)] : []), _toConsumableArray(Object.entries(params).reduce(function (prev, _ref) { 341 | var _ref2 = _slicedToArray(_ref, 2), 342 | key = _ref2[0], 343 | value = _ref2[1]; 344 | if (value == null) { 345 | return prev; 346 | } 347 | var encodedKey = useCustomEncoder ? customEncoder(key, value) : encodeURIComponent(key); 348 | var encodedValue = key.substr(-2) === '64' ? useCustomEncoder ? customEncoder(value, key) : jsBase64.Base64.encodeURI(value) : useCustomEncoder ? customEncoder(value, key) : encodeURIComponent(value); 349 | prev.push("".concat(encodedKey, "=").concat(encodedValue)); 350 | return prev; 351 | }, []))); 352 | return "".concat(queryParams.length > 0 ? '?' : '').concat(queryParams.join('&')); 353 | } 354 | }, { 355 | key: "_signParams", 356 | value: function _signParams(path, queryParams) { 357 | var signatureBase = this.settings.secureURLToken + path + queryParams; 358 | var signature = md5__default["default"](signatureBase); 359 | return queryParams.length > 0 ? queryParams + '&s=' + signature : '?s=' + signature; 360 | } 361 | 362 | /** 363 | * "Sanitize" the path of the image URL. 364 | * Ensures that the path has a leading slash, and that the path is correctly 365 | * encoded. If it's a proxy path (begins with http/https), then encode the 366 | * whole path as a URI component, otherwise only encode specific characters. 367 | * @param {string} path The URL path of the image 368 | * @param {Object} options Sanitization options 369 | * @param {boolean} options.encode Whether to encode the path, default true 370 | * @returns {string} The sanitized path 371 | */ 372 | }, { 373 | key: "_sanitizePath", 374 | value: function _sanitizePath(path) { 375 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 376 | // Strip leading slash first (we'll re-add after encoding) 377 | var _path = path.replace(/^\//, ''); 378 | if (options.disablePathEncoding) { 379 | return '/' + _path; 380 | } 381 | if (options.encoder) { 382 | _path = options.encoder(_path); 383 | } else if (/^https?:\/\//.test(_path)) { 384 | // Use de/encodeURIComponent to ensure *all* characters are handled, 385 | // since it's being used as a path 386 | _path = encodeURIComponent(_path); 387 | } else { 388 | // Use de/encodeURI if we think the path is just a path, 389 | // so it leaves legal characters like '/' and '@' alone 390 | _path = encodeURI(_path).replace(/[#?:+]/g, encodeURIComponent); 391 | } 392 | return '/' + _path; 393 | } 394 | }, { 395 | key: "buildSrcSet", 396 | value: function buildSrcSet(path) { 397 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 398 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 399 | var w = params.w, 400 | h = params.h; 401 | if (w || h) { 402 | return this._buildDPRSrcSet(path, params, options); 403 | } else { 404 | return this._buildSrcSetPairs(path, params, options); 405 | } 406 | } 407 | 408 | /** 409 | * _buildSrcSet static method allows full URLs to be used when generating 410 | * imgix formatted `srcset` string values. 411 | * 412 | * - If the source URL has included parameters, they are merged with 413 | * the `params` passed in as an argument. 414 | * - URL must match `{host}/{pathname}?{query}` otherwise an error is thrown. 415 | * 416 | * @param {String} url - full source URL path string, required 417 | * @param {Object} params - imgix params object, optional 418 | * @param {Object} srcsetModifiers - srcset modifiers, optional 419 | * @param {Object} clientOptions - imgix client options, optional 420 | * @returns imgix `srcset` for full URLs. 421 | */ 422 | }, { 423 | key: "_buildSrcSetPairs", 424 | value: function _buildSrcSetPairs(path) { 425 | var _this = this; 426 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 427 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 428 | var _validateAndDestructu = validateAndDestructureOptions(options), 429 | _validateAndDestructu2 = _slicedToArray(_validateAndDestructu, 3), 430 | widthTolerance = _validateAndDestructu2[0], 431 | minWidth = _validateAndDestructu2[1], 432 | maxWidth = _validateAndDestructu2[2]; 433 | var targetWidthValues; 434 | if (options.widths) { 435 | validateWidths(options.widths); 436 | targetWidthValues = _toConsumableArray(options.widths); 437 | } else { 438 | targetWidthValues = ImgixClient.targetWidths(minWidth, maxWidth, widthTolerance, this.targetWidthsCache); 439 | } 440 | var srcset = targetWidthValues.map(function (w) { 441 | return "".concat(_this.buildURL(path, _objectSpread2(_objectSpread2({}, params), {}, { 442 | w: w 443 | }), options), " ").concat(w, "w"); 444 | }); 445 | return srcset.join(',\n'); 446 | } 447 | }, { 448 | key: "_buildDPRSrcSet", 449 | value: function _buildDPRSrcSet(path) { 450 | var _this2 = this; 451 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 452 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 453 | if (options.devicePixelRatios) { 454 | validateDevicePixelRatios(options.devicePixelRatios); 455 | } 456 | var targetRatios = options.devicePixelRatios || DEFAULT_DPR; 457 | var disableVariableQuality = options.disableVariableQuality || false; 458 | if (!disableVariableQuality) { 459 | validateVariableQuality(disableVariableQuality); 460 | } 461 | if (options.variableQualities) { 462 | validateVariableQualities(options.variableQualities); 463 | } 464 | var qualities = _objectSpread2(_objectSpread2({}, DPR_QUALITIES), options.variableQualities); 465 | var withQuality = function withQuality(path, params, dpr) { 466 | return "".concat(_this2.buildURL(path, _objectSpread2(_objectSpread2({}, params), {}, { 467 | dpr: dpr, 468 | q: params.q || qualities[dpr] || qualities[Math.floor(dpr)] 469 | }), options), " ").concat(dpr, "x"); 470 | }; 471 | var srcset = disableVariableQuality ? targetRatios.map(function (dpr) { 472 | return "".concat(_this2.buildURL(path, _objectSpread2(_objectSpread2({}, params), {}, { 473 | dpr: dpr 474 | }), options), " ").concat(dpr, "x"); 475 | }) : targetRatios.map(function (dpr) { 476 | return withQuality(path, params, dpr); 477 | }); 478 | return srcset.join(',\n'); 479 | } 480 | }], [{ 481 | key: "version", 482 | value: function version() { 483 | return VERSION; 484 | } 485 | }, { 486 | key: "_buildURL", 487 | value: function _buildURL(url) { 488 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 489 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 490 | if (url == null) { 491 | return ''; 492 | } 493 | var _extractUrl = extractUrl({ 494 | url: url, 495 | useHTTPS: options.useHTTPS 496 | }), 497 | host = _extractUrl.host, 498 | pathname = _extractUrl.pathname, 499 | search = _extractUrl.search; 500 | // merge source URL parameters with options parameters 501 | var combinedParams = _objectSpread2(_objectSpread2({}, ufo.getQuery(search)), params); 502 | 503 | // throw error if no host or no pathname present 504 | if (!host.length || !pathname.length) { 505 | throw new Error('_buildURL: URL must match {host}/{pathname}?{query}'); 506 | } 507 | var client = new ImgixClient(_objectSpread2({ 508 | domain: host 509 | }, options)); 510 | return client.buildURL(pathname, combinedParams); 511 | } 512 | }, { 513 | key: "_buildSrcSet", 514 | value: function _buildSrcSet(url) { 515 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 516 | var srcsetModifiers = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 517 | var clientOptions = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; 518 | if (url == null) { 519 | return ''; 520 | } 521 | var _extractUrl2 = extractUrl({ 522 | url: url, 523 | useHTTPS: clientOptions.useHTTPS 524 | }), 525 | host = _extractUrl2.host, 526 | pathname = _extractUrl2.pathname, 527 | search = _extractUrl2.search; 528 | // merge source URL parameters with options parameters 529 | var combinedParams = _objectSpread2(_objectSpread2({}, ufo.getQuery(search)), params); 530 | 531 | // throw error if no host or no pathname present 532 | if (!host.length || !pathname.length) { 533 | throw new Error('_buildOneStepURL: URL must match {host}/{pathname}?{query}'); 534 | } 535 | var client = new ImgixClient(_objectSpread2({ 536 | domain: host 537 | }, clientOptions)); 538 | return client.buildSrcSet(pathname, combinedParams, srcsetModifiers); 539 | } 540 | 541 | // returns an array of width values used during srcset generation 542 | }, { 543 | key: "targetWidths", 544 | value: function targetWidths() { 545 | var minWidth = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 100; 546 | var maxWidth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 8192; 547 | var widthTolerance = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0.08; 548 | var cache = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; 549 | var minW = Math.floor(minWidth); 550 | var maxW = Math.floor(maxWidth); 551 | validateRange(minWidth, maxWidth); 552 | validateWidthTolerance(widthTolerance); 553 | var cacheKey = widthTolerance + '/' + minW + '/' + maxW; 554 | 555 | // First, check the cache. 556 | if (cacheKey in cache) { 557 | return cache[cacheKey]; 558 | } 559 | if (minW === maxW) { 560 | return [minW]; 561 | } 562 | var resolutions = []; 563 | var currentWidth = minW; 564 | while (currentWidth < maxW) { 565 | // While the currentWidth is less than the maxW, push the rounded 566 | // width onto the list of resolutions. 567 | resolutions.push(Math.round(currentWidth)); 568 | currentWidth *= 1 + widthTolerance * 2; 569 | } 570 | 571 | // At this point, the last width in resolutions is less than the 572 | // currentWidth that caused the loop to terminate. This terminating 573 | // currentWidth is greater than or equal to the maxW. We want to 574 | // to stop at maxW, so we make sure our maxW is larger than the last 575 | // width in resolutions before pushing it (if it's equal we're done). 576 | if (resolutions[resolutions.length - 1] < maxW) { 577 | resolutions.push(maxW); 578 | } 579 | cache[cacheKey] = resolutions; 580 | return resolutions; 581 | } 582 | }]); 583 | return ImgixClient; 584 | }(); 585 | 586 | module.exports = ImgixClient; 587 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | declare class ImgixClient { 2 | domain: string; 3 | useHTTPS: boolean; 4 | includeLibraryParam: boolean; 5 | secureURLToken: string; 6 | 7 | constructor(opts: { 8 | domain: string; 9 | secureURLToken?: string; 10 | useHTTPS?: boolean; 11 | includeLibraryParam?: boolean; 12 | }); 13 | 14 | buildURL( 15 | path: string, 16 | params?: {}, 17 | options?: { disablePathEncoding?: boolean }, 18 | ): string; 19 | _sanitizePath(path: string, options?: _sanitizePathOptions): string; 20 | _buildParams(params: {}, options?: _buildParamsOptions): string; 21 | _signParams(path: string, queryParams?: {}): string; 22 | buildSrcSet(path: string, params?: {}, options?: SrcSetOptions): string; 23 | _buildSrcSetPairs(path: string, params?: {}, options?: SrcSetOptions): string; 24 | _buildDPRSrcSet(path: string, params?: {}, options?: SrcSetOptions): string; 25 | static targetWidths( 26 | minWidth?: number, 27 | maxWidth?: number, 28 | widthTolerance?: number, 29 | cache?: {}, 30 | ): number[]; 31 | static _buildURL(path: string, params?: {}, options?: {}): string; 32 | static _buildSrcSet( 33 | path: string, 34 | params?: {}, 35 | srcSetOptions?: {}, 36 | clientOptions?: {}, 37 | ): string; 38 | } 39 | 40 | export type DevicePixelRatio = 1 | 2 | 3 | 4 | 5 | number; 41 | 42 | export type VariableQualities = { [key in DevicePixelRatio]?: number }; 43 | 44 | export interface SrcSetOptions { 45 | widths?: number[]; 46 | widthTolerance?: number; 47 | minWidth?: number; 48 | maxWidth?: number; 49 | disableVariableQuality?: boolean; 50 | devicePixelRatios?: DevicePixelRatio[]; 51 | variableQualities?: VariableQualities; 52 | disablePathEncoding?: boolean; 53 | } 54 | 55 | export interface _sanitizePathOptions { 56 | disablePathEncoding?: boolean, 57 | encoder?: (path: string) => string 58 | } 59 | 60 | export interface _buildParamsOptions { 61 | encoder?: (value: string, key?: string) => string 62 | } 63 | 64 | export default ImgixClient; 65 | -------------------------------------------------------------------------------- /dist/index.esm.js: -------------------------------------------------------------------------------- 1 | import { Base64 } from 'js-base64'; 2 | import md5 from 'md5'; 3 | import { hasProtocol, parseURL, getQuery } from 'ufo'; 4 | 5 | function _iterableToArrayLimit(arr, i) { 6 | var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"]; 7 | if (null != _i) { 8 | var _s, 9 | _e, 10 | _x, 11 | _r, 12 | _arr = [], 13 | _n = !0, 14 | _d = !1; 15 | try { 16 | if (_x = (_i = _i.call(arr)).next, 0 === i) { 17 | if (Object(_i) !== _i) return; 18 | _n = !1; 19 | } else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0); 20 | } catch (err) { 21 | _d = !0, _e = err; 22 | } finally { 23 | try { 24 | if (!_n && null != _i.return && (_r = _i.return(), Object(_r) !== _r)) return; 25 | } finally { 26 | if (_d) throw _e; 27 | } 28 | } 29 | return _arr; 30 | } 31 | } 32 | function ownKeys(object, enumerableOnly) { 33 | var keys = Object.keys(object); 34 | if (Object.getOwnPropertySymbols) { 35 | var symbols = Object.getOwnPropertySymbols(object); 36 | enumerableOnly && (symbols = symbols.filter(function (sym) { 37 | return Object.getOwnPropertyDescriptor(object, sym).enumerable; 38 | })), keys.push.apply(keys, symbols); 39 | } 40 | return keys; 41 | } 42 | function _objectSpread2(target) { 43 | for (var i = 1; i < arguments.length; i++) { 44 | var source = null != arguments[i] ? arguments[i] : {}; 45 | i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { 46 | _defineProperty(target, key, source[key]); 47 | }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { 48 | Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); 49 | }); 50 | } 51 | return target; 52 | } 53 | function _typeof(obj) { 54 | "@babel/helpers - typeof"; 55 | 56 | return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { 57 | return typeof obj; 58 | } : function (obj) { 59 | return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 60 | }, _typeof(obj); 61 | } 62 | function _classCallCheck(instance, Constructor) { 63 | if (!(instance instanceof Constructor)) { 64 | throw new TypeError("Cannot call a class as a function"); 65 | } 66 | } 67 | function _defineProperties(target, props) { 68 | for (var i = 0; i < props.length; i++) { 69 | var descriptor = props[i]; 70 | descriptor.enumerable = descriptor.enumerable || false; 71 | descriptor.configurable = true; 72 | if ("value" in descriptor) descriptor.writable = true; 73 | Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); 74 | } 75 | } 76 | function _createClass(Constructor, protoProps, staticProps) { 77 | if (protoProps) _defineProperties(Constructor.prototype, protoProps); 78 | if (staticProps) _defineProperties(Constructor, staticProps); 79 | Object.defineProperty(Constructor, "prototype", { 80 | writable: false 81 | }); 82 | return Constructor; 83 | } 84 | function _defineProperty(obj, key, value) { 85 | key = _toPropertyKey(key); 86 | if (key in obj) { 87 | Object.defineProperty(obj, key, { 88 | value: value, 89 | enumerable: true, 90 | configurable: true, 91 | writable: true 92 | }); 93 | } else { 94 | obj[key] = value; 95 | } 96 | return obj; 97 | } 98 | function _slicedToArray(arr, i) { 99 | return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); 100 | } 101 | function _toConsumableArray(arr) { 102 | return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); 103 | } 104 | function _arrayWithoutHoles(arr) { 105 | if (Array.isArray(arr)) return _arrayLikeToArray(arr); 106 | } 107 | function _arrayWithHoles(arr) { 108 | if (Array.isArray(arr)) return arr; 109 | } 110 | function _iterableToArray(iter) { 111 | if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); 112 | } 113 | function _unsupportedIterableToArray(o, minLen) { 114 | if (!o) return; 115 | if (typeof o === "string") return _arrayLikeToArray(o, minLen); 116 | var n = Object.prototype.toString.call(o).slice(8, -1); 117 | if (n === "Object" && o.constructor) n = o.constructor.name; 118 | if (n === "Map" || n === "Set") return Array.from(o); 119 | if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); 120 | } 121 | function _arrayLikeToArray(arr, len) { 122 | if (len == null || len > arr.length) len = arr.length; 123 | for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; 124 | return arr2; 125 | } 126 | function _nonIterableSpread() { 127 | throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); 128 | } 129 | function _nonIterableRest() { 130 | throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); 131 | } 132 | function _toPrimitive(input, hint) { 133 | if (typeof input !== "object" || input === null) return input; 134 | var prim = input[Symbol.toPrimitive]; 135 | if (prim !== undefined) { 136 | var res = prim.call(input, hint || "default"); 137 | if (typeof res !== "object") return res; 138 | throw new TypeError("@@toPrimitive must return a primitive value."); 139 | } 140 | return (hint === "string" ? String : Number)(input); 141 | } 142 | function _toPropertyKey(arg) { 143 | var key = _toPrimitive(arg, "string"); 144 | return typeof key === "symbol" ? key : String(key); 145 | } 146 | 147 | // package version used in the ix-lib parameter 148 | var VERSION = '3.7.1'; 149 | // regex pattern used to determine if a domain is valid 150 | var DOMAIN_REGEX = /^(?:[a-z\d\-_]{1,62}\.){0,125}(?:[a-z\d](?:\-(?=\-*[a-z\d])|[a-z]|\d){0,62}\.)[a-z\d]{1,63}$/i; 151 | // minimum generated srcset width 152 | var MIN_SRCSET_WIDTH = 100; 153 | // maximum generated srcset width 154 | var MAX_SRCSET_WIDTH = 8192; 155 | // default tolerable percent difference between srcset pair widths 156 | var DEFAULT_SRCSET_WIDTH_TOLERANCE = 0.08; 157 | 158 | // default quality parameter values mapped by each dpr srcset entry 159 | var DPR_QUALITIES = { 160 | 1: 75, 161 | 2: 50, 162 | 3: 35, 163 | 4: 23, 164 | 5: 20 165 | }; 166 | var DEFAULT_DPR = [1, 2, 3, 4, 5]; 167 | var DEFAULT_OPTIONS = { 168 | domain: null, 169 | useHTTPS: true, 170 | includeLibraryParam: true, 171 | urlPrefix: 'https://', 172 | secureURLToken: null 173 | }; 174 | 175 | /** 176 | * `extractUrl()` extracts URL components from a source URL string. 177 | * It does this by matching the URL against regular expressions. The irrelevant 178 | * (entire URL) matches are removed and the rest stored as their corresponding 179 | * URL components. 180 | * 181 | * `url` can be a partial, full URL, or full proxy URL. `useHttps` boolean 182 | * defaults to false. 183 | * 184 | * @returns {Object} `{ protocol, auth, host, pathname, search, hash }` 185 | * extracted from the URL. 186 | */ 187 | function extractUrl(_ref) { 188 | var _ref$url = _ref.url, 189 | url = _ref$url === void 0 ? '' : _ref$url, 190 | _ref$useHttps = _ref.useHttps, 191 | useHttps = _ref$useHttps === void 0 ? false : _ref$useHttps; 192 | var defaultProto = useHttps ? 'https://' : 'http://'; 193 | if (!hasProtocol(url, true)) { 194 | return extractUrl({ 195 | url: defaultProto + url 196 | }); 197 | } 198 | /** 199 | * Regex are hard to parse. Leaving this breakdown here for reference. 200 | * - `protocol`: ([^:/]+:)? - all not `:` or `/` & preceded by `:`, 0-1 times 201 | * - `auth`: ([^/@]+@)? - all not `/` or `@` & preceded by `@`, 0-1 times 202 | * - `domainAndPath`: (.*) /) - all except line breaks 203 | * - `domain`: `([^/]*)` - all before a `/` token 204 | */ 205 | return parseURL(url); 206 | } 207 | 208 | function validateAndDestructureOptions(options) { 209 | var widthTolerance; 210 | if (options.widthTolerance !== undefined) { 211 | validateWidthTolerance(options.widthTolerance); 212 | widthTolerance = options.widthTolerance; 213 | } else { 214 | widthTolerance = DEFAULT_SRCSET_WIDTH_TOLERANCE; 215 | } 216 | var minWidth = options.minWidth === undefined ? MIN_SRCSET_WIDTH : options.minWidth; 217 | var maxWidth = options.maxWidth === undefined ? MAX_SRCSET_WIDTH : options.maxWidth; 218 | 219 | // Validate the range unless we're using defaults for both 220 | if (minWidth != MIN_SRCSET_WIDTH || maxWidth != MAX_SRCSET_WIDTH) { 221 | validateRange(minWidth, maxWidth); 222 | } 223 | return [widthTolerance, minWidth, maxWidth]; 224 | } 225 | function validateRange(min, max) { 226 | if (!(Number.isInteger(min) && Number.isInteger(max)) || min <= 0 || max <= 0 || min > max) { 227 | throw new Error("The min and max srcset widths can only be passed positive Number values, and min must be less than max. Found min: ".concat(min, " and max: ").concat(max, ".")); 228 | } 229 | } 230 | function validateWidthTolerance(widthTolerance) { 231 | if (typeof widthTolerance != 'number' || widthTolerance < 0.01) { 232 | throw new Error('The srcset widthTolerance must be a number greater than or equal to 0.01'); 233 | } 234 | } 235 | function validateWidths(customWidths) { 236 | if (!Array.isArray(customWidths) || !customWidths.length) { 237 | throw new Error('The widths argument can only be passed a valid non-empty array of integers'); 238 | } else { 239 | var allPositiveIntegers = customWidths.every(function (width) { 240 | return Number.isInteger(width) && width > 0; 241 | }); 242 | if (!allPositiveIntegers) { 243 | throw new Error('A custom widths argument can only contain positive integer values'); 244 | } 245 | } 246 | } 247 | function validateVariableQuality(disableVariableQuality) { 248 | if (typeof disableVariableQuality != 'boolean') { 249 | throw new Error('The disableVariableQuality argument can only be passed a Boolean value'); 250 | } 251 | } 252 | function validateDevicePixelRatios(devicePixelRatios) { 253 | if (!Array.isArray(devicePixelRatios) || !devicePixelRatios.length) { 254 | throw new Error('The devicePixelRatios argument can only be passed a valid non-empty array of integers'); 255 | } else { 256 | var allValidDPR = devicePixelRatios.every(function (dpr) { 257 | return typeof dpr === 'number' && dpr >= 1 && dpr <= 5; 258 | }); 259 | if (!allValidDPR) { 260 | throw new Error('The devicePixelRatios argument can only contain positive integer values between 1 and 5'); 261 | } 262 | } 263 | } 264 | function validateVariableQualities(variableQualities) { 265 | if (_typeof(variableQualities) !== 'object') { 266 | throw new Error('The variableQualities argument can only be an object'); 267 | } 268 | } 269 | 270 | var ImgixClient = /*#__PURE__*/function () { 271 | function ImgixClient() { 272 | var opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 273 | _classCallCheck(this, ImgixClient); 274 | this.settings = _objectSpread2(_objectSpread2({}, DEFAULT_OPTIONS), opts); 275 | // a cache to store memoized srcset width-pairs 276 | this.targetWidthsCache = {}; 277 | if (typeof this.settings.domain != 'string') { 278 | throw new Error('ImgixClient must be passed a valid string domain'); 279 | } 280 | if (DOMAIN_REGEX.exec(this.settings.domain) == null) { 281 | throw new Error('Domain must be passed in as fully-qualified ' + 'domain name and should not include a protocol or any path ' + 'element, i.e. "example.imgix.net".'); 282 | } 283 | if (this.settings.includeLibraryParam) { 284 | this.settings.libraryParam = 'js-' + ImgixClient.version(); 285 | } 286 | this.settings.urlPrefix = this.settings.useHTTPS ? 'https://' : 'http://'; 287 | } 288 | _createClass(ImgixClient, [{ 289 | key: "buildURL", 290 | value: function buildURL() { 291 | var rawPath = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 292 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 293 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 294 | var path = this._sanitizePath(rawPath, options); 295 | var finalParams = this._buildParams(params, options); 296 | if (!!this.settings.secureURLToken) { 297 | finalParams = this._signParams(path, finalParams); 298 | } 299 | return this.settings.urlPrefix + this.settings.domain + path + finalParams; 300 | } 301 | 302 | /** 303 | *`_buildURL` static method allows full URLs to be formatted for use with 304 | * imgix. 305 | * 306 | * - If the source URL has included parameters, they are merged with 307 | * the `params` passed in as an argument. 308 | * - URL must match `{host}/{pathname}?{query}` otherwise an error is thrown. 309 | * 310 | * @param {String} url - full source URL path string, required 311 | * @param {Object} params - imgix params object, optional 312 | * @param {Object} options - imgix client options, optional 313 | * 314 | * @returns URL string formatted to imgix specifications. 315 | * 316 | * @example 317 | * const client = ImgixClient 318 | * const params = { w: 100 } 319 | * const opts = { useHttps: true } 320 | * const src = "sdk-test.imgix.net/amsterdam.jpg?h=100" 321 | * const url = client._buildURL(src, params, opts) 322 | * console.log(url) 323 | * // => "https://sdk-test.imgix.net/amsterdam.jpg?h=100&w=100" 324 | */ 325 | }, { 326 | key: "_buildParams", 327 | value: function _buildParams() { 328 | var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 329 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 330 | // If a custom encoder is present, use it 331 | // Otherwise just use the encodeURIComponent 332 | var useCustomEncoder = !!options.encoder; 333 | var customEncoder = options.encoder; 334 | var queryParams = [].concat(_toConsumableArray(this.settings.libraryParam ? ["ixlib=".concat(this.settings.libraryParam)] : []), _toConsumableArray(Object.entries(params).reduce(function (prev, _ref) { 335 | var _ref2 = _slicedToArray(_ref, 2), 336 | key = _ref2[0], 337 | value = _ref2[1]; 338 | if (value == null) { 339 | return prev; 340 | } 341 | var encodedKey = useCustomEncoder ? customEncoder(key, value) : encodeURIComponent(key); 342 | var encodedValue = key.substr(-2) === '64' ? useCustomEncoder ? customEncoder(value, key) : Base64.encodeURI(value) : useCustomEncoder ? customEncoder(value, key) : encodeURIComponent(value); 343 | prev.push("".concat(encodedKey, "=").concat(encodedValue)); 344 | return prev; 345 | }, []))); 346 | return "".concat(queryParams.length > 0 ? '?' : '').concat(queryParams.join('&')); 347 | } 348 | }, { 349 | key: "_signParams", 350 | value: function _signParams(path, queryParams) { 351 | var signatureBase = this.settings.secureURLToken + path + queryParams; 352 | var signature = md5(signatureBase); 353 | return queryParams.length > 0 ? queryParams + '&s=' + signature : '?s=' + signature; 354 | } 355 | 356 | /** 357 | * "Sanitize" the path of the image URL. 358 | * Ensures that the path has a leading slash, and that the path is correctly 359 | * encoded. If it's a proxy path (begins with http/https), then encode the 360 | * whole path as a URI component, otherwise only encode specific characters. 361 | * @param {string} path The URL path of the image 362 | * @param {Object} options Sanitization options 363 | * @param {boolean} options.encode Whether to encode the path, default true 364 | * @returns {string} The sanitized path 365 | */ 366 | }, { 367 | key: "_sanitizePath", 368 | value: function _sanitizePath(path) { 369 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 370 | // Strip leading slash first (we'll re-add after encoding) 371 | var _path = path.replace(/^\//, ''); 372 | if (options.disablePathEncoding) { 373 | return '/' + _path; 374 | } 375 | if (options.encoder) { 376 | _path = options.encoder(_path); 377 | } else if (/^https?:\/\//.test(_path)) { 378 | // Use de/encodeURIComponent to ensure *all* characters are handled, 379 | // since it's being used as a path 380 | _path = encodeURIComponent(_path); 381 | } else { 382 | // Use de/encodeURI if we think the path is just a path, 383 | // so it leaves legal characters like '/' and '@' alone 384 | _path = encodeURI(_path).replace(/[#?:+]/g, encodeURIComponent); 385 | } 386 | return '/' + _path; 387 | } 388 | }, { 389 | key: "buildSrcSet", 390 | value: function buildSrcSet(path) { 391 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 392 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 393 | var w = params.w, 394 | h = params.h; 395 | if (w || h) { 396 | return this._buildDPRSrcSet(path, params, options); 397 | } else { 398 | return this._buildSrcSetPairs(path, params, options); 399 | } 400 | } 401 | 402 | /** 403 | * _buildSrcSet static method allows full URLs to be used when generating 404 | * imgix formatted `srcset` string values. 405 | * 406 | * - If the source URL has included parameters, they are merged with 407 | * the `params` passed in as an argument. 408 | * - URL must match `{host}/{pathname}?{query}` otherwise an error is thrown. 409 | * 410 | * @param {String} url - full source URL path string, required 411 | * @param {Object} params - imgix params object, optional 412 | * @param {Object} srcsetModifiers - srcset modifiers, optional 413 | * @param {Object} clientOptions - imgix client options, optional 414 | * @returns imgix `srcset` for full URLs. 415 | */ 416 | }, { 417 | key: "_buildSrcSetPairs", 418 | value: function _buildSrcSetPairs(path) { 419 | var _this = this; 420 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 421 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 422 | var _validateAndDestructu = validateAndDestructureOptions(options), 423 | _validateAndDestructu2 = _slicedToArray(_validateAndDestructu, 3), 424 | widthTolerance = _validateAndDestructu2[0], 425 | minWidth = _validateAndDestructu2[1], 426 | maxWidth = _validateAndDestructu2[2]; 427 | var targetWidthValues; 428 | if (options.widths) { 429 | validateWidths(options.widths); 430 | targetWidthValues = _toConsumableArray(options.widths); 431 | } else { 432 | targetWidthValues = ImgixClient.targetWidths(minWidth, maxWidth, widthTolerance, this.targetWidthsCache); 433 | } 434 | var srcset = targetWidthValues.map(function (w) { 435 | return "".concat(_this.buildURL(path, _objectSpread2(_objectSpread2({}, params), {}, { 436 | w: w 437 | }), options), " ").concat(w, "w"); 438 | }); 439 | return srcset.join(',\n'); 440 | } 441 | }, { 442 | key: "_buildDPRSrcSet", 443 | value: function _buildDPRSrcSet(path) { 444 | var _this2 = this; 445 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 446 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 447 | if (options.devicePixelRatios) { 448 | validateDevicePixelRatios(options.devicePixelRatios); 449 | } 450 | var targetRatios = options.devicePixelRatios || DEFAULT_DPR; 451 | var disableVariableQuality = options.disableVariableQuality || false; 452 | if (!disableVariableQuality) { 453 | validateVariableQuality(disableVariableQuality); 454 | } 455 | if (options.variableQualities) { 456 | validateVariableQualities(options.variableQualities); 457 | } 458 | var qualities = _objectSpread2(_objectSpread2({}, DPR_QUALITIES), options.variableQualities); 459 | var withQuality = function withQuality(path, params, dpr) { 460 | return "".concat(_this2.buildURL(path, _objectSpread2(_objectSpread2({}, params), {}, { 461 | dpr: dpr, 462 | q: params.q || qualities[dpr] || qualities[Math.floor(dpr)] 463 | }), options), " ").concat(dpr, "x"); 464 | }; 465 | var srcset = disableVariableQuality ? targetRatios.map(function (dpr) { 466 | return "".concat(_this2.buildURL(path, _objectSpread2(_objectSpread2({}, params), {}, { 467 | dpr: dpr 468 | }), options), " ").concat(dpr, "x"); 469 | }) : targetRatios.map(function (dpr) { 470 | return withQuality(path, params, dpr); 471 | }); 472 | return srcset.join(',\n'); 473 | } 474 | }], [{ 475 | key: "version", 476 | value: function version() { 477 | return VERSION; 478 | } 479 | }, { 480 | key: "_buildURL", 481 | value: function _buildURL(url) { 482 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 483 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 484 | if (url == null) { 485 | return ''; 486 | } 487 | var _extractUrl = extractUrl({ 488 | url: url, 489 | useHTTPS: options.useHTTPS 490 | }), 491 | host = _extractUrl.host, 492 | pathname = _extractUrl.pathname, 493 | search = _extractUrl.search; 494 | // merge source URL parameters with options parameters 495 | var combinedParams = _objectSpread2(_objectSpread2({}, getQuery(search)), params); 496 | 497 | // throw error if no host or no pathname present 498 | if (!host.length || !pathname.length) { 499 | throw new Error('_buildURL: URL must match {host}/{pathname}?{query}'); 500 | } 501 | var client = new ImgixClient(_objectSpread2({ 502 | domain: host 503 | }, options)); 504 | return client.buildURL(pathname, combinedParams); 505 | } 506 | }, { 507 | key: "_buildSrcSet", 508 | value: function _buildSrcSet(url) { 509 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 510 | var srcsetModifiers = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 511 | var clientOptions = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; 512 | if (url == null) { 513 | return ''; 514 | } 515 | var _extractUrl2 = extractUrl({ 516 | url: url, 517 | useHTTPS: clientOptions.useHTTPS 518 | }), 519 | host = _extractUrl2.host, 520 | pathname = _extractUrl2.pathname, 521 | search = _extractUrl2.search; 522 | // merge source URL parameters with options parameters 523 | var combinedParams = _objectSpread2(_objectSpread2({}, getQuery(search)), params); 524 | 525 | // throw error if no host or no pathname present 526 | if (!host.length || !pathname.length) { 527 | throw new Error('_buildOneStepURL: URL must match {host}/{pathname}?{query}'); 528 | } 529 | var client = new ImgixClient(_objectSpread2({ 530 | domain: host 531 | }, clientOptions)); 532 | return client.buildSrcSet(pathname, combinedParams, srcsetModifiers); 533 | } 534 | 535 | // returns an array of width values used during srcset generation 536 | }, { 537 | key: "targetWidths", 538 | value: function targetWidths() { 539 | var minWidth = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 100; 540 | var maxWidth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 8192; 541 | var widthTolerance = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0.08; 542 | var cache = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; 543 | var minW = Math.floor(minWidth); 544 | var maxW = Math.floor(maxWidth); 545 | validateRange(minWidth, maxWidth); 546 | validateWidthTolerance(widthTolerance); 547 | var cacheKey = widthTolerance + '/' + minW + '/' + maxW; 548 | 549 | // First, check the cache. 550 | if (cacheKey in cache) { 551 | return cache[cacheKey]; 552 | } 553 | if (minW === maxW) { 554 | return [minW]; 555 | } 556 | var resolutions = []; 557 | var currentWidth = minW; 558 | while (currentWidth < maxW) { 559 | // While the currentWidth is less than the maxW, push the rounded 560 | // width onto the list of resolutions. 561 | resolutions.push(Math.round(currentWidth)); 562 | currentWidth *= 1 + widthTolerance * 2; 563 | } 564 | 565 | // At this point, the last width in resolutions is less than the 566 | // currentWidth that caused the loop to terminate. This terminating 567 | // currentWidth is greater than or equal to the maxW. We want to 568 | // to stop at maxW, so we make sure our maxW is larger than the last 569 | // width in resolutions before pushing it (if it's equal we're done). 570 | if (resolutions[resolutions.length - 1] < maxW) { 571 | resolutions.push(maxW); 572 | } 573 | cache[cacheKey] = resolutions; 574 | return resolutions; 575 | } 576 | }]); 577 | return ImgixClient; 578 | }(); 579 | 580 | export { ImgixClient as default }; 581 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@imgix/js-core", 3 | "description": "A JavaScript client library for generating image URLs with imgix", 4 | "version": "3.8.0", 5 | "repository": "https://github.com/imgix/js-core", 6 | "license": "BSD-2-Clause", 7 | "main": "dist/index.cjs.js", 8 | "module": "dist/index.esm.js", 9 | "types": "dist/index.d.ts", 10 | "dependencies": { 11 | "js-base64": "~3.7.0", 12 | "md5": "^2.2.1", 13 | "ufo": "^1.0.0" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "7.22.5", 17 | "@babel/preset-env": "7.22.5", 18 | "@babel/register": "7.22.5", 19 | "@google/semantic-release-replace-plugin": "1.2.0", 20 | "@semantic-release/changelog": "6.0.3", 21 | "@semantic-release/commit-analyzer": "9.0.2", 22 | "@semantic-release/git": "10.0.1", 23 | "@semantic-release/github": "8.1.0", 24 | "@semantic-release/npm": "9.0.2", 25 | "@semantic-release/release-notes-generator": "10.0.3", 26 | "benchmark": "2.1.4", 27 | "esm": "3.2.25", 28 | "mocha": "8.4.0", 29 | "prettier": "2.8.8", 30 | "rollup": "3.26.2", 31 | "rollup-plugin-babel": "4.4.0", 32 | "rollup-plugin-commonjs": "10.1.0", 33 | "@rollup/plugin-node-resolve": "15.1.0", 34 | "rollup-plugin-uglify": "6.0.4", 35 | "sinon": "15.1.0", 36 | "tsd": "0.28.1", 37 | "typescript": "5.1.3", 38 | "uglify-js": "3.17.4" 39 | }, 40 | "scripts": { 41 | "assert_version": "node assert_version.cjs", 42 | "build": "rollup -c --bundleConfigAsCjs", 43 | "compile": "cp ./types/index.d.ts ./dist/index.d.ts && tsc", 44 | "dev": "rollup -c -w", 45 | "prepare": "npm run build && npm run compile && npm run assert_version", 46 | "prepublishOnly": "npm run build && npm run compile && npm run assert_version", 47 | "pretest": "npm run build", 48 | "pretty": "prettier --write '{src,test,types}/**/*.{js,ts}'", 49 | "test": "mocha --require esm --recursive ./test/*.js && npm run tsd", 50 | "test:watch": "mocha --require esm --recursive ./test/*.js --watch", 51 | "test:performance": "mocha --require esm --recursive test/performance/*.js", 52 | "tsd": "echo Running tsd; tsd", 53 | "release:dryRun": "npx node-env-run --exec 'semantic-release --dryRun'", 54 | "release:publish": "semantic-release" 55 | }, 56 | "publishConfig": { 57 | "access": "public" 58 | }, 59 | "files": [ 60 | "dist", 61 | "README.md", 62 | "src" 63 | ], 64 | "tsd": { 65 | "directory": "types" 66 | }, 67 | "release": { 68 | "branches": [ 69 | "main", 70 | { 71 | "name": "next", 72 | "prerelease": "rc" 73 | }, 74 | { 75 | "name": "beta", 76 | "prerelease": true 77 | }, 78 | { 79 | "name": "alpha", 80 | "prerelease": true 81 | } 82 | ], 83 | "plugins": [ 84 | "@semantic-release/commit-analyzer", 85 | "@semantic-release/release-notes-generator", 86 | [ 87 | "@google/semantic-release-replace-plugin", 88 | { 89 | "replacements": [ 90 | { 91 | "files": [ 92 | "src/constants.js" 93 | ], 94 | "from": "const VERSION = '.*'", 95 | "to": "const VERSION = '${nextRelease.version}'", 96 | "results": [ 97 | { 98 | "file": "src/constants.js", 99 | "hasChanged": true, 100 | "numMatches": 1, 101 | "numReplacements": 1 102 | } 103 | ], 104 | "countMatches": true 105 | } 106 | ] 107 | } 108 | ], 109 | "@semantic-release/changelog", 110 | "@semantic-release/npm", 111 | [ 112 | "@semantic-release/git", 113 | { 114 | "assets": [ 115 | "src/**", 116 | "dist/**", 117 | "package.json", 118 | "changelog.md" 119 | ], 120 | "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes} [skip ci]" 121 | } 122 | ], 123 | [ 124 | "@semantic-release/github", 125 | { 126 | "assets": [ 127 | { 128 | "path": "dist/imgix-js-core.umd.js", 129 | "label": "Standalone UMD build" 130 | }, 131 | { 132 | "path": "dist/index.cjs.js", 133 | "label": "Standalone CJS build" 134 | }, 135 | { 136 | "path": "dist/index.esm.js", 137 | "label": "Standalone ESM build" 138 | }, 139 | { 140 | "path": "dist/index.d.ts", 141 | "label": "Type declarations file" 142 | } 143 | ] 144 | } 145 | ] 146 | ] 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const { nodeResolve } = require('@rollup/plugin-node-resolve'); 2 | const commonjs = require('rollup-plugin-commonjs'); 3 | const { uglify } = require('rollup-plugin-uglify'); 4 | const babel = require('rollup-plugin-babel'); 5 | const pkg = require('./package.json'); 6 | 7 | export default [ 8 | // Browser-friendly UMD build. 9 | { 10 | input: 'src/index.js', 11 | output: { 12 | name: 'ImgixClient', 13 | file: 'dist/imgix-js-core.umd.js', 14 | format: 'umd', 15 | }, 16 | plugins: [ 17 | nodeResolve(), 18 | commonjs(), 19 | babel({ 20 | exclude: ['node_modules/**'], 21 | }), 22 | uglify(), 23 | ], 24 | }, 25 | { 26 | input: 'src/index.js', 27 | external: ['md5', 'js-base64', 'assert'], 28 | output: [ 29 | { file: pkg.main, format: 'cjs', exports: 'default'}, 30 | { file: pkg.module, format: 'es', exports: 'default' }, 31 | ], 32 | plugins: [ 33 | babel({ 34 | exclude: ['node_modules/**'], 35 | }), 36 | ], 37 | }, 38 | ]; 39 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // package version used in the ix-lib parameter 2 | export const VERSION = '3.8.0'; 3 | // regex pattern used to determine if a domain is valid 4 | export const DOMAIN_REGEX = /^(?:[a-z\d\-_]{1,62}\.){0,125}(?:[a-z\d](?:\-(?=\-*[a-z\d])|[a-z]|\d){0,62}\.)[a-z\d]{1,63}$/i; 5 | // minimum generated srcset width 6 | export const MIN_SRCSET_WIDTH = 100; 7 | // maximum generated srcset width 8 | export const MAX_SRCSET_WIDTH = 8192; 9 | // default tolerable percent difference between srcset pair widths 10 | export const DEFAULT_SRCSET_WIDTH_TOLERANCE = 0.08; 11 | 12 | // default quality parameter values mapped by each dpr srcset entry 13 | export const DPR_QUALITIES = { 14 | 1: 75, 15 | 2: 50, 16 | 3: 35, 17 | 4: 23, 18 | 5: 20, 19 | }; 20 | 21 | export const DEFAULT_DPR = [1, 2, 3, 4, 5]; 22 | 23 | export const DEFAULT_OPTIONS = { 24 | domain: null, 25 | useHTTPS: true, 26 | includeLibraryParam: true, 27 | urlPrefix: 'https://', 28 | secureURLToken: null, 29 | }; 30 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | import { parseURL, hasProtocol } from 'ufo'; 2 | 3 | /** 4 | * `extractUrl()` extracts URL components from a source URL string. 5 | * It does this by matching the URL against regular expressions. The irrelevant 6 | * (entire URL) matches are removed and the rest stored as their corresponding 7 | * URL components. 8 | * 9 | * `url` can be a partial, full URL, or full proxy URL. `useHttps` boolean 10 | * defaults to false. 11 | * 12 | * @returns {Object} `{ protocol, auth, host, pathname, search, hash }` 13 | * extracted from the URL. 14 | */ 15 | export function extractUrl({ url = '', useHttps = false }) { 16 | const defaultProto = useHttps ? 'https://' : 'http://'; 17 | if (!hasProtocol(url, true)) { 18 | return extractUrl({ url: defaultProto + url }); 19 | } 20 | /** 21 | * Regex are hard to parse. Leaving this breakdown here for reference. 22 | * - `protocol`: ([^:/]+:)? - all not `:` or `/` & preceded by `:`, 0-1 times 23 | * - `auth`: ([^/@]+@)? - all not `/` or `@` & preceded by `@`, 0-1 times 24 | * - `domainAndPath`: (.*) /) - all except line breaks 25 | * - `domain`: `([^/]*)` - all before a `/` token 26 | */ 27 | return parseURL(url); 28 | } 29 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { Base64 } from 'js-base64'; 2 | import md5 from 'md5'; 3 | import { getQuery } from 'ufo'; 4 | import { 5 | DEFAULT_DPR, 6 | DEFAULT_OPTIONS, 7 | DOMAIN_REGEX, 8 | DPR_QUALITIES, 9 | VERSION 10 | } from './constants.js'; 11 | import { extractUrl } from './helpers'; 12 | import { 13 | validateAndDestructureOptions, 14 | validateDevicePixelRatios, 15 | validateRange, 16 | validateVariableQualities, 17 | validateVariableQuality, 18 | validateWidths, 19 | validateWidthTolerance 20 | } from './validators.js'; 21 | 22 | export default class ImgixClient { 23 | constructor(opts = {}) { 24 | this.settings = { ...DEFAULT_OPTIONS, ...opts }; 25 | // a cache to store memoized srcset width-pairs 26 | this.targetWidthsCache = {}; 27 | if (typeof this.settings.domain != 'string') { 28 | throw new Error('ImgixClient must be passed a valid string domain'); 29 | } 30 | 31 | if (DOMAIN_REGEX.exec(this.settings.domain) == null) { 32 | throw new Error( 33 | 'Domain must be passed in as fully-qualified ' + 34 | 'domain name and should not include a protocol or any path ' + 35 | 'element, i.e. "example.imgix.net".', 36 | ); 37 | } 38 | 39 | if (this.settings.includeLibraryParam) { 40 | this.settings.libraryParam = 'js-' + ImgixClient.version(); 41 | } 42 | 43 | this.settings.urlPrefix = this.settings.useHTTPS ? 'https://' : 'http://'; 44 | } 45 | 46 | static version() { 47 | return VERSION; 48 | } 49 | 50 | buildURL(rawPath = '', params = {}, options = {}) { 51 | const path = this._sanitizePath(rawPath, options); 52 | 53 | let finalParams = this._buildParams(params, options); 54 | if (!!this.settings.secureURLToken) { 55 | finalParams = this._signParams(path, finalParams); 56 | } 57 | return this.settings.urlPrefix + this.settings.domain + path + finalParams; 58 | } 59 | 60 | /** 61 | *`_buildURL` static method allows full URLs to be formatted for use with 62 | * imgix. 63 | * 64 | * - If the source URL has included parameters, they are merged with 65 | * the `params` passed in as an argument. 66 | * - URL must match `{host}/{pathname}?{query}` otherwise an error is thrown. 67 | * 68 | * @param {String} url - full source URL path string, required 69 | * @param {Object} params - imgix params object, optional 70 | * @param {Object} options - imgix client options, optional 71 | * 72 | * @returns URL string formatted to imgix specifications. 73 | * 74 | * @example 75 | * const client = ImgixClient 76 | * const params = { w: 100 } 77 | * const opts = { useHttps: true } 78 | * const src = "sdk-test.imgix.net/amsterdam.jpg?h=100" 79 | * const url = client._buildURL(src, params, opts) 80 | * console.log(url) 81 | * // => "https://sdk-test.imgix.net/amsterdam.jpg?h=100&w=100" 82 | */ 83 | static _buildURL(url, params = {}, options = {}) { 84 | if (url == null) { 85 | return ''; 86 | } 87 | 88 | const { host, pathname, search } = extractUrl({ 89 | url, 90 | useHTTPS: options.useHTTPS, 91 | }); 92 | // merge source URL parameters with options parameters 93 | const combinedParams = { ...getQuery(search), ...params }; 94 | 95 | // throw error if no host or no pathname present 96 | if (!host.length || !pathname.length) { 97 | throw new Error('_buildURL: URL must match {host}/{pathname}?{query}'); 98 | } 99 | 100 | const client = new ImgixClient({ domain: host, ...options }); 101 | 102 | return client.buildURL(pathname, combinedParams); 103 | } 104 | 105 | _buildParams(params = {}, options = {}) { 106 | // If a custom encoder is present, use it 107 | // Otherwise just use the encodeURIComponent 108 | const useCustomEncoder = !!options.encoder; 109 | const customEncoder = options.encoder; 110 | 111 | const queryParams = [ 112 | // Set the libraryParam if applicable. 113 | ...(this.settings.libraryParam 114 | ? [`ixlib=${this.settings.libraryParam}`] 115 | : []), 116 | 117 | // Map over the key-value pairs in params while applying applicable encoding. 118 | ...Object.entries(params).reduce((prev, [key, value]) => { 119 | if (value == null) { 120 | return prev; 121 | } 122 | const encodedKey = useCustomEncoder ? customEncoder(key, value) : encodeURIComponent(key); 123 | const encodedValue = 124 | key.substr(-2) === '64' 125 | ? useCustomEncoder ? customEncoder(value, key) : Base64.encodeURI(value) 126 | : useCustomEncoder ? customEncoder(value, key) : encodeURIComponent(value); 127 | prev.push(`${encodedKey}=${encodedValue}`); 128 | 129 | return prev; 130 | }, []), 131 | ]; 132 | 133 | return `${queryParams.length > 0 ? '?' : ''}${queryParams.join('&')}`; 134 | } 135 | 136 | _signParams(path, queryParams) { 137 | const signatureBase = this.settings.secureURLToken + path + queryParams; 138 | const signature = md5(signatureBase); 139 | 140 | return queryParams.length > 0 141 | ? queryParams + '&s=' + signature 142 | : '?s=' + signature; 143 | } 144 | 145 | /** 146 | * "Sanitize" the path of the image URL. 147 | * Ensures that the path has a leading slash, and that the path is correctly 148 | * encoded. If it's a proxy path (begins with http/https), then encode the 149 | * whole path as a URI component, otherwise only encode specific characters. 150 | * @param {string} path The URL path of the image 151 | * @param {Object} options Sanitization options 152 | * @param {boolean} options.encode Whether to encode the path, default true 153 | * @returns {string} The sanitized path 154 | */ 155 | _sanitizePath(path, options = {}) { 156 | // Strip leading slash first (we'll re-add after encoding) 157 | let _path = path.replace(/^\//, ''); 158 | 159 | if (options.disablePathEncoding) { 160 | return '/' + _path; 161 | } 162 | 163 | if (options.encoder) { 164 | _path = options.encoder(_path); 165 | } else if (/^https?:\/\//.test(_path)) { 166 | // Use de/encodeURIComponent to ensure *all* characters are handled, 167 | // since it's being used as a path 168 | _path = encodeURIComponent(_path); 169 | } else { 170 | // Use de/encodeURI if we think the path is just a path, 171 | // so it leaves legal characters like '/' and '@' alone 172 | _path = encodeURI(_path).replace(/[#?:+]/g, encodeURIComponent); 173 | } 174 | return '/' + _path; 175 | } 176 | 177 | buildSrcSet(path, params = {}, options = {}) { 178 | const { w, h } = params; 179 | 180 | if (w || h) { 181 | return this._buildDPRSrcSet(path, params, options); 182 | } else { 183 | return this._buildSrcSetPairs(path, params, options); 184 | } 185 | } 186 | 187 | /** 188 | * _buildSrcSet static method allows full URLs to be used when generating 189 | * imgix formatted `srcset` string values. 190 | * 191 | * - If the source URL has included parameters, they are merged with 192 | * the `params` passed in as an argument. 193 | * - URL must match `{host}/{pathname}?{query}` otherwise an error is thrown. 194 | * 195 | * @param {String} url - full source URL path string, required 196 | * @param {Object} params - imgix params object, optional 197 | * @param {Object} srcsetModifiers - srcset modifiers, optional 198 | * @param {Object} clientOptions - imgix client options, optional 199 | * @returns imgix `srcset` for full URLs. 200 | */ 201 | static _buildSrcSet( 202 | url, 203 | params = {}, 204 | srcsetModifiers = {}, 205 | clientOptions = {}, 206 | ) { 207 | if (url == null) { 208 | return ''; 209 | } 210 | 211 | const { host, pathname, search } = extractUrl({ 212 | url, 213 | useHTTPS: clientOptions.useHTTPS, 214 | }); 215 | // merge source URL parameters with options parameters 216 | const combinedParams = { ...getQuery(search), ...params }; 217 | 218 | // throw error if no host or no pathname present 219 | if (!host.length || !pathname.length) { 220 | throw new Error( 221 | '_buildOneStepURL: URL must match {host}/{pathname}?{query}', 222 | ); 223 | } 224 | 225 | const client = new ImgixClient({ domain: host, ...clientOptions }); 226 | return client.buildSrcSet(pathname, combinedParams, srcsetModifiers); 227 | } 228 | 229 | // returns an array of width values used during srcset generation 230 | static targetWidths( 231 | minWidth = 100, 232 | maxWidth = 8192, 233 | widthTolerance = 0.08, 234 | cache = {}, 235 | ) { 236 | const minW = Math.floor(minWidth); 237 | const maxW = Math.floor(maxWidth); 238 | validateRange(minWidth, maxWidth); 239 | validateWidthTolerance(widthTolerance); 240 | const cacheKey = widthTolerance + '/' + minW + '/' + maxW; 241 | 242 | // First, check the cache. 243 | if (cacheKey in cache) { 244 | return cache[cacheKey]; 245 | } 246 | 247 | if (minW === maxW) { 248 | return [minW]; 249 | } 250 | 251 | const resolutions = []; 252 | let currentWidth = minW; 253 | while (currentWidth < maxW) { 254 | // While the currentWidth is less than the maxW, push the rounded 255 | // width onto the list of resolutions. 256 | resolutions.push(Math.round(currentWidth)); 257 | currentWidth *= 1 + widthTolerance * 2; 258 | } 259 | 260 | // At this point, the last width in resolutions is less than the 261 | // currentWidth that caused the loop to terminate. This terminating 262 | // currentWidth is greater than or equal to the maxW. We want to 263 | // to stop at maxW, so we make sure our maxW is larger than the last 264 | // width in resolutions before pushing it (if it's equal we're done). 265 | if (resolutions[resolutions.length - 1] < maxW) { 266 | resolutions.push(maxW); 267 | } 268 | 269 | cache[cacheKey] = resolutions; 270 | 271 | return resolutions; 272 | } 273 | 274 | _buildSrcSetPairs(path, params = {}, options = {}) { 275 | const [widthTolerance, minWidth, maxWidth] = 276 | validateAndDestructureOptions(options); 277 | 278 | let targetWidthValues; 279 | if (options.widths) { 280 | validateWidths(options.widths); 281 | targetWidthValues = [...options.widths]; 282 | } else { 283 | targetWidthValues = ImgixClient.targetWidths( 284 | minWidth, 285 | maxWidth, 286 | widthTolerance, 287 | this.targetWidthsCache, 288 | ); 289 | } 290 | 291 | const srcset = targetWidthValues.map( 292 | (w) => 293 | `${this.buildURL( 294 | path, 295 | { ...params, w }, 296 | options, 297 | )} ${w}w`, 298 | ); 299 | 300 | return srcset.join(',\n'); 301 | } 302 | 303 | _buildDPRSrcSet(path, params = {}, options = {}) { 304 | if (options.devicePixelRatios) { 305 | validateDevicePixelRatios(options.devicePixelRatios); 306 | } 307 | 308 | const targetRatios = options.devicePixelRatios || DEFAULT_DPR; 309 | 310 | const disableVariableQuality = options.disableVariableQuality || false; 311 | 312 | if (!disableVariableQuality) { 313 | validateVariableQuality(disableVariableQuality); 314 | } 315 | 316 | if (options.variableQualities) { 317 | validateVariableQualities(options.variableQualities); 318 | } 319 | 320 | const qualities = { ...DPR_QUALITIES, ...options.variableQualities }; 321 | 322 | const withQuality = (path, params, dpr) => { 323 | return `${this.buildURL( 324 | path, 325 | { 326 | ...params, 327 | dpr: dpr, 328 | q: params.q || qualities[dpr] || qualities[Math.floor(dpr)], 329 | }, 330 | options, 331 | )} ${dpr}x`; 332 | }; 333 | 334 | const srcset = disableVariableQuality 335 | ? targetRatios.map( 336 | (dpr) => 337 | `${this.buildURL( 338 | path, 339 | { ...params, dpr }, 340 | options, 341 | )} ${dpr}x`, 342 | ) 343 | : targetRatios.map((dpr) => withQuality(path, params, dpr)); 344 | 345 | return srcset.join(',\n'); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/validators.js: -------------------------------------------------------------------------------- 1 | import { 2 | MIN_SRCSET_WIDTH, 3 | MAX_SRCSET_WIDTH, 4 | DEFAULT_SRCSET_WIDTH_TOLERANCE, 5 | } from './constants.js'; 6 | 7 | export function validateAndDestructureOptions(options) { 8 | let widthTolerance; 9 | if (options.widthTolerance !== undefined) { 10 | validateWidthTolerance(options.widthTolerance); 11 | widthTolerance = options.widthTolerance; 12 | } else { 13 | widthTolerance = DEFAULT_SRCSET_WIDTH_TOLERANCE; 14 | } 15 | 16 | const minWidth = 17 | options.minWidth === undefined ? MIN_SRCSET_WIDTH : options.minWidth; 18 | const maxWidth = 19 | options.maxWidth === undefined ? MAX_SRCSET_WIDTH : options.maxWidth; 20 | 21 | // Validate the range unless we're using defaults for both 22 | if (minWidth != MIN_SRCSET_WIDTH || maxWidth != MAX_SRCSET_WIDTH) { 23 | validateRange(minWidth, maxWidth); 24 | } 25 | 26 | return [widthTolerance, minWidth, maxWidth]; 27 | } 28 | 29 | export function validateRange(min, max) { 30 | if ( 31 | !(Number.isInteger(min) && Number.isInteger(max)) || 32 | min <= 0 || 33 | max <= 0 || 34 | min > max 35 | ) { 36 | throw new Error( 37 | `The min and max srcset widths can only be passed positive Number values, and min must be less than max. Found min: ${min} and max: ${max}.`, 38 | ); 39 | } 40 | } 41 | 42 | export function validateWidthTolerance(widthTolerance) { 43 | if (typeof widthTolerance != 'number' || widthTolerance < 0.01) { 44 | throw new Error( 45 | 'The srcset widthTolerance must be a number greater than or equal to 0.01', 46 | ); 47 | } 48 | } 49 | 50 | export function validateWidths(customWidths) { 51 | if (!Array.isArray(customWidths) || !customWidths.length) { 52 | throw new Error( 53 | 'The widths argument can only be passed a valid non-empty array of integers', 54 | ); 55 | } else { 56 | const allPositiveIntegers = customWidths.every(function (width) { 57 | return Number.isInteger(width) && width > 0; 58 | }); 59 | if (!allPositiveIntegers) { 60 | throw new Error( 61 | 'A custom widths argument can only contain positive integer values', 62 | ); 63 | } 64 | } 65 | } 66 | 67 | export function validateVariableQuality(disableVariableQuality) { 68 | if (typeof disableVariableQuality != 'boolean') { 69 | throw new Error( 70 | 'The disableVariableQuality argument can only be passed a Boolean value', 71 | ); 72 | } 73 | } 74 | 75 | export function validateDevicePixelRatios(devicePixelRatios) { 76 | if (!Array.isArray(devicePixelRatios) || !devicePixelRatios.length) { 77 | throw new Error( 78 | 'The devicePixelRatios argument can only be passed a valid non-empty array of integers', 79 | ); 80 | } else { 81 | const allValidDPR = devicePixelRatios.every(function (dpr) { 82 | return typeof dpr === 'number' && dpr >= 1 && dpr <= 5; 83 | }); 84 | 85 | if (!allValidDPR) { 86 | throw new Error( 87 | 'The devicePixelRatios argument can only contain positive integer values between 1 and 5', 88 | ); 89 | } 90 | } 91 | } 92 | 93 | export function validateVariableQualities(variableQualities) { 94 | if (typeof variableQualities !== 'object') { 95 | throw new Error('The variableQualities argument can only be an object'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/performance/test-benchmarks.js: -------------------------------------------------------------------------------- 1 | import Benchmark from 'benchmark'; 2 | import ImgixClient from '../../src/index.js'; 3 | 4 | const client = new ImgixClient({ 5 | domain: 'testing.imgix.net', 6 | includeLibraryParam: false, 7 | }); 8 | 9 | const suite = new Benchmark.Suite('srcset generation', { 10 | onStart: function () { 11 | console.log(`\tBenchmarking ${this.name}\n`); 12 | }, 13 | 14 | onCycle: function () { 15 | // clears the target width cache between runs to ensure that 16 | // memoization does not affect subsequent benchmark cycles 17 | const memoizedCache = client.targetWidthsCache; 18 | Object.getOwnPropertyNames(memoizedCache).forEach(function (prop) { 19 | delete memoizedCache[prop]; 20 | }); 21 | }, 22 | 23 | onComplete: function () { 24 | for (const key in this) { 25 | const benchmark = this[key]; 26 | try { 27 | if (benchmark.constructor.name == 'Benchmark') { 28 | console.log('\t\t' + benchmark.toString() + '\n'); 29 | } 30 | } catch (error) {} 31 | } 32 | }, 33 | }); 34 | 35 | suite.add('responsive', function () { 36 | client.buildSrcSet('image.jpg'); 37 | }); 38 | 39 | suite.add('responsive with fixed height', function () { 40 | client.buildSrcSet('image.jpg', { h: 1000 }); 41 | }); 42 | 43 | suite.add('responsive with fixed aspect ratio', function () { 44 | client.buildSrcSet('image.jpg', { ar: '4:5' }); 45 | }); 46 | 47 | suite.add('fixed width', function () { 48 | client.buildSrcSet('image.jpg', { w: 1000 }); 49 | }); 50 | 51 | suite.add('fixed width with aspect ratio', function () { 52 | client.buildSrcSet('image.jpg', { w: 1000, ar: '4:5' }); 53 | }); 54 | 55 | suite.add('fixed width with variable quality disabled', function () { 56 | client.buildSrcSet( 57 | 'image.jpg', 58 | { w: 1000 }, 59 | { disableVariableQuality: true }, 60 | ); 61 | }); 62 | 63 | suite.add('with custom widths', function () { 64 | client.buildSrcSet('image.jpg', {}, { widths: [100, 400, 800, 1200, 1800] }); 65 | }); 66 | 67 | suite.add('with minWidth and maxWidth defined', function () { 68 | client.buildSrcSet('image.jpg', {}, { minWidth: 1800, maxWidth: 3000 }); 69 | }); 70 | 71 | suite.run({ async: false }); 72 | -------------------------------------------------------------------------------- /test/test-_buildSrcSet.js: -------------------------------------------------------------------------------- 1 | import md5 from 'md5'; 2 | import assert from 'assert'; 3 | import ImgixClient from '../src/index.js'; 4 | 5 | function assertWidthsIncreaseByTolerance(srcset, tolerance) { 6 | const srcsetWidths = srcset.split(',').map((u) => { 7 | const tail = u.split(' ')[1]; 8 | const width = tail.slice(0, -1); 9 | return Number.parseFloat(width); 10 | }); 11 | 12 | // Make two equal sized arrays one for the numerators, e.g. 13 | // [x1, x2, ..., xN] and another for the denominators, e.g. 14 | // [x0, x1,..., x(N-1)]. 15 | const numerators = srcsetWidths.slice(1); 16 | const denominators = srcsetWidths.slice(0, -1); 17 | 18 | // Zip the numerator/denominator pairs. 19 | const pairs = numerators.map((n, i) => { 20 | return [n, denominators[i]]; 21 | }); 22 | 23 | // Be as tolerant as we can. 24 | const tolerancePlus = tolerance + 0.004; 25 | 26 | // Divide the zipped pairs, e.g. (x1 / x0), (x2 / x1)... 27 | pairs.map((p) => { 28 | assert(p[0] / p[1] - 1 < tolerancePlus); 29 | }); 30 | } 31 | 32 | function assertCorrectSigning(srcset, path, token) { 33 | const _ = srcset.split(',').map((u) => { 34 | // Split srcset into list of URLs. 35 | const url = u.split(' ')[0]; 36 | assert(url.includes('s=')); 37 | 38 | // Get the signature without the otherParams. 39 | const signature = url.slice(url.indexOf('s=') + 2, url.length); 40 | 41 | // Use the otherParams, path, and token to create the expected signature. 42 | const otherParams = url.slice(url.indexOf('?'), url.indexOf('s=') - 1); 43 | const expected = md5(token + path + otherParams).toString(); 44 | 45 | assert.strictEqual(signature, expected); 46 | }); 47 | } 48 | 49 | function assertMinMaxWidthBounds(srcset, minBound, maxBound) { 50 | const srcsetSplit = srcset.split(','); 51 | const min = Number.parseFloat(srcsetSplit[0].split(' ')[1].slice(0, -1)); 52 | const max = Number.parseFloat( 53 | srcsetSplit[srcsetSplit.length - 1].split(' ')[1].slice(0, -1), 54 | ); 55 | assert(min >= minBound); 56 | assert(max <= maxBound); 57 | } 58 | 59 | function assertCorrectWidthDescriptors(srcset, descriptors) { 60 | const srcsetSplit = srcset.split(','); 61 | srcsetSplit.map((u, i) => { 62 | const width = parseInt(u.split(' ')[1].slice(0, -1), 10); 63 | assert.strictEqual(width, descriptors[i]); 64 | }); 65 | } 66 | 67 | function assertIncludesQualities(srcset, qualities) { 68 | srcset.split(',').map((u, i) => { 69 | const url = u.split(' ')[0]; 70 | assert(url.includes(`q=${qualities[i]}`)); 71 | }); 72 | } 73 | 74 | function assertIncludesQualityOverride(srcset, qOverride) { 75 | srcset.split(',').map((u) => { 76 | const url = u.split(' ')[0]; 77 | assert(url.includes(`q=${qOverride}`)); 78 | }); 79 | } 80 | 81 | function assertIncludesDefaultDprParamAndDescriptor(srcset) { 82 | const srcsetSplit = srcset.split(','); 83 | assert.strictEqual(srcsetSplit.length, 5); 84 | 85 | const parts = srcsetSplit.map((u) => u.split(' ')); 86 | 87 | // The firstParts contains the URLs without the width descriptors, 88 | // i.e. ['https://test.imgix.net/image.jpg?dpr=1...',...] 89 | const firstParts = parts.map((p) => p[0]); 90 | firstParts.map((u, i) => { 91 | assert(u.includes(`dpr=${i + 1}`)); 92 | }); 93 | 94 | // The lastParts contain the width descriptors without the URLs, 95 | // i.e. [ '1x', '2x', '3x', '4x', '5x' ] 96 | const lastParts = parts.map((p) => p[1]); 97 | lastParts.map((d, i) => { 98 | // assert 1x === `${0 + 1}x`, etc. 99 | assert.strictEqual(d, `${i + 1}x`); 100 | }); 101 | } 102 | 103 | function assertDoesNotIncludeQuality(srcset) { 104 | const _ = srcset.split(',').map((u) => { 105 | const url = u.split(' ')[0]; 106 | assert(!url.includes(`q=`)); 107 | }); 108 | } 109 | 110 | const RESOLUTIONS = [ 111 | 100, 112 | 116, 113 | 135, 114 | 156, 115 | 181, 116 | 210, 117 | 244, 118 | 283, 119 | 328, 120 | 380, 121 | 441, 122 | 512, 123 | 594, 124 | 689, 125 | 799, 126 | 927, 127 | 1075, 128 | 1247, 129 | 1446, 130 | 1678, 131 | 1946, 132 | 2257, 133 | 2619, 134 | 3038, 135 | 3524, 136 | 4087, 137 | 4741, 138 | 5500, 139 | 6380, 140 | 7401, 141 | 8192, 142 | ]; 143 | 144 | describe('URL Builder:', function describeSuite() { 145 | describe('Calling _buildSrcSet()', function describeSuite() { 146 | let client, params, url, srcsetModifiers, clientOptions; 147 | 148 | describe('on a one-step URL', function describeSuite() { 149 | url = 'https://testing.imgix.net/image.jpg'; 150 | clientOptions = { 151 | includeLibraryParam: false, 152 | useHTTPS: true, 153 | secureURLToken: 'MYT0KEN', 154 | }; 155 | srcsetModifiers = {}; 156 | client = ImgixClient; 157 | const srcset = client._buildSrcSet( 158 | url, 159 | params, 160 | srcsetModifiers, 161 | clientOptions, 162 | ); 163 | 164 | describe('with no parameters', function describeSuite() { 165 | params = {}; 166 | it('should generate the expected default srcset pair values', function testSpec() { 167 | assertCorrectWidthDescriptors(srcset, RESOLUTIONS); 168 | }); 169 | 170 | it('should return the expected number of `url widthDescriptor` pairs', function testSpec() { 171 | assert.strictEqual(srcset.split(',').length, 31); 172 | }); 173 | 174 | it('should not exceed the bounds of [100, 8192]', function testSpec() { 175 | assertMinMaxWidthBounds(srcset, 100, 8192); 176 | }); 177 | 178 | // a 17% testing threshold is used to account for rounding 179 | it('should not increase more than 17% every iteration', function testSpec() { 180 | assertWidthsIncreaseByTolerance(srcset, 0.17); 181 | }); 182 | 183 | it('should correctly sign each URL', function testSpec() { 184 | assertCorrectSigning(srcset, '/image.jpg', 'MYT0KEN'); 185 | }); 186 | }); 187 | 188 | describe('with a width parameter provided', function describeSuite() { 189 | params = { w: 100 }; 190 | const DPR_QUALITY = [75, 50, 35, 23, 20]; 191 | const srcset = client._buildSrcSet( 192 | url, 193 | params, 194 | srcsetModifiers, 195 | clientOptions, 196 | ); 197 | 198 | it('should be in the form src 1x, src 2x, src 3x, src 4x, src 5x', function testSpec() { 199 | assertIncludesDefaultDprParamAndDescriptor(srcset); 200 | }); 201 | 202 | it('should correctly sign each URL', function testSpec() { 203 | assertCorrectSigning(srcset, '/image.jpg', 'MYT0KEN'); 204 | }); 205 | 206 | it('should include a dpr param per specified src', function testSpec() { 207 | assertIncludesDefaultDprParamAndDescriptor(srcset); 208 | }); 209 | 210 | it('should include variable qualities by default', function testSpec() { 211 | assertIncludesQualities(srcset, DPR_QUALITY); 212 | }); 213 | 214 | it('should override variable quality if quality parameter provided', function testSpec() { 215 | const QUALITY_OVERRIDE = 100; 216 | params = { w: 800, q: QUALITY_OVERRIDE }; 217 | const srcset = client._buildSrcSet( 218 | url, 219 | params, 220 | srcsetModifiers, 221 | clientOptions, 222 | ); 223 | 224 | assertIncludesQualityOverride(srcset, QUALITY_OVERRIDE); 225 | }); 226 | 227 | it("should disable variable qualities if 'disableVariableQuality'", function testSpec() { 228 | params = { w: 800 }; 229 | srcsetModifiers = { disableVariableQuality: true }; 230 | const srcset = client._buildSrcSet( 231 | url, 232 | params, 233 | srcsetModifiers, 234 | clientOptions, 235 | ); 236 | assertDoesNotIncludeQuality(srcset); 237 | }); 238 | 239 | it('should respect quality param when variable qualities disabled', function testSpec() { 240 | const QUALITY_OVERRIDE = 100; 241 | params = { w: 800, q: QUALITY_OVERRIDE }; 242 | srcsetModifiers = { disableVariableQuality: true }; 243 | const srcset = client._buildSrcSet( 244 | url, 245 | params, 246 | srcsetModifiers, 247 | clientOptions, 248 | ); 249 | assertIncludesQualityOverride(srcset, QUALITY_OVERRIDE); 250 | }); 251 | }); 252 | 253 | describe('using srcset parameters', function describeSuite() { 254 | describe('with a minWidth and/or maxWidth provided', function describeSuite() { 255 | const MIN = 500; 256 | const MAX = 2000; 257 | params = {}; 258 | srcsetModifiers = { minWidth: MIN, maxWidth: MAX }; 259 | const srcset = client._buildSrcSet( 260 | url, 261 | params, 262 | srcsetModifiers, 263 | clientOptions, 264 | ); 265 | 266 | it('should return correct number of `url widthDescriptor` pairs', function testSpec() { 267 | assert.strictEqual(srcset.split(',').length, 11); 268 | }); 269 | 270 | it('should generate the default srcset pair values', function testSpec() { 271 | const resolutions = [ 272 | 500, 273 | 580, 274 | 673, 275 | 780, 276 | 905, 277 | 1050, 278 | 1218, 279 | 1413, 280 | 1639, 281 | 1901, 282 | 2000, 283 | ]; 284 | assertCorrectWidthDescriptors(srcset, resolutions); 285 | }); 286 | 287 | it('should not exceed the bounds of [100, 8192]', function testSpec() { 288 | assertMinMaxWidthBounds(srcset, 100, 8192); 289 | }); 290 | }); 291 | }); 292 | }); 293 | }); 294 | }); 295 | -------------------------------------------------------------------------------- /test/test-_buildURL.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import ImgixClient from '../src/index.js'; 3 | 4 | describe('URL Builder:', function describeSuite() { 5 | describe('Calling _buildURL()', function describeSuite() { 6 | let client, params, url, options; 7 | 8 | beforeEach(function setupClient() { 9 | client = ImgixClient; 10 | }); 11 | 12 | describe('on a full URL', function describeSuite() { 13 | url = 'https://assets.imgix.net/images/1.png'; 14 | params = { h: 100 }; 15 | options = { includeLibraryParam: false, useHTTPS: true }; 16 | 17 | it('should return a URL with formatted imgix params', function testSpec() { 18 | const expectation = url + '?h=100'; 19 | const result = client._buildURL(url, params, options); 20 | 21 | assert.strictEqual(result, expectation); 22 | }); 23 | 24 | describe('that has no scheme', function describeSuite() { 25 | const url = 'assets.imgix.net/images/1.png'; 26 | const params = {}; 27 | const options = { includeLibraryParam: false, useHTTPS: true }; 28 | 29 | it('should prepend the scheme to the returned URL', function testSpec() { 30 | const expectation = 'https://' + url; 31 | const result = client._buildURL(url, params, options); 32 | 33 | assert.strictEqual(result, expectation); 34 | }); 35 | }); 36 | 37 | describe('that has a proxy path', function describeSuite() { 38 | const url = 'https://assets.imgix.net/https://sdk-test/images/1.png'; 39 | const params = {}; 40 | const options = { includeLibraryParam: false, useHTTPS: true }; 41 | 42 | it('should correctly encode the proxy path', function testSpec() { 43 | const expectation = new client({ 44 | domain: 'assets.imgix.net', 45 | ...options, 46 | }).buildURL('https://sdk-test/images/1.png', params); 47 | const result = client._buildURL(url, params, options); 48 | 49 | assert.strictEqual(result, expectation); 50 | }); 51 | }); 52 | 53 | describe('that has a insecure source and secure proxy', function describeSuite() { 54 | const url = 55 | 'http://assets.imgix.net/https://sdk-test.imgix.net/images/1.png'; 56 | const params = {}; 57 | const options = { includeLibraryParam: false, useHTTPS: false }; 58 | 59 | it('should not modify the source or proxy schemes', function testSpec() { 60 | const expectation = 61 | 'http://assets.imgix.net/https%3A%2F%2Fsdk-test.imgix.net%2Fimages%2F1.png'; 62 | const result = client._buildURL(url, params, options); 63 | 64 | assert.strictEqual(result, expectation); 65 | }); 66 | }); 67 | 68 | describe('that has a secure source and insecure proxy', function describeSuite() { 69 | const url = 70 | 'https://assets.imgix.net/http://sdk-test.imgix.net/images/1.png'; 71 | const params = {}; 72 | const options = { includeLibraryParam: false, useHTTPS: true }; 73 | 74 | it('should not modify the source or proxy schemes', function testSpec() { 75 | const expectation = 76 | 'https://assets.imgix.net/http%3A%2F%2Fsdk-test.imgix.net%2Fimages%2F1.png'; 77 | const result = client._buildURL(url, params, options); 78 | 79 | assert.strictEqual(result, expectation); 80 | }); 81 | }); 82 | }); 83 | 84 | describe('on a malformed URLs', function describeSuite() { 85 | const error = new Error( 86 | '_buildURL: URL must match {host}/{pathname}?{query}', 87 | ); 88 | const params = {}; 89 | const options = { includeLibraryParam: false, useHTTPS: true }; 90 | 91 | it('should throw an error if no hostname', function testSpec() { 92 | const url = '/image.png'; 93 | assert.throws(function () { 94 | client._buildURL(url, params, options); 95 | }, error); 96 | }); 97 | 98 | it('should throw an error if no pathname', function testSpec() { 99 | const url = 'assets.imgix.net'; 100 | assert.throws(function () { 101 | client._buildURL(url, params, options); 102 | }, error); 103 | }); 104 | }); 105 | 106 | describe('that has parameters in the URL', function describeSuite() { 107 | const url = 'https://assets.imgix.net/images/1.png?w=100&h=100'; 108 | const params = { h: 200 }; 109 | const options = { includeLibraryParam: false, useHTTPS: true }; 110 | 111 | it('should overwrite url params with opts params', function testSpec() { 112 | const expectation = 'https://assets.imgix.net/images/1.png?w=100&h=200'; 113 | const result = client._buildURL(url, params, options); 114 | 115 | assert.strictEqual(result, expectation); 116 | }); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /test/test-buildURL.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Base64 } from 'js-base64'; 3 | import ImgixClient from '../src/index.js'; 4 | 5 | describe('URL Builder:', function describeSuite() { 6 | describe('Calling _sanitizePath()', function describeSuite() { 7 | let client; 8 | 9 | beforeEach(function setupClient() { 10 | client = new ImgixClient({ 11 | domain: 'testing.imgix.net', 12 | }); 13 | }); 14 | 15 | describe('with a simple path', function describeSuite() { 16 | const path = 'images/1.png'; 17 | 18 | it('prepends a leading slash', function testSpec() { 19 | const expectation = '/'; 20 | const result = client._sanitizePath(path); 21 | 22 | assert.strictEqual(result.substring(0, 1), expectation); 23 | }); 24 | 25 | it('otherwise returns the same exact path', function testSpec() { 26 | const expectation = path; 27 | const result = client._sanitizePath(path); 28 | 29 | assert.strictEqual(result.substring(1), expectation); 30 | }); 31 | }); 32 | 33 | describe('with a path that contains a leading slash', function describeSuite() { 34 | const path = '/images/1.png'; 35 | 36 | it('retains the leading slash', function testSpec() { 37 | const expectation = '/'; 38 | const result = client._sanitizePath(path); 39 | 40 | assert.strictEqual(result.substring(0, 1), expectation); 41 | }); 42 | 43 | it('otherwise returns the same exact path', function testSpec() { 44 | const expectation = path.substring(1); 45 | const result = client._sanitizePath(path); 46 | 47 | assert.strictEqual(result.substring(1), expectation); 48 | }); 49 | }); 50 | 51 | describe('with a path that contains unencoded characters', function describeSuite() { 52 | const path = 'images/"image 1".png'; 53 | 54 | it('prepends a leading slash', function testSpec() { 55 | const expectation = '/'; 56 | const result = client._sanitizePath(path); 57 | 58 | assert.strictEqual(expectation, result.substring(0, 1)); 59 | }); 60 | 61 | it('otherwise returns the same path, except with the characters encoded properly', function testSpec() { 62 | const expectation = encodeURI(path); 63 | const result = client._sanitizePath(path); 64 | 65 | assert.strictEqual(result.substring(1), expectation); 66 | }); 67 | }); 68 | 69 | describe('with a path that contains a hash character', function describeSuite() { 70 | const path = '#blessed.png'; 71 | 72 | it('properly encodes the hash character', function testSpec() { 73 | const expectation = path.replace(/^#/, '%23'); 74 | const result = client._sanitizePath(path); 75 | 76 | assert.strictEqual(result.substring(1), expectation); 77 | }); 78 | }); 79 | 80 | describe('with a path that contains a question mark', function describeSuite() { 81 | const path = '?what.png'; 82 | 83 | it('properly encodes the question mark', function testSpec() { 84 | const expectation = path.replace(/^\?/, '%3F'); 85 | const result = client._sanitizePath(path); 86 | 87 | assert.strictEqual(result.substring(1), expectation); 88 | }); 89 | }); 90 | 91 | describe('with a path that contains a colon', function describeSuite() { 92 | const path = ':emoji.png'; 93 | 94 | it('properly encodes the colon', function testSpec() { 95 | const expectation = path.replace(/^\:/, '%3A'); 96 | const result = client._sanitizePath(path); 97 | 98 | assert.strictEqual(result.substring(1), expectation); 99 | }); 100 | }); 101 | 102 | describe('with a full HTTP URL', function describeSuite() { 103 | const path = 'http://example.com/images/1.png'; 104 | 105 | it('prepends a leading slash, unencoded', function testSpec() { 106 | const expectation = '/'; 107 | const result = client._sanitizePath(path); 108 | 109 | assert.strictEqual(result.substring(0, 1), expectation); 110 | }); 111 | 112 | it('otherwise returns a fully-encoded version of the given URL', function testSpec() { 113 | const expectation = encodeURIComponent(path); 114 | const result = client._sanitizePath(path); 115 | 116 | assert.strictEqual(result.substring(1), expectation); 117 | }); 118 | }); 119 | 120 | describe('with a full HTTPS URL', function describeSuite() { 121 | const path = 'https://example.com/images/1.png'; 122 | 123 | it('prepends a leading slash, unencoded', function testSpec() { 124 | const expectation = '/'; 125 | const result = client._sanitizePath(path); 126 | 127 | assert.strictEqual(result.substring(0, 1), expectation); 128 | }); 129 | 130 | it('otherwise returns a fully-encoded version of the given URL', function testSpec() { 131 | const expectation = encodeURIComponent(path); 132 | const result = client._sanitizePath(path); 133 | 134 | assert.strictEqual(result.substring(1), expectation); 135 | }); 136 | }); 137 | 138 | describe('with a full URL that contains a leading slash', function describeSuite() { 139 | const path = '/http://example.com/images/1.png'; 140 | 141 | it('retains the leading slash, unencoded', function testSpec() { 142 | const expectation = '/'; 143 | const result = client._sanitizePath(path); 144 | 145 | assert.strictEqual(result.substring(0, 1), expectation); 146 | }); 147 | 148 | it('otherwise returns a fully-encoded version of the given URL', function testSpec() { 149 | const expectation = encodeURIComponent(path.substring(1)); 150 | const result = client._sanitizePath(path); 151 | 152 | assert.strictEqual(result.substring(1), expectation); 153 | }); 154 | }); 155 | 156 | describe('with a full URL that contains encoded characters', function describeSuite() { 157 | const path = 'http://example.com/images/1.png?foo=%20'; 158 | 159 | it('prepends a leading slash, unencoded', function testSpec() { 160 | const expectation = '/'; 161 | const result = client._sanitizePath(path); 162 | 163 | assert.strictEqual(result.substring(0, 1), expectation); 164 | }); 165 | 166 | it('otherwise returns a fully-encoded version of the given URL', function testSpec() { 167 | const expectation = encodeURIComponent(path); 168 | const result = client._sanitizePath(path); 169 | 170 | assert.strictEqual(result.substring(1), expectation); 171 | }); 172 | 173 | it('double-encodes the original encoded characters', function testSpec() { 174 | const expectation1 = -1; 175 | const expectation2 = encodeURIComponent(path).length - 4; 176 | const result = client._sanitizePath(path); 177 | 178 | // Result should not contain the string "%20" 179 | assert.strictEqual(result.indexOf('%20'), expectation1); 180 | 181 | // Result should instead contain the string "%2520" 182 | assert.strictEqual(result.indexOf('%2520'), expectation2); 183 | }); 184 | }); 185 | 186 | describe('with a custom encoder defined', function describeSuite() { 187 | const path = 'http://example.com/images and photos/1.png?foo=%20'; 188 | 189 | it("encodes the path given custom logic", function testSpec() { 190 | const expectation = path.replaceAll(' ', '+'); 191 | const result = client._sanitizePath(path, { encoder: (path) => path.replaceAll(' ', '+')}); 192 | 193 | assert.strictEqual(result.substring(1), expectation); 194 | }); 195 | 196 | it("does not alter path encoding when custom encoder optional 'key' parameter is set", function testSpec() { 197 | const expectation = path.replaceAll(' ', '+'); 198 | const result = client._sanitizePath(path, { encoder: (path, optionalValue) => path.replaceAll(' ', '+')}); 199 | 200 | assert.strictEqual(result.substring(1), expectation); 201 | }); 202 | }); 203 | }); 204 | 205 | describe('Calling _buildParams()', function describeSuite() { 206 | let client; 207 | 208 | beforeEach(function setupClient() { 209 | client = new ImgixClient({ 210 | domain: 'testing.imgix.net', 211 | includeLibraryParam: false, 212 | }); 213 | }); 214 | 215 | it('returns an empty string if no parameters are given', function testSpec() { 216 | const params = {}; 217 | const expectation = ''; 218 | const result = client._buildParams(params); 219 | 220 | assert.strictEqual(result, expectation); 221 | }); 222 | 223 | it('returns a properly-formatted query string if a single parameter is given', function testSpec() { 224 | const params = { w: 400 }; 225 | const expectation = '?w=400'; 226 | const result = client._buildParams(params); 227 | 228 | assert.strictEqual(result, expectation); 229 | }); 230 | 231 | it('returns a properly-formatted query string if multiple parameters are given', function testSpec() { 232 | const params = { w: 400, h: 300 }; 233 | const expectation = '?w=400&h=300'; 234 | const result = client._buildParams(params); 235 | 236 | assert.strictEqual(result, expectation); 237 | }); 238 | 239 | it('does not modify its input-argument', function testSpec() { 240 | const emptyParams = {}; 241 | const emptyResult = client._buildParams(emptyParams); 242 | 243 | assert.strictEqual(Object.keys(emptyParams).length, 0); 244 | }); 245 | 246 | it('includes an `ixlib` param if the `libraryParam` setting is truthy', function testSpec() { 247 | client.settings.libraryParam = 'test'; 248 | const result = client._buildParams({}); 249 | assert(result.match(/ixlib=test/)); 250 | }); 251 | 252 | it('url-encodes parameter keys properly', function testSpec() { 253 | const params = { w$: 400 }; 254 | const expectation = '?w%24=400'; 255 | const result = client._buildParams(params); 256 | 257 | assert.strictEqual(result, expectation); 258 | }); 259 | 260 | it('url-encodes parameter values properly', function testSpec() { 261 | const params = { w: '$400' }; 262 | const expectation = '?w=%24400'; 263 | const result = client._buildParams(params); 264 | 265 | assert.strictEqual(result, expectation); 266 | }); 267 | 268 | it('base64-encodes parameter values whose keys end in `64`', function testSpec() { 269 | const params = { txt64: 'lorem ipsum' }; 270 | const expectation = '?txt64=bG9yZW0gaXBzdW0'; 271 | const result = client._buildParams(params); 272 | 273 | assert.strictEqual(result, expectation); 274 | }); 275 | 276 | it('does not base64-encode when a custom encoder is defined', () => { 277 | const params = { txt64: 'bG9yZW0gaXBzdW0' }; 278 | const expectation = '?txt64=bG9yZW0gaXBzdW0'; 279 | const result = client._buildParams(params, { encoder: (param) => param }); 280 | 281 | assert.strictEqual(result, expectation); 282 | }); 283 | }); 284 | 285 | describe('Calling _signParams()', function describeSuite() { 286 | let client; 287 | const path = 'images/1.png'; 288 | 289 | beforeEach(function setupClient() { 290 | client = new ImgixClient({ 291 | domain: 'testing.imgix.net', 292 | secureURLToken: 'MYT0KEN', 293 | includeLibraryParam: false, 294 | }); 295 | }); 296 | 297 | it('returns a query string containing only a proper signature parameter, if no other query parameters are provided', function testSpec() { 298 | const expectation = '?s=6d82410f89cc6d80a6aa9888dcf85825'; 299 | const result = client._signParams(path, ''); 300 | 301 | assert.strictEqual(result, expectation); 302 | }); 303 | 304 | it('returns a query string with a proper signature parameter appended, if other query parameters are provided', function testSpec() { 305 | const expectation = '?w=400&s=990916ef8cc640c58d909833e47f6c31'; 306 | const result = client._signParams(path, '?w=400'); 307 | 308 | assert.strictEqual(result, expectation); 309 | }); 310 | }); 311 | 312 | describe('Calling buildURL()', function describeSuite() { 313 | let client; 314 | 315 | beforeEach(function setupClient() { 316 | client = new ImgixClient({ 317 | domain: 'test.imgix.net', 318 | includeLibraryParam: false, 319 | }); 320 | }); 321 | 322 | it('is an idempotent operation with empty args', function testSpec() { 323 | const result1 = client.buildURL('', {}); 324 | const result2 = client.buildURL('', {}); 325 | 326 | assert.strictEqual(result1, result2); 327 | }); 328 | 329 | it('is an idempotent operation with args', function testSpec() { 330 | const path = '/image/stöked.png'; 331 | const params = { w: 100 }; 332 | const result1 = client.buildURL(path, params); 333 | const result2 = client.buildURL(path, params); 334 | const expected = 'https://test.imgix.net/image/st%C3%B6ked.png?w=100'; 335 | 336 | assert.strictEqual(result1, expected); 337 | assert.strictEqual(result2, expected); 338 | }); 339 | 340 | it('does not modify empty args', function testSpec() { 341 | const path = ''; 342 | const params = {}; 343 | const result1 = client.buildURL(path, params); 344 | const result2 = client.buildURL(path, params); 345 | const expected = 'https://test.imgix.net/'; 346 | 347 | assert.strictEqual(path, ''); 348 | assert.strictEqual(expected, result1); 349 | assert.strictEqual(expected, result2); 350 | 351 | assert.strictEqual(Object.keys(params).length, 0); 352 | assert.strictEqual(params.constructor, Object); 353 | }); 354 | 355 | it('does not modify its args', function testSpec() { 356 | const path = 'image/1.png'; 357 | const params = { w: 100 }; 358 | const result1 = client.buildURL(path, params); 359 | const result2 = client.buildURL(path, params); 360 | const expected = 'https://test.imgix.net/image/1.png?w=100'; 361 | 362 | assert.strictEqual(path, 'image/1.png'); 363 | assert.strictEqual(result1, result2, expected); 364 | assert.strictEqual(params.w, 100); 365 | 366 | assert.strictEqual(Object.keys(params).length, 1); 367 | assert.strictEqual(params.constructor, Object); 368 | }); 369 | 370 | it('correctly encodes plus signs (+) in paths', function testSpec() { 371 | const actual = client.buildURL('&$+,:;=?@#.jpg', {}); 372 | const expected = 'https://test.imgix.net/&$%2B,%3A;=%3F@%23.jpg'; 373 | 374 | assert.strictEqual(actual, expected); 375 | }); 376 | 377 | it('should not include undefined parameters in url', function testSpect() { 378 | const actual = client.buildURL('test.jpg', { ar: undefined, txt: null }); 379 | assert(!actual.includes('ar=undefined')); 380 | assert(!actual.includes('ar=null')); 381 | assert(!actual.includes('txt=undefined')); 382 | assert(!actual.includes('txt=null')); 383 | }); 384 | 385 | it('should be able to use a custom encoder', function testSpec() { 386 | const actual = client.buildURL( 387 | "unsplash/walrus.jpg", 388 | { 389 | txt: "test!(')", 390 | "txt-color": "000", 391 | "txt-size": 400, 392 | "txt-font": "Avenir-Black", 393 | "txt-x": 800, 394 | "txt-y": 600 395 | }, 396 | { 397 | encoder: (path) => encodeURI(path).replace("'", "%27") 398 | } 399 | ) 400 | 401 | 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' 402 | 403 | assert.strictEqual(actual, expected) 404 | }); 405 | 406 | it('can custom encode the parameter value based on the parameter key', () => { 407 | const params = { txt64: 'lorem ipsum', txt: 'Hello World' }; 408 | const expectation = '?txt64=bG9yZW0gaXBzdW0&txt=Hello+World'; 409 | const result = client._buildParams(params, { encoder: (value, key) => key && key.substr(-2) === '64' ? Base64.encodeURI(value) : value.replace(" ", "+") }); 410 | 411 | assert.strictEqual(result, expectation); 412 | }); 413 | }); 414 | }); 415 | -------------------------------------------------------------------------------- /test/test-client.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import ImgixClient from '../src/index.js'; 3 | import { VERSION } from '../src/constants.js'; 4 | 5 | describe('Imgix client:', function describeSuite() { 6 | describe('The constructor', function describeSuite() { 7 | it('uses HTTPS by default', function testSpec() { 8 | const client = new ImgixClient({ domain: 'test.imgix.net' }); 9 | assert.strictEqual(client.settings.useHTTPS, true); 10 | }); 11 | 12 | it('has no assigned token by default', function testSpec() { 13 | const client = new ImgixClient({ domain: 'test.imgix.net' }); 14 | assert.strictEqual(client.settings.secureURLToken, null); 15 | }); 16 | 17 | it('initializes with a token', function testSpec() { 18 | const expectedToken = 'MYT0KEN'; 19 | 20 | const client = new ImgixClient({ 21 | domain: 'test.imgix.net', 22 | secureURLToken: expectedToken, 23 | }); 24 | 25 | assert.strictEqual(client.settings.secureURLToken, expectedToken); 26 | }); 27 | 28 | it('initializes with token when using HTTP', function testSpec() { 29 | const client = new ImgixClient({ 30 | domain: 'my-host.imgix.net', 31 | secureURLToken: 'MYT0KEN', 32 | useHTTPS: false, 33 | }); 34 | assert.strictEqual(client.settings.secureURLToken, 'MYT0KEN'); 35 | assert.strictEqual(client.settings.useHTTPS, false); 36 | }); 37 | 38 | it('appends ixlib param by default', function testSpec() { 39 | const domain = 'test.imgix.net'; 40 | const expectedURL = `https://${domain}/image.jpg?ixlib=js-${VERSION}`; 41 | const client = new ImgixClient({ domain: domain }); 42 | 43 | assert.strictEqual(client.buildURL('image.jpg'), expectedURL); 44 | }); 45 | 46 | it('errors with invalid domain - appended slash', function testSpec() { 47 | assert.throws(function () { 48 | new ImgixClient({ 49 | domain: 'my-host1.imgix.net/', 50 | }); 51 | }, Error); 52 | }); 53 | 54 | it('errors with invalid domain - prepended scheme ', function testSpec() { 55 | assert.throws(function () { 56 | new ImgixClient({ 57 | domain: 'https://my-host1.imgix.net', 58 | }); 59 | }, Error); 60 | }); 61 | 62 | it('errors with invalid domain - appended dash ', function testSpec() { 63 | assert.throws(function () { 64 | new ImgixClient({ 65 | domain: 'my-host1.imgix.net-', 66 | }); 67 | }, Error); 68 | }); 69 | 70 | it('errors when domain is any non-string value', function testSpec() { 71 | assert.throws(function () { 72 | new ImgixClient({ 73 | domain: ['my-host.imgix.net', 'another-domain.imgix.net'], 74 | }); 75 | }, Error); 76 | }); 77 | 78 | it('errors when no domain is passed', function testSpec() { 79 | assert.throws(function () { 80 | new ImgixClient({}); 81 | }, Error); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/test-extractURL.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { extractUrl } from '../src/helpers.js'; 3 | 4 | describe('extractURL', () => { 5 | describe('For non-proxy path URLs', () => { 6 | const image = 'https://assets.imgix.net/bridge.jpg?w=100'; 7 | const expectation = { 8 | host: 'assets.imgix.net', 9 | pathname: '/bridge.jpg', 10 | search: '?w=100', 11 | }; 12 | const result = extractUrl({ url: image }); 13 | it('should extract a host from URL', () => { 14 | assert.strictEqual(result.host, expectation.host); 15 | }); 16 | it('should extract a pathname from URL', () => { 17 | assert.strictEqual(result.pathname, expectation.pathname); 18 | }); 19 | it('should extract existing query from URL', () => { 20 | assert.strictEqual(result.search, expectation.search); 21 | }); 22 | }); 23 | describe('For proxy path URLs', () => { 24 | it('should extract a proxy path from full URLs', () => { 25 | const proxyPath = 26 | 'https://assets.imgix.net/https://sdk-test.imgix.net/amsterdam.jpg'; 27 | const result = extractUrl({ url: proxyPath }); 28 | const expectation = { 29 | host: 'assets.imgix.net', 30 | pathname: '/https://sdk-test.imgix.net/amsterdam.jpg', 31 | }; 32 | assert.strictEqual(result.host, expectation.host); 33 | assert.strictEqual(result.pathname, expectation.pathname); 34 | }); 35 | 36 | it('should extract a proxy path from partial URLs', () => { 37 | const proxyPath = 38 | 'assets.imgix.net/https://sdk-test.imgix.net/amsterdam.jpg'; 39 | const result = extractUrl({ url: proxyPath }); 40 | const expectation = { 41 | host: 'assets.imgix.net', 42 | pathname: '/https://sdk-test.imgix.net/amsterdam.jpg', 43 | }; 44 | assert.strictEqual(result.host, expectation.host); 45 | assert.strictEqual(result.pathname, expectation.pathname); 46 | }); 47 | 48 | it('should not modify proxy scheme', () => { 49 | const proxyPath = 50 | 'https://assets.imgix.net/http://sdk-test.imgix.net/amsterdam.jpg'; 51 | const result = extractUrl({ 52 | url: proxyPath, 53 | options: { useHttps: true }, 54 | }); 55 | const expectation = { 56 | host: 'assets.imgix.net', 57 | pathname: '/http://sdk-test.imgix.net/amsterdam.jpg', 58 | }; 59 | assert.strictEqual(result.host, expectation.host); 60 | assert.strictEqual(result.pathname, expectation.pathname); 61 | }); 62 | 63 | it('should prepend source scheme when missing', () => { 64 | const proxyPath = 65 | 'assets.imgix.net/http://sdk-test.imgix.net/amsterdam.jpg'; 66 | const result = extractUrl({ 67 | url: proxyPath, 68 | options: { useHttps: false }, 69 | }); 70 | const expectation = { 71 | protocol: 'http:', 72 | host: 'assets.imgix.net', 73 | pathname: '/http://sdk-test.imgix.net/amsterdam.jpg', 74 | }; 75 | assert.strictEqual(result.protocol, expectation.protocol); 76 | assert.strictEqual(result.host, expectation.host); 77 | assert.strictEqual(result.pathname, expectation.pathname); 78 | }); 79 | 80 | it('should use https for source when defined in options', () => { 81 | const proxyPath = 82 | 'assets.imgix.net/http://sdk-test.imgix.net/amsterdam.jpg'; 83 | const result = extractUrl({ 84 | url: proxyPath, 85 | useHttps: true, 86 | }); 87 | const expectation = { 88 | protocol: 'https:', 89 | host: 'assets.imgix.net', 90 | pathname: '/http://sdk-test.imgix.net/amsterdam.jpg', 91 | }; 92 | assert.strictEqual(result.protocol, expectation.protocol); 93 | assert.strictEqual(result.host, expectation.host); 94 | assert.strictEqual(result.pathname, expectation.pathname); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/test-pathEncoding.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import ImgixClient from '../src/index.js'; 3 | 4 | describe('Path Encoding:', function describeSuite() { 5 | // NOTE: the image urls tested bellow actually resolve to an image. 6 | describe('buildURL', function describeSuite() { 7 | let client; 8 | 9 | beforeEach(function setupClient() { 10 | client = new ImgixClient({ 11 | domain: 'sdk-test.imgix.net', 12 | includeLibraryParam: false, 13 | }); 14 | }); 15 | 16 | it('correctly encodes reserved delimiters', function testSpec() { 17 | const actual = client.buildURL(' <>[]{}|\\^%.jpg', {}); 18 | const expected = 19 | 'https://sdk-test.imgix.net/%20%3C%3E%5B%5D%7B%7D%7C%5C%5E%25.jpg'; 20 | 21 | assert.strictEqual(actual, expected); 22 | }); 23 | 24 | it('correctly encodes reserved characters', function testSpec() { 25 | const actual = client.buildURL('&$+,:;=?@#.jpg', {}); 26 | const expected = 'https://sdk-test.imgix.net/&$%2B,%3A;=%3F@%23.jpg'; 27 | 28 | assert.strictEqual(actual, expected); 29 | }); 30 | 31 | it('correctly encodes UNICODE characters', function testSpec() { 32 | const actual = client.buildURL('/ساندویچ.jpg', {}); 33 | const expected = 34 | 'https://sdk-test.imgix.net/%D8%B3%D8%A7%D9%86%D8%AF%D9%88%DB%8C%DA%86.jpg'; 35 | 36 | assert.strictEqual(actual, expected); 37 | }); 38 | 39 | it('passes through a path unencoded if disablePathEncoding is set', () => { 40 | const actual = client.buildURL( 41 | '/file+with%20some+crazy?things.jpg', 42 | {}, 43 | { 44 | disablePathEncoding: true, 45 | }, 46 | ); 47 | 48 | const expected = 49 | 'https://sdk-test.imgix.net/file+with%20some+crazy?things.jpg'; 50 | assert.strictEqual(actual, expected); 51 | }); 52 | it('prepends / to path when disablePathEncoding is set', () => { 53 | const actual = client.buildURL( 54 | 'file+with%20some+crazy?things.jpg', 55 | {}, 56 | { 57 | disablePathEncoding: true, 58 | }, 59 | ); 60 | 61 | const expected = 62 | 'https://sdk-test.imgix.net/file+with%20some+crazy?things.jpg'; 63 | assert(actual.includes(expected), 'srcset should include expected url'); 64 | }); 65 | it('signs a pre-encoded path correctly', () => { 66 | const client = new ImgixClient({ 67 | domain: 'sdk-test.imgix.net', 68 | secureURLToken: 'abcde1234', 69 | includeLibraryParam: false, 70 | }); 71 | const actual = client.buildURL( 72 | 'file+with%20some+crazy%20things.jpg', 73 | {}, 74 | { 75 | disablePathEncoding: true, 76 | }, 77 | ); 78 | 79 | // The signing param for this URL was taken from a known working implementation 80 | const expected = 81 | 'https://sdk-test.imgix.net/file+with%20some+crazy%20things.jpg?s=4aadfc1a58f27729a41d05831c52116f'; 82 | assert.strictEqual(actual, expected); 83 | }); 84 | }); 85 | describe('buildSrcSet', () => { 86 | let client; 87 | 88 | beforeEach(function setupClient() { 89 | client = new ImgixClient({ 90 | domain: 'sdk-test.imgix.net', 91 | includeLibraryParam: false, 92 | }); 93 | }); 94 | it('passes through a path unencoded for a fixed srcset if disablePathEncoding is set', () => { 95 | const actual = client.buildSrcSet( 96 | '/file+with%20some+crazy?things.jpg', 97 | { 98 | w: 100, 99 | }, 100 | { 101 | disablePathEncoding: true, 102 | }, 103 | ); 104 | 105 | const expected = 106 | 'https://sdk-test.imgix.net/file+with%20some+crazy?things.jpg'; 107 | assert(actual.includes(expected), 'srcset should include expected url'); 108 | }); 109 | it('passes through a path unencoded for a fixed srcset if disablePathEncoding and disableVariableQuality is set', () => { 110 | const actual = client.buildSrcSet( 111 | '/file+with%20some+crazy?things.jpg', 112 | { 113 | w: 100, 114 | }, 115 | { 116 | disablePathEncoding: true, 117 | disableVariableQuality: true, 118 | }, 119 | ); 120 | 121 | const expected = 122 | 'https://sdk-test.imgix.net/file+with%20some+crazy?things.jpg'; 123 | assert(actual.includes(expected), 'srcset should include expected url'); 124 | }); 125 | it('passes through a path unencoded for a fluid srcset if disablePathEncoding is set', () => { 126 | const actual = client.buildSrcSet( 127 | '/file+with%20some+crazy?things.jpg', 128 | {}, 129 | { 130 | disablePathEncoding: true, 131 | }, 132 | ); 133 | 134 | const expected = 135 | 'https://sdk-test.imgix.net/file+with%20some+crazy?things.jpg'; 136 | assert(actual.includes(expected), 'srcset should include expected url'); 137 | }); 138 | it('prepends / to path when disablePathEncoding is set', () => { 139 | const actual = client.buildSrcSet( 140 | 'file+with%20some+crazy?things.jpg', 141 | {}, 142 | { 143 | disablePathEncoding: true, 144 | }, 145 | ); 146 | 147 | const expected = 148 | 'https://sdk-test.imgix.net/file+with%20some+crazy?things.jpg'; 149 | assert(actual.includes(expected), 'srcset should include expected url'); 150 | }); 151 | it('signs a pre-encoded path correctly', () => { 152 | const client = new ImgixClient({ 153 | domain: 'sdk-test.imgix.net', 154 | secureURLToken: 'abcde1234', 155 | includeLibraryParam: false, 156 | }); 157 | const actual = client.buildSrcSet( 158 | 'file+with%20some+crazy%20things.jpg', 159 | {}, 160 | { 161 | disablePathEncoding: true, 162 | }, 163 | ); 164 | 165 | // The signing param for this URL was taken from a known working implementation 166 | const expected = '524748753f8aa52fb41b9359f79c3188'; 167 | const firstURLSignature = new URL( 168 | actual.split(',')[0].split(' ')[0], 169 | ).searchParams.get('s'); 170 | assert.strictEqual(firstURLSignature, expected); 171 | }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /test/test-validators.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import { 4 | validateRange, 5 | validateWidths, 6 | validateWidthTolerance, 7 | validateVariableQuality, 8 | } from '../src/validators.js'; 9 | 10 | describe('Validators:', function () { 11 | describe('Testing validateWidths', function () { 12 | it('throws if a width in width list is negative', () => { 13 | assert.throws(() => { 14 | validateWidths([100, 200, 300, -400]); 15 | }); 16 | }); 17 | 18 | it('throws if given an empty list', () => { 19 | assert.throws(() => { 20 | validateWidths([]); 21 | }); 22 | }); 23 | 24 | it('throws if given a list of non-numeric input', () => { 25 | assert.throws(() => { 26 | validateWidths([100, 200, 300, '400', '500']); 27 | }); 28 | }); 29 | 30 | it('throws if given a list of non-integer input', () => { 31 | assert.throws(() => { 32 | validateWidths([399.99, 499.5]); 33 | }); 34 | }); 35 | 36 | it('does not throw given valid width list', () => { 37 | assert.doesNotThrow(() => { 38 | validateWidths([100, 200, 300, 400, 500]); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('Testing validateRange', function () { 44 | it('throws if minWidth is not an integer', () => { 45 | assert.throws(() => { 46 | validateRange(500.9123, 1000); 47 | }); 48 | }); 49 | 50 | it('throws if maxWidth is not an integer', () => { 51 | assert.throws(() => { 52 | validateRange(100, 500.9123); 53 | }); 54 | }); 55 | 56 | it('throws if minWidth is less than 0', () => { 57 | assert.throws(() => { 58 | validateRange(-1, 100); 59 | }); 60 | }); 61 | 62 | it('throws if maxWidth is less than 0', () => { 63 | assert.throws(() => { 64 | validateRange(100, -1); 65 | }); 66 | }); 67 | 68 | it('throws if maxWidth is less than minWidth', () => { 69 | assert.throws(() => { 70 | validateRange(500, 100); 71 | }); 72 | }); 73 | 74 | it('does not throw given a valid range', () => { 75 | assert.doesNotThrow(() => { 76 | validateRange(100, 8192); 77 | }); 78 | }); 79 | }); 80 | 81 | describe('Testing validateWidthTolerance', function () { 82 | it('throws if widthTolerance is not a number', () => { 83 | assert.throws(() => { 84 | validateWidthTolerance('0.08'); 85 | }); 86 | }); 87 | 88 | it('throws if widthTolerance is < 0.01', () => { 89 | assert.throws(() => { 90 | validateWidthTolerance(0.00999999999); 91 | }); 92 | }); 93 | 94 | it('throws if widthTolerance is less than 0', () => { 95 | assert.throws(() => { 96 | validateWidthTolerance(-3); 97 | }); 98 | }); 99 | 100 | it('does not throw on valid widthTolerance', () => { 101 | assert.doesNotThrow(() => { 102 | validateWidthTolerance(0.08); 103 | }); 104 | }); 105 | 106 | it('does not throw on valid lower bound of 0.01', () => { 107 | assert.doesNotThrow(() => { 108 | validateWidthTolerance(0.01); 109 | }); 110 | }); 111 | 112 | it('does not throw when passed a large value', () => { 113 | assert.doesNotThrow(() => { 114 | validateWidthTolerance(99999999.99); 115 | }); 116 | }); 117 | }); 118 | 119 | describe('Testing validateVariableQuality', function () { 120 | it('throws if variable quality flag is not a boolean', () => { 121 | assert.throws(() => { 122 | validateVariableQuality('false'); 123 | }); 124 | 125 | assert.throws(() => { 126 | validateVariableQuality('true'); 127 | }); 128 | 129 | assert.throws(() => { 130 | validateVariableQuality(0); 131 | }); 132 | 133 | assert.throws(() => { 134 | validateVariableQuality(1); 135 | }); 136 | }); 137 | 138 | it('does not throw when variable quality flag is a boolean', () => { 139 | assert.doesNotThrow(() => { 140 | validateVariableQuality(true); 141 | }); 142 | 143 | assert.doesNotThrow(() => { 144 | validateVariableQuality(false); 145 | }); 146 | }); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "umd" 5 | }, 6 | "files": ["types/index.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare class ImgixClient { 2 | domain: string; 3 | useHTTPS: boolean; 4 | includeLibraryParam: boolean; 5 | secureURLToken: string; 6 | 7 | constructor(opts: { 8 | domain: string; 9 | secureURLToken?: string; 10 | useHTTPS?: boolean; 11 | includeLibraryParam?: boolean; 12 | }); 13 | 14 | buildURL( 15 | path: string, 16 | params?: {}, 17 | options?: { disablePathEncoding?: boolean }, 18 | ): string; 19 | _sanitizePath(path: string, options?: _sanitizePathOptions): string; 20 | _buildParams(params: {}, options?: _buildParamsOptions): string; 21 | _signParams(path: string, queryParams?: {}): string; 22 | buildSrcSet(path: string, params?: {}, options?: SrcSetOptions): string; 23 | _buildSrcSetPairs(path: string, params?: {}, options?: SrcSetOptions): string; 24 | _buildDPRSrcSet(path: string, params?: {}, options?: SrcSetOptions): string; 25 | static targetWidths( 26 | minWidth?: number, 27 | maxWidth?: number, 28 | widthTolerance?: number, 29 | cache?: {}, 30 | ): number[]; 31 | static _buildURL(path: string, params?: {}, options?: {}): string; 32 | static _buildSrcSet( 33 | path: string, 34 | params?: {}, 35 | srcSetOptions?: {}, 36 | clientOptions?: {}, 37 | ): string; 38 | } 39 | 40 | export type DevicePixelRatio = 1 | 2 | 3 | 4 | 5 | number; 41 | 42 | export type VariableQualities = { [key in DevicePixelRatio]?: number }; 43 | 44 | export interface SrcSetOptions { 45 | widths?: number[]; 46 | widthTolerance?: number; 47 | minWidth?: number; 48 | maxWidth?: number; 49 | disableVariableQuality?: boolean; 50 | devicePixelRatios?: DevicePixelRatio[]; 51 | variableQualities?: VariableQualities; 52 | disablePathEncoding?: boolean; 53 | } 54 | 55 | export interface _sanitizePathOptions { 56 | disablePathEncoding?: boolean, 57 | encoder?: (path: string) => string 58 | } 59 | 60 | export interface _buildParamsOptions { 61 | encoder?: (value: string, key?: string) => string 62 | } 63 | 64 | export default ImgixClient; 65 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import ImgixClient, { SrcSetOptions } from '.'; 3 | 4 | const expectedToken = 'MYT0KEN'; 5 | const client = new ImgixClient({ 6 | domain: 'test.imgix.net', 7 | secureURLToken: expectedToken, 8 | }); 9 | expectType(client); 10 | 11 | const path = 'image.jpg'; 12 | 13 | expectType(client.buildURL(path)); 14 | 15 | let params = {}; 16 | params = { w: 100 }; 17 | 18 | expectType(client.buildURL(path, params)); 19 | 20 | const buildURLOptions = { 21 | disablePathEncoding: true, 22 | }; 23 | params = {}; 24 | expectType(client.buildURL('foo/bar/baz', params, buildURLOptions)); 25 | 26 | expectType(client._sanitizePath(path)); 27 | expectType(client._sanitizePath(path, { disablePathEncoding: true, encoder: (path) => path })); 28 | 29 | expectType(client._buildParams(params)); 30 | expectType(client._buildParams(params, { encoder: (value, key) => value })); 31 | 32 | expectType(client._signParams(path, params)); 33 | 34 | const srcsetOptions: SrcSetOptions = { 35 | widths: [100, 500, 1000], 36 | widthTolerance: 0.05, 37 | minWidth: 500, 38 | maxWidth: 2000, 39 | disableVariableQuality: false, 40 | devicePixelRatios: [1, 2], 41 | variableQualities: { 42 | 1: 45, 43 | 2: 30, 44 | }, 45 | disablePathEncoding: true, 46 | }; 47 | 48 | expectType(client.buildSrcSet(path)); 49 | expectType(client.buildSrcSet(path, params)); 50 | expectType(client.buildSrcSet(path, params, srcsetOptions)); 51 | 52 | expectType(client._buildSrcSetPairs(path)); 53 | expectType(client._buildSrcSetPairs(path, params)); 54 | expectType(client._buildSrcSetPairs(path, params, srcsetOptions)); 55 | 56 | expectType(client._buildDPRSrcSet(path)); 57 | expectType(client._buildDPRSrcSet(path, params)); 58 | expectType(client._buildDPRSrcSet(path, params, srcsetOptions)); 59 | 60 | const minWidth = 200; 61 | const maxWidth = 1000; 62 | const widthTol = 0.05; 63 | const cache = {}; 64 | 65 | expectType(ImgixClient.targetWidths()); 66 | expectType(ImgixClient.targetWidths(minWidth)); 67 | expectType(ImgixClient.targetWidths(minWidth, maxWidth)); 68 | expectType(ImgixClient.targetWidths(minWidth, maxWidth, widthTol)); 69 | expectType( 70 | ImgixClient.targetWidths(minWidth, maxWidth, widthTol, cache), 71 | ); 72 | 73 | const absoluteURL = 'https://test.imgix.net/image.jpg'; 74 | 75 | expectType(ImgixClient._buildURL(absoluteURL, params, {})); 76 | expectType(ImgixClient._buildURL(absoluteURL, params, buildURLOptions)); 77 | 78 | expectType(ImgixClient._buildSrcSet(absoluteURL)); 79 | expectType(ImgixClient._buildSrcSet(absoluteURL, params)); 80 | expectType( 81 | ImgixClient._buildSrcSet(absoluteURL, params, srcsetOptions), 82 | ); 83 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6"], 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "strictNullChecks": true, 8 | "strictFunctionTypes": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | 12 | // If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index". 13 | // If the library is global (cannot be imported via `import` or `require`), leave this out. 14 | "baseUrl": "." 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /types/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json", 3 | "rules": { 4 | "semicolon": false, 5 | "indent": [true, "spaces", 2] 6 | } 7 | } 8 | --------------------------------------------------------------------------------