diff --git a/.gitignore b/.gitignore
index 614beed..6a22854 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,5 @@
.idea/
.tmp/
coverage/
-esm/
-lib/
+dist/
node_modules/
-reports/
-webpack.stats.json
diff --git a/README.md b/README.md
index c1ecacf..3ddb5af 100644
--- a/README.md
+++ b/README.md
@@ -43,11 +43,11 @@ export default function App() {
## Props
**src** {string} - **required**.
-The SVG file you want to load. It can be a require, URL or a string (base64 or url encoded).
-_If you are using create-react-app and your file resides in the `public` directory you can use the path directly without require._
+The SVG file you want to load. It can be a require, URL, or a string (base64 or URL encoded).
+_If you are using create-react-app and your file resides in the `public` directory, you can use the path directly without require._
**baseURL** {string}
-An URL to prefix each ID in case you are using the ` ` tag and `uniquifyIDs`.
+An URL to prefix each ID in case you use the ` ` tag and `uniquifyIDs`.
**children** {ReactNode}
The fallback content in case of a fetch error or unsupported browser.
@@ -59,7 +59,8 @@ The fallback content in case of a fetch error or unsupported browser.
```
**cacheRequests** {boolean} ▶︎ `true`
-Cache remote SVGs.
+Cache remote SVGs.
+Starting in version 4.x, you can also cache the files permanently, read more [below](#caching).
**description** {string}
A description for your SVG. It will override an existing `` tag.
@@ -100,7 +101,7 @@ This will receive a single argument with:
**onLoad** {function}.
A callback to be invoked upon successful load.
-This will receive 2 arguments: the `src` prop and a `hasCache` boolean
+This will receive 2 arguments: the `src` prop and an `isCached` boolean
**preProcessor** {function} ▶︎ `string`
A function to process the contents of the SVG text before parsing.
@@ -126,7 +127,7 @@ Create unique IDs for each icon.
description="The React logo"
loader={Loading... }
onError={(error) => console.log(error.message)}
- onLoad={(src, hasCache) => console.log(src, hasCache)}
+ onLoad={(src, isCached) => console.log(src, isCached)}
preProcessor={(code) => code.replace(/fill=".*?"/g, 'fill="currentColor"')}
src="https://cdn.svgporn.com/logos/react.svg"
title="React"
@@ -137,23 +138,37 @@ Create unique IDs for each icon.
## Caching
-The internal cache is exported as `cacheStore` if you need to debug or pre-cache some files.
-⚠️ Use it at your own risk.
+You can use the browser's cache to store the SVGs permanently.
+To set it up, wrap your app with the cache provider:
+
+```typescript
+import { createRoot } from 'react-dom/client';
+import CacheProvider from 'react-inlinesvg/provider';
+import App from './App';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
+```
+
+> Be aware of the limitations of the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
## Browser Support
Any browsers that support inlining [SVGs](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg) and [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) will work.
-If you need to support legacy browsers you'll need to include a polyfiil for `fetch` and `Number.isNaN` in your app. Take a look at [react-app-polyfill](https://www.npmjs.com/package/react-app-polyfill) or [polyfill.io](https://polyfill.io/v3/).
+If you need to support legacy browsers, include a polyfill for `fetch` and `Number.isNaN` in your app. Take a look at [react-app-polyfill](https://www.npmjs.com/package/react-app-polyfill) or [polyfill.io](https://polyfill.io/v3/).
## CORS
-If you are loading remote SVGs, you'll need to make sure it has [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) support.
+If you are loading remote SVGs, you must ensure it has [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) support.
-## Why you need this package?
+## Why do you need this package?
-One of the reasons SVGs are awesome is because you can style them with CSS.
-Unfortunately, this winds up not being too useful in practice because the style element has to be in the same document. This leaves you with three bad options:
+One of the reasons SVGs are awesome is that you can style them with CSS.
+Unfortunately, this is not useful in practice because the style element has to be in the same document. This leaves you with three bad options:
1. Embed the CSS in the SVG document
- Can't use your CSS preprocessors (LESS, SASS)
@@ -178,5 +193,5 @@ The SVG [``](http://css-tricks.com/svg-use-external-source) element can be
## Credits
-Thanks to [@matthewwithanm](https://github.com/matthewwithanm) for creating this component and so kindly transfer it to me.
-I'll definitely keep the good work! ❤️
+Thanks to [@matthewwithanm](https://github.com/matthewwithanm) for creating this component and so kindly transferring it to me.
+I'll definitely keep up the good work! ❤️
diff --git a/demo/package.json b/demo/package.json
index c19aeb9..1d029b5 100755
--- a/demo/package.json
+++ b/demo/package.json
@@ -1,19 +1,18 @@
{
"name": "react-inlinesvg-demo",
- "version": "3.0.2",
+ "version": "4.0.0",
"description": "An SVG loader component for ReactJS",
"keywords": [],
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-inlinesvg": "^3.0.2",
+ "react-inlinesvg": "latest",
"react-scripts": "^5.0.1",
- "styled-components": "^5.3.6",
+ "styled-components": "^6.0.7",
"@types/node": "^18.14.1",
- "@types/react": "^18.0.28",
- "@types/react-dom": "^18.0.11",
- "@types/styled-components": "^5.1.26",
- "typescript": "^4.9.5"
+ "@types/react": "^18.2.19",
+ "@types/react-dom": "^18.2.7",
+ "typescript": "^5.1.6"
},
"scripts": {
"start": "react-scripts start",
diff --git a/package-lock.json b/package-lock.json
index 109be93..593a882 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"@gilbarbara/prettier-config": "^1.0.0",
"@gilbarbara/tsconfig": "^0.1.1",
"@size-limit/preset-small-lib": "^8.2.6",
+ "@swc/core": "^1.3.75",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.0.0",
"@types/exenv": "^1.2.0",
@@ -25,8 +26,9 @@
"@types/jest": "^29.5.3",
"@types/node": "^20.4.8",
"@types/node-fetch": "^2.6.4",
- "@types/react": "^18.2.18",
+ "@types/react": "^18.2.19",
"@types/react-dom": "^18.2.7",
+ "browser-cache-mock": "^0.1.7",
"cross-fetch": "^4.0.0",
"del-cli": "^5.0.0",
"http-server": "^14.1.1",
@@ -44,6 +46,7 @@
"start-server-and-test": "^2.0.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
+ "tsup": "^7.2.0",
"typescript": "^5.1.6"
},
"peerDependencies": {
@@ -2099,6 +2102,200 @@
"size-limit": "8.2.6"
}
},
+ "node_modules/@swc/core": {
+ "version": "1.3.75",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.75.tgz",
+ "integrity": "sha512-YLqd5oZVnaOq/OzkjRSsJUQqAfKYiD0fzUyVUPVlNNCoQEfVfSMcXH80hLmYe9aDH0T/a7qEMjWyIr/0kWqy1A==",
+ "dev": true,
+ "hasInstallScript": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.3.75",
+ "@swc/core-darwin-x64": "1.3.75",
+ "@swc/core-linux-arm-gnueabihf": "1.3.75",
+ "@swc/core-linux-arm64-gnu": "1.3.75",
+ "@swc/core-linux-arm64-musl": "1.3.75",
+ "@swc/core-linux-x64-gnu": "1.3.75",
+ "@swc/core-linux-x64-musl": "1.3.75",
+ "@swc/core-win32-arm64-msvc": "1.3.75",
+ "@swc/core-win32-ia32-msvc": "1.3.75",
+ "@swc/core-win32-x64-msvc": "1.3.75"
+ },
+ "peerDependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.3.75",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.75.tgz",
+ "integrity": "sha512-anDnx9L465lGbjB2mvcV54NGHW6illr0IDvVV7JmkabYUVneaRdQvTr0tbHv3xjHnjrK1wuwVOHKV0LcQF2tnQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.3.75",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.75.tgz",
+ "integrity": "sha512-dIHDfrLmeZfr2xwi1whO7AmzdI3HdamgvxthaL+S8L1x8TeczAZEvsmZTjy3s8p3Va4rbGXcb3+uBhmfkqCbfw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
+ "version": "1.3.75",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.75.tgz",
+ "integrity": "sha512-qeJmvMGrjC6xt+G0R4kVqqxvlhxJx7tTzhcEoWgLJnfvGZiF6SJdsef4OSM7HuReXrlBoEtJbfGPrLJtbV+C0w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.3.75",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.75.tgz",
+ "integrity": "sha512-sqA9JqHEJBF4AdNuwo5zRqq0HC3l31SPsG9zpRa4nRzG5daBBJ80H7fi6PZQud1rfNNq+Q08gjYrdrxwHstvjw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-musl": {
+ "version": "1.3.75",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.75.tgz",
+ "integrity": "sha512-95rQT5xTAL3eKhMJbJbLsZHHP9EUlh1rcrFoLf0gUApoVF8g94QjZ9hYZiI72mMP5WPjgTEXQVnVB9O2GxeaLw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.3.75",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.75.tgz",
+ "integrity": "sha512-If7UpAhnPduMmtC+TSgPpZ1UXZfp2hIpjUFxpeCmHHYLS6Fn/2GZC5hpEiu+wvFJF0hzPh93eNAHa9gUxGUG+w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-musl": {
+ "version": "1.3.75",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.75.tgz",
+ "integrity": "sha512-HOhxX0YNHTElCZqIviquka3CGYTN8rSQ6BdFfSk/K0O+ZEHx3qGte0qr+gGLPF/237GxreUkp3OMaWKuURtuCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-arm64-msvc": {
+ "version": "1.3.75",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.75.tgz",
+ "integrity": "sha512-7QPI+mvBXAerVfWahrgBNe+g7fK8PuetxFnZSEmXUcDXvWcdJXAndD7GjAJzbDyjQpLKHbsDKMiHYvfNxZoN/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-ia32-msvc": {
+ "version": "1.3.75",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.75.tgz",
+ "integrity": "sha512-EfABCy4Wlq7O5ShWsm32FgDkSjyeyj/SQ4wnUIvWpkXhgfT1iNXky7KRU1HtX+SmnVk/k/NnabVZpIklYbjtZA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-x64-msvc": {
+ "version": "1.3.75",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.75.tgz",
+ "integrity": "sha512-cTvP0pOD9C3pSp1cwtt85ZsrUkQz8RZfSPhM+jCGxKxmoowDCnInoOQ4Ica/ehyuUnQ4/IstSdYtYpO5yzPDJg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz",
@@ -2434,9 +2631,9 @@
"dev": true
},
"node_modules/@types/react": {
- "version": "18.2.18",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.18.tgz",
- "integrity": "sha512-da4NTSeBv/P34xoZPhtcLkmZuJ+oYaCxHmyHzwaDQo9RQPBeXV+06gEk2FpqEcsX9XrnNLvRpVh6bdavDSjtiQ==",
+ "version": "18.2.19",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.19.tgz",
+ "integrity": "sha512-e2S8wmY1ePfM517PqCG80CcE48Xs5k0pwJzuDZsfE8IZRRBfOMCF+XqnFxu6mWtyivum1MQm4aco+WIt6Coimw==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
@@ -3039,6 +3236,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true
+ },
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -3459,6 +3662,12 @@
"node": ">=8"
}
},
+ "node_modules/browser-cache-mock": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/browser-cache-mock/-/browser-cache-mock-0.1.7.tgz",
+ "integrity": "sha512-+0bM5cjnj8Z8caAYBe1A9AA30juciI8fBHhw+aPBSSGL89UHPUcBA/E6CrQAecC635MEfx8qadTnaocEwE2q8Q==",
+ "dev": true
+ },
"node_modules/browserslist": {
"version": "4.21.10",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz",
@@ -3545,6 +3754,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/bundle-require": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.1.tgz",
+ "integrity": "sha512-9NQkRHlNdNpDBGmLpngF3EFDcwodhMUuLz9PaWYciVcQF9SE4LFjM2DB/xV1Li5JiuDMv7ZUWuC3rGbqR0MAXQ==",
+ "dev": true,
+ "dependencies": {
+ "load-tsconfig": "^0.2.3"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "esbuild": ">=0.17"
+ }
+ },
"node_modules/bytes-iec": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/bytes-iec/-/bytes-iec-3.1.1.tgz",
@@ -3554,6 +3778,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -8485,6 +8718,15 @@
"@sideway/pinpoint": "^2.0.0"
}
},
+ "node_modules/joycon": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
+ "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -8697,6 +8939,15 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
+ "node_modules/load-tsconfig": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
+ "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/loader-runner": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -8740,6 +8991,12 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
+ "node_modules/lodash.sortby": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
+ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
+ "dev": true
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -9062,6 +9319,17 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
@@ -9688,6 +9956,35 @@
"mkdirp": "bin/cmd.js"
}
},
+ "node_modules/postcss-load-config": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz",
+ "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==",
+ "dev": true,
+ "dependencies": {
+ "lilconfig": "^2.0.5",
+ "yaml": "^2.1.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -10282,6 +10579,22 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/rollup": {
+ "version": "3.27.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.27.2.tgz",
+ "integrity": "sha512-YGwmHf7h2oUHkVBT248x0yt6vZkYQ3/rvE5iQuVBh3WO8GcJ6BNeOkpoX1yMHIiBm18EMLjBPIoUDkhgnyxGOQ==",
+ "dev": true,
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=14.18.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
"node_modules/run-applescript": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",
@@ -10842,6 +11155,57 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/sucrase": {
+ "version": "3.34.0",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
+ "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "glob": "7.1.6",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sucrase/node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/sucrase/node_modules/glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -10971,6 +11335,27 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -11043,6 +11428,15 @@
"node": ">=12"
}
},
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
"node_modules/trim-newlines": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.0.2.tgz",
@@ -11067,6 +11461,12 @@
"typescript": ">=4.2.0"
}
},
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true
+ },
"node_modules/ts-jest": {
"version": "29.1.1",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
@@ -11201,6 +11601,98 @@
"integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==",
"dev": true
},
+ "node_modules/tsup": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/tsup/-/tsup-7.2.0.tgz",
+ "integrity": "sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==",
+ "dev": true,
+ "dependencies": {
+ "bundle-require": "^4.0.0",
+ "cac": "^6.7.12",
+ "chokidar": "^3.5.1",
+ "debug": "^4.3.1",
+ "esbuild": "^0.18.2",
+ "execa": "^5.0.0",
+ "globby": "^11.0.3",
+ "joycon": "^3.0.1",
+ "postcss-load-config": "^4.0.1",
+ "resolve-from": "^5.0.0",
+ "rollup": "^3.2.5",
+ "source-map": "0.8.0-beta.0",
+ "sucrase": "^3.20.3",
+ "tree-kill": "^1.2.2"
+ },
+ "bin": {
+ "tsup": "dist/cli-default.js",
+ "tsup-node": "dist/cli-node.js"
+ },
+ "engines": {
+ "node": ">=16.14"
+ },
+ "peerDependencies": {
+ "@swc/core": "^1",
+ "postcss": "^8.4.12",
+ "typescript": ">=4.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tsup/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tsup/node_modules/source-map": {
+ "version": "0.8.0-beta.0",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
+ "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-url": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/tsup/node_modules/tr46": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
+ "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/tsup/node_modules/webidl-conversions": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
+ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
+ "dev": true
+ },
+ "node_modules/tsup/node_modules/whatwg-url": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
+ "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
+ "dev": true,
+ "dependencies": {
+ "lodash.sortby": "^4.7.0",
+ "tr46": "^1.0.1",
+ "webidl-conversions": "^4.0.2"
+ }
+ },
"node_modules/tsutils": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
@@ -11809,6 +12301,15 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
+ "node_modules/yaml": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
+ "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
diff --git a/package.json b/package.json
index 36b4c5a..09496bb 100644
--- a/package.json
+++ b/package.json
@@ -22,14 +22,28 @@
"url": "https://github.com/gilbarbara/react-inlinesvg/issues"
},
"homepage": "https://github.com/gilbarbara/react-inlinesvg#readme",
- "main": "lib/index.js",
- "module": "esm/index.js",
+ "main": "./dist/index.js",
+ "module": "./dist/index.mjs",
+ "exports": {
+ ".": {
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.js"
+ },
+ "./provider": {
+ "import": "./dist/provider.mjs",
+ "default": "./dist/provider.js"
+ }
+ },
"files": [
- "esm",
- "lib",
+ "dist",
"src"
],
- "types": "esm",
+ "typesVersions": {
+ "*": {
+ "provider": ["dist/provider.d.ts"]
+ }
+ },
+ "types": "dist/index.d.ts",
"sideEffects": false,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
@@ -44,6 +58,7 @@
"@gilbarbara/prettier-config": "^1.0.0",
"@gilbarbara/tsconfig": "^0.1.1",
"@size-limit/preset-small-lib": "^8.2.6",
+ "@swc/core": "^1.3.75",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.0.0",
"@types/exenv": "^1.2.0",
@@ -51,8 +66,9 @@
"@types/jest": "^29.5.3",
"@types/node": "^20.4.8",
"@types/node-fetch": "^2.6.4",
- "@types/react": "^18.2.18",
+ "@types/react": "^18.2.19",
"@types/react-dom": "^18.2.7",
+ "browser-cache-mock": "^0.1.7",
"cross-fetch": "^4.0.0",
"del-cli": "^5.0.0",
"http-server": "^14.1.1",
@@ -70,15 +86,13 @@
"start-server-and-test": "^2.0.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
+ "tsup": "^7.2.0",
"typescript": "^5.1.6"
},
"scripts": {
- "build": "npm run clean && npm run build:cjs && npm run build:esm",
- "build:cjs": "tsc",
- "build:esm": "tsc -m es6 --outDir esm",
- "clean": "del lib/* && del esm/*",
- "watch:cjs": "npm run build:cjs -- -w",
- "watch:esm": "npm run build:esm -- -w",
+ "build": "npm run clean && tsup",
+ "watch": "tsup --watch",
+ "clean": "del dist/*",
"start": "http-server test/__fixtures__ -p 1337 --cors",
"test": "start-server-and-test start http://127.0.0.1:1337 test:coverage",
"test:coverage": "jest --coverage --bail",
@@ -90,6 +104,19 @@
"prepublishOnly": "npm run validate",
"prepare": "husky install"
},
+ "tsup": {
+ "dts": true,
+ "entry": [
+ "src/index.tsx",
+ "src/provider.tsx"
+ ],
+ "format": [
+ "cjs",
+ "esm"
+ ],
+ "sourcemap": true,
+ "splitting": false
+ },
"eslintConfig": {
"extends": [
"@gilbarbara/eslint-config"
@@ -111,14 +138,14 @@
"prettier": "@gilbarbara/prettier-config",
"size-limit": [
{
- "name": "lib",
- "path": "./lib/index.js",
- "limit": "9 kB"
+ "name": "commonjs",
+ "path": "./dist/index.js",
+ "limit": "11 KB"
},
{
"name": "esm",
- "path": "./esm/index.js",
- "limit": "9 kB"
+ "path": "./dist/index.mjs",
+ "limit": "9 KB"
}
]
}
diff --git a/sonar-project.properties b/sonar-project.properties
index 85cf299..af72527 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -2,4 +2,5 @@ sonar.projectKey=gilbarbara_react-inlinesvg
sonar.organization=gilbarbara-github
sonar.source=./src
sonar.javascript.lcov.reportPaths=./coverage/lcov.info
+sonar.exclusions=**/demo/**/*.*,**/test/**/*.*
sonar.coverage.exclusions=**/test/**/*.*,**/jest.config.js
diff --git a/src/cache.ts b/src/cache.ts
new file mode 100644
index 0000000..45720e0
--- /dev/null
+++ b/src/cache.ts
@@ -0,0 +1,164 @@
+import { CACHE_MAX_RETRIES, CACHE_NAME, STATUS } from './config';
+import { request, sleep } from './helpers';
+import { StorageItem } from './types';
+
+export default class CacheStore {
+ private cacheApi: Cache | undefined;
+ private readonly cacheStore: Map;
+ private readonly subscribers: Array<() => void> = [];
+ private readonly usePersistentCache: boolean;
+ public isReady = false;
+
+ constructor() {
+ this.cacheStore = new Map();
+
+ this.usePersistentCache =
+ 'REACT_INLINESVG_PERSISTENT_CACHE' in window && !!window.REACT_INLINESVG_PERSISTENT_CACHE;
+
+ if (this.usePersistentCache) {
+ caches.open(CACHE_NAME).then(cache => {
+ this.cacheApi = cache;
+ this.isReady = true;
+
+ this.subscribers.forEach(callback => callback());
+ });
+ } else {
+ this.isReady = true;
+ }
+ }
+
+ public onReady(callback: () => void) {
+ if (this.isReady) {
+ callback();
+ } else {
+ this.subscribers.push(callback);
+ }
+ }
+
+ public async get(url: string, fetchOptions?: RequestInit) {
+ await (this.usePersistentCache
+ ? this.fetchAndAddToPersistentCache(url, fetchOptions)
+ : this.fetchAndAddToInternalCache(url, fetchOptions));
+
+ return this.cacheStore.get(url)?.content ?? '';
+ }
+
+ public set(url: string, data: StorageItem) {
+ this.cacheStore.set(url, data);
+ }
+
+ public isCached(url: string) {
+ return this.cacheStore.get(url)?.status === STATUS.LOADED;
+ }
+
+ private async fetchAndAddToInternalCache(url: string, fetchOptions?: RequestInit) {
+ const cache = this.cacheStore.get(url);
+
+ if (cache?.status === STATUS.LOADING) {
+ await this.handleLoading(url, async () => {
+ this.cacheStore.set(url, { content: '', status: STATUS.IDLE });
+ await this.fetchAndAddToInternalCache(url, fetchOptions);
+ });
+
+ return;
+ }
+
+ if (!cache?.content) {
+ this.cacheStore.set(url, { content: '', status: STATUS.LOADING });
+
+ try {
+ const content = await request(url, fetchOptions);
+
+ this.cacheStore.set(url, { content, status: STATUS.LOADED });
+ } catch (error: any) {
+ this.cacheStore.set(url, { content: '', status: STATUS.FAILED });
+ throw error;
+ }
+ }
+ }
+
+ private async fetchAndAddToPersistentCache(url: string, fetchOptions?: RequestInit) {
+ const cache = this.cacheStore.get(url);
+
+ if (cache?.status === STATUS.LOADED) {
+ return;
+ }
+
+ if (cache?.status === STATUS.LOADING) {
+ await this.handleLoading(url, async () => {
+ this.cacheStore.set(url, { content: '', status: STATUS.IDLE });
+ await this.fetchAndAddToPersistentCache(url, fetchOptions);
+ });
+
+ return;
+ }
+
+ this.cacheStore.set(url, { content: '', status: STATUS.LOADING });
+
+ const data = await this.cacheApi?.match(url);
+
+ if (data) {
+ const content = await data.text();
+
+ this.cacheStore.set(url, { content, status: STATUS.LOADED });
+
+ return;
+ }
+
+ try {
+ await this.cacheApi?.add(new Request(url, fetchOptions));
+
+ const response = await this.cacheApi?.match(url);
+ const content = (await response?.text()) ?? '';
+
+ this.cacheStore.set(url, { content, status: STATUS.LOADED });
+ } catch (error: any) {
+ this.cacheStore.set(url, { content: '', status: STATUS.FAILED });
+ throw error;
+ }
+ }
+
+ private async handleLoading(url: string, callback: () => Promise) {
+ let retryCount = 0;
+
+ // eslint-disable-next-line no-await-in-loop
+ while (this.cacheStore.get(url)?.status === STATUS.LOADING && retryCount < CACHE_MAX_RETRIES) {
+ // eslint-disable-next-line no-await-in-loop
+ await sleep(0.1);
+ retryCount += 1;
+ }
+
+ if (retryCount >= CACHE_MAX_RETRIES) {
+ await callback();
+ }
+ }
+
+ public keys(): Array {
+ return [...this.cacheStore.keys()];
+ }
+
+ public data(): Array> {
+ return [...this.cacheStore.entries()].map(([key, value]) => ({ [key]: value }));
+ }
+
+ public async delete(url: string) {
+ if (this.cacheApi) {
+ await this.cacheApi.delete(url);
+ }
+
+ this.cacheStore.delete(url);
+ }
+
+ public async clear() {
+ if (this.cacheApi) {
+ const keys = await this.cacheApi.keys();
+
+ for (const key of keys) {
+ // eslint-disable-next-line no-await-in-loop
+ await this.cacheApi.delete(key);
+ }
+ }
+
+ this.cacheStore.clear();
+ }
+}
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..3f1de06
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,11 @@
+export const CACHE_NAME = 'react-inlinesvg';
+export const CACHE_MAX_RETRIES = 10;
+
+export const STATUS = {
+ IDLE: 'idle',
+ LOADING: 'loading',
+ LOADED: 'loaded',
+ FAILED: 'failed',
+ READY: 'ready',
+ UNSUPPORTED: 'unsupported',
+} as const;
diff --git a/src/helpers.ts b/src/helpers.ts
index 1e12268..7a25691 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -2,15 +2,6 @@ import { canUseDOM as canUseDOMFlag } from 'exenv';
import type { PlainObject } from './types';
-export const STATUS = {
- IDLE: 'idle',
- LOADING: 'loading',
- LOADED: 'loaded',
- FAILED: 'failed',
- READY: 'ready',
- UNSUPPORTED: 'unsupported',
-} as const;
-
export function canUseDOM(): boolean {
return canUseDOMFlag;
}
@@ -19,6 +10,28 @@ export function isSupportedEnvironment(): boolean {
return supportsInlineSVG() && typeof window !== 'undefined' && window !== null;
}
+export async function request(url: string, options?: RequestInit) {
+ const response = await fetch(url, options);
+ const contentType = response.headers.get('content-type');
+ const [fileType] = (contentType || '').split(/ ?; ?/);
+
+ if (response.status > 299) {
+ throw new Error('Not found');
+ }
+
+ if (!['image/svg+xml', 'text/plain'].some(d => fileType.includes(d))) {
+ throw new Error(`Content type isn't valid: ${fileType}`);
+ }
+
+ return response.text();
+}
+
+export function sleep(seconds = 1) {
+ return new Promise(resolve => {
+ setTimeout(resolve, seconds * 1000);
+ });
+}
+
export function supportsInlineSVG(): boolean {
/* istanbul ignore next */
if (!document) {
diff --git a/src/index.tsx b/src/index.tsx
index 880dec9..fcb9051 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,12 +1,15 @@
import * as React from 'react';
import convert from 'react-from-dom';
-import { canUseDOM, isSupportedEnvironment, omit, randomString, STATUS } from './helpers';
-import { FetchError, Props, State, Status, StorageItem } from './types';
+import CacheStore from './cache';
+import { STATUS } from './config';
+import { canUseDOM, isSupportedEnvironment, omit, randomString, request } from './helpers';
+import { FetchError, Props, State, Status } from './types';
-export const cacheStore: { [key: string]: StorageItem } = Object.create(null);
+// eslint-disable-next-line import/no-mutable-exports
+export let cacheStore: CacheStore;
-export default class InlineSVG extends React.PureComponent {
+class ReactInlineSVG extends React.PureComponent {
private readonly hash: string;
private isActive = false;
private isInitialized = false;
@@ -22,7 +25,7 @@ export default class InlineSVG extends React.PureComponent {
this.state = {
content: '',
element: null,
- hasCache: !!props.cacheRequests && !!cacheStore[props.src],
+ isCached: !!props.cacheRequests && cacheStore.isCached(props.src),
status: STATUS.IDLE,
};
@@ -66,13 +69,13 @@ export default class InlineSVG extends React.PureComponent {
return;
}
- const { hasCache, status } = this.state;
+ const { isCached, status } = this.state;
const { onLoad, src } = this.props;
if (previousState.status !== STATUS.READY && status === STATUS.READY) {
/* istanbul ignore else */
if (onLoad) {
- onLoad(src, hasCache);
+ onLoad(src, isCached);
}
}
@@ -91,6 +94,14 @@ export default class InlineSVG extends React.PureComponent {
this.isActive = false;
}
+ private fetchContent = async () => {
+ const { fetchOptions, src } = this.props;
+
+ const content: string = await request(src, fetchOptions);
+
+ this.handleLoad(content);
+ };
+
private getElement() {
try {
const node = this.getNode() as Node;
@@ -178,7 +189,7 @@ export default class InlineSVG extends React.PureComponent {
this.setState(
{
content,
- hasCache,
+ isCached: hasCache,
status: STATUS.LOADED,
},
this.getElement,
@@ -193,18 +204,11 @@ export default class InlineSVG extends React.PureComponent {
{
content: '',
element: null,
- hasCache: false,
+ isCached: false,
status: STATUS.LOADING,
},
- () => {
- const { cacheRequests, src } = this.props;
- const cache = cacheRequests && cacheStore[src];
-
- if (cache && cache.status === STATUS.LOADED) {
- this.handleLoad(cache.content, true);
-
- return;
- }
+ async () => {
+ const { cacheRequests, fetchOptions, src } = this.props;
const dataURI = src.match(/^data:image\/svg[^,]*?(;base64)?,(.*)/u);
let inlineSrc;
@@ -221,7 +225,17 @@ export default class InlineSVG extends React.PureComponent {
return;
}
- this.request();
+ try {
+ if (cacheRequests) {
+ const content = await cacheStore.get(src, fetchOptions);
+
+ this.handleLoad(content, true);
+ } else {
+ await this.fetchContent();
+ }
+ } catch (error: any) {
+ this.handleError(error);
+ }
},
);
}
@@ -238,65 +252,6 @@ export default class InlineSVG extends React.PureComponent {
return content;
}
- private request = async () => {
- const { cacheRequests, fetchOptions, src } = this.props;
-
- if (cacheRequests) {
- cacheStore[src] = { content: '', status: STATUS.LOADING };
- }
-
- try {
- const response = await fetch(src, fetchOptions);
- const contentType = response.headers.get('content-type');
- const [fileType] = (contentType || '').split(/ ?; ?/);
-
- if (response.status > 299) {
- throw new Error('Not found');
- }
-
- if (!['image/svg+xml', 'text/plain'].some(d => fileType.includes(d))) {
- throw new Error(`Content type isn't valid: ${fileType}`);
- }
-
- const content: string = await response.text();
- const { src: currentSrc } = this.props;
-
- // the current src don't match the previous one, skipping...
- if (src !== currentSrc) {
- if (cacheStore[src].status === STATUS.LOADING) {
- delete cacheStore[src];
- }
-
- return;
- }
-
- this.handleLoad(content);
-
- /* istanbul ignore else */
- if (cacheRequests) {
- const cache = cacheStore[src];
-
- /* istanbul ignore else */
- if (cache) {
- cache.content = content;
- cache.status = STATUS.LOADED;
- }
- }
- } catch (error: any) {
- this.handleError(error);
-
- /* istanbul ignore else */
- if (cacheRequests) {
- const cache = cacheStore[src];
-
- /* istanbul ignore else */
- if (cache) {
- delete cacheStore[src];
- }
- }
- }
- };
-
private updateSVGAttributes(node: SVGSVGElement): SVGSVGElement {
const { baseURL = '', uniquifyIDs } = this.props;
const replaceableAttributes = ['id', 'href', 'xlink:href', 'xlink:role', 'xlink:arcrole'];
@@ -377,4 +332,30 @@ export default class InlineSVG extends React.PureComponent {
}
}
+export default function InlineSVG(props: Props) {
+ if (!cacheStore) {
+ cacheStore = new CacheStore();
+ }
+
+ const { loader } = props;
+ const hasCallback = React.useRef(false);
+ const [isReady, setReady] = React.useState(cacheStore.isReady);
+
+ React.useEffect(() => {
+ if (!hasCallback.current) {
+ cacheStore.onReady(() => {
+ setReady(true);
+ });
+
+ hasCallback.current = true;
+ }
+ }, []);
+
+ if (!isReady) {
+ return loader;
+ }
+
+ return ;
+}
+
export * from './types';
diff --git a/src/provider.tsx b/src/provider.tsx
new file mode 100644
index 0000000..6ddd28a
--- /dev/null
+++ b/src/provider.tsx
@@ -0,0 +1,25 @@
+import { ReactNode, useEffect } from 'react';
+
+interface Props {
+ children: ReactNode;
+}
+
+declare global {
+ interface Window {
+ REACT_INLINESVG_PERSISTENT_CACHE?: boolean;
+ }
+}
+
+export default function CacheProvider({ children }: Props) {
+ window.REACT_INLINESVG_PERSISTENT_CACHE = true;
+
+ useEffect(() => {
+ window.REACT_INLINESVG_PERSISTENT_CACHE = true;
+
+ return () => {
+ delete window.REACT_INLINESVG_PERSISTENT_CACHE;
+ };
+ }, []);
+
+ return children;
+}
diff --git a/src/types.ts b/src/types.ts
index d573cb2..067c94e 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,6 +1,6 @@
import * as React from 'react';
-import { STATUS } from './helpers';
+import { STATUS } from './config';
export type ErrorCallback = (error: Error | FetchError) => void;
export type LoadCallback = (src: string, isCached: boolean) => void;
@@ -27,7 +27,7 @@ export interface Props extends Omit, 'onLoad' | 'onEr
export interface State {
content: string;
element: React.ReactNode;
- hasCache: boolean;
+ isCached: boolean;
status: Status;
}
diff --git a/test/__snapshots__/index.spec.tsx.snap b/test/__snapshots__/index.spec.tsx.snap
index 8879abf..66575bc 100644
--- a/test/__snapshots__/index.spec.tsx.snap
+++ b/test/__snapshots__/index.spec.tsx.snap
@@ -283,7 +283,7 @@ exports[`react-inlinesvg basic functionality should render a base64 src 1`] = `
exports[`react-inlinesvg basic functionality should render a loader 1`] = `
`;
@@ -2422,38 +2422,6 @@ exports[`react-inlinesvg basic functionality should uniquify ids with a custom u
`;
-exports[`react-inlinesvg cached requests should handle cached entries with loading status 1`] = `
-{
- "http://127.0.0.1:1337/react.svg": {
- "content":
-
- React
-
-
-
- ,
- "status": "loaded",
- },
-}
-`;
-
-exports[`react-inlinesvg cached requests should request an SVG only once with cacheRequests prop 1`] = `
-{
- "https://cdn.svgporn.com/logos/react.svg": {
- "content":
-
- React
-
-
-
- ,
- "status": "loaded",
- },
-}
-`;
-
-exports[`react-inlinesvg cached requests should skip the cache if \`cacheRequest\` is false 1`] = `{}`;
-
exports[`react-inlinesvg integration should handle pre-cached entries in the cacheStore 1`] = `
@@ -2461,28 +2429,10 @@ exports[`react-inlinesvg integration should handle pre-cached entries in the cac
`;
exports[`react-inlinesvg integration should handle race condition with fast src changes: cacheStore 1`] = `
-{
- "http://127.0.0.1:1337/react.svg": {
- "content":
-
- React
-
-
-
- ,
- "status": "loaded",
- },
- "https://cdn.svgporn.com/logos/javascript.svg": {
- "content":
-
- React
-
-
-
- ,
- "status": "loaded",
- },
-}
+[
+ "http://127.0.0.1:1337/react.svg",
+ "https://cdn.svgporn.com/logos/javascript.svg",
+]
`;
exports[`react-inlinesvg integration should handle race condition with fast src changes: svg 1`] = `
diff --git a/test/cache.spec.ts b/test/cache.spec.ts
new file mode 100644
index 0000000..504c14a
--- /dev/null
+++ b/test/cache.spec.ts
@@ -0,0 +1,235 @@
+import { waitFor } from '@testing-library/react';
+import CacheMock from 'browser-cache-mock';
+import fetchMock from 'jest-fetch-mock';
+
+import CacheStore from '../src/cache';
+import { STATUS } from '../src/config';
+
+const cacheMock = new CacheMock();
+
+Object.defineProperty(window, 'caches', {
+ value: {
+ ...window.caches,
+ open: async () =>
+ new Promise(resolve => {
+ setTimeout(() => {
+ resolve(cacheMock);
+ }, 500);
+ }),
+ ...cacheMock,
+ },
+});
+
+fetchMock.enableMocks();
+
+const reactUrl = 'https://cdn.svgporn.com/logos/react.svg';
+const reactContent = 'React ';
+const jsUrl = 'https://cdn.svgporn.com/logos/javascript.svg';
+const jsContent = 'JS ';
+
+describe('CacheStore (internal)', () => {
+ const cacheStore = new CacheStore();
+
+ afterEach(async () => {
+ fetchMock.mockClear();
+ await cacheStore.clear();
+ });
+
+ it('should fetch the remote url and add to the cache', async () => {
+ fetchMock.mockResponseOnce(() => Promise.resolve(reactContent));
+
+ await expect(cacheStore.get(reactUrl)).resolves.toBe(reactContent);
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+
+ await expect(cacheStore.get(reactUrl)).resolves.toBe(reactContent);
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ expect(cacheStore.isCached(reactUrl)).toBeTrue();
+
+ await cacheStore.clear();
+ expect(cacheStore.isCached(reactUrl)).toBeFalse();
+ });
+
+ it('should handle multiple simultaneous requests', async () => {
+ fetchMock.mockResponse(() => {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve(reactContent);
+ }, 300);
+ });
+ });
+
+ expect(cacheStore.get(reactUrl)).toEqual(expect.any(Promise));
+
+ await expect(cacheStore.get(reactUrl)).resolves.toBe(reactContent);
+ await expect(cacheStore.get(reactUrl)).resolves.toBe(reactContent);
+
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ expect(cacheStore.isCached(reactUrl)).toBeTrue();
+ });
+
+ it('should handle adding to cache manually', async () => {
+ cacheStore.set(jsUrl, { content: jsContent, status: STATUS.LOADED });
+
+ await expect(cacheStore.get(jsUrl)).resolves.toBe(jsContent);
+ expect(fetchMock).toHaveBeenCalledTimes(0);
+
+ expect(cacheStore.isCached(jsUrl)).toBeTrue();
+ expect(cacheStore.keys()).toEqual([jsUrl]);
+ });
+
+ it(`should handle stalled entries with ${STATUS.LOADING}`, async () => {
+ fetchMock.mockResponseOnce(() => Promise.resolve(jsContent));
+
+ cacheStore.set(jsUrl, { content: jsContent, status: STATUS.LOADING });
+ expect(fetchMock).toHaveBeenCalledTimes(0);
+
+ await expect(cacheStore.get(jsUrl)).resolves.toBe(jsContent);
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('should handle fetch errors', async () => {
+ fetchMock.mockRejectOnce(new Error('Failed to fetch'));
+
+ await expect(cacheStore.get(jsUrl)).rejects.toThrow('Failed to fetch');
+ expect(cacheStore.isCached(jsUrl)).toBeFalse();
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('should return the cached keys', async () => {
+ fetchMock.mockResponseOnce(() => Promise.resolve(reactContent));
+
+ await cacheStore.get(reactUrl);
+
+ expect(cacheStore.keys()).toEqual([reactUrl]);
+ });
+
+ it('should return the cached data', async () => {
+ fetchMock.mockResponseOnce(() => Promise.resolve(reactContent));
+
+ await cacheStore.get(reactUrl);
+
+ expect(cacheStore.data()).toEqual([
+ {
+ [reactUrl]: { content: reactContent, status: STATUS.LOADED },
+ },
+ ]);
+ });
+
+ it('should delete an item from the cache', async () => {
+ await cacheStore.get(reactUrl);
+ expect(cacheStore.keys()).toHaveLength(1);
+
+ await cacheStore.delete(reactUrl);
+ expect(cacheStore.keys()).toHaveLength(0);
+ });
+
+ it('should clear the cache items', async () => {
+ await cacheStore.get(reactUrl);
+ expect(cacheStore.keys()).toHaveLength(1);
+
+ await cacheStore.clear();
+ expect(cacheStore.keys()).toHaveLength(0);
+ });
+});
+
+describe('CacheStore (external)', () => {
+ Object.defineProperty(window, 'REACT_INLINESVG_PERSISTENT_CACHE', {
+ value: true,
+ });
+ const mockReady = jest.fn();
+ const cacheStore = new CacheStore();
+
+ // wait for the cache to be ready
+ cacheStore.onReady(mockReady);
+
+ beforeEach(() => {
+ fetchMock.mockResponse(() => Promise.resolve(reactContent));
+ });
+
+ afterEach(async () => {
+ fetchMock.mockClear();
+ await cacheStore.clear();
+ });
+
+ it('should handle initialization', async () => {
+ await waitFor(() => {
+ expect(mockReady).toHaveBeenCalledTimes(1);
+ });
+
+ cacheStore.onReady(mockReady);
+ expect(mockReady).toHaveBeenCalledTimes(2);
+ });
+
+ it('should fetch the remote url and add to the cache', async () => {
+ await expect(cacheStore.get(reactUrl)).resolves.toBe(reactContent);
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+
+ await expect(cacheStore.get(reactUrl)).resolves.toBe(reactContent);
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+
+ expect(cacheStore.isCached(reactUrl)).toBeTrue();
+ expect(cacheStore.keys()).toEqual([reactUrl]);
+ });
+
+ it('should handle multiple simultaneous requests', async () => {
+ fetchMock.mockResponse(() => {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve(reactContent);
+ }, 300);
+ });
+ });
+
+ expect(cacheStore.get(reactUrl)).toEqual(expect.any(Promise));
+
+ await expect(cacheStore.get(reactUrl)).resolves.toBe(reactContent);
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('should read from the persistent cache if the store is empty', async () => {
+ // add to the persistent cache directly
+ await cacheMock.add(reactUrl);
+ fetchMock.mockClear();
+
+ await expect(cacheStore.get(reactUrl)).resolves.toBe(reactContent);
+ expect(fetchMock).toHaveBeenCalledTimes(0);
+ });
+
+ it('should handle delete', async () => {
+ await cacheStore.get(reactUrl);
+
+ await cacheStore.delete(reactUrl);
+
+ expect(cacheStore.keys()).toHaveLength(0);
+ await expect(cacheMock.keys()).resolves.toHaveLength(0);
+ });
+
+ it('should handle clear', async () => {
+ fetchMock.mockResponseOnce(() => Promise.resolve(reactContent));
+
+ await cacheStore.get(reactUrl);
+
+ await cacheStore.clear();
+
+ expect(cacheStore.keys()).toHaveLength(0);
+ await expect(cacheMock.keys()).resolves.toHaveLength(0);
+ });
+
+ it(`should handle stalled entries with ${STATUS.LOADING}`, async () => {
+ fetchMock.mockResponseOnce(() => Promise.resolve(jsContent));
+
+ cacheStore.set(jsUrl, { content: jsContent, status: STATUS.LOADING });
+ expect(fetchMock).toHaveBeenCalledTimes(0);
+
+ await expect(cacheStore.get(jsUrl)).resolves.toBe(jsContent);
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('should handle fetch errors', async () => {
+ fetchMock.mockRejectOnce(new Error('Failed to fetch'));
+
+ await expect(cacheStore.get(jsUrl)).rejects.toThrow('Failed to fetch');
+ expect(cacheStore.isCached(jsUrl)).toBeFalse();
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/test/index.spec.tsx b/test/index.spec.tsx
index 2067eb9..f5d5249 100644
--- a/test/index.spec.tsx
+++ b/test/index.spec.tsx
@@ -1,12 +1,10 @@
import * as React from 'react';
-import { render, waitFor } from '@testing-library/react';
+import { act, render, waitFor as waitForBase } from '@testing-library/react';
import fetchMock from 'jest-fetch-mock';
import ReactInlineSVG, { cacheStore, Props } from '../src/index';
-function Loader() {
- return
;
-}
+jest.useFakeTimers();
const fixtures = {
circles: 'http://127.0.0.1:1337/circles.svg',
@@ -35,6 +33,21 @@ const fixtures = {
const mockOnError = jest.fn();
const mockOnLoad = jest.fn();
+async function waitFor(callback: () => void) {
+ await waitForBase(callback, {
+ onTimeout: error => {
+ console.log('waitFor timeout', error);
+
+ return error;
+ },
+ timeout: 2000,
+ });
+}
+
+function Loader() {
+ return
;
+}
+
function setup({ onLoad, ...rest }: Props) {
return render(
} onError={mockOnError} onLoad={mockOnLoad} {...rest} />,
@@ -49,9 +62,7 @@ describe('react-inlinesvg', () => {
afterEach(() => {
jest.clearAllMocks();
- Object.keys(cacheStore).forEach(d => {
- delete cacheStore[d];
- });
+ cacheStore.clear();
});
describe('basic functionality', () => {
@@ -396,7 +407,7 @@ describe('react-inlinesvg', () => {
fetchMock.disableMocks();
});
- it('should request an SVG only once with cacheRequests prop', async () => {
+ it('should request an SVG only once', async () => {
fetchMock.mockResponseOnce(
() =>
new Promise(resolve => {
@@ -414,7 +425,7 @@ describe('react-inlinesvg', () => {
setup({ src: fixtures.url });
await waitFor(() => {
- expect(mockOnLoad).toHaveBeenNthCalledWith(1, fixtures.url, false);
+ expect(mockOnLoad).toHaveBeenNthCalledWith(1, fixtures.url, true);
});
setup({ src: fixtures.url });
@@ -425,7 +436,30 @@ describe('react-inlinesvg', () => {
expect(fetchMock).toHaveBeenNthCalledWith(1, fixtures.url, undefined);
- expect(cacheStore).toMatchSnapshot();
+ expect(cacheStore.isCached(fixtures.url)).toBeTrue();
+ });
+
+ it('should handle multiple simultaneous instances with the same url', async () => {
+ fetchMock.mockResponseOnce(() =>
+ Promise.resolve({
+ body: 'React ',
+ headers: { 'Content-Type': 'image/svg+xml' },
+ }),
+ );
+
+ render(
+ <>
+
+
+
+ >,
+ );
+
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+
+ await waitFor(() => {
+ expect(mockOnLoad).toHaveBeenNthCalledWith(3, fixtures.url, true);
+ });
});
it('should handle request fail with multiple instances', async () => {
@@ -456,20 +490,24 @@ describe('react-inlinesvg', () => {
}),
);
- cacheStore[fixtures.react] = {
+ cacheStore.set(fixtures.react, {
content: '',
status: 'loading',
- };
+ });
setup({ src: fixtures.react });
+ await act(async () => {
+ jest.runAllTimers();
+ });
+
await waitFor(() => {
- expect(mockOnLoad).toHaveBeenNthCalledWith(1, fixtures.react, false);
+ expect(mockOnLoad).toHaveBeenNthCalledWith(1, fixtures.react, true);
});
expect(fetchMock).toHaveBeenCalledTimes(1);
- expect(cacheStore).toMatchSnapshot();
+ expect(cacheStore.keys()).toEqual([fixtures.react]);
});
it('should handle cached entries with loading status on error', async () => {
@@ -477,13 +515,17 @@ describe('react-inlinesvg', () => {
fetchMock.mockResponseOnce(() => Promise.reject(error));
- cacheStore[fixtures.react] = {
+ cacheStore.set(fixtures.react, {
content: '',
status: 'loading',
- };
+ });
setup({ src: fixtures.react });
+ await act(async () => {
+ jest.runAllTimers();
+ });
+
await waitFor(() => {
expect(mockOnError).toHaveBeenNthCalledWith(1, error);
});
@@ -510,7 +552,7 @@ describe('react-inlinesvg', () => {
expect(fetchMock.mock.calls).toHaveLength(1);
- expect(cacheStore).toMatchSnapshot();
+ expect(cacheStore.keys()).toHaveLength(0);
});
});
@@ -556,7 +598,7 @@ describe('react-inlinesvg', () => {
});
await waitFor(() => {
- expect(mockOnLoad).toHaveBeenNthCalledWith(1, fixtures.react, false);
+ expect(mockOnLoad).toHaveBeenNthCalledWith(1, fixtures.react, true);
});
rerender(
@@ -574,7 +616,7 @@ describe('react-inlinesvg', () => {
});
await waitFor(() => {
- expect(mockOnLoad).toHaveBeenNthCalledWith(2, fixtures.url2, false);
+ expect(mockOnLoad).toHaveBeenNthCalledWith(2, fixtures.url2, true);
});
rerender(
@@ -594,7 +636,7 @@ describe('react-inlinesvg', () => {
expect(container.querySelector('svg')).toMatchSnapshot('svg');
- expect(cacheStore).toMatchSnapshot('cacheStore');
+ expect(cacheStore.keys()).toMatchSnapshot('cacheStore');
fetchMock.disableMocks();
});
@@ -620,10 +662,10 @@ describe('react-inlinesvg', () => {
it('should handle pre-cached entries in the cacheStore', async () => {
fetchMock.enableMocks();
- cacheStore[fixtures.react] = {
+ cacheStore.set(fixtures.react, {
content: ' ',
status: 'loaded',
- };
+ });
const { container } = render( );
@@ -692,7 +734,7 @@ describe('react-inlinesvg', () => {
});
it('should trigger an error if the request content-type is not valid', async () => {
- await setup({ src: fixtures.react_png });
+ setup({ src: fixtures.react_png });
await waitFor(() => {
expect(mockOnError).toHaveBeenCalledWith(new Error("Content type isn't valid: image/png"));
@@ -700,7 +742,7 @@ describe('react-inlinesvg', () => {
});
it('should trigger an error if the content is not valid', async () => {
- await setup({ src: fixtures.html });
+ setup({ src: fixtures.html });
await waitFor(() => {
expect(mockOnError).toHaveBeenCalledWith(
diff --git a/test/indexWithPersistentCache.spec.tsx b/test/indexWithPersistentCache.spec.tsx
new file mode 100644
index 0000000..43ec8fb
--- /dev/null
+++ b/test/indexWithPersistentCache.spec.tsx
@@ -0,0 +1,94 @@
+/* eslint-disable import/first */
+import * as React from 'react';
+import { render, waitFor } from '@testing-library/react';
+import CacheMock from 'browser-cache-mock';
+import fetchMock from 'jest-fetch-mock';
+
+const cacheMock = new CacheMock();
+
+Object.defineProperty(window, 'caches', {
+ value: {
+ ...window.caches,
+ open: async () =>
+ new Promise(resolve => {
+ setTimeout(() => {
+ resolve(cacheMock);
+ }, 500);
+ }),
+ ...cacheMock,
+ },
+});
+
+import ReactInlineSVG, { cacheStore, Props } from '../src/index';
+import CacheProvider from '../src/provider';
+
+function Loader() {
+ return
;
+}
+
+const mockOnError = jest.fn();
+const mockOnLoad = jest.fn();
+
+const url = 'https://cdn.svgporn.com/logos/react.svg';
+
+fetchMock.enableMocks();
+
+fetchMock.mockResponse(() =>
+ Promise.resolve({
+ body: 'React ',
+ headers: { 'Content-Type': 'image/svg+xml' },
+ }),
+);
+
+function setup({ onLoad, ...rest }: Props) {
+ return render(
+ } onError={mockOnError} onLoad={mockOnLoad} {...rest} />,
+ { wrapper: ({ children }) => {children} },
+ );
+}
+
+describe('react-inlinesvg (with persistent cache)', () => {
+ afterEach(async () => {
+ fetchMock.mockClear();
+ await cacheStore.clear();
+ });
+
+ it('should request an SVG only once', async () => {
+ setup({ src: url });
+
+ await waitFor(() => {
+ expect(mockOnLoad).toHaveBeenNthCalledWith(1, url, true);
+ });
+
+ setup({ src: url });
+
+ await waitFor(() => {
+ expect(mockOnLoad).toHaveBeenNthCalledWith(2, url, true);
+ });
+
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ expect(cacheStore.isCached(url)).toBeTrue();
+ });
+
+ it('should handle multiple simultaneous instances with the same url', async () => {
+ render(
+ <>
+
+
+
+ >,
+ );
+
+ await waitFor(() => {
+ expect(mockOnLoad).toHaveBeenNthCalledWith(3, url, true);
+ });
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+
+ setup({ src: url });
+
+ await waitFor(() => {
+ expect(mockOnLoad).toHaveBeenNthCalledWith(4, url, true);
+ });
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/test/unsupported.spec.tsx b/test/unsupported.spec.tsx
index 25c24da..1ecade9 100644
--- a/test/unsupported.spec.tsx
+++ b/test/unsupported.spec.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { render } from '@testing-library/react';
+import { render, waitFor } from '@testing-library/react';
import InlineSVG, { Props } from '../src';
@@ -50,19 +50,22 @@ describe('unsupported environments', () => {
mockCanUseDOM = true;
mockIsSupportedEnvironment = true;
- const { container } = await setup();
+ const { container } = setup();
+
+ await waitFor(() => {
+ expect(mockOnError).toHaveBeenCalledWith(new Error('fetch is not a function'));
+ });
- expect(mockOnError).toHaveBeenCalledWith(new Error('fetch is not a function'));
expect(container.firstChild).toMatchSnapshot();
window.fetch = globalFetch;
});
- it("shouldn't not render anything if is an unsupported browser", async () => {
+ it("shouldn't not render anything if is an unsupported browser", () => {
mockCanUseDOM = true;
mockIsSupportedEnvironment = false;
- const { container } = await setup();
+ const { container } = setup();
expect(mockOnError).toHaveBeenCalledWith(new Error('Browser does not support SVG'));
expect(container.firstChild).toMatchSnapshot();
diff --git a/tsconfig.json b/tsconfig.json
index a520e6b..423c53f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,8 +2,7 @@
"extends": "@gilbarbara/tsconfig",
"compilerOptions": {
"downlevelIteration": true,
- "lib": ["dom", "dom.iterable", "esnext"],
- "outDir": "./lib",
+ "noEmit": true,
"target": "es5"
},
"include": ["src/**/*"]