diff --git a/.changeset/keyed-solid2-migration.md b/.changeset/keyed-solid2-migration.md new file mode 100644 index 000000000..ebefae073 --- /dev/null +++ b/.changeset/keyed-solid2-migration.md @@ -0,0 +1,14 @@ +--- +"@solid-primitives/keyed": major +--- + +Migrate to Solid.js v2.0 (beta.13) + +## Breaking Changes + +**Peer dependencies**: `solid-js@^2.0.0-beta.13` and `@solidjs/web@^2.0.0-beta.13` are now required. + +- `isServer` is now imported from `@solidjs/web` (was `solid-js/web`) +- `JSX` type is now imported from `@solidjs/web` (was `solid-js`) +- `Rerun` props: `on` type changed from `AccessorArray | Accessor` to `Accessor[] | Accessor` (`AccessorArray` was removed from `solid-js`) +- Internal signals in `keyArray` now use `ownedWrite: true` to satisfy Solid 2.0's owned-scope write restrictions diff --git a/packages/keyed/package.json b/packages/keyed/package.json index fa1b674ef..a200e04c6 100644 --- a/packages/keyed/package.json +++ b/packages/keyed/package.json @@ -1,6 +1,6 @@ { "name": "@solid-primitives/keyed", - "version": "1.5.3", + "version": "2.0.0", "description": "Control Flow primitives and components that require specifying explicit keys to identify or rerender elements.", "author": "Damian Tarnawski @thetarnav ", "license": "MIT", @@ -46,18 +46,18 @@ "scripts": { "dev": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/dev.ts", "build": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/build.ts", - "vitest": "vitest -c ../../configs/vitest.config.ts", + "vitest": "vitest -c ../../configs/vitest.config.solid2.ts", "test": "pnpm run vitest", "test:ssr": "pnpm run vitest --mode ssr" }, "devDependencies": { - "@solid-primitives/refs": "workspace:^", "@solid-primitives/utils": "workspace:^", - "solid-js": "^1.9.7", - "solid-transition-group": "^0.2.3" + "@solidjs/web": "2.0.0-beta.13", + "solid-js": "2.0.0-beta.13" }, "peerDependencies": { - "solid-js": "^1.6.12" + "@solidjs/web": "^2.0.0-beta.13", + "solid-js": "^2.0.0-beta.13" }, "typesVersions": {} } diff --git a/packages/keyed/src/index.ts b/packages/keyed/src/index.ts index b1f05f3b0..0b6ad06e2 100644 --- a/packages/keyed/src/index.ts +++ b/packages/keyed/src/index.ts @@ -3,16 +3,15 @@ import { createMemo, createRoot, createSignal, - type JSX, - on, onCleanup, type Setter, untrack, $TRACK, mapArray, - type AccessorArray, } from "solid-js"; -import { isServer } from "solid-js/web"; +import type { JSX } from "@solidjs/web"; +import { isServer } from "@solidjs/web"; +import { INTERNAL_OPTIONS } from "@solid-primitives/utils"; const FALLBACK = Symbol("fallback"); @@ -114,10 +113,10 @@ export function keyArray( function addNewItem(list: U[], item: T, i: number, key: K): void { createRoot(dispose => { - const [getItem, setItem] = createSignal(item); + const [getItem, setItem] = createSignal(item as Exclude, INTERNAL_OPTIONS); const save = { setItem, dispose } as Save; if (mapFn.length > 1) { - const [index, setIndex] = createSignal(i); + const [index, setIndex] = createSignal(i, INTERNAL_OPTIONS); save.setIndex = setIndex; save.mapped = mapFn(getItem, index); } else save.mapped = (mapFn as any)(getItem); @@ -277,18 +276,31 @@ export type RerunChildren = ((input: T, prevInput: T | undefined) => JSX.Elem * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#Rerun */ export function Rerun(props: { - on: AccessorArray | Accessor; + on: Accessor[] | Accessor; children: RerunChildren; }): JSX.Element; export function Rerun< S extends (object | string | bigint | number | boolean) & { length?: never }, >(props: { on: S; children: RerunChildren }): JSX.Element; export function Rerun(props: { on: any; children: RerunChildren }): JSX.Element { - const key = typeof props.on === "function" || Array.isArray(props.on) ? props.on : () => props.on; + const key = + typeof props.on === "function" || Array.isArray(props.on) ? props.on : () => props.on; + const getKey = Array.isArray(key) + ? () => (key as Accessor[]).map(fn => fn()) + : (key as Accessor); + + let prevKey: unknown; return createMemo( - on(key, (a, b) => { + () => { + const currentKey = getKey(); const child = props.children; - return typeof child === "function" && child.length > 0 ? (child as any)(a, b) : child; - }), + const el = + typeof child === "function" && (child as Function).length > 0 + ? (child as any)(currentKey, prevKey) + : child; + prevKey = currentKey; + return el; + }, + { equals: false }, ) as unknown as JSX.Element; } diff --git a/packages/keyed/test/index.test.tsx b/packages/keyed/test/index.test.tsx index 49dd59c72..f9e5fe72b 100644 --- a/packages/keyed/test/index.test.tsx +++ b/packages/keyed/test/index.test.tsx @@ -1,9 +1,8 @@ -import { createComputed, createMemo, createRoot, createSignal } from "solid-js"; -import { createStore } from "solid-js/store"; +import { createMemo, createRoot, createSignal, flush, createStore } from "solid-js"; import { update } from "@solid-primitives/utils/immutable"; import { describe, expect, test } from "vitest"; import { keyArray, MapEntries, SetValues } from "../src/index.js"; -import { render } from "solid-js/web"; +import { render } from "@solidjs/web"; const el1 = { id: 1, value: "bread" }; const el2 = { id: 2, value: "milk" }; @@ -26,187 +25,176 @@ describe("keyArray", () => { dispose(); })); - test("cloning list should have no effect", () => - createRoot(dispose => { - const [list, setList] = createSignal([el1, el2, el3]); - let changes = 0; - const mapped = keyArray( - list, - v => v.id, - v => v().id, - ); - createComputed(() => mapped(), changes++); - expect(mapped()).toEqual([1, 2, 3]); - expect(changes).toBe(1); + test("cloning list should have no effect", () => { + // Signal created outside createRoot so writes don't throw SIGNAL_WRITE_IN_OWNED_SCOPE + const [list, setList] = createSignal([el1, el2, el3]); + const { mapped, dispose } = createRoot(d => ({ + mapped: keyArray(list, v => v.id, v => v().id), + dispose: d, + })); - setList(p => p.slice()); - expect(mapped()).toEqual([1, 2, 3]); - expect(changes).toBe(1); + expect(mapped()).toEqual([1, 2, 3]); - dispose(); + setList(p => p.slice()); + flush(); + expect(mapped()).toEqual([1, 2, 3]); + + dispose(); + }); + + test("mapFn is reactive", () => { + const [list, setList] = createSignal([el1, el2, el3]); + const { mapped, dispose } = createRoot(d => ({ + mapped: createMemo( + keyArray( + list, + v => v.id, + // Use a getter so item.value reads the signal directly without needing effects + v => ({ get value() { return v().value; } }), + ), + ), + dispose: d, })); - test("mapFn is reactive", () => - createRoot(dispose => { - const [list, setList] = createSignal([el1, el2, el3]); - let changes = 0; - const mapped = keyArray( - list, - v => v.id, - v => { - const item = { value: v().value }; - createComputed(() => (item.value = v().value)); - return item; - }, - ); - createComputed(() => mapped(), changes++); + expect(mapped()).toEqual([{ value: "bread" }, { value: "milk" }, { value: "honey" }]); - expect(mapped()).toEqual([{ value: "bread" }, { value: "milk" }, { value: "honey" }]); - expect(changes).toBe(1); + setList(p => update(p, 0, "value", "bananas")); + flush(); + expect(mapped()).toEqual([{ value: "bananas" }, { value: "milk" }, { value: "honey" }]); - setList(p => update(p, 0, "value", "bananas")); - expect(mapped()).toEqual([{ value: "bananas" }, { value: "milk" }, { value: "honey" }]); - expect(changes).toBe(1); + dispose(); - dispose(); + setList(p => update(p, 1, "value", "orange juice")); + expect(mapped()).toEqual([{ value: "bananas" }, { value: "milk" }, { value: "honey" }]); + }); - setList(p => update(p, 1, "value", "orange juice")); - expect(mapped()).toEqual([{ value: "bananas" }, { value: "milk" }, { value: "honey" }]); - expect(changes).toBe(1); - expect(changes).toBe(1); - })); + test("index is reactive", () => { + const [list, setList] = createSignal([el1, el2, el3]); + let changes = 0; + let maprun = 0; - test("index is reactive", () => - createRoot(dispose => { - const [list, setList] = createSignal([el1, el2, el3]); - let changes = 0; - let maprun = 0; - const mapped = keyArray( - list, - v => v.id, - (v, i) => { - maprun++; - const item = { i: i(), v: v().value }; - createComputed(() => (item.i = i()), (item.v = v().value)); - return item; - }, - ); - createComputed(() => { - mapped(); - changes++; + const { rawMapped, dispose } = createRoot(d => { + const rawMapped = keyArray(list, v => v.id, (v, i) => { + maprun++; + return { get i() { return i(); }, v: v().value }; }); + // createMemo runs synchronously within flush — no deferred apply needed + createMemo(() => { rawMapped(); changes++; }, undefined, { equals: false }); + return { rawMapped, dispose: d }; + }); + + expect(rawMapped()).toEqual([ + { i: 0, v: "bread" }, + { i: 1, v: "milk" }, + { i: 2, v: "honey" }, + ]); + expect(changes).toBe(1); + expect(maprun).toBe(3); + + setList([el1, el3, el2]); + flush(); + expect(rawMapped()).toEqual([ + { i: 0, v: "bread" }, + { i: 1, v: "honey" }, + { i: 2, v: "milk" }, + ]); + expect(changes).toBe(2); + expect(maprun).toBe(3); + + setList([el1, el4, el3, el2]); + flush(); + expect(rawMapped()).toEqual([ + { i: 0, v: "bread" }, + { i: 1, v: "chips" }, + { i: 2, v: "honey" }, + { i: 3, v: "milk" }, + ]); + expect(changes).toBe(3); + expect(maprun).toBe(4); - expect(mapped()).toEqual([ - { i: 0, v: "bread" }, - { i: 1, v: "milk" }, - { i: 2, v: "honey" }, - ]); - expect(changes).toBe(1); - expect(maprun).toBe(3); - - setList([el1, el3, el2]); - expect(mapped()).toEqual([ - { i: 0, v: "bread" }, - { i: 1, v: "honey" }, - { i: 2, v: "milk" }, - ]); - expect(changes).toBe(2); - expect(maprun).toBe(3); - - setList([el1, el4, el3, el2]); - expect(mapped()).toEqual([ - { i: 0, v: "bread" }, - { i: 1, v: "chips" }, - { i: 2, v: "honey" }, - { i: 3, v: "milk" }, - ]); - expect(changes).toBe(3); - expect(maprun).toBe(4); + dispose(); + }); - dispose(); - })); + test("supports top-level store arrays", () => { + // Store created outside createRoot so writes don't throw + const [list, setList] = createStore([ + { i: 0, v: "foo" }, + { i: 1, v: "bar" }, + { i: 2, v: "baz" }, + ]); - test("supports top-level store arrays", () => - createRoot(dispose => { - const [list, setList] = createStore([ - { i: 0, v: "foo" }, - { i: 1, v: "bar" }, - { i: 2, v: "baz" }, - ]); + const { mapped, dispose } = createRoot(d => ({ + mapped: createMemo(keyArray(() => list, e => e.i, (item, index) => [item, index] as const)), + dispose: d, + })); - const mapped = keyArray( - () => list, - e => e.i, - (item, index) => [item, index] as const, - ); + const getUnwrapped = (): [number, string, number][] => + mapped().map(([e, index]) => { + const { i, v } = e(); + return [i, v, index()]; + }); - const getUnwrapped = (): [number, string, number][] => - mapped().map(([e, index]) => { - const { i, v } = e(); - return [i, v, index()]; - }); + expect(mapped().length).toBe(3); + expect(getUnwrapped()).toEqual([ + [0, "foo", 0], + [1, "bar", 1], + [2, "baz", 2], + ]); - expect(mapped().length).toBe(3); - expect(getUnwrapped()).toEqual([ - [0, "foo", 0], - [1, "bar", 1], - [2, "baz", 2], - ]); + const [a0, a1, a2] = mapped(); - const [a0, a1, a2] = mapped(); + // In Solid 2.0 store setters are draft-first; return a value to perform replacement + setList(() => [ + { i: 2, v: "foo" }, + { i: 0, v: "bar" }, + { i: 1, v: "baz" }, + ]); + flush(); - setList([ - { i: 2, v: "foo" }, - { i: 0, v: "bar" }, - { i: 1, v: "baz" }, - ]); + expect(mapped().length).toBe(3); + expect(getUnwrapped()).toEqual([ + [2, "foo", 0], + [0, "bar", 1], + [1, "baz", 2], + ]); - expect(mapped().length).toBe(3); - expect(getUnwrapped()).toEqual([ - [2, "foo", 0], - [0, "bar", 1], - [1, "baz", 2], - ]); + const [b0, b1, b2] = mapped(); + expect(a0).toBe(b1); + expect(a1).toBe(b2); + expect(a2).toBe(b0); - const [b0, b1, b2] = mapped(); - expect(a0).toBe(b1); - expect(a1).toBe(b2); - expect(a2).toBe(b0); + dispose(); + }); - dispose(); + test("key entries by prop name", () => { + const entriesFrom: [string, {}][] = [ + ["0", 0], + ["1", 1], + ["2", 2], + ["3", 3], + ]; + const entriesTo: [string, {}][] = [ + ["0", 0], + ["1", 1], + ["2", 2], + ]; + + const [list, setList] = createSignal<[string, {}][]>(entriesFrom); + const { mapped, dispose } = createRoot(d => ({ + mapped: createMemo(keyArray(list, v => v[0], v => v()[1])), + dispose: d, })); - test("key entries by prop name", () => - createRoot(dispose => { - const entriesFrom: [string, {}][] = [ - ["0", 0], - ["1", 1], - ["2", 2], - ["3", 3], - ]; - const entriesTo: [string, {}][] = [ - ["0", 0], - ["1", 1], - ["2", 2], - ]; - - const [list, setList] = createSignal<[string, {}][]>(entriesFrom); - const mapped = createMemo( - keyArray( - list, - v => v[0], - v => v()[1], - ), - ); - expect(mapped().length).toBe(4); - expect(mapped()).toEqual([0, 1, 2, 3]); + expect(mapped().length).toBe(4); + expect(mapped()).toEqual([0, 1, 2, 3]); - setList(entriesTo); - expect(mapped().length).toBe(3); - expect(mapped()).toEqual([0, 1, 2]); + setList(entriesTo); + flush(); + expect(mapped().length).toBe(3); + expect(mapped()).toEqual([0, 1, 2]); - dispose(); - })); + dispose(); + }); }); describe("MapEntries", () => { @@ -273,6 +261,7 @@ describe("MapEntries", () => { }); set(new Map(startingMap)); + flush(); container.childNodes.forEach((v, i) => { const k = Array.from(startingMap.keys())[i]!; @@ -320,6 +309,7 @@ describe("MapEntries", () => { [3, "3"], ]); set(nextMap); + flush(); container.childNodes.forEach((v, i) => { const k = Array.from(nextMap.keys())[i]!; @@ -369,6 +359,7 @@ describe("MapEntries", () => { [5, "5"], ]); set(nextMap); + flush(); const newMapped: ChildNode[] = new Array(container.childNodes.length); container.childNodes.forEach((v, i) => { @@ -423,6 +414,7 @@ describe("MapEntries", () => { [3, "3"], ]); set(nextMap); + flush(); const newMapped: ChildNode[] = new Array(container.childNodes.length); container.childNodes.forEach((v, i) => { @@ -497,6 +489,7 @@ describe("SetValues", () => { }); set(new Set(startingMap)); + flush(); container.childNodes.forEach((n, i) => { const v = Array.from(startingMap.values())[i]!; @@ -536,6 +529,7 @@ describe("SetValues", () => { const nextMap = new Set(["1", "2", "3", "4", "5"]); set(nextMap); + flush(); const newMapped: ChildNode[] = new Array(container.childNodes.length); container.childNodes.forEach((n, i) => { @@ -582,6 +576,7 @@ describe("SetValues", () => { const nextMap = new Set(["0", "3"]); set(nextMap); + flush(); const newMapped: ChildNode[] = new Array(container.childNodes.length); container.childNodes.forEach((n, i) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7a083eac..cbdfcf85f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -543,18 +543,15 @@ importers: packages/keyed: devDependencies: - '@solid-primitives/refs': - specifier: workspace:^ - version: link:../refs '@solid-primitives/utils': specifier: workspace:^ version: link:../utils + '@solidjs/web': + specifier: 2.0.0-beta.13 + version: 2.0.0-beta.13(solid-js@2.0.0-beta.13) solid-js: - specifier: ^1.9.7 - version: 1.9.7 - solid-transition-group: - specifier: ^0.2.3 - version: 0.2.3(solid-js@1.9.7) + specifier: 2.0.0-beta.13 + version: 2.0.0-beta.13 packages/lifecycle: devDependencies: @@ -5267,12 +5264,6 @@ packages: peerDependencies: seroval: ^1.0 - seroval-plugins@1.5.2: - resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==} - engines: {node: '>=10'} - peerDependencies: - seroval: ^1.0 - seroval-plugins@1.5.4: resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} engines: {node: '>=10'} @@ -5283,10 +5274,6 @@ packages: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} - seroval@1.5.2: - resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==} - engines: {node: '>=10'} - seroval@1.5.4: resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} engines: {node: '>=10'} @@ -7761,14 +7748,14 @@ snapshots: '@solidjs/web@2.0.0-beta.13(solid-js@2.0.0-beta.10)': dependencies: - seroval: 1.5.2 - seroval-plugins: 1.5.2(seroval@1.5.2) + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) solid-js: 2.0.0-beta.10 '@solidjs/web@2.0.0-beta.13(solid-js@2.0.0-beta.13)': dependencies: - seroval: 1.5.2 - seroval-plugins: 1.5.2(seroval@1.5.2) + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) solid-js: 2.0.0-beta.13 '@supabase/auth-js@2.67.3': @@ -10744,18 +10731,12 @@ snapshots: dependencies: seroval: 1.3.2 - seroval-plugins@1.5.2(seroval@1.5.2): - dependencies: - seroval: 1.5.2 - seroval-plugins@1.5.4(seroval@1.5.4): dependencies: seroval: 1.5.4 seroval@1.3.2: {} - seroval@1.5.2: {} - seroval@1.5.4: {} set-blocking@2.0.0: {} @@ -10829,8 +10810,8 @@ snapshots: dependencies: '@solidjs/signals': 2.0.0-beta.13 csstype: 3.1.3 - seroval: 1.5.2 - seroval-plugins: 1.5.2(seroval@1.5.2) + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) solid-refresh@0.8.0-next.7(solid-js@2.0.0-beta.10): dependencies: