├── .all-contributorsrc ├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── docs ├── LICENSE ├── README.md ├── next-env.d.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── src │ ├── components │ │ ├── CodeBlock │ │ │ ├── CodeBlock.js │ │ │ ├── CodeBlock.module.scss │ │ │ └── index.js │ │ ├── HeaderImage │ │ │ ├── HeaderImage.js │ │ │ ├── HeaderImage.module.scss │ │ │ └── index.js │ │ ├── SchemaTable │ │ │ ├── SchemaTable.tsx │ │ │ └── index.ts │ │ └── Table │ │ │ ├── Table.jsx │ │ │ └── index.ts │ ├── lib │ │ └── util.ts │ ├── pages │ │ ├── _app.mdx │ │ ├── _meta.json │ │ ├── index.mdx │ │ └── url-loader │ │ │ ├── _meta.json │ │ │ ├── analyticsoptions.mdx │ │ │ ├── assetoptions.mdx │ │ │ ├── configoptions.mdx │ │ │ ├── constructcloudinaryurl.mdx │ │ │ ├── imageoptions.mdx │ │ │ ├── installation.mdx │ │ │ └── videooptions.mdx │ └── styles │ │ └── global.css ├── tailwind.config.js ├── theme.config.tsx └── tsconfig.json ├── package.json ├── packages ├── types │ ├── .releaserc │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── types │ │ ├── cloudinary-product-gallery.ts │ │ ├── cloudinary-upload-widget.ts │ │ ├── cloudinary-video-player.ts │ │ ├── configuration.ts │ │ └── resources.ts ├── url-loader │ ├── .releaserc │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── constants │ │ │ ├── parameters.ts │ │ │ └── qualifiers.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── cloudinary.ts │ │ │ ├── plugin.ts │ │ │ ├── transformations.ts │ │ │ ├── upload-widget.ts │ │ │ ├── upload.ts │ │ │ ├── utils.ts │ │ │ └── video-player.ts │ │ ├── plugins │ │ │ ├── abr.ts │ │ │ ├── cropping.ts │ │ │ ├── default-image.ts │ │ │ ├── effects.ts │ │ │ ├── enhance.ts │ │ │ ├── extract.ts │ │ │ ├── fill-background.ts │ │ │ ├── flags.ts │ │ │ ├── named-transformations.ts │ │ │ ├── overlays.ts │ │ │ ├── preserve-transformations.ts │ │ │ ├── raw-transformations.ts │ │ │ ├── recolor.ts │ │ │ ├── remove-background.ts │ │ │ ├── remove.ts │ │ │ ├── replace-background.ts │ │ │ ├── replace.ts │ │ │ ├── restore.ts │ │ │ ├── sanitize.ts │ │ │ ├── seo.ts │ │ │ ├── underlays.ts │ │ │ ├── version.ts │ │ │ └── zoompan.ts │ │ └── types │ │ │ ├── asset.ts │ │ │ ├── image.ts │ │ │ ├── plugins.ts │ │ │ ├── qualifiers.ts │ │ │ └── video.ts │ ├── tests │ │ ├── lib │ │ │ ├── cloudinary.spec.ts │ │ │ ├── upload-widget.spec.ts │ │ │ ├── upload.spec.ts │ │ │ └── video-player.spec.ts │ │ └── plugins │ │ │ ├── abr.spec.ts │ │ │ ├── cropping.spec.ts │ │ │ ├── default.spec.ts │ │ │ ├── effects.spec.ts │ │ │ ├── extract.spec.ts │ │ │ ├── fill-background.spec.ts │ │ │ ├── flags.spec.ts │ │ │ ├── named-transformations.spec.ts │ │ │ ├── overlays.spec.ts │ │ │ ├── preserve-transformations.spec.ts │ │ │ ├── raw-transformations.spec.ts │ │ │ ├── recolor.spec.ts │ │ │ ├── remove-background.spec.ts │ │ │ ├── remove.spec.ts │ │ │ ├── replace-background.spec.ts │ │ │ ├── replace.spec.ts │ │ │ ├── restore.spec.ts │ │ │ ├── sanitize.spec.ts │ │ │ ├── underlays.spec.ts │ │ │ └── zoompan.spec.ts │ └── tsconfig.json └── util │ ├── .releaserc │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── index.ts │ └── lib │ │ ├── cloudinary.ts │ │ ├── colors.ts │ │ └── util.ts │ ├── tests │ └── lib │ │ ├── cloudinary.spec.js │ │ └── colors.spec.js │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── turbo.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitConvention": "angular", 8 | "contributors": [ 9 | { 10 | "login": "colbyfayock", 11 | "name": "Colby Fayock", 12 | "avatar_url": "https://avatars.githubusercontent.com/u/1045274?v=4", 13 | "profile": "https://colbyfayock.com/newsletter", 14 | "contributions": [ 15 | "code", 16 | "doc" 17 | ] 18 | }, 19 | { 20 | "login": "kitravee", 21 | "name": "Trong", 22 | "avatar_url": "https://avatars.githubusercontent.com/u/50334192?v=4", 23 | "profile": "https://github.com/kitravee", 24 | "contributions": [ 25 | "code" 26 | ] 27 | }, 28 | { 29 | "login": "funbunch", 30 | "name": "Shannan Bunch", 31 | "avatar_url": "https://avatars.githubusercontent.com/u/3441200?v=4", 32 | "profile": "http://www.bunchofideas.com", 33 | "contributions": [ 34 | "test" 35 | ] 36 | }, 37 | { 38 | "login": "Insidiae", 39 | "name": "Kobe Ruado", 40 | "avatar_url": "https://avatars.githubusercontent.com/u/28495550?v=4", 41 | "profile": "https://github.com/Insidiae", 42 | "contributions": [ 43 | "code" 44 | ] 45 | }, 46 | { 47 | "login": "michaeljolley", 48 | "name": "Michael Jolley", 49 | "avatar_url": "https://avatars.githubusercontent.com/u/1228996?v=4", 50 | "profile": "https://baldbeardedbuilder.com/", 51 | "contributions": [ 52 | "code" 53 | ] 54 | }, 55 | { 56 | "login": "ssalbdivad", 57 | "name": "David Blass", 58 | "avatar_url": "https://avatars.githubusercontent.com/u/10645823?v=4", 59 | "profile": "https://arktype.io", 60 | "contributions": [ 61 | "code" 62 | ] 63 | }, 64 | { 65 | "login": "nickytonline", 66 | "name": "Nick Taylor", 67 | "avatar_url": "https://avatars.githubusercontent.com/u/833231?v=4", 68 | "profile": "https://oss.fyi/nickytonline", 69 | "contributions": [ 70 | "code" 71 | ] 72 | }, 73 | { 74 | "login": "ghostdevv", 75 | "name": "Willow (GHOST)", 76 | "avatar_url": "https://avatars.githubusercontent.com/u/47755378?v=4", 77 | "profile": "http://ghostdev.xyz", 78 | "contributions": [ 79 | "code" 80 | ] 81 | }, 82 | { 83 | "login": "Yash-sudo-web", 84 | "name": "Yash Mathur", 85 | "avatar_url": "https://avatars.githubusercontent.com/u/69838816?v=4", 86 | "profile": "http://yashmathur.live", 87 | "contributions": [ 88 | "code" 89 | ] 90 | }, 91 | { 92 | "login": "AvaterClasher", 93 | "name": "Soumyadip Moni", 94 | "avatar_url": "https://avatars.githubusercontent.com/u/116944847?v=4", 95 | "profile": "http://soumyadipmoni.vercel.app", 96 | "contributions": [ 97 | "code" 98 | ] 99 | }, 100 | { 101 | "login": "nooras", 102 | "name": "NOORAS FATIMA ANSARI", 103 | "avatar_url": "https://avatars.githubusercontent.com/u/30138146?v=4", 104 | "profile": "https://noorasfatima.netlify.app/", 105 | "contributions": [ 106 | "code" 107 | ] 108 | }, 109 | { 110 | "login": "ayan-joshi", 111 | "name": "Ayan Joshi", 112 | "avatar_url": "https://avatars.githubusercontent.com/u/96243602?v=4", 113 | "profile": "https://github.com/ayan-joshi", 114 | "contributions": [ 115 | "code" 116 | ] 117 | }, 118 | { 119 | "login": "Mrinank-Bhowmick", 120 | "name": "Mrinank Bhowmick", 121 | "avatar_url": "https://avatars.githubusercontent.com/u/77621953?v=4", 122 | "profile": "https://www.mrinank.me", 123 | "contributions": [ 124 | "code" 125 | ] 126 | }, 127 | { 128 | "login": "saai-syvendra", 129 | "name": "Saai Syvendra", 130 | "avatar_url": "https://avatars.githubusercontent.com/u/157691467?v=4", 131 | "profile": "https://github.com/saai-syvendra", 132 | "contributions": [ 133 | "code" 134 | ] 135 | } 136 | ], 137 | "contributorsPerLine": 7, 138 | "skipCi": true, 139 | "repoType": "github", 140 | "repoHost": "https://github.com", 141 | "projectName": "cloudinary-util", 142 | "projectOwner": "cloudinary-community", 143 | "commitType": "docs" 144 | } 145 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { defineConfig } = require("eslint-define-config"); 3 | 4 | module.exports = defineConfig({ 5 | root: true, 6 | parser: "@typescript-eslint/parser", 7 | plugins: ["@typescript-eslint", "import", "only-warn"], 8 | extends: [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:import/typescript", 12 | "prettier", 13 | "next", 14 | ], 15 | ignorePatterns: ["**/dist/**", "**/node_modules/**", "**/*js"], 16 | settings: { 17 | "import/parsers": { 18 | "@typescript-eslint/parser": ["ts", "tsx"], 19 | }, 20 | "import/resolver": { 21 | typescript: true, 22 | node: true, 23 | }, 24 | next: { 25 | rootDir: ["docs/"], 26 | }, 27 | }, 28 | rules: { 29 | "@next/next/no-html-link-for-pages": "off", 30 | "react/jsx-key": "off", 31 | "import/no-cycle": "warn", 32 | "@typescript-eslint/consistent-type-imports": [ 33 | "warn", 34 | { fixStyle: "inline-type-imports" }, 35 | ], 36 | "@typescript-eslint/no-import-type-side-effects": "warn", 37 | "import/no-duplicates": ["warn", { "prefer-inline": true }], 38 | "@typescript-eslint/ban-types": "off", 39 | "@typescript-eslint/no-explicit-any": "off", 40 | "@typescript-eslint/no-non-null-assertion": "off", 41 | "@typescript-eslint/no-namespace": ["warn", { allowDeclarations: true }], 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] " 5 | labels: "Type: Bug" 6 | --- 7 | 8 | # **Bug Report** 9 | 10 | ## **Describe the bug** 11 | 12 | 13 | 14 | ## **Is this a regression?** 15 | 16 | 17 | 18 | 19 | ## **Steps To Reproduce the error** 20 | 21 | 22 | 23 | 1. 24 | 2. 25 | 3. 26 | 4. 27 | 28 | ## **Expected behaviour** 29 | 30 | 31 | 32 | ## **CodeSandbox or Live Example of Bug** 33 | 34 | 35 | 36 | ## **Screenshot or Video Recording** 37 | 38 | 39 | 40 | ### **Your environment** 41 | 42 | 44 | 45 | - OS: 46 | - Node version: 47 | - Npm version: 48 | - Browser name and version: 49 | 50 | ## **Additional context** 51 | 52 | 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature Request" 3 | about: "Suggest an idea or possible new feature for this project." 4 | title: "[Feature] " 5 | labels: "Type: Feature" 6 | --- 7 | 8 | # **Feature Request** 9 | 10 | ## **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | 14 | ## **Describe the solution you'd like** 15 | 16 | 17 | 18 | 19 | ## **Describe alternatives you've considered** 20 | 21 | 22 | 23 | ## **Additional context** 24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | 4 | 5 | ## Issue Ticket Number 6 | 7 | Fixes # 8 | 9 | 10 | 11 | 12 | ## Type of change 13 | 14 | 15 | 16 | - [ ] Bug fix (non-breaking change which fixes an issue) 17 | - [ ] New feature (non-breaking change which adds functionality) 18 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 19 | - [ ] Fix or improve the documentation 20 | - [ ] This change requires a documentation update 21 | 22 | # Checklist 23 | 24 | 25 | 26 | - [ ] I have followed the contributing guidelines of this project as mentioned in [CONTRIBUTING.md](/CONTRIBUTING.md) 27 | - [ ] I have created an [issue](https://github.com/colbyfayock/cloudinary-util/issues) ticket for this PR 28 | - [ ] I have checked to ensure there aren't other open [Pull Requests](https://github.com/colbyfayock/cloudinary-util/pulls) for the same update/change? 29 | - [ ] I have performed a self-review of my own code 30 | - [ ] I have run tests locally to ensure they all pass 31 | - [ ] I have commented my code, particularly in hard-to-understand areas 32 | - [ ] I have made corresponding changes needed to the documentation 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Test & Release 2 | on: [push, pull_request] 3 | 4 | concurrency: ${{ github.workflow }}-${{ github.ref }} 5 | 6 | jobs: 7 | tests: 8 | name: Test 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node: [ '18', '20' ] 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: pnpm/action-setup@v4 17 | with: 18 | version: 9.1.4 19 | 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node }} 23 | 24 | - run: pnpm install 25 | 26 | - run: pnpm build 27 | 28 | - run: pnpm test:packages 29 | 30 | - run: pnpm lint 31 | 32 | - run: pnpm lint:attw 33 | 34 | - run: pnpm lint:publint 35 | release: 36 | name: Release 37 | if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'beta') }} 38 | needs: tests 39 | runs-on: ubuntu-latest 40 | strategy: 41 | matrix: 42 | node: [ '18' ] 43 | steps: 44 | - uses: actions/checkout@v4 45 | 46 | - uses: pnpm/action-setup@v4 47 | with: 48 | version: 9.1.4 49 | 50 | - uses: actions/setup-node@v4 51 | with: 52 | node-version: ${{ matrix.node }} 53 | # https://github.com/pnpm/pnpm/issues/3141 54 | registry-url: 'https://registry.npmjs.org' 55 | 56 | - run: pnpm install 57 | 58 | - run: pnpm release 59 | env: 60 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 62 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .turbo 4 | *.log 5 | .next 6 | dist 7 | dist-ssr 8 | *.local 9 | .env 10 | .cache 11 | server/dist 12 | public/dist 13 | .turbo 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | git-checks=false -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Breaks code blocks in MDX 2 | installation.mdx -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.codeActionsOnSave": [ 4 | "editor.formatOnSave", 5 | "source.fixAll.eslint", 6 | "source.sortImports" 7 | ], 8 | "eslint.codeActionsOnSave.rules": [ 9 | "@typescript-eslint/consistent-type-imports", 10 | "import/no-duplicates", 11 | "@typescript-eslint/no-import-type-side-effects" 12 | ], 13 | "typescript.preferences.preferTypeOnlyAutoImports": true, 14 | "typescript.preferences.autoImportFileExcludePatterns": ["dist"], 15 | "typescript.tsserver.experimental.enableProjectDiagnostics": true 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Colby Fayock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-14-orange.svg?style=flat-square)](#contributors-) 4 | 5 | 6 | 7 | Cloudinary 8 | 9 | 10 | ###### 11 | 12 | GitHub 13 | 14 | # Cloudinary Util 15 | 16 | Libraries intended to help plugin or integration developers have an easier time working with [Cloudinary](https://cloudinary.com/). 17 | 18 | Getting StartedWorking Locally 19 | 20 | **This is a community library supported by the Cloudinary Developer Experience team.** 21 | 22 | ## 🚀 Getting Started 23 | 24 | Learn what tools are available and how to install them: 25 | 26 | - [URL Loader](https://github.com/colbyfayock/cloudinary-util/tree/main/packages/url-loader): Construct a Cloudinary URL given a set of options 27 | - [Util](https://github.com/colbyfayock/cloudinary-util/tree/main/packages/util): Set of functions to help working with Cloudinary 28 | 29 | _The minimum node version officially supported is version 18._ 30 | 31 | ## 🧰 Working Locally with Cloudinary Util 32 | 33 | ### Installation 34 | 35 | - Install all dependencies via [pnpm](https://pnpm.io/): 36 | 37 | ``` 38 | pnpm install 39 | ``` 40 | 41 | ### Development 42 | 43 | - Start a development server for all projects: 44 | 45 | ``` 46 | pnpm dev 47 | ``` 48 | 49 | ### Testing 50 | 51 | - Run all tests: 52 | 53 | ``` 54 | pnpm test 55 | ``` 56 | 57 | ### Building 58 | 59 | - Build all packages: 60 | 61 | ``` 62 | pnpm build 63 | ``` 64 | 65 | ## ✨ Contributors 66 | 67 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 |
Colby Fayock
Colby Fayock

💻 📖
Trong
Trong

💻
Shannan Bunch
Shannan Bunch

⚠️
Kobe Ruado
Kobe Ruado

💻
Michael Jolley
Michael Jolley

💻
David Blass
David Blass

💻
Nick Taylor
Nick Taylor

💻
Willow (GHOST)
Willow (GHOST)

💻
Yash Mathur
Yash Mathur

💻
Soumyadip Moni
Soumyadip Moni

💻
NOORAS FATIMA ANSARI
NOORAS FATIMA ANSARI

💻
Ayan Joshi
Ayan Joshi

💻
Mrinank Bhowmick
Mrinank Bhowmick

💻
Saai Syvendra
Saai Syvendra

