@barefootjs/jsx 0.33.3 → 0.33.6

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.
Files changed (87) hide show
  1. package/dist/adapters/child-scope.d.ts +13 -0
  2. package/dist/adapters/child-scope.d.ts.map +1 -1
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/errors.d.ts +1 -0
  5. package/dist/errors.d.ts.map +1 -1
  6. package/dist/index.d.ts +2 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1392 -1117
  9. package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/child-components.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/component-scope.d.ts +20 -0
  13. package/dist/ir-to-client-js/component-scope.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +2 -4
  17. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/shared.d.ts +4 -3
  19. package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/stringify/component-loop.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/csr-substitute.d.ts +35 -9
  24. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/emit-reactive.d.ts +10 -0
  26. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/html-template.d.ts +49 -2
  28. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/imports.d.ts +2 -2
  30. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  31. package/dist/ir-to-client-js/index.d.ts.map +1 -1
  32. package/dist/ir-to-client-js/rewrite-props-object.d.ts +12 -1
  33. package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
  34. package/dist/jsx-to-ir.d.ts.map +1 -1
  35. package/dist/prop-rewrite.d.ts +5 -2
  36. package/dist/prop-rewrite.d.ts.map +1 -1
  37. package/dist/props-binding.d.ts +49 -11
  38. package/dist/props-binding.d.ts.map +1 -1
  39. package/dist/types.d.ts +19 -5
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +2 -2
  42. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +15 -15
  43. package/src/__tests__/aliased-import-child-registry-2777.test.ts +112 -0
  44. package/src/__tests__/date-lowering.test.ts +18 -4
  45. package/src/__tests__/fragment-body-loop-key.test.ts +95 -0
  46. package/src/__tests__/inner-loop-mathml-namespace.test.ts +1 -1
  47. package/src/__tests__/inner-loop-svg-namespace.test.ts +4 -4
  48. package/src/__tests__/issue-2724-prop-alias-loop-not-static.test.ts +355 -0
  49. package/src/__tests__/issue-2737-props-object-alias-template.test.ts +86 -0
  50. package/src/__tests__/issue-2756-input-skeleton-bails.test.ts +123 -0
  51. package/src/__tests__/issue-2778-getter-alias-csr-template.test.ts +113 -0
  52. package/src/__tests__/issue-2806-rest-bag-template-rewrite.test.ts +136 -0
  53. package/src/__tests__/issue-2835-props-type-member-name-collision.test.ts +259 -0
  54. package/src/__tests__/loop-item-root-scope.test.ts +7 -3
  55. package/src/__tests__/markup-prop-brand.test.ts +165 -26
  56. package/src/__tests__/multi-root-loop-body.test.ts +7 -3
  57. package/src/__tests__/namespace-import-primitive.test.ts +244 -0
  58. package/src/__tests__/preamble-declarations.test.ts +42 -0
  59. package/src/__tests__/rewrite-destructured-props.test.ts +53 -0
  60. package/src/__tests__/static-loop-csr-materialize.test.ts +2 -2
  61. package/src/adapters/child-scope.ts +23 -0
  62. package/src/analyzer.ts +130 -0
  63. package/src/errors.ts +14 -1
  64. package/src/index.ts +7 -0
  65. package/src/ir-to-client-js/build-references.ts +7 -0
  66. package/src/ir-to-client-js/child-components.ts +8 -1
  67. package/src/ir-to-client-js/collect-elements.ts +10 -38
  68. package/src/ir-to-client-js/component-scope.ts +60 -0
  69. package/src/ir-to-client-js/compute-inlinability.ts +46 -11
  70. package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +19 -1
  71. package/src/ir-to-client-js/control-flow/plan/loop.ts +2 -4
  72. package/src/ir-to-client-js/control-flow/shared.ts +12 -5
  73. package/src/ir-to-client-js/control-flow/stringify/component-loop.ts +7 -0
  74. package/src/ir-to-client-js/control-flow/stringify/loop.ts +8 -2
  75. package/src/ir-to-client-js/control-flow.ts +12 -0
  76. package/src/ir-to-client-js/csr-substitute.ts +82 -16
  77. package/src/ir-to-client-js/emit-reactive.ts +29 -13
  78. package/src/ir-to-client-js/emit-registration.ts +1 -1
  79. package/src/ir-to-client-js/html-template.ts +194 -82
  80. package/src/ir-to-client-js/imports.ts +4 -0
  81. package/src/ir-to-client-js/index.ts +34 -23
  82. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +7 -2
  83. package/src/ir-to-client-js/rewrite-props-object.ts +14 -0
  84. package/src/jsx-to-ir.ts +318 -54
  85. package/src/prop-rewrite.ts +23 -5
  86. package/src/props-binding.ts +74 -18
  87. package/src/types.ts +19 -5
