From 591095ffba770c13684c74f13b0b7eef9fe7925f Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Mon, 18 May 2026 21:28:03 -0400 Subject: [PATCH] Migration filesystem fo Solid 2 beta 13 --- .changeset/filesystem-solid2-migration.md | 13 ++ packages/filesystem/package.json | 7 +- packages/filesystem/src/adapter-node.ts | 2 +- packages/filesystem/src/adapter-web.ts | 2 +- packages/filesystem/src/reactive.ts | 136 ++++++++++-------- packages/filesystem/src/tools.ts | 16 ++- packages/filesystem/test/index.test.ts | 166 ++++++++++++---------- pnpm-lock.yaml | 7 +- 8 files changed, 194 insertions(+), 155 deletions(-) create mode 100644 .changeset/filesystem-solid2-migration.md diff --git a/.changeset/filesystem-solid2-migration.md b/.changeset/filesystem-solid2-migration.md new file mode 100644 index 000000000..56ac9b202 --- /dev/null +++ b/.changeset/filesystem-solid2-migration.md @@ -0,0 +1,13 @@ +--- +"@solid-primitives/filesystem": major +--- + +Migrate to Solid.js v2.0 (beta.13). + +Breaking changes: +- `solid-js` peer dependency updated to `^2.0.0-beta.13` +- `@solidjs/web` is now a required peer dependency +- `isServer` is now imported from `@solidjs/web` +- `createSyncFileSystem` and `createAsyncFileSystem` internal signals use `ownedWrite: true` to support writes from reactive scopes +- `createAsyncFileSystem` no longer uses `createResource` — reads are backed by plain signals with manual `Promise`-based fetching, eliminating `ResourceActions` from the API +- The `toPromise` helper in `tools.ts` uses the Solid 2.0 split `createEffect(compute, apply)` pattern diff --git a/packages/filesystem/package.json b/packages/filesystem/package.json index b46d18cac..60278960f 100644 --- a/packages/filesystem/package.json +++ b/packages/filesystem/package.json @@ -1,6 +1,6 @@ { "name": "@solid-primitives/filesystem", - "version": "1.3.3", + "version": "2.0.0", "description": "A primitive for convenient file system access.", "author": "Alex Lohr ", "contributors": [], @@ -59,14 +59,15 @@ "test:ssr": "pnpm run vitest --mode ssr" }, "devDependencies": { + "@solidjs/web": "2.0.0-beta.13", "@tauri-apps/api": "^1.5.3", "@types/wicg-file-system-access": "2023.10.2", "chokidar": "^3.5.3", - "solid-js": "^1.9.7" + "solid-js": "2.0.0-beta.13" }, "peerDependencies": { "chokidar": "^3.5.3", - "solid-js": "^1.6.12" + "solid-js": "^2.0.0-beta.13" }, "peerDependenciesMeta": { "chokidar": { diff --git a/packages/filesystem/src/adapter-node.ts b/packages/filesystem/src/adapter-node.ts index 9aaa25b23..868550ba6 100644 --- a/packages/filesystem/src/adapter-node.ts +++ b/packages/filesystem/src/adapter-node.ts @@ -1,5 +1,5 @@ import { limitPath } from "./tools.js"; -import { isServer } from "solid-js/web"; +import { isServer } from "@solidjs/web"; export const makeNodeFileSystem = isServer ? async (basePath: string = "/") => { diff --git a/packages/filesystem/src/adapter-web.ts b/packages/filesystem/src/adapter-web.ts index 38490e075..ba2121e46 100644 --- a/packages/filesystem/src/adapter-web.ts +++ b/packages/filesystem/src/adapter-web.ts @@ -1,5 +1,5 @@ /// -import { isServer } from "solid-js/web"; +import { isServer } from "@solidjs/web"; import { type DirEntries } from "./types.js"; /** diff --git a/packages/filesystem/src/reactive.ts b/packages/filesystem/src/reactive.ts index 66ce16482..9484a7f45 100644 --- a/packages/filesystem/src/reactive.ts +++ b/packages/filesystem/src/reactive.ts @@ -1,5 +1,7 @@ -import { batch, createResource, createSignal } from "solid-js"; -import type { Accessor, Resource, ResourceActions, Setter } from "solid-js"; +import { createSignal } from "solid-js"; +import type { Accessor, Setter, SignalOptions } from "solid-js"; + +const OWNED_WRITE: SignalOptions = { ownedWrite: true }; import type { ItemType, @@ -14,7 +16,27 @@ import type { import { getItemName, getParentDir } from "./tools.js"; type SignalMap = Map, Setter]>; -type ResourceMap = Map, ResourceActions]>; + +type AsyncEntry = { + read: Accessor; + mutate: (val: T | undefined) => void; + refetch: () => void; +}; + +const makeAsyncEntry = (fetch: () => Promise): AsyncEntry => { + const [read, write] = createSignal(undefined, OWNED_WRITE); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const set = write as (v: T | undefined) => void; + const doFetch = () => { + fetch().then(v => set(v)); + }; + doFetch(); + return { + read, + mutate: (val: T | undefined) => set(val), + refetch: doFetch, + }; +}; /** makes a synchronous filesystem reactive */ export const createSyncFileSystem = ( @@ -27,21 +49,21 @@ export const createSyncFileSystem = ( const fs: SyncFileSystem = { getType: (path, refresh) => { if (!getTypeMap.has(path)) { - getTypeMap.set(path, createSignal()); + getTypeMap.set(path, createSignal(undefined, OWNED_WRITE)); } const [fileType, setFileType] = getTypeMap.get(path)!; if (refresh || fileType() === undefined) { - setFileType(adapter.getType(path)); + setFileType(adapter.getType(path) as Exclude); } return fileType(); }, readdir: (path, refresh) => { if (!readdirMap.has(path)) { - readdirMap.set(path, createSignal()); + readdirMap.set(path, createSignal(undefined, OWNED_WRITE)); } const [files, setFiles] = readdirMap.get(path)!; if (refresh || files() === undefined) { - setFiles(adapter.readdir(path)); + setFiles(adapter.readdir(path) as Exclude); } return files(); }, @@ -51,7 +73,7 @@ export const createSyncFileSystem = ( }, readFile: (path, refresh) => { if (!readFileMap.has(path)) { - readFileMap.set(path, createSignal()); + readFileMap.set(path, createSignal(undefined, OWNED_WRITE)); } const [data, setData] = readFileMap.get(path)!; if (refresh || data() === undefined) { @@ -144,35 +166,26 @@ export const createAsyncFileSystem = ( adapter: AsyncFileSystemAdapter, watcher?: Watcher, ): AsyncFileSystem => { - const getTypeMap: ResourceMap = new Map(); - const readdirMap: ResourceMap = new Map(); - const readFileMap: ResourceMap = new Map(); + const getTypeMap = new Map>(); + const readdirMap = new Map>(); + const readFileMap = new Map>(); + const fs: AsyncFileSystem = { getType: (path, refresh) => { if (!getTypeMap.has(path)) { - getTypeMap.set( - path, - createResource(() => adapter.getType(path)), - ); + getTypeMap.set(path, makeAsyncEntry(() => adapter.getType(path))); } - const [fileType, { refetch }] = getTypeMap.get(path)!; - if (refresh) { - refetch(); - } - return fileType(); + const entry = getTypeMap.get(path)!; + if (refresh) entry.refetch(); + return entry.read(); }, readdir: (path, refresh) => { if (!readdirMap.has(path)) { - readdirMap.set( - path, - createResource(() => adapter.readdir(path)), - ); + readdirMap.set(path, makeAsyncEntry(() => adapter.readdir(path))); } - const [files, { refetch }] = readdirMap.get(path)!; - if (refresh) { - refetch(); - } - return files(); + const entry = readdirMap.get(path)!; + if (refresh) entry.refetch(); + return entry.read(); }, mkdir: path => adapter.mkdir(path).then(() => { @@ -181,33 +194,30 @@ export const createAsyncFileSystem = ( if (!getTypeMap.has(subPath)) { fs.getType(subPath); } else { - getTypeMap.get(subPath)![1].refetch(); + getTypeMap.get(subPath)!.refetch(); } }); - readdirMap.get(getParentDir(path))?.[1].refetch(); + readdirMap.get(getParentDir(path))?.refetch(); }), readFile: (path, refresh) => { if (!readFileMap.has(path)) { - readFileMap.set( - path, - createResource(() => adapter.readFile(path)), - ); + readFileMap.set(path, makeAsyncEntry(() => adapter.readFile(path))); } - const [data, { refetch }] = readFileMap.get(path)!; - if (refresh) { - refetch(); - } - return data(); + const entry = readFileMap.get(path)!; + if (refresh) entry.refetch(); + return entry.read(); }, writeFile: (path, data) => adapter.writeFile(path, data).then(() => { - readFileMap.get(path)?.[1].mutate(data); + readFileMap.get(path)?.mutate(data); const name = getItemName(path); - readdirMap - .get(getParentDir(path))?.[1] - .mutate((items = []) => - items.includes(name as never) ? items : ([...items, name] as DirEntries), - ); + const readdirEntry = readdirMap.get(getParentDir(path)); + if (readdirEntry) { + const items = readdirEntry.read() ?? []; + if (!items.includes(name as never)) { + readdirEntry.mutate([...items, name] as DirEntries); + } + } }), rename: async (previous, next) => { const previousType = await adapter.getType(previous); @@ -233,40 +243,42 @@ export const createAsyncFileSystem = ( } await adapter.rm(previous); } - batch(() => { - getTypeMap.get(previous)?.[1].mutate(undefined); - getTypeMap.delete(previous); - const previousParent = getParentDir(previous); - readdirMap.get(previousParent)?.[1].refetch(); - const nextParent = getParentDir(next); - if (previousParent !== nextParent) readdirMap.get(nextParent)?.[1].refetch(); - }); + getTypeMap.get(previous)?.mutate(undefined); + getTypeMap.delete(previous); + const previousParent = getParentDir(previous); + readdirMap.get(previousParent)?.refetch(); + const nextParent = getParentDir(next); + if (previousParent !== nextParent) readdirMap.get(nextParent)?.refetch(); }, rm: path => adapter.rm(path).then(() => { - getTypeMap.get(path)?.[1].mutate(undefined); + getTypeMap.get(path)?.mutate(undefined); getTypeMap.delete(path); [...readdirMap.keys()].forEach(item => { if (item.startsWith(`${path}/`)) { - getTypeMap.get(item)?.[1].mutate(undefined); + getTypeMap.get(item)?.mutate(undefined); getTypeMap.delete(item); } }); - readdirMap.get(getParentDir(path))?.[1].refetch(); + readdirMap.get(getParentDir(path))?.refetch(); }), }; if (watcher) { watcher((operation, path) => { if (operation === "mkdir" || operation === "rm") { - readdirMap - .get(getParentDir(path))?.[1] - .mutate((items = []) => (items.includes(path as never) ? items : [...items, path])); + const readdirEntry = readdirMap.get(getParentDir(path)); + if (readdirEntry) { + const items = readdirEntry.read() ?? []; + if (!items.includes(path as never)) { + readdirEntry.mutate([...items, path] as DirEntries); + } + } } if (operation === "rm") { - getTypeMap.get(path)?.[1].mutate(null); + getTypeMap.get(path)?.mutate(null); } if (operation === "writeFile") { - readFileMap.get(path)?.[1].refetch(); + readFileMap.get(path)?.refetch(); } }); } diff --git a/packages/filesystem/src/tools.ts b/packages/filesystem/src/tools.ts index 746410218..5909422b9 100644 --- a/packages/filesystem/src/tools.ts +++ b/packages/filesystem/src/tools.ts @@ -26,13 +26,15 @@ export const limitPath = export const toPromise = (command: () => T | undefined): Promise => new Promise(resolve => createRoot(dispose => - createEffect(() => { - const result = command(); - if (result !== undefined) { - resolve(result); - dispose(); - } - }), + createEffect( + () => command(), + result => { + if (result !== undefined) { + resolve(result as T); + dispose(); + } + }, + ), ), ); diff --git a/packages/filesystem/test/index.test.ts b/packages/filesystem/test/index.test.ts index 2911d938f..f90e058af 100644 --- a/packages/filesystem/test/index.test.ts +++ b/packages/filesystem/test/index.test.ts @@ -12,7 +12,7 @@ import { makeTauriFileSystem, rsync, } from "../src/index.js"; -import { catchError, createEffect, createRoot } from "solid-js"; +import { createEffect, createRoot } from "solid-js"; describe("makeNoFileSystem", () => { const fs = makeNoFileSystem(); @@ -128,17 +128,21 @@ describe("createFileSystem (sync) relays file system errors", () => { test("a deleted file stored in a signal throws an error", () => new Promise((done, fail) => { setTimeout(() => fail(new Error("did not throw")), 100); - const fs = createFileSystem(makeVirtualFileSystem({ "test.json": "{}" })); - catchError( - () => { - createEffect(() => fs.readFile("test.json")); - setTimeout(() => fs.rm("test.json"), 30); - }, - error => { - expect(error).toEqual(new Error('"test.json" is not a file')); - done(); - }, - ); + const vfs = makeVirtualFileSystem({ "test.json": "{}" }); + const fs = createFileSystem(vfs); + createRoot(() => { + createEffect( + () => fs.readFile("test.json"), + { + effect: () => {}, + error: err => { + expect(err).toEqual(new Error('"test.json" is not a file')); + done(); + }, + }, + ); + setTimeout(() => fs.rm("test.json"), 30); + }); })); }); @@ -185,10 +189,13 @@ describe("createFileSystem(makeWebAccessFileSystem)", async () => { let captured: unknown; let dispose = createRoot(dispose => { - createEffect(() => { - captured = fs.getType("/src/index.ts"); - resolve?.(); - }); + createEffect( + () => fs.getType("/src/index.ts"), + val => { + captured = val; + if (val !== undefined) resolve?.(); + }, + ); return dispose; }); @@ -201,15 +208,18 @@ describe("createFileSystem(makeWebAccessFileSystem)", async () => { dispose(); dispose = createRoot(dispose => { - createEffect(() => { - captured = fs.getType("/src"); - resolve?.(); - }); + createEffect( + () => fs.getType("/src"), + val => { + captured = val; + if (val !== undefined) resolve?.(); + }, + ); return dispose; }); - expect(captured).toEqual(undefined); + expect(captured).toEqual("file"); // from previous await new Promise(r => (resolve = r)); expect(captured).toEqual("dir"); @@ -222,10 +232,13 @@ describe("createFileSystem(makeWebAccessFileSystem)", async () => { let captured: unknown; const dispose = createRoot(dispose => { - createEffect(() => { - captured = fs.readdir("src"); - resolve?.(); - }); + createEffect( + () => fs.readdir("src"), + val => { + captured = val; + if (val !== undefined) resolve?.(); + }, + ); return dispose; }); @@ -240,77 +253,74 @@ describe("createFileSystem(makeWebAccessFileSystem)", async () => { test("fs.mkdir updates existing readdir", async () => { let resolve: (() => void) | undefined; - const captured: unknown[] = []; + let captured: unknown; const dispose = createRoot(dispose => { - createEffect(() => { - captured.push(fs.readdir("/")); - resolve?.(); - }); + createEffect( + () => fs.readdir("/"), + val => { + captured = val; + if (val !== undefined) resolve?.(); + }, + ); return dispose; }); - expect(captured).toEqual([undefined]); - captured.length = 0; - await new Promise(r => (resolve = r)); - expect(captured).toEqual([["src", "data"]]); - captured.length = 0; + expect(captured).toEqual(["src", "data"]); fs.mkdir("/test"); await new Promise(r => (resolve = r)); - expect(captured).toEqual([["src", "data", "test"]]); + expect(captured).toEqual(["src", "data", "test"]); dispose(); }); test("fs.readFile returns the content", async () => { let resolve: (() => void) | undefined; - const captured: unknown[] = []; + let captured: unknown; const dispose = createRoot(dispose => { - createEffect(() => { - captured.push(fs.readFile("/src/index.ts")); - resolve?.(); - }); + createEffect( + () => fs.readFile("/src/index.ts"), + val => { + captured = val; + if (val !== undefined) resolve?.(); + }, + ); return dispose; }); - expect(captured).toEqual([undefined]); - captured.length = 0; - await new Promise(r => (resolve = r)); - expect(captured).toEqual(["// test"]); - captured.length = 0; + expect(captured).toEqual("// test"); dispose(); }); test("fs.writeFile updates a readFile resource", async () => { let resolve: (() => void) | undefined; - const captured: unknown[] = []; + let captured: unknown; const dispose = createRoot(dispose => { - createEffect(() => { - captured.push(fs.readFile("/data/data.json")); - resolve?.(); - }); + createEffect( + () => fs.readFile("/data/data.json"), + val => { + captured = val; + if (val !== undefined) resolve?.(); + }, + ); return dispose; }); - expect(captured).toEqual([undefined]); - captured.length = 0; - await new Promise(r => (resolve = r)); - expect(captured).toEqual(["[1, 2, 3]"]); - captured.length = 0; + expect(captured).toEqual("[1, 2, 3]"); fs.writeFile("/data/data.json", "[1, 2, 3, 4]"); await new Promise(r => (resolve = r)); - expect(captured).toEqual(["[1, 2, 3, 4]"]); + expect(captured).toEqual("[1, 2, 3, 4]"); dispose(); }); @@ -319,27 +329,26 @@ describe("createFileSystem(makeWebAccessFileSystem)", async () => { await fs.mkdir("/src/subdir"); let resolve: (() => void) | undefined; - const captured: unknown[] = []; + let captured: unknown; const dispose = createRoot(dispose => { - createEffect(() => { - captured.push(fs.readdir("/src")); - resolve?.(); - }); + createEffect( + () => fs.readdir("/src"), + val => { + captured = val; + if (val !== undefined) resolve?.(); + }, + ); return dispose; }); - expect(captured).toEqual([undefined]); - captured.length = 0; - await new Promise(r => (resolve = r)); - expect(captured).toEqual([["index.ts", "subdir"]]); - captured.length = 0; + expect(captured).toEqual(["index.ts", "subdir"]); fs.rm("/src/subdir"); await new Promise(r => (resolve = r)); - expect(captured).toEqual([["index.ts"]]); + expect(captured).toEqual(["index.ts"]); dispose(); }); @@ -349,27 +358,26 @@ describe("createFileSystem(makeWebAccessFileSystem)", async () => { await fs.writeFile("/data/subdir/test.ts", "// test"); let resolve: (() => void) | undefined; - const captured: unknown[] = []; + let captured: unknown; const dispose = createRoot(dispose => { - createEffect(() => { - captured.push(fs.readdir("/data/subdir")); - resolve?.(); - }); + createEffect( + () => fs.readdir("/data/subdir"), + val => { + captured = val; + if (val !== undefined) resolve?.(); + }, + ); return dispose; }); - expect(captured).toEqual([undefined]); - captured.length = 0; - await new Promise(r => (resolve = r)); - expect(captured).toEqual([["test.ts"]]); - captured.length = 0; + expect(captured).toEqual(["test.ts"]); fs.rename("/data/subdir/test.ts", "/data/subdir/index.ts"); await new Promise(r => (resolve = r)); - expect(captured).toEqual([["index.ts"]]); + expect(captured).toEqual(["index.ts"]); fs.rm("/data/subdir"); dispose(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce8026663..c01b721d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -335,6 +335,9 @@ importers: packages/filesystem: devDependencies: + '@solidjs/web': + specifier: 2.0.0-beta.13 + version: 2.0.0-beta.13(solid-js@2.0.0-beta.13) '@tauri-apps/api': specifier: ^1.5.3 version: 1.6.0 @@ -345,8 +348,8 @@ importers: specifier: ^3.5.3 version: 3.6.0 solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.13 + version: 2.0.0-beta.13 packages/flux-store: devDependencies: