@jeffy-g/universal-fs
v0.5.1
Published
Universal file system utils for Node.js and Browser in TypeScript.
Maintainers
Readme
@jeffy-g/universal-fs
Universal file system utilities for Node.js and Browser environments
A lightweight, TypeScript-first library that provides consistent file I/O operations across different JavaScript environments. Write once, run everywhere – whether you're building for Node.js servers or browser applications.
✨ Features
- 🌍 Universal: Works seamlessly in Node.js and browser environments
- 📝 Type-safe: Full TypeScript support with comprehensive type definitions and advanced type inference
- 🔄 Format-aware: Converts input based on the explicitly specified format (
text,json,binary,blob,arrayBuffer). No automatic detection is performed. - 📁 Smart: Auto-creates directories in Node.js, triggers downloads in browsers
- 🎯 Consistent: Same API across all supported environments
- 🚀 Modern: Built with ESM-first approach using native APIs
- 🔗 Flexible Input: Supports URLs, File objects, and Blob objects in browsers
- ⚡ Lazy Loading: Optimized bundle size with environment-specific lazy loading
- 📦 Runtime Module Loading: Resolves installed ESM packages in Node.js and jsDelivr ESM packages in browsers and workers
🚀 Quick Start
npm install @jeffy-g/universal-fsufs is the main entry point implementing the universal-fs API (IUniversalFs).
All file operations (readFile, writeFile, etc.) are accessible via this object.
import { ufs } from "@jeffy-g/universal-fs";
// Write text file
await ufs.writeText("hello.txt", "Hello, World!");
// Read JSON file with type safety
const config = await ufs.readJSON<{ name: string; version: string }>("config.json");
console.log(config.name); // Type-safe access
// Read with detailed metadata
const result = await ufs.readText("hello.txt", { useDetails: true });
console.log(result.filename); // "hello.txt"
console.log(result.size); // File size in bytes
console.log(result.strategy); // "node" or "browser"
console.log(result.data); // File content
// Write binary data
const buffer = new Uint8Array([72, 101, 108, 108, 111]);
// Both Uint8Array and ArrayBuffer are supported
await ufs.writeBuffer("data.bin", buffer);
await ufs.writeBuffer("data2.bin", buffer.buffer);Browser-specific features
// Read from File input
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
const content = await ufs.readText(file);
// Read from Blob
const blob = new Blob(["Hello World"], { type: "text/plain" });
const text = await ufs.readText(blob);
// Read from URL
const data = await ufs.readJSON("https://api.example.com/config.json");Using in CommonJS
(async () => {
const { ufs } = await import('@jeffy-g/universal-fs');
const result = await ufs.readText('hello.txt');
console.log(result);
})();🌐 CDN Usage
You can load @jeffy-g/universal-fs directly via CDN:
Note: When loading via CDN (
<script type="module">), TypeScript type inference is not available.
For full type support, install via npm or bun and use in a TypeScript project.
✅ ESM via jsDelivr
<script type="module">
const mod = await import("https://cdn.jsdelivr.net/npm/@jeffy-g/universal-fs@latest/dist/index.js");
const { ufs } = mod;
const result = await ufs.readFile("https://example.com/data.json", { format: "json" });
console.log(result);
</script>✅ Optimized ESM (Recommended)
<script type="module">
import { ufs } from "https://cdn.jsdelivr.net/npm/@jeffy-g/universal-fs@latest/+esm";
const result = await ufs.readFile("https://example.com/data.json", { format: "json" });
console.log(result);
</script>✅ With SRI (Subresource Integrity)
<script type="module" integrity="sha384-xxxxxxxx" crossorigin="anonymous">
import { ufs } from "https://cdn.jsdelivr.net/npm/@jeffy-g/universal-fs@latest/+esm";
</script>To get the sha384 hash:
curl -sL "https://cdn.jsdelivr.net/npm/@jeffy-g/[email protected]/+esm" | openssl dgst -sha384 -binary | openssl base64 -AImportant: Always use a fixed version when using SRI (e.g.,
@0.5.0instead oflatest).
or use THIS -> https://www.srihash.org/
🦕 Deno
You can import via npm: specifier or CDN:
// Using npm:
import { ufs } from "npm:@jeffy-g/[email protected]";
// Using CDN (jsDelivr):
import { ufs } from "https://cdn.jsdelivr.net/npm/@jeffy-g/[email protected]/+esm";
const result = await ufs.readFile("https://example.com/file.json", { format: "json" });🍞 Bun
Install and use:
bun add @jeffy-g/universal-fsimport { ufs } from "@jeffy-g/universal-fs";
const result = await ufs.readFile("https://example.com/file.json", { format: "json" });🌐 Platform Support
| Environment | Status | Read Support | Write Support | Notes | |-------------|--------|--------------|---------------|-------| | Node.js | ✅ Full | Local files | File system | Complete filesystem access | | Browser | ✅ Full | URLs, File, Blob | Download trigger | Secure, sandboxed environment | | Bun | ⚠️ Limited | Basic | Basic | Uses Node.js compatibility layer | | Deno | ⚠️ Limited | Basic | Basic | Experimental support |
Environment-specific Notes:
- Node.js: Full filesystem access with automatic directory creation
- Browser:
read*supports URLs (via fetch), File objects, and Blob objects;write*triggers secure file downloads - Input Types:
- Node.js:
string(file paths) - Browser:
string(URLs),File,Blob
- Node.js:
📚 API Reference
Core Methods
readFile<T>(filename, options?)
Universal file reader with explicit format selection and advanced type inference.
// Read as text (default, inferred from no format specified)
const textResult = await ufs.readFile("document.txt");
// Type: string
// Read as JSON with type safety
const jsonResult = await ufs.readFile("config.json", { format: "json" });
// Type: Record<string, unknown> | unknown[] | object
// Explicit type parameter for JSON
const typedResult = await ufs.readFile<{name: string}>("config.json", { format: "json" });
// Type: {name: string}
// Read binary data
const binaryResult = await ufs.readFile("image.png", { format: "arrayBuffer" });
// Type: ArrayBuffer
// Read with detailed metadata
const detailedResult = await ufs.readFile("data.txt", { useDetails: true });
// Type: TUFSResult<string> with filename, size, strategy, etc.
// Browser: Read from File object
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const file = fileInput.files![0];
const content = await ufs.readFile(file, { format: "text" });writeFile(filename, data, options?)
Universal file writer with smart environment handling and download timeout protection.
// In Node.js: writes to filesystem with automatic directory creation
// In Browser: triggers secure download with 30-second timeout
await ufs.writeFile("output.txt", "Hello World");
// Write with detailed result
const result = await ufs.writeFile("output.txt", "Hello World", { useDetails: true });
console.log(result.size); // File size
console.log(result.strategy); // "node" or "browser"
console.log(result.timestamp); // Operation timestampConvenience Methods
| Method | Input Type | Returns | Description |
|--------|------------|---------|-------------|
| readText() | TUFSInputType | string | Read file as UTF-8 text |
| readJSON<T>() | string | T | Parse JSON with type safety |
| readBlob() | TUFSInputType | Blob | Read as Blob object |
| readBuffer() | TUFSInputType | ArrayBuffer | Read as raw binary data |
| writeText() | string, string | void | Write UTF-8 text |
| writeJSON() | string, any | void | Serialize and write JSON |
| writeBlob() | string, Blob | void | Write Blob data |
| writeBuffer() | string, ArrayBuffer \| Uint8Array | void | Write binary data |
Note:
- All read methods support the
useDetailsoption to return metadataTUFSInputType=string | File | Blob(environment-dependent)- The
readJSON<T>()method supports object, array, and custom types- All write methods support both simple and detailed return modes
Environment Helpers
selectFromEnv(key, cb)
Environment-aware helper for reading simple flags across Node.js and browser runtimes.
- Node.js: reads from
process.env[key] - Browser: reads from
globalThis[key](e.g.window.FEATURE_FLAG) - Other environments: passes
undefinedto the callback
import { selectFromEnv } from "@jeffy-g/universal-fs";
// ✅ Toggle a feature flag: "1" -> enabled
const featureEnabled = selectFromEnv("FEATURE_EXPORT", (flag?: "1" | "0") => {
return flag === "1";
});
// ✅ Use Case 2: Explicitly specifying the type
const featureEnabled2 = selectFromEnv<"1" | "0", boolean>(
"FEATURE_EXPORT",
(flag) => {
return flag === "1";
}
);
// ✅ Use Case 3: More complex return types
const config = selectFromEnv("APP_ENV", (env?: "dev" | "prod" | "test") => {
return {
isDev: env === "dev",
isProd: env === "prod",
logLevel: env === "prod" ? "error" : "debug"
};
}); // { isDev: boolean; isProd: boolean; logLevel: string }
// ✅ Use Case 4: Filtering Types
const port = selectFromEnv<`${number}`, number>("PORT", (p) => {
return p ? parseInt(p, 10) : 3000;
});This keeps environment branching logic localized and type-safe while avoiding direct checks against ufs.env in application code.
loadModule<Module>(modId, version?)
Experimental in v0.5.0
Dynamically imports an ESM module using a specifier appropriate for the current runtime.
- Node.js: passes
modIddirectly to nativeimport(). The optionalversionis ignored. - Non-Node runtimes, including browsers and workers: convert the arguments to
https://cdn.jsdelivr.net/npm/<modId>[@version]/+esm. - Caching: delegated to the runtime's native ESM module cache. Repeated imports of the same resolved specifier return the same module namespace.
- Bundlers: the generated dynamic import is left for runtime resolution in Vite and webpack builds.
import { loadModule } from "@jeffy-g/universal-fs";
// Node.js imports the installed package.
// Browsers and workers import [email protected] from jsDelivr.
const fflate = await loadModule<typeof import("fflate")>("fflate", "0.8.3");
const source = new TextEncoder().encode("hello");
const compressed = fflate.gzipSync(source);The generic type parameter only describes the expected module namespace to TypeScript; it does not validate the loaded module at runtime.
For a package subpath that must work in both Node.js and non-Node runtimes, use an unversioned subpath and omit the second argument. Node.js resolves the installed package version, while jsDelivr resolves its default version:
const mod = await loadModule("package-name/subpath");For a pinned, non-Node-only subpath, include the version in modId, for example [email protected]/subpath. A versioned npm specifier is not valid as a Node.js bare import, and the separate version argument is intended for package roots rather than subpaths.
In browsers and workers, the jsDelivr request must be allowed by the page's network policy and Content Security Policy. loadModule is a small environment switch, not a package resolver or fallback loader; loading failures reject the returned promise unchanged.
Options & Types
/**
* Input types supported by universal-fs
*/
export type TUFSInputType = string | File | Blob;
/**
* Format-keyed mapping to the corresponding TypeScript type for file data.
*/
export interface IUFSFormatMap {
text: string;
json: Record<string, unknown> | unknown[] | object;
arrayBuffer: ArrayBuffer;
blob: Blob;
binary: Uint8Array; // Node.js Buffer is sub-class of Uint8Array
}
/**
* Supported format types
*/
export type TUFSFormat = keyof IUFSFormatMap;
// "text" | "json" | "arrayBuffer" | "blob" | "binary"
/**
* Supported data types for universal file operations.
*/
export type TUFSData = IUFSFormatMap[keyof IUFSFormatMap];
/**
* Options used for both reading and writing files universally.
*/
export type TUFSOptions = {
/**
* Character encoding used for reading and writing operations.
* Default: "utf8"
*/
encoding?: BufferEncoding;
/** Format used when reading (ignored on write). */
format?: TUFSFormat;
/** Return detailed metadata including size, strategy, timestamp */
useDetails?: true;
};
/**
* Generic result type for universal file operations.
* @template T - Optional data returned when reading.
*/
export type TUFSResult<T extends TUFSData | undefined = undefined> = {
filename: string;
size: number;
strategy: "node" | "browser";
timestamp: number;
path?: string; // Node.js only
url?: string; // Browser only
mimeType?: TMimeType; // Inferred MIME type
} & ([T] extends [undefined] ? {} : { data: T });
/**
* MIME type representation
*/
export type TMimeType = `${string}/${string}`;🏗️ Advanced Examples
Configuration Management
interface AppConfig {
apiUrl: string;
features: string[];
debug: boolean;
}
// Read configuration with type safety
const config = await ufs.readJSON<AppConfig>("app-config.json");
// Update and save
config.debug = false;
await ufs.writeJSON("app-config.json", config);
// Read with metadata
const configWithMeta = await ufs.readJSON<AppConfig>("app-config.json", { useDetails: true });
console.log(`Config loaded from ${configWithMeta.strategy} environment`);
console.log(`File size: ${configWithMeta.size} bytes`);Binary Data Processing
// Read image file
const imageBuffer = await ufs.readBuffer("photo.jpg");
// Process the binary data
const processedData = processImage(imageBuffer);
// Save processed result with metadata
const result = await ufs.writeBuffer("processed-photo.jpg", processedData, { useDetails: true });
console.log(`Processed image saved: ${result.size} bytes`);Cross-Platform File Utilities
class FileManager {
static async backup<T>(filename: string): Promise<void> {
const original = await ufs.readFile(filename);
const backupName = `${filename}.backup.${Date.now()}`;
await ufs.writeFile(backupName, original);
}
static async migrate(oldPath: string, newPath: string): Promise<void> {
const content = await ufs.readFile(oldPath);
await ufs.writeFile(newPath, content);
// Note: Deletion not supported in browser environment
}
static async getFileInfo(filename: string) {
const result = await ufs.readFile(filename, { useDetails: true });
return {
name: result.filename,
size: result.size,
mimeType: result.mimeType,
environment: result.strategy
};
}
}Browser File Handling
// Handle file input changes
document.getElementById('fileInput')?.addEventListener('change', async (e) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
try {
// Read file content
const content = await ufs.readText(file);
console.log('File content:', content);
// Get file info
const info = await ufs.readFile(file, { useDetails: true });
console.log(`File: ${info.filename}, Size: ${info.size} bytes`);
} catch (error) {
console.error('Failed to read file:', error);
}
}
});
// Process and download modified content
async function processAndDownload(originalFile: File) {
const content = await ufs.readText(originalFile);
const processed = content.toUpperCase(); // Example processing
// This will trigger a download in the browser
await ufs.writeText(`processed_${originalFile.name}`, processed);
}🔧 Environment Behavior (Node.js 15.7.0+ required for Blob support)
Node.js Environment
- Reading: Direct filesystem access using
fs.promises - Writing: Creates directories automatically, writes to disk
- Binary Support: Full support including
Bufferandbinaryformat - Blob Support: Requires Node.js v15.7.0 or higher for the
BlobAPI.
On older versions,ufs.readBlob()andufs.writeBlob()will return aBufferinstead of aBlob.
Browser Environment
- Reading:
- HTTP(S) URLs via
fetch()API Fileobjects from input elements or drag & dropBlobobjects created programmatically
- HTTP(S) URLs via
- Writing: Triggers secure file downloads via Blob URLs with:
- 30-second timeout protection
- Automatic cleanup of object URLs
- Filename sanitization for security
- Limitations: No direct filesystem access (security restrictions)
Error Handling
import { UniversalFsError } from "@jeffy-g/universal-fs";
try {
const result = await ufs.readFile("nonexistent.txt");
} catch (error) {
if (error instanceof UniversalFsError) {
console.log(`Operation: ${error.operation}`); // "read" | "write"
console.log(`Strategy: ${error.strategy}`); // "node" | "browser"
console.log(`Filename: ${error.filename}`); // File that caused error
console.log(`Original cause:`, error.cause); // Original error object
}
}🔍 MIME Type Handling
universal-fs automatically detects the MIME type from the file extension when performing read/write operations. The library includes comprehensive MIME type mappings:
Text Formats
.txt→text/plain.json→application/json.html→text/html.css→text/css.js→application/javascript.ts→application/typescript
Image Formats
.png→image/png.jpg,.jpeg→image/jpeg.gif→image/gif.webp→image/webp.svg→image/svg+xml
Audio/Video Formats
.mp3→audio/mpeg.wav→audio/wav.mp4→video/mp4.mid,.midi→audio/midi
Archive Formats
.zip→application/zip.tar→application/x-tar.gz→application/gzip.als→application/gzip(Ableton format)
If the extension is unknown, it defaults to:
application/octet-stream⚠️ Current Limitations
- Browser Security: Only supports URLs accessible via CORS and File/Blob objects
- Bun/Deno: Limited testing, may have compatibility issues
- Blob Support: Node.js requires v15.7.0+ for full Blob support
- Download Timeout: Browser downloads have a 30-second timeout limit
- Runtime Module Loading: Browser and worker imports require network access to jsDelivr and a compatible Content Security Policy
🛣️ Roadmap
- [x] File object support in browsers (drag & drop, input files) ✅ v0.0.10
- [x] Enhanced type inference system ✅ v0.0.10
- [x] Detailed metadata support with
useDetailsoption ✅ v0.0.10 - [x] Environment-aware runtime ESM loading ✅ v0.5.0
- [x] Real-browser tests with Vitest Browser Mode and Playwright ✅ v0.5.0
- [ ] Stream-based operations for large files
- [ ] Enhanced Bun and Deno compatibility
- [ ] Directory operations (list, create, remove)
- [ ] Compression/decompression utilities
- [ ] Progress callbacks for large operations
- [ ] Custom MIME type override options
🧪 Development & Testing
Install dependencies before running tests:
yarn install --frozen-lockfile
yarn test --run
node smoke.test.mjsRun the browser suites in a real headless Chromium instance:
yarn test:browser:install
yarn test:browserOn Linux CI hosts, install Chromium together with its system dependencies:
yarn playwright install --with-deps chromiumThe CI workflow validates Node.js 20, 22, and 24, runs Node.js 22 on Linux, macOS, and Windows, and treats the Chromium suite as a required check.
🤝 Contributing
Contributions are welcome. Please open an issue before proposing a substantial behavioral or API change.
📄 License
MIT © jeffy-g
Need help? Check the API reference above or open an issue.
