@acusti/styling 0.6.0 → 0.7.1

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/dist/Style.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- /// <reference types="react" />
1
+ import * as React from 'react';
2
2
  type Props = {
3
3
  children: string;
4
4
  };
5
- declare const Style: ({ children }: Props) => JSX.Element | null;
5
+ declare const Style: ({ children }: Props) => React.JSX.Element | null;
6
6
  export default Style;
package/dist/Style.js CHANGED
@@ -1,10 +1,15 @@
1
1
  import * as React from 'react';
2
- import { unregisterStyles, updateStyles } from './style-registry.js';
2
+ import { getRegisteredStyles, registerStyles, unregisterStyles, updateStyles, } from './style-registry.js';
3
3
  const { useCallback, useEffect, useMemo, useRef, useState } = React;
4
4
  const Style = ({ children }) => {
5
5
  // Minify CSS styles by replacing consecutive whitespace (including \n) with ' '
6
6
  const styles = useMemo(() => children.replace(/\s+/gm, ' '), [children]);
7
7
  const [ownerDocument, setOwnerDocument] = useState(null);
8
+ const isMountedRef = useRef(false);
9
+ useEffect(() => {
10
+ isMountedRef.current = true;
11
+ unregisterStyles({ ownerDocument: 'global', styles });
12
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
8
13
  useEffect(() => () => {
9
14
  if (!ownerDocument)
10
15
  return;
@@ -28,9 +33,13 @@ const Style = ({ children }) => {
28
33
  }, []);
29
34
  if (ownerDocument)
30
35
  return null;
31
- return (React.createElement("style", { dangerouslySetInnerHTML: {
32
- __html: styles,
33
- }, ref: handleRef }));
36
+ // Avoid duplicate style rendering during SSR via style registry
37
+ if (!isMountedRef.current) {
38
+ if (getRegisteredStyles({ ownerDocument: 'global', styles }))
39
+ return null;
40
+ registerStyles({ ownerDocument: 'global', styles });
41
+ }
42
+ return React.createElement("style", { dangerouslySetInnerHTML: { __html: styles }, ref: handleRef });
34
43
  };
35
44
  export default Style;
36
45
  //# sourceMappingURL=Style.js.map
@@ -5,8 +5,9 @@
5
5
  * @flow
6
6
  */
7
7
 
8
+ import * as React from "react";
8
9
  declare type Props = {|
9
10
  children: string,
10
11
  |};
11
- declare var Style: (x: Props) => React$Node | null;
12
+ declare var Style: (x: Props) => React.JSX.Element | null;
12
13
  declare export default typeof Style;
package/dist/Style.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Style.js","sourceRoot":"","sources":["../src/Style.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAErE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;AAMpE,MAAM,KAAK,GAAG,CAAC,EAAE,QAAQ,EAAS,EAAE,EAAE;IAClC,gFAAgF;IAChF,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzE,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAkB,IAAI,CAAC,CAAC;IAE1E,SAAS,CACL,GAAG,EAAE,CAAC,GAAG,EAAE;QACP,IAAI,CAAC,aAAa;YAAE,OAAO;QAC3B,gBAAgB,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,CAAC;IAChD,CAAC,EACD,CAAC,aAAa,CAAC,CAClB,CAAC;IAEF,MAAM,iBAAiB,GAAG,MAAM,CAAS,EAAE,CAAC,CAAC;IAE7C,SAAS,CAAC,GAAG,EAAE;QACX,IAAI,CAAC,aAAa;YAAE,OAAO;QAE3B,YAAY,CAAC;YACT,aAAa;YACb,cAAc,EAAE,iBAAiB,CAAC,OAAO;YACzC,MAAM;SACT,CAAC,CAAC;QAEH,iBAAiB,CAAC,OAAO,GAAG,MAAM,CAAC;IACvC,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;IAE5B,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,OAA2B,EAAE,EAAE;QAC1D,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,gBAAgB,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5C,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,IAAI,aAAa;QAAE,OAAO,IAAI,CAAC;IAE/B,OAAO,CACH,+BACI,uBAAuB,EAAE;YACrB,MAAM,EAAE,MAAM;SACjB,EACD,GAAG,EAAE,SAAS,GAChB,CACL,CAAC;AACN,CAAC,CAAC;AAEF,eAAe,KAAK,CAAC"}
1
+ {"version":3,"file":"Style.js","sourceRoot":"","sources":["../src/Style.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EACH,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,YAAY,GACf,MAAM,qBAAqB,CAAC;AAE7B,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;AAMpE,MAAM,KAAK,GAAG,CAAC,EAAE,QAAQ,EAAS,EAAE,EAAE;IAClC,gFAAgF;IAChF,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzE,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAkB,IAAI,CAAC,CAAC;IAC1E,MAAM,YAAY,GAAG,MAAM,CAAU,KAAK,CAAC,CAAC;IAE5C,SAAS,CAAC,GAAG,EAAE;QACX,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;QAC5B,gBAAgB,CAAC,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1D,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,kDAAkD;IAE1D,SAAS,CACL,GAAG,EAAE,CAAC,GAAG,EAAE;QACP,IAAI,CAAC,aAAa;YAAE,OAAO;QAC3B,gBAAgB,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,CAAC;IAChD,CAAC,EACD,CAAC,aAAa,CAAC,CAClB,CAAC;IAEF,MAAM,iBAAiB,GAAG,MAAM,CAAS,EAAE,CAAC,CAAC;IAE7C,SAAS,CAAC,GAAG,EAAE;QACX,IAAI,CAAC,aAAa;YAAE,OAAO;QAE3B,YAAY,CAAC;YACT,aAAa;YACb,cAAc,EAAE,iBAAiB,CAAC,OAAO;YACzC,MAAM;SACT,CAAC,CAAC;QAEH,iBAAiB,CAAC,OAAO,GAAG,MAAM,CAAC;IACvC,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;IAE5B,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,OAA2B,EAAE,EAAE;QAC1D,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,gBAAgB,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5C,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,IAAI,aAAa;QAAE,OAAO,IAAI,CAAC;IAE/B,gEAAgE;IAChE,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;QACxB,IAAI,mBAAmB,CAAC,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1E,cAAc,CAAC,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,OAAO,+BAAO,uBAAuB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,SAAS,GAAI,CAAC;AAClF,CAAC,CAAC;AAEF,eAAe,KAAK,CAAC"}
@@ -1,7 +1,11 @@
1
1
  type Payload = {
2
- ownerDocument: Document;
2
+ ownerDocument: Document | 'global';
3
3
  styles: string;
4
4
  };
5
+ export declare const getRegisteredStyles: ({ ownerDocument, styles }: Payload) => {
6
+ element: HTMLStyleElement | null;
7
+ referenceCount: number;
8
+ } | null;
5
9
  export declare const registerStyles: ({ ownerDocument, styles }: Payload) => void;
6
10
  export declare const unregisterStyles: ({ ownerDocument, styles }: Payload) => void;
7
11
  type UpdatePayload = {
@@ -10,4 +14,6 @@ type UpdatePayload = {
10
14
  styles: string;
11
15
  };
12
16
  export declare const updateStyles: ({ ownerDocument, previousStyles, styles, }: UpdatePayload) => void;
17
+ export declare const getStyleRegistryKeys: () => IterableIterator<string>;
18
+ export declare const clearRegistry: () => void;
13
19
  export {};
@@ -1,18 +1,41 @@
1
1
  const styleRegistry = new Map();
2
+ export const getRegisteredStyles = ({ ownerDocument, styles }) => {
3
+ var _a;
4
+ if (!styles)
5
+ return null;
6
+ const stylesMap = styleRegistry.get(styles);
7
+ if (!stylesMap)
8
+ return null;
9
+ return (_a = stylesMap.get(ownerDocument)) !== null && _a !== void 0 ? _a : null;
10
+ };
11
+ // NOTE a more idiomatic API than (register|unregister)Styles would be
12
+ // to make registerStyles a thunk that returns a cleanup function
2
13
  export const registerStyles = ({ ownerDocument, styles }) => {
3
14
  if (!styles)
4
15
  return;
5
- const stylesMap = styleRegistry.get(styles);
6
- const existingStylesItem = stylesMap === null || stylesMap === void 0 ? void 0 : stylesMap.get(ownerDocument);
16
+ const existingStylesItem = getRegisteredStyles({ ownerDocument, styles });
7
17
  if (existingStylesItem) {
8
18
  existingStylesItem.referenceCount++;
9
19
  return;
10
20
  }
21
+ if (ownerDocument === 'global') {
22
+ const stylesItem = { element: null, referenceCount: 1 };
23
+ let stylesMap = styleRegistry.get(styles);
24
+ if (stylesMap) {
25
+ stylesMap.set(ownerDocument, stylesItem);
26
+ }
27
+ else {
28
+ stylesMap = new Map([[ownerDocument, stylesItem]]);
29
+ }
30
+ styleRegistry.set(styles, stylesMap);
31
+ return;
32
+ }
11
33
  const element = ownerDocument.createElement('style');
12
34
  element.setAttribute('data-ukt-styling', '');
13
35
  element.innerHTML = styles;
14
36
  ownerDocument.head.appendChild(element);
15
37
  const stylesItem = { element, referenceCount: 1 };
38
+ const stylesMap = styleRegistry.get(styles);
16
39
  if (stylesMap) {
17
40
  stylesMap.set(ownerDocument, stylesItem);
18
41
  return;
@@ -22,19 +45,21 @@ export const registerStyles = ({ ownerDocument, styles }) => {
22
45
  export const unregisterStyles = ({ ownerDocument, styles }) => {
23
46
  if (!styles)
24
47
  return;
25
- const stylesMap = styleRegistry.get(styles);
26
- const stylesItem = stylesMap === null || stylesMap === void 0 ? void 0 : stylesMap.get(ownerDocument);
27
- if (!stylesMap || !stylesItem)
48
+ const stylesItem = getRegisteredStyles({ ownerDocument, styles });
49
+ if (!stylesItem)
28
50
  return;
29
51
  stylesItem.referenceCount--;
30
52
  if (stylesItem.referenceCount)
31
53
  return;
32
54
  // If no more references to these styles in this document, remove <style> element from the DOM
33
- const { parentElement } = stylesItem.element;
34
- if (parentElement) {
35
- parentElement.removeChild(stylesItem.element);
55
+ if (stylesItem.element) {
56
+ const { parentElement } = stylesItem.element;
57
+ if (parentElement) {
58
+ parentElement.removeChild(stylesItem.element);
59
+ }
36
60
  }
37
61
  // Then remove the document Map
62
+ const stylesMap = styleRegistry.get(styles);
38
63
  stylesMap.delete(ownerDocument);
39
64
  if (stylesMap.size)
40
65
  return;
@@ -44,9 +69,26 @@ export const unregisterStyles = ({ ownerDocument, styles }) => {
44
69
  export const updateStyles = ({ ownerDocument, previousStyles, styles, }) => {
45
70
  if (previousStyles === styles)
46
71
  return;
72
+ const stylesMap = styleRegistry.get(previousStyles);
73
+ const stylesItem = stylesMap === null || stylesMap === void 0 ? void 0 : stylesMap.get(ownerDocument);
74
+ if (stylesMap && (stylesItem === null || stylesItem === void 0 ? void 0 : stylesItem.element) && (stylesItem === null || stylesItem === void 0 ? void 0 : stylesItem.referenceCount) === 1) {
75
+ // Mutate existing <style> element with updated styles
76
+ stylesItem.element.innerHTML = styles;
77
+ styleRegistry.set(styles, new Map([[ownerDocument, stylesItem]]));
78
+ // Cleanup previous stylesMap
79
+ stylesMap.delete(ownerDocument);
80
+ if (!stylesMap.size) {
81
+ styleRegistry.delete(previousStyles);
82
+ }
83
+ return;
84
+ }
47
85
  if (previousStyles) {
48
86
  unregisterStyles({ ownerDocument, styles: previousStyles });
49
87
  }
50
88
  registerStyles({ ownerDocument, styles });
51
89
  };
90
+ export const getStyleRegistryKeys = () => styleRegistry.keys();
91
+ export const clearRegistry = () => {
92
+ styleRegistry.clear();
93
+ };
52
94
  //# sourceMappingURL=style-registry.js.map
@@ -6,9 +6,13 @@
6
6
  */
7
7
 
8
8
  declare type Payload = {|
9
- ownerDocument: Document,
9
+ ownerDocument: Document | "global",
10
10
  styles: string,
11
11
  |};
12
+ declare export var getRegisteredStyles: (x: Payload) => {|
13
+ element: HTMLStyleElement | null,
14
+ referenceCount: number,
15
+ |} | null;
12
16
  declare export var registerStyles: (x: Payload) => void;
13
17
  declare export var unregisterStyles: (x: Payload) => void;
14
18
  declare type UpdatePayload = {|
@@ -17,3 +21,5 @@ declare type UpdatePayload = {|
17
21
  styles: string,
18
22
  |};
19
23
  declare export var updateStyles: (x: UpdatePayload) => void;
24
+ declare export var getStyleRegistryKeys: () => IterableIterator<string>;
25
+ declare export var clearRegistry: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"style-registry.js","sourceRoot":"","sources":["../src/style-registry.ts"],"names":[],"mappings":"AAKA,MAAM,aAAa,GAAkB,IAAI,GAAG,EAAE,CAAC;AAI/C,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,EAAE,aAAa,EAAE,MAAM,EAAW,EAAE,EAAE;IACjE,IAAI,CAAC,MAAM;QAAE,OAAO;IAEpB,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,kBAAkB,GAAG,SAAS,aAAT,SAAS,uBAAT,SAAS,CAAE,GAAG,CAAC,aAAa,CAAC,CAAC;IAEzD,IAAI,kBAAkB,EAAE;QACpB,kBAAkB,CAAC,cAAc,EAAE,CAAC;QACpC,OAAO;KACV;IAED,MAAM,OAAO,GAAG,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IACrD,OAAO,CAAC,YAAY,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;IAC7C,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC;IAC3B,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACxC,MAAM,UAAU,GAAG,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,EAAE,CAAC;IAElD,IAAI,SAAS,EAAE;QACX,SAAS,CAAC,GAAG,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;QACzC,OAAO;KACV;IAED,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AACtE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,EAAE,aAAa,EAAE,MAAM,EAAW,EAAE,EAAE;IACnE,IAAI,CAAC,MAAM;QAAE,OAAO;IAEpB,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,SAAS,aAAT,SAAS,uBAAT,SAAS,CAAE,GAAG,CAAC,aAAa,CAAC,CAAC;IACjD,IAAI,CAAC,SAAS,IAAI,CAAC,UAAU;QAAE,OAAO;IAEtC,UAAU,CAAC,cAAc,EAAE,CAAC;IAC5B,IAAI,UAAU,CAAC,cAAc;QAAE,OAAO;IAEtC,8FAA8F;IAC9F,MAAM,EAAE,aAAa,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC;IAC7C,IAAI,aAAa,EAAE;QACf,aAAa,CAAC,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;KACjD;IACD,+BAA+B;IAC/B,SAAS,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAEhC,IAAI,SAAS,CAAC,IAAI;QAAE,OAAO;IAC3B,4EAA4E;IAC5E,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACjC,CAAC,CAAC;AAIF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,EACzB,aAAa,EACb,cAAc,EACd,MAAM,GACM,EAAE,EAAE;IAChB,IAAI,cAAc,KAAK,MAAM;QAAE,OAAO;IAEtC,IAAI,cAAc,EAAE;QAChB,gBAAgB,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;KAC/D;IAED,cAAc,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,CAAC;AAC9C,CAAC,CAAC"}
1
+ {"version":3,"file":"style-registry.js","sourceRoot":"","sources":["../src/style-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,114 @@
1
+ // @vitest-environment happy-dom
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+ import { clearRegistry, getRegisteredStyles, getStyleRegistryKeys, registerStyles, unregisterStyles, updateStyles, } from './style-registry.js';
4
+ describe('@acusti/styling', () => {
5
+ describe('style-registry.ts', () => {
6
+ const mockStyles = '.test { color: red; }';
7
+ // reset styleRegistry before each test
8
+ beforeEach(clearRegistry);
9
+ describe('registerStyles', () => {
10
+ it('should add styles to the registry keyed by the style string', () => {
11
+ const payload = { ownerDocument: document, styles: mockStyles };
12
+ registerStyles(payload);
13
+ const styleRegistryKeys = getStyleRegistryKeys();
14
+ const keysArray = [...styleRegistryKeys];
15
+ expect(keysArray.length).toBe(1);
16
+ expect(keysArray[0]).toBe(mockStyles);
17
+ const result = getRegisteredStyles(payload);
18
+ expect(result.element).toBeDefined();
19
+ expect(result.referenceCount).toBe(1);
20
+ });
21
+ it('should allow registering styles without a DOM via ownerDocument: "global"', () => {
22
+ const payload = Object.freeze({
23
+ ownerDocument: 'global',
24
+ styles: mockStyles,
25
+ });
26
+ registerStyles(payload);
27
+ const registryKeys = [...getStyleRegistryKeys()];
28
+ expect(registryKeys.length).toBe(1);
29
+ expect(registryKeys[0]).toBe(mockStyles);
30
+ const result = getRegisteredStyles(payload);
31
+ expect(result.element).toBeNull();
32
+ expect(result.referenceCount).toBe(1);
33
+ });
34
+ });
35
+ describe('getRegisteredStyles', () => {
36
+ it('should retrieve registered styles', () => {
37
+ const payload = { ownerDocument: document, styles: mockStyles };
38
+ registerStyles(payload);
39
+ const result = getRegisteredStyles(payload);
40
+ expect(result.element.tagName).toBe('STYLE');
41
+ expect(result.referenceCount).toBe(1);
42
+ });
43
+ });
44
+ describe('unregisterStyles', () => {
45
+ it('should remove styles from the registry if no other references to same styles exist', () => {
46
+ const payload = { ownerDocument: document, styles: mockStyles };
47
+ const otherPayload = Object.freeze({
48
+ ownerDocument: 'global',
49
+ styles: mockStyles,
50
+ });
51
+ registerStyles(payload);
52
+ registerStyles(otherPayload);
53
+ expect(getStyleRegistryKeys().next().value).toBe(mockStyles);
54
+ expect(getRegisteredStyles(payload).referenceCount).toBe(1);
55
+ expect(getRegisteredStyles(otherPayload).referenceCount).toBe(1);
56
+ unregisterStyles(payload);
57
+ // style registry for this style should still exist
58
+ expect(getStyleRegistryKeys().next().value).toBe(mockStyles);
59
+ // but this document’s styles item should be cleared
60
+ expect(getRegisteredStyles(payload)).toBeNull();
61
+ unregisterStyles(otherPayload);
62
+ // now the style registry should be empty
63
+ expect([...getStyleRegistryKeys()].length).toBe(0);
64
+ });
65
+ });
66
+ describe('updateStyles', () => {
67
+ it('should update styles in the registry, reusing the existing DOM element if no other reference', () => {
68
+ const previousStyles = '.previous { color: blue; }';
69
+ const payload = {
70
+ ownerDocument: document,
71
+ styles: previousStyles,
72
+ };
73
+ registerStyles(payload);
74
+ const previousStylesItem = getRegisteredStyles(payload);
75
+ expect(previousStylesItem.element.innerText).toBe(previousStyles);
76
+ updateStyles({
77
+ ownerDocument: document,
78
+ previousStyles,
79
+ styles: mockStyles,
80
+ });
81
+ const stylesItem = getRegisteredStyles({
82
+ ownerDocument: document,
83
+ styles: mockStyles,
84
+ });
85
+ expect(previousStylesItem.element).toBe(stylesItem.element);
86
+ expect(stylesItem.element.innerText).toBe(mockStyles);
87
+ expect(getRegisteredStyles(payload)).toBeNull();
88
+ });
89
+ it('should update styles in the registry, creating a new DOM element if there are other references', () => {
90
+ const previousStyles = '.previous { color: blue; }';
91
+ const payload = { ownerDocument: document, styles: previousStyles };
92
+ registerStyles(payload);
93
+ // create multiple references to the same styles and document
94
+ registerStyles(payload);
95
+ const previousStylesItem = getRegisteredStyles(payload);
96
+ expect(previousStylesItem.referenceCount).toBe(2);
97
+ expect(previousStylesItem.element.innerText).toBe(previousStyles);
98
+ updateStyles({
99
+ ownerDocument: document,
100
+ previousStyles,
101
+ styles: mockStyles,
102
+ });
103
+ const stylesItem = getRegisteredStyles({
104
+ ownerDocument: document,
105
+ styles: mockStyles,
106
+ });
107
+ expect(previousStylesItem.element).not.toBe(stylesItem.element);
108
+ expect(stylesItem.element.innerText).toBe(mockStyles);
109
+ expect(getRegisteredStyles(payload).referenceCount).toBe(1);
110
+ });
111
+ });
112
+ });
113
+ });
114
+ //# sourceMappingURL=style-registry.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Flowtype definitions for style-registry.test
3
+ * Generated by Flowgen from a Typescript Definition
4
+ * Flowgen v1.20.1
5
+ * @flow
6
+ */
@@ -0,0 +1 @@
1
+ {"version":3,"file":"style-registry.test.js","sourceRoot":"","sources":["../src/style-registry.test.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE1D,OAAO,EACH,aAAa,EACb,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,YAAY,GACf,MAAM,qBAAqB,CAAC;AAE7B,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC7B,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;QAC/B,MAAM,UAAU,GAAG,uBAAuB,CAAC;QAE3C,uCAAuC;QACvC,UAAU,CAAC,aAAa,CAAC,CAAC;QAE1B,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;YAC5B,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;gBACnE,MAAM,OAAO,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;gBAChE,cAAc,CAAC,OAAO,CAAC,CAAC;gBACxB,MAAM,iBAAiB,GAAG,oBAAoB,EAAE,CAAC;gBACjD,MAAM,SAAS,GAAG,CAAC,GAAG,iBAAiB,CAAC,CAAC;gBACzC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACjC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBACtC,MAAM,MAAM,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;gBAC5C,MAAM,CAAC,MAAO,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;gBACtC,MAAM,CAAC,MAAO,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC3C,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,2EAA2E,EAAE,GAAG,EAAE;gBACjF,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC;oBAC1B,aAAa,EAAE,QAAQ;oBACvB,MAAM,EAAE,UAAU;iBACrB,CAAC,CAAC;gBACH,cAAc,CAAC,OAAO,CAAC,CAAC;gBACxB,MAAM,YAAY,GAAG,CAAC,GAAG,oBAAoB,EAAE,CAAC,CAAC;gBACjD,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACpC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBACzC,MAAM,MAAM,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;gBAC5C,MAAM,CAAC,MAAO,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;gBACnC,MAAM,CAAC,MAAO,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC3C,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;YACjC,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;gBACzC,MAAM,OAAO,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;gBAChE,cAAc,CAAC,OAAO,CAAC,CAAC;gBACxB,MAAM,MAAM,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;gBAC5C,MAAM,CAAC,MAAO,CAAC,OAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC/C,MAAM,CAAC,MAAO,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC3C,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;YAC9B,EAAE,CAAC,oFAAoF,EAAE,GAAG,EAAE;gBAC1F,MAAM,OAAO,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;gBAChE,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC;oBAC/B,aAAa,EAAE,QAAQ;oBACvB,MAAM,EAAE,UAAU;iBACrB,CAAC,CAAC;gBACH,cAAc,CAAC,OAAO,CAAC,CAAC;gBACxB,cAAc,CAAC,YAAY,CAAC,CAAC;gBAC7B,MAAM,CAAC,oBAAoB,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBAC7D,MAAM,CAAC,mBAAmB,CAAC,OAAO,CAAE,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAC7D,MAAM,CAAC,mBAAmB,CAAC,YAAY,CAAE,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAClE,gBAAgB,CAAC,OAAO,CAAC,CAAC;gBAC1B,mDAAmD;gBACnD,MAAM,CAAC,oBAAoB,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBAC7D,oDAAoD;gBACpD,MAAM,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;gBAChD,gBAAgB,CAAC,YAAY,CAAC,CAAC;gBAC/B,yCAAyC;gBACzC,MAAM,CAAC,CAAC,GAAG,oBAAoB,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACvD,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;YAC1B,EAAE,CAAC,8FAA8F,EAAE,GAAG,EAAE;gBACpG,MAAM,cAAc,GAAG,4BAA4B,CAAC;gBACpD,MAAM,OAAO,GAAG;oBACZ,aAAa,EAAE,QAAQ;oBACvB,MAAM,EAAE,cAAc;iBACzB,CAAC;gBACF,cAAc,CAAC,OAAO,CAAC,CAAC;gBACxB,MAAM,kBAAkB,GAAG,mBAAmB,CAAC,OAAO,CAAE,CAAC;gBACzD,MAAM,CAAC,kBAAkB,CAAC,OAAQ,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;gBACnE,YAAY,CAAC;oBACT,aAAa,EAAE,QAAQ;oBACvB,cAAc;oBACd,MAAM,EAAE,UAAU;iBACrB,CAAC,CAAC;gBACH,MAAM,UAAU,GAAG,mBAAmB,CAAC;oBACnC,aAAa,EAAE,QAAQ;oBACvB,MAAM,EAAE,UAAU;iBACrB,CAAE,CAAC;gBACJ,MAAM,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;gBAC5D,MAAM,CAAC,UAAU,CAAC,OAAQ,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBACvD,MAAM,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;YACpD,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,gGAAgG,EAAE,GAAG,EAAE;gBACtG,MAAM,cAAc,GAAG,4BAA4B,CAAC;gBACpD,MAAM,OAAO,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;gBACpE,cAAc,CAAC,OAAO,CAAC,CAAC;gBACxB,6DAA6D;gBAC7D,cAAc,CAAC,OAAO,CAAC,CAAC;gBACxB,MAAM,kBAAkB,GAAG,mBAAmB,CAAC,OAAO,CAAE,CAAC;gBACzD,MAAM,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAClD,MAAM,CAAC,kBAAkB,CAAC,OAAQ,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;gBACnE,YAAY,CAAC;oBACT,aAAa,EAAE,QAAQ;oBACvB,cAAc;oBACd,MAAM,EAAE,UAAU;iBACrB,CAAC,CAAC;gBACH,MAAM,UAAU,GAAG,mBAAmB,CAAC;oBACnC,aAAa,EAAE,QAAQ;oBACvB,MAAM,EAAE,UAAU;iBACrB,CAAE,CAAC;gBACJ,MAAM,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;gBAChE,MAAM,CAAC,UAAU,CAAC,OAAQ,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBACvD,MAAM,CAAC,mBAAmB,CAAC,OAAO,CAAE,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjE,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,15 @@
1
1
  {
2
2
  "name": "@acusti/styling",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "exports": "./dist/index.js",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "src"
12
+ ],
4
13
  "description": "React component that renders a CSS style string as a <style> element in the <head> of the document (with global style de-duplication)",
5
14
  "keywords": [
6
15
  "react",
@@ -13,15 +22,9 @@
13
22
  "ts",
14
23
  "flow"
15
24
  ],
16
- "type": "module",
17
- "sideEffects": false,
18
- "exports": "./dist/index.js",
19
- "main": "./dist/index.js",
20
- "types": "./dist/index.d.ts",
21
- "files": [
22
- "dist",
23
- "src"
24
- ],
25
+ "scripts": {
26
+ "test": "vitest"
27
+ },
25
28
  "repository": {
26
29
  "type": "git",
27
30
  "url": "https://github.com/acusti/uikit.git",
@@ -34,8 +37,15 @@
34
37
  },
35
38
  "homepage": "https://github.com/acusti/uikit/tree/main/packages/styling#readme",
36
39
  "devDependencies": {
37
- "@types/react": "^18.0.25",
38
- "typescript": "^4.9.3"
40
+ "@testing-library/dom": "^9.3.1",
41
+ "@testing-library/react": "^14.0.0",
42
+ "@testing-library/user-event": "^14.4.3",
43
+ "@types/react": "^18.2.45",
44
+ "happy-dom": "^12.10.3",
45
+ "react": "^18",
46
+ "react-dom": "^18",
47
+ "typescript": "^5.3.3",
48
+ "vitest": "^1.1.0"
39
49
  },
40
50
  "peerDependencies": {
41
51
  "react": "^16.8 || ^17 || ^18",
package/src/Style.tsx CHANGED
@@ -1,6 +1,11 @@
1
1
  import * as React from 'react';
2
2
 
3
- import { unregisterStyles, updateStyles } from './style-registry.js';
3
+ import {
4
+ getRegisteredStyles,
5
+ registerStyles,
6
+ unregisterStyles,
7
+ updateStyles,
8
+ } from './style-registry.js';
4
9
 
5
10
  const { useCallback, useEffect, useMemo, useRef, useState } = React;
6
11
 
@@ -12,6 +17,12 @@ const Style = ({ children }: Props) => {
12
17
  // Minify CSS styles by replacing consecutive whitespace (including \n) with ' '
13
18
  const styles = useMemo(() => children.replace(/\s+/gm, ' '), [children]);
14
19
  const [ownerDocument, setOwnerDocument] = useState<Document | null>(null);
20
+ const isMountedRef = useRef<boolean>(false);
21
+
22
+ useEffect(() => {
23
+ isMountedRef.current = true;
24
+ unregisterStyles({ ownerDocument: 'global', styles });
25
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
15
26
 
16
27
  useEffect(
17
28
  () => () => {
@@ -42,14 +53,13 @@ const Style = ({ children }: Props) => {
42
53
 
43
54
  if (ownerDocument) return null;
44
55
 
45
- return (
46
- <style
47
- dangerouslySetInnerHTML={{
48
- __html: styles,
49
- }}
50
- ref={handleRef}
51
- />
52
- );
56
+ // Avoid duplicate style rendering during SSR via style registry
57
+ if (!isMountedRef.current) {
58
+ if (getRegisteredStyles({ ownerDocument: 'global', styles })) return null;
59
+ registerStyles({ ownerDocument: 'global', styles });
60
+ }
61
+
62
+ return <style dangerouslySetInnerHTML={{ __html: styles }} ref={handleRef} />;
53
63
  };
54
64
 
55
65
  export default Style;
@@ -0,0 +1,129 @@
1
+ // @vitest-environment happy-dom
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+
4
+ import {
5
+ clearRegistry,
6
+ getRegisteredStyles,
7
+ getStyleRegistryKeys,
8
+ registerStyles,
9
+ unregisterStyles,
10
+ updateStyles,
11
+ } from './style-registry.js';
12
+
13
+ describe('@acusti/styling', () => {
14
+ describe('style-registry.ts', () => {
15
+ const mockStyles = '.test { color: red; }';
16
+
17
+ // reset styleRegistry before each test
18
+ beforeEach(clearRegistry);
19
+
20
+ describe('registerStyles', () => {
21
+ it('should add styles to the registry keyed by the style string', () => {
22
+ const payload = { ownerDocument: document, styles: mockStyles };
23
+ registerStyles(payload);
24
+ const styleRegistryKeys = getStyleRegistryKeys();
25
+ const keysArray = [...styleRegistryKeys];
26
+ expect(keysArray.length).toBe(1);
27
+ expect(keysArray[0]).toBe(mockStyles);
28
+ const result = getRegisteredStyles(payload);
29
+ expect(result!.element).toBeDefined();
30
+ expect(result!.referenceCount).toBe(1);
31
+ });
32
+
33
+ it('should allow registering styles without a DOM via ownerDocument: "global"', () => {
34
+ const payload = Object.freeze({
35
+ ownerDocument: 'global',
36
+ styles: mockStyles,
37
+ });
38
+ registerStyles(payload);
39
+ const registryKeys = [...getStyleRegistryKeys()];
40
+ expect(registryKeys.length).toBe(1);
41
+ expect(registryKeys[0]).toBe(mockStyles);
42
+ const result = getRegisteredStyles(payload);
43
+ expect(result!.element).toBeNull();
44
+ expect(result!.referenceCount).toBe(1);
45
+ });
46
+ });
47
+
48
+ describe('getRegisteredStyles', () => {
49
+ it('should retrieve registered styles', () => {
50
+ const payload = { ownerDocument: document, styles: mockStyles };
51
+ registerStyles(payload);
52
+ const result = getRegisteredStyles(payload);
53
+ expect(result!.element!.tagName).toBe('STYLE');
54
+ expect(result!.referenceCount).toBe(1);
55
+ });
56
+ });
57
+
58
+ describe('unregisterStyles', () => {
59
+ it('should remove styles from the registry if no other references to same styles exist', () => {
60
+ const payload = { ownerDocument: document, styles: mockStyles };
61
+ const otherPayload = Object.freeze({
62
+ ownerDocument: 'global',
63
+ styles: mockStyles,
64
+ });
65
+ registerStyles(payload);
66
+ registerStyles(otherPayload);
67
+ expect(getStyleRegistryKeys().next().value).toBe(mockStyles);
68
+ expect(getRegisteredStyles(payload)!.referenceCount).toBe(1);
69
+ expect(getRegisteredStyles(otherPayload)!.referenceCount).toBe(1);
70
+ unregisterStyles(payload);
71
+ // style registry for this style should still exist
72
+ expect(getStyleRegistryKeys().next().value).toBe(mockStyles);
73
+ // but this document’s styles item should be cleared
74
+ expect(getRegisteredStyles(payload)).toBeNull();
75
+ unregisterStyles(otherPayload);
76
+ // now the style registry should be empty
77
+ expect([...getStyleRegistryKeys()].length).toBe(0);
78
+ });
79
+ });
80
+
81
+ describe('updateStyles', () => {
82
+ it('should update styles in the registry, reusing the existing DOM element if no other reference', () => {
83
+ const previousStyles = '.previous { color: blue; }';
84
+ const payload = {
85
+ ownerDocument: document,
86
+ styles: previousStyles,
87
+ };
88
+ registerStyles(payload);
89
+ const previousStylesItem = getRegisteredStyles(payload)!;
90
+ expect(previousStylesItem.element!.innerText).toBe(previousStyles);
91
+ updateStyles({
92
+ ownerDocument: document,
93
+ previousStyles,
94
+ styles: mockStyles,
95
+ });
96
+ const stylesItem = getRegisteredStyles({
97
+ ownerDocument: document,
98
+ styles: mockStyles,
99
+ })!;
100
+ expect(previousStylesItem.element).toBe(stylesItem.element);
101
+ expect(stylesItem.element!.innerText).toBe(mockStyles);
102
+ expect(getRegisteredStyles(payload)).toBeNull();
103
+ });
104
+
105
+ it('should update styles in the registry, creating a new DOM element if there are other references', () => {
106
+ const previousStyles = '.previous { color: blue; }';
107
+ const payload = { ownerDocument: document, styles: previousStyles };
108
+ registerStyles(payload);
109
+ // create multiple references to the same styles and document
110
+ registerStyles(payload);
111
+ const previousStylesItem = getRegisteredStyles(payload)!;
112
+ expect(previousStylesItem.referenceCount).toBe(2);
113
+ expect(previousStylesItem.element!.innerText).toBe(previousStyles);
114
+ updateStyles({
115
+ ownerDocument: document,
116
+ previousStyles,
117
+ styles: mockStyles,
118
+ });
119
+ const stylesItem = getRegisteredStyles({
120
+ ownerDocument: document,
121
+ styles: mockStyles,
122
+ })!;
123
+ expect(previousStylesItem.element).not.toBe(stylesItem.element);
124
+ expect(stylesItem.element!.innerText).toBe(mockStyles);
125
+ expect(getRegisteredStyles(payload)!.referenceCount).toBe(1);
126
+ });
127
+ });
128
+ });
129
+ });
@@ -1,29 +1,49 @@
1
1
  type StyleRegistry = Map<
2
2
  string,
3
- Map<Document, { element: HTMLStyleElement; referenceCount: number }>
3
+ Map<Document | 'global', { element: HTMLStyleElement | null; referenceCount: number }>
4
4
  >;
5
5
 
6
6
  const styleRegistry: StyleRegistry = new Map();
7
7
 
8
- type Payload = { ownerDocument: Document; styles: string };
8
+ type Payload = { ownerDocument: Document | 'global'; styles: string };
9
9
 
10
+ export const getRegisteredStyles = ({ ownerDocument, styles }: Payload) => {
11
+ if (!styles) return null;
12
+ const stylesMap = styleRegistry.get(styles);
13
+ if (!stylesMap) return null;
14
+ return stylesMap.get(ownerDocument) ?? null;
15
+ };
16
+
17
+ // NOTE a more idiomatic API than (register|unregister)Styles would be
18
+ // to make registerStyles a thunk that returns a cleanup function
10
19
  export const registerStyles = ({ ownerDocument, styles }: Payload) => {
11
20
  if (!styles) return;
12
21
 
13
- const stylesMap = styleRegistry.get(styles);
14
- const existingStylesItem = stylesMap?.get(ownerDocument);
15
-
22
+ const existingStylesItem = getRegisteredStyles({ ownerDocument, styles });
16
23
  if (existingStylesItem) {
17
24
  existingStylesItem.referenceCount++;
18
25
  return;
19
26
  }
20
27
 
28
+ if (ownerDocument === 'global') {
29
+ const stylesItem = { element: null, referenceCount: 1 };
30
+ let stylesMap = styleRegistry.get(styles);
31
+ if (stylesMap) {
32
+ stylesMap.set(ownerDocument, stylesItem);
33
+ } else {
34
+ stylesMap = new Map([[ownerDocument, stylesItem]]);
35
+ }
36
+ styleRegistry.set(styles, stylesMap);
37
+ return;
38
+ }
39
+
21
40
  const element = ownerDocument.createElement('style');
22
41
  element.setAttribute('data-ukt-styling', '');
23
42
  element.innerHTML = styles;
24
43
  ownerDocument.head.appendChild(element);
25
44
  const stylesItem = { element, referenceCount: 1 };
26
45
 
46
+ const stylesMap = styleRegistry.get(styles);
27
47
  if (stylesMap) {
28
48
  stylesMap.set(ownerDocument, stylesItem);
29
49
  return;
@@ -35,19 +55,22 @@ export const registerStyles = ({ ownerDocument, styles }: Payload) => {
35
55
  export const unregisterStyles = ({ ownerDocument, styles }: Payload) => {
36
56
  if (!styles) return;
37
57
 
38
- const stylesMap = styleRegistry.get(styles);
39
- const stylesItem = stylesMap?.get(ownerDocument);
40
- if (!stylesMap || !stylesItem) return;
58
+ const stylesItem = getRegisteredStyles({ ownerDocument, styles });
59
+ if (!stylesItem) return;
41
60
 
42
61
  stylesItem.referenceCount--;
43
62
  if (stylesItem.referenceCount) return;
44
63
 
45
64
  // If no more references to these styles in this document, remove <style> element from the DOM
46
- const { parentElement } = stylesItem.element;
47
- if (parentElement) {
48
- parentElement.removeChild(stylesItem.element);
65
+ if (stylesItem.element) {
66
+ const { parentElement } = stylesItem.element;
67
+ if (parentElement) {
68
+ parentElement.removeChild(stylesItem.element);
69
+ }
49
70
  }
71
+
50
72
  // Then remove the document Map
73
+ const stylesMap = styleRegistry.get(styles)!;
51
74
  stylesMap.delete(ownerDocument);
52
75
 
53
76
  if (stylesMap.size) return;
@@ -64,9 +87,29 @@ export const updateStyles = ({
64
87
  }: UpdatePayload) => {
65
88
  if (previousStyles === styles) return;
66
89
 
90
+ const stylesMap = styleRegistry.get(previousStyles);
91
+ const stylesItem = stylesMap?.get(ownerDocument);
92
+ if (stylesMap && stylesItem?.element && stylesItem?.referenceCount === 1) {
93
+ // Mutate existing <style> element with updated styles
94
+ stylesItem.element.innerHTML = styles;
95
+ styleRegistry.set(styles, new Map([[ownerDocument, stylesItem]]));
96
+ // Cleanup previous stylesMap
97
+ stylesMap.delete(ownerDocument);
98
+ if (!stylesMap.size) {
99
+ styleRegistry.delete(previousStyles);
100
+ }
101
+ return;
102
+ }
103
+
67
104
  if (previousStyles) {
68
105
  unregisterStyles({ ownerDocument, styles: previousStyles });
69
106
  }
70
107
 
71
108
  registerStyles({ ownerDocument, styles });
72
109
  };
110
+
111
+ export const getStyleRegistryKeys = () => styleRegistry.keys();
112
+
113
+ export const clearRegistry = () => {
114
+ styleRegistry.clear();
115
+ };