@barefootjs/client 0.17.1 → 0.18.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.
@@ -209,6 +209,11 @@ function __bfReportOutput(changed) {
209
209
  var Owner = null;
210
210
  var Listener = null;
211
211
  var MAX_EFFECT_RUNS = 100;
212
+ function registerChild(owner, child) {
213
+ if (!owner.children)
214
+ owner.children = new Set;
215
+ owner.children.add(child);
216
+ }
212
217
  var BatchDepth = 0;
213
218
  var PendingEffects = new Set;
214
219
  function createSignal(initialValue, __bfId) {
@@ -218,10 +223,18 @@ function createSignal(initialValue, __bfId) {
218
223
  subscribers.__bfSignalId = id;
219
224
  const get = () => {
220
225
  if (Listener) {
221
- subscribers.add(Listener);
222
- Listener.dependencies.add(subscribers);
223
- if (profilerSink)
226
+ if (profilerSink) {
227
+ subscribers.add(Listener);
228
+ subscribers.__bfSnapshot = null;
229
+ Listener.dependencies.set(subscribers, Listener.gen);
224
230
  profilerSink.subscribeAdd(id, Listener.id);
231
+ } else if (!Listener.dependencies.has(subscribers)) {
232
+ subscribers.add(Listener);
233
+ subscribers.__bfSnapshot = null;
234
+ Listener.dependencies.set(subscribers, Listener.gen);
235
+ } else {
236
+ Listener.dependencies.set(subscribers, Listener.gen);
237
+ }
225
238
  }
226
239
  return value;
227
240
  };
@@ -238,9 +251,12 @@ function createSignal(initialValue, __bfId) {
238
251
  PendingEffects.add(effect);
239
252
  }
240
253
  } else {
241
- const effectsToRun = [...subscribers];
242
- for (const effect of effectsToRun) {
243
- runEffect(effect);
254
+ let snapshot = subscribers.__bfSnapshot;
255
+ if (!snapshot) {
256
+ snapshot = subscribers.__bfSnapshot = [...subscribers];
257
+ }
258
+ for (let i = 0;i < snapshot.length; i++) {
259
+ runEffect(snapshot[i]);
244
260
  }
245
261
  }
246
262
  };
@@ -250,9 +266,10 @@ function createEffect(fn, __bfId, __bfKind = "effect") {
250
266
  const effect = {
251
267
  fn,
252
268
  cleanup: null,
253
- dependencies: new Set,
269
+ dependencies: new Map,
270
+ gen: 0,
254
271
  owner: Owner,
255
- children: [],
272
+ children: null,
256
273
  disposed: false,
257
274
  runCount: 0,
258
275
  id: __bfId ?? (profilerSink ? `e${++subscriberSeq}` : ""),
@@ -263,7 +280,7 @@ function createEffect(fn, __bfId, __bfKind = "effect") {
263
280
  if (profilerSink)
264
281
  profilerSink.effectCreate(effect.id, effect.kind);
265
282
  if (Owner)
266
- Owner.children.push(effect);
283
+ registerChild(Owner, effect);
267
284
  runEffect(effect);
268
285
  }
269
286
  function runEffect(effect) {
@@ -274,22 +291,26 @@ function runEffect(effect) {
274
291
  effect.runCount = 0;
275
292
  throw new Error(`Circular dependency detected: effect re-entered itself ${MAX_EFFECT_RUNS} times.`);
276
293
  }
294
+ const isOutermostRun = effect.runCount === 1;
277
295
  if (profilerSink)
278
296
  profilerSink.effectEnter(effect.id);
279
297
  if (effect.cleanup) {
280
298
  effect.cleanup();
281
299
  effect.cleanup = null;
282
300
  }
283
- for (const dep of effect.dependencies) {
284
- dep.delete(effect);
285
- if (profilerSink)
301
+ if (profilerSink) {
302
+ for (const dep of effect.dependencies.keys()) {
303
+ dep.delete(effect);
304
+ dep.__bfSnapshot = null;
286
305
  profilerSink.subscribeRemove(dep.__bfSignalId ?? "", effect.id);
306
+ }
307
+ effect.dependencies.clear();
287
308
  }
288
- effect.dependencies.clear();
289
309
  const prevOwner = Owner;
290
310
  const prevListener = Listener;
291
311
  Owner = effect;
292
312
  Listener = effect;
313
+ effect.gen++;
293
314
  effect.outputReported = false;
294
315
  effect.outputChanged = false;
295
316
  const start = profilerSink ? performance.now() : 0;
@@ -302,6 +323,15 @@ function runEffect(effect) {
302
323
  Owner = prevOwner;
303
324
  Listener = prevListener;
304
325
  effect.runCount--;
326
+ if (!profilerSink && isOutermostRun) {
327
+ for (const [dep, gen] of effect.dependencies) {
328
+ if (gen !== effect.gen) {
329
+ effect.dependencies.delete(dep);
330
+ dep.delete(effect);
331
+ dep.__bfSnapshot = null;
332
+ }
333
+ }
334
+ }
305
335
  if (profilerSink) {
306
336
  profilerSink.effectExit(effect.id, performance.now() - start);
307
337
  if (effect.outputReported)
@@ -313,16 +343,19 @@ function disposeSubtree(effect) {
313
343
  if (effect.disposed)
314
344
  return;
315
345
  effect.disposed = true;
316
- for (const child of effect.children) {
317
- disposeSubtree(child);
346
+ if (effect.children) {
347
+ for (const child of effect.children) {
348
+ disposeSubtree(child);
349
+ }
350
+ effect.children.clear();
318
351
  }
319
- effect.children.length = 0;
320
352
  if (effect.cleanup) {
321
353
  effect.cleanup();
322
354
  effect.cleanup = null;
323
355
  }
324
- for (const dep of effect.dependencies) {
356
+ for (const dep of effect.dependencies.keys()) {
325
357
  dep.delete(effect);
358
+ dep.__bfSnapshot = null;
326
359
  if (profilerSink)
327
360
  profilerSink.subscribeRemove(dep.__bfSignalId ?? "", effect.id);
328
361
  }
@@ -335,9 +368,7 @@ function disposeEffect(effect) {
335
368
  if (effect.disposed)
336
369
  return;
337
370
  if (effect.owner) {
338
- const idx = effect.owner.children.indexOf(effect);
339
- if (idx >= 0)
340
- effect.owner.children.splice(idx, 1);
371
+ effect.owner.children?.delete(effect);
341
372
  }
342
373
  disposeSubtree(effect);
343
374
  }
@@ -345,9 +376,10 @@ function createRoot(fn) {
345
376
  const root = {
346
377
  fn: () => {},
347
378
  cleanup: null,
348
- dependencies: new Set,
379
+ dependencies: new Map,
380
+ gen: 0,
349
381
  owner: Owner,
350
- children: [],
382
+ children: null,
351
383
  disposed: false,
352
384
  runCount: 0,
353
385
  id: profilerSink ? `r${++subscriberSeq}` : "",
@@ -358,7 +390,7 @@ function createRoot(fn) {
358
390
  if (profilerSink)
359
391
  profilerSink.effectCreate(root.id, "root");
360
392
  if (Owner)
361
- Owner.children.push(root);
393
+ registerChild(Owner, root);
362
394
  const prevOwner = Owner;
363
395
  const prevListener = Listener;
364
396
  Owner = root;
@@ -379,9 +411,10 @@ function createDisposableEffect(fn, __bfId) {
379
411
  return fn();
380
412
  },
381
413
  cleanup: null,
382
- dependencies: new Set,
414
+ dependencies: new Map,
415
+ gen: 0,
383
416
  owner: Owner,
384
- children: [],
417
+ children: null,
385
418
  disposed: false,
386
419
  runCount: 0,
387
420
  id: __bfId ?? (profilerSink ? `e${++subscriberSeq}` : ""),
@@ -392,7 +425,7 @@ function createDisposableEffect(fn, __bfId) {
392
425
  if (profilerSink)
393
426
  profilerSink.effectCreate(effect.id, effect.kind);
394
427
  if (Owner)
395
- Owner.children.push(effect);
428
+ registerChild(Owner, effect);
396
429
  runEffect(effect);
397
430
  return () => {
398
431
  disposeEffect(effect);
@@ -1574,6 +1607,8 @@ function escapeAttr(value) {
1574
1607
  return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1575
1608
  }
1576
1609
  function escapeText(value) {
1610
+ if (value == null)
1611
+ return "";
1577
1612
  return escapeAttr(value);
1578
1613
  }
1579
1614
  var SVG_NS = "http://www.w3.org/2000/svg";
@@ -1993,7 +2028,7 @@ function findLoopMarkers2(container, markerId) {
1993
2028
  if (markerId) {
1994
2029
  const startVal = loopStartMarker(markerId);
1995
2030
  const endVal = loopEndMarker(markerId);
1996
- for (const node of Array.from(container.childNodes)) {
2031
+ for (let node = container.firstChild;node; node = node.nextSibling) {
1997
2032
  if (node.nodeType !== Node.COMMENT_NODE)
1998
2033
  continue;
1999
2034
  const value = node.nodeValue;
@@ -2005,7 +2040,7 @@ function findLoopMarkers2(container, markerId) {
2005
2040
  } else {
2006
2041
  const startPrefix = `${BF_LOOP_START}:`;
2007
2042
  const endPrefix = `${BF_LOOP_END}:`;
2008
- for (const node of Array.from(container.childNodes)) {
2043
+ for (let node = container.firstChild;node; node = node.nextSibling) {
2009
2044
  if (node.nodeType !== Node.COMMENT_NODE)
2010
2045
  continue;
2011
2046
  const value = node.nodeValue ?? "";
@@ -2045,12 +2080,44 @@ function findItemRanges(start, end) {
2045
2080
  }
2046
2081
  return ranges.filter((r) => r.primaryEl !== null);
2047
2082
  }
2048
- function insertScope(scope, container, anchor) {
2083
+ function insertScope(scope, target, anchor) {
2049
2084
  if (scope.startMarker)
2050
- container.insertBefore(scope.startMarker, anchor);
2051
- container.insertBefore(scope.primaryEl, anchor);
2085
+ target.insertBefore(scope.startMarker, anchor);
2086
+ target.insertBefore(scope.primaryEl, anchor);
2052
2087
  for (const ex of scope.extras)
2053
- container.insertBefore(ex, anchor);
2088
+ target.insertBefore(ex, anchor);
2089
+ }
2090
+ function longestIncreasingSubsequenceIndices(arr) {
2091
+ const n = arr.length;
2092
+ if (n === 0)
2093
+ return [];
2094
+ const tails = [];
2095
+ const predecessors = new Array(n).fill(-1);
2096
+ for (let i = 0;i < n; i++) {
2097
+ const value = arr[i];
2098
+ let lo = 0;
2099
+ let hi = tails.length;
2100
+ while (lo < hi) {
2101
+ const mid = lo + hi >>> 1;
2102
+ if (arr[tails[mid]] < value)
2103
+ lo = mid + 1;
2104
+ else
2105
+ hi = mid;
2106
+ }
2107
+ if (lo > 0)
2108
+ predecessors[i] = tails[lo - 1];
2109
+ if (lo === tails.length)
2110
+ tails.push(i);
2111
+ else
2112
+ tails[lo] = i;
2113
+ }
2114
+ const result = new Array(tails.length);
2115
+ let k = tails[tails.length - 1];
2116
+ for (let i = tails.length - 1;i >= 0; i--) {
2117
+ result[i] = k;
2118
+ k = predecessors[k];
2119
+ }
2120
+ return result;
2054
2121
  }
2055
2122
  function removeScope(scope) {
2056
2123
  if (scope.startMarker?.parentNode)
@@ -2093,37 +2160,48 @@ function mapArray(accessor, container, getKey, renderItem, markerId, bfId) {
2093
2160
  return;
2094
2161
  const scopes = new Map;
2095
2162
  let hydrated = false;
2163
+ let cachedStart = null;
2164
+ let cachedEnd = null;
2165
+ const resolveMarkers = () => {
2166
+ if (cachedStart && cachedEnd && cachedStart.isConnected && cachedEnd.isConnected) {
2167
+ return { start: cachedStart, end: cachedEnd };
2168
+ }
2169
+ const found = findLoopMarkers2(container, markerId);
2170
+ cachedStart = found.start;
2171
+ cachedEnd = found.end;
2172
+ return found;
2173
+ };
2096
2174
  createEffect(() => {
2097
2175
  const items = accessor();
2098
2176
  if (!items)
2099
2177
  return;
2100
- const { start: startMarker, end: endMarker } = findLoopMarkers2(container, markerId);
2178
+ const { start: startMarker, end: endMarker } = resolveMarkers();
2101
2179
  const anchor = endMarker ?? null;
2102
2180
  if (!hydrated) {
2103
2181
  hydrated = true;
2104
2182
  const existingRanges = startMarker ? findItemRanges(startMarker, endMarker) : Array.from(container.children).map((el) => ({ startMarker: null, primaryEl: el, extras: [] }));
2105
2183
  const needsHydration = existingRanges.length > 0 && (!existingRanges[0]?.primaryEl.hasAttribute("data-key") || scopes.size === 0);
2106
2184
  if (needsHydration) {
2107
- for (let i = 0;i < existingRanges.length && i < items.length; i++) {
2108
- const range = existingRanges[i];
2109
- const item = items[i];
2110
- const key = getKey ? getKey(item, i) : String(i);
2185
+ for (let i2 = 0;i2 < existingRanges.length && i2 < items.length; i2++) {
2186
+ const range = existingRanges[i2];
2187
+ const item = items[i2];
2188
+ const key = getKey ? getKey(item, i2) : String(i2);
2111
2189
  range.primaryEl.setAttribute(BF_KEY, key);
2112
- const scope = createItemScope(item, i, renderItem, range.primaryEl, range.extras, range.startMarker);
2190
+ const scope = createItemScope(item, i2, renderItem, range.primaryEl, range.extras, range.startMarker);
2113
2191
  scopes.set(key, scope);
2114
2192
  hydratedScopes.add(range.primaryEl);
2115
2193
  }
2116
- for (let i = existingRanges.length;i < items.length; i++) {
2117
- const item = items[i];
2118
- const key = getKey ? getKey(item, i) : String(i);
2119
- const scope = createItemScope(item, i, renderItem);
2194
+ for (let i2 = existingRanges.length;i2 < items.length; i2++) {
2195
+ const item = items[i2];
2196
+ const key = getKey ? getKey(item, i2) : String(i2);
2197
+ const scope = createItemScope(item, i2, renderItem);
2120
2198
  if (!scope.primaryEl.dataset.key)
2121
2199
  scope.primaryEl.setAttribute(BF_KEY, key);
2122
2200
  scopes.set(key, scope);
2123
2201
  insertScope(scope, container, anchor);
2124
2202
  }
2125
- for (let i = items.length;i < existingRanges.length; i++) {
2126
- const range = existingRanges[i];
2203
+ for (let i2 = items.length;i2 < existingRanges.length; i2++) {
2204
+ const range = existingRanges[i2];
2127
2205
  if (range.startMarker?.parentNode)
2128
2206
  range.startMarker.remove();
2129
2207
  if (range.primaryEl.parentNode)
@@ -2151,12 +2229,40 @@ function mapArray(accessor, container, getKey, renderItem, markerId, bfId) {
2151
2229
  }
2152
2230
  }
2153
2231
  }
2232
+ if (items.length === 0) {
2233
+ if (scopes.size > 0) {
2234
+ for (const scope of scopes.values())
2235
+ scope.dispose();
2236
+ if (startMarker && endMarker) {
2237
+ const range = document.createRange();
2238
+ range.setStartAfter(startMarker);
2239
+ range.setEndBefore(endMarker);
2240
+ range.deleteContents();
2241
+ } else {
2242
+ let expectedNodeCount = 0;
2243
+ for (const scope of scopes.values()) {
2244
+ expectedNodeCount += 1 + scope.extras.length + (scope.startMarker ? 1 : 0);
2245
+ }
2246
+ let actualNodeCount = 0;
2247
+ for (let node = container.firstChild;node; node = node.nextSibling)
2248
+ actualNodeCount++;
2249
+ if (actualNodeCount === expectedNodeCount) {
2250
+ container.textContent = "";
2251
+ } else {
2252
+ for (const scope of scopes.values())
2253
+ removeScope(scope);
2254
+ }
2255
+ }
2256
+ scopes.clear();
2257
+ }
2258
+ return;
2259
+ }
2154
2260
  const newKeys = new Set;
2155
2261
  const warnedKeys = new Set;
2156
2262
  const desiredOrder = [];
2157
- for (let i = 0;i < items.length; i++) {
2158
- const item = items[i];
2159
- const key = getKey ? getKey(item, i) : String(i);
2263
+ for (let i2 = 0;i2 < items.length; i2++) {
2264
+ const item = items[i2];
2265
+ const key = getKey ? getKey(item, i2) : String(i2);
2160
2266
  if (newKeys.has(key) && !warnedKeys.has(key)) {
2161
2267
  warnedKeys.add(key);
2162
2268
  console.warn(`[BarefootJS] mapArray: duplicate key "${key}" — items with this key collapse to a single DOM scope, ` + `so only the last one renders. Use a per-item identifier (e.g. \`key={item.id}\`) for correct reconciliation.`);
@@ -2167,7 +2273,7 @@ function mapArray(accessor, container, getKey, renderItem, markerId, bfId) {
2167
2273
  existing.setItem(item);
2168
2274
  desiredOrder.push(existing);
2169
2275
  } else {
2170
- const scope = createItemScope(item, i, renderItem);
2276
+ const scope = createItemScope(item, i2, renderItem);
2171
2277
  if (!scope.primaryEl.dataset.key)
2172
2278
  scope.primaryEl.setAttribute(BF_KEY, key);
2173
2279
  scopes.set(key, scope);
@@ -2181,32 +2287,41 @@ function mapArray(accessor, container, getKey, renderItem, markerId, bfId) {
2181
2287
  scopes.delete(key);
2182
2288
  }
2183
2289
  }
2184
- let inOrder = true;
2185
- let checkNode = startMarker ? startMarker.nextSibling : container.firstChild;
2186
- for (const scope of desiredOrder) {
2187
- while (checkNode && checkNode.nodeType !== Node.ELEMENT_NODE)
2188
- checkNode = checkNode.nextSibling;
2189
- if (checkNode !== scope.primaryEl) {
2190
- inOrder = false;
2191
- break;
2192
- }
2193
- checkNode = checkNode.nextSibling;
2194
- for (let i = 0;i < scope.extras.length; i++) {
2195
- while (checkNode && checkNode.nodeType !== Node.ELEMENT_NODE)
2196
- checkNode = checkNode.nextSibling;
2197
- if (checkNode !== scope.extras[i]) {
2198
- inOrder = false;
2199
- break;
2200
- }
2201
- checkNode = checkNode.nextSibling;
2202
- }
2203
- if (!inOrder)
2204
- break;
2290
+ const primaryElToDesiredIndex = new Map;
2291
+ for (let i2 = 0;i2 < desiredOrder.length; i2++) {
2292
+ primaryElToDesiredIndex.set(desiredOrder[i2].primaryEl, i2);
2205
2293
  }
2206
- if (!inOrder) {
2207
- for (const scope of desiredOrder) {
2208
- insertScope(scope, container, anchor);
2294
+ const domOrderIndices = [];
2295
+ for (let node = startMarker ? startMarker.nextSibling : container.firstChild;node && node !== anchor; node = node.nextSibling) {
2296
+ if (node.nodeType !== Node.ELEMENT_NODE)
2297
+ continue;
2298
+ const idx = primaryElToDesiredIndex.get(node);
2299
+ if (idx !== undefined)
2300
+ domOrderIndices.push(idx);
2301
+ }
2302
+ const stationary = new Array(desiredOrder.length).fill(false);
2303
+ for (const pos of longestIncreasingSubsequenceIndices(domOrderIndices)) {
2304
+ stationary[domOrderIndices[pos]] = true;
2305
+ }
2306
+ let i = 0;
2307
+ while (i < desiredOrder.length) {
2308
+ if (stationary[i]) {
2309
+ i++;
2310
+ continue;
2311
+ }
2312
+ let j = i;
2313
+ while (j < desiredOrder.length && !stationary[j])
2314
+ j++;
2315
+ const before = j < desiredOrder.length ? desiredOrder[j].startMarker ?? desiredOrder[j].primaryEl : anchor;
2316
+ if (j - i === 1) {
2317
+ insertScope(desiredOrder[i], container, before);
2318
+ } else {
2319
+ const runFragment = document.createDocumentFragment();
2320
+ for (let k = i;k < j; k++)
2321
+ insertScope(desiredOrder[k], runFragment, null);
2322
+ container.insertBefore(runFragment, before);
2209
2323
  }
2324
+ i = j;
2210
2325
  }
2211
2326
  }, bfId);
2212
2327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/client",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "BarefootJS client package: reactive primitives (SSR-safe) plus browser runtime under the `/runtime` subpath (compiler target)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -55,7 +55,7 @@
55
55
  "directory": "packages/client"
56
56
  },
57
57
  "dependencies": {
58
- "@barefootjs/shared": "0.17.1"
58
+ "@barefootjs/shared": "0.18.0"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@barefootjs/jsx": ">=0.2.0"
package/src/build.ts CHANGED
@@ -58,6 +58,20 @@ export interface CSRBuildOptions {
58
58
  adapter?: TemplateAdapter
59
59
  /** Options forwarded to the default `CSRAdapter`. Ignored when `adapter` is set. */
60
60
  adapterOptions?: CSRAdapterOptions
61
+ /**
62
+ * How the CLI produces `barefoot.js`. Defaults to `'treeshake'`, which
63
+ * bundles only the runtime exports this project's compiled client JS
64
+ * actually imports (plus the always-kept public mount API). Set to
65
+ * `'full'` to copy the entire prebuilt runtime bundle verbatim, matching
66
+ * pre-tree-shaking behavior.
67
+ */
68
+ runtimeBundle?: 'treeshake' | 'full'
69
+ /**
70
+ * Extra `@barefootjs/client*` export names to force-keep in `barefoot.js`
71
+ * under `runtimeBundle: 'treeshake'` — for names only ever referenced
72
+ * from hand-written page scripts the CLI never compiles.
73
+ */
74
+ runtimeKeep?: string[]
61
75
  }
62
76
 
63
77
  /**
@@ -85,6 +99,8 @@ export function createConfig(options: CSRBuildOptions = {}) {
85
99
  bundleEntries: options.bundleEntries,
86
100
  outputLayout: options.outputLayout,
87
101
  postBuild: options.postBuild,
102
+ runtimeBundle: options.runtimeBundle,
103
+ runtimeKeep: options.runtimeKeep,
88
104
  }
89
105
  }
90
106