Last active
January 11, 2025 04:26
-
-
Save bigmistqke/eed046d5dda67a67a927ba942424d073 to your computer and use it in GitHub Desktop.
reactive filesystem with solid
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
createSignal, | |
type Accessor, | |
type Setter, | |
createResource, | |
type Resource, | |
} from "solid-js"; | |
import { createStore, produce } from "solid-js/store"; | |
/**********************************************************************************/ | |
/* */ | |
/* Types */ | |
/* */ | |
/**********************************************************************************/ | |
type FileType = "js" | "css" | "wasm" | "unknown"; | |
type Module = Record<string, unknown>; | |
interface File { | |
type: FileType; | |
get: Accessor<string>; | |
set: Setter<string>; | |
module: { | |
cache: Resource<unknown>; | |
invalidate: () => void; | |
create<T extends Module>(): Promise<T>; | |
}; | |
url: { | |
cache: Resource<string>; | |
invalidate: () => void; | |
create(): string | Promise<string>; | |
}; | |
} | |
interface Dir { | |
type: "dir"; | |
} | |
type DirEnt = File | Dir; | |
type FileSystem = ReturnType<typeof createFileSystem>; | |
/**********************************************************************************/ | |
/* */ | |
/* Create File */ | |
/* */ | |
/**********************************************************************************/ | |
export function createFile({ | |
type, | |
initial, | |
createObjectURL, | |
}: { | |
type: "js" | "css" | "wasm" | "unknown"; | |
initial: string; | |
createObjectURL: (source: string) => string | Promise<string>; | |
}): File { | |
const [get, set] = createSignal<string>(initial); | |
const [url, { refetch }] = createResource(get, createObjectURL); | |
const [module] = createResource(url, (url) => import(url)); | |
function createUrl() { | |
return createObjectURL(get()); | |
} | |
async function createModule() { | |
return import(await createObjectURL(get())); | |
} | |
return { | |
type, | |
get, | |
set, | |
module: { | |
cache: module, | |
create: createModule, | |
invalidate: refetch, | |
}, | |
url: { | |
cache: url, | |
create: createUrl, | |
invalidate: refetch, | |
}, | |
}; | |
} | |
/**********************************************************************************/ | |
/* */ | |
/* Create File System */ | |
/* */ | |
/**********************************************************************************/ | |
export function createFileSystem( | |
extensions: Record< | |
string, | |
(path: string, source: string, fs: FileSystem) => File | |
>, | |
) { | |
const [dirEnts, setDirEnts] = createStore<Record<string, DirEnt>>({}); | |
function normalizePath(path: string) { | |
return path.replace(/^\/+/, ""); | |
} | |
function getExtension(path: string) { | |
return path.split("/").slice(-1)[0].split(".")[1]; | |
} | |
function getParentDirectory(path: string) { | |
return path.split("/").slice(0, -1).join("/"); | |
} | |
function assertPathExists(path: string) { | |
const parts = path.split("/"); | |
const pathExists = parts | |
.map((_, index) => parts.slice(0, index + 1).join("/")) | |
.filter(Boolean) | |
.every((path) => path in dirEnts); | |
if (!pathExists) { | |
throw `Path is invalid ${path}`; | |
} | |
} | |
function getFile(path: string) { | |
path = normalizePath(path); | |
assertPathExists(path); | |
const dirEnt = dirEnts[path]; | |
if (dirEnt.type === "dir") { | |
throw `Path is not a file: ${dirEnt}`; | |
} | |
return dirEnt; | |
} | |
function module(path: string) { | |
return getFile(path).module.cache(); | |
} | |
module.invalidate = (path: string) => { | |
return getFile(path).module.invalidate(); | |
}; | |
module.create = (path: string) => { | |
return getFile(path).module.create(); | |
}; | |
function url(path: string) { | |
return getFile(path).url.cache(); | |
} | |
url.invalidate = (path: string) => { | |
return getFile(path).url.invalidate(); | |
}; | |
url.create = (path: string) => { | |
return getFile(path).url.create(); | |
}; | |
const fs = { | |
module, | |
url(path: string) { | |
path = normalizePath(path); | |
assertPathExists(path); | |
const dirEnt = dirEnts[path]; | |
if (dirEnt.type === "dir") { | |
throw `Path is not a file: ${dirEnt}`; | |
} | |
return dirEnt.url; | |
}, | |
getType(path: string): DirEnt["type"] { | |
path = normalizePath(path); | |
assertPathExists(path); | |
return dirEnts[path].type; | |
}, | |
readdir(path: string, options?: { withFileTypes?: boolean }) { | |
path = normalizePath(path); | |
assertPathExists(path); | |
if (options?.withFileTypes) { | |
return Object.entries(dirEnts) | |
.filter(([_path]) => getParentDirectory(_path) === path) | |
.map(([path, file]) => ({ | |
type: file.type, | |
path, | |
})); | |
} | |
return Object.keys(dirEnts).filter( | |
(_path) => getParentDirectory(_path) === path, | |
); | |
}, | |
mkdir(path: string, options?: { recursive?: boolean }) { | |
path = normalizePath(path); | |
if (options?.recursive) { | |
const parts = path.split("/"); | |
parts.forEach((_, index) => { | |
setDirEnts(parts.slice(0, index).join("/"), { type: "dir" }); | |
}); | |
return; | |
} | |
assertPathExists(getParentDirectory(path)); | |
setDirEnts(path, { type: "dir" }); | |
}, | |
readFile(path: string) { | |
path = normalizePath(path); | |
assertPathExists(path); | |
const dirEnt = dirEnts[path]; | |
if (dirEnt.type === "dir") { | |
throw `Path is not a file ${path}`; | |
} | |
return dirEnt.get(); | |
}, | |
rename(previous: string, next: string) { | |
previous = normalizePath(previous); | |
next = normalizePath(next); | |
assertPathExists(previous); | |
setDirEnts( | |
produce((files) => { | |
Object.keys(dirEnts).forEach((path) => { | |
if (path.startsWith(previous)) { | |
const newPath = path.replace(previous, next); | |
const file = files[path]; | |
files[newPath] = file; | |
delete files[path]; | |
} | |
}); | |
}), | |
); | |
}, | |
rm(path: string, options?: { force?: boolean; recursive?: boolean }) { | |
path = normalizePath(path); | |
if (!options || !options.force) { | |
assertPathExists(path); | |
} | |
if (!options || !options.recursive) { | |
const _dirEnts = Object.keys(dirEnts).filter((value) => { | |
if (value === path) return false; | |
return value.includes(path); | |
}); | |
if (_dirEnts.length > 0) { | |
throw `Directory is not empty ${_dirEnts}`; | |
} | |
} | |
setDirEnts( | |
produce((files) => { | |
Object.keys(files) | |
.filter((value) => value.includes(path)) | |
.forEach((path) => delete files[path]); | |
}), | |
); | |
}, | |
writeFile(path: string, source: string) { | |
path = normalizePath(path); | |
assertPathExists(getParentDirectory(path)); | |
const dirEnt = dirEnts[path]; | |
if (dirEnt?.type === "dir") { | |
throw `A directory already exist with the same name: ${path}`; | |
} | |
const extension = getExtension(path); | |
if (dirEnt) { | |
dirEnt.set(source); | |
} else { | |
const dirEnt = | |
extensions[extension]?.(path, source, fs) ?? | |
createFile({ | |
type: "unknown", | |
initial: source, | |
createObjectURL: () => new Promise<string>(() => {}), | |
}); | |
setDirEnts(path, dirEnt); | |
} | |
}, | |
}; | |
return fs; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment