Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/publish-rigelbuild.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Publish (RigelBuild)

# Fork-owned publish for @rigelbuild/solid-virtual. Upstream release.yml is
# gated `if: github.repository_owner == 'TanStack'` and stays untouched
# (upstreamable); this workflow is gated to the fork owner and publishes only
# the one renamed package via a direct filtered `pnpm publish`, computing the
# dist-tag from the version (prerelease -> next, GA -> latest). It reuses the
# monorepo install/build/test tooling; only the changesets *publish step* is
# bypassed, since it cannot scope a single renamed package's publish here.

permissions:
contents: read

on:
push:
tags:
- 'v*'
workflow_dispatch:

jobs:
publish:
name: Publish
if: github.repository_owner == 'RigelBuild'
runs-on: ubuntu-latest
permissions:
# No provenance attestation is generated (see the publish step's
# pnpm_config_provenance=false), so no OIDC token is minted and the job
# needs no id-token:write — read is the whole grant, matching the
# @rigelbuild/solid-markdown precedent. Auth is the NPM_TOKEN secret.
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Tools
uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main
- name: Install Playwright browsers
run: pnpm exec playwright install chromium
- name: Build and test the workspace
# `test:ci` = nx run-many (not `affected`): a tag push has no affected
# base, so `affected` would run nothing and could publish an unbuilt
# dist. run-many runs every package's checks + the `build` target in
# dependency order (virtual-core before solid-virtual), so the dist
# this job publishes is freshly built and verified. Mirrors pr.yml.
run: pnpm run test:ci
- name: Resolve npm dist-tag from version
id: disttag
run: |
version=$(node -p "require('./packages/solid-virtual/package.json').version")
if [[ "$version" == *-* ]]; then
echo "tag=next" >> "$GITHUB_OUTPUT"
else
echo "tag=latest" >> "$GITHUB_OUTPUT"
fi
- name: Publish @rigelbuild/solid-virtual
# dist-tag passed through env, not interpolated into the run string, so
# the `${{ }}` expansion can't reach the shell command line (zizmor
# template-injection). Auth is the NPM_TOKEN secret, not npm trusted
# publishing (OIDC): the token is provisioned + custodied via the orion
# Pulumi github stack per the frozen design record (rigelbuild-solid-
# virtual-publish, DL-015), so the use-trusted-publishing audit is
# deliberately ignored on the publish line below.
run: pnpm publish --filter @rigelbuild/solid-virtual --tag "$TAG" --no-git-checks # zizmor: ignore[use-trusted-publishing]
env:
TAG: ${{ steps.disttag.outputs.tag }}
# pnpm authenticates via a per-registry _authToken, not the legacy
# global `token` (NPM_CONFIG_TOKEN) which registry.npmjs.org no longer
# accepts, nor NODE_AUTH_TOKEN (a setup-node convention pnpm doesn't
# read). The URL-scoped env var is honored natively by pnpm (>=11.6),
# is file-free, and can't be redirected to another host since the
# registry is baked into the key.
pnpm_config_//registry.npmjs.org/:_authToken: ${{ secrets.NPM_TOKEN }}
# Force npm provenance OFF for this publish. The fork's Solid-2 branch
# keeps repository.url -> the upstream TanStack/virtual repo (frozen
# in DL-015: honest for a fork), but provenance attestation requires
# repository.url to match the *publishing* repo (RigelBuild/virtual)
# or the registry rejects the upload (422). We publish from a fork
# with an upstream-pointed manifest and no provenance — same posture
# as the @rigelbuild/solid-markdown precedent. pnpm >=11 ignores a
# `provenance` key in any .npmrc (only auth/network keys are read), so
# the repo-root .npmrc's provenance=true is already inert here; this
# env var (pnpm_config_*, the only surface pnpm reads it from) makes
# OFF explicit and version-proof rather than relying on that default.
pnpm_config_provenance: 'false'
13 changes: 8 additions & 5 deletions packages/solid-virtual/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"name": "@tanstack/solid-virtual",
"version": "3.13.37",
"name": "@rigelbuild/solid-virtual",
"version": "3.0.0-rc.0",
"publishConfig": {
"access": "public"
},
"description": "Headless UI for virtualizing scrollable elements in Solid",
"author": "Tanner Linsley",
"license": "MIT",
Expand Down Expand Up @@ -57,10 +60,10 @@
"@tanstack/virtual-core": "workspace:*"
},
"devDependencies": {
"solid-js": "^1.9.7",
"vite-plugin-solid": "^2.11.6"
"solid-js": "2.0.0-rc.0",
"vite-plugin-solid": "3.0.0-next.27"
},
"peerDependencies": {
"solid-js": "^1.3.0"
"solid-js": "^2.0.0-rc.0"
}
}
79 changes: 46 additions & 33 deletions packages/solid-virtual/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import {
} from '@tanstack/virtual-core'

import {
createComputed,
createEffect,
createSignal,
mergeProps,
onCleanup,
onMount,
createStore,
merge,
onSettled,
reconcile,
} from 'solid-js'
import { createStore, reconcile } from 'solid-js/store'
import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core'

export * from '@tanstack/virtual-core'
Expand All @@ -27,7 +27,7 @@ function createVirtualizerBase<
options: VirtualizerOptions<TScrollElement, TItemElement>,
): Virtualizer<TScrollElement, TItemElement> {
const resolvedOptions: VirtualizerOptions<TScrollElement, TItemElement> =
mergeProps(options)
merge(options)

const instance = new Virtualizer<TScrollElement, TItemElement>(
resolvedOptions,
Expand All @@ -36,7 +36,13 @@ function createVirtualizerBase<
const [virtualItems, setVirtualItems] = createStore(
instance.getVirtualItems(),
)
const [totalSize, setTotalSize] = createSignal(instance.getTotalSize())
const [totalSize, setTotalSize] = createSignal(instance.getTotalSize(), {
// virtual-core drives these bridge signals from its imperative API
// (resizeItem, measure, scroll handlers), which consumers may call from
// an owned scope. The write is the adapter's own internal notify bridge,
// not app state escaping a computation, so opt in to owned writes.
ownedWrite: true,
})

const handler = {
get(
Expand All @@ -57,34 +63,41 @@ function createVirtualizerBase<
const virtualizer = new Proxy(instance, handler)
virtualizer.setOptions(resolvedOptions)

onMount(() => {
onSettled(() => {
const cleanup = virtualizer._didMount()
virtualizer._willUpdate()
onCleanup(cleanup)
return cleanup
})

createComputed(() => {
virtualizer.setOptions(
mergeProps(resolvedOptions, options, {
onChange: (
instance: Virtualizer<TScrollElement, TItemElement>,
sync: boolean,
) => {
instance._willUpdate()
setVirtualItems(
reconcile(instance.getVirtualItems(), {
key: 'index',
}),
)
setTotalSize(instance.getTotalSize())
options.onChange?.(instance, sync)
},
}),
)
virtualizer._willUpdate()
setVirtualItems(reconcile(instance.getVirtualItems(), { key: 'index' }))
setTotalSize(instance.getTotalSize())
})
createEffect(
() => {
// Compute phase: (re)apply options, tracking every reactive getter
// read inside `setOptions`. No store/signal writes happen here — those
// are forbidden inside a reactive scope in v2. The `onChange` callback
// installed below fires from event/observer context, where writes are
// legal.
virtualizer.setOptions(
merge(resolvedOptions, options, {
onChange: (
instance: Virtualizer<TScrollElement, TItemElement>,
sync: boolean,
) => {
instance._willUpdate()
setVirtualItems(reconcile(instance.getVirtualItems(), 'index'))
setTotalSize(instance.getTotalSize())
options.onChange?.(instance, sync)
},
}),
)
},
() => {
// Apply phase: run the layout update and push the results into the
// store/signal. Writes are permitted here.
virtualizer._willUpdate()
setVirtualItems(reconcile(instance.getVirtualItems(), 'index'))
setTotalSize(instance.getTotalSize())
},
)

return virtualizer
}
Expand All @@ -99,7 +112,7 @@ export function createVirtualizer<
>,
): Virtualizer<TScrollElement, TItemElement> {
return createVirtualizerBase<TScrollElement, TItemElement>(
mergeProps(
merge(
{
observeElementRect: observeElementRect,
observeElementOffset: observeElementOffset,
Expand All @@ -120,7 +133,7 @@ export function createWindowVirtualizer<TItemElement extends Element>(
>,
): Virtualizer<Window, TItemElement> {
return createVirtualizerBase<Window, TItemElement>(
mergeProps(
merge(
{
getScrollElement: () =>
typeof document !== 'undefined' ? window : null,
Expand Down
10 changes: 8 additions & 2 deletions packages/solid-virtual/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { expect, test } from 'vitest'
import { createRoot, createSignal } from 'solid-js'
import { createRoot, createSignal, flush } from 'solid-js'

import { createVirtualizer } from '../src/index'

test('preserves measured sizes when reactive options change', () => {
createRoot((dispose) => {
const [count, setCount] = createSignal(2)
// In an app the count signal is written from an event handler (an
// unowned scope); here the write happens inside createRoot, so opt in.
const [count, setCount] = createSignal(2, { ownedWrite: true })
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
get count() {
return count()
Expand All @@ -17,9 +19,13 @@ test('preserves measured sizes when reactive options change', () => {

expect(virtualizer.getTotalSize()).toBe(120)
virtualizer.resizeItem(0, 100)
// v2 setters land on the next microtask flush; force it so the read below
// observes the resize synchronously.
flush()
expect(virtualizer.getTotalSize()).toBe(160)

setCount(3)
flush()

expect(virtualizer.itemSizeCache.get(0)).toBe(100)
expect(virtualizer.getTotalSize()).toBe(220)
Expand Down
Loading
Loading