@@ -0,0 +1,244 @@
1
+ /**
2
+ * #2771 — `import * as bf from '@barefootjs/client'` followed by
3
+ * `bf.createSignal(0)` (or any other reactive primitive accessed off the
4
+ * namespace binding) compiled with ZERO diagnostics, but `resolvePrimitiveKind`'s
5
+ * fast path only matches a bare identifier callee — the checker-based slow
6
+ * path that DOES understand a namespace-qualified call only runs when a
7
+ * shared `ts.Program` is supplied (`CompileOptions.program`). Without one,
8
+ * the declaration is silently dropped from the compiled output and every
9
+ * reference to it throws `ReferenceError` at hydrate.
10
+ *
11
+ * BF013 refuses loudly instead, gated on NON-recognition — a compile that
12
+ * DOES supply a program (see `primitive-resolver-alias.test.ts`'s pattern)
13
+ * must never see this diagnostic, since that path already lowers the shape
14
+ * correctly.
15
+ */
16
+ import { describe, test, expect } from 'bun:test'
17
+ import path from 'path'
18
+ import ts from 'typescript'
19
+ import { compileJSX } from '../compiler'
20
+ import { analyzeComponent } from '../index'
21
+ import { TestAdapter } from '../adapters/test-adapter'
22
+
23
+ const adapter = new TestAdapter()
24
+
25
+ function errorsFor(source: string): Array<{ code?: string; severity: string }> {
26
+ const result = compileJSX(source, 'Counter.tsx', { adapter })
27
+ return result.errors.map(e => ({ code: e.code, severity: e.severity }))
28
+ }
29
+
30
+ function bf013(source: string) {
31
+ return errorsFor(source).find(e => e.code === 'BF013')
32
+ }
33
+
34
+ describe('#2771 — BF013: reactive primitive called through an unresolved namespace import', () => {
35
+ test('bf.createSignal(...) (array destructure) fires BF013', () => {
36
+ const result = compileJSX(
37
+ `
38
+ 'use client'
39
+ import * as bf from '@barefootjs/client'
40
+ export function Counter() {
41
+ const [n, setN] = bf.createSignal(0)
42
+ return <button onClick={() => setN(n() + 1)}>{n()}</button>
43
+ }
44
+ `,
45
+ 'Counter.tsx',
46
+ { adapter },
47
+ )
48
+ const err = result.errors.find(e => e.code === 'BF013')
49
+ expect(err).toBeDefined()
50
+ expect(err!.severity).toBe('error')
51
+ expect(err!.message).toContain('bf.createSignal')
52
+ expect(err!.suggestion?.escape).toEqual([{ kind: 'rewrite' }])
53
+ })
54
+
55
+ test('bf.createMemo(...) (array destructure) fires BF013', () => {
56
+ const err = bf013(`
57
+ 'use client'
58
+ import * as bf from '@barefootjs/client'
59
+ import { createSignal } from '@barefootjs/client'
60
+ export function Counter() {
61
+ const [n] = createSignal(1)
62
+ const [doubled] = bf.createMemo(() => n() * 2)
63
+ return <div>{doubled()}</div>
64
+ }
65
+ `)
66
+ expect(err).toBeDefined()
67
+ })
68
+
69
+ test('bf.createEffect(...) (expression statement) fires BF013', () => {
70
+ const err = bf013(`
71
+ 'use client'
72
+ import * as bf from '@barefootjs/client'
73
+ import { createSignal } from '@barefootjs/client'
74
+ export function Counter() {
75
+ const [n] = createSignal(0)
76
+ bf.createEffect(() => { console.log(n()) })
77
+ return <div>{n()}</div>
78
+ }
79
+ `)
80
+ expect(err).toBeDefined()
81
+ })
82
+
83
+ test('bf.onMount(...) (expression statement) fires BF013', () => {
84
+ const err = bf013(`
85
+ 'use client'
86
+ import * as bf from '@barefootjs/client'
87
+ export function Counter() {
88
+ bf.onMount(() => { console.log('mounted') })
89
+ return <div>static</div>
90
+ }
91
+ `)
92
+ expect(err).toBeDefined()
93
+ })
94
+
95
+ test('bf.onCleanup(...) (expression statement) fires BF013', () => {
96
+ const err = bf013(`
97
+ 'use client'
98
+ import * as bf from '@barefootjs/client'
99
+ export function Counter() {
100
+ bf.onCleanup(() => { console.log('cleaned up') })
101
+ return <div>static</div>
102
+ }
103
+ `)
104
+ expect(err).toBeDefined()
105
+ })
106
+
107
+ test('bf.createSearchParams(...) (array destructure) fires BF013', () => {
108
+ const err = bf013(`
109
+ 'use client'
110
+ import * as bf from '@barefootjs/client'
111
+ export function Counter() {
112
+ const [searchParams] = bf.createSearchParams()
113
+ return <div>{searchParams().get('sort')}</div>
114
+ }
115
+ `)
116
+ expect(err).toBeDefined()
117
+ })
118
+
119
+ test('bf.createSignal(0)[0] (element access) fires BF013', () => {
120
+ const err = bf013(`
121
+ 'use client'
122
+ import * as bf from '@barefootjs/client'
123
+ export function Counter() {
124
+ const n = bf.createSignal(0)[0]
125
+ return <div>{n()}</div>
126
+ }
127
+ `)
128
+ expect(err).toBeDefined()
129
+ })
130
+
131
+ test('a bare named import is unaffected (no BF013)', () => {
132
+ const errs = errorsFor(`
133
+ 'use client'
134
+ import { createSignal } from '@barefootjs/client'
135
+ export function Counter() {
136
+ const [n, setN] = createSignal(0)
137
+ return <button onClick={() => setN(n() + 1)}>{n()}</button>
138
+ }
139
+ `)
140
+ expect(errs.find(e => e.code === 'BF013')).toBeUndefined()
141
+ })
142
+
143
+ test('a namespace import of a DIFFERENT module is unaffected (no BF013)', () => {
144
+ const result = compileJSX(
145
+ `
146
+ 'use client'
147
+ import * as store from './store'
148
+ export function Counter() {
149
+ const [n, setN] = store.createSignal(0)
150
+ return <button onClick={() => setN(n() + 1)}>{n()}</button>
151
+ }
152
+ `,
153
+ 'Counter.tsx',
154
+ { adapter },
155
+ )
156
+ expect(result.errors.find(e => e.code === 'BF013')).toBeUndefined()
157
+ })
158
+
159
+ test('a component-local binding shadowing the namespace name is unaffected (no BF013)', () => {
160
+ const result = compileJSX(
161
+ `
162
+ 'use client'
163
+ import * as bf from '@barefootjs/client'
164
+ export function Counter() {
165
+ const bf = { createSignal: (x: number) => [() => x, (v: number) => {}] as const }
166
+ const [n, setN] = bf.createSignal(0)
167
+ return <button onClick={() => setN(n() + 1)}>{n()}</button>
168
+ }
169
+ `,
170
+ 'Counter.tsx',
171
+ { adapter },
172
+ )
173
+ expect(result.errors.find(e => e.code === 'BF013')).toBeUndefined()
174
+ })
175
+
176
+ test('a type-only namespace import is unaffected (no BF013)', () => {
177
+ const result = compileJSX(
178
+ `
179
+ 'use client'
180
+ import type * as bf from '@barefootjs/client'
181
+ export function Counter() {
182
+ return <div>static</div>
183
+ }
184
+ `,
185
+ 'Counter.tsx',
186
+ { adapter },
187
+ )
188
+ expect(result.errors.find(e => e.code === 'BF013')).toBeUndefined()
189
+ })
190
+ })
191
+
192
+ describe('#2771 — checker-path pin: a shared Program resolves the namespace and never fires BF013', () => {
193
+ const CLIENT_DIR = path.resolve(__dirname, '../../../client/src')
194
+
195
+ function programFor(filePath: string, source: string): ts.Program {
196
+ const absolute = path.resolve(filePath)
197
+ const compilerOptions: ts.CompilerOptions = {
198
+ target: ts.ScriptTarget.Latest,
199
+ module: ts.ModuleKind.ESNext,
200
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
201
+ jsx: ts.JsxEmit.ReactJSX,
202
+ strict: true,
203
+ skipLibCheck: true,
204
+ noEmit: true,
205
+ esModuleInterop: true,
206
+ baseUrl: path.dirname(absolute),
207
+ }
208
+ const defaultHost = ts.createCompilerHost(compilerOptions)
209
+ const host: ts.CompilerHost = {
210
+ ...defaultHost,
211
+ getSourceFile(fileName, languageVersion) {
212
+ if (path.resolve(fileName) === absolute) {
213
+ return ts.createSourceFile(fileName, source, languageVersion, true, ts.ScriptKind.TSX)
214
+ }
215
+ return defaultHost.getSourceFile(fileName, languageVersion)
216
+ },
217
+ fileExists(fileName) {
218
+ if (path.resolve(fileName) === absolute) return true
219
+ return defaultHost.fileExists(fileName)
220
+ },
221
+ readFile(fileName) {
222
+ if (path.resolve(fileName) === absolute) return source
223
+ return defaultHost.readFile(fileName)
224
+ },
225
+ }
226
+ return ts.createProgram([absolute], compilerOptions, host)
227
+ }
228
+
229
+ test('with a shared Program, bf.createSignal resolves and BF013 never fires', () => {
230
+ const source = `
231
+ 'use client'
232
+ import * as bf from '@barefootjs/client'
233
+ export function Counter() {
234
+ const [n, setN] = bf.createSignal(0)
235
+ return <button onClick={() => setN(n() + 1)}>{n()}</button>
236
+ }
237
+ `
238
+ const testFile = path.join(CLIENT_DIR, '__ns_primitive_checker_pin__.tsx')
239
+ const program = programFor(testFile, source)
240
+ const ctx = analyzeComponent(source, testFile, undefined, program)
241
+ expect(ctx.signals.length).toBe(1)
242
+ expect(ctx.errors.find(e => e.code === 'BF013')).toBeUndefined()
243
+ })
244
+ })
@@ -205,3 +205,45 @@ export function Rows(props: { rows: Row[] }) {
205
205
  expect(dslErrors(source)).toEqual([])
206
206
  })
