@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
package/src/debug.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* paths, and component reactive structure — all without running any code.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import ts from 'typescript'
|
|
8
9
|
import type {
|
|
9
10
|
ComponentIR,
|
|
10
11
|
IRNode,
|
|
@@ -27,6 +28,7 @@ import { buildMetadata } from './compiler.ts'
|
|
|
27
28
|
import { analyzeClientNeeds } from './ir-to-client-js/index.ts'
|
|
28
29
|
import type { WrapReason } from './ir-to-client-js/reactivity.ts'
|
|
29
30
|
import { decideWrapFromAstFlags } from './ir-to-client-js/reactivity.ts'
|
|
31
|
+
import { tokenContainsIdent } from './ir-to-client-js/utils.ts'
|
|
30
32
|
|
|
31
33
|
// =============================================================================
|
|
32
34
|
// Types
|
|
@@ -274,7 +276,12 @@ export function buildComponentGraph(source: string, filePath: string, componentN
|
|
|
274
276
|
errors: [],
|
|
275
277
|
}
|
|
276
278
|
|
|
277
|
-
|
|
279
|
+
const graph = buildGraphFromIR(componentIR)
|
|
280
|
+
// `findSourceFile` extracts the path from signal/memo/effect metadata; for
|
|
281
|
+
// components with no reactive state it returns '' because there are no
|
|
282
|
+
// located nodes. Fall back to the caller-supplied filePath so callers
|
|
283
|
+
// always get a non-empty sourceFile. (#1690 Bug A)
|
|
284
|
+
return graph.sourceFile ? graph : { ...graph, sourceFile: filePath }
|
|
278
285
|
}
|
|
279
286
|
|
|
280
287
|
/**
|
|
@@ -286,9 +293,73 @@ export function buildGraphFromIR(ir: ComponentIR): ComponentGraph {
|
|
|
286
293
|
const memoNames = new Set(meta.memos.map(m => m.name))
|
|
287
294
|
const signalSetters = new Map(meta.signals.filter(s => s.setter).map(s => [s.setter!, s.getter]))
|
|
288
295
|
|
|
296
|
+
// Does an attribute expression read a component prop? Mirrors the emitter's
|
|
297
|
+
// `needsEffectWrapper` prop gate (`reactivity.ts`):
|
|
298
|
+
// - any individual destructured prop name (`{ className }` → `class={className}`),
|
|
299
|
+
// - or a `<propsObject>.x` member access (`class={props.className}`),
|
|
300
|
+
// both excluding `children` (server-rendered, never wrapped). Detection is
|
|
301
|
+
// structural, not a raw-string regex: destructured names use the IR's
|
|
302
|
+
// lexer-resolved `freeIdentifiers` (falling back to the lexer-aware
|
|
303
|
+
// `tokenContainsIdent`), and the props-object case parses the expression and
|
|
304
|
+
// walks for a real `props.member` access — so a `props.` inside a string
|
|
305
|
+
// literal / comment can't false-match, and bare `props` (`id={props}`) or
|
|
306
|
+
// `props.children` are correctly NOT treated as reactive (matching the
|
|
307
|
+
// emitter, which only wraps `<propsObject>.<non-children>`).
|
|
308
|
+
//
|
|
309
|
+
// Prop-driven attribute bindings are wrapped in a `createEffect` by the
|
|
310
|
+
// emitter (and so emit a `#binding:<slot>` profiler id), but the debug-side
|
|
311
|
+
// collector only tracked signal/memo deps before — so those ids resolved to
|
|
312
|
+
// `(unresolved)` in `bf debug profile` (#1844 follow-up). Detecting prop reads
|
|
313
|
+
// here closes that emit↔analyzer gap.
|
|
314
|
+
const destructuredPropNames = new Set(meta.propsParams.map(p => p.name).filter(n => n !== 'children'))
|
|
315
|
+
const propsObjectName = meta.propsObjectName
|
|
316
|
+
|
|
317
|
+
// A binding often reads a prop *indirectly* through a local const:
|
|
318
|
+
// `const classes = `…${variant}…${size}…${className}``, then
|
|
319
|
+
// `<button class={classes}>` / `<Slot className={classes}>`. The emitter
|
|
320
|
+
// inlines `classes`, sees the prop reads, and wraps the binding in a
|
|
321
|
+
// `createEffect` (emitting `#binding:<slot>`) — but the expression's only free
|
|
322
|
+
// identifier is the local `classes`, so the direct prop check above misses it
|
|
323
|
+
// and the id resolves to `(unresolved)` (the Slot-composed-button case, #1863).
|
|
324
|
+
// Precompute every local const whose value transitively derives from a prop,
|
|
325
|
+
// so reading such a const counts as reading a prop. Module-level consts can't
|
|
326
|
+
// reach component props, so they fall out naturally.
|
|
327
|
+
const constByName = new Map(meta.localConstants.map(c => [c.name, c]))
|
|
328
|
+
const propDerivedConsts = new Set<string>()
|
|
329
|
+
{
|
|
330
|
+
const onStack = new Set<string>()
|
|
331
|
+
const derivesFromProp = (name: string): boolean => {
|
|
332
|
+
if (propDerivedConsts.has(name)) return true
|
|
333
|
+
const c = constByName.get(name)
|
|
334
|
+
if (!c?.freeIdentifiers || onStack.has(name)) return false
|
|
335
|
+
onStack.add(name)
|
|
336
|
+
let derived = false
|
|
337
|
+
for (const free of c.freeIdentifiers) {
|
|
338
|
+
if (destructuredPropNames.has(free) || free === propsObjectName || derivesFromProp(free)) {
|
|
339
|
+
derived = true
|
|
340
|
+
break
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
onStack.delete(name)
|
|
344
|
+
if (derived) propDerivedConsts.add(name)
|
|
345
|
+
return derived
|
|
346
|
+
}
|
|
347
|
+
for (const c of meta.localConstants) derivesFromProp(c.name)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const exprReadsProp = (expr: string, freeIds?: ReadonlySet<string>): boolean => {
|
|
351
|
+
for (const name of destructuredPropNames) {
|
|
352
|
+
if (freeIds ? freeIds.has(name) : tokenContainsIdent(expr, name)) return true
|
|
353
|
+
}
|
|
354
|
+
for (const name of propDerivedConsts) {
|
|
355
|
+
if (freeIds ? freeIds.has(name) : tokenContainsIdent(expr, name)) return true
|
|
356
|
+
}
|
|
357
|
+
return propsObjectName ? exprReadsPropMember(expr, propsObjectName) : false
|
|
358
|
+
}
|
|
359
|
+
|
|
289
360
|
// Collect DOM bindings from IR tree
|
|
290
361
|
const domBindings: DomBinding[] = []
|
|
291
|
-
collectDomBindings(ir.root, domBindings, signalGetters, memoNames)
|
|
362
|
+
collectDomBindings(ir.root, domBindings, signalGetters, memoNames, undefined, new Set(), exprReadsProp)
|
|
292
363
|
|
|
293
364
|
// Build consumer lists for signals
|
|
294
365
|
const signalConsumers = new Map<string, string[]>()
|
|
@@ -381,8 +452,8 @@ export function buildGraphFromIR(ir: ComponentIR): ComponentGraph {
|
|
|
381
452
|
* Callers that need the raw IR tree (events, loops, why-update) use this
|
|
382
453
|
* instead of `buildComponentGraph` to avoid a redundant analysis round.
|
|
383
454
|
*/
|
|
384
|
-
export function buildComponentAnalysis(source: string, filePath: string, componentName?: string): ComponentAnalysis {
|
|
385
|
-
const ctx = analyzeComponent(source, filePath, componentName)
|
|
455
|
+
export function buildComponentAnalysis(source: string, filePath: string, componentName?: string, program?: ts.Program): ComponentAnalysis {
|
|
456
|
+
const ctx = analyzeComponent(source, filePath, componentName, program)
|
|
386
457
|
const emptyIR: ComponentIR = {
|
|
387
458
|
version: '0.1',
|
|
388
459
|
metadata: buildMetadata(ctx),
|
|
@@ -411,8 +482,8 @@ export function buildComponentAnalysis(source: string, filePath: string, compone
|
|
|
411
482
|
* Build a complete event summary for a component, including setter resolution
|
|
412
483
|
* and downstream update paths.
|
|
413
484
|
*/
|
|
414
|
-
export function buildEventSummary(source: string, filePath: string, componentName?: string): EventSummary {
|
|
415
|
-
const { graph, ir } = buildComponentAnalysis(source, filePath, componentName)
|
|
485
|
+
export function buildEventSummary(source: string, filePath: string, componentName?: string, program?: ts.Program): EventSummary {
|
|
486
|
+
const { graph, ir } = buildComponentAnalysis(source, filePath, componentName, program)
|
|
416
487
|
const setterToSignal = new Map<string, string>()
|
|
417
488
|
for (const s of ir.metadata.signals) {
|
|
418
489
|
if (s.setter) setterToSignal.set(s.setter, s.getter)
|
|
@@ -1473,8 +1544,8 @@ export function formatFallbackExplanations(
|
|
|
1473
1544
|
// Component Summary (hydration/size overview)
|
|
1474
1545
|
// =============================================================================
|
|
1475
1546
|
|
|
1476
|
-
export function buildComponentSummary(source: string, filePath: string, componentName?: string): ComponentSummary {
|
|
1477
|
-
const { graph, ir } = buildComponentAnalysis(source, filePath, componentName)
|
|
1547
|
+
export function buildComponentSummary(source: string, filePath: string, componentName?: string, program?: ts.Program): ComponentSummary {
|
|
1548
|
+
const { graph, ir } = buildComponentAnalysis(source, filePath, componentName, program)
|
|
1478
1549
|
const meta = ir.metadata
|
|
1479
1550
|
const clientNeeds = analyzeClientNeeds(ir)
|
|
1480
1551
|
const hasReactiveState = meta.signals.length > 0 || meta.memos.length > 0 || meta.effects.length > 0
|
|
@@ -1604,7 +1675,30 @@ function collectDomBindings(
|
|
|
1604
1675
|
signalGetters: Set<string>,
|
|
1605
1676
|
memoNames: Set<string>,
|
|
1606
1677
|
parentTag?: string,
|
|
1678
|
+
// Loop-param names in scope (#1690, #1795 Phase 2). Inside a `map(it => …)`
|
|
1679
|
+
// body the emitter rewrites every `it.x` read into a reactive accessor and
|
|
1680
|
+
// wraps the binding in `createEffect`, yet `it` is neither a signal nor a
|
|
1681
|
+
// memo — so without this context loop-child text / attribute bindings are
|
|
1682
|
+
// invisible to the graph. When a binding expression references one of these
|
|
1683
|
+
// names it is treated as reactive (matching the emitter's gate), giving the
|
|
1684
|
+
// profiler a `domBinding` (slotId + loc) to resolve `<Comp>#binding:<slotId>`.
|
|
1685
|
+
loopParams: Set<string> = new Set(),
|
|
1686
|
+
// Predicate: does an attribute expression read a component prop? Mirrors the
|
|
1687
|
+
// emitter's `needsEffectWrapper` prop detection so a prop-driven attribute
|
|
1688
|
+
// (wrapped in `createEffect` at codegen, hence emitting `#binding:<slot>`) is
|
|
1689
|
+
// tracked here too — otherwise its profiler id resolves to `(unresolved)`.
|
|
1690
|
+
readsProp: (expr: string, freeIds?: ReadonlySet<string>) => boolean = () => false,
|
|
1607
1691
|
): void {
|
|
1692
|
+
// Does a loop-child binding read a loop param (or index)? Use the analyzer's
|
|
1693
|
+
// lexer-resolved metadata, NOT a raw-string regex — so a param name that only
|
|
1694
|
+
// appears inside a string literal (index `i` vs `'i'`) is not mistaken for a
|
|
1695
|
+
// reactive read. Text expressions carry `origin.freeRefs` (a `render-item`
|
|
1696
|
+
// kind == map-callback param); attributes carry `freeIdentifiers` (bare
|
|
1697
|
+
// identifier set). This matches the emitter's actual loop-param gate.
|
|
1698
|
+
const exprReadsLoopParam = (n: IRExpression): boolean =>
|
|
1699
|
+
loopParams.size > 0 && (n.origin?.freeRefs?.some(r => loopParams.has(r.name)) ?? false)
|
|
1700
|
+
const attrReadsLoopParam = (free: ReadonlySet<string> | undefined): boolean =>
|
|
1701
|
+
loopParams.size > 0 && free !== undefined && [...loopParams].some(p => free.has(p))
|
|
1608
1702
|
switch (node.type) {
|
|
1609
1703
|
case 'element': {
|
|
1610
1704
|
// Dynamic attribute bindings (style, class, aria-*, data-*, etc.)
|
|
@@ -1615,11 +1709,19 @@ function collectDomBindings(
|
|
|
1615
1709
|
// deps list is the statically-proven-reactive case.
|
|
1616
1710
|
for (const attr of node.attrs) {
|
|
1617
1711
|
if (attr.value.kind !== 'expression' && attr.value.kind !== 'template' && attr.value.kind !== 'spread') continue
|
|
1712
|
+
// `key` is consumed by the loop's keyFn, never emitted as an attribute
|
|
1713
|
+
// effect — skip it inside loops so a `key={it.id}` read isn't mistaken
|
|
1714
|
+
// for a reactive binding (matches `collectLoopChildBindings`).
|
|
1715
|
+
if (attr.name === 'key' && loopParams.size > 0) continue
|
|
1618
1716
|
const expr = attrValueToString(attr.value)
|
|
1619
1717
|
if (!expr) continue
|
|
1620
1718
|
const deps = extractReactiveDeps(expr, signalGetters, memoNames)
|
|
1621
|
-
const isReactive = deps.length > 0
|
|
1622
|
-
|
|
1719
|
+
const isReactive = deps.length > 0 || attrReadsLoopParam(attr.freeIdentifiers)
|
|
1720
|
+
// A prop-driven attribute (`id={props.id}`, `class={`…${props.x}`}`) is
|
|
1721
|
+
// wrapped in a `createEffect` by the emitter even with no signal/memo
|
|
1722
|
+
// dep — match that gate so its `#binding:<slot>` id resolves.
|
|
1723
|
+
const hasPropsRef = readsProp(expr, attr.freeIdentifiers)
|
|
1724
|
+
const wrapReason = inferWrapReasonForAttrLike(isReactive, hasPropsRef, attr)
|
|
1623
1725
|
if (wrapReason) {
|
|
1624
1726
|
bindings.push({
|
|
1625
1727
|
kind: 'dom',
|
|
@@ -1627,7 +1729,7 @@ function collectDomBindings(
|
|
|
1627
1729
|
slotId: node.slotId ?? '?',
|
|
1628
1730
|
deps,
|
|
1629
1731
|
type: 'attribute',
|
|
1630
|
-
classification: isReactive ? 'reactive' : 'fallback',
|
|
1732
|
+
classification: isReactive || hasPropsRef ? 'reactive' : 'fallback',
|
|
1631
1733
|
expression: expr,
|
|
1632
1734
|
wrapReason,
|
|
1633
1735
|
loc: attr.loc,
|
|
@@ -1653,7 +1755,7 @@ function collectDomBindings(
|
|
|
1653
1755
|
}
|
|
1654
1756
|
// Recurse — pass element tag as parent context for text bindings
|
|
1655
1757
|
for (const child of node.children) {
|
|
1656
|
-
collectDomBindings(child, bindings, signalGetters, memoNames, node.tag)
|
|
1758
|
+
collectDomBindings(child, bindings, signalGetters, memoNames, node.tag, loopParams, readsProp)
|
|
1657
1759
|
}
|
|
1658
1760
|
break
|
|
1659
1761
|
}
|
|
@@ -1661,7 +1763,8 @@ function collectDomBindings(
|
|
|
1661
1763
|
// Widened to match emitter gate in collect-elements.ts:
|
|
1662
1764
|
// `node.reactive || node.callsReactiveGetters || node.hasFunctionCalls`.
|
|
1663
1765
|
const decision = decideWrapFromAstFlags(node)
|
|
1664
|
-
|
|
1766
|
+
const loopReactive = exprReadsLoopParam(node)
|
|
1767
|
+
if ((decision.wrap || loopReactive) && node.slotId) {
|
|
1665
1768
|
const deps = extractReactiveDeps(node.expr, signalGetters, memoNames)
|
|
1666
1769
|
const preview = parentTag
|
|
1667
1770
|
? `<${parentTag}>{${truncateExpr(node.expr)}}</${parentTag}>`
|
|
@@ -1672,9 +1775,12 @@ function collectDomBindings(
|
|
|
1672
1775
|
slotId: node.slotId,
|
|
1673
1776
|
deps,
|
|
1674
1777
|
type: 'text',
|
|
1675
|
-
classification:
|
|
1778
|
+
classification:
|
|
1779
|
+
(decision.wrap && decision.reason === 'proven-reactive') || loopReactive
|
|
1780
|
+
? 'reactive'
|
|
1781
|
+
: 'fallback',
|
|
1676
1782
|
expression: node.expr,
|
|
1677
|
-
wrapReason: decision.reason,
|
|
1783
|
+
wrapReason: decision.wrap ? decision.reason : 'string-reactive',
|
|
1678
1784
|
loc: node.loc,
|
|
1679
1785
|
jsxPreview: preview,
|
|
1680
1786
|
})
|
|
@@ -1683,7 +1789,12 @@ function collectDomBindings(
|
|
|
1683
1789
|
}
|
|
1684
1790
|
case 'conditional': {
|
|
1685
1791
|
const decision = decideWrapFromAstFlags(node)
|
|
1686
|
-
|
|
1792
|
+
// A loop-child conditional whose condition reads a loop param is reactive
|
|
1793
|
+
// (the emitter wraps its `insert()` in a per-item effect) even though the
|
|
1794
|
+
// param is neither signal nor memo. Use the resolved `origin.freeRefs`.
|
|
1795
|
+
const loopReactive =
|
|
1796
|
+
loopParams.size > 0 && (node.origin?.freeRefs?.some(r => loopParams.has(r.name)) ?? false)
|
|
1797
|
+
if ((decision.wrap || loopReactive) && node.slotId) {
|
|
1687
1798
|
const deps = extractReactiveDeps(node.condition, signalGetters, memoNames)
|
|
1688
1799
|
bindings.push({
|
|
1689
1800
|
kind: 'dom',
|
|
@@ -1691,15 +1802,18 @@ function collectDomBindings(
|
|
|
1691
1802
|
slotId: node.slotId,
|
|
1692
1803
|
deps,
|
|
1693
1804
|
type: 'conditional',
|
|
1694
|
-
classification:
|
|
1805
|
+
classification:
|
|
1806
|
+
(decision.wrap && decision.reason === 'proven-reactive') || loopReactive
|
|
1807
|
+
? 'reactive'
|
|
1808
|
+
: 'fallback',
|
|
1695
1809
|
expression: node.condition,
|
|
1696
|
-
wrapReason: decision.reason,
|
|
1810
|
+
wrapReason: decision.wrap ? decision.reason : 'string-reactive',
|
|
1697
1811
|
loc: node.loc,
|
|
1698
1812
|
jsxPreview: `{${truncateExpr(node.condition)} ? ... : ...}`,
|
|
1699
1813
|
})
|
|
1700
1814
|
}
|
|
1701
|
-
collectDomBindings(node.whenTrue, bindings, signalGetters, memoNames, parentTag)
|
|
1702
|
-
collectDomBindings(node.whenFalse, bindings, signalGetters, memoNames, parentTag)
|
|
1815
|
+
collectDomBindings(node.whenTrue, bindings, signalGetters, memoNames, parentTag, loopParams, readsProp)
|
|
1816
|
+
collectDomBindings(node.whenFalse, bindings, signalGetters, memoNames, parentTag, loopParams, readsProp)
|
|
1703
1817
|
break
|
|
1704
1818
|
}
|
|
1705
1819
|
case 'loop': {
|
|
@@ -1710,7 +1824,13 @@ function collectDomBindings(
|
|
|
1710
1824
|
// (e.g. `getItems().map(...)` with an opaque helper).
|
|
1711
1825
|
if (node.slotId) {
|
|
1712
1826
|
const deps = extractReactiveDeps(node.array, signalGetters, memoNames)
|
|
1713
|
-
|
|
1827
|
+
// An inner loop whose array reads an outer loop param (`r.tags.map(...)`)
|
|
1828
|
+
// is reactive per item — use the resolved `arrayFreeIdentifiers`.
|
|
1829
|
+
const loopReactive =
|
|
1830
|
+
loopParams.size > 0 &&
|
|
1831
|
+
node.arrayFreeIdentifiers !== undefined &&
|
|
1832
|
+
[...loopParams].some(p => node.arrayFreeIdentifiers!.has(p))
|
|
1833
|
+
const isReactive = deps.length > 0 || node.callsReactiveGetters === true || loopReactive
|
|
1714
1834
|
const isFallback = !isReactive && node.hasFunctionCalls === true
|
|
1715
1835
|
if (isReactive || isFallback) {
|
|
1716
1836
|
// IRLoop has no `.reactive` flag (unlike IRExpression/IRConditional),
|
|
@@ -1719,7 +1839,7 @@ function collectDomBindings(
|
|
|
1719
1839
|
// getter (`items()` where `items` is a signal) is proven-reactive,
|
|
1720
1840
|
// not fallback — flip the string evidence before handing the AST
|
|
1721
1841
|
// flags to the helper.
|
|
1722
|
-
const wrapReason: WrapReason = deps.length > 0
|
|
1842
|
+
const wrapReason: WrapReason = deps.length > 0 || loopReactive
|
|
1723
1843
|
? 'string-reactive'
|
|
1724
1844
|
: node.callsReactiveGetters
|
|
1725
1845
|
? 'proven-reactive'
|
|
@@ -1738,8 +1858,12 @@ function collectDomBindings(
|
|
|
1738
1858
|
})
|
|
1739
1859
|
}
|
|
1740
1860
|
}
|
|
1861
|
+
// Loop-param names enter scope for the children (#1690, #1795 Phase 2).
|
|
1862
|
+
const childLoopParams = new Set(loopParams)
|
|
1863
|
+
for (const p of extractLoopParamNames(node.param, node)) childLoopParams.add(p)
|
|
1864
|
+
if (node.index) childLoopParams.add(node.index)
|
|
1741
1865
|
for (const child of node.children) {
|
|
1742
|
-
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag)
|
|
1866
|
+
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag, childLoopParams, readsProp)
|
|
1743
1867
|
}
|
|
1744
1868
|
break
|
|
1745
1869
|
}
|
|
@@ -1751,7 +1875,13 @@ function collectDomBindings(
|
|
|
1751
1875
|
const propValue = attrValueToString(prop.value) ?? ''
|
|
1752
1876
|
if (!propValue) continue
|
|
1753
1877
|
const deps = extractReactiveDeps(propValue, signalGetters, memoNames)
|
|
1754
|
-
|
|
1878
|
+
// Mirror the element-attr gate: a child prop is wrapped (and emits
|
|
1879
|
+
// `#binding:<slot>`) when it reads a prop directly or via a prop-derived
|
|
1880
|
+
// local const (`<Slot className={classes}>`). The previous
|
|
1881
|
+
// `includes('props.')` check missed both destructured props and the
|
|
1882
|
+
// local-const indirection, leaving the forwarded binding `(unresolved)`
|
|
1883
|
+
// (#1863).
|
|
1884
|
+
const hasPropsRef = readsProp(propValue, prop.freeIdentifiers)
|
|
1755
1885
|
const isReactive = deps.length > 0 || hasPropsRef
|
|
1756
1886
|
const wrapReason = inferWrapReasonForAttrLike(deps.length > 0, hasPropsRef, prop)
|
|
1757
1887
|
if (wrapReason) {
|
|
@@ -1772,21 +1902,21 @@ function collectDomBindings(
|
|
|
1772
1902
|
}
|
|
1773
1903
|
}
|
|
1774
1904
|
for (const child of node.children) {
|
|
1775
|
-
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag)
|
|
1905
|
+
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag, loopParams, readsProp)
|
|
1776
1906
|
}
|
|
1777
1907
|
break
|
|
1778
1908
|
}
|
|
1779
1909
|
case 'fragment':
|
|
1780
1910
|
case 'provider': {
|
|
1781
1911
|
for (const child of node.children) {
|
|
1782
|
-
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag)
|
|
1912
|
+
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag, loopParams, readsProp)
|
|
1783
1913
|
}
|
|
1784
1914
|
break
|
|
1785
1915
|
}
|
|
1786
1916
|
case 'if-statement': {
|
|
1787
|
-
collectDomBindings(node.consequent, bindings, signalGetters, memoNames, parentTag)
|
|
1917
|
+
collectDomBindings(node.consequent, bindings, signalGetters, memoNames, parentTag, loopParams, readsProp)
|
|
1788
1918
|
if (node.alternate) {
|
|
1789
|
-
collectDomBindings(node.alternate, bindings, signalGetters, memoNames, parentTag)
|
|
1919
|
+
collectDomBindings(node.alternate, bindings, signalGetters, memoNames, parentTag, loopParams, readsProp)
|
|
1790
1920
|
}
|
|
1791
1921
|
break
|
|
1792
1922
|
}
|
|
@@ -1798,6 +1928,40 @@ function truncateExpr(expr: string, max: number = 40): string {
|
|
|
1798
1928
|
return s.length > max ? s.slice(0, max - 1) + '…' : s
|
|
1799
1929
|
}
|
|
1800
1930
|
|
|
1931
|
+
/**
|
|
1932
|
+
* True when `expr` contains a genuine `<propsObject>.<member>` property access
|
|
1933
|
+
* with `member !== 'children'` — the emitter's prop gate for the props object
|
|
1934
|
+
* (`needsEffectWrapper`, `reactivity.ts`). Parses the expression rather than
|
|
1935
|
+
* regex-matching the raw string, so a `props.` inside a string literal/comment
|
|
1936
|
+
* doesn't false-match, and bare `props` / `props.children` are excluded. Parse
|
|
1937
|
+
* failures (e.g. an attribute expression carrying JSX) fall back to `false` —
|
|
1938
|
+
* conservative: we never invent a binding the emitter wouldn't wrap.
|
|
1939
|
+
*/
|
|
1940
|
+
function exprReadsPropMember(expr: string, propsObjectName: string): boolean {
|
|
1941
|
+
let sf: ts.SourceFile
|
|
1942
|
+
try {
|
|
1943
|
+
sf = ts.createSourceFile('__attr.tsx', `(${expr})`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
|
1944
|
+
} catch {
|
|
1945
|
+
return false
|
|
1946
|
+
}
|
|
1947
|
+
let found = false
|
|
1948
|
+
const visit = (n: ts.Node): void => {
|
|
1949
|
+
if (found) return
|
|
1950
|
+
if (
|
|
1951
|
+
ts.isPropertyAccessExpression(n) &&
|
|
1952
|
+
ts.isIdentifier(n.expression) &&
|
|
1953
|
+
n.expression.text === propsObjectName &&
|
|
1954
|
+
n.name.text !== 'children'
|
|
1955
|
+
) {
|
|
1956
|
+
found = true
|
|
1957
|
+
return
|
|
1958
|
+
}
|
|
1959
|
+
ts.forEachChild(n, visit)
|
|
1960
|
+
}
|
|
1961
|
+
visit(sf)
|
|
1962
|
+
return found
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1801
1965
|
/** Convert an `AttrValue` to a flat string for reactive dep extraction. */
|
|
1802
1966
|
function attrValueToString(value: AttrValue): string | null {
|
|
1803
1967
|
switch (value.kind) {
|
package/src/expression-parser.ts
CHANGED
|
@@ -476,6 +476,59 @@ export function extractArrowBodyExpression(source: string): string | null {
|
|
|
476
476
|
return expr.body.getText(sf).trim()
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
+
/**
|
|
480
|
+
* A single entry of a JSX `style={{ … }}` object, lowered for SSR. The key is
|
|
481
|
+
* already CSS-cased (`backgroundColor` → `background-color`); the value is
|
|
482
|
+
* either a static string literal or a raw JS expression for the adapter to
|
|
483
|
+
* lower (`color` → its template interpolation).
|
|
484
|
+
*/
|
|
485
|
+
export type StyleObjectEntry =
|
|
486
|
+
| { cssKey: string; kind: 'literal'; value: string }
|
|
487
|
+
| { cssKey: string; kind: 'expr'; expr: string }
|
|
488
|
+
|
|
489
|
+
/** camelCase → kebab-case for CSS property names (`backgroundColor` →
|
|
490
|
+
* `background-color`, `WebkitTransform` → `-webkit-transform`). The `ms`
|
|
491
|
+
* vendor prefix is lowercase in React style keys (`msTransform`) yet the CSS
|
|
492
|
+
* property carries a leading dash (`-ms-transform`), so special-case it the
|
|
493
|
+
* same way React's `hyphenateStyleName` does. */
|
|
494
|
+
export function cssKebabCase(name: string): string {
|
|
495
|
+
return name.replace(/[A-Z]/g, m => '-' + m.toLowerCase()).replace(/^ms-/, '-ms-')
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Parse a JSX `style={{ … }}` object-literal source into CSS entries, or
|
|
500
|
+
* `null` when the shape isn't a plain object of static-keyed properties
|
|
501
|
+
* (spread, computed key, shorthand, method, getter) — the adapter then keeps
|
|
502
|
+
* refusing it. A bare `{…}` parses as a block statement, so the source is
|
|
503
|
+
* wrapped in parens to force expression context. Used by the template adapters
|
|
504
|
+
* to lower `style={{ backgroundColor: color, padding: '8px' }}` to a CSS
|
|
505
|
+
* string instead of emitting BF101.
|
|
506
|
+
*/
|
|
507
|
+
export function parseStyleObjectEntries(source: string): StyleObjectEntry[] | null {
|
|
508
|
+
const sf = ts.createSourceFile('__style__.ts', `(${source})`, ts.ScriptTarget.Latest, true)
|
|
509
|
+
const stmt = sf.statements[0]
|
|
510
|
+
if (!stmt || !ts.isExpressionStatement(stmt) || sf.statements.length !== 1) return null
|
|
511
|
+
let expr: ts.Expression = stmt.expression
|
|
512
|
+
while (ts.isParenthesizedExpression(expr)) expr = expr.expression
|
|
513
|
+
if (!ts.isObjectLiteralExpression(expr)) return null
|
|
514
|
+
const entries: StyleObjectEntry[] = []
|
|
515
|
+
for (const prop of expr.properties) {
|
|
516
|
+
if (!ts.isPropertyAssignment(prop)) return null // shorthand/spread/method/getter
|
|
517
|
+
let key: string
|
|
518
|
+
if (ts.isIdentifier(prop.name)) key = prop.name.text
|
|
519
|
+
else if (ts.isStringLiteral(prop.name)) key = prop.name.text
|
|
520
|
+
else return null // computed / numeric key
|
|
521
|
+
const cssKey = cssKebabCase(key)
|
|
522
|
+
const init = prop.initializer
|
|
523
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
524
|
+
entries.push({ cssKey, kind: 'literal', value: init.text })
|
|
525
|
+
} else {
|
|
526
|
+
entries.push({ cssKey, kind: 'expr', expr: init.getText(sf).trim() })
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return entries.length > 0 ? entries : null
|
|
530
|
+
}
|
|
531
|
+
|
|
479
532
|
/**
|
|
480
533
|
* Parse a JavaScript expression string into a ParsedExpr tree.
|
|
481
534
|
*/
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,9 @@ export { generateModuleExports, extractFunctionParams, formatParamWithType, find
|
|
|
59
59
|
|
|
60
60
|
// Adapters
|
|
61
61
|
export { BaseAdapter } from './adapters/interface.ts'
|
|
62
|
+
// Dependency-free adapter for tooling that only needs client JS (e.g. the
|
|
63
|
+
// profiler scenario driver) — the client output is adapter-independent.
|
|
64
|
+
export { TestAdapter, testAdapter } from './adapters/test-adapter.ts'
|
|
62
65
|
export type {
|
|
63
66
|
TemplateAdapter,
|
|
64
67
|
AdapterOutput,
|
|
@@ -244,10 +247,12 @@ export {
|
|
|
244
247
|
export { ErrorCodes, createError, formatError, generateCodeFrame } from './errors.ts'
|
|
245
248
|
|
|
246
249
|
// Expression Parser
|
|
247
|
-
export { parseExpression, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, containsHigherOrder, extractArrowBodyExpression } from './expression-parser.ts'
|
|
250
|
+
export { parseExpression, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries } from './expression-parser.ts'
|
|
251
|
+
export type { StyleObjectEntry } from './expression-parser.ts'
|
|
248
252
|
export type { ParsedExpr, ParsedStatement, SortComparator, SortKey, ReduceOp, FlatDepth, FlatMapOp, FlatMapLeaf, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
|
|
249
253
|
export { buildLoopChainExpr } from './loop-chain.ts'
|
|
250
254
|
export type { LoopChainInputs } from './loop-chain.ts'
|
|
255
|
+
export { isLowerableObjectRestDestructure } from './loop-destructure.ts'
|
|
251
256
|
|
|
252
257
|
// Debug analysis
|
|
253
258
|
export {
|
|
@@ -277,6 +282,72 @@ export {
|
|
|
277
282
|
export type { ComponentGraph, ComponentAnalysis, SignalNode, MemoNode, EffectNode, DomBinding, UpdatePath, SignalTrace, EventBinding, SetterRef, FnSetterResolution, EventSummary, LoopInfo, LoopChildBinding, LoopSummary, WhyUpdateResult, WhyUpdateDep, WhyUpdateSource, FallbackExplanation, ComponentSummary } from './debug.ts'
|
|
278
283
|
export type { WrapReason } from './ir-to-client-js/reactivity.ts'
|
|
279
284
|
|
|
285
|
+
// Reactive performance profiler (#1690). Static half (SR5 budget, SR6 diff) +
|
|
286
|
+
// dynamic half (SR2/SR4 join, SR7 report, v1 analyses).
|
|
287
|
+
export {
|
|
288
|
+
buildStaticBudget,
|
|
289
|
+
formatStaticBudget,
|
|
290
|
+
diffStaticBudget,
|
|
291
|
+
formatBudgetDiff,
|
|
292
|
+
buildProfileReport,
|
|
293
|
+
formatProfileReport,
|
|
294
|
+
buildIdIndex,
|
|
295
|
+
joinProfilerEvents,
|
|
296
|
+
parseProfilerId,
|
|
297
|
+
analyzeHotSubscribers,
|
|
298
|
+
formatHotSubscribers,
|
|
299
|
+
findUninstrumentedEffects,
|
|
300
|
+
analyzeWastedReReruns,
|
|
301
|
+
formatWastedReReruns,
|
|
302
|
+
analyzeBatchAdvisor,
|
|
303
|
+
formatBatchAdvisor,
|
|
304
|
+
} from './profiler.ts'
|
|
305
|
+
export type {
|
|
306
|
+
StaticBudget,
|
|
307
|
+
StaticBudgetOptions,
|
|
308
|
+
FanOutEntry,
|
|
309
|
+
BudgetDiff,
|
|
310
|
+
FanOutChange,
|
|
311
|
+
ProfileReport,
|
|
312
|
+
ProfileReportInput,
|
|
313
|
+
ProfileCoverage,
|
|
314
|
+
DiagnosticsSummary,
|
|
315
|
+
EffectCandidate,
|
|
316
|
+
IdIndex,
|
|
317
|
+
ResolvedNode,
|
|
318
|
+
JoinResult,
|
|
319
|
+
JoinedEvent,
|
|
320
|
+
UnattributedId,
|
|
321
|
+
HotSubscribersResult,
|
|
322
|
+
HotSubscriber,
|
|
323
|
+
HotSubscribersOptions,
|
|
324
|
+
WastedReRunsResult,
|
|
325
|
+
WastedSubscriber,
|
|
326
|
+
WastedReRunsOptions,
|
|
327
|
+
BatchAdvisorResult,
|
|
328
|
+
BatchCandidate,
|
|
329
|
+
BatchSafety,
|
|
330
|
+
} from './profiler.ts'
|
|
331
|
+
|
|
332
|
+
// Reactive profile — findings layer (#1690 dogfood: Bug A/C/D fixes, batch-candidate dedup,
|
|
333
|
+
// fallback-heavy detection, multi-component table, SR6 compile-diff).
|
|
334
|
+
export {
|
|
335
|
+
buildReactiveProfile,
|
|
336
|
+
buildProfileFromGraph,
|
|
337
|
+
diffProfiles,
|
|
338
|
+
formatSingleProfile,
|
|
339
|
+
formatProfileTable,
|
|
340
|
+
formatProfileDiff,
|
|
341
|
+
profileToJSON,
|
|
342
|
+
} from './debug-profile.ts'
|
|
343
|
+
export type {
|
|
344
|
+
ComponentProfile,
|
|
345
|
+
ComponentProfileMetrics,
|
|
346
|
+
ProfileFinding,
|
|
347
|
+
ProfileDiff,
|
|
348
|
+
ProfileDiffEntry,
|
|
349
|
+
} from './debug-profile.ts'
|
|
350
|
+
|
|
280
351
|
// HTML constants
|
|
281
352
|
export { BOOLEAN_ATTRS, isBooleanAttr } from './html-constants.ts'
|
|
282
353
|
|
|
@@ -70,6 +70,12 @@ export interface BranchPlainLoopPlan {
|
|
|
70
70
|
* multi-line renderItem with multi-root template clone (#1212).
|
|
71
71
|
*/
|
|
72
72
|
bodyIsMultiRoot: boolean
|
|
73
|
+
/**
|
|
74
|
+
* Profile-mode loop id (#1690, #1795 Phase 3): `<Component>#binding:<slotId>`
|
|
75
|
+
* for the branch loop's `mapArray` (the loop node shares its container slot).
|
|
76
|
+
* Undefined off → byte-identical (SR8).
|
|
77
|
+
*/
|
|
78
|
+
profileLoopId?: string
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
export interface BranchCompositeLoopPlan {
|
|
@@ -22,7 +22,7 @@ import type {
|
|
|
22
22
|
BranchPlainLoopPlan,
|
|
23
23
|
} from './branch-loop.ts'
|
|
24
24
|
|
|
25
|
-
export function buildBranchLoopPlan(loop: BranchLoop): BranchLoopPlan {
|
|
25
|
+
export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: string): BranchLoopPlan {
|
|
26
26
|
const containerSlotId = loop.containerSlotId
|
|
27
27
|
const cv = varSlotId(containerSlotId)
|
|
28
28
|
const containerVar = `__loop_${cv}`
|
|
@@ -30,7 +30,7 @@ export function buildBranchLoopPlan(loop: BranchLoop): BranchLoopPlan {
|
|
|
30
30
|
if (loop.useElementReconciliation && (loop.nestedComponents?.length || loop.innerLoops?.length)) {
|
|
31
31
|
const composite: BranchCompositeLoopPlan = {
|
|
32
32
|
kind: 'composite',
|
|
33
|
-
composite: buildBranchCompositePlan(loop, cv),
|
|
33
|
+
composite: buildBranchCompositePlan(loop, cv, profileComponentName),
|
|
34
34
|
containerSlotId,
|
|
35
35
|
containerVar,
|
|
36
36
|
}
|
|
@@ -65,11 +65,13 @@ export function buildBranchLoopPlan(loop: BranchLoop): BranchLoopPlan {
|
|
|
65
65
|
conditionals: loop.bindings.conditionals,
|
|
66
66
|
loopParam: loop.param,
|
|
67
67
|
loopParamBindings: loop.paramBindings,
|
|
68
|
+
profileComponentName,
|
|
68
69
|
})
|
|
69
70
|
: null,
|
|
70
|
-
eventDelegation: buildBranchLoopDelegationPlan(loop, cv),
|
|
71
|
+
eventDelegation: buildBranchLoopDelegationPlan(loop, cv, profileComponentName),
|
|
71
72
|
childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
|
|
72
73
|
bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
|
|
74
|
+
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${containerSlotId}` : undefined,
|
|
73
75
|
}
|
|
74
76
|
return plan
|
|
75
77
|
}
|
|
@@ -31,7 +31,7 @@ import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
|
31
31
|
import type { ComponentLoopPlan, NestedComponentInit } from './types.ts'
|
|
32
32
|
|
|
33
33
|
/** @internal — prefer `buildLoopPlan`. */
|
|
34
|
-
export function buildComponentLoopPlan(elem: TopLevelLoop): ComponentLoopPlan {
|
|
34
|
+
export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?: string): ComponentLoopPlan {
|
|
35
35
|
const { name } = elem.childComponent!
|
|
36
36
|
const propsExpr = buildComponentPropsExpr(elem.childComponent!, elem.param)
|
|
37
37
|
const keyExpr = wrapLoopParamAsAccessor(elem.key || '__idx', elem.param, elem.paramBindings)
|
|
@@ -77,6 +77,7 @@ export function buildComponentLoopPlan(elem: TopLevelLoop): ComponentLoopPlan {
|
|
|
77
77
|
// by the type so the structural invariant (every variant has a
|
|
78
78
|
// `childRefs`) is preserved; populated as empty.
|
|
79
79
|
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
|
|
80
|
+
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
|
|
80
81
|
childConditionalEffects: hasChildConds
|
|
81
82
|
? buildReactiveEffectsPlan({
|
|
82
83
|
attrs: [],
|
|
@@ -84,6 +85,7 @@ export function buildComponentLoopPlan(elem: TopLevelLoop): ComponentLoopPlan {
|
|
|
84
85
|
conditionals: elem.bindings.conditionals,
|
|
85
86
|
loopParam: elem.param,
|
|
86
87
|
loopParamBindings: elem.paramBindings,
|
|
88
|
+
profileComponentName,
|
|
87
89
|
})
|
|
88
90
|
: null,
|
|
89
91
|
}
|
|
@@ -26,7 +26,7 @@ import { buildInnerLoopsPlan } from './build-inner-loop.ts'
|
|
|
26
26
|
import type { CompositeLoopPlan } from './types.ts'
|
|
27
27
|
|
|
28
28
|
/** @internal — prefer `buildLoopPlan`. */
|
|
29
|
-
export function buildTopLevelCompositePlan(elem: TopLevelLoop): CompositeLoopPlan {
|
|
29
|
+
export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentName?: string): CompositeLoopPlan {
|
|
30
30
|
const nestedComps = elem.nestedComponents!
|
|
31
31
|
const depthLevels = buildDepthLevels(elem.innerLoops ?? [], nestedComps, elem.bindings.events)
|
|
32
32
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings)
|
|
@@ -63,16 +63,19 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop): CompositeLoopPla
|
|
|
63
63
|
conditionals: elem.bindings.conditionals,
|
|
64
64
|
loopParam: elem.param,
|
|
65
65
|
loopParamBindings: elem.paramBindings,
|
|
66
|
+
profileComponentName,
|
|
66
67
|
})
|
|
67
68
|
: null,
|
|
68
69
|
branchClearChildren: false,
|
|
69
70
|
topIndent: ' ',
|
|
70
71
|
bodyIndent: ' ',
|
|
71
72
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
73
|
+
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
|
|
74
|
+
profileComponentName,
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
export function buildBranchCompositePlan(loop: BranchLoop, cv: string): CompositeLoopPlan {
|
|
78
|
+
export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileComponentName?: string): CompositeLoopPlan {
|
|
76
79
|
const nestedComps = loop.nestedComponents!
|
|
77
80
|
const innerLoops = loop.innerLoops ?? []
|
|
78
81
|
const childEvents = loop.bindings.events
|
|
@@ -114,12 +117,15 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string): Composit
|
|
|
114
117
|
conditionals: loop.bindings.conditionals,
|
|
115
118
|
loopParam: loop.param,
|
|
116
119
|
loopParamBindings: loop.paramBindings,
|
|
120
|
+
profileComponentName,
|
|
117
121
|
})
|
|
118
122
|
: null,
|
|
119
123
|
branchClearChildren: true,
|
|
120
124
|
topIndent: ' ',
|
|
121
125
|
bodyIndent: ' ',
|
|
122
126
|
bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
|
|
127
|
+
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${loop.containerSlotId}` : undefined,
|
|
128
|
+
profileComponentName,
|
|
123
129
|
}
|
|
124
130
|
}
|
|
125
131
|
|