@wrdagency/react-islands 0.1.1 → 0.2.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.
@@ -14,4 +14,4 @@ type Island = {
14
14
  render: (opts: IslandRenderOpts) => void;
15
15
  };
16
16
 
17
- export type { IslandOpts as I, Island as a };
17
+ export type { Island as I, IslandOpts as a };
@@ -14,4 +14,4 @@ type Island = {
14
14
  render: (opts: IslandRenderOpts) => void;
15
15
  };
16
16
 
17
- export type { IslandOpts as I, Island as a };
17
+ export type { Island as I, IslandOpts as a };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrdagency/react-islands",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "files": [
@@ -27,7 +27,7 @@
27
27
  "author": "Kyle Thomas Cooper @ WRD",
28
28
  "license": "MIT",
29
29
  "devDependencies": {
30
- "@types/react-dom": "^18.2.23",
30
+ "@types/react-dom": "^19.1.6",
31
31
  "tsup": "^8.0.2",
32
32
  "typescript": "^5.4.3"
33
33
  },
@@ -36,6 +36,6 @@
36
36
  "url": "https://github.com/kyletcooper/react-islands"
37
37
  },
38
38
  "peerDependencies": {
39
- "react-dom": "^18.2.0"
39
+ "react-dom": "^19.1.0"
40
40
  }
41
41
  }
