@barefootjs/jsx 0.33.4 → 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.
- package/dist/adapters/child-scope.d.ts +13 -0
- package/dist/adapters/child-scope.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1298 -1106
- package/dist/ir-to-client-js/child-components.d.ts.map +1 -1
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/component-scope.d.ts +20 -0
- package/dist/ir-to-client-js/component-scope.d.ts.map +1 -1
- package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +4 -3
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +35 -9
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +10 -0
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +49 -2
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/rewrite-props-object.d.ts +12 -1
- package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +5 -2
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +49 -11
- package/dist/props-binding.d.ts.map +1 -1
- package/dist/types.d.ts +6 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +15 -15
- package/src/__tests__/aliased-import-child-registry-2777.test.ts +112 -0
- package/src/__tests__/date-lowering.test.ts +18 -4
- package/src/__tests__/inner-loop-mathml-namespace.test.ts +1 -1
- package/src/__tests__/inner-loop-svg-namespace.test.ts +4 -4
- package/src/__tests__/issue-2724-prop-alias-loop-not-static.test.ts +355 -0
- package/src/__tests__/issue-2737-props-object-alias-template.test.ts +86 -0
- package/src/__tests__/issue-2756-input-skeleton-bails.test.ts +123 -0
- package/src/__tests__/issue-2778-getter-alias-csr-template.test.ts +113 -0
- package/src/__tests__/issue-2806-rest-bag-template-rewrite.test.ts +136 -0
- package/src/__tests__/issue-2835-props-type-member-name-collision.test.ts +259 -0
- package/src/__tests__/loop-item-root-scope.test.ts +7 -3
- package/src/__tests__/markup-prop-brand.test.ts +165 -26
- package/src/__tests__/namespace-import-primitive.test.ts +244 -0
- package/src/__tests__/rewrite-destructured-props.test.ts +53 -0
- package/src/__tests__/static-loop-csr-materialize.test.ts +2 -2
- package/src/adapters/child-scope.ts +23 -0
- package/src/analyzer.ts +130 -0
- package/src/errors.ts +14 -1
- package/src/index.ts +7 -0
- package/src/ir-to-client-js/child-components.ts +8 -1
- package/src/ir-to-client-js/collect-elements.ts +10 -38
- package/src/ir-to-client-js/component-scope.ts +60 -0
- package/src/ir-to-client-js/compute-inlinability.ts +46 -11
- package/src/ir-to-client-js/control-flow/shared.ts +12 -5
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +8 -2
- package/src/ir-to-client-js/csr-substitute.ts +82 -16
- package/src/ir-to-client-js/emit-reactive.ts +29 -13
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +141 -70
- package/src/ir-to-client-js/imports.ts +4 -0
- package/src/ir-to-client-js/index.ts +34 -23
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +7 -2
- package/src/ir-to-client-js/rewrite-props-object.ts +14 -0
- package/src/jsx-to-ir.ts +200 -42
- package/src/prop-rewrite.ts +23 -5
- package/src/props-binding.ts +74 -18
- package/src/types.ts +6 -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
|
+
})
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -6,7 +6,7 @@ import { type IRNode, type IRElement, type IRComponent, type IRLoop, type IRProp
|
|
|
6
6
|
import type { ClientJsContext, ConditionalBranchChildComponent, ConditionalBranchReactiveAttr, BranchLoop, ConditionalBranchTextEffect, ConditionalElement, LoopChildBindings, LoopChildBranchSummary, LoopChildConditional, LoopOffset, NestedLoop } from './types.ts'
|
|
7
7
|
import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM } from './utils.ts'
|
|
8
8
|
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings, buildLoopRowScope, anyNameIn } from './reactivity.ts'
|
|
9
|
-
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, renderFlatMapClientBody, renderFlatMapProjectionClientBody, flatMapCallbackHasKeyedLeaf, type SkeletonSlotPaths } from './html-template.ts'
|
|
9
|
+
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, jsxChildrenPropGetterExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, renderFlatMapClientBody, renderFlatMapProjectionClientBody, flatMapCallbackHasKeyedLeaf, type SkeletonSlotPaths } from './html-template.ts'
|
|
10
10
|
import { detectRootNamespaceWrapTag } from './control-flow/stringify/template-parse.ts'
|
|
11
11
|
import { expandDynamicPropValue, expandConstantForReactivity, resolveRestSpreadOrigin, resolveRestSpreadNames } from './prop-handling.ts'
|
|
12
12
|
import { extractFreeIdentifiersFromText } from './csr-substitute.ts'
|
|
@@ -475,29 +475,6 @@ function jsxChildrenContainComponent(nodes: IRNode[]): boolean {
|
|
|
475
475
|
return false
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
-
/**
|
|
479
|
-
* Narrow gate for branding a `jsx-children` getter's value with `bfMarkup()`
|
|
480
|
-
* (#2651). The constructor (`jsx-to-ir.ts`'s `processComponentProps`) always
|
|
481
|
-
* stores exactly one node here (`AttrValueOf.jsxChildren([...])`), so
|
|
482
|
-
* `nodes[0]` is the whole payload; `irChildrenToJsExpr` (`html-template.ts`)
|
|
483
|
-
* turns a single `'element'` node into ONE HTML-string template literal —
|
|
484
|
-
* the same shape the `renderChild` / `irToComponentTemplateWithOpts` /
|
|
485
|
-
* `generateCsrTemplateWithOpts` "jsx-children" doors always produce (they
|
|
486
|
-
* join every child into one string regardless of shape), and the shape
|
|
487
|
-
* this fixture's `header={<strong>Title</strong>}` exercises.
|
|
488
|
-
*
|
|
489
|
-
* Deliberately narrow — NOT a general "does this reduce to one string"
|
|
490
|
-
* check: a `'fragment'` node (`header={<>text<strong/></>}`, multiple
|
|
491
|
-
* children) or a `'conditional'` node reduces through `irChildrenToJsExpr`
|
|
492
|
-
* to an array literal or a nested ternary of un-escaped-vs-escaped parts,
|
|
493
|
-
* for which `escapeTextOrNode`/`escapeTextOrMarkup` have no (array) or an
|
|
494
|
-
* unproven (ternary) contract. Those shapes are left unbranded — see
|
|
495
|
-
* #2651's door inventory — rather than guessed at here.
|
|
496
|
-
*/
|
|
497
|
-
function isSingleElementJsxChildren(nodes: IRNode[]): boolean {
|
|
498
|
-
return nodes.length === 1 && nodes[0].type === 'element'
|
|
499
|
-
}
|
|
500
|
-
|
|
501
478
|
/** Build propsExpr for a child component from its IR props. */
|
|
502
479
|
function buildComponentPropsExpr(props: IRProp[], ctx: ClientJsContext): string {
|
|
503
480
|
const knownSpreadProp = props.find(p => {
|
|
@@ -526,19 +503,13 @@ function buildComponentPropsExpr(props: IRProp[], ctx: ClientJsContext): string
|
|
|
526
503
|
}
|
|
527
504
|
switch (prop.value.kind) {
|
|
528
505
|
case 'jsx-children': {
|
|
529
|
-
const jsxExpr = irChildrenToJsExpr(prop.value.children)
|
|
530
506
|
if (jsxChildrenContainComponent(prop.value.children)) {
|
|
507
|
+
const jsxExpr = irChildrenToJsExpr(prop.value.children)
|
|
531
508
|
propsForInit.push(`get ${quotePropName(prop.name)}() { return __slot(() => ${jsxExpr}) }`)
|
|
532
|
-
} else if (prop.name !== 'children' && isSingleElementJsxChildren(prop.value.children)) {
|
|
533
|
-
// Brand (#2651): `jsxExpr` is the single HTML-string template
|
|
534
|
-
// literal `irChildrenToJsExpr` builds for a lone 'element' node —
|
|
535
|
-
// see `isSingleElementJsxChildren`'s docstring for why only this
|
|
536
|
-
// shape is branded here. Excludes an explicit `children={<jsx/>}`
|
|
537
|
-
// prop (out of scope, unchanged) — the child's `{children}`
|
|
538
|
-
// interpolation is a bare passthrough with no unwrap call, so a
|
|
539
|
-
// branded object there would stringify to `[object Object]`.
|
|
540
|
-
propsForInit.push(`get ${quotePropName(prop.name)}() { return bfMarkup(${jsxExpr}) }`)
|
|
541
509
|
} else {
|
|
510
|
+
// Brand via the shared door (#2651/#2702) — see
|
|
511
|
+
// `jsxChildrenPropGetterExpr`'s docstring.
|
|
512
|
+
const jsxExpr = jsxChildrenPropGetterExpr(prop.value.children, prop.name)
|
|
542
513
|
propsForInit.push(`get ${quotePropName(prop.name)}() { return ${jsxExpr} }`)
|
|
543
514
|
}
|
|
544
515
|
break
|
|
@@ -772,10 +743,11 @@ export function collectElements(
|
|
|
772
743
|
// passed), matching the plain-element path's `staticItemTemplate`.
|
|
773
744
|
if (l.isStaticArray && l.children[0]) {
|
|
774
745
|
// `l.children[0]` is the loop's row-root component — already
|
|
775
|
-
// tagged `loopItemRoot` in the IR, so `
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
746
|
+
// tagged `loopItemRoot` in the IR, so `renderChildScopeArgs`
|
|
747
|
+
// passes its slotId with the `loopItemRoot` flag (#2833) instead
|
|
748
|
+
// of deriving its own scope from the parent slot. Each iteration
|
|
749
|
+
// still owns a distinct scope identified by `data-key`, mirroring
|
|
750
|
+
// the SSR template's renderChild emit.
|
|
779
751
|
staticItemTemplate = irToHtmlTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, undefined, undefined)
|
|
780
752
|
}
|
|
781
753
|
} else if (l.children[0] && !projectionInner) {
|