@barefootjs/jsx 0.10.1 → 0.11.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/compiler.d.ts.map +1 -1
- package/dist/debug-profile.d.ts +115 -0
- package/dist/debug-profile.d.ts.map +1 -0
- package/dist/debug.d.ts +4 -3
- package/dist/debug.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +31 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1872 -207
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +6 -0
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts +2 -2
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts +3 -3
- 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/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts +2 -0
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +2 -0
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +4 -2
- 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/build-reactive-effects.d.ts +3 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +7 -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/plan/inner-loop.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/insert.d.ts +8 -0
- package/dist/ir-to-client-js/control-flow/plan/insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +8 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +28 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/composite-loop.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/control-flow/stringify/event-listener.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-listener.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +2 -2
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.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/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.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/index.d.ts +2 -2
- package/dist/ir-to-client-js/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/effects-and-on-mounts.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/event-handlers.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +6 -0
- package/dist/ir-to-client-js/plan/declaration-emit.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 +29 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts +26 -0
- package/dist/loop-destructure.d.ts.map +1 -0
- package/dist/profiler.d.ts +492 -0
- package/dist/profiler.d.ts.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/debug-profile.test.ts +405 -0
- package/src/__tests__/expression-parser.test.ts +44 -1
- package/src/__tests__/profile-bfid-emission.test.ts +63 -0
- package/src/__tests__/profile-binding-ids.test.ts +123 -0
- package/src/__tests__/profile-cond-binding-ids.test.ts +80 -0
- package/src/__tests__/profile-loop-binding-ids.test.ts +106 -0
- package/src/__tests__/profile-nested-binding-ids.test.ts +153 -0
- package/src/__tests__/profile-turn-markers-branch.test.ts +83 -0
- package/src/__tests__/profile-turn-markers-delegation.test.ts +63 -0
- package/src/__tests__/profile-turn-markers.test.ts +54 -0
- package/src/__tests__/profiler-batch-advisor.test.ts +198 -0
- package/src/__tests__/profiler-coverage-conformance.test.ts +360 -0
- package/src/__tests__/profiler-e2e.test.ts +104 -0
- package/src/__tests__/profiler-hot-subscribers.test.ts +263 -0
- package/src/__tests__/profiler-wasted-re-runs.test.ts +147 -0
- package/src/__tests__/profiler.test.ts +408 -0
- package/src/compiler.ts +3 -0
- package/src/debug-profile.ts +543 -0
- package/src/debug.ts +192 -28
- package/src/expression-parser.ts +53 -0
- package/src/index.ts +72 -1
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +5 -3
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +3 -1
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +8 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +19 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -0
- package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +9 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +12 -8
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +10 -4
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +7 -0
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +7 -0
- package/src/ir-to-client-js/control-flow/plan/insert.ts +8 -0
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +8 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +28 -0
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +5 -3
- package/src/ir-to-client-js/control-flow/stringify/component-loop.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +6 -3
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +14 -2
- package/src/ir-to-client-js/control-flow/stringify/event-listener.ts +5 -2
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +13 -11
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +19 -7
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +18 -13
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +9 -7
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +18 -14
- package/src/ir-to-client-js/control-flow.ts +12 -6
- package/src/ir-to-client-js/emit-reactive.ts +18 -5
- package/src/ir-to-client-js/imports.ts +2 -0
- package/src/ir-to-client-js/index.ts +6 -1
- package/src/ir-to-client-js/phases/effects-and-on-mounts.ts +10 -4
- package/src/ir-to-client-js/phases/event-handlers.ts +6 -2
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +7 -1
- package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +12 -6
- package/src/ir-to-client-js/types.ts +5 -0
- package/src/ir-to-client-js/utils.ts +37 -0
- package/src/jsx-to-ir.ts +2 -2
- package/src/loop-destructure.ts +170 -0
- package/src/profiler.ts +1488 -0
- package/src/types.ts +8 -0
|
@@ -62,6 +62,7 @@ export function buildDeclarationEmitPlan(
|
|
|
62
62
|
kind: 'memo',
|
|
63
63
|
name: decl.info.name,
|
|
64
64
|
computationExpr: decl.info.computation,
|
|
65
|
+
bfId: ctx.profile ? `${ctx.componentName}#memo:${decl.info.name}` : undefined,
|
|
65
66
|
}
|
|
66
67
|
case 'function': {
|
|
67
68
|
const fn = decl.info
|
|
@@ -84,13 +85,18 @@ function buildSignalPlan(
|
|
|
84
85
|
ctx: ClientJsContext,
|
|
85
86
|
lookups: DeclarationEmitLookups,
|
|
86
87
|
): SignalEmitPlan {
|
|
88
|
+
const controlledEffect = buildControlledSignalEffect(signal, lookups)
|
|
89
|
+
if (controlledEffect && ctx.profile) {
|
|
90
|
+
controlledEffect.bfId = `${ctx.componentName}#effect:controlled:${signal.setter}`
|
|
91
|
+
}
|
|
87
92
|
return {
|
|
88
93
|
kind: 'signal',
|
|
89
94
|
getter: signal.getter,
|
|
90
95
|
setter: signal.setter,
|
|
91
96
|
initialValueExpr: resolveSignalInitialValue(signal, ctx, lookups),
|
|
92
|
-
controlledEffect
|
|
97
|
+
controlledEffect,
|
|
93
98
|
branchCondition: signal.branchCondition,
|
|
99
|
+
bfId: ctx.profile ? `${ctx.componentName}#signal:${signal.getter}` : undefined,
|
|
94
100
|
}
|
|
95
101
|
}
|
|
96
102
|
|
|
@@ -52,6 +52,8 @@ export interface SignalEmitPlan {
|
|
|
52
52
|
* cell #8.
|
|
53
53
|
*/
|
|
54
54
|
branchCondition?: string
|
|
55
|
+
/** Profile-mode IR-aligned id, appended as the `createSignal` 2nd arg (#1690). */
|
|
56
|
+
bfId?: string
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export interface ControlledSignalEffectPlan {
|
|
@@ -59,6 +61,8 @@ export interface ControlledSignalEffectPlan {
|
|
|
59
61
|
setter: string
|
|
60
62
|
/** Fully-resolved accessor expression read inside the effect. */
|
|
61
63
|
accessorExpr: string
|
|
64
|
+
/** Profile-mode IR-aligned id, appended as the `createEffect` 2nd arg (#1690). */
|
|
65
|
+
bfId?: string
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
export interface MemoEmitPlan {
|
|
@@ -66,6 +70,8 @@ export interface MemoEmitPlan {
|
|
|
66
70
|
name: string
|
|
67
71
|
/** Source expression passed to `createMemo(...)`. */
|
|
68
72
|
computationExpr: string
|
|
73
|
+
/** Profile-mode IR-aligned id, appended as the `createMemo` 2nd arg (#1690). */
|
|
74
|
+
bfId?: string
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
export interface FunctionEmitPlan {
|
|
@@ -59,7 +59,13 @@ function emitConstant(lines: string[], plan: ConstantEmitPlan): void {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Profile-mode trailing `__bfId` argument (`, "Comp#signal:x"`), or '' when off. */
|
|
63
|
+
function bfIdArg(bfId: string | undefined): string {
|
|
64
|
+
return bfId ? `, ${JSON.stringify(bfId)}` : ''
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
function emitSignal(lines: string[], plan: SignalEmitPlan): void {
|
|
68
|
+
const id = bfIdArg(plan.bfId)
|
|
63
69
|
if (plan.branchCondition) {
|
|
64
70
|
// #1414 cell #8: signal declared inside an early-return `if`-block.
|
|
65
71
|
// Hoist as `let` so closures and event handlers hoisted to outer
|
|
@@ -70,12 +76,12 @@ function emitSignal(lines: string[], plan: SignalEmitPlan): void {
|
|
|
70
76
|
if (plan.setter) {
|
|
71
77
|
lines.push(` let ${plan.getter}, ${plan.setter}`)
|
|
72
78
|
lines.push(` if (${plan.branchCondition}) {`)
|
|
73
|
-
lines.push(` ;[${plan.getter}, ${plan.setter}] = createSignal(${plan.initialValueExpr})`)
|
|
79
|
+
lines.push(` ;[${plan.getter}, ${plan.setter}] = createSignal(${plan.initialValueExpr}${id})`)
|
|
74
80
|
lines.push(` }`)
|
|
75
81
|
} else {
|
|
76
82
|
lines.push(` let ${plan.getter}`)
|
|
77
83
|
lines.push(` if (${plan.branchCondition}) {`)
|
|
78
|
-
lines.push(` ;[${plan.getter}] = createSignal(${plan.initialValueExpr})`)
|
|
84
|
+
lines.push(` ;[${plan.getter}] = createSignal(${plan.initialValueExpr}${id})`)
|
|
79
85
|
lines.push(` }`)
|
|
80
86
|
}
|
|
81
87
|
// Controlled effects don't apply to branch-conditioned signals — a
|
|
@@ -84,9 +90,9 @@ function emitSignal(lines: string[], plan: SignalEmitPlan): void {
|
|
|
84
90
|
return
|
|
85
91
|
}
|
|
86
92
|
if (plan.setter) {
|
|
87
|
-
lines.push(` const [${plan.getter}, ${plan.setter}] = createSignal(${plan.initialValueExpr})`)
|
|
93
|
+
lines.push(` const [${plan.getter}, ${plan.setter}] = createSignal(${plan.initialValueExpr}${id})`)
|
|
88
94
|
} else {
|
|
89
|
-
lines.push(` const [${plan.getter}] = createSignal(${plan.initialValueExpr})`)
|
|
95
|
+
lines.push(` const [${plan.getter}] = createSignal(${plan.initialValueExpr}${id})`)
|
|
90
96
|
}
|
|
91
97
|
if (plan.controlledEffect) {
|
|
92
98
|
emitControlledEffect(lines, plan.controlledEffect)
|
|
@@ -97,11 +103,11 @@ function emitControlledEffect(lines: string[], plan: ControlledSignalEffectPlan)
|
|
|
97
103
|
lines.push(` createEffect(() => {`)
|
|
98
104
|
lines.push(` const __val = ${plan.accessorExpr}`)
|
|
99
105
|
lines.push(` if (__val !== undefined) ${plan.setter}(__val)`)
|
|
100
|
-
lines.push(` })`)
|
|
106
|
+
lines.push(` }${bfIdArg(plan.bfId)})`)
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
function emitMemo(lines: string[], plan: MemoEmitPlan): void {
|
|
104
|
-
lines.push(` const ${plan.name} = createMemo(${plan.computationExpr})`)
|
|
110
|
+
lines.push(` const ${plan.name} = createMemo(${plan.computationExpr}${bfIdArg(plan.bfId)})`)
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
function emitFunction(lines: string[], plan: FunctionEmitPlan): void {
|
|
@@ -40,6 +40,11 @@ export interface ClientJsContext {
|
|
|
40
40
|
* collide with same-named exports from other modules.
|
|
41
41
|
*/
|
|
42
42
|
nonExportedSiblings: Set<string>
|
|
43
|
+
/**
|
|
44
|
+
* Profile mode (#1690, SR3). When true, emit IR-aligned `__bfId` args at
|
|
45
|
+
* reactive creation sites. Off by default → byte-identical output (SR8).
|
|
46
|
+
*/
|
|
47
|
+
profile: boolean
|
|
43
48
|
signals: SignalInfo[]
|
|
44
49
|
memos: MemoInfo[]
|
|
45
50
|
effects: EffectInfo[]
|
|
@@ -50,6 +50,25 @@ export function varSlotId(slotId: string): string {
|
|
|
50
50
|
return slotId.startsWith('^') ? slotId.slice(1) : slotId
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Profile-mode DOM-binding id suffix (#1690, SR4). Returns the trailing
|
|
55
|
+
* `, "<Component>#binding:<slotId>"` argument for a binding effect's
|
|
56
|
+
* `createEffect` / `createDisposableEffect` / `insert` / `mapArray` call when
|
|
57
|
+
* `componentName` is set (profile on), else `''` so the emitted code stays
|
|
58
|
+
* byte-identical (SR8). Centralised so every binding emit site — top-level,
|
|
59
|
+
* conditional branch, loop child, inner loop — uses one id convention.
|
|
60
|
+
*
|
|
61
|
+
* A `'?'` slotId (an inner loop whose container element carries no `bf` slot
|
|
62
|
+
* marker) is NOT emittable: `buildIdIndex` keys on real `domBinding` slotIds, so
|
|
63
|
+
* `#binding:?` could never resolve and would be a guaranteed coverage gap. The
|
|
64
|
+
* analyzer likewise emits no `domBinding` for a slot-less loop, so suppressing
|
|
65
|
+
* the id here keeps both sides silent and consistent.
|
|
66
|
+
*/
|
|
67
|
+
export function profileBindingId(componentName: string | undefined, slotId: string): string {
|
|
68
|
+
if (!componentName || slotId === '?') return ''
|
|
69
|
+
return `, ${JSON.stringify(`${componentName}#binding:${slotId}`)}`
|
|
70
|
+
}
|
|
71
|
+
|
|
53
72
|
/**
|
|
54
73
|
* Convert a `template` variant's parts into a JS template-literal string.
|
|
55
74
|
* Shared by both `attrValueToString` and any consumer that wants to flatten
|
|
@@ -241,6 +260,24 @@ export function wrapHandlerInBlock(handler: string): string {
|
|
|
241
260
|
return trimmed
|
|
242
261
|
}
|
|
243
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Profile mode (#1690, SR3): wrap an event handler so a profiling run can
|
|
265
|
+
* attribute the reactive work it triggers to one turn. The original handler
|
|
266
|
+
* expression (arrow or identifier) is invoked verbatim with the forwarded
|
|
267
|
+
* args, bracketed by `beginTurn`/`endTurn`:
|
|
268
|
+
*
|
|
269
|
+
* (...__bfa) => { beginTurn("Comp#handler:slot:click"); try { return (HANDLER)(...__bfa) } finally { endTurn() } }
|
|
270
|
+
*
|
|
271
|
+
* Measurement-only: the handler's behavior and the synchronous `set()`
|
|
272
|
+
* semantics are unchanged — the markers just stamp a turn id onto the events
|
|
273
|
+
* emitted while it runs. Used at every handler emit site in profile mode so
|
|
274
|
+
* no path is left unattributed.
|
|
275
|
+
*/
|
|
276
|
+
export function wrapHandlerForTurn(handler: string, handlerId: string, loc?: string): string {
|
|
277
|
+
const idArg = loc ? `${JSON.stringify(handlerId)}, ${JSON.stringify(loc)}` : JSON.stringify(handlerId)
|
|
278
|
+
return `(...__bfa) => { beginTurn(${idArg}); try { return (${handler.trim()})(...__bfa) } finally { endTurn() } }`
|
|
279
|
+
}
|
|
280
|
+
|
|
244
281
|
/**
|
|
245
282
|
* Emit a ref-binding call `(callback)(elementVar)`, optionally guarded so the
|
|
246
283
|
* call no-ops when the callback is undefined.
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
AttrValueOf,
|
|
34
34
|
} from './types.ts'
|
|
35
35
|
import { type AnalyzerContext, type MultiReturnJsxInfo, getSourceLocation } from './analyzer-context.ts'
|
|
36
|
-
import { parseExpression, isSupported, parseBlockBody, extractSortComparatorFromTS, type ParsedExpr, type ParsedStatement, type SortComparator } from './expression-parser.ts'
|
|
36
|
+
import { parseExpression, isSupported, parseBlockBody, extractSortComparatorFromTS, cssKebabCase, type ParsedExpr, type ParsedStatement, type SortComparator } from './expression-parser.ts'
|
|
37
37
|
import { createError, ErrorCodes, internalInvariant } from './errors.ts'
|
|
38
38
|
import { containsReactiveExpression } from './reactivity-checker.ts'
|
|
39
39
|
import {
|
|
@@ -3804,7 +3804,7 @@ function tryStaticStyleObjectToCss(expr: ts.ObjectLiteralExpression): string | n
|
|
|
3804
3804
|
if (!ts.isPropertyAssignment(prop)) return null
|
|
3805
3805
|
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) return null
|
|
3806
3806
|
if (!ts.isStringLiteral(prop.initializer)) return null
|
|
3807
|
-
const key = prop.name.text
|
|
3807
|
+
const key = cssKebabCase(prop.name.text)
|
|
3808
3808
|
parts.push(`${key}:${prop.initializer.text}`)
|
|
3809
3809
|
}
|
|
3810
3810
|
return parts.join(';')
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { IRLoop, IRNode, AttrValue, IRTemplatePart } from './types.ts'
|
|
2
|
+
|
|
3
|
+
const SIMPLE_FIELD = /^\.[A-Za-z_$][\w$]*$/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* True when a loop's destructure param is the single shape the non-JS SSR
|
|
7
|
+
* adapters (Go template / Mojolicious / Xslate) can lower today:
|
|
8
|
+
*
|
|
9
|
+
* `arr.map(({ id, title, ...rest }) => …)`
|
|
10
|
+
*
|
|
11
|
+
* where every binding is a simple `.field` access or an **object-rest read only
|
|
12
|
+
* via member access** (`rest.flag`). The adapters lower the rest binding as an
|
|
13
|
+
* alias to the whole iteration item, which matches JS rest semantics only for
|
|
14
|
+
* member reads of non-consumed keys — so any other use must be refused:
|
|
15
|
+
*
|
|
16
|
+
* - array-rest / array-index / nested paths (`[a, ...t]`, `{ cells: [h] }`)
|
|
17
|
+
* need index/slice the `range`/`for` can't express inline;
|
|
18
|
+
* - spread (`{...rest}`) and bare value uses (`String(rest)`, `{rest}`,
|
|
19
|
+
* `fn(rest)`) would observe the consumed keys too — they need a residual
|
|
20
|
+
* object the templates can't build inline;
|
|
21
|
+
* - a chained `.filter().map(destructure)` would need the filter-param
|
|
22
|
+
* rewrite to target the synthetic per-item var, so it's refused as well.
|
|
23
|
+
*
|
|
24
|
+
* Unsupported shapes fall through to the adapters' BF104 diagnostic. The scan
|
|
25
|
+
* is conservative: an ambiguous use refuses (false-negative is safe — it keeps
|
|
26
|
+
* the existing build-time error rather than shipping wrong output).
|
|
27
|
+
*/
|
|
28
|
+
export function isLowerableObjectRestDestructure(loop: IRLoop): boolean {
|
|
29
|
+
const bindings = loop.paramBindings
|
|
30
|
+
if (!bindings || bindings.length === 0) return false
|
|
31
|
+
if (loop.filterPredicate) return false
|
|
32
|
+
// The SSR adapters emit a synthetic per-item loop variable in the reserved
|
|
33
|
+
// `__bf_item` namespace (depth-suffixed on Go). A user destructure binding —
|
|
34
|
+
// or the `index` param — in that namespace would collide with / shadow the
|
|
35
|
+
// synthetic var (duplicate `my $__bf_item …` locals, ambiguous accessors), so
|
|
36
|
+
// refuse the lowering (→ BF104) rather than emit broken template locals.
|
|
37
|
+
for (const name of [...bindings.map(b => b.name), loop.index]) {
|
|
38
|
+
if (name && name.startsWith('__bf_')) return false
|
|
39
|
+
}
|
|
40
|
+
for (const b of bindings) {
|
|
41
|
+
if (b.rest) {
|
|
42
|
+
if (b.rest.kind !== 'object') return false
|
|
43
|
+
} else if (!SIMPLE_FIELD.test(b.path)) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const restNames = bindings.filter(b => b.rest).map(b => b.name)
|
|
48
|
+
if (restNames.length === 0) return true
|
|
49
|
+
return !restNamesMisused(loop, restNames)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Walks the whole loop subtree (every node type, every expression-bearing
|
|
54
|
+
* field) and reports whether any rest name is referenced as something other
|
|
55
|
+
* than a member-access base (`rest.flag` / `rest?.flag`). A spread expr
|
|
56
|
+
* (`{...rest}` → expr `"rest"`) and bare value uses are caught by the same
|
|
57
|
+
* regex; a property of an unrelated object (`foo.rest`) is excluded by the
|
|
58
|
+
* lookbehind.
|
|
59
|
+
*
|
|
60
|
+
* The scan covers the gated loop's own non-children expression fields too
|
|
61
|
+
* (`array` / `key` / `mapPreamble` / `flatMapCallback` body) — a bare rest use
|
|
62
|
+
* can surface there (e.g. `.map(({ ...rest }) => { const x = rest; … })`
|
|
63
|
+
* lifts `const x = rest` into `mapPreamble`), plus intrinsic element event
|
|
64
|
+
* handlers (`onClick={() => fn(rest)}`).
|
|
65
|
+
*/
|
|
66
|
+
function restNamesMisused(loop: IRLoop, names: string[]): boolean {
|
|
67
|
+
const valueUse = names.map(
|
|
68
|
+
n => new RegExp(`(?<![\\w.$])${escapeRe(n)}(?!\\s*\\??\\.)(?![\\w$])`),
|
|
69
|
+
)
|
|
70
|
+
let misused = false
|
|
71
|
+
const check = (s: string | undefined | null): void => {
|
|
72
|
+
if (!s || misused) return
|
|
73
|
+
for (const re of valueUse) {
|
|
74
|
+
if (re.test(s)) {
|
|
75
|
+
misused = true
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const attr = (v: AttrValue): void => {
|
|
81
|
+
if (v.kind === 'expression' || v.kind === 'spread') {
|
|
82
|
+
check(v.expr)
|
|
83
|
+
check(v.templateExpr)
|
|
84
|
+
} else if (v.kind === 'template') {
|
|
85
|
+
v.parts.forEach(part)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const part = (p: IRTemplatePart): void => {
|
|
89
|
+
if (p.type === 'ternary') {
|
|
90
|
+
check(p.condition)
|
|
91
|
+
check(p.templateCondition)
|
|
92
|
+
check(p.whenTrue)
|
|
93
|
+
check(p.whenFalse)
|
|
94
|
+
} else if (p.type === 'lookup') {
|
|
95
|
+
check(p.key)
|
|
96
|
+
check(p.templateKey)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const visitLoop = (l: IRLoop): void => {
|
|
100
|
+
if (misused) return
|
|
101
|
+
check(l.array)
|
|
102
|
+
check(l.templateArray)
|
|
103
|
+
check(l.key)
|
|
104
|
+
check(l.mapPreamble)
|
|
105
|
+
check(l.templateMapPreamble)
|
|
106
|
+
if (l.flatMapCallback) {
|
|
107
|
+
check(l.flatMapCallback.body)
|
|
108
|
+
check(l.flatMapCallback.templateBody)
|
|
109
|
+
l.flatMapCallback.fragments.forEach(f => visit(f.ir))
|
|
110
|
+
}
|
|
111
|
+
l.children.forEach(visit)
|
|
112
|
+
}
|
|
113
|
+
const visit = (node: IRNode): void => {
|
|
114
|
+
if (misused) return
|
|
115
|
+
switch (node.type) {
|
|
116
|
+
case 'expression':
|
|
117
|
+
check(node.expr)
|
|
118
|
+
check(node.templateExpr)
|
|
119
|
+
break
|
|
120
|
+
case 'conditional':
|
|
121
|
+
check(node.condition)
|
|
122
|
+
check(node.templateCondition)
|
|
123
|
+
visit(node.whenTrue)
|
|
124
|
+
visit(node.whenFalse)
|
|
125
|
+
break
|
|
126
|
+
case 'if-statement':
|
|
127
|
+
check(node.condition)
|
|
128
|
+
check(node.templateCondition)
|
|
129
|
+
for (const sv of node.scopeVariables) {
|
|
130
|
+
check(sv.initializer)
|
|
131
|
+
check(sv.templateInitializer)
|
|
132
|
+
}
|
|
133
|
+
visit(node.consequent)
|
|
134
|
+
if (node.alternate) visit(node.alternate)
|
|
135
|
+
break
|
|
136
|
+
case 'element':
|
|
137
|
+
node.attrs.forEach(a => attr(a.value))
|
|
138
|
+
node.events.forEach(e => check(e.handler))
|
|
139
|
+
node.children.forEach(visit)
|
|
140
|
+
break
|
|
141
|
+
case 'component':
|
|
142
|
+
node.props.forEach(p => attr(p.value))
|
|
143
|
+
node.children.forEach(visit)
|
|
144
|
+
break
|
|
145
|
+
case 'provider':
|
|
146
|
+
attr(node.valueProp.value)
|
|
147
|
+
node.children.forEach(visit)
|
|
148
|
+
break
|
|
149
|
+
case 'fragment':
|
|
150
|
+
node.children.forEach(visit)
|
|
151
|
+
break
|
|
152
|
+
case 'async':
|
|
153
|
+
visit(node.fallback)
|
|
154
|
+
node.children.forEach(visit)
|
|
155
|
+
break
|
|
156
|
+
case 'loop':
|
|
157
|
+
visitLoop(node)
|
|
158
|
+
break
|
|
159
|
+
case 'text':
|
|
160
|
+
case 'slot':
|
|
161
|
+
break
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
visitLoop(loop)
|
|
165
|
+
return misused
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function escapeRe(s: string): string {
|
|
169
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
170
|
+
}
|