207
207
  })
208
+
209
+ describe('preamble value declarations — function-literal elision (#2797)', () => {
210
+ // An event handler hoisted into the preamble (rather than written inline in
211
+ // the JSX attribute) has no template-expression form on any DSL backend —
212
+ // but every adapter's own SSR prop-builder already skips an event-handler
213
+ // prop outright, so a name read ONLY that way needs no SSR representation
214
+ // at all. Elided, not refused: `declarations` legally ends up `[]`.
215
+ test('a handler read only as an event-handler prop value elides clean, no refusal', () => {
216
+ const source = ROWS(`const handleClick = () => {}`, `<li key={r.id} onClick={handleClick}>{r.label}</li>`)
217
+ expect(loopOf(source).preamble?.declarations).toEqual([])
218
+ expect(dslErrors(source)).toEqual([])
219
+ })
220
+
221
+ test('an elided handler coexists with a genuinely lowered declaration', () => {
222
+ const loop = loopOf(
223
+ ROWS(
224
+ `const cls = r.done ? 'done' : 'open'\n const handleClick = () => {}`,
225
+ `<li key={r.id} class={cls} onClick={handleClick}>{r.label}</li>`,
226
+ ),
227
+ )
228
+ // Only the lowerable declaration survives — the elided one contributes no entry.
229
+ expect(loop.preamble?.declarations?.map(d => d.name)).toEqual(['cls'])
230
+ })
231
+
232
+ // Soundness: a function-valued local read ANYWHERE other than an
233
+ // event-handler prop value still refuses — elision only widens what's
234
+ // SAFE, it never lets an SSR-relevant function read through unassigned.
235
+ test('a handler also read in a non-event position still refuses', () => {
236
+ const errors = dslErrors(
237
+ ROWS(`const handleClick = () => {}`, `<li key={r.id}>{handleClick.name}</li>`),
238
+ )
239
+ expect(errors.length).toBeGreaterThan(0)
240
+ expect(errors[0]).toMatch(/not a sequence of value declarations/)
241
+ })
242
+
243
+ test('a handler passed to a NON-event child prop still refuses', () => {
244
+ const errors = dslErrors(
245
+ ROWS(`const handleClick = () => {}`, `<li key={r.id} data-fn={handleClick}>{r.label}</li>`),
246
+ )
247
+ expect(errors.length).toBeGreaterThan(0)
248
+ })
249
+ })
@@ -71,3 +71,56 @@ export { Btn }
71
71
  expect(js).not.toMatch(/\(_p\.size[^)]*\)-[0-9]/)
