@barefootjs/jsx 0.18.3 → 0.18.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/parsed-expr-emitter.d.ts +41 -2
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +2 -1
- 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 +172 -48
- 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/event-delegation.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/plan/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 +1 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +9 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +25 -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/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +1 -1
- package/src/__tests__/event-delegation-index-param.test.ts +130 -0
- package/src/__tests__/expression-parser.test.ts +38 -0
- package/src/__tests__/ir-const-resolution.test.ts +1 -1
- package/src/__tests__/ir-provider.test.ts +2 -2
- package/src/__tests__/ir-walker.test.ts +1 -0
- package/src/__tests__/materialize-getter-calls.test.ts +1 -0
- package/src/__tests__/tagged-template-interleave.test.ts +2 -2
- package/src/adapters/parsed-expr-emitter.ts +76 -1
- package/src/expression-parser.ts +59 -25
- package/src/index.ts +1 -1
- package/src/ir-to-client-js/collect-elements.ts +3 -0
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +10 -0
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +31 -7
- package/src/ir-to-client-js/csr-substitute.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +57 -11
- package/src/ir-to-client-js/types.ts +10 -0
- package/src/ir-to-client-js/utils.ts +34 -1
- package/src/jsx-to-ir.ts +110 -13
- package/src/types.ts +68 -0
|
@@ -10,7 +10,7 @@ import { nameForRegistryRef } from './component-scope.ts'
|
|
|
10
10
|
import { assertNever } from './walker.ts'
|
|
11
11
|
import { buildSignalMemoEnv, csrSubstitute, applyPropsRewrite, type CsrEnv } from './csr-substitute.ts'
|
|
12
12
|
import type { ClientJsContext } from './types.ts'
|
|
13
|
-
import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE } from '@barefootjs/shared'
|
|
13
|
+
import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE, escapeHtml } from '@barefootjs/shared'
|
|
14
14
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -145,6 +145,42 @@ function applyIterationShape(
|
|
|
145
145
|
callbackParam: `(${node.param})`,
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
+
// `objectIteration` (#2168 object-entries-map): reconstruct the STATIC
|
|
149
|
+
// `Object.entries/keys/values(x)` call the compiler stripped at IR-build
|
|
150
|
+
// time (`isObjectIteratorCall`, `jsx-to-ir.ts`) — `arrayExpr` here is just
|
|
151
|
+
// `x` (the plain object), so the client, unlike a template adapter,
|
|
152
|
+
// re-wraps it in real JS to get the actual entries/keys/values array
|
|
153
|
+
// (this runs in a real JS engine, so no per-language lowering is needed).
|
|
154
|
+
//
|
|
155
|
+
// Unlike the array `iterationShape` case, the 'entries' ARRAY wrap does
|
|
156
|
+
// NOT require `node.index` — that field is only populated for a CLEAN
|
|
157
|
+
// 2-identifier destructure (`([word, n]) => …`); an elided/nested
|
|
158
|
+
// pattern (`([, cfg]) => …`) falls through to the generic `paramBindings`
|
|
159
|
+
// machinery instead (`node.param` stays the raw destructure TEXT, e.g.
|
|
160
|
+
// `"[, cfg]"`, which is already a syntactically valid callback param that
|
|
161
|
+
// correctly destructures a `[key, value]` pair — see the trailing
|
|
162
|
+
// fallback below). Only the ARRAY needs wrapping in that case; the
|
|
163
|
+
// callback param is unaffected either way.
|
|
164
|
+
if (node.objectIteration === 'entries') {
|
|
165
|
+
return {
|
|
166
|
+
array: `Object.entries(${arrayExpr})`,
|
|
167
|
+
callbackParam: node.index
|
|
168
|
+
? `([${node.index}, ${node.param}])`
|
|
169
|
+
: `(${node.param}${indexParam})`,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (node.objectIteration === 'keys') {
|
|
173
|
+
return {
|
|
174
|
+
array: `Object.keys(${arrayExpr})`,
|
|
175
|
+
callbackParam: `(${node.param})`,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (node.objectIteration === 'values') {
|
|
179
|
+
return {
|
|
180
|
+
array: `Object.values(${arrayExpr})`,
|
|
181
|
+
callbackParam: `(${node.param})`,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
148
184
|
return { array: arrayExpr, callbackParam: `(${node.param}${indexParam})` }
|
|
149
185
|
}
|
|
150
186
|
|
|
@@ -313,7 +349,7 @@ function renderTemplateAttrPart(
|
|
|
313
349
|
case 'boolean-attr':
|
|
314
350
|
return attrName
|
|
315
351
|
case 'literal':
|
|
316
|
-
return `${attrName}="${v.value}"`
|
|
352
|
+
return `${attrName}="${escapeHtml(v.value)}"`
|
|
317
353
|
case 'expression': {
|
|
318
354
|
const valExpr = wrap(v.expr)
|
|
319
355
|
return templateAttrExpr(attrName, valExpr, v.presenceOrUndefined)
|
|
@@ -550,7 +586,9 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
550
586
|
}
|
|
551
587
|
|
|
552
588
|
case 'text':
|
|
553
|
-
|
|
589
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
590
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
591
|
+
return escapeHtml(node.value)
|
|
554
592
|
|
|
555
593
|
case 'expression':
|
|
556
594
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -757,7 +795,7 @@ export function buildLoopSkeletonTemplate(node: IRNode, safe: LoopSkeletonSafeSl
|
|
|
757
795
|
const v = a.value
|
|
758
796
|
switch (v.kind) {
|
|
759
797
|
case 'literal':
|
|
760
|
-
attrParts.push(`${toHtmlAttrName(a.name)}="${v.value}"`)
|
|
798
|
+
attrParts.push(`${toHtmlAttrName(a.name)}="${escapeHtml(v.value)}"`)
|
|
761
799
|
break
|
|
762
800
|
case 'boolean-attr':
|
|
763
801
|
attrParts.push(toHtmlAttrName(a.name))
|
|
@@ -796,7 +834,9 @@ export function buildLoopSkeletonTemplate(node: IRNode, safe: LoopSkeletonSafeSl
|
|
|
796
834
|
}
|
|
797
835
|
|
|
798
836
|
case 'text':
|
|
799
|
-
|
|
837
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
838
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
839
|
+
return escapeHtml(node.value)
|
|
800
840
|
|
|
801
841
|
case 'expression':
|
|
802
842
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -870,7 +910,9 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Set<stri
|
|
|
870
910
|
}
|
|
871
911
|
|
|
872
912
|
case 'text':
|
|
873
|
-
|
|
913
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
914
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
915
|
+
return escapeHtml(node.value)
|
|
874
916
|
|
|
875
917
|
case 'expression':
|
|
876
918
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -1228,7 +1270,7 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1228
1270
|
return templateAttrExpr(keyName, transformExpr(tmplStr))
|
|
1229
1271
|
}
|
|
1230
1272
|
case 'literal':
|
|
1231
|
-
return `${keyName}="${v.value}"`
|
|
1273
|
+
return `${keyName}="${escapeHtml(v.value)}"`
|
|
1232
1274
|
default:
|
|
1233
1275
|
return ''
|
|
1234
1276
|
}
|
|
@@ -1238,7 +1280,7 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1238
1280
|
case 'boolean-attr':
|
|
1239
1281
|
return attrName
|
|
1240
1282
|
case 'literal':
|
|
1241
|
-
return `${attrName}="${v.value}"`
|
|
1283
|
+
return `${attrName}="${escapeHtml(v.value)}"`
|
|
1242
1284
|
case 'expression':
|
|
1243
1285
|
return templateAttrExpr(attrName, transformExpr(v.expr, v.templateExpr), v.presenceOrUndefined)
|
|
1244
1286
|
case 'template': {
|
|
@@ -1269,7 +1311,9 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1269
1311
|
}
|
|
1270
1312
|
|
|
1271
1313
|
case 'text':
|
|
1272
|
-
|
|
1314
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
1315
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
1316
|
+
return escapeHtml(node.value)
|
|
1273
1317
|
|
|
1274
1318
|
case 'expression':
|
|
1275
1319
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -1806,7 +1850,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
1806
1850
|
case 'boolean-attr':
|
|
1807
1851
|
return attrName
|
|
1808
1852
|
case 'literal':
|
|
1809
|
-
return `${attrName}="${v.value}"`
|
|
1853
|
+
return `${attrName}="${escapeHtml(v.value)}"`
|
|
1810
1854
|
case 'expression':
|
|
1811
1855
|
return templateAttrExpr(attrName, transformExpr(v.expr, v.templateExpr), v.presenceOrUndefined)
|
|
1812
1856
|
case 'template': {
|
|
@@ -1837,7 +1881,9 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
1837
1881
|
}
|
|
1838
1882
|
|
|
1839
1883
|
case 'text':
|
|
1840
|
-
|
|
1884
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
1885
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
1886
|
+
return escapeHtml(node.value)
|
|
1841
1887
|
|
|
1842
1888
|
case 'expression':
|
|
1843
1889
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -283,6 +283,16 @@ export interface LoopCore {
|
|
|
283
283
|
* (#1448 Tier B). Threaded from `IRLoop.iterationShape`.
|
|
284
284
|
*/
|
|
285
285
|
iterationShape?: 'entries' | 'keys'
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Object iteration shape from the STATIC `Object.entries(x)` /
|
|
289
|
+
* `.keys(x)` / `.values(x)` call form (#2168 object-entries-map) —
|
|
290
|
+
* distinct from {@link iterationShape} for the same reason `IRLoop`'s
|
|
291
|
+
* own field is (see that field's docstring, `packages/jsx/src/types.ts`):
|
|
292
|
+
* `x` is a plain object, not an array. Threaded from
|
|
293
|
+
* `IRLoop.objectIteration`.
|
|
294
|
+
*/
|
|
295
|
+
objectIteration?: 'entries' | 'keys' | 'values'
|
|
286
296
|
}
|
|
287
297
|
|
|
288
298
|
/**
|
|
@@ -147,6 +147,38 @@ export function exhaustiveAttrValue(value: never): never {
|
|
|
147
147
|
throw new Error(`Unhandled AttrValue kind: ${JSON.stringify(value)}`)
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Reconstruct the `Object.entries/keys/values(x)` call the compiler
|
|
152
|
+
* stripped off at IR-build time (`isObjectIteratorCall`, `jsx-to-ir.ts`)
|
|
153
|
+
* for the CLIENT's array expression — unlike a template adapter, the
|
|
154
|
+
* client runs real JS, so it just re-wraps the plain object `x` (the
|
|
155
|
+
* loop's `array`) to get back the actual iterable `mapArray` needs.
|
|
156
|
+
*
|
|
157
|
+
* Deliberately does NOT also handle the array-only `iterationShape`
|
|
158
|
+
* (`arr.entries()`/`.keys()`) — that shape's `mapArray` callback already
|
|
159
|
+
* gets what it needs from `mapArray`'s own native `(value, index)`
|
|
160
|
+
* signature (the compiler synthesizes `param`/`index` to match those two
|
|
161
|
+
* positions directly), so wrapping the array there would double up. See
|
|
162
|
+
* `IRLoop.objectIteration`'s docstring (`types.ts`) for why the two
|
|
163
|
+
* fields are distinct, and `applyIterationShape` in `html-template.ts`
|
|
164
|
+
* (a different consumer — the CSR template-literal's own inline `.map()`
|
|
165
|
+
* — which does handle both fields, since its shape doesn't reuse
|
|
166
|
+
* `mapArray`'s positional signature).
|
|
167
|
+
*
|
|
168
|
+
* The callback's OWN head (`__bfItem` vs a plain param name) is unrelated
|
|
169
|
+
* and unaffected — that's `destructureLoopParam`'s job
|
|
170
|
+
* (`control-flow/shared.ts`), driven entirely by `param`/`paramBindings`.
|
|
171
|
+
*/
|
|
172
|
+
export function applyObjectIterationWrap(
|
|
173
|
+
node: { objectIteration?: 'entries' | 'keys' | 'values' },
|
|
174
|
+
arrayExpr: string,
|
|
175
|
+
): string {
|
|
176
|
+
if (node.objectIteration === 'entries') return `Object.entries(${arrayExpr})`
|
|
177
|
+
if (node.objectIteration === 'keys') return `Object.keys(${arrayExpr})`
|
|
178
|
+
if (node.objectIteration === 'values') return `Object.values(${arrayExpr})`
|
|
179
|
+
return arrayExpr
|
|
180
|
+
}
|
|
181
|
+
|
|
150
182
|
/**
|
|
151
183
|
* Build the chained array expression for reconcileList. Thin
|
|
152
184
|
* adapter over `buildLoopChainExpr` that unpacks the collected
|
|
@@ -156,12 +188,13 @@ export function exhaustiveAttrValue(value: never): never {
|
|
|
156
188
|
* branch preserves the chain (#1434).
|
|
157
189
|
*/
|
|
158
190
|
export function buildChainedArrayExpr(elem: TopLevelLoop | BranchLoop): string {
|
|
159
|
-
|
|
191
|
+
const chained = buildLoopChainExpr({
|
|
160
192
|
base: elem.array,
|
|
161
193
|
sortComparator: elem.sortComparator,
|
|
162
194
|
filterPredicate: elem.filterPredicate,
|
|
163
195
|
chainOrder: elem.chainOrder,
|
|
164
196
|
})
|
|
197
|
+
return applyObjectIterationWrap(elem, chained)
|
|
165
198
|
}
|
|
166
199
|
|
|
167
200
|
/**
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -48,6 +48,7 @@ import { resolveFreeRefs, isNameBound as isNameBoundInEnv, type BindingEnvironme
|
|
|
48
48
|
import { computeFileScope } from './ir-to-client-js/component-scope.ts'
|
|
49
49
|
import { extractFreeIdentifiersFromNode, initializerShapeContainsJsx } from './analyzer.ts'
|
|
50
50
|
import { iterateJsTokens, replaceInExprContexts } from './scanner/js-scanner.ts'
|
|
51
|
+
import { toHTMLAttrName, decodeEntities } from '@barefootjs/shared'
|
|
51
52
|
|
|
52
53
|
// =============================================================================
|
|
53
54
|
// Transform Context
|
|
@@ -81,6 +82,17 @@ interface TransformContext {
|
|
|
81
82
|
_destructuredPropNames?: Set<string> | null
|
|
82
83
|
/** Active loop parameter names for slotId assignment to loop-param-dependent expressions */
|
|
83
84
|
loopParams: Set<string>
|
|
85
|
+
/**
|
|
86
|
+
* Count of enclosing `.map()` loops (0 = outermost), incremented/
|
|
87
|
+
* decremented in lockstep with entering/leaving `transformMapCall`.
|
|
88
|
+
* Unlike `loopParams` (a name Set that can gain several entries for
|
|
89
|
+
* ONE loop level via destructuring), this is a plain per-level
|
|
90
|
+
* counter — the single source of truth `IRLoop.depth` is stamped
|
|
91
|
+
* from, so every adapter's `data-key`/`data-key-N` suffix derives
|
|
92
|
+
* from one IR-computed value instead of each adapter re-deriving
|
|
93
|
+
* nesting depth its own way (#2168 nested-loop-outer-binding).
|
|
94
|
+
*/
|
|
95
|
+
loopDepth: number
|
|
84
96
|
/** Counter for async boundary IDs (a0, a1, ...) */
|
|
85
97
|
asyncIdCounter: number
|
|
86
98
|
/** Counter for <Region> structural index (0, 1, ...) within a file. */
|
|
@@ -394,6 +406,7 @@ function createTransformContext(analyzer: AnalyzerContext): TransformContext {
|
|
|
394
406
|
isRoot: true,
|
|
395
407
|
insideComponentChildren: false,
|
|
396
408
|
loopParams: new Set(),
|
|
409
|
+
loopDepth: 0,
|
|
397
410
|
patterns: {
|
|
398
411
|
signals: analyzer.signals.map(s => ({
|
|
399
412
|
getter: s.getter,
|
|
@@ -1562,7 +1575,12 @@ function transformText(node: ts.JsxText, ctx: TransformContext): IRText | null {
|
|
|
1562
1575
|
|
|
1563
1576
|
return {
|
|
1564
1577
|
type: 'text',
|
|
1565
|
-
|
|
1578
|
+
// JSX decodes character references at parse time (`©` IS the
|
|
1579
|
+
// text `©`), so the IR carries the DECODED value — the semantics —
|
|
1580
|
+
// and each adapter re-escapes for its own emission context.
|
|
1581
|
+
// Decode AFTER whitespace normalization: ` ` yields U+00A0,
|
|
1582
|
+
// which `\s+` would otherwise collapse into a plain space.
|
|
1583
|
+
value: decodeEntities(text),
|
|
1566
1584
|
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
1567
1585
|
}
|
|
1568
1586
|
}
|
|
@@ -2482,6 +2500,31 @@ function isIteratorShapeCall(
|
|
|
2482
2500
|
return { array: node.expression.expression, shape: name }
|
|
2483
2501
|
}
|
|
2484
2502
|
|
|
2503
|
+
/**
|
|
2504
|
+
* Check if a node is the STATIC `Object.entries(x)` / `Object.keys(x)` /
|
|
2505
|
+
* `Object.values(x)` call form (#2168 object-entries-map) — the
|
|
2506
|
+
* one-argument form where `x` is a plain object/Record being iterated,
|
|
2507
|
+
* as opposed to {@link isIteratorShapeCall}'s zero-arg instance-method
|
|
2508
|
+
* form (`arr.entries()`) on an actual array. Returns the object
|
|
2509
|
+
* expression (any expression — `props.x`, `x ?? {}`, not just a bare
|
|
2510
|
+
* identifier) and the iteration shape so `transformMapCall` can strip
|
|
2511
|
+
* the `Object.<method>(...)` wrapper and record it on the IRLoop as
|
|
2512
|
+
* `objectIteration` (see that field's docstring in `types.ts` for why
|
|
2513
|
+
* this is a distinct field from `iterationShape`, not a shared one).
|
|
2514
|
+
*/
|
|
2515
|
+
function isObjectIteratorCall(
|
|
2516
|
+
node: ts.Expression,
|
|
2517
|
+
): { object: ts.Expression; shape: 'entries' | 'keys' | 'values' } | null {
|
|
2518
|
+
if (!ts.isCallExpression(node)) return null
|
|
2519
|
+
if (!ts.isPropertyAccessExpression(node.expression)) return null
|
|
2520
|
+
if (!ts.isIdentifier(node.expression.expression)) return null
|
|
2521
|
+
if (node.expression.expression.text !== 'Object') return null
|
|
2522
|
+
if (node.arguments.length !== 1) return null
|
|
2523
|
+
const name = node.expression.name.text
|
|
2524
|
+
if (name !== 'entries' && name !== 'keys' && name !== 'values') return null
|
|
2525
|
+
return { object: node.arguments[0], shape: name }
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2485
2528
|
type SortExtractionResult = {
|
|
2486
2529
|
result: IRLoopSort | null
|
|
2487
2530
|
unsupportedReason?: string
|
|
@@ -3245,6 +3288,10 @@ function transformMapCall(
|
|
|
3245
3288
|
// Capture nesting depth before we register this map's own params.
|
|
3246
3289
|
// ctx.loopParams is populated by the *outer* map; if non-empty we are inside one.
|
|
3247
3290
|
const isNested = ctx.loopParams.size > 0
|
|
3291
|
+
// This loop's own depth (0 = outermost) is however many enclosing
|
|
3292
|
+
// loops are already active, captured before `ctx.loopDepth` below is
|
|
3293
|
+
// bumped for THIS loop's own descendants.
|
|
3294
|
+
const depth = ctx.loopDepth
|
|
3248
3295
|
|
|
3249
3296
|
const propAccess = node.expression as ts.PropertyAccessExpression
|
|
3250
3297
|
const mapSource = propAccess.expression
|
|
@@ -3270,6 +3317,7 @@ function transformMapCall(
|
|
|
3270
3317
|
let templateMapPreamble: string | undefined
|
|
3271
3318
|
let typedMapPreamble: string | undefined
|
|
3272
3319
|
let iterationShape: 'entries' | 'keys' | undefined
|
|
3320
|
+
let objectIteration: 'entries' | 'keys' | 'values' | undefined
|
|
3273
3321
|
|
|
3274
3322
|
// Helper to set both array and templateArray
|
|
3275
3323
|
const setArray = (node: ts.Expression) => {
|
|
@@ -3283,8 +3331,11 @@ function transformMapCall(
|
|
|
3283
3331
|
// adapters emit the right loop variable bindings. `.values()` is a
|
|
3284
3332
|
// no-op (same as plain `.map()`) so it's stripped but not recorded.
|
|
3285
3333
|
// The inner expression (after stripping) feeds into the standard
|
|
3286
|
-
// filter/sort chain detection below.
|
|
3287
|
-
|
|
3334
|
+
// filter/sort chain detection below. Widened to `ts.Expression` (not
|
|
3335
|
+
// narrowed to `mapSource`'s own `LeftHandSideExpression` type) since
|
|
3336
|
+
// `isObjectIteratorCall`'s stripped argument can be any expression
|
|
3337
|
+
// (`x ?? {}`, not just a `LeftHandSideExpression`).
|
|
3338
|
+
let chainSource: ts.Expression = mapSource
|
|
3288
3339
|
const iteratorInfo = isIteratorShapeCall(mapSource)
|
|
3289
3340
|
if (iteratorInfo) {
|
|
3290
3341
|
chainSource = iteratorInfo.array
|
|
@@ -3294,6 +3345,18 @@ function transformMapCall(
|
|
|
3294
3345
|
iterationShape = 'keys'
|
|
3295
3346
|
}
|
|
3296
3347
|
// 'values' is a no-op — same as plain .map()
|
|
3348
|
+
} else {
|
|
3349
|
+
// Detect the STATIC `Object.entries(x)` / `.keys(x)` / `.values(x)`
|
|
3350
|
+
// form (#2168 object-entries-map) — see `isObjectIteratorCall`'s and
|
|
3351
|
+
// `IRLoop.objectIteration`'s docstrings for why this is a SEPARATE
|
|
3352
|
+
// shape from the array-instance-method case above, not a shared one.
|
|
3353
|
+
// Unlike that case, `'values'` DOES need recording here (it isn't a
|
|
3354
|
+
// no-op: `x` itself isn't iterable as a plain object).
|
|
3355
|
+
const objectIteratorInfo = isObjectIteratorCall(mapSource)
|
|
3356
|
+
if (objectIteratorInfo) {
|
|
3357
|
+
chainSource = objectIteratorInfo.object
|
|
3358
|
+
objectIteration = objectIteratorInfo.shape
|
|
3359
|
+
}
|
|
3297
3360
|
}
|
|
3298
3361
|
|
|
3299
3362
|
const filterInfo = isFilterCall(chainSource)
|
|
@@ -3444,8 +3507,12 @@ function transformMapCall(
|
|
|
3444
3507
|
// `.entries()` synthesises `[index, value]` — when the callback
|
|
3445
3508
|
// destructures exactly two array elements, extract the names into
|
|
3446
3509
|
// `index` and `param` so the loop renders with proper bindings and
|
|
3447
|
-
// the BF104 destructure-param refusal doesn't fire.
|
|
3448
|
-
|
|
3510
|
+
// the BF104 destructure-param refusal doesn't fire. `Object.entries(x)`
|
|
3511
|
+
// (`objectIteration === 'entries'`) synthesises the SAME `[key,
|
|
3512
|
+
// value]` 2-tuple shape — `index` just holds a string key instead
|
|
3513
|
+
// of a numeric position — so it reuses this exact extraction.
|
|
3514
|
+
const isEntriesShape = iterationShape === 'entries' || objectIteration === 'entries'
|
|
3515
|
+
if (isEntriesShape && ts.isArrayBindingPattern(firstParam.name)) {
|
|
3449
3516
|
const elements = firstParam.name.elements.filter(
|
|
3450
3517
|
el => !ts.isOmittedExpression(el),
|
|
3451
3518
|
)
|
|
@@ -3484,7 +3551,7 @@ function transformMapCall(
|
|
|
3484
3551
|
}
|
|
3485
3552
|
}
|
|
3486
3553
|
}
|
|
3487
|
-
if (callback.parameters.length > 1 && iterationShape !== 'entries') {
|
|
3554
|
+
if (callback.parameters.length > 1 && iterationShape !== 'entries' && objectIteration !== 'entries') {
|
|
3488
3555
|
const secondParam = callback.parameters[1]
|
|
3489
3556
|
index = secondParam.name.getText(ctx.sourceFile)
|
|
3490
3557
|
if (secondParam.type) {
|
|
@@ -3503,6 +3570,7 @@ function transformMapCall(
|
|
|
3503
3570
|
ctx.loopParams.add(param)
|
|
3504
3571
|
}
|
|
3505
3572
|
if (index) ctx.loopParams.add(index)
|
|
3573
|
+
ctx.loopDepth++
|
|
3506
3574
|
|
|
3507
3575
|
// Logical control flow (`cond && <X/>`, `a ?? themeLogo()`) as the map
|
|
3508
3576
|
// body. This is not a JSX literal, ternary, or block, so without this
|
|
@@ -3625,6 +3693,7 @@ function transformMapCall(
|
|
|
3625
3693
|
ctx.loopParams.delete(param)
|
|
3626
3694
|
}
|
|
3627
3695
|
if (index) ctx.loopParams.delete(index)
|
|
3696
|
+
ctx.loopDepth--
|
|
3628
3697
|
}
|
|
3629
3698
|
|
|
3630
3699
|
// If no JSX children were found (e.g., callback returns a function call),
|
|
@@ -3704,6 +3773,16 @@ function transformMapCall(
|
|
|
3704
3773
|
!isSignalOrMemoArray(array, ctx)
|
|
3705
3774
|
&& !isDirectPropArray
|
|
3706
3775
|
&& !hasCalls
|
|
3776
|
+
// `objectIteration` (#2168 object-entries-map): `array` here is the
|
|
3777
|
+
// STRIPPED object expression (`Object.entries(x)`'s `x`), which can
|
|
3778
|
+
// itself be a static module-scope const object literal and would
|
|
3779
|
+
// otherwise satisfy every check above — but a plain OBJECT has no
|
|
3780
|
+
// `.forEach()`/`.map()` (the static-array client codegen's own
|
|
3781
|
+
// methods), unlike an actual array literal. Force the dynamic
|
|
3782
|
+
// `mapArray()` path instead, which this shape's client-JS array-expr
|
|
3783
|
+
// reconstruction (`applyObjectIterationWrap`, `ir-to-client-js/utils.ts`)
|
|
3784
|
+
// already handles correctly.
|
|
3785
|
+
&& !objectIteration
|
|
3707
3786
|
|
|
3708
3787
|
// Collect nested components for both static and dynamic arrays.
|
|
3709
3788
|
// Static arrays: needed for initChild hydration.
|
|
@@ -3743,6 +3822,8 @@ function transformMapCall(
|
|
|
3743
3822
|
sortComparator,
|
|
3744
3823
|
chainOrder,
|
|
3745
3824
|
iterationShape,
|
|
3825
|
+
objectIteration,
|
|
3826
|
+
depth,
|
|
3746
3827
|
clientOnly: isClientOnly || undefined,
|
|
3747
3828
|
mapPreamble,
|
|
3748
3829
|
templateMapPreamble,
|
|
@@ -4066,13 +4147,13 @@ function processAttributes(
|
|
|
4066
4147
|
|
|
4067
4148
|
if (!ts.isJsxAttribute(attr)) continue
|
|
4068
4149
|
|
|
4069
|
-
const
|
|
4150
|
+
const rawName = attr.name.getText(ctx.sourceFile)
|
|
4070
4151
|
|
|
4071
4152
|
// ref is captured separately (not pushed into `attrs`) because it never
|
|
4072
4153
|
// appears in the rendered HTML — it's a compile-time binding from the
|
|
4073
4154
|
// JSX call site to the runtime DOM element, surfaced via the IRElement
|
|
4074
4155
|
// `ref` field for the client-JS emitter.
|
|
4075
|
-
if (
|
|
4156
|
+
if (rawName === 'ref') {
|
|
4076
4157
|
if (attr.initializer && ts.isJsxExpression(attr.initializer) && attr.initializer.expression) {
|
|
4077
4158
|
reportJsxBranchLocalInCallback(attr.initializer.expression, ctx)
|
|
4078
4159
|
ref = ctx.getJS(attr.initializer.expression)
|
|
@@ -4084,13 +4165,13 @@ function processAttributes(
|
|
|
4084
4165
|
// they're wired up at hydration time (delegated event registration) and
|
|
4085
4166
|
// must not leak into rendered HTML. The DOM event name is lowercase
|
|
4086
4167
|
// (`click`, not `Click`), so strip the `on` prefix and downcase.
|
|
4087
|
-
if (/^on[A-Z]/.test(
|
|
4168
|
+
if (/^on[A-Z]/.test(rawName)) {
|
|
4088
4169
|
if (attr.initializer && ts.isJsxExpression(attr.initializer) && attr.initializer.expression) {
|
|
4089
|
-
const eventName =
|
|
4170
|
+
const eventName = rawName.slice(2).toLowerCase()
|
|
4090
4171
|
reportJsxBranchLocalInCallback(attr.initializer.expression, ctx)
|
|
4091
4172
|
events.push({
|
|
4092
4173
|
name: eventName,
|
|
4093
|
-
originalAttr:
|
|
4174
|
+
originalAttr: rawName,
|
|
4094
4175
|
handler: ctx.getJS(attr.initializer.expression),
|
|
4095
4176
|
loc: getSourceLocation(attr, ctx.sourceFile, ctx.filePath),
|
|
4096
4177
|
})
|
|
@@ -4098,6 +4179,19 @@ function processAttributes(
|
|
|
4098
4179
|
continue
|
|
4099
4180
|
}
|
|
4100
4181
|
|
|
4182
|
+
// Normalize the JSX prop spelling to the HTML/SVG attribute name ONCE,
|
|
4183
|
+
// here in Phase 1, so IRAttribute.name is already the name every
|
|
4184
|
+
// adapter emits verbatim (#2172): React-style HTML camelCase aliases
|
|
4185
|
+
// lower (`htmlFor` → `for`, `tabIndex` → `tabindex`, `readOnly` →
|
|
4186
|
+
// the BOOLEAN_ATTRS member `readonly`), SVG presentation attrs
|
|
4187
|
+
// kebab-case (`strokeWidth` → `stroke-width`), case-sensitive SVG XML
|
|
4188
|
+
// names (`viewBox`) and everything unknown (`data-*`, custom-element
|
|
4189
|
+
// attrs) pass through. Previously each adapter re-derived (at most)
|
|
4190
|
+
// `className` → `class` itself and every other alias leaked into the
|
|
4191
|
+
// emitted HTML as an unknown attribute the browser ignores. Intrinsic
|
|
4192
|
+
// elements only — component props (IRProp) keep the user's API names.
|
|
4193
|
+
const name = toHTMLAttrName(rawName)
|
|
4194
|
+
|
|
4101
4195
|
let value = getAttributeValue(attr, ctx)
|
|
4102
4196
|
let clientOnly: boolean | undefined
|
|
4103
4197
|
if (attr.initializer && ts.isJsxExpression(attr.initializer) && attr.initializer.expression) {
|
|
@@ -4143,9 +4237,12 @@ function getAttributeValue(attr: ts.JsxAttribute, ctx: TransformContext): AttrVa
|
|
|
4143
4237
|
return AttrValueOf.booleanAttr()
|
|
4144
4238
|
}
|
|
4145
4239
|
|
|
4146
|
-
// String literal: <div id="main"
|
|
4240
|
+
// String literal: <div id="main" />. JSX decodes character references
|
|
4241
|
+
// in quoted attribute values just like in text children, so the IR
|
|
4242
|
+
// carries the decoded string (`title="Fish & Chips"` IS the value
|
|
4243
|
+
// `Fish & Chips`); adapters re-escape on emit.
|
|
4147
4244
|
if (ts.isStringLiteral(attr.initializer)) {
|
|
4148
|
-
return AttrValueOf.literal(attr.initializer.text)
|
|
4245
|
+
return AttrValueOf.literal(decodeEntities(attr.initializer.text))
|
|
4149
4246
|
}
|
|
4150
4247
|
|
|
4151
4248
|
// Expression: <div class={className} />
|
package/src/types.ts
CHANGED
|
@@ -574,6 +574,74 @@ export interface IRLoop {
|
|
|
574
574
|
*/
|
|
575
575
|
iterationShape?: 'entries' | 'keys'
|
|
576
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Pre-`.map()` object iteration (#2168 object-entries-map). Distinct
|
|
579
|
+
* from {@link iterationShape}, which is scoped ENTIRELY to an array's
|
|
580
|
+
* own zero-arg `.entries()`/`.keys()`/`.values()` methods — those
|
|
581
|
+
* synthesize a real numeric index off the array's position, and every
|
|
582
|
+
* adapter's consumption of `iterationShape` assumes an actual
|
|
583
|
+
* array/slice underneath.
|
|
584
|
+
*
|
|
585
|
+
* `objectIteration` instead records the STATIC `Object.entries(x)` /
|
|
586
|
+
* `Object.keys(x)` / `Object.values(x)` call form, where `x` is a
|
|
587
|
+
* plain object/Record (not an array): the "index" bound for `'entries'`
|
|
588
|
+
* is a STRING KEY, not a numeric position, and the collection an
|
|
589
|
+
* adapter must iterate is its native map/dict/hash type, not an
|
|
590
|
+
* array/slice. `transformMapCall` strips the `Object.<method>(...)`
|
|
591
|
+
* wrapper the same way it strips `arr.entries()` — `array`/`arrayParsed`
|
|
592
|
+
* end up holding just `x` — and records the shape here so each
|
|
593
|
+
* adapter's loop renderer picks the right native construct:
|
|
594
|
+
*
|
|
595
|
+
* - `'entries'` → both `index` (bound to the KEY) and `param` (bound
|
|
596
|
+
* to the VALUE), synthesized from the 2-element destructure the
|
|
597
|
+
* same way `iterationShape: 'entries'` is (see `jsx-to-ir.ts`'s
|
|
598
|
+
* `transformMapCall`) — e.g. Jinja `for k, v in x.items()`.
|
|
599
|
+
* - `'keys'` → `param` bound to the key only — e.g. Jinja
|
|
600
|
+
* `for k in x.keys()`.
|
|
601
|
+
* - `'values'` → `param` bound to the value only — e.g. Jinja
|
|
602
|
+
* `for v in x.values()`. Unlike the array case, `'values'` is NOT
|
|
603
|
+
* a no-op here: `Object.values(x)` genuinely differs from
|
|
604
|
+
* iterating `x` itself (`x` isn't iterable at all as a plain
|
|
605
|
+
* object), so it must be recorded.
|
|
606
|
+
*
|
|
607
|
+
* Iteration ORDER is native-map-dependent: Python `dict`/PHP
|
|
608
|
+
* array-object/Ruby `Hash` preserve the source object's insertion
|
|
609
|
+
* order (matching JS `Object.entries()` semantics exactly), so Jinja,
|
|
610
|
+
* Twig, Blade, and ERB lower directly to their native map/dict/hash
|
|
611
|
+
* iteration. Go's `map[string]T`, Rust's `BTreeMap` (deliberate design,
|
|
612
|
+
* see `num.rs`), and Perl's hash (Xslate/Mojolicious) have NO
|
|
613
|
+
* order-preserving native map type, so those four instead lower to a
|
|
614
|
+
* DETERMINISTIC SORTED-BY-KEY iteration — Go's `{{range}}` (the
|
|
615
|
+
* stdlib's own `fmtsort`), minijinja's `BTreeMap` (already sorted),
|
|
616
|
+
* Kolon's `.kv()`/`.keys()`/`.values()` (verified empirically sorted),
|
|
617
|
+
* and Mojolicious's explicit `sort keys %$hash` (mirroring the
|
|
618
|
+
* existing `spread_attrs`/`_style_to_css` convention in
|
|
619
|
+
* `BarefootJS.pm`). This is a documented, permanent known limitation
|
|
620
|
+
* relative to JS's insertion-order guarantee (not a follow-up TODO) —
|
|
621
|
+
* true insertion order is physically unrecoverable from those
|
|
622
|
+
* languages' native map types once constructed, so sorted order is the
|
|
623
|
+
* best available deterministic approximation, not an interim refusal.
|
|
624
|
+
*
|
|
625
|
+
* Only the OUTERMOST, unchained `Object.<method>(x).map(cb)` shape is
|
|
626
|
+
* recognized — mirroring `iterationShape`'s own scope, chaining
|
|
627
|
+
* (`Object.entries(x).filter(pred).map(cb)`) is not (yet) recognized
|
|
628
|
+
* either, same as the array case.
|
|
629
|
+
*/
|
|
630
|
+
objectIteration?: 'entries' | 'keys' | 'values'
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Count of enclosing `.map()` loops (0 = outermost, 1 = nested one
|
|
634
|
+
* level deep, ...). Adapters use this to derive the loop body's
|
|
635
|
+
* `key`/`data-key` attribute suffix — `depth > 0 ? 'data-key-' +
|
|
636
|
+
* depth : 'data-key'` — matching `keyAttrName()` in
|
|
637
|
+
* `ir-to-client-js/utils.ts`, which the CSR path and the Hono SSR
|
|
638
|
+
* adapter each already derive independently (a recursion counter and
|
|
639
|
+
* a push/pop stack respectively). Before this field, the 8 template
|
|
640
|
+
* (non-JS) adapters had no depth awareness at all and always emitted
|
|
641
|
+
* plain `data-key` on nested-loop items (#2168 nested-loop-outer-binding).
|
|
642
|
+
*/
|
|
643
|
+
depth: number
|
|
644
|
+
|
|
577
645
|
/**
|
|
578
646
|
* When true, loop should be evaluated on client side only.
|
|
579
647
|
* SSR adapters should skip rendering and output placeholder markers.
|