├── .eslintignore ├── .github ├── renovate.json └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── _README.template.md ├── package.json ├── pnpm-lock.yaml ├── scripts └── generateDocs.ts ├── src ├── asserters.ts ├── constants.ts ├── errors.ts ├── hotspotCrop.ts ├── index.ts ├── parse.ts ├── paths.ts ├── resolve.ts ├── types.ts ├── urls.ts └── utils.ts ├── test ├── asserters.test.ts ├── fixtures.ts ├── hotspotCrop.test.ts ├── index.test.ts ├── parse.test.ts └── resolve.test.ts ├── tsconfig.json └── typedoc.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | docs 3 | node_modules 4 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["local>sanity-io/renovate-config"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 22 14 | - 20 15 | - 18 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: pnpm/action-setup@v2 19 | - uses: actions/setup-node@v4 20 | with: 21 | cache: pnpm 22 | node-version: ${{ matrix.node-version }} 23 | - run: pnpm install 24 | - run: pnpm test 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | inputs: 9 | publish: 10 | description: Publish to NPM 11 | required: true 12 | default: false 13 | type: boolean 14 | 15 | permissions: 16 | id-token: write # to enable use of OIDC for npm provenance 17 | 18 | jobs: 19 | release: 20 | name: Release 21 | permissions: 22 | id-token: write # to enable use of OIDC for npm provenance 23 | # permissions for pushing commits and opening PRs are handled by the `generate-token` step 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/create-github-app-token@v1 27 | id: app-token 28 | with: 29 | app-id: ${{ secrets.ECOSPARK_APP_ID }} 30 | private-key: ${{ secrets.ECOSPARK_APP_PRIVATE_KEY }} 31 | - name: Get GitHub App User ID 32 | id: get-user-id 33 | run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" 34 | env: 35 | GH_TOKEN: ${{ steps.app-token.outputs.token }} 36 | # Configure git for pushing back changes 37 | - run: | 38 | git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' 39 | git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' 40 | # This action will create a release PR when regular conventional commits are pushed to main, it'll also detect if a release PR is merged and npm publish should happen 41 | - uses: googleapis/release-please-action@v4 42 | id: release 43 | with: 44 | release-type: node 45 | token: ${{ steps.app-token.outputs.token }} 46 | # Publish to NPM on new releases 47 | - uses: actions/checkout@v4 48 | if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} 49 | with: 50 | token: ${{ steps.app-token.outputs.token }} 51 | - uses: pnpm/action-setup@v2 52 | if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} 53 | - uses: actions/setup-node@v4 54 | if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} 55 | with: 56 | cache: pnpm 57 | node-version: 20 58 | registry-url: 'https://registry.npmjs.org' 59 | - run: corepack enable && pnpm --version && pnpm install 60 | if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} 61 | # Update docs with any changes, and do so pre-publish to ensure we include it in the tarball 62 | - name: Update readme documentation 63 | if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} 64 | run: npx tsx scripts/generateDocs.ts --commit --tag ${{steps.release.outputs.tag_name}} && git push 65 | - name: Set publishing config 66 | run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" 67 | if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} 68 | env: 69 | NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}} 70 | # Release Please has already incremented versions and published tags, so we just 71 | # need to publish the new version to npm here 72 | - run: pnpm publish 73 | if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} 74 | env: 75 | NPM_CONFIG_PROVENANCE: true 76 | # Publish steps cleans up docs pre-publish, so re-generate them 77 | - name: Update readme documentation 78 | if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} 79 | run: npx tsx scripts/generateDocs.ts 80 | - name: Update GitHub pages documentation 81 | uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4 82 | if: ${{ github.ref == 'refs/heads/main' && (steps.release.outputs.release_created || github.event.inputs.publish == 'true') }} 83 | with: 84 | github_token: ${{ steps.app-token.outputs.token }} 85 | publish_dir: ./docs 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # MacOS 12 | .DS_Store 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | 42 | # Editors / IDEs 43 | *.sublime-project 44 | *.sublime-workspace 45 | 46 | # Compiled output 47 | /dist 48 | 49 | # Generated docs 50 | /docs 51 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.2.1](https://github.com/sanity-io/asset-utils/compare/v2.2.0...v2.2.1) (2024-11-26) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * support custom CDNs in `getIdFromString` ([#45](https://github.com/sanity-io/asset-utils/issues/45)) ([8e88a60](https://github.com/sanity-io/asset-utils/commit/8e88a602600bdb4c57a231f2d15f58374dc7751e)) 9 | 10 | ## [2.2.0](https://github.com/sanity-io/asset-utils/compare/v2.1.0...v2.2.0) (2024-11-14) 11 | 12 | 13 | ### Features 14 | 15 | * add regex pattern for custom cdn urls ([#43](https://github.com/sanity-io/asset-utils/issues/43)) ([211f387](https://github.com/sanity-io/asset-utils/commit/211f38794fcb7dca38387f89c36d21e32254e6c3)) 16 | 17 | ## [2.1.0](https://github.com/sanity-io/asset-utils/compare/v2.0.7...v2.1.0) (2024-11-04) 18 | 19 | 20 | ### Features 21 | 22 | * allow specifying custom baseUrl in image/file URL builders ([#41](https://github.com/sanity-io/asset-utils/issues/41)) ([146bc23](https://github.com/sanity-io/asset-utils/commit/146bc237e1e6ba9580db862ad54998fb1827c157)) 23 | 24 | ## [2.0.7](https://github.com/sanity-io/asset-utils/compare/v2.0.6...v2.0.7) (2024-10-31) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * correct return values for `try*` functions ([1cca70d](https://github.com/sanity-io/asset-utils/commit/1cca70d2bfebc9345ba17807aafeb52d807c82b2)) 30 | 31 | ## [2.0.6](https://github.com/sanity-io/asset-utils/compare/v2.0.5...v2.0.6) (2024-10-03) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * rename docs script to be more precise ([3920c25](https://github.com/sanity-io/asset-utils/commit/3920c250e922112a04886d2e6afdac202ab796e5)) 37 | 38 | ## [2.0.5](https://github.com/sanity-io/asset-utils/compare/v2.0.4...v2.0.5) (2024-10-03) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * (ci): use correct token for github pages ([8180702](https://github.com/sanity-io/asset-utils/commit/818070286a045aa8c3f964bafdc064e416c90279)) 44 | * improve commit message for readme generation ([10fcede](https://github.com/sanity-io/asset-utils/commit/10fcede0f71ddc93ad94156aea37c657643b4018)) 45 | 46 | ## [2.0.4](https://github.com/sanity-io/asset-utils/compare/v2.0.3...v2.0.4) (2024-10-03) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * include latest, generated readme in published module ([ddd3dfe](https://github.com/sanity-io/asset-utils/commit/ddd3dfef1912e1f0201fbe0092f4ea17e0fc7fd0)) 52 | 53 | ## [2.0.3](https://github.com/sanity-io/asset-utils/compare/v2.0.2...v2.0.3) (2024-10-03) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * remove unnecessary prepublish step ([fe1e393](https://github.com/sanity-io/asset-utils/commit/fe1e3936b37c5fd3fe91d9ad50807c98f7fca58b)) 59 | 60 | ## [2.0.2](https://github.com/sanity-io/asset-utils/compare/v2.0.1...v2.0.2) (2024-10-03) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * clarify that the module is synchronous, no async calls for information ([a8894c4](https://github.com/sanity-io/asset-utils/commit/a8894c42b95d48ce4dd2df765d0618a61d95689b)) 66 | * prevent readme template from being published ([49c6feb](https://github.com/sanity-io/asset-utils/commit/49c6feb8fe8481236f2562038f5e9aee83df2990)) 67 | 68 | ## [2.0.1](https://github.com/sanity-io/asset-utils/compare/v2.0.0...v2.0.1) (2024-10-03) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * **docs:** correct links to html documentation ([a872adf](https://github.com/sanity-io/asset-utils/commit/a872adfb493e81c1be351f6888f39e9a2620452e)) 74 | * **docs:** correct links to readme sections ([b946af2](https://github.com/sanity-io/asset-utils/commit/b946af2f009da613932d755bb3c3e4e08851ec75)) 75 | * **docs:** sort members by kind - functions first ([a12ce9e](https://github.com/sanity-io/asset-utils/commit/a12ce9ef34644f9289cde0a4c33e5db9411852fc)) 76 | 77 | ## [2.0.0](https://github.com/sanity-io/asset-utils/compare/v1.3.2...v2.0.0) (2024-10-03) 78 | 79 | 80 | ### ⚠ BREAKING CHANGES 81 | 82 | * Remove `isObject` method from exported functions. 83 | 84 | ### Features 85 | 86 | * modernize tooling, switch to ESM by default, require node 18 ([b432ddc](https://github.com/sanity-io/asset-utils/commit/b432ddc2089757437ba0016ff34376d2a4559736)) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * make docs generation compatible with latest typedoc ([517695b](https://github.com/sanity-io/asset-utils/commit/517695b82fa675ee03409cd78c1887ed4a2a28b4)) 92 | * typos in tsdocs ([b856d1a](https://github.com/sanity-io/asset-utils/commit/b856d1a920d90abe4edad544efcbf6e6513ac0a3)) 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sanity.io 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 | # @sanity/asset-utils 4 | 5 | Reusable utility functions for dealing with image and file assets in Sanity. 6 | 7 | > [!IMPORTANT] 8 | > This package does _not_ resolve any information from the Sanity APIs - it does no asynchronous operations and only has the information that you pass it. You cannot, for instance, get the LQIP, BlurHash, image palette and similar information without requesting it from the Sanity API. 9 | 10 | ## Installing 11 | 12 | ```sh 13 | $ npm install @sanity/asset-utils 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```js 19 | // ESM / TypeScript 20 | import {someUtilityFunction} from '@sanity/asset-utils' 21 | 22 | // CommonJS 23 | const {someUtilityFunction} = require('@sanity/asset-utils') 24 | ``` 25 | 26 | ## Documentation 27 | 28 | An [HTML version](https://sanity-io.github.io/asset-utils/) is also available, which also includes interface definitions, constants and more. 29 | 30 | ### Functions 31 | 32 | - [buildFilePath](#buildfilepath) 33 | - [buildFileUrl](#buildfileurl) 34 | - [buildImagePath](#buildimagepath) 35 | - [buildImageUrl](#buildimageurl) 36 | - [getAssetDocumentId](#getassetdocumentid) 37 | - [getAssetUrlType](#getasseturltype) 38 | - [getDefaultCrop](#getdefaultcrop) 39 | - [getDefaultHotspot](#getdefaulthotspot) 40 | - [getExtension](#getextension) 41 | - [getFile](#getfile) 42 | - [getFileAsset](#getfileasset) 43 | - [getIdFromString](#getidfromstring) 44 | - [getImage](#getimage) 45 | - [getImageAsset](#getimageasset) 46 | - [getImageDimensions](#getimagedimensions) 47 | - [getProject](#getproject) 48 | - [getUrlFilename](#geturlfilename) 49 | - [getUrlPath](#geturlpath) 50 | - [getVanityStub](#getvanitystub) 51 | - [isAssetFilename](#isassetfilename) 52 | - [isAssetId](#isassetid) 53 | - [isAssetIdStub](#isassetidstub) 54 | - [isAssetObjectStub](#isassetobjectstub) 55 | - [isAssetPathStub](#isassetpathstub) 56 | - [isAssetUrlStub](#isasseturlstub) 57 | - [isDefaultCrop](#isdefaultcrop) 58 | - [isDefaultHotspot](#isdefaulthotspot) 59 | - [isFileAssetFilename](#isfileassetfilename) 60 | - [isFileAssetId](#isfileassetid) 61 | - [isFileSource](#isfilesource) 62 | - [isImageAssetFilename](#isimageassetfilename) 63 | - [isImageAssetId](#isimageassetid) 64 | - [isImageSource](#isimagesource) 65 | - [isReference](#isreference) 66 | - [isSanityAssetUrl](#issanityasseturl) 67 | - [isSanityFileAsset](#issanityfileasset) 68 | - [isSanityFileUrl](#issanityfileurl) 69 | - [isSanityImageAsset](#issanityimageasset) 70 | - [isSanityImageUrl](#issanityimageurl) 71 | - [isUnresolvableError](#isunresolvableerror) 72 | - [isValidFilename](#isvalidfilename) 73 | - [parseAssetFilename](#parseassetfilename) 74 | - [parseAssetId](#parseassetid) 75 | - [parseAssetUrl](#parseasseturl) 76 | - [parseFileAssetId](#parsefileassetid) 77 | - [parseFileAssetUrl](#parsefileasseturl) 78 | - [parseImageAssetId](#parseimageassetid) 79 | - [parseImageAssetUrl](#parseimageasseturl) 80 | - [tryGetAssetDocumentId](#trygetassetdocumentid) 81 | - [tryGetAssetPath](#trygetassetpath) 82 | - [tryGetExtension](#trygetextension) 83 | - [tryGetFile](#trygetfile) 84 | - [tryGetFileAsset](#trygetfileasset) 85 | - [tryGetIdFromString](#trygetidfromstring) 86 | - [tryGetImage](#trygetimage) 87 | - [tryGetImageAsset](#trygetimageasset) 88 | - [tryGetImageDimensions](#trygetimagedimensions) 89 | - [tryGetProject](#trygetproject) 90 | - [tryGetUrlFilename](#trygeturlfilename) 91 | - [tryGetUrlPath](#trygeturlpath) 92 | 93 | ### buildFilePath 94 | 95 | ▸ **buildFilePath**(`asset`: [SanityFileUrlParts](https://sanity-io.github.io/asset-utils/interfaces/SanityFileUrlParts.html) | [FileUrlBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/FileUrlBuilderOptions.html), `options`?: [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html)): _string_ 96 | 97 | Builds the base file path from the minimal set of parts required to assemble it 98 | 99 | | Name | Type | Description | 100 | | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | 101 | | `asset` | [SanityFileUrlParts](https://sanity-io.github.io/asset-utils/interfaces/SanityFileUrlParts.html) \| [FileUrlBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/FileUrlBuilderOptions.html) | An asset-like shape defining ID, dimensions and extension | 102 | | `options` | [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html) | (_Optional_) Project ID and dataset the file belongs to, along with other options | 103 | 104 | **Returns:** _string_ 105 | 106 | _Defined in [src/paths.ts:77](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/paths.ts#L77)_ 107 | 108 | ### buildFileUrl 109 | 110 | ▸ **buildFileUrl**(`asset`: [FileUrlBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/FileUrlBuilderOptions.html), `options`?: [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html)): _string_ 111 | 112 | Builds the base file URL from the minimal set of parts required to assemble it 113 | 114 | | Name | Type | Description | 115 | | --------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | 116 | | `asset` | [FileUrlBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/FileUrlBuilderOptions.html) | An asset-like shape defining ID and extension | 117 | | `options` | [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html) | (_Optional_) Project ID and dataset the file belongs to, along with other options | 118 | 119 | **Returns:** _string_ 120 | 121 | _Defined in [src/paths.ts:102](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/paths.ts#L102)_ 122 | 123 | ### buildImagePath 124 | 125 | ▸ **buildImagePath**(`asset`: [SanityImageUrlParts](https://sanity-io.github.io/asset-utils/interfaces/SanityImageUrlParts.html) | [ImageUrlBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/ImageUrlBuilderOptions.html), `options`?: [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html)): _string_ 126 | 127 | Builds the base image path from the minimal set of parts required to assemble it 128 | 129 | | Name | Type | Description | 130 | | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | 131 | | `asset` | [SanityImageUrlParts](https://sanity-io.github.io/asset-utils/interfaces/SanityImageUrlParts.html) \| [ImageUrlBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/ImageUrlBuilderOptions.html) | An asset-like shape defining ID, dimensions and extension | 132 | | `options` | [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html) | (_Optional_) Project ID and dataset the image belongs to, along with other options | 133 | 134 | **Returns:** _string_ 135 | 136 | _Defined in [src/paths.ts:33](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/paths.ts#L33)_ 137 | 138 | ### buildImageUrl 139 | 140 | ▸ **buildImageUrl**(`asset`: [SanityImageUrlParts](https://sanity-io.github.io/asset-utils/interfaces/SanityImageUrlParts.html) | [ImageUrlBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/ImageUrlBuilderOptions.html), `options`?: [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html)): _string_ 141 | 142 | Builds the base image URL from the minimal set of parts required to assemble it 143 | 144 | | Name | Type | Description | 145 | | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | 146 | | `asset` | [SanityImageUrlParts](https://sanity-io.github.io/asset-utils/interfaces/SanityImageUrlParts.html) \| [ImageUrlBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/ImageUrlBuilderOptions.html) | An asset-like shape defining ID, dimensions and extension | 147 | | `options` | [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html) | (_Optional_) Project ID and dataset the image belongs to | 148 | 149 | **Returns:** _string_ 150 | 151 | _Defined in [src/paths.ts:61](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/paths.ts#L61)_ 152 | 153 | ### getAssetDocumentId 154 | 155 | ▸ **getAssetDocumentId**(`src`: unknown): _string_ 156 | 157 | Tries to resolve the asset document ID from any inferrable structure 158 | 159 | | Name | Type | Description | 160 | | ----- | ------- | ----------------------------------------------------------------- | 161 | | `src` | unknown | Input source (image/file object, asset, reference, id, url, path) | 162 | 163 | **Returns:** _string_ 164 | 165 | _Defined in [src/resolve.ts:264](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L264)_ 166 | 167 | ### getAssetUrlType 168 | 169 | ▸ **getAssetUrlType**(`url`: string): _"image" | "file" | false_ 170 | 171 | Validates that a given URL is a Sanity asset URL, and returns the asset type if valid. 172 | 173 | | Name | Type | Description | 174 | | ----- | ------ | ------------------------------ | 175 | | `url` | string | URL to extract asset type from | 176 | 177 | **Returns:** _"image" | "file" | false_ 178 | 179 | _Defined in [src/parse.ts:183](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/parse.ts#L183)_ 180 | 181 | ### getDefaultCrop 182 | 183 | ▸ **getDefaultCrop**(): _[SanityImageCrop](https://sanity-io.github.io/asset-utils/interfaces/SanityImageCrop.html)_ 184 | 185 | Returns cloned version of the default crop (prevents accidental mutations) 186 | 187 | **Returns:** _[SanityImageCrop](https://sanity-io.github.io/asset-utils/interfaces/SanityImageCrop.html)_ 188 | 189 | _Defined in [src/hotspotCrop.ts:33](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/hotspotCrop.ts#L33)_ 190 | 191 | ### getDefaultHotspot 192 | 193 | ▸ **getDefaultHotspot**(): _[SanityImageHotspot](https://sanity-io.github.io/asset-utils/interfaces/SanityImageHotspot.html)_ 194 | 195 | Returns cloned version of the default hotspot (prevents accidental mutations) 196 | 197 | **Returns:** _[SanityImageHotspot](https://sanity-io.github.io/asset-utils/interfaces/SanityImageHotspot.html)_ 198 | 199 | _Defined in [src/hotspotCrop.ts:41](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/hotspotCrop.ts#L41)_ 200 | 201 | ### getExtension 202 | 203 | ▸ **getExtension**(`src`: [SanityAssetSource](https://sanity-io.github.io/asset-utils/types/SanityAssetSource.html)): _string_ 204 | 205 | Returns the file extension for a given asset 206 | 207 | | Name | Type | Description | 208 | | ----- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | 209 | | `src` | [SanityAssetSource](https://sanity-io.github.io/asset-utils/types/SanityAssetSource.html) | Input source (file/image object, asset, reference, id, url, path) | 210 | 211 | **Returns:** _string_ 212 | 213 | _Defined in [src/resolve.ts:81](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L81)_ 214 | 215 | ### getFile 216 | 217 | ▸ **getFile**(`src`: [SanityFileSource](https://sanity-io.github.io/asset-utils/types/SanityFileSource.html), `project`?: [SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html)): _[ResolvedSanityFile](https://sanity-io.github.io/asset-utils/interfaces/ResolvedSanityFile.html)_ 218 | 219 | Tries to resolve an file object with as much information as possible, from any inferrable structure (id, url, path, file object etc) 220 | 221 | | Name | Type | Description | 222 | | --------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | 223 | | `src` | [SanityFileSource](https://sanity-io.github.io/asset-utils/types/SanityFileSource.html) | Input source (file object, asset, reference, id, url, path) | 224 | | `project` | [SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html) | (_Optional_) Project ID and dataset the file belongs to | 225 | 226 | **Returns:** _[ResolvedSanityFile](https://sanity-io.github.io/asset-utils/interfaces/ResolvedSanityFile.html)_ 227 | 228 | _Defined in [src/resolve.ts:195](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L195)_ 229 | 230 | ### getFileAsset 231 | 232 | ▸ **getFileAsset**(`src`: [SanityFileSource](https://sanity-io.github.io/asset-utils/types/SanityFileSource.html), `options`?: [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html)): _[SanityFileAsset](https://sanity-io.github.io/asset-utils/types/SanityFileAsset.html)_ 233 | 234 | Tries to resolve a (partial) file asset document with as much information as possible, from any inferrable structure (id, url, path, file object etc) 235 | 236 | | Name | Type | Description | 237 | | --------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | 238 | | `src` | [SanityFileSource](https://sanity-io.github.io/asset-utils/types/SanityFileSource.html) | Input source (file object, asset, reference, id, url, path) | 239 | | `options` | [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html) | (_Optional_) Project ID and dataset the file belongs to, along with other options | 240 | 241 | **Returns:** _[SanityFileAsset](https://sanity-io.github.io/asset-utils/types/SanityFileAsset.html)_ 242 | 243 | _Defined in [src/resolve.ts:220](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L220)_ 244 | 245 | ### getIdFromString 246 | 247 | ▸ **getIdFromString**(`str`: string): _string_ 248 | 249 | Tries to cooerce a string (ID, URL or path) to an image asset ID 250 | 251 | | Name | Type | Description | 252 | | ----- | ------ | ------------------------------ | 253 | | `str` | string | Input string (ID, URL or path) | 254 | 255 | **Returns:** _string_ 256 | 257 | _Defined in [src/resolve.ts:306](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L306)_ 258 | 259 | ### getImage 260 | 261 | ▸ **getImage**(`src`: [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html), `project`?: [SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html)): _[ResolvedSanityImage](https://sanity-io.github.io/asset-utils/interfaces/ResolvedSanityImage.html)_ 262 | 263 | Tries to resolve an image object with as much information as possible, from any inferrable structure (id, url, path, image object etc) 264 | 265 | | Name | Type | Description | 266 | | --------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | 267 | | `src` | [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html) | Input source (image object, asset, reference, id, url, path) | 268 | | `project` | [SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html) | (_Optional_) Project ID and dataset the image belongs to | 269 | 270 | **Returns:** _[ResolvedSanityImage](https://sanity-io.github.io/asset-utils/interfaces/ResolvedSanityImage.html)_ 271 | 272 | _Defined in [src/resolve.ts:106](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L106)_ 273 | 274 | ### getImageAsset 275 | 276 | ▸ **getImageAsset**(`src`: [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html), `project`?: [SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html)): _[SanityImageAsset](https://sanity-io.github.io/asset-utils/types/SanityImageAsset.html)_ 277 | 278 | Tries to resolve a (partial) image asset document with as much information as possible, from any inferrable structure (id, url, path, image object etc) 279 | 280 | | Name | Type | Description | 281 | | --------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | 282 | | `src` | [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html) | Input source (image object, asset, reference, id, url, path) | 283 | | `project` | [SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html) | (_Optional_) Project ID and dataset the image belongs to | 284 | 285 | **Returns:** _[SanityImageAsset](https://sanity-io.github.io/asset-utils/types/SanityImageAsset.html)_ 286 | 287 | _Defined in [src/resolve.ts:140](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L140)_ 288 | 289 | ### getImageDimensions 290 | 291 | ▸ **getImageDimensions**(`src`: [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html)): _[SanityImageDimensions](https://sanity-io.github.io/asset-utils/types/SanityImageDimensions.html)_ 292 | 293 | Returns the width, height and aspect ratio of a passed image asset, from any inferrable structure (id, url, path, asset document, image object etc) 294 | 295 | | Name | Type | Description | 296 | | ----- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------ | 297 | | `src` | [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html) | Input source (image object, asset, reference, id, url, path) | 298 | 299 | **Returns:** _[SanityImageDimensions](https://sanity-io.github.io/asset-utils/types/SanityImageDimensions.html)_ 300 | 301 | _Defined in [src/resolve.ts:57](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L57)_ 302 | 303 | ### getProject 304 | 305 | ▸ **getProject**(`src`: [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html)): _[SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html)_ 306 | 307 | Resolves project ID and dataset the image belongs to, based on full URL or path 308 | 309 | | Name | Type | Description | 310 | | ----- | ----------------------------------------------------------------------------------------- | ----------------- | 311 | | `src` | [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html) | Image URL or path | 312 | 313 | **Returns:** _[SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html)_ 314 | 315 | _Defined in [src/resolve.ts:368](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L368)_ 316 | 317 | ### getUrlFilename 318 | 319 | ▸ **getUrlFilename**(`url`: string): _string_ 320 | 321 | Strips the CDN URL, path and query params from a URL, eg: `https://cdn.sanity.io/images/project/dataset/filename-200x200.jpg?foo=bar` → `filename-200x200.jpg` 322 | 323 | | Name | Type | Description | 324 | | ----- | ------ | ------------------------ | 325 | | `url` | string | URL to get filename from | 326 | 327 | **Returns:** _string_ 328 | 329 | _Defined in [src/paths.ts:189](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/paths.ts#L189)_ 330 | 331 | ### getUrlPath 332 | 333 | ▸ **getUrlPath**(`url`: string): _string_ 334 | 335 | Strips the CDN URL and query params from a URL, eg: `https://cdn.sanity.io/images/project/dataset/filename-200x200.jpg?foo=bar` → `images/project/dataset/filename-200x200.jpg` 336 | 337 | | Name | Type | Description | 338 | | ----- | ------ | ------------------------- | 339 | | `url` | string | URL to get path name from | 340 | 341 | **Returns:** _string_ 342 | 343 | _Defined in [src/paths.ts:159](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/paths.ts#L159)_ 344 | 345 | ### getVanityStub 346 | 347 | ▸ **getVanityStub**(`originalFilename`: string | undefined, `vanityFilename`: string | undefined, `options`?: [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html)): _string_ 348 | 349 | Get the "path stub" at the end of the path, if the user hasn't explicitly opted out of this behavior 350 | 351 | | Name | Type | Description | 352 | | ------------------ | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | 353 | | `originalFilename` | string \| undefined | The original filename of the asset | 354 | | `vanityFilename` | string \| undefined | The vanity filename of the asset | 355 | | `options` | [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html) | (_Optional_) Options to control the behavior of the path builder | 356 | 357 | **Returns:** _string_ 358 | 359 | _Defined in [src/paths.ts:226](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/paths.ts#L226)_ 360 | 361 | ### isAssetFilename 362 | 363 | ▸ **isAssetFilename**(`filename`: string): _boolean_ 364 | 365 | Returns whether or not the passed filename is a valid file or image asset filename 366 | 367 | | Name | Type | Description | 368 | | ---------- | ------ | -------------------- | 369 | | `filename` | string | Filename to validate | 370 | 371 | **Returns:** _boolean_ 372 | 373 | _Defined in [src/resolve.ts:418](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L418)_ 374 | 375 | ### isAssetId 376 | 377 | ▸ **isAssetId**(`documentId`: string): _boolean_ 378 | 379 | Checks whether or not the given document ID is a valid Sanity asset document ID (file or image) 380 | 381 | | Name | Type | Description | 382 | | ------------ | ------ | -------------------- | 383 | | `documentId` | string | Document ID to check | 384 | 385 | **Returns:** _boolean_ 386 | 387 | _Defined in [src/asserters.ts:119](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/asserters.ts#L119)_ 388 | 389 | ### isAssetIdStub 390 | 391 | ▸ **isAssetIdStub**(`stub`: unknown): _stub is [SanityAssetIdStub](https://sanity-io.github.io/asset-utils/interfaces/SanityAssetIdStub.html)_ 392 | 393 | Checks whether or not the given source is an asset ID stub (an object containing an `_id` property) 394 | 395 | | Name | Type | Description | 396 | | ------ | ------- | ---------------------- | 397 | | `stub` | unknown | Possible asset id stub | 398 | 399 | **Returns:** _stub is [SanityAssetIdStub](https://sanity-io.github.io/asset-utils/interfaces/SanityAssetIdStub.html)_ 400 | 401 | _Defined in [src/asserters.ts:38](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/asserters.ts#L38)_ 402 | 403 | ### isAssetObjectStub 404 | 405 | ▸ **isAssetObjectStub**(`stub`: unknown): _stub is [SanityAssetObjectStub](https://sanity-io.github.io/asset-utils/types/SanityAssetObjectStub.html)_ 406 | 407 | Checks whether or not the given source is an asset object stub 408 | 409 | | Name | Type | Description | 410 | | ------ | ------- | -------------------------- | 411 | | `stub` | unknown | Possible asset object stub | 412 | 413 | **Returns:** _stub is [SanityAssetObjectStub](https://sanity-io.github.io/asset-utils/types/SanityAssetObjectStub.html)_ 414 | 415 | _Defined in [src/asserters.ts:130](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/asserters.ts#L130)_ 416 | 417 | ### isAssetPathStub 418 | 419 | ▸ **isAssetPathStub**(`stub`: unknown): _stub is [SanityAssetPathStub](https://sanity-io.github.io/asset-utils/interfaces/SanityAssetPathStub.html)_ 420 | 421 | Checks whether or not the given source is an asset path stub (an object containing a `path` property) 422 | 423 | | Name | Type | Description | 424 | | ------ | ------- | ------------------------ | 425 | | `stub` | unknown | Possible asset path stub | 426 | 427 | **Returns:** _stub is [SanityAssetPathStub](https://sanity-io.github.io/asset-utils/interfaces/SanityAssetPathStub.html)_ 428 | 429 | _Defined in [src/asserters.ts:50](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/asserters.ts#L50)_ 430 | 431 | ### isAssetUrlStub 432 | 433 | ▸ **isAssetUrlStub**(`stub`: unknown): _stub is [SanityAssetUrlStub](https://sanity-io.github.io/asset-utils/interfaces/SanityAssetUrlStub.html)_ 434 | 435 | Checks whether or not the given source is an asset URL stub (an object containing a `url` property) 436 | 437 | | Name | Type | Description | 438 | | ------ | ------- | ----------------------- | 439 | | `stub` | unknown | Possible asset url stub | 440 | 441 | **Returns:** _stub is [SanityAssetUrlStub](https://sanity-io.github.io/asset-utils/interfaces/SanityAssetUrlStub.html)_ 442 | 443 | _Defined in [src/asserters.ts:62](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/asserters.ts#L62)_ 444 | 445 | ### isDefaultCrop 446 | 447 | ▸ **isDefaultCrop**(`crop`: [SanityImageCrop](https://sanity-io.github.io/asset-utils/interfaces/SanityImageCrop.html)): _boolean_ 448 | 449 | Returns whether or not the passed crop has the default values for a crop region 450 | 451 | | Name | Type | Description | 452 | | ------ | ------------------------------------------------------------------------------------------ | ----------------------------------------------------- | 453 | | `crop` | [SanityImageCrop](https://sanity-io.github.io/asset-utils/interfaces/SanityImageCrop.html) | The crop to return whether or not is the default crop | 454 | 455 | **Returns:** _boolean_ 456 | 457 | _Defined in [src/hotspotCrop.ts:50](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/hotspotCrop.ts#L50)_ 458 | 459 | ### isDefaultHotspot 460 | 461 | ▸ **isDefaultHotspot**(`hotspot`: [SanityImageHotspot](https://sanity-io.github.io/asset-utils/interfaces/SanityImageHotspot.html)): _boolean_ 462 | 463 | Returns whether or not the passed hotspot has the default values for a hotspot region 464 | 465 | | Name | Type | Description | 466 | | --------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | 467 | | `hotspot` | [SanityImageHotspot](https://sanity-io.github.io/asset-utils/interfaces/SanityImageHotspot.html) | The hotspot to return whether or not is the default hotspot | 468 | 469 | **Returns:** _boolean_ 470 | 471 | _Defined in [src/hotspotCrop.ts:71](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/hotspotCrop.ts#L71)_ 472 | 473 | ### isFileAssetFilename 474 | 475 | ▸ **isFileAssetFilename**(`filename`: string): _boolean_ 476 | 477 | Returns whether or not the passed filename is a valid file asset filename 478 | 479 | | Name | Type | Description | 480 | | ---------- | ------ | -------------------- | 481 | | `filename` | string | Filename to validate | 482 | 483 | **Returns:** _boolean_ 484 | 485 | _Defined in [src/resolve.ts:407](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L407)_ 486 | 487 | ### isFileAssetId 488 | 489 | ▸ **isFileAssetId**(`documentId`: string): _boolean_ 490 | 491 | Checks whether or not the given document ID is a valid Sanity file asset document ID 492 | 493 | | Name | Type | Description | 494 | | ------------ | ------ | -------------------- | 495 | | `documentId` | string | Document ID to check | 496 | 497 | **Returns:** _boolean_ 498 | 499 | _Defined in [src/asserters.ts:108](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/asserters.ts#L108)_ 500 | 501 | ### isFileSource 502 | 503 | ▸ **isFileSource**(`src`: unknown): _src is [SanityFileSource](https://sanity-io.github.io/asset-utils/types/SanityFileSource.html)_ 504 | 505 | Return whether or not the passed source is a file source 506 | 507 | | Name | Type | Description | 508 | | ----- | ------- | --------------- | 509 | | `src` | unknown | Source to check | 510 | 511 | **Returns:** _src is [SanityFileSource](https://sanity-io.github.io/asset-utils/types/SanityFileSource.html)_ 512 | 513 | _Defined in [src/resolve.ts:429](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L429)_ 514 | 515 | ### isImageAssetFilename 516 | 517 | ▸ **isImageAssetFilename**(`filename`: string): _boolean_ 518 | 519 | Returns whether or not the passed filename is a valid image asset filename 520 | 521 | | Name | Type | Description | 522 | | ---------- | ------ | -------------------- | 523 | | `filename` | string | Filename to validate | 524 | 525 | **Returns:** _boolean_ 526 | 527 | _Defined in [src/resolve.ts:396](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L396)_ 528 | 529 | ### isImageAssetId 530 | 531 | ▸ **isImageAssetId**(`documentId`: string): _boolean_ 532 | 533 | Checks whether or not the given document ID is a valid Sanity image asset document ID 534 | 535 | | Name | Type | Description | 536 | | ------------ | ------ | -------------------- | 537 | | `documentId` | string | Document ID to check | 538 | 539 | **Returns:** _boolean_ 540 | 541 | _Defined in [src/asserters.ts:97](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/asserters.ts#L97)_ 542 | 543 | ### isImageSource 544 | 545 | ▸ **isImageSource**(`src`: unknown): _src is [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html)_ 546 | 547 | Return whether or not the passed source is an image source 548 | 549 | | Name | Type | Description | 550 | | ----- | ------- | --------------- | 551 | | `src` | unknown | Source to check | 552 | 553 | **Returns:** _src is [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html)_ 554 | 555 | _Defined in [src/resolve.ts:441](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L441)_ 556 | 557 | ### isReference 558 | 559 | ▸ **isReference**(`ref`: unknown): _ref is [SanityReference](https://sanity-io.github.io/asset-utils/interfaces/SanityReference.html)_ 560 | 561 | Checks whether or not the given source is a Sanity reference (an object containing \_ref string key) 562 | 563 | | Name | Type | Description | 564 | | ----- | ------- | ------------------ | 565 | | `ref` | unknown | Possible reference | 566 | 567 | **Returns:** _ref is [SanityReference](https://sanity-io.github.io/asset-utils/interfaces/SanityReference.html)_ 568 | 569 | _Defined in [src/asserters.ts:26](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/asserters.ts#L26)_ 570 | 571 | ### isSanityAssetUrl 572 | 573 | ▸ **isSanityAssetUrl**(`url`: string): _boolean_ 574 | 575 | Checks whether or not a given URL is a valid Sanity asset URL 576 | 577 | | Name | Type | Description | 578 | | ----- | ------ | ----------- | 579 | | `url` | string | URL to test | 580 | 581 | **Returns:** _boolean_ 582 | 583 | _Defined in [src/urls.ts:10](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/urls.ts#L10)_ 584 | 585 | ### isSanityFileAsset 586 | 587 | ▸ **isSanityFileAsset**(`src`: unknown): _src is [SanityFileAsset](https://sanity-io.github.io/asset-utils/types/SanityFileAsset.html)_ 588 | 589 | Checks whether or not the given source is a (partial) sanity file asset document. Only checks the `_type` property, all other properties _may_ be missing 590 | 591 | | Name | Type | Description | 592 | | ----- | ------- | --------------- | 593 | | `src` | unknown | Source to check | 594 | 595 | **Returns:** _src is [SanityFileAsset](https://sanity-io.github.io/asset-utils/types/SanityFileAsset.html)_ 596 | 597 | _Defined in [src/asserters.ts:74](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/asserters.ts#L74)_ 598 | 599 | ### isSanityFileUrl 600 | 601 | ▸ **isSanityFileUrl**(`url`: string): _boolean_ 602 | 603 | Checks whether or not a given URL is a valid Sanity file asset URL 604 | 605 | | Name | Type | Description | 606 | | ----- | ------ | ----------- | 607 | | `url` | string | URL to test | 608 | 609 | **Returns:** _boolean_ 610 | 611 | _Defined in [src/urls.ts:32](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/urls.ts#L32)_ 612 | 613 | ### isSanityImageAsset 614 | 615 | ▸ **isSanityImageAsset**(`src`: unknown): _src is [SanityImageAsset](https://sanity-io.github.io/asset-utils/types/SanityImageAsset.html)_ 616 | 617 | Checks whether or not the given source is a (partial) sanity image asset document. Only checks the `_type` property, all other properties _may_ be missing 618 | 619 | | Name | Type | Description | 620 | | ----- | ------- | --------------- | 621 | | `src` | unknown | Source to check | 622 | 623 | **Returns:** _src is [SanityImageAsset](https://sanity-io.github.io/asset-utils/types/SanityImageAsset.html)_ 624 | 625 | _Defined in [src/asserters.ts:86](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/asserters.ts#L86)_ 626 | 627 | ### isSanityImageUrl 628 | 629 | ▸ **isSanityImageUrl**(`url`: string): _boolean_ 630 | 631 | Checks whether or not a given URL is a valid Sanity image asset URL 632 | 633 | | Name | Type | Description | 634 | | ----- | ------ | ----------- | 635 | | `url` | string | URL to test | 636 | 637 | **Returns:** _boolean_ 638 | 639 | _Defined in [src/urls.ts:21](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/urls.ts#L21)_ 640 | 641 | ### isUnresolvableError 642 | 643 | ▸ **isUnresolvableError**(`err`: unknown): _err is [UnresolvableError](https://sanity-io.github.io/asset-utils/classes/UnresolvableError.html)_ 644 | 645 | Checks whether or not an error instance is of type UnresolvableError 646 | 647 | | Name | Type | Description | 648 | | ----- | ------- | ------------------------------------------ | 649 | | `err` | unknown | Error to check for unresolvable error type | 650 | 651 | **Returns:** _err is [UnresolvableError](https://sanity-io.github.io/asset-utils/classes/UnresolvableError.html)_ 652 | 653 | _Defined in [src/errors.ts:29](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/errors.ts#L29)_ 654 | 655 | ### isValidFilename 656 | 657 | ▸ **isValidFilename**(`filename`: string): _boolean_ 658 | 659 | Checks whether or not a given filename matches the expected Sanity asset filename pattern 660 | 661 | | Name | Type | Description | 662 | | ---------- | ------ | ------------------------------ | 663 | | `filename` | string | Filename to check for validity | 664 | 665 | **Returns:** _boolean_ 666 | 667 | _Defined in [src/paths.ts:213](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/paths.ts#L213)_ 668 | 669 | ### parseAssetFilename 670 | 671 | ▸ **parseAssetFilename**(`filename`: string): _[SanityAssetIdParts](https://sanity-io.github.io/asset-utils/types/SanityAssetIdParts.html)_ 672 | 673 | Parses a Sanity asset filename into individual parts (type, id, extension, width, height) 674 | 675 | | Name | Type | Description | 676 | | ---------- | ------ | ---------------------------------- | 677 | | `filename` | string | Filename to parse into named parts | 678 | 679 | **Returns:** _[SanityAssetIdParts](https://sanity-io.github.io/asset-utils/types/SanityAssetIdParts.html)_ 680 | 681 | _Defined in [src/parse.ts:94](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/parse.ts#L94)_ 682 | 683 | ### parseAssetId 684 | 685 | ▸ **parseAssetId**(`documentId`: string): _[SanityAssetIdParts](https://sanity-io.github.io/asset-utils/types/SanityAssetIdParts.html)_ 686 | 687 | Parses a Sanity asset document ID into individual parts (type, id, extension, width/height etc) 688 | 689 | | Name | Type | Description | 690 | | ------------ | ------ | ------------------------------------- | 691 | | `documentId` | string | Document ID to parse into named parts | 692 | 693 | **Returns:** _[SanityAssetIdParts](https://sanity-io.github.io/asset-utils/types/SanityAssetIdParts.html)_ 694 | 695 | _Defined in [src/parse.ts:36](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/parse.ts#L36)_ 696 | 697 | ### parseAssetUrl 698 | 699 | ▸ **parseAssetUrl**(`url`: string): _[SanityAssetUrlParts](https://sanity-io.github.io/asset-utils/types/SanityAssetUrlParts.html)_ 700 | 701 | Parses a full Sanity asset URL into individual parts (type, project ID, dataset, id, extension, width, height) 702 | 703 | | Name | Type | Description | 704 | | ----- | ------ | ---------------------------------- | 705 | | `url` | string | Full URL to parse into named parts | 706 | 707 | **Returns:** _[SanityAssetUrlParts](https://sanity-io.github.io/asset-utils/types/SanityAssetUrlParts.html)_ 708 | 709 | _Defined in [src/parse.ts:118](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/parse.ts#L118)_ 710 | 711 | ### parseFileAssetId 712 | 713 | ▸ **parseFileAssetId**(`documentId`: string): _[SanityFileAssetIdParts](https://sanity-io.github.io/asset-utils/interfaces/SanityFileAssetIdParts.html)_ 714 | 715 | Parses a Sanity file asset document ID into individual parts (type, id, extension) 716 | 717 | | Name | Type | Description | 718 | | ------------ | ------ | ------------------------------------------------ | 719 | | `documentId` | string | File asset document ID to parse into named parts | 720 | 721 | **Returns:** _[SanityFileAssetIdParts](https://sanity-io.github.io/asset-utils/interfaces/SanityFileAssetIdParts.html)_ 722 | 723 | _Defined in [src/parse.ts:56](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/parse.ts#L56)_ 724 | 725 | ### parseFileAssetUrl 726 | 727 | ▸ **parseFileAssetUrl**(`url`: string): _[SanityFileUrlParts](https://sanity-io.github.io/asset-utils/interfaces/SanityFileUrlParts.html)_ 728 | 729 | Parses a full Sanity file asset URL into individual parts (type, project ID, dataset, id, extension, width, height) 730 | 731 | | Name | Type | Description | 732 | | ----- | ------ | ---------------------------------- | 733 | | `url` | string | Full URL to parse into named parts | 734 | 735 | **Returns:** _[SanityFileUrlParts](https://sanity-io.github.io/asset-utils/interfaces/SanityFileUrlParts.html)_ 736 | 737 | _Defined in [src/parse.ts:167](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/parse.ts#L167)_ 738 | 739 | ### parseImageAssetId 740 | 741 | ▸ **parseImageAssetId**(`documentId`: string): _[SanityImageAssetIdParts](https://sanity-io.github.io/asset-utils/interfaces/SanityImageAssetIdParts.html)_ 742 | 743 | Parses a Sanity image asset document ID into individual parts (type, id, extension, width, height) 744 | 745 | | Name | Type | Description | 746 | | ------------ | ------ | ------------------------------------------------- | 747 | | `documentId` | string | Image asset document ID to parse into named parts | 748 | 749 | **Returns:** _[SanityImageAssetIdParts](https://sanity-io.github.io/asset-utils/interfaces/SanityImageAssetIdParts.html)_ 750 | 751 | _Defined in [src/parse.ts:75](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/parse.ts#L75)_ 752 | 753 | ### parseImageAssetUrl 754 | 755 | ▸ **parseImageAssetUrl**(`url`: string): _[SanityImageUrlParts](https://sanity-io.github.io/asset-utils/interfaces/SanityImageUrlParts.html)_ 756 | 757 | Parses a full Sanity image asset URL into individual parts (type, project ID, dataset, id, extension, width, height) 758 | 759 | | Name | Type | Description | 760 | | ----- | ------ | ---------------------------------- | 761 | | `url` | string | Full URL to parse into named parts | 762 | 763 | **Returns:** _[SanityImageUrlParts](https://sanity-io.github.io/asset-utils/interfaces/SanityImageUrlParts.html)_ 764 | 765 | _Defined in [src/parse.ts:149](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/parse.ts#L149)_ 766 | 767 | ### tryGetAssetDocumentId 768 | 769 | ▸ **tryGetAssetDocumentId**(`src`: unknown): _string | undefined_ 770 | 771 | Tries to resolve the asset document ID from any inferrable structure 772 | 773 | | Name | Type | Description | 774 | | ----- | ------- | ----------------------------------------------------------------- | 775 | | `src` | unknown | Input source (image/file object, asset, reference, id, url, path) | 776 | 777 | **Returns:** _string | undefined_ 778 | 779 | _Defined in [src/resolve.ts:293](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L293)_ 780 | 781 | ### tryGetAssetPath 782 | 783 | ▸ **tryGetAssetPath**(`src`: [SanityAssetSource](https://sanity-io.github.io/asset-utils/types/SanityAssetSource.html)): _string | undefined_ 784 | 785 | Tries to get the asset path from a given asset source 786 | 787 | | Name | Type | Description | 788 | | ----- | ----------------------------------------------------------------------------------------- | -------------------------------------------- | 789 | | `src` | [SanityAssetSource](https://sanity-io.github.io/asset-utils/types/SanityAssetSource.html) | The source image to infer an asset path from | 790 | 791 | **Returns:** _string | undefined_ 792 | 793 | _Defined in [src/paths.ts:125](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/paths.ts#L125)_ 794 | 795 | ### tryGetExtension 796 | 797 | ▸ **tryGetExtension**(`src`: [SanityAssetSource](https://sanity-io.github.io/asset-utils/types/SanityAssetSource.html)): _string | undefined_ 798 | 799 | Returns the file extension for a given asset 800 | 801 | | Name | Type | Description | 802 | | ----- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | 803 | | `src` | [SanityAssetSource](https://sanity-io.github.io/asset-utils/types/SanityAssetSource.html) | Input source (file/image object, asset, reference, id, url, path) | 804 | 805 | **Returns:** _string | undefined_ 806 | 807 | _Defined in [src/resolve.ts:92](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L92)_ 808 | 809 | ### tryGetFile 810 | 811 | ▸ **tryGetFile**(`src`: [SanityFileSource](https://sanity-io.github.io/asset-utils/types/SanityFileSource.html), `project`?: [SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html)): _[ResolvedSanityFile](https://sanity-io.github.io/asset-utils/interfaces/ResolvedSanityFile.html) | undefined_ 812 | 813 | Tries to resolve an file object with as much information as possible, from any inferrable structure (id, url, path, file object etc) 814 | 815 | | Name | Type | Description | 816 | | --------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | 817 | | `src` | [SanityFileSource](https://sanity-io.github.io/asset-utils/types/SanityFileSource.html) | Input source (file object, asset, reference, id, url, path) | 818 | | `project` | [SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html) | (_Optional_) Project ID and dataset the file belongs to | 819 | 820 | **Returns:** _[ResolvedSanityFile](https://sanity-io.github.io/asset-utils/interfaces/ResolvedSanityFile.html) | undefined_ 821 | 822 | _Defined in [src/resolve.ts:206](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L206)_ 823 | 824 | ### tryGetFileAsset 825 | 826 | ▸ **tryGetFileAsset**(`src`: [SanityFileSource](https://sanity-io.github.io/asset-utils/types/SanityFileSource.html), `options`?: [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html)): _[SanityFileAsset](https://sanity-io.github.io/asset-utils/types/SanityFileAsset.html) | undefined_ 827 | 828 | Tries to resolve a (partial) file asset document with as much information as possible, from any inferrable structure (id, url, path, file object etc) 829 | 830 | | Name | Type | Description | 831 | | --------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | 832 | | `src` | [SanityFileSource](https://sanity-io.github.io/asset-utils/types/SanityFileSource.html) | Input source (file object, asset, reference, id, url, path) | 833 | | `options` | [PathBuilderOptions](https://sanity-io.github.io/asset-utils/interfaces/PathBuilderOptions.html) | (_Optional_) Project ID and dataset the file belongs to, along with other options | 834 | 835 | **Returns:** _[SanityFileAsset](https://sanity-io.github.io/asset-utils/types/SanityFileAsset.html) | undefined_ 836 | 837 | _Defined in [src/resolve.ts:252](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L252)_ 838 | 839 | ### tryGetIdFromString 840 | 841 | ▸ **tryGetIdFromString**(`str`: string): _string | undefined_ 842 | 843 | Tries to cooerce a string (ID, URL or path) to an image asset ID 844 | 845 | | Name | Type | Description | 846 | | ----- | ------ | ------------------------------ | 847 | | `str` | string | Input string (ID, URL or path) | 848 | 849 | **Returns:** _string | undefined_ 850 | 851 | _Defined in [src/resolve.ts:343](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L343)_ 852 | 853 | ### tryGetImage 854 | 855 | ▸ **tryGetImage**(`src`: [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html), `project`?: [SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html)): _[ResolvedSanityImage](https://sanity-io.github.io/asset-utils/interfaces/ResolvedSanityImage.html) | undefined_ 856 | 857 | Tries to resolve an image object with as much information as possible, from any inferrable structure (id, url, path, image object etc) 858 | 859 | | Name | Type | Description | 860 | | --------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | 861 | | `src` | [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html) | Input source (image object, asset, reference, id, url, path) | 862 | | `project` | [SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html) | (_Optional_) Project ID and dataset the image belongs to | 863 | 864 | **Returns:** _[ResolvedSanityImage](https://sanity-io.github.io/asset-utils/interfaces/ResolvedSanityImage.html) | undefined_ 865 | 866 | _Defined in [src/resolve.ts:126](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L126)_ 867 | 868 | ### tryGetImageAsset 869 | 870 | ▸ **tryGetImageAsset**(`src`: [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html), `project`?: [SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html)): _[SanityImageAsset](https://sanity-io.github.io/asset-utils/types/SanityImageAsset.html) | undefined_ 871 | 872 | Tries to resolve a (partial) image asset document with as much information as possible, from any inferrable structure (id, url, path, image object etc) 873 | 874 | | Name | Type | Description | 875 | | --------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | 876 | | `src` | [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html) | Input source (image object, asset, reference, id, url, path) | 877 | | `project` | [SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html) | (_Optional_) Project ID and dataset the image belongs to | 878 | 879 | **Returns:** _[SanityImageAsset](https://sanity-io.github.io/asset-utils/types/SanityImageAsset.html) | undefined_ 880 | 881 | _Defined in [src/resolve.ts:181](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L181)_ 882 | 883 | ### tryGetImageDimensions 884 | 885 | ▸ **tryGetImageDimensions**(`src`: [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html)): _[SanityImageDimensions](https://sanity-io.github.io/asset-utils/types/SanityImageDimensions.html) | undefined_ 886 | 887 | Returns the width, height and aspect ratio of a passed image asset, from any inferrable structure (id, url, path, asset document, image object etc) 888 | 889 | | Name | Type | Description | 890 | | ----- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------ | 891 | | `src` | [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html) | Input source (image object, asset, reference, id, url, path) | 892 | 893 | **Returns:** _[SanityImageDimensions](https://sanity-io.github.io/asset-utils/types/SanityImageDimensions.html) | undefined_ 894 | 895 | _Defined in [src/resolve.ts:69](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L69)_ 896 | 897 | ### tryGetProject 898 | 899 | ▸ **tryGetProject**(`src`: [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html)): _[SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html) | undefined_ 900 | 901 | Resolves project ID and dataset the image belongs to, based on full URL or path 902 | 903 | | Name | Type | Description | 904 | | ----- | ----------------------------------------------------------------------------------------- | ----------------- | 905 | | `src` | [SanityImageSource](https://sanity-io.github.io/asset-utils/types/SanityImageSource.html) | Image URL or path | 906 | 907 | **Returns:** _[SanityProjectDetails](https://sanity-io.github.io/asset-utils/interfaces/SanityProjectDetails.html) | undefined_ 908 | 909 | _Defined in [src/resolve.ts:387](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/resolve.ts#L387)_ 910 | 911 | ### tryGetUrlFilename 912 | 913 | ▸ **tryGetUrlFilename**(`url`: string): _string | undefined_ 914 | 915 | Strips the CDN URL, path and query params from a URL, eg: `https://cdn.sanity.io/images/project/dataset/filename-200x200.jpg?foo=bar` → `filename-200x200.jpg` 916 | 917 | | Name | Type | Description | 918 | | ----- | ------ | ------------------------ | 919 | | `url` | string | URL to get filename from | 920 | 921 | **Returns:** _string | undefined_ 922 | 923 | _Defined in [src/paths.ts:204](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/paths.ts#L204)_ 924 | 925 | ### tryGetUrlPath 926 | 927 | ▸ **tryGetUrlPath**(`url`: string): _string | undefined_ 928 | 929 | Strips the CDN URL and query params from a URL, eg: `https://cdn.sanity.io/images/project/dataset/filename-200x200.jpg?foo=bar` → `images/project/dataset/filename-200x200.jpg` 930 | 931 | | Name | Type | Description | 932 | | ----- | ------ | ------------------------- | 933 | | `url` | string | URL to get path name from | 934 | 935 | **Returns:** _string | undefined_ 936 | 937 | _Defined in [src/paths.ts:177](https://github.com/sanity-io/asset-utils/blob/v2.2.1/src/paths.ts#L177)_ 938 | 939 | ## License 940 | 941 | MIT-licensed. See LICENSE. 942 | -------------------------------------------------------------------------------- /_README.template.md: -------------------------------------------------------------------------------- 1 | # @sanity/asset-utils 2 | 3 | Reusable utility functions for dealing with image and file assets in Sanity. 4 | 5 | > [!IMPORTANT] 6 | > This package does _not_ resolve any information from the Sanity APIs - it does no asynchronous operations and only has the information that you pass it. You cannot, for instance, get the LQIP, BlurHash, image palette and similar information without requesting it from the Sanity API. 7 | 8 | ## Installing 9 | 10 | ```sh 11 | $ npm install @sanity/asset-utils 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```js 17 | // ESM / TypeScript 18 | import {someUtilityFunction} from '@sanity/asset-utils' 19 | 20 | // CommonJS 21 | const {someUtilityFunction} = require('@sanity/asset-utils') 22 | ``` 23 | 24 | ## License 25 | 26 | MIT-licensed. See LICENSE. 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sanity/asset-utils", 3 | "version": "2.2.1", 4 | "author": "Sanity.io ", 5 | "license": "MIT", 6 | "type": "module", 7 | "sideEffects": false, 8 | "exports": { 9 | ".": { 10 | "source": "./src/index.ts", 11 | "import": "./dist/index.js", 12 | "require": "./dist/index.cjs", 13 | "default": "./dist/index.js" 14 | }, 15 | "./package.json": "./package.json" 16 | }, 17 | "main": "./dist/index.cjs", 18 | "module": "./dist/index.js", 19 | "types": "./dist/index.d.ts", 20 | "files": [ 21 | "dist", 22 | "src" 23 | ], 24 | "engines": { 25 | "node": ">=18" 26 | }, 27 | "scripts": { 28 | "prepare": "npm run clean && npm run pkg:build && npm run pkg:check", 29 | "clean": "rimraf dist docs", 30 | "pkg:build": "pkg build --strict", 31 | "pkg:check": "pkg check --strict", 32 | "docs": "tsx scripts/generateDocs.ts", 33 | "test": "vitest" 34 | }, 35 | "browserslist": "extends @sanity/browserslist-config", 36 | "devDependencies": { 37 | "@sanity/pkg-utils": "^6.11.8", 38 | "@types/node": "^20.16.10", 39 | "@typescript-eslint/eslint-plugin": "^7.18.0", 40 | "@typescript-eslint/parser": "^7.18.0", 41 | "cheerio": "^1.0.0", 42 | "eslint": "^8.57.1", 43 | "eslint-config-prettier": "^7.2.0", 44 | "eslint-config-sanity": "^7.1.3", 45 | "eslint-plugin-prettier": "^5.2.1", 46 | "outdent": "^0.8.0", 47 | "prettier": "^3.3.3", 48 | "rimraf": "^5.0.10", 49 | "semantic-release": "^24.2.0", 50 | "tsx": "^4.19.2", 51 | "typedoc": "^0.26.11", 52 | "typescript": "^5.6.3", 53 | "vitest": "^2.1.4" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "git+ssh://git@github.com/sanity-io/asset-utils.git" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/sanity-io/asset-utils/issues" 61 | }, 62 | "homepage": "https://github.com/sanity-io/asset-utils#readme", 63 | "keywords": [ 64 | "sanity", 65 | "sanity.io", 66 | "asset", 67 | "utility" 68 | ], 69 | "eslintConfig": { 70 | "root": true, 71 | "env": { 72 | "node": true, 73 | "browser": true 74 | }, 75 | "parser": "@typescript-eslint/parser", 76 | "extends": [ 77 | "plugin:@typescript-eslint/recommended", 78 | "prettier/@typescript-eslint", 79 | "sanity", 80 | "prettier" 81 | ], 82 | "rules": { 83 | "prettier/prettier": "error", 84 | "operator-linebreak": "off", 85 | "no-use-before-define": "off", 86 | "@typescript-eslint/no-use-before-define": "off" 87 | }, 88 | "plugins": [ 89 | "@typescript-eslint", 90 | "prettier" 91 | ] 92 | }, 93 | "release": { 94 | "branches": [ 95 | "main" 96 | ], 97 | "plugins": [ 98 | [ 99 | "@semantic-release/commit-analyzer", 100 | { 101 | "preset": "conventionalcommits", 102 | "releaseRules": [ 103 | { 104 | "type": "build", 105 | "scope": "deps", 106 | "release": "patch" 107 | } 108 | ] 109 | } 110 | ], 111 | [ 112 | "@semantic-release/release-notes-generator", 113 | { 114 | "preset": "conventionalcommits", 115 | "presetConfig": { 116 | "types": [ 117 | { 118 | "type": "feat", 119 | "section": "Features" 120 | }, 121 | { 122 | "type": "fix", 123 | "section": "Bug Fixes" 124 | }, 125 | { 126 | "type": "build", 127 | "section": "Dependencies and Other Build Updates", 128 | "hidden": false 129 | } 130 | ] 131 | } 132 | } 133 | ], 134 | "@semantic-release/npm", 135 | "@semantic-release/github" 136 | ] 137 | }, 138 | "packageManager": "pnpm@9.12.0" 139 | } 140 | -------------------------------------------------------------------------------- /scripts/generateDocs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Yeah this isn't the prettiest code, but it's just a simple docs generator 3 | */ 4 | 5 | import childProcess from 'node:child_process' 6 | import fs from 'node:fs' 7 | import path from 'node:path' 8 | import {parseArgs} from 'node:util' 9 | 10 | import * as cheerio from 'cheerio' 11 | import outdent from 'outdent' 12 | import {JSONOutput, ReflectionKind} from 'typedoc' 13 | 14 | const {values: flags} = parseArgs({ 15 | options: {commit: {type: 'boolean', default: false}, tag: {type: 'string'}}, 16 | }) 17 | 18 | const basePath = path.resolve(import.meta.dirname, '..') 19 | 20 | const pkg = JSON.parse(fs.readFileSync(path.resolve(basePath, 'package.json'), 'utf8')) 21 | const ghUrl = pkg.homepage.replace(/#.*/, '').replace(/\/+$/, '') 22 | 23 | const gitTarget = 24 | flags.tag || 25 | childProcess.execSync('git rev-parse --short HEAD', {cwd: basePath, encoding: 'utf8'}).trim() 26 | 27 | const doCommit = flags.commit 28 | const slugRe = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g 29 | const htmlDocsUrl = `https://sanity-io.github.io/asset-utils` 30 | 31 | let docs: JSONOutput.ProjectReflection 32 | let allNodes: Array 33 | let functions: Array 34 | 35 | const mdFilename = 'README.md' 36 | const mdTemplate = fs.readFileSync(path.join(basePath, '_README.template.md'), 'utf8') 37 | const mdBase = `\n${mdTemplate}` 38 | 39 | generateHtmlDocs() 40 | modifyHtmlDocs() 41 | readJsonDocs() 42 | extractFunctions() 43 | rewriteTryFunctions() 44 | createMarkdownDocs() 45 | prettify() 46 | 47 | if (doCommit) { 48 | commit() 49 | } 50 | 51 | /** 52 | * Utilities 53 | */ 54 | function readJsonDocs() { 55 | try { 56 | docs = JSON.parse(fs.readFileSync(path.resolve(basePath, 'docs', 'docs.json'), 'utf8')) 57 | } catch (err) { 58 | console.error(`Failed to read ${path.resolve(basePath, 'docs', 'docs.json')}:`, err) 59 | process.exit(1) 60 | } 61 | } 62 | 63 | function extractFunctions() { 64 | allNodes = docs.children || [] 65 | functions = (docs.children || []).filter( 66 | (node) => node.variant === 'declaration' && node.kind === ReflectionKind.Function, 67 | ) 68 | } 69 | 70 | function rewriteTryFunctions() { 71 | functions = functions.map((fn) => (fn.name.startsWith('try') ? rewriteTryFunction(fn) : fn)) 72 | } 73 | 74 | function rewriteTryFunction( 75 | fn: JSONOutput.DeclarationReflection, 76 | ): JSONOutput.DeclarationReflection { 77 | const srcName = fn.name.replace(/^try/, '').replace(/^[A-Z]/, (char) => char.toLowerCase()) 78 | const src = functions.find((node) => node.name === srcName) 79 | if (!src) { 80 | return fn 81 | } 82 | 83 | const fnSignatures = fn.signatures || [] 84 | const srcSignatures = src.signatures || [] 85 | 86 | const sigs = Math.min(srcSignatures.length, fnSignatures.length) 87 | for (let i = 0; i < sigs; i++) { 88 | const fnSig = fnSignatures[i] 89 | const srcSig = srcSignatures[i] 90 | if (!fnSig.comment?.summary.length) { 91 | fnSig.comment = {...fnSig.comment, summary: srcSig.comment?.summary || []} 92 | } 93 | if (fnSig.parameters?.length === 1 && fnSig.parameters[0].name === 'args') { 94 | fnSig.parameters = srcSig.parameters 95 | } 96 | } 97 | 98 | return fn 99 | } 100 | 101 | function slugify(str: string): string { 102 | if (typeof str !== 'string') { 103 | return '' 104 | } 105 | 106 | return str 107 | .trim() 108 | .replace(/[A-Z]+/g, (sub) => sub.toLowerCase()) 109 | .replace(/<[^>\d]+>/g, '') 110 | .replace(slugRe, '') 111 | .replace(/\s/g, '-') 112 | .replace(/-+/g, '-') 113 | .replace(/^(\d)/, '_$1') 114 | } 115 | 116 | function createMarkdownDocs() { 117 | const content = mdBase.trimStart().replace(/## License/, `${createMarkdownBody()}\n\n## License`) 118 | fs.writeFileSync(path.join(basePath, mdFilename), content) 119 | } 120 | 121 | function createMarkdownToc() { 122 | return outdent` 123 | ### Functions 124 | 125 | ${functions.map((child) => `- [${child.name}](#${slugify(child.name)})`).join('\n')} 126 | ` 127 | } 128 | 129 | function createMarkdownBody() { 130 | const header = outdent` 131 | ## Documentation 132 | 133 | An [HTML version](https://sanity-io.github.io/asset-utils/) is also available, which also includes interface definitions, constants and more. 134 | ` 135 | const toc = createMarkdownToc() 136 | const fns = functions.map((child) => createMarkdownFnDoc(child)).join('\n\n') 137 | return `${header}\n\n${toc}\n\n${fns}` 138 | } 139 | 140 | function createMarkdownParameterSignature(param: JSONOutput.ParameterReflection) { 141 | const type = formatMarkdownRef(param.type) 142 | const opt = param.flags?.isOptional ? '?' : '' 143 | return `${code(param.name)}${opt}: ${type}` 144 | } 145 | 146 | function sortUnionTypes(types: JSONOutput.SomeType[]) { 147 | const undefinedIndex = types.findIndex( 148 | (type) => type.type === 'intrinsic' && type.name === 'undefined', 149 | ) 150 | if (undefinedIndex === -1) { 151 | return types 152 | } 153 | 154 | const sorted = types.slice() 155 | const [undef] = sorted.splice(undefinedIndex, 1) 156 | return sorted.concat(undef) 157 | } 158 | 159 | function getUrlForNode(node: JSONOutput.DeclarationReflection) { 160 | const isFunction = node.kind === ReflectionKind.Function 161 | if (isFunction) { 162 | return `${mdFilename}#${node.name}` 163 | } 164 | 165 | if (node.kind === ReflectionKind.Interface) { 166 | return `${htmlDocsUrl}/interfaces/${node.name}.html` 167 | } 168 | 169 | if (node.kind === ReflectionKind.TypeAlias) { 170 | return `${htmlDocsUrl}/types/${node.name}.html` 171 | } 172 | 173 | if (node.kind === ReflectionKind.Variable) { 174 | return `${htmlDocsUrl}/variables/${node.name}.html` 175 | } 176 | 177 | if (node.kind === ReflectionKind.Class) { 178 | return `${htmlDocsUrl}/classes/${node.name}.html` 179 | } 180 | 181 | return `${htmlDocsUrl}/index.html#${node.name}` 182 | } 183 | 184 | function formatMarkdownRef(thing: JSONOutput.ParameterReflection['type']): string { 185 | if (!thing) { 186 | return 'unknown' 187 | } 188 | 189 | const refNode = 190 | thing.type === 'reference' && 191 | allNodes.find((node) => (thing.target ? node.id === thing.target : node.name === thing.name)) 192 | 193 | if (refNode) { 194 | const url = getUrlForNode(refNode) 195 | return `[${thing.name}](${url})` 196 | } 197 | 198 | if (thing.type === 'reference' || thing.type === 'intrinsic') { 199 | return thing.name 200 | } 201 | 202 | if (thing.type === 'union') { 203 | return sortUnionTypes(thing.types) 204 | .map((type) => formatMarkdownRef(type)) 205 | .join(' | ') 206 | } 207 | 208 | if (thing.type === 'predicate') { 209 | return `${thing.name} is ${formatMarkdownRef(thing.targetType)}` 210 | } 211 | 212 | if (thing.type === 'literal') { 213 | return JSON.stringify(thing.value, null, 2) 214 | } 215 | 216 | if (thing.type === 'tuple') { 217 | return `[${(thing.elements || []).map(formatMarkdownRef).join(', ')}]` 218 | } 219 | 220 | if (thing.type === 'namedTupleMember') { 221 | return `${thing.name}: ${formatMarkdownRef(thing.element)}` 222 | } 223 | 224 | if (thing.type === 'reflection') { 225 | return 'unknown' 226 | } 227 | 228 | throw new Error(`Unknown parameter type: ${JSON.stringify(thing, null, 2)}`) 229 | } 230 | 231 | function createMarkdownReturnValue(sig: JSONOutput.SignatureReflection) { 232 | const returnType = formatMarkdownRef(sig.type) 233 | return em(returnType) 234 | } 235 | 236 | function createMarkdownFnSignature( 237 | sig: JSONOutput.SignatureReflection, 238 | parent: JSONOutput.DeclarationReflection, 239 | ) { 240 | const params = sig.parameters || [] 241 | const description = getTextComment(sig.comment) || getTextComment(parent.comment) 242 | const paramsTable = generateParamsTable(params) 243 | const returns = `${bold('Returns:')} ${createMarkdownReturnValue(sig)}` 244 | const signature = [''] 245 | .concat(`▸ ${bold(sig.name)}(`) 246 | .concat(params.map(createMarkdownParameterSignature).join(', ')) 247 | .concat(`): ${createMarkdownReturnValue(sig)}`) 248 | .join('') 249 | 250 | return [signature].concat(description).concat(paramsTable).concat(returns).join('\n\n') 251 | } 252 | 253 | function getTextComment(comment: JSONOutput.Comment | undefined): string { 254 | if (!comment || !comment.summary) { 255 | return '' 256 | } 257 | 258 | return comment.summary 259 | .map((node) => { 260 | if (node.kind === 'text') { 261 | return node.text 262 | } 263 | if (node.kind === 'code') { 264 | return code(node.text) 265 | } 266 | if (node.kind === 'relative-link') { 267 | return relativeLink(node) 268 | } 269 | 270 | return node.text 271 | }) 272 | .join(' ') 273 | .replace(/(?): string { 278 | const paramsHasDescription = params.some((param) => 279 | param.comment?.summary.some((node) => node.kind === 'text' && node.text.trim().length > 0), 280 | ) 281 | const headers = paramsHasDescription ? `| Name | Type | Description |` : '| Name | Type |' 282 | const separat = paramsHasDescription ? `| ---- | ---- | ----------- |` : '| ---- | ---- |' 283 | const rows = params.map((param) => { 284 | const type = formatMarkdownRef(param.type) 285 | const prefix = param.flags?.isOptional ? '(_Optional_) ' : '' 286 | const stub: string[] = prefix ? [prefix] : [] 287 | const description = paramsHasDescription ? [prefix + getTextComment(param.comment)] : stub 288 | 289 | const parts = ['', code(param.name), type, ...description, ''].map((part) => 290 | part.replace(/\|/g, '\\|'), 291 | ) 292 | return parts.join(' | ').trim() 293 | }) 294 | 295 | return rows.length > 0 ? [headers, separat, ...rows].join('\n') : '' 296 | } 297 | 298 | function createMarkdownFnDoc(fn: JSONOutput.DeclarationReflection) { 299 | return outdent` 300 | ### ${fn.name} 301 | 302 | ${(fn.signatures || []).map((sig) => createMarkdownFnSignature(sig, fn)).join('\n')} 303 | 304 | ${(fn.sources || []).map((src) => em(`Defined in ${srcLink(src)}`)).join('\n')} 305 | ` 306 | } 307 | 308 | function code(str: string) { 309 | return `\`${str}\`` 310 | } 311 | 312 | function em(str: string) { 313 | return `_${str}_` 314 | } 315 | 316 | function bold(str: string) { 317 | return `**${str}**` 318 | } 319 | 320 | function relativeLink(src: JSONOutput.RelativeLinkDisplayPart) { 321 | return src.text 322 | } 323 | 324 | function srcLink(src: JSONOutput.SourceReference) { 325 | return `[${src.fileName}:${src.line}](${srcUrl(src)})` 326 | } 327 | 328 | function srcUrl(src: JSONOutput.SourceReference) { 329 | return `${ghUrl}/blob/${gitTarget}/${src.fileName}#L${src.line}` 330 | } 331 | 332 | function prettify() { 333 | childProcess.spawnSync(path.join(basePath, 'node_modules', '.bin', 'prettier'), [ 334 | '--write', 335 | path.join(basePath, 'README.md'), 336 | ]) 337 | } 338 | 339 | function commit() { 340 | childProcess.spawnSync('git', ['add', 'README.md'], { 341 | cwd: basePath, 342 | stdio: 'inherit', 343 | }) 344 | 345 | let message = 'docs: generate new readme from source' 346 | if (flags.tag && /^v\d+\.\d+\.\d+\$/.test(flags.tag)) { 347 | message = `docs: generate new readme for ${flags.tag}` 348 | } 349 | 350 | childProcess.spawnSync('git', ['commit', '-m', message], { 351 | cwd: basePath, 352 | stdio: 'inherit', 353 | }) 354 | } 355 | 356 | function generateHtmlDocs() { 357 | childProcess.spawnSync(path.join(basePath, 'node_modules', '.bin', 'typedoc'), { 358 | cwd: basePath, 359 | stdio: 'inherit', 360 | }) 361 | } 362 | 363 | function modifyHtmlDocs() { 364 | const htmlPath = path.join(basePath, 'docs', 'index.html') 365 | const $ = cheerio.load(fs.readFileSync(htmlPath)) 366 | 367 | // If encountering (broken) @link tags - throw 368 | $(':contains("@link")').each(function () { 369 | const html = $(this).html() || '' 370 | html.replace(/{@link (.*?)}/g, (_, target) => { 371 | throw new Error(`Found BROKEN @link reference to "${target}"`) 372 | }) 373 | }) 374 | 375 | // Replace [!IMPORTANT] and [!NOTE] blocks from GitHub flavored markdown 376 | $('blockquote').each(function () { 377 | const html = $(this).html() || '' 378 | if (!html.includes('[!IMPORTANT]') && !html.includes('[!NOTE]')) { 379 | return 380 | } 381 | 382 | $(this).html(html.replace(/\[!(IMPORTANT|NOTE)\]/g, '$1:')) 383 | }) 384 | 385 | fs.writeFileSync(htmlPath, $.html()) 386 | } 387 | -------------------------------------------------------------------------------- /src/asserters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cdnUrlPattern, 3 | customCdnUrlPattern, 4 | fileAssetIdPattern, 5 | imageAssetIdPattern, 6 | } from './constants.js' 7 | import type { 8 | SanityAssetIdStub, 9 | SanityAssetObjectStub, 10 | SanityAssetPathStub, 11 | SanityAssetUrlStub, 12 | SanityFileAsset, 13 | SanityImageAsset, 14 | SanityReference, 15 | } from './types.js' 16 | import {isObject} from './utils.js' 17 | 18 | /** 19 | * Checks whether or not the given source is a Sanity reference 20 | * (an object containing _ref string key) 21 | * 22 | * @param ref - Possible reference 23 | * @returns Whether or not the passed object is a reference 24 | * @public 25 | */ 26 | export function isReference(ref: unknown): ref is SanityReference { 27 | return isObject(ref) && typeof (ref as SanityReference)._ref === 'string' 28 | } 29 | 30 | /** 31 | * Checks whether or not the given source is an asset ID stub 32 | * (an object containing an `_id` property) 33 | * 34 | * @param stub - Possible asset id stub 35 | * @returns Whether or not the passed object is an object id stub 36 | * @public 37 | */ 38 | export function isAssetIdStub(stub: unknown): stub is SanityAssetIdStub { 39 | return isObject(stub) && typeof (stub as SanityAssetIdStub)._id === 'string' 40 | } 41 | 42 | /** 43 | * Checks whether or not the given source is an asset path stub 44 | * (an object containing a `path` property) 45 | * 46 | * @param stub - Possible asset path stub 47 | * @returns Whether or not the passed object is an object path stub 48 | * @public 49 | */ 50 | export function isAssetPathStub(stub: unknown): stub is SanityAssetPathStub { 51 | return isObject(stub) && typeof (stub as SanityAssetPathStub).path === 'string' 52 | } 53 | 54 | /** 55 | * Checks whether or not the given source is an asset URL stub 56 | * (an object containing a `url` property) 57 | * 58 | * @param stub - Possible asset url stub 59 | * @returns Whether or not the passed object is an object url stub 60 | * @public 61 | */ 62 | export function isAssetUrlStub(stub: unknown): stub is SanityAssetUrlStub { 63 | return isObject(stub) && typeof (stub as SanityAssetUrlStub).url === 'string' 64 | } 65 | 66 | /** 67 | * Checks whether or not the given source is a (partial) sanity file asset document. 68 | * Only checks the `_type` property, all other properties _may_ be missing 69 | * 70 | * @param src - Source to check 71 | * @returns Whether or not the given source is a file asset 72 | * @public 73 | */ 74 | export function isSanityFileAsset(src: unknown): src is SanityFileAsset { 75 | return isObject(src) && (src as SanityFileAsset)._type === 'sanity.fileAsset' 76 | } 77 | 78 | /** 79 | * Checks whether or not the given source is a (partial) sanity image asset document. 80 | * Only checks the `_type` property, all other properties _may_ be missing 81 | * 82 | * @param src - Source to check 83 | * @returns Whether or not the given source is a file asset 84 | * @public 85 | */ 86 | export function isSanityImageAsset(src: unknown): src is SanityImageAsset { 87 | return isObject(src) && (src as SanityImageAsset)._type === 'sanity.imageAsset' 88 | } 89 | 90 | /** 91 | * Checks whether or not the given document ID is a valid Sanity image asset document ID 92 | * 93 | * @param documentId - Document ID to check 94 | * @returns Whether or not the given document ID is a Sanity image asset document ID 95 | * @public 96 | */ 97 | export function isImageAssetId(documentId: string): boolean { 98 | return imageAssetIdPattern.test(documentId) 99 | } 100 | 101 | /** 102 | * Checks whether or not the given document ID is a valid Sanity file asset document ID 103 | * 104 | * @param documentId - Document ID to check 105 | * @returns Whether or not the given document ID is a Sanity file asset document ID 106 | * @public 107 | */ 108 | export function isFileAssetId(documentId: string): boolean { 109 | return fileAssetIdPattern.test(documentId) 110 | } 111 | 112 | /** 113 | * Checks whether or not the given document ID is a valid Sanity asset document ID (file or image) 114 | * 115 | * @param documentId - Document ID to check 116 | * @returns Whether or not the given document ID is a Sanity asset document ID (file or image) 117 | * @public 118 | */ 119 | export function isAssetId(documentId: string): boolean { 120 | return isImageAssetId(documentId) || isFileAssetId(documentId) 121 | } 122 | 123 | /** 124 | * Checks whether or not the given source is an asset object stub 125 | * 126 | * @param stub - Possible asset object stub 127 | * @returns Whether or not the passed object is an object stub 128 | * @public 129 | */ 130 | export function isAssetObjectStub(stub: unknown): stub is SanityAssetObjectStub { 131 | const item = stub as SanityAssetObjectStub 132 | return isObject(item) && item.asset && typeof item.asset === 'object' 133 | } 134 | 135 | /** 136 | * Checks whether or not the given URL is a valid Sanity CDN URL or what appears to be a valid 137 | * custom CDN URL (has to be prefixed with `cdn.`, and follow the same path pattern as Sanity CDN ) 138 | * 139 | * @param url - URL to check 140 | * @returns True if Sanity CDN URL/Custom CDN URL, false otherwise 141 | * @internal 142 | */ 143 | export function isCdnUrl(url: string): boolean { 144 | return cdnUrlPattern.test(url) || customCdnUrlPattern.test(url) 145 | } 146 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | */ 4 | export const cdnUrl = 'https://cdn.sanity.io' 5 | 6 | /** 7 | * @internal 8 | */ 9 | export const cdnUrlPattern = /^https:\/\/cdn\.sanity\./ 10 | 11 | /** 12 | * @internal 13 | */ 14 | export const customCdnUrlPattern = 15 | /^https:\/\/cdn\.[^/]+\/(images|files)\/[^/]+\/.*?[a-zA-Z0-9_]{24,40}.*$/ 16 | 17 | /** 18 | * @internal 19 | */ 20 | export const fileAssetFilenamePattern = /^([a-zA-Z0-9_]{24,40}|[a-f0-9]{40})+\.[a-z0-9]+$/ 21 | 22 | /** 23 | * @internal 24 | */ 25 | export const fileAssetIdPattern = /^file-([a-zA-Z0-9_]{24,40}|[a-f0-9]{40})+-[a-z0-9]+$/ 26 | 27 | /** 28 | * @internal 29 | */ 30 | export const imageAssetFilenamePattern = /^([a-zA-Z0-9_]{24,40}|[a-f0-9]{40})-\d+x\d+\.[a-z0-9]+$/ 31 | 32 | /** 33 | * @internal 34 | */ 35 | export const imageAssetIdPattern = /^image-([a-zA-Z0-9_]{24,40}|[a-f0-9]{40})+-\d+x\d+-[a-z0-9]+$/ 36 | 37 | /** 38 | * @internal 39 | */ 40 | export const assetFilenamePattern = 41 | /^(([a-zA-Z0-9_]{24,40}|[a-f0-9]{40})+|([a-zA-Z0-9_]{24,40}|[a-f0-9]{40})+-\d+x\d+\.[a-z0-9]+)$/ 42 | 43 | /** 44 | * @internal 45 | */ 46 | export const pathPattern = /^(images|files)\/([a-z0-9]+)\/([a-z0-9][-\w]*)\// 47 | 48 | /** 49 | * @internal 50 | */ 51 | export const idPattern = 52 | /^(?:image-(?:[a-zA-Z0-9_]{24,40}|[a-f0-9]{40})+-\d+x\d+-[a-z0-9]+|file-(?:[a-zA-Z0-9_]{24,40}|[a-f0-9]{40})+-[a-z0-9]+)$/ 53 | 54 | /** 55 | * For use in cases where the project and dataset doesn't really matter 56 | * 57 | * @internal 58 | */ 59 | export const dummyProject = {projectId: 'a', dataset: 'b'} 60 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error type thrown when the library fails to resolve a value, such as an asset ID, 3 | * filename or project ID/dataset information. 4 | * 5 | * The `input` property holds the value passed as the input, which failed to be 6 | * resolved to something meaningful. 7 | * 8 | * @public 9 | */ 10 | export class UnresolvableError extends Error { 11 | unresolvable = true 12 | 13 | // The input may not be a valid source, so let's not type it as one 14 | input?: unknown 15 | 16 | constructor(inputSource: unknown, message = 'Failed to resolve asset ID from source') { 17 | super(message) 18 | this.input = inputSource 19 | } 20 | } 21 | 22 | /** 23 | * Checks whether or not an error instance is of type UnresolvableError 24 | * 25 | * @param err - Error to check for unresolvable error type 26 | * @returns True if the passed error instance appears to be an unresolvable error 27 | * @public 28 | */ 29 | export function isUnresolvableError(err: unknown): err is UnresolvableError { 30 | const error = err as UnresolvableError 31 | return Boolean(error.unresolvable && 'input' in error) 32 | } 33 | -------------------------------------------------------------------------------- /src/hotspotCrop.ts: -------------------------------------------------------------------------------- 1 | import type {SanityImageCrop, SanityImageHotspot} from './types.js' 2 | 3 | /** 4 | * Default crop (equals to "whole image") 5 | * 6 | * @public 7 | */ 8 | export const DEFAULT_CROP: Readonly = Object.freeze({ 9 | left: 0, 10 | top: 0, 11 | bottom: 0, 12 | right: 0, 13 | }) 14 | 15 | /** 16 | * Default hotspot (equals to horizontal/vertical center, full size of image) 17 | * 18 | * @public 19 | */ 20 | export const DEFAULT_HOTSPOT: Readonly = Object.freeze({ 21 | x: 0.5, 22 | y: 0.5, 23 | height: 1, 24 | width: 1, 25 | }) 26 | 27 | /** 28 | * Returns cloned version of the default crop (prevents accidental mutations) 29 | * 30 | * @returns Default image crop object 31 | * @public 32 | */ 33 | export const getDefaultCrop = (): SanityImageCrop => ({...DEFAULT_CROP}) 34 | 35 | /** 36 | * Returns cloned version of the default hotspot (prevents accidental mutations) 37 | * 38 | * @returns Default image hotspot object 39 | * @public 40 | */ 41 | export const getDefaultHotspot = (): SanityImageHotspot => ({...DEFAULT_HOTSPOT}) 42 | 43 | /** 44 | * Returns whether or not the passed crop has the default values for a crop region 45 | * 46 | * @param crop - The crop to return whether or not is the default crop 47 | * @returns True if passed crop matches default, false otherwise 48 | * @public 49 | */ 50 | export const isDefaultCrop = (crop: SanityImageCrop): boolean => { 51 | const {top, bottom, left, right} = crop 52 | const { 53 | top: defaultTop, 54 | bottom: defaultBottom, 55 | left: defaultLeft, 56 | right: defaultRight, 57 | } = DEFAULT_CROP 58 | 59 | return ( 60 | top === defaultTop && bottom === defaultBottom && left === defaultLeft && right === defaultRight 61 | ) 62 | } 63 | 64 | /** 65 | * Returns whether or not the passed hotspot has the default values for a hotspot region 66 | * 67 | * @param hotspot - The hotspot to return whether or not is the default hotspot 68 | * @returns True if passed hotspot matches default, false otherwise 69 | * @public 70 | */ 71 | export const isDefaultHotspot = (hotspot: SanityImageHotspot): boolean => { 72 | const {x, y, width, height} = hotspot 73 | const {x: defaultX, y: defaultY, width: defaultWidth, height: defaultHeight} = DEFAULT_HOTSPOT 74 | 75 | return x === defaultX && y === defaultY && width === defaultWidth && height === defaultHeight 76 | } 77 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | isAssetId, 3 | isAssetIdStub, 4 | isAssetObjectStub, 5 | isAssetPathStub, 6 | isAssetUrlStub, 7 | isFileAssetId, 8 | isImageAssetId, 9 | isReference, 10 | isSanityFileAsset, 11 | isSanityImageAsset, 12 | } from './asserters.js' 13 | export {isUnresolvableError, UnresolvableError} from './errors.js' 14 | export { 15 | DEFAULT_CROP, 16 | DEFAULT_HOTSPOT, 17 | getDefaultCrop, 18 | getDefaultHotspot, 19 | isDefaultCrop, 20 | isDefaultHotspot, 21 | } from './hotspotCrop.js' 22 | export { 23 | getAssetUrlType, 24 | parseAssetFilename, 25 | parseAssetId, 26 | parseAssetUrl, 27 | parseFileAssetId, 28 | parseFileAssetUrl, 29 | parseImageAssetId, 30 | parseImageAssetUrl, 31 | } from './parse.js' 32 | export { 33 | buildFilePath, 34 | buildFileUrl, 35 | buildImagePath, 36 | buildImageUrl, 37 | getUrlFilename, 38 | getUrlPath, 39 | getVanityStub, 40 | isValidFilename, 41 | tryGetAssetPath, 42 | tryGetUrlFilename, 43 | tryGetUrlPath, 44 | } from './paths.js' 45 | export { 46 | getAssetDocumentId, 47 | getExtension, 48 | getFile, 49 | getFileAsset, 50 | getIdFromString, 51 | getImage, 52 | getImageAsset, 53 | getImageDimensions, 54 | getProject, 55 | isAssetFilename, 56 | isFileAssetFilename, 57 | isFileSource, 58 | isImageAssetFilename, 59 | isImageSource, 60 | tryGetAssetDocumentId, 61 | tryGetExtension, 62 | tryGetFile, 63 | tryGetFileAsset, 64 | tryGetIdFromString, 65 | tryGetImage, 66 | tryGetImageAsset, 67 | tryGetImageDimensions, 68 | tryGetProject, 69 | } from './resolve.js' 70 | export type { 71 | AbsoluteRectangle, 72 | FileUrlBuilderOptions, 73 | ImageUrlBuilderOptions, 74 | PathBuilderOptions, 75 | Rectangle, 76 | ResolvedSanityFile, 77 | ResolvedSanityImage, 78 | SafeFunction, 79 | SanityAsset, 80 | SanityAssetIdParts, 81 | SanityAssetIdStub, 82 | SanityAssetObjectStub, 83 | SanityAssetPathStub, 84 | SanityAssetSource, 85 | SanityAssetUrlParts, 86 | SanityAssetUrlStub, 87 | SanityFileAsset, 88 | SanityFileAssetIdParts, 89 | SanityFileObjectStub, 90 | SanityFileSource, 91 | SanityFileUrlParts, 92 | SanityImageAsset, 93 | SanityImageAssetIdParts, 94 | SanityImageCrop, 95 | SanityImageDimensions, 96 | SanityImageFitResult, 97 | SanityImageHotspot, 98 | SanityImageMetadata, 99 | SanityImageObjectStub, 100 | SanityImagePalette, 101 | SanityImageSize, 102 | SanityImageSource, 103 | SanityImageSwatch, 104 | SanityImageUrlParts, 105 | SanityProjectDetails, 106 | SanityReference, 107 | SanitySwatchName, 108 | } from './types.js' 109 | export {isSanityAssetUrl, isSanityFileUrl, isSanityImageUrl} from './urls.js' 110 | -------------------------------------------------------------------------------- /src/parse.ts: -------------------------------------------------------------------------------- 1 | import {isCdnUrl} from './asserters.js' 2 | import { 3 | fileAssetIdPattern, 4 | imageAssetFilenamePattern, 5 | imageAssetIdPattern, 6 | pathPattern, 7 | } from './constants.js' 8 | import {isValidFilename, tryGetUrlFilename} from './paths.js' 9 | import type { 10 | SanityAssetIdParts, 11 | SanityAssetUrlParts, 12 | SanityFileAssetIdParts, 13 | SanityFileUrlParts, 14 | SanityImageAssetIdParts, 15 | SanityImageUrlParts, 16 | } from './types.js' 17 | 18 | /** 19 | * @internal 20 | */ 21 | const exampleFileId = 'file-027401f31c3ac1e6d78c5d539ccd1beff72b9b11-pdf' 22 | 23 | /** 24 | * @internal 25 | */ 26 | const exampleImageId = 'image-027401f31c3ac1e6d78c5d539ccd1beff72b9b11-2000x3000-jpg' 27 | 28 | /** 29 | * Parses a Sanity asset document ID into individual parts (type, id, extension, width/height etc) 30 | * 31 | * @param documentId - Document ID to parse into named parts 32 | * @returns Object of named properties 33 | * @public 34 | * @throws If document ID is invalid 35 | */ 36 | export function parseAssetId(documentId: string): SanityAssetIdParts { 37 | if (imageAssetIdPattern.test(documentId)) { 38 | return parseImageAssetId(documentId) 39 | } 40 | 41 | if (fileAssetIdPattern.test(documentId)) { 42 | return parseFileAssetId(documentId) 43 | } 44 | 45 | throw new Error(`Invalid image/file asset ID: ${documentId}`) 46 | } 47 | 48 | /** 49 | * Parses a Sanity file asset document ID into individual parts (type, id, extension) 50 | * 51 | * @param documentId - File asset document ID to parse into named parts 52 | * @returns Object of named properties 53 | * @public 54 | * @throws If document ID invalid 55 | */ 56 | export function parseFileAssetId(documentId: string): SanityFileAssetIdParts { 57 | if (!fileAssetIdPattern.test(documentId)) { 58 | throw new Error( 59 | `Malformed file asset ID '${documentId}'. Expected an id like "${exampleFileId}"`, 60 | ) 61 | } 62 | 63 | const [, assetId, extension] = documentId.split('-') 64 | return {type: 'file', assetId, extension} 65 | } 66 | 67 | /** 68 | * Parses a Sanity image asset document ID into individual parts (type, id, extension, width, height) 69 | * 70 | * @param documentId - Image asset document ID to parse into named parts 71 | * @returns Object of named properties 72 | * @public 73 | * @throws If document ID invalid 74 | */ 75 | export function parseImageAssetId(documentId: string): SanityImageAssetIdParts { 76 | const [, assetId, dimensionString, extension] = documentId.split('-') 77 | const [width, height] = (dimensionString || '').split('x').map(Number) 78 | 79 | if (!assetId || !dimensionString || !extension || !(width > 0) || !(height > 0)) { 80 | throw new Error(`Malformed asset ID '${documentId}'. Expected an id like "${exampleImageId}".`) 81 | } 82 | 83 | return {type: 'image', assetId, width, height, extension} 84 | } 85 | 86 | /** 87 | * Parses a Sanity asset filename into individual parts (type, id, extension, width, height) 88 | * 89 | * @param filename - Filename to parse into named parts 90 | * @returns Object of named properties 91 | * @public 92 | * @throws If image/filename is invalid 93 | */ 94 | export function parseAssetFilename(filename: string): SanityAssetIdParts { 95 | const file = tryGetUrlFilename(filename) || '' 96 | if (!isValidFilename(file)) { 97 | throw new Error(`Invalid image/file asset filename: ${filename}`) 98 | } 99 | 100 | try { 101 | const type = imageAssetFilenamePattern.test(file) ? 'image' : 'file' 102 | const assetId = file.replace(/\.([a-z0-9+]+)$/i, '-$1') 103 | return parseAssetId(`${type}-${assetId}`) 104 | } catch (err) { 105 | throw new Error(`Invalid image/file asset filename: ${filename}`) 106 | } 107 | } 108 | 109 | /** 110 | * Parses a full Sanity asset URL into individual parts 111 | * (type, project ID, dataset, id, extension, width, height) 112 | * 113 | * @param url - Full URL to parse into named parts 114 | * @returns Object of named properties 115 | * @public 116 | * @throws If URL is invalid or not a Sanity asset URL 117 | */ 118 | export function parseAssetUrl(url: string): SanityAssetUrlParts { 119 | if (!isCdnUrl(url)) { 120 | throw new Error(`URL is not a valid Sanity asset URL: ${url}`) 121 | } 122 | 123 | const path = new URL(url).pathname.replace(/^\/+/, '') 124 | const [projectPath, , projectId, dataset] = path.match(pathPattern) || [] 125 | if (!projectPath || !projectId || !dataset) { 126 | throw new Error(`URL is not a valid Sanity asset URL: ${url}`) 127 | } 128 | 129 | const [filename, vanityFilename] = path.slice(projectPath.length).split('/') 130 | const parsed = parseAssetFilename(filename) 131 | 132 | return { 133 | ...parsed, 134 | projectId, 135 | dataset, 136 | vanityFilename, 137 | } 138 | } 139 | 140 | /** 141 | * Parses a full Sanity image asset URL into individual parts 142 | * (type, project ID, dataset, id, extension, width, height) 143 | * 144 | * @param url - Full URL to parse into named parts 145 | * @returns Object of named properties 146 | * @public 147 | * @throws If URL is invalid or not a Sanity image asset URL 148 | */ 149 | export function parseImageAssetUrl(url: string): SanityImageUrlParts { 150 | const parsed = parseAssetUrl(url) 151 | if (parsed.type !== 'image') { 152 | throw new Error(`URL is not a valid Sanity image asset URL: ${url}`) 153 | } 154 | 155 | return parsed 156 | } 157 | 158 | /** 159 | * Parses a full Sanity file asset URL into individual parts 160 | * (type, project ID, dataset, id, extension, width, height) 161 | * 162 | * @param url - Full URL to parse into named parts 163 | * @returns Object of named properties 164 | * @public 165 | * @throws If URL is invalid or not a Sanity file asset URL 166 | */ 167 | export function parseFileAssetUrl(url: string): SanityFileUrlParts { 168 | const parsed = parseAssetUrl(url) 169 | if (parsed.type !== 'file') { 170 | throw new Error(`URL is not a valid Sanity file asset URL: ${url}`) 171 | } 172 | 173 | return parsed 174 | } 175 | 176 | /** 177 | * Validates that a given URL is a Sanity asset URL, and returns the asset type if valid. 178 | * 179 | * @param url - URL to extract asset type from 180 | * @returns Asset type if valid URL, false otherwise 181 | * @public 182 | */ 183 | export function getAssetUrlType(url: string): 'image' | 'file' | false { 184 | try { 185 | return parseAssetUrl(url).type 186 | } catch (err) { 187 | return false 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/paths.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isAssetObjectStub, 3 | isAssetPathStub, 4 | isAssetUrlStub, 5 | isCdnUrl, 6 | isReference, 7 | } from './asserters.js' 8 | import { 9 | cdnUrl, 10 | fileAssetFilenamePattern, 11 | imageAssetFilenamePattern, 12 | pathPattern, 13 | } from './constants.js' 14 | import {UnresolvableError} from './errors.js' 15 | import type { 16 | FileUrlBuilderOptions, 17 | ImageUrlBuilderOptions, 18 | PathBuilderOptions, 19 | SanityAssetSource, 20 | SanityFileUrlParts, 21 | SanityImageUrlParts, 22 | } from './types.js' 23 | import {getForgivingResolver} from './utils.js' 24 | 25 | /** 26 | * Builds the base image path from the minimal set of parts required to assemble it 27 | * 28 | * @param asset - An asset-like shape defining ID, dimensions and extension 29 | * @param options - Project ID and dataset the image belongs to, along with other options 30 | * @returns The path to the image 31 | * @public 32 | */ 33 | export function buildImagePath( 34 | asset: ImageUrlBuilderOptions | SanityImageUrlParts, 35 | options?: PathBuilderOptions, 36 | ): string { 37 | const projectId = options?.projectId || asset.projectId 38 | const dataset = options?.dataset || asset.dataset 39 | if (!projectId || !dataset) { 40 | throw new Error('Project details (projectId and dataset) required to resolve path for image') 41 | } 42 | 43 | const dimensions = 44 | 'metadata' in asset ? asset.metadata.dimensions : {width: asset.width, height: asset.height} 45 | const originalFilename = 'originalFilename' in asset ? asset.originalFilename : undefined 46 | const {assetId, extension, vanityFilename} = asset 47 | const {width, height} = dimensions 48 | const vanity = getVanityStub(originalFilename, vanityFilename, options) 49 | 50 | return `images/${projectId}/${dataset}/${assetId}-${width}x${height}.${extension}${vanity}` 51 | } 52 | 53 | /** 54 | * Builds the base image URL from the minimal set of parts required to assemble it 55 | * 56 | * @param asset - An asset-like shape defining ID, dimensions and extension 57 | * @param options - Project ID and dataset the image belongs to 58 | * @returns The URL to the image, as a string 59 | * @public 60 | */ 61 | export function buildImageUrl( 62 | asset: ImageUrlBuilderOptions | SanityImageUrlParts, 63 | options?: PathBuilderOptions, 64 | ): string { 65 | const baseUrl = options?.baseUrl || cdnUrl 66 | return `${baseUrl}/${buildImagePath(asset, options)}` 67 | } 68 | 69 | /** 70 | * Builds the base file path from the minimal set of parts required to assemble it 71 | * 72 | * @param asset - An asset-like shape defining ID, dimensions and extension 73 | * @param options - Project ID and dataset the file belongs to, along with other options 74 | * @returns The path to the file 75 | * @public 76 | */ 77 | export function buildFilePath( 78 | asset: FileUrlBuilderOptions | SanityFileUrlParts, 79 | options?: PathBuilderOptions, 80 | ): string { 81 | const projectId = options?.projectId || asset.projectId 82 | const dataset = options?.dataset || asset.dataset 83 | if (!projectId || !dataset) { 84 | throw new Error('Project details (projectId and dataset) required to resolve path for file') 85 | } 86 | 87 | const originalFilename = 'originalFilename' in asset ? asset.originalFilename : undefined 88 | const {assetId, extension, vanityFilename} = asset 89 | const vanity = getVanityStub(originalFilename, vanityFilename, options) 90 | 91 | return `files/${projectId}/${dataset}/${assetId}.${extension}${vanity}` 92 | } 93 | 94 | /** 95 | * Builds the base file URL from the minimal set of parts required to assemble it 96 | * 97 | * @param asset - An asset-like shape defining ID and extension 98 | * @param options - Project ID and dataset the file belongs to, along with other options 99 | * @returns The URL to the file, as a string 100 | * @public 101 | */ 102 | export function buildFileUrl(asset: FileUrlBuilderOptions, options?: PathBuilderOptions): string { 103 | const baseUrl = options?.baseUrl || cdnUrl 104 | return `${baseUrl}/${buildFilePath(asset, options)}` 105 | } 106 | 107 | /** 108 | * Checks whether or not the given URL contains an asset path 109 | * 110 | * @param url - URL or path name 111 | * @returns Whether or not it contained an asset path 112 | * @public 113 | */ 114 | function hasPath(urlOrPath: string): boolean { 115 | return pathPattern.test(tryGetUrlPath(urlOrPath) || '') 116 | } 117 | 118 | /** 119 | * Tries to get the asset path from a given asset source 120 | * 121 | * @param src - The source image to infer an asset path from 122 | * @returns A path if resolvable, undefined otherwise 123 | * @public 124 | */ 125 | export function tryGetAssetPath(src: SanityAssetSource): string | undefined { 126 | if (isAssetObjectStub(src)) { 127 | return tryGetAssetPath(src.asset) 128 | } 129 | 130 | if (isReference(src)) { 131 | return undefined 132 | } 133 | 134 | if (typeof src === 'string') { 135 | return hasPath(src) ? getUrlPath(src) : undefined 136 | } 137 | 138 | if (isAssetPathStub(src)) { 139 | return src.path 140 | } 141 | 142 | if (isAssetUrlStub(src)) { 143 | return getUrlPath(src.url) 144 | } 145 | 146 | return undefined 147 | } 148 | 149 | /** 150 | * Strips the CDN URL and query params from a URL, eg: 151 | * `https://cdn.sanity.io/images/project/dataset/filename-200x200.jpg?foo=bar` → 152 | * `images/project/dataset/filename-200x200.jpg` 153 | * 154 | * @param url - URL to get path name from 155 | * @returns The path of a CDN URL 156 | * @public 157 | * @throws If URL is not a valid Sanity asset URL 158 | */ 159 | export function getUrlPath(url: string): string { 160 | if (pathPattern.test(url)) { 161 | // Already just a path 162 | return url 163 | } 164 | 165 | if (!isCdnUrl(url)) { 166 | throw new UnresolvableError(`Failed to resolve path from URL "${url}"`) 167 | } 168 | 169 | return new URL(url).pathname.replace(/^\/+/, '') 170 | } 171 | 172 | /** 173 | * {@inheritDoc getUrlPath} 174 | * @returns Returns `undefined` instead of throwing if a value cannot be resolved 175 | * @public 176 | */ 177 | export const tryGetUrlPath = getForgivingResolver(getUrlPath) 178 | 179 | /** 180 | * Strips the CDN URL, path and query params from a URL, eg: 181 | * `https://cdn.sanity.io/images/project/dataset/filename-200x200.jpg?foo=bar` → 182 | * `filename-200x200.jpg` 183 | * 184 | * @param url - URL to get filename from 185 | * @returns The filename of an URL, if URL matches the CDN URL 186 | * @public 187 | * @throws If URL is not a valid Sanity asset URL 188 | */ 189 | export function getUrlFilename(url: string): string { 190 | const path = tryGetUrlPath(url) || url 191 | const filename = path.replace(/^(images|files)\/[a-z0-9]+\/[a-z0-9][-\w]\/*/, '') 192 | if (!isValidFilename(filename)) { 193 | throw new UnresolvableError(`Failed to resolve filename from URL "${url}"`) 194 | } 195 | 196 | return filename 197 | } 198 | 199 | /** 200 | * {@inheritDoc getUrlFilename} 201 | * @returns Returns `undefined` instead of throwing if a value cannot be resolved 202 | * @public 203 | */ 204 | export const tryGetUrlFilename = getForgivingResolver(getUrlFilename) 205 | 206 | /** 207 | * Checks whether or not a given filename matches the expected Sanity asset filename pattern 208 | * 209 | * @param filename - Filename to check for validity 210 | * @returns Whether or not the specified filename is valid 211 | * @public 212 | */ 213 | export function isValidFilename(filename: string): boolean { 214 | return fileAssetFilenamePattern.test(filename) || imageAssetFilenamePattern.test(filename) 215 | } 216 | 217 | /** 218 | * Get the "path stub" at the end of the path, if the user hasn't explicitly opted out of this behavior 219 | * 220 | * @param originalFilename - The original filename of the asset 221 | * @param vanityFilename - The vanity filename of the asset 222 | * @param options - Options to control the behavior of the path builder 223 | * @returns The vanity stub, if any 224 | * @public 225 | */ 226 | export function getVanityStub( 227 | originalFilename: string | undefined, 228 | vanityFilename: string | undefined, 229 | options?: PathBuilderOptions, 230 | ): string { 231 | const vanity = vanityFilename || originalFilename 232 | return options?.useVanityName === false || !vanity ? '' : `/${vanity}` 233 | } 234 | -------------------------------------------------------------------------------- /src/resolve.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isAssetIdStub, 3 | isAssetObjectStub, 4 | isAssetPathStub, 5 | isAssetUrlStub, 6 | isCdnUrl, 7 | isReference, 8 | isSanityFileAsset, 9 | isSanityImageAsset, 10 | } from './asserters.js' 11 | import { 12 | cdnUrl, 13 | dummyProject, 14 | fileAssetFilenamePattern, 15 | idPattern, 16 | imageAssetFilenamePattern, 17 | pathPattern, 18 | } from './constants.js' 19 | import {UnresolvableError} from './errors.js' 20 | import {getDefaultCrop, getDefaultHotspot} from './hotspotCrop.js' 21 | import {parseFileAssetId, parseImageAssetId} from './parse.js' 22 | import { 23 | buildFilePath, 24 | buildFileUrl, 25 | buildImagePath, 26 | buildImageUrl, 27 | getUrlPath, 28 | tryGetAssetPath, 29 | } from './paths.js' 30 | import type { 31 | PathBuilderOptions, 32 | ResolvedSanityFile, 33 | ResolvedSanityImage, 34 | SanityAssetSource, 35 | SanityFileAsset, 36 | SanityFileObjectStub, 37 | SanityFileSource, 38 | SanityImageAsset, 39 | SanityImageDimensions, 40 | SanityImageObjectStub, 41 | SanityImageSource, 42 | SanityProjectDetails, 43 | } from './types.js' 44 | import {getForgivingResolver} from './utils.js' 45 | 46 | /** 47 | * Returns the width, height and aspect ratio of a passed image asset, from any 48 | * inferrable structure (id, url, path, asset document, image object etc) 49 | * 50 | * @param src - Input source (image object, asset, reference, id, url, path) 51 | * @returns Object with width, height and aspect ratio properties 52 | * 53 | * @throws {@link UnresolvableError} 54 | * Throws if passed image source could not be resolved to an asset ID 55 | * @public 56 | */ 57 | export function getImageDimensions(src: SanityImageSource): SanityImageDimensions { 58 | const imageId = getAssetDocumentId(src) 59 | const {width, height} = parseImageAssetId(imageId) 60 | const aspectRatio = width / height 61 | return {width, height, aspectRatio} 62 | } 63 | 64 | /** 65 | * {@inheritDoc getImageDimensions} 66 | * @returns Returns `undefined` instead of throwing if a value cannot be resolved 67 | * @public 68 | */ 69 | export const tryGetImageDimensions = getForgivingResolver(getImageDimensions) 70 | 71 | /** 72 | * Returns the file extension for a given asset 73 | * 74 | * @param src - Input source (file/image object, asset, reference, id, url, path) 75 | * @returns The file extension, if resolvable (no `.` included) 76 | * 77 | * @throws {@link UnresolvableError} 78 | * Throws if passed asset source could not be resolved to an asset ID 79 | * @public 80 | */ 81 | export function getExtension(src: SanityAssetSource): string { 82 | return isFileSource(src) 83 | ? getFile(src, dummyProject).asset.extension 84 | : getImage(src, dummyProject).asset.extension 85 | } 86 | 87 | /** 88 | * {@inheritDoc getExtension} 89 | * @returns Returns `undefined` instead of throwing if a value cannot be resolved 90 | * @public 91 | */ 92 | export const tryGetExtension = getForgivingResolver(getExtension) 93 | 94 | /** 95 | * Tries to resolve an image object with as much information as possible, 96 | * from any inferrable structure (id, url, path, image object etc) 97 | * 98 | * @param src - Input source (image object, asset, reference, id, url, path) 99 | * @param project - Project ID and dataset the image belongs to 100 | * @returns Image object 101 | * 102 | * @throws {@link UnresolvableError} 103 | * Throws if passed image source could not be resolved to an asset ID 104 | * @public 105 | */ 106 | export function getImage( 107 | src: SanityImageSource, 108 | project?: SanityProjectDetails, 109 | ): ResolvedSanityImage { 110 | const projectDetails = project || tryGetProject(src) 111 | const asset = getImageAsset(src, projectDetails) 112 | 113 | const img = src as SanityImageObjectStub 114 | return { 115 | asset, 116 | crop: img.crop || getDefaultCrop(), 117 | hotspot: img.hotspot || getDefaultHotspot(), 118 | } 119 | } 120 | 121 | /** 122 | * {@inheritDoc getImage} 123 | * @returns Returns `undefined` instead of throwing if a value cannot be resolved 124 | * @public 125 | */ 126 | export const tryGetImage = getForgivingResolver(getImage) 127 | 128 | /** 129 | * Tries to resolve a (partial) image asset document with as much information as possible, 130 | * from any inferrable structure (id, url, path, image object etc) 131 | * 132 | * @param src - Input source (image object, asset, reference, id, url, path) 133 | * @param project - Project ID and dataset the image belongs to 134 | * @returns Image asset document 135 | * 136 | * @throws {@link UnresolvableError} 137 | * Throws if passed image source could not be resolved to an asset ID 138 | * @public 139 | */ 140 | export function getImageAsset( 141 | src: SanityImageSource, 142 | project?: SanityProjectDetails, 143 | ): SanityImageAsset { 144 | const projectDetails = project || getProject(src) 145 | const pathOptions: PathBuilderOptions = {...projectDetails, useVanityName: false} 146 | 147 | const _id = getAssetDocumentId(src) 148 | const sourceObj = src as SanityImageObjectStub 149 | const source = (sourceObj.asset || src) as SanityImageAsset 150 | const metadata = source.metadata || {} 151 | const {assetId, width, height, extension} = parseImageAssetId(_id) 152 | const aspectRatio = width / height 153 | const baseAsset: SanityImageAsset = { 154 | ...(isSanityImageAsset(src) ? src : {}), 155 | _id, 156 | _type: 'sanity.imageAsset', 157 | assetId, 158 | extension, 159 | metadata: { 160 | ...metadata, 161 | dimensions: {width, height, aspectRatio}, 162 | }, 163 | 164 | // Placeholders, overwritten below 165 | url: '', 166 | path: '', 167 | } 168 | 169 | return { 170 | ...baseAsset, 171 | path: buildImagePath(baseAsset, pathOptions), 172 | url: buildImageUrl(baseAsset, pathOptions), 173 | } 174 | } 175 | 176 | /** 177 | * {@inheritDoc getImageAsset} 178 | * @returns Returns `undefined` instead of throwing if a value cannot be resolved 179 | * @public 180 | */ 181 | export const tryGetImageAsset = getForgivingResolver(getImageAsset) 182 | 183 | /** 184 | * Tries to resolve an file object with as much information as possible, 185 | * from any inferrable structure (id, url, path, file object etc) 186 | * 187 | * @param src - Input source (file object, asset, reference, id, url, path) 188 | * @param project - Project ID and dataset the file belongs to 189 | * @returns File object 190 | * 191 | * @throws {@link UnresolvableError} 192 | * Throws if passed file source could not be resolved to an asset ID 193 | * @public 194 | */ 195 | export function getFile(src: SanityFileSource, project?: SanityProjectDetails): ResolvedSanityFile { 196 | const projectDetails = project || tryGetProject(src) 197 | const asset = getFileAsset(src, projectDetails) 198 | return {asset} 199 | } 200 | 201 | /** 202 | * {@inheritDoc getFile} 203 | * @returns Returns `undefined` instead of throwing if a value cannot be resolved 204 | * @public 205 | */ 206 | export const tryGetFile = getForgivingResolver(getFile) 207 | 208 | /** 209 | * Tries to resolve a (partial) file asset document with as much information as possible, 210 | * from any inferrable structure (id, url, path, file object etc) 211 | * 212 | * @param src - Input source (file object, asset, reference, id, url, path) 213 | * @param options - Project ID and dataset the file belongs to, along with other options 214 | * @returns File asset document 215 | * 216 | * @throws {@link UnresolvableError} 217 | * Throws if passed file source could not be resolved to an asset ID 218 | * @public 219 | */ 220 | export function getFileAsset(src: SanityFileSource, options?: PathBuilderOptions): SanityFileAsset { 221 | const projectDetails: PathBuilderOptions = {...(options || getProject(src)), useVanityName: false} 222 | 223 | const _id = getAssetDocumentId(src) 224 | const sourceObj = src as SanityFileObjectStub 225 | const source = (sourceObj.asset || src) as SanityFileAsset 226 | const {assetId, extension} = parseFileAssetId(_id) 227 | const baseAsset: SanityFileAsset = { 228 | ...(isSanityFileAsset(src) ? src : {}), 229 | _id, 230 | _type: 'sanity.fileAsset', 231 | assetId, 232 | extension, 233 | metadata: source.metadata || {}, 234 | 235 | // Placeholders, overwritten below 236 | url: '', 237 | path: '', 238 | } 239 | 240 | return { 241 | ...baseAsset, 242 | path: buildFilePath(baseAsset, projectDetails), 243 | url: buildFileUrl(baseAsset, projectDetails), 244 | } 245 | } 246 | 247 | /** 248 | * {@inheritDoc getFileAsset} 249 | * @returns Returns `undefined` instead of throwing if a value cannot be resolved 250 | * @public 251 | */ 252 | export const tryGetFileAsset = getForgivingResolver(getFileAsset) 253 | 254 | /** 255 | * Tries to resolve the asset document ID from any inferrable structure 256 | * 257 | * @param src - Input source (image/file object, asset, reference, id, url, path) 258 | * @returns The asset document ID 259 | * 260 | * @throws {@link UnresolvableError} 261 | * Throws if passed asset source could not be resolved to an asset document ID 262 | * @public 263 | */ 264 | export function getAssetDocumentId(src: unknown): string { 265 | const source = isAssetObjectStub(src) ? src.asset : src 266 | 267 | let id = '' 268 | if (typeof source === 'string') { 269 | id = getIdFromString(source) 270 | } else if (isReference(source)) { 271 | id = source._ref 272 | } else if (isAssetIdStub(source)) { 273 | id = source._id 274 | } else if (isAssetPathStub(source)) { 275 | id = idFromUrl(`${cdnUrl}/${source.path}`) 276 | } else if (isAssetUrlStub(source)) { 277 | id = idFromUrl(source.url) 278 | } 279 | 280 | const hasId = id && idPattern.test(id) 281 | if (!hasId) { 282 | throw new UnresolvableError(src) 283 | } 284 | 285 | return id 286 | } 287 | 288 | /** 289 | * {@inheritDoc getAssetDocumentId} 290 | * @returns Returns `undefined` instead of throwing if a value cannot be resolved 291 | * @public 292 | */ 293 | export const tryGetAssetDocumentId = getForgivingResolver(getAssetDocumentId) 294 | 295 | /** 296 | * Tries to cooerce a string (ID, URL or path) to an image asset ID 297 | * 298 | * @param str - Input string (ID, URL or path) 299 | * @returns string 300 | * 301 | * 302 | * @throws {@link UnresolvableError} 303 | * Throws if passed image source could not be resolved to an asset ID 304 | * @public 305 | */ 306 | export function getIdFromString(str: string): string { 307 | if (idPattern.test(str)) { 308 | // Already an ID 309 | return str 310 | } 311 | 312 | const isAbsoluteUrl = isCdnUrl(str) 313 | const path = isAbsoluteUrl ? new URL(str).pathname : str 314 | 315 | if (path.indexOf('/images') === 0 || path.indexOf('/files') === 0) { 316 | // Full URL 317 | return idFromUrl(str) 318 | } 319 | 320 | if (pathPattern.test(str)) { 321 | // Path 322 | return idFromUrl(`${cdnUrl}/${str}`) 323 | } 324 | 325 | if (isFileAssetFilename(str)) { 326 | // Just a filename (projectId/dataset irrelevant: just need asset ID) 327 | return idFromUrl(`${cdnUrl}/files/a/b/${str}`) 328 | } 329 | 330 | if (isImageAssetFilename(str)) { 331 | // Just a filename (projectId/dataset irrelevant: just need asset ID) 332 | return idFromUrl(`${cdnUrl}/images/a/b/${str}`) 333 | } 334 | 335 | throw new UnresolvableError(str) 336 | } 337 | 338 | /** 339 | * {@inheritDoc getIdFromString} 340 | * @returns Returns `undefined` instead of throwing if a value cannot be resolved 341 | * @public 342 | */ 343 | export const tryGetIdFromString = getForgivingResolver(getIdFromString) 344 | 345 | /** 346 | * Converts from a full asset URL to just the asset document ID 347 | * 348 | * @param url - A full asset URL to convert 349 | * @returns string 350 | * @public 351 | */ 352 | function idFromUrl(url: string): string { 353 | const path = getUrlPath(url) 354 | const [type, , , fileName] = path.split('/') 355 | const prefix = type.replace(/s$/, '') 356 | return `${prefix}-${fileName.replace(/\./g, '-')}` 357 | } 358 | 359 | /** 360 | * Resolves project ID and dataset the image belongs to, based on full URL or path 361 | * @param src - Image URL or path 362 | * @returns object | undefined 363 | * 364 | * @throws {@link UnresolvableError} 365 | * Throws if passed image source could not be resolved to an asset ID 366 | * @public 367 | */ 368 | export function getProject(src: SanityImageSource): SanityProjectDetails { 369 | const path = tryGetAssetPath(src) 370 | if (!path) { 371 | throw new UnresolvableError(src, 'Failed to resolve project ID and dataset from source') 372 | } 373 | 374 | const [, , projectId, dataset] = path.match(pathPattern) || [] 375 | if (!projectId || !dataset) { 376 | throw new UnresolvableError(src, 'Failed to resolve project ID and dataset from source') 377 | } 378 | 379 | return {projectId, dataset} 380 | } 381 | 382 | /** 383 | * {@inheritDoc getProject} 384 | * @returns Returns `undefined` instead of throwing if a value cannot be resolved 385 | * @public 386 | */ 387 | export const tryGetProject = getForgivingResolver(getProject) 388 | 389 | /** 390 | * Returns whether or not the passed filename is a valid image asset filename 391 | * 392 | * @param filename - Filename to validate 393 | * @returns Whether or not the filename is an image asset filename 394 | * @public 395 | */ 396 | export function isImageAssetFilename(filename: string): boolean { 397 | return imageAssetFilenamePattern.test(filename) 398 | } 399 | 400 | /** 401 | * Returns whether or not the passed filename is a valid file asset filename 402 | * 403 | * @param filename - Filename to validate 404 | * @returns Whether or not the filename is a file asset filename 405 | * @public 406 | */ 407 | export function isFileAssetFilename(filename: string): boolean { 408 | return fileAssetFilenamePattern.test(filename) 409 | } 410 | 411 | /** 412 | * Returns whether or not the passed filename is a valid file or image asset filename 413 | * 414 | * @param filename - Filename to validate 415 | * @returns Whether or not the filename is an asset filename 416 | * @public 417 | */ 418 | export function isAssetFilename(filename: string): boolean { 419 | return isImageAssetFilename(filename) || isFileAssetFilename(filename) 420 | } 421 | 422 | /** 423 | * Return whether or not the passed source is a file source 424 | * 425 | * @param src - Source to check 426 | * @returns Whether or not the given source is a file source 427 | * @public 428 | */ 429 | export function isFileSource(src: unknown): src is SanityFileSource { 430 | const assetId = tryGetAssetDocumentId(src) 431 | return assetId ? assetId.startsWith('file-') : false 432 | } 433 | 434 | /** 435 | * Return whether or not the passed source is an image source 436 | * 437 | * @param src - Source to check 438 | * @returns Whether or not the given source is an image source 439 | * @public 440 | */ 441 | export function isImageSource(src: unknown): src is SanityImageSource { 442 | const assetId = tryGetAssetDocumentId(src) 443 | return assetId ? assetId.startsWith('image-') : false 444 | } 445 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @public 3 | */ 4 | export interface SanityProjectDetails { 5 | projectId: string 6 | dataset: string 7 | baseUrl?: string 8 | } 9 | 10 | /** 11 | * @public 12 | */ 13 | export interface PathBuilderOptions extends Partial { 14 | useVanityName?: boolean 15 | } 16 | 17 | /** 18 | * @public 19 | */ 20 | export type SanityAssetIdParts = SanityFileAssetIdParts | SanityImageAssetIdParts 21 | 22 | /** 23 | * @public 24 | */ 25 | export type SanityAssetUrlParts = SanityFileUrlParts | SanityImageUrlParts 26 | 27 | /** 28 | * @public 29 | */ 30 | export interface SanityImageAssetIdParts { 31 | type: 'image' 32 | assetId: string 33 | extension: string 34 | width: number 35 | height: number 36 | } 37 | 38 | /** 39 | * @public 40 | */ 41 | export interface SanityImageUrlParts extends SanityProjectDetails, SanityImageAssetIdParts { 42 | vanityFilename?: string 43 | } 44 | 45 | /** 46 | * @public 47 | */ 48 | export interface ImageUrlBuilderOptions extends Partial { 49 | assetId: string 50 | extension: string 51 | metadata: { 52 | dimensions: { 53 | width: number 54 | height: number 55 | } 56 | } 57 | 58 | /** 59 | * Alias of `vanityFilename` - prefers `vanityFilename` if both are set 60 | */ 61 | originalFilename?: string 62 | 63 | /** 64 | * Alias of `originalFilename` - prefers `vanityFilename` if both are set 65 | */ 66 | vanityFilename?: string 67 | } 68 | 69 | /** 70 | * @public 71 | */ 72 | export interface SanityFileAssetIdParts { 73 | type: 'file' 74 | assetId: string 75 | extension: string 76 | } 77 | 78 | /** 79 | * @public 80 | */ 81 | export interface SanityFileUrlParts extends SanityProjectDetails, SanityFileAssetIdParts { 82 | vanityFilename?: string 83 | } 84 | 85 | /** 86 | * @public 87 | */ 88 | export interface FileUrlBuilderOptions extends Partial { 89 | assetId: string 90 | extension: string 91 | 92 | /** 93 | * Alias of `vanityFilename` - prefers `vanityFilename` if both are set 94 | */ 95 | originalFilename?: string 96 | 97 | /** 98 | * Alias of `originalFilename` - prefers `vanityFilename` if both are set 99 | */ 100 | vanityFilename?: string 101 | } 102 | 103 | /** 104 | * @public 105 | */ 106 | export type SanityAssetSource = SanityFileSource | SanityImageSource 107 | 108 | /** 109 | * @public 110 | */ 111 | export type SanityFileSource = 112 | | string 113 | | SanityReference 114 | | SanityFileAsset 115 | | SanityAssetIdStub 116 | | SanityAssetUrlStub 117 | | SanityAssetPathStub 118 | | SanityFileObjectStub 119 | 120 | /** 121 | * @public 122 | */ 123 | export type SanityImageSource = 124 | | string 125 | | SanityReference 126 | | SanityImageAsset 127 | | SanityAssetIdStub 128 | | SanityAssetUrlStub 129 | | SanityAssetPathStub 130 | | SanityImageObjectStub 131 | 132 | /** 133 | * @public 134 | */ 135 | export type SanitySwatchName = 136 | | 'darkMuted' 137 | | 'darkVibrant' 138 | | 'dominant' 139 | | 'lightMuted' 140 | | 'lightVibrant' 141 | | 'muted' 142 | | 'vibrant' 143 | 144 | /** 145 | * @public 146 | */ 147 | export interface Rectangle { 148 | x: number 149 | y: number 150 | width: number 151 | height: number 152 | } 153 | 154 | /** 155 | * @public 156 | */ 157 | export interface AbsoluteRectangle { 158 | top: number 159 | left: number 160 | right: number 161 | bottom: number 162 | } 163 | 164 | /** 165 | * @public 166 | */ 167 | export interface SanityReference { 168 | _ref: string 169 | _weak?: boolean 170 | } 171 | 172 | /** 173 | * @public 174 | */ 175 | export interface SanityAssetIdStub { 176 | _id: string 177 | } 178 | 179 | /** 180 | * @public 181 | */ 182 | export interface SanityAssetPathStub { 183 | path: string 184 | } 185 | 186 | /** 187 | * @public 188 | */ 189 | export interface SanityAssetUrlStub { 190 | url: string 191 | } 192 | 193 | /** 194 | * @public 195 | */ 196 | export interface SanityAsset { 197 | _id: string 198 | _type: string 199 | url: string 200 | path: string 201 | assetId: string 202 | extension: string 203 | originalFilename?: string 204 | } 205 | 206 | /** 207 | * @public 208 | */ 209 | export type SanityImageAsset = SanityAsset & { 210 | _type: 'sanity.imageAsset' 211 | metadata: SanityImageMetadata 212 | } 213 | 214 | /** 215 | * @public 216 | */ 217 | export type SanityFileAsset = SanityAsset & { 218 | _type: 'sanity.fileAsset' 219 | metadata: {[key: string]: unknown} 220 | } 221 | 222 | /** 223 | * @public 224 | */ 225 | export interface SanityImageMetadata { 226 | dimensions: SanityImageDimensions 227 | lqip?: string 228 | blurHash?: string 229 | palette?: SanityImagePalette 230 | [key: string]: unknown 231 | } 232 | 233 | /** 234 | * @public 235 | */ 236 | export interface SanityImageSize { 237 | height: number 238 | width: number 239 | } 240 | 241 | /** 242 | * @public 243 | */ 244 | export type SanityImageDimensions = SanityImageSize & { 245 | aspectRatio: number 246 | } 247 | 248 | /** 249 | * @public 250 | */ 251 | export interface SanityImageCrop { 252 | _type?: string 253 | left: number 254 | bottom: number 255 | right: number 256 | top: number 257 | } 258 | 259 | /** 260 | * @public 261 | */ 262 | export interface SanityImageHotspot { 263 | _type?: string 264 | width: number 265 | height: number 266 | x: number 267 | y: number 268 | } 269 | 270 | /** 271 | * @public 272 | */ 273 | export interface SanityFileObjectStub { 274 | _type?: string 275 | asset: 276 | | SanityReference 277 | | SanityFileAsset 278 | | SanityAssetIdStub 279 | | SanityAssetPathStub 280 | | SanityAssetUrlStub 281 | [key: string]: unknown 282 | } 283 | 284 | /** 285 | * @public 286 | */ 287 | export interface SanityImageObjectStub { 288 | _type?: string 289 | asset: 290 | | SanityReference 291 | | SanityImageAsset 292 | | SanityAssetIdStub 293 | | SanityAssetPathStub 294 | | SanityAssetUrlStub 295 | crop?: SanityImageCrop 296 | hotspot?: SanityImageHotspot 297 | [key: string]: unknown 298 | } 299 | 300 | /** 301 | * @public 302 | */ 303 | export interface ResolvedSanityImage { 304 | _type?: string 305 | asset: SanityImageAsset 306 | crop: SanityImageCrop 307 | hotspot: SanityImageHotspot 308 | [key: string]: unknown 309 | } 310 | 311 | /** 312 | * @public 313 | */ 314 | export interface ResolvedSanityFile { 315 | _type?: string 316 | asset: SanityFileAsset 317 | [key: string]: unknown 318 | } 319 | 320 | /** 321 | * @public 322 | */ 323 | export type SanityAssetObjectStub = SanityFileObjectStub | SanityImageObjectStub 324 | 325 | /** 326 | * @public 327 | */ 328 | export interface SanityImagePalette { 329 | _type?: string 330 | darkMuted?: SanityImageSwatch 331 | darkVibrant?: SanityImageSwatch 332 | dominant?: SanityImageSwatch 333 | lightMuted?: SanityImageSwatch 334 | lightVibrant?: SanityImageSwatch 335 | muted?: SanityImageSwatch 336 | vibrant?: SanityImageSwatch 337 | [key: string]: unknown 338 | } 339 | 340 | /** 341 | * @public 342 | */ 343 | export interface SanityImageSwatch { 344 | background: string 345 | foreground: string 346 | population: number 347 | title?: string 348 | } 349 | 350 | /** 351 | * @public 352 | */ 353 | export interface SanityImageFitResult { 354 | width?: number 355 | height?: number 356 | rect: Rectangle 357 | } 358 | 359 | /** 360 | * A "safe function" is a wrapped function that would normally throw an UnresolvableError, 361 | * but will instead return `undefined`. Other errors are still thrown. 362 | * 363 | * @public 364 | */ 365 | export type SafeFunction = (...args: Args) => Return | undefined 366 | -------------------------------------------------------------------------------- /src/urls.ts: -------------------------------------------------------------------------------- 1 | import {getAssetUrlType} from './parse.js' 2 | 3 | /** 4 | * Checks whether or not a given URL is a valid Sanity asset URL 5 | * 6 | * @param url - URL to test 7 | * @returns True if url is a valid Sanity asset URL, false otherwise 8 | * @public 9 | */ 10 | export function isSanityAssetUrl(url: string): boolean { 11 | return getAssetUrlType(url) !== false 12 | } 13 | 14 | /** 15 | * Checks whether or not a given URL is a valid Sanity image asset URL 16 | * 17 | * @param url - URL to test 18 | * @returns True if url is a valid Sanity image asset URL, false otherwise 19 | * @public 20 | */ 21 | export function isSanityImageUrl(url: string): boolean { 22 | return getAssetUrlType(url) === 'image' 23 | } 24 | 25 | /** 26 | * Checks whether or not a given URL is a valid Sanity file asset URL 27 | * 28 | * @param url - URL to test 29 | * @returns True if url is a valid Sanity file asset URL, false otherwise 30 | * @public 31 | */ 32 | export function isSanityFileUrl(url: string): boolean { 33 | return getAssetUrlType(url) === 'file' 34 | } 35 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import {isUnresolvableError} from './errors' 2 | import type {SafeFunction} from './types' 3 | 4 | /** 5 | * Returns a getter which returns `undefined` instead of throwing, 6 | * if encountering an `UnresolvableError` 7 | * 8 | * @param method - Function to use as resolver 9 | * @returns Function that returns `undefined` if passed resolver throws UnresolvableError 10 | * @internal 11 | */ 12 | export function getForgivingResolver( 13 | method: (...args: Args) => Return, 14 | ): SafeFunction { 15 | return (...args: Args): Return | undefined => { 16 | try { 17 | return method(...args) 18 | } catch (err) { 19 | if (isUnresolvableError(err)) { 20 | return undefined 21 | } 22 | 23 | throw err 24 | } 25 | } 26 | } 27 | 28 | /** 29 | * Checks whether or not the passed object is an object (and not `null`) 30 | * 31 | * @param obj Item to check whether or not is an object 32 | * @returns Whether or not `obj` is an object 33 | * @internal 34 | */ 35 | export function isObject(obj: unknown): obj is object { 36 | return obj !== null && !Array.isArray(obj) && typeof obj === 'object' 37 | } 38 | -------------------------------------------------------------------------------- /test/asserters.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from 'vitest' 2 | 3 | import { 4 | isAssetId, 5 | isFileAssetId, 6 | isImageAssetId, 7 | isSanityAssetUrl, 8 | isSanityFileUrl, 9 | isSanityImageUrl, 10 | } from '../src/index.js' 11 | 12 | // isSanityAssetUrl() 13 | test('isSanityAssetUrl(): returns true for image urls', () => { 14 | expect( 15 | isSanityAssetUrl( 16 | 'https://cdn.sanity.io/images/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840.jpg', 17 | ), 18 | ).toBe(true) 19 | }) 20 | 21 | test('isSanityAssetUrl(): returns true for image urls (staging)', () => { 22 | expect( 23 | isSanityAssetUrl( 24 | 'https://cdn.sanity.staging/images/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840.jpg', 25 | ), 26 | ).toBe(true) 27 | }) 28 | 29 | test('isSanityAssetUrl(): returns true for file urls', () => { 30 | expect( 31 | isSanityAssetUrl( 32 | 'https://cdn.sanity.io/files/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870.pdf', 33 | ), 34 | ).toBe(true) 35 | }) 36 | 37 | test('isSanityAssetUrl(): returns true for file urls (staging)', () => { 38 | expect( 39 | isSanityAssetUrl( 40 | 'https://cdn.sanity.staging/files/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870.pdf', 41 | ), 42 | ).toBe(true) 43 | }) 44 | 45 | test('isSanityAssetUrl(): returns false for invalid urls', () => { 46 | expect(isSanityAssetUrl('https://cdn.not.sanity/rottifnatti/lol.jpg')).toBe(false) 47 | }) 48 | 49 | // isSanityFileUrl 50 | test('isSanityFileUrl(): returns false for image urls', () => { 51 | expect( 52 | isSanityFileUrl( 53 | 'https://cdn.sanity.io/images/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840.jpg', 54 | ), 55 | ).toBe(false) 56 | }) 57 | 58 | test('isSanityFileUrl(): returns false for image urls (staging)', () => { 59 | expect( 60 | isSanityFileUrl( 61 | 'https://cdn.sanity.staging/images/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840.jpg', 62 | ), 63 | ).toBe(false) 64 | }) 65 | 66 | test('isSanityFileUrl(): returns true for file urls', () => { 67 | expect( 68 | isSanityFileUrl( 69 | 'https://cdn.sanity.io/files/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870.pdf', 70 | ), 71 | ).toBe(true) 72 | }) 73 | 74 | test('isSanityFileUrl(): returns true for file urls (staging)', () => { 75 | expect( 76 | isSanityFileUrl( 77 | 'https://cdn.sanity.staging/files/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870.pdf', 78 | ), 79 | ).toBe(true) 80 | }) 81 | 82 | test('isSanityFileUrl(): returns false for invalid urls', () => { 83 | expect(isSanityFileUrl('https://cdn.not.sanity/rottifnatti/lol.jpg')).toBe(false) 84 | }) 85 | 86 | // isSanityImageUrl 87 | test('isSanityImageUrl(): returns true for image urls', () => { 88 | expect( 89 | isSanityImageUrl( 90 | 'https://cdn.sanity.io/images/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840.jpg', 91 | ), 92 | ).toBe(true) 93 | }) 94 | 95 | test('isSanityImageUrl(): returns true for image urls (staging)', () => { 96 | expect( 97 | isSanityImageUrl( 98 | 'https://cdn.sanity.staging/images/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840.jpg', 99 | ), 100 | ).toBe(true) 101 | }) 102 | 103 | test('isSanityImageUrl(): returns false for file urls', () => { 104 | expect( 105 | isSanityImageUrl( 106 | 'https://cdn.sanity.io/files/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870.pdf', 107 | ), 108 | ).toBe(false) 109 | }) 110 | 111 | test('isSanityImageUrl(): returns false for file urls (staging)', () => { 112 | expect( 113 | isSanityImageUrl( 114 | 'https://cdn.sanity.staging/files/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870.pdf', 115 | ), 116 | ).toBe(false) 117 | }) 118 | 119 | test('isSanityImageUrl(): returns false for invalid urls', () => { 120 | expect(isSanityImageUrl('https://cdn.not.sanity/rottifnatti/lol.jpg')).toBe(false) 121 | }) 122 | 123 | // isImageAssetId() 124 | test('isImageAssetId(): returns true for image ids', () => { 125 | expect(isImageAssetId('image-756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840-jpg')).toBe(true) 126 | }) 127 | 128 | test('isImageAssetId(): returns false for file urls', () => { 129 | expect(isImageAssetId('file-756e4bd9c0a04ada3d3cc396cf81f1c433b07870-pdf')).toBe(false) 130 | }) 131 | 132 | test('isImageAssetId(): returns false for invalid ids', () => { 133 | expect(isImageAssetId('not-an-id')).toBe(false) 134 | }) 135 | 136 | // isFileAssetId() 137 | test('isFileAssetId(): returns true for file ids', () => { 138 | expect(isFileAssetId('file-756e4bd9c0a04ada3d3cc396cf81f1c433b07870-pdf')).toBe(true) 139 | }) 140 | 141 | test('isFileAssetId(): returns false for image ids', () => { 142 | expect(isFileAssetId('image-756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840-jpg')).toBe(false) 143 | }) 144 | 145 | test('isFileAssetId(): returns false for invalid ids', () => { 146 | expect(isFileAssetId('not-your-pdf')).toBe(false) 147 | }) 148 | 149 | // isAssetId() 150 | test('isAssetId(): returns true for image ids', () => { 151 | expect(isAssetId('image-756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840-jpg')).toBe(true) 152 | }) 153 | 154 | test('isAssetId(): returns true for file ids', () => { 155 | expect(isAssetId('file-756e4bd9c0a04ada3d3cc396cf81f1c433b07870-pdf')).toBe(true) 156 | }) 157 | 158 | test('isAssetId(): returns false for invalid ids', () => { 159 | expect(isAssetId('not_your_image')).toBe(false) 160 | expect(isAssetId('image-but-invalid')).toBe(false) 161 | expect(isAssetId('file-but-invalid')).toBe(false) 162 | }) 163 | -------------------------------------------------------------------------------- /test/fixtures.ts: -------------------------------------------------------------------------------- 1 | import {DEFAULT_CROP, DEFAULT_HOTSPOT} from '../src/hotspotCrop.js' 2 | 3 | export const projectId = 'abc123' 4 | export const dataset = 'blog' 5 | export const id = 'image-734d6889e614ff0c6788105c88cfe071aa3146e5-4240x2832-jpg' 6 | export const lqip = 7 | // eslint-disable-next-line max-len 8 | 'data:image/jpeg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAANABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAABwAFCP/EACoQAAEDAwEFCQEAAAAAAAAAAAECAwQABQYRBxIhMUEIExUjJDJhcXKh/8QAFAEBAAAAAAAAAAAAAAAAAAAAAv/EABoRAAICAwAAAAAAAAAAAAAAAAABAhIRIjH/2gAMAwEAAhEDEQA/ANvHsTzJNmkpvs5ttp1B7wOH2j7oDXfRYsrc9c7LituKQrdVzT8V1VtMwNy5W65y3MguSBzQylXloB6adaO8c7P9jnxG5c65SnCRvFCUBIP9oVS4OzYfq2i2XXhGlEfoVUzt7IcRaQEeHhW7w1UTqaqGIi2P/9k=' 9 | export const ref = {_ref: id} 10 | export const asset = {_id: id} 11 | export const image = {asset: ref} 12 | export const path = 'images/abc123/blog/734d6889e614ff0c6788105c88cfe071aa3146e5-4240x2832.jpg' 13 | export const url = 14 | 'https://cdn.sanity.io/images/abc123/blog/734d6889e614ff0c6788105c88cfe071aa3146e5-4240x2832.jpg' 15 | 16 | export const urlWithQuery = 17 | 'https://cdn.sanity.io/images/abc123/blog/734d6889e614ff0c6788105c88cfe071aa3146e5-4240x2832.jpg?w=320&h=240' 18 | 19 | export const urlWithCustomFilename = 20 | 'https://cdn.sanity.io/images/abc123/blog/734d6889e614ff0c6788105c88cfe071aa3146e5-4240x2832.jpg/kokos.jpg' 21 | 22 | export const urlWithCustomFilenameAndQuery = 23 | 'https://cdn.sanity.io/images/abc123/blog/734d6889e614ff0c6788105c88cfe071aa3146e5-4240x2832.jpg/kokos.jpg?w=320&h=240' 24 | 25 | export const customBaseUrl = 'https://cdn.mycompany.com' 26 | 27 | export const crop = { 28 | _type: 'sanity.imageCrop', 29 | bottom: 0.03748125937031466, 30 | left: 0.2, 31 | right: 0, 32 | top: 0, 33 | } 34 | 35 | export const hotspot = { 36 | _type: 'sanity.imageHotspot', 37 | height: 0.6042441207142097, 38 | width: 0.4084778420038537, 39 | x: 0.5722543352601153, 40 | y: 0.3194544346323949, 41 | } 42 | 43 | export const cropHotspotImage = { 44 | asset: ref, 45 | crop, 46 | hotspot, 47 | } 48 | 49 | export const resolvedCropHotspotImage = { 50 | asset, 51 | crop, 52 | hotspot, 53 | } 54 | 55 | export const palette = { 56 | _type: 'sanity.imagePalette', 57 | darkMuted: { 58 | _type: 'sanity.imagePaletteSwatch', 59 | background: '#4b433e', 60 | foreground: '#fff', 61 | population: 3.36, 62 | title: '#fff', 63 | }, 64 | darkVibrant: { 65 | _type: 'sanity.imagePaletteSwatch', 66 | background: '#0c0c04', 67 | foreground: '#fff', 68 | population: 2.03, 69 | title: '#fff', 70 | }, 71 | dominant: { 72 | _type: 'sanity.imagePaletteSwatch', 73 | background: '#7f7467', 74 | foreground: '#fff', 75 | population: 8.88, 76 | title: '#fff', 77 | }, 78 | lightMuted: { 79 | _type: 'sanity.imagePaletteSwatch', 80 | background: '#d0c5b9', 81 | foreground: '#000', 82 | population: 6.41, 83 | title: '#fff', 84 | }, 85 | lightVibrant: { 86 | _type: 'sanity.imagePaletteSwatch', 87 | background: '#f3f4fc', 88 | foreground: '#000', 89 | population: 2.6, 90 | title: '#000', 91 | }, 92 | vibrant: { 93 | _type: 'sanity.imagePaletteSwatch', 94 | background: '#caa46b', 95 | foreground: '#000', 96 | population: 0.09, 97 | title: '#fff', 98 | }, 99 | } 100 | 101 | export const assetWithMeta = { 102 | ...asset, 103 | metadata: {dimensions: {width: 4240, height: 2832, aspectRatio: 4240 / 2832}, lqip, palette}, 104 | } 105 | 106 | export const resolvedCropHotspotImageWithMeta = { 107 | asset: assetWithMeta, 108 | crop, 109 | hotspot, 110 | } 111 | 112 | export const resolvedImageWithMeta = { 113 | asset: assetWithMeta, 114 | } 115 | 116 | export const resolvedNoopCropImageWithMeta = { 117 | asset: assetWithMeta, 118 | crop: { 119 | bottom: 0, 120 | left: 0, 121 | right: 0, 122 | top: 0, 123 | }, 124 | } 125 | 126 | export const portraitImage = { 127 | asset: {_id: 'image-734d6889e614ff0c6788105c88cfe071aa3146e5-2832x4240-jpg'}, 128 | } 129 | 130 | export const portraitCropHotspotImage = { 131 | asset: { 132 | _id: 'image-734d6889e614ff0c6788105c88cfe071aa3146e5-2832x4240-jpg', 133 | path, 134 | }, 135 | crop: { 136 | _type: 'sanity.imageCrop', 137 | bottom: 0, 138 | left: 0.03748125937031466, 139 | right: 0, 140 | top: 0.2, 141 | }, 142 | hotspot: { 143 | _type: 'sanity.imageHotspot', 144 | height: 0.4084778420038537, 145 | width: 0.6042441207142097, 146 | y: 0.5722543352601153, 147 | x: 0.3194544346323949, 148 | }, 149 | } 150 | 151 | export const expectedAsset = { 152 | _id: 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png', 153 | _type: 'sanity.imageAsset', 154 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 155 | extension: 'png', 156 | path: 'images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png', 157 | url: 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png', 158 | metadata: { 159 | dimensions: { 160 | width: 320, 161 | height: 240, 162 | aspectRatio: 320 / 240, 163 | }, 164 | }, 165 | } 166 | 167 | export const expectedAssetWithCustomBaseUrl = { 168 | ...expectedAsset, 169 | url: `${customBaseUrl}/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png`, 170 | } 171 | 172 | export const expectedImage = { 173 | asset: expectedAsset, 174 | crop: DEFAULT_CROP, 175 | hotspot: DEFAULT_HOTSPOT, 176 | } 177 | 178 | export const expectedImageWithCustomBaseUrl = { 179 | asset: expectedAssetWithCustomBaseUrl, 180 | crop: DEFAULT_CROP, 181 | hotspot: DEFAULT_HOTSPOT, 182 | } 183 | 184 | export const customHotspot = { 185 | _type: 'sanity.imageHotspot', 186 | height: 0.4084778420038537, 187 | width: 0.6042441207142097, 188 | y: 0.5722543352601153, 189 | x: 0.3194544346323949, 190 | } 191 | 192 | export const customCrop = { 193 | _type: 'sanity.imageCrop', 194 | bottom: 0, 195 | left: 0.03748125937031466, 196 | right: 0, 197 | top: 0.2, 198 | } 199 | 200 | export const testProject = { 201 | projectId: 'a', 202 | dataset: 'b', 203 | } 204 | 205 | export const testProjectWithCustomBaseUrl = { 206 | ...testProject, 207 | baseUrl: customBaseUrl, 208 | } 209 | -------------------------------------------------------------------------------- /test/hotspotCrop.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from 'vitest' 2 | 3 | import { 4 | DEFAULT_CROP, 5 | DEFAULT_HOTSPOT, 6 | getDefaultCrop, 7 | getDefaultHotspot, 8 | isDefaultCrop, 9 | isDefaultHotspot, 10 | } from '../src/hotspotCrop.js' 11 | 12 | // constants 13 | test('constant DEFAULT_CROP matches expected value', () => 14 | expect(DEFAULT_CROP).toEqual({left: 0, top: 0, bottom: 0, right: 0})) 15 | 16 | test('constant DEFAULT_HOTSPOT matches expected value', () => 17 | expect(DEFAULT_HOTSPOT).toEqual({ 18 | x: 0.5, 19 | y: 0.5, 20 | height: 1, 21 | width: 1, 22 | })) 23 | 24 | // functions 25 | test('getDefaultCrop() matches (but not strictly) DEFAULT_CROP', () => { 26 | expect(getDefaultCrop()).not.toBe(DEFAULT_CROP) 27 | expect(getDefaultCrop()).toMatchObject(DEFAULT_CROP) 28 | }) 29 | 30 | test('getDefaultHotspot() matches (but not strictly) DEFAULT_HOTSPOT', () => { 31 | expect(getDefaultHotspot()).not.toBe(DEFAULT_HOTSPOT) 32 | expect(getDefaultHotspot()).toMatchObject(DEFAULT_HOTSPOT) 33 | }) 34 | 35 | test('isDefaultCrop() determines value correctly', () => { 36 | expect(isDefaultCrop(DEFAULT_CROP)).toBe(true) 37 | expect(isDefaultCrop(getDefaultCrop())).toBe(true) 38 | expect(isDefaultCrop({bottom: 0.1, top: 0.2, right: 0.3, left: 0.4})).toBe(false) 39 | }) 40 | 41 | test('isDefaultHotspot() determines value correctly', () => { 42 | expect(isDefaultHotspot(DEFAULT_HOTSPOT)).toBe(true) 43 | expect(isDefaultHotspot(getDefaultHotspot())).toBe(true) 44 | expect(isDefaultHotspot({x: 0.1, y: 0.2, width: 0.5, height: 0.5})).toBe(false) 45 | }) 46 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from 'vitest' 2 | 3 | import * as exported from '../src/index.js' 4 | 5 | test('index: provides all exports', () => { 6 | expect(Object.keys(exported).sort()).toEqual( 7 | [ 8 | 'DEFAULT_CROP', 9 | 'DEFAULT_HOTSPOT', 10 | 'UnresolvableError', 11 | 'buildFilePath', 12 | 'buildFileUrl', 13 | 'buildImagePath', 14 | 'buildImageUrl', 15 | 'getAssetDocumentId', 16 | 'getAssetUrlType', 17 | 'getDefaultCrop', 18 | 'getDefaultHotspot', 19 | 'getExtension', 20 | 'getFile', 21 | 'getFileAsset', 22 | 'getIdFromString', 23 | 'getImage', 24 | 'getImageAsset', 25 | 'getImageDimensions', 26 | 'getProject', 27 | 'getUrlFilename', 28 | 'getUrlPath', 29 | 'getVanityStub', 30 | 'isAssetFilename', 31 | 'isAssetId', 32 | 'isAssetIdStub', 33 | 'isAssetObjectStub', 34 | 'isAssetPathStub', 35 | 'isAssetUrlStub', 36 | 'isDefaultCrop', 37 | 'isDefaultHotspot', 38 | 'isFileAssetFilename', 39 | 'isFileAssetId', 40 | 'isFileSource', 41 | 'isImageAssetFilename', 42 | 'isImageAssetId', 43 | 'isImageSource', 44 | 'isReference', 45 | 'isSanityAssetUrl', 46 | 'isSanityFileAsset', 47 | 'isSanityFileUrl', 48 | 'isSanityImageAsset', 49 | 'isSanityImageUrl', 50 | 'isUnresolvableError', 51 | 'isValidFilename', 52 | 'parseAssetFilename', 53 | 'parseAssetId', 54 | 'parseAssetUrl', 55 | 'parseFileAssetId', 56 | 'parseFileAssetUrl', 57 | 'parseImageAssetId', 58 | 'parseImageAssetUrl', 59 | 'tryGetAssetDocumentId', 60 | 'tryGetAssetPath', 61 | 'tryGetExtension', 62 | 'tryGetFile', 63 | 'tryGetFileAsset', 64 | 'tryGetIdFromString', 65 | 'tryGetImage', 66 | 'tryGetImageAsset', 67 | 'tryGetImageDimensions', 68 | 'tryGetProject', 69 | 'tryGetUrlFilename', 70 | 'tryGetUrlPath', 71 | ].sort(), 72 | ) 73 | }) 74 | -------------------------------------------------------------------------------- /test/parse.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from 'vitest' 2 | 3 | import { 4 | parseAssetFilename, 5 | parseAssetId, 6 | parseAssetUrl, 7 | parseFileAssetId, 8 | parseImageAssetId, 9 | } from '../src/parse.js' 10 | 11 | test('parseAssetId(): throws on invalid document id (generic getter)', () => { 12 | expect(() => parseAssetId('moop')).toThrowErrorMatchingInlineSnapshot( 13 | `[Error: Invalid image/file asset ID: moop]`, 14 | ) 15 | expect(() => parseAssetId('image-hash-300x-200-png')).toThrowErrorMatchingInlineSnapshot( 16 | `[Error: Invalid image/file asset ID: image-hash-300x-200-png]`, 17 | ) 18 | expect(() => parseAssetId('image-hash-fooxbar-png')).toThrowErrorMatchingInlineSnapshot( 19 | `[Error: Invalid image/file asset ID: image-hash-fooxbar-png]`, 20 | ) 21 | expect(() => parseAssetId('image-hash-20x-png')).toThrowErrorMatchingInlineSnapshot( 22 | `[Error: Invalid image/file asset ID: image-hash-20x-png]`, 23 | ) 24 | }) 25 | 26 | test('parseImageAssetId(): throws on invalid document id (image getter)', () => { 27 | expect(() => parseImageAssetId('moop')).toThrowErrorMatchingInlineSnapshot( 28 | `[Error: Malformed asset ID 'moop'. Expected an id like "image-027401f31c3ac1e6d78c5d539ccd1beff72b9b11-2000x3000-jpg".]`, 29 | ) 30 | expect(() => parseImageAssetId('image-hash-300x-200-png')).toThrowErrorMatchingInlineSnapshot( 31 | `[Error: Malformed asset ID 'image-hash-300x-200-png'. Expected an id like "image-027401f31c3ac1e6d78c5d539ccd1beff72b9b11-2000x3000-jpg".]`, 32 | ) 33 | expect(() => parseImageAssetId('image-hash-fooxbar-png')).toThrowErrorMatchingInlineSnapshot( 34 | `[Error: Malformed asset ID 'image-hash-fooxbar-png'. Expected an id like "image-027401f31c3ac1e6d78c5d539ccd1beff72b9b11-2000x3000-jpg".]`, 35 | ) 36 | expect(() => parseImageAssetId('image-hash-20x-png')).toThrowErrorMatchingInlineSnapshot( 37 | `[Error: Malformed asset ID 'image-hash-20x-png'. Expected an id like "image-027401f31c3ac1e6d78c5d539ccd1beff72b9b11-2000x3000-jpg".]`, 38 | ) 39 | }) 40 | 41 | test('parseFileAssetId(): throws on invalid document id (file getter)', () => { 42 | expect(() => parseFileAssetId('moop')).toThrowErrorMatchingInlineSnapshot( 43 | `[Error: Malformed file asset ID 'moop'. Expected an id like "file-027401f31c3ac1e6d78c5d539ccd1beff72b9b11-pdf"]`, 44 | ) 45 | expect(() => parseFileAssetId('file-hash-300x-200-png')).toThrowErrorMatchingInlineSnapshot( 46 | `[Error: Malformed file asset ID 'file-hash-300x-200-png'. Expected an id like "file-027401f31c3ac1e6d78c5d539ccd1beff72b9b11-pdf"]`, 47 | ) 48 | }) 49 | 50 | test('parseAssetFilename(): throws on invalid asset filenames', () => { 51 | expect(() => parseAssetFilename('blatti')).toThrowErrorMatchingInlineSnapshot( 52 | `[Error: Invalid image/file asset filename: blatti]`, 53 | ) 54 | }) 55 | 56 | test('parseAssetFilename(): returns object of named image properties if image filename', () => { 57 | expect(parseAssetFilename('eca53d85ec83704801ead6c8be368fd377f8aaef-200x500.png')) 58 | .toMatchInlineSnapshot(` 59 | { 60 | "assetId": "eca53d85ec83704801ead6c8be368fd377f8aaef", 61 | "extension": "png", 62 | "height": 500, 63 | "type": "image", 64 | "width": 200, 65 | } 66 | `) 67 | }) 68 | 69 | test('parseAssetFilename(): returns object of named image properties if legacy image filename', () => { 70 | expect(parseAssetFilename('LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n-600x578.png')) 71 | .toMatchInlineSnapshot(` 72 | { 73 | "assetId": "LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n", 74 | "extension": "png", 75 | "height": 578, 76 | "type": "image", 77 | "width": 600, 78 | } 79 | `) 80 | }) 81 | 82 | test('parseAssetFilename(): returns object of named image properties if file filename', () => { 83 | expect(parseAssetFilename('ae0ef9f916843d32fef3faffb9a675d4cce046f0.pdf')).toMatchInlineSnapshot(` 84 | { 85 | "assetId": "ae0ef9f916843d32fef3faffb9a675d4cce046f0", 86 | "extension": "pdf", 87 | "type": "file", 88 | } 89 | `) 90 | }) 91 | 92 | test('parseAssetFilename(): returns object of named image properties if file filename with numbers', () => { 93 | expect(parseAssetFilename('ae0ef9f916843d32fef3faffb9a675d4cce046f0.mp4')).toMatchInlineSnapshot(` 94 | { 95 | "assetId": "ae0ef9f916843d32fef3faffb9a675d4cce046f0", 96 | "extension": "mp4", 97 | "type": "file", 98 | } 99 | `) 100 | }) 101 | 102 | test('parseAssetFilename(): returns object of named image properties if legacy file filename', () => { 103 | expect(parseAssetFilename('LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n.pdf')).toMatchInlineSnapshot(` 104 | { 105 | "assetId": "LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n", 106 | "extension": "pdf", 107 | "type": "file", 108 | } 109 | `) 110 | }) 111 | 112 | test.each([ 113 | 'https://cdn.sanity.io/images/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840.jpg/vanity-filename.jpg', 114 | 'https://cdn.sanity.staging/images/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840.jpg/vanity-filename.jpg', 115 | 'https://cdn.autofoos.com/images/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840.jpg/vanity-filename.jpg', 116 | ])( 117 | 'parseAssetUrl(): returns object of named image properties on modern filename with vanity filename', 118 | (url) => { 119 | expect(parseAssetUrl(url)).toMatchObject({ 120 | assetId: '756e4bd9c0a04ada3d3cc396cf81f1c433b07870', 121 | dataset: 'diary', 122 | extension: 'jpg', 123 | height: 3840, 124 | projectId: 'espenhov', 125 | type: 'image', 126 | vanityFilename: 'vanity-filename.jpg', 127 | width: 5760, 128 | }) 129 | }, 130 | ) 131 | 132 | test.each([ 133 | 'https://cdn.sanity.io/images/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840.jpg', 134 | 'https://cdn.sanity.staging/images/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840.jpg', 135 | 'https://cdn.autofoos.com/images/espenhov/diary/756e4bd9c0a04ada3d3cc396cf81f1c433b07870-5760x3840.jpg', 136 | ])('parseAssetUrl(): returns object of named image properties on modern filename', (url) => { 137 | expect(parseAssetUrl(url)).toMatchObject({ 138 | assetId: '756e4bd9c0a04ada3d3cc396cf81f1c433b07870', 139 | dataset: 'diary', 140 | extension: 'jpg', 141 | height: 3840, 142 | projectId: 'espenhov', 143 | type: 'image', 144 | vanityFilename: undefined, 145 | width: 5760, 146 | }) 147 | }) 148 | 149 | test.each([ 150 | 'https://cdn.sanity.io/images/espenhov/diary/LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n-600x578.png', 151 | 'https://cdn.sanity.staging/images/espenhov/diary/LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n-600x578.png', 152 | 'https://cdn.autofoos.com/images/espenhov/diary/LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n-600x578.png', 153 | ])('parseAssetUrl(): returns object of named image properties on legacy filename', (url) => { 154 | expect(parseAssetUrl(url)).toMatchObject({ 155 | assetId: 'LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n', 156 | dataset: 'diary', 157 | extension: 'png', 158 | height: 578, 159 | projectId: 'espenhov', 160 | type: 'image', 161 | vanityFilename: undefined, 162 | width: 600, 163 | }) 164 | }) 165 | 166 | test.each([ 167 | 'https://cdn.sanity.io/files/espenhov/diary/ae0ef9f916843d32fef3faffb9a675d4cce046f0.pdf/oslo-guide.pdf', 168 | 'https://cdn.sanity.staging/files/espenhov/diary/ae0ef9f916843d32fef3faffb9a675d4cce046f0.pdf/oslo-guide.pdf', 169 | 'https://cdn.autofoos.com/files/espenhov/diary/ae0ef9f916843d32fef3faffb9a675d4cce046f0.pdf/oslo-guide.pdf', 170 | ])( 171 | 'parseAssetUrl(): returns object of named file properties on modern filename with vanity filename', 172 | (url) => { 173 | expect(parseAssetUrl(url)).toMatchObject({ 174 | assetId: 'ae0ef9f916843d32fef3faffb9a675d4cce046f0', 175 | dataset: 'diary', 176 | extension: 'pdf', 177 | projectId: 'espenhov', 178 | type: 'file', 179 | vanityFilename: 'oslo-guide.pdf', 180 | }) 181 | }, 182 | ) 183 | 184 | test.each([ 185 | 'https://cdn.sanity.io/files/espenhov/diary/ae0ef9f916843d32fef3faffb9a675d4cce046f0.pdf', 186 | 'https://cdn.sanity.staging/files/espenhov/diary/ae0ef9f916843d32fef3faffb9a675d4cce046f0.pdf', 187 | 'https://cdn.autofoos.com/files/espenhov/diary/ae0ef9f916843d32fef3faffb9a675d4cce046f0.pdf', 188 | ])('parseAssetUrl(): returns object of named file properties on modern filename', (url) => { 189 | expect(parseAssetUrl(url)).toMatchObject({ 190 | assetId: 'ae0ef9f916843d32fef3faffb9a675d4cce046f0', 191 | dataset: 'diary', 192 | extension: 'pdf', 193 | projectId: 'espenhov', 194 | type: 'file', 195 | vanityFilename: undefined, 196 | }) 197 | }) 198 | 199 | test.each([ 200 | 'https://cdn.sanity.io/files/espenhov/diary/LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n.pdf', 201 | 'https://cdn.sanity.staging/files/espenhov/diary/LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n.pdf', 202 | 'https://cdn.autofoos.com/files/espenhov/diary/LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n.pdf', 203 | ])('parseAssetUrl(): returns object of named file properties on legacy filename', (url) => { 204 | expect(parseAssetUrl(url)).toMatchObject({ 205 | assetId: 'LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n', 206 | dataset: 'diary', 207 | extension: 'pdf', 208 | projectId: 'espenhov', 209 | type: 'file', 210 | vanityFilename: undefined, 211 | }) 212 | }) 213 | 214 | test('parseAssetUrl(): throws on invalid URLs', () => { 215 | expect(() => parseAssetUrl('https://not.sanity.url')).toThrowErrorMatchingInlineSnapshot( 216 | `[Error: URL is not a valid Sanity asset URL: https://not.sanity.url]`, 217 | ) 218 | 219 | expect(() => 220 | parseAssetUrl('https://cdn.sanity.io/studios/foo.jpg'), 221 | ).toThrowErrorMatchingInlineSnapshot( 222 | `[Error: URL is not a valid Sanity asset URL: https://cdn.sanity.io/studios/foo.jpg]`, 223 | ) 224 | 225 | expect(() => 226 | parseAssetUrl( 227 | 'https://cdn.sanity.io/files/espenhov/e5p3n+lol/ae0ef9f916843d32fef3faffb9a675d4cce046f0.pdf', 228 | ), 229 | ).toThrowErrorMatchingInlineSnapshot( 230 | `[Error: URL is not a valid Sanity asset URL: https://cdn.sanity.io/files/espenhov/e5p3n+lol/ae0ef9f916843d32fef3faffb9a675d4cce046f0.pdf]`, 231 | ) 232 | 233 | expect(() => 234 | parseAssetUrl( 235 | 'https://cdn.sanity.io/files/espen#hov/diary/ae0ef9f916843d32fef3faffb9a675d4cce046f0.pdf', 236 | ), 237 | ).toThrowErrorMatchingInlineSnapshot( 238 | `[Error: URL is not a valid Sanity asset URL: https://cdn.sanity.io/files/espen#hov/diary/ae0ef9f916843d32fef3faffb9a675d4cce046f0.pdf]`, 239 | ) 240 | 241 | expect(() => 242 | parseAssetUrl( 243 | 'https://cdn.sanity.io/files/espenhov/diary/ae0e-f9f916843d-32fef3faffb9a-675d4cce046f0.pdf', 244 | ), 245 | ).toThrowErrorMatchingInlineSnapshot( 246 | `[Error: Invalid image/file asset filename: ae0e-f9f916843d-32fef3faffb9a-675d4cce046f0.pdf]`, 247 | ) 248 | }) -------------------------------------------------------------------------------- /test/resolve.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from 'vitest' 2 | 3 | import {parseFileAssetUrl, parseImageAssetUrl} from '../src/parse.js' 4 | import {buildFilePath, buildFileUrl, buildImagePath, buildImageUrl} from '../src/paths.js' 5 | import { 6 | getAssetDocumentId, 7 | getExtension, 8 | getIdFromString, 9 | getImage, 10 | getImageAsset, 11 | getImageDimensions, 12 | getProject, 13 | isAssetFilename, 14 | tryGetAssetDocumentId, 15 | tryGetExtension, 16 | tryGetIdFromString, 17 | tryGetImage, 18 | tryGetImageAsset, 19 | tryGetImageDimensions, 20 | } from '../src/resolve.js' 21 | import {SanityFileSource, SanityImageSource} from '../src/types.js' 22 | import { 23 | customCrop, 24 | customHotspot, 25 | expectedAsset, 26 | expectedImage, 27 | expectedImageWithCustomBaseUrl, 28 | testProject, 29 | testProjectWithCustomBaseUrl, 30 | } from './fixtures.js' 31 | 32 | const imgId = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 33 | const imgPath = 'images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 34 | const imgUrl = `https://cdn.sanity.io/${imgPath}` 35 | const imgPathPretty = `${imgPath}/pretty.png` 36 | const imgUrlPretty = `${imgUrl}/pretty.png` 37 | const stagingImgUrl = `https://cdn.sanity.staging/${imgPath}` 38 | const stagingImgUrlPretty = `${stagingImgUrl}/pretty.png` 39 | const customCdnImgUrl = `https://cdn.autofoos.com/${imgPath}` 40 | const customCdnImgUrlPretty = `${customCdnImgUrl}/pretty.png` 41 | const validImgSources: [string, SanityImageSource][] = [ 42 | ['asset id', imgId], 43 | ['reference', {_ref: imgId}], 44 | ['path stub', {path: imgPath}], 45 | ['url stub', {url: imgUrl}], 46 | ['staging url stub', {url: imgUrl}], 47 | ['object reference stub', {asset: {_ref: imgId}}], 48 | ['object id stub', {asset: {_id: imgId}}], 49 | ['object path stub', {asset: {path: imgPath}}], 50 | ['object url stub', {asset: {url: imgUrl}}], 51 | ['staging object url stub', {asset: {url: stagingImgUrl}}], 52 | ['custom cdn object url stub', {asset: {url: customCdnImgUrl}}], 53 | ['path stub (pretty filename)', {path: imgPathPretty}], 54 | ['url stub (pretty filename)', {url: imgUrlPretty}], 55 | ['staging url stub (pretty filename)', {url: stagingImgUrlPretty}], 56 | ['object path stub (pretty filename)', {asset: {path: imgPathPretty}}], 57 | ['object url stub (pretty filename)', {asset: {url: imgUrlPretty}}], 58 | ['staging object url stub (pretty filename)', {asset: {url: stagingImgUrlPretty}}], 59 | ['custom cdn object url stub (pretty filename)', {asset: {url: customCdnImgUrlPretty}}], 60 | ] 61 | 62 | const imgLegacyId = 'image-LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n-600x578-png' 63 | const imgLegacyPath = 'images/a/b/LA5zSofUOP0i_iQwi4B2dEbzHQseitcuORm4n-600x578.png' 64 | const imgLegacyUrl = `https://cdn.sanity.io/${imgLegacyPath}` 65 | const imgLegacyPathPretty = `${imgLegacyPath}/pretty.png` 66 | const imgLegacyUrlPretty = `${imgLegacyUrl}/pretty.png` 67 | const stagingImgLegacyUrl = `https://cdn.sanity.io/${imgLegacyPath}` 68 | const stagingImgLegacyUrlPretty = `${stagingImgLegacyUrl}/pretty.png` 69 | const customCdnImgLegacyUrl = `https://cdn.autofoos.com/${imgLegacyPath}` 70 | const customCdnImgLegacyUrlPretty = `${customCdnImgLegacyUrl}/pretty.png` 71 | const validLegacyImgSources: [string, SanityImageSource][] = [ 72 | ['asset id', imgLegacyId], 73 | ['reference', {_ref: imgLegacyId}], 74 | ['path stub', {path: imgLegacyPath}], 75 | ['url stub', {url: imgLegacyUrl}], 76 | ['staging url stub', {url: stagingImgLegacyUrl}], 77 | ['object reference stub', {asset: {_ref: imgLegacyId}}], 78 | ['object id stub', {asset: {_id: imgLegacyId}}], 79 | ['object path stub', {asset: {path: imgLegacyPath}}], 80 | ['object url stub', {asset: {url: imgLegacyUrl}}], 81 | ['staging object url stub', {asset: {url: stagingImgLegacyUrl}}], 82 | ['custom cdn object url stub', {asset: {url: customCdnImgLegacyUrl}}], 83 | ['path stub (pretty filename)', {path: imgLegacyPathPretty}], 84 | ['url stub (pretty filename)', {url: imgLegacyUrlPretty}], 85 | ['staging url stub (pretty filename)', {url: stagingImgLegacyUrlPretty}], 86 | ['object path stub (pretty filename)', {asset: {path: imgLegacyPathPretty}}], 87 | ['object url stub (pretty filename)', {asset: {url: imgLegacyUrlPretty}}], 88 | ['staging object url stub (pretty filename)', {asset: {url: stagingImgLegacyUrlPretty}}], 89 | ['custom cdn object url stub (pretty filename)', {asset: {url: customCdnImgLegacyUrlPretty}}], 90 | ] 91 | 92 | const fileId = 'file-def987abc12345678909877654321abcdef98765-pdf' 93 | const filePath = 'files/a/b/def987abc12345678909877654321abcdef98765.pdf' 94 | const fileUrl = `https://cdn.sanity.io/${filePath}` 95 | const filePathPretty = `${filePath}/pretty.pdf` 96 | const fileUrlPretty = `${fileUrl}/pretty.pdf` 97 | const stagingFileUrl = `https://cdn.sanity.staging/${filePath}` 98 | const stagingFileUrlPretty = `${stagingFileUrl}/pretty.pdf` 99 | const customCdnFileUrl = `https://cdn.autofoos.com/${filePath}` 100 | const customCdnFileUrlPretty = `${customCdnFileUrl}/pretty.pdf` 101 | const validFileSources: [string, SanityFileSource][] = [ 102 | ['asset id', fileId], 103 | ['reference', {_ref: fileId}], 104 | ['path stub', {path: filePath}], 105 | ['url stub', {url: fileUrl}], 106 | ['staging url stub', {url: stagingFileUrl}], 107 | ['object reference stub', {asset: {_ref: fileId}}], 108 | ['object id stub', {asset: {_id: fileId}}], 109 | ['object path stub', {asset: {path: filePath}}], 110 | ['object url stub', {asset: {url: fileUrl}}], 111 | ['staging object url stub', {asset: {url: stagingFileUrl}}], 112 | ['custom cdn object url stub', {asset: {url: customCdnFileUrl}}], 113 | ['path stub (pretty filename)', {path: filePathPretty}], 114 | ['url stub (pretty filename)', {url: fileUrlPretty}], 115 | ['staging url stub (pretty filename)', {url: stagingFileUrlPretty}], 116 | ['object path stub (pretty filename)', {asset: {path: filePathPretty}}], 117 | ['object url stub (pretty filename)', {asset: {url: fileUrlPretty}}], 118 | ['staging object url stub (pretty filename)', {asset: {url: stagingFileUrlPretty}}], 119 | ['custom cdn object url stub (pretty filename)', {asset: {url: customCdnFileUrlPretty}}], 120 | ] 121 | 122 | // buildImagePath() 123 | test('buildImagePath(): throws if no project id or dataset given', () => { 124 | expect(() => 125 | buildImagePath({ 126 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 127 | extension: 'png', 128 | metadata: {dimensions: {height: 300, width: 500}}, 129 | }), 130 | ).toThrowErrorMatchingInlineSnapshot( 131 | `[Error: Project details (projectId and dataset) required to resolve path for image]`, 132 | ) 133 | }) 134 | 135 | test('buildImagePath(): builds image paths correctly', () => { 136 | const path = buildImagePath( 137 | { 138 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 139 | extension: 'png', 140 | metadata: {dimensions: {height: 300, width: 500}}, 141 | }, 142 | {projectId: 'abc123', dataset: 'foo'}, 143 | ) 144 | 145 | expect(path).toEqual('images/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b-500x300.png') 146 | }) 147 | 148 | test('buildImagePath(): builds image path with vanity filename correctly', () => { 149 | const path = buildImagePath( 150 | { 151 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 152 | extension: 'png', 153 | metadata: {dimensions: {height: 300, width: 500}}, 154 | vanityFilename: 'so-pretty.png', 155 | }, 156 | {projectId: 'abc123', dataset: 'foo'}, 157 | ) 158 | 159 | expect(path).toEqual( 160 | 'images/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b-500x300.png/so-pretty.png', 161 | ) 162 | }) 163 | 164 | // buildImageUrl() 165 | test('buildImageUrl(): throws if no project id or dataset given', () => { 166 | expect(() => 167 | buildImageUrl({ 168 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 169 | extension: 'png', 170 | metadata: {dimensions: {height: 300, width: 500}}, 171 | }), 172 | ).toThrowErrorMatchingInlineSnapshot( 173 | `[Error: Project details (projectId and dataset) required to resolve path for image]`, 174 | ) 175 | }) 176 | 177 | test('buildImageUrl(): builds image urls correctly', () => { 178 | const url = buildImageUrl( 179 | { 180 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 181 | extension: 'png', 182 | metadata: {dimensions: {height: 300, width: 500}}, 183 | }, 184 | {projectId: 'abc123', dataset: 'foo'}, 185 | ) 186 | 187 | expect(url).toEqual( 188 | 'https://cdn.sanity.io/images/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b-500x300.png', 189 | ) 190 | }) 191 | 192 | test('buildImageUrl(): builds image urls correctly with project in asset', () => { 193 | const url = buildImageUrl({ 194 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 195 | extension: 'png', 196 | metadata: {dimensions: {height: 300, width: 500}}, 197 | projectId: 'abc123', 198 | dataset: 'foo', 199 | }) 200 | 201 | expect(url).toEqual( 202 | 'https://cdn.sanity.io/images/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b-500x300.png', 203 | ) 204 | }) 205 | 206 | test('buildImageUrl(): builds image urls correctly with parsed asset url', () => { 207 | const input = 208 | 'https://cdn.sanity.io/images/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b-500x300.png/vanity.png' 209 | const output = buildImageUrl(parseImageAssetUrl(input)) 210 | 211 | expect(input).toEqual(output) 212 | }) 213 | 214 | test('buildImageUrl(): builds image urls correctly', () => { 215 | const url = buildImageUrl( 216 | { 217 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 218 | extension: 'png', 219 | metadata: {dimensions: {height: 300, width: 500}}, 220 | originalFilename: 'pretty.png', 221 | vanityFilename: 'prettier.png', 222 | }, 223 | {projectId: 'abc123', dataset: 'foo'}, 224 | ) 225 | 226 | expect(url).toEqual( 227 | 'https://cdn.sanity.io/images/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b-500x300.png/prettier.png', 228 | ) 229 | }) 230 | 231 | test('buildImageUrl(): uses custom baseUrl if specified', () => { 232 | const input = 233 | 'https://cdn.sanity.io/images/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b-500x300.png/vanity.png' 234 | const output = buildImageUrl(parseImageAssetUrl(input), {baseUrl: 'https://cdn.mycompany.com'}) 235 | 236 | expect(output).toEqual(input.replace('https://cdn.sanity.io', 'https://cdn.mycompany.com')) 237 | }) 238 | 239 | // buildFilePath() 240 | test('buildFilePath(): throws if no project id or dataset given', () => { 241 | expect(() => 242 | buildFilePath({ 243 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 244 | extension: 'pdf', 245 | }), 246 | ).toThrowErrorMatchingInlineSnapshot( 247 | `[Error: Project details (projectId and dataset) required to resolve path for file]`, 248 | ) 249 | }) 250 | 251 | test('buildFilePath(): builds file paths correctly', () => { 252 | const path = buildFilePath( 253 | { 254 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 255 | extension: 'txt', 256 | }, 257 | {projectId: 'abc123', dataset: 'foo'}, 258 | ) 259 | 260 | expect(path).toEqual('files/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b.txt') 261 | }) 262 | 263 | test('buildFilePath(): builds file paths correctly with project in asset', () => { 264 | const path = buildFilePath({ 265 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 266 | extension: 'txt', 267 | projectId: 'abc123', 268 | dataset: 'foo', 269 | }) 270 | 271 | expect(path).toEqual('files/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b.txt') 272 | }) 273 | 274 | test('buildFilePath(): builds file path with vanity filename correctly', () => { 275 | const path = buildFilePath( 276 | { 277 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 278 | extension: 'mp4', 279 | vanityFilename: 'kokos-zoomies.mp4', 280 | }, 281 | {projectId: 'abc123', dataset: 'foo'}, 282 | ) 283 | 284 | expect(path).toEqual( 285 | 'files/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b.mp4/kokos-zoomies.mp4', 286 | ) 287 | }) 288 | 289 | // buildFileUrl() 290 | test('buildFileUrl(): throws if no project id or dataset given', () => { 291 | expect(() => 292 | buildFileUrl({ 293 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 294 | extension: 'mp4', 295 | }), 296 | ).toThrowErrorMatchingInlineSnapshot( 297 | `[Error: Project details (projectId and dataset) required to resolve path for file]`, 298 | ) 299 | }) 300 | 301 | test('buildFileUrl(): builds file urls correctly', () => { 302 | const url = buildFileUrl( 303 | { 304 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 305 | extension: 'mov', 306 | }, 307 | {projectId: 'abc123', dataset: 'foo'}, 308 | ) 309 | 310 | expect(url).toEqual( 311 | 'https://cdn.sanity.io/files/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b.mov', 312 | ) 313 | }) 314 | 315 | test('buildFileUrl(): builds file urls correctly with project in asset', () => { 316 | const url = buildFileUrl({ 317 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 318 | extension: 'mov', 319 | projectId: 'abc123', 320 | dataset: 'foo', 321 | }) 322 | 323 | expect(url).toEqual( 324 | 'https://cdn.sanity.io/files/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b.mov', 325 | ) 326 | }) 327 | 328 | test('buildFileUrl(): builds file urls correctly with parsed asset url', () => { 329 | const input = 330 | 'https://cdn.sanity.io/files/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b.mov/vanity.mov' 331 | const output = buildFileUrl(parseFileAssetUrl(input)) 332 | 333 | expect(input).toEqual(output) 334 | }) 335 | 336 | test('buildFileUrl(): uses custom baseUrl if specified', () => { 337 | const input = 338 | 'https://cdn.sanity.io/files/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b.mov/vanity.mov' 339 | const output = buildFileUrl(parseFileAssetUrl(input), {baseUrl: 'https://cdn.mycompany.com'}) 340 | 341 | expect(output).toEqual(input.replace('https://cdn.sanity.io', 'https://cdn.mycompany.com')) 342 | }) 343 | 344 | test('buildFileUrl(): builds file urls correctly', () => { 345 | const url = buildFileUrl( 346 | { 347 | assetId: 'f00baaf00baaf00baaf00baaf00baaf00baaf00b', 348 | extension: 'mp3', 349 | originalFilename: 'episode-1.mp3', 350 | vanityFilename: 's01e01-the-one-with-assets.mp3', 351 | }, 352 | {projectId: 'abc123', dataset: 'foo'}, 353 | ) 354 | 355 | expect(url).toEqual( 356 | 'https://cdn.sanity.io/files/abc123/foo/f00baaf00baaf00baaf00baaf00baaf00baaf00b.mp3/s01e01-the-one-with-assets.mp3', 357 | ) 358 | }) 359 | 360 | // getIdFromString() 361 | test('getIdFromString(): already an id', () => { 362 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 363 | expect(getIdFromString(id)).toBe(id) 364 | }) 365 | 366 | test('getIdFromString(): from URL', () => { 367 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 368 | const url = 369 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 370 | expect(getIdFromString(url)).toBe(id) 371 | }) 372 | 373 | test('getIdFromString(): from URL (staging)', () => { 374 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 375 | const url = 376 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 377 | expect(getIdFromString(url)).toBe(id) 378 | }) 379 | 380 | test('getIdFromString(): from URL with pretty filename', () => { 381 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 382 | const url = 383 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/pretty.png' 384 | expect(getIdFromString(url)).toBe(id) 385 | }) 386 | 387 | test('getIdFromString(): from URL with pretty filename (staging)', () => { 388 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 389 | const url = 390 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/pretty.png' 391 | expect(getIdFromString(url)).toBe(id) 392 | }) 393 | 394 | test('getIdFromString(): from URL with query', () => { 395 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 396 | const url = 397 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 398 | expect(getIdFromString(url)).toBe(id) 399 | }) 400 | 401 | test('getIdFromString(): from URL with query (staging)', () => { 402 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 403 | const url = 404 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 405 | expect(getIdFromString(url)).toBe(id) 406 | }) 407 | 408 | test('getIdFromString(): from URL with query + pretty filename', () => { 409 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 410 | const url = 411 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/pretty.png?w=120&h=120' 412 | expect(getIdFromString(url)).toBe(id) 413 | }) 414 | 415 | test('getIdFromString(): from URL with query + pretty filename (staging)', () => { 416 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 417 | const url = 418 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/pretty.png?w=120&h=120' 419 | expect(getIdFromString(url)).toBe(id) 420 | }) 421 | 422 | test('getIdFromString(): from path', () => { 423 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 424 | const path = 'images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 425 | expect(getIdFromString(path)).toBe(id) 426 | }) 427 | 428 | test('getIdFromString(): from filename', () => { 429 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 430 | const path = 'f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 431 | expect(getIdFromString(path)).toBe(id) 432 | }) 433 | 434 | test('getIdFromString(): no match', () => { 435 | expect(() => getIdFromString('who knows')).toThrow(/failed to resolve asset id/i) 436 | }) 437 | 438 | test('tryGetIdFromString(): no match', () => { 439 | expect(tryGetIdFromString('who knows')).toBe(undefined) 440 | }) 441 | 442 | test('tryGetIdFromString(): match', () => { 443 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 444 | const path = 'f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 445 | expect(tryGetIdFromString(path)).toBe(id) 446 | }) 447 | 448 | // getAssetDocumentId() 449 | test('getAssetDocumentId(): already an id', () => { 450 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 451 | expect(getAssetDocumentId(id)).toBe(id) 452 | }) 453 | 454 | test('getAssetDocumentId(): from URL', () => { 455 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 456 | const url = 457 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 458 | expect(getAssetDocumentId(url)).toBe(id) 459 | }) 460 | 461 | test('getAssetDocumentId(): from URL (staging)', () => { 462 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 463 | const url = 464 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 465 | expect(getAssetDocumentId(url)).toBe(id) 466 | }) 467 | 468 | test('getAssetDocumentId(): from URL (custom cdn)', () => { 469 | const id = 'image-9d884e501c64d640a321ca5700d32ac4250064ca-600x400-jpg' 470 | const url = 471 | 'https://cdn.content.mydomain.com/images/foo/bar/9d884e501c64d640a321ca5700d32ac4250064ca-600x400.jpg' 472 | expect(getAssetDocumentId(url)).toBe(id) 473 | }) 474 | 475 | test('getAssetDocumentId(): from URL with pretty filename', () => { 476 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 477 | const url = 478 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/pretty.png' 479 | expect(getAssetDocumentId(url)).toBe(id) 480 | }) 481 | 482 | test('getAssetDocumentId(): from URL with pretty filename (staging)', () => { 483 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 484 | const url = 485 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/pretty.png' 486 | expect(getAssetDocumentId(url)).toBe(id) 487 | }) 488 | 489 | test('getAssetDocumentId(): from URL with query', () => { 490 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 491 | const url = 492 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 493 | expect(getAssetDocumentId(url)).toBe(id) 494 | }) 495 | 496 | test('getAssetDocumentId(): from URL with query (staging)', () => { 497 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 498 | const url = 499 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 500 | expect(getAssetDocumentId(url)).toBe(id) 501 | }) 502 | 503 | test('getAssetDocumentId(): from URL with query and pretty filename', () => { 504 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 505 | const url = 506 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/pretty.png?w=120&h=120' 507 | expect(getAssetDocumentId(url)).toBe(id) 508 | }) 509 | 510 | test('getAssetDocumentId(): from URL with query and pretty filename (staging)', () => { 511 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 512 | const url = 513 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/pretty.png?w=120&h=120' 514 | expect(getAssetDocumentId(url)).toBe(id) 515 | }) 516 | 517 | test('getAssetDocumentId(): from path', () => { 518 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 519 | const path = 'images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 520 | expect(getAssetDocumentId(path)).toBe(id) 521 | }) 522 | 523 | test('getAssetDocumentId(): from image filename', () => { 524 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 525 | const path = 'f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 526 | expect(getAssetDocumentId(path)).toBe(id) 527 | }) 528 | 529 | test('getAssetDocumentId(): from filename', () => { 530 | const id = 'file-f00baaf00baaf00baaf00baaf00baaf00baaf00b-pdf' 531 | const path = 'f00baaf00baaf00baaf00baaf00baaf00baaf00b.pdf' 532 | expect(getAssetDocumentId(path)).toBe(id) 533 | }) 534 | 535 | test('getAssetDocumentId(): from reference', () => { 536 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 537 | expect(getAssetDocumentId({_ref: id})).toBe(id) 538 | }) 539 | 540 | test('getAssetDocumentId(): from asset stub (id)', () => { 541 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 542 | expect(getAssetDocumentId({_id: id})).toBe(id) 543 | }) 544 | 545 | test('getAssetDocumentId(): from asset stub (path)', () => { 546 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 547 | const path = 'images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 548 | expect(getAssetDocumentId({path})).toBe(id) 549 | }) 550 | 551 | test('getAssetDocumentId(): from asset stub (url)', () => { 552 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 553 | const url = 554 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 555 | expect(getAssetDocumentId({url})).toBe(id) 556 | }) 557 | 558 | test('getAssetDocumentId(): from asset stub (url, stagin)', () => { 559 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 560 | const url = 561 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 562 | expect(getAssetDocumentId({url})).toBe(id) 563 | }) 564 | 565 | test('getAssetDocumentId(): from asset stub (url with query)', () => { 566 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 567 | const url = 568 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 569 | expect(getAssetDocumentId({url})).toBe(id) 570 | }) 571 | 572 | test('getAssetDocumentId(): from asset stub (url with query, staging)', () => { 573 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 574 | const url = 575 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 576 | expect(getAssetDocumentId({url})).toBe(id) 577 | }) 578 | 579 | test('getAssetDocumentId(): from asset stub (url + pretty filename)', () => { 580 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 581 | const url = 582 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png' 583 | expect(getAssetDocumentId({url})).toBe(id) 584 | }) 585 | 586 | test('getAssetDocumentId(): from asset stub (url + pretty filename, staging)', () => { 587 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 588 | const url = 589 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png' 590 | expect(getAssetDocumentId({url})).toBe(id) 591 | }) 592 | 593 | test('getAssetDocumentId(): from asset stub (url with query + pretty filename)', () => { 594 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 595 | const url = 596 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png?w=120&h=120' 597 | expect(getAssetDocumentId({url})).toBe(id) 598 | }) 599 | 600 | test('getAssetDocumentId(): from asset stub (url with query + pretty filename, staging)', () => { 601 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 602 | const url = 603 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png?w=120&h=120' 604 | expect(getAssetDocumentId({url})).toBe(id) 605 | }) 606 | 607 | test('getAssetDocumentId(): from deep reference', () => { 608 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 609 | expect(getAssetDocumentId({asset: {_ref: id}})).toBe(id) 610 | }) 611 | 612 | test('getAssetDocumentId(): from deep asset stub (id)', () => { 613 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 614 | expect(getAssetDocumentId({asset: {_id: id}})).toBe(id) 615 | }) 616 | 617 | test('getAssetDocumentId(): from deep asset stub (path)', () => { 618 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 619 | const path = 'images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 620 | expect(getAssetDocumentId({asset: {path}})).toBe(id) 621 | }) 622 | 623 | test('getAssetDocumentId(): from deep asset stub (url)', () => { 624 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 625 | const url = 626 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 627 | expect(getAssetDocumentId({asset: {url}})).toBe(id) 628 | }) 629 | 630 | test('getAssetDocumentId(): from deep asset stub (url, staging)', () => { 631 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 632 | const url = 633 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 634 | expect(getAssetDocumentId({asset: {url}})).toBe(id) 635 | }) 636 | 637 | test('getAssetDocumentId(): from deep asset stub (url with query)', () => { 638 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 639 | const url = 640 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 641 | expect(getAssetDocumentId({asset: {url}})).toBe(id) 642 | }) 643 | 644 | test('getAssetDocumentId(): from deep asset stub (url with query, staging)', () => { 645 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 646 | const url = 647 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 648 | expect(getAssetDocumentId({asset: {url}})).toBe(id) 649 | }) 650 | 651 | test('getAssetDocumentId(): from deep asset stub (url with query + prettier filename)', () => { 652 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 653 | const url = 654 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png?w=200' 655 | expect(getAssetDocumentId({asset: {url}})).toBe(id) 656 | }) 657 | 658 | test('getAssetDocumentId(): from deep asset stub (url with query + prettier filename, staging)', () => { 659 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 660 | const url = 661 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png?w=200' 662 | expect(getAssetDocumentId({asset: {url}})).toBe(id) 663 | }) 664 | 665 | test('getAssetDocumentId(): no match', () => { 666 | expect(() => getAssetDocumentId('who knows')).toThrow(/failed to resolve asset id/i) 667 | }) 668 | 669 | test('tryGetAssetDocumentId(): no match', () => { 670 | expect(tryGetAssetDocumentId('who knows')).toBe(undefined) 671 | }) 672 | 673 | test('tryGetAssetDocumentId(): match', () => { 674 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 675 | const url = 676 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 677 | expect(tryGetAssetDocumentId({asset: {url}})).toBe(id) 678 | }) 679 | 680 | test('tryGetAssetDocumentId(): match, staging', () => { 681 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 682 | const url = 683 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 684 | expect(tryGetAssetDocumentId({asset: {url}})).toBe(id) 685 | }) 686 | 687 | // getImageAsset() 688 | test('getImageAsset(): from ID', () => { 689 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 690 | expect(getImageAsset(id, testProject)).toEqual(expectedAsset) 691 | }) 692 | 693 | test('getImageAsset(): from URL', () => { 694 | const url = 695 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 696 | expect(getImageAsset(url, testProject)).toEqual(expectedAsset) 697 | }) 698 | 699 | test('getImageAsset(): from URL (staging)', () => { 700 | const url = 701 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 702 | expect(getImageAsset(url, testProject)).toEqual(expectedAsset) 703 | }) 704 | 705 | test('getImageAsset(): from URL with query', () => { 706 | const url = 707 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 708 | expect(getImageAsset(url, testProject)).toEqual(expectedAsset) 709 | }) 710 | 711 | test('getImageAsset(): from URL with query (staging)', () => { 712 | const url = 713 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 714 | expect(getImageAsset(url, testProject)).toEqual(expectedAsset) 715 | }) 716 | 717 | test('getImageAsset(): from URL, inferred project', () => { 718 | const url = 719 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 720 | expect(getImageAsset(url)).toEqual(expectedAsset) 721 | }) 722 | 723 | test('getImageAsset(): from URL, inferred project (staging)', () => { 724 | const url = 725 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 726 | expect(getImageAsset(url)).toEqual(expectedAsset) 727 | }) 728 | 729 | test('getImageAsset(): from URL with query, inferred project', () => { 730 | const url = 731 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 732 | expect(getImageAsset(url)).toEqual(expectedAsset) 733 | }) 734 | 735 | test('getImageAsset(): from URL with query, inferred project (staging)', () => { 736 | const url = 737 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 738 | expect(getImageAsset(url)).toEqual(expectedAsset) 739 | }) 740 | 741 | test('getImageAsset(): from URL with pretty filename', () => { 742 | const url = 743 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png' 744 | expect(getImageAsset(url, testProject)).toEqual(expectedAsset) 745 | }) 746 | 747 | test('getImageAsset(): from URL with pretty filename (staging)', () => { 748 | const url = 749 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png' 750 | expect(getImageAsset(url, testProject)).toEqual(expectedAsset) 751 | }) 752 | 753 | test('getImageAsset(): from URL with query + prettier filename', () => { 754 | const url = 755 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/foo.png?w=120&h=120' 756 | expect(getImageAsset(url, testProject)).toEqual(expectedAsset) 757 | }) 758 | 759 | test('getImageAsset(): from URL with query + prettier filename (staging)', () => { 760 | const url = 761 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/foo.png?w=120&h=120' 762 | expect(getImageAsset(url, testProject)).toEqual(expectedAsset) 763 | }) 764 | 765 | test('getImageAsset(): from URL, inferred project + prettier filename', () => { 766 | const url = 767 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png' 768 | expect(getImageAsset(url)).toEqual(expectedAsset) 769 | }) 770 | 771 | test('getImageAsset(): from URL, inferred project + prettier filename (staging)', () => { 772 | const url = 773 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png' 774 | expect(getImageAsset(url)).toEqual(expectedAsset) 775 | }) 776 | 777 | test('getImageAsset(): from URL with query + prettier filename, inferred project', () => { 778 | const url = 779 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png?w=120&h=120' 780 | expect(getImageAsset(url)).toEqual(expectedAsset) 781 | }) 782 | 783 | test('getImageAsset(): from URL with query + prettier filename, inferred project (staging)', () => { 784 | const url = 785 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png?w=120&h=120' 786 | expect(getImageAsset(url)).toEqual(expectedAsset) 787 | }) 788 | 789 | test('getImageAsset(): from path', () => { 790 | const path = 'images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 791 | expect(getImageAsset(path, testProject)).toEqual(expectedAsset) 792 | }) 793 | 794 | test('getImageAsset(): from path, inferred project', () => { 795 | const path = 'images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 796 | expect(getImageAsset(path)).toEqual(expectedAsset) 797 | }) 798 | 799 | test('getImageAsset(): from filename', () => { 800 | const path = 'f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 801 | expect(getImageAsset(path, testProject)).toEqual(expectedAsset) 802 | }) 803 | 804 | test('getImageAsset(): from reference', () => { 805 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 806 | expect(getImageAsset({_ref: id}, testProject)).toEqual(expectedAsset) 807 | }) 808 | 809 | test('getImageAsset(): from asset stub (id)', () => { 810 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 811 | expect(getImageAsset({_id: id}, testProject)).toEqual(expectedAsset) 812 | }) 813 | 814 | test('getImageAsset(): from asset stub (path)', () => { 815 | const path = 'images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 816 | expect(getImageAsset({path}, testProject)).toEqual(expectedAsset) 817 | }) 818 | 819 | test('getImageAsset(): from asset stub (path), inferred project', () => { 820 | const path = 'images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 821 | expect(getImageAsset({path})).toEqual(expectedAsset) 822 | }) 823 | 824 | test('getImageAsset(): from asset stub (url)', () => { 825 | const url = 826 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 827 | expect(getImageAsset({url}, testProject)).toEqual(expectedAsset) 828 | }) 829 | 830 | test('getImageAsset(): from asset stub (url, staging)', () => { 831 | const url = 832 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 833 | expect(getImageAsset({url}, testProject)).toEqual(expectedAsset) 834 | }) 835 | 836 | test('getImageAsset(): from asset stub (url), inferred project', () => { 837 | const url = 838 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 839 | expect(getImageAsset({url})).toEqual(expectedAsset) 840 | }) 841 | 842 | test('getImageAsset(): from asset stub (url), inferred project, staging', () => { 843 | const url = 844 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 845 | expect(getImageAsset({url})).toEqual(expectedAsset) 846 | }) 847 | 848 | test('getImageAsset(): from asset stub (url with query)', () => { 849 | const url = 850 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 851 | expect(getImageAsset({url}, testProject)).toEqual(expectedAsset) 852 | }) 853 | 854 | test('getImageAsset(): from asset stub (url with query), staging', () => { 855 | const url = 856 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 857 | expect(getImageAsset({url}, testProject)).toEqual(expectedAsset) 858 | }) 859 | 860 | test('getImageAsset(): from asset stub (url with query), inferred project', () => { 861 | const url = 862 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 863 | expect(getImageAsset({url})).toEqual(expectedAsset) 864 | }) 865 | 866 | test('getImageAsset(): from asset stub (url with query), inferred project, staging', () => { 867 | const url = 868 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 869 | expect(getImageAsset({url})).toEqual(expectedAsset) 870 | }) 871 | 872 | test('getImageAsset(): from full asset document', () => { 873 | const inferFrom = {...expectedAsset, originalFilename: 'mootools.png'} 874 | expect(getImageAsset(inferFrom)).toEqual(inferFrom) 875 | }) 876 | 877 | test('getImageAsset(): from deep reference', () => { 878 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 879 | expect(getImageAsset({asset: {_ref: id}}, testProject)).toEqual(expectedAsset) 880 | }) 881 | 882 | test('getImageAsset(): from deep asset stub (id)', () => { 883 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 884 | expect(getImageAsset({asset: {_id: id}}, testProject)).toEqual(expectedAsset) 885 | }) 886 | 887 | test('getImageAsset(): from deep asset stub (path)', () => { 888 | const path = 'images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 889 | expect(getImageAsset({asset: {path}}, testProject)).toEqual(expectedAsset) 890 | }) 891 | 892 | test('getImageAsset(): from deep asset stub (path), inferred project', () => { 893 | const path = 'images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 894 | expect(getImageAsset({asset: {path}})).toEqual(expectedAsset) 895 | }) 896 | 897 | test('getImageAsset(): from deep asset stub (url)', () => { 898 | const url = 899 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 900 | expect(getImageAsset({asset: {url}}, testProject)).toEqual(expectedAsset) 901 | }) 902 | 903 | test('getImageAsset(): from deep asset stub (url, staging)', () => { 904 | const url = 905 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 906 | expect(getImageAsset({asset: {url}}, testProject)).toEqual(expectedAsset) 907 | }) 908 | 909 | test('getImageAsset(): from deep asset stub (url), inferred project', () => { 910 | const url = 911 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 912 | expect(getImageAsset({asset: {url}})).toEqual(expectedAsset) 913 | }) 914 | 915 | test('getImageAsset(): from deep asset stub (url), inferred project (staging)', () => { 916 | const url = 917 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 918 | expect(getImageAsset({asset: {url}})).toEqual(expectedAsset) 919 | }) 920 | 921 | test('getImageAsset(): from deep asset stub (url with query)', () => { 922 | const url = 923 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 924 | expect(getImageAsset({asset: {url}}, testProject)).toEqual(expectedAsset) 925 | }) 926 | 927 | test('getImageAsset(): from deep asset stub (url with query, staging)', () => { 928 | const url = 929 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 930 | expect(getImageAsset({asset: {url}}, testProject)).toEqual(expectedAsset) 931 | }) 932 | 933 | test('getImageAsset(): from deep asset stub (url with query), inferred project', () => { 934 | const url = 935 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 936 | expect(getImageAsset({asset: {url}})).toEqual(expectedAsset) 937 | }) 938 | 939 | test('getImageAsset(): from deep asset stub (url with query), inferred project, staging', () => { 940 | const url = 941 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 942 | expect(getImageAsset({asset: {url}})).toEqual(expectedAsset) 943 | }) 944 | 945 | test('getImageAsset(): no match', () => { 946 | expect(() => getImageAsset('ey hold up')).toThrow(/failed to resolve/i) 947 | }) 948 | 949 | test('tryGetImageAsset(): no match', () => { 950 | expect(tryGetImageAsset('ey hold up')).toBe(undefined) 951 | }) 952 | 953 | test('tryGetImageAsset(): match', () => { 954 | const url = 955 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 956 | expect(tryGetImageAsset({asset: {url}})).toEqual(expectedAsset) 957 | }) 958 | 959 | test('tryGetImageAsset(): match, staging', () => { 960 | const url = 961 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 962 | expect(tryGetImageAsset({asset: {url}})).toEqual(expectedAsset) 963 | }) 964 | 965 | test('tryGetImageAsset(): match (pretty filename)', () => { 966 | const url = 967 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png?w=200' 968 | expect(tryGetImageAsset({asset: {url}})).toEqual(expectedAsset) 969 | }) 970 | 971 | test('tryGetImageAsset(): match (pretty filename), staging', () => { 972 | const url = 973 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png?w=200' 974 | expect(tryGetImageAsset({asset: {url}})).toEqual(expectedAsset) 975 | }) 976 | 977 | test('getImageAsset(): requires projectId/dataset on just ID', () => { 978 | expect(() => 979 | getImageAsset('image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png'), 980 | ).toThrowErrorMatchingInlineSnapshot( 981 | `[Error: Failed to resolve project ID and dataset from source]`, 982 | ) 983 | }) 984 | 985 | test('getImageAsset(): requires projectId/dataset on invalid path', () => { 986 | expect(() => 987 | getImageAsset({ 988 | _id: 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png', 989 | path: '', 990 | }), 991 | ).toThrowErrorMatchingInlineSnapshot( 992 | `[Error: Failed to resolve project ID and dataset from source]`, 993 | ) 994 | }) 995 | 996 | // getImage() 997 | test('getImage(): from ID', () => { 998 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 999 | expect(getImage(id, testProject)).toEqual(expectedImage) 1000 | }) 1001 | 1002 | test('getImage(): from ID, with custom base url', () => { 1003 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 1004 | expect(getImage(id, testProjectWithCustomBaseUrl)).toEqual(expectedImageWithCustomBaseUrl) 1005 | }) 1006 | 1007 | test('getImage(): from URL', () => { 1008 | const url = 1009 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1010 | expect(getImage(url, testProject)).toEqual(expectedImage) 1011 | }) 1012 | 1013 | test('getImage(): from URL, staging', () => { 1014 | const url = 1015 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1016 | expect(getImage(url, testProject)).toEqual(expectedImage) 1017 | }) 1018 | 1019 | test('getImage(): from URL, override base url', () => { 1020 | const url = 1021 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1022 | expect(getImage(url, testProjectWithCustomBaseUrl)).toEqual(expectedImageWithCustomBaseUrl) 1023 | }) 1024 | 1025 | test('getImage(): from URL with prettier filename', () => { 1026 | const url = 1027 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png' 1028 | expect(getImage(url, testProject)).toEqual(expectedImage) 1029 | }) 1030 | 1031 | test('getImage(): from URL with prettier filename, staging', () => { 1032 | const url = 1033 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png' 1034 | expect(getImage(url, testProject)).toEqual(expectedImage) 1035 | }) 1036 | 1037 | test('getImage(): from URL with prettier filename, staging, custom base url', () => { 1038 | const url = 1039 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png' 1040 | expect(getImage(url, testProjectWithCustomBaseUrl)).toEqual(expectedImageWithCustomBaseUrl) 1041 | }) 1042 | 1043 | test('getImage(): from URL with query', () => { 1044 | const url = 1045 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 1046 | expect(getImage(url, testProject)).toEqual(expectedImage) 1047 | }) 1048 | 1049 | test('getImage(): from URL with query, staging', () => { 1050 | const url = 1051 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 1052 | expect(getImage(url, testProject)).toEqual(expectedImage) 1053 | }) 1054 | 1055 | test('getImage(): from URL with query, staging, with custom base url', () => { 1056 | const url = 1057 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 1058 | expect(getImage(url, testProjectWithCustomBaseUrl)).toEqual(expectedImageWithCustomBaseUrl) 1059 | }) 1060 | 1061 | test('getImage(): from URL with query + prettier filename', () => { 1062 | const url = 1063 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png?w=120&h=120' 1064 | expect(getImage(url, testProject)).toEqual(expectedImage) 1065 | }) 1066 | 1067 | test('getImage(): from URL with query + prettier filename, staging', () => { 1068 | const url = 1069 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png?w=120&h=120' 1070 | expect(getImage(url, testProject)).toEqual(expectedImage) 1071 | }) 1072 | 1073 | test('getImage(): from URL with query + prettier filename, staging, with custom base url', () => { 1074 | const url = 1075 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png?w=120&h=120' 1076 | expect(getImage(url, testProjectWithCustomBaseUrl)).toEqual(expectedImageWithCustomBaseUrl) 1077 | }) 1078 | 1079 | test('getImage(): from URL, inferred project', () => { 1080 | const url = 1081 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1082 | expect(getImage(url)).toEqual(expectedImage) 1083 | }) 1084 | 1085 | test('getImage(): from URL, inferred project, staging', () => { 1086 | const url = 1087 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1088 | expect(getImage(url)).toEqual(expectedImage) 1089 | }) 1090 | 1091 | test('getImage(): from URL + prettier filename, inferred project', () => { 1092 | const url = 1093 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png' 1094 | expect(getImage(url)).toEqual(expectedImage) 1095 | }) 1096 | 1097 | test('getImage(): from URL + prettier filename, inferred project, staging', () => { 1098 | const url = 1099 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png/prettier.png' 1100 | expect(getImage(url)).toEqual(expectedImage) 1101 | }) 1102 | 1103 | test('getImage(): from URL with query, inferred project', () => { 1104 | const url = 1105 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 1106 | expect(getImage(url)).toEqual(expectedImage) 1107 | }) 1108 | 1109 | test('getImage(): from URL with query, inferred project, staging', () => { 1110 | const url = 1111 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 1112 | expect(getImage(url)).toEqual(expectedImage) 1113 | }) 1114 | 1115 | test('getImage(): from URL with query, override project/dataset', () => { 1116 | const url = 1117 | 'https://cdn.sanity.io/images/c/d/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 1118 | expect(getImage(url, {projectId: 'a', dataset: 'b'})).toEqual(expectedImage) 1119 | }) 1120 | 1121 | test('getImage(): from path', () => { 1122 | const path = 'images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1123 | expect(getImage(path, testProject)).toEqual(expectedImage) 1124 | }) 1125 | 1126 | test('getImage(): from path, inferred project', () => { 1127 | const path = 'images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1128 | expect(getImage(path)).toEqual(expectedImage) 1129 | }) 1130 | 1131 | test('getImage(): from path, override project ID/dataset', () => { 1132 | const path = 'images/c/d/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1133 | expect(getImage(path, {projectId: 'a', dataset: 'b'})).toEqual(expectedImage) 1134 | }) 1135 | 1136 | test('getImage(): from filename', () => { 1137 | const path = 'f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1138 | expect(getImage(path, testProject)).toEqual(expectedImage) 1139 | }) 1140 | 1141 | test('getImage(): from reference', () => { 1142 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 1143 | expect(getImage({_ref: id}, testProject)).toEqual(expectedImage) 1144 | }) 1145 | 1146 | test('getImage(): from asset stub (id)', () => { 1147 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 1148 | expect(getImage({_id: id}, testProject)).toEqual(expectedImage) 1149 | }) 1150 | 1151 | test('getImage(): from asset stub (path)', () => { 1152 | const path = 'images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1153 | expect(getImage({path}, testProject)).toEqual(expectedImage) 1154 | }) 1155 | 1156 | test('getImage(): from asset stub (path), inferred project', () => { 1157 | const path = 'images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1158 | expect(getImage({path})).toEqual(expectedImage) 1159 | }) 1160 | 1161 | test('getImage(): from asset stub (url)', () => { 1162 | const url = 1163 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1164 | expect(getImage({url}, testProject)).toEqual(expectedImage) 1165 | }) 1166 | 1167 | test('getImage(): from asset stub (url, staging)', () => { 1168 | const url = 1169 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1170 | expect(getImage({url}, testProject)).toEqual(expectedImage) 1171 | }) 1172 | 1173 | test('getImage(): from asset stub (url), inferred project', () => { 1174 | const url = 1175 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1176 | expect(getImage({url})).toEqual(expectedImage) 1177 | }) 1178 | 1179 | test('getImage(): from asset stub (url,), inferred project, staging', () => { 1180 | const url = 1181 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1182 | expect(getImage({url})).toEqual(expectedImage) 1183 | }) 1184 | 1185 | test('getImage(): from asset stub (url with query)', () => { 1186 | const url = 1187 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 1188 | expect(getImage({url}, testProject)).toEqual(expectedImage) 1189 | }) 1190 | 1191 | test('getImage(): from asset stub (url with query), staging', () => { 1192 | const url = 1193 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 1194 | expect(getImage({url}, testProject)).toEqual(expectedImage) 1195 | }) 1196 | 1197 | test('getImage(): from asset stub (url with query), inferred project', () => { 1198 | const url = 1199 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 1200 | expect(getImage({url})).toEqual(expectedImage) 1201 | }) 1202 | 1203 | test('getImage(): from asset stub (url with query), inferred project, staging', () => { 1204 | const url = 1205 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=120&h=120' 1206 | expect(getImage({url})).toEqual(expectedImage) 1207 | }) 1208 | 1209 | test('getImage(): from deep reference', () => { 1210 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 1211 | expect(getImage({asset: {_ref: id}}, testProject)).toEqual(expectedImage) 1212 | }) 1213 | 1214 | test('getImage(): from deep reference, custom crop/hotspot', () => { 1215 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 1216 | const custom = {crop: customCrop, hotspot: customHotspot} 1217 | expect(getImage({asset: {_ref: id}, ...custom}, testProject)).toEqual({ 1218 | ...expectedImage, 1219 | ...custom, 1220 | }) 1221 | }) 1222 | 1223 | test('getImage(): from deep asset stub (id)', () => { 1224 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 1225 | expect(getImage({asset: {_id: id}}, testProject)).toEqual(expectedImage) 1226 | }) 1227 | 1228 | test('getImage(): from deep asset stub (id), custom crop/hotspot', () => { 1229 | const id = 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png' 1230 | const custom = {crop: customCrop, hotspot: customHotspot} 1231 | expect(getImage({asset: {_id: id}, ...custom}, testProject)).toEqual({ 1232 | ...expectedImage, 1233 | ...custom, 1234 | }) 1235 | }) 1236 | 1237 | test('getImage(): from deep asset stub (path)', () => { 1238 | const path = 'images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1239 | expect(getImage({asset: {path}}, testProject)).toEqual(expectedImage) 1240 | }) 1241 | 1242 | test('getImage(): from deep asset stub (path), custom crop/hotspot', () => { 1243 | const path = 'images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1244 | const custom = {crop: customCrop, hotspot: customHotspot} 1245 | expect(getImage({asset: {path}, ...custom}, testProject)).toEqual({ 1246 | ...expectedImage, 1247 | ...custom, 1248 | }) 1249 | }) 1250 | 1251 | test('getImage(): from deep asset stub (path), inferred project', () => { 1252 | const path = 'images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1253 | expect(getImage({asset: {path}})).toEqual(expectedImage) 1254 | }) 1255 | 1256 | test('getImage(): from deep asset stub (path), inferred project, custom crop/hotspot', () => { 1257 | const path = 'images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1258 | const custom = {crop: customCrop, hotspot: customHotspot} 1259 | expect(getImage({asset: {path}, ...custom})).toEqual({...expectedImage, ...custom}) 1260 | }) 1261 | 1262 | test('getImage(): from deep asset stub (url)', () => { 1263 | const url = 1264 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1265 | expect(getImage({asset: {url}}, testProject)).toEqual(expectedImage) 1266 | }) 1267 | 1268 | test('getImage(): from deep asset stub (url), staging', () => { 1269 | const url = 1270 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1271 | expect(getImage({asset: {url}}, testProject)).toEqual(expectedImage) 1272 | }) 1273 | 1274 | test('getImage(): from deep asset stub (url), custom crop/hotspot', () => { 1275 | const url = 1276 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1277 | const custom = {crop: customCrop, hotspot: customHotspot} 1278 | expect(getImage({asset: {url}, ...custom}, testProject)).toEqual({ 1279 | ...expectedImage, 1280 | ...custom, 1281 | }) 1282 | }) 1283 | 1284 | test('getImage(): from deep asset stub (url), custom crop/hotspot, staging', () => { 1285 | const url = 1286 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1287 | const custom = {crop: customCrop, hotspot: customHotspot} 1288 | expect(getImage({asset: {url}, ...custom}, testProject)).toEqual({ 1289 | ...expectedImage, 1290 | ...custom, 1291 | }) 1292 | }) 1293 | 1294 | test('getImage(): from deep asset stub (url), inferred project', () => { 1295 | const url = 1296 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1297 | expect(getImage({asset: {url}})).toEqual(expectedImage) 1298 | }) 1299 | 1300 | test('getImage(): from deep asset stub (url), inferred project, staging', () => { 1301 | const url = 1302 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1303 | expect(getImage({asset: {url}})).toEqual(expectedImage) 1304 | }) 1305 | 1306 | test('getImage(): from deep asset stub (url), inferred project, custom crop/hotspot', () => { 1307 | const url = 1308 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1309 | const custom = {crop: customCrop, hotspot: customHotspot} 1310 | expect(getImage({asset: {url}, ...custom})).toEqual({...expectedImage, ...custom}) 1311 | }) 1312 | 1313 | test('getImage(): from deep asset stub (url), inferred project, custom crop/hotspot, staging', () => { 1314 | const url = 1315 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png' 1316 | const custom = {crop: customCrop, hotspot: customHotspot} 1317 | expect(getImage({asset: {url}, ...custom})).toEqual({...expectedImage, ...custom}) 1318 | }) 1319 | 1320 | test('getImage(): from deep asset stub (url with query)', () => { 1321 | const url = 1322 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 1323 | expect(getImage({asset: {url}}, testProject)).toEqual(expectedImage) 1324 | }) 1325 | 1326 | test('getImage(): from deep asset stub (url with query), staging', () => { 1327 | const url = 1328 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 1329 | expect(getImage({asset: {url}}, testProject)).toEqual(expectedImage) 1330 | }) 1331 | 1332 | test('getImage(): from deep asset stub (url with query), custom crop/hotspot', () => { 1333 | const url = 1334 | 'https://cdn.sanity.io/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 1335 | const custom = {crop: customCrop, hotspot: customHotspot} 1336 | expect(getImage({asset: {url}, ...custom}, testProject)).toEqual({ 1337 | ...expectedImage, 1338 | ...custom, 1339 | }) 1340 | }) 1341 | 1342 | test('getImage(): from deep asset stub (url with query), custom crop/hotspot, staging', () => { 1343 | const url = 1344 | 'https://cdn.sanity.staging/images/foo/bar/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 1345 | const custom = {crop: customCrop, hotspot: customHotspot} 1346 | expect(getImage({asset: {url}, ...custom}, testProject)).toEqual({ 1347 | ...expectedImage, 1348 | ...custom, 1349 | }) 1350 | }) 1351 | 1352 | test('getImage(): from deep asset stub (url with query), inferred project', () => { 1353 | const url = 1354 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 1355 | expect(getImage({asset: {url}})).toEqual(expectedImage) 1356 | }) 1357 | 1358 | test('getImage(): from deep asset stub (url with query), inferred project, staging', () => { 1359 | const url = 1360 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 1361 | expect(getImage({asset: {url}})).toEqual(expectedImage) 1362 | }) 1363 | 1364 | test('getImage(): from deep asset stub (url with query), inferred project, custom crop/hotspot', () => { 1365 | const url = 1366 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 1367 | const custom = {crop: customCrop, hotspot: customHotspot} 1368 | expect(getImage({asset: {url}, ...custom})).toEqual({...expectedImage, ...custom}) 1369 | }) 1370 | 1371 | test('getImage(): from deep asset stub (url with query), inferred project, custom crop/hotspot, staging', () => { 1372 | const url = 1373 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 1374 | const custom = {crop: customCrop, hotspot: customHotspot} 1375 | expect(getImage({asset: {url}, ...custom})).toEqual({...expectedImage, ...custom}) 1376 | }) 1377 | 1378 | test('getImage(): no match', () => { 1379 | expect(() => getImage('ey hold up')).toThrow(/failed to resolve/i) 1380 | }) 1381 | 1382 | test('tryGetImage(): no match', () => { 1383 | expect(tryGetImage('ey hold up')).toBe(undefined) 1384 | }) 1385 | 1386 | test('tryGetImage(): match', () => { 1387 | const url = 1388 | 'https://cdn.sanity.io/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 1389 | expect(tryGetImage(url)).toEqual(expectedImage) 1390 | }) 1391 | 1392 | test('tryGetImage(): match, staging', () => { 1393 | const url = 1394 | 'https://cdn.sanity.staging/images/a/b/f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240.png?w=200' 1395 | expect(tryGetImage(url)).toEqual(expectedImage) 1396 | }) 1397 | 1398 | test('getImage(): requires projectId/dataset on just ID', () => { 1399 | expect(() => 1400 | getImage('image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png'), 1401 | ).toThrowErrorMatchingInlineSnapshot( 1402 | `[Error: Failed to resolve project ID and dataset from source]`, 1403 | ) 1404 | }) 1405 | 1406 | test('getImage(): requires projectId/dataset on invalid path', () => { 1407 | expect(() => 1408 | getImage({ 1409 | _id: 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-320x240-png', 1410 | path: '', 1411 | }), 1412 | ).toThrowErrorMatchingInlineSnapshot( 1413 | `[Error: Failed to resolve project ID and dataset from source]`, 1414 | ) 1415 | }) 1416 | 1417 | // getImageDimensions() 1418 | test.each(validImgSources)('getImageDimensions() can resolve from %s', (_, source) => { 1419 | expect(getImageDimensions(source)).toEqual({width: 320, height: 240, aspectRatio: 320 / 240}) 1420 | }) 1421 | 1422 | test.each(validLegacyImgSources)('getImageDimensions() can resolve from legacy %s', (_, source) => { 1423 | expect(getImageDimensions(source)).toEqual({width: 600, height: 578, aspectRatio: 600 / 578}) 1424 | }) 1425 | 1426 | test('getImageDimensions(): throws on invalid source', () => { 1427 | expect(() => getImageDimensions('whatever')).toThrowErrorMatchingInlineSnapshot( 1428 | `[Error: Failed to resolve asset ID from source]`, 1429 | ) 1430 | }) 1431 | 1432 | test('tryGetImageDimensions(): returns undefined on invalid source', () => { 1433 | expect(tryGetImageDimensions('whatever')).toBe(undefined) 1434 | }) 1435 | 1436 | test.each(validImgSources)('tryGetImageDimensions() can resolve from %s', (_, source) => { 1437 | expect(tryGetImageDimensions(source)).toEqual({width: 320, height: 240, aspectRatio: 320 / 240}) 1438 | }) 1439 | 1440 | test.each(validLegacyImgSources)( 1441 | 'tryGetImageDimensions() can resolve from legacy %s', 1442 | (_, source) => { 1443 | expect(tryGetImageDimensions(source)).toEqual({width: 600, height: 578, aspectRatio: 600 / 578}) 1444 | }, 1445 | ) 1446 | 1447 | // getExtension() 1448 | test.each(validImgSources)('getExtension() can resolve from image %s', (_, source) => { 1449 | expect(getExtension(source)).toEqual('png') 1450 | }) 1451 | 1452 | test.each(validLegacyImgSources)('getExtension() can resolve from legacy %s', (_, source) => { 1453 | expect(getExtension(source)).toEqual('png') 1454 | }) 1455 | 1456 | test.each(validFileSources)('getExtension() can resolve from file %s', (_, source) => { 1457 | expect(getExtension(source)).toEqual('pdf') 1458 | }) 1459 | 1460 | test('getExtension(): throws on invalid source', () => { 1461 | expect(() => getExtension('whatever')).toThrowErrorMatchingInlineSnapshot( 1462 | `[Error: Failed to resolve asset ID from source]`, 1463 | ) 1464 | }) 1465 | 1466 | test('tryGetExtension(): returns undefined on invalid source', () => { 1467 | expect(tryGetExtension('whatever')).toBe(undefined) 1468 | }) 1469 | 1470 | test.each(validImgSources)('tryGetExtension() can resolve from image %s', (_, source) => { 1471 | expect(getExtension(source)).toEqual('png') 1472 | }) 1473 | 1474 | test.each(validFileSources)('tryGetExtension() can resolve from file %s', (_, source) => { 1475 | expect(getExtension(source)).toEqual('pdf') 1476 | }) 1477 | 1478 | // getProject() 1479 | test('getProject(): throws if passing a reference', () => { 1480 | expect(() => 1481 | getProject({_ref: 'image-f00baaf00baaf00baaf00baaf00baaf00baaf00b-200x300-png'}), 1482 | ).toThrowErrorMatchingInlineSnapshot( 1483 | `[Error: Failed to resolve project ID and dataset from source]`, 1484 | ) 1485 | }) 1486 | 1487 | // isAssetFilename() 1488 | test('isAssetFilename(): returns true for image filenames', () => { 1489 | expect(isAssetFilename('f00baaf00baaf00baaf00baaf00baaf00baaf00b-200x300.png')).toBe(true) 1490 | }) 1491 | 1492 | test('isAssetFilename(): returns true for file filenames', () => { 1493 | expect(isAssetFilename('f00baaf00baaf00baaf00baaf00baaf00baaf00b.mp4')).toBe(true) 1494 | }) 1495 | 1496 | test('isAssetFilename(): returns false for invalid filenames', () => { 1497 | expect(isAssetFilename('foobar-lol.rottifnatti')).toBe(false) 1498 | }) 1499 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src", "./scripts"], 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "./dist", 6 | 7 | // Completeness 8 | "skipLibCheck": true, 9 | 10 | // Interop constraints 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | 14 | // Language and environment 15 | "target": "ES2020", 16 | 17 | // Modules 18 | "module": "ES2020", 19 | "moduleResolution": "Node", 20 | 21 | // Type checking 22 | "alwaysStrict": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noImplicitAny": true, 25 | "noImplicitReturns": true, 26 | "noImplicitThis": true, 27 | "noUnusedLocals": true, 28 | "noUnusedParameters": true, 29 | "strict": true, 30 | "strictFunctionTypes": true, 31 | "strictNullChecks": true, 32 | "strictPropertyInitialization": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["./src/index.ts"], 3 | "theme": "default", 4 | "json": "./docs/docs.json", 5 | "out": "./docs", 6 | "readme": "./_README.template.md", 7 | "hideGenerator": true, 8 | "groupOrder": ["Functions", "*"], 9 | "sort": ["kind", "instance-first", "alphabetical"], 10 | "kindSortOrder": [ 11 | "Function", 12 | "Variable", 13 | "Reference", 14 | "Project", 15 | "Module", 16 | "Namespace", 17 | "Enum", 18 | "EnumMember", 19 | "Class", 20 | "Interface", 21 | "TypeAlias", 22 | "Constructor", 23 | "Property", 24 | "Accessor", 25 | "Method", 26 | "Parameter", 27 | "TypeParameter", 28 | "TypeLiteral", 29 | "CallSignature", 30 | "ConstructorSignature", 31 | "IndexSignature", 32 | "GetSignature", 33 | "SetSignature" 34 | ] 35 | } 36 | --------------------------------------------------------------------------------