react-inlinesvg 2.2.1 → 3.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-inlinesvg",
3
- "version": "2.2.1",
3
+ "version": "3.0.0",
4
4
  "description": "An SVG loader for React",
5
5
  "author": "Gil Barbara <gilbarbara@gmail.com>",
6
6
  "contributors": [
@@ -30,57 +30,47 @@
30
30
  "src"
31
31
  ],
32
32
  "types": "esm",
33
- "sideEffects": true,
33
+ "sideEffects": false,
34
34
  "peerDependencies": {
35
- "react": "^16.8.0 || ^17.0.0"
35
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
36
36
  },
37
37
  "dependencies": {
38
38
  "exenv": "^1.2.2",
39
- "react-from-dom": "^0.5.0"
39
+ "react-from-dom": "^0.6.2"
40
40
  },
41
41
  "devDependencies": {
42
+ "@gilbarbara/eslint-config": "^0.2.1",
43
+ "@gilbarbara/helpers": "^0.5.1",
44
+ "@gilbarbara/prettier-config": "^0.1.0",
42
45
  "@gilbarbara/tsconfig": "^0.1.0",
43
- "@size-limit/preset-small-lib": "^4.9.1",
44
- "@types/enzyme": "^3.10.8",
45
- "@types/enzyme-adapter-react-16": "^1.0.6",
46
+ "@size-limit/preset-small-lib": "^7.0.8",
47
+ "@testing-library/jest-dom": "^5.16.4",
48
+ "@testing-library/react": "^13.0.1",
46
49
  "@types/exenv": "^1.2.0",
47
- "@types/fetch-mock": "^7.3.3",
48
- "@types/jest": "^26.0.18",
49
- "@types/node": "^14.14.12",
50
- "@types/node-fetch": "^2.5.7",
51
- "@types/react": "^16.14.2",
52
- "@types/react-dom": "^16.9.10",
53
- "@typescript-eslint/eslint-plugin": "^4.9.1",
54
- "@typescript-eslint/parser": "^4.9.1",
50
+ "@types/fetch-mock": "^7.3.5",
51
+ "@types/jest": "^27.4.1",
52
+ "@types/node": "^17.0.24",
53
+ "@types/node-fetch": "^2.6.1",
54
+ "@types/react": "^18.0.5",
55
+ "@types/react-dom": "^18.0.1",
56
+ "cross-fetch": "^3.1.5",
55
57
  "del-cli": "^3.0.1",
56
- "enzyme": "^3.11.0",
57
- "enzyme-adapter-react-16": "^1.15.5",
58
- "eslint": "^7.15.0",
59
- "eslint-config-airbnb": "^18.2.1",
60
- "eslint-config-prettier": "^7.0.0",
61
- "eslint-plugin-import": "^2.22.1",
62
- "eslint-plugin-jsx-a11y": "^6.4.1",
63
- "eslint-plugin-prettier": "^3.2.0",
64
- "eslint-plugin-react": "^7.21.5",
65
- "eslint-plugin-react-hooks": "^4.2.0",
66
- "http-server": "^0.12.3",
67
- "husky": "^4.3.5",
68
- "jest": "^26.6.3",
58
+ "http-server": "^14.1.0",
59
+ "husky": "^7.0.4",
60
+ "jest": "^27.5.1",
69
61
  "jest-chain": "^1.1.5",
70
- "jest-enzyme": "^7.1.2",
71
- "jest-extended": "^0.11.5",
62
+ "jest-extended": "^2.0.0",
72
63
  "jest-fetch-mock": "^3.0.3",
73
- "jest-serializer-html": "^7.0.0",
74
- "jest-watch-typeahead": "^0.6.1",
75
- "node-fetch": "^2.6.1",
76
- "prettier": "^2.2.1",
77
- "react": "^16.14.0",
78
- "react-dom": "^16.14.0",
79
- "repo-tools": "^0.2.0",
80
- "size-limit": "^4.9.1",
81
- "start-server-and-test": "^1.11.6",
82
- "ts-jest": "^26.4.4",
83
- "typescript": "^4.1.2"
64
+ "jest-serializer-html": "^7.1.0",
65
+ "jest-watch-typeahead": "^1.0.0",
66
+ "react": "^18.0.0",
67
+ "react-dom": "^18.0.0",
68
+ "repo-tools": "^0.2.2",
69
+ "size-limit": "^7.0.8",
70
+ "start-server-and-test": "^1.14.0",
71
+ "ts-jest": "^27.1.4",
72
+ "ts-node": "^10.7.0",
73
+ "typescript": "^4.6.3"
84
74
  },
