@barefootjs/jsx 0.11.0 → 0.12.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/index.js CHANGED
@@ -19844,8 +19844,8 @@ function buildStaticBudget(source, filePath, componentName, options = {}) {
19844
19844
  const summary = buildComponentSummary(source, filePath, componentName, program);
19845
19845
  const subscriptions = graph.signals.reduce((n, s) => n + s.consumers.filter((c) => !isEventHandlerConsumer(c)).length, 0);
19846
19846
  const fanOut = graph.signals.map((s) => {
19847
- const subscribers = transitiveSubscriberCount(graph, s.name);
19848
- return { signal: s.name, subscribers, hot: subscribers >= threshold, loc: s.loc };
19847
+ const { direct, total } = subscriberCounts(graph, s.name);
19848
+ return { signal: s.name, subscribers: total, direct, hot: direct >= threshold, loc: s.loc };
19849
19849
  }).sort((a, b) => b.subscribers - a.subscribers);
19850
19850
  const { depth, chain } = longestMemoChain(graph);
19851
19851
  const hasReactiveState = graph.signals.length > 0 || graph.memos.length > 0;
@@ -19873,20 +19873,27 @@ function isEventHandlerConsumer(consumer) {
19873
19873
  const i = consumer.indexOf(":");
19874
19874
  return i > 0 && isEventHandlerEntry(consumer.slice(0, i), consumer.slice(i + 1));
19875
19875
  }
19876
- function transitiveSubscriberCount(graph, name) {
19876
+ function subscriberCounts(graph, name) {
19877
19877
  const path = traceUpdatePath(graph, name);
19878
19878
  if (!path)
19879
- return 0;
19879
+ return { direct: 0, total: 0 };
19880
19880
  const seen = new Set;
19881
- const walk = (entries) => {
19881
+ const directSeen = new Set;
19882
+ const walk = (entries, depth) => {
19882
19883
  for (const e of entries) {
19883
- if (!isEventHandlerEntry(e.kind, e.name))
19884
+ if (!isEventHandlerEntry(e.kind, e.name)) {
19884
19885
  seen.add(`${e.kind}:${e.name}`);
19885
- walk(e.children);
19886
+ if (depth === 0)
19887
+ directSeen.add(`${e.kind}:${e.name}`);
19888
+ }
19889
+ walk(e.children, depth + 1);
19886
19890
  }
19887
19891
  };
19888
- walk(path.dependents);
19889
- return seen.size;
19892
+ walk(path.dependents, 0);
19893
+ return { direct: directSeen.size, total: seen.size };
19894
+ }
19895
+ function transitiveSubscriberCount(graph, name) {
19896
+ return subscriberCounts(graph, name).total;
19890
19897
  }
19891
19898
  function longestMemoChain(graph) {
19892
19899
  const memoChainFrom = (entry) => {
@@ -19927,7 +19934,9 @@ function formatStaticBudget(b) {
19927
19934
  if (shown.length > 0) {
19928
19935
  lines.push(" fan-out (top):");
19929
19936
  for (const f of shown) {
19930
- lines.push(` ${f.signal.padEnd(12)} ${f.subscribers} subscribers${f.hot ? " ⚠ high" : ""}`);
19937
+ const indirect = f.subscribers - f.direct;
19938
+ const detail = indirect > 0 ? ` (${f.direct} direct · ${indirect} via memo)` : "";
19939
+ lines.push(` ${f.signal.padEnd(12)} → ${f.subscribers} subscribers${detail}${f.hot ? " ⚠ high" : ""}`);
19931
19940
  }
19932
19941
  }
19933
19942
  if (b.crossComponentOnly) {
@@ -19940,8 +19949,8 @@ function formatStaticBudget(b) {
19940
19949
  `);
19941
19950
  }
19942
19951
  function diffStaticBudget(base, head) {
19943
- const baseFan = new Map(base.fanOut.map((f) => [f.signal, f.subscribers]));
19944
- const headFan = new Map(head.fanOut.map((f) => [f.signal, f.subscribers]));
19952
+ const baseFan = new Map(base.fanOut.map((f) => [f.signal, f.direct]));
19953
+ const headFan = new Map(head.fanOut.map((f) => [f.signal, f.direct]));
19945
19954
  const signals = new Set([...baseFan.keys(), ...headFan.keys()]);
19946
19955
  const fanOut = [];
19947
19956
  for (const sig of signals) {
@@ -19981,7 +19990,7 @@ function formatBudgetDiff(d) {
19981
19990
  lines.push(` memo chain ${d.memoChainDepth > 0 ? "deepened" : "shortened"} by ${Math.abs(d.memoChainDepth)}`);
19982
19991
  }
19983
19992
  for (const f of d.fanOut) {
19984
- lines.push(` signal \`${f.signal}\` fan-out ${f.before}→${f.after}`);
19993
+ lines.push(` signal \`${f.signal}\` direct fan-out ${f.before}→${f.after}`);
19985
19994
  }
19986
19995
  if (lines.length === 1)
19987
19996
  lines.push(" no structural reactivity change");
@@ -25,7 +25,17 @@ export interface FanOutEntry {
25
25
  signal: string;
26
26
  /** Distinct transitive subscribers (memos + effects + DOM bindings). */
27
27
  subscribers: number;
28
- /** True when `subscribers` exceeds the configured threshold. */
28
+ /**
29
+ * Distinct subscribers that read the signal *directly* (not through a memo).
30
+ * This is the real per-write re-run pressure: a direct write re-runs exactly
31
+ * these. The remaining `subscribers − direct` sit behind a memo barrier and
32
+ * only re-run when that memo's value actually changes — so adding a memo
33
+ * barrier *lowers* `direct` while it can *raise* `subscribers` (more nodes
34
+ * become statically attributable). Read `direct`, not `subscribers`, to judge
35
+ * cost.
36
+ */
37
+ direct: number;
38
+ /** True when *direct* fan-out (not the transitive total) meets the threshold. */
29
39
  hot: boolean;
30
40
  loc: {
31
41
  file: string;
@@ -90,7 +100,7 @@ export interface BudgetDiff {
90
100
  loops: number;
91
101
  subscriptions: number;
92
102
  memoChainDepth: number;
93
- /** Signals whose fan-out changed (added/removed signals included as 0↔n). */
103
+ /** Signals whose *direct* fan-out changed (added/removed signals as 0↔n). */
94
104
  fanOut: FanOutChange[];
95
105
  /** True when any tracked metric regressed (grew) past zero. */
96
106
  regressed: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"profiler.d.ts","sourceRoot":"","sources":["../src/profiler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"profiler.d.ts","sourceRoot":"","sources":["../src/profiler.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/jsx",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "JSX compiler for BarefootJS - transforms JSX to server HTML + client JS",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,7 +53,7 @@
53
53
  "directory": "packages/jsx"
54
54
  },
55
55
  "dependencies": {
56
- "@barefootjs/shared": "0.11.0"
56
+ "@barefootjs/shared": "0.12.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@barefootjs/client": ">=0.2.0",
@@ -103,6 +103,30 @@ describe('buildStaticBudget (SR5)', () => {
103
103
  expect(cold.fanOut.every(f => f.hot === false)).toBe(true)
104
104
  })
105
105
 
106
+ test('splits fan-out into direct vs via-memo and keys `hot` off direct', () => {
107
+ // count → a → b → c → {effect, text}: exactly ONE direct subscriber (memo
108
+ // a); the rest are reached only through memo barriers.
109
+ const b = buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc')
110
+ const count = b.fanOut.find(f => f.signal === 'count')!
111
+ expect(count.direct).toBe(1)
112
+ expect(count.subscribers).toBeGreaterThanOrEqual(4)
113
+ // The transitive total clears a threshold of 3, but direct (1) does not — so
114
+ // the signal is NOT hot. `hot` tracks real per-write pressure, not the total.
115
+ const mid = buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc', { fanOutThreshold: 3 })
116
+ const c = mid.fanOut.find(f => f.signal === 'count')!
117
+ expect(c.subscribers).toBeGreaterThanOrEqual(3)
118
+ expect(c.hot).toBe(false)
119
+ })
120
+
121
+ test('formats the direct/via-memo split only when a memo barrier routes', () => {
122
+ // Memo chain → the split is shown.
123
+ const withMemo = formatStaticBudget(buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc'))
124
+ expect(withMemo).toMatch(/count\s+→ \d+ subscribers \(1 direct · \d+ via memo\)/)
125
+ // Counter reads its signal directly (no memo) → no parenthetical.
126
+ const direct = formatStaticBudget(buildStaticBudget(counterSource, 'Counter.tsx', 'Counter'))
127
+ expect(direct).not.toContain('via memo')
128
+ })
129
+
106
130
  test('formats a human-readable budget', () => {
107
131
  const out = formatStaticBudget(buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc'))
108
132
  expect(out).toContain('static reactivity budget')
@@ -175,6 +199,40 @@ describe('diffStaticBudget (SR6)', () => {
175
199
  expect(formatBudgetDiff(diff)).toContain('no structural reactivity change')
176
200
  })
177
201
 
202
+ test('a memo barrier that lowers direct fan-out is not a fan-out regression', () => {
203
+ // Before: `count` is read directly by an effect AND a text binding (direct 2).
204
+ const before = `
205
+ 'use client'
206
+ import { createSignal, createEffect } from '@barefootjs/client'
207
+ export function R() {
208
+ const [count, setCount] = createSignal(0)
209
+ createEffect(() => console.log(count()))
210
+ return <button>{count()}</button>
211
+ }
212
+ `
213
+ // After: both reads go through a memo, so `count`'s direct fan-out drops to 1
214
+ // (the memo) even though a memo was added and the transitive total rose.
215
+ const after = `
216
+ 'use client'
217
+ import { createSignal, createMemo, createEffect } from '@barefootjs/client'
218
+ export function R() {
219
+ const [count, setCount] = createSignal(0)
220
+ const m = createMemo(() => count())
221
+ createEffect(() => console.log(m()))
222
+ return <button>{m()}</button>
223
+ }
224
+ `
225
+ const base = buildStaticBudget(before, 'R.tsx', 'R')
226
+ const head = buildStaticBudget(after, 'R.tsx', 'R')
227
+ expect(base.fanOut.find(f => f.signal === 'count')!.direct).toBe(2)
228
+ expect(head.fanOut.find(f => f.signal === 'count')!.direct).toBe(1)
229
+ // The fan-out delta reads the refactor as the improvement it is: direct
230
+ // fan-out shrank, so the recorded change is a decrease, not growth.
231
+ const diff = diffStaticBudget(base, head)
232
+ const ch = diff.fanOut.find(f => f.signal === 'count')!
233
+ expect(ch.after).toBeLessThan(ch.before)
234
+ })
235
+
178
236
  test('carries a "diff" kind discriminator (#1849 B2)', () => {
179
237
  // The three JSON modes must be distinguishable: a zero-delta diff
180
238
  // (signals: 0 = "no change") must not look like a static budget
package/src/profiler.ts CHANGED
@@ -39,7 +39,17 @@ export interface FanOutEntry {
39
39
  signal: string
40
40
  /** Distinct transitive subscribers (memos + effects + DOM bindings). */
41
41
  subscribers: number
42
- /** True when `subscribers` exceeds the configured threshold. */
42
+ /**
43
+ * Distinct subscribers that read the signal *directly* (not through a memo).
44
+ * This is the real per-write re-run pressure: a direct write re-runs exactly
45
+ * these. The remaining `subscribers − direct` sit behind a memo barrier and
46
+ * only re-run when that memo's value actually changes — so adding a memo
47
+ * barrier *lowers* `direct` while it can *raise* `subscribers` (more nodes
48
+ * become statically attributable). Read `direct`, not `subscribers`, to judge
49
+ * cost.
50
+ */
51
+ direct: number
52
+ /** True when *direct* fan-out (not the transitive total) meets the threshold. */
43
53
  hot: boolean
44
54
  loc: { file: string; line: number }
45
55
  }
@@ -107,8 +117,10 @@ export function buildStaticBudget(
107
117
 
108
118
  const fanOut: FanOutEntry[] = graph.signals
109
119
  .map(s => {
110
- const subscribers = transitiveSubscriberCount(graph, s.name)
111
- return { signal: s.name, subscribers, hot: subscribers >= threshold, loc: s.loc }
120
+ const { direct, total } = subscriberCounts(graph, s.name)
121
+ // `hot` keys off *direct* fan-out a signal driving 12 nodes through one
122
+ // memo (direct 1) isn't hot; one driving 12 nodes directly is.
123
+ return { signal: s.name, subscribers: total, direct, hot: direct >= threshold, loc: s.loc }
112
124
  })
113
125
  .sort((a, b) => b.subscribers - a.subscribers)
114
126
 
@@ -161,23 +173,35 @@ function isEventHandlerConsumer(consumer: string): boolean {
161
173
  }
162
174
 
163
175
  /**
164
- * Distinct transitive subscribers of a signal/memo. Walks the same tagged
165
- * `consumers` tree `traceUpdatePath` builds (`debug.ts`), deduplicating across
166
- * branches so a diamond dependency counts each subscriber once. Event handlers
167
- * are excluded they read but don't react.
176
+ * Subscriber counts for a signal/memo: `direct` (read it without a memo in
177
+ * between — the top level of the dependent tree) and `total` (every distinct
178
+ * transitive subscriber). Walks the same tagged `consumers` tree
179
+ * `traceUpdatePath` builds (`debug.ts`), deduplicating across branches so a
180
+ * diamond dependency counts each subscriber once. Event handlers are excluded —
181
+ * they read but don't react. `total − direct` are the nodes reached only
182
+ * through a memo barrier.
168
183
  */
169
- function transitiveSubscriberCount(graph: ComponentGraph, name: string): number {
184
+ function subscriberCounts(graph: ComponentGraph, name: string): { direct: number; total: number } {
170
185
  const path = traceUpdatePath(graph, name)
171
- if (!path) return 0
186
+ if (!path) return { direct: 0, total: 0 }
172
187
  const seen = new Set<string>()
173
- const walk = (entries: UpdatePathEntry[]): void => {
188
+ const directSeen = new Set<string>()
189
+ const walk = (entries: UpdatePathEntry[], depth: number): void => {
174
190
  for (const e of entries) {
175
- if (!isEventHandlerEntry(e.kind, e.name)) seen.add(`${e.kind}:${e.name}`)
176
- walk(e.children)
191
+ if (!isEventHandlerEntry(e.kind, e.name)) {
192
+ seen.add(`${e.kind}:${e.name}`)
193
+ if (depth === 0) directSeen.add(`${e.kind}:${e.name}`)
194
+ }
195
+ walk(e.children, depth + 1)
177
196
  }
178
197
  }
179
- walk(path.dependents)
180
- return seen.size
198
+ walk(path.dependents, 0)
199
+ return { direct: directSeen.size, total: seen.size }
200
+ }
201
+
202
+ /** Distinct transitive subscribers — the `total` of {@link subscriberCounts}. */
203
+ function transitiveSubscriberCount(graph: ComponentGraph, name: string): number {
204
+ return subscriberCounts(graph, name).total
181
205
  }
182
206
 
183
207
  /**
@@ -223,7 +247,11 @@ export function formatStaticBudget(b: StaticBudget): string {
223
247
  if (shown.length > 0) {
224
248
  lines.push(' fan-out (top):')
225
249
  for (const f of shown) {
226
- lines.push(` ${f.signal.padEnd(12)} ${f.subscribers} subscribers${f.hot ? ' ⚠ high' : ''}`)
250
+ // Show the direct/indirect split only when a memo barrier routes some of
251
+ // the subscribers — otherwise the bare count is already the direct count.
252
+ const indirect = f.subscribers - f.direct
253
+ const detail = indirect > 0 ? ` (${f.direct} direct · ${indirect} via memo)` : ''
254
+ lines.push(` ${f.signal.padEnd(12)} → ${f.subscribers} subscribers${detail}${f.hot ? ' ⚠ high' : ''}`)
227
255
  }
228
256
  }
229
257
  if (b.crossComponentOnly) {
@@ -260,7 +288,7 @@ export interface BudgetDiff {
260
288
  loops: number
261
289
  subscriptions: number
262
290
  memoChainDepth: number
263
- /** Signals whose fan-out changed (added/removed signals included as 0↔n). */
291
+ /** Signals whose *direct* fan-out changed (added/removed signals as 0↔n). */
264
292
  fanOut: FanOutChange[]
265
293
  /** True when any tracked metric regressed (grew) past zero. */
266
294
  regressed: boolean
@@ -272,8 +300,12 @@ export interface BudgetDiff {
272
300
  * `regressed` is true (or gate on a specific metric threshold).
273
301
  */
274
302
  export function diffStaticBudget(base: StaticBudget, head: StaticBudget): BudgetDiff {
275
- const baseFan = new Map(base.fanOut.map(f => [f.signal, f.subscribers]))
276
- const headFan = new Map(head.fanOut.map(f => [f.signal, f.subscribers]))
303
+ // Track *direct* fan-out: a refactor that routes subscribers through a new
304
+ // memo lowers direct fan-out (less re-run pressure) even as it can raise the
305
+ // transitive total, so a direct-fan-out delta reads such a change as the
306
+ // improvement it is instead of a false regression.
307
+ const baseFan = new Map(base.fanOut.map(f => [f.signal, f.direct]))
308
+ const headFan = new Map(head.fanOut.map(f => [f.signal, f.direct]))
277
309
  const signals = new Set([...baseFan.keys(), ...headFan.keys()])
278
310
 
279
311
  const fanOut: FanOutChange[] = []
@@ -318,7 +350,7 @@ export function formatBudgetDiff(d: BudgetDiff): string {
318
350
  lines.push(` memo chain ${d.memoChainDepth > 0 ? 'deepened' : 'shortened'} by ${Math.abs(d.memoChainDepth)}`)
319
351
  }
320
352
  for (const f of d.fanOut) {
321
- lines.push(` signal \`${f.signal}\` fan-out ${f.before}→${f.after}`)
353
+ lines.push(` signal \`${f.signal}\` direct fan-out ${f.before}→${f.after}`)
322
354
  }
323
355
  if (lines.length === 1) lines.push(' no structural reactivity change')
324
356
  else lines.push(d.regressed ? ' ⚠ reactivity regressed' : ' ✓ no regression')