@barefootjs/jsx 0.35.2 → 0.35.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2863 -2796
- package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +19 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +26 -0
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/branch-local-in-attr-position.test.ts +24 -12
- package/src/__tests__/client-only-date-lowering.test.ts +29 -1
- package/src/__tests__/issue-2798-static-nested-loop-bindings.test.ts +127 -0
- package/src/__tests__/issue-2856-branch-local-shorthand.test.ts +86 -0
- package/src/__tests__/rewrite-destructured-props.test.ts +10 -5
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +59 -19
- package/src/ir-to-client-js/plan/static-array-child-init.ts +19 -1
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +39 -0
- package/src/jsx-to-ir.ts +16 -4
- package/src/prop-rewrite.ts +74 -33
|
@@ -58,6 +58,10 @@ import type {
|
|
|
58
58
|
StaticArrayChildInitsPlan,
|
|
59
59
|
} from '../plan/static-array-child-init.ts'
|
|
60
60
|
import { nameForRegistryRef } from '../component-scope.ts'
|
|
61
|
+
import { varSlotId } from '../utils.ts'
|
|
62
|
+
import { emitDedupedAttrUpdate, DEDUP_STORE_DECL } from '../emit-reactive.ts'
|
|
63
|
+
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from '../control-flow/stringify/claim-plan.ts'
|
|
64
|
+
import { emitLoopChildRefs } from '../control-flow/stringify/loop.ts'
|
|
61
65
|
|
|
62
66
|
export function stringifyStaticArrayChildInits(
|
|
63
67
|
lines: string[],
|
|
@@ -142,6 +146,9 @@ function emitInnerLoopNested(lines: string[], plan: InnerLoopNestedInitPlan): vo
|
|
|
142
146
|
innerPreludeStatements,
|
|
143
147
|
depth,
|
|
144
148
|
comps,
|
|
149
|
+
attrsBySlot,
|
|
150
|
+
texts,
|
|
151
|
+
refs,
|
|
145
152
|
} = plan
|
|
146
153
|
lines.push(` // Initialize inner-loop components in static array (depth ${depth})`)
|
|
147
154
|
lines.push(` if (${containerVar}) {`)
|
|
@@ -174,6 +181,38 @@ function emitInnerLoopNested(lines: string[], plan: InnerLoopNestedInitPlan): vo
|
|
|
174
181
|
lines.push(` const ${compElVar} = qsaChildScope(__innerEl, ${comp.selector})`)
|
|
175
182
|
lines.push(` if (${compElVar}) initChild('${nameForRegistryRef(comp.componentName)}', ${compElVar}, ${comp.propsExpr})`)
|
|
176
183
|
})
|
|
184
|
+
// Plain-element reactive attrs / texts / refs inside this inner loop's
|
|
185
|
+
// body (#2798) — mirrors `stringifyStaticLoop`'s own per-row wiring for
|
|
186
|
+
// the OUTER static loop, one nesting level in. `forEach` binds
|
|
187
|
+
// `innerParam` as the raw item value, so `texts`/`attrsBySlot`
|
|
188
|
+
// expressions stay unwrapped, same as the outer row.
|
|
189
|
+
if (attrsBySlot.length > 0) lines.push(` ${DEDUP_STORE_DECL}`)
|
|
190
|
+
let ordinal = 0
|
|
191
|
+
for (const [slotId, attrs] of attrsBySlot) {
|
|
192
|
+
const varName = `__t_${varSlotId(slotId)}`
|
|
193
|
+
lines.push(` const ${varName} = qsa(__innerEl, '[bf="${slotId}"]')`)
|
|
194
|
+
lines.push(` if (${varName}) {`)
|
|
195
|
+
for (const attr of attrs) {
|
|
196
|
+
lines.push(` createEffect(() => {`)
|
|
197
|
+
for (const stmt of emitDedupedAttrUpdate(varName, attr.attrName, attr.expression, attr, ordinal++)) {
|
|
198
|
+
lines.push(` ${stmt}`)
|
|
199
|
+
}
|
|
200
|
+
lines.push(` })`)
|
|
201
|
+
}
|
|
202
|
+
lines.push(` }`)
|
|
203
|
+
}
|
|
204
|
+
if (texts.length > 0) {
|
|
205
|
+
const slots: ClaimSlotSpec[] = texts.map(t => ({ id: t.slotId, kind: 'text', path: [] }))
|
|
206
|
+
const writer = claimWriterVarName(slots, varSlotId)
|
|
207
|
+
lines.push(` const ${writer} = lazySlots(__innerEl, ${claimPlanLiteral(slots)})`)
|
|
208
|
+
for (const text of texts) {
|
|
209
|
+
lines.push(` createEffect(() => { ${writer}('${text.slotId}', String(${text.expression})) })`)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Ref callbacks fire on every forEach iteration — initial mount and any
|
|
213
|
+
// future array-change-driven re-iteration. For a static array the array
|
|
214
|
+
// is non-reactive, so refs effectively fire once per item (#1244).
|
|
215
|
+
emitLoopChildRefs(lines, refs, { indent: ' ', elVar: '__innerEl', bodyIsMultiRoot: false })
|
|
177
216
|
lines.push(` })`)
|
|
178
217
|
lines.push(` })`)
|
|
179
218
|
lines.push(` }`)
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { containsReactiveExpression, collectReactiveBrandLeaves } from './reacti
|
|
|
49
49
|
import {
|
|
50
50
|
rewriteBarePropRefs as rewriteBarePropRefsCore,
|
|
51
51
|
collectAstPropRefs,
|
|
52
|
+
rewriteScopedValueRefs,
|
|
52
53
|
} from './prop-rewrite.ts'
|
|
53
54
|
import { boundPropLocalNames, buildPropAliasMap, resolveAliasOrigin, resolveRestSpreadOriginCore } from './props-binding.ts'
|
|
54
55
|
import { resolveFreeRefs, isNameBound as isNameBoundInEnv, type BindingEnvironment } from './free-refs.ts'
|
|
@@ -8456,10 +8457,19 @@ function inferExpressionType(
|
|
|
8456
8457
|
|
|
8457
8458
|
/**
|
|
8458
8459
|
* Substitute branch-local identifier references in `text` with the
|
|
8459
|
-
* value returned by `resolve(name)`.
|
|
8460
|
-
*
|
|
8461
|
-
*
|
|
8462
|
-
*
|
|
8460
|
+
* value returned by `resolve(name)`. Tries the AST-based, scope-aware
|
|
8461
|
+
* `rewriteScopedValueRefs` (`prop-rewrite.ts`) first — it correctly
|
|
8462
|
+
* expands an object-literal shorthand property (`{ local }`) to
|
|
8463
|
+
* `{ local: <resolved> }` instead of corrupting it to `{ (<resolved>) }`
|
|
8464
|
+
* (#2856), and understands inner-scope shadowing. `allowStatements`
|
|
8465
|
+
* covers raw-captured text that isn't guaranteed to be a bare
|
|
8466
|
+
* expression (e.g. a `.map()`-callback preamble statement).
|
|
8467
|
+
*
|
|
8468
|
+
* Falls back to the legacy `replaceInExprContexts` scanner — which
|
|
8469
|
+
* skips occurrences inside string / regex / template-body / comment
|
|
8470
|
+
* tokens, so a string like `'mergedClass'` never gets rewritten into
|
|
8471
|
+
* invalid JS, but has no notion of AST position — only when `text`
|
|
8472
|
+
* doesn't parse under either attempt. Identifier boundaries use
|
|
8463
8473
|
* `[\w$]` lookarounds rather than `\b`, since JS regex's word-char
|
|
8464
8474
|
* class excludes `$` — a bare `\b` would mis-match the `foo` inside
|
|
8465
8475
|
* `$foo`. Branch-local names are syntactically `[A-Za-z_$][A-Za-z0-9_$]*`
|
|
@@ -8478,6 +8488,8 @@ function replaceBranchLocalRefs(
|
|
|
8478
8488
|
resolve: (name: string) => string,
|
|
8479
8489
|
): string {
|
|
8480
8490
|
if (branchNames.length === 0) return text
|
|
8491
|
+
const astResult = rewriteScopedValueRefs(text, new Set(branchNames), resolve, { allowStatements: true })
|
|
8492
|
+
if (astResult !== null) return astResult
|
|
8481
8493
|
const pattern = new RegExp(`(?<![\\w$])(${branchNames.join('|')})(?![\\w$])`, 'g')
|
|
8482
8494
|
return replaceInExprContexts(text, pattern, (_match, name) => resolve(name))
|
|
8483
8495
|
}
|
package/src/prop-rewrite.ts
CHANGED
|
@@ -89,8 +89,16 @@ function walkWithScope(
|
|
|
89
89
|
/**
|
|
90
90
|
* True when `n` sits in a non-value position where a prop rewrite must
|
|
91
91
|
* never apply: an object-literal key, a member-access name, a binding
|
|
92
|
-
* position (parameter / variable / binding-element name),
|
|
93
|
-
*
|
|
92
|
+
* position (parameter / variable / binding-element name), a destructuring
|
|
93
|
+
* declaration's SOURCE key (`propertyName`, e.g. the `local` in
|
|
94
|
+
* `const { local: renamed } = obj`), or a type reference.
|
|
95
|
+
*
|
|
96
|
+
* The `propertyName` check matters even though `BindingElement.name` is
|
|
97
|
+
* already excluded above it: a renamed destructuring pattern has BOTH —
|
|
98
|
+
* `propertyName` is the source key (`local`) and `name` is the local
|
|
99
|
+
* binding (`renamed`) — and pullfrog review on #2889 caught that only the
|
|
100
|
+
* latter was excluded, so a substituted name colliding with the SOURCE key
|
|
101
|
+
* still corrupted to invalid JS (`const { (_p.tag): renamed } = obj`).
|
|
94
102
|
*/
|
|
95
103
|
function isNonValuePosition(n: ts.Identifier, parent: ts.Node | undefined): boolean {
|
|
96
104
|
if (!parent) return false
|
|
@@ -98,6 +106,7 @@ function isNonValuePosition(n: ts.Identifier, parent: ts.Node | undefined): bool
|
|
|
98
106
|
if (ts.isPropertyAccessExpression(parent) && parent.name === n) return true
|
|
99
107
|
if (ts.isQualifiedName(parent) && parent.right === n) return true
|
|
100
108
|
if ((ts.isParameter(parent) || ts.isVariableDeclaration(parent) || ts.isBindingElement(parent)) && parent.name === n) return true
|
|
109
|
+
if (ts.isBindingElement(parent) && parent.propertyName === n) return true
|
|
101
110
|
if (ts.isTypeReferenceNode(parent)) return true
|
|
102
111
|
return false
|
|
103
112
|
}
|
|
@@ -130,49 +139,54 @@ export function collectAstPropRefs(
|
|
|
130
139
|
|
|
131
140
|
/**
|
|
132
141
|
* Scope-aware rewrite: parse `text` as an expression, walk it with the
|
|
133
|
-
* binding stack, and splice
|
|
134
|
-
* identifier references that are (a) in `
|
|
135
|
-
*
|
|
136
|
-
* (`{ org }` → `{ org:
|
|
137
|
-
* valid
|
|
142
|
+
* binding stack, and splice `replacementFor(name)` onto exactly the
|
|
143
|
+
* identifier references that are (a) in `names` and (b) not shadowed by
|
|
144
|
+
* a binding inside the text. Shorthand properties expand
|
|
145
|
+
* (`{ org }` → `{ org: <replacement> }`) so the result stays
|
|
146
|
+
* syntactically valid — this is the one place in the compiler that
|
|
147
|
+
* correctly distinguishes a value reference from an object-literal key
|
|
148
|
+
* / property-access name / shorthand property / binding name, so every
|
|
149
|
+
* caller doing this class of substitution (prop refs, #1425 branch-local
|
|
150
|
+
* refs, …) should route through this rather than growing its own
|
|
151
|
+
* regex-based text splice (#2856 — a second, regex-based substitution
|
|
152
|
+
* mechanism doesn't know shorthand is simultaneously a key and a value,
|
|
153
|
+
* and drops the key).
|
|
138
154
|
*
|
|
139
|
-
* `
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* destructure default. Kept as a caller override rather than a
|
|
144
|
-
* duplicate AST walk in that module, per the "one decision, one
|
|
145
|
-
* implementation" rule (CLAUDE.md) — this walk is already the one place
|
|
146
|
-
* that correctly distinguishes a value reference from an object-literal
|
|
147
|
-
* key / property-access name / shorthand property / binding name.
|
|
155
|
+
* With `allowStatements`, a `text` that fails to parse as a bare
|
|
156
|
+
* expression is retried as a standalone statement list (no wrapping
|
|
157
|
+
* parens) — for callers whose raw-captured text isn't guaranteed to be
|
|
158
|
+
* expression-shaped (e.g. a `.map()`-callback preamble statement).
|
|
148
159
|
*
|
|
149
|
-
* Returns null when `text`
|
|
150
|
-
* the caller falls back to
|
|
160
|
+
* Returns null when `text` doesn't parse cleanly under either attempt —
|
|
161
|
+
* the caller falls back to its own legacy mechanism.
|
|
151
162
|
*/
|
|
152
|
-
function
|
|
163
|
+
export function rewriteScopedValueRefs(
|
|
153
164
|
text: string,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
165
|
+
names: ReadonlySet<string>,
|
|
166
|
+
replacementFor: (name: string) => string,
|
|
167
|
+
opts?: { allowStatements?: boolean },
|
|
157
168
|
): string | null {
|
|
158
169
|
// Wrap in parens so object literals and arrows parse as expressions.
|
|
159
170
|
const prefix = '('
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
let sf = ts.createSourceFile('__bf_scoped_rewrite.ts', `${prefix}${text}\n)`, ts.ScriptTarget.Latest, true)
|
|
172
|
+
let parseDiagnostics = (sf as unknown as { parseDiagnostics?: unknown[] }).parseDiagnostics
|
|
173
|
+
let usedPrefix = prefix
|
|
174
|
+
if (parseDiagnostics && parseDiagnostics.length > 0) {
|
|
175
|
+
if (!opts?.allowStatements) return null
|
|
176
|
+
sf = ts.createSourceFile('__bf_scoped_rewrite.ts', text, ts.ScriptTarget.Latest, true)
|
|
177
|
+
parseDiagnostics = (sf as unknown as { parseDiagnostics?: unknown[] }).parseDiagnostics
|
|
178
|
+
if (parseDiagnostics && parseDiagnostics.length > 0) return null
|
|
179
|
+
usedPrefix = ''
|
|
180
|
+
}
|
|
163
181
|
|
|
164
182
|
const edits: Array<{ start: number; end: number; replacement: string }> = []
|
|
165
183
|
walkWithScope(sf, (n, parent, shadowed) => {
|
|
166
|
-
if (shadowed || !
|
|
184
|
+
if (shadowed || !names.has(n.text)) return
|
|
167
185
|
if (isNonValuePosition(n, parent)) return
|
|
168
|
-
const start = n.getStart(sf) -
|
|
169
|
-
const end = n.getEnd() -
|
|
186
|
+
const start = n.getStart(sf) - usedPrefix.length
|
|
187
|
+
const end = n.getEnd() - usedPrefix.length
|
|
170
188
|
if (start < 0 || end > text.length) return
|
|
171
|
-
|
|
172
|
-
// — #2524 CSR half); the local binding (`n.text`) only survives on the
|
|
173
|
-
// left of a shorthand expansion.
|
|
174
|
-
const callerKey = propAliases?.get(n.text) ?? n.text
|
|
175
|
-
const value = replacementFor ? replacementFor(n.text, callerKey) : `${PROPS_PARAM}.${callerKey}`
|
|
189
|
+
const value = replacementFor(n.text)
|
|
176
190
|
if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === n) {
|
|
177
191
|
edits.push({ start, end, replacement: `${n.text}: ${value}` })
|
|
178
192
|
return
|
|
@@ -188,6 +202,33 @@ function applyScopedPropRefRewrite(
|
|
|
188
202
|
return result
|
|
189
203
|
}
|
|
190
204
|
|
|
205
|
+
/**
|
|
206
|
+
* `applyScopedPropRefRewrite` specializes `rewriteScopedValueRefs` for
|
|
207
|
+
* the destructured-prop case: the substitution defaults to
|
|
208
|
+
* `${PROPS_PARAM}.<callerKey>`, where the caller-facing key comes from
|
|
209
|
+
* `propAliases` (`_p` is always keyed by the caller-facing name —
|
|
210
|
+
* `sourceName ?? name`, #2524 CSR half — the local binding only
|
|
211
|
+
* survives on the left of a shorthand expansion).
|
|
212
|
+
*
|
|
213
|
+
* `replacementFor` overrides the substitution text for a value
|
|
214
|
+
* reference — e.g. the reactive client-JS emit path
|
|
215
|
+
* (`emit-reactive.ts`'s `rewriteDestructuredPropsInExpr`) wraps it in a
|
|
216
|
+
* `(_p.x ?? <default>)` fallback for a prop with a destructure default.
|
|
217
|
+
* Kept as a caller override rather than a duplicate AST walk in that
|
|
218
|
+
* module, per the "one decision, one implementation" rule (CLAUDE.md).
|
|
219
|
+
*/
|
|
220
|
+
function applyScopedPropRefRewrite(
|
|
221
|
+
text: string,
|
|
222
|
+
propRefs: Set<string>,
|
|
223
|
+
propAliases?: ReadonlyMap<string, string>,
|
|
224
|
+
replacementFor?: (localName: string, callerKey: string) => string,
|
|
225
|
+
): string | null {
|
|
226
|
+
return rewriteScopedValueRefs(text, propRefs, (name) => {
|
|
227
|
+
const callerKey = propAliases?.get(name) ?? name
|
|
228
|
+
return replacementFor ? replacementFor(name, callerKey) : `${PROPS_PARAM}.${callerKey}`
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
191
232
|
/**
|
|
192
233
|
* Apply the targeted regex rewrite for one or more prop names on a
|
|
193
234
|
* type-stripped expression text. Idempotent under `_p.X` (negative
|