85
75
  "scripts": {
86
76
  "build": "npm run clean && npm run build:cjs && npm run build:esm",
@@ -95,30 +85,40 @@
95
85
  "test:watch": "jest --watchAll --verbose",
96
86
  "lint": "eslint --ext .ts,.tsx src test",
97
87
  "format": "prettier \"**/*.{js,jsx,json,yml,yaml,css,less,scss,ts,tsx,md,graphql,mdx}\" --write",
98
- "validate": "npm run lint && npm run test && npm run build && npm run size",
88
+ "validate": "npm run lint && npm run test && npm run build && npm run size",
99
89
  "size": "size-limit",
100
- "prepublishOnly": "npm run validate"
90
+ "prepublishOnly": "npm run validate",
91
+ "prepare": "husky install"
101
92
  },
102
- "prettier": {
103
- "jsxBracketSameLine": false,
104
- "printWidth": 100,
105
- "singleQuote": true,
106
- "trailingComma": "all"
93
+ "eslintConfig": {
94
+ "extends": [
95
+ "@gilbarbara/eslint-config"
96
+ ],
97
+ "overrides": [
98
+ {
99
+ "files": [
100
+ "test/**/*.ts?(x)"
101
+ ],
102
+ "rules": {
103
+ "@typescript-eslint/ban-ts-comment": "off",
104
+ "no-console": "off",
105
+ "testing-library/no-container": "off",
106
+ "testing-library/no-node-access": "off"
107
+ }
108
+ }
109
+ ]
107
110
  },
111
+ "prettier": "@gilbarbara/prettier-config",
108
112
  "size-limit": [
109
113
  {
114
+ "name": "lib",
110
115
  "path": "./lib/index.js",
111
- "limit": "8 kB"
116
+ "limit": "9 kB"
112
117
  },
113
118
  {
119
+ "name": "esm",
114
120
  "path": "./esm/index.js",
115
- "limit": "8 kB"
121
+ "limit": "9 kB"
116
122
  }
117
- ],
118
- "husky": {
119
- "hooks": {
120
- "post-merge": "repo-tools install-packages",
121
- "pre-commit": "repo-tools check-remote && npm run validate"
122
- }
123
- }
123
+ ]
124
124
  }
package/src/helpers.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { canUseDOM as canUseDOMFlag } from 'exenv';
2
2
 
