diff --git a/README.md b/README.md index e5cb531..8481602 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,67 @@ selected parts of `node:fs/promises` as a foundation for core operations, strategically diverging to offer performance optimizations or broader compatibility. +For browser environments, the library uses [ZenFS](https://zenfs.dev/core/) to +provide a Node.js-compatible filesystem API with support for various storage +backends including in-memory, IndexedDB, and more. + For cross rutime _path_ operations, [jsr.io/@std/path](https://jsr.io/@std/path) will cover most scenarios, this library focuses on the file system operations. +## Browser Support + +This library supports browser environments through ZenFS, which provides a +Node.js-compatible filesystem API. By default, an in-memory filesystem is used, +but you can configure different backends such as IndexedDB for persistent +storage. + +### Basic Browser Usage + +```ts +import { readFile, writeFile } from "@cross/fs/io"; +import { exists, mkdir } from "@cross/fs"; + +// Use filesystem operations - in-memory by default +await mkdir("/data"); +await writeFile("/data/test.txt", "Hello from the browser!"); +console.log(await readFile("/data/test.txt", "utf8")); +// "Hello from the browser!" +``` + +### Configuring Persistent Storage + +For persistent browser storage using IndexedDB or other backends: + +```ts +import { configureBrowserFS } from "@cross/fs"; + +// Import ZenFS backends you want to use +// Note: Install @zenfs/dom separately for IndexedDB and WebAccess backends +const { IndexedDB, InMemory } = await import("@zenfs/dom"); + +// Configure before using filesystem operations +await configureBrowserFS({ + mounts: { + '/storage': { backend: IndexedDB }, // Persistent storage + '/tmp': { backend: InMemory } // Temporary in-memory storage + } +}); + +// Now use the filesystem with persistent storage +import { writeFile, readFile } from "@cross/fs/io"; +await writeFile("/storage/config.json", JSON.stringify({ theme: "dark" })); +``` + +### Browser Limitations + +Some operations are not available or have limitations in browser environments: + +- `chdir()` - Not supported (throws error) +- `cwd()` - Always returns "/" +- `which()` - Not applicable in browsers +- File permissions (chmod, chown) - Limited or not supported +- Symbolic links - Depends on ZenFS backend + ## Modules ### Stat @@ -42,19 +100,19 @@ console.log(await which("bun")); Methods: -| Method | Deno | Node | Bun | Base implementation | -| --------- | ---- | ---- | --- | ------------------- | -| stat | X | X | X | runtime native | -| lstat | X | X | X | node:fs/promises | -| exists | X | X | X | custom | -| isDir | X | X | X | custom | -| isFile | X | X | X | custom | -| isSymlink | X | X | X | custom | -| size | X | X | X | custom | -| find | X | X | X | custom | -| diskusage | X | X | X | custom | -| hash | X | X | X | custom | -| which | X | X | X | custom | +| Method | Deno | Node | Bun | Browser | Base implementation | +| --------- | ---- | ---- | --- | ------- | ------------------- | +| stat | X | X | X | X | runtime native | +| lstat | X | X | X | X | node:fs/promises | +| exists | X | X | X | X | custom | +| isDir | X | X | X | X | custom | +| isFile | X | X | X | X | custom | +| isSymlink | X | X | X | X | custom | +| size | X | X | X | X | custom | +| find | X | X | X | X | custom | +| diskusage | X | X | X | - | custom | +| hash | X | X | X | X | custom | +| which | X | X | X | - | custom | ### Io @@ -69,11 +127,10 @@ console.log(await readFile("my/file")); Methods: -| Method | Deno | Node | Bun | Base implementation | -| ---------- | ---- | ---- | --- | ------------------- | -| appendFile | X | X | X | node:fs/promises | -| readFile | X | X | X | node:fs/promises | -| writeFile | X | X | X | node:fs/promises | +| Method | Deno | Node | Bun | Browser | Base implementation | +| ---------- | ---- | ---- | --- | ------- | ------------------- | +| readFile | X | X | X | X | node:fs/promises | +| writeFile | X | X | X | X | node:fs/promises | ### Ops @@ -91,38 +148,38 @@ console.log(await dirpath("config")); Methods: -| Method | Deno | Node | Bun | Base implementation | -| --------- | ---- | ---- | --- | ------------------- | -| FsWatcher | X | X | X | custom | -| unlink | X | X | X | node:fs/promises | -| dirpath | X | X | X | @cross/dir | -| mkdir | X | X | X | node:fs/promises | -| cwd | X | X | X | custom | -| chdir | X | X | X | custom | -| mktempdir | X | X | X | custom | -| rm | X | X | X | node:fs/promises | -| rmdir | X | X | X | node:fs/promises | -| cp | X | X | X | node:fs/promises | -| tempfile | X | X | X | custom | -| link | X | X | X | node:fs/promises | -| unlink | X | X | X | node:fs/promises | -| readdir | X | X | X | node:fs/promises | -| readlink | X | X | X | node:fs/promises | -| realpath | X | X | X | node:fs/promises | -| rename | X | X | X | node:fs/promises | -| chmod | X | X | X | node:fs/promises | -| chown | X | X | X | node:fs/promises | -| rename | X | X | X | node:fs/promises | -| truncate | X | X | X | node:fs/promises | -| open | X | X | X | node:fs/promises | -| access | X | X | X | node:fs/promises | -| constants | X | X | X | node:fs/promises | +| Method | Deno | Node | Bun | Browser | Base implementation | +| --------- | ---- | ---- | --- | ------- | ------------------- | +| FsWatcher | X | X | X | - | custom | +| unlink | X | X | X | X | node:fs/promises | +| dirpath | X | X | X | - | @cross/dir | +| mkdir | X | X | X | X | node:fs/promises | +| cwd | X | X | X | X* | custom | +| chdir | X | X | X | - | custom | +| mktempdir | X | X | X | X | custom | +| rm | X | X | X | X | node:fs/promises | +| rmdir | X | X | X | X | node:fs/promises | +| cp | X | X | X | X | node:fs/promises | +| tempfile | X | X | X | X | custom | +| link | X | X | X | - | node:fs/promises | +| readdir | X | X | X | X | node:fs/promises | +| readlink | X | X | X | - | node:fs/promises | +| realpath | X | X | X | - | node:fs/promises | +| rename | X | X | X | X | node:fs/promises | +| chmod | X | X | X | - | node:fs/promises | +| chown | X | X | X | - | node:fs/promises | +| truncate | X | X | X | - | node:fs/promises | +| open | X | X | X | - | node:fs/promises | +| access | X | X | X | X | node:fs/promises | +| constants | X | X | X | X | node:fs/promises | + +*Browser: `cwd()` always returns "/" Types: -| Method | Deno | Node | Bun | Base implementation | -| --------- | ---- | ---- | --- | ------------------- | -| FSWatcher | X | X | X | node:fs/promises | +| Method | Deno | Node | Bun | Browser | Base implementation | +| --------- | ---- | ---- | --- | ------- | ------------------- | +| FSWatcher | X | X | X | - | node:fs/promises | Examples: diff --git a/deno.json b/deno.json index 89f8986..d221acd 100644 --- a/deno.json +++ b/deno.json @@ -14,7 +14,8 @@ "@cross/test": "jsr:@cross/test@^0.0.10", "@cross/utils": "jsr:@cross/utils@^0.16.0", "@std/assert": "jsr:@std/assert@^1.0.12", - "@std/path": "jsr:@std/path@^1.0.8" + "@std/path": "jsr:@std/path@^1.0.8", + "@zenfs/core": "npm:@zenfs/core@^1.0.9" }, "publish": { "exclude": [".github", "*.test.ts"] diff --git a/mod.ts b/mod.ts index 3903835..8065015 100644 --- a/mod.ts +++ b/mod.ts @@ -1,9 +1,11 @@ /** * @cross/fs - A cross-runtime utility library for file system operations * - * This package provides cross-runtime compatible file system operations for Node.js, Deno, and Bun. + * This package provides cross-runtime compatible file system operations for Node.js, Deno, Bun, and browsers. * It includes utilities for file/directory operations, file system watching, and common file system tasks. * + * For browser environments, it uses ZenFS to provide a Node.js-compatible filesystem API. + * * @example * ```typescript * import { exists, find, hash } from "@cross/fs"; @@ -18,9 +20,31 @@ * const fileHash = await hash("README.md", "sha256"); * ``` * + * @example Browser usage with custom ZenFS configuration + * ```typescript + * import { configureBrowserFS } from "@cross/fs"; + * import { IndexedDB } from "@zenfs/dom"; + * + * // Configure ZenFS before using filesystem operations + * await configureBrowserFS({ + * mounts: { + * '/storage': IndexedDB, + * '/tmp': { backend: InMemory } + * } + * }); + * + * // Now use filesystem operations normally + * import { writeFile, readFile } from "@cross/fs/io"; + * await writeFile("/storage/myfile.txt", "Hello browser!"); + * console.log(await readFile("/storage/myfile.txt", "utf8")); + * ``` + * * @module */ export * from "./src/stat/mod.ts"; export * from "./src/io/mod.ts"; export * from "./src/ops/mod.ts"; + +// Export browser configuration utilities +export { configureBrowserFS, getBrowserFS, isBrowserFSInitialized } from "./src/utils/browser-fs.ts"; diff --git a/src/io/mod.ts b/src/io/mod.ts index 5786058..5595d34 100644 --- a/src/io/mod.ts +++ b/src/io/mod.ts @@ -2,7 +2,8 @@ * @cross/fs/io - File input/output operations * * This module provides cross-runtime compatible functions for reading and writing files. - * It re-exports the core file I/O functions from node:fs/promises for consistency. + * It re-exports the core file I/O functions from node:fs/promises for Node.js/Deno/Bun, + * and uses ZenFS for browser environments. * * @example * ```typescript @@ -18,4 +19,60 @@ * @module */ -export { readFile, writeFile } from "node:fs/promises"; +import { CurrentRuntime, Runtime } from "@cross/runtime"; + +/** + * Reads the entire contents of a file. + * + * @param path - The path to the file + * @param options - Optional encoding or options object + * @returns The contents of the file + */ +export async function readFile( + path: string | URL, + options?: { encoding?: null; flag?: string } | null +): Promise; +export async function readFile( + path: string | URL, + options: { encoding: BufferEncoding; flag?: string } | BufferEncoding +): Promise; +export async function readFile( + path: string | URL, + options?: { encoding?: BufferEncoding | null; flag?: string } | BufferEncoding | null +): Promise { + if (CurrentRuntime === Runtime.Browser) { + const { getBrowserFS } = await import("../utils/browser-fs.ts"); + const fs = await getBrowserFS(); + //@ts-ignore ZenFS typing + return await fs.promises.readFile(path, options); + } else { + const { readFile: nodeReadFile } = await import("node:fs/promises"); + //@ts-ignore Cross-runtime typing + return await nodeReadFile(path, options); + } +} + +/** + * Writes data to a file, replacing the file if it already exists. + * + * @param path - The path to the file + * @param data - The data to write + * @param options - Optional encoding or options object + */ +export async function writeFile( + path: string | URL, + data: string | Uint8Array, + options?: { encoding?: BufferEncoding | null; mode?: number; flag?: string } | BufferEncoding | null +): Promise { + if (CurrentRuntime === Runtime.Browser) { + const { getBrowserFS } = await import("../utils/browser-fs.ts"); + const fs = await getBrowserFS(); + //@ts-ignore ZenFS typing + await fs.promises.writeFile(path, data, options); + } else { + const { writeFile: nodeWriteFile } = await import("node:fs/promises"); + //@ts-ignore Cross-runtime typing + await nodeWriteFile(path, data, options); + } +} + diff --git a/src/ops/chdir.ts b/src/ops/chdir.ts index 6d0a27e..574041e 100644 --- a/src/ops/chdir.ts +++ b/src/ops/chdir.ts @@ -4,6 +4,9 @@ import { CurrentRuntime, Runtime } from "@cross/runtime"; /** * Changes the current working directory in a cross-runtime compatible manner. * + * Note: This operation is not supported in browser environments as browsers don't have + * a mutable working directory concept. Calling this in a browser will throw an error. + * * @param {string} path - The new working directory path. * @throws If the directory change fails or unsupported runtime is encountered. * @example @@ -25,6 +28,8 @@ export function chdir(path: string): void { ) { //@ts-ignore cross-runtime process.chdir(path); + } else if (CurrentRuntime === Runtime.Browser) { + throw new Error("chdir is not supported in browser environments"); } else { throw new Error("Cannot change directory in the current runtime."); } diff --git a/src/ops/cwd.ts b/src/ops/cwd.ts index c5e0f8f..a3b2911 100644 --- a/src/ops/cwd.ts +++ b/src/ops/cwd.ts @@ -4,6 +4,9 @@ import { CurrentRuntime, Runtime } from "@cross/runtime"; /** * Returns the current working directory in a cross-runtime compatible manner. * + * Note: In browser environments, this returns "/" as browsers don't have a traditional + * working directory concept. + * * @returns {string} The current working directory path. * @throws * @example @@ -21,6 +24,9 @@ export function cwd(): string { ) { //@ts-ignore cross-runtime return process.cwd(); + } else if (CurrentRuntime === Runtime.Browser) { + // In browser, return root path as there's no traditional cwd concept + return "/"; } else { throw new Error( "Cannot determine working directory using current runtime.", diff --git a/src/ops/mod.ts b/src/ops/mod.ts index 6e1479c..192a16f 100644 --- a/src/ops/mod.ts +++ b/src/ops/mod.ts @@ -27,22 +27,7 @@ * @module */ -export { - chmod, - chown, - cp, - link, - mkdir, - open, - readdir, - readlink, - realpath, - rename, - rm, - rmdir, - truncate, - unlink, -} from "node:fs/promises"; +import { CurrentRuntime, Runtime } from "@cross/runtime"; export type { FSWatcher } from "node:fs"; @@ -51,3 +36,207 @@ export * from "./tempfile.ts"; export * from "./chdir.ts"; export * from "./cwd.ts"; export * from "./watch.ts"; + +/** + * Creates a directory. + */ +export async function mkdir( + path: string, + options?: { recursive?: boolean; mode?: number } +): Promise { + if (CurrentRuntime === Runtime.Browser) { + const { getBrowserFS } = await import("../utils/browser-fs.ts"); + const fs = await getBrowserFS(); + //@ts-ignore ZenFS typing + return await fs.promises.mkdir(path, options); + } else { + const { mkdir: nodeMkdir } = await import("node:fs/promises"); + return await nodeMkdir(path, options); + } +} + +/** + * Removes a directory. + */ +export async function rmdir(path: string, options?: { recursive?: boolean }): Promise { + if (CurrentRuntime === Runtime.Browser) { + const { getBrowserFS } = await import("../utils/browser-fs.ts"); + const fs = await getBrowserFS(); + //@ts-ignore ZenFS typing + await fs.promises.rmdir(path, options); + } else { + const { rmdir: nodeRmdir } = await import("node:fs/promises"); + await nodeRmdir(path, options); + } +} + +/** + * Removes a file or directory. + */ +export async function rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise { + if (CurrentRuntime === Runtime.Browser) { + const { getBrowserFS } = await import("../utils/browser-fs.ts"); + const fs = await getBrowserFS(); + //@ts-ignore ZenFS typing + await fs.promises.rm(path, options); + } else { + const { rm: nodeRm } = await import("node:fs/promises"); + await nodeRm(path, options); + } +} + +/** + * Removes a file. + */ +export async function unlink(path: string): Promise { + if (CurrentRuntime === Runtime.Browser) { + const { getBrowserFS } = await import("../utils/browser-fs.ts"); + const fs = await getBrowserFS(); + //@ts-ignore ZenFS typing + await fs.promises.unlink(path); + } else { + const { unlink: nodeUnlink } = await import("node:fs/promises"); + await nodeUnlink(path); + } +} + +/** + * Reads the contents of a directory. + */ +export async function readdir( + path: string, + options?: { encoding?: BufferEncoding | null; withFileTypes?: false } | BufferEncoding | null +): Promise; +export async function readdir( + path: string, + options: { encoding?: BufferEncoding | null; withFileTypes: true } +): Promise>; +export async function readdir( + path: string, + options?: { encoding?: BufferEncoding | null; withFileTypes?: boolean } | BufferEncoding | null +): Promise> { + if (CurrentRuntime === Runtime.Browser) { + const { getBrowserFS } = await import("../utils/browser-fs.ts"); + const fs = await getBrowserFS(); + //@ts-ignore ZenFS typing + return await fs.promises.readdir(path, options); + } else { + const { readdir: nodeReaddir } = await import("node:fs/promises"); + //@ts-ignore Cross-runtime typing + return await nodeReaddir(path, options); + } +} + +/** + * Renames a file or directory. + */ +export async function rename(oldPath: string, newPath: string): Promise { + if (CurrentRuntime === Runtime.Browser) { + const { getBrowserFS } = await import("../utils/browser-fs.ts"); + const fs = await getBrowserFS(); + //@ts-ignore ZenFS typing + await fs.promises.rename(oldPath, newPath); + } else { + const { rename: nodeRename } = await import("node:fs/promises"); + await nodeRename(oldPath, newPath); + } +} + +/** + * Copies a file or directory. + */ +export async function cp(source: string, destination: string, options?: { recursive?: boolean }): Promise { + if (CurrentRuntime === Runtime.Browser) { + const { getBrowserFS } = await import("../utils/browser-fs.ts"); + const fs = await getBrowserFS(); + //@ts-ignore ZenFS typing + await fs.promises.cp(source, destination, options); + } else { + const { cp: nodeCp } = await import("node:fs/promises"); + await nodeCp(source, destination, options); + } +} + +/** + * Operations that require platform-specific implementations or are not supported in browsers. + * These throw errors when called in browser environments. + */ + +/** + * Changes file permissions (not supported in browsers). + */ +export async function chmod(path: string, mode: number): Promise { + if (CurrentRuntime === Runtime.Browser) { + throw new Error("chmod is not supported in browser environments"); + } + const { chmod: nodeChmod } = await import("node:fs/promises"); + await nodeChmod(path, mode); +} + +/** + * Changes file ownership (not supported in browsers). + */ +export async function chown(path: string, uid: number, gid: number): Promise { + if (CurrentRuntime === Runtime.Browser) { + throw new Error("chown is not supported in browser environments"); + } + const { chown: nodeChown } = await import("node:fs/promises"); + await nodeChown(path, uid, gid); +} + +/** + * Creates a hard link (not supported in browsers). + */ +export async function link(existingPath: string, newPath: string): Promise { + if (CurrentRuntime === Runtime.Browser) { + throw new Error("link is not supported in browser environments"); + } + const { link: nodeLink } = await import("node:fs/promises"); + await nodeLink(existingPath, newPath); +} + +/** + * Opens a file (not supported in browsers, use readFile/writeFile instead). + */ +export async function open(path: string, flags?: string | number, mode?: number): Promise { + if (CurrentRuntime === Runtime.Browser) { + throw new Error("open is not supported in browser environments - use readFile/writeFile instead"); + } + const { open: nodeOpen } = await import("node:fs/promises"); + return await nodeOpen(path, flags, mode); +} + +/** + * Reads the value of a symbolic link (not supported in browsers). + */ +export async function readlink(path: string, options?: { encoding?: BufferEncoding | null }): Promise { + if (CurrentRuntime === Runtime.Browser) { + throw new Error("readlink is not supported in browser environments"); + } + const { readlink: nodeReadlink } = await import("node:fs/promises"); + //@ts-ignore Cross-runtime typing + return await nodeReadlink(path, options); +} + +/** + * Resolves to the canonical absolute pathname (not supported in browsers). + */ +export async function realpath(path: string, options?: { encoding?: BufferEncoding | null }): Promise { + if (CurrentRuntime === Runtime.Browser) { + throw new Error("realpath is not supported in browser environments"); + } + const { realpath: nodeRealpath } = await import("node:fs/promises"); + //@ts-ignore Cross-runtime typing + return await nodeRealpath(path, options); +} + +/** + * Truncates a file to a specified length (not supported in browsers). + */ +export async function truncate(path: string, len?: number): Promise { + if (CurrentRuntime === Runtime.Browser) { + throw new Error("truncate is not supported in browser environments"); + } + const { truncate: nodeTruncate } = await import("node:fs/promises"); + await nodeTruncate(path, len); +} diff --git a/src/stat/find.ts b/src/stat/find.ts index 2b74471..e1a3b42 100644 --- a/src/stat/find.ts +++ b/src/stat/find.ts @@ -1,4 +1,4 @@ -import { readdir } from "node:fs/promises"; +import { readdir } from "../ops/mod.ts"; import { stat } from "./mod.ts"; import type { StatResult } from "./mod.ts"; import { isAbsolute, join, resolve } from "@std/path"; diff --git a/src/stat/hash.ts b/src/stat/hash.ts index 19b2656..588acbb 100644 --- a/src/stat/hash.ts +++ b/src/stat/hash.ts @@ -1,20 +1,43 @@ -import crypto from "node:crypto"; +import { CurrentRuntime, Runtime } from "@cross/runtime"; import { readFile } from "../io/mod.ts"; /** - * Calculates the MD5 hash of a file. - * - uses node:crypto for widest compatibility + * Calculates the hash of a file. + * - Uses node:crypto in Node.js/Deno/Bun (supports sha256, sha1, md5, etc.) + * - Uses Web Crypto API in browsers (supports sha256, sha1, sha384, sha512) * * @param filePath - The path to the file. - * @param algorithm - The algorithm to use + * @param algorithm - The algorithm to use. In browsers, only sha256, sha1, sha384, sha512 are supported. * @returns The hash as a hexadecimal string. */ export async function hash( filePath: string, algorithm: string = "sha256", ): Promise { - const hash = crypto.createHash(algorithm); const fileData = await readFile(filePath); - hash.update(fileData); - return hash.digest("hex"); -} + + if (CurrentRuntime === Runtime.Browser) { + // Use Web Crypto API in browsers + const algoMap: Record = { + 'sha256': 'SHA-256', + 'sha1': 'SHA-1', + 'sha384': 'SHA-384', + 'sha512': 'SHA-512', + }; + + const webAlgo = algoMap[algorithm.toLowerCase()]; + if (!webAlgo) { + throw new Error(`Hash algorithm '${algorithm}' is not supported in browsers. Supported: sha256, sha1, sha384, sha512`); + } + + const hashBuffer = await crypto.subtle.digest(webAlgo, fileData); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + } else { + // Use node:crypto in Node.js/Deno/Bun + const nodeCrypto = await import("node:crypto"); + const hash = nodeCrypto.createHash(algorithm); + hash.update(fileData); + return hash.digest("hex"); + } +} \ No newline at end of file diff --git a/src/stat/mod.ts b/src/stat/mod.ts index 407a100..96bd372 100644 --- a/src/stat/mod.ts +++ b/src/stat/mod.ts @@ -140,8 +140,21 @@ async function statWrap( throw err; } } - case Runtime.Browser: // Add browser case for clarity - throw new Error("File system access not supported in the browser"); + case Runtime.Browser: + try { + const { getBrowserFS } = await import("../utils/browser-fs.ts"); + const fs = await getBrowserFS(); + //@ts-ignore ZenFS typing + const stats = await fs.promises.stat(path); + return mapNodeStats(stats, options?.bigInt === undefined ? true : false); + } catch (err) { + //@ts-ignore Cross + if (err.code === "ENOENT") { + throw new NotFoundError(`File not found: ${path}`); + } else { + throw err; + } + } default: throw new Error("Unsupported Runtime"); @@ -207,7 +220,47 @@ function mapDenoStats(stats: Deno.FileInfo): StatResult { } export { statWrap as stat }; -export { access, constants, lstat } from "node:fs/promises"; + +/** + * Tests a user's permissions for the file or directory specified by path. + */ +export async function access(path: string, mode?: number): Promise { + if (CurrentRuntime === Runtime.Browser) { + const { getBrowserFS } = await import("../utils/browser-fs.ts"); + const fs = await getBrowserFS(); + //@ts-ignore ZenFS typing + await fs.promises.access(path, mode); + } else { + const { access: nodeAccess } = await import("node:fs/promises"); + await nodeAccess(path, mode); + } +} + +/** + * Gets information about a symbolic link. + */ +export async function lstat( + path: string, + options?: StatOptions, +): Promise { + if (CurrentRuntime === Runtime.Browser) { + const { getBrowserFS } = await import("../utils/browser-fs.ts"); + const fs = await getBrowserFS(); + //@ts-ignore ZenFS typing + const stats = await fs.promises.lstat(path); + return mapNodeStats(stats, options?.bigInt === undefined ? true : false); + } else { + const { lstat: nodeLstat } = await import("node:fs/promises"); + //@ts-ignore Cross + const stats = await nodeLstat(path, options); + return mapNodeStats(stats, options?.bigInt === undefined ? true : false); + } +} + +// Re-export constants from node:fs/promises for non-browser environments +// For browser, these will need to be imported from ZenFS if needed +export { constants } from "node:fs/promises"; + export * from "./is.ts"; export * from "./exists.ts"; export * from "./size.ts"; diff --git a/src/utils/browser-fs.test.ts b/src/utils/browser-fs.test.ts new file mode 100644 index 0000000..fa2653b --- /dev/null +++ b/src/utils/browser-fs.test.ts @@ -0,0 +1,51 @@ +import { assertEquals } from "@std/assert"; +import { test } from "@cross/test"; +import { CurrentRuntime, Runtime } from "@cross/runtime"; + +test("browser-fs module exports are available", async () => { + const browserFS = await import("./browser-fs.ts"); + + assertEquals(typeof browserFS.getBrowserFS, "function"); + assertEquals(typeof browserFS.isBrowserFSInitialized, "function"); + assertEquals(typeof browserFS.configureBrowserFS, "function"); +}); + +test("getBrowserFS throws error in non-browser environment", async () => { + if (CurrentRuntime !== Runtime.Browser) { + const { getBrowserFS } = await import("./browser-fs.ts"); + + try { + await getBrowserFS(); + // Should not reach here + assertEquals(true, false, "Should have thrown an error"); + } catch (error) { + assertEquals( + (error as Error).message, + "Browser filesystem can only be used in browser runtime" + ); + } + } +}); + +test("configureBrowserFS throws error in non-browser environment", async () => { + if (CurrentRuntime !== Runtime.Browser) { + const { configureBrowserFS } = await import("./browser-fs.ts"); + + try { + await configureBrowserFS({ mounts: {} }); + // Should not reach here + assertEquals(true, false, "Should have thrown an error"); + } catch (error) { + assertEquals( + (error as Error).message, + "Browser filesystem can only be configured in browser runtime" + ); + } + } +}); + +test("isBrowserFSInitialized returns false initially", async () => { + const { isBrowserFSInitialized } = await import("./browser-fs.ts"); + // Note: This might be true if other tests have initialized it + assertEquals(typeof isBrowserFSInitialized(), "boolean"); +}); diff --git a/src/utils/browser-fs.ts b/src/utils/browser-fs.ts new file mode 100644 index 0000000..31e2398 --- /dev/null +++ b/src/utils/browser-fs.ts @@ -0,0 +1,110 @@ +/** + * Browser filesystem adapter using ZenFS + * + * This module provides a unified interface for filesystem operations in the browser + * using ZenFS. It initializes ZenFS with an in-memory backend by default. + * + * @module + */ + +import { CurrentRuntime, Runtime } from "@cross/runtime"; + +let zenFS: typeof import("@zenfs/core").fs | null = null; +let zenFSInitialized = false; +let initializationPromise: Promise | null = null; + +/** + * Initializes ZenFS for browser environment with default configuration + * Uses InMemory backend by default for maximum compatibility + */ +async function initializeZenFS() { + // Return existing initialization if in progress or completed + if (initializationPromise) { + return initializationPromise; + } + + if (zenFSInitialized) { + return zenFS!; + } + + if (CurrentRuntime !== Runtime.Browser) { + throw new Error("ZenFS should only be initialized in browser runtime"); + } + + initializationPromise = (async () => { + try { + const { configure, InMemory, fs } = await import("@zenfs/core"); + + // Configure with in-memory filesystem by default + await configure({ + mounts: { + '/': { backend: InMemory }, + } + }); + + zenFS = fs; + zenFSInitialized = true; + return zenFS; + } catch (error) { + initializationPromise = null; // Reset on failure + throw new Error(`Failed to initialize ZenFS: ${error}`); + } + })(); + + return initializationPromise; +} + +/** + * Gets the ZenFS instance, initializing it if necessary + * @returns The ZenFS fs instance + */ +export async function getBrowserFS(): Promise { + if (CurrentRuntime !== Runtime.Browser) { + throw new Error("Browser filesystem can only be used in browser runtime"); + } + + if (!zenFSInitialized) { + await initializeZenFS(); + } + + return zenFS!; +} + +/** + * Checks if ZenFS is available and initialized + * @returns True if ZenFS is initialized + */ +export function isBrowserFSInitialized(): boolean { + return zenFSInitialized; +} + +/** + * Allows manual configuration of ZenFS with custom backends + * This should be called before any filesystem operations + * + * @param config - ZenFS configuration object + */ +export async function configureBrowserFS(config: Parameters[0]) { + if (CurrentRuntime !== Runtime.Browser) { + throw new Error("Browser filesystem can only be configured in browser runtime"); + } + + // If initialization is already in progress or complete, wait for it + if (initializationPromise) { + await initializationPromise; + // Reset to allow reconfiguration + zenFSInitialized = false; + initializationPromise = null; + } + + // Set up new initialization promise + initializationPromise = (async () => { + const { configure, fs } = await import("@zenfs/core"); + await configure(config); + zenFS = fs; + zenFSInitialized = true; + return fs; + })(); + + await initializationPromise; +}