@barefootjs/hono 0.30.6 → 0.31.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 +43 -6
- package/dist/adapter/hono-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +44 -8
- package/dist/app.d.ts +11 -55
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +1 -13
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -8
- package/dist/preload.d.ts +10 -1
- package/dist/preload.d.ts.map +1 -1
- package/dist/render.d.ts +1 -1
- package/dist/scripts.d.ts +76 -0
- package/dist/scripts.d.ts.map +1 -1
- package/dist/scripts.js +81 -17
- package/dist/vite.d.ts +37 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +2749 -0
- package/package.json +19 -11
- package/src/__tests__/scaffold.test.ts +9 -2
- package/src/__tests__/script-assets.test.ts +228 -0
- package/src/__tests__/vite.test.ts +144 -0
- package/src/adapter/hono-adapter.ts +134 -15
- package/src/app.ts +12 -73
- package/src/index.ts +1 -1
- package/src/preload.tsx +15 -2
- package/src/render.ts +1 -1
- package/src/scripts.tsx +136 -0
- package/src/vite.ts +210 -0
- package/dist/build.d.ts +0 -65
- package/dist/build.d.ts.map +0 -1
- package/dist/build.js +0 -188137
- package/dist/dev.d.ts +0 -36
- package/dist/dev.d.ts.map +0 -1
- package/dist/dev.js +0 -508
- package/src/__tests__/build.test.ts +0 -299
- package/src/__tests__/dev.test.tsx +0 -123
- package/src/__tests__/import-map.test.ts +0 -98
- package/src/build.ts +0 -230
- package/src/dev.tsx +0 -154
|
@@ -69,11 +69,10 @@ export interface HonoAdapterOptions {
|
|
|
69
69
|
clientJsFilename?: string
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* Display name surfaced through `JsxAdapter.name
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* fact that CSR currently reuses HonoAdapter under the hood.
|
|
72
|
+
* Display name surfaced through `JsxAdapter.name`. Defaults to `'hono'`.
|
|
73
|
+
* (CSR mode no longer reuses `HonoAdapter` under the hood — see
|
|
74
|
+
* `@barefootjs/client/csr-adapter`'s own in-package `CSRAdapter` — so
|
|
75
|
+
* this option has no CSR-specific caller today.)
|
|
77
76
|
*/
|
|
78
77
|
name?: string
|
|
79
78
|
}
|
|
@@ -125,9 +124,6 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
125
124
|
name = 'hono'
|
|
126
125
|
extension = '.tsx'
|
|
127
126
|
clientShimSource = '@barefootjs/hono/client-shim'
|
|
128
|
-
// Importmap is injected at render time by the `BfImportMap` component
|
|
129
|
-
// (reads `barefoot-externals.json`), so `bf build` emits no static snippet.
|
|
130
|
-
importMapInjection = 'component' as const
|
|
131
127
|
|
|
132
128
|
// The Hono SSR runtime is JavaScript (Node / Bun / CF Workers), so any
|
|
133
129
|
// synchronous JS call the user writes can be rendered as-is at template
|
|
@@ -165,6 +161,28 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
165
161
|
private rewriteRelativeImport?: (importPath: string) => string
|
|
166
162
|
/** Stack of loop keys for generating data-key / data-key-1 attributes on loop items */
|
|
167
163
|
private loopKeyStack: Array<{ key: string | null; param: string }> = []
|
|
164
|
+
/**
|
|
165
|
+
* Per-call `AdapterGenerateOptions.scriptAssets`, stashed for the
|
|
166
|
+
* duration of one `generate()` call (same lifecycle as
|
|
167
|
+
* `rewriteRelativeImport`) so `generateImports`/`generateComponent`/
|
|
168
|
+
* `renderIfStatement` can all see it without threading it through every
|
|
169
|
+
* method signature. `undefined` means "the caller didn't resolve
|
|
170
|
+
* scriptAssets", so no script-registration codegen is emitted; `[]`
|
|
171
|
+
* means "resolved, and empty" (a server-only file, or a client file
|
|
172
|
+
* whose bundle isn't in the manifest yet); a non-empty array means
|
|
173
|
+
* "bake exactly these URLs in". See `AdapterGenerateOptions.
|
|
174
|
+
* scriptAssets`'s docstring for the full precedence contract this
|
|
175
|
+
* mirrors from `GoTemplateAdapter`.
|
|
176
|
+
*/
|
|
177
|
+
private scriptAssets?: string[]
|
|
178
|
+
/**
|
|
179
|
+
* Per-call `AdapterGenerateOptions.preloadAssets`, same stash-for-the-
|
|
180
|
+
* duration-of-one-`generate()`-call lifecycle as `scriptAssets`. Only
|
|
181
|
+
* consulted when `hasScriptAssets()` is also true — see
|
|
182
|
+
* `AdapterGenerateOptions.preloadAssets`'s docstring: preloads are only
|
|
183
|
+
* meaningful alongside a non-empty `scriptAssets`.
|
|
184
|
+
*/
|
|
185
|
+
private preloadAssets?: string[]
|
|
168
186
|
|
|
169
187
|
constructor(options: HonoAdapterOptions = {}) {
|
|
170
188
|
super()
|
|
@@ -180,6 +198,19 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
180
198
|
this.componentName = ir.metadata.componentName
|
|
181
199
|
this.isClientComponent = ir.metadata.isClientComponent
|
|
182
200
|
this.rewriteRelativeImport = options?.rewriteRelativeImport
|
|
201
|
+
// `skipScriptRegistration` wins over `scriptAssets`/`preloadAssets`
|
|
202
|
+
// unconditionally — see both fields' docstrings on
|
|
203
|
+
// `AdapterGenerateOptions`. Stashing `undefined` for both here (rather
|
|
204
|
+
// than gating each call site individually) makes every downstream
|
|
205
|
+
// `hasScriptAssets()`/`hasPreloadAssets()` check "just work" without
|
|
206
|
+
// its own skip check.
|
|
207
|
+
if (options?.skipScriptRegistration) {
|
|
208
|
+
this.scriptAssets = undefined
|
|
209
|
+
this.preloadAssets = undefined
|
|
210
|
+
} else {
|
|
211
|
+
this.scriptAssets = options?.scriptAssets
|
|
212
|
+
this.preloadAssets = options?.preloadAssets
|
|
213
|
+
}
|
|
183
214
|
|
|
184
215
|
// Generate component body FIRST so we can scan it for used imports
|
|
185
216
|
const component = this.generateComponent(ir)
|
|
@@ -216,9 +247,34 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
216
247
|
extension: this.extension,
|
|
217
248
|
}
|
|
218
249
|
this.rewriteRelativeImport = undefined
|
|
250
|
+
this.scriptAssets = undefined
|
|
251
|
+
this.preloadAssets = undefined
|
|
219
252
|
return result
|
|
220
253
|
}
|
|
221
254
|
|
|
255
|
+
/**
|
|
256
|
+
* True when `options.scriptAssets` was resolved AND non-empty for this
|
|
257
|
+
* `generate()` call — the single guard `generateImports`/
|
|
258
|
+
* `generateComponent`/`renderIfStatement` all consult before emitting
|
|
259
|
+
* script-registration codegen. See `scriptAssets`'s field docstring for
|
|
260
|
+
* why `undefined` and `[]` are both "no" here.
|
|
261
|
+
*/
|
|
262
|
+
private hasScriptAssets(): boolean {
|
|
263
|
+
return !!this.scriptAssets && this.scriptAssets.length > 0
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* True when `options.preloadAssets` was resolved AND non-empty for this
|
|
268
|
+
* `generate()` call, AND `hasScriptAssets()` also holds — see
|
|
269
|
+
* `AdapterGenerateOptions.preloadAssets`'s docstring: preloads are only
|
|
270
|
+
* meaningful alongside a non-empty `scriptAssets`. Consulted by
|
|
271
|
+
* `generateImports`/`generateComponent`/`renderIfStatement` alongside
|
|
272
|
+
* `hasScriptAssets()` before emitting preload-registration codegen.
|
|
273
|
+
*/
|
|
274
|
+
private hasPreloadAssets(): boolean {
|
|
275
|
+
return this.hasScriptAssets() && !!this.preloadAssets && this.preloadAssets.length > 0
|
|
276
|
+
}
|
|
277
|
+
|
|
222
278
|
private generateModuleLevelContextBindings(ir: ComponentIR): string {
|
|
223
279
|
const lines: string[] = []
|
|
224
280
|
for (const c of ir.metadata.localConstants) {
|
|
@@ -251,6 +307,18 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
251
307
|
lines.push(`import { ${utilImports.join(', ')} } from '@barefootjs/hono/utils'`)
|
|
252
308
|
}
|
|
253
309
|
|
|
310
|
+
// Only imported when this call's `scriptAssets` (see the field
|
|
311
|
+
// docstring, and `generateComponent`) actually resolved to a non-empty
|
|
312
|
+
// URL list, so a server-only file emits no dead import.
|
|
313
|
+
// `registerComponentPreloads` is imported alongside them only when
|
|
314
|
+
// `preloadAssets` also resolved non-empty (`hasPreloadAssets()`).
|
|
315
|
+
if (this.hasScriptAssets()) {
|
|
316
|
+
const names = this.hasPreloadAssets()
|
|
317
|
+
? ['registerComponentScripts', 'registerComponentPreloads', 'wrapWithInlineScripts']
|
|
318
|
+
: ['registerComponentScripts', 'wrapWithInlineScripts']
|
|
319
|
+
lines.push(`import { ${names.join(', ')} } from '@barefootjs/hono/scripts'`)
|
|
320
|
+
}
|
|
321
|
+
|
|
254
322
|
// Import Suspense / ErrorBoundary when async boundaries are used. Both
|
|
255
323
|
// are imported under `__Bf`-prefixed aliases (and emitted as such by
|
|
256
324
|
// `renderAsync`) so the generated tags can never collide with a user
|
|
@@ -580,6 +648,25 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
580
648
|
const exportPrefix = ir.metadata.isExported === false ? '' : 'export '
|
|
581
649
|
lines.push(`${exportPrefix}function ${name}(${fullPropsDestructure}${typeAnnotation}${noArgDefault}) {`)
|
|
582
650
|
|
|
651
|
+
// Vite-pipeline script registration (see `scriptAssets`'s field
|
|
652
|
+
// docstring): register this call's resolved URLs against the request
|
|
653
|
+
// context right away, independent of props — `__bfInlineScripts` is
|
|
654
|
+
// then threaded through every `return (...)` this function can reach
|
|
655
|
+
// (the plain tail return below, and both branches of an if-statement
|
|
656
|
+
// root via `renderIfStatement`) so a component rendered after
|
|
657
|
+
// `<BfScripts />` (e.g. inside Suspense) ships its scripts inline
|
|
658
|
+
// instead of silently dropping them from the already-flushed collector.
|
|
659
|
+
// `__bfInlinePreloads` (registered FIRST, so preloads flush ahead of
|
|
660
|
+
// scripts wherever `<BfScripts />` renders them) follows the exact
|
|
661
|
+
// same pattern for `preloadAssets` — see `AdapterGenerateOptions.
|
|
662
|
+
// preloadAssets` and `hasPreloadAssets()`.
|
|
663
|
+
if (this.hasPreloadAssets()) {
|
|
664
|
+
lines.push(` const __bfInlinePreloads = registerComponentPreloads(${JSON.stringify(this.preloadAssets)})`)
|
|
665
|
+
}
|
|
666
|
+
if (this.hasScriptAssets()) {
|
|
667
|
+
lines.push(` const __bfInlineScripts = registerComponentScripts(${JSON.stringify(this.scriptAssets)})`)
|
|
668
|
+
}
|
|
669
|
+
|
|
583
670
|
// Add props extraction for SolidJS-style pattern
|
|
584
671
|
if (propsExtraction) {
|
|
585
672
|
lines.push(propsExtraction)
|
|
@@ -627,9 +714,19 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
627
714
|
return lines.join('\n')
|
|
628
715
|
}
|
|
629
716
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
717
|
+
if (this.hasScriptAssets()) {
|
|
718
|
+
lines.push(` return wrapWithInlineScripts((`)
|
|
719
|
+
lines.push(` ${jsxBody}`)
|
|
720
|
+
lines.push(
|
|
721
|
+
this.hasPreloadAssets()
|
|
722
|
+
? ` ), __bfInlineScripts, __bfInlinePreloads)`
|
|
723
|
+
: ` ), __bfInlineScripts)`,
|
|
724
|
+
)
|
|
725
|
+
} else {
|
|
726
|
+
lines.push(` return (`)
|
|
727
|
+
lines.push(` ${jsxBody}`)
|
|
728
|
+
lines.push(` )`)
|
|
729
|
+
}
|
|
633
730
|
lines.push(`}`)
|
|
634
731
|
|
|
635
732
|
return lines.join('\n')
|
|
@@ -1032,11 +1129,27 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
1032
1129
|
// Render the consequent (then branch) JSX
|
|
1033
1130
|
const consequent = this.renderNode(ifStmt.consequent, ctx)
|
|
1034
1131
|
|
|
1132
|
+
// Every early-return branch below wraps with `wrapWithInlineScripts`
|
|
1133
|
+
// when this call's `scriptAssets` resolved non-empty — same
|
|
1134
|
+
// `__bfInlineScripts` binding `generateComponent` declares at the top
|
|
1135
|
+
// of the function, reused across every branch this if-chain can take.
|
|
1136
|
+
// `__bfInlinePreloads` (same declare-once-reuse-every-branch pattern)
|
|
1137
|
+
// is threaded in as a third argument when `preloadAssets` also
|
|
1138
|
+
// resolved non-empty.
|
|
1139
|
+
const wrap = this.hasScriptAssets()
|
|
1140
|
+
const wrapPreload = this.hasPreloadAssets()
|
|
1141
|
+
const openReturn = wrap ? ' return wrapWithInlineScripts((' : ' return ('
|
|
1142
|
+
const closeReturn = wrap
|
|
1143
|
+
? wrapPreload
|
|
1144
|
+
? ' ), __bfInlineScripts, __bfInlinePreloads)'
|
|
1145
|
+
: ' ), __bfInlineScripts)'
|
|
1146
|
+
: ' )'
|
|
1147
|
+
|
|
1035
1148
|
// Build the if statement
|
|
1036
1149
|
lines.unshift(` if (${ifStmt.condition}) {`)
|
|
1037
|
-
lines.push(
|
|
1150
|
+
lines.push(openReturn)
|
|
1038
1151
|
lines.push(` ${consequent}`)
|
|
1039
|
-
lines.push(
|
|
1152
|
+
lines.push(closeReturn)
|
|
1040
1153
|
lines.push(` }`)
|
|
1041
1154
|
|
|
1042
1155
|
// Handle the alternate (else branch)
|
|
@@ -1049,9 +1162,15 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
1049
1162
|
} else {
|
|
1050
1163
|
// Final else branch with regular JSX
|
|
1051
1164
|
const alternate = this.renderNode(ifStmt.alternate, ctx)
|
|
1052
|
-
lines.push(
|
|
1165
|
+
lines.push(wrap ? ' return wrapWithInlineScripts((' : ' return (')
|
|
1053
1166
|
lines.push(` ${alternate}`)
|
|
1054
|
-
lines.push(
|
|
1167
|
+
lines.push(
|
|
1168
|
+
wrap
|
|
1169
|
+
? wrapPreload
|
|
1170
|
+
? ' ), __bfInlineScripts, __bfInlinePreloads)'
|
|
1171
|
+
: ' ), __bfInlineScripts)'
|
|
1172
|
+
: ' )',
|
|
1173
|
+
)
|
|
1055
1174
|
}
|
|
1056
1175
|
} else {
|
|
1057
1176
|
// No alternate - return null
|
package/src/app.ts
CHANGED
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Two pieces:
|
|
11
11
|
*
|
|
12
|
-
* - **JSX components** (`<
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* are required props.
|
|
12
|
+
* - **JSX components** (`<BfScripts />`, `<BfDevReload />`) — return
|
|
13
|
+
* raw HTML the caller composes inside the Layout passed to Hono's
|
|
14
|
+
* `jsxRenderer`. All URL/data inputs are required props.
|
|
16
15
|
*
|
|
17
16
|
* - **Middleware** (`barefootDevReload`) — registers the SSE endpoint
|
|
18
17
|
* paired with `<BfDevReload />`. Both `endpoint` and `enabled` are
|
|
@@ -30,9 +29,6 @@ import type { MiddlewareHandler } from 'hono'
|
|
|
30
29
|
import { html, raw } from 'hono/html'
|
|
31
30
|
import type { HtmlEscapedString } from 'hono/utils/html'
|
|
32
31
|
import { useRequestContext } from 'hono/jsx-renderer'
|
|
33
|
-
// Zero-dependency subpath — keeps the compiler (and its `typescript` dep) out
|
|
34
|
-
// of this runtime/Workers-bundled module while sharing one importmap renderer.
|
|
35
|
-
import { renderImportMapHtml, type ImportMapManifest } from '@barefootjs/jsx/import-map'
|
|
36
32
|
import { createDevReloader } from './dev-worker.ts'
|
|
37
33
|
// Side-effect import: auto-wires request-scoped `searchParams()` for SSR so any
|
|
38
34
|
// Hono app rendering the BarefootJS scripts gets it without an opt-in step.
|
|
@@ -43,10 +39,10 @@ const DEV_RELOAD_ENDPOINT_KEY = 'bfDevReloadEndpoint'
|
|
|
43
39
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
44
40
|
|
|
45
41
|
/**
|
|
46
|
-
* Build manifest shape
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* `"components/Counter.client.js"`.
|
|
42
|
+
* Build manifest shape read by `manifestToScriptUrls` (and `BfScripts`
|
|
43
|
+
* below) to emit per-component `<script>` tags. Each compiled component
|
|
44
|
+
* is keyed by its manifest name; `__barefoot__` is the runtime entry.
|
|
45
|
+
* `clientJs` is a path under `dist/`, e.g. `"components/Counter.client.js"`.
|
|
50
46
|
*
|
|
51
47
|
* `stubDeps` lists the manifest keys of every `'use client'` sibling
|
|
52
48
|
* this bundle reaches via a stub rewrite (i.e. via an imperative
|
|
@@ -59,8 +55,7 @@ const DEV_RELOAD_ENDPOINT_KEY = 'bfDevReloadEndpoint'
|
|
|
59
55
|
* `ui/button/index.tsx`), not the runtime registry name passed to
|
|
60
56
|
* `createComponent(...)` (e.g. `"Button"`). For top-level
|
|
61
57
|
* single-component files the two coincide; for nested layouts they
|
|
62
|
-
* differ.
|
|
63
|
-
* writing this field.
|
|
58
|
+
* differ.
|
|
64
59
|
*/
|
|
65
60
|
export interface BarefootBuildManifest {
|
|
66
61
|
__barefoot__?: { clientJs?: string }
|
|
@@ -94,62 +89,6 @@ export function relPathFromComponentsBase(p: string): string {
|
|
|
94
89
|
|
|
95
90
|
// ── JSX components ─────────────────────────────────────────────────────────
|
|
96
91
|
|
|
97
|
-
export interface BfImportMapProps {
|
|
98
|
-
/** Base URL where the runtime + component bundles are served. */
|
|
99
|
-
base: string
|
|
100
|
-
/**
|
|
101
|
-
* Contents of `barefoot-externals.json` (import it and pass it
|
|
102
|
-
* through). Its `importmap.imports` are merged on top of the
|
|
103
|
-
* built-in `@barefootjs/client*` mappings so islands importing
|
|
104
|
-
* configured externals (e.g. `zod`, `@barefootjs/form`) resolve in
|
|
105
|
-
* the browser. When omitted, only the `@barefootjs/client*`
|
|
106
|
-
* mappings are emitted — the pre-#1639 behavior.
|
|
107
|
-
*
|
|
108
|
-
* Typed with the shared {@link ImportMapManifest} from `@barefootjs/jsx`,
|
|
109
|
-
* so the component and the `bf build` snippet path describe the manifest
|
|
110
|
-
* with one type.
|
|
111
|
-
*/
|
|
112
|
-
externals?: ImportMapManifest
|
|
113
|
-
/**
|
|
114
|
-
* Whether to also emit `<link rel="modulepreload">` for the
|
|
115
|
-
* manifest's `preloads`. Defaults to `true`; set `false` to emit
|
|
116
|
-
* the importmap only.
|
|
117
|
-
*/
|
|
118
|
-
preload?: boolean
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Emits the `<script type="importmap">` that maps the bare
|
|
123
|
-
* `@barefootjs/client` / `@barefootjs/client/runtime` specifiers to
|
|
124
|
-
* the runtime bundle, plus any externals from `barefoot-externals.json`
|
|
125
|
-
* passed via the `externals` prop. Also emits `<link rel="modulepreload">`
|
|
126
|
-
* for the manifest's `preloads` unless `preload` is `false`. Place in
|
|
127
|
-
* `<head>`.
|
|
128
|
-
*
|
|
129
|
-
* The merge of the `@barefootjs/client*` defaults (synthesized from `base`
|
|
130
|
-
* for prop-less / hand-written manifests) is Hono-specific; the actual HTML
|
|
131
|
-
* rendering — importmap JSON escaping, `<link rel="modulepreload">`
|
|
132
|
-
* emission with `crossorigin` (#1648) — is delegated to the shared
|
|
133
|
-
* `renderImportMapHtml` so this path can never drift from the static
|
|
134
|
-
* `barefoot-importmap.html` snippet `bf build` emits for template-string
|
|
135
|
-
* adapters (#1644). Imported from the `@barefootjs/jsx/import-map` subpath,
|
|
136
|
-
* a zero-dependency module, to keep this runtime file free of the compiler.
|
|
137
|
-
*/
|
|
138
|
-
export function BfImportMap(props: BfImportMapProps): HtmlEscapedString | Promise<HtmlEscapedString> {
|
|
139
|
-
const base = props.base.replace(/\/$/, '')
|
|
140
|
-
// Built-in defaults first, then manifest imports so a configured
|
|
141
|
-
// `@barefootjs/client` mapping (emitted by `bf build` against the
|
|
142
|
-
// build's `externalsBasePath`) wins over the prop-derived one.
|
|
143
|
-
const imports: Record<string, string> = {
|
|
144
|
-
'@barefootjs/client': `${base}/barefoot.js`,
|
|
145
|
-
'@barefootjs/client/runtime': `${base}/barefoot.js`,
|
|
146
|
-
...(props.externals?.importmap?.imports ?? {}),
|
|
147
|
-
}
|
|
148
|
-
const preloads = props.preload === false ? [] : props.externals?.preloads ?? []
|
|
149
|
-
|
|
150
|
-
return html`${raw(renderImportMapHtml({ importmap: { imports }, preloads }))}`
|
|
151
|
-
}
|
|
152
|
-
|
|
153
92
|
export interface BfScriptsProps {
|
|
154
93
|
/** Base URL where the runtime + component bundles are served. */
|
|
155
94
|
base: string
|
|
@@ -168,9 +107,9 @@ let __bfEmptyManifestWarned = false
|
|
|
168
107
|
* manifest, runtime first. Place at the end of `<body>`.
|
|
169
108
|
*
|
|
170
109
|
* Logs a one-time warning when the manifest is empty — a strong
|
|
171
|
-
* signal the user is running the server before
|
|
172
|
-
*
|
|
173
|
-
*
|
|
110
|
+
* signal the user is running the server before a build has produced
|
|
111
|
+
* anything, which would otherwise present as a silent "page renders
|
|
112
|
+
* but nothing is interactive."
|
|
174
113
|
*/
|
|
175
114
|
export function BfScripts(props: BfScriptsProps): HtmlEscapedString | Promise<HtmlEscapedString> {
|
|
176
115
|
const urls = manifestToScriptUrls(props.manifest, props.base)
|
|
@@ -178,7 +117,7 @@ export function BfScripts(props: BfScriptsProps): HtmlEscapedString | Promise<Ht
|
|
|
178
117
|
__bfEmptyManifestWarned = true
|
|
179
118
|
console.warn(
|
|
180
119
|
'[barefootjs] BfScripts: manifest is empty — no <script> tags emitted. ' +
|
|
181
|
-
'Run `
|
|
120
|
+
'Run `vite build` to compile components and rebuild the manifest.',
|
|
182
121
|
)
|
|
183
122
|
}
|
|
184
123
|
const tags = urls
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { conformancePins } from './conformance-pins.ts'
|
|
|
11
11
|
|
|
12
12
|
// BfScripts is exported from a separate entry point to avoid JSX runtime issues in tests
|
|
13
13
|
// Usage: import { BfScripts } from '@barefootjs/hono/scripts'
|
|
14
|
-
export type { CollectedScript } from './scripts.tsx'
|
|
14
|
+
export type { CollectedScript, CollectedPreload } from './scripts.tsx'
|
|
15
15
|
|
|
16
16
|
// Portal components for SSR
|
|
17
17
|
// Usage: import { BfPortals, Portal } from '@barefootjs/hono/portals'
|
package/src/preload.tsx
CHANGED
|
@@ -40,6 +40,12 @@ export interface ManifestEntry {
|
|
|
40
40
|
markedTemplate: string
|
|
41
41
|
clientJs?: string
|
|
42
42
|
props?: Array<{ name: string; type: string; optional: boolean }>
|
|
43
|
+
/**
|
|
44
|
+
* Dependency component names for recursive preloading.
|
|
45
|
+
* Note: no current producer (neither legacy site build nor @barefootjs/vite)
|
|
46
|
+
* emits this field, so dependency recursion only activates for
|
|
47
|
+
* hand-authored manifests. Kept for API compatibility.
|
|
48
|
+
*/
|
|
43
49
|
dependencies?: string[]
|
|
44
50
|
}
|
|
45
51
|
|
|
@@ -84,7 +90,11 @@ export interface BfPreloadProps {
|
|
|
84
90
|
* Resolves the full dependency chain for given components.
|
|
85
91
|
* Uses a visited set to prevent infinite loops from circular dependencies.
|
|
86
92
|
*
|
|
87
|
-
*
|
|
93
|
+
* Manifest keys are whatever the manifest producer used — the legacy site
|
|
94
|
+
* build uses path-qualified keys like `ui/button`, so component name entries
|
|
95
|
+
* must match those exactly. A key mismatch is silently skipped (no error).
|
|
96
|
+
*
|
|
97
|
+
* @param components - Component names/keys to resolve (must match manifest keys exactly)
|
|
88
98
|
* @param manifest - Component manifest with dependency information
|
|
89
99
|
* @param visited - Set of already visited component names (for cycle detection)
|
|
90
100
|
* @returns Array of clientJs paths for all dependencies
|
|
@@ -126,7 +136,10 @@ function resolveDependencyChain(
|
|
|
126
136
|
* by all BarefootJS components.
|
|
127
137
|
*
|
|
128
138
|
* When manifest and components props are provided, automatically
|
|
129
|
-
* preloads the full dependency chain for those components.
|
|
139
|
+
* preloads the full dependency chain for those components. The `components`
|
|
140
|
+
* array entries must match manifest keys exactly — for the legacy site
|
|
141
|
+
* build, use path-qualified keys like `ui/button`, not component names.
|
|
142
|
+
* A mismatched key is silently skipped.
|
|
130
143
|
*/
|
|
131
144
|
export function BfPreload({
|
|
132
145
|
staticPath = '/static',
|
package/src/render.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `<Suspense>` boundaries flush out-of-order.
|
|
16
16
|
*
|
|
17
17
|
* Both accept a `hono/jsx` node (typically a full page including the
|
|
18
|
-
* layout shell
|
|
18
|
+
* layout shell and `<BfScripts>`).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { renderToReadableStream } from 'hono/jsx/streaming'
|
package/src/scripts.tsx
CHANGED
|
@@ -46,6 +46,10 @@ export type CollectedScript = {
|
|
|
46
46
|
src: string
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export type CollectedPreload = {
|
|
50
|
+
href: string
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
export interface BfScriptsProps {
|
|
50
54
|
/**
|
|
51
55
|
* Build manifest from `dist/components/manifest.json`. When supplied
|
|
@@ -103,6 +107,7 @@ export function BfScripts(props: BfScriptsProps = {}) {
|
|
|
103
107
|
c.set('bfScriptsRendered', true)
|
|
104
108
|
|
|
105
109
|
const scripts: CollectedScript[] = c.get('bfCollectedScripts') || []
|
|
110
|
+
const preloads: CollectedPreload[] = c.get('bfCollectedPreloads') || []
|
|
106
111
|
const outputSet: Set<string> = c.get('bfOutputScripts') || new Set()
|
|
107
112
|
const { manifest, base, entryRoots } = props
|
|
108
113
|
// `entryRoots` extends both the walk-root set AND the `excluded` set.
|
|
@@ -146,8 +151,16 @@ export function BfScripts(props: BfScriptsProps = {}) {
|
|
|
146
151
|
...componentScripts.reverse(),
|
|
147
152
|
]
|
|
148
153
|
|
|
154
|
+
// Preloads carry no execution-order constraint the way scripts do (they
|
|
155
|
+
// are just hints, not code that runs) — insertion order is emitted as-
|
|
156
|
+
// is, ahead of every `<script type="module">` tag, so the browser sees
|
|
157
|
+
// the hint before it would otherwise discover the same chunk as an
|
|
158
|
+
// import of one of those scripts.
|
|
149
159
|
return (
|
|
150
160
|
<Fragment>
|
|
161
|
+
{preloads.map(({ href }) => (
|
|
162
|
+
<link rel="modulepreload" crossorigin="" href={href} />
|
|
163
|
+
))}
|
|
151
164
|
{finalScripts.map(({ src }) => (
|
|
152
165
|
<script type="module" src={src} />
|
|
153
166
|
))}
|
|
@@ -159,6 +172,129 @@ export function BfScripts(props: BfScriptsProps = {}) {
|
|
|
159
172
|
}
|
|
160
173
|
}
|
|
161
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Register `urls` (a component's resolved script URLs for THIS render) into
|
|
177
|
+
* the same request-context collector `BfScripts` reads, and return the
|
|
178
|
+
* subset that must be rendered INLINE right here instead — see
|
|
179
|
+
* `BfScripts`'s docstring on `bfScriptsRendered` (a component rendering
|
|
180
|
+
* after `<BfScripts />` has already run, e.g. inside a Suspense boundary,
|
|
181
|
+
* can't rely on the end-of-body collector; its scripts have to ship inline
|
|
182
|
+
* at the point of render for streaming to deliver them at all).
|
|
183
|
+
*
|
|
184
|
+
* `HonoAdapter.generate()` calls this directly from CODEGEN — one call per
|
|
185
|
+
* rendered component, with `urls` baked in as
|
|
186
|
+
* `AdapterGenerateOptions.scriptAssets` (the Vite plugin's already-resolved,
|
|
187
|
+
* manifest-hashed or dev-origin URL list). Two properties follow from that:
|
|
188
|
+
*
|
|
189
|
+
* - No separate `barefoot.js` runtime registration: `scriptAssets` under
|
|
190
|
+
* Vite is just the component's own bundled entry (see
|
|
191
|
+
* `@barefootjs/vite`'s `resolveScriptAssets` docstring) — the
|
|
192
|
+
* `@barefootjs/client` runtime arrives as a shared ESM chunk that entry
|
|
193
|
+
* imports, which the browser follows on its own with no extra
|
|
194
|
+
* `<script src>` needed. Go's `.Scripts.Register` calls (generated by
|
|
195
|
+
* `GoTemplateAdapter`) make exactly the same simplification for the same
|
|
196
|
+
* reason.
|
|
197
|
+
* - Dedup keys on the URL itself (there is no separate `componentId` to key
|
|
198
|
+
* by): rendering the SAME component N times (e.g. inside a `.map()`) or
|
|
199
|
+
* two different components that happen to share a chunk both correctly
|
|
200
|
+
* collapse to one `<script>` tag.
|
|
201
|
+
*
|
|
202
|
+
* Swallows a missing request context (no `jsxRenderer` in the render path),
|
|
203
|
+
* returning `[]` — SSR still renders, just without hydration scripts.
|
|
204
|
+
*/
|
|
205
|
+
export function registerComponentScripts(urls: string[]): string[] {
|
|
206
|
+
try {
|
|
207
|
+
const c = useRequestContext()
|
|
208
|
+
const scripts: CollectedScript[] = c.get('bfCollectedScripts') || []
|
|
209
|
+
const outputScripts: Set<string> = c.get('bfOutputScripts') || new Set()
|
|
210
|
+
const rendered = c.get('bfScriptsRendered')
|
|
211
|
+
const inline: string[] = []
|
|
212
|
+
for (const src of urls) {
|
|
213
|
+
if (outputScripts.has(src)) continue
|
|
214
|
+
outputScripts.add(src)
|
|
215
|
+
if (rendered) inline.push(src)
|
|
216
|
+
else scripts.push({ src })
|
|
217
|
+
}
|
|
218
|
+
c.set('bfCollectedScripts', scripts)
|
|
219
|
+
c.set('bfOutputScripts', outputScripts)
|
|
220
|
+
return inline
|
|
221
|
+
} catch {
|
|
222
|
+
return []
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Register `urls` (a component's resolved TRANSITIVE-chunk preload URLs for
|
|
228
|
+
* THIS render, per `AdapterGenerateOptions.preloadAssets`) into the same
|
|
229
|
+
* request-context collector `BfScripts` reads, and return the subset that
|
|
230
|
+
* must be rendered INLINE right here instead — same
|
|
231
|
+
* already-rendered-`<BfScripts />` escape hatch as `registerComponentScripts`
|
|
232
|
+
* (see that function's docstring), and the same reasons apply: a component
|
|
233
|
+
* whose SSR happens inside a Suspense boundary that renders after
|
|
234
|
+
* `<BfScripts />` has already flushed can't rely on the end-of-body
|
|
235
|
+
* collector.
|
|
236
|
+
*
|
|
237
|
+
* Kept as a SEPARATE collector (`bfCollectedPreloads` / `bfOutputPreloads`)
|
|
238
|
+
* from `registerComponentScripts`'s `bfCollectedScripts` /
|
|
239
|
+
* `bfOutputScripts` — a preload URL and a script URL are never the same
|
|
240
|
+
* concern (preloads are the entry's TRANSITIVE deps, never the entry
|
|
241
|
+
* itself — see `AdapterGenerateOptions.preloadAssets`), so there is no
|
|
242
|
+
* cross-dedup to do between the two sets; keeping them separate avoids
|
|
243
|
+
* conflating "this was preloaded" with "this was registered as a script".
|
|
244
|
+
*
|
|
245
|
+
* Dedup keys on the URL itself, same as `registerComponentScripts`:
|
|
246
|
+
* rendering the SAME component N times (e.g. inside a `.map()`), or two
|
|
247
|
+
* different components that happen to share a transitive chunk, both
|
|
248
|
+
* correctly collapse to one `<link rel="modulepreload">` tag.
|
|
249
|
+
*
|
|
250
|
+
* Swallows a missing request context, returning `[]` — SSR still renders,
|
|
251
|
+
* just without preload hints.
|
|
252
|
+
*/
|
|
253
|
+
export function registerComponentPreloads(urls: string[]): string[] {
|
|
254
|
+
try {
|
|
255
|
+
const c = useRequestContext()
|
|
256
|
+
const preloads: CollectedPreload[] = c.get('bfCollectedPreloads') || []
|
|
257
|
+
const outputPreloads: Set<string> = c.get('bfOutputPreloads') || new Set()
|
|
258
|
+
const rendered = c.get('bfScriptsRendered')
|
|
259
|
+
const inline: string[] = []
|
|
260
|
+
for (const href of urls) {
|
|
261
|
+
if (outputPreloads.has(href)) continue
|
|
262
|
+
outputPreloads.add(href)
|
|
263
|
+
if (rendered) inline.push(href)
|
|
264
|
+
else preloads.push({ href })
|
|
265
|
+
}
|
|
266
|
+
c.set('bfCollectedPreloads', preloads)
|
|
267
|
+
c.set('bfOutputPreloads', outputPreloads)
|
|
268
|
+
return inline
|
|
269
|
+
} catch {
|
|
270
|
+
return []
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Wrap `jsx` with a leading `<link rel="modulepreload">` per entry in
|
|
276
|
+
* `inlinePreloads` and a trailing `<script type="module">` per entry in
|
|
277
|
+
* `inlineScripts` (the return values of `registerComponentPreloads` and
|
|
278
|
+
* `registerComponentScripts` respectively) — a single shared function
|
|
279
|
+
* every generated component imports, rather than per-file duplicated
|
|
280
|
+
* inline-asset logic. Returns `jsx` unchanged when both are empty (the
|
|
281
|
+
* common case: collected, not inline).
|
|
282
|
+
*
|
|
283
|
+
* `inlinePreloads` defaults to `[]` so existing generated call sites that
|
|
284
|
+
* only ever pass `inlineScripts` (a component with `scriptAssets` but no
|
|
285
|
+
* `preloadAssets`) keep compiling and behaving exactly as before.
|
|
286
|
+
*/
|
|
287
|
+
export function wrapWithInlineScripts(jsx: unknown, inlineScripts: string[], inlinePreloads: string[] = []) {
|
|
288
|
+
if (inlineScripts.length === 0 && inlinePreloads.length === 0) return jsx
|
|
289
|
+
return (
|
|
290
|
+
<Fragment>
|
|
291
|
+
{inlinePreloads.map(href => <link rel="modulepreload" crossorigin="" href={href} />)}
|
|
292
|
+
{jsx as never}
|
|
293
|
+
{inlineScripts.map(src => <script type="module" src={src} />)}
|
|
294
|
+
</Fragment>
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
162
298
|
/**
|
|
163
299
|
* Walk stub-rewrite edges from each manifest entry in `roots`,
|
|
164
300
|
* returning the script URLs for every transitively reachable
|