@wrdagency/react-islands 0.1.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 +104 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +3294 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3280 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server.d.mts +9 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +3235 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +3223 -0
- package/dist/server.mjs.map +1 -0
- package/dist/types-Dn7YlQVB.d.mts +17 -0
- package/dist/types-Dn7YlQVB.d.ts +17 -0
- package/package.json +29 -0
- package/src/index.tsx +111 -0
- package/src/server.tsx +38 -0
- package/src/types.ts +17 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +10 -0
package/src/index.tsx
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { FC, PropsWithChildren, useEffect, useRef } from "react";
|
|
2
|
+
import { Island, IslandOpts, IslandRenderOpts } from "./types";
|
|
3
|
+
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
4
|
+
|
|
5
|
+
export default function createIsland(component: FC, opts: IslandOpts): Island {
|
|
6
|
+
const mergedOpts: Required<IslandOpts> = {
|
|
7
|
+
multiple: false,
|
|
8
|
+
selector: `[data-hydrate="${opts.name}"]`,
|
|
9
|
+
keepChildren: false,
|
|
10
|
+
...opts,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const island: Island = {
|
|
14
|
+
type: "island",
|
|
15
|
+
component,
|
|
16
|
+
opts: mergedOpts,
|
|
17
|
+
render(opts) {
|
|
18
|
+
renderIsland(this, opts);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return island;
|
|
23
|
+
}
|
|
24
|
+
|
|
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() {
|
|
32
|
+
return !(typeof window != "undefined" && window.document);
|
|
33
|
+
}
|
|
34
|
+
|
|
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
|
+
}
|
package/src/server.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Island } from "./types";
|
|
4
|
+
import { renderToString } from "react-dom/server";
|
|
5
|
+
|
|
6
|
+
interface PrerenderIslandsOpts {
|
|
7
|
+
islands: Record<string, Island>;
|
|
8
|
+
outDir: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function prerenderIslands(opts: PrerenderIslandsOpts) {
|
|
12
|
+
const { islands, outDir } = opts;
|
|
13
|
+
|
|
14
|
+
// Clean or create outDir.
|
|
15
|
+
try {
|
|
16
|
+
await fs.access(outDir);
|
|
17
|
+
|
|
18
|
+
for (const file of await fs.readdir(outDir)) {
|
|
19
|
+
await fs.unlink(path.resolve(outDir, file));
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
await fs.mkdir(outDir);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// For each island.
|
|
26
|
+
for (const island of Object.values(islands)) {
|
|
27
|
+
const { component, opts } = island;
|
|
28
|
+
const { name } = opts;
|
|
29
|
+
const Component = component;
|
|
30
|
+
|
|
31
|
+
// Render it to [outDir]/[name].html.
|
|
32
|
+
const html = renderToString(<Component />);
|
|
33
|
+
const file = path.resolve(outDir, `${name}.html`);
|
|
34
|
+
|
|
35
|
+
console.log(`Rendering component ${name} to ${file}`);
|
|
36
|
+
fs.writeFile(file, html, { flag: "w+" });
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface IslandOpts {
|
|
2
|
+
name: string;
|
|
3
|
+
selector?: string;
|
|
4
|
+
multiple?: boolean;
|
|
5
|
+
keepChildren?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface IslandRenderOpts {
|
|
9
|
+
hydrate?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type Island = {
|
|
13
|
+
type: "island";
|
|
14
|
+
component: React.FC;
|
|
15
|
+
opts: Required<IslandOpts>;
|
|
16
|
+
render: (opts: IslandRenderOpts) => void;
|
|
17
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "./dist/",
|
|
4
|
+
"target": "es2016",
|
|
5
|
+
"module": "Node16",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"exclude": ["dist", "node_modules"]
|
|
14
|
+
}
|