react-inlinesvg 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -34
- package/dist/cache-NLB60kAd.d.mts +142 -0
- package/dist/cache-NLB60kAd.d.ts +142 -0
- package/dist/index.d.mts +7 -73
- package/dist/index.d.ts +7 -73
- package/dist/index.js +258 -200
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +254 -204
- package/dist/index.mjs.map +1 -1
- package/dist/provider.d.mts +5 -3
- package/dist/provider.d.ts +5 -3
- package/dist/provider.js +187 -6
- package/dist/provider.js.map +1 -1
- package/dist/provider.mjs +176 -6
- package/dist/provider.mjs.map +1 -1
- package/package.json +25 -41
- package/src/index.tsx +29 -284
- package/src/modules/cache.ts +79 -59
- package/src/modules/helpers.ts +1 -9
- package/src/modules/hooks.tsx +6 -1
- package/src/modules/useInlineSVG.ts +272 -0
- package/src/modules/utils.ts +36 -1
- package/src/provider.tsx +10 -7
- package/src/types.ts +67 -1
- package/src/global.d.ts +0 -6
package/dist/provider.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/modules/helpers.ts","../src/provider.tsx"],"sourcesContent":["import type { PlainObject } from '../types';\n\nfunction randomCharacter(character: string) {\n return character[Math.floor(Math.random() * character.length)];\n}\n\nexport function canUseDOM(): boolean {\n return !!(typeof window !== 'undefined' && window.document?.createElement);\n}\n\nexport function isSupportedEnvironment(): boolean {\n return supportsInlineSVG() && typeof window !== 'undefined' && window !== null;\n}\n\n/**\n * Remove properties from an object\n */\nexport function omit<T extends PlainObject, K extends keyof T>(\n input: T,\n ...filter: K[]\n): Omit<T, K> {\n const output: any = {};\n\n for (const key in input) {\n if ({}.hasOwnProperty.call(input, key)) {\n if (!filter.includes(key as unknown as K)) {\n output[key] = input[key];\n }\n }\n }\n\n return output as Omit<T, K>;\n}\n\nexport function randomString(length: number): string {\n const letters = 'abcdefghijklmnopqrstuvwxyz';\n const numbers = '1234567890';\n const charset = `${letters}${letters.toUpperCase()}${numbers}`;\n\n let R = '';\n\n for (let index = 0; index < length; index++) {\n R += randomCharacter(charset);\n }\n\n return R;\n}\n\nexport async function request(url: string, options?: RequestInit) {\n const response = await fetch(url, options);\n const contentType = response.headers.get('content-type');\n const [fileType] = (contentType ?? '').split(/ ?; ?/);\n\n if (response.status > 299) {\n throw new Error('Not found');\n }\n\n if (!['image/svg+xml', 'text/plain'].some(d => fileType.includes(d))) {\n throw new Error(`Content type isn't valid: ${fileType}`);\n }\n\n return response.text();\n}\n\nexport function sleep(seconds = 1) {\n return new Promise(resolve => {\n setTimeout(resolve, seconds * 1000);\n });\n}\n\nexport function supportsInlineSVG(): boolean {\n /* c8 ignore next 3 */\n if (!document) {\n return false;\n }\n\n const div = document.createElement('div');\n\n div.innerHTML = '<svg />';\n const svg = div.firstChild as SVGSVGElement;\n\n return !!svg && svg.namespaceURI === 'http://www.w3.org/2000/svg';\n}\n","import { ReactNode } from 'react';\n\nimport { canUseDOM } from './modules/helpers';\n\ninterface Props {\n children: ReactNode;\n name?: string;\n}\n\nexport default function CacheProvider({ children, name }: Props) {\n if (canUseDOM()) {\n window.REACT_INLINESVG_CACHE_NAME = name;\n window.REACT_INLINESVG_PERSISTENT_CACHE = true;\n }\n\n return children;\n}\n"],"mappings":";;;AAMO,SAAS,YAAqB;AACnC,SAAO,CAAC,EAAE,OAAO,WAAW,eAAe,OAAO,UAAU;AAC9D;;;ACCe,SAAR,cAA+B,EAAE,UAAU,KAAK,GAAU;AAC/D,MAAI,UAAU,GAAG;AACf,WAAO,6BAA6B;AACpC,WAAO,mCAAmC;AAAA,EAC5C;AAEA,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/provider.tsx","../src/config.ts","../src/modules/helpers.ts","../src/modules/cache.ts"],"sourcesContent":["import React, { createContext, ReactNode, useContext, useState } from 'react';\n\nimport CacheStore from './modules/cache';\n\nconst CacheContext = createContext<CacheStore | null>(null);\n\ninterface Props {\n children: ReactNode;\n name?: string;\n}\n\nexport default function CacheProvider({ children, name }: Props) {\n const [store] = useState(() => new CacheStore({ name, persistent: true }));\n\n return <CacheContext.Provider value={store}>{children}</CacheContext.Provider>;\n}\n\nexport function useCacheStore(): CacheStore | null {\n return useContext(CacheContext);\n}\n","export const CACHE_NAME = 'react-inlinesvg';\nexport const CACHE_MAX_RETRIES = 10;\n\nexport const STATUS = {\n IDLE: 'idle',\n LOADING: 'loading',\n LOADED: 'loaded',\n FAILED: 'failed',\n READY: 'ready',\n UNSUPPORTED: 'unsupported',\n} as const;\n","function randomCharacter(character: string) {\n return character[Math.floor(Math.random() * character.length)];\n}\n\nexport function canUseDOM(): boolean {\n return !!(typeof window !== 'undefined' && window.document?.createElement);\n}\n\nexport function isSupportedEnvironment(): boolean {\n return supportsInlineSVG() && typeof window !== 'undefined' && window !== null;\n}\n\n/**\n * Remove properties from an object\n */\nexport function omit<T extends Record<string, unknown>, K extends keyof T>(\n input: T,\n ...filter: K[]\n): Omit<T, K> {\n const output: any = {};\n\n for (const key in input) {\n if ({}.hasOwnProperty.call(input, key)) {\n if (!filter.includes(key as unknown as K)) {\n output[key] = input[key];\n }\n }\n }\n\n return output as Omit<T, K>;\n}\n\nexport function randomString(length: number): string {\n const letters = 'abcdefghijklmnopqrstuvwxyz';\n const numbers = '1234567890';\n const charset = `${letters}${letters.toUpperCase()}${numbers}`;\n\n let R = '';\n\n for (let index = 0; index < length; index++) {\n R += randomCharacter(charset);\n }\n\n return R;\n}\n\nexport async function request(url: string, options?: RequestInit) {\n const response = await fetch(url, options);\n const contentType = response.headers.get('content-type');\n const [fileType] = (contentType ?? '').split(/ ?; ?/);\n\n if (response.status > 299) {\n throw new Error('Not found');\n }\n\n if (!['image/svg+xml', 'text/plain'].some(d => fileType.includes(d))) {\n throw new Error(`Content type isn't valid: ${fileType}`);\n }\n\n return response.text();\n}\n\nexport function supportsInlineSVG(): boolean {\n /* c8 ignore next 3 */\n if (!document) {\n return false;\n }\n\n const div = document.createElement('div');\n\n div.innerHTML = '<svg />';\n const svg = div.firstChild as SVGSVGElement;\n\n return !!svg && svg.namespaceURI === 'http://www.w3.org/2000/svg';\n}\n","import { CACHE_MAX_RETRIES, CACHE_NAME, STATUS } from '../config';\nimport { StorageItem } from '../types';\n\nimport { canUseDOM, request } from './helpers';\n\nexport interface CacheStoreOptions {\n name?: string;\n persistent?: boolean;\n}\n\nexport default class CacheStore {\n private cacheApi: Cache | undefined;\n private readonly cacheStore: Map<string, StorageItem>;\n private readonly subscribers: Array<() => void> = [];\n public isReady = false;\n\n constructor(options: CacheStoreOptions = {}) {\n const { name = CACHE_NAME, persistent = false } = options;\n\n this.cacheStore = new Map<string, StorageItem>();\n\n const usePersistentCache = persistent && canUseDOM() && 'caches' in window;\n\n if (usePersistentCache) {\n // eslint-disable-next-line promise/catch-or-return\n caches\n .open(name)\n .then(cache => {\n this.cacheApi = cache;\n })\n .catch(error => {\n // eslint-disable-next-line no-console\n console.error(`Failed to open cache: ${error.message}`);\n this.cacheApi = undefined;\n })\n .finally(() => {\n this.isReady = true;\n // Copy to avoid mutation issues\n const callbacks = [...this.subscribers];\n\n // Clear array efficiently\n this.subscribers.length = 0;\n\n callbacks.forEach(callback => {\n try {\n callback();\n } catch (error: any) {\n // eslint-disable-next-line no-console\n console.error(`Error in CacheStore subscriber callback: ${error.message}`);\n }\n });\n });\n } else {\n this.isReady = true;\n }\n }\n\n public onReady(callback: () => void): () => void {\n if (this.isReady) {\n callback();\n\n return () => {};\n }\n\n this.subscribers.push(callback);\n\n return () => {\n const index = this.subscribers.indexOf(callback);\n\n if (index >= 0) {\n this.subscribers.splice(index, 1);\n }\n };\n }\n\n private waitForReady(): Promise<void> {\n if (this.isReady) {\n return Promise.resolve();\n }\n\n return new Promise(resolve => {\n this.onReady(resolve);\n });\n }\n\n public async get(url: string, fetchOptions?: RequestInit) {\n await this.fetchAndCache(url, fetchOptions);\n\n return this.cacheStore.get(url)?.content ?? '';\n }\n\n public getContent(url: string): string {\n return this.cacheStore.get(url)?.content ?? '';\n }\n\n public set(url: string, data: StorageItem) {\n this.cacheStore.set(url, data);\n }\n\n public isCached(url: string) {\n return this.cacheStore.get(url)?.status === STATUS.LOADED;\n }\n\n private async fetchAndCache(url: string, fetchOptions?: RequestInit) {\n if (!this.isReady) {\n await this.waitForReady();\n }\n\n const cache = this.cacheStore.get(url);\n\n if (cache?.status === STATUS.LOADED) {\n return;\n }\n\n if (cache?.status === STATUS.LOADING) {\n await this.handleLoading(url, fetchOptions?.signal || undefined, async () => {\n this.cacheStore.set(url, { content: '', status: STATUS.IDLE });\n await this.fetchAndCache(url, fetchOptions);\n });\n\n return;\n }\n\n this.cacheStore.set(url, { content: '', status: STATUS.LOADING });\n\n try {\n const content = this.cacheApi\n ? await this.fetchFromPersistentCache(url, fetchOptions)\n : await request(url, fetchOptions);\n\n this.cacheStore.set(url, { content, status: STATUS.LOADED });\n } catch (error: any) {\n this.cacheStore.set(url, { content: '', status: STATUS.FAILED });\n throw error;\n }\n }\n\n private async fetchFromPersistentCache(url: string, fetchOptions?: RequestInit): Promise<string> {\n const data = await this.cacheApi?.match(url);\n\n if (data) {\n return data.text();\n }\n\n await this.cacheApi?.add(new Request(url, fetchOptions));\n\n const response = await this.cacheApi?.match(url);\n\n return (await response?.text()) ?? '';\n }\n\n private async handleLoading(\n url: string,\n signal: AbortSignal | undefined,\n callback: () => Promise<void>,\n ) {\n for (let retryCount = 0; retryCount < CACHE_MAX_RETRIES; retryCount++) {\n if (signal?.aborted) {\n throw signal.reason instanceof Error\n ? signal.reason\n : new DOMException('The operation was aborted.', 'AbortError');\n }\n\n if (this.cacheStore.get(url)?.status !== STATUS.LOADING) {\n return;\n }\n\n await sleep(0.1);\n }\n\n await callback();\n }\n\n public keys(): Array<string> {\n return [...this.cacheStore.keys()];\n }\n\n public data(): Array<Record<string, StorageItem>> {\n return [...this.cacheStore.entries()].map(([key, value]) => ({ [key]: value }));\n }\n\n public async delete(url: string) {\n if (this.cacheApi) {\n await this.cacheApi.delete(url);\n }\n\n this.cacheStore.delete(url);\n }\n\n public async clear() {\n if (this.cacheApi) {\n const keys = await this.cacheApi.keys();\n\n await Promise.allSettled(keys.map(key => this.cacheApi!.delete(key)));\n }\n\n this.cacheStore.clear();\n }\n}\n\nfunction sleep(seconds = 1) {\n return new Promise(resolve => {\n setTimeout(resolve, seconds * 1000);\n });\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-inlinesvg",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "An SVG loader for React",
|
|
5
5
|
"author": "Gil Barbara <gilbarbara@gmail.com>",
|
|
6
6
|
"contributors": [
|
|
@@ -54,35 +54,37 @@
|
|
|
54
54
|
"react-from-dom": "^0.7.5"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@arethetypeswrong/cli": "^0.
|
|
58
|
-
"@gilbarbara/eslint-config": "^
|
|
57
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
58
|
+
"@gilbarbara/eslint-config": "^1.1.0",
|
|
59
59
|
"@gilbarbara/prettier-config": "^1.0.0",
|
|
60
|
-
"@gilbarbara/tsconfig": "^0.
|
|
61
|
-
"@size-limit/preset-small-lib": "^
|
|
62
|
-
"@testing-library/jest-dom": "^6.
|
|
63
|
-
"@testing-library/react": "^16.2
|
|
64
|
-
"@types/node": "^
|
|
65
|
-
"@types/react": "^
|
|
66
|
-
"@types/react-dom": "^
|
|
67
|
-
"@vitejs/plugin-react": "^
|
|
68
|
-
"@vitest/coverage-v8": "^
|
|
60
|
+
"@gilbarbara/tsconfig": "^1.0.0",
|
|
61
|
+
"@size-limit/preset-small-lib": "^12.0.1",
|
|
62
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
63
|
+
"@testing-library/react": "^16.3.2",
|
|
64
|
+
"@types/node": "^24.12.0",
|
|
65
|
+
"@types/react": "^19.2.14",
|
|
66
|
+
"@types/react-dom": "^19.2.3",
|
|
67
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
68
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
69
69
|
"browser-cache-mock": "^0.1.7",
|
|
70
|
-
"del-cli": "^
|
|
70
|
+
"del-cli": "^7.0.0",
|
|
71
|
+
"eslint": "^9.39.4",
|
|
71
72
|
"fix-tsup-cjs": "^1.2.0",
|
|
72
73
|
"http-server": "^14.1.1",
|
|
73
74
|
"husky": "^9.1.7",
|
|
74
|
-
"jest-extended": "^
|
|
75
|
-
"jsdom": "^
|
|
76
|
-
"
|
|
77
|
-
"react
|
|
75
|
+
"jest-extended": "^7.0.0",
|
|
76
|
+
"jsdom": "^29.0.1",
|
|
77
|
+
"prettier": "^3.8.1",
|
|
78
|
+
"react": "^19.2.4",
|
|
79
|
+
"react-dom": "^19.2.4",
|
|
78
80
|
"repo-tools": "^0.3.1",
|
|
79
|
-
"size-limit": "^
|
|
80
|
-
"start-server-and-test": "^
|
|
81
|
+
"size-limit": "^12.0.1",
|
|
82
|
+
"start-server-and-test": "^3.0.0",
|
|
81
83
|
"ts-node": "^10.9.2",
|
|
82
|
-
"tsup": "^8.
|
|
83
|
-
"typescript": "^5.
|
|
84
|
-
"vitest": "^
|
|
85
|
-
"vitest-fetch-mock": "^0.4.
|
|
84
|
+
"tsup": "^8.5.1",
|
|
85
|
+
"typescript": "^5.9.3",
|
|
86
|
+
"vitest": "^4.1.2",
|
|
87
|
+
"vitest-fetch-mock": "^0.4.5"
|
|
86
88
|
},
|
|
87
89
|
"scripts": {
|
|
88
90
|
"build": "pnpm run clean && tsup && fix-tsup-cjs",
|
|
@@ -117,24 +119,6 @@
|
|
|
117
119
|
"sourcemap": true,
|
|
118
120
|
"splitting": false
|
|
119
121
|
},
|
|
120
|
-
"eslintConfig": {
|
|
121
|
-
"extends": [
|
|
122
|
-
"@gilbarbara/eslint-config"
|
|
123
|
-
],
|
|
124
|
-
"overrides": [
|
|
125
|
-
{
|
|
126
|
-
"files": [
|
|
127
|
-
"test/**/*.ts?(x)"
|
|
128
|
-
],
|
|
129
|
-
"rules": {
|
|
130
|
-
"@typescript-eslint/ban-ts-comment": "off",
|
|
131
|
-
"no-console": "off",
|
|
132
|
-
"testing-library/no-container": "off",
|
|
133
|
-
"testing-library/no-node-access": "off"
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
]
|
|
137
|
-
},
|
|
138
122
|
"prettier": "@gilbarbara/prettier-config",
|
|
139
123
|
"size-limit": [
|
|
140
124
|
{
|
package/src/index.tsx
CHANGED
|
@@ -1,275 +1,45 @@
|
|
|
1
|
-
import
|
|
2
|
-
cloneElement,
|
|
3
|
-
isValidElement,
|
|
4
|
-
ReactElement,
|
|
5
|
-
useCallback,
|
|
6
|
-
useEffect,
|
|
7
|
-
useReducer,
|
|
8
|
-
useRef,
|
|
9
|
-
useState,
|
|
10
|
-
} from 'react';
|
|
11
|
-
import convert from 'react-from-dom';
|
|
1
|
+
import { cloneElement, ReactElement, SVGProps } from 'react';
|
|
12
2
|
|
|
13
3
|
import { STATUS } from './config';
|
|
14
4
|
import CacheStore from './modules/cache';
|
|
15
|
-
import { canUseDOM,
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
5
|
+
import { canUseDOM, omit } from './modules/helpers';
|
|
6
|
+
import useInlineSVG from './modules/useInlineSVG';
|
|
7
|
+
import { useCacheStore } from './provider';
|
|
8
|
+
import { Props, Status } from './types';
|
|
19
9
|
|
|
20
|
-
|
|
21
|
-
export let cacheStore: CacheStore;
|
|
10
|
+
export const cacheStore = new CacheStore();
|
|
22
11
|
|
|
23
|
-
function
|
|
24
|
-
const {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
description,
|
|
28
|
-
fetchOptions,
|
|
29
|
-
innerRef,
|
|
30
|
-
loader = null,
|
|
31
|
-
onError,
|
|
32
|
-
onLoad,
|
|
33
|
-
src,
|
|
34
|
-
title,
|
|
35
|
-
uniqueHash,
|
|
36
|
-
} = props;
|
|
37
|
-
const [state, setState] = useReducer(
|
|
38
|
-
(previousState: State, nextState: Partial<State>) => ({
|
|
39
|
-
...previousState,
|
|
40
|
-
...nextState,
|
|
41
|
-
}),
|
|
42
|
-
{
|
|
43
|
-
content: '',
|
|
44
|
-
element: null,
|
|
45
|
-
|
|
46
|
-
isCached: cacheRequests && cacheStore.isCached(props.src),
|
|
47
|
-
status: STATUS.IDLE,
|
|
48
|
-
},
|
|
49
|
-
);
|
|
50
|
-
const { content, element, isCached, status } = state;
|
|
51
|
-
const previousProps = usePrevious(props);
|
|
52
|
-
const previousState = usePrevious(state);
|
|
53
|
-
|
|
54
|
-
const hash = useRef(uniqueHash ?? randomString(8));
|
|
55
|
-
const isActive = useRef(false);
|
|
56
|
-
const isInitialized = useRef(false);
|
|
57
|
-
|
|
58
|
-
const handleError = useCallback(
|
|
59
|
-
(error: Error | FetchError) => {
|
|
60
|
-
if (isActive.current) {
|
|
61
|
-
setState({
|
|
62
|
-
status:
|
|
63
|
-
error.message === 'Browser does not support SVG' ? STATUS.UNSUPPORTED : STATUS.FAILED,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
onError?.(error);
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
[onError],
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const handleLoad = useCallback((loadedContent: string, hasCache = false) => {
|
|
73
|
-
if (isActive.current) {
|
|
74
|
-
setState({
|
|
75
|
-
content: loadedContent,
|
|
76
|
-
isCached: hasCache,
|
|
77
|
-
status: STATUS.LOADED,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}, []);
|
|
81
|
-
|
|
82
|
-
const fetchContent = useCallback(async () => {
|
|
83
|
-
const responseContent: string = await request(src, fetchOptions);
|
|
84
|
-
|
|
85
|
-
handleLoad(responseContent);
|
|
86
|
-
}, [fetchOptions, handleLoad, src]);
|
|
87
|
-
|
|
88
|
-
const getElement = useCallback(() => {
|
|
89
|
-
try {
|
|
90
|
-
const node = getNode({ ...props, handleError, hash: hash.current, content }) as Node;
|
|
91
|
-
const convertedElement = convert(node);
|
|
92
|
-
|
|
93
|
-
if (!convertedElement || !isValidElement(convertedElement)) {
|
|
94
|
-
throw new Error('Could not convert the src to a React element');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
setState({
|
|
98
|
-
element: convertedElement,
|
|
99
|
-
status: STATUS.READY,
|
|
100
|
-
});
|
|
101
|
-
} catch (error: any) {
|
|
102
|
-
handleError(error);
|
|
103
|
-
}
|
|
104
|
-
}, [content, handleError, props]);
|
|
105
|
-
|
|
106
|
-
const getContent = useCallback(async () => {
|
|
107
|
-
const dataURI = /^data:image\/svg[^,]*?(;base64)?,(.*)/u.exec(src);
|
|
108
|
-
let inlineSrc;
|
|
109
|
-
|
|
110
|
-
if (dataURI) {
|
|
111
|
-
inlineSrc = dataURI[1] ? window.atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
|
|
112
|
-
} else if (src.includes('<svg')) {
|
|
113
|
-
inlineSrc = src;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (inlineSrc) {
|
|
117
|
-
handleLoad(inlineSrc);
|
|
118
|
-
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
if (cacheRequests) {
|
|
124
|
-
const cachedContent = await cacheStore.get(src, fetchOptions);
|
|
125
|
-
|
|
126
|
-
handleLoad(cachedContent, true);
|
|
127
|
-
} else {
|
|
128
|
-
await fetchContent();
|
|
129
|
-
}
|
|
130
|
-
} catch (error: any) {
|
|
131
|
-
handleError(error);
|
|
132
|
-
}
|
|
133
|
-
}, [cacheRequests, fetchContent, fetchOptions, handleError, handleLoad, src]);
|
|
134
|
-
|
|
135
|
-
const load = useCallback(async () => {
|
|
136
|
-
if (isActive.current) {
|
|
137
|
-
setState({
|
|
138
|
-
content: '',
|
|
139
|
-
element: null,
|
|
140
|
-
isCached: false,
|
|
141
|
-
status: STATUS.LOADING,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}, []);
|
|
145
|
-
|
|
146
|
-
// Run on mount
|
|
147
|
-
useEffect(
|
|
148
|
-
() => {
|
|
149
|
-
isActive.current = true;
|
|
150
|
-
|
|
151
|
-
if (!canUseDOM() || isInitialized.current) {
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
if (status === STATUS.IDLE) {
|
|
157
|
-
if (!isSupportedEnvironment()) {
|
|
158
|
-
throw new Error('Browser does not support SVG');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (!src) {
|
|
162
|
-
throw new Error('Missing src');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
load();
|
|
166
|
-
}
|
|
167
|
-
} catch (error: any) {
|
|
168
|
-
handleError(error);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
isInitialized.current = true;
|
|
172
|
-
|
|
173
|
-
return () => {
|
|
174
|
-
isActive.current = false;
|
|
175
|
-
};
|
|
176
|
-
},
|
|
177
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
178
|
-
[],
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
// Handles `src` changes
|
|
182
|
-
useEffect(() => {
|
|
183
|
-
if (!canUseDOM() || !previousProps) {
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (previousProps.src !== src) {
|
|
188
|
-
if (!src) {
|
|
189
|
-
handleError(new Error('Missing src'));
|
|
190
|
-
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
load();
|
|
195
|
-
}
|
|
196
|
-
}, [handleError, load, previousProps, src]);
|
|
197
|
-
|
|
198
|
-
// Handles content loading
|
|
199
|
-
useEffect(() => {
|
|
200
|
-
if (status === STATUS.LOADED) {
|
|
201
|
-
getElement();
|
|
202
|
-
}
|
|
203
|
-
}, [status, getElement]);
|
|
204
|
-
|
|
205
|
-
// Handles `title` and `description` changes
|
|
206
|
-
useEffect(() => {
|
|
207
|
-
if (!canUseDOM() || !previousProps || previousProps.src !== src) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (previousProps.title !== title || previousProps.description !== description) {
|
|
212
|
-
getElement();
|
|
213
|
-
}
|
|
214
|
-
}, [description, getElement, previousProps, src, title]);
|
|
215
|
-
|
|
216
|
-
// handle state
|
|
217
|
-
useEffect(() => {
|
|
218
|
-
if (!previousState) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
switch (status) {
|
|
223
|
-
case STATUS.LOADING: {
|
|
224
|
-
if (previousState.status !== STATUS.LOADING) {
|
|
225
|
-
getContent();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
case STATUS.LOADED: {
|
|
231
|
-
if (previousState.status !== STATUS.LOADED) {
|
|
232
|
-
getElement();
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
case STATUS.READY: {
|
|
238
|
-
if (previousState.status !== STATUS.READY) {
|
|
239
|
-
onLoad?.(src, isCached);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}, [getContent, getElement, isCached, onLoad, previousState, src, status]);
|
|
12
|
+
export default function InlineSVG(props: Props) {
|
|
13
|
+
const { children = null, innerRef, loader = null } = props;
|
|
14
|
+
const contextStore = useCacheStore();
|
|
15
|
+
const store = contextStore ?? cacheStore;
|
|
246
16
|
|
|
247
|
-
const
|
|
248
|
-
props,
|
|
249
|
-
'baseURL',
|
|
250
|
-
'cacheRequests',
|
|
251
|
-
'children',
|
|
252
|
-
'description',
|
|
253
|
-
'fetchOptions',
|
|
254
|
-
'innerRef',
|
|
255
|
-
'loader',
|
|
256
|
-
'onError',
|
|
257
|
-
'onLoad',
|
|
258
|
-
'preProcessor',
|
|
259
|
-
'src',
|
|
260
|
-
'title',
|
|
261
|
-
'uniqueHash',
|
|
262
|
-
'uniquifyIDs',
|
|
263
|
-
);
|
|
17
|
+
const { element, status } = useInlineSVG(props, store);
|
|
264
18
|
|
|
265
19
|
if (!canUseDOM()) {
|
|
266
20
|
return loader;
|
|
267
21
|
}
|
|
268
22
|
|
|
269
23
|
if (element) {
|
|
270
|
-
return cloneElement(element as ReactElement
|
|
24
|
+
return cloneElement(element as ReactElement<SVGProps<SVGElement>>, {
|
|
271
25
|
ref: innerRef,
|
|
272
|
-
...
|
|
26
|
+
...omit(
|
|
27
|
+
props,
|
|
28
|
+
'baseURL',
|
|
29
|
+
'cacheRequests',
|
|
30
|
+
'children',
|
|
31
|
+
'description',
|
|
32
|
+
'fetchOptions',
|
|
33
|
+
'innerRef',
|
|
34
|
+
'loader',
|
|
35
|
+
'onError',
|
|
36
|
+
'onLoad',
|
|
37
|
+
'preProcessor',
|
|
38
|
+
'src',
|
|
39
|
+
'title',
|
|
40
|
+
'uniqueHash',
|
|
41
|
+
'uniquifyIDs',
|
|
42
|
+
),
|
|
273
43
|
});
|
|
274
44
|
}
|
|
275
45
|
|
|
@@ -280,29 +50,4 @@ function ReactInlineSVG(props: Props) {
|
|
|
280
50
|
return loader;
|
|
281
51
|
}
|
|
282
52
|
|
|
283
|
-
export default function InlineSVG(props: Props) {
|
|
284
|
-
if (!cacheStore) {
|
|
285
|
-
cacheStore = new CacheStore();
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const { loader } = props;
|
|
289
|
-
const [isReady, setReady] = useState(cacheStore.isReady);
|
|
290
|
-
|
|
291
|
-
useEffect(() => {
|
|
292
|
-
if (isReady) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
cacheStore.onReady(() => {
|
|
297
|
-
setReady(true);
|
|
298
|
-
});
|
|
299
|
-
}, [isReady]);
|
|
300
|
-
|
|
301
|
-
if (!isReady) {
|
|
302
|
-
return loader;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return <ReactInlineSVG {...props} />;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
53
|
export * from './types';
|
package/src/modules/cache.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { CACHE_MAX_RETRIES, CACHE_NAME, STATUS } from '../config';
|
|
2
2
|
import { StorageItem } from '../types';
|
|
3
3
|
|
|
4
|
-
import { canUseDOM, request
|
|
4
|
+
import { canUseDOM, request } from './helpers';
|
|
5
|
+
|
|
6
|
+
export interface CacheStoreOptions {
|
|
7
|
+
name?: string;
|
|
8
|
+
persistent?: boolean;
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
export default class CacheStore {
|
|
7
12
|
private cacheApi: Cache | undefined;
|
|
@@ -9,20 +14,17 @@ export default class CacheStore {
|
|
|
9
14
|
private readonly subscribers: Array<() => void> = [];
|
|
10
15
|
public isReady = false;
|
|
11
16
|
|
|
12
|
-
constructor() {
|
|
17
|
+
constructor(options: CacheStoreOptions = {}) {
|
|
18
|
+
const { name = CACHE_NAME, persistent = false } = options;
|
|
19
|
+
|
|
13
20
|
this.cacheStore = new Map<string, StorageItem>();
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
let usePersistentCache = false;
|
|
17
|
-
|
|
18
|
-
if (canUseDOM()) {
|
|
19
|
-
cacheName = window.REACT_INLINESVG_CACHE_NAME ?? CACHE_NAME;
|
|
20
|
-
usePersistentCache = !!window.REACT_INLINESVG_PERSISTENT_CACHE && 'caches' in window;
|
|
21
|
-
}
|
|
22
|
+
const usePersistentCache = persistent && canUseDOM() && 'caches' in window;
|
|
22
23
|
|
|
23
24
|
if (usePersistentCache) {
|
|
25
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
24
26
|
caches
|
|
25
|
-
.open(
|
|
27
|
+
.open(name)
|
|
26
28
|
.then(cache => {
|
|
27
29
|
this.cacheApi = cache;
|
|
28
30
|
})
|
|
@@ -53,22 +55,44 @@ export default class CacheStore {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
public onReady(callback: () => void) {
|
|
58
|
+
public onReady(callback: () => void): () => void {
|
|
57
59
|
if (this.isReady) {
|
|
58
60
|
callback();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
|
|
62
|
+
return () => {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.subscribers.push(callback);
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
const index = this.subscribers.indexOf(callback);
|
|
69
|
+
|
|
70
|
+
if (index >= 0) {
|
|
71
|
+
this.subscribers.splice(index, 1);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private waitForReady(): Promise<void> {
|
|
77
|
+
if (this.isReady) {
|
|
78
|
+
return Promise.resolve();
|
|
61
79
|
}
|
|
80
|
+
|
|
81
|
+
return new Promise(resolve => {
|
|
82
|
+
this.onReady(resolve);
|
|
83
|
+
});
|
|
62
84
|
}
|
|
63
85
|
|
|
64
86
|
public async get(url: string, fetchOptions?: RequestInit) {
|
|
65
|
-
await
|
|
66
|
-
? this.fetchAndAddToPersistentCache(url, fetchOptions)
|
|
67
|
-
: this.fetchAndAddToInternalCache(url, fetchOptions));
|
|
87
|
+
await this.fetchAndCache(url, fetchOptions);
|
|
68
88
|
|
|
69
89
|
return this.cacheStore.get(url)?.content ?? '';
|
|
70
90
|
}
|
|
71
91
|
|
|
92
|
+
public getContent(url: string): string {
|
|
93
|
+
return this.cacheStore.get(url)?.content ?? '';
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
public set(url: string, data: StorageItem) {
|
|
73
97
|
this.cacheStore.set(url, data);
|
|
74
98
|
}
|
|
@@ -77,33 +101,11 @@ export default class CacheStore {
|
|
|
77
101
|
return this.cacheStore.get(url)?.status === STATUS.LOADED;
|
|
78
102
|
}
|
|
79
103
|
|
|
80
|
-
private async
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (cache?.status === STATUS.LOADING) {
|
|
84
|
-
await this.handleLoading(url, async () => {
|
|
85
|
-
this.cacheStore.set(url, { content: '', status: STATUS.IDLE });
|
|
86
|
-
await this.fetchAndAddToInternalCache(url, fetchOptions);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
return;
|
|
104
|
+
private async fetchAndCache(url: string, fetchOptions?: RequestInit) {
|
|
105
|
+
if (!this.isReady) {
|
|
106
|
+
await this.waitForReady();
|
|
90
107
|
}
|
|
91
108
|
|
|
92
|
-
if (!cache?.content) {
|
|
93
|
-
this.cacheStore.set(url, { content: '', status: STATUS.LOADING });
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const content = await request(url, fetchOptions);
|
|
97
|
-
|
|
98
|
-
this.cacheStore.set(url, { content, status: STATUS.LOADED });
|
|
99
|
-
} catch (error: any) {
|
|
100
|
-
this.cacheStore.set(url, { content: '', status: STATUS.FAILED });
|
|
101
|
-
throw error;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private async fetchAndAddToPersistentCache(url: string, fetchOptions?: RequestInit) {
|
|
107
109
|
const cache = this.cacheStore.get(url);
|
|
108
110
|
|
|
109
111
|
if (cache?.status === STATUS.LOADED) {
|
|
@@ -111,9 +113,9 @@ export default class CacheStore {
|
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
if (cache?.status === STATUS.LOADING) {
|
|
114
|
-
await this.handleLoading(url, async () => {
|
|
116
|
+
await this.handleLoading(url, fetchOptions?.signal || undefined, async () => {
|
|
115
117
|
this.cacheStore.set(url, { content: '', status: STATUS.IDLE });
|
|
116
|
-
await this.
|
|
118
|
+
await this.fetchAndCache(url, fetchOptions);
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
return;
|
|
@@ -121,21 +123,10 @@ export default class CacheStore {
|
|
|
121
123
|
|
|
122
124
|
this.cacheStore.set(url, { content: '', status: STATUS.LOADING });
|
|
123
125
|
|
|
124
|
-
const data = await this.cacheApi?.match(url);
|
|
125
|
-
|
|
126
|
-
if (data) {
|
|
127
|
-
const content = await data.text();
|
|
128
|
-
|
|
129
|
-
this.cacheStore.set(url, { content, status: STATUS.LOADED });
|
|
130
|
-
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
126
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const content = (await response?.text()) ?? '';
|
|
127
|
+
const content = this.cacheApi
|
|
128
|
+
? await this.fetchFromPersistentCache(url, fetchOptions)
|
|
129
|
+
: await request(url, fetchOptions);
|
|
139
130
|
|
|
140
131
|
this.cacheStore.set(url, { content, status: STATUS.LOADED });
|
|
141
132
|
} catch (error: any) {
|
|
@@ -144,13 +135,36 @@ export default class CacheStore {
|
|
|
144
135
|
}
|
|
145
136
|
}
|
|
146
137
|
|
|
147
|
-
private async
|
|
138
|
+
private async fetchFromPersistentCache(url: string, fetchOptions?: RequestInit): Promise<string> {
|
|
139
|
+
const data = await this.cacheApi?.match(url);
|
|
140
|
+
|
|
141
|
+
if (data) {
|
|
142
|
+
return data.text();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await this.cacheApi?.add(new Request(url, fetchOptions));
|
|
146
|
+
|
|
147
|
+
const response = await this.cacheApi?.match(url);
|
|
148
|
+
|
|
149
|
+
return (await response?.text()) ?? '';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async handleLoading(
|
|
153
|
+
url: string,
|
|
154
|
+
signal: AbortSignal | undefined,
|
|
155
|
+
callback: () => Promise<void>,
|
|
156
|
+
) {
|
|
148
157
|
for (let retryCount = 0; retryCount < CACHE_MAX_RETRIES; retryCount++) {
|
|
158
|
+
if (signal?.aborted) {
|
|
159
|
+
throw signal.reason instanceof Error
|
|
160
|
+
? signal.reason
|
|
161
|
+
: new DOMException('The operation was aborted.', 'AbortError');
|
|
162
|
+
}
|
|
163
|
+
|
|
149
164
|
if (this.cacheStore.get(url)?.status !== STATUS.LOADING) {
|
|
150
165
|
return;
|
|
151
166
|
}
|
|
152
167
|
|
|
153
|
-
// eslint-disable-next-line no-await-in-loop
|
|
154
168
|
await sleep(0.1);
|
|
155
169
|
}
|
|
156
170
|
|
|
@@ -183,3 +197,9 @@ export default class CacheStore {
|
|
|
183
197
|
this.cacheStore.clear();
|
|
184
198
|
}
|
|
185
199
|
}
|
|
200
|
+
|
|
201
|
+
function sleep(seconds = 1) {
|
|
202
|
+
return new Promise(resolve => {
|
|
203
|
+
setTimeout(resolve, seconds * 1000);
|
|
204
|
+
});
|
|
205
|
+
}
|