72
72
  })
73
73
  })
74
+
75
+ // #2741: `rewriteDestructuredPropsInExpr` (the reactive `createEffect`
76
+ // update path) used to run a bare `\b<prop>\b` regex over the whole
77
+ // expression text, so an object-literal property whose KEY happened to
78
+ // share a prop's name (`{ tag: tag }`) had the KEY rewritten too
79
+ // (`{ _p.tag: _p.tag }`), which is not valid JS — the whole client module
80
+ // failed to parse, silently (no diagnostic). Now delegates to the same
81
+ // AST-based `rewriteBarePropRefs` (`prop-rewrite.ts`) the hydrate template
82
+ // lambda already used, which distinguishes a key from a value reference.
83
+ describe('reactive-effect prop rewrite does not touch object-literal keys (#2741)', () => {
84
+ test('an explicit-key object literal used in a reactive attribute keeps its keys', () => {
85
+ const source = `
86
+ "use client"
87
+ import { queryHref } from '@barefootjs/client'
88
+ function Comp({ base, tag, page }: { base: string; tag: string; page: string }) {
89
+ return <a href={queryHref(base, { tag: tag, page: page ? page : '' })}>filter</a>
90
+ }
91
+ export { Comp }
92
+ `
93
+ const js = compileClientJs(source)
94
+ expect(js).toContain("{ tag: _p.tag, page: _p.page ? _p.page : '' }")
95
+ expect(js).not.toContain('_p.tag:')
96
+ expect(js).not.toContain('_p.page:')
97
+ })
98
+
99
+ test('a keyed prop with a destructure default still gets the (_p.x ?? default) wrap only in value position', () => {
100
+ const source = `
101
+ "use client"
102
+ import { queryHref } from '@barefootjs/client'
103
+ function Comp({ base, tag = 'all', page }: { base: string; tag?: string; page: string }) {
104
+ return <a href={queryHref(base, { tag: tag, page: page ? page : '' })}>filter</a>
105
+ }
106
+ export { Comp }
107
+ `
108
+ const js = compileClientJs(source)
109
+ expect(js).toContain("{ tag: (_p.tag ?? 'all'), page: _p.page ? _p.page : '' }")
110
+ expect(js).not.toContain("(_p.tag ?? 'all'):")
111
+ })
112
+
113
+ test('a prop name shadowed by an inner callback parameter is not rewritten inside that callback', () => {
114
+ const source = `
115
+ "use client"
116
+ import { createSignal } from '@barefootjs/client'
117
+ function Comp({ tag, items }: { tag: string; items: { a: string }[] }) {
118
+ const [n] = createSignal(0)
119
+ return <div data-x={n() + items.map((tag) => tag.a).join(',') + tag}></div>
120
+ }
121
+ export { Comp }
122
+ `
123
+ const js = compileClientJs(source)
124
+ expect(js).toContain("items.map((tag) => tag.a).join(',') + _p.tag")
125
+ })
126
+ })
@@ -242,7 +242,7 @@ describe('#1247 — static-loop CSR self-heal', () => {
242
242
  expect(clientJs).toMatch(/if \(!__iterEl\)/)
243
243
  // The cloned template body is a single `renderChild('Tag', ...)`
244
244
  // expression — props read from the destructured forEach param.
245
- const matMatch = clientJs.match(/__tpl\.innerHTML = `([^`]*)`/)
245
+ const matMatch = clientJs.match(/__tpl\.innerHTML = withParentScope\(__scopeId, \(\) => `([^`]*)`\)/)
246
246
  expect(matMatch).toBeTruthy()
247
247
  const inner = matMatch![1]
248
248
  expect(inner).toMatch(/\$\{renderChild\('Tag', \{[^}]*id: id[^}]*variant: t\.variant[^}]*\}, id/)