3
+ import { PlainObject } from './types';
4
+
3
5
  export const STATUS = {
4
6
  FAILED: 'failed',
5
7
  LOADED: 'loaded',
@@ -13,6 +15,10 @@ export function canUseDOM(): boolean {
13
15
  return canUseDOMFlag;
14
16
  }
15
17
 
18
+ export function isSupportedEnvironment(): boolean {
19
+ return supportsInlineSVG() && typeof window !== 'undefined' && window !== null;
20
+ }
21
+
16
22
  export function supportsInlineSVG(): boolean {
17
23
  /* istanbul ignore next */
18
24
  if (!document) {
@@ -20,12 +26,15 @@ export function supportsInlineSVG(): boolean {
20
26
  }
21
27
 
22
28
  const div = document.createElement('div');
29
+
23
30
  div.innerHTML = '<svg />';
24
- return !!div.firstChild && div.firstChild.namespaceURI === 'http://www.w3.org/2000/svg';
31
+ const svg = div.firstChild as SVGSVGElement;
32
+
33
+ return !!svg && svg.namespaceURI === 'http://www.w3.org/2000/svg';
25
34
  }
26
35
 
27
- export function isSupportedEnvironment(): boolean {
28
- return supportsInlineSVG() && typeof window !== 'undefined' && window !== null;
36
+ function randomCharacter(character: string) {
37
+ return character[Math.floor(Math.random() * character.length)];
29
38
  }
30
39
 
31
40
  export function randomString(length: number): string {
@@ -33,13 +42,32 @@ export function randomString(length: number): string {
33
42
  const numbers = '1234567890';
34
43
  const charset = `${letters}${letters.toUpperCase()}${numbers}`;
35
44
 
36
- const randomCharacter = (character: string) =>
37
- character[Math.floor(Math.random() * character.length)];
38
-
39
45
  let R = '';
40
- for (let i = 0; i < length; i++) {
46
+
47
+ for (let index = 0; index < length; index++) {
41
48
  R += randomCharacter(charset);
42
49
  }
43
50
 
44
51
  return R;
45
52
  }
53
+
54
+ /**
55
+ * Remove properties from an object
56
+ */
57
+ export function omit<T extends PlainObject, K extends keyof T>(
58
+ input: T,
59
+ ...filter: K[]
60
+ ): Omit<T, K> {
61
+ const output: any = {};
62
+
63
+ for (const key in input) {
64
+ /* istanbul ignore else */
65
+ if ({}.hasOwnProperty.call(input, key)) {
66
+ if (!filter.includes(key as unknown as K)) {
67
+ output[key] = input[key];
68
+ }
69
+ }
70
+ }
71
+
72
+ return output as Omit<T, K>;
73
+ }
package/src/index.tsx CHANGED
@@ -1,13 +1,20 @@
1
1
  import * as React from 'react';
2
-
3
2
  import convert from 'react-from-dom';
4
3
 
5
- import { STATUS, canUseDOM, isSupportedEnvironment, randomString } from './helpers';
4
+ import { canUseDOM, isSupportedEnvironment, omit, randomString, STATUS } from './helpers';
6
5
  import { FetchError, Props, State, StorageItem } from './types';
7
6
 
8
- const cacheStore: { [key: string]: StorageItem } = Object.create(null);
7
+ export const cacheStore: { [key: string]: StorageItem } = Object.create(null);
9
8
 
10
9
  export default class InlineSVG extends React.PureComponent<Props, State> {
10
+ private isActive = false;
11
+ private readonly hash: string;
12
+
13
+ public static defaultProps = {
14
+ cacheRequests: true,
15
+ uniquifyIDs: false,
16
+ };
17
+
11
18
  constructor(props: Props) {
12
19
  super(props);
13
20
 
@@ -21,14 +28,6 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
21
28
  this.hash = props.uniqueHash || randomString(8);
22
29
  }
23
30
 
24
- private isActive = false;
25
- private readonly hash: string;
26
-
27
- public static defaultProps = {
28
- cacheRequests: true,
29
- uniquifyIDs: false,
30
- };
31
-
32
31
  public componentDidMount(): void {
33
32
  this.isActive = true;
34
33
 
@@ -54,12 +53,12 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
54
53
 
55
54
  this.load();
56
55
  }
57
- } catch (error) {
56
+ } catch (error: any) {
58
57
  this.handleError(error);
59
58
  }
60
59
  }
61
60
 
62
- public componentDidUpdate(prevProps: Props, prevState: State): void {
61
+ public componentDidUpdate(previousProps: Props, previousState: State): void {
63
62
  if (!canUseDOM()) {
64
63
  return;
65
64
  }
@@ -67,16 +66,17 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
67
66
  const { hasCache, status } = this.state;
68
67
  const { onLoad, src } = this.props;
69
68
 
70
- if (prevState.status !== STATUS.READY && status === STATUS.READY) {
69
+ if (previousState.status !== STATUS.READY && status === STATUS.READY) {
71
70
  /* istanbul ignore else */
72
71
  if (onLoad) {
73
72
  onLoad(src, hasCache);
74
73
  }
75
74
  }
76
75
 
77
- if (prevProps.src !== src) {
76
+ if (previousProps.src !== src) {
78
77
  if (!src) {
79
78
  this.handleError(new Error('Missing src'));
79
+
80
80
  return;
81
81
  }
82
82
 
@@ -88,60 +88,6 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
88
88
  this.isActive = false;
89
89
  }
90
90
 
91
- private processSVG() {
92
- const { content } = this.state;
93
- const { preProcessor } = this.props;
94
-
95
- if (preProcessor) {
96
- return preProcessor(content);
97
- }
98
-
99
- return content;
100
- }
101
-
102
- private updateSVGAttributes(node: SVGSVGElement): SVGSVGElement {
103
- const { baseURL = '', uniquifyIDs } = this.props;
104
- const replaceableAttributes = ['id', 'href', 'xlink:href', 'xlink:role', 'xlink:arcrole'];
105
- const linkAttributes = ['href', 'xlink:href'];
106
- const isDataValue = (name: string, value: string) =>
107
- linkAttributes.indexOf(name) >= 0 && (value ? value.indexOf('#') < 0 : false);
108
-
109
- if (!uniquifyIDs) {
110
- return node;
111
- }
112
-
113
- [...node.children].map((d) => {
114
- if (d.attributes && d.attributes.length) {
115
- const attributes = Object.values(d.attributes).map((a) => {
116
- const attr = a;
117
- const match = a.value.match(/url\((.*?)\)/);
118
-
119
- if (match && match[1]) {
120
- attr.value = a.value.replace(match[0], `url(${baseURL}${match[1]}__${this.hash})`);
121
- }
122
-
123
- return attr;
124
- });
125
-
126
- replaceableAttributes.forEach((r) => {
127
- const attribute = attributes.find((a) => a.name === r);
128
-
129
- if (attribute && !isDataValue(r, attribute.value)) {
130
- attribute.value = `${attribute.value}__${this.hash}`;
131
- }
132
- });
133
- }
134
-
135
- if (d.children.length) {
136
- return this.updateSVGAttributes(d as SVGSVGElement);
137
- }
138
-
139
- return d;
140
- });
141
-
142
- return node;
143
- }
144
-
145
91
  private getNode() {
146
92
  const { description, title } = this.props;
147
93
 
@@ -163,6 +109,7 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
163
109
  }
164
110
 
165
111
  const descElement = document.createElement('desc');
112
+
166
113
  descElement.innerHTML = description;
167
114
  svg.prepend(descElement);
168
115
  }
@@ -175,12 +122,13 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
175
122
  }
176
123
 
177
124
  const titleElement = document.createElement('title');
125
+
178
126
  titleElement.innerHTML = title;
179
127
  svg.prepend(titleElement);
180
128
  }
181
129
 
182
130
  return svg;
183
- } catch (error) {
131
+ } catch (error: any) {
184
132
  return this.handleError(error);
185
133
  }
186
134
  }
@@ -198,64 +146,11 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
198
146
  element,
199
147
  status: STATUS.READY,
200
148
  });
201
- } catch (error) {
149
+ } catch (error: any) {
202
150
  this.handleError(new Error(error.message));
203
151
  }
