@ricsam/isolate-fs 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,300 +0,0 @@
1
- import * as nodeFs from "node:fs";
2
- import * as nodePath from "node:path";
3
- import { lookup as mimeLookup } from "mime-types";
4
- import type { FileSystemHandler } from "./index.ts";
5
-
6
- export interface NodeFileSystemHandlerOptions {
7
- /** Custom fs module (e.g., memfs for testing). Defaults to Node.js fs */
8
- fs?: typeof nodeFs;
9
- }
10
-
11
- /**
12
- * Create a FileSystemHandler backed by the Node.js filesystem
13
- *
14
- * @param rootPath - Absolute path to the root directory for the sandbox
15
- * @param options - Optional configuration
16
- * @returns FileSystemHandler implementation
17
- *
18
- * @example
19
- * import { createNodeFileSystemHandler } from "@ricsam/isolate-fs";
20
- *
21
- * const handler = createNodeFileSystemHandler("/tmp/sandbox");
22
- *
23
- * // Use with createRuntime
24
- * const runtime = await createRuntime({
25
- * fs: { handler }
26
- * });
27
- */
28
- export function createNodeFileSystemHandler(
29
- rootPath: string,
30
- options?: NodeFileSystemHandlerOptions
31
- ): FileSystemHandler {
32
- const fs = options?.fs ?? nodeFs;
33
- const fsPromises = fs.promises;
34
-
35
- // Resolve the root path to ensure it's absolute
36
- const resolvedRoot = nodePath.resolve(rootPath);
37
-
38
- /**
39
- * Map a virtual path to a real filesystem path
40
- * Virtual paths always start with "/" and are relative to rootPath
41
- */
42
- function toRealPath(virtualPath: string): string {
43
- // Normalize the virtual path
44
- const normalized = nodePath.normalize(virtualPath);
45
- // Join with root, handling the leading slash
46
- const relativePath = normalized.startsWith("/")
47
- ? normalized.slice(1)
48
- : normalized;
49
- return nodePath.join(resolvedRoot, relativePath);
50
- }
51
-
52
- /**
53
- * Map Node.js errors to DOMException-style error messages
54
- */
55
- function mapError(err: unknown, operation: string): Error {
56
- if (!(err instanceof Error)) {
57
- return new Error(`[Error]${operation} failed`);
58
- }
59
-
60
- const nodeError = err as NodeJS.ErrnoException;
61
-
62
- switch (nodeError.code) {
63
- case "ENOENT":
64
- return new Error(`[NotFoundError]${operation}: path not found`);
65
- case "EISDIR":
66
- return new Error(
67
- `[TypeMismatchError]${operation}: expected file but found directory`
68
- );
69
- case "ENOTDIR":
70
- return new Error(
71
- `[TypeMismatchError]${operation}: expected directory but found file`
72
- );
73
- case "ENOTEMPTY":
74
- return new Error(
75
- `[InvalidModificationError]${operation}: directory not empty`
76
- );
77
- case "EEXIST":
78
- return new Error(`[InvalidModificationError]${operation}: already exists`);
79
- case "EACCES":
80
- case "EPERM":
81
- return new Error(`[NotAllowedError]${operation}: permission denied`);
82
- default:
83
- return new Error(`[Error]${operation}: ${nodeError.message}`);
84
- }
85
- }
86
-
87
- /**
88
- * Get MIME type for a file based on extension
89
- */
90
- function getMimeType(filePath: string): string {
91
- const result = mimeLookup(filePath);
92
- return result || "application/octet-stream";
93
- }
94
-
95
- return {
96
- async getFileHandle(
97
- path: string,
98
- options?: { create?: boolean }
99
- ): Promise<void> {
100
- const realPath = toRealPath(path);
101
-
102
- try {
103
- const stats = await fsPromises.stat(realPath);
104
- if (stats.isDirectory()) {
105
- throw new Error(
106
- "[TypeMismatchError]getFileHandle: expected file but found directory"
107
- );
108
- }
109
- // File exists and is a file - success
110
- } catch (err) {
111
- const nodeError = err as NodeJS.ErrnoException;
112
- if (nodeError.code === "ENOENT") {
113
- if (options?.create) {
114
- // Create empty file
115
- await fsPromises.writeFile(realPath, "");
116
- return;
117
- }
118
- throw new Error("[NotFoundError]getFileHandle: file not found");
119
- }
120
- throw mapError(err, "getFileHandle");
121
- }
122
- },
123
-
124
- async getDirectoryHandle(
125
- path: string,
126
- options?: { create?: boolean }
127
- ): Promise<void> {
128
- const realPath = toRealPath(path);
129
-
130
- try {
131
- const stats = await fsPromises.stat(realPath);
132
- if (!stats.isDirectory()) {
133
- throw new Error(
134
- "[TypeMismatchError]getDirectoryHandle: expected directory but found file"
135
- );
136
- }
137
- // Directory exists - success
138
- } catch (err) {
139
- const nodeError = err as NodeJS.ErrnoException;
140
- if (nodeError.code === "ENOENT") {
141
- if (options?.create) {
142
- // Create directory
143
- await fsPromises.mkdir(realPath, { recursive: true });
144
- return;
145
- }
146
- throw new Error(
147
- "[NotFoundError]getDirectoryHandle: directory not found"
148
- );
149
- }
150
- throw mapError(err, "getDirectoryHandle");
151
- }
152
- },
153
-
154
- async removeEntry(
155
- path: string,
156
- options?: { recursive?: boolean }
157
- ): Promise<void> {
158
- const realPath = toRealPath(path);
159
-
160
- try {
161
- const stats = await fsPromises.stat(realPath);
162
-
163
- if (stats.isDirectory()) {
164
- if (options?.recursive) {
165
- // Use rm with recursive for non-empty directories
166
- await fsPromises.rm(realPath, { recursive: true });
167
- } else {
168
- // Try rmdir for empty directories (will fail if not empty)
169
- await fsPromises.rmdir(realPath);
170
- }
171
- } else {
172
- // Use unlink for files
173
- await fsPromises.unlink(realPath);
174
- }
175
- } catch (err) {
176
- throw mapError(err, "removeEntry");
177
- }
178
- },
179
-
180
- async readDirectory(
181
- path: string
182
- ): Promise<Array<{ name: string; kind: "file" | "directory" }>> {
183
- const realPath = toRealPath(path);
184
-
185
- try {
186
- const entries = await fsPromises.readdir(realPath, {
187
- withFileTypes: true,
188
- });
189
-
190
- return entries.map((entry) => ({
191
- name: entry.name,
192
- kind: entry.isDirectory() ? ("directory" as const) : ("file" as const),
193
- }));
194
- } catch (err) {
195
- throw mapError(err, "readDirectory");
196
- }
197
- },
198
-
199
- async readFile(
200
- path: string
201
- ): Promise<{
202
- data: Uint8Array;
203
- size: number;
204
- lastModified: number;
205
- type: string;
206
- }> {
207
- const realPath = toRealPath(path);
208
-
209
- try {
210
- const [data, stats] = await Promise.all([
211
- fsPromises.readFile(realPath),
212
- fsPromises.stat(realPath),
213
- ]);
214
-
215
- if (stats.isDirectory()) {
216
- throw new Error(
217
- "[TypeMismatchError]readFile: expected file but found directory"
218
- );
219
- }
220
-
221
- return {
222
- data: new Uint8Array(data),
223
- size: stats.size,
224
- lastModified: stats.mtimeMs,
225
- type: getMimeType(realPath),
226
- };
227
- } catch (err) {
228
- if (err instanceof Error && err.message.startsWith("[")) {
229
- throw err;
230
- }
231
- throw mapError(err, "readFile");
232
- }
233
- },
234
-
235
- async writeFile(
236
- path: string,
237
- data: Uint8Array,
238
- position?: number
239
- ): Promise<void> {
240
- const realPath = toRealPath(path);
241
-
242
- try {
243
- // Check file exists first (matches WHATWG semantics where file must exist via getFileHandle)
244
- await fsPromises.access(realPath);
245
-
246
- if (position !== undefined) {
247
- // Position-based write - need to use r+ to preserve existing content
248
- const fh = await fsPromises.open(realPath, "r+");
249
- try {
250
- await fh.write(data, 0, data.length, position);
251
- } finally {
252
- await fh.close();
253
- }
254
- } else {
255
- // Replace entire content
256
- await fsPromises.writeFile(realPath, data);
257
- }
258
- } catch (err) {
259
- throw mapError(err, "writeFile");
260
- }
261
- },
262
-
263
- async truncateFile(path: string, size: number): Promise<void> {
264
- const realPath = toRealPath(path);
265
-
266
- try {
267
- await fsPromises.truncate(realPath, size);
268
- } catch (err) {
269
- throw mapError(err, "truncateFile");
270
- }
271
- },
272
-
273
- async getFileMetadata(
274
- path: string
275
- ): Promise<{ size: number; lastModified: number; type: string }> {
276
- const realPath = toRealPath(path);
277
-
278
- try {
279
- const stats = await fsPromises.stat(realPath);
280
-
281
- if (stats.isDirectory()) {
282
- throw new Error(
283
- "[TypeMismatchError]getFileMetadata: expected file but found directory"
284
- );
285
- }
286
-
287
- return {
288
- size: stats.size,
289
- lastModified: stats.mtimeMs,
290
- type: getMimeType(realPath),
291
- };
292
- } catch (err) {
293
- if (err instanceof Error && err.message.startsWith("[")) {
294
- throw err;
295
- }
296
- throw mapError(err, "getFileMetadata");
297
- }
298
- },
299
- };
300
- }