@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.
- package/README.md +52 -0
- package/dist/cjs/index.cjs +752 -0
- package/dist/cjs/index.cjs.map +10 -0
- package/dist/cjs/node-adapter.cjs +230 -0
- package/dist/cjs/node-adapter.cjs.map +10 -0
- package/dist/cjs/package.json +5 -0
- package/dist/mjs/index.mjs +708 -0
- package/dist/mjs/index.mjs.map +10 -0
- package/dist/mjs/node-adapter.mjs +186 -0
- package/dist/mjs/node-adapter.mjs.map +10 -0
- package/dist/mjs/package.json +5 -0
- package/dist/types/index.d.ts +70 -0
- package/dist/types/isolate.d.ts +308 -0
- package/dist/types/node-adapter.d.ts +24 -0
- package/package.json +41 -15
- package/CHANGELOG.md +0 -9
- package/src/fixtures/test-image.png +0 -0
- package/src/index.test.ts +0 -882
- package/src/index.ts +0 -997
- package/src/integration.test.ts +0 -288
- package/src/node-adapter.test.ts +0 -337
- package/src/node-adapter.ts +0 -300
- package/src/streaming.test.ts +0 -634
- package/tsconfig.json +0 -8
package/src/node-adapter.ts
DELETED
|
@@ -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
|
-
}
|