@@ -287,7 +287,7 @@ describe('#1247 — static-loop CSR self-heal', () => {
287
287
  // Materialize branch present.
288
288
  expect(clientJs).toMatch(/if \(!__iterEl\)/)
289
289
  // Per-iteration template is `<li ...>...renderChild('Cell', ...)...</li>`.
290
- const matMatch = clientJs.match(/__tpl\.innerHTML = `([^`]*)`/)
290
+ const matMatch = clientJs.match(/__tpl\.innerHTML = withParentScope\(__scopeId, \(\) => `([^`]*)`\)/)
291
291
  expect(matMatch).toBeTruthy()
292
292
  const inner = matMatch![1]
293
293
  expect(inner).toMatch(/^<li\b/)
@@ -20,3 +20,26 @@ export function derivesScopeFromSlot(
20
20
  ): boolean {
21
21
  return comp.slotId != null && comp.loopItemRoot !== true
22
22
  }
23
+
24
+ /**
25
+ * The `renderChild(name, props, key, <here>)` tail arguments — the single
26
+ * decision point both CSR template emitters (`html-template.ts`'s two
27
+ * `case 'component'` branches) share, so they can't drift on it the way
28
+ * #2444 happened for `derivesScopeFromSlot` itself.
29
+ *
30
+ * A loop item root (`!derivesScopeFromSlot`) still gets its `slotId` and the
31
+ * `loopItemRoot` flag (#2833) rather than dropping the slot argument
32
+ * entirely as before: Hono stamps `bf-h`/`bf-m` on a row root too (just
33
+ * without deriving its `bf-s` from them), and a pure-CSR mount's static
34
+ * `qsaChildScopes` init selector needs that same marker to find the row.
35
+ */
36
+ export function renderChildScopeArgs(
37
+ comp: Pick<IRComponent, 'slotId' | 'loopItemRoot'>,
38
+ keyArg: string
39
+ ): string {
40
+ if (comp.slotId == null) return keyArg
41
+ const key = keyArg || ', undefined'
42
+ return derivesScopeFromSlot(comp)
43
+ ? `${key}, '${comp.slotId}'`
44
+ : `${key}, '${comp.slotId}', true`
45
+ }
package/src/analyzer.ts CHANGED
@@ -3058,9 +3058,25 @@ function collectConstant(
3058
3058
  const value = defaultValueExpr ? `${baseValue} ?? ${defaultValueExpr}` : baseValue
3059
3059
  const containsArrow = el.initializer ? nodeContainsArrow(el.initializer) : false
3060
3060
  const freeIdentifiers = el.initializer ? extractFreeIdentifiersFromNode(el.initializer) : new Set([propsName])
3061
+ // Structured parse of `value`, same as the plain-identifier branch
3062
+ // below (#2724 review finding): without this, a RENAMED destructure
3063
+ // (`const { items: rows } = props`) left `parsed` undefined forever,
3064
+ // so `isArrayExprDirectPropRef`'s member-access recognition (which
3065
+ // reads `.parsed`, not `.name`) never saw it — `rows` stayed
3066
+ // classified `isStaticArray: true` even though it's `props.items`
3067
+ // itself. Since #2835, the name-only branch reads `boundPropNames`
3068
+ // (empty for a whole-`props`-param component) rather than the
3069
+ // type-member-name set, so this `.parsed` is the only path for the
3070
+ // un-renamed case too, not just the rename case. `parseExpression`
3071
+ // naturally keeps a defaulted destructure (`value` ends in
3072
+ // `?? <default>`) OUT of the
3073
+ // `kind: 'member'` shape (it parses as a logical-or instead), which
3074
+ // is what keeps that case correctly static per #2806.
3075
+ const parsed = parseExpression(`(${value.trim()})`)
3061
3076
  ctx.localConstants.push({
3062
3077
  name: localName,
3063
3078
  value,
3079
+ parsed,
3064
3080
  declarationKind,
3065
3081
  isExported,
3066
3082
  type: null,
@@ -3889,6 +3905,11 @@ function validateContext(ctx: AnalyzerContext): void {
3889
3905
  // failures before factory inlining landed (#931).
3890
3906
  validateReactiveFactoryCalls(ctx)
3891
3907
 
3908
+ // BF013: flag a reactive primitive invoked through a namespace import
3909
+ // that the analyzer could not resolve (#2771) — same "silently dropped,
3910
+ // ReferenceError at hydrate" failure shape as BF011.
3911
+ validateNamespaceQualifiedPrimitives(ctx)
3912
+
3892
3913
  // BF003: a `"use client"` file may not import a component from a
3893
3914
  // non-`"use client"` source file (#1501). Server-side knowledge must
3894
3915
  // not transitively cross into the client bundle, and hydration-marker
@@ -5864,6 +5885,115 @@ export function validateReactiveFactoryCalls(ctx: AnalyzerContext): void {
5864
5885
  }
5865
5886
  }
5866
5887
 
5888
+ /**
5889
+ * BF013 (#2771): a reactive primitive called through a namespace import
5890
+ * (`import * as bf from '@barefootjs/client'`, `bf.createSignal(...)`)
5891
+ * that `resolvePrimitiveKind`'s checker-based slow path could not resolve
5892
+ * — no shared `ts.Program` was supplied for this compile. Fast-path
5893
+ * recognition only matches a bare identifier callee, so the call is
5894
+ * silently dropped from `ctx.signals`/`ctx.memos`/etc. while every
5895
+ * reference to its result survives in the emitted output, throwing
5896
+ * ReferenceError at hydrate.
5897
+ *
5898
+ * Gated on non-recognition, not on the namespace-import shape itself:
5899
+ * when a checker IS available (vite always supplies one via
5900
+ * `corpus-program.ts`), `resolvePrimitiveKind` already resolves this
5901
+ * shape soundly end-to-end, and this pass must not flag what the
5902
+ * checker-aided path already handles correctly.
5903
+ *
5904
+ * Scoped to the component's own top-level statements, mirroring
5905
+ * `validateReactiveFactoryCalls` above — a namespace-qualified call
5906
+ * inside a handler/effect body is emitted verbatim and runs fine at
5907
+ * hydrate (the namespace import itself is re-emitted, #2767 follow-up),
5908
+ * so walking into nested bodies would produce false refusals.
5909
+ */
5910
+ function validateNamespaceQualifiedPrimitives(ctx: AnalyzerContext): void {
5911
+ if (!ctx.componentNode) return
5912
+ const body = ts.isFunctionDeclaration(ctx.componentNode)
5913
+ ? ctx.componentNode.body
5914
+ : (ts.isBlock(ctx.componentNode.body) ? ctx.componentNode.body : null)
5915
+ if (!body) return
5916
+
5917
+ for (const stmt of body.statements) {
5918
+ const calls: ts.CallExpression[] = []
5919
+ if (ts.isVariableStatement(stmt)) {
5920
+ for (const decl of stmt.declarationList.declarations) {
5921
+ let init = decl.initializer
5922
+ // `const [n] = bf.createSignal(0)[0]` — vanishingly rare, but the
5923
+ // element-access wrapper must not hide the call underneath it.
5924
+ if (init && ts.isElementAccessExpression(init)) init = init.expression
5925
+ if (init && ts.isCallExpression(init)) calls.push(init)
5926
+ }
5927
+ } else if (ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression)) {
5928
+ calls.push(stmt.expression)
5929
+ }
5930
+
5931
+ for (const call of calls) {
5932
+ const callee = call.expression
5933
+ if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.expression)) continue
5934
+ const primitive = callee.name.text
5935
+ if (!(primitive in PRIMITIVE_CANONICAL_NAMES)) continue
5936
+ if (resolvePrimitiveKind(call, ctx) !== null) continue // checker resolved it — sound, no diagnostic
5937
+ const ns = callee.expression.text
5938
+ if (!isClientNamespaceImportName(ns, ctx)) continue // import identity, not the bare name
5939
+ if (isNamespaceNameShadowedAtComponentTopLevel(ns, body, ctx)) continue
5940
+
5941
+ const loc = getSourceLocation(stmt, ctx.sourceFile, ctx.filePath)
5942
+ ctx.errors.push(
5943
+ createError(ErrorCodes.PRIMITIVE_VIA_NAMESPACE_IMPORT, loc, {
5944
+ severity: 'error',
5945
+ message:
5946
+ `'${ns}.${primitive}(...)' calls a @barefootjs/client reactive primitive through the namespace ` +
5947
+ `import 'import * as ${ns}' and was not recognized by the analyzer. The declaration is dropped ` +
5948
+ `from the compiled output and every reference to it throws ReferenceError at hydrate.`,
5949
+ suggestion: {
5950
+ message:
5951
+ `Import the primitive by name: import { ${primitive} } from '@barefootjs/client' and call ` +
5952
+ `${primitive}(...) directly. (A shared ts.Program passed via CompileOptions.program lets the ` +
5953
+ `analyzer resolve the namespace through the TypeChecker; without one the named form is required.)`,
5954
+ escape: [{ kind: 'rewrite' }],
5955
+ },
5956
+ })
5957
+ )
5958
+ }
5959
+ }
5960
+ }
5961
+
5962
+ function isClientNamespaceImportName(name: string, ctx: AnalyzerContext): boolean {
5963
+ return ctx.imports.some(
5964
+ imp =>
5965
+ imp.source === '@barefootjs/client' &&
5966
+ !imp.isTypeOnly &&
5967
+ imp.specifiers.some(s => s.isNamespace && s.name === name)
5968
+ )
5969
+ }
5970
+
5971
+ /**
5972
+ * A component-top-level binding that shadows the namespace import name
5973
+ * (a local `const bf = ...`/function `bf`, or a prop param/props-object
5974
+ * named `bf`) means `bf.createSignal` in THIS component isn't the client
5975
+ * namespace at all — refusing would be a false positive. Does not model a
5976
+ * shadow introduced in a nested block above the call site (same accepted
5977
+ * posture as the checker-aided fast path).
5978
+ */
5979
+ function isNamespaceNameShadowedAtComponentTopLevel(
5980
+ name: string,
5981
+ body: ts.Block,
5982
+ ctx: AnalyzerContext
5983
+ ): boolean {
5984
+ if (ctx.propsObjectName === name) return true
5985
+ if (ctx.propsParams.some(p => p.name === name)) return true
5986
+ for (const stmt of body.statements) {
5987
+ if (ts.isFunctionDeclaration(stmt) && stmt.name?.text === name) return true
5988
+ if (ts.isVariableStatement(stmt)) {
5989
+ for (const decl of stmt.declarationList.declarations) {
5990
+ if (ts.isIdentifier(decl.name) && decl.name.text === name) return true
5991
+ }
5992
+ }
5993
+ }
5994
+ return false
5995
+ }
5996
+
5867
5997
  /**
5868
5998
  * Object-destructure half of `validateReactiveFactoryCalls` (#2325). Split
5869
5999
  * out so the tuple path above stays a straight read of the pre-#2325 logic
package/src/errors.ts CHANGED
@@ -19,8 +19,18 @@ export const ErrorCodes = {
19
19
  MISSING_USE_CLIENT: 'BF001',
20
20
  CLIENT_IMPORTING_SERVER: 'BF003',
21
21
 
22
- // Signal/Memo errors (BF011-BF019)
22
+ // Signal/Memo errors (BF011-BF019). BF012 is retired (see
23
+ // `invalid-signal-usage.audit.test.ts`) — BF013 is the next free slot.
23
24
  SIGNAL_OUTSIDE_COMPONENT: 'BF011',
25
+ // A reactive primitive (createSignal/createMemo/createEffect/onMount/
26
+ // onCleanup/createSearchParams) called through a namespace import
27
+ // (`import * as bf from '@barefootjs/client'`, `bf.createSignal(...)`)
28
+ // that the analyzer could not resolve — no shared `ts.Program` was
29
+ // supplied, so `resolvePrimitiveKind`'s checker-based slow path never
30
+ // ran. Same failure shape as BF011: the declaration is silently dropped
31
+ // from the compiled output and every reference to it throws
32
+ // ReferenceError at hydrate. #2771.
33
+ PRIMITIVE_VIA_NAMESPACE_IMPORT: 'BF013',
24
34
 
25
35
  // JSX errors (BF021-BF029). BF022 was retired (see
26
36
  // `invalid-jsx-attribute.audit.test.ts`) and BF026 is reserved by
@@ -150,6 +160,9 @@ const errorMessages: Record<ErrorCode, string> = {
150
160
  'Module-level reactive declaration (createSignal / createMemo) is not allowed. ' +
151
161
  'The downstream codegen drops the declaration silently and every reference becomes a ReferenceError at SSR and at hydrate. ' +
152
162
  'Move the declaration inside a component function so each mount gets its own state.',
163
+ [ErrorCodes.PRIMITIVE_VIA_NAMESPACE_IMPORT]:
164
+ "Reactive primitive called through a namespace import of '@barefootjs/client' (import * as ns) is not recognized; " +
165
+ 'the declaration is dropped and its references throw ReferenceError at hydrate. Import the primitive by name instead.',
153
166
 
154
167
  [ErrorCodes.UNSUPPORTED_JSX_PATTERN]: 'Unsupported JSX pattern',
155
168
  [ErrorCodes.MISSING_KEY_IN_LIST]:
package/src/index.ts CHANGED
@@ -15,6 +15,12 @@ export { extractSsrDefaults, deriveStashFromDefaults } from './ssr-defaults.ts'
15
15
  export { propsDestructureBinding, buildPropAliasMap, isIdentifierName } from './props-binding.ts'
16
16
  export type { SsrDefault } from './ssr-defaults.ts'
17
17
 
18
+ // Import-alias resolution (#2777 client-JS registry key; #2822 SSR cross-template
19
+ // call names) — a local alias -> declared/exported name map built from
20
+ // `ir.metadata.imports`. DSL adapters build this once per compile and look up
21
+ // `aliasMap.get(comp.name) ?? comp.name` at each cross-template-call-name site.
22
+ export { buildImportAliasMap } from './ir-to-client-js/component-scope.ts'
23
+
18
24
  // Backend-neutral SSR seed plan (in-template derived signal/memo seeding)
19
25
  export { computeSsrSeedPlan } from './ssr-seed-plan.ts'
20
26
  export type { SsrSeedPlan, SsrSeedStep } from './ssr-seed-plan.ts'
@@ -63,6 +69,7 @@ export type {
63
69
  ConformancePin,
64
70
  ConformancePins,
65
71
  RenderDivergences,
72
+ ImportInfo,
66
73
  } from './types.ts'
67
74
 
68
75
  // Analyzer
@@ -302,6 +302,13 @@ export function buildReferencesGraph(ctx: ClientJsContext, irRoot: IRNode): Refe
302
302
  if (v && attr.value.kind !== 'literal') addExprEdges(ROOT_SOURCE, v, 'template-closure')
303
303
  }
304
304
  for (const ev of el.events) addExprEdges(ROOT_SOURCE, ev.handler, 'init-body')
305
+ // A `ref` callback runs in `initX`'s closure, same context as an event
306
+ // handler — not the template closure. Without this edge, a ref inside
307
+ // a loop nested past the top level (`elem.innerLoops`, never walked by
308
+ // Phase 1's dedicated ref traces) is invisible to reachability, so
309
+ // `computeDeclarationScopes` (compute-scope.ts) drops its declaration
310
+ // as dead code while the emitter still emits the call site (#2750).
311
+ if (el.ref) addExprEdges(ROOT_SOURCE, el.ref, 'init-body')
305
312
  descend()
306
313
  },
307
314
  component: ({ node: c, descend, descendJsxChildren }) => {
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { IRNode } from '../types.ts'
12
12
  import type { ClientJsContext, ConditionalElement } from './types.ts'
13
+ import { resolveImportedComponentName } from './component-scope.ts'
13
14
 
14
15
  /** Emit child-component import marker lines for every component name
15
16
  * reachable from the current component's IR (minus siblings). */
@@ -39,7 +40,13 @@ export function emitChildComponentImports(
39
40
  }
40
41
  for (const childName of childComponentNames) {
41
42
  if (!siblingSet.has(childName)) {
42
- lines.push(`import '/* @bf-child:${childName} */'`)
43
+ // #2777 — resolve an import alias (`import { Foo as Bar }`, `<Bar/>`)
44
+ // to the declared name (`Foo`) the child's own module registers
45
+ // under, so `packages/vite`'s child-name index (keyed by exported
46
+ // names) and `combine-client-js.ts`'s `hydrate('Name')` scan both
47
+ // find it. `siblingSet` is checked against the LOCAL name above —
48
+ // a same-file sibling is never reached through an import alias.
49
+ lines.push(`import '/* @bf-child:${resolveImportedComponentName(childName)} */'`)
43
50
  }
44
51
  }
45
52
  }