@barefootjs/hono 0.31.0 → 0.31.2
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 +67 -2
- package/dist/adapter/hono-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +46 -187398
- package/dist/app.js +0 -71
- package/dist/async.js +0 -71
- package/dist/client-shim.js +0 -71
- package/dist/dev-worker.js +0 -71
- package/dist/dialog-context.js +0 -71
- package/dist/index.js +46 -187398
- package/dist/jsx/jsx-dev-runtime/index.d.ts +3 -1
- package/dist/jsx/jsx-dev-runtime/index.d.ts.map +1 -1
- package/dist/jsx/jsx-dev-runtime/index.js +14 -69
- package/dist/jsx/jsx-runtime/index.d.ts +4 -1
- package/dist/jsx/jsx-runtime/index.d.ts.map +1 -1
- package/dist/jsx/jsx-runtime/index.js +24 -69
- package/dist/jsx/resolve-dangerously-set-inner-html.d.ts +2 -0
- package/dist/jsx/resolve-dangerously-set-inner-html.d.ts.map +1 -0
- package/dist/portal-ssr.js +0 -71
- package/dist/portals.js +0 -71
- package/dist/preload.js +0 -71
- package/dist/render.js +0 -71
- package/dist/request-env.js +0 -71
- package/dist/scripts.d.ts +3 -2
- package/dist/scripts.d.ts.map +1 -1
- package/dist/scripts.js +0 -71
- package/dist/utils.js +0 -71
- package/dist/vite.js +313 -142
- package/package.json +2 -2
- package/src/__tests__/aliased-destructured-prop.test.ts +8 -7
- package/src/__tests__/consumer-typecheck.test.ts +403 -0
- package/src/__tests__/corpus-typecheck.test.ts +130 -0
- package/src/__tests__/dangerously-set-inner-html.test.ts +70 -0
- package/src/__tests__/nested-ternary-bare-branch.test.ts +70 -0
- package/src/adapter/hono-adapter.ts +123 -166
- package/src/jsx/jsx-dev-runtime/index.ts +13 -1
- package/src/jsx/jsx-runtime/index.ts +24 -1
- package/src/jsx/resolve-dangerously-set-inner-html.ts +34 -0
- package/src/scripts.tsx +3 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2470: a nested ternary chain (`a ? … : b ? … : …`) sitting in the
|
|
3
|
+
* ALTERNATE of a NON-reactive outer conditional (no signal/prop/call —
|
|
4
|
+
* a module-level `const`) used to emit
|
|
5
|
+
*
|
|
6
|
+
* {MODE === 'a' ? <span>A</span> : {MODE === 'b' ? <span>B</span> : <span>C</span>}}
|
|
7
|
+
*
|
|
8
|
+
* — the nested conditional re-wrapped in its own `{…}` in a position where
|
|
9
|
+
* only a bare JS expression is legal, breaking the `.tsx` parse
|
|
10
|
+
* (`Expected "}" but found "==="`) with zero diagnostics. See
|
|
11
|
+
* `renderConditionalBody` / `renderBareBranch` in `hono-adapter.ts`.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, test } from 'bun:test'
|
|
14
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
15
|
+
import { join, resolve } from 'node:path'
|
|
16
|
+
import ts from 'typescript'
|
|
17
|
+
import { compileJSX } from '@barefootjs/jsx'
|
|
18
|
+
import { HonoAdapter } from '../adapter/index.ts'
|
|
19
|
+
|
|
20
|
+
const HERE = resolve(import.meta.dir)
|
|
21
|
+
|
|
22
|
+
const SOURCE = `
|
|
23
|
+
const MODE = 'b'
|
|
24
|
+
export function Chain() {
|
|
25
|
+
return <div>{MODE === 'a' ? <span>A</span> : MODE === 'b' ? <span>B</span> : <span>C</span>}</div>
|
|
26
|
+
}
|
|
27
|
+
`
|
|
28
|
+
|
|
29
|
+
describe('nested ternary under a non-reactive outer condition (#2470)', () => {
|
|
30
|
+
test('the emitted template has no double-braced nested conditional', () => {
|
|
31
|
+
const result = compileJSX(SOURCE, '/virtual/Chain.tsx', {
|
|
32
|
+
adapter: new HonoAdapter(),
|
|
33
|
+
})
|
|
34
|
+
expect(result.errors.filter(e => e.severity === 'error')).toEqual([])
|
|
35
|
+
const template = result.files.find(f => f.type === 'markedTemplate')?.content
|
|
36
|
+
expect(template).toBeDefined()
|
|
37
|
+
|
|
38
|
+
// The nested conditional's branch must stay a bare `cond ? … : …`
|
|
39
|
+
// splice, not a second `{…}`-wrapped expression, inside the outer
|
|
40
|
+
// ternary's alternate.
|
|
41
|
+
expect(template).toContain(
|
|
42
|
+
"MODE === 'a' ? <span>A</span> : MODE === 'b' ? <span>B</span> : <span>C</span>",
|
|
43
|
+
)
|
|
44
|
+
expect(template).not.toContain(': {MODE ===')
|
|
45
|
+
|
|
46
|
+
// The template must actually PARSE as valid TSX — this is the
|
|
47
|
+
// regression the bug produced silently (no compiler diagnostic, just a
|
|
48
|
+
// downstream syntax error at build time). `getSyntacticDiagnostics` is
|
|
49
|
+
// the public API for this (mirrors `consumer-typecheck.test.ts`'s
|
|
50
|
+
// `ts.createProgram` usage, scoped to syntax only since this file has
|
|
51
|
+
// no import to resolve).
|
|
52
|
+
const tmp = mkdtempSync(join(HERE, '.nested-ternary-bare-branch-'))
|
|
53
|
+
try {
|
|
54
|
+
const file = join(tmp, 'Chain.tsx')
|
|
55
|
+
writeFileSync(file, template!)
|
|
56
|
+
const program = ts.createProgram([file], {
|
|
57
|
+
noEmit: true,
|
|
58
|
+
target: ts.ScriptTarget.ESNext,
|
|
59
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
60
|
+
jsxImportSource: '@barefootjs/hono/jsx',
|
|
61
|
+
allowImportingTsExtensions: true,
|
|
62
|
+
skipLibCheck: true,
|
|
63
|
+
})
|
|
64
|
+
const syntaxDiagnostics = program.getSyntacticDiagnostics(program.getSourceFile(file))
|
|
65
|
+
expect(syntaxDiagnostics.map(d => ts.flattenDiagnosticMessageText(d.messageText, ' '))).toEqual([])
|
|
66
|
+
} finally {
|
|
67
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
68
|
+
}
|
|
69
|
+
}, 30_000)
|
|
70
|
+
})
|
|
@@ -34,8 +34,8 @@ import {
|
|
|
34
34
|
emitIRNode,
|
|
35
35
|
emitAttrValue,
|
|
36
36
|
buildLoopChainExpr,
|
|
37
|
+
propsDestructureBinding,
|
|
37
38
|
} from '@barefootjs/jsx'
|
|
38
|
-
import ts from 'typescript'
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Hono adapter's IRNode render context: which surrounding render
|
|
@@ -98,28 +98,6 @@ function applyHonoLoopChain(loop: IRLoop): string {
|
|
|
98
98
|
})
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
/**
|
|
102
|
-
* Authoritative IdentifierName classification for a destructure-pattern
|
|
103
|
-
* property key, built on TS's own `isIdentifierStart` / `isIdentifierPart`
|
|
104
|
-
* primitives (Unicode-aware, stays aligned with what TS itself accepts as
|
|
105
|
-
* a bare property key). Mirrors the `isIdent` precedent in
|
|
106
|
-
* `jsx-to-ir.ts` (#1244) — a source key like `data-key` or `aria-label`
|
|
107
|
-
* can't be emitted as a bare `key: local` destructure and must be quoted
|
|
108
|
-
* (`"data-key": local`).
|
|
109
|
-
*/
|
|
110
|
-
function isIdentifierName(key: string): boolean {
|
|
111
|
-
if (key.length === 0) return false
|
|
112
|
-
for (let i = 0; i < key.length; ) {
|
|
113
|
-
const cp = key.codePointAt(i)!
|
|
114
|
-
const ok = i === 0
|
|
115
|
-
? ts.isIdentifierStart(cp, ts.ScriptTarget.Latest)
|
|
116
|
-
: ts.isIdentifierPart(cp, ts.ScriptTarget.Latest)
|
|
117
|
-
if (!ok) return false
|
|
118
|
-
i += cp > 0xFFFF ? 2 : 1
|
|
119
|
-
}
|
|
120
|
-
return true
|
|
121
|
-
}
|
|
122
|
-
|
|
123
101
|
export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderCtx> {
|
|
124
102
|
name = 'hono'
|
|
125
103
|
extension = '.tsx'
|
|
@@ -212,18 +190,16 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
212
190
|
this.preloadAssets = options?.preloadAssets
|
|
213
191
|
}
|
|
214
192
|
|
|
215
|
-
// Generate component body FIRST so we can scan it for used imports
|
|
193
|
+
// Generate component body FIRST so we can scan it for used imports.
|
|
194
|
+
// Module-scope declarations stay at module scope (#2570) — see
|
|
195
|
+
// `generateModuleScopeDeclarations`; they join the scan text so a
|
|
196
|
+
// helper referenced only from a hoisted declaration still pulls its
|
|
197
|
+
// import.
|
|
216
198
|
const component = this.generateComponent(ir)
|
|
217
|
-
const types = this.generateTypes(ir
|
|
218
|
-
const
|
|
199
|
+
const types = this.generateTypes(ir)
|
|
200
|
+
const moduleConstants = this.generateModuleScopeDeclarations(ir)
|
|
201
|
+
const componentCode = [moduleConstants, types, component].filter(Boolean).join('\n')
|
|
219
202
|
const imports = this.generateImports(ir, componentCode)
|
|
220
|
-
// Module-level Context bindings (`const Ctx = createContext()`) are
|
|
221
|
-
// skipped from the SSR signal-initializer block by JsxAdapter — they
|
|
222
|
-
// need to live at module scope so providers and consumers in the same
|
|
223
|
-
// render share the same Context object identity. Emitted in a dedicated
|
|
224
|
-
// section so multi-component dedup works on the full block (not per
|
|
225
|
-
// line, which would split multi-line `({...})` arguments).
|
|
226
|
-
const moduleConstants = this.generateModuleLevelContextBindings(ir)
|
|
227
203
|
|
|
228
204
|
const defaultExport = ir.metadata.hasDefaultExport
|
|
229
205
|
? `\nexport default ${this.componentName}`
|
|
@@ -235,6 +211,7 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
235
211
|
component,
|
|
236
212
|
defaultExport,
|
|
237
213
|
moduleConstants,
|
|
214
|
+
moduleConstantsIncludeExports: true,
|
|
238
215
|
}
|
|
239
216
|
|
|
240
217
|
// Assemble template for backward compat (external consumers using output.template)
|
|
@@ -275,20 +252,6 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
275
252
|
return this.hasScriptAssets() && !!this.preloadAssets && this.preloadAssets.length > 0
|
|
276
253
|
}
|
|
277
254
|
|
|
278
|
-
private generateModuleLevelContextBindings(ir: ComponentIR): string {
|
|
279
|
-
const lines: string[] = []
|
|
280
|
-
for (const c of ir.metadata.localConstants) {
|
|
281
|
-
if (!c.isModule) continue
|
|
282
|
-
if (c.isExported) continue
|
|
283
|
-
if (c.systemConstructKind !== 'createContext') continue
|
|
284
|
-
if (!c.value) continue
|
|
285
|
-
const keyword = c.declarationKind ?? 'const'
|
|
286
|
-
const value = this.jsxConfig.preserveTypes ? (c.typedValue ?? c.value) : c.value
|
|
287
|
-
lines.push(`${keyword} ${c.name} = ${value}`)
|
|
288
|
-
}
|
|
289
|
-
return lines.join('\n')
|
|
290
|
-
}
|
|
291
|
-
|
|
292
255
|
// ===========================================================================
|
|
293
256
|
// Imports Generation
|
|
294
257
|
// ===========================================================================
|
|
@@ -369,73 +332,23 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
369
332
|
// Types Generation
|
|
370
333
|
// ===========================================================================
|
|
371
334
|
|
|
372
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Per-component synthesized types only. The source module's own type
|
|
337
|
+
* declarations are NOT emitted here — `generateModuleScopeDeclarations`
|
|
338
|
+
* carries them once, file-wide, in the module-constants section (#2570).
|
|
339
|
+
* The per-component reachability scan that used to live here (#1453) is
|
|
340
|
+
* gone with them: pruning per component both duplicated shared types
|
|
341
|
+
* across components' sections (TS2300 — the compiler dedups sections by
|
|
342
|
+
* whole string) and dropped exported types no component referenced
|
|
343
|
+
* (TS2305 for any consumer's `import type`).
|
|
344
|
+
*/
|
|
345
|
+
generateTypes(ir: ComponentIR): string | null {
|
|
373
346
|
const lines: string[] = []
|
|
374
347
|
|
|
375
|
-
// Include original type definitions — only those referenced in the component body
|
|
376
|
-
// or transitively referenced by other included type definitions
|
|
377
|
-
if (componentBody && ir.metadata.typeDefinitions.length > 0) {
|
|
378
|
-
const propsTypeName = this.getPropsTypeName(ir)
|
|
379
|
-
// Seed the reachability scan with everything that ends up referencing
|
|
380
|
-
// a type name in the FINAL emitted file, not just the component body.
|
|
381
|
-
//
|
|
382
|
-
// - `propsTypeName` is referenced by the synthesized
|
|
383
|
-
// `${Name}PropsWithHydration = ${propsTypeName} & {...}` alias the
|
|
384
|
-
// destructured-props branch emits below — but that alias is built
|
|
385
|
-
// AFTER this scan, so the body never literally mentions e.g.
|
|
386
|
-
// `ButtonProps`. Without seeding it here the alias references an
|
|
387
|
-
// undeclared name (TS2304) and TS widens `variant`/`size` to `any`
|
|
388
|
-
// at every `Record[variant]` lookup site (TS7053) downstream.
|
|
389
|
-
//
|
|
390
|
-
// - Named re-export blocks (`export type { ButtonVariant, ButtonSize,
|
|
391
|
-
// ButtonProps }`) are emitted by the compiler's `generateModuleExports`
|
|
392
|
-
// AFTER `s.types`. Each re-exported local name needs its declaration
|
|
393
|
-
// carried forward too. Issue #1453 covers the full reproduction.
|
|
394
|
-
const seedText = [
|
|
395
|
-
componentBody,
|
|
396
|
-
propsTypeName && !ir.metadata.propsObjectName ? propsTypeName : '',
|
|
397
|
-
...ir.metadata.namedExports
|
|
398
|
-
.filter((block) => block.source === null)
|
|
399
|
-
.flatMap((block) => block.specifiers.map((s) => s.name)),
|
|
400
|
-
].filter(Boolean).join('\n')
|
|
401
|
-
|
|
402
|
-
const included = new Set<string>()
|
|
403
|
-
// First pass: include types directly referenced in the seed text
|
|
404
|
-
for (const typeDef of ir.metadata.typeDefinitions) {
|
|
405
|
-
if (new RegExp(`\\b${typeDef.name}\\b`).test(seedText)) {
|
|
406
|
-
included.add(typeDef.name)
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
// Transitive pass: include types referenced by already-included types
|
|
410
|
-
let changed = true
|
|
411
|
-
while (changed) {
|
|
412
|
-
changed = false
|
|
413
|
-
for (const typeDef of ir.metadata.typeDefinitions) {
|
|
414
|
-
if (included.has(typeDef.name)) continue
|
|
415
|
-
for (const name of included) {
|
|
416
|
-
const includedDef = ir.metadata.typeDefinitions.find(t => t.name === name)
|
|
417
|
-
if (includedDef && new RegExp(`\\b${typeDef.name}\\b`).test(includedDef.definition)) {
|
|
418
|
-
included.add(typeDef.name)
|
|
419
|
-
changed = true
|
|
420
|
-
break
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
for (const typeDef of ir.metadata.typeDefinitions) {
|
|
426
|
-
if (included.has(typeDef.name)) lines.push(typeDef.definition)
|
|
427
|
-
}
|
|
428
|
-
} else {
|
|
429
|
-
for (const typeDef of ir.metadata.typeDefinitions) {
|
|
430
|
-
lines.push(typeDef.definition)
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
348
|
// Generate hydration props type (only when destructured-props pattern uses it;
|
|
435
349
|
// SolidJS-style props use inline type annotation instead)
|
|
436
350
|
const propsTypeName = this.getPropsTypeName(ir)
|
|
437
351
|
if (propsTypeName && !ir.metadata.propsObjectName) {
|
|
438
|
-
lines.push('')
|
|
439
352
|
lines.push(`type ${this.componentName}PropsWithHydration = ${propsTypeName} & {`)
|
|
440
353
|
lines.push(' __instanceId?: string')
|
|
441
354
|
lines.push(' __bfScope?: string')
|
|
@@ -594,25 +507,11 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
594
507
|
} else {
|
|
595
508
|
const hydrationProps = `__instanceId, ${bfScopeAlias}, ${bfChildAlias}, ${bfParentPropsAlias}, ${bfParentAlias}, ${bfMountAlias}, ${dataKeyAlias}`
|
|
596
509
|
const parts: string[] = []
|
|
510
|
+
// Rename-aware `key: local` bindings (b4f5075), shared with
|
|
511
|
+
// TestAdapter via `propsDestructureBinding` — see its docstring for
|
|
512
|
+
// the `class` → `className` reserved-word rationale.
|
|
597
513
|
const propsParams = ir.metadata.propsParams
|
|
598
|
-
.map((p: ParamInfo) =>
|
|
599
|
-
// The caller-facing key is `sourceName ?? name` (ParamInfo's own
|
|
600
|
-
// rule) — `name` is only ever the LOCAL binding. Emit the plain
|
|
601
|
-
// shorthand when they match (byte-identical to before this was
|
|
602
|
-
// rename-aware); emit a `key: local` rename otherwise. This also
|
|
603
|
-
// covers the `class` → `className` rename correctly: a source
|
|
604
|
-
// prop literally named `class` can only reach `propsParams` via
|
|
605
|
-
// an aliased destructure (`{ class: className }` — `class` is a
|
|
606
|
-
// reserved word, so it can never be an un-aliased binding), which
|
|
607
|
-
// already sets `sourceName: 'class'` and is handled by the rename
|
|
608
|
-
// branch below (`class: className`), not a bare `className`.
|
|
609
|
-
const callerKey = p.sourceName ?? p.name
|
|
610
|
-
const localName = p.name
|
|
611
|
-
const binding = callerKey === localName
|
|
612
|
-
? localName
|
|
613
|
-
: `${isIdentifierName(callerKey) ? callerKey : JSON.stringify(callerKey)}: ${localName}`
|
|
614
|
-
return p.defaultValue ? `${binding} = ${p.defaultValue}` : binding
|
|
615
|
-
})
|
|
514
|
+
.map((p: ParamInfo) => propsDestructureBinding(p))
|
|
616
515
|
.join(', ')
|
|
617
516
|
if (propsParams) {
|
|
618
517
|
parts.push(propsParams)
|
|
@@ -696,7 +595,12 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
696
595
|
// Skip functions and JSX elements (they can't be JSON serialized)
|
|
697
596
|
// Use propsObjectName.propName for SolidJS-style, direct propName for destructured
|
|
698
597
|
const propAccess = propsObjectName ? `${propsObjectName}.${p.name}` : p.name
|
|
699
|
-
|
|
598
|
+
// The `bf-p` blob key is always the caller-facing name (#2524 CSR
|
|
599
|
+
// half) — every non-Hono `_p` producer/consumer keys the same way,
|
|
600
|
+
// so a renaming destructure (`{ n: count }`) must serialize under
|
|
601
|
+
// `n`, not the local binding `count`.
|
|
602
|
+
const callerKey = p.sourceName ?? p.name
|
|
603
|
+
lines.push(` if (typeof ${propAccess} !== 'function' && !(typeof ${propAccess} === 'object' && ${propAccess} !== null && 'isEscaped' in ${propAccess})) __hydrateProps['${callerKey}'] = ${propAccess}`)
|
|
700
604
|
}
|
|
701
605
|
lines.push(` const __bfPropsJson = __bfParentProps || (Object.keys(__hydrateProps).length > 0 ? JSON.stringify(__hydrateProps) : undefined)`)
|
|
702
606
|
} else if (hasClientInteractivity && isRootComponent) {
|
|
@@ -796,7 +700,7 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
796
700
|
const v = node.valueProp.value
|
|
797
701
|
switch (v.kind) {
|
|
798
702
|
case 'literal': return JSON.stringify(v.value)
|
|
799
|
-
case 'expression':
|
|
703
|
+
case 'expression': return this.expressionValueToJs(v)
|
|
800
704
|
case 'spread': return v.expr
|
|
801
705
|
case 'template': return this.renderTemplateLiteralParts(v.parts)
|
|
802
706
|
case 'boolean-attr':
|
|
@@ -908,6 +812,39 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
908
812
|
return `{bfComment("cond-start:${cond.slotId}")}{bfComment("cond-end:${cond.slotId}")}`
|
|
909
813
|
}
|
|
910
814
|
|
|
815
|
+
return `{${this.renderConditionalBody(cond, ctx)}}`
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* The bare ternary text for `cond` — everything `renderConditional` would
|
|
820
|
+
* wrap in `{…}`, minus that wrapping. `renderConditional` is the ONLY
|
|
821
|
+
* place that should add the enclosing braces for a JSX-child position.
|
|
822
|
+
*
|
|
823
|
+
* The two branches below need genuinely different embedding rules, not
|
|
824
|
+
* just a brace/no-brace toggle:
|
|
825
|
+
*
|
|
826
|
+
* - Non-reactive (`cond.slotId` is null): the whole thing collapses to a
|
|
827
|
+
* FLAT `cond ? whenTrue : whenFalse` — one JS expression, wrapped in
|
|
828
|
+
* `{…}` exactly once by whichever caller owns that position (either
|
|
829
|
+
* `renderConditional` for a JSX-child position, or an ENCLOSING
|
|
830
|
+
* ternary's own bare-branch splice when this conditional is itself
|
|
831
|
+
* nested — see `renderBareBranch`). So whenTrue/whenFalse must stay
|
|
832
|
+
* bare all the way down: a nested conditional branch renders through
|
|
833
|
+
* `renderConditionalBody` again (bare), never through the generic
|
|
834
|
+
* `renderNode`/`emitConditional` dispatch, which would re-brace it and
|
|
835
|
+
* break the .tsx parse where only a plain expression is legal (#2470).
|
|
836
|
+
*
|
|
837
|
+
* - Reactive (`cond.slotId` set): each branch is spliced through
|
|
838
|
+
* `wrapWithCondMarker`, which expects a self-contained renderable
|
|
839
|
+
* chunk — an HTML-element string it can tag with `bf-c`, or otherwise
|
|
840
|
+
* text/JSX content it splices directly as JSX CHILDREN inside a
|
|
841
|
+
* `<>…</>` fragment. A nested conditional branch here must keep going
|
|
842
|
+
* through the ordinary `renderNode`/`emitConditional` dispatch (via
|
|
843
|
+
* `renderNodeRawCtx`, unchanged) so it comes back as a complete,
|
|
844
|
+
* already-`{…}`-wrapped JSX expression container — valid as fragment
|
|
845
|
+
* children, which bare ternary text would not be.
|
|
846
|
+
*/
|
|
847
|
+
private renderConditionalBody(cond: IRConditional, ctx?: HonoRenderCtx): string {
|
|
911
848
|
// A conditional that is itself a loop item root (#1665 whole-item
|
|
912
849
|
// conditional: `arr.map(t => cond && <li/>)`) makes its branch element the
|
|
913
850
|
// loop item's root, so the `data-key` that reconciliation/hydration expect
|
|
@@ -915,26 +852,29 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
915
852
|
// the flag through so `renderElement` emits `data-key`, matching the Go /
|
|
916
853
|
// CSR adapters' generic `key`→`data-key` rewrite.
|
|
917
854
|
const branchCtx: HonoRenderCtx | undefined = ctx?.isLoopItemRoot ? { isLoopItemRoot: true } : undefined
|
|
855
|
+
|
|
856
|
+
if (!cond.slotId) {
|
|
857
|
+
const whenTrue = this.renderBareBranch(cond.whenTrue, branchCtx)
|
|
858
|
+
let whenFalse = this.renderBareBranch(cond.whenFalse, branchCtx)
|
|
859
|
+
if (!whenFalse || whenFalse === '' || whenFalse === 'null') {
|
|
860
|
+
whenFalse = 'null'
|
|
861
|
+
}
|
|
862
|
+
return `${cond.condition} ? ${whenTrue} : ${whenFalse}`
|
|
863
|
+
}
|
|
864
|
+
|
|
918
865
|
const whenTrue = this.renderNodeRawCtx(cond.whenTrue, branchCtx)
|
|
919
866
|
let whenFalse = this.renderNodeRawCtx(cond.whenFalse, branchCtx)
|
|
920
|
-
|
|
921
|
-
// Handle empty/null whenFalse
|
|
922
867
|
if (!whenFalse || whenFalse === '' || whenFalse === 'null') {
|
|
923
868
|
whenFalse = 'null'
|
|
924
869
|
}
|
|
925
870
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
? `<>{bfComment("cond-start:${cond.slotId}")}{bfComment("cond-end:${cond.slotId}")}</>`
|
|
932
|
-
: this.wrapWithCondMarker(cond.whenFalse, whenFalse, cond.slotId)
|
|
933
|
-
|
|
934
|
-
return `{${cond.condition} ? ${trueWithMarker} : ${falseWithMarker}}`
|
|
935
|
-
}
|
|
871
|
+
const trueWithMarker = this.wrapWithCondMarker(cond.whenTrue, whenTrue, cond.slotId)
|
|
872
|
+
// For null false branch, render comment markers so client can insert content later
|
|
873
|
+
const falseWithMarker = cond.whenFalse.type === 'expression' && cond.whenFalse.expr === 'null'
|
|
874
|
+
? `<>{bfComment("cond-start:${cond.slotId}")}{bfComment("cond-end:${cond.slotId}")}</>`
|
|
875
|
+
: this.wrapWithCondMarker(cond.whenFalse, whenFalse, cond.slotId)
|
|
936
876
|
|
|
937
|
-
return
|
|
877
|
+
return `${cond.condition} ? ${trueWithMarker} : ${falseWithMarker}`
|
|
938
878
|
}
|
|
939
879
|
|
|
940
880
|
/**
|
|
@@ -951,6 +891,30 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
951
891
|
return this.renderNode(node, ctx)
|
|
952
892
|
}
|
|
953
893
|
|
|
894
|
+
/**
|
|
895
|
+
* A branch of a NON-reactive conditional's flat ternary (#2470) — the
|
|
896
|
+
* value must be a bare JS expression, since it's spliced directly into
|
|
897
|
+
* `cond ? whenTrue : whenFalse`. Mirrors `renderNodeRawCtx`'s existing
|
|
898
|
+
* `null`/`undefined` special case, extended to a nested conditional: that
|
|
899
|
+
* branch renders through `renderConditionalBody` itself (bare), instead
|
|
900
|
+
* of falling through to `renderNode`/`emitConditional`, which would wrap
|
|
901
|
+
* it in its own `{…}` and break the .tsx parse in this brace-free
|
|
902
|
+
* position. The `@client`-directive combination is excluded — its
|
|
903
|
+
* rendering is a pair of independent marker expressions, not a single JS
|
|
904
|
+
* expression, so it can't be spliced bare into a ternary branch either
|
|
905
|
+
* way; that shape falls through to the pre-existing (unrelated) behavior.
|
|
906
|
+
*/
|
|
907
|
+
private renderBareBranch(node: IRNode, ctx?: HonoRenderCtx): string {
|
|
908
|
+
if (node.type === 'expression') {
|
|
909
|
+
if (node.expr === 'null' || node.expr === 'undefined') return 'null'
|
|
910
|
+
return node.expr
|
|
911
|
+
}
|
|
912
|
+
if (node.type === 'conditional' && !(node.clientOnly && node.slotId)) {
|
|
913
|
+
return this.renderConditionalBody(node, ctx)
|
|
914
|
+
}
|
|
915
|
+
return this.renderNode(node, ctx)
|
|
916
|
+
}
|
|
917
|
+
|
|
954
918
|
private wrapWithCondMarker(node: IRNode, content: string, condId: string): string {
|
|
955
919
|
// Components don't reliably forward bf-c to their root element.
|
|
956
920
|
// Use comment markers so insert() can find them via TreeWalker.
|
|
@@ -1292,13 +1256,14 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
1292
1256
|
// runtime sees the decoded string and escapes it on render).
|
|
1293
1257
|
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
1294
1258
|
emitExpression: (value, name) => {
|
|
1259
|
+
const expr = this.expressionValueToJs(value)
|
|
1295
1260
|
// Boolean attrs / presence-folded expressions: pass `undefined` when
|
|
1296
1261
|
// falsy so Hono omits the attribute. Wrap in parens to keep `??`
|
|
1297
1262
|
// operators inside `expr` from breaking the surrounding `|| undefined`.
|
|
1298
1263
|
if (isBooleanAttr(name) || value.presenceOrUndefined) {
|
|
1299
|
-
return `${name}={(${
|
|
1264
|
+
return `${name}={(${expr}) || undefined}`
|
|
1300
1265
|
}
|
|
1301
|
-
return `${name}={${
|
|
1266
|
+
return `${name}={${expr}}`
|
|
1302
1267
|
},
|
|
1303
1268
|
emitBooleanAttr: (_value, name) => name,
|
|
1304
1269
|
emitBooleanShorthand: () => '',
|
|
@@ -1325,7 +1290,7 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
1325
1290
|
// `calc(...)`) from a JS expression. The decoded value re-encodes
|
|
1326
1291
|
// so the JSX parser hands the component the same decoded string.
|
|
1327
1292
|
`${name}="${escapeHtml(value.value)}"`,
|
|
1328
|
-
emitExpression: (value, name) => `${name}={${value
|
|
1293
|
+
emitExpression: (value, name) => `${name}={${this.expressionValueToJs(value)}}`,
|
|
1329
1294
|
emitBooleanAttr: (_value, name) => name,
|
|
1330
1295
|
emitBooleanShorthand: (_value, name) => name,
|
|
1331
1296
|
emitTemplate: (value, name) => `${name}={${this.renderTemplateLiteralParts(value.parts)}}`,
|
|
@@ -1397,7 +1362,7 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
1397
1362
|
private attrValueToJsExpr(value: AttrValue): string {
|
|
1398
1363
|
switch (value.kind) {
|
|
1399
1364
|
case 'literal': return JSON.stringify(value.value)
|
|
1400
|
-
case 'expression':
|
|
1365
|
+
case 'expression': return this.expressionValueToJs(value)
|
|
1401
1366
|
case 'spread': return value.expr
|
|
1402
1367
|
case 'template': return this.renderTemplateLiteralParts(value.parts)
|
|
1403
1368
|
case 'boolean-shorthand':
|
|
@@ -1406,28 +1371,20 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
1406
1371
|
}
|
|
1407
1372
|
}
|
|
1408
1373
|
|
|
1374
|
+
/**
|
|
1375
|
+
* Hono runs JS at SSR time, so a structured template can be
|
|
1376
|
+
* re-materialised as the equivalent runtime JS — a `${MAP[KEY]}` lookup
|
|
1377
|
+
* becomes an indexed access against the resolved cases, runtime-identical
|
|
1378
|
+
* to the client emit path. Delegates to the single renderer shared with
|
|
1379
|
+
* that path and with the IR-time component-prop collapse, which also adds
|
|
1380
|
+
* the `preserveTypes` index annotation for this .tsx output (#2565).
|
|
1381
|
+
*
|
|
1382
|
+
* The parts' RAW keys/conditions are used (not their `templateX`
|
|
1383
|
+
* projections) because this output runs inside the destructured-prop
|
|
1384
|
+
* scope of the emitted component.
|
|
1385
|
+
*/
|
|
1409
1386
|
private renderTemplateLiteralParts(parts: IRTemplatePart[]): string {
|
|
1410
|
-
|
|
1411
|
-
for (const part of parts) {
|
|
1412
|
-
if (part.type === 'string') {
|
|
1413
|
-
output += part.value
|
|
1414
|
-
} else if (part.type === 'ternary') {
|
|
1415
|
-
output += `\${${part.condition} ? '${part.whenTrue}' : '${part.whenFalse}'}`
|
|
1416
|
-
} else if (part.type === 'lookup') {
|
|
1417
|
-
// Hono runs JS at SSR time, so a `${MAP[KEY]}` lookup can be
|
|
1418
|
-
// re-materialised as a runtime indexed access against the
|
|
1419
|
-
// resolved cases — byte-identical to the client emit path in
|
|
1420
|
-
// `ir-to-client-js/utils.ts`. Use `part.key` (raw JS source)
|
|
1421
|
-
// because this output runs inside the destructured-prop scope
|
|
1422
|
-
// of the component, mirroring the `'ternary'` branch above.
|
|
1423
|
-
const obj = '{' + Object.entries(part.cases).map(
|
|
1424
|
-
([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
|
|
1425
|
-
).join(', ') + '}'
|
|
1426
|
-
output += `\${(${obj})[${part.key}]}`
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
output += '`'
|
|
1430
|
-
return output
|
|
1387
|
+
return this.renderTemplatePartsAsJs(parts)
|
|
1431
1388
|
}
|
|
1432
1389
|
|
|
1433
1390
|
}
|
|
@@ -5,5 +5,17 @@
|
|
|
5
5
|
* JSX namespace as the production runtime so dev builds see identical types.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import { jsxDEV as honoJsxDEV, Fragment } from 'hono/jsx/jsx-dev-runtime'
|
|
9
|
+
import { resolveDangerouslySetInnerHTML } from '../resolve-dangerously-set-inner-html.ts'
|
|
10
|
+
|
|
11
|
+
export { Fragment }
|
|
9
12
|
export type { JSX } from '../jsx-runtime/index.ts'
|
|
13
|
+
|
|
14
|
+
// See `../resolve-dangerously-set-inner-html.ts` for why this is needed —
|
|
15
|
+
// hono's own `jsxFn` throws for a childless `<svg>`/`<head>` element using
|
|
16
|
+
// `dangerouslySetInnerHTML` (https://github.com/piconic-ai/barefootjs/issues/2557).
|
|
17
|
+
// Intrinsic string tags only: a function component must receive the caller's
|
|
18
|
+
// props untouched (it may forward `dangerouslySetInnerHTML` itself).
|
|
19
|
+
export function jsxDEV(tag: string | Function, props: Record<string, unknown>, key?: string) {
|
|
20
|
+
return honoJsxDEV(tag, typeof tag === 'string' ? resolveDangerouslySetInnerHTML(props) : props, key)
|
|
21
|
+
}
|
|
@@ -11,7 +11,30 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
// Runtime functions from hono/jsx.
|
|
14
|
-
|
|
14
|
+
import {
|
|
15
|
+
jsx as honoJsx,
|
|
16
|
+
jsxs as honoJsxs,
|
|
17
|
+
Fragment,
|
|
18
|
+
jsxAttr,
|
|
19
|
+
jsxEscape,
|
|
20
|
+
jsxTemplate,
|
|
21
|
+
} from 'hono/jsx/jsx-runtime'
|
|
22
|
+
import { resolveDangerouslySetInnerHTML } from '../resolve-dangerously-set-inner-html.ts'
|
|
23
|
+
|
|
24
|
+
export { Fragment, jsxAttr, jsxEscape, jsxTemplate }
|
|
25
|
+
|
|
26
|
+
// See `../resolve-dangerously-set-inner-html.ts` for why this is needed —
|
|
27
|
+
// hono's own `jsxFn` throws for a childless `<svg>`/`<head>` element using
|
|
28
|
+
// `dangerouslySetInnerHTML` (https://github.com/piconic-ai/barefootjs/issues/2557).
|
|
29
|
+
// Intrinsic string tags only: a function component must receive the caller's
|
|
30
|
+
// props untouched (it may forward `dangerouslySetInnerHTML` itself).
|
|
31
|
+
export function jsx(tag: string | Function, props: Record<string, unknown>, key?: string) {
|
|
32
|
+
return honoJsx(tag, typeof tag === 'string' ? resolveDangerouslySetInnerHTML(props) : props, key)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function jsxs(tag: string | Function, props: Record<string, unknown>, key?: string) {
|
|
36
|
+
return honoJsxs(tag, typeof tag === 'string' ? resolveDangerouslySetInnerHTML(props) : props, key)
|
|
37
|
+
}
|
|
15
38
|
|
|
16
39
|
// Re-export JSX namespace from @barefootjs/jsx, but override Element type for Hono.
|
|
17
40
|
import type { JSX as BaseJSX } from '@barefootjs/jsx/jsx-runtime'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared by `./jsx-runtime/index.ts` and `./jsx-dev-runtime/index.ts`.
|
|
3
|
+
*
|
|
4
|
+
* hono's own `jsxFn` (`hono/dist/jsx/base.js`) always wraps `<svg>` /
|
|
5
|
+
* `<head>` children in an internal namespace-context node — even when the
|
|
6
|
+
* caller passed no children at all. That phantom wrapper makes the outer
|
|
7
|
+
* `JSXNode`'s `children.length > 0` true, which trips hono's own
|
|
8
|
+
* "Can only set one of `children` or `props.dangerouslySetInnerHTML`" guard
|
|
9
|
+
* for any childless `<svg>`/`<head>` element using `dangerouslySetInnerHTML`
|
|
10
|
+
* — see https://github.com/piconic-ai/barefootjs/issues/2557.
|
|
11
|
+
*
|
|
12
|
+
* Work around it at the one place BarefootJS's compiled output calls into
|
|
13
|
+
* hono's JSX runtime: when `dangerouslySetInnerHTML` is present and no real
|
|
14
|
+
* `children` prop was given, resolve it into `children` ourselves (mirroring
|
|
15
|
+
* what hono would do internally) before delegating to hono. If the caller
|
|
16
|
+
* supplied *both* real children and `dangerouslySetInnerHTML`, that's a
|
|
17
|
+
* genuine conflict — leave `props` untouched so hono's own guard still
|
|
18
|
+
* rejects it.
|
|
19
|
+
*/
|
|
20
|
+
import { raw } from 'hono/html'
|
|
21
|
+
|
|
22
|
+
export function resolveDangerouslySetInnerHTML(props: Record<string, unknown>): Record<string, unknown> {
|
|
23
|
+
if (
|
|
24
|
+
props &&
|
|
25
|
+
'dangerouslySetInnerHTML' in props &&
|
|
26
|
+
props.dangerouslySetInnerHTML != null &&
|
|
27
|
+
!('children' in props)
|
|
28
|
+
) {
|
|
29
|
+
const { dangerouslySetInnerHTML, ...rest } = props
|
|
30
|
+
const html = (dangerouslySetInnerHTML as { __html: string }).__html
|
|
31
|
+
return { ...rest, children: raw(html) }
|
|
32
|
+
}
|
|
33
|
+
return props
|
|
34
|
+
}
|
package/src/scripts.tsx
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
import { useRequestContext } from 'hono/jsx-renderer'
|
|
42
42
|
import { Fragment } from 'hono/jsx'
|
|
43
|
+
import type { JSX } from 'hono/jsx/jsx-runtime'
|
|
43
44
|
import { relPathFromComponentsBase, type BarefootBuildManifest } from './app.ts'
|
|
44
45
|
|
|
45
46
|
export type CollectedScript = {
|
|
@@ -284,8 +285,8 @@ export function registerComponentPreloads(urls: string[]): string[] {
|
|
|
284
285
|
* only ever pass `inlineScripts` (a component with `scriptAssets` but no
|
|
285
286
|
* `preloadAssets`) keep compiling and behaving exactly as before.
|
|
286
287
|
*/
|
|
287
|
-
export function wrapWithInlineScripts(jsx: unknown, inlineScripts: string[], inlinePreloads: string[] = []) {
|
|
288
|
-
if (inlineScripts.length === 0 && inlinePreloads.length === 0) return jsx
|
|
288
|
+
export function wrapWithInlineScripts(jsx: unknown, inlineScripts: string[], inlinePreloads: string[] = []): JSX.Element {
|
|
289
|
+
if (inlineScripts.length === 0 && inlinePreloads.length === 0) return jsx as JSX.Element
|
|
289
290
|
return (
|
|
290
291
|
<Fragment>
|
|
291
292
|
{inlinePreloads.map(href => <link rel="modulepreload" crossorigin="" href={href} />)}
|