alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
1,441 lines (1,440 loc) • 35.3 kB
JavaScript
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