@punkestu/react-html 0.1.0 → 0.1.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.
Files changed (88) hide show
  1. package/dist/cli/docs-content.d.ts +8 -0
  2. package/dist/cli/docs-content.d.ts.map +1 -0
  3. package/dist/cli/docs-content.js +261 -0
  4. package/dist/cli/docs-content.js.map +1 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +113 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli/tui-docs.d.ts +5 -0
  10. package/dist/cli/tui-docs.d.ts.map +1 -0
  11. package/dist/cli/tui-docs.js +191 -0
  12. package/dist/cli/tui-docs.js.map +1 -0
  13. package/dist/client/hydrate.d.ts +4 -0
  14. package/dist/client/hydrate.d.ts.map +1 -0
  15. package/dist/client/hydrate.js +80 -0
  16. package/dist/client/hydrate.js.map +1 -0
  17. package/dist/core/bundler.d.ts +7 -0
  18. package/dist/core/bundler.d.ts.map +1 -0
  19. package/dist/core/bundler.js +103 -0
  20. package/dist/core/bundler.js.map +1 -0
  21. package/dist/core/formatter.d.ts +6 -0
  22. package/dist/core/formatter.d.ts.map +1 -0
  23. package/dist/core/formatter.js +74 -0
  24. package/dist/core/formatter.js.map +1 -0
  25. package/dist/core/island-client.d.ts +9 -0
  26. package/dist/core/island-client.d.ts.map +1 -0
  27. package/dist/core/island-client.js +8 -0
  28. package/dist/core/island-client.js.map +1 -0
  29. package/dist/core/island-registry.d.ts +15 -0
  30. package/dist/core/island-registry.d.ts.map +1 -0
  31. package/dist/core/island-registry.js +17 -0
  32. package/dist/core/island-registry.js.map +1 -0
  33. package/dist/core/island.d.ts +32 -0
  34. package/dist/core/island.d.ts.map +1 -0
  35. package/dist/core/island.js +47 -0
  36. package/dist/core/island.js.map +1 -0
  37. package/dist/core/loader.d.ts +11 -0
  38. package/dist/core/loader.d.ts.map +1 -0
  39. package/dist/core/loader.js +73 -0
  40. package/dist/core/loader.js.map +1 -0
  41. package/dist/core/renderer.d.ts +20 -0
  42. package/dist/core/renderer.d.ts.map +1 -0
  43. package/dist/core/renderer.js +53 -0
  44. package/dist/core/renderer.js.map +1 -0
  45. package/dist/index.d.ts +23 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +104 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/server/dev-server.d.ts +8 -0
  50. package/dist/server/dev-server.d.ts.map +1 -0
  51. package/dist/server/dev-server.js +112 -0
  52. package/dist/server/dev-server.js.map +1 -0
  53. package/dist/server/engine.d.ts +46 -0
  54. package/dist/server/engine.d.ts.map +1 -0
  55. package/dist/server/engine.js +222 -0
  56. package/dist/server/engine.js.map +1 -0
  57. package/{src/server/index.ts → dist/server/index.d.ts} +1 -1
  58. package/dist/server/index.d.ts.map +1 -0
  59. package/dist/server/index.js +3 -0
  60. package/dist/server/index.js.map +1 -0
  61. package/package.json +4 -1
  62. package/.react-html/cache/page-fefe905ae5d2.mjs +0 -9
  63. package/DOCS.md +0 -317
  64. package/example/public/styles.css +0 -184
  65. package/example/src/components/Card.tsx +0 -17
  66. package/example/src/components/Counter.island.tsx +0 -35
  67. package/example/src/components/Footer.tsx +0 -13
  68. package/example/src/components/Header.tsx +0 -20
  69. package/example/src/components/Layout.tsx +0 -39
  70. package/example/src/components/Tabs.island.tsx +0 -59
  71. package/example/src/pages/index.tsx +0 -49
  72. package/example/src/pages/pure.tsx +0 -56
  73. package/src/cli/docs-content.ts +0 -268
  74. package/src/cli/index.ts +0 -113
  75. package/src/cli/tui-docs.ts +0 -214
  76. package/src/client/hydrate.ts +0 -84
  77. package/src/core/bundler.ts +0 -122
  78. package/src/core/formatter.ts +0 -92
  79. package/src/core/island-client.ts +0 -23
  80. package/src/core/island-registry.ts +0 -35
  81. package/src/core/island.tsx +0 -110
  82. package/src/core/loader.ts +0 -89
  83. package/src/core/renderer.ts +0 -80
  84. package/src/index.ts +0 -138
  85. package/src/server/dev-server.ts +0 -125
  86. package/src/server/engine.ts +0 -270
  87. package/tests/engine.test.mjs +0 -288
  88. package/tsconfig.json +0 -21
