@barefootjs/jsx 0.31.3 → 0.31.5
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/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/loop-bound-names.d.ts +18 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/adapters/template-imports.d.ts +27 -15
- package/dist/adapters/template-imports.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/augment-inherited-props.d.ts +12 -2
- package/dist/augment-inherited-props.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/debug.d.ts.map +1 -1
- package/dist/free-refs.d.ts +11 -2
- package/dist/free-refs.d.ts.map +1 -1
- package/dist/identifier-pattern.d.ts +62 -0
- package/dist/identifier-pattern.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +968 -789
- package/dist/ir-to-client-js/collect-elements.d.ts +23 -2
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +12 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +20 -12
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/prop-handling.d.ts +25 -2
- package/dist/ir-to-client-js/prop-handling.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +46 -2
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +16 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +15 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/module-exports.d.ts +64 -0
- package/dist/module-exports.d.ts.map +1 -1
- package/dist/relocate.d.ts.map +1 -1
- package/dist/scope/binding-scope.d.ts +1 -1
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/binding-scope-ratchet.test.ts +146 -21
- package/src/__tests__/component-type-parameters.test.ts +70 -0
- package/src/__tests__/csr-materialize-loop-preamble-shadow.test.ts +85 -0
- package/src/__tests__/csr-substitute-enclosing-scope.test.ts +77 -0
- package/src/__tests__/free-refs.test.ts +1 -1
- package/src/__tests__/identifier-pattern.test.ts +170 -0
- package/src/__tests__/loop-child-reactive-attr-const-shadow.test.ts +107 -0
- package/src/__tests__/mutable-binding-writers.test.ts +133 -0
- package/src/__tests__/preamble-conditional-reactivity.test.ts +191 -0
- package/src/__tests__/rewrite-dynamic-imports.test.ts +98 -0
- package/src/__tests__/signal-setter-updater-type.test.ts +81 -0
- package/src/adapters/jsx-adapter.ts +31 -4
- package/src/adapters/loop-bound-names.ts +18 -0
- package/src/adapters/template-imports.ts +93 -0
- package/src/adapters/test-adapter.ts +4 -1
- package/src/augment-inherited-props.ts +13 -1
- package/src/compiler.ts +18 -0
- package/src/debug.ts +38 -24
- package/src/free-refs.ts +14 -5
- package/src/identifier-pattern.ts +79 -0
- package/src/index.ts +1 -1
- package/src/ir-to-client-js/collect-elements.ts +60 -16
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +2 -1
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +2 -1
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +12 -2
- package/src/ir-to-client-js/csr-substitute.ts +19 -2
- package/src/ir-to-client-js/html-template.ts +42 -31
- package/src/ir-to-client-js/imports.ts +2 -1
- package/src/ir-to-client-js/prop-handling.ts +28 -1
- package/src/ir-to-client-js/reactivity.ts +58 -6
- package/src/ir-to-client-js/rewrite-props-object.ts +2 -1
- package/src/ir-to-client-js/types.ts +16 -0
- package/src/ir-to-client-js/utils.ts +24 -8
- package/src/jsx-to-ir.ts +135 -10
- package/src/module-exports.ts +140 -2
- package/src/relocate.ts +2 -1
- package/src/scope/binding-scope.ts +1 -1
- package/src/types.ts +41 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit coverage for `rewriteDynamicImportsInSource` (#2588) — the source-text
|
|
3
|
+
* counterpart to `rewriteImportsForTemplate`.
|
|
4
|
+
*
|
|
5
|
+
* The e2e half lives in `packages/vite/src/__tests__/dynamic-import-rewrite.test.ts`
|
|
6
|
+
* (real `vite build`, real emitted template). This half pins the cases that
|
|
7
|
+
* an e2e fixture can't isolate: which AST nodes count, and the false matches
|
|
8
|
+
* a regex-based implementation would produce. Those false-match cases are
|
|
9
|
+
* the entire reason this parses (CLAUDE.md: never parse JS/TS with regex) —
|
|
10
|
+
* without them, a regex rewrite would pass every other assertion here.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, test, expect } from 'bun:test'
|
|
13
|
+
import { rewriteDynamicImportsInSource } from '../adapters/template-imports.ts'
|
|
14
|
+
|
|
15
|
+
/** Stand-in for `buildRelativeImportRewriter`: shifts one directory deeper. */
|
|
16
|
+
const deeper = (spec: string): string => `../${spec}`
|
|
17
|
+
|
|
18
|
+
describe('rewriteDynamicImportsInSource', () => {
|
|
19
|
+
test('rewrites a dynamic import call', () => {
|
|
20
|
+
expect(rewriteDynamicImportsInSource(`const m = import('./heavy')`, deeper))
|
|
21
|
+
.toBe(`const m = import('.././heavy')`)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('rewrites an import type node (`typeof import(...)`)', () => {
|
|
25
|
+
expect(rewriteDynamicImportsInSource(`let p: Promise<typeof import('../lib/x')> | null = null`, deeper))
|
|
26
|
+
.toBe(`let p: Promise<typeof import('../../lib/x')> | null = null`)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('rewrites a qualified import type (`import(...).Foo`)', () => {
|
|
30
|
+
expect(rewriteDynamicImportsInSource(`let v: import('../lib/x').Foo`, deeper))
|
|
31
|
+
.toBe(`let v: import('../../lib/x').Foo`)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('rewrites every occurrence, keeping earlier spans intact', () => {
|
|
35
|
+
const out = rewriteDynamicImportsInSource(
|
|
36
|
+
`const a = import('./one'); const b = import('./two'); const c = import('./three')`,
|
|
37
|
+
deeper,
|
|
38
|
+
)
|
|
39
|
+
expect(out).toBe(
|
|
40
|
+
`const a = import('.././one'); const b = import('.././two'); const c = import('.././three')`,
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('leaves bare specifiers alone', () => {
|
|
45
|
+
const src = `const m = import('hono/jsx')`
|
|
46
|
+
expect(rewriteDynamicImportsInSource(src, deeper)).toBe(src)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('leaves a non-literal specifier alone', () => {
|
|
50
|
+
const src = `const m = import(chunkPath)`
|
|
51
|
+
expect(rewriteDynamicImportsInSource(src, deeper)).toBe(src)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('leaves static import statements to rewriteImportsForTemplate', () => {
|
|
55
|
+
// The adapter rewrites those from the parsed `templateImports` list; if
|
|
56
|
+
// this touched them too they would be rewritten twice.
|
|
57
|
+
const src = `import { x } from './sibling'`
|
|
58
|
+
expect(rewriteDynamicImportsInSource(src, deeper)).toBe(src)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('does not touch `import(` inside a string literal', () => {
|
|
62
|
+
const src = `const code = "const m = import('./heavy')"`
|
|
63
|
+
expect(rewriteDynamicImportsInSource(src, deeper)).toBe(src)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('does not touch `import(` inside a template literal', () => {
|
|
67
|
+
const src = 'const code = `await import(\'./heavy\')`'
|
|
68
|
+
expect(rewriteDynamicImportsInSource(src, deeper)).toBe(src)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('does not touch `import(` inside a comment', () => {
|
|
72
|
+
const src = `// const m = import('./heavy')\nconst n = 1`
|
|
73
|
+
expect(rewriteDynamicImportsInSource(src, deeper)).toBe(src)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('rewrites inside a TSX component body without disturbing the JSX', () => {
|
|
77
|
+
const src = [
|
|
78
|
+
`export function Lazy() {`,
|
|
79
|
+
` const onClick = async () => { await import('./heavy') }`,
|
|
80
|
+
` return <button onClick={onClick} data-x="import('./nope')">go</button>`,
|
|
81
|
+
`}`,
|
|
82
|
+
].join('\n')
|
|
83
|
+
const out = rewriteDynamicImportsInSource(src, deeper)
|
|
84
|
+
expect(out).toContain(`await import('.././heavy')`)
|
|
85
|
+
// The attribute string is data, not a module reference.
|
|
86
|
+
expect(out).toContain(`data-x="import('./nope')"`)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('returns the input unchanged when the rewriter is a no-op', () => {
|
|
90
|
+
const src = `const m = import('./heavy')`
|
|
91
|
+
expect(rewriteDynamicImportsInSource(src, (s) => s)).toBe(src)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('returns the input unchanged when there is nothing to rewrite', () => {
|
|
95
|
+
const src = `export const answer = 42`
|
|
96
|
+
expect(rewriteDynamicImportsInSource(src, deeper)).toBe(src)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for #2573 (chart TS7006 family): the SSR no-op signal
|
|
3
|
+
* setter stub was declared as `(..._args: any[]) => {}`. Calling it with
|
|
4
|
+
* an updater function — `setBars((prev) => [...prev, bar])` — puts that
|
|
5
|
+
* arrow in a rest-`any[]` argument position, not a function-typed one, so
|
|
6
|
+
* TypeScript has no contextual signature to infer the arrow's own
|
|
7
|
+
* parameter from and flags it implicit-any (TS7006). Runtime output
|
|
8
|
+
* (client JS) was always correct; this is a type-level emission defect in
|
|
9
|
+
* the SSR template only.
|
|
10
|
+
*
|
|
11
|
+
* The real `createSignal<T>` setter accepts `T | ((prev: T) => T)`
|
|
12
|
+
* (`packages/client/src/reactive.ts`'s `Signal<T>`). The stub now mirrors
|
|
13
|
+
* that signature whenever the signal's type is known (`SignalInfo.type`,
|
|
14
|
+
* the same field `needsTypeAssertion` already reads for the getter), so
|
|
15
|
+
* the updater arrow's parameter infers from the real element type.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, test, expect } from 'bun:test'
|
|
19
|
+
import { compileJSX } from '../compiler'
|
|
20
|
+
import { HonoAdapter } from '../../../../packages/adapter-hono/src/adapter/hono-adapter'
|
|
21
|
+
|
|
22
|
+
describe('signal setter updater-function typing in emitted templates (#2573)', () => {
|
|
23
|
+
test('a typed signal gets an updater-aware setter stub', () => {
|
|
24
|
+
const honoAdapter = new HonoAdapter()
|
|
25
|
+
const source = `
|
|
26
|
+
'use client'
|
|
27
|
+
import { createSignal } from '@barefootjs/client'
|
|
28
|
+
|
|
29
|
+
interface Bar { id: string; height: number }
|
|
30
|
+
|
|
31
|
+
export function Chart() {
|
|
32
|
+
const [bars, setBars] = createSignal<Bar[]>([])
|
|
33
|
+
|
|
34
|
+
// Called directly from the returned JSX (not from an onXxx handler
|
|
35
|
+
// prop, which SSR stubs to a no-op) so it — and the setter call
|
|
36
|
+
// inside it — stays reachable into the emitted template.
|
|
37
|
+
const addBar = (bar: Bar) => {
|
|
38
|
+
setBars((prev) => [...prev, bar])
|
|
39
|
+
return bars().length
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return <div>{addBar({ id: 'a', height: 1 })}</div>
|
|
43
|
+
}
|
|
44
|
+
`
|
|
45
|
+
|
|
46
|
+
const result = compileJSX(source, 'Chart.tsx', { adapter: honoAdapter })
|
|
47
|
+
expect(result.errors).toHaveLength(0)
|
|
48
|
+
|
|
49
|
+
const template = result.files.find((f) => f.type === 'markedTemplate')!
|
|
50
|
+
expect(template).toBeDefined()
|
|
51
|
+
expect(template.content).toContain(
|
|
52
|
+
'const setBars: (valueOrFn: Bar[] | ((prev: Bar[]) => Bar[])) => void = () => {}',
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('a signal with no resolvable type keeps the untyped rest-args stub', () => {
|
|
57
|
+
const honoAdapter = new HonoAdapter()
|
|
58
|
+
const source = `
|
|
59
|
+
'use client'
|
|
60
|
+
import { createSignal } from '@barefootjs/client'
|
|
61
|
+
|
|
62
|
+
export function Widget(props: { initial: unknown }) {
|
|
63
|
+
const [value, setValue] = createSignal(props.initial)
|
|
64
|
+
|
|
65
|
+
const reset = () => {
|
|
66
|
+
setValue(props.initial)
|
|
67
|
+
return value()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return <div>{String(reset())}</div>
|
|
71
|
+
}
|
|
72
|
+
`
|
|
73
|
+
|
|
74
|
+
const result = compileJSX(source, 'Widget.tsx', { adapter: honoAdapter })
|
|
75
|
+
expect(result.errors).toHaveLength(0)
|
|
76
|
+
|
|
77
|
+
const template = result.files.find((f) => f.type === 'markedTemplate')!
|
|
78
|
+
expect(template).toBeDefined()
|
|
79
|
+
expect(template.content).toContain('const setValue = (..._args: any[]) => {}')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -17,8 +17,9 @@ import { BF_SCOPE, BF_SLOT, BF_COND } from '@barefootjs/shared'
|
|
|
17
17
|
import { BaseAdapter } from './interface.ts'
|
|
18
18
|
import type { CallbackBodyAcceptor } from './interface.ts'
|
|
19
19
|
import { ENV_SIGNAL_CLIENT_FACTORY } from './env-signal.ts'
|
|
20
|
-
import { formatParamWithType,
|
|
20
|
+
import { formatParamWithType, closeOverWritersOfMutableBindings } from '../module-exports.ts'
|
|
21
21
|
import { extractFreeIdentifiersFromText } from '../ir-to-client-js/csr-substitute.ts'
|
|
22
|
+
import { identifierPattern } from '../identifier-pattern.ts'
|
|
22
23
|
|
|
23
24
|
export interface JsxAdapterConfig {
|
|
24
25
|
/** Use typed versions (typedInitialValue, etc.) for type-safe .tsx output */
|
|
@@ -112,7 +113,15 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
112
113
|
]
|
|
113
114
|
|
|
114
115
|
// Find reachable declarations via transitive dependency analysis
|
|
115
|
-
const reachable =
|
|
116
|
+
const reachable = closeOverWritersOfMutableBindings(
|
|
117
|
+
primaryRefText,
|
|
118
|
+
declarations,
|
|
119
|
+
new Set(
|
|
120
|
+
ir.metadata.localConstants
|
|
121
|
+
.filter(c => (c.declarationKind ?? 'const') !== 'const')
|
|
122
|
+
.map(c => c.name),
|
|
123
|
+
),
|
|
124
|
+
)
|
|
116
125
|
|
|
117
126
|
// Also check which signal setters are referenced
|
|
118
127
|
const reachableBodies = [...reachable].map(name => {
|
|
@@ -165,9 +174,27 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
165
174
|
|
|
166
175
|
// Create a no-op setter for SSR — omit entirely if not referenced anywhere
|
|
167
176
|
if (signal.setter) {
|
|
168
|
-
const setterUsed =
|
|
177
|
+
const setterUsed = identifierPattern(signal.setter).test(setterRefText)
|
|
169
178
|
if (setterUsed) {
|
|
170
|
-
|
|
179
|
+
// The real `createSignal<T>` setter accepts `T | ((prev: T) =>
|
|
180
|
+
// T)` (`packages/client/src/reactive.ts`'s `Signal<T>`). The
|
|
181
|
+
// untyped `(..._args: any[]) => {}` stub gave every `setX((prev)
|
|
182
|
+
// => ...)` updater-function call site an `any[]`-typed argument
|
|
183
|
+
// position — not a function-typed one — so the arrow's own
|
|
184
|
+
// `prev` parameter had no contextual type to infer from and
|
|
185
|
+
// `tsc` flagged it implicit-any (TS7006, #2573 chart family).
|
|
186
|
+
// Mirror the real setter's signature whenever the signal's type
|
|
187
|
+
// is known, so updater-function callers keep their inference;
|
|
188
|
+
// fall back to the untyped stub only when it isn't (matches
|
|
189
|
+
// `needsTypeAssertion`'s `'unknown'` guard just above).
|
|
190
|
+
const setterType = preserveTypes && signal.type.kind !== 'unknown'
|
|
191
|
+
? `(valueOrFn: ${signal.type.raw} | ((prev: ${signal.type.raw}) => ${signal.type.raw})) => void`
|
|
192
|
+
: null
|
|
193
|
+
lines.push(
|
|
194
|
+
setterType
|
|
195
|
+
? ` const ${signal.setter}: ${setterType} = () => {}`
|
|
196
|
+
: ` const ${signal.setter} = (..._args: any[]) => {}`,
|
|
197
|
+
)
|
|
171
198
|
}
|
|
172
199
|
}
|
|
173
200
|
}
|
|
@@ -16,6 +16,24 @@
|
|
|
16
16
|
* degrades to the ALREADY-accepted residual (`+` falls back to numeric,
|
|
17
17
|
* same as before #2212 for an unresolvable operand) rather than ever
|
|
18
18
|
* producing silently-wrong output.
|
|
19
|
+
*
|
|
20
|
+
* #2482 (BindingScope) does NOT replace this: it answers "is NAME bound
|
|
21
|
+
* AT THIS POSITION", a live, position-accurate query built by walking
|
|
22
|
+
* INTO scopes during a render-time (or render-shaped) tree walk. This
|
|
23
|
+
* function answers a deliberately different, coarser question —
|
|
24
|
+
* "is NAME EVER a loop-bound name ANYWHERE in the component" — a single
|
|
25
|
+
* whole-component prepass run once at `generate()` entry, before any
|
|
26
|
+
* loop scope exists to thread. The two are complementary, not
|
|
27
|
+
* duplicative: swapping this for `BindingScope` would require re-deriving
|
|
28
|
+
* a position-accurate answer at every string-typed-operand call site,
|
|
29
|
+
* which is exactly the coarse-but-safe trade-off this function exists to
|
|
30
|
+
* avoid. Stays outside the `binding-scope-ratchet.test.ts` ledger for an
|
|
31
|
+
* incidental reason too — a lowercase-`l`-led spelling of this file's own
|
|
32
|
+
* export once lived as a per-adapter ref-counted-map field name (fully
|
|
33
|
+
* migrated away in an earlier #2482 stage) that the ledger's scan tracked;
|
|
34
|
+
* this function's own `collectLoopBoundNames` name is capitalized
|
|
35
|
+
* differently (a capital `L`) and so was never inside that scan's reach in
|
|
36
|
+
* the first place.
|
|
19
37
|
*/
|
|
20
38
|
|
|
21
39
|
import type { ComponentIR, IRNode } from '../types.ts'
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* Adapters are responsible for calling this themselves before emitting any
|
|
17
17
|
* import block. The compiler hands them `metadata.imports` unchanged.
|
|
18
18
|
*/
|
|
19
|
+
import ts from 'typescript'
|
|
19
20
|
import type { ImportInfo, ImportSpecifier } from '../types.ts'
|
|
20
21
|
|
|
21
22
|
const CLIENT_PACKAGE_SOURCES = new Set([
|
|
@@ -77,3 +78,95 @@ export function rewriteImportsForTemplate(
|
|
|
77
78
|
function specKey(s: ImportSpecifier): string {
|
|
78
79
|
return `${s.isDefault ? 'd' : ''}${s.isNamespace ? 'n' : ''}:${s.name}:${s.alias ?? ''}`
|
|
79
80
|
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Re-anchor relative specifiers carried inside emitted SOURCE TEXT — the
|
|
84
|
+
* counterpart to `rewriteImportsForTemplate`, which only sees the parsed
|
|
85
|
+
* static import list (`metadata.templateImports`).
|
|
86
|
+
*
|
|
87
|
+
* Declaration bodies re-emitted verbatim into a template
|
|
88
|
+
* (`generateModuleScopeDeclarations`' consts/functions, a component body's
|
|
89
|
+
* local handlers) can carry their own module references that never appear
|
|
90
|
+
* in that list:
|
|
91
|
+
*
|
|
92
|
+
* - `import('./x')` — a dynamic import expression
|
|
93
|
+
* - `typeof import('./x')` — an import TYPE node
|
|
94
|
+
*
|
|
95
|
+
* Those specifiers are written relative to the SOURCE file, so they break
|
|
96
|
+
* once the template is emitted to a directory at a different depth — the
|
|
97
|
+
* same depth shift `rewriteImportsForTemplate` already fixes for static
|
|
98
|
+
* imports (#1453, #2588).
|
|
99
|
+
*
|
|
100
|
+
* Only literal relative paths beginning with `.` are rewritten; bare
|
|
101
|
+
* specifiers pass through, matching `remap`'s guard above. A non-literal
|
|
102
|
+
* argument (`import(someVar)`) is left alone — there is no specifier to
|
|
103
|
+
* re-anchor, and guessing would be worse than leaving the source as-is.
|
|
104
|
+
*
|
|
105
|
+
* Parsed with the TS AST and applied by span splicing rather than by
|
|
106
|
+
* matching text: a regex would false-match `import(` inside a string or a
|
|
107
|
+
* comment, which is exactly the class of bug the repo-wide "never parse JS
|
|
108
|
+
* with regex" rule exists to prevent. Splices are applied back-to-front so
|
|
109
|
+
* earlier spans keep their offsets.
|
|
110
|
+
*/
|
|
111
|
+
export function rewriteDynamicImportsInSource(
|
|
112
|
+
sourceText: string,
|
|
113
|
+
rewriteRelative: (importPath: string) => string,
|
|
114
|
+
): string {
|
|
115
|
+
// Cheap pre-check: skip the parse entirely for the overwhelmingly common
|
|
116
|
+
// case of text with no dynamic import at all. Substring presence is not
|
|
117
|
+
// used to LOCATE anything — the AST still does that — so a false positive
|
|
118
|
+
// here costs one wasted parse and a false negative is impossible.
|
|
119
|
+
if (!sourceText.includes('import')) return sourceText
|
|
120
|
+
|
|
121
|
+
const sf = ts.createSourceFile(
|
|
122
|
+
'bf-template-fragment.tsx',
|
|
123
|
+
sourceText,
|
|
124
|
+
ts.ScriptTarget.Latest,
|
|
125
|
+
/* setParentNodes */ false,
|
|
126
|
+
ts.ScriptKind.TSX,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const edits: Array<{ start: number, end: number, text: string }> = []
|
|
130
|
+
|
|
131
|
+
const visit = (node: ts.Node): void => {
|
|
132
|
+
// `import('./x')` — the argument is the first (and only) call argument.
|
|
133
|
+
if (
|
|
134
|
+
ts.isCallExpression(node) &&
|
|
135
|
+
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
|
136
|
+
node.arguments.length > 0 &&
|
|
137
|
+
ts.isStringLiteralLike(node.arguments[0])
|
|
138
|
+
) {
|
|
139
|
+
collect(node.arguments[0] as ts.StringLiteralLike)
|
|
140
|
+
}
|
|
141
|
+
// `typeof import('./x')` / `import('./x').Foo` — a TYPE-position node
|
|
142
|
+
// whose argument is a literal type wrapping the string.
|
|
143
|
+
if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
|
144
|
+
const literal = node.argument.literal
|
|
145
|
+
if (ts.isStringLiteralLike(literal)) collect(literal)
|
|
146
|
+
}
|
|
147
|
+
ts.forEachChild(node, visit)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const collect = (literal: ts.StringLiteralLike): void => {
|
|
151
|
+
const specifier = literal.text
|
|
152
|
+
if (!specifier.startsWith('.')) return
|
|
153
|
+
const next = rewriteRelative(specifier)
|
|
154
|
+
if (next === specifier) return
|
|
155
|
+
edits.push({
|
|
156
|
+
start: literal.getStart(sf),
|
|
157
|
+
end: literal.getEnd(),
|
|
158
|
+
// Re-quote rather than reusing the original delimiters: a rewritten
|
|
159
|
+
// POSIX-relative path never contains a quote to escape.
|
|
160
|
+
text: `'${next}'`,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
ts.forEachChild(sf, visit)
|
|
165
|
+
if (edits.length === 0) return sourceText
|
|
166
|
+
|
|
167
|
+
let out = sourceText
|
|
168
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
169
|
+
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end)
|
|
170
|
+
}
|
|
171
|
+
return out
|
|
172
|
+
}
|
|
@@ -164,7 +164,10 @@ export class TestAdapter extends JsxAdapter {
|
|
|
164
164
|
// Module-export keyword belongs to the adapter: it knows the target language
|
|
165
165
|
// and whether the source declared the component as exported.
|
|
166
166
|
const exportPrefix = ir.metadata.isExported === false ? '' : 'export '
|
|
167
|
-
|
|
167
|
+
// Carry the source component's own generic type parameters, if any
|
|
168
|
+
// (mirrors `HonoAdapter` — see `IRMetadata.typeParameters`'s docstring).
|
|
169
|
+
const typeParameters = ir.metadata.typeParameters ?? ''
|
|
170
|
+
lines.push(`${exportPrefix}function ${name}${typeParameters}(${fullPropsDestructure}${typeAnnotation}${noArgDefault}) {`)
|
|
168
171
|
|
|
169
172
|
// Generate scope ID
|
|
170
173
|
if (hasClientInteractivity) {
|
|
@@ -464,14 +464,26 @@ export function collectModuleStringConsts(
|
|
|
464
464
|
* compile-time lookup (the icon registry's `strokePaths['chevron-down']`,
|
|
465
465
|
* pagination's `variantClasses.ghost`; #1896 / #1897). Returns the
|
|
466
466
|
* looked-up scalar, or `null` for any other shape so callers fall back
|
|
467
|
-
* to their generic lowering. Shared by all
|
|
467
|
+
* to their generic lowering. Shared by all seven template-string adapters;
|
|
468
468
|
* the prop-KEYED variant of the pattern lives in `parseRecordIndexAccess`.
|
|
469
|
+
*
|
|
470
|
+
* `isShadowed` is REQUIRED (#2482 Stage 2) rather than left to caller
|
|
471
|
+
* discipline: an enclosing loop callback's own param/index/destructure/
|
|
472
|
+
* preamble binding of the same name as `objectName` (`.map((cfg) =>
|
|
473
|
+
* <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`) must resolve to
|
|
474
|
+
* the ROW value, not the outer const's member — every call site used to
|
|
475
|
+
* check this itself, against its own ad-hoc per-adapter shadow-name
|
|
476
|
+
* bookkeeping, with no static guarantee a new caller wouldn't forget it.
|
|
477
|
+
* Pass `scope.isBound` (or `scope.asShadowPredicate()`) from the caller's
|
|
478
|
+
* threaded `BindingScope`.
|
|
469
479
|
*/
|
|
470
480
|
export function lookupStaticRecordLiteral(
|
|
471
481
|
objectName: string,
|
|
472
482
|
key: string,
|
|
473
483
|
constants: IRMetadata['localConstants'] | undefined,
|
|
484
|
+
isShadowed: (name: string) => boolean,
|
|
474
485
|
): { kind: 'string' | 'number'; text: string } | null {
|
|
486
|
+
if (isShadowed(objectName)) return null
|
|
475
487
|
const constInfo = (constants ?? []).find(c => c.name === objectName && c.isModule)
|
|
476
488
|
if (constInfo?.value === undefined) return null
|
|
477
489
|
const sf = ts.createSourceFile(
|
package/src/compiler.ts
CHANGED
|
@@ -624,6 +624,23 @@ function compileMultipleComponents(
|
|
|
624
624
|
// Helpers
|
|
625
625
|
// =============================================================================
|
|
626
626
|
|
|
627
|
+
/**
|
|
628
|
+
* Verbatim text of the component function's own generic type parameter
|
|
629
|
+
* list (`<NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase
|
|
630
|
+
* = EdgeBase>`), or `null` when the component isn't generic. Source text
|
|
631
|
+
* per parameter (`node.getText(sourceFile)`), not a re-printed AST, so
|
|
632
|
+
* constraints/defaults/comments round-trip exactly like `ConstantInfo.
|
|
633
|
+
* typeAnnotation` does for `let` (#2589) — see `IRMetadata.typeParameters`.
|
|
634
|
+
*/
|
|
635
|
+
function componentTypeParametersText(
|
|
636
|
+
componentNode: ts.FunctionDeclaration | ts.ArrowFunction | null,
|
|
637
|
+
sourceFile: ts.SourceFile,
|
|
638
|
+
): string | null {
|
|
639
|
+
const typeParameters = componentNode?.typeParameters
|
|
640
|
+
if (!typeParameters || typeParameters.length === 0) return null
|
|
641
|
+
return `<${typeParameters.map(p => p.getText(sourceFile)).join(', ')}>`
|
|
642
|
+
}
|
|
643
|
+
|
|
627
644
|
export function buildMetadata(
|
|
628
645
|
ctx: ReturnType<typeof analyzeComponent>,
|
|
629
646
|
): IRMetadata {
|
|
@@ -634,6 +651,7 @@ export function buildMetadata(
|
|
|
634
651
|
isClientComponent: ctx.hasUseClientDirective,
|
|
635
652
|
typeDefinitions: ctx.typeDefinitions,
|
|
636
653
|
propsType: ctx.propsType,
|
|
654
|
+
typeParameters: componentTypeParametersText(ctx.componentNode, ctx.sourceFile),
|
|
637
655
|
propsParams: ctx.propsParams,
|
|
638
656
|
propsObjectName: ctx.propsObjectName,
|
|
639
657
|
restPropsName: ctx.restPropsName,
|
package/src/debug.ts
CHANGED
|
@@ -29,6 +29,8 @@ import { analyzeClientNeeds } from './ir-to-client-js/index.ts'
|
|
|
29
29
|
import type { WrapReason } from './ir-to-client-js/reactivity.ts'
|
|
30
30
|
import { decideWrapFromAstFlags } from './ir-to-client-js/reactivity.ts'
|
|
31
31
|
import { tokenContainsIdent } from './ir-to-client-js/utils.ts'
|
|
32
|
+
import { identifierCallPattern } from './identifier-pattern.ts'
|
|
33
|
+
import { BindingScope } from './scope/binding-scope.ts'
|
|
32
34
|
|
|
33
35
|
// =============================================================================
|
|
34
36
|
// Types
|
|
@@ -359,7 +361,7 @@ export function buildGraphFromIR(ir: ComponentIR): ComponentGraph {
|
|
|
359
361
|
|
|
360
362
|
// Collect DOM bindings from IR tree
|
|
361
363
|
const domBindings: DomBinding[] = []
|
|
362
|
-
collectDomBindings(ir.root, domBindings, signalGetters, memoNames, undefined,
|
|
364
|
+
collectDomBindings(ir.root, domBindings, signalGetters, memoNames, undefined, BindingScope.EMPTY, exprReadsProp)
|
|
363
365
|
|
|
364
366
|
// Build consumer lists for signals
|
|
365
367
|
const signalConsumers = new Map<string, string[]>()
|
|
@@ -1675,30 +1677,41 @@ function collectDomBindings(
|
|
|
1675
1677
|
signalGetters: Set<string>,
|
|
1676
1678
|
memoNames: Set<string>,
|
|
1677
1679
|
parentTag?: string,
|
|
1678
|
-
// Loop-param names in scope (#1690, #1795 Phase 2
|
|
1680
|
+
// Loop-param names in scope (#1690, #1795 Phase 2; threaded via the shared
|
|
1681
|
+
// `BindingScope` service since #2482 Stage 4). Inside a `map(it => …)`
|
|
1679
1682
|
// body the emitter rewrites every `it.x` read into a reactive accessor and
|
|
1680
1683
|
// wraps the binding in `createEffect`, yet `it` is neither a signal nor a
|
|
1681
1684
|
// memo — so without this context loop-child text / attribute bindings are
|
|
1682
1685
|
// invisible to the graph. When a binding expression references one of these
|
|
1683
1686
|
// names it is treated as reactive (matching the emitter's gate), giving the
|
|
1684
1687
|
// profiler a `domBinding` (slotId + loc) to resolve `<Comp>#binding:<slotId>`.
|
|
1685
|
-
|
|
1688
|
+
// This is the REACTIVITY / SLOT-ID CLASSIFIER consumer class (see
|
|
1689
|
+
// `BindingScope.valueBoundNames`'s docstring) — reads `valueBoundNames()`,
|
|
1690
|
+
// not `boundNames()`.
|
|
1691
|
+
scope: BindingScope = BindingScope.EMPTY,
|
|
1686
1692
|
// Predicate: does an attribute expression read a component prop? Mirrors the
|
|
1687
1693
|
// emitter's `needsEffectWrapper` prop detection so a prop-driven attribute
|
|
1688
1694
|
// (wrapped in `createEffect` at codegen, hence emitting `#binding:<slot>`) is
|
|
1689
1695
|
// tracked here too — otherwise its profiler id resolves to `(unresolved)`.
|
|
1690
1696
|
readsProp: (expr: string, freeIds?: ReadonlySet<string>) => boolean = () => false,
|
|
1691
1697
|
): void {
|
|
1698
|
+
const boundNames = scope.valueBoundNames()
|
|
1692
1699
|
// Does a loop-child binding read a loop param (or index)? Use the analyzer's
|
|
1693
1700
|
// lexer-resolved metadata, NOT a raw-string regex — so a param name that only
|
|
1694
1701
|
// appears inside a string literal (index `i` vs `'i'`) is not mistaken for a
|
|
1695
1702
|
// reactive read. Text expressions carry `origin.freeRefs` (a `render-item`
|
|
1696
1703
|
// kind == map-callback param); attributes carry `freeIdentifiers` (bare
|
|
1697
1704
|
// identifier set). This matches the emitter's actual loop-param gate.
|
|
1705
|
+
// Set-intersection test without spreading into an array — these run per
|
|
1706
|
+
// node in the graph walk, so avoid the per-call allocation.
|
|
1707
|
+
const setSomeIn = (names: ReadonlySet<string>, other: ReadonlySet<string>): boolean => {
|
|
1708
|
+
for (const n of names) if (other.has(n)) return true
|
|
1709
|
+
return false
|
|
1710
|
+
}
|
|
1698
1711
|
const exprReadsLoopParam = (n: IRExpression): boolean =>
|
|
1699
|
-
|
|
1712
|
+
boundNames.size > 0 && (n.origin?.freeRefs?.some(r => boundNames.has(r.name)) ?? false)
|
|
1700
1713
|
const attrReadsLoopParam = (free: ReadonlySet<string> | undefined): boolean =>
|
|
1701
|
-
|
|
1714
|
+
boundNames.size > 0 && free !== undefined && setSomeIn(boundNames, free)
|
|
1702
1715
|
switch (node.type) {
|
|
1703
1716
|
case 'element': {
|
|
1704
1717
|
// Dynamic attribute bindings (style, class, aria-*, data-*, etc.)
|
|
@@ -1712,7 +1725,7 @@ function collectDomBindings(
|
|
|
1712
1725
|
// `key` is consumed by the loop's keyFn, never emitted as an attribute
|
|
1713
1726
|
// effect — skip it inside loops so a `key={it.id}` read isn't mistaken
|
|
1714
1727
|
// for a reactive binding (matches `collectLoopChildBindings`).
|
|
1715
|
-
if (attr.name === 'key' &&
|
|
1728
|
+
if (attr.name === 'key' && boundNames.size > 0) continue
|
|
1716
1729
|
const expr = attrValueToString(attr.value)
|
|
1717
1730
|
if (!expr) continue
|
|
1718
1731
|
const deps = extractReactiveDeps(expr, signalGetters, memoNames)
|
|
@@ -1755,7 +1768,7 @@ function collectDomBindings(
|
|
|
1755
1768
|
}
|
|
1756
1769
|
// Recurse — pass element tag as parent context for text bindings
|
|
1757
1770
|
for (const child of node.children) {
|
|
1758
|
-
collectDomBindings(child, bindings, signalGetters, memoNames, node.tag,
|
|
1771
|
+
collectDomBindings(child, bindings, signalGetters, memoNames, node.tag, scope, readsProp)
|
|
1759
1772
|
}
|
|
1760
1773
|
break
|
|
1761
1774
|
}
|
|
@@ -1793,7 +1806,7 @@ function collectDomBindings(
|
|
|
1793
1806
|
// (the emitter wraps its `insert()` in a per-item effect) even though the
|
|
1794
1807
|
// param is neither signal nor memo. Use the resolved `origin.freeRefs`.
|
|
1795
1808
|
const loopReactive =
|
|
1796
|
-
|
|
1809
|
+
boundNames.size > 0 && (node.origin?.freeRefs?.some(r => boundNames.has(r.name)) ?? false)
|
|
1797
1810
|
if ((decision.wrap || loopReactive) && node.slotId) {
|
|
1798
1811
|
const deps = extractReactiveDeps(node.condition, signalGetters, memoNames)
|
|
1799
1812
|
bindings.push({
|
|
@@ -1812,8 +1825,8 @@ function collectDomBindings(
|
|
|
1812
1825
|
jsxPreview: `{${truncateExpr(node.condition)} ? ... : ...}`,
|
|
1813
1826
|
})
|
|
1814
1827
|
}
|
|
1815
|
-
collectDomBindings(node.whenTrue, bindings, signalGetters, memoNames, parentTag,
|
|
1816
|
-
collectDomBindings(node.whenFalse, bindings, signalGetters, memoNames, parentTag,
|
|
1828
|
+
collectDomBindings(node.whenTrue, bindings, signalGetters, memoNames, parentTag, scope, readsProp)
|
|
1829
|
+
collectDomBindings(node.whenFalse, bindings, signalGetters, memoNames, parentTag, scope, readsProp)
|
|
1817
1830
|
break
|
|
1818
1831
|
}
|
|
1819
1832
|
case 'loop': {
|
|
@@ -1827,9 +1840,9 @@ function collectDomBindings(
|
|
|
1827
1840
|
// An inner loop whose array reads an outer loop param (`r.tags.map(...)`)
|
|
1828
1841
|
// is reactive per item — use the resolved `arrayFreeIdentifiers`.
|
|
1829
1842
|
const loopReactive =
|
|
1830
|
-
|
|
1843
|
+
boundNames.size > 0 &&
|
|
1831
1844
|
node.arrayFreeIdentifiers !== undefined &&
|
|
1832
|
-
|
|
1845
|
+
setSomeIn(boundNames, node.arrayFreeIdentifiers)
|
|
1833
1846
|
const isReactive = deps.length > 0 || node.callsReactiveGetters === true || loopReactive
|
|
1834
1847
|
const isFallback = !isReactive && node.hasFunctionCalls === true
|
|
1835
1848
|
if (isReactive || isFallback) {
|
|
@@ -1858,12 +1871,13 @@ function collectDomBindings(
|
|
|
1858
1871
|
})
|
|
1859
1872
|
}
|
|
1860
1873
|
}
|
|
1861
|
-
// Loop-param names enter scope for the children (#1690, #1795 Phase 2)
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1874
|
+
// Loop-param names enter scope for the children (#1690, #1795 Phase 2),
|
|
1875
|
+
// via the same `BindingScope.enterLoopRow` the emitter uses — `IRLoop`
|
|
1876
|
+
// already satisfies `LoopBindingSource` structurally (`param`/`index`/
|
|
1877
|
+
// `paramBindings`/`preamble`), so no bespoke Set bookkeeping is needed.
|
|
1878
|
+
const childScope = scope.enterLoopRow(node)
|
|
1865
1879
|
for (const child of node.children) {
|
|
1866
|
-
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag,
|
|
1880
|
+
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag, childScope, readsProp)
|
|
1867
1881
|
}
|
|
1868
1882
|
break
|
|
1869
1883
|
}
|
|
@@ -1902,21 +1916,21 @@ function collectDomBindings(
|
|
|
1902
1916
|
}
|
|
1903
1917
|
}
|
|
1904
1918
|
for (const child of node.children) {
|
|
1905
|
-
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag,
|
|
1919
|
+
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag, scope, readsProp)
|
|
1906
1920
|
}
|
|
1907
1921
|
break
|
|
1908
1922
|
}
|
|
1909
1923
|
case 'fragment':
|
|
1910
1924
|
case 'provider': {
|
|
1911
1925
|
for (const child of node.children) {
|
|
1912
|
-
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag,
|
|
1926
|
+
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag, scope, readsProp)
|
|
1913
1927
|
}
|
|
1914
1928
|
break
|
|
1915
1929
|
}
|
|
1916
1930
|
case 'if-statement': {
|
|
1917
|
-
collectDomBindings(node.consequent, bindings, signalGetters, memoNames, parentTag,
|
|
1931
|
+
collectDomBindings(node.consequent, bindings, signalGetters, memoNames, parentTag, scope, readsProp)
|
|
1918
1932
|
if (node.alternate) {
|
|
1919
|
-
collectDomBindings(node.alternate, bindings, signalGetters, memoNames, parentTag,
|
|
1933
|
+
collectDomBindings(node.alternate, bindings, signalGetters, memoNames, parentTag, scope, readsProp)
|
|
1920
1934
|
}
|
|
1921
1935
|
break
|
|
1922
1936
|
}
|
|
@@ -1990,12 +2004,12 @@ function attrValueToString(value: AttrValue): string | null {
|
|
|
1990
2004
|
function extractReactiveDeps(expr: string, signalGetters: Set<string>, memoNames: Set<string>): string[] {
|
|
1991
2005
|
const deps: string[] = []
|
|
1992
2006
|
for (const getter of signalGetters) {
|
|
1993
|
-
if (
|
|
2007
|
+
if (identifierCallPattern(getter).test(expr)) {
|
|
1994
2008
|
deps.push(getter)
|
|
1995
2009
|
}
|
|
1996
2010
|
}
|
|
1997
2011
|
for (const memo of memoNames) {
|
|
1998
|
-
if (
|
|
2012
|
+
if (identifierCallPattern(memo).test(expr)) {
|
|
1999
2013
|
deps.push(memo)
|
|
2000
2014
|
}
|
|
2001
2015
|
}
|
|
@@ -2012,7 +2026,7 @@ function extractSetterRefs(expr: string, signalGetters: Set<string>): string[] {
|
|
|
2012
2026
|
}
|
|
2013
2027
|
// Also detect signal getter reads in handler
|
|
2014
2028
|
for (const getter of signalGetters) {
|
|
2015
|
-
if (
|
|
2029
|
+
if (identifierCallPattern(getter).test(expr)) {
|
|
2016
2030
|
refs.push(getter)
|
|
2017
2031
|
}
|
|
2018
2032
|
}
|
package/src/free-refs.ts
CHANGED
|
@@ -41,14 +41,23 @@ export interface BindingEnvironment {
|
|
|
41
41
|
localFunctions: readonly FunctionInfo[]
|
|
42
42
|
imports: readonly ImportInfo[]
|
|
43
43
|
ambientGlobals: ReadonlySet<string>
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Active `.map()` callback VALUE-bound names (item/index/destructure —
|
|
46
|
+
* excludes preamble locals and callback params) — present inside loop
|
|
47
|
+
* bodies. Named after, and fed from, `BindingScope.valueBoundNames()`
|
|
48
|
+
* (#2482 Stage 4 rename from `loopParams`, which predated the shared
|
|
49
|
+
* `BindingScope` service and named the mechanism, not the query). This
|
|
50
|
+
* is the REACTIVITY / SLOT-ID CLASSIFIER consumer class — see that
|
|
51
|
+
* method's docstring for why it must stay `valueBoundNames`, not
|
|
52
|
+
* `boundNames`.
|
|
53
|
+
*/
|
|
54
|
+
loopValueBoundNames?: ReadonlySet<string>
|
|
46
55
|
checker: ts.TypeChecker | null
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
/**
|
|
50
59
|
* Per-environment cache for the binding map. `BindingEnvironment` identity
|
|
51
|
-
* is stable per (analyzer,
|
|
60
|
+
* is stable per (analyzer, loopValueBoundNames snapshot) — `jsx-to-ir.ts` memoizes
|
|
52
61
|
* `makeBindingEnv` so the same object is reused across every
|
|
53
62
|
* `resolveFreeRefs` call within a loop scope. With N expressions per
|
|
54
63
|
* component and M bindings per env, this drops binding-table construction
|
|
@@ -114,8 +123,8 @@ function buildBindingMap(env: BindingEnvironment): Map<string, BindingKind> {
|
|
|
114
123
|
map.set(m.name, 'memo-getter')
|
|
115
124
|
}
|
|
116
125
|
// Highest precedence — innermost scope.
|
|
117
|
-
if (env.
|
|
118
|
-
for (const name of env.
|
|
126
|
+
if (env.loopValueBoundNames) {
|
|
127
|
+
for (const name of env.loopValueBoundNames) map.set(name, 'render-item')
|
|
119
128
|
}
|
|
120
129
|
|
|
121
130
|
_bindingMapCache.set(env, map)
|