package/src/index.tsx CHANGED
@@ -1,7 +1,16 @@
1
- import { FC, PropsWithChildren, useEffect, useRef } from "react";
2
- import { Island, IslandOpts, IslandRenderOpts } from "./types";
3
- import { createRoot, hydrateRoot } from "react-dom/client";
4
-
1
+ import { FC } from "react";
2
+ import { hydrateIslands, renderIsland, withProps } from "./render";
3
+ import { Island, IslandOpts } from "./types";
4
+
5
+ /**
6
+ * Create a React Island.
7
+ *
8
+ * You can render the island using the Island.render function.
9
+ *
10
+ * @param component React.FC
11
+ * @param opts IslandOpts
12
+ * @returns Island
13
+ */
5
14
  export function createIsland(component: FC, opts: IslandOpts): Island {
6
15
  const mergedOpts: Required<IslandOpts> = {
7
16
  multiple: false,
@@ -22,90 +31,13 @@ export function createIsland(component: FC, opts: IslandOpts): Island {
22
31
  return island;
23
32
  }
24
33
 
25
- export function withProps<T>(component: FC<T>, setProps: Partial<T>): FC<T> {
26
- return (props: T) => {
27
- return component({ ...props, ...setProps });
28
- };
29
- }
30
-
31
- export function isServer() {
34
+ /**
35
+ * Checks if the current script is running in a pre-render.
36
+ *
37
+ * @returns boolean
38
+ */
39
+ export function isServer(): boolean {
32
40
  return !(typeof window != "undefined" && window.document);
33
41
  }
34
42
 
35
- export function RawHTML({ html }: { html: string }) {
36
- const ref = useRef<HTMLScriptElement>(null);
37
-
38
- // important to not have ANY deps
39
- useEffect(() => {
40
- if (ref.current) {
41
- ref.current.outerHTML = html;
42
- }
43
- }, []);
44
-
45
- return <script ref={ref} />;
46
- }
47
-
48
- function renderIsland(island: Island, renderOpts: IslandRenderOpts = {}) {
49
- const { component, opts } = island;
50
- const { selector, multiple, keepChildren } = opts;
51
-
52
- const nodes = [...document.querySelectorAll(selector)];
53
-
54
- if (!nodes) {
55
- console.warn(
56
- `Could not hydrate React Island because DOM node (${selector}) could not be found.`
57
- );
58
-
59
- return false;
60
- }
61
-
62
- if (nodes.length > 1 && !multiple) {
63
- console.warn(
64
- `Multiple elements matched React Island selector (${selector}) but multiple was not enabled. Choosing first element as root.`
65
- );
66
- }
67
-
68
- for (let i = 0; i < nodes.length; i++) {
69
- if (i > 0 && !multiple) {
70
- break;
71
- }
72
-
73
- const element = nodes[i] as HTMLElement;
74
- let props: PropsWithChildren = {};
75
-
76
- try {
77
- const json = element.dataset.props || "{}";
78
-
79
- if (json) {
80
- const parsed = JSON.parse(json);
81
-
82
- if (typeof parsed !== "object" || Array.isArray(parsed)) {
83
- throw new Error(
84
- `Parsed JSON is not a valid dictionary object: '${json}'`
85
- );
86
- }
87
-
88
- if (parsed) {
89
- // Ignore null.
90
- props = parsed;
91
- }
92
- }
93
- } catch (e) {
94
- console.warn("Could not parse JSON props for React Island.");
95
- console.error(e);
96
- }
97
-
98
- if (keepChildren) {
99
- props.children = <RawHTML html={element.innerHTML} />;
100
- }
101
-
102
- const Component = withProps(component, props);
103
-
104
- if (renderOpts.hydrate) {
105
- hydrateRoot(nodes[i], <Component />);
106
- } else {
107
- const root = createRoot(nodes[i]);
108
- root.render(<Component />);
109
- }
110
- }
111
- }
43
+ export { hydrateIslands, Island, withProps };
package/src/render.tsx ADDED
@@ -0,0 +1,121 @@
1
+ import { FC, PropsWithChildren, useEffect, useRef } from "react";
2
+ import { createRoot, hydrateRoot } from "react-dom/client";
3
+ import { Island, IslandRenderOpts } from "./types";
4
+
5
+ /**
6
+ * Internal component for rendering raw HTML in a React component.
7
+ */
8
+ export function RawHTML({ html }: { html: string }) {
9
+ const ref = useRef<HTMLScriptElement>(null);
10
+
11
+ // important to not have ANY deps
12
+ useEffect(() => {
13
+ if (ref.current) {
14
+ ref.current.outerHTML = html;
15
+ }
16
+ }, []);
17
+
18
+ return <script ref={ref} />;
19
+ }
20
+
21
+ /**
22
+ * Create a higher-order component with certain fixed.
23
+ *
24
+ * Useful for quickly creating multiple variants of the same component to use as islands.
25
+ *
26
+ * @param component FC<T>
27
+ * @param setProps Partial<T>
28
+ * @returns FC<T>
29
+ */
30
+ export function withProps<T>(component: FC<T>, setProps: Partial<T>): FC<T> {
31
+ return (props: T) => {
32
+ return component({ ...props, ...setProps });
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Renders a React Island.
38
+ */
39
+ export function renderIsland(
40
+ island: Island,
41
+ renderOpts: IslandRenderOpts = {}
42
+ ) {
43
+ const { component, opts } = island;
44
+ const { selector, multiple, keepChildren } = opts;
45
+
46
+ const nodes = [...document.querySelectorAll(selector)];
47
+
48
+ if (!nodes) {
49
+ console.warn(
50
+ `Could not hydrate React Island because DOM node (${selector}) could not be found.`
51
+ );
52
+
53
+ return false;
54
+ }
55
+
56
+ if (nodes.length > 1 && !multiple) {
57
+ console.warn(
58
+ `Multiple elements matched React Island selector (${selector}) but multiple was not enabled. Choosing first element as root.`
59
+ );
60
+ }
61
+
62
+ for (let i = 0; i < nodes.length; i++) {
63
+ if (i > 0 && !multiple) {
64
+ break;
65
+ }
66
+
67
+ const element = nodes[i] as HTMLElement;
68
+ let props: PropsWithChildren = {};
69
+
70
+ try {
71
+ const json = element.dataset.props || "{}";
72
+
73
+ if (json) {
74
+ const parsed = JSON.parse(json);
75
+
76
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
77
+ throw new Error(
78
+ `Parsed JSON is not a valid dictionary object: '${json}'`
79
+ );
80
+ }
81
+
82
+ if (parsed) {
83
+ // Ignore null.
84
+ props = parsed;
85
+ }
86
+ }
87
+ } catch (e) {
88
+ console.warn("Could not parse JSON props for React Island.");
89
+ console.error(e);
90
+ }
91
+
92
+ if (keepChildren) {
93
+ props.children = <RawHTML html={element.innerHTML} />;
94
+ }
95
+
96
+ const Component = withProps(component, props);
97
+
98
+ if (renderOpts.hydrate) {
99
+ hydrateRoot(nodes[i], <Component />);
100
+ } else {
101
+ const root = createRoot(nodes[i]);
102
+ root.render(<Component />);
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Render all Islands in a record.
109
+ *
110
+ * Useful if you are importing * from an islands directory.
111
+ *
112
+ * @param islands Record<string, Island>
113
+ */
114
+ export function hydrateIslands(islands: Record<string, Island>): void {
115
+ const isDev =
116
+ (process.env.NODE_ENV || "development").trim() === "development";
117
+
118
+ for (const island of Object.values(islands)) {
119
+ island.render({ hydrate: !isDev });
120
+ }
121
+ }