Skip to content

Instantly share code, notes, and snippets.

@bigmistqke
Last active January 11, 2025 04:26
Show Gist options
  • Save bigmistqke/eed046d5dda67a67a927ba942424d073 to your computer and use it in GitHub Desktop.
Save bigmistqke/eed046d5dda67a67a927ba942424d073 to your computer and use it in GitHub Desktop.
reactive filesystem with solid
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