@alloy-js/core 0.22.0-dev.0 → 0.22.0-dev.3

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.
Files changed (68) hide show
  1. package/dist/src/components/Block.d.ts.map +1 -1
  2. package/dist/src/components/Block.js +24 -7
  3. package/dist/src/components/Block.js.map +1 -1
  4. package/dist/src/components/Indent.d.ts.map +1 -1
  5. package/dist/src/components/Indent.js +2 -1
  6. package/dist/src/components/Indent.js.map +1 -1
  7. package/dist/src/components/Prose.d.ts.map +1 -1
  8. package/dist/src/components/Prose.js +2 -1
  9. package/dist/src/components/Prose.js.map +1 -1
  10. package/dist/src/content-slot.d.ts +51 -0
  11. package/dist/src/content-slot.d.ts.map +1 -0
  12. package/dist/src/content-slot.js +69 -0
  13. package/dist/src/content-slot.js.map +1 -0
  14. package/dist/src/content-slot.test.d.ts +2 -0
  15. package/dist/src/content-slot.test.d.ts.map +1 -0
  16. package/dist/src/content-slot.test.js +57 -0
  17. package/dist/src/content-slot.test.js.map +1 -0
  18. package/dist/src/index.d.ts +1 -0
  19. package/dist/src/index.d.ts.map +1 -1
  20. package/dist/src/index.js +1 -0
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/reactivity.d.ts +15 -1
  23. package/dist/src/reactivity.d.ts.map +1 -1
  24. package/dist/src/reactivity.js +20 -8
  25. package/dist/src/reactivity.js.map +1 -1
  26. package/dist/src/render.d.ts +1 -0
  27. package/dist/src/render.d.ts.map +1 -1
  28. package/dist/src/render.js +66 -2
  29. package/dist/src/render.js.map +1 -1
  30. package/dist/src/symbols/decl.d.ts +8 -0
  31. package/dist/src/symbols/decl.d.ts.map +1 -0
  32. package/dist/src/symbols/decl.js +22 -0
  33. package/dist/src/symbols/decl.js.map +1 -0
  34. package/dist/src/symbols/index.d.ts +1 -0
  35. package/dist/src/symbols/index.d.ts.map +1 -1
  36. package/dist/src/symbols/index.js +1 -0
  37. package/dist/src/symbols/index.js.map +1 -1
  38. package/dist/src/tracer.js +1 -1
  39. package/dist/src/tracer.js.map +1 -1
  40. package/dist/src/utils.d.ts.map +1 -1
  41. package/dist/src/utils.js +212 -15
  42. package/dist/src/utils.js.map +1 -1
  43. package/dist/test/components/block.test.d.ts.map +1 -1
  44. package/dist/test/components/block.test.js +18 -1
  45. package/dist/test/components/block.test.js.map +1 -1
  46. package/dist/test/components/list.test.d.ts.map +1 -1
  47. package/dist/test/components/list.test.js +80 -1
  48. package/dist/test/components/list.test.js.map +1 -1
  49. package/dist/test/control-flow/for.test.js +32 -2
  50. package/dist/test/control-flow/for.test.js.map +1 -1
  51. package/dist/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +1 -1
  53. package/src/components/Block.tsx +18 -6
  54. package/src/components/Indent.tsx +4 -2
  55. package/src/components/Prose.tsx +2 -1
  56. package/src/content-slot.test.tsx +65 -0
  57. package/src/content-slot.tsx +91 -0
  58. package/src/index.ts +1 -0
  59. package/src/reactivity.ts +38 -5
  60. package/src/render.ts +80 -3
  61. package/src/symbols/decl.ts +25 -0
  62. package/src/symbols/index.ts +1 -0
  63. package/src/tracer.ts +1 -1
  64. package/src/utils.tsx +240 -16
  65. package/temp/api.json +469 -0
  66. package/test/components/block.test.tsx +21 -1
  67. package/test/components/list.test.tsx +76 -1
  68. package/test/control-flow/for.test.tsx +43 -2
package/src/utils.tsx CHANGED
@@ -1,9 +1,11 @@
1
- import { toRaw } from "@vue/reactivity";
1
+ import { ref, Ref, toRaw } from "@vue/reactivity";
2
2
  import { BaseListProps } from "./components/List.jsx";
