@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
|
@@ -0,0 +1,1114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BarefootJS Hono Adapter
|
|
3
|
+
*
|
|
4
|
+
* Generates Hono JSX from Pure IR.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type ComponentIR,
|
|
9
|
+
type IRNode,
|
|
10
|
+
type IRElement,
|
|
11
|
+
type IRText,
|
|
12
|
+
type IRExpression,
|
|
13
|
+
type IRConditional,
|
|
14
|
+
type IRLoop,
|
|
15
|
+
type IRComponent,
|
|
16
|
+
type IRFragment,
|
|
17
|
+
type IRIfStatement,
|
|
18
|
+
type IRProvider,
|
|
19
|
+
type IRAsync,
|
|
20
|
+
type IRSlot,
|
|
21
|
+
type AttrValue,
|
|
22
|
+
type IRTemplatePart,
|
|
23
|
+
type ParamInfo,
|
|
24
|
+
type AdapterGenerateOptions,
|
|
25
|
+
type AdapterOutput,
|
|
26
|
+
type TemplateSections,
|
|
27
|
+
type JsxAdapterConfig,
|
|
28
|
+
type IRNodeEmitter,
|
|
29
|
+
type EmitIRNode,
|
|
30
|
+
type AttrValueEmitter,
|
|
31
|
+
JsxAdapter,
|
|
32
|
+
isBooleanAttr,
|
|
33
|
+
rewriteImportsForTemplate,
|
|
34
|
+
emitIRNode,
|
|
35
|
+
emitAttrValue,
|
|
36
|
+
buildLoopChainExpr,
|
|
37
|
+
} from '@barefootjs/jsx'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hono adapter's IRNode render context: which surrounding render
|
|
41
|
+
* state matters when lowering a node. The `IRNodeEmitter` dispatcher
|
|
42
|
+
* threads this `Ctx` unchanged into per-kind methods; per-method
|
|
43
|
+
* documentation below records which flags each kind consults.
|
|
44
|
+
*/
|
|
45
|
+
type HonoRenderCtx = {
|
|
46
|
+
isRootOfClientComponent?: boolean
|
|
47
|
+
isInsideLoop?: boolean
|
|
48
|
+
isLoopItemRoot?: boolean
|
|
49
|
+
}
|
|
50
|
+
import { BF_SCOPE, BF_HOST, BF_AT, BF_ROOT, BF_PROPS } from '@barefootjs/shared'
|
|
51
|
+
|
|
52
|
+
export interface HonoAdapterOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Base path for client JS files (e.g., '/static/components/')
|
|
55
|
+
* Used to generate script src attributes.
|
|
56
|
+
*/
|
|
57
|
+
clientJsBasePath?: string
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Path to barefoot.js runtime (e.g., '/static/components/barefoot.js')
|
|
61
|
+
*/
|
|
62
|
+
barefootJsPath?: string
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Client JS filename (without path). When set, all components use this filename.
|
|
66
|
+
* When not set, uses `{componentName}.client.js`.
|
|
67
|
+
* Useful for files with multiple components that share a single client JS file.
|
|
68
|
+
*/
|
|
69
|
+
clientJsFilename?: string
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Display name surfaced through `JsxAdapter.name` — read by `bf build`
|
|
73
|
+
* for its `Adapter: …` banner. Defaults to `'hono'`. CSR-mode callers
|
|
74
|
+
* (`@barefootjs/client/build`) pass `'csr'` so the banner reflects the
|
|
75
|
+
* mode the user picked at scaffold time instead of leaking the
|
|
76
|
+
* fact that CSR currently reuses HonoAdapter under the hood.
|
|
77
|
+
*/
|
|
78
|
+
name?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Mirror `IRLoop.sortComparator` / `IRLoop.filterPredicate` chaining
|
|
83
|
+
* into the JSX expression that backs the Hono `.map()` call.
|
|
84
|
+
* Delegates to the shared `buildLoopChainExpr` so the chain shape
|
|
85
|
+
* stays byte-equal with the client-template emit
|
|
86
|
+
* (`html-template.ts:applyLoopChain`) and the control-flow plans
|
|
87
|
+
* (`utils.ts:buildChainedArrayExpr`) — drift between the three
|
|
88
|
+
* would silently produce different sorted orders depending on
|
|
89
|
+
* which path consumed the IR. Always uses `.toSorted`
|
|
90
|
+
* (non-mutating) so shared prop arrays aren't reordered in place
|
|
91
|
+
* across renders.
|
|
92
|
+
*/
|
|
93
|
+
function applyHonoLoopChain(loop: IRLoop): string {
|
|
94
|
+
return buildLoopChainExpr({
|
|
95
|
+
base: loop.array,
|
|
96
|
+
sortComparator: loop.sortComparator,
|
|
97
|
+
filterPredicate: loop.filterPredicate,
|
|
98
|
+
chainOrder: loop.chainOrder,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderCtx> {
|
|
103
|
+
name = 'hono'
|
|
104
|
+
extension = '.tsx'
|
|
105
|
+
clientShimSource = '@barefootjs/hono/client-shim'
|
|
106
|
+
|
|
107
|
+
// The Hono SSR runtime is JavaScript (Node / Bun / CF Workers), so any
|
|
108
|
+
// synchronous JS call the user writes can be rendered as-is at template
|
|
109
|
+
// scope — there is no language-level subset to enumerate. Broad
|
|
110
|
+
// acceptance is the contract.
|
|
111
|
+
//
|
|
112
|
+
// What this delegates to the user: Hono accepts the call; whether that
|
|
113
|
+
// call actually works at SSR is the user's responsibility. A function
|
|
114
|
+
// that touches `window` / `document` / `localStorage` will throw a clear
|
|
115
|
+
// ReferenceError at build time rather than silently rendering as
|
|
116
|
+
// `undefined`. The fix is to wrap the offending JSX expression with
|
|
117
|
+
// `/* @client */` so the call is deferred to hydrate.
|
|
118
|
+
//
|
|
119
|
+
// Component-internal bindings (signals, memos, init-locals, destructured
|
|
120
|
+
// props) are still correctly rejected by the shadow guard inside
|
|
121
|
+
// `isCallAcceptedByAdapter` (relocate.ts), regardless of what this
|
|
122
|
+
// predicate returns — those identifiers carry a non-`global`/`module-*`
|
|
123
|
+
// BindingKind, so the guard short-circuits before the predicate runs.
|
|
124
|
+
acceptsTemplateCall = (): boolean => true
|
|
125
|
+
|
|
126
|
+
protected jsxConfig: JsxAdapterConfig = { preserveTypes: true }
|
|
127
|
+
|
|
128
|
+
private options: HonoAdapterOptions
|
|
129
|
+
private isClientComponent: boolean = false
|
|
130
|
+
private hasClientInteractivity: boolean = false
|
|
131
|
+
private currentComponentHasProps: boolean = false
|
|
132
|
+
/**
|
|
133
|
+
* Per-call relative-import rewriter supplied by the build pipeline so
|
|
134
|
+
* source-authored relative paths resolve correctly from the emitted
|
|
135
|
+
* file's on-disk position (#1453). Stashed for the duration of one
|
|
136
|
+
* `generate()` call so `generateImports` can apply it; cleared on exit
|
|
137
|
+
* so a singleton adapter instance does not leak state between
|
|
138
|
+
* components.
|
|
139
|
+
*/
|
|
140
|
+
private rewriteRelativeImport?: (importPath: string) => string
|
|
141
|
+
/** Stack of loop keys for generating data-key / data-key-1 attributes on loop items */
|
|
142
|
+
private loopKeyStack: Array<{ key: string | null; param: string }> = []
|
|
143
|
+
|
|
144
|
+
constructor(options: HonoAdapterOptions = {}) {
|
|
145
|
+
super()
|
|
146
|
+
this.options = {
|
|
147
|
+
clientJsBasePath: options.clientJsBasePath ?? '/static/components/',
|
|
148
|
+
barefootJsPath: options.barefootJsPath ?? '/static/components/barefoot.js',
|
|
149
|
+
clientJsFilename: options.clientJsFilename,
|
|
150
|
+
}
|
|
151
|
+
if (options.name) this.name = options.name
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput {
|
|
155
|
+
this.componentName = ir.metadata.componentName
|
|
156
|
+
this.isClientComponent = ir.metadata.isClientComponent
|
|
157
|
+
this.rewriteRelativeImport = options?.rewriteRelativeImport
|
|
158
|
+
|
|
159
|
+
// Generate component body FIRST so we can scan it for used imports
|
|
160
|
+
const component = this.generateComponent(ir)
|
|
161
|
+
const types = this.generateTypes(ir, component)
|
|
162
|
+
const componentCode = [types, component].filter(Boolean).join('\n')
|
|
163
|
+
const imports = this.generateImports(ir, componentCode)
|
|
164
|
+
// Module-level Context bindings (`const Ctx = createContext()`) are
|
|
165
|
+
// skipped from the SSR signal-initializer block by JsxAdapter — they
|
|
166
|
+
// need to live at module scope so providers and consumers in the same
|
|
167
|
+
// render share the same Context object identity. Emitted in a dedicated
|
|
168
|
+
// section so multi-component dedup works on the full block (not per
|
|
169
|
+
// line, which would split multi-line `({...})` arguments).
|
|
170
|
+
const moduleConstants = this.generateModuleLevelContextBindings(ir)
|
|
171
|
+
|
|
172
|
+
const defaultExport = ir.metadata.hasDefaultExport
|
|
173
|
+
? `\nexport default ${this.componentName}`
|
|
174
|
+
: ''
|
|
175
|
+
|
|
176
|
+
const sections: TemplateSections = {
|
|
177
|
+
imports,
|
|
178
|
+
types: types || '',
|
|
179
|
+
component,
|
|
180
|
+
defaultExport,
|
|
181
|
+
moduleConstants,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Assemble template for backward compat (external consumers using output.template)
|
|
185
|
+
const template = [imports, moduleConstants, types, component].filter(Boolean).join('\n\n') + defaultExport
|
|
186
|
+
|
|
187
|
+
const result: AdapterOutput = {
|
|
188
|
+
template,
|
|
189
|
+
sections,
|
|
190
|
+
types: types || undefined,
|
|
191
|
+
extension: this.extension,
|
|
192
|
+
}
|
|
193
|
+
this.rewriteRelativeImport = undefined
|
|
194
|
+
return result
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private generateModuleLevelContextBindings(ir: ComponentIR): string {
|
|
198
|
+
const lines: string[] = []
|
|
199
|
+
for (const c of ir.metadata.localConstants) {
|
|
200
|
+
if (!c.isModule) continue
|
|
201
|
+
if (c.isExported) continue
|
|
202
|
+
if (c.systemConstructKind !== 'createContext') continue
|
|
203
|
+
if (!c.value) continue
|
|
204
|
+
const keyword = c.declarationKind ?? 'const'
|
|
205
|
+
const value = this.jsxConfig.preserveTypes ? (c.typedValue ?? c.value) : c.value
|
|
206
|
+
lines.push(`${keyword} ${c.name} = ${value}`)
|
|
207
|
+
}
|
|
208
|
+
return lines.join('\n')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ===========================================================================
|
|
212
|
+
// Imports Generation
|
|
213
|
+
// ===========================================================================
|
|
214
|
+
|
|
215
|
+
private generateImports(ir: ComponentIR, componentCode: string): string {
|
|
216
|
+
const lines: string[] = []
|
|
217
|
+
|
|
218
|
+
// Only import bfComment/bfText/bfTextEnd utilities that are actually used
|
|
219
|
+
const utilImports: string[] = []
|
|
220
|
+
for (const util of ['bfComment', 'bfText', 'bfTextEnd']) {
|
|
221
|
+
if (new RegExp(`\\b${util}\\b`).test(componentCode)) {
|
|
222
|
+
utilImports.push(util)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (utilImports.length > 0) {
|
|
226
|
+
lines.push(`import { ${utilImports.join(', ')} } from '@barefootjs/hono/utils'`)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Import Suspense when async boundaries are used
|
|
230
|
+
if (componentCode.includes('<Suspense')) {
|
|
231
|
+
lines.push(`import { Suspense } from 'hono/jsx/streaming'`)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Re-emit template imports, rewriting `@barefootjs/client` to this
|
|
235
|
+
// adapter's SSR shim AND re-anchoring relative paths from the emit
|
|
236
|
+
// location when the caller supplied a `rewriteRelativeImport` hook
|
|
237
|
+
// (#1453). Adapters own both rewrites; the compiler hands us the
|
|
238
|
+
// raw import list.
|
|
239
|
+
const templateImports = rewriteImportsForTemplate(
|
|
240
|
+
ir.metadata.templateImports,
|
|
241
|
+
this.clientShimSource,
|
|
242
|
+
this.rewriteRelativeImport,
|
|
243
|
+
)
|
|
244
|
+
for (const imp of templateImports) {
|
|
245
|
+
if (imp.specifiers.length === 0) {
|
|
246
|
+
if (!imp.isTypeOnly) {
|
|
247
|
+
lines.push(`import '${imp.source}'`)
|
|
248
|
+
}
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
if (imp.isTypeOnly) {
|
|
252
|
+
lines.push(`import type ${this.formatImportSpecifiers(imp.specifiers)} from '${imp.source}'`)
|
|
253
|
+
} else {
|
|
254
|
+
lines.push(`import ${this.formatImportSpecifiers(imp.specifiers)} from '${imp.source}'`)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Provider IR rendering emits `provideContextSSR(...)` calls. Emit the
|
|
259
|
+
// import on its own line so multi-component files dedupe it cleanly via
|
|
260
|
+
// the compiler's per-line import merging.
|
|
261
|
+
if (/\bprovideContextSSR\(/.test(componentCode)) {
|
|
262
|
+
lines.push(`import { provideContextSSR } from '@barefootjs/hono/client-shim'`)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return lines.join('\n')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ===========================================================================
|
|
269
|
+
// Types Generation
|
|
270
|
+
// ===========================================================================
|
|
271
|
+
|
|
272
|
+
generateTypes(ir: ComponentIR, componentBody?: string): string | null {
|
|
273
|
+
const lines: string[] = []
|
|
274
|
+
|
|
275
|
+
// Include original type definitions — only those referenced in the component body
|
|
276
|
+
// or transitively referenced by other included type definitions
|
|
277
|
+
if (componentBody && ir.metadata.typeDefinitions.length > 0) {
|
|
278
|
+
const propsTypeName = this.getPropsTypeName(ir)
|
|
279
|
+
// Seed the reachability scan with everything that ends up referencing
|
|
280
|
+
// a type name in the FINAL emitted file, not just the component body.
|
|
281
|
+
//
|
|
282
|
+
// - `propsTypeName` is referenced by the synthesized
|
|
283
|
+
// `${Name}PropsWithHydration = ${propsTypeName} & {...}` alias the
|
|
284
|
+
// destructured-props branch emits below — but that alias is built
|
|
285
|
+
// AFTER this scan, so the body never literally mentions e.g.
|
|
286
|
+
// `ButtonProps`. Without seeding it here the alias references an
|
|
287
|
+
// undeclared name (TS2304) and TS widens `variant`/`size` to `any`
|
|
288
|
+
// at every `Record[variant]` lookup site (TS7053) downstream.
|
|
289
|
+
//
|
|
290
|
+
// - Named re-export blocks (`export type { ButtonVariant, ButtonSize,
|
|
291
|
+
// ButtonProps }`) are emitted by the compiler's `generateModuleExports`
|
|
292
|
+
// AFTER `s.types`. Each re-exported local name needs its declaration
|
|
293
|
+
// carried forward too. Issue #1453 covers the full reproduction.
|
|
294
|
+
const seedText = [
|
|
295
|
+
componentBody,
|
|
296
|
+
propsTypeName && !ir.metadata.propsObjectName ? propsTypeName : '',
|
|
297
|
+
...ir.metadata.namedExports
|
|
298
|
+
.filter((block) => block.source === null)
|
|
299
|
+
.flatMap((block) => block.specifiers.map((s) => s.name)),
|
|
300
|
+
].filter(Boolean).join('\n')
|
|
301
|
+
|
|
302
|
+
const included = new Set<string>()
|
|
303
|
+
// First pass: include types directly referenced in the seed text
|
|
304
|
+
for (const typeDef of ir.metadata.typeDefinitions) {
|
|
305
|
+
if (new RegExp(`\\b${typeDef.name}\\b`).test(seedText)) {
|
|
306
|
+
included.add(typeDef.name)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Transitive pass: include types referenced by already-included types
|
|
310
|
+
let changed = true
|
|
311
|
+
while (changed) {
|
|
312
|
+
changed = false
|
|
313
|
+
for (const typeDef of ir.metadata.typeDefinitions) {
|
|
314
|
+
if (included.has(typeDef.name)) continue
|
|
315
|
+
for (const name of included) {
|
|
316
|
+
const includedDef = ir.metadata.typeDefinitions.find(t => t.name === name)
|
|
317
|
+
if (includedDef && new RegExp(`\\b${typeDef.name}\\b`).test(includedDef.definition)) {
|
|
318
|
+
included.add(typeDef.name)
|
|
319
|
+
changed = true
|
|
320
|
+
break
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
for (const typeDef of ir.metadata.typeDefinitions) {
|
|
326
|
+
if (included.has(typeDef.name)) lines.push(typeDef.definition)
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
for (const typeDef of ir.metadata.typeDefinitions) {
|
|
330
|
+
lines.push(typeDef.definition)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Generate hydration props type (only when destructured-props pattern uses it;
|
|
335
|
+
// SolidJS-style props use inline type annotation instead)
|
|
336
|
+
const propsTypeName = this.getPropsTypeName(ir)
|
|
337
|
+
if (propsTypeName && !ir.metadata.propsObjectName) {
|
|
338
|
+
lines.push('')
|
|
339
|
+
lines.push(`type ${this.componentName}PropsWithHydration = ${propsTypeName} & {`)
|
|
340
|
+
lines.push(' __instanceId?: string')
|
|
341
|
+
lines.push(' __bfScope?: string')
|
|
342
|
+
lines.push(' __bfChild?: boolean')
|
|
343
|
+
lines.push(' __bfParentProps?: string')
|
|
344
|
+
lines.push(' __bfParent?: string')
|
|
345
|
+
lines.push(' __bfMount?: string')
|
|
346
|
+
lines.push(' "data-key"?: string | number')
|
|
347
|
+
lines.push('}')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return lines.length > 0 ? lines.join('\n') : null
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private getPropsTypeName(ir: ComponentIR): string | null {
|
|
354
|
+
if (ir.metadata.propsType?.raw) {
|
|
355
|
+
return ir.metadata.propsType.raw
|
|
356
|
+
}
|
|
357
|
+
return null
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ===========================================================================
|
|
361
|
+
// Component Generation
|
|
362
|
+
// ===========================================================================
|
|
363
|
+
|
|
364
|
+
private generateComponent(ir: ComponentIR): string {
|
|
365
|
+
const name = ir.metadata.componentName
|
|
366
|
+
const propsTypeName = this.getPropsTypeName(ir)
|
|
367
|
+
|
|
368
|
+
// Validate: only reactive primitives (signals, memos, effects, onMounts) require "use client"
|
|
369
|
+
const hasReactivePrimitives =
|
|
370
|
+
ir.metadata.signals.length > 0 ||
|
|
371
|
+
ir.metadata.memos.length > 0 ||
|
|
372
|
+
ir.metadata.effects.length > 0 ||
|
|
373
|
+
ir.metadata.onMounts.length > 0
|
|
374
|
+
|
|
375
|
+
if (hasReactivePrimitives && !ir.metadata.isClientComponent) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`Component "${name}" has reactive primitives (signals, memos, effects, or onMounts) ` +
|
|
378
|
+
`but is not marked as a client component. Add "use client" directive at the top of the file.`
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// A component needs client interactivity if it has "use client" OR if it has event handlers
|
|
383
|
+
// that need client JS wiring (detected by analyzeClientNeeds)
|
|
384
|
+
const needsClientInit = ir.metadata.clientAnalysis?.needsInit ?? false
|
|
385
|
+
const hasClientInteractivity = ir.metadata.isClientComponent || needsClientInit
|
|
386
|
+
this.hasClientInteractivity = hasClientInteractivity
|
|
387
|
+
|
|
388
|
+
// Check if component uses props object pattern (SolidJS-style)
|
|
389
|
+
const propsObjectName = ir.metadata.propsObjectName
|
|
390
|
+
|
|
391
|
+
// Build props parameter based on pattern
|
|
392
|
+
let fullPropsDestructure: string
|
|
393
|
+
let typeAnnotation: string
|
|
394
|
+
let propsExtraction: string | null = null
|
|
395
|
+
|
|
396
|
+
// Synthetic hydration-only props the generated wrapper destructures
|
|
397
|
+
// out of `props` before reaching the user's body. Kept as a shared
|
|
398
|
+
// constant so the `propsObjectName` (SolidJS-style) and destructured
|
|
399
|
+
// branches both list every hydration field — the destructured
|
|
400
|
+
// branch's fallback used to declare only `__instanceId / __bfScope
|
|
401
|
+
// / __bfChild`, but the generated body destructures `__bfParent /
|
|
402
|
+
// __bfMount / __bfParentProps / data-key` too, so tsc raised
|
|
403
|
+
// TS2339 ("Property '__bfParent' does not exist...") on every
|
|
404
|
+
// emitted SSR template for a component without an explicit Props
|
|
405
|
+
// type. See onboarding round 5 / PR #1450.
|
|
406
|
+
const HYDRATION_PROPS_TYPE =
|
|
407
|
+
'{ __instanceId?: string; __bfScope?: string; __bfChild?: boolean; __bfParentProps?: string; __bfParent?: string; __bfMount?: string; "data-key"?: string | number }'
|
|
408
|
+
|
|
409
|
+
if (propsObjectName) {
|
|
410
|
+
// SolidJS-style: function Component(props: Props)
|
|
411
|
+
// Accept all props as a single object, then destructure hydration props out
|
|
412
|
+
fullPropsDestructure = `__allProps`
|
|
413
|
+
typeAnnotation = propsTypeName
|
|
414
|
+
? `: ${propsTypeName} & ${HYDRATION_PROPS_TYPE}`
|
|
415
|
+
: `: Record<string, unknown> & ${HYDRATION_PROPS_TYPE}`
|
|
416
|
+
// propsExtraction is rebuilt after jsxBody generation with unused-aware aliases
|
|
417
|
+
} else {
|
|
418
|
+
// Destructured props pattern — fullPropsDestructure rebuilt after jsxBody with unused-aware aliases
|
|
419
|
+
fullPropsDestructure = '' // placeholder, rebuilt below
|
|
420
|
+
typeAnnotation = propsTypeName
|
|
421
|
+
? `: ${name}PropsWithHydration`
|
|
422
|
+
: `: ${HYDRATION_PROPS_TYPE}`
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Generate props serialization for hydration (for components with props)
|
|
426
|
+
// Only serialize props that the client JS init function actually reads
|
|
427
|
+
const clientUsedProps = new Set(ir.metadata.clientAnalysis?.usedProps ?? [])
|
|
428
|
+
const needsInit = ir.metadata.clientAnalysis?.needsInit ?? false
|
|
429
|
+
const propsToSerialize = ir.metadata.propsParams.filter(p => {
|
|
430
|
+
// Skip function props and internal props
|
|
431
|
+
return !p.name.startsWith('on') && !p.name.startsWith('__') && clientUsedProps.has(p.name)
|
|
432
|
+
})
|
|
433
|
+
const hasPropsToSerialize = propsToSerialize.length > 0 && hasClientInteractivity && needsInit
|
|
434
|
+
|
|
435
|
+
// Check if root is an if-statement (early return pattern)
|
|
436
|
+
const isIfStatement = ir.root.type === 'if-statement'
|
|
437
|
+
|
|
438
|
+
// Generate JSX body (for non-if-statement roots)
|
|
439
|
+
// Pass isRootOfClientComponent flag when the root is a component and this is a client component
|
|
440
|
+
// This ensures the child component receives __instanceId instead of __bfScope
|
|
441
|
+
const isRootComponent = ir.root.type === 'component'
|
|
442
|
+
|
|
443
|
+
// currentComponentHasProps: true when we need to emit bf-p on the root element.
|
|
444
|
+
// This is needed when: (1) the component has its own props to serialize, OR
|
|
445
|
+
// (2) the component's root is a component and it's a client component (namespaced props pass-through)
|
|
446
|
+
this.currentComponentHasProps = hasPropsToSerialize || (hasClientInteractivity && isRootComponent)
|
|
447
|
+
let jsxBody = isIfStatement ? '' : this.renderNode(ir.root, {
|
|
448
|
+
isRootOfClientComponent: hasClientInteractivity && isRootComponent
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
// Component roots of client components need comment-based scope markers.
|
|
452
|
+
// Unlike element roots (which get bf-s directly), the root component is
|
|
453
|
+
// a plain function whose output has no hydration markers.
|
|
454
|
+
if (!isIfStatement && hasClientInteractivity && isRootComponent) {
|
|
455
|
+
jsxBody = this.wrapWithScopeComment(jsxBody)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// For if-statement roots, render branches early so they're included in reference analysis
|
|
459
|
+
const ifCode = isIfStatement
|
|
460
|
+
? this.renderIfStatement(ir.root as IRIfStatement, { isRootOfClientComponent: true })
|
|
461
|
+
: ''
|
|
462
|
+
|
|
463
|
+
// Generate signal initializers with unused-aware prefixing (needs jsxBody for reference analysis)
|
|
464
|
+
const fullBodyText = jsxBody + '\n' + ifCode
|
|
465
|
+
const signalInits = this.generateSignalInitializers(ir, fullBodyText)
|
|
466
|
+
|
|
467
|
+
// Determine which hydration params are actually used in the generated body
|
|
468
|
+
// Include scopeId line content for accurate reference checking
|
|
469
|
+
const scopeIdLine = hasClientInteractivity
|
|
470
|
+
? `__instanceId`
|
|
471
|
+
: `__bfScope || __instanceId`
|
|
472
|
+
const bodyRefText = [
|
|
473
|
+
fullBodyText,
|
|
474
|
+
signalInits,
|
|
475
|
+
scopeIdLine,
|
|
476
|
+
// Props serialization references __bfParentProps
|
|
477
|
+
(hasPropsToSerialize || (hasClientInteractivity && isRootComponent)) ? '__bfParentProps' : '',
|
|
478
|
+
].join('\n')
|
|
479
|
+
|
|
480
|
+
// Rebuild hydration props with _ prefix for unused ones
|
|
481
|
+
const bfScopeAlias = /\b__bfScope\b/.test(bodyRefText) ? '__bfScope' : '__bfScope: _bfScope'
|
|
482
|
+
const bfChildAlias = /\b__bfChild\b/.test(bodyRefText) ? '__bfChild' : '__bfChild: _bfChild'
|
|
483
|
+
const bfParentPropsAlias = /\b__bfParentProps\b/.test(bodyRefText) ? '__bfParentProps' : '__bfParentProps: _bfParentProps'
|
|
484
|
+
const bfParentAlias = /\b__bfParent\b/.test(bodyRefText) ? '__bfParent' : '__bfParent: _bfParent'
|
|
485
|
+
const bfMountAlias = /\b__bfMount\b/.test(bodyRefText) ? '__bfMount' : '__bfMount: _bfMount'
|
|
486
|
+
const dataKeyAlias = /\b__dataKey\b/.test(bodyRefText) ? '"data-key": __dataKey' : '"data-key": _dataKey'
|
|
487
|
+
|
|
488
|
+
if (propsObjectName) {
|
|
489
|
+
propsExtraction = ` const { __instanceId, ${bfScopeAlias}, ${bfChildAlias}, ${bfParentPropsAlias}, ${bfParentAlias}, ${bfMountAlias}, ${dataKeyAlias}, ...${propsObjectName} } = __allProps`
|
|
490
|
+
} else {
|
|
491
|
+
const hydrationProps = `__instanceId, ${bfScopeAlias}, ${bfChildAlias}, ${bfParentPropsAlias}, ${bfParentAlias}, ${bfMountAlias}, ${dataKeyAlias}`
|
|
492
|
+
const parts: string[] = []
|
|
493
|
+
const propsParams = ir.metadata.propsParams
|
|
494
|
+
.map((p: ParamInfo) => {
|
|
495
|
+
const paramName = p.name === 'class' ? 'className' : p.name
|
|
496
|
+
return p.defaultValue ? `${paramName} = ${p.defaultValue}` : paramName
|
|
497
|
+
})
|
|
498
|
+
.join(', ')
|
|
499
|
+
if (propsParams) {
|
|
500
|
+
parts.push(propsParams)
|
|
501
|
+
}
|
|
502
|
+
parts.push(hydrationProps)
|
|
503
|
+
const restPropsName = ir.metadata.restPropsName
|
|
504
|
+
if (restPropsName) {
|
|
505
|
+
parts.push(`...${restPropsName}`)
|
|
506
|
+
}
|
|
507
|
+
fullPropsDestructure = `{ ${parts.join(', ')} }`
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const lines: string[] = []
|
|
511
|
+
// Module-export keyword belongs to the adapter: it knows the target language
|
|
512
|
+
// and whether the source declared the component as exported.
|
|
513
|
+
const exportPrefix = ir.metadata.isExported === false ? '' : 'export '
|
|
514
|
+
lines.push(`${exportPrefix}function ${name}(${fullPropsDestructure}${typeAnnotation}) {`)
|
|
515
|
+
|
|
516
|
+
// Add props extraction for SolidJS-style pattern
|
|
517
|
+
if (propsExtraction) {
|
|
518
|
+
lines.push(propsExtraction)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Generate scope ID
|
|
522
|
+
if (hasClientInteractivity) {
|
|
523
|
+
// Interactive components always generate their own unique ID with component name prefix
|
|
524
|
+
// This ensures client JS query `[bf-s^="ComponentName_"]` matches
|
|
525
|
+
lines.push(` const __scopeId = __instanceId || \`${name}_\${Math.random().toString(36).slice(2, 8)}\``)
|
|
526
|
+
} else {
|
|
527
|
+
// Non-interactive components can inherit parent's scope or use fallback
|
|
528
|
+
lines.push(` const __scopeId = __bfScope || __instanceId || \`${name}_\${Math.random().toString(36).slice(2, 8)}\``)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (signalInits) {
|
|
532
|
+
lines.push(signalInits)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Generate props serialization code (flat format)
|
|
536
|
+
// Only the outermost component reads bf-p via hydrate(); children get props via initChild().
|
|
537
|
+
if (hasPropsToSerialize) {
|
|
538
|
+
lines.push('')
|
|
539
|
+
lines.push(` // Serialize props for client hydration`)
|
|
540
|
+
lines.push(` const __hydrateProps: Record<string, unknown> = {}`)
|
|
541
|
+
for (const p of propsToSerialize) {
|
|
542
|
+
// Skip functions and JSX elements (they can't be JSON serialized)
|
|
543
|
+
// Use propsObjectName.propName for SolidJS-style, direct propName for destructured
|
|
544
|
+
const propAccess = propsObjectName ? `${propsObjectName}.${p.name}` : p.name
|
|
545
|
+
lines.push(` if (typeof ${propAccess} !== 'function' && !(typeof ${propAccess} === 'object' && ${propAccess} !== null && 'isEscaped' in ${propAccess})) __hydrateProps['${p.name}'] = ${propAccess}`)
|
|
546
|
+
}
|
|
547
|
+
lines.push(` const __bfPropsJson = __bfParentProps || (Object.keys(__hydrateProps).length > 0 ? JSON.stringify(__hydrateProps) : undefined)`)
|
|
548
|
+
} else if (hasClientInteractivity && isRootComponent) {
|
|
549
|
+
// No own props, but root is a component — pass through parent's props
|
|
550
|
+
lines.push('')
|
|
551
|
+
lines.push(` const __bfPropsJson = __bfParentProps`)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
lines.push('')
|
|
555
|
+
|
|
556
|
+
// Handle if-statement roots (early return pattern)
|
|
557
|
+
if (isIfStatement) {
|
|
558
|
+
lines.push(ifCode)
|
|
559
|
+
lines.push(`}`)
|
|
560
|
+
return lines.join('\n')
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
lines.push(` return (`)
|
|
564
|
+
lines.push(` ${jsxBody}`)
|
|
565
|
+
lines.push(` )`)
|
|
566
|
+
lines.push(`}`)
|
|
567
|
+
|
|
568
|
+
return lines.join('\n')
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ===========================================================================
|
|
572
|
+
// Node Rendering
|
|
573
|
+
// ===========================================================================
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Public entry point for node rendering. Delegates to the shared
|
|
577
|
+
* `IRNodeEmitter` dispatcher (#1290 step 1); per-kind logic lives in
|
|
578
|
+
* the `IRNodeEmitter` methods below.
|
|
579
|
+
*/
|
|
580
|
+
renderNode(node: IRNode, ctx?: HonoRenderCtx): string {
|
|
581
|
+
return emitIRNode<HonoRenderCtx>(node, this, ctx ?? {})
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ===========================================================================
|
|
585
|
+
// IRNodeEmitter implementation (Hono JSX)
|
|
586
|
+
// ===========================================================================
|
|
587
|
+
|
|
588
|
+
emitElement(node: IRElement, ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string {
|
|
589
|
+
return this.renderElement(node, ctx)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
emitText(node: IRText): string {
|
|
593
|
+
return this.renderText(node)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
emitExpression(node: IRExpression): string {
|
|
597
|
+
return this.renderExpression(node)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
emitConditional(node: IRConditional, _ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string {
|
|
601
|
+
return this.renderConditional(node)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
emitLoop(node: IRLoop, _ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string {
|
|
605
|
+
return this.renderLoop(node)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
emitComponent(node: IRComponent, ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string {
|
|
609
|
+
return this.renderComponent(node, ctx)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
emitFragment(node: IRFragment, _ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string {
|
|
613
|
+
return this.renderFragment(node)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
emitSlot(_node: IRSlot): string {
|
|
617
|
+
return '{children}'
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
emitIfStatement(_node: IRIfStatement, _ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string {
|
|
621
|
+
// If-statements are rendered at the component level (early-return pattern),
|
|
622
|
+
// never inline. This arm is unreachable in practice but is required by
|
|
623
|
+
// the IRNodeEmitter exhaustiveness contract.
|
|
624
|
+
return ''
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
emitProvider(node: IRProvider, _ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string {
|
|
628
|
+
const children = this.renderChildren(node.children)
|
|
629
|
+
// Quote literal values; expression / template / spread variants emit
|
|
630
|
+
// their JS source verbatim into the Hono JSX output.
|
|
631
|
+
const valueExpr = (() => {
|
|
632
|
+
const v = node.valueProp.value
|
|
633
|
+
switch (v.kind) {
|
|
634
|
+
case 'literal': return JSON.stringify(v.value)
|
|
635
|
+
case 'expression':
|
|
636
|
+
case 'spread': return v.expr
|
|
637
|
+
case 'template': return this.renderTemplateLiteralParts(v.parts)
|
|
638
|
+
case 'boolean-attr':
|
|
639
|
+
case 'boolean-shorthand': return 'true'
|
|
640
|
+
case 'jsx-children': return 'undefined'
|
|
641
|
+
}
|
|
642
|
+
})()
|
|
643
|
+
// Bridge BarefootJS Context to Hono's per-render context stack so
|
|
644
|
+
// descendants that call useContext() at SSR see the provided value.
|
|
645
|
+
// `provideContextSSR` is a helper exported from the client shim
|
|
646
|
+
// (`@barefootjs/hono/client-shim`); generateImports auto-injects the
|
|
647
|
+
// import when this expression is present in the rendered output.
|
|
648
|
+
// The outer fragment makes the form valid JSX whether the provider
|
|
649
|
+
// appears as the component root or nested inside JSX siblings.
|
|
650
|
+
return `<>{provideContextSSR(${node.contextName}, ${valueExpr}, <>${children}</>)}</>`
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
emitAsync(node: IRAsync, _ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string {
|
|
654
|
+
return this.renderAsync(node)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
renderElement(element: IRElement, ctx?: { isLoopItemRoot?: boolean }): string {
|
|
658
|
+
const tag = element.tag
|
|
659
|
+
const attrs = this.renderAttributes(element)
|
|
660
|
+
const children = this.renderChildren(element.children)
|
|
661
|
+
|
|
662
|
+
// Add hydration markers
|
|
663
|
+
let hydrationAttrs = ''
|
|
664
|
+
if (element.needsScope) {
|
|
665
|
+
// Hydration markers (see spec/compiler.md "Slot identity"):
|
|
666
|
+
// bf-s = addressable scope id
|
|
667
|
+
// bf-h / bf-m = slot identity of a child scope
|
|
668
|
+
// bf-r = root-of-client-component marker
|
|
669
|
+
// bf-p = serialized props (root only; children receive props via initChild)
|
|
670
|
+
hydrationAttrs += ` ${BF_SCOPE}={__scopeId}`
|
|
671
|
+
hydrationAttrs += ` {...(__bfParent ? { "${BF_HOST}": __bfParent } : {})}`
|
|
672
|
+
hydrationAttrs += ` {...(__bfMount ? { "${BF_AT}": __bfMount } : {})}`
|
|
673
|
+
hydrationAttrs += ` {...(!__bfChild ? { "${BF_ROOT}": "" } : {})}`
|
|
674
|
+
if (this.currentComponentHasProps) {
|
|
675
|
+
hydrationAttrs += ` {...(!__bfChild && __bfPropsJson ? { "${BF_PROPS}": __bfPropsJson } : {})}`
|
|
676
|
+
}
|
|
677
|
+
// Add data-key for list reconciliation (only on root elements with scope)
|
|
678
|
+
hydrationAttrs += ' {...(__dataKey !== undefined ? { "data-key": __dataKey } : {})}'
|
|
679
|
+
}
|
|
680
|
+
// Add data-key-N for loop items so event delegation can identify inner items
|
|
681
|
+
if (ctx?.isLoopItemRoot && this.loopKeyStack.length > 0) {
|
|
682
|
+
const loop = this.loopKeyStack[this.loopKeyStack.length - 1]
|
|
683
|
+
if (loop.key) {
|
|
684
|
+
const keyAttrName = this.loopKeyStack.length === 1 ? 'data-key' : `data-key-${this.loopKeyStack.length - 1}`
|
|
685
|
+
hydrationAttrs += ` ${keyAttrName}={String(${loop.key})}`
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (element.slotId) {
|
|
689
|
+
hydrationAttrs += ` bf="${element.slotId}"`
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (children) {
|
|
693
|
+
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
|
|
694
|
+
} else {
|
|
695
|
+
return `<${tag}${attrs}${hydrationAttrs} />`
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private renderText(text: IRText): string {
|
|
700
|
+
return text.value
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
renderExpression(expr: IRExpression): string {
|
|
704
|
+
// Keep null as 'null' for proper JSX rendering
|
|
705
|
+
if (expr.expr === 'null' || expr.expr === 'undefined') {
|
|
706
|
+
return 'null'
|
|
707
|
+
}
|
|
708
|
+
// Handle @client directive - render comment marker for client-side evaluation
|
|
709
|
+
if (expr.clientOnly && expr.slotId) {
|
|
710
|
+
return `{bfComment("client:${expr.slotId}")}`
|
|
711
|
+
}
|
|
712
|
+
// Mark expressions with slotId using comment nodes for client JS to find.
|
|
713
|
+
// This includes reactive expressions AND loop-param-dependent expressions
|
|
714
|
+
// (which become reactive via per-item signals on the client).
|
|
715
|
+
if (expr.slotId) {
|
|
716
|
+
return `{bfText("${expr.slotId}")}{${expr.expr}}{bfTextEnd()}`
|
|
717
|
+
}
|
|
718
|
+
return `{${expr.expr}}`
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
renderConditional(cond: IRConditional): string {
|
|
722
|
+
// Handle @client directive - render comment markers for client-side evaluation
|
|
723
|
+
if (cond.clientOnly && cond.slotId) {
|
|
724
|
+
return `{bfComment("cond-start:${cond.slotId}")}{bfComment("cond-end:${cond.slotId}")}`
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const whenTrue = this.renderNodeRaw(cond.whenTrue)
|
|
728
|
+
let whenFalse = this.renderNodeRaw(cond.whenFalse)
|
|
729
|
+
|
|
730
|
+
// Handle empty/null whenFalse
|
|
731
|
+
if (!whenFalse || whenFalse === '' || whenFalse === 'null') {
|
|
732
|
+
whenFalse = 'null'
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// If reactive, wrap with markers
|
|
736
|
+
if (cond.slotId) {
|
|
737
|
+
const trueWithMarker = this.wrapWithCondMarker(cond.whenTrue, whenTrue, cond.slotId)
|
|
738
|
+
// For null false branch, render comment markers so client can insert content later
|
|
739
|
+
const falseWithMarker = cond.whenFalse.type === 'expression' && cond.whenFalse.expr === 'null'
|
|
740
|
+
? `<>{bfComment("cond-start:${cond.slotId}")}{bfComment("cond-end:${cond.slotId}")}</>`
|
|
741
|
+
: this.wrapWithCondMarker(cond.whenFalse, whenFalse, cond.slotId)
|
|
742
|
+
|
|
743
|
+
return `{${cond.condition} ? ${trueWithMarker} : ${falseWithMarker}}`
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return `{${cond.condition} ? ${whenTrue} : ${whenFalse}}`
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private wrapWithCondMarker(node: IRNode, content: string, condId: string): string {
|
|
750
|
+
// Components don't reliably forward bf-c to their root element.
|
|
751
|
+
// Use comment markers so insert() can find them via TreeWalker.
|
|
752
|
+
// This matches the client-side template behavior (renderChild returns
|
|
753
|
+
// ${...} expressions which also get comment-wrapped by addCondAttrToTemplate).
|
|
754
|
+
if (node.type === 'component') {
|
|
755
|
+
return `<>{bfComment("cond-start:${condId}")}${content}{bfComment("cond-end:${condId}")}</>`
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// If content is a single raw HTML element, add bf-c attribute.
|
|
759
|
+
// For fragments (multiple sibling elements), use comment markers.
|
|
760
|
+
if (content.startsWith('<') && node.type !== 'fragment') {
|
|
761
|
+
const match = content.match(/^<(\w+)/)
|
|
762
|
+
if (match) {
|
|
763
|
+
return content.replace(`<${match[1]}`, `<${match[1]} bf-c="${condId}"`)
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Expression node: wrap in braces for valid JSX
|
|
768
|
+
if (node.type === 'expression') {
|
|
769
|
+
return `<>{bfComment("cond-start:${condId}")}{${content}}{bfComment("cond-end:${condId}")}</>`
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Text node or other: output as text
|
|
773
|
+
return `<>{bfComment("cond-start:${condId}")}${content}{bfComment("cond-end:${condId}")}</>`
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
renderLoop(loop: IRLoop): string {
|
|
777
|
+
// clientOnly loops must not render items at SSR time, but must still emit
|
|
778
|
+
// <!--bf-loop:<id>--><!--bf-/loop:<id>--> boundary markers so that mapArray()
|
|
779
|
+
// on the client can locate the correct anchor node when inserting items.
|
|
780
|
+
// Without the markers, mapArray() resolves anchor = null and appends new
|
|
781
|
+
// elements after sibling markers (e.g. <!--bf-cond-start-->). (#872)
|
|
782
|
+
// The marker id disambiguates sibling `.map()` calls under the same
|
|
783
|
+
// parent (#1087).
|
|
784
|
+
if (loop.clientOnly) {
|
|
785
|
+
return `{bfComment('loop:${loop.markerId}')}{bfComment('/loop:${loop.markerId}')}`
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Preserve type annotations for loop params in .tsx output
|
|
789
|
+
const paramAnnotation = loop.paramType ? `: ${loop.paramType}` : ''
|
|
790
|
+
const indexAnnotation = loop.indexType ? `: ${loop.indexType}` : ''
|
|
791
|
+
const indexParam = loop.index ? `, ${loop.index}${indexAnnotation}` : ''
|
|
792
|
+
// Push loop key info for data-key attribute generation on loop items
|
|
793
|
+
this.loopKeyStack.push({ key: loop.key, param: loop.param })
|
|
794
|
+
// Render children with isInsideLoop flag so components generate their own scope IDs
|
|
795
|
+
const children = this.renderChildrenInLoop(loop.children)
|
|
796
|
+
this.loopKeyStack.pop()
|
|
797
|
+
|
|
798
|
+
let mapExpr: string
|
|
799
|
+
// Use typed mapPreamble when available to preserve type annotations in .tsx output
|
|
800
|
+
const preamble = loop.typedMapPreamble ?? loop.mapPreamble
|
|
801
|
+
// When the rendered children are a JSX expression-container (e.g. a single
|
|
802
|
+
// ternary `{cond ? <A/> : <B/>}` from renderConditional), they cannot be
|
|
803
|
+
// used directly as an arrow body — `(x) => {…}` is parsed as a block
|
|
804
|
+
// statement and the function returns undefined. Wrap with a fragment so
|
|
805
|
+
// the body is unambiguously a JSX expression.
|
|
806
|
+
let safeChildren = children.startsWith('{') ? `<>${children}</>` : children
|
|
807
|
+
// Multi-root Fragment items (#1212): prepend a per-item start marker so
|
|
808
|
+
// mapArray can pair each key with all of its DOM nodes. Wrap the body
|
|
809
|
+
// in a Fragment so the prefix and the existing children share an arrow
|
|
810
|
+
// expression body.
|
|
811
|
+
if (loop.bodyIsMultiRoot) {
|
|
812
|
+
// Per-item start marker: BF_LOOP_ITEM ('bf-loop-i'). Hardcoded
|
|
813
|
+
// literal here to match the adapter's existing convention of
|
|
814
|
+
// emitting comment-marker strings directly.
|
|
815
|
+
safeChildren = `<>{bfComment('bf-loop-i')}${children}</>`
|
|
816
|
+
}
|
|
817
|
+
// Apply chained `.sort()` / `.filter()` extracted to
|
|
818
|
+
// `loop.sortComparator` / `loop.filterPredicate` (#1448 Tier B).
|
|
819
|
+
// Pre-Tier-B this used `loop.array` directly — fine when an
|
|
820
|
+
// SSR-side adapter (Go's `bf_sort`) applied the sort separately,
|
|
821
|
+
// broken on Hono where the emitted JSX is the source of truth
|
|
822
|
+
// for both SSR (runtime-eval) and CSR (template fallback).
|
|
823
|
+
// `.toSorted` (non-mutating) preserves shared prop arrays across
|
|
824
|
+
// renders — `.sort()` here would reorder `_p.items` in place.
|
|
825
|
+
const chainedArray = applyHonoLoopChain(loop)
|
|
826
|
+
const iterMethod = loop.method ?? 'map'
|
|
827
|
+
|
|
828
|
+
if (loop.flatMapCallback) {
|
|
829
|
+
// Complex flatMap: use the original raw callback body (preserves JSX
|
|
830
|
+
// for Hono's runtime JSX evaluation).
|
|
831
|
+
mapExpr = `{${chainedArray}.flatMap(${loop.flatMapCallback.params} => ${loop.flatMapCallback.rawBody})}`
|
|
832
|
+
} else if (preamble) {
|
|
833
|
+
mapExpr = `{${chainedArray}.${iterMethod}((${loop.param}${paramAnnotation}${indexParam}) => { ${preamble} return ${safeChildren} })}`
|
|
834
|
+
} else {
|
|
835
|
+
mapExpr = `{${chainedArray}.${iterMethod}((${loop.param}${paramAnnotation}${indexParam}) => ${safeChildren})}`
|
|
836
|
+
}
|
|
837
|
+
// Wrap with loop boundary markers so reconciliation doesn't affect siblings.
|
|
838
|
+
// bfComment('loop:<id>') → <!--bf-loop:<id>-->. The marker id is unique
|
|
839
|
+
// per loop call site so sibling `.map()` calls under the same parent
|
|
840
|
+
// get their own reconciliation range (#1087).
|
|
841
|
+
return `{bfComment('loop:${loop.markerId}')}${mapExpr}{bfComment('/loop:${loop.markerId}')}`
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
private renderChildrenInLoop(children: IRNode[]): string {
|
|
845
|
+
return children.map((child) => this.renderNode(child, { isInsideLoop: true, isLoopItemRoot: true })).join('')
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Render an if-statement chain as function-level code.
|
|
850
|
+
* This is used for components with early return patterns.
|
|
851
|
+
*/
|
|
852
|
+
renderIfStatement(ifStmt: IRIfStatement, ctx?: { isRootOfClientComponent?: boolean }): string {
|
|
853
|
+
const lines: string[] = []
|
|
854
|
+
|
|
855
|
+
// Generate scope variables declared in this if block. The Hono SSR
|
|
856
|
+
// template is a `.tsx` file checked by tsc, so prefer the typed
|
|
857
|
+
// initializer when present to keep `as <T>` casts intact — without
|
|
858
|
+
// them, an emitted `const Tag = children.tag` (cast lost) raises
|
|
859
|
+
// TS2604 at `<Tag/>` because `unknown` has no call signature. See
|
|
860
|
+
// IRIfStatement.scopeVariables.typedInitializer docstring (#1453).
|
|
861
|
+
for (const v of ifStmt.scopeVariables) {
|
|
862
|
+
const init = (this.jsxConfig.preserveTypes && v.typedInitializer) || v.initializer
|
|
863
|
+
lines.push(` const ${v.name} = ${init}`)
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Render the consequent (then branch) JSX
|
|
867
|
+
const consequent = this.renderNode(ifStmt.consequent, ctx)
|
|
868
|
+
|
|
869
|
+
// Build the if statement
|
|
870
|
+
lines.unshift(` if (${ifStmt.condition}) {`)
|
|
871
|
+
lines.push(` return (`)
|
|
872
|
+
lines.push(` ${consequent}`)
|
|
873
|
+
lines.push(` )`)
|
|
874
|
+
lines.push(` }`)
|
|
875
|
+
|
|
876
|
+
// Handle the alternate (else branch)
|
|
877
|
+
if (ifStmt.alternate) {
|
|
878
|
+
if (ifStmt.alternate.type === 'if-statement') {
|
|
879
|
+
// else if chain - recursively render
|
|
880
|
+
const elseIfCode = this.renderIfStatement(ifStmt.alternate as IRIfStatement, ctx)
|
|
881
|
+
// Replace the leading 'if' with 'else if'
|
|
882
|
+
lines.push(elseIfCode.replace(/^\s*if/, ' else if'))
|
|
883
|
+
} else {
|
|
884
|
+
// Final else branch with regular JSX
|
|
885
|
+
const alternate = this.renderNode(ifStmt.alternate, ctx)
|
|
886
|
+
lines.push(` return (`)
|
|
887
|
+
lines.push(` ${alternate}`)
|
|
888
|
+
lines.push(` )`)
|
|
889
|
+
}
|
|
890
|
+
} else {
|
|
891
|
+
// No alternate - return null
|
|
892
|
+
lines.push(` return null`)
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return lines.join('\n')
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
renderAsync(node: IRAsync): string {
|
|
899
|
+
const fallback = this.renderNode(node.fallback)
|
|
900
|
+
const children = this.renderChildren(node.children)
|
|
901
|
+
return `<Suspense fallback={<>${fallback}</>}>${children}</Suspense>`
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
renderComponent(comp: IRComponent, ctx?: { isRootOfClientComponent?: boolean; isInsideLoop?: boolean; isLoopItemRoot?: boolean }): string {
|
|
905
|
+
const props = this.renderComponentProps(comp)
|
|
906
|
+
const children = this.renderChildren(comp.children)
|
|
907
|
+
|
|
908
|
+
// Determine how to pass scope to child component
|
|
909
|
+
let scopeAttr: string
|
|
910
|
+
// Mark child components with slotId for parent-first hydration
|
|
911
|
+
// Add __bfChild when parent has client interactivity (will call initChild)
|
|
912
|
+
const bfChildAttr = (comp.slotId && this.hasClientInteractivity) ? ' __bfChild={true}' : ''
|
|
913
|
+
// Pass parent scope + slot id to the child so it can stamp bf-h / bf-m
|
|
914
|
+
// for upsertChild's (bf-h, bf-m) primary lookup (#1249).
|
|
915
|
+
const bfMountAttr = comp.slotId ? ` __bfParent={__scopeId} __bfMount={'${comp.slotId}'}` : ''
|
|
916
|
+
if (ctx?.isRootOfClientComponent) {
|
|
917
|
+
// Root component: if it has a slotId, include it so client JS can find it
|
|
918
|
+
// with [bf-s$="_sX"] selector. Otherwise pass parent's scope directly.
|
|
919
|
+
// Note: Do NOT add __bfChild here - the root is the main hydration target, not a child.
|
|
920
|
+
// Pass __bfParentProps so child component can use parent's serialized props
|
|
921
|
+
const propsPassAttr = this.currentComponentHasProps ? ' __bfParentProps={__bfPropsJson}' : ''
|
|
922
|
+
if (comp.slotId) {
|
|
923
|
+
scopeAttr = ` __instanceId={\`\${__scopeId}_${comp.slotId}\`}${propsPassAttr}${bfMountAttr}`
|
|
924
|
+
} else {
|
|
925
|
+
scopeAttr = ` __instanceId={__scopeId}${propsPassAttr}`
|
|
926
|
+
}
|
|
927
|
+
// Also pass bf-s for asChild/Slot patterns where the component
|
|
928
|
+
// forwards props to a DOM element via {...props}.
|
|
929
|
+
scopeAttr += ` ${BF_SCOPE}={__scopeId}`
|
|
930
|
+
} else if (ctx?.isInsideLoop) {
|
|
931
|
+
// Components inside loops should generate their own unique scope IDs
|
|
932
|
+
// Pass __bfScope so they use it as fallback but generate unique IDs
|
|
933
|
+
// This ensures each loop iteration has a distinct component instance
|
|
934
|
+
if (comp.slotId) {
|
|
935
|
+
scopeAttr = ` __bfScope={\`\${__scopeId}_${comp.slotId}\`}${bfChildAttr}${bfMountAttr}`
|
|
936
|
+
} else {
|
|
937
|
+
scopeAttr = ' __bfScope={__scopeId}'
|
|
938
|
+
}
|
|
939
|
+
} else if (comp.slotId) {
|
|
940
|
+
// Components with slotId need unique scope with slot suffix
|
|
941
|
+
// Format: ParentName_slotX for client JS matching
|
|
942
|
+
scopeAttr = ` __instanceId={\`\${__scopeId}_${comp.slotId}\`}${bfChildAttr}${bfMountAttr}`
|
|
943
|
+
} else {
|
|
944
|
+
// Non-interactive components inherit parent's scope
|
|
945
|
+
scopeAttr = ' __instanceId={__scopeId}'
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (children) {
|
|
949
|
+
return `<${comp.name}${props}${scopeAttr}>${children}</${comp.name}>`
|
|
950
|
+
} else {
|
|
951
|
+
return `<${comp.name}${props}${scopeAttr} />`
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
private renderFragment(fragment: IRFragment): string {
|
|
956
|
+
const children = this.renderChildren(fragment.children)
|
|
957
|
+
if (fragment.needsScopeComment) {
|
|
958
|
+
return this.wrapWithScopeComment(children)
|
|
959
|
+
}
|
|
960
|
+
return `<>${children}</>`
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Wrap `body` in a fragment-rooted scope comment.
|
|
965
|
+
* Shape (matches `hydrate.ts::hydrateCommentScope`):
|
|
966
|
+
* root: <!--bf-scope:<scopeId>|<propsJson>-->
|
|
967
|
+
* child: <!--bf-scope:<scopeId>|h=<host>|m=<slot>|<propsJson>-->
|
|
968
|
+
* `<scopeId>` stays at the front so child detection can anchor on `|h=`.
|
|
969
|
+
*/
|
|
970
|
+
private wrapWithScopeComment(body: string): string {
|
|
971
|
+
const hostExpr = '${__bfParent ? `|h=${__bfParent}|m=${__bfMount}` : ""}'
|
|
972
|
+
const propsExpr = this.currentComponentHasProps
|
|
973
|
+
? '${__bfPropsJson ? `|${__bfPropsJson}` : ""}'
|
|
974
|
+
: ''
|
|
975
|
+
return `<>{bfComment(\`scope:\${__scopeId}${hostExpr}${propsExpr}\`)}${body}</>`
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ===========================================================================
|
|
979
|
+
// Attribute Rendering
|
|
980
|
+
// ===========================================================================
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* AttrValue lowering for intrinsic-element attributes (Hono JSX).
|
|
984
|
+
* Per-kind logic that used to live in a `switch (v.kind)` inside
|
|
985
|
+
* `renderAttributes`; routed through the shared dispatcher so a new
|
|
986
|
+
* AttrValue kind becomes a TS compile error here (#1290 step 2).
|
|
987
|
+
*/
|
|
988
|
+
private readonly elementAttrEmitter: AttrValueEmitter = {
|
|
989
|
+
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
990
|
+
emitExpression: (value, name) => {
|
|
991
|
+
// Boolean attrs / presence-folded expressions: pass `undefined` when
|
|
992
|
+
// falsy so Hono omits the attribute. Wrap in parens to keep `??`
|
|
993
|
+
// operators inside `expr` from breaking the surrounding `|| undefined`.
|
|
994
|
+
if (isBooleanAttr(name) || value.presenceOrUndefined) {
|
|
995
|
+
return `${name}={(${value.expr}) || undefined}`
|
|
996
|
+
}
|
|
997
|
+
return `${name}={${value.expr}}`
|
|
998
|
+
},
|
|
999
|
+
emitBooleanAttr: (_value, name) => name,
|
|
1000
|
+
emitBooleanShorthand: () => '',
|
|
1001
|
+
emitTemplate: (value, name) => `${name}={${this.renderTemplateLiteralParts(value.parts)}}`,
|
|
1002
|
+
emitSpread: (value) => `{...${value.expr}}`,
|
|
1003
|
+
// Neither boolean-shorthand nor jsx-children is legal on intrinsic
|
|
1004
|
+
// elements. Returning empty string drops the entry silently — matches
|
|
1005
|
+
// pre-#1290 behavior.
|
|
1006
|
+
emitJsxChildren: () => '',
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* AttrValue lowering for component-invocation props (Hono JSX).
|
|
1011
|
+
* Component props differ from intrinsic attrs in several places —
|
|
1012
|
+
* `jsx-children` is rendered as `<>…</>`, `expression` skips the
|
|
1013
|
+
* boolean-attr fold, etc. Kept as a separate emitter so each method
|
|
1014
|
+
* does one thing.
|
|
1015
|
+
*/
|
|
1016
|
+
private readonly componentPropEmitter: AttrValueEmitter = {
|
|
1017
|
+
emitLiteral: (value, name) =>
|
|
1018
|
+
// IR-authoritative string literal: `<X fill="var(--c)" />`.
|
|
1019
|
+
// Emitting verbatim is what distinguishes a CSS-shaped value
|
|
1020
|
+
// (`var(...)`, `url(...)`, `calc(...)`) from a JS expression.
|
|
1021
|
+
`${name}="${value.value}"`,
|
|
1022
|
+
emitExpression: (value, name) => `${name}={${value.expr}}`,
|
|
1023
|
+
emitBooleanAttr: (_value, name) => name,
|
|
1024
|
+
emitBooleanShorthand: (_value, name) => name,
|
|
1025
|
+
emitTemplate: (value, name) => `${name}={${this.renderTemplateLiteralParts(value.parts)}}`,
|
|
1026
|
+
emitSpread: (value) => `{...${value.expr}}`,
|
|
1027
|
+
emitJsxChildren: (value, name) => {
|
|
1028
|
+
const rendered = value.children.map((c) => this.renderNode(c)).join('')
|
|
1029
|
+
return `${name}={<>${rendered}</>}`
|
|
1030
|
+
},
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
private renderAttributes(element: IRElement): string {
|
|
1034
|
+
const parts: string[] = []
|
|
1035
|
+
|
|
1036
|
+
for (const attr of element.attrs) {
|
|
1037
|
+
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attr.name)
|
|
1038
|
+
if (lowered) parts.push(lowered)
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Add event handlers (as no-op for SSR)
|
|
1042
|
+
for (const event of element.events) {
|
|
1043
|
+
const handlerName = event.originalAttr ?? `on${event.name.charAt(0).toUpperCase()}${event.name.slice(1)}`
|
|
1044
|
+
parts.push(`${handlerName}={() => {}}`)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return parts.length > 0 ? ' ' + parts.join(' ') : ''
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
private renderComponentProps(comp: IRComponent): string {
|
|
1051
|
+
const parts: string[] = []
|
|
1052
|
+
let keyValue: string | null = null
|
|
1053
|
+
|
|
1054
|
+
for (const prop of comp.props) {
|
|
1055
|
+
if (prop.name === 'key') {
|
|
1056
|
+
// JSX key → data-key only. Hono JSX strips `key` from HTML output
|
|
1057
|
+
// (delete props["key"]), so emitting key={} is a no-op. We only need
|
|
1058
|
+
// data-key which the BarefootJS client runtime uses for reconciliation.
|
|
1059
|
+
keyValue = this.attrValueToJsExpr(prop.value)
|
|
1060
|
+
continue
|
|
1061
|
+
}
|
|
1062
|
+
const lowered = emitAttrValue(prop.value, this.componentPropEmitter, prop.name)
|
|
1063
|
+
if (lowered) parts.push(lowered)
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Add data-key prop when key is present for client-side reconciliation
|
|
1067
|
+
// This allows the child component to add data-key attribute to its root element
|
|
1068
|
+
if (keyValue) {
|
|
1069
|
+
parts.push(`data-key={${keyValue}}`)
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
return parts.length > 0 ? ' ' + parts.join(' ') : ''
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
private attrValueToJsExpr(value: AttrValue): string {
|
|
1076
|
+
switch (value.kind) {
|
|
1077
|
+
case 'literal': return JSON.stringify(value.value)
|
|
1078
|
+
case 'expression':
|
|
1079
|
+
case 'spread': return value.expr
|
|
1080
|
+
case 'template': return this.renderTemplateLiteralParts(value.parts)
|
|
1081
|
+
case 'boolean-shorthand':
|
|
1082
|
+
case 'boolean-attr': return 'true'
|
|
1083
|
+
case 'jsx-children': return 'undefined'
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
private renderTemplateLiteralParts(parts: IRTemplatePart[]): string {
|
|
1088
|
+
let output = '`'
|
|
1089
|
+
for (const part of parts) {
|
|
1090
|
+
if (part.type === 'string') {
|
|
1091
|
+
output += part.value
|
|
1092
|
+
} else if (part.type === 'ternary') {
|
|
1093
|
+
output += `\${${part.condition} ? '${part.whenTrue}' : '${part.whenFalse}'}`
|
|
1094
|
+
} else if (part.type === 'lookup') {
|
|
1095
|
+
// Hono runs JS at SSR time, so a `${MAP[KEY]}` lookup can be
|
|
1096
|
+
// re-materialised as a runtime indexed access against the
|
|
1097
|
+
// resolved cases — byte-identical to the client emit path in
|
|
1098
|
+
// `ir-to-client-js/utils.ts`. Use `part.key` (raw JS source)
|
|
1099
|
+
// because this output runs inside the destructured-prop scope
|
|
1100
|
+
// of the component, mirroring the `'ternary'` branch above.
|
|
1101
|
+
const obj = '{' + Object.entries(part.cases).map(
|
|
1102
|
+
([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
|
|
1103
|
+
).join(', ') + '}'
|
|
1104
|
+
output += `\${(${obj})[${part.key}]}`
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
output += '`'
|
|
1108
|
+
return output
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Export singleton instance for convenience
|
|
1114
|
+
export const honoAdapter = new HonoAdapter()
|