@barefootjs/jsx 0.1.3 → 1.0.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/debug.d.ts +66 -1
- package/dist/debug.d.ts.map +1 -1
- package/dist/html-constants.d.ts +4 -9
- package/dist/html-constants.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8628 -8071
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +15 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +5 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +2 -8
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +135 -0
- package/src/__tests__/boolean-attributes.test.ts +2 -1
- package/src/__tests__/conditional-branch-reactive-text.test.ts +108 -0
- package/src/__tests__/debug.test.ts +422 -9
- package/src/__tests__/doc-examples.test.ts +7 -0
- package/src/__tests__/ir-provider.test.ts +98 -0
- package/src/__tests__/rewrite-destructured-props.test.ts +73 -0
- package/src/debug.ts +637 -32
- package/src/html-constants.ts +4 -27
- package/src/index.ts +6 -1
- package/src/ir-to-client-js/collect-elements.ts +3 -0
- package/src/ir-to-client-js/emit-reactive.ts +5 -5
- package/src/ir-to-client-js/html-template.ts +97 -11
- package/src/ir-to-client-js/types.ts +6 -0
- package/src/ir-to-client-js/utils.ts +4 -65
- package/src/jsx-to-ir.ts +92 -17
- package/src/prop-rewrite.ts +6 -2
- package/src/types.ts +21 -0
package/src/html-constants.ts
CHANGED
|
@@ -1,29 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HTML boolean attributes
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* When false/null/undefined: omit from output entirely
|
|
2
|
+
* HTML boolean attributes — re-exported from the shared classifier so the
|
|
3
|
+
* compiler keeps its existing import path while the source of truth lives
|
|
4
|
+
* in @barefootjs/shared/dom-prop.
|
|
6
5
|
*/
|
|
7
|
-
export
|
|
8
|
-
'checked',
|
|
9
|
-
'disabled',
|
|
10
|
-
'readonly',
|
|
11
|
-
'selected',
|
|
12
|
-
'required',
|
|
13
|
-
'hidden',
|
|
14
|
-
'autofocus',
|
|
15
|
-
'autoplay',
|
|
16
|
-
'controls',
|
|
17
|
-
'loop',
|
|
18
|
-
'muted',
|
|
19
|
-
'open',
|
|
20
|
-
'multiple',
|
|
21
|
-
'novalidate',
|
|
22
|
-
])
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Check if an attribute name is a boolean attribute.
|
|
26
|
-
*/
|
|
27
|
-
export function isBooleanAttr(name: string): boolean {
|
|
28
|
-
return BOOLEAN_ATTRS.has(name.toLowerCase())
|
|
29
|
-
}
|
|
6
|
+
export { BOOLEAN_ATTRS, isBooleanAttr } from '@barefootjs/shared'
|
package/src/index.ts
CHANGED
|
@@ -246,15 +246,20 @@ export type { LoopChainInputs } from './loop-chain'
|
|
|
246
246
|
// Debug analysis
|
|
247
247
|
export {
|
|
248
248
|
buildComponentGraph,
|
|
249
|
+
buildComponentAnalysis,
|
|
249
250
|
buildGraphFromIR,
|
|
251
|
+
buildEventSummary,
|
|
252
|
+
buildLoopSummary,
|
|
250
253
|
traceUpdatePath,
|
|
251
254
|
formatComponentGraph,
|
|
252
255
|
formatUpdatePath,
|
|
256
|
+
formatEventSummary,
|
|
257
|
+
formatLoopSummary,
|
|
253
258
|
formatSignalTrace,
|
|
254
259
|
generateStaticTrace,
|
|
255
260
|
graphToJSON,
|
|
256
261
|
} from './debug'
|
|
257
|
-
export type { ComponentGraph, SignalNode, MemoNode, EffectNode, DomBinding, UpdatePath, SignalTrace } from './debug'
|
|
262
|
+
export type { ComponentGraph, ComponentAnalysis, SignalNode, MemoNode, EffectNode, DomBinding, UpdatePath, SignalTrace, EventBinding, SetterRef, EventSummary, LoopInfo, LoopChildBinding, LoopSummary } from './debug'
|
|
258
263
|
export type { WrapReason } from './ir-to-client-js/reactivity'
|
|
259
264
|
|
|
260
265
|
// HTML constants
|
|
@@ -231,6 +231,7 @@ export function collectInnerLoops(
|
|
|
231
231
|
key: n.key,
|
|
232
232
|
markerId: n.markerId,
|
|
233
233
|
bodyIsMultiRoot: n.bodyIsMultiRoot,
|
|
234
|
+
iterationShape: n.iterationShape,
|
|
234
235
|
containerSlotId: scope.parentSlotId,
|
|
235
236
|
template,
|
|
236
237
|
mapPreamble: n.mapPreamble,
|
|
@@ -563,6 +564,7 @@ export function collectElements(
|
|
|
563
564
|
key: l.key,
|
|
564
565
|
markerId: l.markerId,
|
|
565
566
|
bodyIsMultiRoot: l.bodyIsMultiRoot,
|
|
567
|
+
iterationShape: l.iterationShape,
|
|
566
568
|
template,
|
|
567
569
|
staticItemTemplate,
|
|
568
570
|
childEventHandlers: childHandlers,
|
|
@@ -896,6 +898,7 @@ function collectBranchLoops(
|
|
|
896
898
|
key: n.key,
|
|
897
899
|
markerId: n.markerId,
|
|
898
900
|
bodyIsMultiRoot: n.bodyIsMultiRoot,
|
|
901
|
+
iterationShape: n.iterationShape,
|
|
899
902
|
template: childTemplate,
|
|
900
903
|
containerSlotId: containerSlot,
|
|
901
904
|
mapPreamble: n.mapPreamble ?? null,
|
|
@@ -8,6 +8,7 @@ import type { AttrMeta } from '../types'
|
|
|
8
8
|
import { isBooleanAttr } from '../html-constants'
|
|
9
9
|
import type { ClientJsContext } from './types'
|
|
10
10
|
import { toHtmlAttrName, varSlotId, PROPS_PARAM } from './utils'
|
|
11
|
+
import { createTemplateAwareStringProtector } from './html-template'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Generate JS statements to update a DOM attribute reactively.
|
|
@@ -53,10 +54,11 @@ export function emitAttrUpdate(target: string, attrName: string, expression: str
|
|
|
53
54
|
* Only applies when the component uses destructured props (not props.xxx style).
|
|
54
55
|
*/
|
|
55
56
|
export function rewriteDestructuredPropsInExpr(expr: string, ctx: ClientJsContext): string {
|
|
56
|
-
// Skip if the component already uses props object access (not destructuring)
|
|
57
57
|
if (ctx.propsObjectName) return expr
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
const { protect, restore } = createTemplateAwareStringProtector()
|
|
60
|
+
let result = protect(expr)
|
|
61
|
+
|
|
60
62
|
for (const prop of ctx.propsParams) {
|
|
61
63
|
if (prop.name === 'children') continue
|
|
62
64
|
const pattern = new RegExp(`(?<![-.])\\b${prop.name}\\b`, 'g')
|
|
@@ -69,7 +71,7 @@ export function rewriteDestructuredPropsInExpr(expr: string, ctx: ClientJsContex
|
|
|
69
71
|
result = result.replace(new RegExp(`(?<![-.])\\b${prop.name}\\b`, 'g'), replacement)
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
return result
|
|
74
|
+
return restore(result)
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
/** Emit createEffect blocks that update text nodes for reactive expressions. */
|
|
@@ -152,8 +154,6 @@ export function emitReactiveAttributeUpdates(lines: string[], ctx: ClientJsConte
|
|
|
152
154
|
lines.push(` createEffect(() => {`)
|
|
153
155
|
lines.push(` if (_${v}) {`)
|
|
154
156
|
for (const attr of attrs) {
|
|
155
|
-
// Rewrite destructured prop references to props.xxx for live reactivity.
|
|
156
|
-
// Destructured props are const-captured once; effects must read from props object.
|
|
157
157
|
const expression = rewriteDestructuredPropsInExpr(attr.expression, ctx)
|
|
158
158
|
for (const stmt of emitAttrUpdate(`_${v}`, attr.attrName, expression, attr)) {
|
|
159
159
|
lines.push(` ${stmt}`)
|
|
@@ -36,6 +36,62 @@ export function createStringProtector(): {
|
|
|
36
36
|
return { protect, restore }
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Split a template literal body into static segments and `${...}` interpolations,
|
|
41
|
+
* correctly handling nested braces (e.g. object literals inside interpolations).
|
|
42
|
+
*/
|
|
43
|
+
export function splitTemplateInterpolations(inner: string): string[] {
|
|
44
|
+
const parts: string[] = []
|
|
45
|
+
let i = 0
|
|
46
|
+
let segStart = 0
|
|
47
|
+
while (i < inner.length) {
|
|
48
|
+
if (inner[i] === '$' && inner[i + 1] === '{') {
|
|
49
|
+
if (i > segStart) parts.push(inner.slice(segStart, i))
|
|
50
|
+
let depth = 1
|
|
51
|
+
let j = i + 2
|
|
52
|
+
while (j < inner.length && depth > 0) {
|
|
53
|
+
if (inner[j] === '{') depth++
|
|
54
|
+
else if (inner[j] === '}') depth--
|
|
55
|
+
if (depth > 0) j++
|
|
56
|
+
}
|
|
57
|
+
j++
|
|
58
|
+
parts.push(inner.slice(i, j))
|
|
59
|
+
i = j
|
|
60
|
+
segStart = j
|
|
61
|
+
} else {
|
|
62
|
+
i++
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (segStart < inner.length) parts.push(inner.slice(segStart))
|
|
66
|
+
return parts
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Protect both template-literal static segments AND quoted string literals
|
|
71
|
+
* from regex-based prop substitution. Used by prop-rewrite.ts (Phase 1)
|
|
72
|
+
* and emit-reactive.ts (Phase 2) to avoid corrupting CSS selectors and
|
|
73
|
+
* class values during prop name replacement.
|
|
74
|
+
*/
|
|
75
|
+
export function createTemplateAwareStringProtector(): {
|
|
76
|
+
protect: (s: string) => string
|
|
77
|
+
restore: (s: string) => string
|
|
78
|
+
} {
|
|
79
|
+
const stash: string[] = []
|
|
80
|
+
const save = (s: string) => { const i = stash.length; stash.push(s); return `__STRLIT_${i}__` }
|
|
81
|
+
const protect = (s: string): string => {
|
|
82
|
+
s = s.replace(/`([^`]*)`/g, (_full, inner: string) => {
|
|
83
|
+
const parts = splitTemplateInterpolations(inner)
|
|
84
|
+
return '`' + parts.map(p => p.startsWith('${') ? p : save(p)).join('') + '`'
|
|
85
|
+
})
|
|
86
|
+
s = s.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, m => save(m))
|
|
87
|
+
return s
|
|
88
|
+
}
|
|
89
|
+
const restore = (s: string): string => {
|
|
90
|
+
return s.replace(/__STRLIT_(\d+)__/g, (_, i) => stash[Number(i)])
|
|
91
|
+
}
|
|
92
|
+
return { protect, restore }
|
|
93
|
+
}
|
|
94
|
+
|
|
39
95
|
const VOID_ELEMENTS = new Set([
|
|
40
96
|
'area', 'base', 'br', 'col', 'embed', 'hr', 'img',
|
|
41
97
|
'input', 'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
@@ -67,6 +123,31 @@ function applyLoopChain(loop: import('../types').IRLoop, base: string = loop.arr
|
|
|
67
123
|
})
|
|
68
124
|
}
|
|
69
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Wrap an array expression with the iterator shape and build the
|
|
128
|
+
* `.map()` callback parameter for `entries` / `keys` iteration.
|
|
129
|
+
* Returns the (possibly wrapped) array and the callback param string.
|
|
130
|
+
*/
|
|
131
|
+
function applyIterationShape(
|
|
132
|
+
node: import('../types').IRLoop,
|
|
133
|
+
arrayExpr: string,
|
|
134
|
+
indexParam: string,
|
|
135
|
+
): { array: string; callbackParam: string } {
|
|
136
|
+
if (node.iterationShape === 'entries' && node.index) {
|
|
137
|
+
return {
|
|
138
|
+
array: `[...${arrayExpr}.entries()]`,
|
|
139
|
+
callbackParam: `([${node.index}, ${node.param}])`,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (node.iterationShape === 'keys') {
|
|
143
|
+
return {
|
|
144
|
+
array: `[...${arrayExpr}.keys()]`,
|
|
145
|
+
callbackParam: `(${node.param})`,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { array: arrayExpr, callbackParam: `(${node.param}${indexParam})` }
|
|
149
|
+
}
|
|
150
|
+
|
|
70
151
|
function childrenPropEntry(
|
|
71
152
|
children: IRNode[],
|
|
72
153
|
recurse: (n: IRNode) => string,
|
|
@@ -488,7 +569,9 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
488
569
|
// where the template is the only source of truth. The chain
|
|
489
570
|
// mirrors `buildChainedArrayExpr` so reconcileList sees the
|
|
490
571
|
// same array shape this template emits.
|
|
491
|
-
const
|
|
572
|
+
const rawChainedArray = applyLoopChain(node)
|
|
573
|
+
const { array: iterArray, callbackParam } = applyIterationShape(node, rawChainedArray, indexParam)
|
|
574
|
+
const wrappedArray = wrapExpr(iterArray)
|
|
492
575
|
const iterMethod = node.method ?? 'map'
|
|
493
576
|
let mapExpr: string
|
|
494
577
|
|
|
@@ -501,9 +584,9 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
501
584
|
}
|
|
502
585
|
mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`
|
|
503
586
|
} else if (node.mapPreamble) {
|
|
504
|
-
mapExpr = `\${${wrappedArray}.${iterMethod}(
|
|
587
|
+
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${node.mapPreamble} return \`${childTemplate}\` }).join('')}`
|
|
505
588
|
} else {
|
|
506
|
-
mapExpr = `\${${wrappedArray}.${iterMethod}(
|
|
589
|
+
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
|
|
507
590
|
}
|
|
508
591
|
// Wrap with loop boundary markers so reconciliation doesn't affect siblings
|
|
509
592
|
return `<!--${loopStartMarker(node.markerId)}-->${mapExpr}<!--${loopEndMarker(node.markerId)}-->`
|
|
@@ -600,7 +683,9 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Set<stri
|
|
|
600
683
|
const indexParam = node.index ? `, ${node.index}` : ''
|
|
601
684
|
// Apply sort / filter chain (#1448 Tier B) — same shape as the
|
|
602
685
|
// `irToHtmlTemplate` loop case above.
|
|
603
|
-
const
|
|
686
|
+
const rawChainedArray = applyLoopChain(node)
|
|
687
|
+
const { array: iterArray, callbackParam } = applyIterationShape(node, rawChainedArray, indexParam)
|
|
688
|
+
const wrappedArray = wrapExpr(iterArray)
|
|
604
689
|
const iterMethod = node.method ?? 'map'
|
|
605
690
|
let mapExpr: string
|
|
606
691
|
if (node.flatMapCallback) {
|
|
@@ -611,9 +696,9 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Set<stri
|
|
|
611
696
|
}
|
|
612
697
|
mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`
|
|
613
698
|
} else if (node.mapPreamble) {
|
|
614
|
-
mapExpr = `\${${wrappedArray}.${iterMethod}(
|
|
699
|
+
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${node.mapPreamble} return \`${childTemplate}\` }).join('')}`
|
|
615
700
|
} else {
|
|
616
|
-
mapExpr = `\${${wrappedArray}.${iterMethod}(
|
|
701
|
+
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
|
|
617
702
|
}
|
|
618
703
|
return `<!--${loopStartMarker(node.markerId)}-->${mapExpr}<!--${loopEndMarker(node.markerId)}-->`
|
|
619
704
|
}
|
|
@@ -1417,8 +1502,9 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
1417
1502
|
const chainedTemplateArray = node.sortComparator || node.filterPredicate
|
|
1418
1503
|
? applyLoopChain(node, node.templateArray)
|
|
1419
1504
|
: node.templateArray
|
|
1420
|
-
const
|
|
1421
|
-
const
|
|
1505
|
+
const rawArrayExpr = transformExpr(node.array, chainedTemplateArray)
|
|
1506
|
+
const safeRawArrayExpr = rawArrayExpr === UNSAFE_TEMPLATE_EXPR ? '[]' : rawArrayExpr
|
|
1507
|
+
const { array: iterArrayExpr, callbackParam } = applyIterationShape(node, safeRawArrayExpr, indexParam)
|
|
1422
1508
|
const iterMethod = node.method ?? 'map'
|
|
1423
1509
|
let mapExpr: string
|
|
1424
1510
|
if (node.flatMapCallback) {
|
|
@@ -1428,13 +1514,13 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
1428
1514
|
body = body.replace(frag.placeholder, `\`${renderedIr}\``)
|
|
1429
1515
|
}
|
|
1430
1516
|
body = applyPropsRewrite(body, propsObjectName ?? null)
|
|
1431
|
-
mapExpr = `\${${
|
|
1517
|
+
mapExpr = `\${${iterArrayExpr}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`
|
|
1432
1518
|
} else if (node.mapPreamble) {
|
|
1433
1519
|
const rawPreamble = node.templateMapPreamble ?? node.mapPreamble
|
|
1434
1520
|
const preamble = applyPropsRewrite(rawPreamble, propsObjectName ?? null)
|
|
1435
|
-
mapExpr = `\${${
|
|
1521
|
+
mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`
|
|
1436
1522
|
} else {
|
|
1437
|
-
mapExpr = `\${${
|
|
1523
|
+
mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
|
|
1438
1524
|
}
|
|
1439
1525
|
return `<!--${loopStartMarker(node.markerId)}-->${mapExpr}<!--${loopEndMarker(node.markerId)}-->`
|
|
1440
1526
|
}
|
|
@@ -222,6 +222,12 @@ export interface LoopCore {
|
|
|
222
222
|
* names as a recurring source of "forgotten variant" defects.
|
|
223
223
|
*/
|
|
224
224
|
bindings: LoopChildBindings
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Iteration shape from `.entries()` / `.keys()` / `.values()` chain
|
|
228
|
+
* (#1448 Tier B). Threaded from `IRLoop.iterationShape`.
|
|
229
|
+
*/
|
|
230
|
+
iterationShape?: 'entries' | 'keys'
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
/**
|
|
@@ -16,9 +16,10 @@ import {
|
|
|
16
16
|
BF_LOOP_END,
|
|
17
17
|
loopStartMarker,
|
|
18
18
|
loopEndMarker,
|
|
19
|
+
toHTMLAttrName as toHtmlAttrName,
|
|
19
20
|
} from '@barefootjs/shared'
|
|
20
21
|
|
|
21
|
-
export { DATA_KEY, DATA_KEY_PREFIX, DATA_BF_PH, BF_LOOP_START, BF_LOOP_END, loopStartMarker, loopEndMarker }
|
|
22
|
+
export { DATA_KEY, DATA_KEY_PREFIX, DATA_BF_PH, BF_LOOP_START, BF_LOOP_END, loopStartMarker, loopEndMarker, toHtmlAttrName }
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Parameter name for the props object in generated init/template functions.
|
|
@@ -167,70 +168,8 @@ export function quotePropName(name: string): string {
|
|
|
167
168
|
return JSON.stringify(name)
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
* (React-compatible spelling) and must be emitted as kebab-case at the
|
|
173
|
-
* DOM/HTML layer.
|
|
174
|
-
*
|
|
175
|
-
* Why this exists: SSR template output and client-side reactive
|
|
176
|
-
* `setAttribute` both flow through `toHtmlAttrName`. If they disagree on
|
|
177
|
-
* the spelling, SSR emits `stroke-width="1.5"` while hydration writes
|
|
178
|
-
* `setAttribute('strokeWidth', '2.5')`, leaving both attributes on the
|
|
179
|
-
* DOM. The SVG renderer reads the kebab-case form, so reactive updates
|
|
180
|
-
* become invisible. This map keeps both paths in sync. Surfaced by the
|
|
181
|
-
* Graph/DAG Editor block (#135) where edge selection failed to thicken
|
|
182
|
-
* the stroke even though `selectedEdgeId()` updated correctly.
|
|
183
|
-
*
|
|
184
|
-
* Listed names are SVG-only — none of them collide with HTML attributes.
|
|
185
|
-
*/
|
|
186
|
-
const SVG_CAMEL_TO_KEBAB: Record<string, string> = {
|
|
187
|
-
// stroke
|
|
188
|
-
strokeWidth: 'stroke-width',
|
|
189
|
-
strokeLinecap: 'stroke-linecap',
|
|
190
|
-
strokeLinejoin: 'stroke-linejoin',
|
|
191
|
-
strokeDasharray: 'stroke-dasharray',
|
|
192
|
-
strokeDashoffset: 'stroke-dashoffset',
|
|
193
|
-
strokeMiterlimit: 'stroke-miterlimit',
|
|
194
|
-
strokeOpacity: 'stroke-opacity',
|
|
195
|
-
// fill
|
|
196
|
-
fillOpacity: 'fill-opacity',
|
|
197
|
-
fillRule: 'fill-rule',
|
|
198
|
-
// gradient stops
|
|
199
|
-
stopColor: 'stop-color',
|
|
200
|
-
stopOpacity: 'stop-opacity',
|
|
201
|
-
// text presentation
|
|
202
|
-
textAnchor: 'text-anchor',
|
|
203
|
-
dominantBaseline: 'dominant-baseline',
|
|
204
|
-
alignmentBaseline: 'alignment-baseline',
|
|
205
|
-
fontFamily: 'font-family',
|
|
206
|
-
fontSize: 'font-size',
|
|
207
|
-
fontWeight: 'font-weight',
|
|
208
|
-
fontStyle: 'font-style',
|
|
209
|
-
letterSpacing: 'letter-spacing',
|
|
210
|
-
wordSpacing: 'word-spacing',
|
|
211
|
-
// common presentation / interaction
|
|
212
|
-
pointerEvents: 'pointer-events',
|
|
213
|
-
vectorEffect: 'vector-effect',
|
|
214
|
-
colorInterpolation: 'color-interpolation',
|
|
215
|
-
clipPath: 'clip-path',
|
|
216
|
-
clipRule: 'clip-rule',
|
|
217
|
-
// marker references
|
|
218
|
-
markerStart: 'marker-start',
|
|
219
|
-
markerMid: 'marker-mid',
|
|
220
|
-
markerEnd: 'marker-end',
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Convert JSX attribute name to HTML attribute name.
|
|
225
|
-
* Handles React-style naming conventions (e.g., className → class) and
|
|
226
|
-
* SVG presentation attributes (e.g., strokeWidth → stroke-width).
|
|
227
|
-
*/
|
|
228
|
-
export function toHtmlAttrName(jsxAttrName: string): string {
|
|
229
|
-
if (jsxAttrName === 'className') return 'class'
|
|
230
|
-
const svgKebab = SVG_CAMEL_TO_KEBAB[jsxAttrName]
|
|
231
|
-
if (svgKebab !== undefined) return svgKebab
|
|
232
|
-
return jsxAttrName
|
|
233
|
-
}
|
|
171
|
+
// toHtmlAttrName is now re-exported from @barefootjs/shared (classifyDOMProp's
|
|
172
|
+
// toHTMLAttrName), keeping the same public name for downstream consumers.
|
|
234
173
|
|
|
235
174
|
/**
|
|
236
175
|
* Wrap arrow function handler in block to prevent accidental return false.
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -2013,15 +2013,20 @@ function transformConditionalBranch(
|
|
|
2013
2013
|
effect: 'pure',
|
|
2014
2014
|
freeRefs: resolveFreeRefs(node, makeBindingEnv(ctx)),
|
|
2015
2015
|
}
|
|
2016
|
+
const callsReactive = exprCallsReactiveGetters(node, ctx)
|
|
2017
|
+
const hasCalls = exprHasFunctionCalls(node)
|
|
2018
|
+
const reactive = isReactiveExpression(exprText, ctx, node) || isReactiveOrigin(branchOrigin)
|
|
2019
|
+
const needsSlot = reactive || callsReactive
|
|
2020
|
+
const slotId = needsSlot ? generateSlotId(ctx) : null
|
|
2016
2021
|
return {
|
|
2017
2022
|
type: 'expression',
|
|
2018
2023
|
expr: exprText,
|
|
2019
2024
|
templateExpr: rewriteBarePropRefs(exprText, node, ctx),
|
|
2020
2025
|
typeInfo: inferExpressionType(node, ctx),
|
|
2021
|
-
reactive
|
|
2022
|
-
slotId
|
|
2023
|
-
callsReactiveGetters:
|
|
2024
|
-
hasFunctionCalls:
|
|
2026
|
+
reactive,
|
|
2027
|
+
slotId,
|
|
2028
|
+
callsReactiveGetters: callsReactive || undefined,
|
|
2029
|
+
hasFunctionCalls: hasCalls || undefined,
|
|
2025
2030
|
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
2026
2031
|
origin: branchOrigin,
|
|
2027
2032
|
}
|
|
@@ -2077,6 +2082,23 @@ function isSortCall(node: ts.Expression): { array: ts.Expression; callback: ts.E
|
|
|
2077
2082
|
}
|
|
2078
2083
|
}
|
|
2079
2084
|
|
|
2085
|
+
/**
|
|
2086
|
+
* Check if a node is an `.entries()`, `.keys()`, or `.values()` call
|
|
2087
|
+
* (zero-arg, property-access form). Returns the underlying array expression
|
|
2088
|
+
* and the iteration shape so `transformMapCall` can strip the iterator
|
|
2089
|
+
* method and record it on the IRLoop.
|
|
2090
|
+
*/
|
|
2091
|
+
function isIteratorShapeCall(
|
|
2092
|
+
node: ts.Expression,
|
|
2093
|
+
): { array: ts.LeftHandSideExpression; shape: 'entries' | 'keys' | 'values' } | null {
|
|
2094
|
+
if (!ts.isCallExpression(node)) return null
|
|
2095
|
+
if (!ts.isPropertyAccessExpression(node.expression)) return null
|
|
2096
|
+
if (node.arguments.length !== 0) return null
|
|
2097
|
+
const name = node.expression.name.text
|
|
2098
|
+
if (name !== 'entries' && name !== 'keys' && name !== 'values') return null
|
|
2099
|
+
return { array: node.expression.expression, shape: name }
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2080
2102
|
type SortExtractionResult = {
|
|
2081
2103
|
result: SortComparator | null
|
|
2082
2104
|
unsupportedReason?: string
|
|
@@ -2672,6 +2694,7 @@ function transformMapCall(
|
|
|
2672
2694
|
// 2. filter().map()
|
|
2673
2695
|
// 3. filter().sort().map() (outermost = sort, inner = filter)
|
|
2674
2696
|
// 4. sort().filter().map() (outermost = filter, inner = sort)
|
|
2697
|
+
// 5. entries().map() / keys().map() / values().map()
|
|
2675
2698
|
|
|
2676
2699
|
let array: string = ''
|
|
2677
2700
|
let templateArray: string | undefined
|
|
@@ -2686,6 +2709,7 @@ function transformMapCall(
|
|
|
2686
2709
|
let mapPreamble: string | undefined
|
|
2687
2710
|
let templateMapPreamble: string | undefined
|
|
2688
2711
|
let typedMapPreamble: string | undefined
|
|
2712
|
+
let iterationShape: 'entries' | 'keys' | undefined
|
|
2689
2713
|
|
|
2690
2714
|
// Helper to set both array and templateArray
|
|
2691
2715
|
const setArray = (node: ts.Expression) => {
|
|
@@ -2694,8 +2718,26 @@ function transformMapCall(
|
|
|
2694
2718
|
arrayExpr = node
|
|
2695
2719
|
}
|
|
2696
2720
|
|
|
2697
|
-
|
|
2698
|
-
|
|
2721
|
+
// Detect `.entries()`, `.keys()`, `.values()` as the outermost wrapper
|
|
2722
|
+
// on the map source. Strip the iterator method and record the shape so
|
|
2723
|
+
// adapters emit the right loop variable bindings. `.values()` is a
|
|
2724
|
+
// no-op (same as plain `.map()`) so it's stripped but not recorded.
|
|
2725
|
+
// The inner expression (after stripping) feeds into the standard
|
|
2726
|
+
// filter/sort chain detection below.
|
|
2727
|
+
let chainSource = mapSource
|
|
2728
|
+
const iteratorInfo = isIteratorShapeCall(mapSource)
|
|
2729
|
+
if (iteratorInfo) {
|
|
2730
|
+
chainSource = iteratorInfo.array
|
|
2731
|
+
if (iteratorInfo.shape === 'entries') {
|
|
2732
|
+
iterationShape = 'entries'
|
|
2733
|
+
} else if (iteratorInfo.shape === 'keys') {
|
|
2734
|
+
iterationShape = 'keys'
|
|
2735
|
+
}
|
|
2736
|
+
// 'values' is a no-op — same as plain .map()
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
const filterInfo = isFilterCall(chainSource)
|
|
2740
|
+
const sortInfo = isSortCall(chainSource)
|
|
2699
2741
|
|
|
2700
2742
|
if (sortInfo) {
|
|
2701
2743
|
// Outermost is sort: could be sort().map() or filter().sort().map()
|
|
@@ -2810,8 +2852,8 @@ function transformMapCall(
|
|
|
2810
2852
|
}
|
|
2811
2853
|
}
|
|
2812
2854
|
} else {
|
|
2813
|
-
array = ctx.getJS(
|
|
2814
|
-
arrayExpr =
|
|
2855
|
+
array = ctx.getJS(chainSource)
|
|
2856
|
+
arrayExpr = chainSource
|
|
2815
2857
|
}
|
|
2816
2858
|
|
|
2817
2859
|
// Get callback function
|
|
@@ -2839,18 +2881,50 @@ function transformMapCall(
|
|
|
2839
2881
|
// residual-object / `.slice(n)` accessor at each reference. Only
|
|
2840
2882
|
// computed property keys remain unsupported — those raise `BF025`
|
|
2841
2883
|
// and the emitter falls back to the #950 body-entry unwrap.
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2884
|
+
// `.entries()` synthesises `[index, value]` — when the callback
|
|
2885
|
+
// destructures exactly two array elements, extract the names into
|
|
2886
|
+
// `index` and `param` so the loop renders with proper bindings and
|
|
2887
|
+
// the BF104 destructure-param refusal doesn't fire.
|
|
2888
|
+
if (iterationShape === 'entries' && ts.isArrayBindingPattern(firstParam.name)) {
|
|
2889
|
+
const elements = firstParam.name.elements.filter(
|
|
2890
|
+
el => !ts.isOmittedExpression(el),
|
|
2848
2891
|
)
|
|
2849
|
-
|
|
2850
|
-
|
|
2892
|
+
if (elements.length === 2 &&
|
|
2893
|
+
ts.isBindingElement(elements[0]) && ts.isIdentifier(elements[0].name) &&
|
|
2894
|
+
ts.isBindingElement(elements[1]) && ts.isIdentifier(elements[1].name)) {
|
|
2895
|
+
index = elements[0].name.text
|
|
2896
|
+
param = elements[1].name.text
|
|
2897
|
+
// Don't populate paramBindings — the destructure is fully
|
|
2898
|
+
// resolved into index + param by the iteration shape.
|
|
2899
|
+
} else {
|
|
2900
|
+
// Non-2-element destructure with .entries() — fall through to
|
|
2901
|
+
// standard destructure handling (will trigger BF104 on
|
|
2902
|
+
// template adapters).
|
|
2903
|
+
const bindingResult = extractLoopParamBindings(firstParam.name)
|
|
2904
|
+
if (bindingResult && !Array.isArray(bindingResult)) {
|
|
2905
|
+
ctx.analyzer.errors.push(
|
|
2906
|
+
createError(ErrorCodes.UNSUPPORTED_DESTRUCTURE_REST,
|
|
2907
|
+
getSourceLocation(firstParam, ctx.sourceFile, ctx.filePath),
|
|
2908
|
+
)
|
|
2909
|
+
)
|
|
2910
|
+
} else if (Array.isArray(bindingResult)) {
|
|
2911
|
+
paramBindings = bindingResult
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
} else {
|
|
2915
|
+
const bindingResult = extractLoopParamBindings(firstParam.name)
|
|
2916
|
+
if (bindingResult && !Array.isArray(bindingResult)) {
|
|
2917
|
+
ctx.analyzer.errors.push(
|
|
2918
|
+
createError(ErrorCodes.UNSUPPORTED_DESTRUCTURE_REST,
|
|
2919
|
+
getSourceLocation(firstParam, ctx.sourceFile, ctx.filePath),
|
|
2920
|
+
)
|
|
2921
|
+
)
|
|
2922
|
+
} else if (Array.isArray(bindingResult)) {
|
|
2923
|
+
paramBindings = bindingResult
|
|
2924
|
+
}
|
|
2851
2925
|
}
|
|
2852
2926
|
}
|
|
2853
|
-
if (callback.parameters.length > 1) {
|
|
2927
|
+
if (callback.parameters.length > 1 && iterationShape !== 'entries') {
|
|
2854
2928
|
const secondParam = callback.parameters[1]
|
|
2855
2929
|
index = secondParam.name.getText(ctx.sourceFile)
|
|
2856
2930
|
if (secondParam.type) {
|
|
@@ -3065,6 +3139,7 @@ function transformMapCall(
|
|
|
3065
3139
|
filterPredicate,
|
|
3066
3140
|
sortComparator,
|
|
3067
3141
|
chainOrder,
|
|
3142
|
+
iterationShape,
|
|
3068
3143
|
clientOnly: isClientOnly || undefined,
|
|
3069
3144
|
mapPreamble,
|
|
3070
3145
|
templateMapPreamble,
|
package/src/prop-rewrite.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import ts from 'typescript'
|
|
10
10
|
import { PROPS_PARAM } from './ir-to-client-js/utils'
|
|
11
|
+
import { createTemplateAwareStringProtector } from './ir-to-client-js/html-template'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Walk an AST node for destructured-prop value references and add
|
|
@@ -47,7 +48,9 @@ export function applyRegexPropRefRewrite(
|
|
|
47
48
|
text: string,
|
|
48
49
|
propRefs: Iterable<string>,
|
|
49
50
|
): string {
|
|
50
|
-
|
|
51
|
+
const { protect, restore } = createTemplateAwareStringProtector()
|
|
52
|
+
let result = protect(text)
|
|
53
|
+
|
|
51
54
|
for (const propName of propRefs) {
|
|
52
55
|
const pattern = new RegExp(`(?<!${PROPS_PARAM}\\.)(?<!['"\\w.-])\\b${propName}\\b(?![a-zA-Z0-9_$])`, 'g')
|
|
53
56
|
result = result.replace(pattern, (match, offset, str) => {
|
|
@@ -60,7 +63,8 @@ export function applyRegexPropRefRewrite(
|
|
|
60
63
|
return `${PROPS_PARAM}.${propName}`
|
|
61
64
|
})
|
|
62
65
|
}
|
|
63
|
-
|
|
66
|
+
|
|
67
|
+
return restore(result)
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
/**
|
package/src/types.ts
CHANGED
|
@@ -496,6 +496,27 @@ export interface IRLoop {
|
|
|
496
496
|
*/
|
|
497
497
|
chainOrder?: 'filter-sort' | 'sort-filter'
|
|
498
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Pre-`.map()` iteration shape (#1448 Tier B).
|
|
501
|
+
*
|
|
502
|
+
* When the user writes `arr.entries().map(([i, v]) => ...)`,
|
|
503
|
+
* `arr.keys().map(i => ...)`, or `arr.values().map(v => ...)`,
|
|
504
|
+
* the chain-detection in `transformMapCall` strips the iterator
|
|
505
|
+
* method and records the shape here so adapters can emit the
|
|
506
|
+
* right loop variable bindings:
|
|
507
|
+
*
|
|
508
|
+
* - `'entries'` → both index and value are bound
|
|
509
|
+
* (Go: `$i, $v`; Mojo: `$i` + `$v = $arr->[$i]`)
|
|
510
|
+
* - `'keys'` → only the index is bound
|
|
511
|
+
* (Go: `$i, $_ :=`; Mojo: `$i` with no per-item lookup)
|
|
512
|
+
* - `undefined` / `'values'` → standard iteration (value only)
|
|
513
|
+
*
|
|
514
|
+
* The chain detection also synthesises proper `param` / `index`
|
|
515
|
+
* from the `.entries()` destructure pattern so the BF104
|
|
516
|
+
* destructure-param refusal doesn't fire.
|
|
517
|
+
*/
|
|
518
|
+
iterationShape?: 'entries' | 'keys'
|
|
519
|
+
|
|
499
520
|
/**
|
|
500
521
|
* When true, loop should be evaluated on client side only.
|
|
501
522
|
* SSR adapters should skip rendering and output placeholder markers.
|