3
3
  import {
4
4
  createCustomContext,
5
5
  CustomContext,
6
6
  Disposable,
7
+ effect,
8
+ getContext,
7
9
  memo,
8
10
  onCleanup,
9
11
  root,
@@ -92,16 +94,173 @@ export function mapJoin<T, U, V>(
92
94
  cb: (key: T, valueOrIndex: U | number, index: number) => V,
93
95
  rawOptions: JoinOptions = {},
94
96
  ): () => Children {
97
+ /**
98
+ * Strategy overview:
99
+ * - Initial render: collect a stable array of mapper outputs, cache
100
+ * per-index slot metadata, and create joiner `Ref`s for the gaps between
101
+ * items so the rendered list can be mutated without recreating siblings.
102
+ * - List mutations: diff the incoming iterable against slot metadata, reuse
103
+ * entries with matching keys, recycle disposers past the change point, and
104
+ * prune caches while recalculating first/last non-empty indices so we only
105
+ * edit joiners that are affected by the change.
106
+ * - Content transitions: each child renders inside a custom context that
107
+ * exposes an `isEmpty` flag; when a child gains or loses content we update
108
+ * neighbor joiner refs so separators appear and disappear reactively
109
+ * without re-running the mapper.
110
+ */
95
111
  const options = { ...defaultJoinOptions, ...rawOptions };
96
112
  const ender =
97
113
  options.ender === true ? options.joiner : options.ender || undefined;
98
- const currentItems: (T | [T, U])[] = [];
99
- const disposables: Disposable[] = [];
114
+ type MapJoinItem = T | [T, U];
115
+ interface MapJoinSlot {
116
+ item?: MapJoinItem;
117
+ disposer?: Disposable;
118
+ joiner?: Ref<Children | undefined>;
119
+ isEmpty: Ref<boolean>;
120
+ }
121
+ const itemSlots: MapJoinSlot[] = [];
122
+
123
+ function getOrCreateSlot(index: number): MapJoinSlot {
124
+ let slot = itemSlots[index];
125
+ if (!slot) {
126
+ slot = { isEmpty: ref(true) };
127
+ itemSlots[index] = slot;
128
+ }
129
+ return slot;
130
+ }
131
+ const firstNonEmptyIndex = ref(-1);
132
+ const lastNonEmptyIndex = ref(-1);
100
133
  const mapped: Children[] = [];
134
+ let enderMemo: (() => Children) | undefined;
135
+
136
+ // Creates a ref placeholder that stores the joiner node for a boundary.
137
+ function createJoinerRef(): Ref<Children | undefined> {
138
+ return ref<unknown>(undefined) as Ref<Children | undefined>;
139
+ }
140
+
141
+ // Makes sure we have a joiner ref at the requested boundary index.
142
+ function ensureJoinerSlot(index: number) {
143
+ if (index === 0 || options.joiner === undefined) {
144
+ return;
145
+ }
146
+
147
+ const slot = getOrCreateSlot(index);
148
+ slot.joiner ??= createJoinerRef();
149
+ mapped[index * 2 - 1] = slot.joiner!;
150
+ }
151
+
152
+ // Sets the joiner ref based on whether neighboring children have content.
153
+ function updateJoinerForIndex(index: number) {
154
+ if (index === 0 || options.joiner === undefined) {
155
+ return;
156
+ }
157
+
158
+ const slot = itemSlots[index];
159
+ const joinerRef = slot?.joiner;
160
+ if (!joinerRef) {
161
+ return;
162
+ }
163
+
164
+ if (index >= itemSlots.length) {
165
+ if (joinerRef.value !== undefined) {
166
+ joinerRef.value = undefined;
167
+ }
168
+ return;
169
+ }
170
+
171
+ const previousNonEmpty = findPrevNonEmpty(index - 1);
172
+ const rightSlot = itemSlots[index];
173
+ const shouldShow =
174
+ previousNonEmpty !== -1 &&
175
+ rightSlot !== undefined &&
176
+ rightSlot.isEmpty.value === false;
177
+
178
+ const newValue = shouldShow ? options.joiner : undefined;
179
+ if (joinerRef.value !== newValue) {
180
+ joinerRef.value = newValue;
181
+ }
182
+ }
183
+
184
+ // Finds the next child that is not empty starting at or after `from`.
185
+ function findNextNonEmpty(from: number) {
186
+ for (let i = Math.max(from, 0); i < itemSlots.length; i++) {
187
+ const slot = itemSlots[i];
188
+ if (slot && slot.isEmpty.value === false) {
189
+ return i;
190
+ }
191
+ }
192
+ return -1;
193
+ }
194
+
195
+ // Finds the previous child that is not empty before or at `from`.
196
+ function findPrevNonEmpty(from: number) {
197
+ for (let i = Math.min(from, itemSlots.length - 1); i >= 0; i--) {
198
+ const slot = itemSlots[i];
199
+ if (slot && slot.isEmpty.value === false) {
200
+ return i;
201
+ }
202
+ }
203
+ return -1;
204
+ }
205
+
206
+ // Recomputes the cached first and last non-empty indices after changes.
207
+ function recalculateNonEmptyBounds() {
208
+ const first = findNextNonEmpty(0);
209
+ firstNonEmptyIndex.value = first;
210
+ lastNonEmptyIndex.value =
211
+ first === -1 ? -1 : findPrevNonEmpty(itemSlots.length - 1);
212
+ refreshJoiners();
213
+ }
214
+
215
+ // Syncs all joiner refs after the structural shape of the list updates.
216
+ function refreshJoiners() {
217
+ if (options.joiner === undefined) {
218
+ return;
219
+ }
220
+
221
+ for (let i = 1; i < itemSlots.length; i++) {
222
+ const slot = itemSlots[i];
223
+ if (slot?.joiner) {
224
+ updateJoinerForIndex(i);
225
+ }
226
+ }
227
+ }
228
+
229
+ // Applies empty-state bookkeeping for an item and updates neighbor joiners.
230
+ function applyEmptyStateChange(
231
+ index: number,
232
+ isEmpty: boolean,
233
+ wasEmpty: boolean,
234
+ ) {
235
+ if (wasEmpty === isEmpty) {
236
+ return;
237
+ }
238
+
239
+ if (isEmpty) {
240
+ if (firstNonEmptyIndex.value === index) {
241
+ firstNonEmptyIndex.value = findNextNonEmpty(index + 1);
242
+ }
243
+ if (lastNonEmptyIndex.value === index) {
244
+ lastNonEmptyIndex.value = findPrevNonEmpty(index - 1);
245
+ }
246
+ } else {
247
+ if (firstNonEmptyIndex.value === -1 || index < firstNonEmptyIndex.value) {
248
+ firstNonEmptyIndex.value = index;
249
+ }
250
+ if (index > lastNonEmptyIndex.value) {
251
+ lastNonEmptyIndex.value = index;
252
+ }
253
+ }
254
+
255
+ updateJoinerForIndex(index);
256
+ updateJoinerForIndex(index + 1);
257
+ }
101
258
  let previousItemsLen = 0;
102
259
 
103
260
  onCleanup(() => {
104
- for (const d of disposables) d();
261
+ for (const slot of itemSlots) {
262
+ slot.disposer?.();
263
+ }
105
264
  });
106
265
 
107
266
  return () => {
@@ -117,6 +276,11 @@ export function mapJoin<T, U, V>(
117
276
  item !== null && item !== undefined && typeof item !== "boolean",
118
277
  );
119
278
  }
279
+
280
+ const context = getContext();
281
+ if (context) {
282
+ context.isEmpty ??= ref(true);
283
+ }
120
284
  // this is important to access here in reactive context so we are
121
285
  // notified of new items from reactives.
122
286
  const itemsLen = items.length;
@@ -127,55 +291,110 @@ export function mapJoin<T, U, V>(
127
291
  let startIndex = 0;
128
292
  for (
129
293
  ;
130
- startIndex < itemsLen && startIndex < currentItems.length;
294
+ startIndex < itemsLen && startIndex < itemSlots.length;
131
295
  startIndex++
132
296
  ) {
133
- if (!compare(items[startIndex], currentItems[startIndex])) {
297
+ const slot = itemSlots[startIndex];
298
+ if (!slot || !compare(items[startIndex], slot.item as any)) {
134
299
  break;
135
300
  }
136
301
  }
137
302
 
138
303
  if (startIndex > 0 && startIndex < itemsLen) {
139
304
  // need to update the previous joiner (might be ender or absent)
140
- mapped[startIndex * 2 - 1] = options.joiner;
305
+ ensureJoinerSlot(startIndex);
141
306
  }
142
307
 
143
308
  for (; startIndex < itemsLen; startIndex++) {
144
- currentItems[startIndex] = items[startIndex];
145
- if (disposables[startIndex]) {
146
- disposables[startIndex]();
309
+ const slot = getOrCreateSlot(startIndex);
310
+ slot.item = items[startIndex];
311
+ const emptyFlag = slot.isEmpty;
312
+
313
+ if (slot.disposer) {
314
+ if (emptyFlag.value === false) {
315
+ if (context) {
316
+ context.childrenWithContent--;
317
+ }
318
+ emptyFlag.value = true;
319
+ applyEmptyStateChange(startIndex, true, false);
320
+ }
321
+ slot.disposer();
147
322
  }
323
+
148
324
  const cleanupIndex = startIndex;
325
+
149
326
  mapped[startIndex * 2] = createCustomContext((cb) => {
150
327
  return root((disposer) => {
151
- disposables[cleanupIndex] = disposer;
328
+ const nestedContext = getContext()!;
329
+ const isEmptyFlag = nestedContext.isEmpty!;
330
+
331
+ slot.disposer = disposer;
152
332
  disposer();
333
+ effect((prev?: boolean) => {
334
+ const isEmpty = isEmptyFlag.value;
335
+ return untrack(() => {
336
+ if (slot.isEmpty.value !== isEmpty) {
337
+ slot.isEmpty.value = isEmpty;
338
+ }
339
+ const wasEmpty = prev ?? true;
340
+
341
+ applyEmptyStateChange(cleanupIndex, isEmpty, wasEmpty);
342
+
343
+ return isEmpty;
344
+ });
345
+ });
153
346
  cb(mapper(items[cleanupIndex], cleanupIndex));
154
347
  });
155
348
  });
156
349
 
157
- mapped[startIndex * 2 + 1] =
158
- startIndex < items.length - 1 ? options.joiner : ender;
350
+ ensureJoinerSlot(startIndex);
159
351
  }
160
352
 
161
353
  mapped.length = startIndex * 2;
162
- mapped[mapped.length - 1] = ender;
354
+ if (mapped.length > 0) {
355
+ if (ender !== undefined) {
356
+ enderMemo ??= memo(() =>
357
+ lastNonEmptyIndex.value === -1 ? undefined : ender,
358
+ );
359
+ mapped[mapped.length - 1] = enderMemo;
360
+ } else {
361
+ mapped[mapped.length - 1] = undefined;
362
+ }
363
+ }
163
364
  for (; startIndex < previousItemsLen; startIndex++) {
164
- disposables[startIndex]?.();
365
+ const slot = itemSlots[startIndex];
366
+ if (!slot) {
367
+ continue;
368
+ }
369
+
370
+ slot.disposer?.();
371
+ if (slot.isEmpty.value === false) {
372
+ if (context) {
373
+ context.childrenWithContent--;
374
+ }
375
+ slot.isEmpty.value = true;
376
+ applyEmptyStateChange(startIndex, true, false);
377
+ }
165
378
  }
166
379
 
167
- previousItemsLen = itemsLen;
380
+ if (previousItemsLen !== itemsLen) {
381
+ itemSlots.length = itemsLen;
382
+ recalculateNonEmptyBounds();
383
+ }
168
384
 
385
+ previousItemsLen = itemsLen;
169
386
  return mapped;
170
387
  });
171
388
  };
172
389
 
390
+ // Chooses the equality function based on the collection type in use.
173
391
  function getCompareFunction(itemsSource: Map<T, U> | T[] | Iterable<T>) {
174
392
  return Array.isArray(itemsSource) || isIterable(itemsSource) ?
175
393
  compareArray
176
394
  : compareMap;
177
395
  }
178
396
 
397
+ // Selects the mapper signature to match the collection type in use.
179
398
  function getMapperFunction(itemsSource: Map<T, U> | T[] | Iterable<T>) {
180
399
  return (
181
400
  Array.isArray(itemsSource) ||
@@ -185,22 +404,27 @@ export function mapJoin<T, U, V>(
185
404
  mapArray
186
405
  : mapMap;
187
406
  }
407
+ // Strict equality check for array-like collections.
188
408
  function compareArray(elem1: T, elem2: T) {
189
409
  return elem1 === elem2;
190
410
  }
191
411
 
412
+ // Equality check for map entries that includes key and value.
192
413
  function compareMap(record1: [T, U], record2: [T, U]) {
193
414
  return record1[0] === record2[0] && record1[1] === record2[1];
194
415
  }
195
416
 
417
+ // Invokes the user mapper for array-like collections.
196
418
  function mapArray(item: T, index: number) {
197
419
  return (cb as any)(item, index);
198
420
  }
199
421
 
422
+ // Invokes the user mapper for map collections.
200
423
  function mapMap(item: [T, U], index: number) {
201
424
  return cb(item[0], item[1], index);
202
425
  }
203
426
 
427
+ // Runtime type guard for iterables returned from custom sources.
204
428
  function isIterable<T>(x: unknown): x is Iterable<T> {
205
429
  return typeof (x as any).next === "function";
206
430
  }