@barefootjs/hono 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/dist/adapter/hono-adapter.d.ts +141 -0
- package/dist/adapter/hono-adapter.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +632 -0
- package/dist/app.d.ts +131 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +139 -0
- package/dist/async.d.ts +15 -0
- package/dist/async.d.ts.map +1 -0
- package/dist/async.js +12 -0
- package/dist/build.d.ts +65 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +785 -0
- package/dist/client-shim.d.ts +59 -0
- package/dist/client-shim.d.ts.map +1 -0
- package/dist/client-shim.js +90 -0
- package/dist/dev-worker.d.ts +25 -0
- package/dist/dev-worker.d.ts.map +1 -0
- package/dist/dev-worker.js +65 -0
- package/dist/dev.d.ts +36 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +418 -0
- package/dist/dialog-context.d.ts +13 -0
- package/dist/dialog-context.d.ts.map +1 -0
- package/dist/dialog-context.js +10 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +632 -0
- package/dist/jsx/jsx-dev-runtime/index.d.ts +9 -0
- package/dist/jsx/jsx-dev-runtime/index.d.ts.map +1 -0
- package/dist/jsx/jsx-dev-runtime/index.js +6 -0
- package/dist/jsx/jsx-runtime/index.d.ts +32 -0
- package/dist/jsx/jsx-runtime/index.d.ts.map +1 -0
- package/dist/jsx/jsx-runtime/index.js +10 -0
- package/dist/portal-ssr.d.ts +22 -0
- package/dist/portal-ssr.d.ts.map +1 -0
- package/dist/portal-ssr.js +73 -0
- package/dist/portals.d.ts +26 -0
- package/dist/portals.d.ts.map +1 -0
- package/dist/portals.js +41 -0
- package/dist/preload.d.ts +56 -0
- package/dist/preload.d.ts.map +1 -0
- package/dist/preload.js +51 -0
- package/dist/scripts.d.ts +80 -0
- package/dist/scripts.d.ts.map +1 -0
- package/dist/scripts.js +198 -0
- package/dist/test-render.d.ts +28 -0
- package/dist/test-render.d.ts.map +1 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +16 -0
- package/package.json +116 -0
- package/src/__tests__/async.test.tsx +106 -0
- package/src/__tests__/bfscripts-entry-roots.test.tsx +135 -0
- package/src/__tests__/build.test.ts +299 -0
- package/src/__tests__/dev.test.tsx +123 -0
- package/src/__tests__/hydration-props-type.test.ts +141 -0
- package/src/__tests__/manifest-scripts.test.ts +87 -0
- package/src/__tests__/scaffold.test.ts +209 -0
- package/src/__tests__/ssr-context-bridge.test.ts +110 -0
- package/src/__tests__/string-literal-css-var-prop.test.ts +84 -0
- package/src/__tests__/stub-deps-scripts.test.ts +183 -0
- package/src/adapter/hono-adapter.ts +1114 -0
- package/src/adapter/index.ts +6 -0
- package/src/app.ts +220 -0
- package/src/async.tsx +55 -0
- package/src/build.ts +230 -0
- package/src/client-shim.ts +164 -0
- package/src/dev-worker.ts +93 -0
- package/src/dev.tsx +146 -0
- package/src/dialog-context.tsx +44 -0
- package/src/index.ts +26 -0
- package/src/jsx/jsx-dev-runtime/index.ts +9 -0
- package/src/jsx/jsx-runtime/index.ts +40 -0
- package/src/portal-ssr.tsx +92 -0
- package/src/portals.tsx +98 -0
- package/src/preload.tsx +166 -0
- package/src/scripts.tsx +220 -0
- package/src/test-render.ts +143 -0
- package/src/utils.ts +26 -0
package/src/preload.tsx
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// @jsxRuntime automatic
|
|
2
|
+
// @jsxImportSource hono/jsx
|
|
3
|
+
//
|
|
4
|
+
// Pragmas as line-comments above the JSDoc; see scripts.tsx for the rationale.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* BfPreload Component
|
|
8
|
+
*
|
|
9
|
+
* Renders modulepreload link tags in the document head for faster module loading.
|
|
10
|
+
* Modulepreload hints tell the browser to fetch and parse JavaScript modules early,
|
|
11
|
+
* reducing the critical path latency.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* ```tsx
|
|
15
|
+
* import { BfPreload } from '@barefootjs/hono/preload'
|
|
16
|
+
* import manifest from './dist/components/manifest.json'
|
|
17
|
+
*
|
|
18
|
+
* <html>
|
|
19
|
+
* <head>
|
|
20
|
+
* <BfPreload />
|
|
21
|
+
* {/* or with additional scripts *\/}
|
|
22
|
+
* <BfPreload scripts={['/static/components/button.js']} />
|
|
23
|
+
* {/* or with manifest-based dependency preloading *\/}
|
|
24
|
+
* <BfPreload manifest={manifest} components={['Button', 'TodoApp']} />
|
|
25
|
+
* </head>
|
|
26
|
+
* <body>
|
|
27
|
+
* {children}
|
|
28
|
+
* <BfScripts />
|
|
29
|
+
* </body>
|
|
30
|
+
* </html>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { Fragment } from 'hono/jsx'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Manifest entry type for dependency tracking.
|
|
38
|
+
*/
|
|
39
|
+
export interface ManifestEntry {
|
|
40
|
+
markedTemplate: string
|
|
41
|
+
clientJs?: string
|
|
42
|
+
props?: Array<{ name: string; type: string; optional: boolean }>
|
|
43
|
+
dependencies?: string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Manifest type mapping component names to their metadata.
|
|
48
|
+
*/
|
|
49
|
+
export type Manifest = Record<string, ManifestEntry>
|
|
50
|
+
|
|
51
|
+
export interface BfPreloadProps {
|
|
52
|
+
/**
|
|
53
|
+
* Path to static files directory.
|
|
54
|
+
* @default '/static'
|
|
55
|
+
*/
|
|
56
|
+
staticPath?: string
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Additional script URLs to preload.
|
|
60
|
+
* These are added in addition to the barefoot runtime.
|
|
61
|
+
*/
|
|
62
|
+
scripts?: string[]
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether to preload the barefoot runtime.
|
|
66
|
+
* @default true
|
|
67
|
+
*/
|
|
68
|
+
includeRuntime?: boolean
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Component manifest with dependency information.
|
|
72
|
+
* Used for automatic dependency chain preloading.
|
|
73
|
+
*/
|
|
74
|
+
manifest?: Manifest
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Component names to preload with their dependencies.
|
|
78
|
+
* Requires manifest to be provided.
|
|
79
|
+
*/
|
|
80
|
+
components?: string[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolves the full dependency chain for given components.
|
|
85
|
+
* Uses a visited set to prevent infinite loops from circular dependencies.
|
|
86
|
+
*
|
|
87
|
+
* @param components - Component names to resolve
|
|
88
|
+
* @param manifest - Component manifest with dependency information
|
|
89
|
+
* @param visited - Set of already visited component names (for cycle detection)
|
|
90
|
+
* @returns Array of clientJs paths for all dependencies
|
|
91
|
+
*/
|
|
92
|
+
function resolveDependencyChain(
|
|
93
|
+
components: string[],
|
|
94
|
+
manifest: Manifest,
|
|
95
|
+
visited = new Set<string>()
|
|
96
|
+
): string[] {
|
|
97
|
+
const result: string[] = []
|
|
98
|
+
|
|
99
|
+
for (const compName of components) {
|
|
100
|
+
if (visited.has(compName)) continue
|
|
101
|
+
visited.add(compName)
|
|
102
|
+
|
|
103
|
+
const entry = manifest[compName]
|
|
104
|
+
if (!entry) continue
|
|
105
|
+
|
|
106
|
+
// Add this component's clientJs
|
|
107
|
+
if (entry.clientJs) {
|
|
108
|
+
result.push(entry.clientJs)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Recursively add dependencies
|
|
112
|
+
if (entry.dependencies && entry.dependencies.length > 0) {
|
|
113
|
+
const childScripts = resolveDependencyChain(entry.dependencies, manifest, visited)
|
|
114
|
+
result.push(...childScripts)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Renders modulepreload link tags for BarefootJS scripts.
|
|
123
|
+
* Place this component in your <head> element.
|
|
124
|
+
*
|
|
125
|
+
* By default, preloads the barefoot.js runtime which is required
|
|
126
|
+
* by all BarefootJS components.
|
|
127
|
+
*
|
|
128
|
+
* When manifest and components props are provided, automatically
|
|
129
|
+
* preloads the full dependency chain for those components.
|
|
130
|
+
*/
|
|
131
|
+
export function BfPreload({
|
|
132
|
+
staticPath = '/static',
|
|
133
|
+
scripts = [],
|
|
134
|
+
includeRuntime = true,
|
|
135
|
+
manifest,
|
|
136
|
+
components = [],
|
|
137
|
+
}: BfPreloadProps = {}) {
|
|
138
|
+
const urls: string[] = []
|
|
139
|
+
|
|
140
|
+
// Always preload the barefoot runtime first (most critical)
|
|
141
|
+
if (includeRuntime) {
|
|
142
|
+
urls.push(`${staticPath}/components/barefoot.js`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Auto-preload component dependencies from manifest
|
|
146
|
+
if (manifest && components.length > 0) {
|
|
147
|
+
const dependencyScripts = resolveDependencyChain(components, manifest)
|
|
148
|
+
for (const script of dependencyScripts) {
|
|
149
|
+
urls.push(`${staticPath}/${script}`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Add additional scripts
|
|
154
|
+
urls.push(...scripts)
|
|
155
|
+
|
|
156
|
+
// Deduplicate URLs while preserving order
|
|
157
|
+
const uniqueUrls = [...new Set(urls)]
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<Fragment>
|
|
161
|
+
{uniqueUrls.map((url) => (
|
|
162
|
+
<link rel="modulepreload" href={url} />
|
|
163
|
+
))}
|
|
164
|
+
</Fragment>
|
|
165
|
+
)
|
|
166
|
+
}
|
package/src/scripts.tsx
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// @jsxRuntime automatic
|
|
2
|
+
// @jsxImportSource hono/jsx
|
|
3
|
+
//
|
|
4
|
+
// Both pragmas must stay as the first lines of the file. The runtime
|
|
5
|
+
// pragma enables the React-17-style import transform — without it
|
|
6
|
+
// esbuild drops the import-source pragma on the floor and the JSX
|
|
7
|
+
// compiles against the consumer's tsconfig runtime. In the scaffold's
|
|
8
|
+
// case that's `@barefootjs/hono/jsx` (the marked-template adapter),
|
|
9
|
+
// which doesn't register Hono's request context, so
|
|
10
|
+
// `useRequestContext()` throws, the catch below swallows it, and
|
|
11
|
+
// BfScripts returns null on every request — pages render server HTML
|
|
12
|
+
// but never hydrate. esbuild also only looks for these pragmas in the
|
|
13
|
+
// FIRST comment block in the file; a JSDoc above them silently wins.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* BfScripts Component
|
|
17
|
+
*
|
|
18
|
+
* Renders collected script tags at the end of the document body.
|
|
19
|
+
* BarefootJS components collect their script URLs during SSR render,
|
|
20
|
+
* and this component outputs them all at once to avoid DOM traversal issues.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* ```tsx
|
|
24
|
+
* import { BfScripts } from '@barefoot/hono/scripts'
|
|
25
|
+
*
|
|
26
|
+
* <html>
|
|
27
|
+
* <body>
|
|
28
|
+
* {children}
|
|
29
|
+
* <BfScripts manifest={manifest} base="/static/components/" />
|
|
30
|
+
* </body>
|
|
31
|
+
* </html>
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* Pass `manifest` + `base` to follow stub references emitted by the
|
|
35
|
+
* 'use client' import rewriter (#1241). Without those props the
|
|
36
|
+
* component falls back to the JSX-driven script set, which misses
|
|
37
|
+
* components reached only through imperative `createComponent()`
|
|
38
|
+
* stub calls — see issue #1243.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { useRequestContext } from 'hono/jsx-renderer'
|
|
42
|
+
import { Fragment } from 'hono/jsx'
|
|
43
|
+
import { relPathFromComponentsBase, type BarefootBuildManifest } from './app'
|
|
44
|
+
|
|
45
|
+
export type CollectedScript = {
|
|
46
|
+
src: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BfScriptsProps {
|
|
50
|
+
/**
|
|
51
|
+
* Build manifest from `dist/components/manifest.json`. When supplied
|
|
52
|
+
* alongside `base`, the component follows each rendered entry's
|
|
53
|
+
* `stubDeps` transitively and emits a `<script>` for every reachable
|
|
54
|
+
* `.client.js` — necessary for pages that only touch a child
|
|
55
|
+
* component through an imperative stub call (issue #1243).
|
|
56
|
+
*
|
|
57
|
+
* When omitted, behavior matches the pre-#1243 collector: only
|
|
58
|
+
* components whose SSR function executed get a script tag.
|
|
59
|
+
*/
|
|
60
|
+
manifest?: BarefootBuildManifest
|
|
61
|
+
/**
|
|
62
|
+
* URL base where the component bundles are served (e.g.
|
|
63
|
+
* `/static/components/`). Required when `manifest` is supplied —
|
|
64
|
+
* stubDep entries store dist-relative paths and the component
|
|
65
|
+
* needs the URL prefix to emit a working `<script src>`.
|
|
66
|
+
*/
|
|
67
|
+
base?: string
|
|
68
|
+
/**
|
|
69
|
+
* Extra manifest keys to treat as walk roots in addition to the
|
|
70
|
+
* SSR-rendered set. Use this for pages that mount a `'use client'`
|
|
71
|
+
* component via an inline `<script type="module">import "X.client.js"; render(root, "X", …)`
|
|
72
|
+
* instead of SSR'ing `<X />` directly. Without `entryRoots`, the
|
|
73
|
+
* walker has no anchor for `X`'s `stubDeps` and any sibling `'use
|
|
74
|
+
* client'` `.tsx` reached only through the imperative
|
|
75
|
+
* `createComponent` stub rewrite (#1240) never ships as a
|
|
76
|
+
* `<script>`, leaving the runtime registry empty and rendering
|
|
77
|
+
* `[ComponentName]` placeholders.
|
|
78
|
+
*
|
|
79
|
+
* The caller's inline `<script type="module">import …</script>`
|
|
80
|
+
* already loads the root bundle, so the root itself is *not*
|
|
81
|
+
* emitted as a separate `<script src>` — only the transitively
|
|
82
|
+
* reached `stubDeps` are. See #1431.
|
|
83
|
+
*/
|
|
84
|
+
entryRoots?: string[]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Renders all collected BarefootJS script tags.
|
|
89
|
+
* Place this component at the end of your <body> element.
|
|
90
|
+
*
|
|
91
|
+
* After rendering, sets 'bfScriptsRendered' flag to true.
|
|
92
|
+
* Components rendered after BfScripts (e.g., inside Suspense boundaries)
|
|
93
|
+
* will check this flag and output their scripts inline instead of
|
|
94
|
+
* collecting them here.
|
|
95
|
+
*/
|
|
96
|
+
export function BfScripts(props: BfScriptsProps = {}) {
|
|
97
|
+
try {
|
|
98
|
+
const c = useRequestContext()
|
|
99
|
+
|
|
100
|
+
// Mark that BfScripts has been rendered.
|
|
101
|
+
// Components rendered after this point (e.g., inside Suspense)
|
|
102
|
+
// should output their scripts inline.
|
|
103
|
+
c.set('bfScriptsRendered', true)
|
|
104
|
+
|
|
105
|
+
const scripts: CollectedScript[] = c.get('bfCollectedScripts') || []
|
|
106
|
+
const outputSet: Set<string> = c.get('bfOutputScripts') || new Set()
|
|
107
|
+
const { manifest, base, entryRoots } = props
|
|
108
|
+
// `entryRoots` extends both the walk-root set AND the `excluded` set.
|
|
109
|
+
// Walk-root so the root's `stubDeps` get visited (the manually-mounted
|
|
110
|
+
// component never SSR'd, so it isn't in `bfOutputScripts`). Excluded
|
|
111
|
+
// so the root's own `.client.js` isn't re-emitted — the caller's
|
|
112
|
+
// inline `<script type="module">import "X.client.js"; render(...)`
|
|
113
|
+
// already loaded it. See #1431.
|
|
114
|
+
const roots = entryRoots && entryRoots.length > 0
|
|
115
|
+
? new Set([...outputSet, ...entryRoots])
|
|
116
|
+
: outputSet
|
|
117
|
+
if (entryRoots) for (const r of entryRoots) outputSet.add(r)
|
|
118
|
+
const stubScripts = manifest && base
|
|
119
|
+
? collectStubDepScripts(manifest, base, roots, outputSet)
|
|
120
|
+
: new Map<string, CollectedScript>()
|
|
121
|
+
// Record stub-derived names on the same context flag so any later
|
|
122
|
+
// SSR pass (e.g. a Suspense boundary that renders after this point)
|
|
123
|
+
// won't double-emit the same `.client.js`. The flag's name keeps
|
|
124
|
+
// it symmetric with the snippet in `addScriptCollection`.
|
|
125
|
+
if (stubScripts.size > 0) c.set('bfOutputScripts', outputSet)
|
|
126
|
+
|
|
127
|
+
// Reverse script order so child components load before parents.
|
|
128
|
+
// During SSR, parent components render first and collect their scripts,
|
|
129
|
+
// then child components add their scripts. But for hydration, children
|
|
130
|
+
// need to register their templates before parents try to use createComponent().
|
|
131
|
+
// barefoot.js must stay first since it provides the runtime.
|
|
132
|
+
//
|
|
133
|
+
// Stub-derived scripts must come BEFORE the component scripts in the
|
|
134
|
+
// emitted order — `<script type="module">` tags evaluate in document
|
|
135
|
+
// order with microtask checkpoints between them, so a parent's
|
|
136
|
+
// `hydrate()`-scheduled walk fires (and its `init` calls
|
|
137
|
+
// `createComponent(...)` against the stub) before any later module
|
|
138
|
+
// script has registered. Within stubScripts, `collectStubDepScripts`
|
|
139
|
+
// already returns DFS post-order (deps before their dependent), so
|
|
140
|
+
// a chain A→B→C ships C, B, then the component bundle that stubs A.
|
|
141
|
+
const barefootScript = scripts.find(s => s.src.includes('barefoot.js'))
|
|
142
|
+
const componentScripts = scripts.filter(s => !s.src.includes('barefoot.js'))
|
|
143
|
+
const finalScripts = [
|
|
144
|
+
...(barefootScript ? [barefootScript] : []),
|
|
145
|
+
...stubScripts.values(),
|
|
146
|
+
...componentScripts.reverse(),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<Fragment>
|
|
151
|
+
{finalScripts.map(({ src }) => (
|
|
152
|
+
<script type="module" src={src} />
|
|
153
|
+
))}
|
|
154
|
+
</Fragment>
|
|
155
|
+
)
|
|
156
|
+
} catch {
|
|
157
|
+
// Context unavailable (e.g., not using jsxRenderer)
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Walk stub-rewrite edges from each manifest entry in `roots`,
|
|
164
|
+
* returning the script URLs for every transitively reachable
|
|
165
|
+
* `.client.js` in **DFS post-order** — every dep precedes its
|
|
166
|
+
* dependent in iteration order. Skips any name already present in
|
|
167
|
+
* `excluded` (these have a script tag elsewhere). Mutates `excluded`
|
|
168
|
+
* to record every dep that's been resolved so the caller can pass
|
|
169
|
+
* it to the next SSR pass without double-emitting. Exported for tests.
|
|
170
|
+
*
|
|
171
|
+
* Typical call shape: `roots` ⊆ `excluded`. The caller passes the
|
|
172
|
+
* set of components whose SSR function already pushed a `<script>`
|
|
173
|
+
* (`bfOutputScripts`) as BOTH arguments — "these are already
|
|
174
|
+
* emitted, now walk their stubDeps." A `roots` value already in
|
|
175
|
+
* `excluded` is still walked (we need its `stubDeps`); a `dep`
|
|
176
|
+
* already in `excluded` is recorded as visited but not re-emitted.
|
|
177
|
+
*
|
|
178
|
+
* Why post-order: `<script type="module">` tags evaluate in document
|
|
179
|
+
* order with microtask checkpoints between them, so the first
|
|
180
|
+
* script's `hydrate()`-scheduled walk fires before any later
|
|
181
|
+
* module loads. A chain A→B→C must ship C, B, then A's bundle —
|
|
182
|
+
* BFS order (B, C) would let B's hydration call
|
|
183
|
+
* `createComponent('C', ...)` against an empty registry. Post-order
|
|
184
|
+
* also handles DAG edges like A→B, A→C, C→B correctly
|
|
185
|
+
* (deepest-first, dependencies-first).
|
|
186
|
+
*
|
|
187
|
+
* Cycle-safe: a visited set short-circuits any A → B → A loop.
|
|
188
|
+
*/
|
|
189
|
+
export function collectStubDepScripts(
|
|
190
|
+
manifest: BarefootBuildManifest,
|
|
191
|
+
base: string,
|
|
192
|
+
roots: Iterable<string>,
|
|
193
|
+
excluded: Set<string>,
|
|
194
|
+
): Map<string, CollectedScript> {
|
|
195
|
+
const result = new Map<string, CollectedScript>()
|
|
196
|
+
const prefix = base.endsWith('/') ? base : base + '/'
|
|
197
|
+
const visited = new Set<string>()
|
|
198
|
+
|
|
199
|
+
function visit(name: string): void {
|
|
200
|
+
if (name === '__barefoot__') return
|
|
201
|
+
if (visited.has(name)) return
|
|
202
|
+
visited.add(name)
|
|
203
|
+
const entry = manifest[name]
|
|
204
|
+
if (entry?.stubDeps) {
|
|
205
|
+
// Recurse FIRST so deps are emitted before this node — that's
|
|
206
|
+
// what produces post-order. Cycles short-circuit at the
|
|
207
|
+
// `visited.has(name)` check at the top of each call.
|
|
208
|
+
for (const dep of entry.stubDeps) visit(dep)
|
|
209
|
+
}
|
|
210
|
+
if (excluded.has(name)) return
|
|
211
|
+
excluded.add(name)
|
|
212
|
+
if (entry?.clientJs) {
|
|
213
|
+
const src = prefix + relPathFromComponentsBase(entry.clientJs)
|
|
214
|
+
result.set(name, { src })
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const name of roots) visit(name)
|
|
219
|
+
return result
|
|
220
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono test renderer
|
|
3
|
+
*
|
|
4
|
+
* Compiles JSX source with HonoAdapter and renders to HTML via Hono's app.request().
|
|
5
|
+
* Used by adapter-tests conformance runner.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { compileJSX } from '@barefootjs/jsx'
|
|
9
|
+
import type { TemplateAdapter } from '@barefootjs/jsx'
|
|
10
|
+
import { Hono } from 'hono'
|
|
11
|
+
import { mkdir, rm } from 'node:fs/promises'
|
|
12
|
+
import { resolve } from 'node:path'
|
|
13
|
+
|
|
14
|
+
// Place temp files inside the hono package so hono/jsx resolves correctly
|
|
15
|
+
const RENDER_TEMP_DIR = resolve(import.meta.dir, '../.render-temp')
|
|
16
|
+
|
|
17
|
+
export interface RenderOptions {
|
|
18
|
+
/** JSX source code */
|
|
19
|
+
source: string
|
|
20
|
+
/** Template adapter to use */
|
|
21
|
+
adapter: TemplateAdapter
|
|
22
|
+
/** Props to inject (optional) */
|
|
23
|
+
props?: Record<string, unknown>
|
|
24
|
+
/** Additional component files (filename → source) */
|
|
25
|
+
components?: Record<string, string>
|
|
26
|
+
/**
|
|
27
|
+
* Explicit component to render when the source declares multiple
|
|
28
|
+
* exports. When omitted, the first function-valued export in
|
|
29
|
+
* `Object.keys(mod)` iteration order is picked — that order is
|
|
30
|
+
* alphabetical for dynamically imported ES modules in Bun/V8, so
|
|
31
|
+
* relying on declaration order can pick the wrong component
|
|
32
|
+
* (e.g. `PropsReactivityComparison` before `ReactiveProps`).
|
|
33
|
+
*/
|
|
34
|
+
componentName?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function renderHonoComponent(options: RenderOptions): Promise<string> {
|
|
38
|
+
const { source, adapter, props, components, componentName: requestedName } = options
|
|
39
|
+
|
|
40
|
+
// Compile child components first
|
|
41
|
+
const childCodes: string[] = []
|
|
42
|
+
const componentKeys = new Set<string>()
|
|
43
|
+
if (components) {
|
|
44
|
+
for (const [filename, childSource] of Object.entries(components)) {
|
|
45
|
+
componentKeys.add(filename)
|
|
46
|
+
const childResult = compileJSX(childSource, filename, { adapter })
|
|
47
|
+
const childErrors = childResult.errors.filter(e => e.severity === 'error')
|
|
48
|
+
if (childErrors.length > 0) {
|
|
49
|
+
throw new Error(`Compilation errors in ${filename}:\n${childErrors.map(e => e.message).join('\n')}`)
|
|
50
|
+
}
|
|
51
|
+
const childTemplate = childResult.files.find(f => f.type === 'markedTemplate')
|
|
52
|
+
if (!childTemplate) throw new Error(`No marked template for ${filename}`)
|
|
53
|
+
// Strip export keywords so only the parent component is exported
|
|
54
|
+
const localCode = childTemplate.content.replace(/\bexport\s+(default\s+)?/g, '')
|
|
55
|
+
childCodes.push(localCode)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Compile parent source
|
|
60
|
+
const result = compileJSX(source, 'component.tsx', { adapter })
|
|
61
|
+
|
|
62
|
+
const errors = result.errors.filter(e => e.severity === 'error')
|
|
63
|
+
if (errors.length > 0) {
|
|
64
|
+
throw new Error(`Compilation errors:\n${errors.map(e => e.message).join('\n')}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const templateFile = result.files.find(f => f.type === 'markedTemplate')
|
|
68
|
+
if (!templateFile) throw new Error('No marked template in compile output')
|
|
69
|
+
|
|
70
|
+
let parentCode = templateFile.content
|
|
71
|
+
// Strip import lines that reference component files
|
|
72
|
+
if (componentKeys.size > 0) {
|
|
73
|
+
parentCode = parentCode
|
|
74
|
+
.split('\n')
|
|
75
|
+
.filter(line => {
|
|
76
|
+
const importMatch = line.match(/^\s*import\s+.*from\s+['"](.+?)['"]/)
|
|
77
|
+
if (!importMatch) return true
|
|
78
|
+
const importPath = importMatch[1]
|
|
79
|
+
// Match against component keys: './badge' matches './badge.tsx'
|
|
80
|
+
for (const key of componentKeys) {
|
|
81
|
+
const keyWithoutExt = key.replace(/\.tsx?$/, '')
|
|
82
|
+
if (importPath === keyWithoutExt || importPath === key) return false
|
|
83
|
+
}
|
|
84
|
+
return true
|
|
85
|
+
})
|
|
86
|
+
.join('\n')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Combine: JSX pragma + child compiled functions + parent compiled code
|
|
90
|
+
const codeParts = ['/** @jsxImportSource hono/jsx */']
|
|
91
|
+
for (const childCode of childCodes) {
|
|
92
|
+
codeParts.push(childCode)
|
|
93
|
+
}
|
|
94
|
+
codeParts.push(parentCode)
|
|
95
|
+
const code = codeParts.join('\n')
|
|
96
|
+
|
|
97
|
+
await mkdir(RENDER_TEMP_DIR, { recursive: true })
|
|
98
|
+
// Unique filename per render to avoid Bun's process-level module cache
|
|
99
|
+
// (bun#12371: re-importing the same path returns stale module)
|
|
100
|
+
const tempFile = resolve(
|
|
101
|
+
RENDER_TEMP_DIR,
|
|
102
|
+
`render-${Date.now()}-${Math.random().toString(36).slice(2)}.tsx`,
|
|
103
|
+
)
|
|
104
|
+
await Bun.write(tempFile, code)
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const mod = await import(tempFile)
|
|
108
|
+
|
|
109
|
+
// Explicit `componentName` wins; otherwise pick the first
|
|
110
|
+
// function-valued export. `Object.keys` for dynamically imported
|
|
111
|
+
// modules iterates alphabetically in Bun/V8, so the fallback can
|
|
112
|
+
// surprise multi-component files — pass `componentName` to pin.
|
|
113
|
+
let resolvedName: string | undefined = requestedName
|
|
114
|
+
if (resolvedName) {
|
|
115
|
+
if (typeof mod[resolvedName] !== 'function') {
|
|
116
|
+
const available = Object.keys(mod).filter(k => typeof mod[k] === 'function')
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Requested component "${resolvedName}" not found in compiled module. Available: ${available.join(', ')}`,
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
resolvedName = Object.keys(mod).find(k => typeof mod[k] === 'function')
|
|
123
|
+
if (!resolvedName) throw new Error('No component function found in compiled module')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const Component = mod[resolvedName]
|
|
127
|
+
|
|
128
|
+
// Render using Hono's app.request()
|
|
129
|
+
const app = new Hono()
|
|
130
|
+
app.get('/', (c) =>
|
|
131
|
+
c.html(Component({ __instanceId: 'test', __bfChild: false, ...props })),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const res = await app.request('/')
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
const body = await res.text()
|
|
137
|
+
throw new Error(`Render failed with status ${res.status}: ${body}`)
|
|
138
|
+
}
|
|
139
|
+
return await res.text()
|
|
140
|
+
} finally {
|
|
141
|
+
await rm(tempFile, { force: true }).catch(() => {})
|
|
142
|
+
}
|
|
143
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { raw } from 'hono/html'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Output HTML comment marker for conditional reconciliation.
|
|
5
|
+
* Same signature as Go template bfComment function.
|
|
6
|
+
*/
|
|
7
|
+
export function bfComment(key: string) {
|
|
8
|
+
return raw(`<!--bf-${key}-->`)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Output opening comment marker for reactive text expressions.
|
|
13
|
+
* Renders <!--bf:slotId-->
|
|
14
|
+
*/
|
|
15
|
+
export function bfText(slotId: string) {
|
|
16
|
+
return raw(`<!--bf:${slotId}-->`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Output closing comment marker for reactive text expressions.
|
|
21
|
+
* Renders <!--/-->
|
|
22
|
+
*/
|
|
23
|
+
export function bfTextEnd() {
|
|
24
|
+
return raw('<!--/-->')
|
|
25
|
+
}
|
|
26
|
+
|