@barefootjs/jsx 0.6.0 → 0.7.0
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/parsed-expr-emitter.d.ts +14 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +137 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +335 -5
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts +4 -0
- 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 +46 -2
- 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/ir-to-client-js/types.d.ts +8 -1
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +7 -7
- package/src/__tests__/child-components-in-map.test.ts +84 -0
- package/src/__tests__/client-js-generation.test.ts +51 -0
- package/src/__tests__/compiler-stress-1244.test.ts +43 -0
- package/src/__tests__/expression-parser.test.ts +109 -1
- package/src/__tests__/foreach-client-only.test.ts +80 -0
- package/src/__tests__/ir-async.test.ts +64 -0
- package/src/__tests__/ir-dynamic-tag.test.ts +104 -0
- package/src/__tests__/ir-reduce-op.test.ts +51 -0
- package/src/__tests__/reduce-op.test.ts +201 -0
- package/src/adapters/parsed-expr-emitter.ts +43 -1
- package/src/expression-parser.ts +570 -4
- package/src/index.ts +1 -1
- package/src/ir-to-client-js/collect-elements.ts +27 -4
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +55 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +47 -1
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +69 -0
- package/src/ir-to-client-js/types.ts +8 -1
- package/src/jsx-to-ir.ts +69 -0
- package/src/types.ts +9 -0
|
@@ -428,9 +428,15 @@ function decideLoopRendering(
|
|
|
428
428
|
ctx: ClientJsContext | undefined,
|
|
429
429
|
): { useElementReconciliation: boolean; innerLoops: NestedLoop[] | undefined } {
|
|
430
430
|
const hasNestedComps = (loop.nestedComponents?.length ?? 0) > 0
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
431
|
+
// Collect inner loops even when the outer item is a child component
|
|
432
|
+
// (#1725): a `.map()` of components living inside the child component's
|
|
433
|
+
// JSX children (e.g. `<SelectGroup>{items.map(...)}</SelectGroup>`) needs
|
|
434
|
+
// its own `initChild` pass. `loop.children` is the single child-component
|
|
435
|
+
// node; `collectInnerLoops` descends into its children to find the nested
|
|
436
|
+
// loop. These only surface for static arrays (gated at the call site via
|
|
437
|
+
// `isStaticArray && innerLoops.length`) so dynamic child-component loops —
|
|
438
|
+
// which render through `createComponent` — are unaffected.
|
|
439
|
+
const innerLoops = collectInnerLoops(loop.children, siblingOffsets, loop.param, ctx)
|
|
434
440
|
const hasInnerLoops = (innerLoops?.length ?? 0) > 0
|
|
435
441
|
const useElementReconciliation =
|
|
436
442
|
!loop.childComponent && !loop.isStaticArray && (hasNestedComps || hasInnerLoops)
|
|
@@ -847,9 +853,26 @@ function collectFromElement(element: IRElement, ctx: ClientJsContext, insideCond
|
|
|
847
853
|
const elemRestName = ctx.restPropsName
|
|
848
854
|
const elemPropsObjName = ctx.propsObjectName
|
|
849
855
|
if (spreadVal && (spreadVal === elemRestName || spreadVal === elemPropsObjName)) {
|
|
850
|
-
|
|
856
|
+
// `applyRestAttrs(_el, _p, exclude)` is handed the FULL props
|
|
857
|
+
// object (`PROPS_PARAM`), not a computed JS rest binding, and the
|
|
858
|
+
// runtime filters by SOURCE KEY (`source[key]`). So `exclude` must
|
|
859
|
+
// list every prop the component already consumed, keyed the way it
|
|
860
|
+
// arrives on `_p`. For the destructured `...rest` form that set is
|
|
861
|
+
// exactly the destructured param names (the JS rest-exclusion set):
|
|
862
|
+
// it covers every statically/reactively bound attr AND the
|
|
863
|
+
// separately-wired event/ref handlers. Without it, applyRestAttrs
|
|
864
|
+
// re-binds those events (double-fire) and re-emits consumed-but-
|
|
865
|
+
// unbound props under their raw key (e.g. `error` → `error="…"`,
|
|
866
|
+
// `describedBy` → `describedBy="…"`). The element-attr-name list
|
|
867
|
+
// alone was wrong on both counts — it keys on HTML attr names
|
|
868
|
+
// (`aria-invalid` ≠ source key `error`) and omits event handlers.
|
|
869
|
+
// (#1467)
|
|
870
|
+
const consumedKeys =
|
|
871
|
+
spreadVal === elemRestName ? ctx.propsParams.map(p => p.name) : []
|
|
872
|
+
const staticAttrKeys = element.attrs
|
|
851
873
|
.filter(a => a.name !== '...')
|
|
852
874
|
.map(a => a.name)
|
|
875
|
+
const excludeKeys = [...new Set([...consumedKeys, ...staticAttrKeys])]
|
|
853
876
|
ctx.restAttrElements.push({
|
|
854
877
|
slotId: element.slotId,
|
|
855
878
|
source: PROPS_PARAM,
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* 2. `outer-nested` for each depth-0 entry in `elem.nestedComponents`.
|
|
9
9
|
* 3. `inner-loop-nested` for each `elem.innerLoops` entry that has
|
|
10
10
|
* matching depth-N components.
|
|
11
|
+
* 4. `component-rooted-inner-loop` instead of (3) when the outer item is
|
|
12
|
+
* itself a child component (#1725) — the inner `.map()` lives inside
|
|
13
|
+
* the component's JSX children, so it's addressed by a document-order
|
|
14
|
+
* zip rather than element offsets.
|
|
11
15
|
*
|
|
12
16
|
* Selector / propsExpr / offset decisions all resolve here. The
|
|
13
17
|
* stringifier never inspects raw IR.
|
|
@@ -23,6 +27,7 @@ import { buildCompSelector } from '../control-flow/shared'
|
|
|
23
27
|
/** The inline prop shape carried on `IRLoopChildComponent.props`. */
|
|
24
28
|
type LoopChildCompProp = IRLoopChildComponent['props'][number]
|
|
25
29
|
import type {
|
|
30
|
+
ComponentRootedInnerLoopInitPlan,
|
|
26
31
|
InnerLoopComp,
|
|
27
32
|
InnerLoopNestedInitPlan,
|
|
28
33
|
OuterNestedInitPlan,
|
|
@@ -56,7 +61,16 @@ export function buildStaticArrayChildInitsPlan(
|
|
|
56
61
|
(c.loopDepth ?? 0) === innerLoop.depth && c.innerLoopArray === innerLoop.array,
|
|
57
62
|
)
|
|
58
63
|
if (innerComps.length === 0) continue
|
|
59
|
-
|
|
64
|
+
// Component-rooted outer item (#1725): the inner `.map()` lives
|
|
65
|
+
// inside the child component's JSX children. The element-offset
|
|
66
|
+
// addressing of `inner-loop-nested` can't reach a fragment-rooted
|
|
67
|
+
// passthrough's flattened items, so use the document-order zip
|
|
68
|
+
// shape instead.
|
|
69
|
+
plans.push(
|
|
70
|
+
elem.childComponent
|
|
71
|
+
? buildComponentRootedInnerLoopPlan(elem, innerLoop, innerComps)
|
|
72
|
+
: buildInnerLoopNestedPlan(elem, innerLoop, innerComps),
|
|
73
|
+
)
|
|
60
74
|
}
|
|
61
75
|
}
|
|
62
76
|
}
|
|
@@ -134,6 +148,46 @@ function buildInnerLoopNestedPlan(
|
|
|
134
148
|
}
|
|
135
149
|
}
|
|
136
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Build the document-order-zip plan for an inner `.map()` of components living
|
|
153
|
+
* inside a component-rooted loop item (#1725).
|
|
154
|
+
*
|
|
155
|
+
* Known limitation (shared with `inner-loop-nested`): the emitted `forEach`
|
|
156
|
+
* iterates `innerLoop.array` — the *base* inner array. `NestedLoop` doesn't
|
|
157
|
+
* carry `filterPredicate` / `sortComparator`, so a `.filter()` / `.sort()` on
|
|
158
|
+
* the inner `.map()` makes the iteration order diverge from the SSR render
|
|
159
|
+
* order. `inner-loop-nested` masks this per-group (each group re-indexes
|
|
160
|
+
* `__ic.children` from 0, so a trailing filtered-out item just reads
|
|
161
|
+
* `undefined`); the zip's single document-order cursor instead misaligns every
|
|
162
|
+
* later group. Both are wrong for non-trailing filtered items — filter/sort on
|
|
163
|
+
* a nested static-array loop is unsupported across this family, not a
|
|
164
|
+
* regression introduced here.
|
|
165
|
+
*/
|
|
166
|
+
function buildComponentRootedInnerLoopPlan(
|
|
167
|
+
elem: TopLevelLoop,
|
|
168
|
+
innerLoop: NestedLoop,
|
|
169
|
+
innerComps: readonly IRLoopChildComponent[],
|
|
170
|
+
): ComponentRootedInnerLoopInitPlan {
|
|
171
|
+
const comps: InnerLoopComp[] = innerComps.map(comp => ({
|
|
172
|
+
componentName: comp.name,
|
|
173
|
+
selector: buildCompSelector(comp),
|
|
174
|
+
propsExpr: buildStaticPropsExpr(comp.props),
|
|
175
|
+
}))
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
kind: 'component-rooted-inner-loop',
|
|
179
|
+
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
180
|
+
outerArrayExpr: elem.array,
|
|
181
|
+
outerParam: elem.param,
|
|
182
|
+
outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
|
|
183
|
+
innerArrayExpr: innerLoop.array,
|
|
184
|
+
innerParam: innerLoop.param,
|
|
185
|
+
innerPreludeStatements: innerLoop.mapPreamble ? [innerLoop.mapPreamble] : [],
|
|
186
|
+
depth: innerLoop.depth,
|
|
187
|
+
comps,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
137
191
|
/**
|
|
138
192
|
* Build the props object expression used by static-array child inits.
|
|
139
193
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plan types for `emitStaticArrayChildInits` — the
|
|
2
|
+
* Plan types for `emitStaticArrayChildInits` — the shapes that
|
|
3
3
|
* `static array` loops emit for child component initialisation:
|
|
4
4
|
*
|
|
5
5
|
* - `single-comp` — `loop.childComponent` ケース。一つの child component
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* `__iterEl.querySelector(...)` 経由で initChild。
|
|
9
9
|
* - `inner-loop-nested` — depth > 0 の `nestedComponents`。outer + inner
|
|
10
10
|
* forEach の二重ループで initChild。
|
|
11
|
+
* - `component-rooted-inner-loop`
|
|
12
|
+
* — outer の loop item root が **child component**
|
|
13
|
+
* (`loop.childComponent`) で、その JSX children に
|
|
14
|
+
* component の nested `.map()` を持つケース (#1725)。
|
|
15
|
+
* element offset では fragment-root passthrough の
|
|
16
|
+
* flatten 済み items に届かないため、document order
|
|
17
|
+
* の zip (`qsaChildScopes` + cursor) で initChild。
|
|
11
18
|
*
|
|
12
19
|
* All decisions (selector, propsExpr, offset expressions) are resolved at
|
|
13
20
|
* build time so the stringifier becomes a deterministic walk.
|
|
@@ -129,9 +136,48 @@ export interface InnerLoopNestedInitPlan {
|
|
|
129
136
|
comps: readonly InnerLoopComp[]
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Plan for an inner `.map()` of components living inside a **component-rooted**
|
|
141
|
+
* outer loop item (#1725). The outer loop body is a single child component
|
|
142
|
+
* (`loop.childComponent`, e.g. a `SelectGroup` passthrough) whose JSX
|
|
143
|
+
* `children` contain a nested `.map()` of components (e.g. `SelectItem`).
|
|
144
|
+
*
|
|
145
|
+
* `inner-loop-nested` can't be reused here: it addresses inner components via
|
|
146
|
+
* `containerVar.children[outerOffset]`, which assumes the outer loop item is a
|
|
147
|
+
* DOM **element**. A fragment-rooted passthrough component (`<>{children}</>`)
|
|
148
|
+
* emits no wrapper element, so its rendered items are flattened directly under
|
|
149
|
+
* the parent container with no per-group element to index.
|
|
150
|
+
*
|
|
151
|
+
* Instead this shape zips the inner component scopes — found in document order
|
|
152
|
+
* via `qsaChildScopes(container, <selector>)` — against the flattened
|
|
153
|
+
* `outer.forEach(o => inner.forEach(i => ...))` iteration. Document order is
|
|
154
|
+
* the SSR render order, so position `__ci++` pairs each scope with its data
|
|
155
|
+
* item regardless of whether the outer component root is an element or a
|
|
156
|
+
* fragment.
|
|
157
|
+
*/
|
|
158
|
+
export interface ComponentRootedInnerLoopInitPlan {
|
|
159
|
+
kind: 'component-rooted-inner-loop'
|
|
160
|
+
containerVar: string
|
|
161
|
+
/** Outer loop's array expression. */
|
|
162
|
+
outerArrayExpr: string
|
|
163
|
+
outerParam: string
|
|
164
|
+
/** Outer `.map()` callback preamble locals (#1064). */
|
|
165
|
+
outerPreludeStatements: PreludeStatements
|
|
166
|
+
/** Inner loop's array expression (references the outer param). */
|
|
167
|
+
innerArrayExpr: string
|
|
168
|
+
innerParam: string
|
|
169
|
+
/** Inner `.map()` callback preamble locals (#1064). */
|
|
170
|
+
innerPreludeStatements: PreludeStatements
|
|
171
|
+
/** Depth used in the leading comment line. */
|
|
172
|
+
depth: number
|
|
173
|
+
/** Per-component initialisers emitted inside the inner forEach body. */
|
|
174
|
+
comps: readonly InnerLoopComp[]
|
|
175
|
+
}
|
|
176
|
+
|
|
132
177
|
export type StaticArrayChildInitPlan =
|
|
133
178
|
| SingleCompInitPlan
|
|
134
179
|
| OuterNestedInitPlan
|
|
135
180
|
| InnerLoopNestedInitPlan
|
|
181
|
+
| ComponentRootedInnerLoopInitPlan
|
|
136
182
|
|
|
137
183
|
export type StaticArrayChildInitsPlan = readonly StaticArrayChildInitPlan[]
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
import type {
|
|
53
|
+
ComponentRootedInnerLoopInitPlan,
|
|
53
54
|
InnerLoopNestedInitPlan,
|
|
54
55
|
OuterNestedInitPlan,
|
|
55
56
|
SingleCompInitPlan,
|
|
@@ -78,6 +79,9 @@ function stringifyOne(lines: string[], plan: StaticArrayChildInitPlan): void {
|
|
|
78
79
|
case 'inner-loop-nested':
|
|
79
80
|
emitInnerLoopNested(lines, plan)
|
|
80
81
|
break
|
|
82
|
+
case 'component-rooted-inner-loop':
|
|
83
|
+
emitComponentRootedInnerLoop(lines, plan)
|
|
84
|
+
break
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
87
|
|
|
@@ -174,3 +178,68 @@ function emitInnerLoopNested(lines: string[], plan: InnerLoopNestedInitPlan): vo
|
|
|
174
178
|
lines.push(` }`)
|
|
175
179
|
lines.push('')
|
|
176
180
|
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Emit shape for `component-rooted-inner-loop` (#1725):
|
|
184
|
+
*
|
|
185
|
+
* <i>// Initialize component-rooted inner-loop components (depth N)
|
|
186
|
+
* <i>if (<container>) {
|
|
187
|
+
* <i> const <scopes_c> = qsaChildScopes(<container>, <selector_c>) // per comp
|
|
188
|
+
* <i> let <cursor_c> = 0
|
|
189
|
+
* <i> <outerArr>.forEach((<outerParam>) => {
|
|
190
|
+
* <i> <outerPreludeStatements*>
|
|
191
|
+
* <i> <innerArr>.forEach((<innerParam>) => {
|
|
192
|
+
* <i> <innerPreludeStatements*>
|
|
193
|
+
* <i> const <compEl_c> = <scopes_c>[<cursor_c>++] // per comp
|
|
194
|
+
* <i> if (<compEl_c>) initChild('<name>', <compEl_c>, <props>)
|
|
195
|
+
* <i> })
|
|
196
|
+
* <i> })
|
|
197
|
+
* <i>}
|
|
198
|
+
*
|
|
199
|
+
* The scopes are queried once over the whole container and consumed in
|
|
200
|
+
* document order by a per-component cursor, so the SSR render order
|
|
201
|
+
* (outer-then-inner, depth-first) pairs each scope with its data item
|
|
202
|
+
* whether the outer component root is an element or a fragment.
|
|
203
|
+
*/
|
|
204
|
+
function emitComponentRootedInnerLoop(lines: string[], plan: ComponentRootedInnerLoopInitPlan): void {
|
|
205
|
+
const {
|
|
206
|
+
containerVar,
|
|
207
|
+
outerArrayExpr,
|
|
208
|
+
outerParam,
|
|
209
|
+
outerPreludeStatements,
|
|
210
|
+
innerArrayExpr,
|
|
211
|
+
innerParam,
|
|
212
|
+
innerPreludeStatements,
|
|
213
|
+
depth,
|
|
214
|
+
comps,
|
|
215
|
+
} = plan
|
|
216
|
+
// A single comp uses the bare `__compScopes` / `__ci` names; multiple
|
|
217
|
+
// comps (e.g. `{items.map(it => <><A/><B/></>)}`) get index suffixes so
|
|
218
|
+
// each keeps its own document-order cursor.
|
|
219
|
+
const scopesVar = (i: number) => (comps.length > 1 ? `__compScopes${i}` : '__compScopes')
|
|
220
|
+
const cursorVar = (i: number) => (comps.length > 1 ? `__ci${i}` : '__ci')
|
|
221
|
+
const compElVar = (i: number) => (comps.length > 1 ? `__compEl${i}` : '__compEl')
|
|
222
|
+
|
|
223
|
+
lines.push(` // Initialize component-rooted inner-loop components (depth ${depth})`)
|
|
224
|
+
lines.push(` if (${containerVar}) {`)
|
|
225
|
+
comps.forEach((comp, i) => {
|
|
226
|
+
lines.push(` const ${scopesVar(i)} = qsaChildScopes(${containerVar}, ${comp.selector})`)
|
|
227
|
+
lines.push(` let ${cursorVar(i)} = 0`)
|
|
228
|
+
})
|
|
229
|
+
lines.push(` ${outerArrayExpr}.forEach((${outerParam}) => {`)
|
|
230
|
+
for (const stmt of outerPreludeStatements) {
|
|
231
|
+
lines.push(` ${stmt}`)
|
|
232
|
+
}
|
|
233
|
+
lines.push(` ${innerArrayExpr}.forEach((${innerParam}) => {`)
|
|
234
|
+
for (const stmt of innerPreludeStatements) {
|
|
235
|
+
lines.push(` ${stmt}`)
|
|
236
|
+
}
|
|
237
|
+
comps.forEach((comp, i) => {
|
|
238
|
+
lines.push(` const ${compElVar(i)} = ${scopesVar(i)}[${cursorVar(i)}++]`)
|
|
239
|
+
lines.push(` if (${compElVar(i)}) initChild('${nameForRegistryRef(comp.componentName)}', ${compElVar(i)}, ${comp.propsExpr})`)
|
|
240
|
+
})
|
|
241
|
+
lines.push(` })`)
|
|
242
|
+
lines.push(` })`)
|
|
243
|
+
lines.push(` }`)
|
|
244
|
+
lines.push('')
|
|
245
|
+
}
|
|
@@ -550,6 +550,13 @@ export interface RestAttrElement {
|
|
|
550
550
|
slotId: string
|
|
551
551
|
/** The spread source expression (e.g., 'rest', 'props') */
|
|
552
552
|
source: string
|
|
553
|
-
/**
|
|
553
|
+
/**
|
|
554
|
+
* Prop SOURCE KEYS already consumed by the component (exclude from
|
|
555
|
+
* applyRestAttrs). For the destructured `...rest` form this is the
|
|
556
|
+
* destructured param names (the JS rest-exclusion set) unioned with any
|
|
557
|
+
* statically-set attr names; applyRestAttrs filters `source[key]` by these
|
|
558
|
+
* so it neither re-binds separately-wired events nor re-emits consumed
|
|
559
|
+
* props under their raw key. See collect-elements.ts for the rationale.
|
|
560
|
+
*/
|
|
554
561
|
excludeKeys: string[]
|
|
555
562
|
}
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -1050,6 +1050,7 @@ function transformComponentElement(
|
|
|
1050
1050
|
children,
|
|
1051
1051
|
template: name.toLowerCase(),
|
|
1052
1052
|
slotId,
|
|
1053
|
+
...(isDynamicTagLocal(name, ctx) ? { dynamicTag: true } : {}),
|
|
1053
1054
|
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
1054
1055
|
}
|
|
1055
1056
|
}
|
|
@@ -1079,6 +1080,7 @@ function transformSelfClosingComponent(
|
|
|
1079
1080
|
children: [],
|
|
1080
1081
|
template: name.toLowerCase(),
|
|
1081
1082
|
slotId,
|
|
1083
|
+
...(isDynamicTagLocal(name, ctx) ? { dynamicTag: true } : {}),
|
|
1082
1084
|
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
1083
1085
|
}
|
|
1084
1086
|
}
|
|
@@ -3959,6 +3961,73 @@ function findLocalConst(name: string, ctx: TransformContext) {
|
|
|
3959
3961
|
return pool[pool.length - 1]
|
|
3960
3962
|
}
|
|
3961
3963
|
|
|
3964
|
+
/**
|
|
3965
|
+
* Detect a PascalCase JSX tag that is really a *dynamic tag* local
|
|
3966
|
+
* (`const Tag = children.tag`) rather than a component reference.
|
|
3967
|
+
*
|
|
3968
|
+
* Such a "component" has no registrable template — the tag is chosen at
|
|
3969
|
+
* runtime. The Go template adapter consumes the resulting `dynamicTag`
|
|
3970
|
+
* flag to lower the node to a children passthrough so its dead branch
|
|
3971
|
+
* registers cleanly (Hono/CSR/Mojo ignore the flag).
|
|
3972
|
+
*
|
|
3973
|
+
* A name qualifies only when (a) a `const <name> = <expr>.tag` binding
|
|
3974
|
+
* exists somewhere in the source — at any nesting depth, since the
|
|
3975
|
+
* canonical pattern lives inside an `if (isValidElement(children)) {…}`
|
|
3976
|
+
* block and never reaches the analyzer's `localConstants` (which only
|
|
3977
|
+
* collects component-body-level bindings) — AND (b) the name is NOT a
|
|
3978
|
+
* JSX-producing local (those are tracked in the analyzer's jsx* /
|
|
3979
|
+
* inlineable sets). The `.tag` initializer check already excludes real
|
|
3980
|
+
* imported components (their binding is an import, not a `.tag` const)
|
|
3981
|
+
* and local component factories like `const Foo = () => <div/>` (an
|
|
3982
|
+
* arrow initializer, not a `.tag` access); the jsx* guards are a
|
|
3983
|
+
* belt-and-suspenders second line. Each set is guarded defensively in
|
|
3984
|
+
* case it is absent on a given analyzer context.
|
|
3985
|
+
*/
|
|
3986
|
+
function isDynamicTagLocal(name: string, ctx: TransformContext): boolean {
|
|
3987
|
+
if (!hasDynamicTagBinding(name, ctx.sourceFile)) return false
|
|
3988
|
+
const a = ctx.analyzer
|
|
3989
|
+
if (a.jsxConstants?.has(name)) return false
|
|
3990
|
+
if (a.jsxFunctions?.has(name)) return false
|
|
3991
|
+
if (a.jsxMultiReturnFunctions?.has(name)) return false
|
|
3992
|
+
if (a.inlineableJsxConsts?.has(name)) return false
|
|
3993
|
+
return true
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
/**
|
|
3997
|
+
* True when the source contains a `const <name> = <expr>.tag` (the
|
|
3998
|
+
* dynamic-tag pattern), at any nesting depth. The initializer may be
|
|
3999
|
+
* wrapped in an `as`/`satisfies`/parenthesized cast (`children.tag as any`).
|
|
4000
|
+
*/
|
|
4001
|
+
function hasDynamicTagBinding(name: string, sourceFile: ts.SourceFile): boolean {
|
|
4002
|
+
let found = false
|
|
4003
|
+
const visit = (node: ts.Node): void => {
|
|
4004
|
+
if (found) return
|
|
4005
|
+
if (
|
|
4006
|
+
ts.isVariableDeclaration(node) &&
|
|
4007
|
+
ts.isIdentifier(node.name) &&
|
|
4008
|
+
node.name.text === name &&
|
|
4009
|
+
node.initializer
|
|
4010
|
+
) {
|
|
4011
|
+
let init: ts.Expression = node.initializer
|
|
4012
|
+
while (
|
|
4013
|
+
ts.isAsExpression(init) ||
|
|
4014
|
+
ts.isSatisfiesExpression(init) ||
|
|
4015
|
+
ts.isParenthesizedExpression(init) ||
|
|
4016
|
+
ts.isNonNullExpression(init)
|
|
4017
|
+
) {
|
|
4018
|
+
init = init.expression
|
|
4019
|
+
}
|
|
4020
|
+
if (ts.isPropertyAccessExpression(init) && init.name.text === 'tag') {
|
|
4021
|
+
found = true
|
|
4022
|
+
return
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
ts.forEachChild(node, visit)
|
|
4026
|
+
}
|
|
4027
|
+
visit(sourceFile)
|
|
4028
|
+
return found
|
|
4029
|
+
}
|
|
4030
|
+
|
|
3962
4031
|
/**
|
|
3963
4032
|
* Resolve a `className={ident}` reference where `ident` is a local
|
|
3964
4033
|
* const bound to a template literal — typically the cva-style pattern
|
package/src/types.ts
CHANGED
|
@@ -674,6 +674,15 @@ export interface IRComponent {
|
|
|
674
674
|
children: IRNode[]
|
|
675
675
|
template: string // Reference to partial
|
|
676
676
|
slotId: string | null // For components with event handlers
|
|
677
|
+
/**
|
|
678
|
+
* True when `name` is a dynamic-tag local (`const Tag = children.tag`)
|
|
679
|
+
* rather than a real component reference. Such a "component" has no
|
|
680
|
+
* registrable template — it is a runtime-chosen tag. Consumed ONLY by
|
|
681
|
+
* the Go template adapter, which lowers it to a children passthrough so
|
|
682
|
+
* the dead branch registers cleanly (Hono/CSR/Mojo ignore this flag).
|
|
683
|
+
* Omitted (undefined) for ordinary components to keep IR diffs minimal.
|
|
684
|
+
*/
|
|
685
|
+
dynamicTag?: boolean
|
|
677
686
|
loc: SourceLocation
|
|
678
687
|
}
|
|
679
688
|
|