@tanstack/marko-virtual 3.14.3 → 3.15.1

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/README.md CHANGED
@@ -1,8 +1,25 @@
1
+ <div align="center">
2
+ <picture>
3
+ <source
4
+ media="(prefers-color-scheme: dark)"
5
+ srcset="https://tanstack.com/api/readme/virtual.png?framework=marko&theme=dark"
6
+ />
7
+ <source
8
+ media="(prefers-color-scheme: light)"
9
+ srcset="https://tanstack.com/api/readme/virtual.png?framework=marko"
10
+ />
11
+ <img
12
+ src="https://tanstack.com/api/readme/virtual.png?framework=marko"
13
+ alt="TanStack Marko Virtual"
14
+ width="900"
15
+ />
16
+ </picture>
17
+ </div>
1
18
  # @tanstack/marko-virtual
2
19
 
3
20
  Headless UI for virtualizing scrollable elements in [Marko 6](https://markojs.com), built on top of [`@tanstack/virtual-core`](https://tanstack.com/virtual).
4
21
 
5
- Part of the [TanStack Virtual](https://tanstack.com/virtual) family.
22
+ Part of the [TanStack Virtual](https://tanstack.com/virtual) family. Full documentation: [tanstack.com/virtual — Marko](https://tanstack.com/virtual/latest/docs/framework/marko/marko-virtual).
6
23
 
7
24
  ## Installation
8
25
 
@@ -16,7 +33,7 @@ pnpm add @tanstack/marko-virtual
16
33
 
17
34
  ## Setup
18
35
 
19
- Add to your project's `marko.json` to make the tags available:
36
+ The tags are discovered automatically by the Marko compiler when the package is installed — no imports are needed in your `.marko` files. If your build setup does not pick them up (monorepo links, unusual resolution), add the taglib explicitly to your project's `marko.json`:
20
37
 
21
38
  ```json
22
39
  {
@@ -26,118 +43,233 @@ Add to your project's `marko.json` to make the tags available:
26
43
 
27
44
  ## Usage
28
45
 
46
+ Each tag is used **self-closing** and exposes a **tag variable** (`<virtualizer/v .../>`). You own the markup: read `v.virtualItems` and `v.totalSize` to render the visible rows. Declare the virtualizer **before** the markup that reads it, and pass `getScrollElement` as an **arrow thunk** (`() => scrollEl()`) so the element ref resolves at call time.
47
+
29
48
  ### `<virtualizer>` — scrollable container
30
49
 
31
50
  Use when you control the scroll element (a div with `overflow: auto`).
32
51
 
33
52
  ```marko
34
- <!DOCTYPE html>
35
- <html>
36
- <body>
37
-
38
- <let/mounted = false/>
39
- <script() { mounted = true }/>
40
-
41
- <div/scrollEl style="height: 400px; overflow-y: auto">
42
- <if=mounted>
43
- <virtualizer|{ virtualItems, totalSize }|
44
- count=10000
45
- estimateSize=() => 35
46
- getScrollElement=scrollEl
47
- >
48
- <div style=`height: ${totalSize}px; position: relative`>
49
- <for|item| of=virtualItems>
50
- <div
51
- data-index=item.index
52
- style=`position: absolute; transform: translateY(${item.start}px);
53
- height: ${item.size}px; width: 100%`
54
- >
55
- Row ${item.index}
56
- </div>
57
- </for>
53
+ <div/scrollEl
54
+ style="height: 400px; width: 400px; overflow-y: auto; position: relative;"
55
+ >
56
+ <virtualizer/v
57
+ count=10000
58
+ estimateSize=() => 35
59
+ getScrollElement=() => scrollEl()
60
+ />
61
+ <div style=`height: ${v.totalSize}px; width: 100%; position: relative`>
62
+ <for|item| of=v.virtualItems>
63
+ <div
64
+ style=`
65
+ position: absolute;
66
+ top: 0;
67
+ left: 0;
68
+ width: 100%;
69
+ height: ${item.size}px;
70
+ transform: translateY(${item.start}px);
71
+ `
72
+ >
73
+ Row ${item.index}
58
74
  </div>
59
- </virtualizer>
60
- </if>
75
+ </for>
76
+ </div>
61
77
  </div>
62
-
63
- </body>
64
- </html>
65
78
  ```
66
79
 
67
- > **Why `mounted`?** The virtualizer measures the scroll element's dimensions on mount. During SSR the DOM has no layout, so `offsetHeight` is 0 and no items would be calculated. The `<if=mounted>` guard ensures the virtualizer only renders after the browser has real dimensions available.
80
+ No mount guard is needed: the tag renders a deterministic initial state on the server (see [SSR](#ssr-server-rendered-slices)) and the live, observing instance takes over on mount.
81
+
82
+ For columns, set `horizontal=true` and swap the width/height and `translateX` accordingly. For grids, compose two `<virtualizer>` tags (rows + columns) sharing the same scroll element — each with its own tag variable and its own `() => scrollEl()` thunk.
68
83
 
69
84
  ### `<window-virtualizer>` — full-page scrolling
70
85
 
71
86
  Use when the page itself is the scroll container.
72
87
 
73
88
  ```marko
74
- <!DOCTYPE html>
75
- <html>
76
- <body>
89
+ <window-virtualizer/v
90
+ count=10000
91
+ estimateSize=() => 35
92
+ />
93
+ <div style=`height: ${v.totalSize}px; position: relative`>
94
+ <for|item| of=v.virtualItems>
95
+ <div
96
+ style=`
97
+ position: absolute;
98
+ top: 0;
99
+ left: 0;
100
+ width: 100%;
101
+ height: ${item.size}px;
102
+ transform: translateY(${item.start}px);
103
+ `
104
+ >
105
+ Row ${item.index}
106
+ </div>
107
+ </for>
108
+ </div>
109
+ ```
77
110
 
78
- <window-virtualizer|{ virtualItems, totalSize }| count=10000 estimateSize=() => 35>
79
- <div style=`height: ${totalSize}px; position: relative`>
80
- <for|item| of=virtualItems>
81
- <div
82
- data-index=item.index
83
- style=`position: absolute; transform: translateY(${item.start}px);
84
- height: ${item.size}px; width: 100%`
85
- >
86
- Row ${item.index}
87
- </div>
88
- </for>
89
- </div>
90
- </window-virtualizer>
111
+ If content sits above the list (it almost always does on a real page), measure the list's `offsetTop` on mount and pass it as `scrollMargin`, then subtract it from `item.start` when positioning — see the `window` example.
112
+
113
+ ## SSR: server-rendered slices
114
+
115
+ Server rendering is **opt-in per virtualizer** via `initialRect` (a viewport size hint). When set, the server paints the initial visible rows directly into the HTML; without it the server renders an empty container and the client fills it on mount — byte-for-byte identical to a client-only build. Pair with `initialOffset` (a number) to server-render at a scroll position (deep links, restore):
91
116
 
92
- </body>
93
- </html>
117
+ ```marko
118
+ <virtualizer/v
119
+ count=10000
120
+ estimateSize=() => 35
121
+ getScrollElement=() => scrollEl()
122
+ initialRect=({ width: 800, height: 400 })
123
+ initialOffset=3500
124
+ />
94
125
  ```
95
126
 
96
- ## Tag parameters
127
+ Nothing live is serialized — the slice is computed as plain data at render time and recomputes identically on resume. See the five `ssr*` examples for the full patterns, including measurement-cache restore.
128
+
129
+ ### Streaming SSR: the tag inside `<await>`
97
130
 
98
- Both tags use Marko 6's tag parameters pattern. The body receives virtual state via `|{ ... }|` destructuring:
131
+ The tags compose with Marko's streaming out of the box put a virtualizer inside an
132
+ `<await>` and the whole pattern above still holds, per streamed chunk:
99
133
 
100
134
  ```marko
101
- <virtualizer|{ virtualItems, totalSize, measureElement, scrollToIndex, scrollToOffset }|
102
- count=...
103
- getScrollElement=...
104
- >
105
- <!-- virtualItems, totalSize etc are in scope here -->
106
- </virtualizer>
135
+ <try>
136
+ <@placeholder>
137
+ <p>Loading people…</p>
138
+ </@placeholder>
139
+ <@catch|err|>
140
+ <p>Failed to load: ${(err as Error).message}</p>
141
+ </@catch>
142
+
143
+ <await|people| value=fetchPeople()>
144
+ <div/scrollEl class="scroll-container">
145
+ <virtualizer/v
146
+ count=people.length
147
+ estimateSize=() => 48
148
+ getScrollElement=() => scrollEl()
149
+ initialRect=({ width: 800, height: 400 })
150
+ />
151
+ <!-- sizer + rows exactly as usual -->
152
+ </div>
153
+ </await>
154
+ </try>
107
155
  ```
108
156
 
109
- ## API Reference
157
+ What happens on the wire: the server flushes the page shell (with the placeholder)
158
+ **immediately**, keeps streaming, and when `fetchPeople()` resolves it flushes the
159
+ awaited subtree as a **later chunk** — including the server-painted rows when
160
+ `initialRect` is set. Each streamed chunk **resumes independently** on the client:
161
+ the live virtualizer instance for this list mounts when its chunk arrives, without
162
+ waiting for the rest of the page. Nothing extra to configure — the seed is computed
163
+ inside the awaited subtree from the resolved data, so server and resumed client
164
+ agree by construction.
110
165
 
111
- ### `<virtualizer>`
166
+ Two things to know:
112
167
 
113
- | Attribute | Type | Required | Description |
114
- | -------------------- | --------------------------- | -------- | --------------------------------------------------- |
115
- | `count` | `number` | ✅ | Total number of items |
116
- | `getScrollElement` | `() => Element \| null` | ✅ | Returns the scroll container |
117
- | `estimateSize` | `(index: number) => number` | | Estimated item size in px (default: `50`) |
118
- | `overscan` | `number` | | Items to render outside the viewport (default: `5`) |
119
- | `horizontal` | `boolean` | | Enable horizontal scrolling (default: `false`) |
120
- | `paddingStart` | `number` | | Padding before the first item in px |
121
- | `paddingEnd` | `number` | | Padding after the last item in px |
122
- | `scrollPaddingStart` | `number` | | Scroll padding at the start |
123
- | `scrollPaddingEnd` | `number` | | Scroll padding at the end |
124
- | `gap` | `number` | | Gap between items in px |
125
- | `lanes` | `number` | | Number of lanes for grid layouts |
126
- | `initialOffset` | `number \| (() => number)` | | Initial scroll offset |
168
+ - Marko streams awaited content **out of order**: the chunk arrives at the end of
169
+ the byte stream and an inline script swaps it into place. With JavaScript
170
+ disabled that swap never runs, so an `<await>`-wrapped list renders blank without
171
+ JS even though the row HTML is present in the source. If no-JS visibility
172
+ matters, fetch **before** render (await the data in the route) instead of
173
+ streaming.
174
+ - The `ssr-fetch` example shows the streamed pattern with client-rendered rows; the
175
+ `ssr-slice` example shows it with **server-painted** rows (`initialRect`) — its
176
+ test suite includes a wire-level assertion that the placeholder flushes first and
177
+ the painted rows arrive in a later chunk.
127
178
 
128
- **Tag parameters provided to body:**
179
+ ## Dynamic / variable item sizes
129
180
 
130
- | Parameter | Type | Description |
131
- | ---------------- | ----------------------------------------------------- | -------------------------------------------------------- |
132
- | `virtualItems` | `VirtualItem[]` | Items to render, with `index`, `start`, `size`, `key` |
133
- | `totalSize` | `number` | Total scrollable size in px — set on the inner container |
134
- | `measureElement` | `(el: Element \| null) => void` | Pass to `ref` for dynamic size measurement |
135
- | `scrollToIndex` | `(index: number, options?: ScrollToOptions) => void` | Scroll to an item by index |
136
- | `scrollToOffset` | `(offset: number, options?: ScrollToOptions) => void` | Scroll to a px offset |
181
+ For items with unknown heights, drive `measureElement` from a per-row `<script>`:
137
182
 
138
- ### `<window-virtualizer>`
183
+ ```marko
184
+ <div/scrollEl style="height: 400px; overflow-y: auto">
185
+ <virtualizer/v
186
+ count=data.length
187
+ estimateSize=() => 50
188
+ getScrollElement=() => scrollEl()
189
+ />
190
+ <div style=`height: ${v.totalSize}px; position: relative`>
191
+ <for|item| of=v.virtualItems>
192
+ <div/el
193
+ data-index=item.index
194
+ style=`position: absolute; top: 0; width: 100%; transform: translateY(${item.start}px)`>
195
+ <script() {
196
+ // re-run when the item changes; measureElement reads the rendered
197
+ // height and feeds it back to the virtualizer
198
+ const _key = item.key
199
+ if (el() && v.measureElement) v.measureElement(el())
200
+ }/>
201
+ ${data[item.index].text}
202
+ </div>
203
+ </for>
204
+ </div>
205
+ </div>
206
+ ```
139
207
 
140
- Same as `<virtualizer>` except there is no `getScrollElement`, `horizontal`, or `initialOffset` the window is always the scroll container.
208
+ > `data-index` is required on measured elements — the virtualizer uses it to map measurements back to items. Measuring under a different attribute name requires passing that name as `indexAttribute`.
209
+
210
+ ## Tag variable reference
211
+
212
+ Both tags expose the same shape. Capture it with `<virtualizer/v .../>` and read `v.property`:
213
+
214
+ | Property | Type | Description |
215
+ | -------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
216
+ | `virtualItems` | `VirtualItem[]` | The currently visible virtual items (`index`, `start`, `size`, `key`, `lane`) |
217
+ | `totalSize` | `number` | Total scrollable size in px — set as the inner container's `height` (or `width` for columns). Margin-free: `scrollMargin` is already subtracted |
218
+ | `range` | `{ startIndex: number; endIndex: number } \| null` | The visible index window (excludes overscan). `null` until there is a window |
219
+ | `measureElement` | `(el: Element \| null) => void` | Ref callback for dynamic item sizing |
220
+ | `scrollToIndex` | `(index: number, options?: ScrollToOptions) => void` | Scroll to an item by index. Default `align: 'auto'` scrolls the minimum |
221
+ | `scrollToOffset` | `(offset: number, options?: ScrollToOptions) => void` | Scroll to a pixel offset |
222
+ | `measure` | `() => void` | Drop all measured sizes and re-measure everything (after a width/font change) |
223
+ | `resizeItem` | `(index: number, size: number) => void` | Set one item's size directly, without a DOM measure |
224
+ | `scrollToEnd` | `(options?: { behavior?: ScrollBehavior }) => void` | Scroll to the very end of the list |
225
+ | `isAtEnd` | `(threshold?: number) => boolean` | Whether the scroll position is at (or within `threshold` px of) the end. `false` before mount |
226
+ | `getDistanceFromEnd` | `() => number` | Pixels between the current scroll position and the end. `Infinity` before mount |
227
+
228
+ > Inside your **own** `onScroll` handler, compute end-proximity from the element (`el.scrollHeight - el.scrollTop - el.clientHeight`) rather than calling `v.isAtEnd()` — your handler runs before the virtualizer's listener during the same event, so the virtualizer's numbers are one event stale there. Everywhere else they are current.
229
+
230
+ ## `<virtualizer>` input reference
231
+
232
+ | Prop | Type | Default | Description |
233
+ | ------------------------------------- | ----------------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
234
+ | `count` | `number` | required | Number of items |
235
+ | `getScrollElement` | `() => Element \| null` | required | Returns the scroll container |
236
+ | `estimateSize` | `(index: number) => number` | `() => 50` | Estimated item size in px |
237
+ | `overscan` | `number` | `5` | Items to render beyond the visible area |
238
+ | `horizontal` | `boolean` | `false` | Virtualise horizontally (columns) |
239
+ | `paddingStart` | `number` | — | Padding before first item |
240
+ | `paddingEnd` | `number` | — | Padding after last item |
241
+ | `scrollPaddingStart` | `number` | — | Scroll padding for `scrollToIndex` |
242
+ | `scrollPaddingEnd` | `number` | — | Scroll padding for `scrollToIndex` |
243
+ | `gap` | `number` | — | Gap between items in px |
244
+ | `lanes` | `number` | `1` | Lanes for masonry layouts |
245
+ | `initialOffset` | `number \| (() => number)` | — | Scroll offset (px) for the server slice — server-render at a scroll position (deep link / restore) |
246
+ | `initialRect` | `{ width: number; height: number }` | — | Viewport hint for a server-rendered slice (SSR). When set, the server paints the initial visible rows; omit for client-only fill |
247
+ | `getItemKey` | `(index: number) => number \| string \| bigint` | the index | Stable per-item identity so cached measurements survive reorder |
248
+ | `rangeExtractor` | `(range: Range) => number[]` | `defaultRangeExtractor` | Hook over the visible range: force extra indexes (e.g. a pinned sticky header) into the rendered window. Compose with `defaultRangeExtractor` from `@tanstack/virtual-core` |
249
+ | `indexAttribute` | `string` | `'data-index'` | DOM attribute carrying the item index for `measureElement`. Give two instances measuring the same element (a grid cell) distinct attributes |
250
+ | `initialMeasurementsCache` | `VirtualItem[]` | — | Pre-measured items (plain data) to seed the measurement cache |
251
+ | `anchorTo` | `'start' \| 'end'` | `'start'` | Anchor the window to the list end (a chat pinned to newest). Client-behavioral: it does not position the server slice — use `initialOffset` for that |
252
+ | `followOnAppend` | `boolean \| ScrollBehavior` | `false` | With `anchorTo="end"`: stay pinned to the end as items append |
253
+ | `scrollEndThreshold` | `number` | `1` | How close (px) to the end still counts as "at the end" |
254
+ | `scrollMargin` | `number` | `0` | The list's offset (px) from the top of its scroll area, when other content sits above it in the same scroller. `item.start` values then INCLUDE this margin — subtract it when positioning items relative to the list (see the window example) |
255
+ | `enabled` | `boolean` | `true` | Disable switch. `false` is not a freeze: the virtualizer unobserves, clears its measurements, and renders an empty window until re-enabled |
256
+ | `isRtl` | `boolean` | `false` | Right-to-left horizontal lists |
257
+ | `isScrollingResetDelay` | `number` | `150` | ms after the last scroll event before "user is scrolling" ends |
258
+ | `useScrollendEvent` | `boolean` | `false` | Use the native `scrollend` event instead of the `isScrollingResetDelay` timer |
259
+ | `useAnimationFrameWithResizeObserver` | `boolean` | `false` | Batch ResizeObserver measurements into animation frames (avoids "ResizeObserver loop" console errors under heavy resize load) |
260
+ | `laneAssignmentMode` | `'estimate' \| 'measured'` | `'estimate'` | Masonry/multi-lane: assign items to lanes by estimated or measured sizes |
261
+ | `useCachedMeasurements` | `boolean` | `false` | Make the default measurer return the cached (or estimated) size instead of reading the DOM — freezes item sizes when they are already known |
262
+ | `debug` | `boolean` | `false` | Verbose engine logging |
263
+ | `measureElement` | `(element, entry, instance) => number` | border-box measurer | Replace HOW an item's size is read from its element (e.g. include margins, or measure a child) |
264
+
265
+ ## `<window-virtualizer>` input reference
266
+
267
+ Same as `<virtualizer>` except `getScrollElement` is not accepted — the scroll element is always `window`. Two notes:
268
+
269
+ - `horizontal` is accepted (the page scrolls sideways) and defaults to `false`.
270
+ - `initialOffset` defaults to the live window scroll position (`window.scrollY`, or `window.scrollX` when `horizontal`) on the client and `0` on the server. Pass a number to override it — e.g. to server-render a slice at a scroll position, paired with `initialRect`.
271
+
272
+ `scrollMargin` is the signature window-virtualizer option: pass the list's offset from the top of the document and subtract it from `item.start` when positioning.
141
273
 
142
274
  ## Examples
143
275
 
@@ -147,46 +279,81 @@ All examples use `@marko/run`. Run any with:
147
279
  pnpm --filter tanstack-marko-virtual-example-<name> dev
148
280
  ```
149
281
 
150
- | Example | Description |
151
- | ----------------- | ----------------------------------------------- |
152
- | `fixed` | Fixed-size rows, columns, and grid |
153
- | `variable` | Variable sizes via `estimateSize` |
154
- | `dynamic` | Unknown sizes measured via `measureElement` |
155
- | `grid` | Two virtualizers sharing one scroll element |
156
- | `smooth-scroll` | `scrollToIndex` with CSS smooth scrolling |
157
- | `infinite-scroll` | Lazy data loading with a fixed total count |
158
- | `window` | Full-page scrolling with `<window-virtualizer>` |
282
+ | Example | Description |
283
+ | ------------------ | ----------------------------------------------------------------------------------------------------------- |
284
+ | `fixed` | Fixed-size rows, columns, and grid |
285
+ | `variable` | Variable sizes via `estimateSize` |
286
+ | `dynamic` | Unknown sizes measured via `measureElement` |
287
+ | `grid` | Two virtualizers sharing one scroll element |
288
+ | `pretext` | Calculated text heights via `@chenglou/pretext` (no estimate error) |
289
+ | `padding` | `paddingStart` / `paddingEnd` |
290
+ | `scroll-padding` | `scrollPaddingStart` with `scrollToIndex` |
291
+ | `sticky` | Sticky group headers via `rangeExtractor` |
292
+ | `infinite-scroll` | Lazy data loading with a fixed total count |
293
+ | `chat` | End-anchored messaging (`anchorTo="end"`, `followOnAppend`), history prepends, a real server-streamed reply |
294
+ | `chat-pretext` | Chat rebuilt on calculated heights: zero-correction prepends, streamed reply growing via `resizeItem` |
295
+ | `smooth-scroll` | `scrollToIndex` with smooth behavior |
296
+ | `table` | Virtualized table rows |
297
+ | `window` | Full-page scrolling with `scrollMargin` measured on mount |
298
+ | `ssr` | SSR: no fetch, rows render on client |
299
+ | `ssr-fetch` | SSR: fetch on server, rows render on client |
300
+ | `ssr-slice` | SSR: fetch on server, rows render on server (`initialRect`) |
301
+ | `ssr-restore` | SSR: server slice at an offset + measurement-cache restore |
302
+ | `window-ssr-slice` | SSR window: rows render on server |
159
303
 
160
- ## Dynamic sizing
304
+ ## TypeScript
161
305
 
162
- Use `measureElement` to measure items whose size isn't known upfront:
306
+ The tags are fully typed. Each tag ships a committed `index.d.marko` (a types-only twin, like a `.d.ts` for a Marko template) that editors and [`@marko/type-check`](https://github.com/marko-js/language-server/tree/main/packages/type-check) prefer over the implementation file. Both tags also export a named handle interface — `VirtualizerHandle` / `WindowVirtualizerHandle` — describing the tag-variable shape, which you can use to type your own variables and functions.
163
307
 
164
- ```marko
165
- <virtualizer|{ virtualItems, totalSize, measureElement }|
166
- count=items.length
167
- estimateSize=() => 50
168
- getScrollElement=scrollEl
169
- >
170
- <div style=`height: ${totalSize}px; position: relative`>
171
- <for|item| of=virtualItems>
172
- <div
173
- ref=measureElement
174
- data-index=item.index
175
- style=`position: absolute; transform: translateY(${item.start}px); width: 100%`
176
- >
177
- ${items[item.index]}
178
- </div>
179
- </for>
180
- </div>
181
- </virtualizer>
308
+ ### Verification (CI)
309
+
310
+ `pnpm test:types` runs `mtc` (marko-type-check) over the package: the `.ts` sources, the `.marko` tags and test fixtures, the tests, and the committed `.d.marko` files. This is what CI runs — CI only ever **verifies**, it never generates. If a tag's surface changes and the `.d.marko` files were not regenerated, this check fails instead of letting the types drift silently. The project file is `tsconfig.typecheck.json`.
311
+
312
+ ### Generating `.d.marko` (manual, on demand)
313
+
314
+ The `.d.marko` files are **generated, committed artifacts** — regenerate them whenever a tag's `Input` or return surface changes:
315
+
316
+ ```bash
317
+ pnpm types:generate
182
318
  ```
183
319
 
184
- > `data-index` is required on measured elementsthe virtualizer uses it to map measurements back to items.
320
+ The script (`scripts/generate-d-marko.mjs`) verifies the sources type-check, deletes the old `.d.marko` (mtc prefers them as input when present), emits fresh ones via `tsconfig.emit.json`, copies them back beside the sources, and re-verifies. Review the diff and commit. Two notes: `TS6059` rootDir warnings during the emit step are a known benign artifact of the cross-package `virtual-core` paths mapping (the script accounts for them), and generation relies on the tags' **named** return interfaces mtc's emitter truncates wide anonymous inline types, so keep returns cast to the exported handle types.
185
321
 
186
- ## TypeScript
322
+ ### Running all e2e Tests
323
+
324
+ To run all the e2e tests under each of `examples/marko/<example-name>/e2e` use the command
325
+
326
+ ```bash
327
+ pnpm -r --workspace-concurrency=1 --filter "./examples/marko/*" run test:e2e
328
+ ```
329
+
330
+ _Note:_ This has to be run sequentially as they all start the same port.
331
+
332
+ _Note:_ All suites run against **dev servers**; production builds are not exercised by
333
+ any test. After bumping `marko` (or other build-path dependencies) or changing the
334
+ package's build/packaging config, sanity-check production builds manually:
335
+ `pnpm --filter <example> build` then `preview` — and check **both** kinds of example:
336
+ `fixed` (render-only — rows must appear; this is the case production tree-shaking can
337
+ silently break) and `chat` (interactive — click through Stream reply).
338
+
339
+ ### Running all unit tests
340
+
341
+ To run all the unit tests specifically for `marko-virtual/tests` use the command
342
+
343
+ ```bash
344
+ pnpm --filter @tanstack/marko-virtual exec vitest run --reporter=verbose
345
+ ```
346
+
347
+ ### Pre-requisite before running tests
348
+
349
+ ```bash
350
+ pnpm install
351
+ pnpm --filter @tanstack/virtual-core build
352
+ pnpm --filter @tanstack/marko-virtual build
353
+ ```
187
354
 
188
- The tags are fully typed. The Marko language server reads the source `.marko` files directly — no `.d.ts` generation is needed. Ensure `@marko/language-tools` is installed in your editor for IDE support.
355
+ ## Author & license
189
356
 
190
- ## License
357
+ Authored and maintained as part of [TanStack Virtual](https://tanstack.com/virtual) by [Tanner Linsley](https://github.com/tannerlinsley) and contributors; Marko adapter contributed by `defunkt-dev` (`@DSz340`). If this library helps you, consider [sponsoring TanStack](https://github.com/sponsors/tannerlinsley).
191
358
 
192
- MIT © [Tanner Linsley](https://github.com/tannerlinsley)
359
+ [MIT](https://github.com/TanStack/virtual/blob/main/LICENSE) © Tanner Linsley
@@ -1,4 +1,4 @@
1
- import { VirtualizerOptions } from '@tanstack/virtual-core';
1
+ import { Virtualizer, Range, VirtualItem, VirtualizerOptions } from '@tanstack/virtual-core';
2
2
  export interface VirtualizerInput {
3
3
  count: number;
4
4
  estimateSize?: (index: number) => number;
@@ -12,5 +12,34 @@ export interface VirtualizerInput {
12
12
  gap?: number;
13
13
  lanes?: number;
14
14
  initialOffset?: number | (() => number);
15
+ initialRect?: {
16
+ width: number;
17
+ height: number;
18
+ };
19
+ getItemKey?: (index: number) => number | string | bigint;
20
+ rangeExtractor?: (range: Range) => Array<number>;
21
+ indexAttribute?: string;
22
+ initialMeasurementsCache?: Array<VirtualItem>;
23
+ anchorTo?: 'start' | 'end';
24
+ followOnAppend?: boolean | ScrollBehavior;
25
+ scrollEndThreshold?: number;
26
+ scrollMargin?: number;
27
+ enabled?: boolean;
28
+ isRtl?: boolean;
29
+ isScrollingResetDelay?: number;
30
+ useScrollendEvent?: boolean;
31
+ useAnimationFrameWithResizeObserver?: boolean;
32
+ laneAssignmentMode?: 'estimate' | 'measured';
33
+ useCachedMeasurements?: boolean;
34
+ debug?: boolean;
35
+ measureElement?: (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer<Element, Element>) => number;
15
36
  }
16
37
  export declare function buildOptions(input: VirtualizerInput, notify: () => void): VirtualizerOptions<Element, Element>;
38
+ export declare function renderSlice(input: VirtualizerInput): {
39
+ items: Array<VirtualItem>;
40
+ size: number;
41
+ range: {
42
+ startIndex: number;
43
+ endIndex: number;
44
+ } | null;
45
+ };
@@ -1,13 +1,44 @@
1
- import { VirtualizerOptions } from '@tanstack/virtual-core';
1
+ import { Virtualizer, Range, VirtualItem, VirtualizerOptions } from '@tanstack/virtual-core';
2
2
  export interface WindowVirtualizerInput {
3
3
  count: number;
4
4
  estimateSize?: (index: number) => number;
5
5
  overscan?: number;
6
+ horizontal?: boolean;
6
7
  paddingStart?: number;
7
8
  paddingEnd?: number;
8
9
  scrollPaddingStart?: number;
9
10
  scrollPaddingEnd?: number;
10
11
  gap?: number;
11
12
  lanes?: number;
13
+ initialOffset?: number | (() => number);
14
+ initialRect?: {
15
+ width: number;
16
+ height: number;
17
+ };
18
+ getItemKey?: (index: number) => number | string | bigint;
19
+ rangeExtractor?: (range: Range) => Array<number>;
20
+ indexAttribute?: string;
21
+ initialMeasurementsCache?: Array<VirtualItem>;
22
+ anchorTo?: 'start' | 'end';
23
+ followOnAppend?: boolean | ScrollBehavior;
24
+ scrollEndThreshold?: number;
25
+ scrollMargin?: number;
26
+ enabled?: boolean;
27
+ isRtl?: boolean;
28
+ isScrollingResetDelay?: number;
29
+ useScrollendEvent?: boolean;
30
+ useAnimationFrameWithResizeObserver?: boolean;
31
+ laneAssignmentMode?: 'estimate' | 'measured';
32
+ useCachedMeasurements?: boolean;
33
+ debug?: boolean;
34
+ measureElement?: (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer<Window, Element>) => number;
12
35
  }
13
36
  export declare function buildOptions(input: WindowVirtualizerInput, notify: () => void): VirtualizerOptions<Window, Element>;
37
+ export declare function renderSlice(input: WindowVirtualizerInput): {
38
+ items: Array<VirtualItem>;
39
+ size: number;
40
+ range: {
41
+ startIndex: number;
42
+ endIndex: number;
43
+ } | null;
44
+ };
@@ -1,4 +1,4 @@
1
- import { VirtualizerOptions } from '@tanstack/virtual-core';
1
+ import { Virtualizer, Range, VirtualItem, VirtualizerOptions } from '@tanstack/virtual-core';
2
2
  export interface VirtualizerInput {
3
3
  count: number;
4
4
  estimateSize?: (index: number) => number;
@@ -12,5 +12,34 @@ export interface VirtualizerInput {
12
12
  gap?: number;
13
13
  lanes?: number;
14
14
  initialOffset?: number | (() => number);
15
+ initialRect?: {
16
+ width: number;
17
+ height: number;
18
+ };
19
+ getItemKey?: (index: number) => number | string | bigint;
20
+ rangeExtractor?: (range: Range) => Array<number>;
21
+ indexAttribute?: string;
22
+ initialMeasurementsCache?: Array<VirtualItem>;
23
+ anchorTo?: 'start' | 'end';
24
+ followOnAppend?: boolean | ScrollBehavior;
25
+ scrollEndThreshold?: number;
26
+ scrollMargin?: number;
27
+ enabled?: boolean;
28
+ isRtl?: boolean;
29
+ isScrollingResetDelay?: number;
30
+ useScrollendEvent?: boolean;
31
+ useAnimationFrameWithResizeObserver?: boolean;
32
+ laneAssignmentMode?: 'estimate' | 'measured';
33
+ useCachedMeasurements?: boolean;
34
+ debug?: boolean;
35
+ measureElement?: (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer<Element, Element>) => number;
15
36
  }
16
37
  export declare function buildOptions(input: VirtualizerInput, notify: () => void): VirtualizerOptions<Element, Element>;
38
+ export declare function renderSlice(input: VirtualizerInput): {
39
+ items: Array<VirtualItem>;
40
+ size: number;
41
+ range: {
42
+ startIndex: number;
43
+ endIndex: number;
44
+ } | null;
45
+ };