@barefootjs/jsx 0.31.1 → 0.31.3
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/adapters/interface.d.ts +11 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts +92 -1
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/css-layer-prefixer.d.ts +16 -0
- package/dist/css-layer-prefixer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/html-types.d.ts +19 -0
- package/dist/html-types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1531 -1155
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-event-handlers.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-extraction.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +6 -4
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-runtime/index.d.ts +2 -8
- package/dist/jsx-runtime/index.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/module-exports.d.ts +9 -1
- package/dist/module-exports.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +8 -3
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +40 -0
- package/dist/props-binding.d.ts.map +1 -0
- package/dist/relocate.d.ts +9 -0
- package/dist/relocate.d.ts.map +1 -1
- package/dist/scope/binding-scope.d.ts +179 -0
- package/dist/scope/binding-scope.d.ts.map +1 -0
- package/dist/ssr-defaults.d.ts +43 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/template-parts.d.ts +53 -0
- package/dist/template-parts.d.ts.map +1 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/adapter-output.test.ts +8 -4
- package/src/__tests__/aliased-destructured-prop-csr.test.ts +112 -0
- package/src/__tests__/binding-scope-preamble-shadowing.test.ts +115 -0
- package/src/__tests__/binding-scope-ratchet.test.ts +194 -0
- package/src/__tests__/binding-scope.test.ts +200 -0
- package/src/__tests__/css-layer-prefixer.test.ts +72 -0
- package/src/__tests__/form-control-value-ssr.test.ts +48 -3
- package/src/__tests__/let-type-annotation.test.ts +208 -0
- package/src/__tests__/memo-deps-comments.test.ts +99 -0
- package/src/__tests__/multi-return-sibling-diagnostic.test.ts +241 -0
- package/src/__tests__/ssr-defaults.test.ts +124 -1
- package/src/__tests__/staged-ir/08-relocate-unit.test.ts +1 -0
- package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +1 -0
- package/src/adapters/interface.ts +11 -0
- package/src/adapters/jsx-adapter.ts +295 -5
- package/src/adapters/test-adapter.ts +13 -10
- package/src/analyzer.ts +70 -11
- package/src/compiler.ts +119 -18
- package/src/css-layer-prefixer.ts +80 -24
- package/src/errors.ts +18 -0
- package/src/html-types.ts +24 -0
- package/src/index.ts +11 -1
- package/src/ir-to-client-js/collect-elements.ts +4 -1
- package/src/ir-to-client-js/emit-reactive.ts +4 -2
- package/src/ir-to-client-js/phases/props-event-handlers.ts +4 -3
- package/src/ir-to-client-js/phases/props-extraction.ts +7 -4
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +6 -3
- package/src/ir-to-client-js/utils.ts +5 -24
- package/src/jsx-runtime/index.ts +2 -7
- package/src/jsx-to-ir.ts +245 -107
- package/src/module-exports.ts +11 -2
- package/src/prop-rewrite.ts +26 -6
- package/src/props-binding.ts +70 -0
- package/src/relocate.ts +19 -2
- package/src/scope/binding-scope.ts +238 -0
- package/src/ssr-defaults.ts +70 -0
- package/src/template-parts.ts +81 -0
- package/src/types.ts +18 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for #2556: a `'use client'` file where a component
|
|
3
|
+
* references a same-file sibling whose body is a multi-return JSX dispatch
|
|
4
|
+
* (`switch` / `if`-`else` chain) previously compiled clean with ZERO
|
|
5
|
+
* diagnostics, but the sibling produced no template — so the emitted
|
|
6
|
+
* `renderChild`/`initChild`/`createComponent` call referenced a component
|
|
7
|
+
* name with nothing registered under it, throwing
|
|
8
|
+
* `ReferenceError: <Name> is not defined` at SSR/hydrate time.
|
|
9
|
+
*
|
|
10
|
+
* Root cause: `listComponentFunctions`'s #932 "preserve verbatim helper"
|
|
11
|
+
* bypass is gated on `!hasUseClient` — in a `'use client'` file, a
|
|
12
|
+
* multi-return sibling like `NavIcon` below IS added to `componentNames`
|
|
13
|
+
* and asked to compile as a standalone component. But `visitComponentBody`
|
|
14
|
+
* only folds `if`/`else`-chain multi-return bodies into `conditionalReturns`
|
|
15
|
+
* (#1401); a top-level `switch` statement is preserved as a verbatim init
|
|
16
|
+
* statement instead, so `ctx.jsxReturn` stays null and
|
|
17
|
+
* `compileMultipleComponents`'s Pass-1 loop silently `continue`s past it —
|
|
18
|
+
* no entry, no template, no error. Meanwhile the referencing sibling's IR
|
|
19
|
+
* still holds a `component` reference to the dropped name.
|
|
20
|
+
*
|
|
21
|
+
* Fix: BF048 detects this structurally — after Pass 1, any name in
|
|
22
|
+
* `componentNames` that did not make it into `entries` is cross-referenced
|
|
23
|
+
* against every compiled sibling's IR component-reference graph (the same
|
|
24
|
+
* walk `@bf-child` import markers use, `collectComponentNamesFromIR`). A
|
|
25
|
+
* hit fails the compile instead of shipping a silent `ReferenceError`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { describe, test, expect } from 'bun:test'
|
|
29
|
+
import { compileJSX } from '../compiler'
|
|
30
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
31
|
+
|
|
32
|
+
const adapter = new TestAdapter()
|
|
33
|
+
|
|
34
|
+
describe('Sibling multi-return JSX dispatch produces no template in a client file (#2556)', () => {
|
|
35
|
+
// Higher timeout: `.map()` in the source trips `needsTypeBasedDetection`,
|
|
36
|
+
// and building the one-time `ts.Program` for BF023/BF024's nullable-key
|
|
37
|
+
// check is slow the first time the TS API is touched in a fresh process
|
|
38
|
+
// (e.g. this file run in isolation) — well past bun's 5s default.
|
|
39
|
+
test('BF048: .map() loop child referencing a switch-dispatch sibling fails the compile', () => {
|
|
40
|
+
const source = `
|
|
41
|
+
"use client"
|
|
42
|
+
import { createSignal } from '@barefootjs/client'
|
|
43
|
+
function NavIcon({ name }: { name: string }) {
|
|
44
|
+
switch (name) {
|
|
45
|
+
case 'home': return <svg><path d="M1"/></svg>
|
|
46
|
+
case 'bell': return <svg><path d="M2"/></svg>
|
|
47
|
+
default: return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function Shell() {
|
|
51
|
+
const [n, setN] = createSignal(0)
|
|
52
|
+
return (
|
|
53
|
+
<nav onClick={() => setN(n() + 1)}>
|
|
54
|
+
{['home', 'bell'].map(item => <NavIcon key={item} name={item} />)}
|
|
55
|
+
</nav>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
`
|
|
59
|
+
|
|
60
|
+
const result = compileJSX(source, 'Shell.tsx', { adapter })
|
|
61
|
+
const errs = result.errors.filter(e => e.severity === 'error')
|
|
62
|
+
expect(errs.length).toBeGreaterThan(0)
|
|
63
|
+
const bf048 = errs.find(e => e.code === 'BF048')
|
|
64
|
+
expect(bf048).toBeDefined()
|
|
65
|
+
expect(bf048!.message).toContain('NavIcon')
|
|
66
|
+
expect(bf048!.message).toContain('Shell')
|
|
67
|
+
expect(bf048!.message).toMatch(/did not compile to a template/)
|
|
68
|
+
}, 20000)
|
|
69
|
+
|
|
70
|
+
test('BF048: direct JSX tag (non-loop) referencing a switch-dispatch sibling fails the compile', () => {
|
|
71
|
+
const source = `
|
|
72
|
+
"use client"
|
|
73
|
+
import { createSignal } from '@barefootjs/client'
|
|
74
|
+
function StatusIcon({ status }: { status: string }) {
|
|
75
|
+
switch (status) {
|
|
76
|
+
case 'ok': return <span class="ok">OK</span>
|
|
77
|
+
case 'err': return <span class="err">ERR</span>
|
|
78
|
+
default: return null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function Panel() {
|
|
82
|
+
const [n, setN] = createSignal(0)
|
|
83
|
+
return <div onClick={() => setN(n() + 1)}><StatusIcon status="ok" /></div>
|
|
84
|
+
}
|
|
85
|
+
`
|
|
86
|
+
|
|
87
|
+
const result = compileJSX(source, 'Panel.tsx', { adapter })
|
|
88
|
+
const errs = result.errors.filter(e => e.severity === 'error')
|
|
89
|
+
const bf048 = errs.find(e => e.code === 'BF048')
|
|
90
|
+
expect(bf048).toBeDefined()
|
|
91
|
+
expect(bf048!.message).toContain('StatusIcon')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('legal neighbor: the identical switch-dispatch shape in a non-"use client" file compiles clean (#932)', () => {
|
|
95
|
+
// Same shape as the failing case above, but without the 'use client'
|
|
96
|
+
// directive: `listComponentFunctions`'s #932 bypass keeps NavIcon OFF
|
|
97
|
+
// `componentNames` entirely, so it is preserved verbatim in the marked
|
|
98
|
+
// template rather than compiled (and dropped) as a component. BF048
|
|
99
|
+
// must not fire here.
|
|
100
|
+
const source = `
|
|
101
|
+
function NavIcon({ name }: { name: string }) {
|
|
102
|
+
switch (name) {
|
|
103
|
+
case 'home': return <svg><path d="M1"/></svg>
|
|
104
|
+
case 'bell': return <svg><path d="M2"/></svg>
|
|
105
|
+
default: return null
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function Shell() {
|
|
109
|
+
return (
|
|
110
|
+
<nav>
|
|
111
|
+
{['home', 'bell'].map(item => <NavIcon key={item} name={item} />)}
|
|
112
|
+
</nav>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
`
|
|
116
|
+
|
|
117
|
+
const result = compileJSX(source, 'Shell.tsx', { adapter })
|
|
118
|
+
const errs = result.errors.filter(e => e.severity === 'error')
|
|
119
|
+
expect(errs.filter(e => e.code === 'BF048')).toHaveLength(0)
|
|
120
|
+
expect(errs).toHaveLength(0)
|
|
121
|
+
|
|
122
|
+
const markedTemplate = result.files.find(f => f.type === 'markedTemplate')
|
|
123
|
+
expect(markedTemplate).toBeDefined()
|
|
124
|
+
expect(markedTemplate!.content).toContain('function NavIcon')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('legal neighbor: a non-"use client" file never trips BF048, even when a sibling lands in componentNames', () => {
|
|
128
|
+
// The #932 bypass only skips NON-exported multi-return functions, so an
|
|
129
|
+
// exported switch-dispatch sibling in a non-client file IS in
|
|
130
|
+
// `componentNames` and produces no entry — the same set-difference shape
|
|
131
|
+
// BF048 keys on. But the diagnostic's premise (the 'use client' branch
|
|
132
|
+
// of #932, `ReferenceError` at hydrate time) doesn't apply outside
|
|
133
|
+
// client files, so BF048 is gated on a compiled client entry and must
|
|
134
|
+
// stay silent here.
|
|
135
|
+
const source = `
|
|
136
|
+
export function NavIcon({ name }: { name: string }) {
|
|
137
|
+
switch (name) {
|
|
138
|
+
case 'home': return <svg><path d="M1"/></svg>
|
|
139
|
+
case 'bell': return <svg><path d="M2"/></svg>
|
|
140
|
+
default: return null
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function Shell() {
|
|
144
|
+
return (
|
|
145
|
+
<nav>
|
|
146
|
+
{['home', 'bell'].map(item => <NavIcon key={item} name={item} />)}
|
|
147
|
+
</nav>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
`
|
|
151
|
+
|
|
152
|
+
const result = compileJSX(source, 'Shell.tsx', { adapter })
|
|
153
|
+
const errs = result.errors.filter(e => e.severity === 'error')
|
|
154
|
+
expect(errs.filter(e => e.code === 'BF048')).toHaveLength(0)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('legal neighbor: a "use client" sibling whose multi-return body DOES compile (if/else chain, #1401) stays clean', () => {
|
|
158
|
+
// if/else-if chains fold into `conditionalReturns` (#1401) and produce
|
|
159
|
+
// a real ternary template, so the sibling ends up in `entries` and the
|
|
160
|
+
// reference resolves. BF048 must not fire.
|
|
161
|
+
const source = `
|
|
162
|
+
"use client"
|
|
163
|
+
import { createSignal } from '@barefootjs/client'
|
|
164
|
+
function Badge({ kind }: { kind: string }) {
|
|
165
|
+
if (kind === 'ok') return <span class="ok">ok</span>
|
|
166
|
+
if (kind === 'warn') return <span class="warn">warn</span>
|
|
167
|
+
return <span class="err">err</span>
|
|
168
|
+
}
|
|
169
|
+
export function Panel() {
|
|
170
|
+
const [n, setN] = createSignal(0)
|
|
171
|
+
return (
|
|
172
|
+
<div onClick={() => setN(n() + 1)}>
|
|
173
|
+
<Badge kind="ok" />
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
`
|
|
178
|
+
|
|
179
|
+
const result = compileJSX(source, 'Panel.tsx', { adapter })
|
|
180
|
+
const errs = result.errors.filter(e => e.severity === 'error')
|
|
181
|
+
expect(errs.filter(e => e.code === 'BF048')).toHaveLength(0)
|
|
182
|
+
|
|
183
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
184
|
+
expect(clientJs).toBeDefined()
|
|
185
|
+
// Both the Panel and Badge components' hydrate() registrations exist
|
|
186
|
+
// with real templates — the sibling reference resolves. Badge is a
|
|
187
|
+
// non-exported sibling so its runtime key is file-scoped
|
|
188
|
+
// (`Badge__<hash>`); match on the `name: 'Badge'` metadata instead of
|
|
189
|
+
// the registry key.
|
|
190
|
+
expect(clientJs!.content).toMatch(/hydrate\('Badge[^']*',\s*\{[^}]*template:/s)
|
|
191
|
+
expect(clientJs!.content).toContain("name: 'Badge'")
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('legal neighbor: single-component "use client" multi-return root (#1401) is unaffected', () => {
|
|
195
|
+
// Only one component in the file, so `compileJSX` never enters the
|
|
196
|
+
// multi-component path where BF048 is computed at all.
|
|
197
|
+
const source = `
|
|
198
|
+
"use client"
|
|
199
|
+
import { createSignal } from '@barefootjs/client'
|
|
200
|
+
export function Toggle(props: { asChild?: boolean }) {
|
|
201
|
+
const [open, setOpen] = createSignal(false)
|
|
202
|
+
if (props.asChild) {
|
|
203
|
+
return <span onClick={() => setOpen(!open())}>child</span>
|
|
204
|
+
}
|
|
205
|
+
return <button onClick={() => setOpen(!open())}>toggle</button>
|
|
206
|
+
}
|
|
207
|
+
`
|
|
208
|
+
|
|
209
|
+
const result = compileJSX(source, 'Toggle.tsx', { adapter })
|
|
210
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
211
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
212
|
+
expect(clientJs).toBeDefined()
|
|
213
|
+
expect(clientJs!.content).toContain('template:')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('legal neighbor: a component-scope local JSX factory does not trip BF048', () => {
|
|
217
|
+
// `listComponentFunctions` recurses into function bodies, so a local
|
|
218
|
+
// factory like `Inner` below lands in `componentNames` and produces no
|
|
219
|
+
// standalone template — but its call sites are handled by the
|
|
220
|
+
// JSX-function-inlining pass, not `createComponent`, so it is NOT the
|
|
221
|
+
// dropped-sibling shape. BF048's uncompiled-sibling set is restricted
|
|
222
|
+
// to module top-level declarations precisely so this stays legal (the
|
|
223
|
+
// first BF048 cut flagged it and broke the ir-dynamic-tag corpus).
|
|
224
|
+
// Two components so the multi-component path (where BF048 runs) engages.
|
|
225
|
+
const source = `
|
|
226
|
+
"use client"
|
|
227
|
+
import { createSignal } from '@barefootjs/client'
|
|
228
|
+
export function Demo() {
|
|
229
|
+
const Inner = () => <span>x</span>
|
|
230
|
+
const [n, setN] = createSignal(0)
|
|
231
|
+
return <div onClick={() => setN(n() + 1)}><Inner /></div>
|
|
232
|
+
}
|
|
233
|
+
export function Other() {
|
|
234
|
+
return <p>other</p>
|
|
235
|
+
}
|
|
236
|
+
`
|
|
237
|
+
|
|
238
|
+
const result = compileJSX(source, 'demo.tsx', { adapter })
|
|
239
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
// computations that derive from those signals (`count() * 2`).
|
|
14
14
|
|
|
15
15
|
import { describe, test, expect } from 'bun:test'
|
|
16
|
-
import { extractSsrDefaults } from '../ssr-defaults'
|
|
16
|
+
import { extractSsrDefaults, deriveStashFromDefaults } from '../ssr-defaults'
|
|
17
|
+
import type { SsrDefault } from '../ssr-defaults'
|
|
17
18
|
import { analyzeComponent } from '../analyzer'
|
|
18
19
|
import { buildMetadata } from '../compiler'
|
|
19
20
|
|
|
@@ -253,3 +254,125 @@ describe('extractSsrDefaults', () => {
|
|
|
253
254
|
expect(defaults?.classes).toEqual({ value: 'a b c d tail' })
|
|
254
255
|
})
|
|
255
256
|
})
|
|
257
|
+
|
|
258
|
+
// TS twin of the Ruby/Python/PHP/Perl/Rust `derive*FromDefaults` runtime
|
|
259
|
+
// ports (#2524 SSR half). Matches `derive_vars_from_defaults`'s edge cases
|
|
260
|
+
// exactly — see that Ruby method's docstring
|
|
261
|
+
// (packages/adapter-erb/lib/barefoot_js.rb:337-360) for the reference
|
|
262
|
+
// semantics this mirrors.
|
|
263
|
+
describe('deriveStashFromDefaults', () => {
|
|
264
|
+
test('aliased prop: resolves the CALLER-facing propName, not the local key', () => {
|
|
265
|
+
// `{ n: count }` — extractSsrDefaults keys the entry by the LOCAL
|
|
266
|
+
// binding (`count`) with `propName: 'n'`. The caller supplies `n`.
|
|
267
|
+
const defaults: Record<string, SsrDefault> = {
|
|
268
|
+
count: { propName: 'n', value: null },
|
|
269
|
+
}
|
|
270
|
+
expect(deriveStashFromDefaults(defaults, { n: 7 })).toEqual({ count: 7 })
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
test('un-aliased prop: propName equals the local key, resolves the same way', () => {
|
|
274
|
+
const defaults: Record<string, SsrDefault> = {
|
|
275
|
+
n: { propName: 'n', value: null },
|
|
276
|
+
}
|
|
277
|
+
expect(deriveStashFromDefaults(defaults, { n: 7 })).toEqual({ n: 7 })
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('caller omits the prop: falls back to the static default value', () => {
|
|
281
|
+
const defaults: Record<string, SsrDefault> = {
|
|
282
|
+
count: { propName: 'n', value: 3 },
|
|
283
|
+
}
|
|
284
|
+
expect(deriveStashFromDefaults(defaults, {})).toEqual({ count: 3 })
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
test('caller supplies null / undefined for the propName: falls back to the static value', () => {
|
|
288
|
+
// Mirrors every runtime port's "present AND defined" check — an
|
|
289
|
+
// explicit null/undefined caller value does not count as an override.
|
|
290
|
+
const defaults: Record<string, SsrDefault> = {
|
|
291
|
+
count: { propName: 'n', value: 3 },
|
|
292
|
+
}
|
|
293
|
+
expect(deriveStashFromDefaults(defaults, { n: null })).toEqual({ count: 3 })
|
|
294
|
+
expect(deriveStashFromDefaults(defaults, { n: undefined })).toEqual({ count: 3 })
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test('caller supplies a falsy-but-defined propName value: the caller value wins', () => {
|
|
298
|
+
// `0` / `''` / `false` are legitimate override values, not "absent".
|
|
299
|
+
const defaults: Record<string, SsrDefault> = {
|
|
300
|
+
count: { propName: 'n', value: 99 },
|
|
301
|
+
}
|
|
302
|
+
expect(deriveStashFromDefaults(defaults, { n: 0 })).toEqual({ count: 0 })
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test('propName-less entry (signal / memo local): always uses the static value', () => {
|
|
306
|
+
// A caller cannot override an internal signal/memo by construction —
|
|
307
|
+
// even a same-named caller prop must not leak in.
|
|
308
|
+
const defaults: Record<string, SsrDefault> = {
|
|
309
|
+
doubled: { value: 10 },
|
|
310
|
+
}
|
|
311
|
+
expect(deriveStashFromDefaults(defaults, { doubled: 999 })).toEqual({ doubled: 10 })
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('isRestProps entry: prefers the caller-assembled rest bag under its OWN key', () => {
|
|
315
|
+
const defaults: Record<string, SsrDefault> = {
|
|
316
|
+
rest: { isRestProps: true, value: {} },
|
|
317
|
+
}
|
|
318
|
+
expect(deriveStashFromDefaults(defaults, { rest: { href: '/x' } })).toEqual({
|
|
319
|
+
rest: { href: '/x' },
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test('isRestProps entry: falls back to the static value (normally {}) when the caller supplied none', () => {
|
|
324
|
+
const defaults: Record<string, SsrDefault> = {
|
|
325
|
+
rest: { isRestProps: true, value: {} },
|
|
326
|
+
}
|
|
327
|
+
expect(deriveStashFromDefaults(defaults, {})).toEqual({ rest: {} })
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('isRestProps entry: an explicit `undefined` caller value still counts as supplied', () => {
|
|
331
|
+
// Unlike the ordinary propName branch (nullish check), the isRestProps
|
|
332
|
+
// branch tests presence via `in` — the rest bag is a caller-assembled
|
|
333
|
+
// aggregate, not a single scalar with a meaningful "absent" state, so
|
|
334
|
+
// an explicit `undefined` still wins over the static fallback instead
|
|
335
|
+
// of falling through to it.
|
|
336
|
+
const defaults: Record<string, SsrDefault> = {
|
|
337
|
+
rest: { isRestProps: true, value: { fallback: true } },
|
|
338
|
+
}
|
|
339
|
+
const result = deriveStashFromDefaults(defaults, { rest: undefined })
|
|
340
|
+
expect('rest' in result).toBe(true)
|
|
341
|
+
expect(result.rest).toBeUndefined()
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('a bare (non-object) entry passes through as-is', () => {
|
|
345
|
+
// Defensive parity with every runtime port's `ref($d) eq 'HASH'` /
|
|
346
|
+
// `isinstance(d, dict)` guard — never emitted by `extractSsrDefaults`
|
|
347
|
+
// itself, but a caller may hand this a manifest round-tripped through a
|
|
348
|
+
// generic JSON domain.
|
|
349
|
+
const defaults = { flag: true } as unknown as Record<string, SsrDefault>
|
|
350
|
+
expect(deriveStashFromDefaults(defaults, {})).toEqual({ flag: true })
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
test('a literal null entry hits the same non-object passthrough guard', () => {
|
|
354
|
+
// `d === null` is checked ALONGSIDE `typeof d !== 'object'` (not just
|
|
355
|
+
// the latter) — `typeof null === 'object'` in JS, so without the
|
|
356
|
+
// explicit `d === null` check a null entry would wrongly fall into the
|
|
357
|
+
// object-shaped branches below and throw reading `d.isRestProps`.
|
|
358
|
+
const defaults = { flag: null } as unknown as Record<string, SsrDefault>
|
|
359
|
+
expect(deriveStashFromDefaults(defaults, {})).toEqual({ flag: null })
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test('mixed defaults map resolves every entry kind independently', () => {
|
|
363
|
+
const defaults: Record<string, SsrDefault> = {
|
|
364
|
+
count: { propName: 'n', value: null },
|
|
365
|
+
text: { propName: 'text', value: null },
|
|
366
|
+
doubled: { value: 0 },
|
|
367
|
+
rest: { isRestProps: true, value: {} },
|
|
368
|
+
}
|
|
369
|
+
expect(
|
|
370
|
+
deriveStashFromDefaults(defaults, { n: 7, rest: { extra: 'x' } }),
|
|
371
|
+
).toEqual({
|
|
372
|
+
count: 7,
|
|
373
|
+
text: null,
|
|
374
|
+
doubled: 0,
|
|
375
|
+
rest: { extra: 'x' },
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
})
|
|
@@ -20,6 +20,7 @@ function envWith(
|
|
|
20
20
|
propsForLift: new Set(
|
|
21
21
|
bindings.filter(([, k]) => k === 'prop').map(([n]) => n),
|
|
22
22
|
),
|
|
23
|
+
propSourceNames: options?.propSourceNames ?? new Map(),
|
|
23
24
|
propsObjectName: options?.propsObjectName ?? 'props',
|
|
24
25
|
allowFallback: options?.allowFallback ?? true,
|
|
25
26
|
}
|
|
@@ -35,6 +35,7 @@ function envWith(
|
|
|
35
35
|
propsForLift: new Set(
|
|
36
36
|
bindings.filter(([, k]) => k === 'prop').map(([n]) => n),
|
|
37
37
|
),
|
|
38
|
+
propSourceNames: options?.propSourceNames ?? new Map(),
|
|
38
39
|
propsObjectName: options?.propsObjectName ?? 'props',
|
|
39
40
|
allowFallback: options?.allowFallback ?? true,
|
|
40
41
|
templatePrimitives: options?.templatePrimitives,
|
|
@@ -28,6 +28,17 @@ export interface TemplateSections {
|
|
|
28
28
|
* component in a source file.
|
|
29
29
|
*/
|
|
30
30
|
moduleConstants?: string
|
|
31
|
+
/**
|
|
32
|
+
* When true, the `moduleConstants` section already carries the module's
|
|
33
|
+
* EXPORTED value declarations (consts/functions, `export` keyword
|
|
34
|
+
* included) interleaved in source order — so the compiler must NOT
|
|
35
|
+
* re-emit them via `generateModuleExports` (only `export { … }`
|
|
36
|
+
* specifier blocks remain its job). JSX adapters set this; keeping the
|
|
37
|
+
* exported and non-exported declarations in one source-ordered section
|
|
38
|
+
* is what prevents a non-exported const that reads an exported one from
|
|
39
|
+
* being emitted above it (a module-load TDZ crash — #2570's input-otp).
|
|
40
|
+
*/
|
|
41
|
+
moduleConstantsIncludeExports?: boolean
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
export interface AdapterOutput {
|