-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: render children in template[data-hono-template] * refactor: exclude "children" in order to reduce serialized data size * feat: support async attribute on Script component * feat: re-create children from rendered template element * test: add tests for Server and Client Composition Patterns * test: update data for integration test * refactor: make createChildren async * refactor: `yarn format:fix` * refactor: import buildCreateChildrenFn dynamically only when needed * chore: `rm bun.lockb && bun install`
- Loading branch information
Showing
19 changed files
with
1,087 additions
and
899 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { Suspense, use } from 'hono/jsx/dom' | ||
import type { CreateElement, CreateChildren } from '../types.js' | ||
|
||
export const buildCreateChildrenFn = (createElement: CreateElement): CreateChildren => { | ||
const createChildren = async (childNodes: NodeListOf<ChildNode>): Promise<Node[]> => { | ||
const children = [] | ||
for (let i = 0; i < childNodes.length; i++) { | ||
const child = childNodes[i] as HTMLElement | ||
if (child.nodeType === 8) { | ||
// skip comments | ||
continue | ||
} else if (child.nodeType === 3) { | ||
// text node | ||
children.push(child.textContent) | ||
} else if (child.nodeName === 'TEMPLATE' && child.id.match(/(?:H|E):\d+/)) { | ||
const placeholderElement = document.createElement('hono-placeholder') | ||
placeholderElement.style.display = 'none' | ||
|
||
let resolve: (nodes: Node[]) => void | ||
const promise = new Promise<Node[]>((r) => (resolve = r)) | ||
|
||
// Suspense: replace content by `replaceWith` when resolved | ||
// ErrorBoundary: replace content by `replaceWith` when error | ||
child.replaceWith = (node: DocumentFragment) => { | ||
createChildren(node.childNodes).then(resolve) | ||
placeholderElement.remove() | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
let fallback: any = [] | ||
|
||
// gather fallback content and find placeholder comment | ||
for ( | ||
// equivalent to i++ | ||
placeholderElement.appendChild(child); | ||
i < childNodes.length; | ||
i++ | ||
) { | ||
const child = childNodes[i] | ||
if (child.nodeType === 8) { | ||
// <!--/$--> or <!--E:1--> | ||
placeholderElement.appendChild(child) | ||
i-- | ||
break | ||
} else if (child.nodeType === 3) { | ||
fallback.push(child.textContent) | ||
} else { | ||
fallback.push( | ||
await createElement(child.nodeName, { | ||
children: await createChildren(child.childNodes), | ||
}) | ||
) | ||
} | ||
} | ||
|
||
// if already resolved or error, get content from added template element | ||
const fallbackTemplates = document.querySelectorAll<HTMLTemplateElement>( | ||
`[data-hono-target="${child.id}"]` | ||
) | ||
if (fallbackTemplates.length > 0) { | ||
const fallbackTemplate = fallbackTemplates[fallbackTemplates.length - 1] | ||
fallback = await createChildren(fallbackTemplate.content.childNodes) | ||
} | ||
|
||
// if no content available, wait for ErrorBoundary fallback content | ||
if (fallback.length === 0 && child.id.startsWith('E:')) { | ||
let resolve: (nodes: Node[]) => void | ||
const promise = new Promise<Node[]>((r) => (resolve = r)) | ||
fallback = await createElement(Suspense, { | ||
fallback: [], | ||
children: [await createElement(() => use(promise), {})], | ||
}) | ||
placeholderElement.insertBefore = ((node: DocumentFragment) => { | ||
createChildren(node.childNodes).then(resolve) | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
}) as any | ||
} | ||
|
||
// wait for content to be resolved by placeholderElement | ||
document.body.appendChild(placeholderElement) | ||
|
||
// render fallback content | ||
children.push( | ||
await createElement(Suspense, { | ||
fallback, | ||
children: [await createElement(() => use(promise), {})], | ||
}) | ||
) | ||
} else { | ||
children.push( | ||
await createElement(child.nodeName, { | ||
children: await createChildren(child.childNodes), | ||
}) | ||
) | ||
} | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return children as any | ||
} | ||
|
||
return createChildren | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export const COMPONENT_NAME = 'component-name' | ||
export const DATA_SERIALIZED_PROPS = 'data-serialized-props' | ||
export const DATA_HONO_TEMPLATE = 'data-hono-template' | ||
export const IMPORTING_ISLANDS_ID = '__importing_islands' as const |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { jsxRenderer } from 'hono/jsx-renderer' | ||
import { Script } from '../../../../src/server' | ||
|
||
export default jsxRenderer( | ||
({ children }) => { | ||
return ( | ||
<html> | ||
<body> | ||
{children} | ||
<Script | ||
async={true} | ||
src='/app/client.ts' | ||
prod={true} | ||
manifest={{ | ||
'app/client.ts': { | ||
file: 'static/client-abc.js', | ||
}, | ||
}} | ||
/> | ||
</body> | ||
</html> | ||
) | ||
}, | ||
{ | ||
docType: false, | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
import { createClient } from '../../../src/client' | ||
|
||
createClient() | ||
|
||
setTimeout(() => { | ||
document.body.setAttribute('data-client-loaded', 'true') | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,21 @@ | ||
import type { PropsWithChildren } from 'hono/jsx' | ||
import { useState } from 'hono/jsx' | ||
|
||
export default function Counter({ initial }: { initial: number }) { | ||
export default function Counter({ | ||
children, | ||
initial = 0, | ||
id = '', | ||
}: PropsWithChildren<{ | ||
initial?: number | ||
id?: string | ||
}>) { | ||
const [count, setCount] = useState(initial) | ||
const increment = () => setCount(count + 1) | ||
return ( | ||
<div> | ||
<div id={id}> | ||
<p>Count: {count}</p> | ||
<button onClick={increment}>Increment</button> | ||
{children} | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,21 @@ | ||
import { jsxRenderer } from 'hono/jsx-renderer' | ||
import { HasIslands } from '../../../../src/server' | ||
|
||
export default jsxRenderer(({ children, title }) => { | ||
return ( | ||
<html> | ||
<head> | ||
<title>{title}</title> | ||
<HasIslands> | ||
<script type='module' src='/app/client.ts'></script> | ||
</HasIslands> | ||
</head> | ||
<body>{children}</body> | ||
</html> | ||
) | ||
}) | ||
export default jsxRenderer( | ||
({ children, title }) => { | ||
return ( | ||
<html> | ||
<head> | ||
<title>{title}</title> | ||
</head> | ||
<body> | ||
{children} | ||
<HasIslands> | ||
<script type='module' async src='/app/client.ts'></script> | ||
</HasIslands> | ||
</body> | ||
</html> | ||
) | ||
}, | ||
{ stream: true } | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import Counter from '../../islands/Counter' | ||
|
||
const AsyncChild = async () => { | ||
return <span>Async child</span> | ||
} | ||
|
||
export default function Interaction() { | ||
return ( | ||
<> | ||
<Counter id='sync'> | ||
<span>Sync child</span> | ||
</Counter> | ||
<Counter id='async' initial={2}> | ||
<AsyncChild /> | ||
</Counter> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { Suspense, ErrorBoundary } from 'hono/jsx' | ||
import Counter from '../../islands/Counter' | ||
|
||
const SuspenseChild = async () => { | ||
await new Promise<void>((resolve) => setTimeout(() => resolve(), 500)) | ||
return <span>Suspense child</span> | ||
} | ||
|
||
const SuspenseFailureChild = async () => { | ||
throw new Error('Suspense failure') | ||
return <span>Suspense child</span> | ||
} | ||
|
||
export default function Interaction() { | ||
return ( | ||
<> | ||
<Counter id='error-boundary-success' initial={2}> | ||
<ErrorBoundary fallback={<span>Something went wrong</span>}> | ||
<Suspense fallback={<span>Loading...</span>}> | ||
<SuspenseChild /> | ||
</Suspense> | ||
</ErrorBoundary> | ||
</Counter> | ||
<Counter id='error-boundary-failure' initial={4}> | ||
<ErrorBoundary fallback={<span>Something went wrong</span>}> | ||
<Suspense fallback={<span>Loading...</span>}> | ||
<SuspenseFailureChild /> | ||
</Suspense> | ||
</ErrorBoundary> | ||
</Counter> | ||
</> | ||
) | ||
} |
Oops, something went wrong.