@barefootjs/jsx 0.31.2 → 0.31.4
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/template-imports.d.ts +27 -15
- package/dist/adapters/template-imports.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/debug.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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +280 -119
- 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/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 +30 -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/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/module-exports.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +1 -1
- package/dist/relocate.d.ts.map +1 -1
- package/dist/scope/binding-scope.d.ts +179 -0
- package/dist/scope/binding-scope.d.ts.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/binding-scope-preamble-shadowing.test.ts +115 -0
- package/src/__tests__/binding-scope-ratchet.test.ts +194 -0
- package/src/__tests__/binding-scope.test.ts +200 -0
- package/src/__tests__/csr-materialize-loop-preamble-shadow.test.ts +76 -0
- package/src/__tests__/csr-substitute-enclosing-scope.test.ts +77 -0
- package/src/__tests__/identifier-pattern.test.ts +170 -0
- package/src/__tests__/let-type-annotation.test.ts +208 -0
- package/src/__tests__/loop-child-reactive-attr-const-shadow.test.ts +107 -0
- package/src/__tests__/rewrite-dynamic-imports.test.ts +98 -0
- package/src/adapters/jsx-adapter.ts +34 -5
- package/src/adapters/template-imports.ts +93 -0
- package/src/analyzer.ts +20 -3
- package/src/debug.ts +4 -3
- package/src/identifier-pattern.ts +79 -0
- package/src/index.ts +5 -1
- package/src/ir-to-client-js/collect-elements.ts +44 -15
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +2 -1
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +2 -1
- package/src/ir-to-client-js/csr-substitute.ts +19 -2
- package/src/ir-to-client-js/html-template.ts +37 -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 +54 -4
- package/src/ir-to-client-js/rewrite-props-object.ts +2 -1
- package/src/ir-to-client-js/utils.ts +9 -8
- package/src/jsx-to-ir.ts +187 -73
- package/src/module-exports.ts +3 -2
- package/src/prop-rewrite.ts +1 -1
- package/src/relocate.ts +2 -1
- package/src/scope/binding-scope.ts +238 -0
- package/src/types.ts +8 -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
|
+
})
|
|
@@ -19,6 +19,7 @@ import type { CallbackBodyAcceptor } from './interface.ts'
|
|
|
19
19
|
import { ENV_SIGNAL_CLIENT_FACTORY } from './env-signal.ts'
|
|
20
20
|
import { formatParamWithType, findReachableNames } 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 */
|
|
@@ -165,7 +166,7 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
165
166
|
|
|
166
167
|
// Create a no-op setter for SSR — omit entirely if not referenced anywhere
|
|
167
168
|
if (signal.setter) {
|
|
168
|
-
const setterUsed =
|
|
169
|
+
const setterUsed = identifierPattern(signal.setter).test(setterRefText)
|
|
169
170
|
if (setterUsed) {
|
|
170
171
|
lines.push(` const ${signal.setter} = (..._args: any[]) => {}`)
|
|
171
172
|
}
|
|
@@ -197,7 +198,10 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
197
198
|
// No initializer (e.g. `let emblaApi: EmblaCarouselType | undefined`)
|
|
198
199
|
// — carry the declared type annotation through so `.tsx` output
|
|
199
200
|
// doesn't fall back to implicit `any` (TS7034/TS7005, #2573).
|
|
200
|
-
const typeAnnotation =
|
|
201
|
+
const typeAnnotation =
|
|
202
|
+
preserveTypes && (constant.typeAnnotation ?? constant.type)
|
|
203
|
+
? `: ${constant.typeAnnotation ?? constant.type?.raw}`
|
|
204
|
+
: ''
|
|
201
205
|
lines.push(` ${keyword} ${constant.name}${typeAnnotation}`)
|
|
202
206
|
continue
|
|
203
207
|
}
|
|
@@ -213,7 +217,17 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
213
217
|
const constValue = preserveTypes
|
|
214
218
|
? (constant.typedValue ?? constant.value)
|
|
215
219
|
: constant.value
|
|
216
|
-
|
|
220
|
+
// Preserve an explicit `let` type annotation from source (#2589) —
|
|
221
|
+
// without it, TS infers the initializer's (often narrower) type and
|
|
222
|
+
// later reassignments/reads fail under strict (TS7034/TS7005, and
|
|
223
|
+
// TS2339 via `never` narrowing). `const` is left alone: its type
|
|
224
|
+
// always infers correctly from the (immutable) initializer, so
|
|
225
|
+
// adding annotations there would only churn snapshots.
|
|
226
|
+
const letTypeAnnotation =
|
|
227
|
+
preserveTypes && keyword === 'let' && constant.typeAnnotation
|
|
228
|
+
? `: ${constant.typeAnnotation}`
|
|
229
|
+
: ''
|
|
230
|
+
lines.push(` ${keyword} ${constant.name}${letTypeAnnotation} = ${constValue}`)
|
|
217
231
|
}
|
|
218
232
|
|
|
219
233
|
// Include local functions — skip unreachable ones (only used in event
|
|
@@ -412,14 +426,29 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
412
426
|
const keyword = c.declarationKind ?? 'const'
|
|
413
427
|
const exportKw = c.isExported ? 'export ' : ''
|
|
414
428
|
if (!c.value) {
|
|
415
|
-
|
|
429
|
+
// No initializer (e.g. module-scope `let pending: number`) — carry
|
|
430
|
+
// the declared type annotation through, mirroring the function-scope
|
|
431
|
+
// fix above (#2573 / #2589).
|
|
432
|
+
const typeAnnotation =
|
|
433
|
+
preserveTypes && (c.typeAnnotation ?? c.type)
|
|
434
|
+
? `: ${c.typeAnnotation ?? c.type?.raw}`
|
|
435
|
+
: ''
|
|
436
|
+
entries.push({ line: c.loc.start.line, text: `${exportKw}${keyword} ${c.name}${typeAnnotation}` })
|
|
416
437
|
continue
|
|
417
438
|
}
|
|
418
439
|
const trimmed = c.value.trim()
|
|
419
440
|
if (/^new WeakMap\b/.test(trimmed)) continue
|
|
420
441
|
if (c.isExported && /^createContext\b/.test(trimmed)) continue
|
|
421
442
|
const value = preserveTypes ? (c.typedValue ?? c.value) : c.value
|
|
422
|
-
|
|
443
|
+
// Preserve an explicit module-scope `let` type annotation from source
|
|
444
|
+
// (#2589) — see the function-scope sibling above for rationale. `const`
|
|
445
|
+
// is left alone: its type always infers correctly from the (immutable)
|
|
446
|
+
// initializer.
|
|
447
|
+
const letTypeAnnotation =
|
|
448
|
+
preserveTypes && keyword === 'let' && c.typeAnnotation
|
|
449
|
+
? `: ${c.typeAnnotation}`
|
|
450
|
+
: ''
|
|
451
|
+
entries.push({ line: c.loc.start.line, text: `${exportKw}${keyword} ${c.name}${letTypeAnnotation} = ${value}` })
|
|
423
452
|
}
|
|
424
453
|
|
|
425
454
|
for (const f of ir.metadata.localFunctions) {
|
|
@@ -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
|
+
}
|
package/src/analyzer.ts
CHANGED
|
@@ -504,8 +504,18 @@ function visit(
|
|
|
504
504
|
collectAmbientGlobals(node, ctx)
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
-
// Module-level constants (outside component)
|
|
508
|
-
|
|
507
|
+
// Module-level constants (outside component). Ambient statements
|
|
508
|
+
// (`declare let X: T`) are type-only contracts with no runtime binding —
|
|
509
|
+
// collectAmbientGlobals above already tracks them for BF052, and
|
|
510
|
+
// re-emitting one as a runtime `let` would shadow the real global, so
|
|
511
|
+
// they must not reach collectConstant. (Previously excluded only by
|
|
512
|
+
// accident: this path required an initializer, which `declare`
|
|
513
|
+
// statements never have — the #2589 uninitialized-`let` fix removed
|
|
514
|
+
// that gate, so the exclusion is now explicit.)
|
|
515
|
+
const isDeclareStatement =
|
|
516
|
+
ts.isVariableStatement(node) &&
|
|
517
|
+
(node.modifiers?.some(m => m.kind === ts.SyntaxKind.DeclareKeyword) ?? false)
|
|
518
|
+
if (ts.isVariableStatement(node) && !ctx.componentNode && !isDeclareStatement) {
|
|
509
519
|
const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false
|
|
510
520
|
const isLet = (node.declarationList.flags & ts.NodeFlags.Let) !== 0
|
|
511
521
|
const isModuleClientDirective = hasLeadingClientDirectiveOnStatement(node, ctx.sourceFile)
|
|
@@ -525,9 +535,15 @@ function visit(
|
|
|
525
535
|
}
|
|
526
536
|
continue
|
|
527
537
|
}
|
|
538
|
+
// An initializer is required for `const` (TS grammar enforces this),
|
|
539
|
+
// but an uninitialized module-scope `let` (e.g. `let pending: number`)
|
|
540
|
+
// is legal source and must still be collected — mirroring the
|
|
541
|
+
// component-scope path below (line ~687), which has never gated on
|
|
542
|
+
// `decl.initializer` — so its declared type carries into the emitted
|
|
543
|
+
// template instead of the declaration vanishing outright (#2589).
|
|
528
544
|
if (
|
|
529
545
|
ts.isIdentifier(decl.name) &&
|
|
530
|
-
decl.initializer &&
|
|
546
|
+
(decl.initializer || isLet) &&
|
|
531
547
|
!isArrowComponentFunction(decl)
|
|
532
548
|
) {
|
|
533
549
|
collectConstant(decl, ctx, true, isLet ? 'let' : 'const', isExported)
|
|
@@ -3203,6 +3219,7 @@ function collectConstant(
|
|
|
3203
3219
|
value,
|
|
3204
3220
|
parsed,
|
|
3205
3221
|
typedValue: typedValue !== value ? typedValue : undefined,
|
|
3222
|
+
typeAnnotation: node.type ? node.type.getText(ctx.sourceFile) : undefined,
|
|
3206
3223
|
valueBranches,
|
|
3207
3224
|
declarationKind,
|
|
3208
3225
|
isExported,
|
package/src/debug.ts
CHANGED
|
@@ -29,6 +29,7 @@ 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'
|
|
32
33
|
|
|
33
34
|
// =============================================================================
|
|
34
35
|
// Types
|
|
@@ -1990,12 +1991,12 @@ function attrValueToString(value: AttrValue): string | null {
|
|
|
1990
1991
|
function extractReactiveDeps(expr: string, signalGetters: Set<string>, memoNames: Set<string>): string[] {
|
|
1991
1992
|
const deps: string[] = []
|
|
1992
1993
|
for (const getter of signalGetters) {
|
|
1993
|
-
if (
|
|
1994
|
+
if (identifierCallPattern(getter).test(expr)) {
|
|
1994
1995
|
deps.push(getter)
|
|
1995
1996
|
}
|
|
1996
1997
|
}
|
|
1997
1998
|
for (const memo of memoNames) {
|
|
1998
|
-
if (
|
|
1999
|
+
if (identifierCallPattern(memo).test(expr)) {
|
|
1999
2000
|
deps.push(memo)
|
|
2000
2001
|
}
|
|
2001
2002
|
}
|
|
@@ -2012,7 +2013,7 @@ function extractSetterRefs(expr: string, signalGetters: Set<string>): string[] {
|
|
|
2012
2013
|
}
|
|
2013
2014
|
// Also detect signal getter reads in handler
|
|
2014
2015
|
for (const getter of signalGetters) {
|
|
2015
|
-
if (
|
|
2016
|
+
if (identifierCallPattern(getter).test(expr)) {
|
|
2016
2017
|
refs.push(getter)
|
|
2017
2018
|
}
|
|
2018
2019
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single door for building "does this expression text reference identifier
|
|
3
|
+
* X?" regexes (#2592).
|
|
4
|
+
*
|
|
5
|
+
* The naive `new RegExp(`\\b${name}\\b`)` idiom used throughout the compiler
|
|
6
|
+
* breaks for `$`-containing identifiers (legal in JS: `$item`, `item$`,
|
|
7
|
+
* `a$b`) in two independent ways:
|
|
8
|
+
*
|
|
9
|
+
* 1. Unescaped: an interpolated `$` is a regex metacharacter (end-of-
|
|
10
|
+
* input/line anchor), so `\b$item\b` / `\ba$b\b` can (almost) never
|
|
11
|
+
* match mid-string — false negative.
|
|
12
|
+
* 2. Even escaped, `\b` requires a `\w`/non-`\w` transition and `$` is
|
|
13
|
+
* not `\w` (`[A-Za-z0-9_]`) — so `\b\$item\b` still fails to match the
|
|
14
|
+
* leading boundary in `($item)` (both `(` and `$` are non-word, so no
|
|
15
|
+
* transition occurs there).
|
|
16
|
+
*
|
|
17
|
+
* `identifierPattern` / `identifierCallPattern` fix both: the identifier
|
|
18
|
+
* text is escaped before interpolation, and the boundary is asserted with
|
|
19
|
+
* lookaround against `\p{ID_Continue}` (Unicode "can continue an
|
|
20
|
+
* identifier") unioned with `$`, so `$` is correctly treated as
|
|
21
|
+
* identifier-like on both sides of the match.
|
|
22
|
+
*
|
|
23
|
+
* Scope: these remain the same *bounded lexical heuristic* the compiler has
|
|
24
|
+
* always used for expression-text scanning (not a general JS/TS parse —
|
|
25
|
+
* see CLAUDE.md's structural-parsing rule, which does not apply to this
|
|
26
|
+
* class of check). This module only fixes the `$` boundary bug; it does not
|
|
27
|
+
* change what the heuristic considers a "reference" (string literals,
|
|
28
|
+
* comments, and member-access tails are still opaque to it — callers that
|
|
29
|
+
* need that precision use `tokenContainsIdent` / `node.freeIdentifiers`
|
|
30
|
+
* instead, per #1267).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Duplicate regex flags throw at construction ('gu' + 'u' -> SyntaxError),
|
|
34
|
+
// so `u` is added only when the caller didn't already pass it.
|
|
35
|
+
function withUnicodeFlag(flags: string): string {
|
|
36
|
+
return flags.includes('u') ? flags : `${flags}u`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Escape regex metacharacters in a literal identifier before interpolation. */
|
|
40
|
+
export function escapeIdentifierForRegex(name: string): string {
|
|
41
|
+
return name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Lookaround fragments asserting "not preceded/followed by an identifier
|
|
46
|
+
* continuation character (including `$`)". `$` is a legal identifier char
|
|
47
|
+
* in JS but is not `\p{ID_Continue}`, so it's unioned in explicitly.
|
|
48
|
+
*
|
|
49
|
+
* Exported for the few call sites that must splice extra assertions
|
|
50
|
+
* between the identifier and the trailing boundary (e.g. "not followed by
|
|
51
|
+
* a call" as well as "not followed by an identifier char") — compose with
|
|
52
|
+
* these fragments rather than reintroducing a bare `\b`.
|
|
53
|
+
*/
|
|
54
|
+
export const ID_BOUNDARY_BEFORE = '(?<![\\p{ID_Continue}$])'
|
|
55
|
+
export const ID_BOUNDARY_AFTER = '(?![\\p{ID_Continue}$])'
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Regex matching a standalone reference to identifier `name` — the `$`-safe
|
|
59
|
+
* replacement for `new RegExp(`\\b${name}\\b`)`. Always carries the `u`
|
|
60
|
+
* (unicode) flag, required for `\p{ID_Continue}`; pass additional flags
|
|
61
|
+
* (e.g. `'g'` for `String.replace`/`matchAll` substitution sites) via
|
|
62
|
+
* `flags`.
|
|
63
|
+
*/
|
|
64
|
+
export function identifierPattern(name: string, flags = ''): RegExp {
|
|
65
|
+
const esc = escapeIdentifierForRegex(name)
|
|
66
|
+
return new RegExp(`${ID_BOUNDARY_BEFORE}${esc}${ID_BOUNDARY_AFTER}`, withUnicodeFlag(flags))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Regex matching identifier `name` used in call position (`name(...)`,
|
|
71
|
+
* allowing whitespace before the paren) — the `$`-safe replacement for
|
|
72
|
+
* `new RegExp(`\\b${name}\\s*\\(`)`. No trailing boundary assertion is
|
|
73
|
+
* needed: `\s`/`(` are already not `\p{ID_Continue}`/`$`, so they can't be
|
|
74
|
+
* mistaken for a continuation of `name`.
|
|
75
|
+
*/
|
|
76
|
+
export function identifierCallPattern(name: string, flags = ''): RegExp {
|
|
77
|
+
const esc = escapeIdentifierForRegex(name)
|
|
78
|
+
return new RegExp(`${ID_BOUNDARY_BEFORE}${esc}\\s*\\(`, withUnicodeFlag(flags))
|
|
79
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -89,7 +89,7 @@ export type {
|
|
|
89
89
|
} from './adapters/interface.ts'
|
|
90
90
|
export { JsxAdapter } from './adapters/jsx-adapter.ts'
|
|
91
91
|
export type { JsxAdapterConfig } from './adapters/jsx-adapter.ts'
|
|
92
|
-
export { rewriteImportsForTemplate } from './adapters/template-imports.ts'
|
|
92
|
+
export { rewriteImportsForTemplate, rewriteDynamicImportsInSource } from './adapters/template-imports.ts'
|
|
93
93
|
export { emitParsedExpr, groupBinaryOperand, isStringTypedOperand, isStringConcatBinary } from './adapters/parsed-expr-emitter.ts'
|
|
94
94
|
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
95
95
|
export { collectLoopBoundNames } from './adapters/loop-bound-names.ts'
|
|
@@ -214,6 +214,10 @@ export { buildLoopChainExpr } from './loop-chain.ts'
|
|
|
214
214
|
export type { LoopChainInputs } from './loop-chain.ts'
|
|
215
215
|
export { isLowerableLoopDestructure, isLowerableObjectRestDestructure } from './loop-destructure.ts'
|
|
216
216
|
|
|
217
|
+
// Binding scope (#2482) — shared loop-bound-name resolution service
|
|
218
|
+
export { BindingScope } from './scope/binding-scope.ts'
|
|
219
|
+
export type { ScopeBindingSource, ScopeBinding, ScopeFrame, LoopBindingSource } from './scope/binding-scope.ts'
|
|
220
|
+
|
|
217
221
|
// Debug analysis
|
|
218
222
|
export {
|
|
219
223
|
buildComponentGraph,
|
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
import { type IRNode, type IRElement, type IRComponent, type IRLoop, type IRProp, pickAttrMetaFromIR } from '../types.ts'
|
|
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
|
-
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings } from './reactivity.ts'
|
|
8
|
+
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings, buildLoopRowScope } from './reactivity.ts'
|
|
9
9
|
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, renderFlatMapClientBody, renderFlatMapProjectionClientBody, flatMapCallbackHasKeyedLeaf, type SkeletonSlotPaths } from './html-template.ts'
|
|
10
10
|
import { templateRootIsSvg } from './control-flow/stringify/template-parse.ts'
|
|
11
11
|
import { expandDynamicPropValue, expandConstantForReactivity } from './prop-handling.ts'
|
|
12
12
|
import { walkIR, stopAt } from './walker.ts'
|
|
13
13
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
14
|
+
import { identifierPattern } from '../identifier-pattern.ts'
|
|
14
15
|
|
|
15
16
|
/** Expressions that render nothing (0 DOM nodes) — `&&` / `?:` empty branches. */
|
|
16
17
|
const EMPTY_RENDER_EXPRS = new Set(['null', 'undefined', 'false', "''", '""', '``'])
|
|
@@ -311,7 +312,7 @@ export function collectInnerLoops(
|
|
|
311
312
|
const template = n.children.map(c => irToPlaceholderTemplate(c, undefined, emitDepth, loopParamsForTemplate)).join('')
|
|
312
313
|
// Check if array expression references the outer loop param
|
|
313
314
|
const refsOuter = outerLoopParam
|
|
314
|
-
?
|
|
315
|
+
? identifierPattern(outerLoopParam).test(n.array)
|
|
315
316
|
: false
|
|
316
317
|
// Per-item bindings for inner loop body, collected uniformly when
|
|
317
318
|
// ctx is available: reactiveTexts / reactiveAttrs / refs are each
|
|
@@ -332,8 +333,8 @@ export function collectInnerLoops(
|
|
|
332
333
|
const innerPreambleNames = preambleNamesOf(n)
|
|
333
334
|
if (ctx) {
|
|
334
335
|
for (const child of n.children) {
|
|
335
|
-
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings))
|
|
336
|
-
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, false, innerPreambleNames))
|
|
336
|
+
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings, false, innerPreambleNames, n.index))
|
|
337
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, false, innerPreambleNames, n.index))
|
|
337
338
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
338
339
|
}
|
|
339
340
|
}
|
|
@@ -375,6 +376,8 @@ export function collectInnerLoops(
|
|
|
375
376
|
siblingOffsets,
|
|
376
377
|
n.param,
|
|
377
378
|
n.paramBindings,
|
|
379
|
+
innerPreambleNames,
|
|
380
|
+
n.index,
|
|
378
381
|
))
|
|
379
382
|
}
|
|
380
383
|
}
|
|
@@ -671,7 +674,7 @@ export function collectElements(
|
|
|
671
674
|
const childHandlers: string[] = []
|
|
672
675
|
const bindings = projectionInner
|
|
673
676
|
? emptyLoopChildBindings()
|
|
674
|
-
: collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings, preambleNamesOf(l))
|
|
677
|
+
: collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings, preambleNamesOf(l), l.index)
|
|
675
678
|
if (!projectionInner) {
|
|
676
679
|
for (const child of l.children) {
|
|
677
680
|
childHandlers.push(...collectEventHandlersFromIR(child))
|
|
@@ -1146,7 +1149,7 @@ function collectBranchLoops(
|
|
|
1146
1149
|
// which caused reactive reads inside simple loop bodies to silently
|
|
1147
1150
|
// no-op for existing items.
|
|
1148
1151
|
const branchBindings = ctx && !projectionInner
|
|
1149
|
-
? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings, preambleNamesOf(n))
|
|
1152
|
+
? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings, preambleNamesOf(n), n.index)
|
|
1150
1153
|
: emptyLoopChildBindings()
|
|
1151
1154
|
|
|
1152
1155
|
loops.push({
|
|
@@ -1346,6 +1349,12 @@ export function collectLoopChildBindings(
|
|
|
1346
1349
|
* `collectLoopChildReactiveAttrs`. Omitted for a loop with no preamble.
|
|
1347
1350
|
*/
|
|
1348
1351
|
preambleNames?: ReadonlySet<string>,
|
|
1352
|
+
/**
|
|
1353
|
+
* The loop's index param name (`.map((item, i) => ...)`'s `i`), when
|
|
1354
|
+
* present — folded into the shadow guard (Copilot review on #2595) so
|
|
1355
|
+
* an index-shadowing const doesn't get const-folded either.
|
|
1356
|
+
*/
|
|
1357
|
+
loopIndex?: string | null,
|
|
1349
1358
|
): LoopChildBindings {
|
|
1350
1359
|
const bindings = emptyLoopChildBindings()
|
|
1351
1360
|
for (const child of children) {
|
|
@@ -1355,10 +1364,10 @@ export function collectLoopChildBindings(
|
|
|
1355
1364
|
// `collectLoopChildConditionals`, which gives each its own insert() +
|
|
1356
1365
|
// arm-scoped attrs/texts (`LoopChildBranchSummary.reactiveAttrs` /
|
|
1357
1366
|
// `.reactiveTexts`) — descending into them here too would double-bind.
|
|
1358
|
-
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings, true, preambleNames))
|
|
1359
|
-
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, loopParam, loopParamBindings, true))
|
|
1367
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings, true, preambleNames, loopIndex))
|
|
1368
|
+
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, loopParam, loopParamBindings, true, preambleNames, loopIndex))
|
|
1360
1369
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
1361
|
-
bindings.conditionals.push(...collectLoopChildConditionals(child, ctx, siblingOffsets, loopParam, loopParamBindings))
|
|
1370
|
+
bindings.conditionals.push(...collectLoopChildConditionals(child, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex))
|
|
1362
1371
|
}
|
|
1363
1372
|
return bindings
|
|
1364
1373
|
}
|
|
@@ -1369,8 +1378,24 @@ export function collectLoopChildConditionals(
|
|
|
1369
1378
|
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1370
1379
|
loopParam?: string,
|
|
1371
1380
|
loopParamBindings?: readonly import('../types.ts').LoopParamBinding[],
|
|
1381
|
+
/**
|
|
1382
|
+
* The enclosing loop's `.map()` callback preamble locals (#2447), when
|
|
1383
|
+
* known — folded into the `expandConstantForReactivity` shadow guard
|
|
1384
|
+
* (#2482 Stage 1b) so a preamble local shadowing a component/module
|
|
1385
|
+
* const doesn't get const-folded into the condition, which would
|
|
1386
|
+
* corrupt `classifyReactivity`'s verdict below (a substituted literal
|
|
1387
|
+
* reads as "not reactive," silently freezing the branch at its initial
|
|
1388
|
+
* value instead of wiring an `insert()`).
|
|
1389
|
+
*/
|
|
1390
|
+
preambleNames?: ReadonlySet<string>,
|
|
1391
|
+
/**
|
|
1392
|
+
* The loop's index param name, when present — see
|
|
1393
|
+
* `collectLoopChildBindings`'s doc comment (Copilot review on #2595).
|
|
1394
|
+
*/
|
|
1395
|
+
loopIndex?: string | null,
|
|
1372
1396
|
): LoopChildConditional[] {
|
|
1373
1397
|
const conditionals: LoopChildConditional[] = []
|
|
1398
|
+
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex)
|
|
1374
1399
|
|
|
1375
1400
|
// Widen the source-level "references loop param" check so destructured
|
|
1376
1401
|
// callbacks fire too — the pattern text `[, cfg]` never word-matches on
|
|
@@ -1402,7 +1427,7 @@ export function collectLoopChildConditionals(
|
|
|
1402
1427
|
// Pre-gate using AST `reactive` flag on the source condition before
|
|
1403
1428
|
// paying for constant expansion — matches the legacy short-circuit.
|
|
1404
1429
|
if (!n.reactive && !refsLoopParamInSource) return
|
|
1405
|
-
const expanded = expandConstantForReactivity(n.condition, ctx, sourceFreeIds)
|
|
1430
|
+
const expanded = expandConstantForReactivity(n.condition, ctx, sourceFreeIds, scope)
|
|
1406
1431
|
// Loop-param conditionals are reactive via per-item signal accessors;
|
|
1407
1432
|
// classifyReactivity sees both paths (signal/memo/prop + loop-param).
|
|
1408
1433
|
if (classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind === 'none') return
|
|
@@ -1421,8 +1446,8 @@ export function collectLoopChildConditionals(
|
|
|
1421
1446
|
condition: expanded.expr,
|
|
1422
1447
|
whenTrueHtml,
|
|
1423
1448
|
whenFalseHtml,
|
|
1424
|
-
whenTrue: summarizeLoopChildBranch(n.whenTrue, ctx, siblingOffsets, loopParam, loopParamBindings),
|
|
1425
|
-
whenFalse: summarizeLoopChildBranch(n.whenFalse, ctx, siblingOffsets, loopParam, loopParamBindings),
|
|
1449
|
+
whenTrue: summarizeLoopChildBranch(n.whenTrue, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex),
|
|
1450
|
+
whenFalse: summarizeLoopChildBranch(n.whenFalse, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex),
|
|
1426
1451
|
...(expanded.freeIds !== undefined && { conditionFreeIdentifiers: expanded.freeIds }),
|
|
1427
1452
|
})
|
|
1428
1453
|
},
|
|
@@ -1444,19 +1469,23 @@ function summarizeLoopChildBranch(
|
|
|
1444
1469
|
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1445
1470
|
loopParam?: string,
|
|
1446
1471
|
loopParamBindings?: readonly import('../types.ts').LoopParamBinding[],
|
|
1472
|
+
/** Enclosing loop's preamble locals (#2447) — see `collectLoopChildConditionals`. */
|
|
1473
|
+
preambleNames?: ReadonlySet<string>,
|
|
1474
|
+
/** Enclosing loop's index param name — see `collectLoopChildConditionals` (Copilot review on #2595). */
|
|
1475
|
+
loopIndex?: string | null,
|
|
1447
1476
|
): LoopChildBranchSummary {
|
|
1448
1477
|
const inner = collectInnerLoops([node], siblingOffsets, loopParam, ctx, branchInnerLoopOptions)
|
|
1449
1478
|
return {
|
|
1450
1479
|
childComponents: collectConditionalBranchChildComponents(node),
|
|
1451
1480
|
innerLoops: inner.length > 0 ? inner : undefined,
|
|
1452
|
-
conditionals: collectLoopChildConditionals(node, ctx, siblingOffsets, loopParam, loopParamBindings),
|
|
1481
|
+
conditionals: collectLoopChildConditionals(node, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex),
|
|
1453
1482
|
events: collectConditionalBranchEvents(node),
|
|
1454
1483
|
// Loop-param-aware — reuses the flat loop-item collectors scoped to just
|
|
1455
1484
|
// this branch's subtree. Both already stop descending into any further
|
|
1456
1485
|
// nested reactive conditional (own insert()/arm), so calling them here
|
|
1457
1486
|
// on the branch root yields exactly this branch's direct bindings
|
|
1458
1487
|
// without re-collecting what a nested arm already owns (#2347).
|
|
1459
|
-
reactiveAttrs: collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings, true),
|
|
1488
|
+
reactiveAttrs: collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings, true, preambleNames, loopIndex),
|
|
1460
1489
|
// Skip ONLY when the branch's entire content is a single bare
|
|
1461
1490
|
// `expression` (no wrapping element) that MAY yield a live DOM node —
|
|
1462
1491
|
// i.e. it contains a call anywhere (`node.hasFunctionCalls`, computed
|
|
@@ -1506,6 +1535,6 @@ function summarizeLoopChildBranch(
|
|
|
1506
1535
|
// two can't disagree on shape).
|
|
1507
1536
|
reactiveTexts: node.type === 'expression' && node.hasFunctionCalls
|
|
1508
1537
|
? []
|
|
1509
|
-
: collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings, true),
|
|
1538
|
+
: collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings, true, preambleNames, loopIndex),
|
|
1510
1539
|
}
|
|
1511
1540
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { TopLevelLoop, BranchLoop, LoopChildEvent } from '../../types.ts'
|
|
13
13
|
import { buildChainedArrayExpr, varSlotId, substituteLoopBindings } from '../../utils.ts'
|
|
14
|
+
import { identifierPattern } from '../../../identifier-pattern.ts'
|
|
14
15
|
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
15
16
|
import type {
|
|
16
17
|
EventDelegationPlan,
|
|
@@ -138,7 +139,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
138
139
|
// we substitute bindings with `item.<path>` directly (#951).
|
|
139
140
|
const keyWithItem = hasBindings
|
|
140
141
|
? substituteLoopBindings(args.key, args.paramBindings!, 'item')
|
|
141
|
-
: args.key.replace(
|
|
142
|
+
: args.key.replace(identifierPattern(args.param, 'g'), 'item')
|
|
142
143
|
return {
|
|
143
144
|
kind: 'keyed',
|
|
144
145
|
arrayExpr: args.array,
|