@barefootjs/hono 0.29.0 → 0.30.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 +0 -2
- package/dist/adapter/hono-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +187313 -9
- package/dist/app.js +71 -0
- package/dist/async.js +71 -0
- package/dist/build.js +187339 -35
- package/dist/client-shim.js +71 -0
- package/dist/dev-worker.js +71 -0
- package/dist/dev.js +95 -5
- package/dist/dialog-context.js +71 -0
- package/dist/index.js +187313 -9
- package/dist/jsx/jsx-dev-runtime/index.js +71 -0
- package/dist/jsx/jsx-runtime/index.js +71 -0
- package/dist/portal-ssr.js +71 -0
- package/dist/portals.js +71 -0
- package/dist/preload.js +71 -0
- package/dist/render.js +71 -0
- package/dist/request-env.js +71 -0
- package/dist/scripts.js +71 -0
- package/dist/utils.js +71 -0
- package/package.json +1 -1
- package/src/__tests__/aliased-destructured-prop.test.ts +128 -0
- package/src/adapter/hono-adapter.ts +50 -10
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aliased (renaming) destructured props (#2460).
|
|
3
|
+
*
|
|
4
|
+
* The Hono adapter used to build its SSR props destructure keyed by
|
|
5
|
+
* `ParamInfo.name` (the LOCAL binding) instead of `sourceName ?? name`
|
|
6
|
+
* (the CALLER-facing key — see `ParamInfo.sourceName`'s docstring in
|
|
7
|
+
* `packages/jsx/src/types.ts`). For a renaming destructure
|
|
8
|
+
* (`{ n: count }`) the emitted SSR function read a `count` property the
|
|
9
|
+
* caller never passed (the caller passes `n`), so the local binding was
|
|
10
|
+
* always `undefined`.
|
|
11
|
+
*
|
|
12
|
+
* These tests render through Hono end-to-end (`renderHonoComponent`) and
|
|
13
|
+
* assert the actual rendered VALUE, not just the emitted destructure
|
|
14
|
+
* text — a byte-identical destructure with the wrong runtime binding
|
|
15
|
+
* would still pass a text-only assertion.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, test, expect } from 'bun:test'
|
|
19
|
+
import { compileJSX } from '@barefootjs/jsx'
|
|
20
|
+
import { renderHonoComponent } from '../test-render'
|
|
21
|
+
import { HonoAdapter } from '../adapter/hono-adapter'
|
|
22
|
+
|
|
23
|
+
describe('aliased destructured props (#2460)', () => {
|
|
24
|
+
test('{ text, n: count } — aliased, no default: the caller-supplied `n` reaches the `count` binding', async () => {
|
|
25
|
+
const html = await renderHonoComponent({
|
|
26
|
+
adapter: new HonoAdapter(),
|
|
27
|
+
source: `
|
|
28
|
+
export function Badge({ text, n: count }: { text: string; n: number }) {
|
|
29
|
+
return <span>{text}:{count}</span>
|
|
30
|
+
}
|
|
31
|
+
`,
|
|
32
|
+
props: { text: 'hello', n: 7 },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
expect(html).toContain('hello')
|
|
36
|
+
expect(html).toContain(':<!--bf:s1-->7<!--/-->')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('{ text, n: count = 7 } — aliased with a destructuring default, caller omits `n`', async () => {
|
|
40
|
+
const html = await renderHonoComponent({
|
|
41
|
+
adapter: new HonoAdapter(),
|
|
42
|
+
source: `
|
|
43
|
+
export function Badge({ text, n: count = 7 }: { text: string; n?: number }) {
|
|
44
|
+
return <span>{text}:{count}</span>
|
|
45
|
+
}
|
|
46
|
+
`,
|
|
47
|
+
props: { text: 'hello' },
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
expect(html).toContain(':<!--bf:s1-->7<!--/-->')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('{ text, n: count = 7 } — aliased with a default, caller-supplied `n` overrides it', async () => {
|
|
54
|
+
const html = await renderHonoComponent({
|
|
55
|
+
adapter: new HonoAdapter(),
|
|
56
|
+
source: `
|
|
57
|
+
export function Badge({ text, n: count = 7 }: { text: string; n?: number }) {
|
|
58
|
+
return <span>{text}:{count}</span>
|
|
59
|
+
}
|
|
60
|
+
`,
|
|
61
|
+
props: { text: 'hello', n: 42 },
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(html).toContain(':<!--bf:s1-->42<!--/-->')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('{ text, n } — un-aliased: emitted destructure text is byte-identical to the pre-fix shorthand', () => {
|
|
68
|
+
const source = `
|
|
69
|
+
export function Badge({ text, n }: { text: string; n: number }) {
|
|
70
|
+
return <span>{text}:{n}</span>
|
|
71
|
+
}
|
|
72
|
+
`
|
|
73
|
+
const result = compileJSX(source, 'Badge.tsx', { adapter: new HonoAdapter() })
|
|
74
|
+
const errors = result.errors.filter((e) => e.severity === 'error')
|
|
75
|
+
expect(errors).toEqual([])
|
|
76
|
+
const tmpl = result.files.find((f) => f.type === 'markedTemplate')
|
|
77
|
+
expect(tmpl).toBeDefined()
|
|
78
|
+
// Plain shorthand destructure — no `sourceKey: localName` rename text
|
|
79
|
+
// for an un-aliased prop.
|
|
80
|
+
expect(tmpl!.content).toContain('export function Badge({ text, n, __instanceId,')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('a renamed `class` prop emits the rename `class: className`, not a bare `className`', () => {
|
|
84
|
+
// `class` is a reserved word and can never be an un-aliased binding
|
|
85
|
+
// identifier (`{ class }` is a syntax error), so the only way a
|
|
86
|
+
// `class`-named caller prop reaches `propsParams` through a
|
|
87
|
+
// destructured component is via an explicit alias.
|
|
88
|
+
const source = `
|
|
89
|
+
export function Chip({ class: className }: { class: string }) {
|
|
90
|
+
return <span class={className}>x</span>
|
|
91
|
+
}
|
|
92
|
+
`
|
|
93
|
+
const result = compileJSX(source, 'Chip.tsx', { adapter: new HonoAdapter() })
|
|
94
|
+
const errors = result.errors.filter((e) => e.severity === 'error')
|
|
95
|
+
expect(errors).toEqual([])
|
|
96
|
+
const tmpl = result.files.find((f) => f.type === 'markedTemplate')
|
|
97
|
+
expect(tmpl).toBeDefined()
|
|
98
|
+
expect(tmpl!.content).toContain('class: className')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('aliased prop reaches client-side hydration serialization with the correct value', async () => {
|
|
102
|
+
// The client init function reads `_p.<localName>` (props-extraction
|
|
103
|
+
// phase), so the SSR-serialized `bf-p` blob must carry the correct
|
|
104
|
+
// value under the LOCAL key (`count`) — the rename only affects the
|
|
105
|
+
// caller-facing key, not the local binding the hydration bridge uses.
|
|
106
|
+
const html = await renderHonoComponent({
|
|
107
|
+
adapter: new HonoAdapter(),
|
|
108
|
+
source: `
|
|
109
|
+
'use client'
|
|
110
|
+
import { createEffect } from '@barefootjs/client'
|
|
111
|
+
export function Badge({ text, n: count }: { text: string; n: number }) {
|
|
112
|
+
createEffect(() => {
|
|
113
|
+
console.log(count)
|
|
114
|
+
})
|
|
115
|
+
return <span>{text}:{count}</span>
|
|
116
|
+
}
|
|
117
|
+
`,
|
|
118
|
+
props: { text: 'hello', n: 7 },
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// The rendered value is correct...
|
|
122
|
+
expect(html).toContain(':<!--bf:s1-->7<!--/-->')
|
|
123
|
+
// ...and the serialized hydration payload carries it under the LOCAL
|
|
124
|
+
// binding name, which is what the generated client JS's
|
|
125
|
+
// `const count = _p.count` extraction reads.
|
|
126
|
+
expect(html).toMatch(/bf-p="[^"]*count[^"]*7/)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
emitAttrValue,
|
|
36
36
|
buildLoopChainExpr,
|
|
37
37
|
} from '@barefootjs/jsx'
|
|
38
|
+
import ts from 'typescript'
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Hono adapter's IRNode render context: which surrounding render
|
|
@@ -44,7 +45,6 @@ import {
|
|
|
44
45
|
*/
|
|
45
46
|
type HonoRenderCtx = {
|
|
46
47
|
isRootOfClientComponent?: boolean
|
|
47
|
-
isInsideLoop?: boolean
|
|
48
48
|
isLoopItemRoot?: boolean
|
|
49
49
|
}
|
|
50
50
|
import { BF_SCOPE, BF_HOST, BF_AT, BF_ROOT, BF_PROPS, BF_REGION, escapeHtml } from '@barefootjs/shared'
|
|
@@ -99,6 +99,28 @@ function applyHonoLoopChain(loop: IRLoop): string {
|
|
|
99
99
|
})
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Authoritative IdentifierName classification for a destructure-pattern
|
|
104
|
+
* property key, built on TS's own `isIdentifierStart` / `isIdentifierPart`
|
|
105
|
+
* primitives (Unicode-aware, stays aligned with what TS itself accepts as
|
|
106
|
+
* a bare property key). Mirrors the `isIdent` precedent in
|
|
107
|
+
* `jsx-to-ir.ts` (#1244) — a source key like `data-key` or `aria-label`
|
|
108
|
+
* can't be emitted as a bare `key: local` destructure and must be quoted
|
|
109
|
+
* (`"data-key": local`).
|
|
110
|
+
*/
|
|
111
|
+
function isIdentifierName(key: string): boolean {
|
|
112
|
+
if (key.length === 0) return false
|
|
113
|
+
for (let i = 0; i < key.length; ) {
|
|
114
|
+
const cp = key.codePointAt(i)!
|
|
115
|
+
const ok = i === 0
|
|
116
|
+
? ts.isIdentifierStart(cp, ts.ScriptTarget.Latest)
|
|
117
|
+
: ts.isIdentifierPart(cp, ts.ScriptTarget.Latest)
|
|
118
|
+
if (!ok) return false
|
|
119
|
+
i += cp > 0xFFFF ? 2 : 1
|
|
120
|
+
}
|
|
121
|
+
return true
|
|
122
|
+
}
|
|
123
|
+
|
|
102
124
|
export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderCtx> {
|
|
103
125
|
name = 'hono'
|
|
104
126
|
extension = '.tsx'
|
|
@@ -506,8 +528,22 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
506
528
|
const parts: string[] = []
|
|
507
529
|
const propsParams = ir.metadata.propsParams
|
|
508
530
|
.map((p: ParamInfo) => {
|
|
509
|
-
|
|
510
|
-
|
|
531
|
+
// The caller-facing key is `sourceName ?? name` (ParamInfo's own
|
|
532
|
+
// rule) — `name` is only ever the LOCAL binding. Emit the plain
|
|
533
|
+
// shorthand when they match (byte-identical to before this was
|
|
534
|
+
// rename-aware); emit a `key: local` rename otherwise. This also
|
|
535
|
+
// covers the `class` → `className` rename correctly: a source
|
|
536
|
+
// prop literally named `class` can only reach `propsParams` via
|
|
537
|
+
// an aliased destructure (`{ class: className }` — `class` is a
|
|
538
|
+
// reserved word, so it can never be an un-aliased binding), which
|
|
539
|
+
// already sets `sourceName: 'class'` and is handled by the rename
|
|
540
|
+
// branch below (`class: className`), not a bare `className`.
|
|
541
|
+
const callerKey = p.sourceName ?? p.name
|
|
542
|
+
const localName = p.name
|
|
543
|
+
const binding = callerKey === localName
|
|
544
|
+
? localName
|
|
545
|
+
: `${isIdentifierName(callerKey) ? callerKey : JSON.stringify(callerKey)}: ${localName}`
|
|
546
|
+
return p.defaultValue ? `${binding} = ${p.defaultValue}` : binding
|
|
511
547
|
})
|
|
512
548
|
.join(', ')
|
|
513
549
|
if (propsParams) {
|
|
@@ -874,7 +910,10 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
874
910
|
const indexParam = loop.index ? `, ${loop.index}${indexAnnotation}` : ''
|
|
875
911
|
// Push loop key info for data-key attribute generation on loop items
|
|
876
912
|
this.loopKeyStack.push({ key: loop.key, param: loop.param })
|
|
877
|
-
// Render children with
|
|
913
|
+
// Render children with isLoopItemRoot so a DIRECT component member gets
|
|
914
|
+
// its own randomized scope id (matching `IRComponent.loopItemRoot`,
|
|
915
|
+
// #2444) rather than deriving from parent scope + slot like an
|
|
916
|
+
// ordinarily-slotted child.
|
|
878
917
|
const children = this.renderChildrenInLoop(loop.children)
|
|
879
918
|
this.loopKeyStack.pop()
|
|
880
919
|
|
|
@@ -969,7 +1008,7 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
969
1008
|
}
|
|
970
1009
|
|
|
971
1010
|
private renderChildrenInLoop(children: IRNode[]): string {
|
|
972
|
-
return children.map((child) => this.renderNode(child, {
|
|
1011
|
+
return children.map((child) => this.renderNode(child, { isLoopItemRoot: true })).join('')
|
|
973
1012
|
}
|
|
974
1013
|
|
|
975
1014
|
/**
|
|
@@ -1040,7 +1079,7 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
1040
1079
|
)
|
|
1041
1080
|
}
|
|
1042
1081
|
|
|
1043
|
-
renderComponent(comp: IRComponent, ctx?: { isRootOfClientComponent?: boolean;
|
|
1082
|
+
renderComponent(comp: IRComponent, ctx?: { isRootOfClientComponent?: boolean; isLoopItemRoot?: boolean }): string {
|
|
1044
1083
|
const props = this.renderComponentProps(comp)
|
|
1045
1084
|
const children = this.renderChildren(comp.children)
|
|
1046
1085
|
|
|
@@ -1066,10 +1105,11 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
|
|
|
1066
1105
|
// Also pass bf-s for asChild/Slot patterns where the component
|
|
1067
1106
|
// forwards props to a DOM element via {...props}.
|
|
1068
1107
|
scopeAttr += ` ${BF_SCOPE}={__scopeId}`
|
|
1069
|
-
} else if (
|
|
1070
|
-
//
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1108
|
+
} else if (comp.loopItemRoot) {
|
|
1109
|
+
// A component that is the DIRECT root of a loop row owns its own
|
|
1110
|
+
// per-row identity — generate a unique scope id rather than deriving
|
|
1111
|
+
// from parent scope + slot (#2444). Pass __bfScope so it uses it as
|
|
1112
|
+
// fallback but still generates a unique id per iteration.
|
|
1073
1113
|
if (comp.slotId) {
|
|
1074
1114
|
scopeAttr = ` __bfScope={\`\${__scopeId}_${comp.slotId}\`}${bfChildAttr}${bfMountAttr}`
|
|
1075
1115
|
} else {
|