From 9d65e32e9634258e587b936bbf5978cbe39d106c Mon Sep 17 00:00:00 2001 From: defunkt-dev Date: Sun, 5 Jul 2026 17:11:39 -0600 Subject: [PATCH 1/6] feat(marko-virtual): option parity, chat-pretext example, e2e suites, type-strict examples --- .changeset/marko-virtual-followups.md | 15 + .gitignore | 1 + docs/config.json | 85 +++- docs/framework/marko/marko-virtual.md | 443 ++++++++++++------ docs/installation.md | 6 + .../marko/chat-pretext/.marko-run/routes.d.ts | 53 +++ examples/marko/chat-pretext/e2e/README.md | 17 + .../chat-pretext/e2e/chat-pretext.spec.ts | 254 ++++++++++ examples/marko/chat-pretext/marko.json | 1 + examples/marko/chat-pretext/package.json | 22 + .../marko/chat-pretext/playwright.config.ts | 23 + .../marko/chat-pretext/src/routes/+page.marko | 309 ++++++++++++ .../src/routes/api/reply/+handler.ts | 38 ++ examples/marko/chat-pretext/tsconfig.json | 24 + examples/marko/chat-pretext/vite.config.ts | 6 + examples/marko/chat/.marko-run/routes.d.ts | 53 +++ examples/marko/chat/e2e/README.md | 14 + examples/marko/chat/e2e/chat.spec.ts | 189 ++++++++ examples/marko/chat/marko.json | 1 + examples/marko/chat/package.json | 21 + examples/marko/chat/playwright.config.ts | 23 + examples/marko/chat/src/routes/+page.marko | 222 +++++++++ .../chat/src/routes/api/reply/+handler.ts | 38 ++ examples/marko/chat/tsconfig.json | 24 + examples/marko/chat/vite.config.ts | 8 + examples/marko/dynamic/.marko-run/routes.d.ts | 34 ++ examples/marko/dynamic/e2e/dynamic.spec.ts | 76 +++ examples/marko/dynamic/package.json | 13 +- examples/marko/dynamic/playwright.config.ts | 23 + examples/marko/dynamic/src/routes/+page.marko | 46 +- examples/marko/dynamic/tsconfig.json | 24 + examples/marko/fixed/.marko-run/routes.d.ts | 34 ++ examples/marko/fixed/e2e/fixed.spec.ts | 76 +++ examples/marko/fixed/marko.json | 1 + examples/marko/fixed/package.json | 13 +- examples/marko/fixed/playwright.config.ts | 23 + examples/marko/fixed/src/routes/+page.marko | 115 ++--- examples/marko/fixed/tsconfig.json | 24 + examples/marko/grid/.marko-run/routes.d.ts | 34 ++ examples/marko/grid/e2e/grid.spec.ts | 45 ++ examples/marko/grid/package.json | 13 +- examples/marko/grid/playwright.config.ts | 23 + examples/marko/grid/src/routes/+page.marko | 51 +- examples/marko/grid/tsconfig.json | 24 + .../infinite-scroll/.marko-run/routes.d.ts | 34 ++ .../e2e/infinite-scroll.spec.ts | 67 +++ examples/marko/infinite-scroll/package.json | 13 +- .../infinite-scroll/playwright.config.ts | 23 + .../infinite-scroll/src/routes/+page.marko | 77 ++- examples/marko/infinite-scroll/tsconfig.json | 24 + examples/marko/padding/.marko-run/routes.d.ts | 34 ++ examples/marko/padding/e2e/padding.spec.ts | 104 ++++ examples/marko/padding/marko.json | 1 + examples/marko/padding/package.json | 21 + examples/marko/padding/playwright.config.ts | 23 + examples/marko/padding/src/routes/+page.marko | 173 +++++++ examples/marko/padding/tsconfig.json | 24 + examples/marko/padding/vite.config.ts | 8 + examples/marko/pretext/.marko-run/routes.d.ts | 34 ++ examples/marko/pretext/e2e/pretext.spec.ts | 91 ++++ examples/marko/pretext/marko.json | 1 + examples/marko/pretext/package.json | 22 + examples/marko/pretext/playwright.config.ts | 23 + examples/marko/pretext/src/routes/+page.marko | 197 ++++++++ examples/marko/pretext/tsconfig.json | 24 + examples/marko/pretext/vite.config.ts | 8 + .../scroll-padding/.marko-run/routes.d.ts | 34 ++ .../scroll-padding/e2e/scroll-padding.spec.ts | 68 +++ examples/marko/scroll-padding/marko.json | 1 + examples/marko/scroll-padding/package.json | 21 + .../marko/scroll-padding/playwright.config.ts | 23 + .../scroll-padding/src/routes/+page.marko | 98 ++++ examples/marko/scroll-padding/tsconfig.json | 24 + examples/marko/scroll-padding/vite.config.ts | 8 + .../smooth-scroll/.marko-run/routes.d.ts | 34 ++ .../smooth-scroll/e2e/smooth-scroll.spec.ts | 47 ++ examples/marko/smooth-scroll/package.json | 13 +- .../marko/smooth-scroll/playwright.config.ts | 23 + .../smooth-scroll/src/routes/+page.marko | 44 +- examples/marko/smooth-scroll/tsconfig.json | 24 + .../marko/ssr-fetch/.marko-run/routes.d.ts | 34 ++ .../marko/ssr-fetch/e2e/ssr-fetch.spec.ts | 71 +++ examples/marko/ssr-fetch/marko.json | 1 + examples/marko/ssr-fetch/package.json | 21 + examples/marko/ssr-fetch/playwright.config.ts | 23 + examples/marko/ssr-fetch/src/data.ts | 21 + .../marko/ssr-fetch/src/routes/+page.marko | 69 +++ examples/marko/ssr-fetch/tsconfig.json | 24 + examples/marko/ssr-fetch/vite.config.ts | 8 + .../marko/ssr-restore/.marko-run/routes.d.ts | 34 ++ .../marko/ssr-restore/e2e/ssr-restore.spec.ts | 90 ++++ examples/marko/ssr-restore/marko.json | 1 + examples/marko/ssr-restore/package.json | 21 + .../marko/ssr-restore/playwright.config.ts | 23 + examples/marko/ssr-restore/src/data.ts | 21 + .../marko/ssr-restore/src/routes/+page.marko | 84 ++++ examples/marko/ssr-restore/tsconfig.json | 24 + examples/marko/ssr-restore/vite.config.ts | 8 + .../marko/ssr-slice/.marko-run/routes.d.ts | 34 ++ .../marko/ssr-slice/e2e/ssr-slice.spec.ts | 130 +++++ examples/marko/ssr-slice/marko.json | 1 + examples/marko/ssr-slice/package.json | 21 + examples/marko/ssr-slice/playwright.config.ts | 23 + examples/marko/ssr-slice/src/data.ts | 21 + .../marko/ssr-slice/src/routes/+page.marko | 70 +++ examples/marko/ssr-slice/tsconfig.json | 24 + examples/marko/ssr-slice/vite.config.ts | 8 + examples/marko/ssr/.marko-run/routes.d.ts | 34 ++ examples/marko/ssr/e2e/ssr.spec.ts | 68 +++ examples/marko/ssr/marko.json | 1 + examples/marko/ssr/package.json | 21 + examples/marko/ssr/playwright.config.ts | 23 + examples/marko/ssr/src/routes/+page.marko | 46 ++ examples/marko/ssr/tsconfig.json | 24 + examples/marko/ssr/vite.config.ts | 8 + examples/marko/sticky/.marko-run/routes.d.ts | 34 ++ examples/marko/sticky/e2e/sticky.spec.ts | 85 ++++ examples/marko/sticky/marko.json | 1 + examples/marko/sticky/package.json | 21 + examples/marko/sticky/playwright.config.ts | 23 + examples/marko/sticky/src/routes/+page.marko | 104 ++++ examples/marko/sticky/tsconfig.json | 24 + examples/marko/sticky/vite.config.ts | 8 + examples/marko/table/.marko-run/routes.d.ts | 34 ++ examples/marko/table/e2e/table.spec.ts | 130 +++++ examples/marko/table/marko.json | 1 + examples/marko/table/package.json | 21 + examples/marko/table/playwright.config.ts | 23 + examples/marko/table/src/routes/+page.marko | 164 +++++++ examples/marko/table/tsconfig.json | 24 + examples/marko/table/vite.config.ts | 8 + .../marko/variable/.marko-run/routes.d.ts | 34 ++ examples/marko/variable/e2e/variable.spec.ts | 70 +++ examples/marko/variable/package.json | 13 +- examples/marko/variable/playwright.config.ts | 23 + .../marko/variable/src/routes/+page.marko | 100 ++-- examples/marko/variable/tsconfig.json | 24 + .../window-ssr-slice/.marko-run/routes.d.ts | 34 ++ .../e2e/window-ssr-slice.spec.ts | 76 +++ examples/marko/window-ssr-slice/marko.json | 1 + examples/marko/window-ssr-slice/package.json | 21 + .../window-ssr-slice/playwright.config.ts | 23 + examples/marko/window-ssr-slice/src/data.ts | 21 + .../window-ssr-slice/src/routes/+page.marko | 66 +++ examples/marko/window-ssr-slice/tsconfig.json | 24 + .../marko/window-ssr-slice/vite.config.ts | 8 + examples/marko/window/.marko-run/routes.d.ts | 34 ++ examples/marko/window/e2e/window.spec.ts | 66 +++ examples/marko/window/package.json | 13 +- examples/marko/window/playwright.config.ts | 23 + examples/marko/window/src/routes/+page.marko | 56 ++- examples/marko/window/tsconfig.json | 24 + packages/marko-virtual/README.md | 381 ++++++++++----- .../e2e/option-gates/.marko-run/routes.d.ts | 243 ++++++++++ .../marko-virtual/e2e/option-gates/README.md | 40 ++ .../e2e/option-gates/e2e/option-gates.spec.ts | 309 ++++++++++++ .../marko-virtual/e2e/option-gates/marko.json | 1 + .../e2e/option-gates/package.json | 18 + .../e2e/option-gates/playwright.config.ts | 21 + .../e2e/option-gates/src/routes/+page.marko | 8 + .../src/routes/cached/+page.marko | 64 +++ .../option-gates/src/routes/debug/+page.marko | 27 ++ .../src/routes/enabled/+page.marko | 29 ++ .../src/routes/lanes-mode/+page.marko | 36 ++ .../src/routes/measure-element/+page.marko | 34 ++ .../option-gates/src/routes/rtl/+page.marko | 29 ++ .../src/routes/scroll-events/+page.marko | 31 ++ .../src/routes/scroll-margin/+page.marko | 30 ++ .../src/routes/window-example/+page.marko | 57 +++ .../src/routes/window-horizontal/+page.marko | 24 + .../routes/window-initial-offset/+page.marko | 27 ++ .../e2e/option-gates/tsconfig.json | 24 + .../e2e/option-gates/vite.config.ts | 17 + packages/marko-virtual/package.json | 11 +- .../scripts/generate-d-marko.mjs | 128 +++++ .../src/tags/virtualizer/index.d.marko | 56 +++ .../src/tags/virtualizer/index.marko | 84 +++- .../src/tags/virtualizer/options.ts | 107 ++++- .../src/tags/window-virtualizer/index.d.marko | 55 +++ .../src/tags/window-virtualizer/index.marko | 84 +++- .../src/tags/window-virtualizer/options.ts | 120 ++++- .../tests/fixtures/count-update-fixture.marko | 33 +- .../tests/fixtures/surface-fixture.marko | 47 ++ .../tests/fixtures/virtualizer-fixture.marko | 36 +- .../fixtures/window-virtualizer-fixture.marko | 35 +- packages/marko-virtual/tests/options.test.ts | 341 ++++++++++++++ .../marko-virtual/tests/render-slice.test.ts | 137 ++++++ packages/marko-virtual/tests/tags.test.ts | 134 ++++++ packages/marko-virtual/tsconfig.emit.json | 16 + .../marko-virtual/tsconfig.typecheck.json | 15 + pnpm-workspace.yaml | 9 + 191 files changed, 8902 insertions(+), 660 deletions(-) create mode 100644 .changeset/marko-virtual-followups.md create mode 100644 examples/marko/chat-pretext/.marko-run/routes.d.ts create mode 100644 examples/marko/chat-pretext/e2e/README.md create mode 100644 examples/marko/chat-pretext/e2e/chat-pretext.spec.ts create mode 100644 examples/marko/chat-pretext/marko.json create mode 100644 examples/marko/chat-pretext/package.json create mode 100644 examples/marko/chat-pretext/playwright.config.ts create mode 100644 examples/marko/chat-pretext/src/routes/+page.marko create mode 100644 examples/marko/chat-pretext/src/routes/api/reply/+handler.ts create mode 100644 examples/marko/chat-pretext/tsconfig.json create mode 100644 examples/marko/chat-pretext/vite.config.ts create mode 100644 examples/marko/chat/.marko-run/routes.d.ts create mode 100644 examples/marko/chat/e2e/README.md create mode 100644 examples/marko/chat/e2e/chat.spec.ts create mode 100644 examples/marko/chat/marko.json create mode 100644 examples/marko/chat/package.json create mode 100644 examples/marko/chat/playwright.config.ts create mode 100644 examples/marko/chat/src/routes/+page.marko create mode 100644 examples/marko/chat/src/routes/api/reply/+handler.ts create mode 100644 examples/marko/chat/tsconfig.json create mode 100644 examples/marko/chat/vite.config.ts create mode 100644 examples/marko/dynamic/.marko-run/routes.d.ts create mode 100644 examples/marko/dynamic/e2e/dynamic.spec.ts create mode 100644 examples/marko/dynamic/playwright.config.ts create mode 100644 examples/marko/dynamic/tsconfig.json create mode 100644 examples/marko/fixed/.marko-run/routes.d.ts create mode 100644 examples/marko/fixed/e2e/fixed.spec.ts create mode 100644 examples/marko/fixed/marko.json create mode 100644 examples/marko/fixed/playwright.config.ts create mode 100644 examples/marko/fixed/tsconfig.json create mode 100644 examples/marko/grid/.marko-run/routes.d.ts create mode 100644 examples/marko/grid/e2e/grid.spec.ts create mode 100644 examples/marko/grid/playwright.config.ts create mode 100644 examples/marko/grid/tsconfig.json create mode 100644 examples/marko/infinite-scroll/.marko-run/routes.d.ts create mode 100644 examples/marko/infinite-scroll/e2e/infinite-scroll.spec.ts create mode 100644 examples/marko/infinite-scroll/playwright.config.ts create mode 100644 examples/marko/infinite-scroll/tsconfig.json create mode 100644 examples/marko/padding/.marko-run/routes.d.ts create mode 100644 examples/marko/padding/e2e/padding.spec.ts create mode 100644 examples/marko/padding/marko.json create mode 100644 examples/marko/padding/package.json create mode 100644 examples/marko/padding/playwright.config.ts create mode 100644 examples/marko/padding/src/routes/+page.marko create mode 100644 examples/marko/padding/tsconfig.json create mode 100644 examples/marko/padding/vite.config.ts create mode 100644 examples/marko/pretext/.marko-run/routes.d.ts create mode 100644 examples/marko/pretext/e2e/pretext.spec.ts create mode 100644 examples/marko/pretext/marko.json create mode 100644 examples/marko/pretext/package.json create mode 100644 examples/marko/pretext/playwright.config.ts create mode 100644 examples/marko/pretext/src/routes/+page.marko create mode 100644 examples/marko/pretext/tsconfig.json create mode 100644 examples/marko/pretext/vite.config.ts create mode 100644 examples/marko/scroll-padding/.marko-run/routes.d.ts create mode 100644 examples/marko/scroll-padding/e2e/scroll-padding.spec.ts create mode 100644 examples/marko/scroll-padding/marko.json create mode 100644 examples/marko/scroll-padding/package.json create mode 100644 examples/marko/scroll-padding/playwright.config.ts create mode 100644 examples/marko/scroll-padding/src/routes/+page.marko create mode 100644 examples/marko/scroll-padding/tsconfig.json create mode 100644 examples/marko/scroll-padding/vite.config.ts create mode 100644 examples/marko/smooth-scroll/.marko-run/routes.d.ts create mode 100644 examples/marko/smooth-scroll/e2e/smooth-scroll.spec.ts create mode 100644 examples/marko/smooth-scroll/playwright.config.ts create mode 100644 examples/marko/smooth-scroll/tsconfig.json create mode 100644 examples/marko/ssr-fetch/.marko-run/routes.d.ts create mode 100644 examples/marko/ssr-fetch/e2e/ssr-fetch.spec.ts create mode 100644 examples/marko/ssr-fetch/marko.json create mode 100644 examples/marko/ssr-fetch/package.json create mode 100644 examples/marko/ssr-fetch/playwright.config.ts create mode 100644 examples/marko/ssr-fetch/src/data.ts create mode 100644 examples/marko/ssr-fetch/src/routes/+page.marko create mode 100644 examples/marko/ssr-fetch/tsconfig.json create mode 100644 examples/marko/ssr-fetch/vite.config.ts create mode 100644 examples/marko/ssr-restore/.marko-run/routes.d.ts create mode 100644 examples/marko/ssr-restore/e2e/ssr-restore.spec.ts create mode 100644 examples/marko/ssr-restore/marko.json create mode 100644 examples/marko/ssr-restore/package.json create mode 100644 examples/marko/ssr-restore/playwright.config.ts create mode 100644 examples/marko/ssr-restore/src/data.ts create mode 100644 examples/marko/ssr-restore/src/routes/+page.marko create mode 100644 examples/marko/ssr-restore/tsconfig.json create mode 100644 examples/marko/ssr-restore/vite.config.ts create mode 100644 examples/marko/ssr-slice/.marko-run/routes.d.ts create mode 100644 examples/marko/ssr-slice/e2e/ssr-slice.spec.ts create mode 100644 examples/marko/ssr-slice/marko.json create mode 100644 examples/marko/ssr-slice/package.json create mode 100644 examples/marko/ssr-slice/playwright.config.ts create mode 100644 examples/marko/ssr-slice/src/data.ts create mode 100644 examples/marko/ssr-slice/src/routes/+page.marko create mode 100644 examples/marko/ssr-slice/tsconfig.json create mode 100644 examples/marko/ssr-slice/vite.config.ts create mode 100644 examples/marko/ssr/.marko-run/routes.d.ts create mode 100644 examples/marko/ssr/e2e/ssr.spec.ts create mode 100644 examples/marko/ssr/marko.json create mode 100644 examples/marko/ssr/package.json create mode 100644 examples/marko/ssr/playwright.config.ts create mode 100644 examples/marko/ssr/src/routes/+page.marko create mode 100644 examples/marko/ssr/tsconfig.json create mode 100644 examples/marko/ssr/vite.config.ts create mode 100644 examples/marko/sticky/.marko-run/routes.d.ts create mode 100644 examples/marko/sticky/e2e/sticky.spec.ts create mode 100644 examples/marko/sticky/marko.json create mode 100644 examples/marko/sticky/package.json create mode 100644 examples/marko/sticky/playwright.config.ts create mode 100644 examples/marko/sticky/src/routes/+page.marko create mode 100644 examples/marko/sticky/tsconfig.json create mode 100644 examples/marko/sticky/vite.config.ts create mode 100644 examples/marko/table/.marko-run/routes.d.ts create mode 100644 examples/marko/table/e2e/table.spec.ts create mode 100644 examples/marko/table/marko.json create mode 100644 examples/marko/table/package.json create mode 100644 examples/marko/table/playwright.config.ts create mode 100644 examples/marko/table/src/routes/+page.marko create mode 100644 examples/marko/table/tsconfig.json create mode 100644 examples/marko/table/vite.config.ts create mode 100644 examples/marko/variable/.marko-run/routes.d.ts create mode 100644 examples/marko/variable/e2e/variable.spec.ts create mode 100644 examples/marko/variable/playwright.config.ts create mode 100644 examples/marko/variable/tsconfig.json create mode 100644 examples/marko/window-ssr-slice/.marko-run/routes.d.ts create mode 100644 examples/marko/window-ssr-slice/e2e/window-ssr-slice.spec.ts create mode 100644 examples/marko/window-ssr-slice/marko.json create mode 100644 examples/marko/window-ssr-slice/package.json create mode 100644 examples/marko/window-ssr-slice/playwright.config.ts create mode 100644 examples/marko/window-ssr-slice/src/data.ts create mode 100644 examples/marko/window-ssr-slice/src/routes/+page.marko create mode 100644 examples/marko/window-ssr-slice/tsconfig.json create mode 100644 examples/marko/window-ssr-slice/vite.config.ts create mode 100644 examples/marko/window/.marko-run/routes.d.ts create mode 100644 examples/marko/window/e2e/window.spec.ts create mode 100644 examples/marko/window/playwright.config.ts create mode 100644 examples/marko/window/tsconfig.json create mode 100644 packages/marko-virtual/e2e/option-gates/.marko-run/routes.d.ts create mode 100644 packages/marko-virtual/e2e/option-gates/README.md create mode 100644 packages/marko-virtual/e2e/option-gates/e2e/option-gates.spec.ts create mode 100644 packages/marko-virtual/e2e/option-gates/marko.json create mode 100644 packages/marko-virtual/e2e/option-gates/package.json create mode 100644 packages/marko-virtual/e2e/option-gates/playwright.config.ts create mode 100644 packages/marko-virtual/e2e/option-gates/src/routes/+page.marko create mode 100644 packages/marko-virtual/e2e/option-gates/src/routes/cached/+page.marko create mode 100644 packages/marko-virtual/e2e/option-gates/src/routes/debug/+page.marko create mode 100644 packages/marko-virtual/e2e/option-gates/src/routes/enabled/+page.marko create mode 100644 packages/marko-virtual/e2e/option-gates/src/routes/lanes-mode/+page.marko create mode 100644 packages/marko-virtual/e2e/option-gates/src/routes/measure-element/+page.marko create mode 100644 packages/marko-virtual/e2e/option-gates/src/routes/rtl/+page.marko create mode 100644 packages/marko-virtual/e2e/option-gates/src/routes/scroll-events/+page.marko create mode 100644 packages/marko-virtual/e2e/option-gates/src/routes/scroll-margin/+page.marko create mode 100644 packages/marko-virtual/e2e/option-gates/src/routes/window-example/+page.marko create mode 100644 packages/marko-virtual/e2e/option-gates/src/routes/window-horizontal/+page.marko create mode 100644 packages/marko-virtual/e2e/option-gates/src/routes/window-initial-offset/+page.marko create mode 100644 packages/marko-virtual/e2e/option-gates/tsconfig.json create mode 100644 packages/marko-virtual/e2e/option-gates/vite.config.ts create mode 100644 packages/marko-virtual/scripts/generate-d-marko.mjs create mode 100644 packages/marko-virtual/src/tags/virtualizer/index.d.marko create mode 100644 packages/marko-virtual/src/tags/window-virtualizer/index.d.marko create mode 100644 packages/marko-virtual/tests/fixtures/surface-fixture.marko create mode 100644 packages/marko-virtual/tests/options.test.ts create mode 100644 packages/marko-virtual/tests/render-slice.test.ts create mode 100644 packages/marko-virtual/tsconfig.emit.json create mode 100644 packages/marko-virtual/tsconfig.typecheck.json diff --git a/.changeset/marko-virtual-followups.md b/.changeset/marko-virtual-followups.md new file mode 100644 index 000000000..fd5ff53d7 --- /dev/null +++ b/.changeset/marko-virtual-followups.md @@ -0,0 +1,15 @@ +--- +'@tanstack/marko-virtual': minor +--- + +Post-release follow-ups for the Marko adapter: full option parity for both tags +(scrollMargin, enabled, isRtl, isScrollingResetDelay, useScrollendEvent, +useAnimationFrameWithResizeObserver, laneAssignmentMode, useCachedMeasurements, +debug, custom measureElement; window tag adds horizontal and initialOffset), +named handle types (VirtualizerHandle / WindowVirtualizerHandle) with committed +.d.marko declarations verified by marko-type-check in CI, a new Chat + Pretext +example (calculated row heights via @chenglou/pretext; streamed replies grow +through resizeItem), browser e2e suites for every example plus option-gate +behavioral proofs, TypeScript-strict cleanups across examples, and chat example +improvements (accurate size estimate, load-ahead history trigger, +overflow-anchor handling). diff --git a/.gitignore b/.gitignore index 2a91fb574..8b0d0de9e 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ vite.config.ts.timestamp-* test-results/ playwright-report/ *.log +**/*.tsbuildinfo \ No newline at end of file diff --git a/docs/config.json b/docs/config.json index e908d013d..04b62a7dc 100644 --- a/docs/config.json +++ b/docs/config.json @@ -9,9 +9,18 @@ { "label": "Getting Started", "children": [ - { "label": "Introduction", "to": "introduction" }, - { "label": "Installation", "to": "installation" }, - { "label": "Text Measurement with Pretext", "to": "pretext" } + { + "label": "Introduction", + "to": "introduction" + }, + { + "label": "Installation", + "to": "installation" + }, + { + "label": "Text Measurement with Pretext", + "to": "pretext" + } ], "frameworks": [ { @@ -35,7 +44,10 @@ { "label": "solid", "children": [ - { "label": "Solid Virtual", "to": "framework/solid/solid-virtual" } + { + "label": "Solid Virtual", + "to": "framework/solid/solid-virtual" + } ] }, { @@ -69,13 +81,24 @@ }, { "label": "Guides", - "children": [{ "label": "Chat", "to": "chat" }] + "children": [ + { + "label": "Chat", + "to": "chat" + } + ] }, { "label": "Core APIs", "children": [ - { "label": "Virtualizer", "to": "api/virtualizer" }, - { "label": "VirtualItem", "to": "api/virtual-item" } + { + "label": "Virtualizer", + "to": "api/virtualizer" + }, + { + "label": "VirtualItem", + "to": "api/virtual-item" + } ] }, { @@ -282,17 +305,65 @@ "to": "framework/marko/examples/grid", "label": "Grid" }, + { + "to": "framework/marko/examples/pretext", + "label": "Pretext" + }, + { + "to": "framework/marko/examples/padding", + "label": "Padding" + }, + { + "to": "framework/marko/examples/scroll-padding", + "label": "Scroll Padding" + }, + { + "to": "framework/marko/examples/sticky", + "label": "Sticky" + }, { "to": "framework/marko/examples/infinite-scroll", "label": "Infinite Scroll" }, + { + "to": "framework/marko/examples/chat", + "label": "Chat" + }, + { + "to": "framework/marko/examples/chat-pretext", + "label": "Chat + Pretext" + }, { "to": "framework/marko/examples/smooth-scroll", "label": "Smooth Scroll" }, + { + "to": "framework/marko/examples/table", + "label": "Table" + }, { "to": "framework/marko/examples/window", "label": "Window" + }, + { + "to": "framework/marko/examples/ssr", + "label": "SSR: No Fetch, Rows Render on Client" + }, + { + "to": "framework/marko/examples/ssr-fetch", + "label": "SSR: Fetch on Server, Rows Render on Client" + }, + { + "to": "framework/marko/examples/ssr-slice", + "label": "SSR: Fetch on Server, Rows Render on Server" + }, + { + "to": "framework/marko/examples/ssr-restore", + "label": "SSR: Fetch on Server, Rows Render on Server at Offset" + }, + { + "to": "framework/marko/examples/window-ssr-slice", + "label": "SSR Window: Fetch on Server, Rows Render on Server" } ] } diff --git a/docs/framework/marko/marko-virtual.md b/docs/framework/marko/marko-virtual.md index e6cf29ccc..f798c64b3 100644 --- a/docs/framework/marko/marko-virtual.md +++ b/docs/framework/marko/marko-virtual.md @@ -1,3 +1,7 @@ +--- +title: Marko Virtual +--- + # Marko Virtual `@tanstack/marko-virtual` is the Marko 6 adapter for TanStack Virtual. It provides @@ -9,6 +13,9 @@ row, column, and grid virtualisation via two auto-discovered Marko tags: Tags are discovered automatically by the Marko compiler when the package is installed. No imports are needed in your `.marko` files. +Each tag is used **self-closing** and exposes a **tag variable** (written ``). +You then own the markup, reading `v.virtualItems` and `v.totalSize` to render the visible rows. + ## Installation ```sh @@ -18,37 +25,31 @@ npm install @tanstack/marko-virtual ## Row virtualisation ```marko - - - - -
- 35 - getScrollElement=scrollEl - > -
- -
- Row ${item.index} -
- +
+ 35 + getScrollElement=() => scrollEl() + /> +
+ +
+ Row ${item.index}
- +
- +
``` ## Column virtualisation @@ -59,72 +60,70 @@ Same tag, `horizontal=true`:
- 100 horizontal=true - getScrollElement=scrollEl - > -
- -
- Column ${item.index} -
- -
- + getScrollElement=() => scrollEl() + /> +
+ +
+ Column ${item.index} +
+ +
``` ## Grid virtualisation Compose two `` tags — one for rows, one for columns — sharing the -same scroll element: +same scroll element. Each returns its own tag variable. Pass `getScrollElement` as an +arrow (`() => ref()`) so each virtualizer resolves its own element: ```marko
- 35 - getScrollElement=scrollEl - > - 100 - horizontal=true - getScrollElement=scrollEl - > -
- - -
- Cell ${row.index}, ${col.index} -
- - -
- - + getScrollElement=() => scrollEl() + /> + 100 + horizontal=true + getScrollElement=() => scrollEl() + /> +
+ + +
+ Cell ${row.index}, ${col.index} +
+ + +
``` @@ -133,69 +132,76 @@ same scroll element: Use `` when the entire page scrolls rather than a container: ```marko - 35 -> -
- -
- Row ${item.index} -
- -
-
+/> +
+ +
+ Row ${item.index} +
+ +
``` ## Dynamic / variable item sizes -For items with unknown heights, use `measureElement` as an effect-driven ref +For items with unknown heights, use `measureElement` as a ` - - 35 - overscan=5 - > -
- -
- Row ${item.index} -
- + 35 + overscan=5 + scrollMargin=listOffset + /> + // totalSize is already margin-free (core subtracts scrollMargin), so it is the + // wrapper's height as-is; only item.start values carry the margin. +
+ +
+ Row ${item.index}
- - + +
diff --git a/examples/marko/window/tsconfig.json b/examples/marko/window/tsconfig.json new file mode 100644 index 000000000..ca908ab1d --- /dev/null +++ b/examples/marko/window/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "moduleResolution": "Bundler", + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src", + "e2e", + "playwright.config.ts", + "vite.config.ts" + ] +} diff --git a/packages/marko-virtual/README.md b/packages/marko-virtual/README.md index c6ab7a6f2..92c40592b 100644 --- a/packages/marko-virtual/README.md +++ b/packages/marko-virtual/README.md @@ -2,7 +2,7 @@ Headless UI for virtualizing scrollable elements in [Marko 6](https://markojs.com), built on top of [`@tanstack/virtual-core`](https://tanstack.com/virtual). -Part of the [TanStack Virtual](https://tanstack.com/virtual) family. +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). ## Installation @@ -16,7 +16,7 @@ pnpm add @tanstack/marko-virtual ## Setup -Add to your project's `marko.json` to make the tags available: +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`: ```json { @@ -26,118 +26,233 @@ Add to your project's `marko.json` to make the tags available: ## Usage +Each tag is used **self-closing** and exposes a **tag variable** (``). 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. + ### `` — scrollable container Use when you control the scroll element (a div with `overflow: auto`). ```marko - - - - - - - -
- - 35 - getScrollElement=scrollEl - > -
- -
- Row ${item.index} -
- +
+ 35 + getScrollElement=() => scrollEl() + /> +
+ +
+ Row ${item.index}
- - + +
- - - ``` -> **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 `` guard ensures the virtualizer only renders after the browser has real dimensions available. +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. + +For columns, set `horizontal=true` and swap the width/height and `translateX` accordingly. For grids, compose two `` tags (rows + columns) sharing the same scroll element — each with its own tag variable and its own `() => scrollEl()` thunk. ### `` — full-page scrolling Use when the page itself is the scroll container. ```marko - - - + 35 +/> +
+ +
+ Row ${item.index} +
+ +
+``` - 35> -
- -
- Row ${item.index} -
- -
-
+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. + +## SSR: server-rendered slices - - +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): + +```marko + 35 + getScrollElement=() => scrollEl() + initialRect=({ width: 800, height: 400 }) + initialOffset=3500 +/> ``` -## Tag parameters +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. -Both tags use Marko 6's tag parameters pattern. The body receives virtual state via `|{ ... }|` destructuring: +### Streaming SSR: the tag inside `` + +The tags compose with Marko's streaming out of the box — put a virtualizer inside an +`` and the whole pattern above still holds, per streamed chunk: ```marko - - - + + <@placeholder> +

Loading people…

+ + <@catch|err|> +

Failed to load: ${(err as Error).message}

+ + + +
+ 48 + getScrollElement=() => scrollEl() + initialRect=({ width: 800, height: 400 }) + /> + +
+ +
``` -## API Reference +What happens on the wire: the server flushes the page shell (with the placeholder) +**immediately**, keeps streaming, and when `fetchPeople()` resolves it flushes the +awaited subtree as a **later chunk** — including the server-painted rows when +`initialRect` is set. Each streamed chunk **resumes independently** on the client: +the live virtualizer instance for this list mounts when its chunk arrives, without +waiting for the rest of the page. Nothing extra to configure — the seed is computed +inside the awaited subtree from the resolved data, so server and resumed client +agree by construction. -### `` +Two things to know: -| Attribute | Type | Required | Description | -| -------------------- | --------------------------- | -------- | --------------------------------------------------- | -| `count` | `number` | ✅ | Total number of items | -| `getScrollElement` | `() => Element \| null` | ✅ | Returns the scroll container | -| `estimateSize` | `(index: number) => number` | | Estimated item size in px (default: `50`) | -| `overscan` | `number` | | Items to render outside the viewport (default: `5`) | -| `horizontal` | `boolean` | | Enable horizontal scrolling (default: `false`) | -| `paddingStart` | `number` | | Padding before the first item in px | -| `paddingEnd` | `number` | | Padding after the last item in px | -| `scrollPaddingStart` | `number` | | Scroll padding at the start | -| `scrollPaddingEnd` | `number` | | Scroll padding at the end | -| `gap` | `number` | | Gap between items in px | -| `lanes` | `number` | | Number of lanes for grid layouts | -| `initialOffset` | `number \| (() => number)` | | Initial scroll offset | +- Marko streams awaited content **out of order**: the chunk arrives at the end of + the byte stream and an inline script swaps it into place. With JavaScript + disabled that swap never runs, so an ``-wrapped list renders blank without + JS even though the row HTML is present in the source. If no-JS visibility + matters, fetch **before** render (await the data in the route) instead of + streaming. +- The `ssr-fetch` example shows the streamed pattern with client-rendered rows; the + `ssr-slice` example shows it with **server-painted** rows (`initialRect`) — its + test suite includes a wire-level assertion that the placeholder flushes first and + the painted rows arrive in a later chunk. -**Tag parameters provided to body:** +## Dynamic / variable item sizes -| Parameter | Type | Description | -| ---------------- | ----------------------------------------------------- | -------------------------------------------------------- | -| `virtualItems` | `VirtualItem[]` | Items to render, with `index`, `start`, `size`, `key` | -| `totalSize` | `number` | Total scrollable size in px — set on the inner container | -| `measureElement` | `(el: Element \| null) => void` | Pass to `ref` for dynamic size measurement | -| `scrollToIndex` | `(index: number, options?: ScrollToOptions) => void` | Scroll to an item by index | -| `scrollToOffset` | `(offset: number, options?: ScrollToOptions) => void` | Scroll to a px offset | +For items with unknown heights, drive `measureElement` from a per-row ` + + 35 + overscan=5 + scrollMargin=listOffset + /> + // totalSize is already margin-free (core subtracts scrollMargin), so it is the + // wrapper's height as-is; only item.start values carry the margin. +
+ +
+ Row ${item.index} +
+ +
+ + diff --git a/packages/marko-virtual/e2e/option-gates/src/routes/window-horizontal/+page.marko b/packages/marko-virtual/e2e/option-gates/src/routes/window-horizontal/+page.marko new file mode 100644 index 000000000..15b1cbd24 --- /dev/null +++ b/packages/marko-virtual/e2e/option-gates/src/routes/window-horizontal/+page.marko @@ -0,0 +1,24 @@ +// GATE: window-virtualizer horizontal — the PAGE scrolls sideways. Scrolling the +// window horizontally must advance the rendered column window. + + + gate: window horizontal + + 100 + overscan=5 + horizontal=true + /> +
+ +
+ Col ${item.index} +
+ +
+ + diff --git a/packages/marko-virtual/e2e/option-gates/src/routes/window-initial-offset/+page.marko b/packages/marko-virtual/e2e/option-gates/src/routes/window-initial-offset/+page.marko new file mode 100644 index 000000000..8a87d884f --- /dev/null +++ b/packages/marko-virtual/e2e/option-gates/src/routes/window-initial-offset/+page.marko @@ -0,0 +1,27 @@ +// GATE: window-virtualizer initialOffset override — SSR seed at a position. +// initialRect + initialOffset=3500 must make the SERVER paint the slice around +// index 100 (35px rows), not index 0. Asserted at view-source level (raw HTML) +// and live (the page loads with those rows present pre-scroll). + + + gate: window initialOffset + + 35 + overscan=5 + initialRect=({ width: 1280, height: 700 }) + initialOffset=3500 + /> +
+ +
+ Row ${item.index} +
+ +
+ + diff --git a/packages/marko-virtual/e2e/option-gates/tsconfig.json b/packages/marko-virtual/e2e/option-gates/tsconfig.json new file mode 100644 index 000000000..ca908ab1d --- /dev/null +++ b/packages/marko-virtual/e2e/option-gates/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "moduleResolution": "Bundler", + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src", + "e2e", + "playwright.config.ts", + "vite.config.ts" + ] +} diff --git a/packages/marko-virtual/e2e/option-gates/vite.config.ts b/packages/marko-virtual/e2e/option-gates/vite.config.ts new file mode 100644 index 000000000..42b1c6a94 --- /dev/null +++ b/packages/marko-virtual/e2e/option-gates/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import marko from '@marko/run/vite' +import path from 'node:path' + +export default defineConfig({ + plugins: [marko() as any], + resolve: { + alias: { + // Run the gates against virtual-core SOURCE so no core build is needed — + // and so the optional core fix can be gate-verified by editing src alone. + '@tanstack/virtual-core': path.resolve( + __dirname, + '../../../virtual-core/src/index.ts', + ), + }, + }, +}) diff --git a/packages/marko-virtual/package.json b/packages/marko-virtual/package.json index 7ae7c56ba..76448ed32 100644 --- a/packages/marko-virtual/package.json +++ b/packages/marko-virtual/package.json @@ -50,19 +50,22 @@ "scripts": { "clean": "premove ./dist ./coverage", "test:eslint": "eslint ./src", - "test:types": "tsc", + "test:types": "mtc -p tsconfig.typecheck.json", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict", - "build": "vite build" + "build": "vite build", + "types:generate": "node scripts/generate-d-marko.mjs" }, "dependencies": { "@tanstack/virtual-core": "workspace:*" }, "devDependencies": { - "@marko/vite": "^3.0.0", + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", + "@marko/vite": "^6.1.0", "@testing-library/dom": "^10.0.0", - "marko": "^6.0.0" + "marko": "^6.2.2" }, "peerDependencies": { "marko": "^6.0.0" diff --git a/packages/marko-virtual/scripts/generate-d-marko.mjs b/packages/marko-virtual/scripts/generate-d-marko.mjs new file mode 100644 index 000000000..4a73e0a53 --- /dev/null +++ b/packages/marko-virtual/scripts/generate-d-marko.mjs @@ -0,0 +1,128 @@ +// Regenerates the committed .d.marko files from the tag sources via +// @marko/type-check (mtc). Manual, on-demand: run `pnpm types:generate` after +// changing a tag's Input or return surface. CI never generates — it only +// VERIFIES (test:types) that the committed .d.marko still agree with the tags. +// +// Pipeline: +// 1. mtc checker must pass on the current sources (don't generate from +// broken types). +// 2. Back up and delete the existing .d.marko (mtc prefers them as input when +// present — leaving them would regenerate from the old files, not the +// tags). The backups are restored automatically on any failure. +// 3. Emit via tsconfig.emit.json (output captured; only shown on failure). +// 4. Locate the emitted index.d.marko for each tag by searching the emit +// directory (output layout depends on TypeScript's rootDir handling, so +// search rather than hardcode), copy them back beside their sources, and +// remove the emit directory. +// 5. mtc checker must pass again with the regenerated files in place. +import { execSync } from 'node:child_process' +import { + cpSync, + existsSync, + mkdtempSync, + readdirSync, + rmSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join, relative } from 'node:path' +import { fileURLToPath } from 'node:url' + +const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..') +const emitDir = join(pkgRoot, '.d-marko-emit') +const tags = ['virtualizer', 'window-virtualizer'] +const dMarkoPath = (tag) => join(pkgRoot, 'src/tags', tag, 'index.d.marko') + +const backupDir = mkdtempSync(join(tmpdir(), 'd-marko-backup-')) + +const restoreBackups = () => { + for (const tag of tags) { + const backup = join(backupDir, `${tag}.d.marko`) + if (existsSync(backup)) cpSync(backup, dMarkoPath(tag)) + } +} + +const walk = (dir, out = []) => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name) + if (entry.isDirectory()) walk(full, out) + else out.push(full) + } + return out +} + +const fail = (message, emitOutput) => { + console.error(`\n[types:generate] FAILED: ${message}`) + if (existsSync(emitDir)) { + const files = walk(emitDir).map((f) => ` ${relative(pkgRoot, f)}`) + console.error( + files.length + ? `Emit directory contents:\n${files.join('\n')}` + : 'Emit directory is EMPTY.', + ) + } else { + console.error('Emit directory was never created.') + } + if (emitOutput?.trim()) console.error(`\nEmit output:\n${emitOutput}`) + console.error('\nThe previous .d.marko files have been restored.') + restoreBackups() + rmSync(emitDir, { recursive: true, force: true }) + rmSync(backupDir, { recursive: true, force: true }) + process.exit(1) +} + +const run = (cmd) => execSync(cmd, { cwd: pkgRoot, stdio: 'inherit' }) + +const check = (label) => { + console.log(`\n[types:generate] ${label}`) + try { + run('mtc -p tsconfig.typecheck.json') + } catch { + fail(`type check failed (${label}) — see output above.`) + } +} + +check('1/5 verifying current sources type-check') + +console.log( + '\n[types:generate] 2/5 backing up and removing existing .d.marko (mtc would prefer them as input)', +) +for (const tag of tags) { + if (existsSync(dMarkoPath(tag))) { + cpSync(dMarkoPath(tag), join(backupDir, `${tag}.d.marko`)) + rmSync(dMarkoPath(tag)) + } +} + +console.log('\n[types:generate] 3/5 emitting') +rmSync(emitDir, { recursive: true, force: true }) +let emitOutput = '' +try { + // Captured, not streamed: benign config-shape complaints in emit mode read + // like failures. Real problems are caught by the emitted-file search below + // and the re-verification in step 5; the captured output is printed then. + emitOutput = execSync('mtc -p tsconfig.emit.json', { + cwd: pkgRoot, + stdio: ['ignore', 'pipe', 'pipe'], + encoding: 'utf8', + }) +} catch (err) { + emitOutput = `${err.stdout ?? ''}${err.stderr ?? ''}` +} + +console.log('\n[types:generate] 4/5 copying generated files back') +if (!existsSync(emitDir)) fail('nothing was emitted.', emitOutput) +const emittedFiles = walk(emitDir) +for (const tag of tags) { + // The emitted path mirrors TypeScript's rootDir handling, which can differ + // by environment — search for the file rather than hardcoding its depth. + const suffix = join('tags', tag, 'index.d.marko') + const emitted = emittedFiles.find((f) => f.endsWith(suffix)) + if (!emitted) fail(`no emitted index.d.marko for "${tag}".`, emitOutput) + cpSync(emitted, dMarkoPath(tag)) + console.log(` ${tag}/index.d.marko regenerated`) +} +rmSync(emitDir, { recursive: true, force: true }) +rmSync(backupDir, { recursive: true, force: true }) + +check('5/5 verifying the regenerated files') +console.log('\n[types:generate] done — review the diff and commit.') diff --git a/packages/marko-virtual/src/tags/virtualizer/index.d.marko b/packages/marko-virtual/src/tags/virtualizer/index.d.marko new file mode 100644 index 000000000..cec5b453c --- /dev/null +++ b/packages/marko-virtual/src/tags/virtualizer/index.d.marko @@ -0,0 +1,56 @@ +import { Virtualizer } from "@tanstack/virtual-core"; +import type { Range, VirtualItem, ScrollToOptions } from "@tanstack/virtual-core"; +export interface VirtualizerHandle { + virtualItems: VirtualItem[]; + totalSize: number; + range: { + startIndex: number; + endIndex: number; + } | null; + measureElement: ((el: Element | null) => void) | undefined; + scrollToIndex: (index: number, options?: ScrollToOptions) => void; + scrollToOffset: (offset: number, options?: ScrollToOptions) => void; + measure: () => void; + resizeItem: (index: number, size: number) => void; + scrollToEnd: (options?: { + behavior?: ScrollToOptions['behavior']; + }) => void; + isAtEnd: (threshold?: number) => boolean; + getDistanceFromEnd: () => number; +} +export interface Input { + count: number; + estimateSize?: (index: number) => number; + getScrollElement: () => Element | null; + overscan?: number; + horizontal?: boolean; + paddingStart?: number; + paddingEnd?: number; + scrollPaddingStart?: number; + scrollPaddingEnd?: number; + gap?: number; + lanes?: number; + initialOffset?: number | (() => number); + initialRect?: { + width: number; + height: number; + }; + getItemKey?: (index: number) => number | string | bigint; + rangeExtractor?: (range: Range) => number[]; + indexAttribute?: string; + initialMeasurementsCache?: VirtualItem[]; + anchorTo?: 'start' | 'end'; + followOnAppend?: boolean | ScrollBehavior; + scrollEndThreshold?: number; + scrollMargin?: number; + enabled?: boolean; + isRtl?: boolean; + isScrollingResetDelay?: number; + useScrollendEvent?: boolean; + useAnimationFrameWithResizeObserver?: boolean; + laneAssignmentMode?: 'estimate' | 'measured'; + useCachedMeasurements?: boolean; + debug?: boolean; + measureElement?: (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer) => number; +} + diff --git a/packages/marko-virtual/src/tags/virtualizer/index.marko b/packages/marko-virtual/src/tags/virtualizer/index.marko index 35ea0ea0b..03835294c 100644 --- a/packages/marko-virtual/src/tags/virtualizer/index.marko +++ b/packages/marko-virtual/src/tags/virtualizer/index.marko @@ -1,6 +1,20 @@ import { Virtualizer } from "@tanstack/virtual-core" -import type { VirtualItem, ScrollToOptions } from "@tanstack/virtual-core" -import { buildOptions } from "./options" +import type { Range, VirtualItem, ScrollToOptions } from "@tanstack/virtual-core" +import { buildOptions, renderSlice } from "./options" + +export interface VirtualizerHandle { + virtualItems: VirtualItem[] + totalSize: number + range: { startIndex: number; endIndex: number } | null + measureElement: ((el: Element | null) => void) | undefined + scrollToIndex: (index: number, options?: ScrollToOptions) => void + scrollToOffset: (offset: number, options?: ScrollToOptions) => void + measure: () => void + resizeItem: (index: number, size: number) => void + scrollToEnd: (options?: { behavior?: ScrollToOptions['behavior'] }) => void + isAtEnd: (threshold?: number) => boolean + getDistanceFromEnd: () => number +} export interface Input { count: number @@ -15,19 +29,41 @@ export interface Input { gap?: number lanes?: number initialOffset?: number | (() => number) - content: Marko.Body<[{ - virtualItems: VirtualItem[] - totalSize: number - measureElement: ((el: Element | null) => void) | undefined - scrollToIndex: (index: number, options?: ScrollToOptions) => void - scrollToOffset: (offset: number, options?: ScrollToOptions) => void - }]> + initialRect?: { width: number; height: number } + getItemKey?: (index: number) => number | string | bigint + rangeExtractor?: (range: Range) => number[] + indexAttribute?: string + initialMeasurementsCache?: VirtualItem[] + anchorTo?: 'start' | 'end' + followOnAppend?: boolean | ScrollBehavior + scrollEndThreshold?: number + scrollMargin?: number + enabled?: boolean + isRtl?: boolean + isScrollingResetDelay?: number + useScrollendEvent?: boolean + useAnimationFrameWithResizeObserver?: boolean + laneAssignmentMode?: 'estimate' | 'measured' + useCachedMeasurements?: boolean + debug?: boolean + measureElement?: ( + element: Element, + entry: ResizeObserverEntry | undefined, + instance: Virtualizer, + ) => number } +// Render-time seed: derive the initial window as plain data via a transient virtual-core +// instance (see renderSlice in ./options). On the server this paints the initial slice into +// the HTML (when `initialRect` is given) or an empty container with the correct total height +// (when it is not) — no guard. The values serialize and recompute identically on +// resume. The live, observing instance is then built client-only in onMount and takes over. + + + + | null)/> void) | null)/> - - onMount() { @@ -36,11 +72,13 @@ export interface Input { if (!inst) return items = inst.getVirtualItems() size = inst.getTotalSize() + range = inst.range } v = new Virtualizer(buildOptions(input, notify)) cleanup = v!._didMount() v!._willUpdate() + notify() } onUpdate() { @@ -52,12 +90,10 @@ export interface Input { if (!inst) return items = inst.getVirtualItems() size = inst.getTotalSize() + range = inst.range } current.setOptions({ ...current.options, ...buildOptions(input, notify) }) - - // _willUpdate() is a no-op when the scroll element is unchanged, so - // notify() explicitly afterwards to recompute on count/option changes. current._willUpdate() notify() } @@ -69,10 +105,16 @@ export interface Input { } /> -<${input.content} - virtualItems=items - totalSize=size - measureElement=v?.measureElement - scrollToIndex=(index: number, options?: ScrollToOptions) => v?.scrollToIndex(index, options) - scrollToOffset=(offset: number, options?: ScrollToOptions) => v?.scrollToOffset(offset, options) -/> + v?.scrollToIndex(index, options), + scrollToOffset: (offset: number, options?: ScrollToOptions) => v?.scrollToOffset(offset, options), + measure: () => v?.measure(), + resizeItem: (index: number, itemSize: number) => v?.resizeItem(index, itemSize), + scrollToEnd: (options?: { behavior?: ScrollToOptions['behavior'] }) => v?.scrollToEnd(options), + isAtEnd: (threshold?: number) => v?.isAtEnd(threshold) ?? false, + getDistanceFromEnd: () => v?.getDistanceFromEnd() ?? Infinity, +}) as VirtualizerHandle)> diff --git a/packages/marko-virtual/src/tags/virtualizer/options.ts b/packages/marko-virtual/src/tags/virtualizer/options.ts index d401a758d..498ddd9df 100644 --- a/packages/marko-virtual/src/tags/virtualizer/options.ts +++ b/packages/marko-virtual/src/tags/virtualizer/options.ts @@ -1,9 +1,10 @@ import { + Virtualizer, elementScroll, observeElementOffset, observeElementRect, } from '@tanstack/virtual-core' -import type { VirtualizerOptions } from '@tanstack/virtual-core' +import type { Range, VirtualItem, VirtualizerOptions } from '@tanstack/virtual-core' // The subset of tag input that maps onto virtual-core options. Kept here // (rather than imported from index.marko) so this helper has no dependency @@ -21,10 +22,65 @@ export interface VirtualizerInput { gap?: number lanes?: number initialOffset?: number | (() => number) + // Viewport size used for the render-time (server / pre-mount) slice. When omitted, + // virtual-core's default 0x0 rect yields an empty window (totalSize is still correct). + initialRect?: { width: number; height: number } + // Stable per-item identity so cached measurements survive reorder (defaults to the index). + getItemKey?: (index: number) => number | string | bigint + // Hook over the visible index range: force extra indexes (e.g. a pinned sticky header) + // into the rendered window. Compose with virtual-core's `defaultRangeExtractor`. + rangeExtractor?: (range: Range) => Array + // DOM attribute carrying the item index for `measureElement` (default 'data-index'). + // Two instances measuring the SAME element (a grid cell) need distinct attributes. + indexAttribute?: string + // Pre-measured items (plain data) to seed the measurement cache. + initialMeasurementsCache?: Array + // Anchor the window to the list 'start' (default) or 'end' (a chat pinned to newest). + anchorTo?: 'start' | 'end' + // With anchorTo 'end': stay pinned to the end as items append + // (true, or a ScrollBehavior for how to get there). + followOnAppend?: boolean | ScrollBehavior + // How close (px) to the end still counts as "at the end" (default 1). + scrollEndThreshold?: number + // The list's offset (px) from the top of its scroll element, when other content sits + // above it inside the same scroller. Shifts all position math; item.start values then + // INCLUDE this margin (subtract it when positioning items relative to the list). + scrollMargin?: number + // Disable switch (default true). false is NOT a freeze: core unobserves, clears + // its measurements, and renders an EMPTY window until re-enabled (re-enabling + // re-windows from the live scroll position). + enabled?: boolean + // Right-to-left horizontal lists (default false). + isRtl?: boolean + // ms after the last scroll event before "user is scrolling" ends (default 150). + isScrollingResetDelay?: number + // Use the native scrollend event instead of the isScrollingResetDelay timer. + useScrollendEvent?: boolean + // Batch ResizeObserver measurements into animation frames (avoids + // "ResizeObserver loop" console errors under heavy resize load). + useAnimationFrameWithResizeObserver?: boolean + // Masonry/multi-lane: how items are assigned to lanes — by 'estimate' (default) + // or by 'measured' sizes. + laneAssignmentMode?: 'estimate' | 'measured' + // Make the default measurer return the cached (or estimated) size instead of + // reading the DOM — freezes sizes when they are already known and DOM + // re-measures are unwanted. + useCachedMeasurements?: boolean + // Verbose engine logging. + debug?: boolean + // Replace HOW an item's size is read from its element (e.g. include margins, + // or measure a child). Defaults to core's border-box measurer. + measureElement?: ( + element: Element, + entry: ResizeObserverEntry | undefined, + instance: Virtualizer, + ) => number } // Single source of truth for the input -> virtual-core option mapping, -// shared by onMount (construction) and onUpdate (setOptions). +// shared by the render-time seed, onMount (construction) and onUpdate (setOptions). +// Optional inputs are forwarded possibly-undefined on purpose: virtual-core's +// setOptions() skips undefined keys, so its own defaults apply. export function buildOptions( input: VirtualizerInput, notify: () => void, @@ -42,9 +98,56 @@ export function buildOptions( gap: input.gap, lanes: input.lanes, initialOffset: input.initialOffset, + initialRect: input.initialRect, + getItemKey: input.getItemKey, + rangeExtractor: input.rangeExtractor, + indexAttribute: input.indexAttribute, + initialMeasurementsCache: input.initialMeasurementsCache, + anchorTo: input.anchorTo, + followOnAppend: input.followOnAppend, + scrollEndThreshold: input.scrollEndThreshold, + scrollMargin: input.scrollMargin, + enabled: input.enabled, + isRtl: input.isRtl, + isScrollingResetDelay: input.isScrollingResetDelay, + useScrollendEvent: input.useScrollendEvent, + useAnimationFrameWithResizeObserver: input.useAnimationFrameWithResizeObserver, + laneAssignmentMode: input.laneAssignmentMode, + useCachedMeasurements: input.useCachedMeasurements, + debug: input.debug, + measureElement: input.measureElement, observeElementRect, observeElementOffset, scrollToFn: elementScroll, onChange: notify, } } + +// Render-time (server / pre-mount) slice. Constructs a transient virtual-core instance, +// reads the initial window as PLAIN DATA, and discards it — nothing live is retained, so +// nothing live is serialized, and the values recompute identically on resume. With +// `initialRect` this is a real slice of rows; without it renderSlice returns the trivial +// window (no items, size 0). The constructor and getVirtualItems()/getTotalSize() are +// SSR-safe (no DOM access); getScrollElement is forced to null so the probe never touches +// the scroll element, which does not exist on the server. +// `range` is the visible window ({ startIndex, endIndex }) the probe computed — plain data, +// null when there is no slice. +export function renderSlice( + input: VirtualizerInput, +): { + items: VirtualItem[] + size: number + range: { startIndex: number; endIndex: number } | null +} { + // Opt-in: without an explicit viewport hint there is no server slice. Returning the + // trivial window keeps every example that does not pass `initialRect` byte-for-byte + // identical to a client-only build (empty container, filled on mount). + if (!input.initialRect) { + return { items: [], size: 0, range: null } + } + const probe = new Virtualizer( + buildOptions({ ...input, getScrollElement: () => null }, () => {}), + ) + const items = probe.getVirtualItems() + return { items, size: probe.getTotalSize(), range: probe.range } +} diff --git a/packages/marko-virtual/src/tags/window-virtualizer/index.d.marko b/packages/marko-virtual/src/tags/window-virtualizer/index.d.marko new file mode 100644 index 000000000..6f06f4ceb --- /dev/null +++ b/packages/marko-virtual/src/tags/window-virtualizer/index.d.marko @@ -0,0 +1,55 @@ +import { Virtualizer } from "@tanstack/virtual-core"; +import type { Range, VirtualItem, ScrollToOptions } from "@tanstack/virtual-core"; +export interface WindowVirtualizerHandle { + virtualItems: VirtualItem[]; + totalSize: number; + range: { + startIndex: number; + endIndex: number; + } | null; + measureElement: ((el: Element | null) => void) | undefined; + scrollToIndex: (index: number, options?: ScrollToOptions) => void; + scrollToOffset: (offset: number, options?: ScrollToOptions) => void; + measure: () => void; + resizeItem: (index: number, size: number) => void; + scrollToEnd: (options?: { + behavior?: ScrollToOptions['behavior']; + }) => void; + isAtEnd: (threshold?: number) => boolean; + getDistanceFromEnd: () => number; +} +export interface Input { + count: number; + estimateSize?: (index: number) => number; + overscan?: number; + horizontal?: boolean; + paddingStart?: number; + paddingEnd?: number; + scrollPaddingStart?: number; + scrollPaddingEnd?: number; + gap?: number; + lanes?: number; + initialOffset?: number | (() => number); + initialRect?: { + width: number; + height: number; + }; + getItemKey?: (index: number) => number | string | bigint; + rangeExtractor?: (range: Range) => number[]; + indexAttribute?: string; + initialMeasurementsCache?: VirtualItem[]; + anchorTo?: 'start' | 'end'; + followOnAppend?: boolean | ScrollBehavior; + scrollEndThreshold?: number; + scrollMargin?: number; + enabled?: boolean; + isRtl?: boolean; + isScrollingResetDelay?: number; + useScrollendEvent?: boolean; + useAnimationFrameWithResizeObserver?: boolean; + laneAssignmentMode?: 'estimate' | 'measured'; + useCachedMeasurements?: boolean; + debug?: boolean; + measureElement?: (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer) => number; +} + diff --git a/packages/marko-virtual/src/tags/window-virtualizer/index.marko b/packages/marko-virtual/src/tags/window-virtualizer/index.marko index 095452a04..8d662380f 100644 --- a/packages/marko-virtual/src/tags/window-virtualizer/index.marko +++ b/packages/marko-virtual/src/tags/window-virtualizer/index.marko @@ -1,30 +1,66 @@ import { Virtualizer } from "@tanstack/virtual-core" -import type { VirtualItem, ScrollToOptions } from "@tanstack/virtual-core" -import { buildOptions } from "./options" +import type { Range, VirtualItem, ScrollToOptions } from "@tanstack/virtual-core" +import { buildOptions, renderSlice } from "./options" + +export interface WindowVirtualizerHandle { + virtualItems: VirtualItem[] + totalSize: number + range: { startIndex: number; endIndex: number } | null + measureElement: ((el: Element | null) => void) | undefined + scrollToIndex: (index: number, options?: ScrollToOptions) => void + scrollToOffset: (offset: number, options?: ScrollToOptions) => void + measure: () => void + resizeItem: (index: number, size: number) => void + scrollToEnd: (options?: { behavior?: ScrollToOptions['behavior'] }) => void + isAtEnd: (threshold?: number) => boolean + getDistanceFromEnd: () => number +} export interface Input { count: number estimateSize?: (index: number) => number overscan?: number + horizontal?: boolean paddingStart?: number paddingEnd?: number scrollPaddingStart?: number scrollPaddingEnd?: number gap?: number lanes?: number - content: Marko.Body<[{ - virtualItems: VirtualItem[] - totalSize: number - measureElement: ((el: Element | null) => void) | undefined - scrollToIndex: (index: number, options?: ScrollToOptions) => void - scrollToOffset: (offset: number, options?: ScrollToOptions) => void - }]> + initialOffset?: number | (() => number) + initialRect?: { width: number; height: number } + getItemKey?: (index: number) => number | string | bigint + rangeExtractor?: (range: Range) => number[] + indexAttribute?: string + initialMeasurementsCache?: VirtualItem[] + anchorTo?: 'start' | 'end' + followOnAppend?: boolean | ScrollBehavior + scrollEndThreshold?: number + scrollMargin?: number + enabled?: boolean + isRtl?: boolean + isScrollingResetDelay?: number + useScrollendEvent?: boolean + useAnimationFrameWithResizeObserver?: boolean + laneAssignmentMode?: 'estimate' | 'measured' + useCachedMeasurements?: boolean + debug?: boolean + measureElement?: ( + element: Element, + entry: ResizeObserverEntry | undefined, + instance: Virtualizer, + ) => number } +// Render-time seed (window counterpart): paints the initial slice into the server HTML when +// `initialRect` is given (empty container with correct total height otherwise), no +// guard. The live, observing instance is built client-only in onMount and takes over. + + + + | null)/> void) | null)/> - - onMount() { @@ -33,11 +69,13 @@ export interface Input { if (!inst) return items = inst.getVirtualItems() size = inst.getTotalSize() + range = inst.range } v = new Virtualizer(buildOptions(input, notify)) cleanup = v!._didMount() v!._willUpdate() + notify() } onUpdate() { @@ -49,12 +87,10 @@ export interface Input { if (!inst) return items = inst.getVirtualItems() size = inst.getTotalSize() + range = inst.range } current.setOptions({ ...current.options, ...buildOptions(input, notify) }) - - // _willUpdate() is a no-op when the scroll element is unchanged, so - // notify() explicitly afterwards to recompute on count/option changes. current._willUpdate() notify() } @@ -66,10 +102,16 @@ export interface Input { } /> -<${input.content} - virtualItems=items - totalSize=size - measureElement=v?.measureElement - scrollToIndex=(index: number, options?: ScrollToOptions) => v?.scrollToIndex(index, options) - scrollToOffset=(offset: number, options?: ScrollToOptions) => v?.scrollToOffset(offset, options) -/> + v?.scrollToIndex(index, options), + scrollToOffset: (offset: number, options?: ScrollToOptions) => v?.scrollToOffset(offset, options), + measure: () => v?.measure(), + resizeItem: (index: number, itemSize: number) => v?.resizeItem(index, itemSize), + scrollToEnd: (options?: { behavior?: ScrollToOptions['behavior'] }) => v?.scrollToEnd(options), + isAtEnd: (threshold?: number) => v?.isAtEnd(threshold) ?? false, + getDistanceFromEnd: () => v?.getDistanceFromEnd() ?? Infinity, +}) as WindowVirtualizerHandle)> diff --git a/packages/marko-virtual/src/tags/window-virtualizer/options.ts b/packages/marko-virtual/src/tags/window-virtualizer/options.ts index ab0c81e04..6c3fa2952 100644 --- a/packages/marko-virtual/src/tags/window-virtualizer/options.ts +++ b/packages/marko-virtual/src/tags/window-virtualizer/options.ts @@ -1,9 +1,10 @@ import { + Virtualizer, observeWindowOffset, observeWindowRect, windowScroll, } from '@tanstack/virtual-core' -import type { VirtualizerOptions } from '@tanstack/virtual-core' +import type { Range, VirtualItem, VirtualizerOptions } from '@tanstack/virtual-core' // The subset of tag input that maps onto virtual-core options. Kept here // (rather than imported from index.marko) so this helper has no dependency @@ -12,16 +13,77 @@ export interface WindowVirtualizerInput { count: number estimateSize?: (index: number) => number overscan?: number + // Horizontal window scrolling (the page scrolls sideways; default false). + horizontal?: boolean paddingStart?: number paddingEnd?: number scrollPaddingStart?: number scrollPaddingEnd?: number gap?: number lanes?: number + // Starting scroll offset. Defaults to reading the live window scroll position + // (window.scrollY, or window.scrollX when horizontal) on the client and 0 on the + // server. Pass a number to seed a server slice at a position (pairs with initialRect). + initialOffset?: number | (() => number) + // Window viewport size for the render-time (server / pre-mount) slice. When omitted, + // there is no server slice. + initialRect?: { width: number; height: number } + // Stable per-item identity so cached measurements survive reorder (defaults to the index). + getItemKey?: (index: number) => number | string | bigint + // Hook over the visible index range: force extra indexes (e.g. a pinned sticky header) + // into the rendered window. Compose with virtual-core's `defaultRangeExtractor`. + rangeExtractor?: (range: Range) => Array + // DOM attribute carrying the item index for `measureElement` (default 'data-index'). + // Two instances measuring the SAME element (a grid cell) need distinct attributes. + indexAttribute?: string + // Pre-measured items (plain data) to seed the measurement cache. + initialMeasurementsCache?: Array + // Anchor the window to the list 'start' (default) or 'end' (a chat pinned to newest). + anchorTo?: 'start' | 'end' + // With anchorTo 'end': stay pinned to the end as items append + // (true, or a ScrollBehavior for how to get there). + followOnAppend?: boolean | ScrollBehavior + // How close (px) to the end still counts as "at the end" (default 1). + scrollEndThreshold?: number + // The list's offset (px) from the top of the page, when other content sits above it. + // Shifts all position math; item.start values then INCLUDE this margin (subtract it + // when positioning items relative to the list). The textbook window-virtualizer knob. + scrollMargin?: number + // Disable switch (default true). false is NOT a freeze: core unobserves, clears + // its measurements, and renders an EMPTY window until re-enabled (re-enabling + // re-windows from the live scroll position). + enabled?: boolean + // Right-to-left horizontal lists (default false). + isRtl?: boolean + // ms after the last scroll event before "user is scrolling" ends (default 150). + isScrollingResetDelay?: number + // Use the native scrollend event instead of the isScrollingResetDelay timer. + useScrollendEvent?: boolean + // Batch ResizeObserver measurements into animation frames (avoids + // "ResizeObserver loop" console errors under heavy resize load). + useAnimationFrameWithResizeObserver?: boolean + // Masonry/multi-lane: how items are assigned to lanes — by 'estimate' (default) + // or by 'measured' sizes. + laneAssignmentMode?: 'estimate' | 'measured' + // Make the default measurer return the cached (or estimated) size instead of + // reading the DOM — freezes sizes when they are already known and DOM + // re-measures are unwanted. + useCachedMeasurements?: boolean + // Verbose engine logging. + debug?: boolean + // Replace HOW an item's size is read from its element (e.g. include margins, + // or measure a child). Defaults to core's border-box measurer. + measureElement?: ( + element: Element, + entry: ResizeObserverEntry | undefined, + instance: Virtualizer, + ) => number } // Single source of truth for the input -> virtual-core option mapping, -// shared by onMount (construction) and onUpdate (setOptions). +// shared by the render-time seed, onMount (construction) and onUpdate (setOptions). +// Optional inputs are forwarded possibly-undefined on purpose: virtual-core's +// setOptions() skips undefined keys, so its own defaults apply. export function buildOptions( input: WindowVirtualizerInput, notify: () => void, @@ -31,16 +93,68 @@ export function buildOptions( estimateSize: input.estimateSize ?? (() => 50), getScrollElement: () => (typeof document !== 'undefined' ? window : null), overscan: input.overscan ?? 5, + horizontal: input.horizontal, paddingStart: input.paddingStart, paddingEnd: input.paddingEnd, scrollPaddingStart: input.scrollPaddingStart, scrollPaddingEnd: input.scrollPaddingEnd, gap: input.gap, lanes: input.lanes, - initialOffset: () => (typeof document !== 'undefined' ? window.scrollY : 0), + // Consumer override first (mirrors react-virtual's spread-over-defaults), then the + // live window position on the client / 0 (top) on the server. + initialOffset: + input.initialOffset ?? + (() => + typeof document !== 'undefined' + ? input.horizontal + ? window.scrollX + : window.scrollY + : 0), + initialRect: input.initialRect, + getItemKey: input.getItemKey, + rangeExtractor: input.rangeExtractor, + indexAttribute: input.indexAttribute, + initialMeasurementsCache: input.initialMeasurementsCache, + anchorTo: input.anchorTo, + followOnAppend: input.followOnAppend, + scrollEndThreshold: input.scrollEndThreshold, + scrollMargin: input.scrollMargin, + enabled: input.enabled, + isRtl: input.isRtl, + isScrollingResetDelay: input.isScrollingResetDelay, + useScrollendEvent: input.useScrollendEvent, + useAnimationFrameWithResizeObserver: input.useAnimationFrameWithResizeObserver, + laneAssignmentMode: input.laneAssignmentMode, + useCachedMeasurements: input.useCachedMeasurements, + debug: input.debug, + measureElement: input.measureElement, observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, onChange: notify, } } + +// Render-time (server / pre-mount) slice — the window counterpart of the element helper. +// Opt-in: without `initialRect` there is no server slice (trivial window), which keeps every +// example that does not pass it byte-for-byte identical to a client-only build. With +// `initialRect` a transient instance derives the initial window as plain data and is discarded; +// nothing live is retained or serialized, and the values recompute identically on resume. +// The probe never observes, so it reads `initialRect` directly (getScrollElement returns null on +// the server and window on the client, but neither is measured before getVirtualItems()). +// `range` is the visible window ({ startIndex, endIndex }) the probe computed — plain data, +// null when there is no slice. +export function renderSlice( + input: WindowVirtualizerInput, +): { + items: VirtualItem[] + size: number + range: { startIndex: number; endIndex: number } | null +} { + if (!input.initialRect) { + return { items: [], size: 0, range: null } + } + const probe = new Virtualizer(buildOptions(input, () => {})) + const items = probe.getVirtualItems() + return { items, size: probe.getTotalSize(), range: probe.range } +} diff --git a/packages/marko-virtual/tests/fixtures/count-update-fixture.marko b/packages/marko-virtual/tests/fixtures/count-update-fixture.marko index c425bbdb8..ad5a2aef9 100644 --- a/packages/marko-virtual/tests/fixtures/count-update-fixture.marko +++ b/packages/marko-virtual/tests/fixtures/count-update-fixture.marko @@ -1,4 +1,4 @@ -// Fixture for testing reactive count changes via onUpdate. +// Fixture for testing reactive count changes via onUpdate (return-a-tag-variable shape). // The tag must recalculate items when count changes — // this requires the explicit notify() call after _willUpdate() in onUpdate. // Without it, count changes silently have no effect. @@ -18,24 +18,23 @@ export interface Input { data-testid="decrement-btn" onClick=() => { count = Math.max(0, count - 1) } /> - 50 getScrollElement=scrollEl + /> +
-
- -
- Row ${item.index} -
- -
- + +
+ Row ${item.index} +
+ +
diff --git a/packages/marko-virtual/tests/fixtures/surface-fixture.marko b/packages/marko-virtual/tests/fixtures/surface-fixture.marko new file mode 100644 index 000000000..8e9654d9e --- /dev/null +++ b/packages/marko-virtual/tests/fixtures/surface-fixture.marko @@ -0,0 +1,47 @@ +// Test fixture for the EXPANDED return surface of : +// range + measure/resizeItem/scrollToEnd/isAtEnd/getDistanceFromEnd. +// Renders the values into the DOM so jsdom tests can assert on them, and +// exposes buttons that invoke the methods so the wiring is exercised. + +export interface Input { + count?: number + itemHeight?: number + rangeExtractor?: (range: { + startIndex: number + endIndex: number + overscan: number + count: number + }) => number[] +} + +
+ input.itemHeight ?? 50 + getScrollElement=() => scrollEl() + getItemKey=(i: number) => `key-${i}` + rangeExtractor=input.rangeExtractor + /> +
${v.range ? v.range.startIndex : 'null'}
+
${v.range ? v.range.endIndex : 'null'}
+
${String(v.isAtEnd())}
+
${v.virtualItems[0] ? String(v.virtualItems[0].key) : 'none'}
+ + + +
+ +
+ Row ${item.index} +
+ +
+
diff --git a/packages/marko-virtual/tests/fixtures/virtualizer-fixture.marko b/packages/marko-virtual/tests/fixtures/virtualizer-fixture.marko index e6a2acc09..410d46a5e 100644 --- a/packages/marko-virtual/tests/fixtures/virtualizer-fixture.marko +++ b/packages/marko-virtual/tests/fixtures/virtualizer-fixture.marko @@ -1,6 +1,7 @@ -// Test fixture for tag. +// Test fixture for tag (return-a-tag-variable shape). // Not a route file — no needed. -// No guard needed since template.mount() runs client-side directly. +// No guard: the tag builds at render time and renders nothing itself; +// the consumer owns the markup below and reads v.virtualItems / v.totalSize. export interface Input { count?: number @@ -10,26 +11,25 @@ export interface Input { }
- input.itemHeight ?? 50 horizontal=input.horizontal ?? false lanes=input.lanes ?? 1 getScrollElement=scrollEl + /> +
-
- -
- Row ${item.index} -
- -
- + +
+ Row ${item.index} +
+ +
diff --git a/packages/marko-virtual/tests/fixtures/window-virtualizer-fixture.marko b/packages/marko-virtual/tests/fixtures/window-virtualizer-fixture.marko index d6647e96d..e79a4f78c 100644 --- a/packages/marko-virtual/tests/fixtures/window-virtualizer-fixture.marko +++ b/packages/marko-virtual/tests/fixtures/window-virtualizer-fixture.marko @@ -1,26 +1,27 @@ -// Test fixture for tag. +// Test fixture for tag (return-a-tag-variable shape). +// No guard: the tag builds at render time and renders nothing itself; +// the consumer owns the markup below and reads v.virtualItems / v.totalSize. export interface Input { count?: number itemHeight?: number } - input.itemHeight ?? 50 +/> +
-
- -
- Row ${item.index} -
- -
- + +
+ Row ${item.index} +
+ +
diff --git a/packages/marko-virtual/tests/options.test.ts b/packages/marko-virtual/tests/options.test.ts new file mode 100644 index 000000000..c8085f05b --- /dev/null +++ b/packages/marko-virtual/tests/options.test.ts @@ -0,0 +1,341 @@ +// Unit tests for the input -> virtual-core option mapping (buildOptions). +// Verifies the SSR knobs (initialRect, initialOffset) are plumbed through. Combined with +// the Node-level proof that virtual-core turns initialRect/initialOffset into a correct +// pre-mount slice, this covers the render-time seed's inputs without needing a DOM. +// The full-parity batch (scrollMargin, enabled, isRtl, isScrollingResetDelay, +// useScrollendEvent, useAnimationFrameWithResizeObserver, laneAssignmentMode, +// useCachedMeasurements, debug, custom measureElement — plus horizontal and an +// initialOffset override on the window tag) is Tier-1 forwarding-tested here; the +// behaviorally-observable subset has real-browser gates (see the session record). +import { describe, expect, test } from 'vitest' +import { Virtualizer, observeWindowRect, windowScroll } from '@tanstack/virtual-core' +import { buildOptions } from '../src/tags/virtualizer/options' +import { buildOptions as buildWindowOptions } from '../src/tags/window-virtualizer/options' + +describe('buildOptions — input to virtual-core option mapping', () => { + const noop = () => {} + + test('maps initialRect through (the SSR slice knob)', () => { + const opts = buildOptions( + { + count: 1000, + estimateSize: () => 40, + getScrollElement: () => null, + initialRect: { width: 800, height: 400 }, + }, + noop, + ) + expect(opts.initialRect).toEqual({ width: 800, height: 400 }) + expect(opts.count).toBe(1000) + expect(opts.onChange).toBe(noop) + }) + + test('maps initialOffset through (the SSR start-position knob)', () => { + const opts = buildOptions( + { count: 10, getScrollElement: () => null, initialOffset: 1200 }, + noop, + ) + expect(opts.initialOffset).toBe(1200) + }) + + test('initialRect is undefined when omitted (core applies its 0x0 default)', () => { + const opts = buildOptions({ count: 10, getScrollElement: () => null }, noop) + expect(opts.initialRect).toBeUndefined() + }) + + test('defaults: estimateSize 50, overscan 5, horizontal false', () => { + const opts = buildOptions({ count: 10, getScrollElement: () => null }, noop) + expect(opts.estimateSize(0)).toBe(50) + expect(opts.overscan).toBe(5) + expect(opts.horizontal).toBe(false) + }) + + test('maps every optional prop through when provided', () => { + const getItemKey = (i: number) => `k${i}` + const rangeExtractor = () => [0] + const cache = [ + { index: 0, start: 0, size: 32, end: 32, key: 0, lane: 0 }, + ] + const opts = buildOptions( + { + count: 500, + estimateSize: () => 32, + getScrollElement: () => null, + overscan: 10, + horizontal: true, + paddingStart: 8, + paddingEnd: 16, + scrollPaddingStart: 4, + scrollPaddingEnd: 12, + gap: 6, + lanes: 3, + initialOffset: 1200, + initialRect: { width: 640, height: 480 }, + getItemKey, + rangeExtractor, + indexAttribute: 'data-row-index', + initialMeasurementsCache: cache, + anchorTo: 'end', + followOnAppend: 'smooth', + scrollEndThreshold: 24, + }, + noop, + ) + expect(opts.estimateSize(0)).toBe(32) + expect(opts.overscan).toBe(10) + expect(opts.horizontal).toBe(true) + expect(opts.paddingStart).toBe(8) + expect(opts.paddingEnd).toBe(16) + expect(opts.scrollPaddingStart).toBe(4) + expect(opts.scrollPaddingEnd).toBe(12) + expect(opts.gap).toBe(6) + expect(opts.lanes).toBe(3) + expect(opts.initialOffset).toBe(1200) + expect(opts.initialRect).toEqual({ width: 640, height: 480 }) + expect(opts.getItemKey).toBe(getItemKey) + expect(opts.rangeExtractor).toBe(rangeExtractor) + expect(opts.indexAttribute).toBe('data-row-index') + expect(opts.initialMeasurementsCache).toBe(cache) + expect(opts.anchorTo).toBe('end') + expect(opts.followOnAppend).toBe('smooth') + expect(opts.scrollEndThreshold).toBe(24) + }) + + test('new optional props stay undefined when omitted (core applies its defaults)', () => { + const opts = buildOptions({ count: 10, getScrollElement: () => null }, noop) + expect(opts.getItemKey).toBeUndefined() + expect(opts.rangeExtractor).toBeUndefined() + expect(opts.indexAttribute).toBeUndefined() + expect(opts.initialMeasurementsCache).toBeUndefined() + expect(opts.anchorTo).toBeUndefined() + expect(opts.followOnAppend).toBeUndefined() + expect(opts.scrollEndThreshold).toBeUndefined() + }) + + test('maps the full-parity batch through when provided', () => { + const measurer = () => 99 + const opts = buildOptions( + { + count: 100, + getScrollElement: () => null, + scrollMargin: 40, + enabled: false, + isRtl: true, + isScrollingResetDelay: 300, + useScrollendEvent: true, + useAnimationFrameWithResizeObserver: true, + laneAssignmentMode: 'measured', + useCachedMeasurements: true, + debug: true, + measureElement: measurer, + }, + noop, + ) + expect(opts.scrollMargin).toBe(40) + expect(opts.enabled).toBe(false) + expect(opts.isRtl).toBe(true) + expect(opts.isScrollingResetDelay).toBe(300) + expect(opts.useScrollendEvent).toBe(true) + expect(opts.useAnimationFrameWithResizeObserver).toBe(true) + expect(opts.laneAssignmentMode).toBe('measured') + expect(opts.useCachedMeasurements).toBe(true) + expect(opts.debug).toBe(true) + expect(opts.measureElement).toBe(measurer) + }) + + test('full-parity batch stays undefined when omitted (core applies its defaults)', () => { + const opts = buildOptions({ count: 10, getScrollElement: () => null }, noop) + expect(opts.scrollMargin).toBeUndefined() + expect(opts.enabled).toBeUndefined() + expect(opts.isRtl).toBeUndefined() + expect(opts.isScrollingResetDelay).toBeUndefined() + expect(opts.useScrollendEvent).toBeUndefined() + expect(opts.useAnimationFrameWithResizeObserver).toBeUndefined() + expect(opts.laneAssignmentMode).toBeUndefined() + expect(opts.useCachedMeasurements).toBeUndefined() + expect(opts.debug).toBeUndefined() + expect(opts.measureElement).toBeUndefined() + }) + + test('core merge contract: undefined forwards land on core defaults; provided values stick', () => { + // The whole undefined-forwarding design leans on core's setOptions skipping + // undefined keys. Characterize that contract against a REAL core instance so a + // core-side change to the merge loop fails here, not in an example. + const bare = new Virtualizer( + buildOptions({ count: 10, getScrollElement: () => null }, noop), + ) + expect(bare.options.scrollMargin).toBe(0) + expect(bare.options.enabled).toBe(true) + expect(bare.options.isRtl).toBe(false) + expect(bare.options.isScrollingResetDelay).toBe(150) + expect(bare.options.useScrollendEvent).toBe(false) + expect(bare.options.useAnimationFrameWithResizeObserver).toBe(false) + expect(bare.options.laneAssignmentMode).toBe('estimate') + expect(bare.options.useCachedMeasurements).toBe(false) + expect(bare.options.debug).toBe(false) + expect(typeof bare.options.measureElement).toBe('function') // core's default measurer + + const measurer = () => 99 + const tuned = new Virtualizer( + buildOptions( + { + count: 10, + getScrollElement: () => null, + scrollMargin: 40, + enabled: false, + laneAssignmentMode: 'measured', + measureElement: measurer, + }, + noop, + ), + ) + expect(tuned.options.scrollMargin).toBe(40) + expect(tuned.options.enabled).toBe(false) + expect(tuned.options.laneAssignmentMode).toBe('measured') + expect(tuned.options.measureElement).toBe(measurer) + }) + + test('scrollMargin shifts the pre-mount slice positions (item.start includes the margin)', () => { + // Same transient-instance path renderSlice uses; proves the margin reaches the math. + const flat = new Virtualizer( + buildOptions( + { + count: 100, + estimateSize: () => 50, + getScrollElement: () => null, + initialRect: { width: 800, height: 400 }, + }, + noop, + ), + ) + const shifted = new Virtualizer( + buildOptions( + { + count: 100, + estimateSize: () => 50, + getScrollElement: () => null, + initialRect: { width: 800, height: 400 }, + scrollMargin: 200, + }, + noop, + ), + ) + expect(flat.getVirtualItems()[0]!.start).toBe(0) + expect(shifted.getVirtualItems()[0]!.start).toBe(200) + // getTotalSize subtracts scrollMargin (it is the LIST's own height, margin-free) + expect(shifted.getTotalSize()).toBe(flat.getTotalSize()) + }) +}) + +describe('buildOptions (window) — input to virtual-core option mapping', () => { + const noop = () => {} + + test('wires the window observers and scroll fn', () => { + const opts = buildWindowOptions({ count: 10 }, noop) + expect(opts.observeElementRect).toBe(observeWindowRect) + expect(opts.scrollToFn).toBe(windowScroll) + // window offset is derived internally (window.scrollY / 0), so it is a function, not a number + expect(typeof opts.initialOffset).toBe('function') + }) + + test('initialOffset override wins over the window.scrollY default', () => { + expect(buildWindowOptions({ count: 10, initialOffset: 4800 }, noop).initialOffset).toBe(4800) + const thunk = () => 1234 + expect(buildWindowOptions({ count: 10, initialOffset: thunk }, noop).initialOffset).toBe(thunk) + }) + + test('horizontal forwards; undefined when omitted (core defaults false)', () => { + expect(buildWindowOptions({ count: 10, horizontal: true }, noop).horizontal).toBe(true) + expect(buildWindowOptions({ count: 10 }, noop).horizontal).toBeUndefined() + }) + + test('maps every optional prop through when provided', () => { + const getItemKey = (i: number) => `k${i}` + const rangeExtractor = () => [0] + const cache = [ + { index: 0, start: 0, size: 32, end: 32, key: 0, lane: 0 }, + ] + const opts = buildWindowOptions( + { + count: 500, + estimateSize: () => 32, + overscan: 10, + paddingStart: 8, + paddingEnd: 16, + scrollPaddingStart: 4, + scrollPaddingEnd: 12, + gap: 6, + lanes: 3, + initialRect: { width: 1280, height: 800 }, + getItemKey, + rangeExtractor, + indexAttribute: 'data-col-index', + initialMeasurementsCache: cache, + anchorTo: 'end', + followOnAppend: true, + scrollEndThreshold: 24, + }, + noop, + ) + expect(opts.estimateSize(0)).toBe(32) + expect(opts.overscan).toBe(10) + expect(opts.paddingStart).toBe(8) + expect(opts.paddingEnd).toBe(16) + expect(opts.scrollPaddingStart).toBe(4) + expect(opts.scrollPaddingEnd).toBe(12) + expect(opts.gap).toBe(6) + expect(opts.lanes).toBe(3) + expect(opts.initialRect).toEqual({ width: 1280, height: 800 }) + expect(opts.getItemKey).toBe(getItemKey) + expect(opts.rangeExtractor).toBe(rangeExtractor) + expect(opts.indexAttribute).toBe('data-col-index') + expect(opts.initialMeasurementsCache).toBe(cache) + expect(opts.anchorTo).toBe('end') + expect(opts.followOnAppend).toBe(true) + expect(opts.scrollEndThreshold).toBe(24) + }) + + test('maps the full-parity batch through when provided', () => { + const measurer = () => 99 + const opts = buildWindowOptions( + { + count: 100, + scrollMargin: 40, + enabled: false, + isRtl: true, + isScrollingResetDelay: 300, + useScrollendEvent: true, + useAnimationFrameWithResizeObserver: true, + laneAssignmentMode: 'measured', + useCachedMeasurements: true, + debug: true, + measureElement: measurer, + }, + noop, + ) + expect(opts.scrollMargin).toBe(40) + expect(opts.enabled).toBe(false) + expect(opts.isRtl).toBe(true) + expect(opts.isScrollingResetDelay).toBe(300) + expect(opts.useScrollendEvent).toBe(true) + expect(opts.useAnimationFrameWithResizeObserver).toBe(true) + expect(opts.laneAssignmentMode).toBe('measured') + expect(opts.useCachedMeasurements).toBe(true) + expect(opts.debug).toBe(true) + expect(opts.measureElement).toBe(measurer) + }) + + test('full-parity batch stays undefined when omitted (core applies its defaults)', () => { + const opts = buildWindowOptions({ count: 10 }, noop) + expect(opts.scrollMargin).toBeUndefined() + expect(opts.enabled).toBeUndefined() + expect(opts.isRtl).toBeUndefined() + expect(opts.isScrollingResetDelay).toBeUndefined() + expect(opts.useScrollendEvent).toBeUndefined() + expect(opts.useAnimationFrameWithResizeObserver).toBeUndefined() + expect(opts.laneAssignmentMode).toBeUndefined() + expect(opts.useCachedMeasurements).toBeUndefined() + expect(opts.debug).toBeUndefined() + expect(opts.measureElement).toBeUndefined() + }) +}) diff --git a/packages/marko-virtual/tests/render-slice.test.ts b/packages/marko-virtual/tests/render-slice.test.ts new file mode 100644 index 000000000..611f5ac2d --- /dev/null +++ b/packages/marko-virtual/tests/render-slice.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it } from 'vitest' +import { renderSlice as renderSliceElement } from '../src/tags/virtualizer/options' +import { renderSlice as renderSliceWindow } from '../src/tags/window-virtualizer/options' + +// renderSlice is the render-time (server / pre-mount) seed: it builds a transient virtual-core +// instance, reads the initial window as plain data, and discards it. These tests pin its two +// contracts — the opt-in gate (no initialRect => trivial window, so non-SSR examples are +// unaffected) and the slice it produces when a viewport hint is given. + +describe('renderSlice — element virtualizer', () => { + const base = { + count: 1000, + estimateSize: () => 48, + getScrollElement: () => null, + } + + it('without initialRect returns the trivial window (client-only build parity)', () => { + const { items, size } = renderSliceElement(base) + expect(items).toEqual([]) + expect(size).toBe(0) + }) + + it('with initialRect paints a contiguous slice from index 0 and the full total size', () => { + const { items, size } = renderSliceElement({ + ...base, + initialRect: { width: 800, height: 400 }, + }) + expect(items.length).toBeGreaterThan(0) + expect(items[0]?.index).toBe(0) + expect(items[items.length - 1]?.index).toBe(items.length - 1) + expect(size).toBe(1000 * 48) + }) + + it('initialOffset shifts the slice away from the top', () => { + const rect = { width: 800, height: 400 } + const top = renderSliceElement({ ...base, initialRect: rect }) + const scrolled = renderSliceElement({ + ...base, + initialRect: rect, + initialOffset: 4800, + }) + expect(top.items[0]?.index).toBe(0) + expect(scrolled.items[0]?.index).toBeGreaterThan(0) + expect(scrolled.size).toBe(top.size) + }) + + it('range is null without initialRect and spans the slice with it', () => { + expect(renderSliceElement(base).range).toBeNull() + const { items, range } = renderSliceElement({ + ...base, + initialRect: { width: 800, height: 400 }, + }) + expect(range).not.toBeNull() + // The slice includes overscan beyond the visible range, so the range sits inside it. + expect(range!.startIndex).toBeGreaterThanOrEqual(items[0]!.index) + expect(range!.endIndex).toBeLessThanOrEqual(items[items.length - 1]!.index) + expect(range!.endIndex).toBeGreaterThanOrEqual(range!.startIndex) + }) + + it('rangeExtractor in the seed forces extra indexes into the slice (sticky pattern)', () => { + const { items } = renderSliceElement({ + ...base, + initialRect: { width: 800, height: 400 }, + initialOffset: 4800, // window starts around index 100, far from 0 + rangeExtractor: (r) => { + const next = new Set([0]) // the pinned index + for (let i = r.startIndex - r.overscan; i <= r.endIndex + r.overscan; i++) { + if (i >= 0 && i < r.count) next.add(i) + } + return [...next].sort((a, b) => a - b) + }, + }) + expect(items[0]?.index).toBe(0) // forced in even though scrolled away + expect(items[1]!.index).toBeGreaterThan(1) // the rest is the scrolled window + }) + + it('getItemKey flows into the seeded items', () => { + const { items } = renderSliceElement({ + ...base, + initialRect: { width: 800, height: 400 }, + getItemKey: (i) => `row-${i}`, + }) + expect(items[0]?.key).toBe('row-0') + }) + + it('anchorTo end alone does NOT move the seed; initialOffset paints the end (chat SSR)', () => { + // anchorTo is client-behavioral (follow-on-append, resize adjustment) and requires a live + // scroll element — it never positions the initial window. A chat page that wants the END + // painted into the server HTML passes initialOffset = totalSize - viewport. + const rect = { width: 800, height: 400 } + const anchoredOnly = renderSliceElement({ + ...base, + initialRect: rect, + anchorTo: 'end', + }) + expect(anchoredOnly.items[0]?.index).toBe(0) // still at the top + + const atEnd = renderSliceElement({ + ...base, + initialRect: rect, + anchorTo: 'end', + initialOffset: 1000 * 48 - 400, + }) + expect(atEnd.items[atEnd.items.length - 1]?.index).toBe(999) + expect(atEnd.size).toBe(1000 * 48) + }) +}) + +describe('renderSlice — window virtualizer', () => { + const base = { + count: 1000, + estimateSize: () => 48, + } + + it('without initialRect returns the trivial window', () => { + const { items, size } = renderSliceWindow(base) + expect(items).toEqual([]) + expect(size).toBe(0) + }) + + it('with initialRect paints a contiguous slice from index 0 and the full total size', () => { + const { items, size, range } = renderSliceWindow({ + ...base, + initialRect: { width: 1280, height: 800 }, + }) + expect(items.length).toBeGreaterThan(0) + expect(items[0]?.index).toBe(0) + expect(items[items.length - 1]?.index).toBe(items.length - 1) + expect(size).toBe(1000 * 48) + expect(range).not.toBeNull() + expect(range!.startIndex).toBe(0) + }) + + it('range is null without initialRect', () => { + expect(renderSliceWindow(base).range).toBeNull() + }) +}) diff --git a/packages/marko-virtual/tests/tags.test.ts b/packages/marko-virtual/tests/tags.test.ts index a53bcef2b..2b71fa082 100644 --- a/packages/marko-virtual/tests/tags.test.ts +++ b/packages/marko-virtual/tests/tags.test.ts @@ -28,12 +28,14 @@ import { waitFor } from '@testing-library/dom' import VirtualizerFixture from './fixtures/virtualizer-fixture.marko' import WindowVirtualizerFixture from './fixtures/window-virtualizer-fixture.marko' import CountUpdateFixture from './fixtures/count-update-fixture.marko' +import SurfaceFixture from './fixtures/surface-fixture.marko' // Cast to any — @marko/vite compiles .marko files as ES modules whose default // export is the template object with mount(input, container): Instance. const Virtualizer = VirtualizerFixture as any const WindowVirtualizer = WindowVirtualizerFixture as any const CountUpdate = CountUpdateFixture as any +const Surface = SurfaceFixture as any // --------------------------------------------------------------------------- // Test helpers @@ -276,3 +278,135 @@ describe('', () => { ) }) }) + +// --------------------------------------------------------------------------- +// Expanded return surface (range + methods) — surface-fixture.marko +// --------------------------------------------------------------------------- + +describe(' expanded return surface', () => { + test('range is populated after mount and matches the rendered window', async () => { + const el = mountFixture(Surface, { count: 100 }) + await waitFor(() => { + const start = el.querySelector("[data-testid='range-start']")?.textContent + const end = el.querySelector("[data-testid='range-end']")?.textContent + expect(start).toBe('0') + expect(Number(end)).toBeGreaterThan(0) + }) + // range excludes overscan; rendered items include it, so items >= range span + const end = Number( + el.querySelector("[data-testid='range-end']")?.textContent, + ) + const items = el.querySelectorAll("[data-testid='virtual-item']") + expect(items.length).toBeGreaterThanOrEqual(end + 1) + }) + + test('getItemKey flows through to the live items', async () => { + const el = mountFixture(Surface, { count: 100 }) + await waitFor(() => + expect( + el.querySelector("[data-testid='first-key']")?.textContent, + ).toBe('key-0'), + ) + }) + + test('isAtEnd() is false at the top of a long list', async () => { + const el = mountFixture(Surface, { count: 100 }) + await waitFor(() => + expect(el.querySelector("[data-testid='at-end']")?.textContent).toBe( + 'false', + ), + ) + }) + + test('resizeItem() resizes one row and the DOM reflects it', async () => { + const el = mountFixture(Surface, { count: 100 }) + await waitFor(() => + expect( + el.querySelectorAll("[data-testid='virtual-item']").length, + ).toBeGreaterThan(0), + ) + ;( + el.querySelector("[data-testid='do-resize']") as HTMLButtonElement + ).click() + await waitFor(() => { + const first = el + .querySelectorAll("[data-testid='virtual-item']") + .item(0) + expect(first?.getAttribute('data-size')).toBe('150') + }) + }) + + test('measure() after resizeItem() drops the cache back to estimates', async () => { + const el = mountFixture(Surface, { count: 100 }) + await waitFor(() => + expect( + el.querySelectorAll("[data-testid='virtual-item']").length, + ).toBeGreaterThan(0), + ) + ;( + el.querySelector("[data-testid='do-resize']") as HTMLButtonElement + ).click() + await waitFor(() => + expect( + el + .querySelectorAll("[data-testid='virtual-item']") + .item(0) + ?.getAttribute('data-size'), + ).toBe('150'), + ) + ;( + el.querySelector("[data-testid='do-measure']") as HTMLButtonElement + ).click() + await waitFor(() => + expect( + el + .querySelectorAll("[data-testid='virtual-item']") + .item(0) + ?.getAttribute('data-size'), + ).toBe('50'), + ) + }) + + test('scrollToEnd() does not throw (scroll effects are e2e territory)', async () => { + const el = mountFixture(Surface, { count: 100 }) + await waitFor(() => + expect( + el.querySelectorAll("[data-testid='virtual-item']").length, + ).toBeGreaterThan(0), + ) + expect(() => + ( + el.querySelector("[data-testid='do-scroll-end']") as HTMLButtonElement + ).click(), + ).not.toThrow() + }) + + test('rangeExtractor forces an extra index into the rendered window', async () => { + const el = mountFixture(Surface, { + count: 100, + rangeExtractor: (r: { + startIndex: number + endIndex: number + overscan: number + count: number + }) => { + const next = new Set([99]) // force the LAST index in while at the top + for ( + let i = Math.max(0, r.startIndex - r.overscan); + i <= Math.min(r.count - 1, r.endIndex + r.overscan); + i++ + ) { + next.add(i) + } + return [...next].sort((a, b) => a - b) + }, + }) + await waitFor(() => { + const indexes = [ + ...el.querySelectorAll("[data-testid='virtual-item']"), + ].map((n) => n.getAttribute('data-index')) + expect(indexes).toContain('99') + expect(indexes).toContain('0') + }) + }) +}) diff --git a/packages/marko-virtual/tsconfig.emit.json b/packages/marko-virtual/tsconfig.emit.json new file mode 100644 index 000000000..d3f95e397 --- /dev/null +++ b/packages/marko-virtual/tsconfig.emit.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.typecheck.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + // rootDir is the PACKAGES dir on purpose: the program includes + // ../virtual-core sources (via the paths mapping), and with the default + // inferred rootDir their outputs compute to outDir/../virtual-core/... — + // escaping the emit directory and littering the sibling package with .d.ts + // files. Rooting at ".." keeps every output inside outDir (and removes the + // TS6059 rootDir errors entirely). + "rootDir": "..", + "outDir": "./.d-marko-emit" + } +} diff --git a/packages/marko-virtual/tsconfig.typecheck.json b/packages/marko-virtual/tsconfig.typecheck.json new file mode 100644 index 000000000..db12f4a80 --- /dev/null +++ b/packages/marko-virtual/tsconfig.typecheck.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "paths": { "@tanstack/virtual-core": ["../virtual-core/src/index.ts"] }, + "types": ["node"], + "noEmit": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.marko", + "tests/**/*.ts", + "tests/**/*.marko", + "../virtual-core/src/**/*.ts" + ] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bf011f51c..de8ca9f66 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,7 @@ preferWorkspacePackages: true packages: - 'packages/*' + - 'packages/marko-virtual/e2e/*' - 'examples/angular/*' - 'examples/react/*' - 'examples/svelte/*' @@ -26,3 +27,11 @@ allowBuilds: # @tanstack/vue-store vue-demi: false # only required for vue 2 support + +overrides: + # Exactly one @marko/compiler in the graph. marko, @marko/vite, and + # @marko/language-tools (via @marko/type-check) all accept ^5.39/^5.40 ranges; + # letting the lockfile resolve them independently can split versions across + # entries, and two compiler instances in one process fail every .marko compile + # with "Unable to access Marko File outside of a compilation". + '@marko/compiler': 5.40.0 From 50f5eda120001640d7f3855ace06f96acbfd44eb Mon Sep 17 00:00:00 2001 From: defunkt-dev Date: Mon, 13 Jul 2026 15:07:52 -0600 Subject: [PATCH 2/6] refactor(marko-virtual): exports-based library packaging per Marko team guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit packaging changed to the canonical Marko library shape per Marko team guidance (marko.json exports → built dist/tags, no tags-dir/script-lang, mtc-built declarations, in-repo consumers resolve tags from source via nested marko.json), plus the sideEffects fix --- examples/marko/chat-pretext/marko.json | 2 +- examples/marko/chat-pretext/package.json | 5 +- examples/marko/chat-pretext/tsconfig.json | 3 +- examples/marko/chat/marko.json | 2 +- examples/marko/chat/package.json | 5 +- examples/marko/chat/tsconfig.json | 3 +- examples/marko/dynamic/marko.json | 2 +- examples/marko/dynamic/tsconfig.json | 3 +- examples/marko/fixed/marko.json | 2 +- examples/marko/fixed/tsconfig.json | 3 +- examples/marko/grid/marko.json | 2 +- examples/marko/grid/tsconfig.json | 3 +- examples/marko/infinite-scroll/marko.json | 2 +- examples/marko/infinite-scroll/tsconfig.json | 3 +- examples/marko/padding/marko.json | 2 +- examples/marko/padding/package.json | 5 +- examples/marko/padding/tsconfig.json | 3 +- examples/marko/pretext/marko.json | 2 +- examples/marko/pretext/package.json | 5 +- examples/marko/pretext/tsconfig.json | 3 +- examples/marko/scroll-padding/marko.json | 2 +- examples/marko/scroll-padding/package.json | 5 +- examples/marko/scroll-padding/tsconfig.json | 3 +- examples/marko/smooth-scroll/marko.json | 2 +- examples/marko/smooth-scroll/tsconfig.json | 3 +- examples/marko/ssr-fetch/marko.json | 2 +- examples/marko/ssr-fetch/package.json | 5 +- examples/marko/ssr-fetch/tsconfig.json | 3 +- examples/marko/ssr-restore/marko.json | 2 +- examples/marko/ssr-restore/package.json | 5 +- examples/marko/ssr-restore/tsconfig.json | 3 +- examples/marko/ssr-slice/marko.json | 2 +- examples/marko/ssr-slice/package.json | 5 +- examples/marko/ssr-slice/tsconfig.json | 3 +- examples/marko/ssr/marko.json | 2 +- examples/marko/ssr/package.json | 5 +- examples/marko/ssr/tsconfig.json | 3 +- examples/marko/sticky/marko.json | 2 +- examples/marko/sticky/package.json | 5 +- examples/marko/sticky/tsconfig.json | 3 +- examples/marko/table/marko.json | 2 +- examples/marko/table/package.json | 5 +- examples/marko/table/src/routes/+page.marko | 2 +- examples/marko/table/tsconfig.json | 3 +- examples/marko/variable/marko.json | 2 +- examples/marko/variable/tsconfig.json | 3 +- examples/marko/window-ssr-slice/marko.json | 2 +- examples/marko/window-ssr-slice/package.json | 5 +- examples/marko/window-ssr-slice/tsconfig.json | 3 +- examples/marko/window/marko.json | 2 +- examples/marko/window/tsconfig.json | 3 +- packages/marko-virtual/README.md | 8 +- .../marko-virtual/e2e/option-gates/marko.json | 2 +- .../e2e/option-gates/package.json | 5 +- .../e2e/option-gates/tsconfig.json | 3 +- packages/marko-virtual/marko.json | 2 +- packages/marko-virtual/package.json | 10 +- .../scripts/generate-d-marko.mjs | 128 ------------------ .../src/tags/virtualizer/index.d.marko | 56 -------- .../src/tags/window-virtualizer/index.d.marko | 55 -------- packages/marko-virtual/tests/marko.json | 1 + packages/marko-virtual/tsconfig.emit.json | 16 --- packages/marko-virtual/tsconfig.tags.json | 18 +++ 63 files changed, 143 insertions(+), 318 deletions(-) delete mode 100644 packages/marko-virtual/scripts/generate-d-marko.mjs delete mode 100644 packages/marko-virtual/src/tags/virtualizer/index.d.marko delete mode 100644 packages/marko-virtual/src/tags/window-virtualizer/index.d.marko create mode 100644 packages/marko-virtual/tests/marko.json delete mode 100644 packages/marko-virtual/tsconfig.emit.json create mode 100644 packages/marko-virtual/tsconfig.tags.json diff --git a/examples/marko/chat-pretext/marko.json b/examples/marko/chat-pretext/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/chat-pretext/marko.json +++ b/examples/marko/chat-pretext/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/chat-pretext/package.json b/examples/marko/chat-pretext/package.json index 06031dfab..6cf2b3f5a 100644 --- a/examples/marko/chat-pretext/package.json +++ b/examples/marko/chat-pretext/package.json @@ -6,7 +6,8 @@ "dev": "marko-run dev", "build": "marko-run build", "preview": "marko-run preview", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@chenglou/pretext": "^0.0.7", @@ -15,6 +16,8 @@ "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/examples/marko/chat-pretext/tsconfig.json b/examples/marko/chat-pretext/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/chat-pretext/tsconfig.json +++ b/examples/marko/chat-pretext/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/chat/marko.json b/examples/marko/chat/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/chat/marko.json +++ b/examples/marko/chat/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/chat/package.json b/examples/marko/chat/package.json index 59c0c9ad5..47091399a 100644 --- a/examples/marko/chat/package.json +++ b/examples/marko/chat/package.json @@ -6,7 +6,8 @@ "dev": "marko-run dev", "build": "marko-run build", "preview": "marko-run preview", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@marko/run": "^0.10.0", @@ -14,6 +15,8 @@ "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/examples/marko/chat/tsconfig.json b/examples/marko/chat/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/chat/tsconfig.json +++ b/examples/marko/chat/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/dynamic/marko.json b/examples/marko/dynamic/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/dynamic/marko.json +++ b/examples/marko/dynamic/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/dynamic/tsconfig.json b/examples/marko/dynamic/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/dynamic/tsconfig.json +++ b/examples/marko/dynamic/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/fixed/marko.json b/examples/marko/fixed/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/fixed/marko.json +++ b/examples/marko/fixed/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/fixed/tsconfig.json b/examples/marko/fixed/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/fixed/tsconfig.json +++ b/examples/marko/fixed/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/grid/marko.json b/examples/marko/grid/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/grid/marko.json +++ b/examples/marko/grid/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/grid/tsconfig.json b/examples/marko/grid/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/grid/tsconfig.json +++ b/examples/marko/grid/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/infinite-scroll/marko.json b/examples/marko/infinite-scroll/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/infinite-scroll/marko.json +++ b/examples/marko/infinite-scroll/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/infinite-scroll/tsconfig.json b/examples/marko/infinite-scroll/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/infinite-scroll/tsconfig.json +++ b/examples/marko/infinite-scroll/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/padding/marko.json b/examples/marko/padding/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/padding/marko.json +++ b/examples/marko/padding/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/padding/package.json b/examples/marko/padding/package.json index f54a53c4b..9ef54cdb6 100644 --- a/examples/marko/padding/package.json +++ b/examples/marko/padding/package.json @@ -6,7 +6,8 @@ "dev": "marko-run dev", "build": "marko-run build", "preview": "marko-run preview", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@marko/run": "^0.10.0", @@ -14,6 +15,8 @@ "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/examples/marko/padding/tsconfig.json b/examples/marko/padding/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/padding/tsconfig.json +++ b/examples/marko/padding/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/pretext/marko.json b/examples/marko/pretext/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/pretext/marko.json +++ b/examples/marko/pretext/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/pretext/package.json b/examples/marko/pretext/package.json index fefaa01a2..b56e6a489 100644 --- a/examples/marko/pretext/package.json +++ b/examples/marko/pretext/package.json @@ -6,7 +6,8 @@ "dev": "marko-run dev", "build": "marko-run build", "preview": "marko-run preview", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@chenglou/pretext": "^0.0.7", @@ -15,6 +16,8 @@ "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/examples/marko/pretext/tsconfig.json b/examples/marko/pretext/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/pretext/tsconfig.json +++ b/examples/marko/pretext/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/scroll-padding/marko.json b/examples/marko/scroll-padding/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/scroll-padding/marko.json +++ b/examples/marko/scroll-padding/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/scroll-padding/package.json b/examples/marko/scroll-padding/package.json index bf1aa4f03..d82c5388d 100644 --- a/examples/marko/scroll-padding/package.json +++ b/examples/marko/scroll-padding/package.json @@ -6,7 +6,8 @@ "dev": "marko-run dev", "build": "marko-run build", "preview": "marko-run preview", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@marko/run": "^0.10.0", @@ -14,6 +15,8 @@ "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/examples/marko/scroll-padding/tsconfig.json b/examples/marko/scroll-padding/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/scroll-padding/tsconfig.json +++ b/examples/marko/scroll-padding/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/smooth-scroll/marko.json b/examples/marko/smooth-scroll/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/smooth-scroll/marko.json +++ b/examples/marko/smooth-scroll/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/smooth-scroll/tsconfig.json b/examples/marko/smooth-scroll/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/smooth-scroll/tsconfig.json +++ b/examples/marko/smooth-scroll/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/ssr-fetch/marko.json b/examples/marko/ssr-fetch/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/ssr-fetch/marko.json +++ b/examples/marko/ssr-fetch/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/ssr-fetch/package.json b/examples/marko/ssr-fetch/package.json index 63763b91a..8f6180dbf 100644 --- a/examples/marko/ssr-fetch/package.json +++ b/examples/marko/ssr-fetch/package.json @@ -6,7 +6,8 @@ "dev": "marko-run dev", "build": "marko-run build", "preview": "marko-run preview", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@marko/run": "^0.10.0", @@ -14,6 +15,8 @@ "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/examples/marko/ssr-fetch/tsconfig.json b/examples/marko/ssr-fetch/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/ssr-fetch/tsconfig.json +++ b/examples/marko/ssr-fetch/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/ssr-restore/marko.json b/examples/marko/ssr-restore/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/ssr-restore/marko.json +++ b/examples/marko/ssr-restore/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/ssr-restore/package.json b/examples/marko/ssr-restore/package.json index 3bc6b1721..34e502910 100644 --- a/examples/marko/ssr-restore/package.json +++ b/examples/marko/ssr-restore/package.json @@ -6,7 +6,8 @@ "dev": "marko-run dev", "build": "marko-run build", "preview": "marko-run preview", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@marko/run": "^0.10.0", @@ -14,6 +15,8 @@ "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/examples/marko/ssr-restore/tsconfig.json b/examples/marko/ssr-restore/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/ssr-restore/tsconfig.json +++ b/examples/marko/ssr-restore/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/ssr-slice/marko.json b/examples/marko/ssr-slice/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/ssr-slice/marko.json +++ b/examples/marko/ssr-slice/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/ssr-slice/package.json b/examples/marko/ssr-slice/package.json index 5afa760a0..f6aa49249 100644 --- a/examples/marko/ssr-slice/package.json +++ b/examples/marko/ssr-slice/package.json @@ -6,7 +6,8 @@ "dev": "marko-run dev", "build": "marko-run build", "preview": "marko-run preview", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@marko/run": "^0.10.0", @@ -14,6 +15,8 @@ "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/examples/marko/ssr-slice/tsconfig.json b/examples/marko/ssr-slice/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/ssr-slice/tsconfig.json +++ b/examples/marko/ssr-slice/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/ssr/marko.json b/examples/marko/ssr/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/ssr/marko.json +++ b/examples/marko/ssr/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/ssr/package.json b/examples/marko/ssr/package.json index 3ea1d8a6f..50367f46f 100644 --- a/examples/marko/ssr/package.json +++ b/examples/marko/ssr/package.json @@ -6,7 +6,8 @@ "dev": "marko-run dev", "build": "marko-run build", "preview": "marko-run preview", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@marko/run": "^0.10.0", @@ -14,6 +15,8 @@ "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/examples/marko/ssr/tsconfig.json b/examples/marko/ssr/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/ssr/tsconfig.json +++ b/examples/marko/ssr/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/sticky/marko.json b/examples/marko/sticky/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/sticky/marko.json +++ b/examples/marko/sticky/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/sticky/package.json b/examples/marko/sticky/package.json index 3e325e5f9..1199e5d19 100644 --- a/examples/marko/sticky/package.json +++ b/examples/marko/sticky/package.json @@ -6,7 +6,8 @@ "dev": "marko-run dev", "build": "marko-run build", "preview": "marko-run preview", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@marko/run": "^0.10.0", @@ -14,6 +15,8 @@ "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/examples/marko/sticky/tsconfig.json b/examples/marko/sticky/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/sticky/tsconfig.json +++ b/examples/marko/sticky/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/table/marko.json b/examples/marko/table/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/table/marko.json +++ b/examples/marko/table/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/table/package.json b/examples/marko/table/package.json index 398a8ded3..cf65a98b2 100644 --- a/examples/marko/table/package.json +++ b/examples/marko/table/package.json @@ -6,7 +6,8 @@ "dev": "marko-run dev", "build": "marko-run build", "preview": "marko-run preview", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@marko/run": "^0.10.0", @@ -14,6 +15,8 @@ "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/examples/marko/table/src/routes/+page.marko b/examples/marko/table/src/routes/+page.marko index bff07e5b7..6abf746d6 100644 --- a/examples/marko/table/src/routes/+page.marko +++ b/examples/marko/table/src/routes/+page.marko @@ -104,7 +104,7 @@ static const ROW_HEIGHT = 34 ROW_HEIGHT - getScrollElement=() => scrollEl() + getScrollElement=(): Element | null => scrollEl() ?? null overscan=20 /> diff --git a/examples/marko/table/tsconfig.json b/examples/marko/table/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/table/tsconfig.json +++ b/examples/marko/table/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/variable/marko.json b/examples/marko/variable/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/variable/marko.json +++ b/examples/marko/variable/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/variable/tsconfig.json b/examples/marko/variable/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/variable/tsconfig.json +++ b/examples/marko/variable/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/window-ssr-slice/marko.json b/examples/marko/window-ssr-slice/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/window-ssr-slice/marko.json +++ b/examples/marko/window-ssr-slice/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/window-ssr-slice/package.json b/examples/marko/window-ssr-slice/package.json index 6562f68cd..c35e04b9d 100644 --- a/examples/marko/window-ssr-slice/package.json +++ b/examples/marko/window-ssr-slice/package.json @@ -6,7 +6,8 @@ "dev": "marko-run dev", "build": "marko-run build", "preview": "marko-run preview", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@marko/run": "^0.10.0", @@ -14,6 +15,8 @@ "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/examples/marko/window-ssr-slice/tsconfig.json b/examples/marko/window-ssr-slice/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/window-ssr-slice/tsconfig.json +++ b/examples/marko/window-ssr-slice/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/examples/marko/window/marko.json b/examples/marko/window/marko.json index 5fbb58877..6f919f11b 100644 --- a/examples/marko/window/marko.json +++ b/examples/marko/window/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } +{ "tags-dir": "../../../packages/marko-virtual/src/tags" } diff --git a/examples/marko/window/tsconfig.json b/examples/marko/window/tsconfig.json index ca908ab1d..382ad5955 100644 --- a/examples/marko/window/tsconfig.json +++ b/examples/marko/window/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../../packages/marko-virtual/src/tags/**/*" ] } diff --git a/packages/marko-virtual/README.md b/packages/marko-virtual/README.md index 92c40592b..22115916d 100644 --- a/packages/marko-virtual/README.md +++ b/packages/marko-virtual/README.md @@ -311,9 +311,11 @@ pnpm -r --workspace-concurrency=1 --filter "./examples/marko/*" run test:e2e _Note:_ This has to be run sequentially as they all start the same port. _Note:_ All suites run against **dev servers**; production builds are not exercised by -any test. After bumping `marko` (or other build-path dependencies), sanity-check a -production build manually: `pnpm --filter build` then `preview`, and click -through the chat example's Stream reply. +any test. After bumping `marko` (or other build-path dependencies) or changing the +package's build/packaging config, sanity-check production builds manually: +`pnpm --filter build` then `preview` — and check **both** kinds of example: +`fixed` (render-only — rows must appear; this is the case production tree-shaking can +silently break) and `chat` (interactive — click through Stream reply). ### Running all unit tests To run all the unit tests specifically for `marko-virtual/tests` use the command diff --git a/packages/marko-virtual/e2e/option-gates/marko.json b/packages/marko-virtual/e2e/option-gates/marko.json index 98e47d290..b4b207868 100644 --- a/packages/marko-virtual/e2e/option-gates/marko.json +++ b/packages/marko-virtual/e2e/option-gates/marko.json @@ -1 +1 @@ -{ "taglib-imports": ["../../marko.json"] } +{ "tags-dir": "../../src/tags" } diff --git a/packages/marko-virtual/e2e/option-gates/package.json b/packages/marko-virtual/e2e/option-gates/package.json index eeaf27fc1..76cc4c4da 100644 --- a/packages/marko-virtual/e2e/option-gates/package.json +++ b/packages/marko-virtual/e2e/option-gates/package.json @@ -4,13 +4,16 @@ "type": "module", "scripts": { "dev": "marko-run dev", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:types": "marko-type-check" }, "dependencies": { "@marko/run": "^0.10.0", "marko": "^6.2.2" }, "devDependencies": { + "@marko/language-tools": "^2.6.2", + "@marko/type-check": "3.1.1", "@playwright/test": "^1.53.1", "typescript": "5.6.3", "vite": "^6.4.2" diff --git a/packages/marko-virtual/e2e/option-gates/tsconfig.json b/packages/marko-virtual/e2e/option-gates/tsconfig.json index ca908ab1d..39dfab224 100644 --- a/packages/marko-virtual/e2e/option-gates/tsconfig.json +++ b/packages/marko-virtual/e2e/option-gates/tsconfig.json @@ -19,6 +19,7 @@ "src", "e2e", "playwright.config.ts", - "vite.config.ts" + "vite.config.ts", + "../../src/tags/**/*" ] } diff --git a/packages/marko-virtual/marko.json b/packages/marko-virtual/marko.json index 82bf8ffd5..bc929c34a 100644 --- a/packages/marko-virtual/marko.json +++ b/packages/marko-virtual/marko.json @@ -1 +1 @@ -{ "tags-dir": "./src/tags", "script-lang": "ts" } +{ "exports": "./dist/tags" } diff --git a/packages/marko-virtual/package.json b/packages/marko-virtual/package.json index 76448ed32..e8c47617d 100644 --- a/packages/marko-virtual/package.json +++ b/packages/marko-virtual/package.json @@ -38,12 +38,13 @@ }, "./package.json": "./package.json", "./marko.json": "./marko.json", - "./src/tags/*": "./src/tags/*" + "./dist/tags/*": "./dist/tags/*" }, - "sideEffects": false, + "sideEffects": [ + "**/*.marko" + ], "files": [ "dist", - "src/tags", "marko.json", "README.md" ], @@ -54,8 +55,7 @@ "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict", - "build": "vite build", - "types:generate": "node scripts/generate-d-marko.mjs" + "build": "vite build && marko-type-check -p tsconfig.tags.json" }, "dependencies": { "@tanstack/virtual-core": "workspace:*" diff --git a/packages/marko-virtual/scripts/generate-d-marko.mjs b/packages/marko-virtual/scripts/generate-d-marko.mjs deleted file mode 100644 index 4a73e0a53..000000000 --- a/packages/marko-virtual/scripts/generate-d-marko.mjs +++ /dev/null @@ -1,128 +0,0 @@ -// Regenerates the committed .d.marko files from the tag sources via -// @marko/type-check (mtc). Manual, on-demand: run `pnpm types:generate` after -// changing a tag's Input or return surface. CI never generates — it only -// VERIFIES (test:types) that the committed .d.marko still agree with the tags. -// -// Pipeline: -// 1. mtc checker must pass on the current sources (don't generate from -// broken types). -// 2. Back up and delete the existing .d.marko (mtc prefers them as input when -// present — leaving them would regenerate from the old files, not the -// tags). The backups are restored automatically on any failure. -// 3. Emit via tsconfig.emit.json (output captured; only shown on failure). -// 4. Locate the emitted index.d.marko for each tag by searching the emit -// directory (output layout depends on TypeScript's rootDir handling, so -// search rather than hardcode), copy them back beside their sources, and -// remove the emit directory. -// 5. mtc checker must pass again with the regenerated files in place. -import { execSync } from 'node:child_process' -import { - cpSync, - existsSync, - mkdtempSync, - readdirSync, - rmSync, -} from 'node:fs' -import { tmpdir } from 'node:os' -import { dirname, join, relative } from 'node:path' -import { fileURLToPath } from 'node:url' - -const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..') -const emitDir = join(pkgRoot, '.d-marko-emit') -const tags = ['virtualizer', 'window-virtualizer'] -const dMarkoPath = (tag) => join(pkgRoot, 'src/tags', tag, 'index.d.marko') - -const backupDir = mkdtempSync(join(tmpdir(), 'd-marko-backup-')) - -const restoreBackups = () => { - for (const tag of tags) { - const backup = join(backupDir, `${tag}.d.marko`) - if (existsSync(backup)) cpSync(backup, dMarkoPath(tag)) - } -} - -const walk = (dir, out = []) => { - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const full = join(dir, entry.name) - if (entry.isDirectory()) walk(full, out) - else out.push(full) - } - return out -} - -const fail = (message, emitOutput) => { - console.error(`\n[types:generate] FAILED: ${message}`) - if (existsSync(emitDir)) { - const files = walk(emitDir).map((f) => ` ${relative(pkgRoot, f)}`) - console.error( - files.length - ? `Emit directory contents:\n${files.join('\n')}` - : 'Emit directory is EMPTY.', - ) - } else { - console.error('Emit directory was never created.') - } - if (emitOutput?.trim()) console.error(`\nEmit output:\n${emitOutput}`) - console.error('\nThe previous .d.marko files have been restored.') - restoreBackups() - rmSync(emitDir, { recursive: true, force: true }) - rmSync(backupDir, { recursive: true, force: true }) - process.exit(1) -} - -const run = (cmd) => execSync(cmd, { cwd: pkgRoot, stdio: 'inherit' }) - -const check = (label) => { - console.log(`\n[types:generate] ${label}`) - try { - run('mtc -p tsconfig.typecheck.json') - } catch { - fail(`type check failed (${label}) — see output above.`) - } -} - -check('1/5 verifying current sources type-check') - -console.log( - '\n[types:generate] 2/5 backing up and removing existing .d.marko (mtc would prefer them as input)', -) -for (const tag of tags) { - if (existsSync(dMarkoPath(tag))) { - cpSync(dMarkoPath(tag), join(backupDir, `${tag}.d.marko`)) - rmSync(dMarkoPath(tag)) - } -} - -console.log('\n[types:generate] 3/5 emitting') -rmSync(emitDir, { recursive: true, force: true }) -let emitOutput = '' -try { - // Captured, not streamed: benign config-shape complaints in emit mode read - // like failures. Real problems are caught by the emitted-file search below - // and the re-verification in step 5; the captured output is printed then. - emitOutput = execSync('mtc -p tsconfig.emit.json', { - cwd: pkgRoot, - stdio: ['ignore', 'pipe', 'pipe'], - encoding: 'utf8', - }) -} catch (err) { - emitOutput = `${err.stdout ?? ''}${err.stderr ?? ''}` -} - -console.log('\n[types:generate] 4/5 copying generated files back') -if (!existsSync(emitDir)) fail('nothing was emitted.', emitOutput) -const emittedFiles = walk(emitDir) -for (const tag of tags) { - // The emitted path mirrors TypeScript's rootDir handling, which can differ - // by environment — search for the file rather than hardcoding its depth. - const suffix = join('tags', tag, 'index.d.marko') - const emitted = emittedFiles.find((f) => f.endsWith(suffix)) - if (!emitted) fail(`no emitted index.d.marko for "${tag}".`, emitOutput) - cpSync(emitted, dMarkoPath(tag)) - console.log(` ${tag}/index.d.marko regenerated`) -} -rmSync(emitDir, { recursive: true, force: true }) -rmSync(backupDir, { recursive: true, force: true }) - -check('5/5 verifying the regenerated files') -console.log('\n[types:generate] done — review the diff and commit.') diff --git a/packages/marko-virtual/src/tags/virtualizer/index.d.marko b/packages/marko-virtual/src/tags/virtualizer/index.d.marko deleted file mode 100644 index cec5b453c..000000000 --- a/packages/marko-virtual/src/tags/virtualizer/index.d.marko +++ /dev/null @@ -1,56 +0,0 @@ -import { Virtualizer } from "@tanstack/virtual-core"; -import type { Range, VirtualItem, ScrollToOptions } from "@tanstack/virtual-core"; -export interface VirtualizerHandle { - virtualItems: VirtualItem[]; - totalSize: number; - range: { - startIndex: number; - endIndex: number; - } | null; - measureElement: ((el: Element | null) => void) | undefined; - scrollToIndex: (index: number, options?: ScrollToOptions) => void; - scrollToOffset: (offset: number, options?: ScrollToOptions) => void; - measure: () => void; - resizeItem: (index: number, size: number) => void; - scrollToEnd: (options?: { - behavior?: ScrollToOptions['behavior']; - }) => void; - isAtEnd: (threshold?: number) => boolean; - getDistanceFromEnd: () => number; -} -export interface Input { - count: number; - estimateSize?: (index: number) => number; - getScrollElement: () => Element | null; - overscan?: number; - horizontal?: boolean; - paddingStart?: number; - paddingEnd?: number; - scrollPaddingStart?: number; - scrollPaddingEnd?: number; - gap?: number; - lanes?: number; - initialOffset?: number | (() => number); - initialRect?: { - width: number; - height: number; - }; - getItemKey?: (index: number) => number | string | bigint; - rangeExtractor?: (range: Range) => number[]; - indexAttribute?: string; - initialMeasurementsCache?: VirtualItem[]; - anchorTo?: 'start' | 'end'; - followOnAppend?: boolean | ScrollBehavior; - scrollEndThreshold?: number; - scrollMargin?: number; - enabled?: boolean; - isRtl?: boolean; - isScrollingResetDelay?: number; - useScrollendEvent?: boolean; - useAnimationFrameWithResizeObserver?: boolean; - laneAssignmentMode?: 'estimate' | 'measured'; - useCachedMeasurements?: boolean; - debug?: boolean; - measureElement?: (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer) => number; -} - diff --git a/packages/marko-virtual/src/tags/window-virtualizer/index.d.marko b/packages/marko-virtual/src/tags/window-virtualizer/index.d.marko deleted file mode 100644 index 6f06f4ceb..000000000 --- a/packages/marko-virtual/src/tags/window-virtualizer/index.d.marko +++ /dev/null @@ -1,55 +0,0 @@ -import { Virtualizer } from "@tanstack/virtual-core"; -import type { Range, VirtualItem, ScrollToOptions } from "@tanstack/virtual-core"; -export interface WindowVirtualizerHandle { - virtualItems: VirtualItem[]; - totalSize: number; - range: { - startIndex: number; - endIndex: number; - } | null; - measureElement: ((el: Element | null) => void) | undefined; - scrollToIndex: (index: number, options?: ScrollToOptions) => void; - scrollToOffset: (offset: number, options?: ScrollToOptions) => void; - measure: () => void; - resizeItem: (index: number, size: number) => void; - scrollToEnd: (options?: { - behavior?: ScrollToOptions['behavior']; - }) => void; - isAtEnd: (threshold?: number) => boolean; - getDistanceFromEnd: () => number; -} -export interface Input { - count: number; - estimateSize?: (index: number) => number; - overscan?: number; - horizontal?: boolean; - paddingStart?: number; - paddingEnd?: number; - scrollPaddingStart?: number; - scrollPaddingEnd?: number; - gap?: number; - lanes?: number; - initialOffset?: number | (() => number); - initialRect?: { - width: number; - height: number; - }; - getItemKey?: (index: number) => number | string | bigint; - rangeExtractor?: (range: Range) => number[]; - indexAttribute?: string; - initialMeasurementsCache?: VirtualItem[]; - anchorTo?: 'start' | 'end'; - followOnAppend?: boolean | ScrollBehavior; - scrollEndThreshold?: number; - scrollMargin?: number; - enabled?: boolean; - isRtl?: boolean; - isScrollingResetDelay?: number; - useScrollendEvent?: boolean; - useAnimationFrameWithResizeObserver?: boolean; - laneAssignmentMode?: 'estimate' | 'measured'; - useCachedMeasurements?: boolean; - debug?: boolean; - measureElement?: (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer) => number; -} - diff --git a/packages/marko-virtual/tests/marko.json b/packages/marko-virtual/tests/marko.json new file mode 100644 index 000000000..31e34666e --- /dev/null +++ b/packages/marko-virtual/tests/marko.json @@ -0,0 +1 @@ +{ "tags-dir": "../src/tags" } diff --git a/packages/marko-virtual/tsconfig.emit.json b/packages/marko-virtual/tsconfig.emit.json deleted file mode 100644 index d3f95e397..000000000 --- a/packages/marko-virtual/tsconfig.emit.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "./tsconfig.typecheck.json", - "compilerOptions": { - "noEmit": false, - "declaration": true, - "emitDeclarationOnly": true, - // rootDir is the PACKAGES dir on purpose: the program includes - // ../virtual-core sources (via the paths mapping), and with the default - // inferred rootDir their outputs compute to outDir/../virtual-core/... — - // escaping the emit directory and littering the sibling package with .d.ts - // files. Rooting at ".." keeps every output inside outDir (and removes the - // TS6059 rootDir errors entirely). - "rootDir": "..", - "outDir": "./.d-marko-emit" - } -} diff --git a/packages/marko-virtual/tsconfig.tags.json b/packages/marko-virtual/tsconfig.tags.json new file mode 100644 index 000000000..e72ac6808 --- /dev/null +++ b/packages/marko-virtual/tsconfig.tags.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "moduleResolution": "Bundler", + "types": [ + "node" + ], + "noEmit": false, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "tsBuildInfoFile": "./dist/tsconfig.tags.tsbuildinfo" + }, + "include": [ + "src/tags/**/*.marko", + "src/tags/**/*.ts" + ] +} From c75014ced0140070dc92aa2979f6d569708748a5 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:32:02 +0000 Subject: [PATCH 3/6] ci: apply automated fixes --- .../marko/chat-pretext/.marko-run/routes.d.ts | 101 ++-- .../chat-pretext/e2e/chat-pretext.spec.ts | 51 +- examples/marko/chat-pretext/tsconfig.json | 6 +- examples/marko/chat/.marko-run/routes.d.ts | 101 ++-- examples/marko/chat/e2e/chat.spec.ts | 31 +- examples/marko/chat/tsconfig.json | 6 +- examples/marko/dynamic/.marko-run/routes.d.ts | 63 ++- examples/marko/dynamic/e2e/dynamic.spec.ts | 6 +- examples/marko/dynamic/tsconfig.json | 6 +- examples/marko/fixed/.marko-run/routes.d.ts | 63 ++- examples/marko/fixed/e2e/fixed.spec.ts | 4 +- examples/marko/fixed/tsconfig.json | 6 +- examples/marko/grid/.marko-run/routes.d.ts | 63 ++- examples/marko/grid/tsconfig.json | 6 +- .../infinite-scroll/.marko-run/routes.d.ts | 63 ++- .../e2e/infinite-scroll.spec.ts | 20 +- examples/marko/infinite-scroll/tsconfig.json | 6 +- examples/marko/padding/.marko-run/routes.d.ts | 63 ++- examples/marko/padding/e2e/padding.spec.ts | 53 +- examples/marko/padding/tsconfig.json | 6 +- examples/marko/pretext/.marko-run/routes.d.ts | 63 ++- examples/marko/pretext/e2e/pretext.spec.ts | 16 +- examples/marko/pretext/tsconfig.json | 6 +- .../scroll-padding/.marko-run/routes.d.ts | 63 ++- .../scroll-padding/e2e/scroll-padding.spec.ts | 8 +- examples/marko/scroll-padding/tsconfig.json | 6 +- .../smooth-scroll/.marko-run/routes.d.ts | 63 ++- examples/marko/smooth-scroll/tsconfig.json | 6 +- .../marko/ssr-fetch/.marko-run/routes.d.ts | 63 ++- .../marko/ssr-fetch/e2e/ssr-fetch.spec.ts | 12 +- examples/marko/ssr-fetch/src/data.ts | 4 +- examples/marko/ssr-fetch/tsconfig.json | 6 +- .../marko/ssr-restore/.marko-run/routes.d.ts | 63 ++- .../marko/ssr-restore/e2e/ssr-restore.spec.ts | 16 +- examples/marko/ssr-restore/src/data.ts | 4 +- examples/marko/ssr-restore/tsconfig.json | 6 +- .../marko/ssr-slice/.marko-run/routes.d.ts | 63 ++- .../marko/ssr-slice/e2e/ssr-slice.spec.ts | 16 +- examples/marko/ssr-slice/src/data.ts | 4 +- examples/marko/ssr-slice/tsconfig.json | 6 +- examples/marko/ssr/.marko-run/routes.d.ts | 63 ++- examples/marko/ssr/tsconfig.json | 6 +- examples/marko/sticky/.marko-run/routes.d.ts | 63 ++- examples/marko/sticky/e2e/sticky.spec.ts | 20 +- examples/marko/sticky/tsconfig.json | 6 +- examples/marko/table/.marko-run/routes.d.ts | 63 ++- examples/marko/table/e2e/table.spec.ts | 29 +- examples/marko/table/tsconfig.json | 6 +- .../marko/variable/.marko-run/routes.d.ts | 63 ++- examples/marko/variable/e2e/variable.spec.ts | 4 +- examples/marko/variable/tsconfig.json | 6 +- .../window-ssr-slice/.marko-run/routes.d.ts | 63 ++- .../e2e/window-ssr-slice.spec.ts | 16 +- examples/marko/window-ssr-slice/src/data.ts | 4 +- examples/marko/window-ssr-slice/tsconfig.json | 6 +- examples/marko/window/.marko-run/routes.d.ts | 63 ++- examples/marko/window/e2e/window.spec.ts | 8 +- examples/marko/window/tsconfig.json | 6 +- packages/marko-virtual/README.md | 137 ++--- .../e2e/option-gates/.marko-run/routes.d.ts | 481 +++++++++++------- .../e2e/option-gates/e2e/option-gates.spec.ts | 42 +- .../e2e/option-gates/tsconfig.json | 6 +- .../src/tags/virtualizer/options.ts | 13 +- .../src/tags/window-virtualizer/options.ts | 13 +- packages/marko-virtual/tests/options.test.ts | 28 +- .../marko-virtual/tests/render-slice.test.ts | 6 +- packages/marko-virtual/tests/tags.test.ts | 10 +- packages/marko-virtual/tsconfig.tags.json | 9 +- 68 files changed, 1528 insertions(+), 930 deletions(-) diff --git a/examples/marko/chat-pretext/.marko-run/routes.d.ts b/examples/marko/chat-pretext/.marko-run/routes.d.ts index c51777cf4..89bb36bd6 100644 --- a/examples/marko/chat-pretext/.marko-run/routes.d.ts +++ b/examples/marko/chat-pretext/.marko-run/routes.d.ts @@ -3,51 +3,80 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - "/api/reply": { verb: "post"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + '/api/reply': { verb: 'post' } + } + }> {} } -declare module "../src/routes/api/reply/+handler" { +declare module '../src/routes/api/reply/+handler' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/api/reply"]; - export type Context = Run.MultiRouteContext; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/api/reply'] + export type Context = Run.MultiRouteContext + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/chat-pretext/e2e/chat-pretext.spec.ts b/examples/marko/chat-pretext/e2e/chat-pretext.spec.ts index ab7ceb555..91062b457 100644 --- a/examples/marko/chat-pretext/e2e/chat-pretext.spec.ts +++ b/examples/marko/chat-pretext/e2e/chat-pretext.spec.ts @@ -43,7 +43,9 @@ async function waitForPin(page: Page) { .toBeLessThanOrEqual(AT_END_PX) } -test('loads pinned to the newest message with status At latest', async ({ page }) => { +test('loads pinned to the newest message with status At latest', async ({ + page, +}) => { await page.goto('/') await waitForPin(page) await expect(page.locator('[data-key="message-44"]')).toBeVisible() @@ -68,7 +70,9 @@ test('does NOT follow appends while reading history', async ({ page }) => { await page.locator('.messages').evaluate((el) => { el.scrollTop = Math.floor(el.scrollHeight / 2) }) - await expect(page.locator('[data-testid="status"]')).toHaveText('Reading history') + await expect(page.locator('[data-testid="status"]')).toHaveText( + 'Reading history', + ) const before = await page.locator('.messages').evaluate((el) => el.scrollTop) await page.locator('[data-testid="add-message"]').click() await page.waitForTimeout(400) @@ -88,13 +92,17 @@ test('prepending history keeps the visible message anchored in place', async ({ await page.locator('.messages').evaluate((el) => { el.scrollTop = Math.floor(el.scrollHeight / 2) }) - await expect(page.locator('[data-testid="status"]')).toHaveText('Reading history') + await expect(page.locator('[data-testid="status"]')).toHaveText( + 'Reading history', + ) // pick whatever message is currently visible near the viewport top and record it const anchorKey = await page.evaluate(() => { const el = document.querySelector('.messages')! const top = el.getBoundingClientRect().top - const rows = Array.from(document.querySelectorAll('.message-row')) + const rows = Array.from( + document.querySelectorAll('.message-row'), + ) const visible = rows.find((row) => { const box = row.getBoundingClientRect() return box.top >= top && box.top < top + 200 @@ -111,7 +119,9 @@ test('prepending history keeps the visible message anchored in place', async ({ .evaluate((el) => el.scrollHeight) await page.locator('[data-testid="load-older"]').click() - await expect(page.locator('[data-testid="status"]')).toHaveText('Loading history') + await expect(page.locator('[data-testid="status"]')).toHaveText( + 'Loading history', + ) // The prepended rows are far above the render window here, so they are // (correctly) virtualized away — DOM attachment is the wrong signal. The prepend // landing is observable as total content growth. @@ -155,7 +165,9 @@ test('Latest returns to the bottom and status flips back to At latest', async ({ await page.locator('.messages').evaluate((el) => { el.scrollTop = 0 }) - await expect(page.locator('[data-testid="status"]')).toHaveText(/Reading history|Loading history/) + await expect(page.locator('[data-testid="status"]')).toHaveText( + /Reading history|Loading history/, + ) await page.locator('[data-testid="latest"]').click() await waitForPin(page) await expect(page.locator('[data-testid="status"]')).toHaveText('At latest') @@ -168,7 +180,8 @@ test('a reply streamed from the server grows progressively and stays pinned', as await page.goto('/') await waitForPin(page) const replyResponse = page.waitForResponse( - (response) => response.url().endsWith('/api/reply') && response.status() === 200, + (response) => + response.url().endsWith('/api/reply') && response.status() === 200, ) await page.locator('[data-testid="stream-reply"]').click() await replyResponse @@ -177,14 +190,18 @@ test('a reply streamed from the server grows progressively and stays pinned', as await expect(streamed).toContainText('Thinking through the failure mode.') const early = (await streamed.textContent()) ?? '' expect(early).not.toContain('drifting off the bottom') - await expect(streamed).toContainText('drifting off the bottom', { timeout: 5000 }) + await expect(streamed).toContainText('drifting off the bottom', { + timeout: 5000, + }) await waitForPin(page) await expect(page.locator('[data-testid="status"]')).toHaveText('At latest') }) // ---- The two assertions this example exists for ---- -test('prepending history causes NO measurement-correction kick', async ({ page }) => { +test('prepending history causes NO measurement-correction kick', async ({ + page, +}) => { await page.goto('/') await waitForPin(page) // Park just OUTSIDE the load-ahead auto-trigger zone (~720px = 1.5 viewport @@ -205,7 +222,9 @@ test('prepending history causes NO measurement-correction kick', async ({ page } const el = document.querySelector('.messages') as HTMLElement const samples: number[] = [] const id = setInterval(() => samples.push(el.scrollTop), 8) - ;(document.querySelector('[data-testid="load-older"]') as HTMLElement).click() + ;( + document.querySelector('[data-testid="load-older"]') as HTMLElement + ).click() await new Promise((resolve) => setTimeout(resolve, 1200)) clearInterval(id) const positive: number[] = [] @@ -238,9 +257,15 @@ test('the streamed reply grows through v.resizeItem and calculated height matche // full reply: it must have GROWN — that growth is delivered by v.resizeItem per // chunk, since this example has no measureElement to observe the DOM. await expect(streamed).toContainText('Thinking through the failure mode.') - const earlyHeight = await streamed.evaluate((el) => parseFloat((el as HTMLElement).style.height)) - await expect(streamed).toContainText('drifting off the bottom', { timeout: 5000 }) - const finalHeight = await streamed.evaluate((el) => parseFloat((el as HTMLElement).style.height)) + const earlyHeight = await streamed.evaluate((el) => + parseFloat((el as HTMLElement).style.height), + ) + await expect(streamed).toContainText('drifting off the bottom', { + timeout: 5000, + }) + const finalHeight = await streamed.evaluate((el) => + parseFloat((el as HTMLElement).style.height), + ) expect(finalHeight).toBeGreaterThan(earlyHeight) // Calculated height is REAL height: the bubble's rendered box must fit the row // exactly (content height == calculated row height, within a pixel). diff --git a/examples/marko/chat-pretext/tsconfig.json b/examples/marko/chat-pretext/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/chat-pretext/tsconfig.json +++ b/examples/marko/chat-pretext/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/chat/.marko-run/routes.d.ts b/examples/marko/chat/.marko-run/routes.d.ts index c51777cf4..89bb36bd6 100644 --- a/examples/marko/chat/.marko-run/routes.d.ts +++ b/examples/marko/chat/.marko-run/routes.d.ts @@ -3,51 +3,80 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - "/api/reply": { verb: "post"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + '/api/reply': { verb: 'post' } + } + }> {} } -declare module "../src/routes/api/reply/+handler" { +declare module '../src/routes/api/reply/+handler' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/api/reply"]; - export type Context = Run.MultiRouteContext; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/api/reply'] + export type Context = Run.MultiRouteContext + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/chat/e2e/chat.spec.ts b/examples/marko/chat/e2e/chat.spec.ts index 66c391972..197e67e19 100644 --- a/examples/marko/chat/e2e/chat.spec.ts +++ b/examples/marko/chat/e2e/chat.spec.ts @@ -43,7 +43,9 @@ async function waitForPin(page: Page) { .toBeLessThanOrEqual(AT_END_PX) } -test('loads pinned to the newest message with status At latest', async ({ page }) => { +test('loads pinned to the newest message with status At latest', async ({ + page, +}) => { await page.goto('/') await waitForPin(page) await expect(page.locator('[data-key="message-44"]')).toBeVisible() @@ -70,7 +72,9 @@ test('does NOT follow appends while reading history', async ({ page }) => { await page.locator('.messages').evaluate((el) => { el.scrollTop = Math.floor(el.scrollHeight / 2) }) - await expect(page.locator('[data-testid="status"]')).toHaveText('Reading history') + await expect(page.locator('[data-testid="status"]')).toHaveText( + 'Reading history', + ) const before = await page.locator('.messages').evaluate((el) => el.scrollTop) await page.locator('[data-testid="add-message"]').click() // give any (incorrect) follow a chance to happen, then assert it did not @@ -91,13 +95,17 @@ test('prepending history keeps the visible message anchored in place', async ({ await page.locator('.messages').evaluate((el) => { el.scrollTop = Math.floor(el.scrollHeight / 2) }) - await expect(page.locator('[data-testid="status"]')).toHaveText('Reading history') + await expect(page.locator('[data-testid="status"]')).toHaveText( + 'Reading history', + ) // pick whatever message is currently visible near the viewport top and record it const anchorKey = await page.evaluate(() => { const el = document.querySelector('.messages')! const top = el.getBoundingClientRect().top - const rows = Array.from(document.querySelectorAll('.message-row')) + const rows = Array.from( + document.querySelectorAll('.message-row'), + ) const visible = rows.find((row) => { const box = row.getBoundingClientRect() return box.top >= top && box.top < top + 200 @@ -114,7 +122,9 @@ test('prepending history keeps the visible message anchored in place', async ({ .evaluate((el) => el.scrollHeight) await page.locator('[data-testid="load-older"]').click() - await expect(page.locator('[data-testid="status"]')).toHaveText('Loading history') + await expect(page.locator('[data-testid="status"]')).toHaveText( + 'Loading history', + ) // The prepended rows are far above the render window here, so they are // (correctly) virtualized away — DOM attachment is the wrong signal. The prepend // landing is observable as total content growth. @@ -159,7 +169,9 @@ test('Latest returns to the bottom and status flips back to At latest', async ({ await page.locator('.messages').evaluate((el) => { el.scrollTop = 0 }) - await expect(page.locator('[data-testid="status"]')).toHaveText(/Reading history|Loading history/) + await expect(page.locator('[data-testid="status"]')).toHaveText( + /Reading history|Loading history/, + ) await page.locator('[data-testid="latest"]').click() await waitForPin(page) await expect(page.locator('[data-testid="status"]')).toHaveText('At latest') @@ -172,7 +184,8 @@ test('a reply streamed from the server grows progressively and stays pinned', as await page.goto('/') await waitForPin(page) const replyResponse = page.waitForResponse( - (response) => response.url().endsWith('/api/reply') && response.status() === 200, + (response) => + response.url().endsWith('/api/reply') && response.status() === 200, ) await page.locator('[data-testid="stream-reply"]').click() await replyResponse // the reply really comes over the network @@ -183,7 +196,9 @@ test('a reply streamed from the server grows progressively and stays pinned', as const early = (await streamed.textContent()) ?? '' expect(early).not.toContain('drifting off the bottom') // ... and the rest arrives afterwards, growing the same row - await expect(streamed).toContainText('drifting off the bottom', { timeout: 5000 }) + await expect(streamed).toContainText('drifting off the bottom', { + timeout: 5000, + }) await waitForPin(page) await expect(page.locator('[data-testid="status"]')).toHaveText('At latest') }) diff --git a/examples/marko/chat/tsconfig.json b/examples/marko/chat/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/chat/tsconfig.json +++ b/examples/marko/chat/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/dynamic/.marko-run/routes.d.ts b/examples/marko/dynamic/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/dynamic/.marko-run/routes.d.ts +++ b/examples/marko/dynamic/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/dynamic/e2e/dynamic.spec.ts b/examples/marko/dynamic/e2e/dynamic.spec.ts index 121817b2d..449918588 100644 --- a/examples/marko/dynamic/e2e/dynamic.spec.ts +++ b/examples/marko/dynamic/e2e/dynamic.spec.ts @@ -51,7 +51,11 @@ test('measured positions stay contiguous: each row starts where the previous end els .map((el) => { const r = (el as HTMLElement).getBoundingClientRect() - return { index: Number(el.getAttribute('data-index')), top: r.top, bottom: r.bottom } + return { + index: Number(el.getAttribute('data-index')), + top: r.top, + bottom: r.bottom, + } }) .sort((a, b) => a.index - b.index), ) diff --git a/examples/marko/dynamic/tsconfig.json b/examples/marko/dynamic/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/dynamic/tsconfig.json +++ b/examples/marko/dynamic/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/fixed/.marko-run/routes.d.ts b/examples/marko/fixed/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/fixed/.marko-run/routes.d.ts +++ b/examples/marko/fixed/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/fixed/e2e/fixed.spec.ts b/examples/marko/fixed/e2e/fixed.spec.ts index c8e133183..7eb360287 100644 --- a/examples/marko/fixed/e2e/fixed.spec.ts +++ b/examples/marko/fixed/e2e/fixed.spec.ts @@ -22,7 +22,9 @@ test.afterEach(() => { expect(consoleErrors).toEqual([]) }) -test('rows: initial window renders and deep scroll re-windows', async ({ page }) => { +test('rows: initial window renders and deep scroll re-windows', async ({ + page, +}) => { await page.goto('/') await expect( page.locator('.scroll-row-container .item', { hasText: /^Row 0$/ }), diff --git a/examples/marko/fixed/tsconfig.json b/examples/marko/fixed/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/fixed/tsconfig.json +++ b/examples/marko/fixed/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/grid/.marko-run/routes.d.ts b/examples/marko/grid/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/grid/.marko-run/routes.d.ts +++ b/examples/marko/grid/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/grid/tsconfig.json b/examples/marko/grid/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/grid/tsconfig.json +++ b/examples/marko/grid/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/infinite-scroll/.marko-run/routes.d.ts b/examples/marko/infinite-scroll/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/infinite-scroll/.marko-run/routes.d.ts +++ b/examples/marko/infinite-scroll/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/infinite-scroll/e2e/infinite-scroll.spec.ts b/examples/marko/infinite-scroll/e2e/infinite-scroll.spec.ts index f963100d2..4c11aaa3c 100644 --- a/examples/marko/infinite-scroll/e2e/infinite-scroll.spec.ts +++ b/examples/marko/infinite-scroll/e2e/infinite-scroll.spec.ts @@ -23,7 +23,9 @@ test.afterEach(() => { expect(consoleErrors).toEqual([]) }) -test('reserves all 500 slots upfront while the first page loads', async ({ page }) => { +test('reserves all 500 slots upfront while the first page loads', async ({ + page, +}) => { await page.goto('/') // Total size is fixed from the start: 500 x 52 regardless of what is loaded. const wrapperHeight = await page @@ -31,18 +33,24 @@ test('reserves all 500 slots upfront while the first page loads', async ({ page .evaluate((el) => (el as HTMLElement).style.height) expect(wrapperHeight).toBe('26000px') // First page (simulated 400ms fetch) arrives. - await expect(page.locator('.item', { hasText: /Row #1\b/ }).first()).toBeVisible() + await expect( + page.locator('.item', { hasText: /Row #1\b/ }).first(), + ).toBeVisible() await expect(page.getByText('20 of 500 rows loaded')).toBeVisible() }) -test('scrolling to the edge of loaded rows fetches the next page', async ({ page }) => { +test('scrolling to the edge of loaded rows fetches the next page', async ({ + page, +}) => { await page.goto('/') await expect(page.getByText('20 of 500 rows loaded')).toBeVisible() await page.locator('.scroll-container').evaluate((el) => { el.scrollTop = 18 * 52 // bring the last loaded rows into the window }) await expect(page.getByText('40 of 500 rows loaded')).toBeVisible() - await expect(page.locator('.item', { hasText: /Row #21\b/ }).first()).toBeVisible() + await expect( + page.locator('.item', { hasText: /Row #21\b/ }).first(), + ).toBeVisible() }) test('a deep jump shows placeholders, then pages load continuously until the region catches up', async ({ @@ -61,7 +69,9 @@ test('a deep jump shows placeholders, then pages load continuously until the reg await expect(page.locator('.item', { hasText: '—' }).first()).toBeVisible() // Catch-up completes: a real row in the jumped-to region replaces its // placeholder (~20 pages x 400ms simulated latency — generous timeout). - await expect(page.locator('.item', { hasText: /Row #401\b/ }).first()).toBeVisible({ + await expect( + page.locator('.item', { hasText: /Row #401\b/ }).first(), + ).toBeVisible({ timeout: 20_000, }) }) diff --git a/examples/marko/infinite-scroll/tsconfig.json b/examples/marko/infinite-scroll/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/infinite-scroll/tsconfig.json +++ b/examples/marko/infinite-scroll/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/padding/.marko-run/routes.d.ts b/examples/marko/padding/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/padding/.marko-run/routes.d.ts +++ b/examples/marko/padding/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/padding/e2e/padding.spec.ts b/examples/marko/padding/e2e/padding.spec.ts index 96cd63d8a..f71d321fb 100644 --- a/examples/marko/padding/e2e/padding.spec.ts +++ b/examples/marko/padding/e2e/padding.spec.ts @@ -22,7 +22,9 @@ test.afterEach(() => { expect(consoleErrors).toEqual([]) }) -test('paddingStart offsets the first row and first column by 100px', async ({ page }) => { +test('paddingStart offsets the first row and first column by 100px', async ({ + page, +}) => { await page.goto('/') const row0 = page.locator('[data-testid="rows-list"] [data-index="0"]') await expect(row0).toBeVisible() @@ -34,19 +36,27 @@ test('paddingStart offsets the first row and first column by 100px', async ({ pa test('vertical and horizontal lists re-window on scroll', async ({ page }) => { await page.goto('/') - await expect(page.locator('[data-testid="rows-list"] [data-index="0"]')).toBeVisible() - await page - .locator('[data-testid="rows-list"]') - .evaluate((el) => { el.scrollTop = 5000 }) - await expect(page.locator('[data-testid="rows-list"] [data-index="0"]')).toHaveCount(0) + await expect( + page.locator('[data-testid="rows-list"] [data-index="0"]'), + ).toBeVisible() + await page.locator('[data-testid="rows-list"]').evaluate((el) => { + el.scrollTop = 5000 + }) + await expect( + page.locator('[data-testid="rows-list"] [data-index="0"]'), + ).toHaveCount(0) const rowIndexes = page.locator('[data-testid="rows-list"] [data-index]') await expect(rowIndexes.first()).toBeVisible() - await page - .locator('[data-testid="cols-list"]') - .evaluate((el) => { el.scrollLeft = 5000 }) - await expect(page.locator('[data-testid="cols-list"] [data-index="0"]')).toHaveCount(0) - await expect(page.locator('[data-testid="cols-list"] [data-index]').first()).toBeVisible() + await page.locator('[data-testid="cols-list"]').evaluate((el) => { + el.scrollLeft = 5000 + }) + await expect( + page.locator('[data-testid="cols-list"] [data-index="0"]'), + ).toHaveCount(0) + await expect( + page.locator('[data-testid="cols-list"] [data-index]').first(), + ).toBeVisible() }) test('grid: first cell offset by 200px on both axes, both index attributes present', async ({ @@ -55,14 +65,19 @@ test('grid: first cell offset by 200px on both axes, both index attributes prese await page.goto('/') const cell = page.locator('[data-row-index="0"][data-column-index="0"]') await expect(cell).toBeVisible() - await expect(cell).toHaveAttribute('style', /translateX\(200px\) translateY\(200px\)/) + await expect(cell).toHaveAttribute( + 'style', + /translateX\(200px\) translateY\(200px\)/, + ) }) test('indexAttribute keeps the two grid virtualizers apart under 2D scrolling', async ({ page, }) => { await page.goto('/') - await expect(page.locator('[data-row-index="0"][data-column-index="0"]')).toBeVisible() + await expect( + page.locator('[data-row-index="0"][data-column-index="0"]'), + ).toBeVisible() // scroll BOTH axes; if either instance read the other's index attribute its // measurements/cache would corrupt and the windows would collapse or misplace await page.locator('[data-testid="grid-list"]').evaluate((el) => { @@ -73,7 +88,9 @@ test('indexAttribute keeps the two grid virtualizers apart under 2D scrolling', await expect( page.locator('[data-row-index="0"][data-column-index="0"]'), ).toHaveCount(0) - const anyCell = page.locator('[data-testid="grid-list"] [data-row-index]').first() + const anyCell = page + .locator('[data-testid="grid-list"] [data-row-index]') + .first() await expect(anyCell).toBeVisible() const attrs = await anyCell.evaluate((el) => ({ row: Number(el.getAttribute('data-row-index')), @@ -87,12 +104,16 @@ test('indexAttribute keeps the two grid virtualizers apart under 2D scrolling', test('scroll-to-index button reaches the halfway row', async ({ page }) => { await page.goto('/') - await expect(page.locator('[data-row-index="0"][data-column-index="0"]')).toBeVisible() + await expect( + page.locator('[data-row-index="0"][data-column-index="0"]'), + ).toBeVisible() await page.locator('[data-testid="scroll-half"]').click() await expect(page.locator('[data-row-index="5000"]').first()).toBeVisible() }) -test('toggle unmounts and remounts the grid; it keeps working after', async ({ page }) => { +test('toggle unmounts and remounts the grid; it keeps working after', async ({ + page, +}) => { await page.goto('/') await expect(page.locator('[data-testid="grid-list"]')).toBeVisible() await page.locator('[data-testid="toggle"]').click() diff --git a/examples/marko/padding/tsconfig.json b/examples/marko/padding/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/padding/tsconfig.json +++ b/examples/marko/padding/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/pretext/.marko-run/routes.d.ts b/examples/marko/pretext/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/pretext/.marko-run/routes.d.ts +++ b/examples/marko/pretext/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/pretext/e2e/pretext.spec.ts b/examples/marko/pretext/e2e/pretext.spec.ts index 5cf4f18a7..2b451120a 100644 --- a/examples/marko/pretext/e2e/pretext.spec.ts +++ b/examples/marko/pretext/e2e/pretext.spec.ts @@ -21,7 +21,9 @@ test.afterEach(() => { expect(consoleErrors).toEqual([]) }) -test('renders a small window of 2000 rows with calculated heights', async ({ page }) => { +test('renders a small window of 2000 rows with calculated heights', async ({ + page, +}) => { await page.goto('/') await expect(page.locator('[data-index="0"]')).toBeVisible() const stat = await page.locator('[data-testid="stat"]').textContent() @@ -30,12 +32,16 @@ test('renders a small window of 2000 rows with calculated heights', async ({ pag expect(rendered).toBeLessThan(60) // calculated row height matches the RENDERED bubble: no post-render correction needed const row = page.locator('[data-index="0"]') - const declared = await row.evaluate((el) => Number.parseFloat(el.style.height)) + const declared = await row.evaluate((el) => + Number.parseFloat(el.style.height), + ) const actual = await row.evaluate((el) => el.getBoundingClientRect().height) expect(Math.abs(declared - actual)).toBeLessThanOrEqual(1) }) -test('Bottom lands exactly on the last message in one jump', async ({ page }) => { +test('Bottom lands exactly on the last message in one jump', async ({ + page, +}) => { await page.goto('/') await expect(page.locator('[data-index="0"]')).toBeVisible() await page.locator('[data-testid="bottom"]').click() @@ -45,7 +51,9 @@ test('Bottom lands exactly on the last message in one jump', async ({ page }) => // mean no correction drift after landing) const list = await page.locator('[data-testid="list"]').boundingBox() const box = await last.boundingBox() - expect(Math.abs(box!.y + box!.height - (list!.y + list!.height))).toBeLessThanOrEqual(2) + expect( + Math.abs(box!.y + box!.height - (list!.y + list!.height)), + ).toBeLessThanOrEqual(2) }) test('Middle then Top round-trips cleanly', async ({ page }) => { diff --git a/examples/marko/pretext/tsconfig.json b/examples/marko/pretext/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/pretext/tsconfig.json +++ b/examples/marko/pretext/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/scroll-padding/.marko-run/routes.d.ts b/examples/marko/scroll-padding/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/scroll-padding/.marko-run/routes.d.ts +++ b/examples/marko/scroll-padding/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/scroll-padding/e2e/scroll-padding.spec.ts b/examples/marko/scroll-padding/e2e/scroll-padding.spec.ts index 23f61717f..a820a5a1d 100644 --- a/examples/marko/scroll-padding/e2e/scroll-padding.spec.ts +++ b/examples/marko/scroll-padding/e2e/scroll-padding.spec.ts @@ -27,7 +27,9 @@ async function headerBottom(page: import('@playwright/test').Page) { return box!.y + box!.height } -test('rows start below the sticky header, not underneath it', async ({ page }) => { +test('rows start below the sticky header, not underneath it', async ({ + page, +}) => { await page.goto('/') const row0 = page.locator('[data-index="0"]') await expect(row0).toBeVisible() @@ -49,7 +51,9 @@ test('scrollToIndex(40) downward aligns to the viewport END (align auto semantic // upward case — next test.) const scroller = await page.locator('.list').boundingBox() const box = await row.boundingBox() - expect(Math.abs(box!.y + box!.height - (scroller!.y + scroller!.height))).toBeLessThanOrEqual(2) + expect( + Math.abs(box!.y + box!.height - (scroller!.y + scroller!.height)), + ).toBeLessThanOrEqual(2) }) test('then scrollToIndex(20) scrolls BACK and aligns row 20 below the header', async ({ diff --git a/examples/marko/scroll-padding/tsconfig.json b/examples/marko/scroll-padding/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/scroll-padding/tsconfig.json +++ b/examples/marko/scroll-padding/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/smooth-scroll/.marko-run/routes.d.ts b/examples/marko/smooth-scroll/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/smooth-scroll/.marko-run/routes.d.ts +++ b/examples/marko/smooth-scroll/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/smooth-scroll/tsconfig.json b/examples/marko/smooth-scroll/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/smooth-scroll/tsconfig.json +++ b/examples/marko/smooth-scroll/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/ssr-fetch/.marko-run/routes.d.ts b/examples/marko/ssr-fetch/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/ssr-fetch/.marko-run/routes.d.ts +++ b/examples/marko/ssr-fetch/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/ssr-fetch/e2e/ssr-fetch.spec.ts b/examples/marko/ssr-fetch/e2e/ssr-fetch.spec.ts index 61c113249..bff15271b 100644 --- a/examples/marko/ssr-fetch/e2e/ssr-fetch.spec.ts +++ b/examples/marko/ssr-fetch/e2e/ssr-fetch.spec.ts @@ -54,7 +54,9 @@ test('rows render on the client from the serialized data — no client re-fetch }) => { await page.goto('/') // Mount fills the container with the SERVER-fetched people. - await expect(page.locator('.email', { hasText: 'person1@example.com' })).toBeVisible() + await expect( + page.locator('.email', { hasText: 'person1@example.com' }), + ).toBeVisible() expect(await page.locator('.item').count()).toBeGreaterThan(5) // No client re-fetch: fetchPeople logs "fetchPeople ran" when it executes; that @@ -66,6 +68,10 @@ test('rows render on the client from the serialized data — no client re-fetch await page.locator('.scroll-container').evaluate((el) => { el.scrollTop = 500 * 48 }) - await expect(page.locator('.email', { hasText: 'person501@example.com' })).toBeVisible() - await expect(page.locator('.email', { hasText: 'person1@example.com' })).toHaveCount(0) + await expect( + page.locator('.email', { hasText: 'person501@example.com' }), + ).toBeVisible() + await expect( + page.locator('.email', { hasText: 'person1@example.com' }), + ).toHaveCount(0) }) diff --git a/examples/marko/ssr-fetch/src/data.ts b/examples/marko/ssr-fetch/src/data.ts index aaea0318a..b118239b1 100644 --- a/examples/marko/ssr-fetch/src/data.ts +++ b/examples/marko/ssr-fetch/src/data.ts @@ -9,9 +9,9 @@ export interface Person { // This runs only on the server during SSR. The resolved rows are serialized into the page // and resume on the client — there is no client-side re-fetch. export async function fetchPeople(): Promise { - console.log("fetchPeople ran") + console.log('fetchPeople ran') await new Promise((resolve) => setTimeout(resolve, 40)) - const roles = ["Engineer", "Designer", "PM", "Analyst", "Support"] + const roles = ['Engineer', 'Designer', 'PM', 'Analyst', 'Support'] return Array.from({ length: 1000 }, (_, i) => ({ id: i + 1, name: `Person ${i + 1}`, diff --git a/examples/marko/ssr-fetch/tsconfig.json b/examples/marko/ssr-fetch/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/ssr-fetch/tsconfig.json +++ b/examples/marko/ssr-fetch/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/ssr-restore/.marko-run/routes.d.ts b/examples/marko/ssr-restore/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/ssr-restore/.marko-run/routes.d.ts +++ b/examples/marko/ssr-restore/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/ssr-restore/e2e/ssr-restore.spec.ts b/examples/marko/ssr-restore/e2e/ssr-restore.spec.ts index ad5c3af24..dbe638e00 100644 --- a/examples/marko/ssr-restore/e2e/ssr-restore.spec.ts +++ b/examples/marko/ssr-restore/e2e/ssr-restore.spec.ts @@ -53,7 +53,9 @@ test('on mount the scroll position is restored to the offset and the painted row }) => { await page.goto('/') // The jump-target row is VISIBLE (not just present): scroll restore worked. - await expect(page.locator('.email', { hasText: 'person101@example.com' })).toBeVisible() + await expect( + page.locator('.email', { hasText: 'person101@example.com' }), + ).toBeVisible() const scrollTop = await page .locator('.scroll-container') .evaluate((el) => el.scrollTop) @@ -64,12 +66,18 @@ test('the restored list is live: scrolling up re-windows to earlier rows', async page, }) => { await page.goto('/') - await expect(page.locator('.email', { hasText: 'person101@example.com' })).toBeVisible() + await expect( + page.locator('.email', { hasText: 'person101@example.com' }), + ).toBeVisible() await page.locator('.scroll-container').evaluate((el) => { el.scrollTop = 0 }) - await expect(page.locator('.email', { hasText: 'person1@example.com' })).toBeVisible() - await expect(page.locator('.email', { hasText: 'person101@example.com' })).toHaveCount(0) + await expect( + page.locator('.email', { hasText: 'person1@example.com' }), + ).toBeVisible() + await expect( + page.locator('.email', { hasText: 'person101@example.com' }), + ).toHaveCount(0) }) test('with JavaScript disabled the offset rows exist in the DOM (positioned deep, unrestored)', async ({ diff --git a/examples/marko/ssr-restore/src/data.ts b/examples/marko/ssr-restore/src/data.ts index aaea0318a..b118239b1 100644 --- a/examples/marko/ssr-restore/src/data.ts +++ b/examples/marko/ssr-restore/src/data.ts @@ -9,9 +9,9 @@ export interface Person { // This runs only on the server during SSR. The resolved rows are serialized into the page // and resume on the client — there is no client-side re-fetch. export async function fetchPeople(): Promise { - console.log("fetchPeople ran") + console.log('fetchPeople ran') await new Promise((resolve) => setTimeout(resolve, 40)) - const roles = ["Engineer", "Designer", "PM", "Analyst", "Support"] + const roles = ['Engineer', 'Designer', 'PM', 'Analyst', 'Support'] return Array.from({ length: 1000 }, (_, i) => ({ id: i + 1, name: `Person ${i + 1}`, diff --git a/examples/marko/ssr-restore/tsconfig.json b/examples/marko/ssr-restore/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/ssr-restore/tsconfig.json +++ b/examples/marko/ssr-restore/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/ssr-slice/.marko-run/routes.d.ts b/examples/marko/ssr-slice/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/ssr-slice/.marko-run/routes.d.ts +++ b/examples/marko/ssr-slice/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/ssr-slice/e2e/ssr-slice.spec.ts b/examples/marko/ssr-slice/e2e/ssr-slice.spec.ts index abed35dde..df9d8684e 100644 --- a/examples/marko/ssr-slice/e2e/ssr-slice.spec.ts +++ b/examples/marko/ssr-slice/e2e/ssr-slice.spec.ts @@ -77,20 +77,28 @@ test('client resumes over the server slice and the list is live (scroll re-windo }) => { await page.goto('/') // The server rows are (still) there post-resume. - await expect(page.locator('.email', { hasText: 'person1@example.com' })).toBeVisible() + await expect( + page.locator('.email', { hasText: 'person1@example.com' }), + ).toBeVisible() // Liveness: scroll deep; the window advances. await page.locator('.scroll-container').evaluate((el) => { el.scrollTop = 700 * 48 }) - await expect(page.locator('.email', { hasText: 'person701@example.com' })).toBeVisible() - await expect(page.locator('.email', { hasText: 'person1@example.com' })).toHaveCount(0) + await expect( + page.locator('.email', { hasText: 'person701@example.com' }), + ).toBeVisible() + await expect( + page.locator('.email', { hasText: 'person1@example.com' }), + ).toHaveCount(0) // And back to the top: the original slice re-renders (round trip). await page.locator('.scroll-container').evaluate((el) => { el.scrollTop = 0 }) - await expect(page.locator('.email', { hasText: 'person1@example.com' })).toBeVisible() + await expect( + page.locator('.email', { hasText: 'person1@example.com' }), + ).toBeVisible() }) test('the awaited slice STREAMS: placeholder flushes first, server-painted rows arrive in a later chunk', async () => { diff --git a/examples/marko/ssr-slice/src/data.ts b/examples/marko/ssr-slice/src/data.ts index aaea0318a..b118239b1 100644 --- a/examples/marko/ssr-slice/src/data.ts +++ b/examples/marko/ssr-slice/src/data.ts @@ -9,9 +9,9 @@ export interface Person { // This runs only on the server during SSR. The resolved rows are serialized into the page // and resume on the client — there is no client-side re-fetch. export async function fetchPeople(): Promise { - console.log("fetchPeople ran") + console.log('fetchPeople ran') await new Promise((resolve) => setTimeout(resolve, 40)) - const roles = ["Engineer", "Designer", "PM", "Analyst", "Support"] + const roles = ['Engineer', 'Designer', 'PM', 'Analyst', 'Support'] return Array.from({ length: 1000 }, (_, i) => ({ id: i + 1, name: `Person ${i + 1}`, diff --git a/examples/marko/ssr-slice/tsconfig.json b/examples/marko/ssr-slice/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/ssr-slice/tsconfig.json +++ b/examples/marko/ssr-slice/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/ssr/.marko-run/routes.d.ts b/examples/marko/ssr/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/ssr/.marko-run/routes.d.ts +++ b/examples/marko/ssr/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/ssr/tsconfig.json b/examples/marko/ssr/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/ssr/tsconfig.json +++ b/examples/marko/ssr/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/sticky/.marko-run/routes.d.ts b/examples/marko/sticky/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/sticky/.marko-run/routes.d.ts +++ b/examples/marko/sticky/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/sticky/e2e/sticky.spec.ts b/examples/marko/sticky/e2e/sticky.spec.ts index cb234456f..3a85bd7fc 100644 --- a/examples/marko/sticky/e2e/sticky.spec.ts +++ b/examples/marko/sticky/e2e/sticky.spec.ts @@ -47,7 +47,9 @@ test('deep inside a group, its header stays pinned though scrolled past', async await pinned(page) // row 0 is header 'A'; 22 'Aaron NN' + 22 'Abby NN' follow. Scroll ~15 rows in: // well past the header's natural 0px position, still inside group A. - await page.locator('[data-testid="list"]').evaluate((el) => { el.scrollTop = 750 }) + await page.locator('[data-testid="list"]').evaluate((el) => { + el.scrollTop = 750 + }) const active = await pinned(page) await expect(active).toHaveText('A') const list = await page.locator('[data-testid="list"]').boundingBox() @@ -61,12 +63,16 @@ test('deep inside a group, its header stays pinned though scrolled past', async await expect(firstName).toContainText(/^A/) }) -test('crossing into the next group hands the pin to its header', async ({ page }) => { +test('crossing into the next group hands the pin to its header', async ({ + page, +}) => { await page.goto('/') await pinned(page) // group A spans header(1) + Aaron(22) + Abby(22) = 45 rows * 50px = 2250px. // Scroll past that boundary into group B. - await page.locator('[data-testid="list"]').evaluate((el) => { el.scrollTop = 2400 }) + await page.locator('[data-testid="list"]').evaluate((el) => { + el.scrollTop = 2400 + }) const active = await pinned(page) await expect(active).toHaveText('B') // exactly one active pin; the A header is no longer force-rendered @@ -76,10 +82,14 @@ test('crossing into the next group hands the pin to its header', async ({ page } test('scrolling far and back returns the pin to A', async ({ page }) => { await page.goto('/') await pinned(page) - await page.locator('[data-testid="list"]').evaluate((el) => { el.scrollTop = 20000 }) + await page.locator('[data-testid="list"]').evaluate((el) => { + el.scrollTop = 20000 + }) const far = await pinned(page) await expect(far).not.toHaveText('A') - await page.locator('[data-testid="list"]').evaluate((el) => { el.scrollTop = 0 }) + await page.locator('[data-testid="list"]').evaluate((el) => { + el.scrollTop = 0 + }) const back = await pinned(page) await expect(back).toHaveText('A') }) diff --git a/examples/marko/sticky/tsconfig.json b/examples/marko/sticky/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/sticky/tsconfig.json +++ b/examples/marko/sticky/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/table/.marko-run/routes.d.ts b/examples/marko/table/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/table/.marko-run/routes.d.ts +++ b/examples/marko/table/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/table/e2e/table.spec.ts b/examples/marko/table/e2e/table.spec.ts index 8b3cfd3b4..084ee1dc5 100644 --- a/examples/marko/table/e2e/table.spec.ts +++ b/examples/marko/table/e2e/table.spec.ts @@ -44,9 +44,9 @@ test('deep scroll re-windows and compensation keeps rows visually contiguous', a }) => { await page.goto('/') await expect(page.locator('tbody tr[data-index="0"]')).toBeVisible() - await page - .locator('[data-testid="container"]') - .evaluate((el) => { el.scrollTop = 34 * 25000 }) + await page.locator('[data-testid="container"]').evaluate((el) => { + el.scrollTop = 34 * 25000 + }) await expect(page.locator('tbody tr[data-index="0"]')).toHaveCount(0) const rows = page.locator('tbody tr') await expect(rows.first()).toBeVisible() @@ -58,7 +58,9 @@ test('deep scroll re-windows and compensation keeps rows visually contiguous', a // the sticky header is still pinned at the container top 850,000px deep const header = page.locator('thead th[data-col="id"]') await expect(header).toBeVisible() - const container = await page.locator('[data-testid="container"]').boundingBox() + const container = await page + .locator('[data-testid="container"]') + .boundingBox() const headBox = await header.boundingBox() expect(Math.abs(headBox!.y - container!.y)).toBeLessThanOrEqual(2) }) @@ -71,10 +73,7 @@ test('clicking Age sorts ascending, again descending, indicator shown', async ({ await page.locator('th[data-col="age"]').click() // ascending: first row holds the minimum age (deterministic data -> 0 exists in 50k) const firstAge = () => - page - .locator('tbody tr[data-index="0"] td') - .nth(3) - .textContent() + page.locator('tbody tr[data-index="0"] td').nth(3).textContent() await expect.poll(firstAge).toBe('0') await expect(page.locator('th[data-col="age"]')).toContainText('\u{1F53C}') await page.locator('th[data-col="age"]').click() @@ -82,13 +81,15 @@ test('clicking Age sorts ascending, again descending, indicator shown', async ({ await expect(page.locator('th[data-col="age"]')).toContainText('\u{1F53D}') }) -test('index vs data: sorting swaps data under a stationary window', async ({ page }) => { +test('index vs data: sorting swaps data under a stationary window', async ({ + page, +}) => { await page.goto('/') await expect(page.locator('tbody tr[data-index="0"]')).toBeVisible() // scroll mid-list - await page - .locator('[data-testid="container"]') - .evaluate((el) => { el.scrollTop = 34 * 1000 }) + await page.locator('[data-testid="container"]').evaluate((el) => { + el.scrollTop = 34 * 1000 + }) const rows = page.locator('tbody tr') await expect .poll(async () => Number(await rows.first().getAttribute('data-index'))) @@ -106,7 +107,9 @@ test('index vs data: sorting swaps data under a stationary window', async ({ pag // position:sticky header inside a deeply-scrolled huge container (its LAYOUT // position is at the table top) and would yank scrollTop — an artifact of the // test driver, not the page (verified: native click leaves scrollTop untouched). - await page.locator('th[data-col="lastName"]').evaluate((el) => (el as HTMLElement).click()) + await page + .locator('th[data-col="lastName"]') + .evaluate((el) => (el as HTMLElement).click()) // "did not move" = stayed within a fraction of one row (sub-row platform variance — // e.g. macOS scroll-anchoring residue — must not fail the lesson; the airtight proof // is the identical index list below) diff --git a/examples/marko/table/tsconfig.json b/examples/marko/table/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/table/tsconfig.json +++ b/examples/marko/table/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/variable/.marko-run/routes.d.ts b/examples/marko/variable/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/variable/.marko-run/routes.d.ts +++ b/examples/marko/variable/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/variable/e2e/variable.spec.ts b/examples/marko/variable/e2e/variable.spec.ts index 36e481656..5c7d4bfda 100644 --- a/examples/marko/variable/e2e/variable.spec.ts +++ b/examples/marko/variable/e2e/variable.spec.ts @@ -25,7 +25,9 @@ test.afterEach(() => { const rowSize = (i: number) => 25 + ((i * 17 + 31) % 100) -test('row heights follow the estimateSize formula exactly', async ({ page }) => { +test('row heights follow the estimateSize formula exactly', async ({ + page, +}) => { await page.goto('/') const row0 = page.locator('.scroll-row .item', { hasText: new RegExp(`^Row 0 — ${rowSize(0)}px$`), diff --git a/examples/marko/variable/tsconfig.json b/examples/marko/variable/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/variable/tsconfig.json +++ b/examples/marko/variable/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/window-ssr-slice/.marko-run/routes.d.ts b/examples/marko/window-ssr-slice/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/window-ssr-slice/.marko-run/routes.d.ts +++ b/examples/marko/window-ssr-slice/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/window-ssr-slice/e2e/window-ssr-slice.spec.ts b/examples/marko/window-ssr-slice/e2e/window-ssr-slice.spec.ts index a4d953037..c57796f07 100644 --- a/examples/marko/window-ssr-slice/e2e/window-ssr-slice.spec.ts +++ b/examples/marko/window-ssr-slice/e2e/window-ssr-slice.spec.ts @@ -63,14 +63,22 @@ test('client resumes and the window virtualizer is live: page scroll re-windows' page, }) => { await page.goto('/') - await expect(page.locator('.email', { hasText: 'person1@example.com' })).toBeVisible() + await expect( + page.locator('.email', { hasText: 'person1@example.com' }), + ).toBeVisible() // Scroll the WINDOW (not a container) deep into the list. await page.evaluate(() => window.scrollTo(0, 600 * 48)) - await expect(page.locator('.email', { hasText: 'person601@example.com' })).toBeVisible() - await expect(page.locator('.email', { hasText: 'person1@example.com' })).toHaveCount(0) + await expect( + page.locator('.email', { hasText: 'person601@example.com' }), + ).toBeVisible() + await expect( + page.locator('.email', { hasText: 'person1@example.com' }), + ).toHaveCount(0) // Round trip to the top: the original slice re-renders. await page.evaluate(() => window.scrollTo(0, 0)) - await expect(page.locator('.email', { hasText: 'person1@example.com' })).toBeVisible() + await expect( + page.locator('.email', { hasText: 'person1@example.com' }), + ).toBeVisible() }) diff --git a/examples/marko/window-ssr-slice/src/data.ts b/examples/marko/window-ssr-slice/src/data.ts index aaea0318a..b118239b1 100644 --- a/examples/marko/window-ssr-slice/src/data.ts +++ b/examples/marko/window-ssr-slice/src/data.ts @@ -9,9 +9,9 @@ export interface Person { // This runs only on the server during SSR. The resolved rows are serialized into the page // and resume on the client — there is no client-side re-fetch. export async function fetchPeople(): Promise { - console.log("fetchPeople ran") + console.log('fetchPeople ran') await new Promise((resolve) => setTimeout(resolve, 40)) - const roles = ["Engineer", "Designer", "PM", "Analyst", "Support"] + const roles = ['Engineer', 'Designer', 'PM', 'Analyst', 'Support'] return Array.from({ length: 1000 }, (_, i) => ({ id: i + 1, name: `Person ${i + 1}`, diff --git a/examples/marko/window-ssr-slice/tsconfig.json b/examples/marko/window-ssr-slice/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/window-ssr-slice/tsconfig.json +++ b/examples/marko/window-ssr-slice/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/examples/marko/window/.marko-run/routes.d.ts b/examples/marko/window/.marko-run/routes.d.ts index 4c22b8431..a122bc38a 100644 --- a/examples/marko/window/.marko-run/routes.d.ts +++ b/examples/marko/window/.marko-run/routes.d.ts @@ -3,32 +3,51 @@ Do NOT manually edit this file or your changes will be lost. */ -import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; -import type * as Run from "@marko/run"; +import { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, +} from '@marko/run/namespace' +import type * as Run from '@marko/run' - -declare module "@marko/run" { - interface AppData extends Run.DefineApp<{ - routes: { - "/": { verb: "get"; }; - } - }> {} +declare module '@marko/run' { + interface AppData extends Run.DefineApp<{ + routes: { + '/': { verb: 'get' } + } + }> {} } -declare module "../src/routes/+page.marko" { +declare module '../src/routes/+page.marko' { namespace MarkoRun { - export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/"]; - export type Context = Run.MultiRouteContext & Marko.Global; - export type Handler = Run.HandlerLike; - export type GET = Run.HandlerLike; - export type HEAD = Run.HandlerLike; - export type POST = Run.HandlerLike; - export type PUT = Run.HandlerLike; - export type DELETE = Run.HandlerLike; - export type PATCH = Run.HandlerLike; - export type OPTIONS = Run.HandlerLike; + export { + NotHandled, + NotMatched, + GetPaths, + PostPaths, + GetablePath, + GetableHref, + PostablePath, + PostableHref, + Platform, + } + export type Route = Run.Routes['/'] + export type Context = Run.MultiRouteContext & Marko.Global + export type Handler = Run.HandlerLike + export type GET = Run.HandlerLike + export type HEAD = Run.HandlerLike + export type POST = Run.HandlerLike + export type PUT = Run.HandlerLike + export type DELETE = Run.HandlerLike + export type PATCH = Run.HandlerLike + export type OPTIONS = Run.HandlerLike /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ - export const route: Run.HandlerTypeFn; + export const route: Run.HandlerTypeFn } } diff --git a/examples/marko/window/e2e/window.spec.ts b/examples/marko/window/e2e/window.spec.ts index 95c21cde7..98067e0f5 100644 --- a/examples/marko/window/e2e/window.spec.ts +++ b/examples/marko/window/e2e/window.spec.ts @@ -39,7 +39,9 @@ test('rows are positioned after the heading (measured scrollMargin applied)', as expect(Math.abs(row0!.y - wrapper!.y)).toBeLessThanOrEqual(1) }) -test('wrapper height is the margin-free totalSize (10000 x 35)', async ({ page }) => { +test('wrapper height is the margin-free totalSize (10000 x 35)', async ({ + page, +}) => { await page.goto('/') await expect(page.locator('.item', { hasText: /^Row 0$/ })).toBeVisible() const wrapperHeight = await page @@ -57,7 +59,9 @@ test('deep window scroll re-windows relative to the LIST, not the page top', asy await page.goto('/') await expect(page.locator('.item', { hasText: /^Row 0$/ })).toBeVisible() const listTop = await page.evaluate(() => { - const el = document.querySelector('div[style*="position: relative"]') as HTMLElement + const el = document.querySelector( + 'div[style*="position: relative"]', + ) as HTMLElement return el.getBoundingClientRect().top + window.scrollY }) await page.evaluate((y) => window.scrollTo(0, y), listTop + 500 * 35) diff --git a/examples/marko/window/tsconfig.json b/examples/marko/window/tsconfig.json index 382ad5955..ec50a5679 100644 --- a/examples/marko/window/tsconfig.json +++ b/examples/marko/window/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, diff --git a/packages/marko-virtual/README.md b/packages/marko-virtual/README.md index 22115916d..d9ff0808d 100644 --- a/packages/marko-virtual/README.md +++ b/packages/marko-virtual/README.md @@ -194,56 +194,56 @@ For items with unknown heights, drive `measureElement` from a per-row `