@barefootjs/jsx 0.4.0 → 0.5.1
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/interface.d.ts +20 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +36 -19
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/import-map.d.ts +56 -0
- package/dist/import-map.d.ts.map +1 -0
- package/dist/import-map.js +18 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +333 -199
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +14 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.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 +0 -14
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +7 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +2 -2
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/scanner/js-scanner.d.ts +10 -0
- package/dist/scanner/js-scanner.d.ts.map +1 -1
- package/dist/scanner/js-scanner.js +5 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -3
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +438 -190
- package/src/__tests__/adapter-output.test.ts +49 -0
- package/src/__tests__/child-components-in-map.test.ts +76 -0
- package/src/__tests__/client-js-generation.test.ts +5 -2
- package/src/__tests__/import-map.test.ts +75 -0
- package/src/__tests__/inline-jsx-callback.test.ts +95 -0
- package/src/__tests__/ir-jsx-props.test.ts +5 -2
- package/src/__tests__/ir-sort-comparator.test.ts +212 -9
- package/src/__tests__/loop-item-conditional-codegen.test.ts +81 -0
- package/src/__tests__/map-logical-jsx-helper.test.ts +159 -0
- package/src/__tests__/missing-key-in-list.test.ts +49 -0
- package/src/__tests__/reactive-attrs-in-map.test.ts +41 -0
- package/src/__tests__/token-contains-ident.test.ts +27 -0
- package/src/__tests__/unsupported-expression.test.ts +42 -13
- package/src/adapters/interface.ts +20 -0
- package/src/adapters/test-adapter.ts +16 -1
- package/src/expression-parser.ts +265 -50
- package/src/import-map.ts +72 -0
- package/src/index.ts +5 -1
- package/src/ir-to-client-js/collect-elements.ts +3 -0
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +17 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +14 -0
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +7 -2
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +60 -0
- package/src/ir-to-client-js/emit-reactive.ts +12 -3
- package/src/ir-to-client-js/html-template.ts +29 -3
- package/src/ir-to-client-js/imports.ts +2 -2
- package/src/ir-to-client-js/reactivity.ts +17 -1
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +8 -4
- package/src/ir-to-client-js/types.ts +7 -0
- package/src/ir-to-client-js/utils.ts +31 -116
- package/src/jsx-to-ir.ts +161 -12
- package/src/preprocess-inline-jsx-callbacks.ts +28 -10
- package/src/scanner/js-scanner.ts +16 -1
- package/src/types.ts +12 -0
|
@@ -120,8 +120,19 @@ export function stringifyPlainLoop(
|
|
|
120
120
|
reactiveEffects,
|
|
121
121
|
childRefs,
|
|
122
122
|
bodyIsMultiRoot,
|
|
123
|
+
anchored,
|
|
124
|
+
anchorKeyExpr,
|
|
123
125
|
} = plan
|
|
124
126
|
|
|
127
|
+
// Whole-item conditional loops (#1665) render 0-or-1 element per item, so
|
|
128
|
+
// they route through `mapArrayAnchored`. The renderItem returns a fragment
|
|
129
|
+
// headed by a `<!--bf-loop-i:KEY-->` anchor and seeded with the
|
|
130
|
+
// conditional's markers; `insert(__anchor, …)` then owns the content.
|
|
131
|
+
if (anchored) {
|
|
132
|
+
stringifyAnchoredLoop(lines, plan, topIndent, anchorKeyExpr)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
125
136
|
// `childRefs` need `__el` as a handle to invoke the user's callback inside
|
|
126
137
|
// the factory, so non-empty refs force the multi-line layout the same way
|
|
127
138
|
// reactive effects do (#1244).
|
|
@@ -155,6 +166,55 @@ export function stringifyPlainLoop(
|
|
|
155
166
|
lines.push(`${topIndent}}, '${markerId}')`)
|
|
156
167
|
}
|
|
157
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Emit a whole-item conditional loop via `mapArrayAnchored` (#1665).
|
|
171
|
+
*
|
|
172
|
+
* The renderItem identifies each item by an always-present
|
|
173
|
+
* `<!--bf-loop-i:KEY-->` anchor instead of a root element (which the item may
|
|
174
|
+
* not have). On CSR it returns a `DocumentFragment` of
|
|
175
|
+
* `[anchor, bf-cond-start, bf-cond-end]` so `insert()`'s first run has the
|
|
176
|
+
* markers to populate; on hydration (`__existing` is the SSR anchor Comment)
|
|
177
|
+
* it returns that anchor and `insert()` adopts the SSR-rendered content. The
|
|
178
|
+
* conditional itself is emitted by the shared reactive-effects stringifier
|
|
179
|
+
* with `elVar: '__anchor'`, so `insert(__anchor, …)` range-scopes the
|
|
180
|
+
* toggle to this item.
|
|
181
|
+
*/
|
|
182
|
+
function stringifyAnchoredLoop(
|
|
183
|
+
lines: string[],
|
|
184
|
+
plan: PlainLoopPlan,
|
|
185
|
+
topIndent: string,
|
|
186
|
+
anchorKeyExpr: string,
|
|
187
|
+
): void {
|
|
188
|
+
const {
|
|
189
|
+
containerVar, markerId, arrayExpr, keyFn,
|
|
190
|
+
paramHead, paramUnwrap, indexParam, mapPreambleWrapped, reactiveEffects,
|
|
191
|
+
} = plan
|
|
192
|
+
|
|
193
|
+
// The single whole-item conditional supplies the slot id used to seed the
|
|
194
|
+
// CSR markers so `insert()`'s first run can find and populate them.
|
|
195
|
+
const condSlot = reactiveEffects?.conditionals[0]?.slotId ?? null
|
|
196
|
+
|
|
197
|
+
lines.push(`${topIndent}mapArrayAnchored(() => ${arrayExpr}, ${containerVar}, ${keyFn}, (${paramHead}, ${indexParam}, __existing) => {`)
|
|
198
|
+
const bodyIndent = topIndent + ' '
|
|
199
|
+
if (paramUnwrap) lines.push(`${bodyIndent}${paramUnwrap}`)
|
|
200
|
+
if (mapPreambleWrapped) lines.push(`${bodyIndent}${mapPreambleWrapped}`)
|
|
201
|
+
lines.push(`${bodyIndent}const __anchor = __existing ?? document.createComment(\`bf-loop-i:\${${anchorKeyExpr}}\`)`)
|
|
202
|
+
lines.push(`${bodyIndent}let __frag = null`)
|
|
203
|
+
lines.push(`${bodyIndent}if (!__existing) {`)
|
|
204
|
+
lines.push(`${bodyIndent} __frag = document.createDocumentFragment()`)
|
|
205
|
+
lines.push(`${bodyIndent} __frag.appendChild(__anchor)`)
|
|
206
|
+
if (condSlot) {
|
|
207
|
+
lines.push(`${bodyIndent} __frag.appendChild(document.createComment('bf-cond-start:${condSlot}'))`)
|
|
208
|
+
lines.push(`${bodyIndent} __frag.appendChild(document.createComment('bf-cond-end:${condSlot}'))`)
|
|
209
|
+
}
|
|
210
|
+
lines.push(`${bodyIndent}}`)
|
|
211
|
+
if (reactiveEffects !== null) {
|
|
212
|
+
stringifyReactiveEffects(lines, reactiveEffects, { indent: bodyIndent, elVar: '__anchor', bodyIsMultiRoot: false })
|
|
213
|
+
}
|
|
214
|
+
lines.push(`${bodyIndent}return __frag ?? __anchor`)
|
|
215
|
+
lines.push(`${topIndent}}, '${markerId}')`)
|
|
216
|
+
}
|
|
217
|
+
|
|
158
218
|
export function stringifyStaticLoop(lines: string[], plan: StaticLoopPlan): void {
|
|
159
219
|
const { containerVar, arrayExpr, param, indexParam, childIndexExpr, attrsBySlot, texts, childRefs, csrMaterialize } = plan
|
|
160
220
|
const hasAttrs = attrsBySlot.length > 0
|
|
@@ -92,18 +92,27 @@ export function emitDynamicTextUpdates(lines: string[], ctx: ClientJsContext): v
|
|
|
92
92
|
const normalElems = elems.filter(e => !e.insideConditional)
|
|
93
93
|
|
|
94
94
|
if (normalElems.length > 0 || conditionalElems.length > 0) {
|
|
95
|
+
// Persistent slot trackers for non-conditional elements. `__bfText`
|
|
96
|
+
// returns the node now occupying the slot; a JSX-valued expression
|
|
97
|
+
// (`{themeLogo(id)}`) replaces the text node with a live element, so
|
|
98
|
+
// the next reactive run must operate on that element, not the stale
|
|
99
|
+
// text node (#1663). Primitive values keep the same text node.
|
|
100
|
+
for (const elem of normalElems) {
|
|
101
|
+
const v = varSlotId(elem.slotId)
|
|
102
|
+
lines.push(` let __anchor_${v} = _${v}`)
|
|
103
|
+
}
|
|
95
104
|
lines.push(` createEffect(() => {`)
|
|
96
105
|
if (normalElems.length > 0) {
|
|
97
106
|
// Expression is always evaluated for non-conditional elements
|
|
98
107
|
lines.push(` const __val = ${expr}`)
|
|
99
108
|
for (const elem of normalElems) {
|
|
100
109
|
const v = varSlotId(elem.slotId)
|
|
101
|
-
lines.push(`
|
|
110
|
+
lines.push(` __anchor_${v} = __bfText(__anchor_${v}, __val)`)
|
|
102
111
|
}
|
|
103
112
|
for (const elem of conditionalElems) {
|
|
104
113
|
const v = varSlotId(elem.slotId)
|
|
105
114
|
lines.push(` const [__el_${v}] = $t(__scope, '${elem.slotId}')`)
|
|
106
|
-
lines.push(`
|
|
115
|
+
lines.push(` __bfText(__el_${v}, __val)`)
|
|
107
116
|
}
|
|
108
117
|
} else {
|
|
109
118
|
// Only conditional elements — evaluate expression unconditionally
|
|
@@ -118,7 +127,7 @@ export function emitDynamicTextUpdates(lines: string[], ctx: ClientJsContext): v
|
|
|
118
127
|
for (const elem of conditionalElems) {
|
|
119
128
|
const v = varSlotId(elem.slotId)
|
|
120
129
|
lines.push(` const [__el_${v}] = $t(__scope, '${elem.slotId}')`)
|
|
121
|
-
lines.push(`
|
|
130
|
+
lines.push(` __bfText(__el_${v}, __val)`)
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
lines.push(` })`)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { AttrValue, IRAttribute, IRNode } from '../types'
|
|
6
6
|
import { isBooleanAttr } from '../html-constants'
|
|
7
|
-
import { toHtmlAttrName, attrValueToString, quotePropName, PROPS_PARAM, DATA_BF_PH, keyAttrName, loopStartMarker, loopEndMarker, freeIdsFromRefs, setIntersects, wrapExprWithLoopParams } from './utils'
|
|
7
|
+
import { toHtmlAttrName, attrValueToString, quotePropName, PROPS_PARAM, DATA_BF_PH, keyAttrName, loopStartMarker, loopEndMarker, loopItemMarker, freeIdsFromRefs, setIntersects, wrapExprWithLoopParams } from './utils'
|
|
8
8
|
import type { LoopParamSpec } from './utils'
|
|
9
9
|
import { nameForRegistryRef } from './component-scope'
|
|
10
10
|
import { assertNever } from './walker'
|
|
@@ -411,6 +411,17 @@ function buildSpreadAttrsMergeCall(args: {
|
|
|
411
411
|
* `generateCsrTemplate` (case `'component'`). Set to `true` when generating
|
|
412
412
|
* the per-iteration `staticItemTemplate` for static loops.
|
|
413
413
|
*/
|
|
414
|
+
/**
|
|
415
|
+
* Build the per-item `<!--bf-loop-i:KEY-->` anchor comment for a whole-item
|
|
416
|
+
* conditional loop (#1665), where `keyExpr` is the loop's per-item key
|
|
417
|
+
* expression (e.g. `t.id`). Emits a live `${keyExpr}` interpolation so each
|
|
418
|
+
* rendered item carries its own key — `loopItemMarker` is reserved for
|
|
419
|
+
* already-evaluated key strings (runtime / static contexts).
|
|
420
|
+
*/
|
|
421
|
+
function itemAnchorTemplate(keyExpr: string): string {
|
|
422
|
+
return `<!--${loopItemMarker('${' + keyExpr + '}')}-->`
|
|
423
|
+
}
|
|
424
|
+
|
|
414
425
|
export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string, insideLoop = false, inHoistedChildren = false): string {
|
|
415
426
|
const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, insideLoop, inHoistedChildren)
|
|
416
427
|
const wrapExpr = (expr: string) => wrapExprWithLoopParams(expr, loopParams)
|
|
@@ -558,7 +569,16 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
558
569
|
// Case 1 — childComponent body materialize); propagating it through
|
|
559
570
|
// every nested loop regressed form-builder's inner-loop Select wiring.
|
|
560
571
|
const innerRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop)
|
|
561
|
-
|
|
572
|
+
let childTemplate = node.children.map(innerRecurse).join('')
|
|
573
|
+
// Whole-item conditional loops (#1665): prepend an always-present
|
|
574
|
+
// `<!--bf-loop-i:KEY-->` anchor before each item's (possibly empty)
|
|
575
|
+
// conditional content. `mapArrayAnchored` tracks items by this anchor,
|
|
576
|
+
// so an item that renders no element still keeps its identity and slot.
|
|
577
|
+
// The key is a per-item expression, so the marker carries a live
|
|
578
|
+
// `${KEY}` interpolation (not the literal key text).
|
|
579
|
+
if (node.bodyIsItemConditional && node.key) {
|
|
580
|
+
childTemplate = `${itemAnchorTemplate(node.key)}${childTemplate}`
|
|
581
|
+
}
|
|
562
582
|
const indexParam = node.index ? `, ${node.index}` : ''
|
|
563
583
|
// Apply chained sort / filter for the SSR-mirror template (#1448
|
|
564
584
|
// Tier B). Pre-Tier-B this just used `node.array` directly,
|
|
@@ -1499,7 +1519,13 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
1499
1519
|
}
|
|
1500
1520
|
|
|
1501
1521
|
case 'loop': {
|
|
1502
|
-
|
|
1522
|
+
let childTemplate = node.children.map(recurseInLoop).join('')
|
|
1523
|
+
// Whole-item conditional loops (#1665): prepend the per-item
|
|
1524
|
+
// `<!--bf-loop-i:KEY-->` anchor so `mapArrayAnchored` can track items
|
|
1525
|
+
// that render no element. Mirrors the `irToHtmlTemplate` loop case.
|
|
1526
|
+
if (node.bodyIsItemConditional && node.key) {
|
|
1527
|
+
childTemplate = `${itemAnchorTemplate(node.key)}${childTemplate}`
|
|
1528
|
+
}
|
|
1503
1529
|
const indexParam = node.index ? `, ${node.index}` : ''
|
|
1504
1530
|
// An init-scope-only array would `undefined.map(...)` ⇒ TypeError.
|
|
1505
1531
|
// Substitute an empty array; init's reconcile pass populates the loop
|
|
@@ -7,12 +7,12 @@ import type { ComponentIR, IRNode } from '../types'
|
|
|
7
7
|
// All exports from @barefootjs/client/runtime that may be used in generated code
|
|
8
8
|
export const RUNTIME_IMPORT_CANDIDATES = [
|
|
9
9
|
'createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount',
|
|
10
|
-
'hydrate', 'insert', 'reconcileElements', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'createDisposableEffect',
|
|
10
|
+
'hydrate', 'insert', 'reconcileElements', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'createDisposableEffect',
|
|
11
11
|
'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild', 'updateClientMarker',
|
|
12
12
|
'createPortal',
|
|
13
13
|
'provideContext', 'createContext', 'useContext',
|
|
14
14
|
'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss',
|
|
15
|
-
'qsa', 'qsaItem', 'qsaChildScope', 'qsaChildScopes', 'upsertChildItem', '__slot', '__bfSlot',
|
|
15
|
+
'qsa', 'qsaItem', 'qsaChildScope', 'qsaChildScopes', 'upsertChildItem', '__slot', '__bfSlot', '__bfText',
|
|
16
16
|
] as const
|
|
17
17
|
|
|
18
18
|
/** @deprecated Use RUNTIME_IMPORT_CANDIDATES */
|
|
@@ -574,7 +574,23 @@ export function collectLoopChildReactiveAttrs(
|
|
|
574
574
|
// SSR template strips the attribute (html-template) and no
|
|
575
575
|
// hydrate-time binding is emitted, leaving the per-item
|
|
576
576
|
// attribute permanently unset.
|
|
577
|
-
|
|
577
|
+
//
|
|
578
|
+
// `classifyReactivity` only proves reactivity for the loop item
|
|
579
|
+
// accessor or a *directly* read signal/memo/prop. It does NOT see
|
|
580
|
+
// through an opaque helper that reads an outer signal by index
|
|
581
|
+
// (e.g. `widthAt(i)` where `const widthAt = (i) => items()[i].w`).
|
|
582
|
+
// The top-level attribute path (`decideWrapForAttr`) wraps those
|
|
583
|
+
// anyway via the Solid-style AST-flag fallback (#940); without the
|
|
584
|
+
// same fallback here, the identical binding on a per-item element
|
|
585
|
+
// freezes at its SSR value (#1673). Apply the same `callsReactiveGetters`
|
|
586
|
+
// / `hasFunctionCalls` fallback so the loop-child path matches the
|
|
587
|
+
// top-level one — a harmless over-wrap at worst (an effect that
|
|
588
|
+
// subscribes to nothing runs once).
|
|
589
|
+
const reactive =
|
|
590
|
+
classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind !== 'none'
|
|
591
|
+
|| attr.callsReactiveGetters
|
|
592
|
+
|| attr.hasFunctionCalls
|
|
593
|
+
if (!attr.clientOnly && !reactive) continue
|
|
578
594
|
attrs.push({
|
|
579
595
|
childSlotId: el.slotId,
|
|
580
596
|
attrName: attr.name,
|
|
@@ -161,10 +161,14 @@ function emitInnerLoopNested(lines: string[], plan: InnerLoopNestedInitPlan): vo
|
|
|
161
161
|
for (const stmt of innerPreludeStatements) {
|
|
162
162
|
lines.push(` ${stmt}`)
|
|
163
163
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
// Each inner-loop component gets a uniquely-suffixed `__compEl` binding.
|
|
165
|
+
// Multiple comps share one inner `forEach` body, so a fixed name would
|
|
166
|
+
// re-declare `const __compEl` in the same scope (#1664).
|
|
167
|
+
comps.forEach((comp, i) => {
|
|
168
|
+
const compElVar = comps.length > 1 ? `__compEl${i}` : '__compEl'
|
|
169
|
+
lines.push(` const ${compElVar} = qsaChildScope(__innerEl, ${comp.selector})`)
|
|
170
|
+
lines.push(` if (${compElVar}) initChild('${nameForRegistryRef(comp.componentName)}', ${compElVar}, ${comp.propsExpr})`)
|
|
171
|
+
})
|
|
168
172
|
lines.push(` })`)
|
|
169
173
|
lines.push(` })`)
|
|
170
174
|
lines.push(` }`)
|
|
@@ -200,6 +200,13 @@ export interface LoopCore {
|
|
|
200
200
|
* key tracks all of its DOM nodes (#1212).
|
|
201
201
|
*/
|
|
202
202
|
bodyIsMultiRoot?: boolean
|
|
203
|
+
/**
|
|
204
|
+
* True when the loop body is a single whole-item conditional whose at
|
|
205
|
+
* least one branch renders no element (#1665). Routes the loop through
|
|
206
|
+
* the anchored emission path (`mapArrayAnchored` + per-item
|
|
207
|
+
* `<!--bf-loop-i:KEY-->` anchors) so 0-or-1-element items reconcile.
|
|
208
|
+
*/
|
|
209
|
+
bodyIsItemConditional?: boolean
|
|
203
210
|
/**
|
|
204
211
|
* Pre-computed free identifiers referenced by the `array` expression
|
|
205
212
|
* (#1267). Populated during IR build from the originating AST node so
|
|
@@ -7,7 +7,12 @@ import ts from 'typescript'
|
|
|
7
7
|
import type { AttrValue, IRTemplatePart, LoopParamBinding, FreeReference, IRNode } from '../types'
|
|
8
8
|
import type { TopLevelLoop, BranchLoop } from './types'
|
|
9
9
|
import { buildLoopChainExpr } from '../loop-chain'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
iterateJsTokens,
|
|
12
|
+
isIdentifierLikeToken,
|
|
13
|
+
isTriviaKind,
|
|
14
|
+
replaceInExprContexts,
|
|
15
|
+
} from '../scanner/js-scanner'
|
|
11
16
|
import {
|
|
12
17
|
BF_KEY as DATA_KEY,
|
|
13
18
|
BF_KEY_PREFIX as DATA_KEY_PREFIX,
|
|
@@ -16,10 +21,11 @@ import {
|
|
|
16
21
|
BF_LOOP_END,
|
|
17
22
|
loopStartMarker,
|
|
18
23
|
loopEndMarker,
|
|
24
|
+
loopItemMarker,
|
|
19
25
|
toHTMLAttrName as toHtmlAttrName,
|
|
20
26
|
} from '@barefootjs/shared'
|
|
21
27
|
|
|
22
|
-
export { DATA_KEY, DATA_KEY_PREFIX, DATA_BF_PH, BF_LOOP_START, BF_LOOP_END, loopStartMarker, loopEndMarker, toHtmlAttrName }
|
|
28
|
+
export { DATA_KEY, DATA_KEY_PREFIX, DATA_BF_PH, BF_LOOP_START, BF_LOOP_END, loopStartMarker, loopEndMarker, loopItemMarker, toHtmlAttrName }
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
31
|
* Parameter name for the props object in generated init/template functions.
|
|
@@ -376,124 +382,33 @@ export function tokenContainsIdent(expr: string, ident: string): boolean {
|
|
|
376
382
|
return scanForIdentifiers(expr, (token) => token === ident)
|
|
377
383
|
}
|
|
378
384
|
|
|
379
|
-
const IDENT_START_RE = /[A-Za-z_$]/
|
|
380
|
-
const IDENT_PART_RE = /[A-Za-z0-9_$]/
|
|
381
|
-
|
|
382
385
|
/**
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* name
|
|
386
|
+
* Walk a JS-like expression string via the shared `ts.createScanner`-based
|
|
387
|
+
* lexer and invoke `predicate` on every identifier-like token found in a
|
|
388
|
+
* position where bare identifiers are semantically possible — i.e. not
|
|
389
|
+
* inside a string / template-string body / comment / regex literal, and
|
|
390
|
+
* not the property name of a member-access expression. Returns true on the
|
|
391
|
+
* first hit.
|
|
392
|
+
*
|
|
393
|
+
* Delegating to `iterateJsTokens` (rather than a hand-rolled char-by-char
|
|
394
|
+
* state machine) means regex literals are recognised: `/it's/.test(foo)`
|
|
395
|
+
* no longer reads the apostrophe as a string opener, and an identifier
|
|
396
|
+
* inside a regex body (`/className/`) is correctly treated as opaque (#1370).
|
|
388
397
|
*/
|
|
389
398
|
function scanForIdentifiers(expr: string, predicate: (token: string) => boolean): boolean {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
let braceDepth = 0
|
|
402
|
-
|
|
403
|
-
while (i < n) {
|
|
404
|
-
const ch = expr[i]
|
|
405
|
-
|
|
406
|
-
switch (state) {
|
|
407
|
-
case 0: // code
|
|
408
|
-
case 4: { // template expression — same lexing rules as code
|
|
409
|
-
// String / template literal openers
|
|
410
|
-
if (ch === "'") { state = 1; i++; continue }
|
|
411
|
-
if (ch === '"') { state = 2; i++; continue }
|
|
412
|
-
if (ch === '`') { state = 3; i++; continue }
|
|
413
|
-
// Comment openers
|
|
414
|
-
if (ch === '/' && i + 1 < n) {
|
|
415
|
-
const next = expr[i + 1]
|
|
416
|
-
if (next === '/') { state = 5; i += 2; continue }
|
|
417
|
-
if (next === '*') { state = 6; i += 2; continue }
|
|
418
|
-
}
|
|
419
|
-
// Track braces only inside template-expression state, so we know when
|
|
420
|
-
// we leave `${ ... }` back to the surrounding template text.
|
|
421
|
-
if (state === 4) {
|
|
422
|
-
if (ch === '{') { braceDepth++; i++; continue }
|
|
423
|
-
if (ch === '}') {
|
|
424
|
-
if (braceDepth === 0) {
|
|
425
|
-
// Closing `}` of `${ ... }` — pop back to enclosing tmpl state.
|
|
426
|
-
const restored = tmplExprStack.pop()
|
|
427
|
-
braceDepth = restored ?? 0
|
|
428
|
-
state = 3
|
|
429
|
-
i++
|
|
430
|
-
continue
|
|
431
|
-
}
|
|
432
|
-
braceDepth--
|
|
433
|
-
i++
|
|
434
|
-
continue
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
// Identifier start
|
|
438
|
-
if (IDENT_START_RE.test(ch)) {
|
|
439
|
-
let j = i + 1
|
|
440
|
-
while (j < n && IDENT_PART_RE.test(expr[j])) j++
|
|
441
|
-
const token = expr.slice(i, j)
|
|
442
|
-
// Skip member-access tail: identifier preceded by `.` (ignoring
|
|
443
|
-
// whitespace).
|
|
444
|
-
let prev = i - 1
|
|
445
|
-
while (prev >= 0 && (expr[prev] === ' ' || expr[prev] === '\t' || expr[prev] === '\n' || expr[prev] === '\r')) prev--
|
|
446
|
-
const isMemberTail = prev >= 0 && expr[prev] === '.' && (prev === 0 || expr[prev - 1] !== '.') // not `..` (spread)
|
|
447
|
-
if (!isMemberTail && predicate(token)) return true
|
|
448
|
-
i = j
|
|
449
|
-
continue
|
|
450
|
-
}
|
|
451
|
-
i++
|
|
452
|
-
continue
|
|
453
|
-
}
|
|
454
|
-
case 1: { // single-quote string
|
|
455
|
-
if (ch === '\\' && i + 1 < n) { i += 2; continue }
|
|
456
|
-
if (ch === "'") { state = 0; i++; continue }
|
|
457
|
-
i++
|
|
458
|
-
continue
|
|
459
|
-
}
|
|
460
|
-
case 2: { // double-quote string
|
|
461
|
-
if (ch === '\\' && i + 1 < n) { i += 2; continue }
|
|
462
|
-
if (ch === '"') { state = 0; i++; continue }
|
|
463
|
-
i++
|
|
464
|
-
continue
|
|
465
|
-
}
|
|
466
|
-
case 3: { // template literal text
|
|
467
|
-
if (ch === '\\' && i + 1 < n) { i += 2; continue }
|
|
468
|
-
if (ch === '`') {
|
|
469
|
-
// Closing the template literal; return to whatever code state we
|
|
470
|
-
// came from (either top-level code or an outer template expression).
|
|
471
|
-
state = tmplExprStack.length > 0 ? 4 : 0
|
|
472
|
-
i++
|
|
473
|
-
continue
|
|
474
|
-
}
|
|
475
|
-
if (ch === '$' && i + 1 < n && expr[i + 1] === '{') {
|
|
476
|
-
// Entering `${ ... }`: save current outer brace depth, reset for new.
|
|
477
|
-
tmplExprStack.push(braceDepth)
|
|
478
|
-
braceDepth = 0
|
|
479
|
-
state = 4
|
|
480
|
-
i += 2
|
|
481
|
-
continue
|
|
482
|
-
}
|
|
483
|
-
i++
|
|
484
|
-
continue
|
|
485
|
-
}
|
|
486
|
-
case 5: { // line comment
|
|
487
|
-
if (ch === '\n' || ch === '\r') { state = 0; i++; continue }
|
|
488
|
-
i++
|
|
489
|
-
continue
|
|
490
|
-
}
|
|
491
|
-
case 6: { // block comment
|
|
492
|
-
if (ch === '*' && i + 1 < n && expr[i + 1] === '/') { state = 0; i += 2; continue }
|
|
493
|
-
i++
|
|
494
|
-
continue
|
|
495
|
-
}
|
|
399
|
+
// Previous *significant* (non-trivia) token kind, used to skip the tail
|
|
400
|
+
// of a member access (`a.foo`, `a?.foo`) while still treating the head
|
|
401
|
+
// (`foo.bar`) and spread targets (`...foo`) as real references.
|
|
402
|
+
let prevSignificant: ts.SyntaxKind | undefined
|
|
403
|
+
for (const tok of iterateJsTokens(expr)) {
|
|
404
|
+
if (isTriviaKind(tok.kind)) continue
|
|
405
|
+
if (isIdentifierLikeToken(tok.kind)) {
|
|
406
|
+
const isMemberTail =
|
|
407
|
+
prevSignificant === ts.SyntaxKind.DotToken
|
|
408
|
+
|| prevSignificant === ts.SyntaxKind.QuestionDotToken
|
|
409
|
+
if (!isMemberTail && predicate(expr.slice(tok.pos, tok.end))) return true
|
|
496
410
|
}
|
|
411
|
+
prevSignificant = tok.kind
|
|
497
412
|
}
|
|
498
413
|
return false
|
|
499
414
|
}
|