@@ -1,214 +0,0 @@
1
- import readline from 'node:readline';
2
- import { DOC_SECTIONS, DocSection } from './docs-content.js';
3
-
4
- // ANSI escape sequences
5
- const CLEAR = '\x1b[2J\x1b[H';
6
- const HIDE_CURSOR = '\x1b[?25l';
7
- const SHOW_CURSOR = '\x1b[?25h';
8
- const BOLD = '\x1b[1m';
9
- const DIM = '\x1b[2m';
10
- const RESET = '\x1b[0m';
11
- const CYAN = '\x1b[36m';
12
- const GREEN = '\x1b[32m';
13
- const YELLOW = '\x1b[33m';
14
- const BLUE = '\x1b[34m';
15
- const MAGENTA = '\x1b[35m';
16
- const INVERT = '\x1b[7m';
17
-
18
- function formatLine(line: string): string {
19
- if (line.startsWith('# ')) {
20
- return `${BOLD}${CYAN}${line}${RESET}`;
21
- }
22
- if (line.startsWith('* ')) {
23
- return `${BOLD}${GREEN}${line}${RESET}`;
24
- }
25
- if (line.startsWith('//') || line.startsWith(' $ ')) {
26
- return `${YELLOW}${line}${RESET}`;
27
- }
28
- if (line.includes('├──') || line.includes('└──') || line.includes('│')) {
29
- return `${DIM}${line}${RESET}`;
30
- }
31
- return line;
32
- }
33
-
34
- export function printSectionDirect(section: DocSection): void {
35
- console.log(`\n${BOLD}${CYAN}=== ${section.title} ===${RESET}\n`);
36
- for (const line of section.content) {
37
- console.log(formatLine(line));
38
- }
39
- console.log('');
40
- }
41
-
42
- export function printAllDocs(): void {
43
- console.log(`\n${BOLD}${CYAN}====================================================${RESET}`);
44
- console.log(`${BOLD}${CYAN} ⚛️ REACT-HTML COMPLETE MANUAL ${RESET}`);
45
- console.log(`${BOLD}${CYAN}====================================================${RESET}\n`);
46
- for (const sec of DOC_SECTIONS) {
47
- printSectionDirect(sec);
48
- console.log(`${DIM}${'─'.repeat(60)}${RESET}\n`);
49
- }
50
- }
51
-
52
- export async function launchDocsTui(initialArg?: string): Promise<void> {
53
- // If specific section or --all requested non-interactively
54
- if (initialArg === '--all' || initialArg === '-a') {
55
- printAllDocs();
56
- return;
57
- }
58
-
59
- if (initialArg) {
60
- const match = DOC_SECTIONS.find(
61
- (s, idx) =>
62
- s.id.toLowerCase() === initialArg.toLowerCase() ||
63
- String(idx + 1) === initialArg ||
64
- s.title.toLowerCase().includes(initialArg.toLowerCase())
65
- );
66
- if (match) {
67
- printSectionDirect(match);
68
- return;
69
- }
70
- }
71
-
72
- // Non-TTY fallback
73
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
74
- printAllDocs();
75
- return;
76
- }
77
-
78
- let selectedIndex = 0;
79
- let viewMode: 'menu' | 'reader' = 'menu';
80
- let scrollOffset = 0;
81
-
82
- readline.emitKeypressEvents(process.stdin);
83
- process.stdin.setRawMode(true);
84
- process.stdout.write(HIDE_CURSOR);
85
-
86
- function cleanup() {
87
- process.stdout.write(SHOW_CURSOR);
88
- if (process.stdin.isTTY) {
89
- process.stdin.setRawMode(false);
90
- }
91
- process.stdin.pause();
92
- console.clear();
93
- }
94
-
95
- function renderMenu() {
96
- const termWidth = Math.max(process.stdout.columns || 80, 60);
97
- const border = '═'.repeat(Math.min(termWidth - 2, 70));
98
-
99
- let out = CLEAR;
100
- out += `${CYAN}╔${border}╗${RESET}\n`;
101
- out += `${CYAN}║${BOLD} ⚛️ REACT-HTML TERMINAL DOCUMENTATION (TUI) ${RESET}${CYAN}║${RESET}\n`;
102
- out += `${CYAN}║${DIM} Modular pure HTML with selective reactive islands compilation ${RESET}${CYAN}║${RESET}\n`;
103
- out += `${CYAN}╚${border}╝${RESET}\n\n`;
104
-
105
- out += `${BOLD}Select a topic using arrow keys and press [Enter]:${RESET}\n\n`;
106
-
107
- DOC_SECTIONS.forEach((sec, idx) => {
108
- const isSelected = idx === selectedIndex;
109
- if (isSelected) {
110
- out += ` ${BOLD}${CYAN}▶ ${INVERT} ${sec.title} ${RESET}\n`;
111
- out += ` ${DIM}${sec.summary}${RESET}\n\n`;
112
- } else {
113
- out += ` ${BOLD}${sec.title}${RESET}\n`;
114
- out += ` ${DIM}${sec.summary}${RESET}\n\n`;
115
- }
116
- });
117
-
118
- out += `\n${DIM}${'─'.repeat(Math.min(termWidth - 2, 70))}${RESET}\n`;
119
- out += `${BOLD}[↑/↓ or j/k]${RESET} Navigate ${BOLD}[Enter]${RESET} Read ${BOLD}[q / Ctrl+C]${RESET} Exit\n`;
120
-
121
- process.stdout.write(out);
122
- }
123
-
124
- function renderReader() {
125
- const sec = DOC_SECTIONS[selectedIndex];
126
- const termHeight = Math.max(process.stdout.rows || 24, 15);
127
- const termWidth = Math.max(process.stdout.columns || 80, 60);
128
- const contentRows = termHeight - 8;
129
-
130
- const visibleLines = sec.content.slice(scrollOffset, scrollOffset + contentRows);
131
-
132
- let out = CLEAR;
133
- out += `${CYAN}${BOLD}⚛️ react-html :: ${sec.title}${RESET}\n`;
134
- out += `${DIM}${'═'.repeat(Math.min(termWidth - 2, 70))}${RESET}\n\n`;
135
-
136
- for (const line of visibleLines) {
137
- out += formatLine(line) + '\n';
138
- }
139
-
140
- // Fill blank lines if content is short
141
- for (let i = visibleLines.length; i < contentRows; i++) {
142
- out += '\n';
143
- }
144
-
145
- const maxScroll = Math.max(0, sec.content.length - contentRows);
146
- const scrollInfo = maxScroll > 0 ? ` [Line ${scrollOffset + 1}/${sec.content.length}]` : '';
147
-
148
- out += `\n${DIM}${'─'.repeat(Math.min(termWidth - 2, 70))}${RESET}\n`;
149
- out += `${BOLD}[↑/↓ or j/k]${RESET} Scroll${scrollInfo} ${BOLD}[b / Esc]${RESET} Back to Menu ${BOLD}[q]${RESET} Quit\n`;
150
-
151
- process.stdout.write(out);
152
- }
153
-
154
- function draw() {
155
- if (viewMode === 'menu') {
156
- renderMenu();
157
- } else {
158
- renderReader();
159
- }
160
- }
161
-
162
- draw();
163
-
164
- return new Promise((resolve) => {
165
- process.stdin.on('keypress', (str, key) => {
166
- if ((key.ctrl && key.name === 'c') || key.name === 'q') {
167
- cleanup();
168
- resolve();
169
- return;
170
- }
171
-
172
- if (viewMode === 'menu') {
173
- if (key.name === 'up' || key.name === 'k') {
174
- selectedIndex = (selectedIndex - 1 + DOC_SECTIONS.length) % DOC_SECTIONS.length;
175
- draw();
176
- } else if (key.name === 'down' || key.name === 'j') {
177
- selectedIndex = (selectedIndex + 1) % DOC_SECTIONS.length;
178
- draw();
179
- } else if (key.name === 'return' || key.name === 'space') {
180
- viewMode = 'reader';
181
- scrollOffset = 0;
182
- draw();
183
- }
184
- } else if (viewMode === 'reader') {
185
- const sec = DOC_SECTIONS[selectedIndex];
186
- const termHeight = Math.max(process.stdout.rows || 24, 15);
187
- const contentRows = termHeight - 8;
188
- const maxScroll = Math.max(0, sec.content.length - contentRows);
189
-
190
- if (key.name === 'escape' || key.name === 'b' || key.name === 'backspace') {
191
- viewMode = 'menu';
192
- draw();
193
- } else if (key.name === 'up' || key.name === 'k') {
194
- if (scrollOffset > 0) {
195
- scrollOffset--;
196
- draw();
197
- }
198
- } else if (key.name === 'down' || key.name === 'j') {
199
- if (scrollOffset < maxScroll) {
200
- scrollOffset++;
201
- draw();
202
- }
203
- } else if (key.name === 'pageup') {
204
- scrollOffset = Math.max(0, scrollOffset - 5);
205
- draw();
206
- } else if (key.name === 'pagedown') {
207
- scrollOffset = Math.min(maxScroll, scrollOffset + 5);
208
- draw();
209
- }
210
- }
211
- });
212
- });
213
- }
214
-
@@ -1,84 +0,0 @@
1
- import React, { ComponentType } from 'react';
2
- import { hydrateRoot, createRoot } from 'react-dom/client';
3
-
4
- export type ClientDirective = 'load' | 'idle' | 'visible';
5
-
6
- export function hydrateIsland(
7
- islandName: string,
8
- Component: ComponentType<any>
9
- ): void {
10
- if (typeof window === 'undefined') return;
11
-
12
- const mountElements = document.querySelectorAll<HTMLElement>(`[data-island="${islandName}"]`);
13
-
14
- mountElements.forEach((el) => {
15
- // Prevent double hydration
16
- if (el.hasAttribute('data-hydrated')) return;
17
-
18
- const rawProps = el.getAttribute('data-props');
19
- let props: Record<string, unknown> = {};
20
- if (rawProps) {
21
- try {
22
- props = JSON.parse(rawProps);
23
- } catch (err) {
24
- console.error(`[react-html] Failed to parse props for island "${islandName}":`, err);
25
- }
26
- }
27
-
28
- const directive = (el.getAttribute('data-client') || 'load') as ClientDirective;
29
-
30
- const doHydrate = () => {
31
- if (el.hasAttribute('data-hydrated')) return;
32
- el.setAttribute('data-hydrated', 'true');
33
-
34
- try {
35
- hydrateRoot(el, React.createElement(Component, props), {
36
- onRecoverableError(error) {
37
- // Suppress non-breaking hydration difference warnings
38
- console.debug(`[react-html] Island "${islandName}" hydration note:`, error);
39
- }
40
- });
41
- } catch (err) {
42
- console.warn(`[react-html] Hydration error on island "${islandName}", recovering with client mount:`, err);
43
- const root = createRoot(el);
44
- root.render(React.createElement(Component, props));
45
- }
46
- };
47
-
48
- switch (directive) {
49
- case 'idle': {
50
- if ('requestIdleCallback' in window) {
51
- (window as any).requestIdleCallback(doHydrate);
52
- } else {
53
- setTimeout(doHydrate, 200);
54
- }
55
- break;
56
- }
57
- case 'visible': {
58
- if ('IntersectionObserver' in window) {
59
- const observer = new IntersectionObserver((entries, obs) => {
60
- entries.forEach((entry) => {
61
- if (entry.isIntersecting) {
62
- obs.disconnect();
63
- doHydrate();
64
- }
65
- });
66
- });
67
- observer.observe(el);
68
- } else {
69
- doHydrate();
70
- }
71
- break;
72
- }
73
- case 'load':
74
- default: {
75
- if (document.readyState === 'loading') {
76
- window.addEventListener('DOMContentLoaded', doHydrate, { once: true });
77
- } else {
78
- doHydrate();
79
- }
80
- break;
81
- }
82
- }
83
- });
84
- }
@@ -1,122 +0,0 @@
1
- import * as esbuild from 'esbuild';
2
- import fs from 'node:fs/promises';
3
- import { existsSync } from 'node:fs';
4
- import path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { IslandInstance } from './island-registry.js';
7
-
8
- export interface BundlerOptions {
9
- outDir: string;
10
- minify?: boolean;
11
- }
12
-
13
- export async function bundleIslands(
14
- islands: IslandInstance[],
15
- options: BundlerOptions
16
- ): Promise<string[]> {
17
- const { outDir, minify = true } = options;
18
- const islandsOutputDir = path.join(outDir, 'assets', 'islands');
19
- await fs.mkdir(islandsOutputDir, { recursive: true });
20
-
21
- const tempDir = path.resolve(process.cwd(), '.react-html/temp-islands');
22
- await fs.mkdir(tempDir, { recursive: true });
23
-
24
- const uniqueIslandsMap = new Map<string, IslandInstance>();
25
- for (const island of islands) {
26
- if (!uniqueIslandsMap.has(island.name)) {
27
- uniqueIslandsMap.set(island.name, island);
28
- }
29
- }
30
-
31
- const bundledFiles: string[] = [];
32
-
33
- // Locate hydrate module
34
- const currentDir = import.meta.dirname || path.dirname(fileURLToPath(import.meta.url));
35
- let hydratePath = path.resolve(currentDir, '../client/hydrate.js');
36
- if (!existsSync(hydratePath)) {
37
- hydratePath = path.resolve(currentDir, '../client/hydrate.ts');
38
- }
39
-
40
- let islandClientPath = path.resolve(currentDir, './island-client.js');
41
- if (!existsSync(islandClientPath)) {
42
- islandClientPath = path.resolve(currentDir, './island-client.ts');
43
- }
44
-
45
- for (const [name, island] of uniqueIslandsMap.entries()) {
46
- let resolvedEntry = island.entryPath;
47
- if (resolvedEntry.startsWith('file://')) {
48
- resolvedEntry = fileURLToPath(resolvedEntry);
49
- } else {
50
- resolvedEntry = path.resolve(resolvedEntry);
51
- }
52
-
53
- try {
54
- await fs.access(resolvedEntry);
55
- } catch {
56
- console.warn(`[react-html] Warning: Island source file not found for "${name}" at "${resolvedEntry}". Skipping client bundle.`);
57
- continue;
58
- }
59
-
60
- // Create a virtual client entrypoint for this island
61
- const virtualEntryFile = path.join(tempDir, `${name}.entry.ts`);
62
- const virtualEntryCode = `
63
- import React from 'react';
64
- import { hydrateIsland } from ${JSON.stringify(hydratePath)};
65
- import * as Module from ${JSON.stringify(resolvedEntry)};
66
-
67
- const raw = Module.default || Module[${JSON.stringify(name)}] || Module;
68
- const Component = (raw && raw.__rawComponent) ? raw.__rawComponent : raw;
69
-
70
- hydrateIsland(${JSON.stringify(name)}, Component);
71
- `;
72
- await fs.writeFile(virtualEntryFile, virtualEntryCode, 'utf8');
73
-
74
- const outfile = path.join(islandsOutputDir, `${name}.js`);
75
-
76
- await esbuild.build({
77
- entryPoints: [virtualEntryFile],
78
- outfile,
79
- bundle: true,
80
- format: 'esm',
81
- platform: 'browser',
82
- target: ['es2022'],
83
- minify,
84
- treeShaking: true,
85
- define: {
86
- 'process.env.NODE_ENV': '"production"'
87
- },
88
- alias: {
89
- 'react-html': islandClientPath
90
- },
91
- plugins: [
92
- {
93
- name: 'redirect-react-html-imports',
94
- setup(build) {
95
- // Redirect any import of react-html or root index to island-client
96
- build.onResolve({ filter: /(react-html|(\/|\\)(src|dist)(\/|\\)index(\.js|\.mjs|\.ts)?)$/ }, () => {
97
- return { path: islandClientPath };
98
- });
99
- }
100
- }
101
- ],
102
- loader: {
103
- '.tsx': 'tsx',
104
- '.ts': 'ts',
105
- '.jsx': 'jsx',
106
- '.js': 'js',
107
- '.css': 'empty'
108
- }
109
- });
110
-
111
- bundledFiles.push(outfile);
112
- }
113
-
114
- // Cleanup virtual entry files
115
- try {
116
- await fs.rm(tempDir, { recursive: true, force: true });
117
- } catch {
118
- // Ignore cleanup error
119
- }
120
-
121
- return bundledFiles;
122
- }
@@ -1,92 +0,0 @@
1
- /**
2
- * Robust HTML formatter to output clean, readable HTML
3
- * Preserves exact inner markup inside reactive islands to ensure seamless React hydration.
4
- */
5
- export function formatHtml(html: string): string {
6
- // Extract and preserve island contents verbatim
7
- const preservedIslands: string[] = [];
8
- let clean = html.replace(
9
- /<!--__ISLAND_START__-->([\s\S]*?)<!--__ISLAND_END__-->/g,
10
- (_, inner) => {
11
- const idx = preservedIslands.length;
12
- preservedIslands.push(inner);
13
- return `%%REACT_HTML_ISLAND_${idx}%%`;
14
- }
15
- );
16
-
17
- // Strip React comment artifacts outside islands if any
18
- clean = clean.replace(/<!-- -->/g, '');
19
-
20
- const tokens = clean.split(/(<[^>]+>)/g).filter((t) => t.trim().length > 0);
21
- let formatted = '';
22
- let indent = 0;
23
- const indentStr = ' ';
24
-
25
- // Void tags that don't need closing or indentation changes
26
- const voidTags = new Set([
27
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
28
- 'link', 'meta', 'param', 'source', 'track', 'wbr', '!doctype'
29
- ]);
30
-
31
- // Tags where content is typically kept on the same line if brief
32
- const inlineTags = new Set([
33
- 'title', 'span', 'strong', 'em', 'b', 'i', 'code', 'small', 'sub', 'sup'
34
- ]);
35
-
36
- for (let i = 0; i < tokens.length; i++) {
37
- const token = tokens[i].trim();
38
-
39
- if (token.startsWith('<!DOCTYPE') || token.startsWith('<!doctype')) {
40
- formatted += token + '\n';
41
- continue;
42
- }
43
-
44
- if (token.startsWith('</')) {
45
- // Closing tag
46
- const match = token.match(/<\/([a-zA-Z0-9\-]+)/);
47
- const tagName = match ? match[1].toLowerCase() : '';
48
-
49
- indent = Math.max(0, indent - 1);
50
- formatted += indentStr.repeat(indent) + token + '\n';
51
- } else if (token.startsWith('<') && !token.startsWith('<!--')) {
52
- // Opening or self-closing tag
53
- const match = token.match(/<([a-zA-Z0-9\-]+)/);
54
- const tagName = match ? match[1].toLowerCase() : '';
55
- const isSelfClosing = token.endsWith('/>') || voidTags.has(tagName);
56
-
57
- // Check if next token is text and next-next token is closing tag
58
- if (
59
- inlineTags.has(tagName) &&
60
- i + 2 < tokens.length &&
61
- !tokens[i + 1].trim().startsWith('<') &&
62
- tokens[i + 2].trim().toLowerCase() === `</${tagName}>`
63
- ) {
64
- const text = tokens[i + 1].trim();
65
- const closeTag = tokens[i + 2].trim();
66
- formatted += indentStr.repeat(indent) + `${token}${text}${closeTag}\n`;
67
- i += 2;
68
- continue;
69
- }
70
-
71
- formatted += indentStr.repeat(indent) + token + '\n';
72
-
73
- if (!isSelfClosing) {
74
- indent++;
75
- }
76
- } else {
77
- // Standalone text content
78
- formatted += indentStr.repeat(indent) + token + '\n';
79
- }
80
- }
81
-
82
- // Restore preserved island markup untouched
83
- let finalHtml = formatted.replace(
84
- /%%REACT_HTML_ISLAND_(\d+)%%/g,
85
- (_, idxStr) => {
86
- const idx = parseInt(idxStr, 10);
87
- return preservedIslands[idx] ?? '';
88
- }
89
- );
90
-
91
- return finalHtml.trim() + '\n';
92
- }
@@ -1,23 +0,0 @@
1
- import React, { ComponentType, ReactNode } from 'react';
2
-
3
- export function Island<P extends Record<string, unknown>>({
4
- component: Component,
5
- props = {} as P,
6
- children
7
- }: {
8
- component: ComponentType<P>;
9
- props?: P;
10
- children?: ReactNode;
11
- [key: string]: unknown;
12
- }): React.ReactElement {
13
- return React.createElement(Component, props, children);
14
- }
15
-
16
- export function island<P extends Record<string, unknown>>(
17
- Component: ComponentType<P>,
18
- _sourcePath?: string,
19
- _customName?: string
20
- ): ComponentType<P> {
21
- return Component;
22
- }
23
-
@@ -1,35 +0,0 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
2
-
3
- export interface IslandInstance {
4
- id: string;
5
- name: string;
6
- entryPath: string;
7
- props: Record<string, unknown>;
8
- clientDirective?: 'load' | 'idle' | 'visible';
9
- }
10
-
11
- export interface RenderContext {
12
- pagePath: string;
13
- islands: IslandInstance[];
14
- }
15
-
16
- const GLOBAL_STORAGE_KEY = Symbol.for('__react_html_render_context__');
17
-
18
- const storage: AsyncLocalStorage<RenderContext> =
19
- (globalThis as any)[GLOBAL_STORAGE_KEY] ||
20
- ((globalThis as any)[GLOBAL_STORAGE_KEY] = new AsyncLocalStorage<RenderContext>());
21
-
22
- export function runWithRenderContext<T>(context: RenderContext, fn: () => T): T {
23
- return storage.run(context, fn);
24
- }
25
-
26
- export function getCurrentRenderContext(): RenderContext | undefined {
27
- return storage.getStore();
28
- }
29
-
30
- export function registerIslandInContext(island: IslandInstance): void {
31
- const ctx = storage.getStore();
32
- if (ctx) {
33
- ctx.islands.push(island);
34
- }
35
- }
@@ -1,110 +0,0 @@
1
- import React, { ComponentType, ReactNode } from 'react';
2
- import { renderToString } from 'react-dom/server';
3
- import { registerIslandInContext } from './island-registry.js';
4
-
5
- let islandCounter = 0;
6
-
7
- export interface IslandProps<P = Record<string, unknown>> {
8
- /** Explicit island name (optional) */
9
- name?: string;
10
- /** Path to the island component file (relative or absolute) */
11
- src?: string;
12
- /** The React component to render */
13
- component: ComponentType<P>;
14
- /** Props passed to the component */
15
- props?: P;
16
- /** Client hydration directive (default: 'load') */
17
- client?: 'load' | 'idle' | 'visible';
18
- /** Optional wrapper tag, default: 'div' */
19
- as?: string;
20
- className?: string;
21
- style?: React.CSSProperties;
22
- children?: ReactNode;
23
- }
24
-
25
- export function Island<P extends Record<string, unknown>>({
26
- name,
27
- src,
28
- component: Component,
29
- props = {} as P,
30
- client = 'load',
31
- as: Tag = 'div',
32
- className,
33
- style,
34
- children
35
- }: IslandProps<P>): React.ReactElement {
36
- const componentName =
37
- name ||
38
- (Component as any).__islandName ||
39
- Component.displayName ||
40
- Component.name ||
41
- 'AnonymousIsland';
42
-
43
- const islandId = `island-${++islandCounter}`;
44
-
45
- // If src is specified or attached to component
46
- const entryPath = src || (Component as any).__islandPath || '';
47
-
48
- registerIslandInContext({
49
- id: islandId,
50
- name: componentName,
51
- entryPath,
52
- props,
53
- clientDirective: client
54
- });
55
-
56
- const Wrapper = Tag as any;
57
-
58
- // Use renderToString for the island's inner tree to produce hydration-compatible SSR markup
59
- const innerHtml = renderToString(React.createElement(Component, props, children));
60
-
61
- return (
62
- <Wrapper
63
- data-island={componentName}
64
- data-island-id={islandId}
65
- data-props={JSON.stringify(props)}
66
- data-client={client}
67
- className={className}
68
- style={style}
69
- dangerouslySetInnerHTML={{ __html: `<!--__ISLAND_START__-->${innerHtml}<!--__ISLAND_END__-->` }}
70
- />
71
- );
72
- }
73
-
74
- /**
75
- * Helper to wrap any React component into a ready-to-use Island component
76
- * Example:
77
- * export const Counter = island(CounterComponent, import.meta.url, 'Counter');
78
- * // In page: <Counter initialCount={5} />
79
- */
80
- export function island<P extends Record<string, unknown>>(
81
- Component: ComponentType<P>,
82
- sourcePath: string,
83
- customName?: string
84
- ): ComponentType<P & { client?: 'load' | 'idle' | 'visible'; as?: string; className?: string; style?: React.CSSProperties }> {
85
- const resolvedName = customName || Component.displayName || Component.name || 'AnonymousIsland';
86
- (Component as any).__islandPath = sourcePath;
87
- (Component as any).__islandName = resolvedName;
88
-
89
- const IslandWrapper = (props: P & { client?: 'load' | 'idle' | 'visible'; as?: string; className?: string; style?: React.CSSProperties }) => {
90
- const { client, as, className, style, ...componentProps } = props;
91
- return (
92
- <Island
93
- name={resolvedName}
94
- src={sourcePath}
95
- component={Component}
96
- props={componentProps as P}
97
- client={client}
98
- as={as}
99
- className={className}
100
- style={style}
101
- />
102
- );
103
- };
104
-
105
- IslandWrapper.displayName = `Island(${resolvedName})`;
106
- (IslandWrapper as any).__islandPath = sourcePath;
107
- (IslandWrapper as any).__islandName = resolvedName;
108
- (IslandWrapper as any).__rawComponent = Component;
109
- return IslandWrapper;
110
- }