💻
94 | 95 | 96 | 97 | 98 | 99 | 100 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 101 | -------------------------------------------------------------------------------- /docs/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Colby Fayock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Getting Started 4 | 5 | 1. Install dependencies using pnpm 6 | 7 | ```shell copy 8 | pnpm install 9 | ``` 10 | 11 | 2. Build Cloudinary Util packages 12 | 13 | This can be done in the root of the project by running: 14 | 15 | ```shell copy 16 | pnpm build 17 | ``` 18 | 19 | Each package under the `packages` directory will be built, where these are then 20 | pulled in automatically to help generate documentation. 21 | 22 | 3. Start development server 23 | 24 | ```shell copy 25 | pnpm dev 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /docs/next.config.js: -------------------------------------------------------------------------------- 1 | const withNextra = require("nextra")({ 2 | theme: "nextra-theme-docs", 3 | themeConfig: "./theme.config.tsx", 4 | }); 5 | 6 | module.exports = withNextra({ 7 | eslint: { 8 | // ESLint behaves weirdly in this monorepo. 9 | ignoreDuringBuilds: true, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudinary-util-docs", 3 | "version": "0.0.1", 4 | "description": "Documentation for Cloudinary Util", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/colbyfayock/cloudinary-util.git" 13 | }, 14 | "author": "Colby Fayock ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/colbyfayock/cloudinary-util/issues" 18 | }, 19 | "homepage": "https://github.com/colbyfayock/cloudinary-util#readme", 20 | "dependencies": { 21 | "@cloudinary-util/url-loader": "workspace:*", 22 | "next": "^14.0.4", 23 | "nextra": "^2.13.2", 24 | "nextra-theme-docs": "^2.13.2", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "sass": "^1.69.7", 28 | "zod": "^3.22.4", 29 | "zod-to-json-schema": "^3.22.3" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "20.10.8", 33 | "autoprefixer": "^10.4.16", 34 | "postcss": "^8.4.33", 35 | "tailwindcss": "^3.4.1", 36 | "typescript": "^5.3.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /docs/src/components/CodeBlock/CodeBlock.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { BsArrowsCollapse, BsArrowsExpand } from "react-icons/bs"; 3 | 4 | import styles from "./CodeBlock.module.scss"; 5 | 6 | export const CodeBlock = ({ children }) => { 7 | const [expanded, setExpanded] = useState(false); 8 | 9 | return ( 10 |
11 |
12 | {children} 13 |
14 |
    15 | {!expanded && ( 16 |
  • 17 | 21 |
  • 22 | )} 23 | {expanded && ( 24 |
  • 25 | 29 |
  • 30 | )} 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default CodeBlock; 37 | -------------------------------------------------------------------------------- /docs/src/components/CodeBlock/CodeBlock.module.scss: -------------------------------------------------------------------------------- 1 | .codeBlock { 2 | position: relative; 3 | margin: 1.5em 0; 4 | } 5 | 6 | .codeBlockCode { 7 | position: relative; 8 | max-height: 12em; 9 | overflow-y: scroll; 10 | 11 | &[data-codeblock-expanded="true"] { 12 | max-height: none; 13 | 14 | &:after { 15 | display: none; 16 | } 17 | } 18 | 19 | &:after { 20 | display: block; 21 | content: ""; 22 | position: sticky; 23 | bottom: 0; 24 | width: 100%; 25 | height: 2em; 26 | background: linear-gradient( 27 | to bottom, 28 | rgba(255, 255, 255, 0), 29 | rgba(255, 255, 255, 1) 30 | ); 31 | 32 | @media (prefers-color-scheme: dark) { 33 | background: linear-gradient( 34 | to bottom, 35 | rgba(#111111, 0), 36 | rgba(#111111, 1) 37 | ); 38 | } 39 | 40 | :global(.dark) & { 41 | background: linear-gradient( 42 | to bottom, 43 | rgba(#111111, 0), 44 | rgba(#111111, 1) 45 | ); 46 | } 47 | } 48 | 49 | pre { 50 | border-radius: 0.2em; 51 | margin-bottom: 0; 52 | } 53 | } 54 | 55 | .codeBlockActions { 56 | display: flex; 57 | justify-content: center; 58 | position: relative; 59 | z-index: 1; 60 | border-radius: 0.2em; 61 | 62 | li { 63 | &:first-child { 64 | margin-left: -0.5em; 65 | } 66 | } 67 | 68 | button { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | gap: 0.5em; 73 | color: #2d69de; 74 | font-size: 0.75em; 75 | padding: 1em; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /docs/src/components/CodeBlock/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./CodeBlock"; 2 | -------------------------------------------------------------------------------- /docs/src/components/HeaderImage/HeaderImage.js: -------------------------------------------------------------------------------- 1 | import styles from "./HeaderImage.module.scss"; 2 | 3 | export const HeaderImage = ({ children, layout }) => { 4 | return ( 5 |
8 |
9 | {children} 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default HeaderImage; 16 | -------------------------------------------------------------------------------- /docs/src/components/HeaderImage/HeaderImage.module.scss: -------------------------------------------------------------------------------- 1 | .headerImage { 2 | width: 100vw; 3 | padding: 1em; 4 | margin-left: -1.5rem; 5 | margin-right: -1.5rem; 6 | margin-top: 1.5rem; 7 | margin-bottom: 1.5rem; 8 | 9 | @media (min-width: 480px) { 10 | width: 100%; 11 | padding: 2em; 12 | border-radius: 0.25em; 13 | margin: 0; 14 | margin-top: 1.5rem; 15 | margin-bottom: 1.5rem; 16 | } 17 | } 18 | 19 | .headerImageChildren { 20 | display: flex; 21 | justify-content: center; 22 | max-width: 500px; 23 | margin: 0 auto; 24 | 25 | &[data-layout="grid"] { 26 | display: grid; 27 | max-width: 100%; 28 | grid-template-columns: 1fr 1fr; 29 | gap: 1em; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/src/components/HeaderImage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./HeaderImage"; 2 | -------------------------------------------------------------------------------- /docs/src/components/SchemaTable/SchemaTable.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { sortByKey } from "../../lib/util"; 4 | 5 | import Table from "@/components/Table"; 6 | 7 | interface Configuration { 8 | anyOf: Array<{ type: string }>; 9 | default: string; 10 | description?: string; 11 | type: string; 12 | } 13 | 14 | interface Property { 15 | name: string; 16 | required: boolean; 17 | types: Array; 18 | defaultValue?: string; 19 | description?: string; 20 | link?: { 21 | label: string; 22 | url: string; 23 | }; 24 | path?: string; 25 | } 26 | 27 | export const SchemaTable = ({ schema, schemaKey }) => { 28 | const { properties, required } = schema.definitions[schemaKey]; 29 | 30 | const formattedProperties = Object.entries(properties).map( 31 | ([name, configuration]: [string, Configuration]) => { 32 | const { type, anyOf, default: defaultValue } = configuration; 33 | 34 | const types = Array.isArray(type) ? type : [type]; 35 | 36 | if (anyOf) { 37 | anyOf 38 | .filter(({ type }) => !!type) 39 | .forEach((ao) => { 40 | types.push(ao.type); 41 | }); 42 | } 43 | 44 | const description = 45 | configuration?.description && JSON.parse(configuration.description); 46 | const { path, text, url } = description || {}; 47 | 48 | const property: Property = { 49 | name, 50 | path, 51 | required: required.includes(name), 52 | types, 53 | defaultValue, 54 | description: text, 55 | link: url && { 56 | label: "More Info", 57 | url, 58 | }, 59 | }; 60 | 61 | return property; 62 | }, 63 | ); 64 | 65 | const sortedProperties = sortByKey(formattedProperties, "name"); 66 | 67 | return ( 68 | { 95 | return { 96 | default: prop.defaultValue || "-", 97 | description: prop.description, 98 | more: prop.link?.url 99 | ? () => {prop.link.label} 100 | : "", 101 | property: () => { 102 | if (prop.path) { 103 | return {prop.name}; 104 | } 105 | return prop.name; 106 | }, 107 | required: prop.required ? "Yes" : "-", 108 | type: 109 | prop.types && prop.types.filter((v) => !!v).length > 0 110 | ? prop.types.join(" | ") 111 | : "-", 112 | }; 113 | })} 114 | /> 115 | ); 116 | }; 117 | 118 | export default SchemaTable; 119 | -------------------------------------------------------------------------------- /docs/src/components/SchemaTable/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./SchemaTable"; 2 | -------------------------------------------------------------------------------- /docs/src/components/Table/Table.jsx: -------------------------------------------------------------------------------- 1 | const Table = ({ columns, data }) => { 2 | return ( 3 |
4 | {Array.isArray(columns) && ( 5 | 6 | 7 | {columns.map(({ title, id }) => { 8 | return ( 9 | 16 | ); 17 | })} 18 | 19 | 20 | )} 21 | {Array.isArray(data) && ( 22 | 23 | {data.map((row, index) => { 24 | return ( 25 | 29 | {columns.map(({ id }, index) => { 30 | let Child = row[id] || " "; 31 | 32 | if (typeof Child === "function") { 33 | Child = ; 34 | } 35 | 36 | if (index === 0) { 37 | return ( 38 | 45 | ); 46 | } 47 | return ( 48 | 51 | ); 52 | })} 53 | 54 | ); 55 | })} 56 | 57 | )} 58 |
14 | {title || " "} 15 |
43 | {Child} 44 | 49 | {Child} 50 |
59 | ); 60 | }; 61 | 62 | export default Table; 63 | -------------------------------------------------------------------------------- /docs/src/components/Table/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Table"; 2 | -------------------------------------------------------------------------------- /docs/src/lib/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * sortByKey 3 | * @description Sort the given array by the key of an object 4 | */ 5 | 6 | export function sortByKey( 7 | array: Array = [], 8 | key: string, 9 | type: string = "asc", 10 | ) { 11 | function compare(a: object, b: object) { 12 | let keyA = a[key]; 13 | let keyB = b[key]; 14 | 15 | if (typeof keyA === "string") { 16 | keyA = keyA.toLowerCase(); 17 | } 18 | 19 | if (typeof keyB === "string") { 20 | keyB = keyB.toLowerCase(); 21 | } 22 | 23 | if (keyA < keyB) { 24 | return -1; 25 | } 26 | 27 | if (keyA > keyB) { 28 | return 1; 29 | } 30 | 31 | return 0; 32 | } 33 | 34 | let newArray = [...array]; 35 | 36 | if (typeof key !== "string") return newArray; 37 | 38 | newArray = newArray.sort(compare); 39 | 40 | if (type === "desc") { 41 | return newArray.reverse(); 42 | } 43 | 44 | return newArray; 45 | } 46 | -------------------------------------------------------------------------------- /docs/src/pages/_app.mdx: -------------------------------------------------------------------------------- 1 | import "nextra-theme-docs/style.css"; 2 | import "../styles/global.css"; 3 | 4 | export default function Nextra({ Component, pageProps }) { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /docs/src/pages/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": { 3 | "title": "Getting Started", 4 | "display": "hidden", 5 | "theme": { 6 | "breadcrumb": false, 7 | "pagination": false, 8 | "toc": false 9 | } 10 | }, 11 | "url-loader": "URL Loader" 12 | } 13 | -------------------------------------------------------------------------------- /docs/src/pages/index.mdx: -------------------------------------------------------------------------------- 1 | # Cloudinary Util 2 | 3 | Cloudinary Util is a collection of libraries intended to help plugin or integration developers have an easier time working with Cloudinary. 4 | 5 | It's based off of the idea of providing a more intuitive way to express transformations largely revolving around props-based APIs. 6 | 7 | ## Util Libraries 8 | 9 | The current makeup of the Util libraries includes: 10 | 11 | - URL Loader: primarily a single function that given a set of props, returns a Cloudinary URL 12 | - Util: helpers for working with Cloudinary systems 13 | 14 | The constructCloudinaryUrl function is designed to provide a consistent API for building URLs. 15 | 16 | It's original design intent was to create a bridge between various frameworks such 17 | as [Next.js](https://next.cloudinary.dev/), [Nuxt](https://cloudinary.nuxtjs.org/), 18 | and [Svelte](https://svelte.cloudinary.dev/) to more easily create framework-first implementations 19 | with the same experience between the projects. 20 | 21 | In upcoming builds, similar functions will be created to allow similarly consistent props-based 22 | designs for widget wrappers such as CldUploadWidget, CldVideoPlayer, and more. 23 | -------------------------------------------------------------------------------- /docs/src/pages/url-loader/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "installation": { 3 | "title": "Installation", 4 | "theme": { 5 | "breadcrumb": false 6 | } 7 | }, 8 | "sep-api": { 9 | "type": "separator", 10 | "title": "API" 11 | }, 12 | "constructcloudinaryurl": "constructCloudinaryUrl", 13 | "sep-types": { 14 | "type": "separator", 15 | "title": "Types" 16 | }, 17 | "analyticsoptions": "AnalyticsOptions", 18 | "assetoptions": "AssetOptions", 19 | "configoptions": "ConfigOptions", 20 | "imageoptions": "ImageOptions", 21 | "videooptions": "VideoOptions" 22 | } -------------------------------------------------------------------------------- /docs/src/pages/url-loader/analyticsoptions.mdx: -------------------------------------------------------------------------------- 1 | # AnalyticsOptions 2 | 3 | The analytics options made available in [constructCloudinaryUrl](/constructcloudinaryurl) are inherited from the Cloudinary URL Gen SDK. 4 | 5 | Types and more information [can be found on GitHub](https://github.com/cloudinary/js-url-gen/blob/8103b542dac284d0b72ec5227d2537453c1ee53d/src/sdkAnalytics/interfaces/IAnalyticsOptions.ts). 6 | -------------------------------------------------------------------------------- /docs/src/pages/url-loader/assetoptions.mdx: -------------------------------------------------------------------------------- 1 | import { assetOptionsSchema } from "@cloudinary-util/url-loader/schema"; 2 | import { zodToJsonSchema } from "zod-to-json-schema"; 3 | 4 | import SchemaTable from "@/components/SchemaTable"; 5 | 6 | # AssetOptions 7 | 8 | Asset Options define the base properties that are shared across all image and video assets. 9 | 10 | The majority of options are defined as part of this core set, but a few type-specific definitions are made available distinctly on the [ImageOptions](/imageoptions) and [VideoOptions](/videooptions) Types. 11 | 12 | ## Properties 13 | 14 | 18 | -------------------------------------------------------------------------------- /docs/src/pages/url-loader/configoptions.mdx: -------------------------------------------------------------------------------- 1 | # ConfigOptions 2 | 3 | The config options made available in [constructCloudinaryUrl](/constructcloudinaryurl) are inherited from the Cloudinary URL Gen SDK. 4 | 5 | Types and more information [can be found on GitHub](https://github.com/cloudinary/js-url-gen/blob/master/src/config/interfaces/Config/ICloudinaryConfigurations.ts). 6 | -------------------------------------------------------------------------------- /docs/src/pages/url-loader/constructcloudinaryurl.mdx: -------------------------------------------------------------------------------- 1 | import { constructUrlPropsSchema } from "@cloudinary-util/url-loader/schema"; 2 | import { zodToJsonSchema } from "zod-to-json-schema"; 3 | 4 | import SchemaTable from "@/components/SchemaTable"; 5 | 6 | # constructCloudinaryUrl 7 | 8 | ## Properties 9 | 10 | 14 | -------------------------------------------------------------------------------- /docs/src/pages/url-loader/imageoptions.mdx: -------------------------------------------------------------------------------- 1 | import { imageOptionsSchema } from "@cloudinary-util/url-loader/schema"; 2 | import { zodToJsonSchema } from "zod-to-json-schema"; 3 | 4 | import SchemaTable from "@/components/SchemaTable"; 5 | 6 | # ImageOptions 7 | 8 | The ImageOptions type is an extension of the [AssetOptions](/assetoptions) type, adding Image-specific properties. 9 | 10 | ## Properties 11 | 12 | 16 | -------------------------------------------------------------------------------- /docs/src/pages/url-loader/installation.mdx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Callout, Steps, Tab, Tabs } from 'nextra-theme-docs'; 3 | 4 | 5 | Installation - Next Cloudinary 6 | 7 | 8 | 9 | 10 | # Installing URL Loader 11 | 12 | ## Getting Started 13 | 14 | 15 | 16 | ### Installation 17 | 18 | 19 | 20 | ```ansi copy 21 | npm install @cloudinary-util/url-loader 22 | ``` 23 | 24 | 25 | ```ansi copy 26 | pnpm install @cloudinary-util/url-loader 27 | ``` 28 | 29 | 30 | ```ansi copy 31 | yarn add @cloudinary-util/url-loader 32 | ``` 33 | 34 | 35 | 36 | ### Import the Dependency 37 | 38 | ```jsx copy 39 | import { constructCloudinaryUrl } from '@cloudinary-util/url-loader'; 40 | ``` 41 | 42 | ### Create a Cloudinary URL 43 | 44 | ```jsx copy 45 | const url = constructCloudinaryUrl({ 46 | options: { 47 | src: 'my-public-id', 48 | width: 800, 49 | height: 600 50 | }, 51 | config: { 52 | cloud: { 53 | cloudName: 'my-cloud' 54 | } 55 | } 56 | }); 57 | ``` 58 | 59 | 60 | Don't have a Cloudinary account? Sign up for free on cloudinary.com! 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /docs/src/pages/url-loader/videooptions.mdx: -------------------------------------------------------------------------------- 1 | import { videoOptionsSchema } from "@cloudinary-util/url-loader/schema"; 2 | import { zodToJsonSchema } from "zod-to-json-schema"; 3 | 4 | import SchemaTable from "@/components/SchemaTable"; 5 | 6 | # VideoOptions 7 | 8 | The VideoOptions type is an extension of the [AssetOptions](/assetoptions) type, adding Video-specific properties. 9 | 10 | ## Properties 11 | 12 | 16 | -------------------------------------------------------------------------------- /docs/src/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class", 'html[class~="dark"]'], 4 | content: [ 5 | "./app/**/*.{js,ts,jsx,tsx,md,mdx}", 6 | "./pages/**/*.{js,ts,jsx,tsx,md,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,md,mdx}", 8 | 9 | // Or if using `src` directory: 10 | "./src/**/*.{js,ts,jsx,tsx,md,mdx}", 11 | ], 12 | theme: { 13 | extend: {}, 14 | }, 15 | plugins: [], 16 | }; 17 | -------------------------------------------------------------------------------- /docs/theme.config.tsx: -------------------------------------------------------------------------------- 1 | import type { DocsThemeConfig } from "nextra-theme-docs"; 2 | import React from "react"; 3 | 4 | const config: DocsThemeConfig = { 5 | logo: Cloudinary Util, 6 | project: { 7 | link: "https://github.com/colbyfayock/cloudinary-util", 8 | }, 9 | docsRepositoryBase: 10 | "https://github.com/colbyfayock/cloudinary-util/tree/main/docs/", 11 | footer: { 12 | text: "Colby Fayock", 13 | }, 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "allowJs": true, 5 | "strict": false, 6 | "exactOptionalPropertyTypes": false, 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "jsx": "preserve", 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["./src/*"] 13 | }, 14 | "lib": ["dom", "dom.iterable", "esnext"], 15 | "skipLibCheck": true, 16 | "noEmit": true, 17 | "incremental": true, 18 | "esModuleInterop": true, 19 | "resolveJsonModule": true, 20 | "isolatedModules": true 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "turbo run build", 5 | "dev": "turbo run dev --no-cache --continue", 6 | "lint": "eslint --max-warnings=0 . && turbo run lint", 7 | "lint:attw": "turbo run lint:attw", 8 | "lint:publint": "turbo run lint:publint", 9 | "clean": "turbo run clean && rm -rf node_modules", 10 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 11 | "release": "turbo run build --filter=docs^... && turbo run semantic-release --concurrency=1", 12 | "test": "turbo run build && turbo run test", 13 | "test:packages": "turbo run build --filter='./packages/*' && turbo run test --filter='./packages/*'" 14 | }, 15 | "devDependencies": { 16 | "@arethetypeswrong/cli": "^0.15.3", 17 | "@arktype/fs": "^0.0.20", 18 | "@babel/core": "^7.20.12", 19 | "@babel/preset-env": "^7.20.2", 20 | "@colbyfayock/semantic-release-pnpm": "^1.0.7", 21 | "@semantic-release/changelog": "^6.0.2", 22 | "@semantic-release/git": "^10.0.1", 23 | "@typescript-eslint/eslint-plugin": "^7.18.0", 24 | "@typescript-eslint/parser": "^7.18.0", 25 | "eslint": "^8.57.0", 26 | "eslint-config-next": "^14.2.4", 27 | "eslint-config-prettier": "^9.1.0", 28 | "eslint-define-config": "^2.1.0", 29 | "eslint-plugin-import": "^2.29.1", 30 | "eslint-plugin-only-warn": "^1.1.0", 31 | "prettier": "^3.3.2", 32 | "publint": "^0.2.9", 33 | "semantic-release": "^20.1.0", 34 | "semantic-release-monorepo": "^7.0.5", 35 | "tsup": "^8.1.0", 36 | "tsx": "^4.16.0", 37 | "turbo": "^1.7.2", 38 | "typescript": "^5.5.2", 39 | "vitest": "^2.0.5" 40 | }, 41 | "packageManager": "pnpm@9.1.4" 42 | } 43 | -------------------------------------------------------------------------------- /packages/types/.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | "next", 5 | "next-major", 6 | { 7 | "name": "beta", 8 | "prerelease": true 9 | }, 10 | { 11 | "name": "alpha", 12 | "prerelease": true 13 | } 14 | ], 15 | "plugins": [ 16 | [ 17 | "@semantic-release/commit-analyzer", 18 | { 19 | "preset": "angular", 20 | "releaseRules": [ 21 | { 22 | "type": "docs", 23 | "scope": "README", 24 | "release": "patch" 25 | } 26 | ], 27 | "parserOpts": { 28 | "noteKeywords": [ 29 | "BREAKING CHANGE", 30 | "BREAKING CHANGES" 31 | ] 32 | } 33 | } 34 | ], 35 | "@semantic-release/release-notes-generator", 36 | [ 37 | "@semantic-release/changelog", 38 | { 39 | "changelogFile": "CHANGELOG.md" 40 | } 41 | ], 42 | [ 43 | "@colbyfayock/semantic-release-pnpm", 44 | { 45 | "publishBranch": "main|beta" 46 | } 47 | ], 48 | [ 49 | "@semantic-release/git", 50 | { 51 | "assets": [ 52 | "./package.json", 53 | "CHANGELOG.md" 54 | ] 55 | } 56 | ], 57 | "@semantic-release/github" 58 | ], 59 | "extends": "semantic-release-monorepo" 60 | } 61 | -------------------------------------------------------------------------------- /packages/types/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cloudinary 5 | 6 | 7 | ###### 8 | 9 | npm GitHub 10 | 11 | # Cloudinary Types 12 | 13 | **This is a community library supported by the Cloudinary Developer Experience team.** 14 | 15 | ## 🚀 Getting Started 16 | 17 | _The minimum node version officially supported is version 18._ 18 | 19 | ``` 20 | npm install @cloudinary-util/types 21 | ``` 22 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudinary-util/types", 3 | "version": "1.6.0", 4 | "type": "module", 5 | "main": "./dist/index.cjs", 6 | "types": "./dist/index.d.cts", 7 | "exports": { 8 | ".": { 9 | "require": "./dist/index.cjs", 10 | "import": "./dist/index.js" 11 | } 12 | }, 13 | "license": "MIT", 14 | "files": [ 15 | "dist/**" 16 | ], 17 | "scripts": { 18 | "build": "tsup src/index.ts --format esm,cjs --dts --clean", 19 | "prepublishOnly": "pnpm build", 20 | "semantic-release": "semantic-release" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/cloudinary-community/cloudinary-util.git" 28 | }, 29 | "keywords": [], 30 | "bugs": { 31 | "url": "https://github.com/cloudinary-community/cloudinary-util/issues" 32 | }, 33 | "homepage": "https://github.com/cloudinary-community/cloudinary-util" 34 | } 35 | -------------------------------------------------------------------------------- /packages/types/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | CloudinaryResource, 3 | CloudinaryResourceAccessMode, 4 | CloudinaryResourceContext, 5 | CloudinaryResourceDeliveryType, 6 | CloudinaryResourceResourceType, 7 | } from "./types/resources.js"; 8 | 9 | export type { 10 | CloudinaryCreateUploadWidget, 11 | CloudinaryUploadWidget, 12 | CloudinaryUploadWidgetError, 13 | CloudinaryUploadWidgetInfo, 14 | CloudinaryUploadWidgetInstanceMethodCloseOptions, 15 | CloudinaryUploadWidgetInstanceMethodDestroyOptions, 16 | CloudinaryUploadWidgetInstanceMethodOpenOptions, 17 | CloudinaryUploadWidgetInstanceMethodUpdateOptions, 18 | CloudinaryUploadWidgetInstanceMethods, 19 | CloudinaryUploadWidgetOptions, 20 | CloudinaryUploadWidgetResults, 21 | CloudinaryUploadWidgetSources, 22 | } from "./types/cloudinary-upload-widget.js"; 23 | 24 | export type { 25 | CloudinaryVideoPlayer, 26 | CloudinaryVideoPlayerOptionPosterOptions, 27 | CloudinaryVideoPlayerOptions, 28 | CloudinaryVideoPlayerOptionsColors, 29 | CloudinaryVideoPlayerOptionsLogo, 30 | CloudinaryVideoPlayerPlaylistByTagOptions, 31 | CloudinaryVideoPlayerPlaylistOptions, 32 | CloudinaryVideoPlayerTextTracks, 33 | CloudinaryVideoPlayerTextTracksTrack, 34 | CloudinaryVideoPlayerTextTracksTrackOptions, 35 | CloudinaryVideoPlayerTextTracksTrackOptionsBox, 36 | CloudinaryVideoPlayerTextTracksTrackOptionsGravity, 37 | CloudinaryVideoPlayerTextTracksTrackOptionsTheme, 38 | } from "./types/cloudinary-video-player.js"; 39 | 40 | export type { CloudinaryProductGallery } from "./types/cloudinary-product-gallery.js"; 41 | 42 | export type { 43 | CloudinaryAssetConfiguration, 44 | CloudinaryAssetConfigurationAuthToken, 45 | CloudinaryAssetConfigurationCloud, 46 | CloudinaryAssetConfigurationUrl, 47 | } from "./types/configuration.js"; 48 | -------------------------------------------------------------------------------- /packages/types/src/types/cloudinary-product-gallery.ts: -------------------------------------------------------------------------------- 1 | export interface CloudinaryProductGallery { 2 | // Required parameters 3 | cloudName?: string; 4 | mediaAssets?: 5 | | { 6 | publicId?: string; 7 | tag?: string; 8 | mediaType?: string; 9 | resourceType?: string; 10 | transformation?: object; 11 | thumbnailTransformation?: object; 12 | altText?: string; 13 | videoPlayerSource?: object; 14 | }[] 15 | | string[]; // string[] is a list of publicIDs 16 | container?: string | HTMLElement; 17 | 18 | // Widget 19 | analytics?: boolean; 20 | displayProps?: { 21 | mode?: "classic" | "expanded"; 22 | spacing?: number; 23 | columns?: number; 24 | topOffset?: number; 25 | bottomOffset?: number; 26 | }; 27 | focus?: boolean; 28 | loaderProps?: { 29 | color?: string; 30 | opacity?: number; 31 | style?: "cloudinary" | "circle" | "custom"; 32 | url?: string; 33 | }; 34 | placeholderImage?: boolean; 35 | sort?: "none" | "asc" | "desc"; 36 | sortProps?: { 37 | source?: string; 38 | id?: string; 39 | direction?: string; 40 | }; 41 | themeProps?: { 42 | primary?: string; // Default: "#FFFFFF" 43 | onPrimary?: string; // Default: "#000000" 44 | active?: string; // Default: "#0078FF" 45 | }; 46 | viewportBreakpoints?: { 47 | breakpoint: number; // Required 48 | [key: string]: any; // Other configuration parameters to override 49 | }[]; 50 | 51 | // Main viewer parameters 52 | accessibilityProps?: { 53 | mediaAltSource?: string; 54 | mediaAltId?: string; 55 | }; 56 | ar3dProps?: { 57 | shadows?: boolean; 58 | showAR?: boolean; 59 | }; 60 | aspectRatio?: 61 | | "square" 62 | | "1:1" 63 | | "3:4" 64 | | "4:3" 65 | | "4:6" 66 | | "6:4" 67 | | "5:7" 68 | | "7:5" 69 | | "5:8" 70 | | "8:5" 71 | | "9:16" 72 | | "16:9"; 73 | borderColor?: string; 74 | borderWidth?: number; 75 | imageBreakpoint?: number; 76 | videoBreakpoint?: number; 77 | preload?: string[]; 78 | radius?: number; 79 | spinProps?: { 80 | animate?: "none" | "start" | "end" | "both"; 81 | spinDirection?: "clockwise" | "counter-clockwise"; 82 | disableZoom?: boolean; 83 | showTip?: "always" | "never" | "touch"; 84 | tipPosition?: "top" | "center" | "bottom"; 85 | tipText?: string; // Default: "Drag to rotate" 86 | tipTouchText?: string; // Default: "Swipe to rotate" 87 | }; 88 | startIndex?: number; 89 | tipProps?: { 90 | textColor?: string; 91 | color?: string; 92 | radius?: number; 93 | opacity?: number; 94 | }; 95 | transition?: "slide" | "fade" | "none"; 96 | videoProps?: { 97 | controls?: string; 98 | sound?: boolean; 99 | autoplay?: boolean; 100 | loop?: boolean; 101 | playerType?: string; 102 | }; 103 | zoom?: boolean; 104 | zoomProps?: any; 105 | zoomPopupProps?: { 106 | backdropColor?: string; 107 | backdropOpacity?: number; 108 | zIndex?: number; 109 | }; 110 | 111 | // Carousel parameters 112 | carouselLocation?: "left" | "right" | "top" | "bottom"; 113 | carouselOffset?: number; 114 | carouselStyle?: "none" | "thumbnails" | "indicators"; 115 | indicatorProps?: { 116 | color?: string; 117 | selectedColor?: string; 118 | shape?: "round" | "square" | "radius"; 119 | size?: number; 120 | spacing?: number; 121 | sticky?: boolean; 122 | }; 123 | thumbnailProps?: any; 124 | 125 | // Navigation parameters 126 | navigation?: "none" | "always" | "mouseover"; 127 | navigationButtonProps?: { 128 | shape?: "none" | "round" | "square" | "radius" | "rectangle"; 129 | iconColor?: string; 130 | color?: string; 131 | size?: number; 132 | }; 133 | navigationOffset?: number; 134 | navigationPosition?: "inside" | "outside" | "offset"; 135 | } 136 | -------------------------------------------------------------------------------- /packages/types/src/types/cloudinary-upload-widget.ts: -------------------------------------------------------------------------------- 1 | import type { CloudinaryResource } from "./resources.js"; 2 | 3 | // Sourced from: https://cloudinary.com/documentation/upload_widget_reference 4 | 5 | type CustomURL = `https://${string}.${string}`; 6 | 7 | export interface CloudinaryUploadWidgetOptions { 8 | // Configuration 9 | 10 | apiKey?: string; 11 | cloudName?: string; 12 | uploadPreset?: string; 13 | 14 | // Widget 15 | 16 | encryption?: { 17 | key: string; 18 | iv: string; 19 | }; 20 | defaultSource?: string; 21 | maxFiles?: number; 22 | multiple?: boolean; 23 | sources?: Array< 24 | | "camera" 25 | | "dropbox" 26 | | "facebook" 27 | | "gettyimages" 28 | | "google_drive" 29 | | "image_search" 30 | | "instagram" 31 | | "istock" 32 | | "local" 33 | | "shutterstock" 34 | | "unsplash" 35 | | "url" 36 | >; 37 | 38 | // Cropping 39 | 40 | cropping?: boolean; 41 | croppingAspectRatio?: number; 42 | croppingCoordinatesMode?: string; 43 | croppingDefaultSelectionRatio?: number; 44 | croppingShowBackButton?: boolean; 45 | croppingShowDimensions?: boolean; 46 | showSkipCropButton?: boolean; 47 | 48 | // Sources 49 | 50 | dropboxAppKey?: string; 51 | facebookAppId?: string; 52 | googleApiKey?: string; 53 | googleDriveClientId?: string; 54 | instagramClientId?: string; 55 | searchByRights?: boolean; 56 | searchBySites?: Array; 57 | 58 | // Upload 59 | 60 | context?: object; 61 | folder?: string; 62 | publicId?: string; 63 | resourceType?: string; 64 | tags?: Array; 65 | uploadSignature?: string | Function; 66 | uploadSignatureTimestamp?: number; 67 | 68 | // Client Side 69 | 70 | clientAllowedFormats?: Array; 71 | croppingValidateDimensions?: boolean; 72 | maxChunkSize?: number; 73 | maxImageFileSize?: number; 74 | maxImageHeight?: number; 75 | maxImageWidth?: number; 76 | maxFileSize?: number; 77 | maxRawFileSize?: number; 78 | maxVideoFileSize?: number; 79 | minImageHeight?: number; 80 | minImageWidth?: number; 81 | validateMaxWidthHeight?: boolean; 82 | 83 | // Containing Page 84 | 85 | fieldName?: string; 86 | form?: string; 87 | thumbnails?: string; 88 | thumbnailTransformation?: string | Array; 89 | 90 | // Customization 91 | 92 | buttonCaption?: string; 93 | buttonClass?: string; 94 | text?: object; 95 | theme?: string; 96 | styles?: object; 97 | 98 | // Advanced 99 | 100 | autoMinimize?: boolean; 101 | detection?: string; 102 | getTags?: Function; 103 | getUploadPresets?: Function; 104 | inlineContainer?: any; // string or DOM element 105 | language?: string; 106 | on_success?: string; 107 | preBatch?: Function; 108 | prepareUploadParams?: Function; 109 | queueViewPosition?: string; 110 | showAdvancedOptions?: boolean; 111 | showCompletedButton?: boolean; 112 | showInsecurePreview?: boolean; 113 | showPoweredBy?: boolean; 114 | showUploadMoreButton?: boolean; 115 | singleUploadAutoClose?: boolean; 116 | } 117 | 118 | // Results 119 | 120 | export interface CloudinaryUploadWidgetResults { 121 | event?: string; 122 | info?: string | CloudinaryUploadWidgetInfo; 123 | } 124 | 125 | export interface CloudinaryUploadWidgetInfo extends CloudinaryResource { 126 | api_key: string; 127 | batchId: string; 128 | etag: string; 129 | hook_execution: Record; 130 | id: string; 131 | original_filename: string; 132 | path: string; 133 | thumbnail_url: string; 134 | } 135 | 136 | // Instance Methods 137 | 138 | export interface CloudinaryUploadWidgetInstanceMethods { 139 | close: (options?: CloudinaryUploadWidgetInstanceMethodCloseOptions) => void; 140 | destroy: ( 141 | options?: CloudinaryUploadWidgetInstanceMethodDestroyOptions, 142 | ) => Promise; 143 | hide: () => void; 144 | isDestroyed: () => boolean; 145 | isMinimized: () => boolean; 146 | isShowing: () => boolean; 147 | minimize: () => void; 148 | open: ( 149 | widgetSource?: CloudinaryUploadWidgetSources, 150 | options?: CloudinaryUploadWidgetInstanceMethodOpenOptions, 151 | ) => void; 152 | show: () => void; 153 | update: (options: CloudinaryUploadWidgetInstanceMethodUpdateOptions) => void; 154 | } 155 | 156 | export type CloudinaryUploadWidgetInstanceMethodCloseOptions = { 157 | quiet: boolean; 158 | }; 159 | 160 | export type CloudinaryUploadWidgetInstanceMethodDestroyOptions = { 161 | removeThumbnails: boolean; 162 | }; 163 | 164 | export type CloudinaryUploadWidgetInstanceMethodOpenOptions = { 165 | files: CustomURL[]; 166 | }; 167 | 168 | export type CloudinaryUploadWidgetInstanceMethodUpdateOptions = Omit< 169 | CloudinaryUploadWidgetOptions, 170 | | "secure" 171 | | "uploadSignature" 172 | | "getTags" 173 | | "preBatch" 174 | | "inlineContainer" 175 | | "fieldName" 176 | > & { 177 | cloudName: string; 178 | uploadPreset: string; 179 | }; 180 | 181 | export type CloudinaryUploadWidgetSources = 182 | | "local" 183 | | "url" 184 | | "camera" 185 | | "image_search" 186 | | "google_drive" 187 | | "dropbox" 188 | | "facebook" 189 | | "instagram" 190 | | "shutterstock" 191 | | "getty" 192 | | "istock" 193 | | "unsplash" 194 | | null; 195 | 196 | // Errors 197 | 198 | export type CloudinaryUploadWidgetError = 199 | | { 200 | status: string; 201 | statusText: string; 202 | } 203 | | string 204 | | null; 205 | 206 | /** 207 | * A Cloudinary Upload Widget instance. 208 | * @see https://cloudinary.com/documentation/upload_widget 209 | */ 210 | export type CloudinaryUploadWidget = CloudinaryUploadWidgetInstanceMethods; 211 | 212 | /** 213 | * This type represents the `window.cloudinary.createUploadWidget` function. 214 | * @see https://cloudinary.com/documentation/upload_widget#how_to_set_up_and_integrate_the_upload_widget_into_your_site_or_app 215 | */ 216 | export type CloudinaryCreateUploadWidget = ( 217 | options: CloudinaryUploadWidgetOptions, 218 | callback: ( 219 | error: CloudinaryUploadWidgetError | null, 220 | results: CloudinaryUploadWidgetResults, 221 | ) => void, 222 | ) => CloudinaryUploadWidget; 223 | -------------------------------------------------------------------------------- /packages/types/src/types/configuration.ts: -------------------------------------------------------------------------------- 1 | // From @cloudinary/url-gen/config/interfaces/Config/ICloudinaryCloudinaryAssetConfigurations 2 | 3 | export interface CloudinaryAssetConfigurationAuthToken { 4 | token_name: string; 5 | duration: string; 6 | start_time: string; 7 | expiration: string; 8 | ip: string; 9 | acl: string; 10 | url: string; 11 | key: string; 12 | } 13 | 14 | export interface CloudinaryAssetConfigurationUrl { 15 | cname?: string; 16 | secureDistribution?: string; 17 | privateCdn?: boolean; 18 | secure?: boolean; 19 | analytics?: boolean; 20 | signUrl?: boolean; 21 | longUrlSignature?: boolean; 22 | shorten?: boolean; 23 | useRootPath?: boolean; 24 | forceVersion?: boolean; 25 | queryParams?: Record | string; 26 | } 27 | 28 | export interface CloudinaryAssetConfigurationCloud { 29 | cloudName?: string; 30 | apiKey?: string; 31 | apiSecret?: string; 32 | authToken?: CloudinaryAssetConfigurationAuthToken; 33 | } 34 | 35 | export interface CloudinaryAssetConfiguration { 36 | cloud?: CloudinaryAssetConfigurationCloud; 37 | url?: CloudinaryAssetConfigurationUrl; 38 | } 39 | -------------------------------------------------------------------------------- /packages/types/src/types/resources.ts: -------------------------------------------------------------------------------- 1 | export type CloudinaryResourceAccessMode = 2 | | "public" 3 | | "authenticated" 4 | | (string & {}); 5 | export type CloudinaryResourceResourceType = 6 | | "image" 7 | | "video" 8 | | "raw" 9 | | "auto" 10 | | (string & {}); 11 | export type CloudinaryResourceDeliveryType = 12 | | "animoto" 13 | | "asset" 14 | | "authenticated" 15 | | "dailymotion" 16 | | "facebook" 17 | | "fetch" 18 | | "gravatar" 19 | | "hulu" 20 | | "instagram" 21 | | "list" 22 | | "multi" 23 | | "private" 24 | | "text" 25 | | "twitter" 26 | | "twitter_name" 27 | | "upload" 28 | | "vimeo" 29 | | "worldstarhiphop" 30 | | "youtube" 31 | | (string & {}); 32 | 33 | export interface CloudinaryResourceContext { 34 | custom?: { 35 | alt?: string; 36 | caption?: string; 37 | [key: string]: unknown; 38 | }; 39 | [key: string]: unknown; 40 | } 41 | 42 | export interface CloudinaryResource { 43 | access_control?: Array; 44 | access_mode?: CloudinaryResourceAccessMode; 45 | asset_id: string; 46 | backup?: boolean; 47 | bytes: number; 48 | context?: CloudinaryResourceContext; 49 | colors?: [string, number][]; 50 | coordinates?: object; 51 | created_at: string; 52 | derived?: Array; 53 | display_name?: string; 54 | exif?: object; 55 | faces?: number[][]; 56 | folder: string; 57 | format: string; 58 | height: number; 59 | image_metadata?: object; 60 | info?: object; 61 | media_metadata?: object; 62 | metadata?: object; 63 | moderation?: object | Array; 64 | pages?: number; 65 | phash?: string; 66 | placeholder?: boolean; 67 | predominant?: object; 68 | public_id: string; 69 | quality_analysis?: number; 70 | resource_type: CloudinaryResourceResourceType; 71 | secure_url: string; 72 | signature?: string; 73 | tags?: Array; 74 | type: CloudinaryResourceDeliveryType; 75 | url: string; 76 | version: number; 77 | width: number; 78 | [key: string]: unknown; 79 | } 80 | -------------------------------------------------------------------------------- /packages/url-loader/.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | "next", 5 | "next-major", 6 | { 7 | "name": "beta", 8 | "prerelease": true 9 | }, 10 | { 11 | "name": "alpha", 12 | "prerelease": true 13 | } 14 | ], 15 | "plugins": [ 16 | [ 17 | "@semantic-release/commit-analyzer", 18 | { 19 | "preset": "angular", 20 | "releaseRules": [ 21 | { 22 | "type": "docs", 23 | "scope": "README", 24 | "release": "patch" 25 | } 26 | ], 27 | "parserOpts": { 28 | "noteKeywords": [ 29 | "BREAKING CHANGE", 30 | "BREAKING CHANGES" 31 | ] 32 | } 33 | } 34 | ], 35 | "@semantic-release/release-notes-generator", 36 | [ 37 | "@semantic-release/changelog", 38 | { 39 | "changelogFile": "CHANGELOG.md" 40 | } 41 | ], 42 | [ 43 | "@colbyfayock/semantic-release-pnpm", 44 | { 45 | "publishBranch": "main|beta" 46 | } 47 | ], 48 | [ 49 | "@semantic-release/git", 50 | { 51 | "assets": [ 52 | "./package.json", 53 | "CHANGELOG.md" 54 | ] 55 | } 56 | ], 57 | "@semantic-release/github" 58 | ], 59 | "extends": "semantic-release-monorepo" 60 | } 61 | -------------------------------------------------------------------------------- /packages/url-loader/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cloudinary 5 | 6 | 7 | ###### 8 | 9 | npm GitHub 10 | 11 | # Cloudinary URL Loader 12 | 13 | A function to construct a Cloudinary URL based on a set of options. 14 | 15 | The loader works by loading a list of "plugins" which provide option-based configuration for features like optimization, cropping, and background removal. 16 | 17 | Getting Started 18 | 19 | **This is a community library supported by the Cloudinary Developer Experience team.** 20 | 21 | ## 🚀 Getting Started 22 | 23 | _The minimum node version officially supported is version 18._ 24 | 25 | - Install Cloudinary URL Loader: 26 | 27 | ``` 28 | npm install @cloudinary-util/url-loader 29 | ``` 30 | 31 | - Import the dependency: 32 | 33 | ``` 34 | import { constructCloudinaryUrl } from '@cloudinary-util/url-loader'; 35 | ``` 36 | 37 | - Create a Cloudinary URL: 38 | 39 | ``` 40 | const url = constructCloudinaryUrl({ 41 | options: { 42 | src: 'my-public-id', 43 | width: 800, 44 | height: 600 45 | }, 46 | config: { 47 | cloud: { 48 | cloudName: 'my-cloud' 49 | } 50 | } 51 | }); 52 | ``` 53 | -------------------------------------------------------------------------------- /packages/url-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudinary-util/url-loader", 3 | "version": "6.0.0", 4 | "type": "module", 5 | "main": "./dist/index.cjs", 6 | "types": "./dist/index.d.cts", 7 | "exports": { 8 | ".": { 9 | "require": "./dist/index.cjs", 10 | "import": "./dist/index.js" 11 | } 12 | }, 13 | "sideEffects": false, 14 | "license": "MIT", 15 | "files": [ 16 | "dist/**" 17 | ], 18 | "scripts": { 19 | "build": "pnpm tsup src --format esm,cjs --dts --clean --no-splitting", 20 | "dev": "tsup src/index.ts src/schema.ts --format esm,cjs --watch --dts", 21 | "lint": "TIMING=1 eslint \"src/**/*.ts*\"", 22 | "lint:attw": "attw --pack . --exclude-entrypoints \"schema\"", 23 | "lint:publint": "publint", 24 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", 25 | "prepublishOnly": "cd ../../ && pnpm build && cd packages/url-loader", 26 | "semantic-release": "semantic-release", 27 | "test": "vitest run", 28 | "test:watch": "vitest" 29 | }, 30 | "dependencies": { 31 | "@cloudinary-util/types": "workspace:*", 32 | "@cloudinary-util/util": "workspace:*", 33 | "@cloudinary/url-gen": "1.15.0" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^17.0.12" 37 | }, 38 | "publishConfig": { 39 | "access": "public" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/cloudinary-community/cloudinary-util.git" 44 | }, 45 | "keywords": [], 46 | "bugs": { 47 | "url": "https://github.com/cloudinary-community/cloudinary-util/issues" 48 | }, 49 | "homepage": "https://github.com/cloudinary-community/cloudinary-util" 50 | } 51 | -------------------------------------------------------------------------------- /packages/url-loader/src/constants/parameters.ts: -------------------------------------------------------------------------------- 1 | import type { StringifiablePrimative } from "../lib/utils.js"; 2 | 3 | /** 4 | * @description Mode to use when cropping an asset. 5 | * @url https://cloudinary.com/documentation/transformation_reference#c_crop_resize 6 | */ 7 | export type CropMode = 8 | | "auto" 9 | | "crop" 10 | | "fill" 11 | | "fill_pad" 12 | | "fit" 13 | | "imagga_crop" 14 | | "imagga_scale" 15 | | "lfill" 16 | | "limit" 17 | | "lpad" 18 | | "mfit" 19 | | "mpad" 20 | | "pad" 21 | | "scale" 22 | | "thumb"; 23 | 24 | /** 25 | * @description Whether to keep the content of the extracted area, or to replace it with a mask. 26 | * @url https://cloudinary.com/documentation/transformation_reference#e_extract 27 | */ 28 | export type ExtractMode = "content" | "mask"; 29 | 30 | /** 31 | * @description Rotates or flips an asset by the specified number of degrees or automatically according to its orientation or available metadata. 32 | * @url https://cloudinary.com/documentation/transformation_reference#a_angle 33 | */ 34 | export type Angle = string | number; 35 | 36 | /** Aspect Ratio */ 37 | 38 | export type AspectRatioMode = 39 | | "vflip" 40 | | "hflip" 41 | | "ignore" 42 | | "auto_right" 43 | | "auto_left"; 44 | 45 | /** 46 | * @description Crops or resizes the asset to a new aspect ratio. 47 | * @url https://cloudinary.com/documentation/transformation_reference#ar_aspect_ratio 48 | */ 49 | export type AspectRatio = AspectRatioMode | number | (string & {}); 50 | 51 | export type Flag = 52 | | "animated" 53 | | "any_format" 54 | | "apng" 55 | | "attachment" 56 | | "awebp" 57 | | "clip" 58 | | "clip_evenodd" 59 | | "cutter" 60 | | "force_icc" 61 | | "force_strip" 62 | | "getinfo" 63 | | "group4" 64 | | "hlsv3" 65 | | "ignore_aspect_ratio" 66 | | "ignore_mask_channels" 67 | | "immutable_cache" 68 | | "keep_attribution" 69 | | "keep_dar" 70 | | "keep_iptc" 71 | | "layer_apply" 72 | | "lossy" 73 | | "mono" 74 | | "no_overflow" 75 | | "no_stream" 76 | | "png8_fl_png24_fl_png32" 77 | | "preserve_transparency" 78 | | "progressive" 79 | | "rasterize" 80 | | "region_relative" 81 | | "relative" 82 | | "replace_image" 83 | | "sanitize" 84 | | "splice" 85 | | "streaming_attachment" 86 | | "strip_profile" 87 | | "text_disallow_overflow" 88 | | "text_no_trim" 89 | | "tiff8_lzw" 90 | | "tiled" 91 | | "truncate_ts" 92 | | "waveform"; 93 | 94 | /** 95 | * @description Alters the regular behavior of another transformation or the overall delivery behavior. 96 | * @url https://cloudinary.com/documentation/transformation_reference#fl_flag 97 | * @qualifier fl 98 | */ 99 | export type FlagsDefinition = ListableFlags | FlagRecord; 100 | 101 | export type ListableFlags = Flag | ReadonlyArray; 102 | 103 | export type FlagRecord = Partial>; 104 | 105 | /** 106 | * @description Converts (if necessary) and delivers an asset in the specified format regardless of the file extension used in the delivery URL. 107 | * @url https://cloudinary.com/documentation/transformation_reference#f_format 108 | * @qualifier f 109 | */ 110 | export type Format = 111 | | "auto" 112 | | "auto:image" 113 | | "auto:animated" 114 | | "gif" 115 | | "png" 116 | | "jpg" 117 | | "bmp" 118 | | "ico" 119 | | "pdf" 120 | | "tiff" 121 | | "eps" 122 | | "jpc" 123 | | "jp2" 124 | | "psd" 125 | | "webp" 126 | | "zip" 127 | | "svg" 128 | | "webm" 129 | | "wdp" 130 | | "hpx" 131 | | "djvu" 132 | | "ai" 133 | | "flif" 134 | | "bpg" 135 | | "miff" 136 | | "tga" 137 | | "heic" 138 | | "default" // library specific feature to turn off automatic optimization 139 | | (string & {}); 140 | 141 | /** 142 | * @description Determines which part of an asset to focus on. Note: Default of auto is applied for supported crop modes only. 143 | * @url https://cloudinary.com/documentation/transformation_reference#g_gravity 144 | */ 145 | export type Gravity = 146 | | "auto" 147 | | "auto_content_aware" 148 | | "center" 149 | | "custom" 150 | | "east" 151 | | "face" 152 | | "face_center" 153 | | "multi_face" 154 | | "north" 155 | | "north_east" 156 | | "north_west" 157 | | "south" 158 | | "south_east" 159 | | "south_west" 160 | | "west" 161 | | "xy_center" 162 | | "face:center" 163 | | "face:auto" 164 | | "faces" 165 | | "faces:center" 166 | | "faces:auto" 167 | | "body" 168 | | "body:face" 169 | | "adv_face" 170 | | "adv_faces" 171 | | "adv_eyes" 172 | | "custom:face" 173 | | "custom:faces" 174 | | "custom:adv_face" 175 | | "custom:adv_faces" 176 | | "auto:adv_face" 177 | | "auto:adv_faces" 178 | | "auto:adv_eyes" 179 | | "auto:body" 180 | | "auto:face" 181 | | "auto:faces" 182 | | "auto:custom_no_override" 183 | | "auto:none" 184 | | "liquid" 185 | | "ocr_text" 186 | | (string & {}); 187 | 188 | /** 189 | * @description A qualifier that determines the height of a transformed asset or an overlay. 190 | * @url https://cloudinary.com/documentation/transformation_reference#h_height 191 | */ 192 | export type Height = number | string; 193 | 194 | /** 195 | * @description Should generative AI features detect multiple instances. 196 | */ 197 | export type Multiple = boolean; 198 | 199 | /** 200 | * @description Natural language descriptions used for generative AI capabilities. 201 | */ 202 | export type ListablePrompts = string | ReadonlyArray; 203 | 204 | /** 205 | * @description A qualifier that sets the desired width of an asset using a specified value, or automatically based on the available width. 206 | * @url https://cloudinary.com/documentation/transformation_reference#w_width 207 | */ 208 | export type Width = number | string; 209 | 210 | /** 211 | * @description Adjusts the starting location or offset of the x axis. 212 | * @url https://cloudinary.com/documentation/transformation_reference#x_y_coordinates 213 | */ 214 | export type X = number | string; 215 | 216 | /** 217 | * @description Adjusts the starting location or offset of the y axis. 218 | * @url https://cloudinary.com/documentation/transformation_reference#x_y_coordinates 219 | */ 220 | export type Y = number | string; 221 | 222 | /** Zoom */ 223 | 224 | /** 225 | * @description Controls how close to crop to the detected coordinates when using face-detection, custom-coordinate, or object-specific gravity. 226 | * @url https://cloudinary.com/documentation/transformation_reference#z_zoom 227 | */ 228 | export type Zoom = number | string; 229 | 230 | // this was originally called PositionOptions but it conflicts with a 231 | // DOM type, leading to confusing results if e.g. the import is deleted, 232 | // the type falls back to the global 233 | export interface PositionalOptions { 234 | angle?: Angle; 235 | gravity?: Gravity; 236 | x?: X; 237 | y?: Y; 238 | } 239 | -------------------------------------------------------------------------------- /packages/url-loader/src/index.ts: -------------------------------------------------------------------------------- 1 | // URL Construction & Plugins 2 | 3 | export { 4 | cloudinaryPluginKeys, 5 | cloudinaryPluginProps, 6 | constructCloudinaryUrl, 7 | transformationPlugins, 8 | type AnalyticsOptions, 9 | type ConfigOptions, 10 | type ConstructUrlProps 11 | } from "./lib/cloudinary.js"; 12 | 13 | // Upload Widget 14 | 15 | export { 16 | UPLOAD_WIDGET_EVENTS, 17 | generateUploadWidgetResultCallback, 18 | getUploadWidgetOptions, 19 | type CloudinaryUploadWidgetErrorCallback, 20 | type CloudinaryUploadWidgetResultCallback, 21 | type GenerateUploadWidgetResultCallback, 22 | type GetUploadWidgetOptions 23 | } from "./lib/upload-widget.js"; 24 | 25 | // Upload Helpers 26 | 27 | export { 28 | generateSignatureCallback, 29 | type GenerateSignatureCallback 30 | } from "./lib/upload.js"; 31 | 32 | // Video Player 33 | 34 | export { 35 | getVideoPlayerOptions, 36 | type GetVideoPlayerOptions, 37 | type GetVideoPlayerOptionsLogo 38 | } from "./lib/video-player.js"; 39 | 40 | // Transformation definitions 41 | 42 | export { 43 | effects, 44 | position as position, 45 | primary as primary, 46 | text 47 | } from "./constants/qualifiers.js"; 48 | 49 | // General Types 50 | 51 | export type { AssetOptions } from "./types/asset.js"; 52 | export type { ImageOptions } from "./types/image.js"; 53 | export type { PluginOptions, PluginResults } from "./types/plugins.js"; 54 | export type { 55 | QualifierConfig as Qualifier, 56 | QualifierConverters 57 | } from "./types/qualifiers.js"; 58 | export type { VideoOptions } from "./types/video.js"; 59 | 60 | -------------------------------------------------------------------------------- /packages/url-loader/src/lib/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { AssetOptions, SupportedAssetTypeInput } from "../types/asset.js"; 2 | import type { ImageOptions } from "../types/image.js"; 3 | import type { PluginResults } from "../types/plugins.js"; 4 | import type { VideoOptions } from "../types/video.js"; 5 | import type { CldAsset } from "./cloudinary.js"; 6 | 7 | export interface AllOptions extends AssetOptions, ImageOptions, VideoOptions {} 8 | 9 | export type CloudinaryKey = keyof AllOptions & {}; 10 | 11 | export type SupportedAssetType = "image" | "video" | "all"; 12 | 13 | export type OptionName = keyof AllOptions; 14 | 15 | export type ApplyWhen = OptionName | ((opts: AllOptions) => boolean); 16 | 17 | export type AlwaysApply = () => true; 18 | 19 | export interface PluginDefinition< 20 | assetType extends SupportedAssetType, 21 | name extends string, 22 | options extends object, 23 | alwaysApply extends boolean, 24 | > { 25 | name: name; 26 | supports: assetType; 27 | apply: PluginApplicationDefinition; 28 | inferOwnOptions: options; 29 | props: Record; 30 | alwaysApply?: alwaysApply; 31 | strict?: boolean; 32 | } 33 | 34 | export interface TransformationPlugin< 35 | assetType extends SupportedAssetType = SupportedAssetType, 36 | name extends string = string, 37 | opts extends object = object, 38 | alwaysApply extends boolean = boolean, 39 | > { 40 | name: name; 41 | supports: assetType; 42 | apply: PluginApplication; 43 | inferOwnOptions: opts; 44 | props: Record; 45 | alwaysApply: alwaysApply; 46 | strict?: boolean; 47 | } 48 | 49 | export type OwnOptionsParam< 50 | opts extends object, 51 | alwaysApply extends boolean, 52 | > = opts & 53 | // if there's only one owned key, we know it must be present if 54 | // apply is being invoked, so require it so we don't have to recheck 55 | // in the implementation unless alwaysApply is true 56 | ([alwaysApply] extends [true] 57 | ? {} 58 | : { [k in singleKeyOf]: Exclude }); 59 | 60 | export type CtxParam = 61 | assetType extends "all" 62 | ? AllOptions 63 | : assetType extends "video" | "videos" 64 | ? VideoOptions 65 | : ImageOptions; 66 | 67 | // extract the key if there is exactly one, otherwise never 68 | type singleKeyOf = { 69 | [k in keyof opts]: keyof opts extends k ? k : never; 70 | }[keyof opts]; 71 | 72 | export type PluginApplicationDefinition< 73 | assetType extends SupportedAssetType, 74 | opts extends object, 75 | alwaysApply extends boolean, 76 | > = ( 77 | cldAsset: CldAsset, 78 | /** Options owned by this plugin */ 79 | opts: OwnOptionsParam, 80 | ctx: CtxParam, 81 | ) => PluginResults; 82 | 83 | export type PluginApplication< 84 | assetType extends SupportedAssetType, 85 | opts extends object, 86 | alwaysApply extends boolean, 87 | > = ( 88 | cldAsset: CldAsset, 89 | // externally, we want the wider assetType options as well 90 | opts: OwnOptionsParam & CtxParam, 91 | ) => PluginResults; 92 | 93 | export const plugin = < 94 | asset extends SupportedAssetType, 95 | name extends string, 96 | opts extends object, 97 | alwaysApply extends boolean, 98 | >( 99 | def: PluginDefinition, 100 | ): TransformationPlugin => 101 | ({ 102 | strict: false, 103 | alwaysApply: false, 104 | ...def, 105 | apply: (cldAsset, ctx) => def.apply(cldAsset, ctx as never, ctx as never), 106 | }) satisfies TransformationPlugin as never; 107 | -------------------------------------------------------------------------------- /packages/url-loader/src/lib/transformations.ts: -------------------------------------------------------------------------------- 1 | import type { QualifierConverters } from "../types/qualifiers.js"; 2 | 3 | /** 4 | * constructTransformation 5 | * @description Constructs a transformation string to append to a URL 6 | * @param {object} settings: Configuration including prefix, qualifier, and value 7 | */ 8 | 9 | export interface ConstructTransformationSettings { 10 | prefix?: string; 11 | qualifier?: string | boolean; 12 | value?: string | number | boolean; 13 | converters?: ReadonlyArray; 14 | } 15 | 16 | export function constructTransformation({ 17 | prefix, 18 | qualifier, 19 | value, 20 | converters, 21 | }: ConstructTransformationSettings) { 22 | let transformation = ""; 23 | 24 | if (prefix) { 25 | transformation = `${prefix}_`; 26 | } 27 | 28 | let transformationValue = value; 29 | 30 | converters?.forEach(({ test, convert }) => { 31 | if (!test(transformationValue)) return; 32 | transformationValue = convert(transformationValue); 33 | }); 34 | 35 | if (transformationValue === true || transformationValue === "true") { 36 | return `${transformation}${qualifier}`; 37 | } 38 | 39 | if ( 40 | typeof transformationValue === "string" || 41 | typeof transformationValue === "number" 42 | ) { 43 | if (prefix) { 44 | return `${transformation}${qualifier}:${transformationValue}`; 45 | } else { 46 | return `${qualifier}_${transformationValue}`; 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * promptArrayToString 53 | */ 54 | 55 | export function promptArrayToString(promptArray: ReadonlyArray) { 56 | return `(${promptArray.join(";")})`; 57 | } 58 | -------------------------------------------------------------------------------- /packages/url-loader/src/lib/upload-widget.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CloudinaryAssetConfiguration, 3 | CloudinaryUploadWidgetError, 4 | CloudinaryUploadWidgetOptions, 5 | CloudinaryUploadWidgetResults, 6 | } from "@cloudinary-util/types"; 7 | 8 | /** 9 | * getUploadWidgetOptions 10 | */ 11 | 12 | export interface GetUploadWidgetOptions extends CloudinaryUploadWidgetOptions { 13 | uploadSignature?: CloudinaryUploadWidgetOptions["uploadSignature"]; 14 | } 15 | 16 | export function getUploadWidgetOptions( 17 | { uploadSignature, ...options }: GetUploadWidgetOptions, 18 | config: CloudinaryAssetConfiguration, 19 | ) { 20 | const signed = typeof uploadSignature === "function"; 21 | 22 | // When creating a signed upload, you need to provide both your Cloudinary API Key 23 | // as well as a signature generator function that will sign any paramters 24 | // either on page load or during the upload process. Read more about signed uploads at: 25 | // https://cloudinary.com/documentation/upload_widget#signed_uploads 26 | 27 | const { cloudName, apiKey } = config?.cloud || {}; 28 | 29 | if (!cloudName) { 30 | throw new Error( 31 | "A Cloudinary Cloud name is required, please make sure your environment variable is set and configured in your environment.", 32 | ); 33 | } 34 | 35 | if (signed && !apiKey) { 36 | throw new Error( 37 | "A Cloudinary API Key is required for signed requests, please make sure your environment variable is set and configured in your environment.", 38 | ); 39 | } 40 | 41 | if (!signed && !options.uploadPreset) { 42 | throw new Error( 43 | "A Cloudinary Upload Preset is required for unsigned uploads. Please specify an uploadPreset or configure signed uploads.", 44 | ); 45 | } 46 | 47 | const uploadOptions: CloudinaryUploadWidgetOptions = { 48 | cloudName, 49 | apiKey, 50 | ...options, 51 | }; 52 | 53 | if (signed) { 54 | uploadOptions.uploadSignature = uploadSignature; 55 | } 56 | 57 | return uploadOptions; 58 | } 59 | 60 | /** 61 | * generateUploadWidgetResultCallback 62 | */ 63 | 64 | export type CloudinaryUploadWidgetResultCallback = ( 65 | results: CloudinaryUploadWidgetResults, 66 | ) => void; 67 | export type CloudinaryUploadWidgetErrorCallback = ( 68 | error: CloudinaryUploadWidgetError, 69 | results: CloudinaryUploadWidgetResults, 70 | ) => void; 71 | 72 | export interface GenerateUploadWidgetResultCallback { 73 | onOpen?: CloudinaryUploadWidgetResultCallback; 74 | /** 75 | * @deprecated use onSuccess instead 76 | */ 77 | onUpload?: CloudinaryUploadWidgetResultCallback; 78 | onAbort?: CloudinaryUploadWidgetResultCallback; 79 | onBatchCancelled?: CloudinaryUploadWidgetResultCallback; 80 | onClose?: CloudinaryUploadWidgetResultCallback; 81 | onDisplayChanged?: CloudinaryUploadWidgetResultCallback; 82 | onPublicId?: CloudinaryUploadWidgetResultCallback; 83 | onQueuesEnd?: CloudinaryUploadWidgetResultCallback; 84 | onQueuesStart?: CloudinaryUploadWidgetResultCallback; 85 | onRetry?: CloudinaryUploadWidgetResultCallback; 86 | onShowCompleted?: CloudinaryUploadWidgetResultCallback; 87 | onSourceChanged?: CloudinaryUploadWidgetResultCallback; 88 | onSuccess?: CloudinaryUploadWidgetResultCallback; 89 | onTags?: CloudinaryUploadWidgetResultCallback; 90 | onUploadAdded?: CloudinaryUploadWidgetResultCallback; 91 | onError: CloudinaryUploadWidgetErrorCallback; 92 | onResult: CloudinaryUploadWidgetResultCallback; 93 | } 94 | 95 | export const UPLOAD_WIDGET_EVENTS: { [key: string]: string } = { 96 | abort: "onAbort", 97 | "batch-cancelled": "onBatchCancelled", 98 | close: "onClose", 99 | "display-changed": "onDisplayChanged", 100 | publicid: "onPublicId", 101 | "queues-end": "onQueuesEnd", 102 | "queues-start": "onQueuesStart", 103 | retry: "onRetry", 104 | "show-completed": "onShowCompleted", 105 | "source-changed": "onSourceChanged", 106 | success: "onSuccess", 107 | tags: "onTags", 108 | "upload-added": "onUploadAdded", 109 | }; 110 | 111 | export function generateUploadWidgetResultCallback( 112 | options: GenerateUploadWidgetResultCallback, 113 | ) { 114 | return function resultCallback( 115 | error: CloudinaryUploadWidgetError, 116 | uploadResult: CloudinaryUploadWidgetResults, 117 | ) { 118 | if (error) { 119 | if (typeof options.onError === "function") { 120 | options.onError(error, uploadResult); 121 | } 122 | } 123 | 124 | if (typeof options.onResult === "function") { 125 | options.onResult(uploadResult); 126 | } 127 | 128 | const widgetEvent = 129 | typeof uploadResult?.event === "string" && 130 | (UPLOAD_WIDGET_EVENTS[uploadResult.event] as keyof typeof options); 131 | 132 | if ( 133 | typeof widgetEvent === "string" && 134 | typeof options[widgetEvent] === "function" 135 | ) { 136 | const callback = options[ 137 | widgetEvent 138 | ] as CloudinaryUploadWidgetResultCallback; 139 | callback(uploadResult); 140 | } 141 | }; 142 | } 143 | -------------------------------------------------------------------------------- /packages/url-loader/src/lib/upload.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * generateSignature 3 | * @description Makes a request to an endpoint to sign Cloudinary parameters as part of widget creation 4 | */ 5 | 6 | export interface GenerateSignatureCallback { 7 | fetch: Function; 8 | signatureEndpoint: string; 9 | } 10 | 11 | export function generateSignatureCallback({ 12 | signatureEndpoint, 13 | fetch: fetcher, 14 | }: GenerateSignatureCallback) { 15 | return function generateSignature( 16 | callback: (signature: string | null, error?: unknown) => void, 17 | paramsToSign: object, 18 | ) { 19 | if (typeof signatureEndpoint === "undefined") { 20 | throw Error( 21 | "Failed to generate signature: signatureEndpoint property undefined.", 22 | ); 23 | } 24 | 25 | if (typeof fetcher === "undefined") { 26 | throw Error("Failed to generate signature: fetch property undefined."); 27 | } 28 | 29 | fetcher(signatureEndpoint, { 30 | method: "POST", 31 | body: JSON.stringify({ 32 | paramsToSign, 33 | }), 34 | headers: { 35 | "Content-Type": "application/json", 36 | }, 37 | }) 38 | .then((response: { json: Function }) => response.json()) 39 | .then((result: { signature: string }) => { 40 | callback(result.signature); 41 | }) 42 | .catch((error: unknown) => { 43 | callback(null, error); 44 | }); 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /packages/url-loader/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | /** Safer alternative to Array.isArray that... 2 | * - doesn't narrow to any[] 3 | * - works with readonly arrays 4 | */ 5 | export const isArray: (data: unknown) => data is ReadonlyArray = 6 | Array.isArray; 7 | 8 | /** 9 | * extracts entries mimicking Object.entries, accounting for whether the 10 | * object is an array 11 | */ 12 | export type entryOf = { 13 | [k in keyof o]-?: [k, o[k] & ({} | null)]; 14 | }[o extends ReadonlyArray ? keyof o & number : keyof o] & 15 | unknown; 16 | 17 | /** 18 | * Object.entries wrapper providing narrowed types for objects with known sets 19 | * of keys, e.g. those defined internally as configs 20 | * 21 | * @param o the object to get narrowed entries from 22 | * @returns a narrowed array of entries based on that object's type 23 | */ 24 | export const entriesOf: (o: o) => entryOf[] = 25 | Object.entries as never; 26 | 27 | /** 28 | * Throws an error with the specified message and constructor. 29 | * 30 | * @param {string} message - The error message. 31 | * @param {new (message: string) => Error} [ctor=Error] - The error constructor. Defaults to the built-in Error constructor. 32 | * @throws {Error} Throws an error with the specified message. 33 | */ 34 | export const throwError: ( 35 | message: string, 36 | ctor?: new (message: string) => Error, 37 | ) => never = (message, ctor = Error) => { 38 | throw new ctor(message); 39 | }; 40 | 41 | /** 42 | * @description A primative value that can be interpolated into a string 43 | */ 44 | export type StringifiablePrimative = string | number | bigint | boolean; 45 | -------------------------------------------------------------------------------- /packages/url-loader/src/lib/video-player.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CloudinaryAssetConfiguration, 3 | CloudinaryVideoPlayerOptions, 4 | CloudinaryVideoPlayerOptionsLogo, 5 | } from "@cloudinary-util/types"; 6 | import { parseUrl } from "@cloudinary-util/util"; 7 | import { 8 | constructCloudinaryUrl, 9 | type ConstructUrlProps, 10 | } from "./cloudinary.js"; 11 | import { isArray } from "./utils.js"; 12 | 13 | /** 14 | * getVideoPlayerOptions 15 | */ 16 | 17 | export type GetVideoPlayerOptions = Omit< 18 | CloudinaryVideoPlayerOptions, 19 | | "cloud_name" 20 | | "autoplayMode" 21 | | "publicId" 22 | | "secure" 23 | | "showLogo" 24 | | "logoImageUrl" 25 | | "logoOnclickUrl" 26 | > & { 27 | logo?: boolean | GetVideoPlayerOptionsLogo; 28 | poster?: string | Partial; 29 | src: string; 30 | quality?: string | number; 31 | }; 32 | 33 | export interface GetVideoPlayerOptionsLogo { 34 | imageUrl?: CloudinaryVideoPlayerOptionsLogo["logoImageUrl"]; 35 | logo?: boolean; 36 | onClickUrl?: CloudinaryVideoPlayerOptionsLogo["logoOnclickUrl"]; 37 | } 38 | 39 | export function getVideoPlayerOptions( 40 | options: GetVideoPlayerOptions, 41 | config: CloudinaryAssetConfiguration, 42 | ) { 43 | const { 44 | autoplay, 45 | controls = true, 46 | language, 47 | languages, 48 | logo = true, 49 | loop = false, 50 | muted = false, 51 | poster, 52 | src, 53 | transformation, 54 | quality = "auto", 55 | ...otherCldVidPlayerOptions 56 | } = options; 57 | 58 | // Configuration for Cloudinary account. Cloud name is required, 59 | // so if one isn't present, throw. 60 | 61 | const { cloudName } = config?.cloud || {}; 62 | const { secureDistribution, privateCdn } = config?.url || {}; 63 | 64 | if (!cloudName) { 65 | throw new Error( 66 | "A Cloudinary Cloud name is required, please make sure your environment variable is set and configured in your environment.", 67 | ); 68 | } 69 | 70 | // If the publicId/src is a URL, attempt to parse it as a Cloudinary URL 71 | // to get the public ID alone 72 | 73 | let publicId = src || ""; 74 | 75 | if (publicId.startsWith("http")) { 76 | try { 77 | const parts = parseUrl(src); 78 | if (typeof parts?.publicId === "string") { 79 | publicId = parts?.publicId; 80 | } 81 | } catch (e) { 82 | // ignore 83 | } 84 | } 85 | 86 | if (!publicId) { 87 | throw new Error( 88 | "Video Player requires a src, please make sure to configure your src as a public ID or Cloudinary URL.", 89 | ); 90 | } 91 | 92 | // We want to apply a quality transformation which defaults 93 | // to auto, but we want it to be in the beginning of the 94 | // transformations array, in the event someone 95 | // has already passed some in, giving them the opportunity 96 | // to override if desired 97 | 98 | const playerTransformations = [ 99 | { quality }, 100 | // Normalize player transformations as an array 101 | ...(isArray(transformation) ? transformation : [transformation]), 102 | ]; 103 | 104 | // Provide an object configuration option for player logos 105 | 106 | let logoOptions: CloudinaryVideoPlayerOptionsLogo = {}; 107 | 108 | if (typeof logo === "boolean") { 109 | logoOptions.showLogo = logo; 110 | } else if (typeof logo === "object") { 111 | logoOptions = { 112 | ...logoOptions, 113 | showLogo: true, 114 | logoImageUrl: logo.imageUrl, 115 | logoOnclickUrl: logo.onClickUrl, 116 | }; 117 | } 118 | 119 | // Parse the value passed to 'autoplay'; 120 | // if its a boolean or a boolean passed as string ("true") set it directly to browser standard prop autoplay else fallback to default; 121 | // if its a string and not a boolean passed as string ("true") set it to cloudinary video player autoplayMode prop else fallback to undefined; 122 | 123 | let autoplayValue: boolean | "true" | "false" = false; 124 | let autoplayModeValue: string | undefined = undefined; 125 | 126 | if ( 127 | typeof autoplay === "boolean" || 128 | autoplay === "true" || 129 | autoplay === "false" 130 | ) { 131 | autoplayValue = autoplay; 132 | } 133 | 134 | if ( 135 | typeof autoplay === "string" && 136 | autoplay !== "true" && 137 | autoplay !== "false" 138 | ) { 139 | autoplayModeValue = autoplay; 140 | } 141 | 142 | // Finally construct the Player Options object 143 | 144 | const playerOptions: CloudinaryVideoPlayerOptions = { 145 | cloud_name: cloudName, 146 | privateCdn, 147 | secureDistribution, 148 | 149 | autoplayMode: autoplayModeValue, 150 | autoplay: autoplayValue, 151 | controls, 152 | language, 153 | languages, 154 | loop, 155 | muted, 156 | publicId, 157 | transformation: playerTransformations, 158 | ...logoOptions, 159 | ...otherCldVidPlayerOptions, 160 | }; 161 | 162 | if ( 163 | playerOptions.width && 164 | playerOptions.height && 165 | !playerOptions.aspectRatio 166 | ) { 167 | playerOptions.aspectRatio = `${playerOptions.width}:${playerOptions.height}`; 168 | } 169 | 170 | if (typeof poster === "string") { 171 | // If poster is a string, assume it's either a public ID 172 | // or a remote URL, in either case pass to `publicId` 173 | playerOptions.posterOptions = { 174 | publicId: poster, 175 | }; 176 | } else if (typeof poster === "object") { 177 | // If poster is an object, we can either customize the 178 | // automatically generated image from the video or generate 179 | // a completely new image from a separate public ID, so look 180 | // to see if the src is explicitly set to determine whether 181 | // or not to use the video's ID or just pass things along 182 | 183 | if (typeof poster.src !== "string") { 184 | playerOptions.posterOptions = { 185 | publicId: constructCloudinaryUrl({ 186 | options: { 187 | src: publicId, 188 | assetType: "video", 189 | format: "auto:image", 190 | ...poster, 191 | }, 192 | config, 193 | }), 194 | }; 195 | } else { 196 | playerOptions.posterOptions = { 197 | publicId: constructCloudinaryUrl({ 198 | options: { 199 | src: publicId, 200 | ...poster, 201 | }, 202 | config, 203 | }), 204 | }; 205 | } 206 | } 207 | 208 | return playerOptions; 209 | } 210 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/abr.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "../lib/plugin.js"; 2 | 3 | export declare namespace AbrPlugin { 4 | export interface Options { 5 | /** 6 | * @description The streaming profile to apply when delivering a video using adaptive bitrate streaming. 7 | * @url https://cloudinary.com/documentation/transformation_reference#sp_streaming_profile 8 | */ 9 | streamingProfile?: string; 10 | } 11 | } 12 | 13 | export const AbrPlugin = /* #__PURE__ */ plugin({ 14 | name: "Abr", 15 | supports: "video", 16 | inferOwnOptions: {} as AbrPlugin.Options, 17 | props: { 18 | streamingProfile: true, 19 | }, 20 | apply: (asset, opts) => { 21 | if (typeof opts.streamingProfile !== "string") return {}; 22 | 23 | asset.addTransformation(`sp_${opts.streamingProfile}`); 24 | 25 | return {}; 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/default-image.ts: -------------------------------------------------------------------------------- 1 | import { getFormat } from "@cloudinary-util/util"; 2 | import { plugin } from "../lib/plugin.js"; 3 | 4 | export declare namespace DefaultImagePlugin { 5 | export interface Options { 6 | /** 7 | * @description Configures the default image to use in case the given public ID is not available. Must include file extension. 8 | * @url https://cloudinary.com/documentation/transformation_reference#d_default_image 9 | */ 10 | defaultImage?: string; 11 | } 12 | } 13 | 14 | export const DefaultImagePlugin = /* #__PURE__ */ plugin({ 15 | name: "DefaultImage", 16 | supports: "image", 17 | inferOwnOptions: {} as DefaultImagePlugin.Options, 18 | props: { defaultImage: true }, 19 | apply: (asset, opts) => { 20 | const { defaultImage } = opts; 21 | 22 | if (typeof defaultImage !== "string") return {}; 23 | 24 | if (!getFormat(defaultImage)) { 25 | console.warn( 26 | `The defaultImage prop may be missing a format and must include it along with the public ID. (Ex: myimage.jpg)` 27 | ); 28 | } 29 | const defaultImageId = defaultImage.replace(/\//g, ":"); 30 | asset.addTransformation(`d_${defaultImageId}`); 31 | 32 | return {}; 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/effects.ts: -------------------------------------------------------------------------------- 1 | import { 2 | effects as qualifiersEffects, 3 | type QualifierOptions, 4 | } from "../constants/qualifiers.js"; 5 | import { plugin } from "../lib/plugin.js"; 6 | import { constructTransformation } from "../lib/transformations.js"; 7 | import { isArray } from "../lib/utils.js"; 8 | 9 | export declare namespace EffectsPlugin { 10 | export interface NestableOptions extends QualifierOptions {} 11 | 12 | export interface Options extends NestableOptions { 13 | /** 14 | * @description Array of objects specifying transformations to be applied to asset. 15 | */ 16 | effects?: ReadonlyArray; 17 | } 18 | } 19 | 20 | export const EffectsPlugin = /* #__PURE__ */ plugin({ 21 | name: "Effects", 22 | supports: "all", 23 | inferOwnOptions: {} as EffectsPlugin.Options, 24 | props: { 25 | angle: true, 26 | art: true, 27 | autoBrightness: true, 28 | autoColor: true, 29 | autoContrast: true, 30 | assistColorblind: true, 31 | background: true, 32 | blackwhite: true, 33 | blur: true, 34 | blurFaces: true, 35 | blurRegion: true, 36 | border: true, 37 | brightness: true, 38 | brightnessHSB: true, 39 | cartoonify: true, 40 | color: true, 41 | colorize: true, 42 | contrast: true, 43 | displace: true, 44 | distort: true, 45 | fillLight: true, 46 | gamma: true, 47 | gradientFade: true, 48 | grayscale: true, 49 | hue: true, 50 | improve: true, 51 | loop: true, 52 | multiply: true, 53 | negate: true, 54 | noise: true, 55 | oilPaint: true, 56 | opacity: true, 57 | outline: true, 58 | pixelate: true, 59 | pixelateFaces: true, 60 | pixelateRegion: true, 61 | radius: true, 62 | redeye: true, 63 | replaceColor: true, 64 | saturation: true, 65 | screen: true, 66 | sepia: true, 67 | shadow: true, 68 | sharpen: true, 69 | shear: true, 70 | simulateColorblind: true, 71 | tint: true, 72 | trim: true, 73 | unsharpMask: true, 74 | vectorize: true, 75 | vibrance: true, 76 | vignette: true, 77 | effects: true, 78 | }, 79 | apply: (cldAsset, opts) => { 80 | // Handle any top-level effect props 81 | 82 | const transformationStrings = constructTransformationString({ 83 | effects: qualifiersEffects, 84 | options: opts, 85 | }); 86 | 87 | transformationStrings.forEach((transformation) => { 88 | if (transformation) { 89 | cldAsset.addTransformation(transformation); 90 | } 91 | }); 92 | 93 | // If we're passing in an effects prop explicitly, treat it as an array of 94 | // effects that we need to process 95 | 96 | if (isArray(opts?.effects)) { 97 | opts?.effects.forEach((effectsSet) => { 98 | const transformationString = constructTransformationString({ 99 | effects: qualifiersEffects, 100 | options: effectsSet, 101 | }) 102 | .filter((t) => !!t) 103 | .join(","); 104 | cldAsset.addTransformation(transformationString); 105 | }); 106 | } 107 | 108 | interface ConstructTransformationStringSettings { 109 | effects: object; 110 | options?: object; 111 | } 112 | 113 | function constructTransformationString({ 114 | effects, 115 | options, 116 | }: ConstructTransformationStringSettings) { 117 | return (Object.keys(effects) as Array).map( 118 | (key) => { 119 | const { prefix, qualifier, converters } = effects[key]; 120 | return constructTransformation({ 121 | qualifier, 122 | prefix, 123 | value: options?.[key], 124 | converters, 125 | }); 126 | }, 127 | ); 128 | } 129 | 130 | return {}; 131 | }, 132 | }); 133 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/enhance.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "../lib/plugin.js"; 2 | 3 | export declare namespace EnhancePlugin { 4 | export interface Options { 5 | /** 6 | * @description Uses AI to analyze an image and make adjustments to enhance the appeal of the image. 7 | * @url https://cloudinary.com/documentation/transformation_reference#e_enhance 8 | */ 9 | enhance?: boolean; 10 | } 11 | } 12 | 13 | export const EnhancePlugin = /* #__PURE__ */ plugin({ 14 | name: "Enhance", 15 | supports: "image", 16 | inferOwnOptions: {} as EnhancePlugin.Options, 17 | props: { 18 | enhance: true, 19 | }, 20 | apply: (cldAsset, opts) => { 21 | if (opts.enhance) { 22 | cldAsset.addTransformation("e_enhance"); 23 | } 24 | 25 | return {}; 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/extract.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ExtractMode, 3 | ListablePrompts, 4 | Multiple, 5 | } from "../constants/parameters.js"; 6 | import { plugin } from "../lib/plugin.js"; 7 | import { isArray } from "../lib/utils.js"; 8 | 9 | export declare namespace ExtractPlugin { 10 | export interface Options { 11 | /** 12 | * @description Extracts an area or multiple areas of an image, described in natural language. 13 | * @url https://cloudinary.com/documentation/transformation_reference#e_extract 14 | */ 15 | extract?: ListablePrompts | NestedOptions; 16 | } 17 | 18 | export interface NestedOptions { 19 | prompt?: ListablePrompts; 20 | invert?: boolean; 21 | mode?: ExtractMode; 22 | multiple?: Multiple; 23 | } 24 | } 25 | 26 | export const ExtractPlugin = /* #__PURE__ */ plugin({ 27 | name: "Extract", 28 | supports: "image", 29 | inferOwnOptions: {} as ExtractPlugin.Options, 30 | props: { 31 | extract: true, 32 | }, 33 | apply: (cldAsset, { extract }) => { 34 | const properties = []; 35 | 36 | if (typeof extract === "string") { 37 | properties.push(`prompt_${extract}`); 38 | } else if (isArray(extract)) { 39 | properties.push(`prompt_${formatPrompts(extract)}`); 40 | } else { 41 | const prompt = formatPrompts(extract.prompt); 42 | 43 | if (prompt) { 44 | properties.push(`prompt_${prompt}`); 45 | } 46 | 47 | if (extract.invert === true) { 48 | properties.push("invert_true"); 49 | } 50 | 51 | if (typeof extract.mode === "string") { 52 | properties.push(`mode_${extract.mode}`); 53 | } 54 | 55 | if (extract.multiple === true) { 56 | properties.push("multiple_true"); 57 | } 58 | } 59 | 60 | if (properties.length > 0) { 61 | const transformation = `e_extract:${properties.join(";")}`; 62 | cldAsset.addTransformation(transformation); 63 | } 64 | 65 | return {}; 66 | }, 67 | }); 68 | 69 | /** 70 | * formatPrompts 71 | */ 72 | 73 | function formatPrompts(prompts: ListablePrompts | undefined) { 74 | if (typeof prompts === "string") return prompts; 75 | 76 | if (isArray(prompts)) { 77 | return `(${prompts.filter((prompt) => typeof prompt === "string").join(";")})`; 78 | } 79 | 80 | return undefined; 81 | } 82 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/fill-background.ts: -------------------------------------------------------------------------------- 1 | import { normalizeNumberParameter } from "@cloudinary-util/util"; 2 | import type { 3 | CropMode, 4 | Gravity, 5 | ListablePrompts, 6 | } from "../constants/parameters.js"; 7 | import { plugin } from "../lib/plugin.js"; 8 | 9 | const defaultCrop = "pad"; 10 | 11 | export declare namespace FillBackgroundPlugin { 12 | export interface Options { 13 | /** 14 | * @description Uses Generative Fill to extended padded image with AI 15 | * @url https://cloudinary.com/documentation/transformation_reference#b_gen_fill 16 | */ 17 | fillBackground?: boolean | NestedOptions; 18 | } 19 | 20 | export interface NestedOptions { 21 | crop?: CropMode; 22 | gravity?: Gravity; 23 | prompt?: ListablePrompts; 24 | } 25 | } 26 | 27 | export const FillBackgroundPlugin = /* #__PURE__ */ plugin({ 28 | name: "FillBackground", 29 | supports: "image", 30 | inferOwnOptions: {} as FillBackgroundPlugin.Options, 31 | props: { 32 | fillBackground: true, 33 | }, 34 | apply: (cldAsset, opts, ctx) => { 35 | const { fillBackground } = opts; 36 | 37 | if (typeof fillBackground === "undefined") return {}; 38 | 39 | const width = normalizeNumberParameter(ctx.width); 40 | const height = normalizeNumberParameter(ctx.height); 41 | const hasDefinedDimensions = 42 | typeof height === "number" && typeof width === "number"; 43 | let aspectRatio = ctx.aspectRatio; 44 | 45 | if (!aspectRatio && hasDefinedDimensions) { 46 | aspectRatio = `${width}:${height}`; 47 | } 48 | 49 | if (!aspectRatio) { 50 | if (process.env.NODE_ENV === "development") { 51 | console.warn( 52 | `Could not determine aspect ratio based on available options to use fillBackground. Please specify width and height or an aspect ratio.` 53 | ); 54 | } 55 | return {}; 56 | } 57 | 58 | if (fillBackground === true) { 59 | const properties = [ 60 | "b_gen_fill", 61 | `ar_${aspectRatio}`, 62 | `c_${defaultCrop}`, 63 | ]; 64 | 65 | cldAsset.addTransformation(properties.join(",")); 66 | } else if (typeof fillBackground === "object") { 67 | const { crop = defaultCrop, gravity, prompt } = fillBackground; 68 | 69 | const properties = [`ar_${aspectRatio}`, `c_${crop}`]; 70 | 71 | if (typeof prompt === "string") { 72 | properties.unshift(`b_gen_fill:${prompt}`); 73 | } else { 74 | properties.unshift(`b_gen_fill`); 75 | } 76 | 77 | if (typeof gravity === "string") { 78 | properties.push(`g_${gravity}`); 79 | } 80 | 81 | cldAsset.addTransformation(properties.join(",")); 82 | } 83 | 84 | return {}; 85 | }, 86 | }); 87 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/flags.ts: -------------------------------------------------------------------------------- 1 | import type { FlagsDefinition } from "../constants/parameters.js"; 2 | import { plugin } from "../lib/plugin.js"; 3 | import { isArray } from "../lib/utils.js"; 4 | 5 | export declare namespace FlagsPlugin { 6 | export interface Options { 7 | flags?: FlagsDefinition; 8 | } 9 | } 10 | 11 | export const FlagsPlugin = /* #__PURE__ */ plugin({ 12 | name: "Flags", 13 | supports: "all", 14 | inferOwnOptions: {} as FlagsPlugin.Options, 15 | props: { 16 | flags: true, 17 | }, 18 | apply: (cldAsset, { flags }) => { 19 | // First iteration of adding flags follows the same pattern 20 | // as the top level option from Cloudinary URL Gen SDK where 21 | // each flag is individually added as its own segment via 22 | // the addFlag method. Flags can have additional context and 23 | // may warrant case-by-case applications 24 | 25 | if (typeof flags === "string") { 26 | flags = [flags]; 27 | } 28 | if (isArray(flags)) { 29 | flags.forEach((flag) => cldAsset.addFlag(flag)); 30 | } else if (typeof flags === "object") { 31 | Object.entries(flags).forEach(([qualifier, value]) => { 32 | // The addFlag method encodes some characters, specifically 33 | // the "." character which breaks some use cases like 34 | // du_2.5 35 | cldAsset.addTransformation(`fl_${qualifier}:${value}`); 36 | }); 37 | } 38 | 39 | return {}; 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/named-transformations.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "../lib/plugin.js"; 2 | import { isArray } from "../lib/utils.js"; 3 | 4 | export declare namespace NamedTransformationsPlugin { 5 | export interface Options { 6 | /** 7 | * @description Named transformations to apply to asset. 8 | * @url https://cloudinary.com/documentation/image_transformations#named_transformations 9 | */ 10 | namedTransformations?: string | ReadonlyArray; 11 | /** 12 | * @deprecated use {@link `namedTransformations`} instead 13 | * @description: Deprecated: use namedTransformations instead 14 | * @url https://cloudinary.com/documentation/image_transformations#named_transformations 15 | */ 16 | transformations?: string | ReadonlyArray; 17 | } 18 | } 19 | 20 | export const NamedTransformationsPlugin = /* #__PURE__ */ plugin({ 21 | name: "NamedTransformations", 22 | strict: true, 23 | supports: "all", 24 | inferOwnOptions: {} as NamedTransformationsPlugin.Options, 25 | props: { 26 | namedTransformations: true, 27 | transformations: true, 28 | }, 29 | apply: (cldAsset, opts) => { 30 | const { transformations, namedTransformations } = opts; 31 | 32 | if (transformations && process.env.NODE_ENVIRONMENT === "development") { 33 | console.warn( 34 | "The transformations prop is deprecated. Please use namedTransformations instead.", 35 | ); 36 | } 37 | 38 | let _namedTransformations = namedTransformations || transformations || []; 39 | 40 | if (!isArray(_namedTransformations)) { 41 | _namedTransformations = [_namedTransformations]; 42 | } 43 | 44 | _namedTransformations.forEach((transformation: string) => { 45 | cldAsset.addTransformation(`t_${transformation}`); 46 | }); 47 | 48 | return {}; 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/preserve-transformations.ts: -------------------------------------------------------------------------------- 1 | import { getTransformations } from "@cloudinary-util/util"; 2 | import { plugin } from "../lib/plugin.js"; 3 | 4 | export declare namespace PreserveTransformationsPlugin { 5 | export interface Options { 6 | /** 7 | * @description Preserves transformations from a Cloudinary URL when using using a Cloudinary URL as the asset source (src). 8 | */ 9 | preserveTransformations?: boolean; 10 | } 11 | } 12 | 13 | export const PreserveTransformationsPlugin = /* #__PURE__ */ plugin({ 14 | name: "PreserveTransformations", 15 | supports: "all", 16 | inferOwnOptions: {} as PreserveTransformationsPlugin.Options, 17 | props: { 18 | preserveTransformations: true, 19 | }, 20 | apply: (cldAsset, opts, ctx) => { 21 | const { preserveTransformations = false } = opts; 22 | 23 | // Try to preserve the original transformations from the Cloudinary URL passed in 24 | // to the function. This only works if the URL has a version number on it and otherwise 25 | // will fail to load 26 | 27 | if (preserveTransformations) { 28 | try { 29 | if (ctx.src === undefined) { 30 | throw new Error("options.src was undefined"); 31 | } 32 | const transformations = getTransformations(ctx.src).map((t) => 33 | t.join(","), 34 | ); 35 | transformations.flat().forEach((transformation) => { 36 | cldAsset.addTransformation(transformation); 37 | }); 38 | } catch (e) { 39 | console.warn( 40 | `Failed to preserve transformations: ${(e as Error).message}`, 41 | ); 42 | } 43 | } 44 | 45 | return {}; 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/raw-transformations.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "../lib/plugin.js"; 2 | import { isArray } from "../lib/utils.js"; 3 | 4 | export declare namespace RawTransformationsPlugin { 5 | export interface Options { 6 | /** 7 | * @description Array of transformation parameters using the Cloudinary URL API to apply to an asset. 8 | * @url https://cloudinary.com/documentation/transformation_reference 9 | */ 10 | rawTransformations?: string | ReadonlyArray; 11 | } 12 | } 13 | 14 | export const RawTransformationsPlugin = /* #__PURE__ */ plugin({ 15 | name: "RawTransformations", 16 | supports: "all", 17 | inferOwnOptions: {} as RawTransformationsPlugin.Options, 18 | props: { 19 | rawTransformations: true, 20 | }, 21 | apply: (cldAsset, opts) => { 22 | let { rawTransformations = [] } = opts; 23 | 24 | if (!isArray(rawTransformations)) { 25 | rawTransformations = [rawTransformations]; 26 | } 27 | 28 | rawTransformations.forEach((transformation) => { 29 | cldAsset.addTransformation(transformation); 30 | }); 31 | 32 | return {}; 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/recolor.ts: -------------------------------------------------------------------------------- 1 | import type { ListablePrompts } from "../constants/parameters.js"; 2 | import { plugin } from "../lib/plugin.js"; 3 | import { promptArrayToString } from "../lib/transformations.js"; 4 | import { isArray } from "../lib/utils.js"; 5 | 6 | export declare namespace RecolorPlugin { 7 | export interface Options { 8 | /** 9 | * @description Uses generative AI to recolor parts of your image, maintaining the relative shading. 10 | * @url https://cloudinary.com/documentation/transformation_reference#e_gen_recolor 11 | */ 12 | recolor?: 13 | | string 14 | | ReadonlyArray 15 | | readonly [ReadonlyArray, ...Array] 16 | | NestedOptions; 17 | } 18 | 19 | export interface NestedOptions { 20 | prompt?: ListablePrompts; 21 | to?: string; 22 | multiple?: boolean; 23 | } 24 | } 25 | 26 | export const RecolorPlugin = /* #__PURE__ */ plugin({ 27 | name: "Recolor", 28 | supports: "image", 29 | inferOwnOptions: {} as RecolorPlugin.Options, 30 | props: { 31 | recolor: true, 32 | }, 33 | apply: (cldAsset, opts) => { 34 | const { recolor } = opts; 35 | 36 | const recolorOptions: Record = { 37 | prompt: undefined, 38 | "to-color": undefined, 39 | multiple: undefined, 40 | }; 41 | 42 | if (isArray(recolor)) { 43 | if (isArray(recolor[0])) { 44 | recolorOptions.prompt = promptArrayToString(recolor[0]); 45 | } else { 46 | recolorOptions.prompt = recolor[0]; 47 | } 48 | 49 | if (typeof recolor[1] === "string") { 50 | recolorOptions["to-color"] = recolor[1]; 51 | } 52 | } else if (typeof recolor === "object") { 53 | // Allow the prompt to still be available as either a string or an array 54 | 55 | if (typeof recolor.prompt === "string") { 56 | recolorOptions.prompt = recolor.prompt; 57 | } else if (isArray(recolor.prompt)) { 58 | recolorOptions.prompt = promptArrayToString(recolor.prompt); 59 | } 60 | 61 | if (typeof recolor.to === "string") { 62 | recolorOptions["to-color"] = recolor.to; 63 | } 64 | 65 | if (recolor.multiple === true) { 66 | recolorOptions.multiple = `true`; 67 | } 68 | } 69 | 70 | const transformation = Object.entries(recolorOptions) 71 | .filter(([, value]) => !!value) 72 | .map(([key, value]) => `${key}_${value}`) 73 | .join(";"); 74 | 75 | if (transformation) { 76 | cldAsset.addTransformation(`e_gen_recolor:${transformation}`); 77 | } 78 | 79 | return {}; 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/remove-background.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "../lib/plugin.js"; 2 | 3 | export declare namespace RemoveBackgroundPlugin { 4 | export interface Options { 5 | /** 6 | * @description Removes the background of an image using the Cloudinary AI Background Removal Add-On (Required). 7 | * @url https://cloudinary.com/documentation/cloudinary_ai_background_removal_addon 8 | */ 9 | removeBackground?: boolean; 10 | } 11 | } 12 | 13 | export const RemoveBackgroundPlugin = /* #__PURE__ */ plugin({ 14 | name: "RemoveBackground", 15 | supports: "image", 16 | inferOwnOptions: {} as RemoveBackgroundPlugin.Options, 17 | props: { 18 | removeBackground: true, 19 | }, 20 | apply: (cldAsset, opts) => { 21 | if (opts.removeBackground) { 22 | cldAsset.addTransformation("e_background_removal"); 23 | } 24 | return {}; 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/remove.ts: -------------------------------------------------------------------------------- 1 | import type { ListablePrompts } from "../constants/parameters.js"; 2 | import { plugin } from "../lib/plugin.js"; 3 | import { promptArrayToString } from "../lib/transformations.js"; 4 | import { isArray } from "../lib/utils.js"; 5 | 6 | export declare namespace RemovePlugin { 7 | export interface Options { 8 | /** 9 | * @description Applies zooming and/or panning to an image, resulting in a video or animated image. 10 | * @url https://cloudinary.com/documentation/transformation_reference#e_zoompan 11 | */ 12 | remove?: ListablePrompts | NestedOptions; 13 | } 14 | 15 | export interface NestedOptions { 16 | prompt?: ListablePrompts; 17 | region?: number[] | number[][]; 18 | multiple?: boolean; 19 | removeShadow?: boolean; 20 | } 21 | } 22 | 23 | export const RemovePlugin = /* #__PURE__ */ plugin({ 24 | name: "Remove", 25 | supports: "image", 26 | inferOwnOptions: {} as RemovePlugin.Options, 27 | props: { 28 | remove: true, 29 | }, 30 | apply: (cldAsset, opts) => { 31 | const { remove } = opts; 32 | 33 | const removeOptions: Record = { 34 | prompt: undefined, 35 | region: undefined, 36 | multiple: undefined, 37 | "remove-shadow": undefined, 38 | }; 39 | 40 | if (typeof remove === "string") { 41 | removeOptions.prompt = remove; 42 | } else if (isArray(remove)) { 43 | removeOptions.prompt = promptArrayToString(remove); 44 | } else if (typeof remove === "object") { 45 | const hasPrompt = 46 | typeof remove.prompt === "string" || isArray(remove.prompt); 47 | const hasRegion = isArray(remove.region); 48 | 49 | if (hasPrompt && hasRegion) { 50 | throw new Error( 51 | "Invalid remove options: you can not have both a prompt and a region. More info: https://cloudinary.com/documentation/transformation_reference#e_gen_remove", 52 | ); 53 | } 54 | 55 | // Allow the prompt to still be available as either a string or an array 56 | 57 | if (typeof remove.prompt === "string") { 58 | removeOptions.prompt = remove.prompt; 59 | } else if (isArray(remove.prompt)) { 60 | removeOptions.prompt = promptArrayToString(remove.prompt); 61 | } 62 | 63 | // Region can be an array of numbers, or an array with 1+ arrays of numbers 64 | 65 | if (isArray(remove.region)) { 66 | removeOptions.region = regionArrayToString(remove.region); 67 | } 68 | 69 | if (remove.multiple === true) { 70 | removeOptions.multiple = `true`; 71 | } 72 | 73 | if (remove.removeShadow === true) { 74 | removeOptions["remove-shadow"] = `true`; 75 | } 76 | } 77 | 78 | const transformation = Object.entries(removeOptions) 79 | .filter(([, value]) => !!value) 80 | .map(([key, value]) => `${key}_${value}`) 81 | .join(";"); 82 | 83 | if (transformation) { 84 | cldAsset.addTransformation(`e_gen_remove:${transformation}`); 85 | } 86 | 87 | return {}; 88 | }, 89 | }); 90 | 91 | /** 92 | * regionArrayToString 93 | */ 94 | 95 | function regionArrayToString( 96 | regionArray: Array>, 97 | ): string { 98 | const indexes: Record = { 99 | 0: "x", 100 | 1: "y", 101 | 2: "w", 102 | 3: "h", 103 | }; 104 | 105 | const regionString = regionArray 106 | .map((region, index) => { 107 | if (isArray(region)) { 108 | return regionArrayToString(region); 109 | } 110 | 111 | const key = indexes[index]; 112 | 113 | return `${key}_${region}`; 114 | }) 115 | .join(";"); 116 | 117 | return `(${regionString})`; 118 | } 119 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/replace-background.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "../lib/plugin.js"; 2 | 3 | export declare namespace ReplaceBackgroundPlugin { 4 | export interface Options { 5 | /** 6 | * @description Replaces the background of an image with an AI-generated background. 7 | * @url https://cloudinary.com/documentation/transformation_reference#e_gen_background_replace 8 | */ 9 | replaceBackground?: NestedOptions | string | boolean; 10 | } 11 | 12 | export interface NestedOptions { 13 | prompt?: string; 14 | seed?: number; 15 | } 16 | } 17 | 18 | export const ReplaceBackgroundPlugin = /* #__PURE__ */ plugin({ 19 | name: "ReplaceBackground", 20 | supports: "image", 21 | inferOwnOptions: {} as ReplaceBackgroundPlugin.Options, 22 | props: { 23 | replaceBackground: true, 24 | }, 25 | apply: (cldAsset, opts) => { 26 | const { replaceBackground } = opts; 27 | 28 | if (!replaceBackground) return {}; 29 | 30 | const properties = []; 31 | 32 | if (typeof replaceBackground === "object") { 33 | if (typeof replaceBackground.prompt !== "undefined") { 34 | properties.push(`prompt_${replaceBackground.prompt}`); 35 | } 36 | 37 | if (typeof replaceBackground.seed === "number") { 38 | properties.push(`seed_${replaceBackground.seed}`); 39 | } 40 | } else if (typeof replaceBackground === "string") { 41 | properties.push(`prompt_${replaceBackground}`); 42 | } 43 | 44 | let transformation = "e_gen_background_replace"; 45 | 46 | if (properties.length > 0) { 47 | transformation = `${transformation}:${properties.join(";")}`; 48 | } 49 | 50 | cldAsset.addTransformation(transformation); 51 | 52 | return {}; 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/replace.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "../lib/plugin.js"; 2 | import { isArray } from "../lib/utils.js"; 3 | 4 | export declare namespace ReplacePlugin { 5 | export interface Options { 6 | /** 7 | * @description Uses generative AI to replace parts of your image with something else. 8 | * @url https://cloudinary.com/documentation/transformation_reference#e_gen_replace 9 | */ 10 | replace?: NestedOptions | ReadonlyArray | ReadonlyArray; 11 | } 12 | 13 | export interface NestedOptions { 14 | from: string; 15 | to: string; 16 | preserveGeometry?: boolean; 17 | } 18 | } 19 | 20 | export const ReplacePlugin = /* #__PURE__ */ plugin({ 21 | name: "Replace", 22 | supports: "image", 23 | inferOwnOptions: {} as ReplacePlugin.Options, 24 | props: { 25 | replace: true, 26 | }, 27 | apply: (cldAsset, opts) => { 28 | const { replace } = opts; 29 | 30 | if (!replace) return {}; 31 | 32 | let from: string, 33 | to: string, 34 | preserveGeometry: boolean = false; 35 | 36 | if (isArray(replace)) { 37 | from = replace[0] as string; 38 | to = replace[1] as string; 39 | preserveGeometry = (replace[2] as boolean) || false; 40 | } else { 41 | from = replace.from; 42 | to = replace.to; 43 | preserveGeometry = replace.preserveGeometry || false; 44 | } 45 | 46 | const properties = [`e_gen_replace:from_${from}`, `to_${to}`]; 47 | 48 | // This property defaults to false, so we only need to pass it if it's true 49 | if (preserveGeometry) { 50 | properties.push(`preserve-geometry_${preserveGeometry}`); 51 | } 52 | 53 | cldAsset.addTransformation(properties.join(";")); 54 | 55 | return {}; 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/restore.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "../lib/plugin.js"; 2 | 3 | export declare namespace RestorePlugin { 4 | export interface Options { 5 | /** 6 | * @description Uses generative AI to restore details in poor quality images or images that may have become degraded through repeated processing and compression. 7 | * @url https://cloudinary.com/documentation/transformation_reference#e_gen_restore 8 | */ 9 | restore?: boolean; 10 | } 11 | } 12 | 13 | export const RestorePlugin = /* #__PURE__ */ plugin({ 14 | name: "Restore", 15 | supports: "image", 16 | inferOwnOptions: {} as RestorePlugin.Options, 17 | props: { 18 | restore: true, 19 | }, 20 | apply: (cldAsset, opts) => { 21 | const { restore } = opts; 22 | 23 | if (restore) { 24 | cldAsset.addTransformation("e_gen_restore"); 25 | } 26 | 27 | return {}; 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/sanitize.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "../lib/plugin.js"; 2 | 3 | export declare namespace SanitizePlugin { 4 | export interface Options { 5 | /** 6 | * @description Runs a sanitizer on SVG images. 7 | * @url https://cloudinary.com/documentation/transformation_reference#fl_sanitize 8 | */ 9 | sanitize?: boolean; 10 | } 11 | } 12 | 13 | export const SanitizePlugin = /* #__PURE__ */ plugin({ 14 | name: "Sanitize", 15 | supports: "image", 16 | inferOwnOptions: {} as SanitizePlugin.Options, 17 | props: { 18 | sanitize: true, 19 | }, 20 | alwaysApply: true, 21 | apply: (cldAsset, opts, ctx) => { 22 | const { sanitize = true } = opts; 23 | 24 | const shouldApplySanitizer: boolean = 25 | sanitize && 26 | (ctx.format === "svg" || 27 | (cldAsset as {} as { publicID: string }).publicID.endsWith(".svg")); 28 | 29 | if (shouldApplySanitizer) { 30 | cldAsset.addTransformation("fl_sanitize"); 31 | } 32 | 33 | return {}; 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/seo.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "../lib/plugin.js"; 2 | 3 | export declare namespace SeoPlugin { 4 | export interface Options { 5 | /** 6 | * @description Configures the URL to include an SEO-friendly suffix in the URL 7 | * @url https://cloudinary.com/documentation/advanced_url_delivery_options#seo_friendly_media_asset_urls 8 | */ 9 | seoSuffix?: string; 10 | } 11 | } 12 | 13 | export const SeoPlugin = /* #__PURE__ */ plugin({ 14 | name: "Seo", 15 | supports: "all", 16 | inferOwnOptions: {} as SeoPlugin.Options, 17 | props: { 18 | seoSuffix: true, 19 | }, 20 | apply: (cldAsset, opts, ctx) => { 21 | const { seoSuffix } = opts; 22 | 23 | if (typeof seoSuffix !== "string") return {}; 24 | 25 | if (ctx.deliveryType === "fetch") { 26 | console.warn("SEO suffix is not supported with a delivery type of fetch"); 27 | } else { 28 | cldAsset.setSuffix(seoSuffix); 29 | } 30 | 31 | return {}; 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/underlays.ts: -------------------------------------------------------------------------------- 1 | import { objectHasKey } from "@cloudinary-util/util"; 2 | import type { 3 | CropMode, 4 | Height, 5 | ListableFlags, 6 | PositionalOptions, 7 | Width, 8 | } from "../constants/parameters.js"; 9 | import { 10 | position as qualifiersPosition, 11 | primary as qualifiersPrimary, 12 | } from "../constants/qualifiers.js"; 13 | import { plugin } from "../lib/plugin.js"; 14 | import { entriesOf, isArray } from "../lib/utils.js"; 15 | 16 | export declare namespace UnderlaysPlugin { 17 | export interface Options { 18 | /** 19 | * @description Public ID of image that is applied under the base image. 20 | * @url https://cloudinary.com/documentation/transformation_reference#l_layer 21 | */ 22 | underlay?: string; 23 | /** 24 | * @description Image layers that are applied under the base image. 25 | * @url https://cloudinary.com/documentation/transformation_reference#l_layer 26 | */ 27 | underlays?: ReadonlyArray; 28 | } 29 | 30 | export interface NestedOptions { 31 | appliedEffects?: ReadonlyArray; 32 | appliedFlags?: ListableFlags; 33 | effects?: ReadonlyArray; 34 | crop?: CropMode; 35 | flags?: ListableFlags; 36 | height?: Height; 37 | position?: PositionalOptions; 38 | publicId?: string; 39 | type?: string; 40 | url?: string; 41 | width?: Width; 42 | } 43 | } 44 | 45 | export const UnderlaysPlugin = /* #__PURE__ */ plugin({ 46 | name: "Underlays", 47 | supports: "all", 48 | inferOwnOptions: {} as UnderlaysPlugin.Options, 49 | props: { 50 | underlay: true, 51 | underlays: true, 52 | }, 53 | apply: (cldAsset, opts) => { 54 | const { underlay, underlays = [] } = opts; 55 | 56 | const typeQualifier = "u"; 57 | 58 | if (isArray(underlays)) { 59 | underlays.forEach(applyUnderlay); 60 | } 61 | 62 | if (typeof underlay === "string") { 63 | const underlayOptions: UnderlaysPlugin.NestedOptions = { 64 | publicId: underlay, 65 | crop: "fill", 66 | width: "1.0", 67 | height: "1.0", 68 | flags: ["relative"], 69 | }; 70 | 71 | applyUnderlay(underlayOptions); 72 | } 73 | 74 | /** 75 | * applyUnderlay 76 | */ 77 | 78 | function applyUnderlay({ 79 | publicId, 80 | type, 81 | position, 82 | effects: layerEffects = [], 83 | flags: layerFlags = [], 84 | appliedFlags = [], 85 | ...options 86 | }: UnderlaysPlugin.NestedOptions) { 87 | const hasPublicId = typeof publicId === "string"; 88 | const hasPosition = typeof position === "object"; 89 | 90 | if (!hasPublicId) { 91 | console.warn(`An ${type} is missing a Public ID`); 92 | return; 93 | } 94 | 95 | // Start to construct the transformation string using the public ID 96 | 97 | let layerTransformation = `${typeQualifier}_${publicId.replace( 98 | /\//g, 99 | ":", 100 | )}`; 101 | 102 | // Begin organizing transformations based on what it is and the location 103 | // it needs to be placed in the URL 104 | 105 | const primary: Array = []; 106 | const applied: Array = []; 107 | 108 | // Gemeral options 109 | 110 | entriesOf(options).forEach(([key, value]) => { 111 | if (!objectHasKey(qualifiersPrimary, key)) return; 112 | const { qualifier } = qualifiersPrimary[key]!; 113 | primary.push(`${qualifier}_${value}`); 114 | }); 115 | 116 | // Layer effects 117 | 118 | layerEffects.forEach((effect) => { 119 | (Object.keys(effect) as Array).forEach((key) => { 120 | if (!objectHasKey(qualifiersPrimary, key)) return; 121 | const { qualifier } = qualifiersPrimary[key]; 122 | primary.push(`${qualifier}_${effect[key]}`); 123 | }); 124 | }); 125 | 126 | // Positioning 127 | 128 | if (hasPosition) { 129 | entriesOf(position).forEach(([key, value]) => { 130 | if (!objectHasKey(qualifiersPosition, key)) return; 131 | const { qualifier } = qualifiersPosition[key]!; 132 | applied.push(`${qualifier}_${value}`); 133 | }); 134 | } 135 | 136 | // Layer Flags 137 | // Add flags to the primary layer transformation segment 138 | // @TODO: accept flag value 139 | 140 | const activeLayerFlags = isArray(layerFlags) ? layerFlags : [layerFlags]; 141 | 142 | activeLayerFlags.forEach((flag) => primary.push(`fl_${flag}`)); 143 | 144 | // Applied Flags 145 | // Add flags to the fl_layer_apply transformation segment 146 | // @TODO: accept flag value 147 | 148 | const activeAppliedFlags = isArray(appliedFlags) 149 | ? appliedFlags 150 | : [appliedFlags]; 151 | 152 | activeAppliedFlags.forEach((flag) => applied.push(`fl_${flag}`)); 153 | 154 | // Add all primary transformations 155 | 156 | layerTransformation = `${layerTransformation},${primary.join(",")}`; 157 | 158 | // Add all applied transformations 159 | 160 | layerTransformation = `${layerTransformation}/fl_layer_apply,fl_no_overflow`; 161 | 162 | if (applied.length > 0) { 163 | layerTransformation = `${layerTransformation},${applied.join(",")}`; 164 | } 165 | 166 | // Finally add it to the image 167 | 168 | cldAsset.addTransformation(layerTransformation); 169 | } 170 | 171 | return {}; 172 | }, 173 | }); 174 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/version.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "../lib/plugin.js"; 2 | 3 | export declare namespace VersionPlugin { 4 | export interface Options { 5 | /** 6 | * @description Custom version number to apply to asset URL. 7 | * @url https://cloudinary.com/documentation/advanced_url_delivery_options#asset_versions 8 | */ 9 | version?: number | string; 10 | } 11 | } 12 | 13 | export const VersionPlugin = /* #__PURE__ */ plugin({ 14 | name: "Version", 15 | supports: "all", 16 | inferOwnOptions: {} as VersionPlugin.Options, 17 | props: { 18 | version: true, 19 | }, 20 | apply: (cldAsset, opts) => { 21 | const { version } = opts; 22 | 23 | if (typeof version !== "string" && typeof version !== "number") return {}; 24 | 25 | // Replace a `v` in the string just in case the caller 26 | // passes it in 27 | cldAsset.setVersion(`${version}`.replace("v", "")); 28 | 29 | return {}; 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /packages/url-loader/src/plugins/zoompan.ts: -------------------------------------------------------------------------------- 1 | import type { QualifierOptions } from "../constants/qualifiers.js"; 2 | import { plugin } from "../lib/plugin.js"; 3 | import type { PluginOptions } from "../types/plugins.js"; 4 | 5 | export declare namespace ZoompanPlugin { 6 | export interface Options { 7 | /** 8 | * @description Applies zooming and/or panning to an image, resulting in a video or animated image. 9 | * @url https://cloudinary.com/documentation/transformation_reference#e_zoompan 10 | */ 11 | zoompan?: string | boolean | NestedOptions; 12 | } 13 | 14 | export interface NestedOptions { 15 | loop?: QualifierOptions["loop"]; 16 | options: string; 17 | } 18 | } 19 | 20 | export const ZoompanPlugin = /* #__PURE__ */ plugin({ 21 | name: "Zoompan", 22 | supports: "image", 23 | inferOwnOptions: {} as ZoompanPlugin.Options, 24 | props: { 25 | zoompan: true, 26 | }, 27 | apply: (cldAsset, opts) => { 28 | const { zoompan = false } = opts; 29 | 30 | const overrides: PluginOptions = { 31 | format: undefined, 32 | }; 33 | 34 | if (zoompan === true) { 35 | cldAsset.addTransformation("e_zoompan"); 36 | } else if (typeof zoompan === "string") { 37 | if (zoompan === "loop") { 38 | cldAsset.addTransformation("e_zoompan"); 39 | cldAsset.addTransformation("e_loop"); 40 | } else { 41 | cldAsset.addTransformation(`e_zoompan:${zoompan}`); 42 | } 43 | } else if (typeof zoompan === "object") { 44 | let zoompanEffect = "e_zoompan"; 45 | 46 | if (typeof zoompan.options === "string") { 47 | zoompanEffect = `${zoompanEffect}:${zoompan.options}`; 48 | } 49 | 50 | cldAsset.addTransformation(zoompanEffect); 51 | 52 | let loopEffect; 53 | 54 | if (zoompan.loop === true) { 55 | loopEffect = "e_loop"; 56 | } else if ( 57 | typeof zoompan.loop === "string" || 58 | typeof zoompan.loop === "number" 59 | ) { 60 | loopEffect = `e_loop:${zoompan.loop}`; 61 | } 62 | 63 | if (loopEffect) { 64 | cldAsset.addTransformation(loopEffect); 65 | } 66 | } 67 | 68 | if (zoompan !== false) { 69 | overrides.format = "auto:animated"; 70 | } 71 | 72 | return { 73 | options: overrides, 74 | }; 75 | }, 76 | }); 77 | -------------------------------------------------------------------------------- /packages/url-loader/src/types/asset.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedAssetType } from "../lib/plugin.js"; 2 | import type { CroppingPlugin } from "../plugins/cropping.js"; 3 | import type { EffectsPlugin } from "../plugins/effects.js"; 4 | import type { FlagsPlugin } from "../plugins/flags.js"; 5 | import type { NamedTransformationsPlugin } from "../plugins/named-transformations.js"; 6 | import type { OverlaysPlugin } from "../plugins/overlays.js"; 7 | import type { PreserveTransformationsPlugin } from "../plugins/preserve-transformations.js"; 8 | import type { RawTransformationsPlugin } from "../plugins/raw-transformations.js"; 9 | import type { RemoveBackgroundPlugin } from "../plugins/remove-background.js"; 10 | import type { SanitizePlugin } from "../plugins/sanitize.js"; 11 | import type { SeoPlugin } from "../plugins/seo.js"; 12 | import type { UnderlaysPlugin } from "../plugins/underlays.js"; 13 | import type { VersionPlugin } from "../plugins/version.js"; 14 | import type { ZoompanPlugin } from "../plugins/zoompan.js"; 15 | 16 | import type { Format } from "../constants/parameters.js"; 17 | 18 | export type SupportedAssetTypeInput = SupportedAssetType | "videos" | "images"; 19 | 20 | export interface BaseAssetOptions< 21 | assetType extends SupportedAssetTypeInput = SupportedAssetTypeInput, 22 | > { 23 | /** 24 | * @description Cloudinary Public ID or versioned Cloudinary URL (/v1234/) 25 | */ 26 | src: string; 27 | /** 28 | * @description The type of asset to deliver. 29 | * @url https://cloudinary.com/documentation/image_transformations#transformation_url_structure 30 | */ 31 | assetType?: assetType; 32 | /** 33 | * @description Delivery method of the asset. 34 | * @url https://cloudinary.com/documentation/image_transformations#delivery_types 35 | */ 36 | deliveryType?: string; 37 | /** 38 | * @description Delivery method of the asset. 39 | * @url https://cloudinary.com/documentation/image_transformations#delivery_types 40 | */ 41 | dpr?: string | number; 42 | /** 43 | * @description Converts (if necessary) and delivers an asset in the specified format. 44 | * @url https://cloudinary.com/documentation/transformation_reference#f_format 45 | */ 46 | format?: Format; 47 | /** 48 | * @description Quality of the delivered asset 49 | * @url https://cloudinary.com/documentation/transformation_reference#q_quality 50 | */ 51 | quality?: string | number | string; 52 | /** 53 | * @description Gives you the ability to have more control over what transformations are permitted to be used from your Cloudinary account. 54 | * @url https://cloudinary.com/documentation/control_access_to_media#strict_transformations 55 | */ 56 | strictTransformations?: boolean; 57 | } 58 | 59 | export interface AssetOptions< 60 | assetType extends SupportedAssetTypeInput = SupportedAssetTypeInput, 61 | > extends BaseAssetOptions, 62 | CroppingPlugin.Options, 63 | EffectsPlugin.Options, 64 | FlagsPlugin.Options, 65 | NamedTransformationsPlugin.Options, 66 | OverlaysPlugin.Options, 67 | PreserveTransformationsPlugin.Options, 68 | RawTransformationsPlugin.Options, 69 | RemoveBackgroundPlugin.Options, 70 | SanitizePlugin.Options, 71 | SeoPlugin.Options, 72 | UnderlaysPlugin.Options, 73 | VersionPlugin.Options, 74 | ZoompanPlugin.Options {} 75 | -------------------------------------------------------------------------------- /packages/url-loader/src/types/image.ts: -------------------------------------------------------------------------------- 1 | import type { DefaultImagePlugin } from "../plugins/default-image.js"; 2 | import type { EnhancePlugin } from "../plugins/enhance.js"; 3 | import type { ExtractPlugin } from "../plugins/extract.js"; 4 | import type { FillBackgroundPlugin } from "../plugins/fill-background.js"; 5 | import type { RecolorPlugin } from "../plugins/recolor.js"; 6 | import type { RemovePlugin } from "../plugins/remove.js"; 7 | import type { ReplaceBackgroundPlugin } from "../plugins/replace-background.js"; 8 | import type { ReplacePlugin } from "../plugins/replace.js"; 9 | import type { RestorePlugin } from "../plugins/restore.js"; 10 | import type { ZoompanPlugin } from "../plugins/zoompan.js"; 11 | import type { AssetOptions } from "./asset.js"; 12 | 13 | export interface ImageOptions 14 | extends AssetOptions, 15 | DefaultImagePlugin.Options, 16 | EnhancePlugin.Options, 17 | ExtractPlugin.Options, 18 | FillBackgroundPlugin.Options, 19 | RecolorPlugin.Options, 20 | RemovePlugin.Options, 21 | ReplacePlugin.Options, 22 | ReplaceBackgroundPlugin.Options, 23 | RestorePlugin.Options, 24 | ZoompanPlugin.Options {} 25 | -------------------------------------------------------------------------------- /packages/url-loader/src/types/plugins.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AspectRatio, 3 | CropMode, 4 | Format, 5 | Gravity, 6 | Height, 7 | Width, 8 | X, 9 | Y, 10 | Zoom, 11 | } from "../constants/parameters.js"; 12 | 13 | export interface PluginOptions { 14 | aspectRatio?: AspectRatio; 15 | crop?: CropMode; 16 | gravity?: Gravity; 17 | height?: Height; 18 | format?: Format; 19 | resize?: string; 20 | x?: X; 21 | y?: Y; 22 | width?: Width; 23 | zoom?: Zoom; 24 | } 25 | 26 | export interface PluginResults { 27 | options?: PluginOptions; 28 | } 29 | -------------------------------------------------------------------------------- /packages/url-loader/src/types/qualifiers.ts: -------------------------------------------------------------------------------- 1 | import type { ConstructTransformationSettings } from "../lib/transformations.js"; 2 | 3 | export interface QualifierConverters { 4 | convert: (value: any) => ConstructTransformationSettings["value"]; 5 | test: (value: any) => boolean; 6 | } 7 | 8 | export interface QualifierConfig { 9 | location?: string; 10 | order?: number; 11 | prefix?: string; 12 | qualifier?: string | boolean; 13 | converters?: ReadonlyArray; 14 | } 15 | -------------------------------------------------------------------------------- /packages/url-loader/src/types/video.ts: -------------------------------------------------------------------------------- 1 | import type { AbrPlugin } from "../plugins/abr.js"; 2 | import type { AssetOptions } from "./asset.js"; 3 | 4 | export interface VideoOptions extends AssetOptions, AbrPlugin.Options {} 5 | -------------------------------------------------------------------------------- /packages/url-loader/tests/lib/upload-widget.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from "vitest"; 2 | 3 | import { 4 | generateUploadWidgetResultCallback, 5 | getUploadWidgetOptions, 6 | } from "../../src/lib/upload-widget.js"; 7 | import { generateSignatureCallback } from "../../src/lib/upload.js"; 8 | 9 | describe("upload-widget", () => { 10 | describe("getUploadWidgetOptions", () => { 11 | it("should return create an options object for unsigned requests", () => { 12 | const options = { 13 | uploadPreset: "mypreset", 14 | }; 15 | 16 | const config = { 17 | cloud: { 18 | cloudName: "testcloud", 19 | apiKey: "abcd1234", 20 | }, 21 | }; 22 | 23 | const expectedOptions = { 24 | cloudName: config.cloud.cloudName, 25 | apiKey: config.cloud.apiKey, 26 | uploadPreset: options.uploadPreset, 27 | }; 28 | 29 | expect(getUploadWidgetOptions(options, config)).toMatchObject( 30 | expectedOptions, 31 | ); 32 | }); 33 | 34 | it("should return create an options object with minimal config for signed requests", () => { 35 | const options = { 36 | uploadSignature: generateSignatureCallback({ 37 | signatureEndpoint: "/asdf", 38 | fetch, 39 | }), 40 | }; 41 | 42 | const config = { 43 | cloud: { 44 | cloudName: "testcloud", 45 | apiKey: "abcd1234", 46 | }, 47 | }; 48 | 49 | const expectedOptions = { 50 | cloudName: config.cloud.cloudName, 51 | apiKey: config.cloud.apiKey, 52 | uploadSignature: options.uploadSignature, 53 | }; 54 | 55 | expect(getUploadWidgetOptions(options, config)).toMatchObject( 56 | expectedOptions, 57 | ); 58 | }); 59 | }); 60 | 61 | describe("getUploadWidgetOptions", () => { 62 | it("should generate a callback function and invoke an error", () => { 63 | function onError() {} 64 | 65 | function onResult() {} 66 | 67 | function onSuccess() {} 68 | 69 | const options = { 70 | onError, 71 | onResult, 72 | onSuccess, 73 | }; 74 | 75 | const spyError = vi.spyOn(options, "onError"); 76 | const spyResult = vi.spyOn(options, "onResult"); 77 | const spySuccess = vi.spyOn(options, "onSuccess"); 78 | 79 | const resultCallback = generateUploadWidgetResultCallback(options); 80 | 81 | const error = "Error"; 82 | const result = {}; 83 | 84 | resultCallback(error, result); 85 | 86 | expect(spyError).toHaveBeenCalledWith(error, result); 87 | expect(spyResult).toHaveBeenCalledWith(result); 88 | expect(spySuccess).not.toHaveBeenCalled(); 89 | }); 90 | 91 | it("should generate a callback function and invoke results on success", () => { 92 | function onError() {} 93 | 94 | function onResult() {} 95 | 96 | function onSuccess() {} 97 | 98 | const options = { 99 | onError, 100 | onResult, 101 | onSuccess, 102 | }; 103 | 104 | const spyError = vi.spyOn(options, "onError"); 105 | const spyResult = vi.spyOn(options, "onResult"); 106 | const spySuccess = vi.spyOn(options, "onSuccess"); 107 | 108 | const resultCallback = generateUploadWidgetResultCallback(options); 109 | 110 | const result = { 111 | event: "success", 112 | }; 113 | 114 | resultCallback(null, result); 115 | 116 | expect(spyError).not.toHaveBeenCalled(); 117 | expect(spyResult).toHaveBeenCalledWith(result); 118 | expect(spySuccess).toHaveBeenCalledWith(result); 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /packages/url-loader/tests/lib/upload.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { generateSignatureCallback } from "../../src/lib/upload.js"; 4 | 5 | describe("upload", () => { 6 | describe("generateSignatureCallback", () => { 7 | it("should generate a signature callback function", async () => { 8 | let results: unknown = undefined; 9 | 10 | const paramsToSign = { 11 | uploadPreset: "mypreset", 12 | timestamp: Date.now(), 13 | }; 14 | 15 | // this isn't really the signature's signature, just an easy way to test as a string 16 | 17 | const signature = JSON.stringify(paramsToSign); 18 | 19 | async function fetcher() { 20 | return { 21 | json: async () => { 22 | return { 23 | signature, 24 | }; 25 | }, 26 | }; 27 | } 28 | 29 | const signatureCallback = generateSignatureCallback({ 30 | signatureEndpoint: "/asdf", 31 | fetch: fetcher, 32 | }); 33 | 34 | function callback(signature: unknown) { 35 | results = signature; 36 | } 37 | 38 | const options = { 39 | callback, 40 | }; 41 | 42 | signatureCallback(options.callback, paramsToSign); 43 | 44 | await expect.poll(() => results).toBe(signature); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/url-loader/tests/lib/video-player.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { getVideoPlayerOptions } from "../../src/lib/video-player.js"; 4 | 5 | describe("video-player", () => { 6 | describe("getVideoPlayerOptions", () => { 7 | describe("Basic", () => { 8 | it("should return create an options object with minimal config", () => { 9 | const options = { 10 | width: "1620", 11 | height: "1080", 12 | src: "videos/mountain-stars", 13 | }; 14 | 15 | const config = { 16 | cloud: { 17 | cloudName: "testcloud", 18 | }, 19 | }; 20 | 21 | const expectedOptions = { 22 | aspectRatio: "1620:1080", 23 | autoplay: false, 24 | autoplayMode: undefined, 25 | cloud_name: "testcloud", 26 | controls: true, 27 | height: "1080", 28 | language: undefined, 29 | languages: undefined, 30 | loop: false, 31 | muted: false, 32 | privateCdn: undefined, 33 | publicId: "videos/mountain-stars", 34 | secureDistribution: undefined, 35 | showLogo: true, 36 | transformation: [{ quality: "auto" }, undefined], 37 | width: "1620", 38 | }; 39 | 40 | expect(getVideoPlayerOptions(options, config)).toMatchObject( 41 | expectedOptions, 42 | ); 43 | }); 44 | 45 | it("should return create an options object without a width or height", () => { 46 | const options = { 47 | src: "videos/mountain-stars", 48 | }; 49 | 50 | const config = { 51 | cloud: { 52 | cloudName: "testcloud", 53 | }, 54 | }; 55 | 56 | const expectedOptions = { 57 | autoplay: false, 58 | autoplayMode: undefined, 59 | cloud_name: "testcloud", 60 | controls: true, 61 | language: undefined, 62 | languages: undefined, 63 | loop: false, 64 | muted: false, 65 | privateCdn: undefined, 66 | publicId: "videos/mountain-stars", 67 | secureDistribution: undefined, 68 | showLogo: true, 69 | transformation: [{ quality: "auto" }, undefined], 70 | }; 71 | 72 | const playerOptions = getVideoPlayerOptions(options, config); 73 | 74 | expect(playerOptions).toMatchObject(expectedOptions); 75 | expect(playerOptions.width).toBeUndefined(); 76 | expect(playerOptions.height).toBeUndefined(); 77 | expect(playerOptions.aspectRatio).toBeUndefined(); 78 | }); 79 | 80 | it("should return create an options object with only an aspect ratio", () => { 81 | const options = { 82 | src: "videos/mountain-stars", 83 | aspectRatio: "16:9", 84 | }; 85 | 86 | const config = { 87 | cloud: { 88 | cloudName: "testcloud", 89 | }, 90 | }; 91 | 92 | const expectedOptions = { 93 | aspectRatio: options.aspectRatio, 94 | autoplay: false, 95 | autoplayMode: undefined, 96 | cloud_name: "testcloud", 97 | controls: true, 98 | language: undefined, 99 | languages: undefined, 100 | loop: false, 101 | muted: false, 102 | privateCdn: undefined, 103 | publicId: "videos/mountain-stars", 104 | secureDistribution: undefined, 105 | showLogo: true, 106 | transformation: [{ quality: "auto" }, undefined], 107 | }; 108 | 109 | const playerOptions = getVideoPlayerOptions(options, config); 110 | 111 | expect(playerOptions).toMatchObject(expectedOptions); 112 | expect(playerOptions.width).toBeUndefined(); 113 | expect(playerOptions.height).toBeUndefined(); 114 | }); 115 | 116 | it("should configure custom quality", () => { 117 | const options = { 118 | width: "1620", 119 | height: "1080", 120 | src: "videos/mountain-stars", 121 | quality: 50, 122 | }; 123 | 124 | const config = { 125 | cloud: { 126 | cloudName: "testcloud", 127 | }, 128 | }; 129 | 130 | expect(getVideoPlayerOptions(options, config)).toMatchObject({ 131 | transformation: [{ quality: options.quality }, undefined], 132 | }); 133 | }); 134 | }); 135 | 136 | describe("Playback", () => { 137 | it("should configure ABR via source types", () => { 138 | const options = { 139 | width: "1620", 140 | height: "1080", 141 | src: "videos/mountain-stars", 142 | transformation: { 143 | streaming_profile: "hd", 144 | }, 145 | sourceTypes: ["hls"], 146 | }; 147 | 148 | const config = { 149 | cloud: { 150 | cloudName: "testcloud", 151 | }, 152 | }; 153 | 154 | expect(getVideoPlayerOptions(options, config)).toMatchObject({ 155 | transformation: [{ quality: "auto" }, { streaming_profile: "hd" }], 156 | sourceTypes: ["hls"], 157 | }); 158 | }); 159 | }); 160 | 161 | describe("Customization", () => { 162 | it("should configure custom logo and colors", () => { 163 | const options = { 164 | width: "1620", 165 | height: "1080", 166 | src: "videos/mountain-stars", 167 | fontFace: "Colby", 168 | logo: { 169 | imageUrl: "https://image.com", 170 | onClickUrl: "https://spacejelly.dev", 171 | }, 172 | colors: { 173 | accent: "#ff0000", 174 | base: "#00ff00", 175 | text: "#0000ff", 176 | }, 177 | }; 178 | 179 | const config = { 180 | cloud: { 181 | cloudName: "testcloud", 182 | }, 183 | }; 184 | 185 | expect(getVideoPlayerOptions(options, config)).toMatchObject({ 186 | fontFace: options.fontFace, 187 | logoImageUrl: options.logo.imageUrl, 188 | logoOnclickUrl: options.logo.onClickUrl, 189 | showLogo: true, 190 | colors: options.colors, 191 | }); 192 | }); 193 | 194 | it("should set a custom poster using a string", () => { 195 | const options = { 196 | width: "1620", 197 | height: "1080", 198 | src: "videos/mountain-stars", 199 | poster: "string", 200 | }; 201 | 202 | const config = { 203 | cloud: { 204 | cloudName: "testcloud", 205 | }, 206 | }; 207 | 208 | const playerOptions = getVideoPlayerOptions(options, config); 209 | 210 | expect(playerOptions.posterOptions?.publicId).toContain(options.poster); 211 | }); 212 | 213 | it("should set a custom poster using constructCloudinaryUrl options syntax with public ID inherited", () => { 214 | const options = { 215 | width: "1620", 216 | height: "1080", 217 | src: "videos/mountain-stars", 218 | poster: { 219 | tint: "equalize:80:blue:blueviolet", 220 | }, 221 | }; 222 | 223 | const config = { 224 | cloud: { 225 | cloudName: "testcloud", 226 | }, 227 | }; 228 | 229 | const playerOptions = getVideoPlayerOptions(options, config); 230 | 231 | expect(playerOptions.posterOptions?.publicId).toContain( 232 | `https://res.cloudinary.com/${config.cloud.cloudName}/video/upload/e_tint:${options.poster.tint}/f_auto:image/q_auto/v1/${options.src}`, 233 | ); 234 | }); 235 | 236 | it("should set a custom poster using constructCloudinaryUrl options syntax with custom thumb public ID", () => { 237 | const options = { 238 | width: "1620", 239 | height: "1080", 240 | src: "videos/mountain-stars", 241 | poster: { 242 | src: "my-custom-public-id", 243 | tint: "equalize:80:blue:blueviolet", 244 | }, 245 | }; 246 | 247 | const config = { 248 | cloud: { 249 | cloudName: "testcloud", 250 | }, 251 | }; 252 | 253 | const playerOptions = getVideoPlayerOptions(options, config); 254 | 255 | expect(playerOptions.posterOptions?.publicId).toContain( 256 | `https://res.cloudinary.com/${config.cloud.cloudName}/image/upload/e_tint:${options.poster.tint}/f_auto/q_auto/v1/${options.poster.src}`, 257 | ); 258 | }); 259 | }); 260 | }); 261 | }); 262 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/abr.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { AbrPlugin } from "../../src/plugins/abr.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | describe("Plugins", () => { 13 | describe("ABR", () => { 14 | it("should include sp_auto on a video with streamingProfile of auto", () => { 15 | const src = "turtle.mp4"; 16 | const cldVideo = cld.video(src); 17 | const streamingProfile = "auto"; 18 | AbrPlugin.apply(cldVideo, { 19 | assetType: "video", 20 | src, 21 | streamingProfile, 22 | }); 23 | expect(cldVideo.toURL()).toContain( 24 | `video/upload/sp_${streamingProfile}/${src}`, 25 | ); 26 | }); 27 | 28 | it("should include streamingProfile with subtitles", () => { 29 | const src = "turtle.mp4"; 30 | const streamingProfile = 31 | "sd:subtitles_((code_en-US;file_outdoors.vtt);(code_es-ES;file_outdoors-es.vtt))"; 32 | const cldVideo = cld.video(src); 33 | AbrPlugin.apply(cldVideo, { 34 | assetType: "video", 35 | src, 36 | streamingProfile, 37 | }); 38 | expect(cldVideo.toURL()).toContain( 39 | `video/upload/sp_${streamingProfile}/${src}`, 40 | ); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/default.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { afterEach, describe, expect, it, vi } from "vitest"; 3 | 4 | import { DefaultImagePlugin } from "../../src/plugins/default-image.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | global.console = { 15 | ...global.console, 16 | warn: vi.fn(), 17 | }; 18 | 19 | describe("Default Image plugin", () => { 20 | afterEach(() => { 21 | // Clears the state of console.warn, in case multiple tests want to monitor it 22 | vi.restoreAllMocks(); 23 | }); 24 | 25 | it("should add a default image", () => { 26 | const cldImage = cld.image(TEST_PUBLIC_ID); 27 | const options = { 28 | src: TEST_PUBLIC_ID, 29 | width: 100, 30 | height: 100, 31 | defaultImage: "my-image.jpg", 32 | }; 33 | DefaultImagePlugin.apply(cldImage, options); 34 | expect(cldImage.toURL()).toContain( 35 | `d_${options.defaultImage}/${TEST_PUBLIC_ID}`, 36 | ); 37 | }); 38 | it("should warn if no format", () => { 39 | const cldImage = cld.image(TEST_PUBLIC_ID); 40 | const options = { 41 | src: TEST_PUBLIC_ID, 42 | width: 100, 43 | height: 100, 44 | defaultImage: "my-image", 45 | }; 46 | DefaultImagePlugin.apply(cldImage, options); 47 | expect((console.warn as any).mock.calls[0][0]).toContain( 48 | "The defaultImage prop may be missing", 49 | ); 50 | expect(cldImage.toURL()).toContain( 51 | `d_${options.defaultImage}/${TEST_PUBLIC_ID}`, 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/effects.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { EffectsPlugin } from "../../src/plugins/effects.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | describe("Plugins", () => { 15 | it("should apply effects ", () => { 16 | const cldImage = cld.image(TEST_PUBLIC_ID); 17 | 18 | const shear = "40:0"; 19 | const opacity = "50"; 20 | 21 | const options = { 22 | src: TEST_PUBLIC_ID, 23 | shear, 24 | opacity, 25 | }; 26 | 27 | EffectsPlugin.apply(cldImage, options); 28 | 29 | expect(cldImage.toURL()).toContain(`/o_${opacity}/e_shear:${shear}/`); 30 | }); 31 | it("should apply effects by array", () => { 32 | const cldImage = cld.image(TEST_PUBLIC_ID); 33 | 34 | const shear = "40:0"; 35 | const gradientFade = true; 36 | const opacity = "50"; 37 | const cartoonify = "50"; 38 | const radius = "150"; 39 | 40 | const options = { 41 | src: TEST_PUBLIC_ID, 42 | effects: [ 43 | { 44 | shear, 45 | opacity, 46 | }, 47 | { 48 | gradientFade, 49 | cartoonify, 50 | radius, 51 | }, 52 | ], 53 | }; 54 | 55 | EffectsPlugin.apply(cldImage, options); 56 | 57 | expect(cldImage.toURL()).toContain( 58 | `/o_${opacity},e_shear:${shear}/e_cartoonify:${cartoonify},e_gradient_fade,r_${radius}/`, 59 | ); 60 | }); 61 | 62 | it("should colorize with a hex color value", () => { 63 | const cldImage = cld.image(TEST_PUBLIC_ID); 64 | 65 | const color = "#ff00ff"; 66 | const colorExpected = "rgb:ff00ff"; 67 | const colorize = 50; 68 | 69 | const options = { 70 | src: TEST_PUBLIC_ID, 71 | effects: [ 72 | { 73 | color, 74 | colorize, 75 | }, 76 | ], 77 | }; 78 | 79 | EffectsPlugin.apply(cldImage, options); 80 | 81 | expect(cldImage.toURL()).toContain( 82 | `/co_${colorExpected},e_colorize:${colorize}/`, 83 | ); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/extract.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { ExtractPlugin } from "../../src/plugins/extract.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | describe("Plugins", () => { 15 | describe("Extract", () => { 16 | it("should extract by single prompt", () => { 17 | const cldImage = cld.image(TEST_PUBLIC_ID); 18 | 19 | const options = { 20 | src: TEST_PUBLIC_ID, 21 | extract: "space jellyfish", 22 | }; 23 | 24 | ExtractPlugin.apply(cldImage, options); 25 | 26 | expect(cldImage.toURL()).toContain( 27 | `/e_extract:prompt_${encodeURIComponent(options.extract)}/`, 28 | ); 29 | }); 30 | 31 | it("should not add extract if no options detected", () => { 32 | const cldImage = cld.image(TEST_PUBLIC_ID); 33 | 34 | const options = { 35 | src: TEST_PUBLIC_ID, 36 | extract: {}, 37 | }; 38 | 39 | ExtractPlugin.apply(cldImage, options); 40 | 41 | expect(cldImage.toURL()).not.toContain(`/e_extract/`); 42 | }); 43 | 44 | it("should extract by array of prompts", () => { 45 | const cldImage = cld.image(TEST_PUBLIC_ID); 46 | 47 | const options = { 48 | src: TEST_PUBLIC_ID, 49 | extract: ["space jellyfish", "octocat"], 50 | }; 51 | 52 | ExtractPlugin.apply(cldImage, options); 53 | 54 | expect(cldImage.toURL()).toContain( 55 | `/e_extract:prompt_(${options.extract.map((p) => encodeURIComponent(p)).join(";")})/`, 56 | ); 57 | }); 58 | 59 | it("should extract by object", () => { 60 | const cldImage = cld.image(TEST_PUBLIC_ID); 61 | 62 | const options = { 63 | src: TEST_PUBLIC_ID, 64 | extract: { 65 | prompt: "space jellyfish", 66 | multiple: true, 67 | mode: "mask", 68 | invert: true, 69 | }, 70 | } as const; 71 | 72 | ExtractPlugin.apply(cldImage, options); 73 | 74 | expect(cldImage.toURL()).toContain( 75 | `/e_extract:prompt_${encodeURIComponent(options.extract.prompt)};invert_${options.extract.invert};mode_${options.extract.mode};multiple_${options.extract.multiple}/`, 76 | ); 77 | }); 78 | 79 | it("should extract by object with array of prompts", () => { 80 | const cldImage = cld.image(TEST_PUBLIC_ID); 81 | 82 | const options = { 83 | src: TEST_PUBLIC_ID, 84 | extract: { 85 | prompt: ["space jellyfish", "octocat"], 86 | mode: "mask", 87 | }, 88 | } as const; 89 | 90 | ExtractPlugin.apply(cldImage, options); 91 | 92 | expect(cldImage.toURL()).toContain( 93 | `/e_extract:prompt_(${options.extract.prompt.map((p) => encodeURIComponent(p)).join(";")});mode_${options.extract.mode}/`, 94 | ); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/fill-background.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { FillBackgroundPlugin } from "../../src/plugins/fill-background.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | describe("Plugins", () => { 15 | describe("Fill Background", () => { 16 | it("should generate a background with basic settings", () => { 17 | const cldImage = cld.image(TEST_PUBLIC_ID); 18 | 19 | const options = { 20 | src: TEST_PUBLIC_ID, 21 | width: 800, 22 | height: 600, 23 | fillBackground: true, 24 | }; 25 | 26 | FillBackgroundPlugin.apply(cldImage, options); 27 | 28 | expect(cldImage.toURL()).toContain( 29 | `b_gen_fill,ar_${options.width}:${options.height},c_pad/${TEST_PUBLIC_ID}`, 30 | ); 31 | }); 32 | 33 | it("should generate with custom options", () => { 34 | const cldImage = cld.image(TEST_PUBLIC_ID); 35 | 36 | const options = { 37 | src: TEST_PUBLIC_ID, 38 | width: 800, 39 | height: 600, 40 | fillBackground: { 41 | gravity: "east", 42 | prompt: "pink and purple flowers", 43 | crop: "mpad", 44 | }, 45 | } as const; 46 | 47 | FillBackgroundPlugin.apply(cldImage, options); 48 | 49 | expect(cldImage.toURL()).toContain( 50 | `b_gen_fill:${encodeURIComponent(options.fillBackground.prompt)},ar_${options.width}:${options.height},c_${options.fillBackground.crop},g_${options.fillBackground.gravity}/${TEST_PUBLIC_ID}`, 51 | ); 52 | }); 53 | 54 | it("should not add generative fill if does not include aspect ratio", () => { 55 | const cldImage = cld.image(TEST_PUBLIC_ID); 56 | 57 | const options = { 58 | src: TEST_PUBLIC_ID, 59 | fillBackground: true, 60 | }; 61 | 62 | FillBackgroundPlugin.apply(cldImage, options); 63 | 64 | expect(cldImage.toURL()).toContain(`image/upload/${TEST_PUBLIC_ID}`); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/flags.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { FlagsPlugin } from "../../src/plugins/flags.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | describe("Plugins", () => { 13 | describe("Flags", () => { 14 | it("should include a flag on an image", () => { 15 | const src = "turtle"; 16 | const assetType = "image"; 17 | const cldVideo = cld.image(src); 18 | const flags = "keep_iptc"; 19 | const options = { 20 | assetType, 21 | src, 22 | flags, 23 | } as const; 24 | 25 | FlagsPlugin.apply(cldVideo, options); 26 | 27 | expect(cldVideo.toURL()).toContain( 28 | `${assetType}/upload/fl_keep_iptc/${src}`, 29 | ); 30 | }); 31 | 32 | it("should add multiple flags to a video", () => { 33 | const src = "turtle.mp4"; 34 | const assetType = "video"; 35 | const cldVideo = cld.video(src); 36 | const flags = ["no_stream", "splice"] as const; 37 | 38 | const options = { 39 | assetType, 40 | src, 41 | flags, 42 | } as const; 43 | 44 | FlagsPlugin.apply(cldVideo, options); 45 | 46 | expect(cldVideo.toURL()).toContain( 47 | `${assetType}/upload/fl_no_stream/fl_splice/${src}`, 48 | ); 49 | }); 50 | 51 | it("should add custom flag definitions via object syntax", () => { 52 | const src = "turtle"; 53 | const assetType = "image"; 54 | const cldVideo = cld.image(src); 55 | const flags = { 56 | splice: "transition_(name_circleopen;du_2.5)", 57 | attachment: "space_jellyfish", 58 | }; 59 | const flagsString = Object.entries(flags) 60 | .map(([q, v]) => `fl_${q}:${v}`) 61 | .join("/"); 62 | 63 | const options = { 64 | assetType, 65 | src, 66 | flags, 67 | } as const; 68 | 69 | FlagsPlugin.apply(cldVideo, options); 70 | expect(cldVideo.toURL()).toContain( 71 | `${assetType}/upload/${flagsString}/${src}`, 72 | ); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/named-transformations.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { NamedTransformationsPlugin } from "../../src/plugins/named-transformations.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | describe("Plugins", () => { 15 | describe("Named Transformations", () => { 16 | it("should apply a single named transformation to a Cloudinary URL", () => { 17 | const cldImage = cld.image(TEST_PUBLIC_ID); 18 | 19 | const options = { 20 | src: TEST_PUBLIC_ID, 21 | namedTransformations: "my-transformation", 22 | }; 23 | 24 | NamedTransformationsPlugin.apply(cldImage, options); 25 | 26 | expect(cldImage.toURL()).toContain( 27 | `t_${options.namedTransformations}/${TEST_PUBLIC_ID}`, 28 | ); 29 | }); 30 | 31 | it("should apply an array of named transformations to a Cloudinary URL", () => { 32 | const cldImage = cld.image(TEST_PUBLIC_ID); 33 | 34 | const options = { 35 | src: TEST_PUBLIC_ID, 36 | namedTransformations: ["my-transformation", "my-other-transformation"], 37 | }; 38 | 39 | NamedTransformationsPlugin.apply(cldImage, options); 40 | 41 | expect(cldImage.toURL()).toContain( 42 | `t_${options.namedTransformations.join("/t_")}/${TEST_PUBLIC_ID}`, 43 | ); 44 | }); 45 | }); 46 | 47 | // @todo - deprecate in favor of namedTransformations 48 | 49 | describe("Named Transformations", () => { 50 | it("should apply a single named transformation to a Cloudinary URL", () => { 51 | const cldImage = cld.image(TEST_PUBLIC_ID); 52 | 53 | const options = { 54 | src: TEST_PUBLIC_ID, 55 | transformations: "my-transformation", 56 | }; 57 | 58 | NamedTransformationsPlugin.apply(cldImage, options); 59 | 60 | expect(cldImage.toURL()).toContain( 61 | `t_${options.transformations}/${TEST_PUBLIC_ID}`, 62 | ); 63 | }); 64 | 65 | it("should apply an array of named transformations to a Cloudinary URL", () => { 66 | const cldImage = cld.image(TEST_PUBLIC_ID); 67 | 68 | const options = { 69 | src: TEST_PUBLIC_ID, 70 | transformations: ["my-transformation", "my-other-transformation"], 71 | }; 72 | 73 | NamedTransformationsPlugin.apply(cldImage, options); 74 | 75 | expect(cldImage.toURL()).toContain( 76 | `t_${options.transformations.join("/t_")}/${TEST_PUBLIC_ID}`, 77 | ); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/preserve-transformations.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { PreserveTransformationsPlugin } from "../../src/plugins/preserve-transformations.js"; 5 | 6 | const cloudName = "test-cloud-name"; 7 | 8 | const cld = new Cloudinary({ 9 | cloud: { 10 | cloudName, 11 | }, 12 | }); 13 | 14 | const TEST_PUBLIC_ID = "test-public-id"; 15 | 16 | describe("Plugins", () => { 17 | describe("Preserve Transformations", () => { 18 | it("should preserve transformations from an existing URL ", () => { 19 | const cldImage = cld.image(TEST_PUBLIC_ID); 20 | 21 | const cloudName = "customtestcloud"; 22 | const transformations = ["c_limit,w_100", "f_auto", "q_auto"]; 23 | const url = `https://res.cloudinary.com/${cloudName}/image/upload/${transformations.join("/")}/v1234/turtle`; 24 | 25 | const options = { 26 | src: url, 27 | preserveTransformations: true, 28 | }; 29 | 30 | PreserveTransformationsPlugin.apply(cldImage, options); 31 | 32 | expect(cldImage.toURL()).toContain( 33 | `/image/upload/${transformations.join("/")}/`, 34 | ); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/raw-transformations.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { RawTransformationsPlugin } from "../../src/plugins/raw-transformations.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | describe("Plugins", () => { 15 | describe("Raw Transformations", () => { 16 | it("should apply a single raw transformations to the end of a Cloudinary URL", () => { 17 | const cldImage = cld.image(TEST_PUBLIC_ID); 18 | 19 | const options = { 20 | src: TEST_PUBLIC_ID, 21 | rawTransformations: ["e_blur:2000"], 22 | }; 23 | 24 | RawTransformationsPlugin.apply(cldImage, options); 25 | 26 | expect(cldImage.toURL()).toContain( 27 | `${options.rawTransformations.join("/")}/${TEST_PUBLIC_ID}`, 28 | ); 29 | }); 30 | 31 | it("should apply an array of raw transformations to the end of a Cloudinary URL", () => { 32 | const cldImage = cld.image(TEST_PUBLIC_ID); 33 | 34 | const options = { 35 | src: TEST_PUBLIC_ID, 36 | rawTransformations: ["e_blur:2000", "e_tint:100:0000FF:0p:FF1493:100p"], 37 | }; 38 | 39 | RawTransformationsPlugin.apply(cldImage, options); 40 | 41 | expect(cldImage.toURL()).toContain( 42 | `${options.rawTransformations.join("/")}/${TEST_PUBLIC_ID}`, 43 | ); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/recolor.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { RecolorPlugin } from "../../src/plugins/recolor.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | describe("Plugins", () => { 15 | describe("Recolor", () => { 16 | it("should recolor an object by Array", () => { 17 | const cldImage = cld.image(TEST_PUBLIC_ID); 18 | 19 | const options = { 20 | src: TEST_PUBLIC_ID, 21 | recolor: ["duck", "blue"], 22 | }; 23 | 24 | RecolorPlugin.apply(cldImage, options); 25 | 26 | expect(cldImage.toURL()).toContain( 27 | `e_gen_recolor:prompt_duck;to-color_blue`, 28 | ); 29 | }); 30 | 31 | it("should recolor multiple objects by an array", () => { 32 | const cldImage = cld.image(TEST_PUBLIC_ID); 33 | 34 | const options = { 35 | src: TEST_PUBLIC_ID, 36 | recolor: [["duck", "horse"], "blue"] as const, 37 | }; 38 | 39 | RecolorPlugin.apply(cldImage, options); 40 | 41 | expect(cldImage.toURL()).toContain( 42 | `e_gen_recolor:prompt_(duck;horse);to-color_blue`, 43 | ); 44 | }); 45 | 46 | it("should recolor an object by object configuration", () => { 47 | const cldImage = cld.image(TEST_PUBLIC_ID); 48 | 49 | const options = { 50 | src: TEST_PUBLIC_ID, 51 | recolor: { 52 | prompt: "duck", 53 | to: "blue", 54 | multiple: true, 55 | }, 56 | }; 57 | 58 | RecolorPlugin.apply(cldImage, options); 59 | 60 | expect(cldImage.toURL()).toContain( 61 | `e_gen_recolor:prompt_duck;to-color_blue;multiple_true`, 62 | ); 63 | }); 64 | 65 | it("should recolor multiple objects by object configuration", () => { 66 | const cldImage = cld.image(TEST_PUBLIC_ID); 67 | 68 | const options = { 69 | src: TEST_PUBLIC_ID, 70 | recolor: { 71 | prompt: ["duck", "horse"], 72 | to: "blue", 73 | }, 74 | }; 75 | 76 | RecolorPlugin.apply(cldImage, options); 77 | 78 | expect(cldImage.toURL()).toContain( 79 | `e_gen_recolor:prompt_(duck;horse);to-color_blue`, 80 | ); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/remove-background.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { RemoveBackgroundPlugin } from "../../src/plugins/remove-background.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | describe("Plugins", () => { 15 | describe("Remove Background", () => { 16 | it("should remove the background", () => { 17 | const cldImage = cld.image(TEST_PUBLIC_ID); 18 | 19 | const options = { 20 | src: TEST_PUBLIC_ID, 21 | removeBackground: true, 22 | }; 23 | 24 | RemoveBackgroundPlugin.apply(cldImage, options); 25 | 26 | expect(cldImage.toURL()).toContain(`e_background_removal`); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/remove.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { RemovePlugin } from "../../src/plugins/remove.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | describe("Plugins", () => { 15 | describe("Remove", () => { 16 | it("should remove an object by string", () => { 17 | const cldImage = cld.image(TEST_PUBLIC_ID); 18 | 19 | const options = { 20 | src: TEST_PUBLIC_ID, 21 | remove: "apple", 22 | }; 23 | 24 | RemovePlugin.apply(cldImage, options); 25 | 26 | expect(cldImage.toURL()).toContain(`e_gen_remove:prompt_apple`); 27 | }); 28 | 29 | it("should remove an object by array", () => { 30 | const cldImage = cld.image(TEST_PUBLIC_ID); 31 | 32 | const options = { 33 | src: TEST_PUBLIC_ID, 34 | remove: ["apple", "banana", "orange"], 35 | }; 36 | 37 | RemovePlugin.apply(cldImage, options); 38 | 39 | expect(cldImage.toURL()).toContain( 40 | `e_gen_remove:prompt_(apple;banana;orange)`, 41 | ); 42 | }); 43 | 44 | it("should remove an object with object configuration", () => { 45 | const cldImage = cld.image(TEST_PUBLIC_ID); 46 | 47 | const options = { 48 | src: TEST_PUBLIC_ID, 49 | remove: { 50 | prompt: ["apple", "banana"], 51 | multiple: true, 52 | removeShadow: true, 53 | }, 54 | }; 55 | 56 | RemovePlugin.apply(cldImage, options); 57 | 58 | expect(cldImage.toURL()).toContain( 59 | `e_gen_remove:prompt_(apple;banana);multiple_true;remove-shadow_true`, 60 | ); 61 | }); 62 | 63 | it("should remove an object with region", () => { 64 | const cldImage = cld.image(TEST_PUBLIC_ID); 65 | 66 | const options = { 67 | src: TEST_PUBLIC_ID, 68 | remove: { 69 | region: [300, 200, 1900, 3500], 70 | }, 71 | }; 72 | 73 | RemovePlugin.apply(cldImage, options); 74 | 75 | expect(cldImage.toURL()).toContain(`region_(x_300;y_200;w_1900;h_3500)`); 76 | }); 77 | 78 | it("should remove an object with multi-level region", () => { 79 | const cldImage = cld.image(TEST_PUBLIC_ID); 80 | 81 | const options = { 82 | src: TEST_PUBLIC_ID, 83 | remove: { 84 | region: [ 85 | [300, 200, 1900, 3500], 86 | [123, 321, 750, 500], 87 | ], 88 | }, 89 | }; 90 | 91 | RemovePlugin.apply(cldImage, options); 92 | 93 | expect(cldImage.toURL()).toContain( 94 | `region_((x_300;y_200;w_1900;h_3500);(x_123;y_321;w_750;h_500))`, 95 | ); 96 | }); 97 | 98 | it("should not allow both a prompt and a region", () => { 99 | const cldImage = cld.image(TEST_PUBLIC_ID); 100 | 101 | const options = { 102 | src: TEST_PUBLIC_ID, 103 | remove: { 104 | prompt: "apple", 105 | region: [ 106 | [300, 200, 1900, 3500], 107 | [123, 321, 750, 500], 108 | ], 109 | }, 110 | }; 111 | 112 | expect(() => RemovePlugin.apply(cldImage, options)).toThrow( 113 | "Invalid remove options: you can not have both a prompt and a region. More info: https://cloudinary.com/documentation/transformation_reference#e_gen_remove", 114 | ); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/replace-background.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { ReplaceBackgroundPlugin } from "../../src/plugins/replace-background.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | describe("Plugins", () => { 15 | describe("Generative Replace Background", () => { 16 | it("should replace the background", () => { 17 | const cldImage = cld.image(TEST_PUBLIC_ID); 18 | 19 | const options = { 20 | src: TEST_PUBLIC_ID, 21 | replaceBackground: {}, 22 | }; 23 | 24 | ReplaceBackgroundPlugin.apply(cldImage, options); 25 | 26 | expect(cldImage.toURL()).toContain("/e_gen_background_replace/"); 27 | }); 28 | 29 | it("should replace the background with a prompt", () => { 30 | const cldImage = cld.image(TEST_PUBLIC_ID); 31 | 32 | const options = { 33 | src: TEST_PUBLIC_ID, 34 | replaceBackground: { 35 | prompt: "space jellyfish in space", 36 | }, 37 | }; 38 | 39 | ReplaceBackgroundPlugin.apply(cldImage, options); 40 | 41 | expect(cldImage.toURL()).toContain( 42 | `/e_gen_background_replace:prompt_${encodeURIComponent(options.replaceBackground.prompt)}/`, 43 | ); 44 | }); 45 | 46 | it("should replace the background with a prompt", () => { 47 | const cldImage = cld.image(TEST_PUBLIC_ID); 48 | 49 | const options = { 50 | src: TEST_PUBLIC_ID, 51 | replaceBackground: { 52 | prompt: "space jellyfish in outer space", 53 | seed: 2, 54 | }, 55 | }; 56 | 57 | ReplaceBackgroundPlugin.apply(cldImage, options); 58 | 59 | expect(cldImage.toURL()).toContain( 60 | `/e_gen_background_replace:prompt_${encodeURIComponent(options.replaceBackground.prompt)};seed_${options.replaceBackground.seed}/`, 61 | ); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/replace.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { ReplacePlugin } from "../../src/plugins/replace.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | describe("Plugins", () => { 15 | describe("Generative Replace", () => { 16 | it("should replace with object", () => { 17 | const cldImage = cld.image(TEST_PUBLIC_ID); 18 | 19 | const options = { 20 | src: TEST_PUBLIC_ID, 21 | replace: { 22 | from: "apple", 23 | to: "orange", 24 | }, 25 | }; 26 | 27 | ReplacePlugin.apply(cldImage, options); 28 | 29 | expect(cldImage.toURL()).toContain(`e_gen_replace:from_apple;to_orange`); 30 | }); 31 | 32 | it("should replace with object and preserved geometry", () => { 33 | const cldImage = cld.image(TEST_PUBLIC_ID); 34 | 35 | const options = { 36 | src: TEST_PUBLIC_ID, 37 | replace: { 38 | from: "apple", 39 | to: "orange", 40 | preserveGeometry: true, 41 | }, 42 | }; 43 | 44 | ReplacePlugin.apply(cldImage, options); 45 | 46 | expect(cldImage.toURL()).toContain( 47 | `e_gen_replace:from_apple;to_orange;preserve-geometry_true`, 48 | ); 49 | }); 50 | 51 | it("should replace with array", () => { 52 | const cldImage = cld.image(TEST_PUBLIC_ID); 53 | 54 | const options = { 55 | src: TEST_PUBLIC_ID, 56 | replace: ["apple", "orange"], 57 | }; 58 | 59 | ReplacePlugin.apply(cldImage, options); 60 | 61 | expect(cldImage.toURL()).toContain(`e_gen_replace:from_apple;to_orange`); 62 | }); 63 | 64 | it("should replace with array and preserved geometry", () => { 65 | const cldImage = cld.image(TEST_PUBLIC_ID); 66 | 67 | const options = { 68 | src: TEST_PUBLIC_ID, 69 | replace: ["apple", "candy bar", "true"], 70 | }; 71 | 72 | ReplacePlugin.apply(cldImage, options); 73 | 74 | expect(cldImage.toURL()).toContain( 75 | `e_gen_replace:from_apple;to_candy%20bar;preserve-geometry_true`, 76 | ); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/restore.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { RestorePlugin } from "../../src/plugins/restore.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | describe("Plugins", () => { 15 | describe("Generative Restore", () => { 16 | it("should restore", () => { 17 | const cldImage = cld.image(TEST_PUBLIC_ID); 18 | 19 | const options = { 20 | src: TEST_PUBLIC_ID, 21 | restore: true, 22 | }; 23 | 24 | RestorePlugin.apply(cldImage, options); 25 | 26 | expect(cldImage.toURL()).toContain(`e_gen_restore`); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/sanitize.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | import { SanitizePlugin } from "../../src/plugins/sanitize.js"; 4 | 5 | const cld = new Cloudinary({ 6 | cloud: { 7 | cloudName: "test-cloud-name", 8 | }, 9 | }); 10 | 11 | describe("Cloudinary Sanitize", () => { 12 | describe("constructCloudinaryUrl", () => { 13 | it("should include fl_sanitize when display image source name end with svg", () => { 14 | const src = "turtle.svg"; 15 | const cldImage = cld.image(src); 16 | SanitizePlugin.apply(cldImage, { 17 | src, 18 | }); 19 | expect(cldImage.toURL()).toContain(`image/upload/fl_sanitize/turtle.svg`); 20 | }); 21 | 22 | it("should include fl_sanitize and f_svg when display format is svg", () => { 23 | const src = "turtle"; 24 | const cldImage = cld.image(src); 25 | SanitizePlugin.apply(cldImage, { 26 | format: "svg", 27 | src, 28 | }); 29 | expect(cldImage.toURL()).toContain(`image/upload/fl_sanitize/turtle`); 30 | }); 31 | 32 | it("should include fl_sanitize and f_svg when display format svg image", () => { 33 | const src = "turtle.svg"; 34 | const cldImage = cld.image(src); 35 | SanitizePlugin.apply(cldImage, { 36 | format: "svg", 37 | src, 38 | }); 39 | expect(cldImage.toURL()).toContain(`image/upload/fl_sanitize/turtle.svg`); 40 | }); 41 | 42 | it("should not include fl_sanitize when set option sanitize to false", () => { 43 | const src = "turtle.svg"; 44 | const cldImage = cld.image(src); 45 | SanitizePlugin.apply(cldImage, { 46 | sanitize: false, 47 | src, 48 | }); 49 | expect(cldImage.toURL()).toContain(`image/upload/turtle.svg`); 50 | }); 51 | 52 | it("should not include fl_sanitize when display other image", () => { 53 | const src = "turtle"; 54 | const cldImage = cld.image(src); 55 | SanitizePlugin.apply(cldImage, { 56 | src, 57 | }); 58 | expect(cldImage.toURL()).toContain(`image/upload/turtle`); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/underlays.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { UnderlaysPlugin } from "../../src/plugins/underlays.js"; 5 | 6 | const cld = new Cloudinary({ 7 | cloud: { 8 | cloudName: "test-cloud-name", 9 | }, 10 | }); 11 | 12 | const TEST_PUBLIC_ID = "test-public-id"; 13 | 14 | describe("Plugins", () => { 15 | describe("Underlays", () => { 16 | it("should add an underlay configured by object", () => { 17 | const cldImage = cld.image(TEST_PUBLIC_ID); 18 | 19 | const publicId = "images/galaxy"; 20 | const width = 1920; 21 | const height = 1200; 22 | const crop = "fill"; 23 | 24 | const options = { 25 | src: TEST_PUBLIC_ID, 26 | underlays: [ 27 | { 28 | publicId, 29 | width, 30 | height, 31 | crop, 32 | }, 33 | ], 34 | } as const; 35 | 36 | UnderlaysPlugin.apply(cldImage, options); 37 | 38 | expect(cldImage.toURL()).toContain( 39 | `u_${publicId.replace(/\//g, ":")},w_${width},h_${height},c_${crop}/fl_layer_apply,fl_no_overflow/${TEST_PUBLIC_ID}`, 40 | ); 41 | }); 42 | 43 | it("should add an underlay by string", () => { 44 | const cldImage = cld.image(TEST_PUBLIC_ID); 45 | 46 | const publicId = "images/galaxy"; 47 | const width = "1.0"; 48 | const height = "1.0"; 49 | const crop = "fill"; 50 | 51 | const options = { 52 | src: TEST_PUBLIC_ID, 53 | underlay: publicId, 54 | }; 55 | 56 | UnderlaysPlugin.apply(cldImage, options); 57 | 58 | expect(cldImage.toURL()).toContain( 59 | `u_${publicId.replace(/\//g, ":")},c_${crop},w_${width},h_${height},fl_relative/fl_layer_apply,fl_no_overflow/${TEST_PUBLIC_ID}`, 60 | ); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /packages/url-loader/tests/plugins/zoompan.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cloudinary } from "@cloudinary/url-gen"; 2 | import { describe, expect, it } from "vitest"; 3 | import { ZoompanPlugin } from "../../src/plugins/zoompan.js"; 4 | 5 | const cld = new Cloudinary({ 6 | cloud: { 7 | cloudName: "test-cloud-name", 8 | }, 9 | }); 10 | 11 | const TEST_PUBLIC_ID = "test-public-id"; 12 | 13 | describe("Plugins", () => { 14 | describe("Zoom pan", () => { 15 | it('should add "e_zoompan"', () => { 16 | const cldImage = cld.image(TEST_PUBLIC_ID); 17 | 18 | const options = { 19 | src: TEST_PUBLIC_ID, 20 | zoompan: true, 21 | }; 22 | 23 | const result = ZoompanPlugin.apply(cldImage, options); 24 | 25 | expect(result.options?.format).toBe("auto:animated"); 26 | expect(cldImage.toURL()).toContain(`e_zoompan`); 27 | }); 28 | 29 | it("should add loop effect ", () => { 30 | const cldImage = cld.image(TEST_PUBLIC_ID); 31 | 32 | const options = { 33 | src: TEST_PUBLIC_ID, 34 | zoompan: "loop", 35 | }; 36 | 37 | ZoompanPlugin.apply(cldImage, options); 38 | 39 | expect(cldImage.toURL()).toContain(`e_zoompan`); 40 | expect(cldImage.toURL()).toContain(`e_loop`); 41 | }); 42 | 43 | it("should add a custom zoompan", () => { 44 | const cldImage = cld.image(TEST_PUBLIC_ID); 45 | 46 | const options = { 47 | src: TEST_PUBLIC_ID, 48 | zoompan: "mode_ofc;maxzoom_3.2;du_5;fps_30", 49 | }; 50 | 51 | ZoompanPlugin.apply(cldImage, options); 52 | 53 | expect(cldImage.toURL()).toContain(`e_zoompan:${options.zoompan}`); 54 | }); 55 | 56 | it("should add a custom options", () => { 57 | const cldImage = cld.image(TEST_PUBLIC_ID); 58 | 59 | const options = { 60 | src: TEST_PUBLIC_ID, 61 | zoompan: { 62 | loop: true, 63 | options: "to_(g_auto;zoom_1.4)", 64 | }, 65 | }; 66 | 67 | ZoompanPlugin.apply(cldImage, options); 68 | 69 | expect(cldImage.toURL()).toContain( 70 | `e_zoompan:${options.zoompan.options}`, 71 | ); 72 | expect(cldImage.toURL()).toContain("e_loop"); 73 | }); 74 | 75 | it("should add a custom loop option", () => { 76 | const cldImage = cld.image(TEST_PUBLIC_ID); 77 | 78 | const options = { 79 | src: TEST_PUBLIC_ID, 80 | zoompan: { 81 | options: "string", 82 | loop: 15, 83 | }, 84 | }; 85 | 86 | ZoompanPlugin.apply(cldImage, options); 87 | 88 | expect(cldImage.toURL()).toContain( 89 | `e_zoompan:${options.zoompan.options}/e_loop:${options.zoompan.loop}`, 90 | ); 91 | }); 92 | 93 | it("should not override format", () => { 94 | const cldImage = cld.image(TEST_PUBLIC_ID); 95 | 96 | const options = { 97 | src: TEST_PUBLIC_ID, 98 | zoompan: false, 99 | }; 100 | 101 | const result = ZoompanPlugin.apply(cldImage, options); 102 | 103 | expect(result.options?.format).toBe(undefined); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /packages/url-loader/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noErrorTruncation": true, 5 | "target": "ES2020", 6 | "types": ["node"] 7 | }, 8 | "include": ["."], 9 | "exclude": ["dist", "build", "node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/util/.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | "next", 5 | "next-major", 6 | { 7 | "name": "beta", 8 | "prerelease": true 9 | }, 10 | { 11 | "name": "alpha", 12 | "prerelease": true 13 | } 14 | ], 15 | "plugins": [ 16 | [ 17 | "@semantic-release/commit-analyzer", 18 | { 19 | "preset": "angular", 20 | "releaseRules": [ 21 | { 22 | "type": "docs", 23 | "scope": "README", 24 | "release": "patch" 25 | } 26 | ], 27 | "parserOpts": { 28 | "noteKeywords": [ 29 | "BREAKING CHANGE", 30 | "BREAKING CHANGES" 31 | ] 32 | } 33 | } 34 | ], 35 | "@semantic-release/release-notes-generator", 36 | [ 37 | "@semantic-release/changelog", 38 | { 39 | "changelogFile": "CHANGELOG.md" 40 | } 41 | ], 42 | [ 43 | "@colbyfayock/semantic-release-pnpm", 44 | { 45 | "publishBranch": "main|beta" 46 | } 47 | ], 48 | [ 49 | "@semantic-release/git", 50 | { 51 | "assets": [ 52 | "./package.json", 53 | "CHANGELOG.md" 54 | ] 55 | } 56 | ], 57 | "@semantic-release/github" 58 | ], 59 | "extends": "semantic-release-monorepo" 60 | } 61 | -------------------------------------------------------------------------------- /packages/util/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cloudinary 5 | 6 | 7 | ###### 8 | 9 | npm GitHub 10 | 11 | # Cloudinary Helpers 12 | 13 | Getting Started 14 | 15 | **This is a community library supported by the Cloudinary Developer Experience team.** 16 | 17 | ## 🚀 Getting Started 18 | 19 | _The minimum node version officially supported is version 18._ 20 | 21 | ``` 22 | npm install @cloudinary-util/util 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/util/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudinary-util/util", 3 | "version": "4.1.0", 4 | "type": "module", 5 | "main": "./dist/index.cjs", 6 | "types": "./dist/index.d.cts", 7 | "exports": { 8 | ".": { 9 | "require": "./dist/index.cjs", 10 | "import": "./dist/index.js" 11 | } 12 | }, 13 | "sideEffects": false, 14 | "license": "MIT", 15 | "files": [ 16 | "dist/**" 17 | ], 18 | "scripts": { 19 | "build": "tsup src/index.ts --format esm,cjs --dts --clean", 20 | "dev": "tsup src/index.ts --format esm,cjs --watch --dts", 21 | "lint": "TIMING=1 eslint \"src/**/*.ts*\"", 22 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", 23 | "prepublishOnly": "cd ../../ && pnpm build && cd packages/util", 24 | "semantic-release": "semantic-release", 25 | "test": "vitest run" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^17.0.12" 29 | }, 30 | "publishConfig": { 31 | "access": "public" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/cloudinary-community/cloudinary-util.git" 36 | }, 37 | "keywords": [], 38 | "bugs": { 39 | "url": "https://github.com/cloudinary-community/cloudinary-util/issues" 40 | }, 41 | "homepage": "https://github.com/cloudinary-community/cloudinary-util" 42 | } 43 | -------------------------------------------------------------------------------- /packages/util/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | getFormat, 3 | getPublicId, 4 | getTransformations, 5 | normalizeNumberParameter, 6 | parseUrl, 7 | pollForProcessingImage, 8 | type ParseUrl, 9 | type PollForProcessingImageOptions, 10 | } from "./lib/cloudinary.js"; 11 | export { convertColorHexToRgb, testColorIsHex } from "./lib/colors.js"; 12 | export { encodeBase64, objectHasKey, sortByKey } from "./lib/util.js"; 13 | -------------------------------------------------------------------------------- /packages/util/src/lib/cloudinary.ts: -------------------------------------------------------------------------------- 1 | const REGEX_VERSION = /\/v\d+\//; 2 | const REGEX_FORMAT = 3 | /\.(ai|avif|gif|png|webp|bmp|bw|djvu|dng|ps|ept|eps|eps3|fbx|flif|gif|glb|gltf|heif|heic|ico|indd|jpg|jpe|jpeg|jp2|wdp|jxr|hdp|obj|pdf|ply|png|psd|arw|cr2|svg|tga|tif|tiff|u3ma|usdz|webp|3g2|3gp|avi|flv|m3u8|ts|m2ts|mts|mov|mkv|mp4|mpeg|mpd|mxf|ogv|webm|wmv)$/i; 4 | const REGEX_URL = 5 | /https?:\/\/(?[^/]+)\/(?[^/]+)?\/?(?image|images|video|videos|raw|files)\/(?upload|fetch|private|authenticated|sprite|facebook|twitter|youtube|vimeo)?\/?(?s--([a-zA-Z0-9_-]{8}|[a-zA-Z0-9_-]{32})--)?\/?(?(?:[^_/]+_[^,/]+,?\/?)*\/)*(?v\d+|\w{1,2})\/(?[^\s]+)$/; 6 | const ASSET_TYPES_SEO = ["images", "videos", "files"]; 7 | 8 | const CLOUDINARY_DEFAULT_HOST = "res.cloudinary.com"; 9 | 10 | /** 11 | * parseUrl 12 | * @description 13 | */ 14 | 15 | export interface ParseUrl { 16 | assetType?: string; 17 | cloudName?: string; 18 | deliveryType?: string; 19 | format?: string; 20 | host?: string; 21 | publicId?: string; 22 | signature?: string; 23 | seoSuffix?: string; 24 | transformations?: Array; 25 | queryParams?: object; 26 | version?: number; 27 | } 28 | 29 | export function parseUrl(src: string): ParseUrl | undefined { 30 | if (typeof src !== "string") { 31 | throw new Error(`Failed to parse URL - Invalid src: Is not a string`); 32 | } 33 | 34 | const hasVersion = REGEX_VERSION.test(src); 35 | 36 | if (!hasVersion) { 37 | throw new Error( 38 | `Failed to parse URL - Invalid src: Does not include version (Ex: /v1234/)`, 39 | ); 40 | } 41 | 42 | const [baseUrlWithExtension, queryString] = src.split("?"); 43 | 44 | const format = getFormat(baseUrlWithExtension); 45 | 46 | let baseUrl = baseUrlWithExtension; 47 | 48 | if (format) { 49 | baseUrl = baseUrlWithExtension.replace(new RegExp(`${format}$`), ""); 50 | } 51 | 52 | const results = baseUrl.match(REGEX_URL); 53 | 54 | const transformations = results?.groups?.transformations 55 | ?.split("/") 56 | .filter((t) => !!t); 57 | 58 | const parts: ParseUrl = { 59 | ...results?.groups, 60 | format, 61 | seoSuffix: undefined, 62 | transformations: transformations || [], 63 | queryParams: {}, 64 | version: results?.groups?.version 65 | ? parseInt(results.groups.version.replace("v", "")) 66 | : undefined, 67 | }; 68 | 69 | if (parts.host === CLOUDINARY_DEFAULT_HOST && !parts.cloudName) { 70 | throw new Error( 71 | "Failed to parse URL - Invalid src: Cloudinary URL delivered from res.cloudinary.com must include Cloud Name (ex: res.cloudinary.com//image/...)", 72 | ); 73 | } 74 | 75 | if (queryString) { 76 | interface QueryParams { 77 | [key: string]: string | undefined; 78 | } 79 | 80 | parts.queryParams = queryString 81 | .split("&") 82 | .reduce((prev: QueryParams, curr: string) => { 83 | const [key, value] = curr.split("="); 84 | prev[key] = value; 85 | return prev; 86 | }, {}); 87 | } 88 | 89 | if (parts.assetType && ASSET_TYPES_SEO.includes(parts.assetType)) { 90 | const publicIdParts = parts.publicId?.split("/") || []; 91 | parts.seoSuffix = publicIdParts.pop(); 92 | parts.publicId = publicIdParts.join("/"); 93 | } 94 | 95 | // The URL Gen SDK which this library relies on will re-encode the public ID. To avoid issues where 96 | // someone is already passing in a URL or ID that's been encoded programmatically, first decode 97 | // the public ID, which should theoretically be harmless since it ends up getting encoded 98 | 99 | if (parts.publicId) { 100 | parts.publicId = decodeURIComponent(parts.publicId); 101 | } 102 | 103 | return parts; 104 | } 105 | 106 | /** 107 | * getPublicId 108 | * @description Retrieves the public id of a Cloudinary image url. If no url is recognized it returns the parameter it self. 109 | * If it's recognized that is a url and it's not possible to get the public id, it warns the user. 110 | * @param {string} src: The Cloudinary url or public id. 111 | */ 112 | 113 | export function getPublicId(src: string): string | undefined { 114 | const { publicId } = parseUrl(src) || {}; 115 | return publicId; 116 | } 117 | 118 | /** 119 | * getTransformations 120 | * @description Retrieves the transformations added to a Cloudinary image url. If no transformation is recognized it returns an empty array. 121 | * @param {string} src: The Cloudinary url 122 | */ 123 | 124 | export function getTransformations(src: string) { 125 | const { transformations = [] } = parseUrl(src) || {}; 126 | return transformations.map((t) => t.split(",")); 127 | } 128 | 129 | /** 130 | * getFormat 131 | * @description Retrieves the format of a given string 132 | * @param {string} src: The Cloudinary url or any string trying to match the format 133 | */ 134 | 135 | export function getFormat(src: string) { 136 | const matches = src.match(REGEX_FORMAT); 137 | if (matches === null) return; 138 | return matches[0]; 139 | } 140 | 141 | /** 142 | * normalizeNumberParameter 143 | * @description Returns a number given a string or number value 144 | * @param {string|number} param: The value to return as a number 145 | */ 146 | 147 | export function normalizeNumberParameter(param: number | string | undefined) { 148 | if (typeof param !== "string") return param; 149 | return parseInt(param); 150 | } 151 | 152 | export interface PollForProcessingImageOptions { 153 | /** 154 | * The image src to check, should be a Cloudinary URL. 155 | */ 156 | src: string; 157 | } 158 | 159 | /** 160 | * Poll for an image that hasn't finished processing. 161 | * Will call itself recurisvely until an image is found, or it fails to fetch. 162 | */ 163 | export interface PollForProcessingImageResponse { 164 | status: number; 165 | success: boolean; 166 | error?: string; 167 | } 168 | 169 | export async function pollForProcessingImage( 170 | options: PollForProcessingImageOptions, 171 | ): Promise { 172 | try { 173 | const response = await fetch(options.src); 174 | 175 | if (response.status === 423) { 176 | await new Promise((resolve) => setTimeout(resolve, 500)); 177 | return await pollForProcessingImage(options); 178 | } 179 | 180 | if (!response.ok) { 181 | return { 182 | success: false, 183 | status: response.status, 184 | error: response.headers.get("x-cld-error") || "Unknown error", 185 | }; 186 | } 187 | 188 | return { 189 | success: true, 190 | status: response.status, 191 | }; 192 | } catch (error) { 193 | return { 194 | success: false, 195 | status: 500, 196 | error: (error as Error).message || "Network error", 197 | }; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /packages/util/src/lib/colors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * testColorIsHex 3 | */ 4 | 5 | export function testColorIsHex(value: unknown) { 6 | if (typeof value !== "string") return false; 7 | return !!value.startsWith("#"); 8 | } 9 | 10 | /** 11 | * convertColorHexToRgb 12 | */ 13 | 14 | export function convertColorHexToRgb(value: string) { 15 | return `rgb:${value.replace("#", "")}`; 16 | } 17 | -------------------------------------------------------------------------------- /packages/util/src/lib/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * encodeBase64 3 | * @description Universally returns a base64 encoded string 4 | * @param {any} value: The value to encode as a string 5 | */ 6 | export function encodeBase64(value: any) { 7 | if (typeof btoa === "function") { 8 | return btoa(value); 9 | } 10 | 11 | if (typeof Buffer !== "undefined") { 12 | return Buffer.from(value).toString("base64"); 13 | } 14 | } 15 | 16 | /** 17 | * objectHasKey 18 | * @description Helper function to check if a key exists on an object 19 | * @param {object} obj: The object to check 20 | * @param {string} key: The key to check against the object 21 | */ 22 | 23 | export function objectHasKey(obj: T, key: PropertyKey): key is keyof T { 24 | return Object.prototype.hasOwnProperty.call(obj, key); 25 | } 26 | 27 | /** 28 | * sortByKey 29 | * @description Sort the given array by the key of an object 30 | */ 31 | 32 | export function sortByKey( 33 | array: Array = [], 34 | key: string, 35 | type: string = "asc", 36 | ) { 37 | function compare(a: any, b: any) { 38 | let keyA = a[key]; 39 | let keyB = b[key]; 40 | 41 | if (typeof keyA === "string") { 42 | keyA = keyA.toLowerCase(); 43 | } 44 | 45 | if (typeof keyB === "string") { 46 | keyB = keyB.toLowerCase(); 47 | } 48 | 49 | if (keyA < keyB) return -1; 50 | 51 | if (keyA > keyB) return 1; 52 | 53 | return 0; 54 | } 55 | 56 | let newArray = [...array]; 57 | 58 | if (typeof key !== "string") return newArray; 59 | 60 | newArray = newArray.sort(compare); 61 | 62 | if (type === "desc") { 63 | return newArray.reverse(); 64 | } 65 | 66 | return newArray; 67 | } 68 | -------------------------------------------------------------------------------- /packages/util/tests/lib/colors.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { testColorIsHex, convertColorHexToRgb } from '../../src/lib/colors'; 4 | 5 | describe('colors', () => { 6 | describe('testColorIsHex', () => { 7 | it('should return true if is a hex with #', () => { 8 | const value = '#ff00ff'; 9 | expect(testColorIsHex(value)).toBe(true) 10 | }); 11 | it('should return false if is not a hex with #', () => { 12 | const value = 'ff00ff'; 13 | expect(testColorIsHex(value)).toBe(false) 14 | }); 15 | }) 16 | 17 | describe('convertColorHexToRgb', () => { 18 | it('should convert a hex with # to Cloudinary rgb value', () => { 19 | const value = 'ff00ff'; 20 | expect(convertColorHexToRgb(`#${value}`)).toBe(`rgb:${value}`) 21 | }); 22 | }) 23 | 24 | }) -------------------------------------------------------------------------------- /packages/util/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | }, 6 | "include": ["."], 7 | "exclude": ["dist", "build", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # - "docs" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "ES2022", 5 | "moduleResolution": "NodeNext", 6 | "lib": ["DOM", "ESNext"], 7 | "noEmit": true, 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "declaration": true, 11 | "verbatimModuleSyntax": true, 12 | "esModuleInterop": true, 13 | "resolveJsonModule": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "stripInternal": true, 16 | "paths": { 17 | "@cloudinary-util/types": ["./packages/types/src/index.ts"], 18 | "@cloudinary-util/url-loader": ["./packages/url-loader/src/index.ts"], 19 | "@cloudinary-util/url-loader/schema": [ 20 | "./packages/url-loader/src/schema.ts" 21 | ], 22 | "@cloudinary-util/util": ["./packages/util/src/index.ts"] 23 | } 24 | }, 25 | "exclude": ["**/out", "**/node_modules", "./docs"] 26 | } 27 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "pipeline": { 5 | "build": { 6 | "outputs": ["dist/**", ".next/**"], 7 | "dependsOn": ["^build"] 8 | }, 9 | "test": { 10 | "outputs": ["coverage/**"], 11 | "dependsOn": [] 12 | }, 13 | "lint": {}, 14 | "lint:attw": {}, 15 | "lint:publint": {}, 16 | "dev": { 17 | "cache": false, 18 | "persistent": true 19 | }, 20 | "clean": { 21 | "cache": false 22 | }, 23 | "semantic-release": {} 24 | } 25 | } 26 | --------------------------------------------------------------------------------