204
152
  }
205
153
 
206
- private load() {
207
- /* istanbul ignore else */
208
- if (this.isActive) {
209
- this.setState(
210
- {
211
- content: '',
212
- element: null,
213
- status: STATUS.LOADING,
214
- },
215
- () => {
216
- const { cacheRequests, src } = this.props;
217
- const cache = cacheRequests && cacheStore[src];
218
-
219
- if (cache) {
220
- /* istanbul ignore else */
221
- if (cache.status === STATUS.LOADING) {
222
- cache.queue.push(this.handleCacheQueue);
223
- } else if (cache.status === STATUS.LOADED) {
224
- this.handleLoad(cache.content);
225
- }
226
- return;
227
- }
228
-
229
- const dataURI = src.match(/data:image\/svg[^,]*?(;base64)?,(.*)/);
230
- let inlineSrc;
231
-
232
- if (dataURI) {
233
- inlineSrc = dataURI[1] ? atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
234
- } else if (src.indexOf('<svg') >= 0) {
235
- inlineSrc = src;
236
- }
237
-
238
- if (inlineSrc) {
239
- this.handleLoad(inlineSrc);
240
- return;
241
- }
242
-
243
- this.request();
244
- },
245
- );
246
- }
247
- }
248
-
249
- private handleCacheQueue = (content: string | Error) => {
250
- /* istanbul ignore else */
251
- if (typeof content === 'string') {
252
- this.handleLoad(content);
253
- return;
254
- }
255
-
256
- this.handleError(content);
257
- };
258
-
259
154
  private handleLoad = (content: string) => {
260
155
  /* istanbul ignore else */
261
156
  if (this.isActive) {
@@ -286,15 +181,15 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
286
181
  };
287
182
 
288
183
  private request = () => {
289
- const { cacheRequests, src } = this.props;
184
+ const { cacheRequests, fetchOptions, src } = this.props;
290
185
 
291
186
  try {
292
187
  if (cacheRequests) {
293
- cacheStore[src] = { content: '', status: STATUS.LOADING, queue: [] };
188
+ cacheStore[src] = { content: '', status: STATUS.LOADING };
294
189
  }
295
190
 
296
- return fetch(src)
297
- .then((response) => {
191
+ return fetch(src, fetchOptions)
192
+ .then(response => {
298
193
  const contentType = response.headers.get('content-type');
299
194
  const [fileType] = (contentType || '').split(/ ?; ?/);
300
195
 
@@ -302,13 +197,24 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
302
197
  throw new Error('Not found');
303
198
  }
304
199
 
305
- if (!['image/svg+xml', 'text/plain'].some((d) => fileType.indexOf(d) >= 0)) {
200
+ if (!['image/svg+xml', 'text/plain'].some(d => fileType.includes(d))) {
306
201
  throw new Error(`Content type isn't valid: ${fileType}`);
307
202
  }
308
203
 
309
204
  return response.text();
310
205
  })
311
- .then((content) => {
206
+ .then(content => {
207
+ const { src: currentSrc } = this.props;
208
+
209
+ // the current src don't match the previous one, skipping...
210
+ if (src !== currentSrc) {
211
+ if (cacheStore[src].status === STATUS.LOADING) {
212
+ delete cacheStore[src];
213
+ }
214
+
215
+ return;
216
+ }
217
+
312
218
  this.handleLoad(content);
313
219
 
314
220
  /* istanbul ignore else */
@@ -319,16 +225,10 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
319
225
  if (cache) {
320
226
  cache.content = content;
321
227
  cache.status = STATUS.LOADED;
322
-
323
- cache.queue = cache.queue.filter((cb) => {
324
- cb(content);
325
-
326
- return false;
327
- });
328
228
  }
329
229
  }
330
230
  })
331
- .catch((error) => {
231
+ .catch(error => {
332
232
  this.handleError(error);
333
233
 
334
234
  /* istanbul ignore else */
@@ -337,47 +237,139 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
337
237
 
338
238
  /* istanbul ignore else */
339
239
  if (cache) {
340
- cache.queue.forEach((cb: (content: string) => void) => {
341
- cb(error);
342
- });
343
-
344
240
  delete cacheStore[src];
345
241
  }
346
242
  }
347
243
  });
348
- } catch (error) {
244
+ } catch (error: any) {
349
245
  return this.handleError(new Error(error.message));
350
246
  }
351
247
  };
352
248
 
249
+ private load() {
250
+ /* istanbul ignore else */
251
+ if (this.isActive) {
252
+ this.setState(
253
+ {
254
+ content: '',
255
+ element: null,
256
+ status: STATUS.LOADING,
257
+ },
258
+ () => {
259
+ const { cacheRequests, src } = this.props;
260
+ const cache = cacheRequests && cacheStore[src];
261
+
262
+ if (cache && cache.status === STATUS.LOADED) {
263
+ this.handleLoad(cache.content);
264
+
265
+ return;
266
+ }
267
+
268
+ const dataURI = src.match(/data:image\/svg[^,]*?(;base64)?,(.*)/);
269
+ let inlineSrc;
270
+
271
+ if (dataURI) {
272
+ inlineSrc = dataURI[1] ? window.atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
273
+ } else if (src.includes('<svg')) {
274
+ inlineSrc = src;
275
+ }
276
+
277
+ if (inlineSrc) {
278
+ this.handleLoad(inlineSrc);
279
+
280
+ return;
281
+ }
282
+
283
+ this.request();
284
+ },
285
+ );
286
+ }
287
+ }
288
+
289
+ private updateSVGAttributes(node: SVGSVGElement): SVGSVGElement {
290
+ const { baseURL = '', uniquifyIDs } = this.props;
291
+ const replaceableAttributes = ['id', 'href', 'xlink:href', 'xlink:role', 'xlink:arcrole'];
292
+ const linkAttributes = ['href', 'xlink:href'];
293
+ const isDataValue = (name: string, value: string) =>
294
+ linkAttributes.includes(name) && (value ? !value.includes('#') : false);
295
+
296
+ if (!uniquifyIDs) {
297
+ return node;
298
+ }
299
+
300
+ [...node.children].map(d => {
301
+ if (d.attributes && d.attributes.length) {
302
+ const attributes = Object.values(d.attributes).map(a => {
303
+ const attribute = a;
304
+ const match = a.value.match(/url\((.*?)\)/);
305
+
306
+ if (match && match[1]) {
307
+ attribute.value = a.value.replace(match[0], `url(${baseURL}${match[1]}__${this.hash})`);
308
+ }
309
+
310
+ return attribute;
311
+ });
312
+
313
+ replaceableAttributes.forEach(r => {
314
+ const attribute = attributes.find(a => a.name === r);
315
+
316
+ if (attribute && !isDataValue(r, attribute.value)) {
317
+ attribute.value = `${attribute.value}__${this.hash}`;
318
+ }
319
+ });
320
+ }
321
+
322
+ if (d.children.length) {
323
+ return this.updateSVGAttributes(d as SVGSVGElement);
324
+ }
325
+
326
+ return d;
327
+ });
328
+
329
+ return node;
330
+ }
331
+
332
+ private processSVG() {
333
+ const { content } = this.state;
334
+ const { preProcessor } = this.props;
335
+
336
+ if (preProcessor) {
337
+ return preProcessor(content);
338
+ }
339
+
340
+ return content;
341
+ }
342
+
353
343
  public render(): React.ReactNode {
354
344
  const { element, status } = this.state;
355
- const {
356
- baseURL,
357
- cacheRequests,
358
- children = null,
359
- description,
360
- innerRef,
361
- loader = null,
362
- onError,
363
- onLoad,
364
- preProcessor,
365
- src,
366
- title,
367
- uniqueHash,
368
- uniquifyIDs,
369
- ...rest
370
- } = this.props;
345
+ const { children = null, innerRef, loader = null } = this.props;
346
+ const elementProps = omit(
347
+ this.props,
348
+ 'baseURL',
349
+ 'cacheRequests',
350
+ 'children',
351
+ 'description',
352
+ 'fetchOptions',
353
+ 'innerRef',
354
+ 'loader',
355
+ 'onError',
356
+ 'onLoad',
357
+ 'preProcessor',
358
+ 'src',
359
+ 'title',
360
+ 'uniqueHash',
361
+ 'uniquifyIDs',
362
+ );
371
363
 
372
364
  if (!canUseDOM()) {
373
365
  return loader;
374
366
  }
375
367
 
376
368
  if (element) {
377
- return React.cloneElement(element as React.ReactElement, { ref: innerRef, ...rest });
369
+ return React.cloneElement(element as React.ReactElement, { ref: innerRef, ...elementProps });
378
370
  }
379
371
 
380
- if ([STATUS.UNSUPPORTED, STATUS.FAILED].indexOf(status) > -1) {
372
+ if ([STATUS.UNSUPPORTED, STATUS.FAILED].includes(status)) {
381
373
  return children;
382
374
  }
383
375