UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

1,441 lines (1,440 loc) 35.3 kB
import { $inject, $module, AlephaError, Json } from "alepha"; import { basename, join } from "node:path"; import { Readable } from "node:stream"; //#region ../../src/system/providers/FileSystemProvider.ts /** * FileSystem interface providing utilities for working with files. */ var FileSystemProvider = class {}; //#endregion //#region ../../src/system/providers/MemoryFileSystemProvider.ts /** * In-memory implementation of FileSystemProvider for testing. * * This provider stores all files and directories in memory, making it ideal for * unit tests that need to verify file operations without touching the real file system. * * @example * ```typescript * // In tests, substitute the real FileSystemProvider with MemoryFileSystemProvider * const alepha = Alepha.create().with({ * provide: FileSystemProvider, * use: MemoryFileSystemProvider, * }); * * // Run code that uses FileSystemProvider * const service = alepha.inject(MyService); * await service.saveFile("test.txt", "Hello World"); * * // Verify the file was written * const memoryFs = alepha.inject(MemoryFileSystemProvider); * expect(memoryFs.files.get("test.txt")?.toString()).toBe("Hello World"); * ``` */ var MemoryFileSystemProvider = class { json = $inject(Json); /** * In-memory storage for files (path -> content) */ files = /* @__PURE__ */ new Map(); /** * In-memory storage for directories */ directories = /* @__PURE__ */ new Set(); /** * Track mkdir calls for test assertions */ mkdirCalls = []; /** * Track writeFile calls for test assertions */ writeFileCalls = []; /** * Track readFile calls for test assertions */ readFileCalls = []; /** * Track rm calls for test assertions */ rmCalls = []; /** * Track join calls for test assertions */ joinCalls = []; /** * Error to throw on mkdir (for testing error handling) */ mkdirError = null; /** * Error to throw on writeFile (for testing error handling) */ writeFileError = null; /** * Error to throw on readFile (for testing error handling) */ readFileError = null; constructor(options = {}) { this.mkdirError = options.mkdirError ?? null; this.writeFileError = options.writeFileError ?? null; this.readFileError = options.readFileError ?? null; } /** * Join path segments using forward slashes. * Uses Node's path.join for proper normalization (handles .. and .) */ join(...paths) { this.joinCalls.push(paths); return join(...paths); } /** * Normalize path separators to forward slashes for consistent internal storage. * This ensures Windows paths work correctly in the in-memory file system. */ normalizePath(path) { return path.replace(/\\/g, "/"); } /** * Create a FileLike object from various sources. */ createFile(options) { if ("path" in options) { const filePath = options.path; const buffer = this.files.get(filePath); if (buffer === void 0) throw new AlephaError(`ENOENT: no such file or directory, open '${filePath}'`); return { name: options.name ?? basename(filePath), type: options.type ?? "application/octet-stream", size: buffer.byteLength, lastModified: Date.now(), stream: () => { throw new AlephaError("Stream not implemented in MemoryFileSystemProvider"); }, arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), text: async () => buffer.toString("utf-8") }; } if ("buffer" in options) { const buffer = options.buffer; return { name: options.name ?? "file", type: options.type ?? "application/octet-stream", size: buffer.byteLength, lastModified: Date.now(), stream: () => { throw new AlephaError("Stream not implemented in MemoryFileSystemProvider"); }, arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), text: async () => buffer.toString("utf-8") }; } if ("text" in options) { const buffer = Buffer.from(options.text, "utf-8"); return { name: options.name ?? "file.txt", type: options.type ?? "text/plain", size: buffer.byteLength, lastModified: Date.now(), stream: () => { throw new AlephaError("Stream not implemented in MemoryFileSystemProvider"); }, arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), text: async () => options.text }; } throw new AlephaError("MemoryFileSystemProvider.createFile: unsupported options. Only buffer and text are supported."); } /** * Remove a file or directory from memory. */ async rm(path, options) { this.rmCalls.push({ path, options }); if (!(this.files.has(path) || this.directories.has(path)) && !options?.force) throw new AlephaError(`ENOENT: no such file or directory, rm '${path}'`); if (this.directories.has(path)) if (options?.recursive) { this.directories.delete(path); for (const filePath of this.files.keys()) if (filePath.startsWith(`${path}/`)) this.files.delete(filePath); for (const dirPath of this.directories) if (dirPath.startsWith(`${path}/`)) this.directories.delete(dirPath); } else throw new AlephaError(`EISDIR: illegal operation on a directory, rm '${path}'`); else this.files.delete(path); } /** * Copy a file or directory in memory. */ async cp(src, dest, options) { if (this.directories.has(src)) { this.directories.add(dest); for (const [filePath, content] of this.files) if (filePath.startsWith(`${src}/`)) { const newPath = filePath.replace(src, dest); this.files.set(newPath, Buffer.from(content)); } } else if (this.files.has(src)) { const content = this.files.get(src); this.files.set(dest, Buffer.from(content)); } else throw new AlephaError(`ENOENT: no such file or directory, cp '${src}'`); } /** * Move/rename a file or directory in memory. */ async mv(src, dest) { if (this.directories.has(src)) { this.directories.delete(src); this.directories.add(dest); for (const [filePath, content] of this.files) if (filePath.startsWith(`${src}/`)) { const newPath = filePath.replace(src, dest); this.files.delete(filePath); this.files.set(newPath, content); } } else if (this.files.has(src)) { const content = this.files.get(src); this.files.delete(src); this.files.set(dest, content); } else throw new AlephaError(`ENOENT: no such file or directory, mv '${src}'`); } /** * Create a directory in memory. */ async mkdir(path, options) { this.mkdirCalls.push({ path, options }); if (this.mkdirError) throw this.mkdirError; const normalizedPath = this.normalizePath(path); if (this.directories.has(normalizedPath) && !options?.recursive) throw new AlephaError(`EEXIST: file already exists, mkdir '${path}'`); this.directories.add(normalizedPath); if (options?.recursive) { const parts = normalizedPath.split("/").filter(Boolean); let current = ""; for (const part of parts) { current = current ? `${current}/${part}` : part; this.directories.add(current); } } } /** * List files in a directory. */ async ls(path, options) { const normalizedPath = this.normalizePath(path).replace(/\/$/, ""); const entries = /* @__PURE__ */ new Set(); for (const filePath of this.files.keys()) { const normalizedFilePath = this.normalizePath(filePath); if (normalizedFilePath.startsWith(`${normalizedPath}/`)) { const relativePath = normalizedFilePath.slice(normalizedPath.length + 1); const parts = relativePath.split("/"); if (options?.recursive) entries.add(relativePath); else entries.add(parts[0]); } } for (const dirPath of this.directories) { const normalizedDirPath = this.normalizePath(dirPath); if (normalizedDirPath.startsWith(`${normalizedPath}/`) && normalizedDirPath !== normalizedPath) { const relativePath = normalizedDirPath.slice(normalizedPath.length + 1); const parts = relativePath.split("/"); if (options?.recursive) entries.add(relativePath); else if (parts.length === 1) entries.add(parts[0]); } } let result = Array.from(entries); if (!options?.hidden) result = result.filter((entry) => !entry.startsWith(".")); return result.sort(); } /** * Check if a file or directory exists in memory. */ async exists(path) { return this.files.has(path) || this.directories.has(path); } /** * Read a file from memory. */ async readFile(path) { this.readFileCalls.push(path); if (this.readFileError) throw this.readFileError; const content = this.files.get(path); if (!content) throw new AlephaError(`ENOENT: no such file or directory, open '${path}'`); return content; } /** * Read a file from memory as text. */ async readTextFile(path) { return (await this.readFile(path)).toString("utf-8"); } /** * Read a file from memory as JSON. */ async readJsonFile(path) { const text = await this.readTextFile(path); return this.json.parse(text); } /** * Write a file to memory. */ async writeFile(path, data) { const dataStr = typeof data === "string" ? data : data instanceof Buffer || data instanceof Uint8Array ? data.toString("utf-8") : await data.text(); this.writeFileCalls.push({ path, data: dataStr }); if (this.writeFileError) throw this.writeFileError; const buffer = typeof data === "string" ? Buffer.from(data, "utf-8") : data instanceof Buffer ? data : data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(await data.text(), "utf-8"); this.files.set(path, buffer); } /** * Reset all in-memory state (useful between tests). */ reset() { this.files.clear(); this.directories.clear(); this.mkdirCalls = []; this.writeFileCalls = []; this.readFileCalls = []; this.rmCalls = []; this.joinCalls = []; this.mkdirError = null; this.writeFileError = null; this.readFileError = null; } /** * Check if a file was written during the test. * * @example * ```typescript * expect(fs.wasWritten("/project/tsconfig.json")).toBe(true); * ``` */ wasWritten(path) { return this.writeFileCalls.some((call) => call.path === path); } /** * Check if a file was written with content matching a pattern. * * @example * ```typescript * expect(fs.wasWrittenMatching("/project/tsconfig.json", /extends/)).toBe(true); * ``` */ wasWrittenMatching(path, pattern) { const call = this.writeFileCalls.find((c) => c.path === path); return call ? pattern.test(call.data) : false; } /** * Check if a file was read during the test. * * @example * ```typescript * expect(fs.wasRead("/project/package.json")).toBe(true); * ``` */ wasRead(path) { return this.readFileCalls.includes(path); } /** * Check if a file was deleted during the test. * * @example * ```typescript * expect(fs.wasDeleted("/project/old-file.txt")).toBe(true); * ``` */ wasDeleted(path) { return this.rmCalls.some((call) => call.path === path); } /** * Get the content of a file as a string (convenience method for testing). */ getFileContent(path) { return this.files.get(path)?.toString("utf-8"); } }; //#endregion //#region ../../src/system/providers/MemoryShellProvider.ts /** * In-memory implementation of ShellProvider for testing. * * Records all commands that would be executed without actually running them. * Can be configured to return specific outputs or throw errors for testing. * * @example * ```typescript * // In tests, substitute the real ShellProvider with MemoryShellProvider * const alepha = Alepha.create().with({ * provide: ShellProvider, * use: MemoryShellProvider, * }); * * // Configure mock behavior * const shell = alepha.inject(MemoryShellProvider); * shell.configure({ * outputs: { "echo hello": "hello\n" }, * errors: { "failing-cmd": "Command failed" }, * }); * * // Or use the fluent API * shell.outputs.set("another-cmd", "output"); * shell.errors.set("another-error", "Error message"); * * // Run code that uses ShellProvider * const service = alepha.inject(MyService); * await service.doSomething(); * * // Verify commands were called * expect(shell.calls).toHaveLength(2); * expect(shell.calls[0].command).toBe("yarn install"); * ``` */ var MemoryShellProvider = class { /** * All recorded shell calls. */ calls = []; /** * Simulated outputs for specific commands. */ outputs = /* @__PURE__ */ new Map(); /** * Commands that should throw an error. */ errors = /* @__PURE__ */ new Map(); /** * Commands considered installed in the system PATH. */ installedCommands = /* @__PURE__ */ new Set(); /** * Configure the mock with predefined outputs, errors, and installed commands. */ configure(options) { if (options.outputs) for (const [cmd, output] of Object.entries(options.outputs)) this.outputs.set(cmd, output); if (options.errors) for (const [cmd, error] of Object.entries(options.errors)) this.errors.set(cmd, error); if (options.installedCommands) for (const cmd of options.installedCommands) this.installedCommands.add(cmd); return this; } /** * Record command and return simulated output. */ async run(command, options = {}) { this.calls.push({ command, options }); const errorMsg = this.errors.get(command); if (errorMsg) throw new AlephaError(errorMsg); return this.outputs.get(command) ?? ""; } /** * Check if a specific command was called. */ wasCalled(command) { return this.calls.some((call) => call.command === command); } /** * Check if a command matching a pattern was called. */ wasCalledMatching(pattern) { return this.calls.some((call) => pattern.test(call.command)); } /** * Get all calls matching a pattern. */ getCallsMatching(pattern) { return this.calls.filter((call) => pattern.test(call.command)); } /** * Check if a command is installed. */ async isInstalled(command) { return this.installedCommands.has(command); } /** * Reset all recorded state. */ reset() { this.calls = []; this.outputs.clear(); this.errors.clear(); this.installedCommands.clear(); } }; //#endregion //#region ../../src/system/providers/ShellProvider.ts /** * Abstract provider for executing shell commands and binaries. * * Implementations: * - `NodeShellProvider` - Real shell execution using Node.js child_process * - `MemoryShellProvider` - In-memory mock for testing * * @example * ```typescript * class MyService { * protected readonly shell = $inject(ShellProvider); * * async build() { * // Run shell command directly * await this.shell.run("yarn install"); * * // Run local binary with resolution * await this.shell.run("vite build", { resolve: true }); * * // Capture output * const output = await this.shell.run("echo hello", { capture: true }); * } * } * ``` */ var ShellProvider = class {}; //#endregion //#region ../../src/system/services/FileDetector.ts /** * Service for detecting file types and getting content types. * * @example * ```typescript * const detector = alepha.inject(FileDetector); * * // Get content type from filename * const mimeType = detector.getContentType("image.png"); // "image/png" * * // Detect file type by magic bytes * const stream = createReadStream('image.png'); * const result = await detector.detectFileType(stream, 'image.png'); * console.log(result.mimeType); // 'image/png' * console.log(result.verified); // true if magic bytes match * ``` */ var FileDetector = class FileDetector { /** * Magic byte signatures for common file formats. * Each signature is represented as an array of bytes or null (wildcard). */ static MAGIC_BYTES = { png: [{ signature: [ 137, 80, 78, 71, 13, 10, 26, 10 ], mimeType: "image/png" }], jpg: [ { signature: [ 255, 216, 255, 224 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 225 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 226 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 227 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 232 ], mimeType: "image/jpeg" } ], jpeg: [ { signature: [ 255, 216, 255, 224 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 225 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 226 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 227 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 232 ], mimeType: "image/jpeg" } ], gif: [{ signature: [ 71, 73, 70, 56, 55, 97 ], mimeType: "image/gif" }, { signature: [ 71, 73, 70, 56, 57, 97 ], mimeType: "image/gif" }], webp: [{ signature: [ 82, 73, 70, 70, null, null, null, null, 87, 69, 66, 80 ], mimeType: "image/webp" }], bmp: [{ signature: [66, 77], mimeType: "image/bmp" }], ico: [{ signature: [ 0, 0, 1, 0 ], mimeType: "image/x-icon" }], tiff: [{ signature: [ 73, 73, 42, 0 ], mimeType: "image/tiff" }, { signature: [ 77, 77, 0, 42 ], mimeType: "image/tiff" }], tif: [{ signature: [ 73, 73, 42, 0 ], mimeType: "image/tiff" }, { signature: [ 77, 77, 0, 42 ], mimeType: "image/tiff" }], pdf: [{ signature: [ 37, 80, 68, 70, 45 ], mimeType: "application/pdf" }], zip: [ { signature: [ 80, 75, 3, 4 ], mimeType: "application/zip" }, { signature: [ 80, 75, 5, 6 ], mimeType: "application/zip" }, { signature: [ 80, 75, 7, 8 ], mimeType: "application/zip" } ], rar: [{ signature: [ 82, 97, 114, 33, 26, 7 ], mimeType: "application/vnd.rar" }], "7z": [{ signature: [ 55, 122, 188, 175, 39, 28 ], mimeType: "application/x-7z-compressed" }], tar: [{ signature: [ 117, 115, 116, 97, 114 ], mimeType: "application/x-tar" }], gz: [{ signature: [31, 139], mimeType: "application/gzip" }], tgz: [{ signature: [31, 139], mimeType: "application/gzip" }], mp3: [ { signature: [255, 251], mimeType: "audio/mpeg" }, { signature: [255, 243], mimeType: "audio/mpeg" }, { signature: [255, 242], mimeType: "audio/mpeg" }, { signature: [ 73, 68, 51 ], mimeType: "audio/mpeg" } ], wav: [{ signature: [ 82, 73, 70, 70, null, null, null, null, 87, 65, 86, 69 ], mimeType: "audio/wav" }], ogg: [{ signature: [ 79, 103, 103, 83 ], mimeType: "audio/ogg" }], flac: [{ signature: [ 102, 76, 97, 67 ], mimeType: "audio/flac" }], mp4: [ { signature: [ null, null, null, null, 102, 116, 121, 112 ], mimeType: "video/mp4" }, { signature: [ null, null, null, null, 102, 116, 121, 112, 105, 115, 111, 109 ], mimeType: "video/mp4" }, { signature: [ null, null, null, null, 102, 116, 121, 112, 109, 112, 52, 50 ], mimeType: "video/mp4" } ], webm: [{ signature: [ 26, 69, 223, 163 ], mimeType: "video/webm" }], avi: [{ signature: [ 82, 73, 70, 70, null, null, null, null, 65, 86, 73, 32 ], mimeType: "video/x-msvideo" }], mov: [{ signature: [ null, null, null, null, 102, 116, 121, 112, 113, 116, 32, 32 ], mimeType: "video/quicktime" }], mkv: [{ signature: [ 26, 69, 223, 163 ], mimeType: "video/x-matroska" }], docx: [{ signature: [ 80, 75, 3, 4 ], mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }], xlsx: [{ signature: [ 80, 75, 3, 4 ], mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }], pptx: [{ signature: [ 80, 75, 3, 4 ], mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation" }], doc: [{ signature: [ 208, 207, 17, 224, 161, 177, 26, 225 ], mimeType: "application/msword" }], xls: [{ signature: [ 208, 207, 17, 224, 161, 177, 26, 225 ], mimeType: "application/vnd.ms-excel" }], ppt: [{ signature: [ 208, 207, 17, 224, 161, 177, 26, 225 ], mimeType: "application/vnd.ms-powerpoint" }] }; /** * All possible format signatures for checking against actual file content */ static ALL_SIGNATURES = Object.entries(FileDetector.MAGIC_BYTES).flatMap(([ext, signatures]) => signatures.map((sig) => ({ ext, ...sig }))); /** * MIME type map for file extensions. * * Can be used to get the content type of file based on its extension. * Feel free to add more mime types in your project! */ static mimeMap = { json: "application/json", txt: "text/plain", html: "text/html", htm: "text/html", xml: "application/xml", csv: "text/csv", pdf: "application/pdf", md: "text/markdown", markdown: "text/markdown", rtf: "application/rtf", css: "text/css", js: "application/javascript", mjs: "application/javascript", ts: "application/typescript", jsx: "text/jsx", tsx: "text/tsx", zip: "application/zip", rar: "application/vnd.rar", "7z": "application/x-7z-compressed", tar: "application/x-tar", gz: "application/gzip", tgz: "application/gzip", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml", bmp: "image/bmp", ico: "image/x-icon", tiff: "image/tiff", tif: "image/tiff", mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", m4a: "audio/mp4", aac: "audio/aac", flac: "audio/flac", mp4: "video/mp4", webm: "video/webm", avi: "video/x-msvideo", mov: "video/quicktime", wmv: "video/x-ms-wmv", flv: "video/x-flv", mkv: "video/x-matroska", doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf", eot: "application/vnd.ms-fontobject" }; /** * Reverse MIME type map for looking up extensions from MIME types. * Prefers shorter, more common extensions when multiple exist. */ static reverseMimeMap = (() => { const reverse = {}; for (const [ext, mimeType] of Object.entries(FileDetector.mimeMap)) if (!reverse[mimeType]) reverse[mimeType] = ext; return reverse; })(); /** * Returns the file extension for a given MIME type. * * @param mimeType - The MIME type to look up * @returns The file extension (without dot), or "bin" if not found * * @example * ```typescript * const detector = alepha.inject(FileDetector); * const ext = detector.getExtensionFromMimeType("image/png"); // "png" * const ext2 = detector.getExtensionFromMimeType("application/octet-stream"); // "bin" * ``` */ getExtensionFromMimeType(mimeType) { return FileDetector.reverseMimeMap[mimeType] || "bin"; } /** * Returns the content type of file based on its filename. * * @param filename - The filename to check * @returns The MIME type * * @example * ```typescript * const detector = alepha.inject(FileDetector); * const mimeType = detector.getContentType("image.png"); // "image/png" * ``` */ getContentType(filename) { const ext = filename.toLowerCase().split(".").pop() || ""; return FileDetector.mimeMap[ext] || "application/octet-stream"; } /** * Detects the file type by checking magic bytes against the stream content. * * @param stream - The readable stream to check * @param filename - The filename (used to get the extension) * @returns File type information including MIME type, extension, and verification status * * @example * ```typescript * const detector = alepha.inject(FileDetector); * const stream = createReadStream('image.png'); * const result = await detector.detectFileType(stream, 'image.png'); * console.log(result.mimeType); // 'image/png' * console.log(result.verified); // true if magic bytes match * ``` */ async detectFileType(stream, filename) { const expectedMimeType = this.getContentType(filename); const lastDotIndex = filename.lastIndexOf("."); const ext = lastDotIndex > 0 ? filename.substring(lastDotIndex + 1).toLowerCase() : ""; const { buffer, stream: newStream } = await this.peekBytes(stream, 16); const expectedSignatures = FileDetector.MAGIC_BYTES[ext]; if (expectedSignatures) { for (const { signature, mimeType } of expectedSignatures) if (this.matchesSignature(buffer, signature)) return { mimeType, extension: ext, verified: true, stream: newStream }; } for (const { ext: detectedExt, signature, mimeType } of FileDetector.ALL_SIGNATURES) if (detectedExt !== ext && this.matchesSignature(buffer, signature)) return { mimeType, extension: detectedExt, verified: true, stream: newStream }; return { mimeType: expectedMimeType, extension: ext, verified: false, stream: newStream }; } /** * Reads all bytes from a stream and returns the first N bytes along with a new stream containing all data. * This approach reads the entire stream upfront to avoid complex async handling issues. * * @protected */ async peekBytes(stream, numBytes) { const chunks = []; for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); const allData = Buffer.concat(chunks); return { buffer: allData.subarray(0, numBytes), stream: Readable.from(allData) }; } /** * Checks if a buffer matches a magic byte signature. * * @protected */ matchesSignature(buffer, signature) { if (buffer.length < signature.length) return false; for (let i = 0; i < signature.length; i++) if (signature[i] !== null && buffer[i] !== signature[i]) return false; return true; } }; //#endregion //#region ../../src/system/errors/FileError.ts var FileError = class extends Error { constructor(message, cause) { super(message); this.name = "FileError"; this.cause = cause; } }; //#endregion //#region ../../src/system/providers/WorkerdFileSystemProvider.ts /** * Web-standard implementation of FileSystemProvider for Cloudflare Workers and other edge runtimes. * * Uses only Web APIs (ReadableStream, TextEncoder, etc.) — no Node.js-specific APIs. * Provides working `createFile` with proper streaming support. * Filesystem operations (rm, cp, mv, etc.) are not available in edge runtimes and will throw. * * @example * ```typescript * const fs = alepha.inject(WorkerdFileSystemProvider); * * // Create from text (returns FileLike with web ReadableStream) * const file = fs.createFile({ text: "Hello!", name: "greeting.txt" }); * const stream = file.stream(); // ReadableStream (web standard) * ``` */ var WorkerdFileSystemProvider = class { detector = $inject(FileDetector); json = $inject(Json); encoder = new TextEncoder(); decoder = new TextDecoder(); join(...paths) { const parts = paths.join("/").replace(/\/+/g, "/").split("/"); const resolved = []; for (const part of parts) if (part === "..") resolved.pop(); else if (part !== ".") resolved.push(part); return resolved.join("/") || "."; } createFile(options) { if ("text" in options) return this.createFileFromText(options.text, { type: options.type, name: options.name }); if ("arrayBuffer" in options) return this.createFileFromArrayBuffer(options.arrayBuffer, { type: options.type, name: options.name }); if ("buffer" in options) { const ab = options.buffer instanceof ArrayBuffer ? options.buffer : options.buffer.buffer.slice(options.buffer.byteOffset, options.buffer.byteOffset + options.buffer.byteLength); return this.createFileFromArrayBuffer(ab, { type: options.type, name: options.name }); } if ("file" in options) return this.createFileFromWebFile(options.file, { type: options.type, name: options.name, size: options.size }); if ("response" in options) return this.createFileFromResponse(options.response, { type: options.type, name: options.name }); if ("stream" in options) return this.createFileFromStream(options.stream, { type: options.type, name: options.name, size: options.size }); if ("url" in options) return this.createFileFromUrl(options.url, { type: options.type, name: options.name }); if ("path" in options) throw new AlephaError("WorkerdFileSystemProvider.createFile: 'path' source is not supported in edge runtimes."); throw new AlephaError("WorkerdFileSystemProvider.createFile: unsupported options."); } createFileFromText(text, options = {}) { const encoded = this.encoder.encode(text); const name = options.name ?? "file.txt"; return { name, type: options.type ?? this.detector.getContentType(name), size: encoded.byteLength, lastModified: Date.now(), stream: () => new ReadableStream({ start(controller) { controller.enqueue(encoded); controller.close(); } }), arrayBuffer: async () => encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength), text: async () => text }; } createFileFromArrayBuffer(source, options = {}) { const name = options.name ?? "file"; const bytes = new Uint8Array(source); return { name, type: options.type ?? this.detector.getContentType(name), size: source.byteLength, lastModified: Date.now(), stream: () => new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } }), arrayBuffer: async () => source, text: async () => this.decoder.decode(source) }; } createFileFromWebFile(source, options = {}) { const name = options.name ?? source.name; return { name, type: options.type ?? (source.type || this.detector.getContentType(name)), size: options.size ?? source.size ?? 0, lastModified: source.lastModified || Date.now(), stream: () => source.stream(), arrayBuffer: async () => await source.arrayBuffer(), text: async () => await source.text() }; } createFileFromResponse(response, options = {}) { if (!response.body) throw new AlephaError("Response has no body stream"); const sizeHeader = response.headers.get("content-length"); const size = sizeHeader ? parseInt(sizeHeader, 10) : 0; let name = options.name; if (!name) { const cd = response.headers.get("content-disposition"); if (cd) { const match = cd.match(/filename="?([^"]+)"?/); if (match) name = match[1]; } } name ??= "file"; const type = options.type ?? response.headers.get("content-type") ?? void 0; return { name, type: type ?? this.detector.getContentType(name), size, lastModified: Date.now(), stream: () => response.body, arrayBuffer: async () => await response.arrayBuffer(), text: async () => await response.text() }; } createFileFromStream(source, options = {}) { const name = options.name ?? "file"; let buffer = null; const consumeStream = async () => { if (buffer) return buffer; const reader = source.getReader(); const chunks = []; let done = false; while (!done) { const result = await reader.read(); done = result.done; if (result.value) chunks.push(result.value); } const total = chunks.reduce((sum, c) => sum + c.byteLength, 0); const merged = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { merged.set(chunk, offset); offset += chunk.byteLength; } buffer = merged.buffer; return buffer; }; return { name, type: options.type ?? this.detector.getContentType(name), size: options.size ?? 0, lastModified: Date.now(), stream: () => source, arrayBuffer: consumeStream, text: async () => this.decoder.decode(await consumeStream()) }; } createFileFromUrl(url, options = {}) { const parsedUrl = new URL(url); const name = options.name ?? parsedUrl.pathname.split("/").pop() ?? "file"; return { name, type: options.type ?? this.detector.getContentType(name), size: 0, lastModified: Date.now(), stream: () => { throw new AlephaError("WorkerdFileSystemProvider: streaming from URL is not supported. Use fetch() and createFile({ response }) instead."); }, arrayBuffer: async () => { return await (await fetch(url)).arrayBuffer(); }, text: async () => { return await (await fetch(url)).text(); } }; } async rm(_path, _options) { throw new AlephaError("WorkerdFileSystemProvider: rm() is not available in edge runtimes."); } async cp(_src, _dest, _options) { throw new AlephaError("WorkerdFileSystemProvider: cp() is not available in edge runtimes."); } async mv(_src, _dest) { throw new AlephaError("WorkerdFileSystemProvider: mv() is not available in edge runtimes."); } async mkdir(_path, _options) { throw new AlephaError("WorkerdFileSystemProvider: mkdir() is not available in edge runtimes."); } async ls(_path, _options) { throw new AlephaError("WorkerdFileSystemProvider: ls() is not available in edge runtimes."); } async exists(_path) { throw new AlephaError("WorkerdFileSystemProvider: exists() is not available in edge runtimes."); } async readFile(_path) { throw new AlephaError("WorkerdFileSystemProvider: readFile() is not available in edge runtimes."); } async writeFile(_path, _data) { throw new AlephaError("WorkerdFileSystemProvider: writeFile() is not available in edge runtimes."); } async readTextFile(_path) { throw new AlephaError("WorkerdFileSystemProvider: readTextFile() is not available in edge runtimes."); } async readJsonFile(_path) { throw new AlephaError("WorkerdFileSystemProvider: readJsonFile() is not available in edge runtimes."); } }; //#endregion //#region ../../src/system/index.browser.ts const AlephaSystem = $module({ name: "alepha.system", services: [ FileDetector, FileSystemProvider, ShellProvider ], variants: [MemoryFileSystemProvider, MemoryShellProvider], register: (alepha) => alepha.with({ optional: true, provide: FileSystemProvider, use: MemoryFileSystemProvider }).with({ optional: true, provide: ShellProvider, use: MemoryShellProvider }) }); //#endregion export { AlephaSystem, FileDetector, FileError, FileSystemProvider, MemoryFileSystemProvider, MemoryShellProvider, ShellProvider, WorkerdFileSystemProvider }; //# sourceMappingURL=index.browser.js.map