opfs-mock 2.5.1 → 2.6.1
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 +1 -1
- package/dist/index.d.mts +32 -4
- package/dist/index.mjs +59 -27
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# opfs-mock
|
|
2
2
|
|
|
3
3
|
In-memory implementation of the [origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system). Its main utility is for testing OPFS-dependent code in Node.js. It's tested
|
|
4
|
-
on Node.js versions
|
|
4
|
+
on all supported Node.js versions (`22.x`, `24.x`, `25.x`, `26.x`).
|
|
5
5
|
|
|
6
6
|
## Installation
|
|
7
7
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
// Experimental properties:
|
|
3
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/queryPermission
|
|
4
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/requestPermission
|
|
5
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/remove
|
|
6
|
+
type PermissionHandler = (descriptor?: FileSystemHandlePermissionDescriptor) => Promise<PermissionState>;
|
|
7
|
+
declare global {
|
|
8
|
+
interface FileSystemHandlePermissionDescriptor {
|
|
9
|
+
mode?: 'read' | 'readwrite';
|
|
10
|
+
}
|
|
11
|
+
interface FileSystemFileHandle extends FileSystemHandle {
|
|
12
|
+
queryPermission(descriptor?: FileSystemHandlePermissionDescriptor): Promise<PermissionState>;
|
|
13
|
+
requestPermission(descriptor?: FileSystemHandlePermissionDescriptor): Promise<PermissionState>;
|
|
14
|
+
remove(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
interface FileSystemDirectoryHandle extends FileSystemHandle {
|
|
17
|
+
queryPermission(descriptor?: FileSystemHandlePermissionDescriptor): Promise<PermissionState>;
|
|
18
|
+
requestPermission(descriptor?: FileSystemHandlePermissionDescriptor): Promise<PermissionState>;
|
|
19
|
+
remove(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
1
23
|
//#region src/index.d.ts
|
|
24
|
+
interface StorageFactoryOptions extends StorageEstimate {
|
|
25
|
+
queryPermission?: PermissionHandler;
|
|
26
|
+
requestPermission?: PermissionHandler;
|
|
27
|
+
}
|
|
2
28
|
declare const storageFactory: ({
|
|
3
29
|
usage,
|
|
4
|
-
quota
|
|
5
|
-
|
|
30
|
+
quota,
|
|
31
|
+
queryPermission,
|
|
32
|
+
requestPermission
|
|
33
|
+
}?: StorageFactoryOptions) => StorageManager;
|
|
6
34
|
declare const mockOPFS: () => void;
|
|
7
|
-
declare const resetMockOPFS: () => void;
|
|
35
|
+
declare const resetMockOPFS: (options?: StorageFactoryOptions) => void;
|
|
8
36
|
//#endregion
|
|
9
|
-
export { mockOPFS, resetMockOPFS, storageFactory };
|
|
37
|
+
export { StorageFactoryOptions, mockOPFS, resetMockOPFS, storageFactory };
|
package/dist/index.mjs
CHANGED
|
@@ -13,31 +13,36 @@ const getSizeOfDirectory = async (directory) => {
|
|
|
13
13
|
} else if (isDirectoryHandle(handle)) totalSize += await getSizeOfDirectory(handle);
|
|
14
14
|
return totalSize;
|
|
15
15
|
};
|
|
16
|
-
|
|
17
16
|
//#endregion
|
|
18
17
|
//#region src/opfs.ts
|
|
19
18
|
const isObject = (v) => typeof v === "object" && v !== null;
|
|
20
19
|
const isLegacyWriteParams = (v) => isObject(v) && !("type" in v) && "data" in v;
|
|
21
|
-
const fileSystemFileHandleFactory = (name, fileData, exists) => {
|
|
20
|
+
const fileSystemFileHandleFactory = (name, fileData, exists, onRemove, permissions) => {
|
|
21
|
+
const checkPermission = async (mode = "read") => {
|
|
22
|
+
if (await (permissions?.queryPermission?.({ mode }) ?? Promise.resolve("granted")) !== "granted") throw new DOMException("Permission denied", "NotAllowedError");
|
|
23
|
+
};
|
|
22
24
|
return {
|
|
23
25
|
kind: "file",
|
|
24
26
|
name,
|
|
25
|
-
queryPermission: async () =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
queryPermission: permissions?.queryPermission ?? (async () => "granted"),
|
|
28
|
+
requestPermission: permissions?.requestPermission ?? (async () => "granted"),
|
|
29
|
+
remove: async () => {
|
|
30
|
+
await checkPermission("readwrite");
|
|
31
|
+
if (!exists()) throw new DOMException("A requested file or directory could not be found at the time an operation was processed.", "NotFoundError");
|
|
32
|
+
onRemove?.();
|
|
30
33
|
},
|
|
31
34
|
isSameEntry: async function(other) {
|
|
32
35
|
return other === this;
|
|
33
36
|
},
|
|
34
37
|
getFile: async () => {
|
|
38
|
+
await checkPermission("read");
|
|
35
39
|
if (!exists()) throw new DOMException("A requested file or directory could not be found at the time an operation was processed.", "NotFoundError");
|
|
36
40
|
const f = new File([fileData.content], name, { lastModified: fileData.lastModified });
|
|
37
41
|
f._opfsId = fileData.id;
|
|
38
42
|
return f;
|
|
39
43
|
},
|
|
40
44
|
createWritable: async (options) => {
|
|
45
|
+
await checkPermission("readwrite");
|
|
41
46
|
const keepExistingData = options?.keepExistingData;
|
|
42
47
|
let abortReason = "";
|
|
43
48
|
let isAborted = false;
|
|
@@ -152,6 +157,7 @@ const fileSystemFileHandleFactory = (name, fileData, exists) => {
|
|
|
152
157
|
});
|
|
153
158
|
},
|
|
154
159
|
createSyncAccessHandle: async () => {
|
|
160
|
+
await checkPermission("readwrite");
|
|
155
161
|
if (fileData.locked) throw new DOMException("A sync access handle is already open for this file", "InvalidStateError");
|
|
156
162
|
fileData.locked = true;
|
|
157
163
|
let closed = false;
|
|
@@ -207,27 +213,39 @@ const fileSystemFileHandleFactory = (name, fileData, exists) => {
|
|
|
207
213
|
}
|
|
208
214
|
};
|
|
209
215
|
};
|
|
210
|
-
const fileSystemDirectoryHandleFactory = (name) => {
|
|
216
|
+
const fileSystemDirectoryHandleFactory = (name, permissions, onRemove) => {
|
|
211
217
|
const files = /* @__PURE__ */ new Map();
|
|
212
218
|
const directories = /* @__PURE__ */ new Map();
|
|
213
219
|
const getJoinedMaps = () => {
|
|
214
220
|
return new Map([...files, ...directories]);
|
|
215
221
|
};
|
|
216
|
-
|
|
222
|
+
const checkPermission = async (mode = "read") => {
|
|
223
|
+
if (await (permissions?.queryPermission?.({ mode }) ?? Promise.resolve("granted")) !== "granted") throw new DOMException("Permission denied", "NotAllowedError");
|
|
224
|
+
};
|
|
225
|
+
const handle = {
|
|
217
226
|
kind: "directory",
|
|
218
227
|
name,
|
|
219
|
-
queryPermission: async () => "granted",
|
|
220
|
-
requestPermission: async () => "granted",
|
|
228
|
+
queryPermission: permissions?.queryPermission ?? (async () => "granted"),
|
|
229
|
+
requestPermission: permissions?.requestPermission ?? (async () => "granted"),
|
|
230
|
+
remove: async () => {
|
|
231
|
+
await checkPermission("readwrite");
|
|
232
|
+
if (!onRemove) throw new DOMException("The root directory cannot be removed.", "InvalidModificationError");
|
|
233
|
+
for await (const _ of handle.values()) throw new DOMException("The directory is not empty", "InvalidModificationError");
|
|
234
|
+
onRemove();
|
|
235
|
+
},
|
|
221
236
|
isSameEntry: async function(other) {
|
|
222
237
|
return other === this;
|
|
223
238
|
},
|
|
224
239
|
getFileHandle: async (fileName, options) => {
|
|
225
240
|
if (directories.has(fileName)) throw new DOMException(`A directory with the same name exists: ${fileName}`, "TypeMismatchError");
|
|
226
|
-
if (!files.has(fileName) && options?.create)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
241
|
+
if (!files.has(fileName) && options?.create) {
|
|
242
|
+
await checkPermission("readwrite");
|
|
243
|
+
files.set(fileName, fileSystemFileHandleFactory(fileName, {
|
|
244
|
+
content: new Uint8Array(),
|
|
245
|
+
lastModified: Date.now(),
|
|
246
|
+
id: Symbol("file")
|
|
247
|
+
}, () => files.has(fileName), () => files.delete(fileName), permissions));
|
|
248
|
+
} else await checkPermission("read");
|
|
231
249
|
const fileHandle = files.get(fileName);
|
|
232
250
|
if (!fileHandle) throw new DOMException(`File not found: ${fileName}`, "NotFoundError");
|
|
233
251
|
return fileHandle;
|
|
@@ -235,14 +253,16 @@ const fileSystemDirectoryHandleFactory = (name) => {
|
|
|
235
253
|
getDirectoryHandle: async (dirName, options) => {
|
|
236
254
|
if (files.has(dirName)) throw new DOMException(`A file with the same name exists: ${dirName}`, "TypeMismatchError");
|
|
237
255
|
if (!directories.has(dirName) && options?.create) {
|
|
238
|
-
|
|
256
|
+
await checkPermission("readwrite");
|
|
257
|
+
const dir = fileSystemDirectoryHandleFactory(dirName, permissions, () => directories.delete(dirName));
|
|
239
258
|
directories.set(dirName, dir);
|
|
240
|
-
}
|
|
259
|
+
} else await checkPermission("read");
|
|
241
260
|
const directoryHandle = directories.get(dirName);
|
|
242
261
|
if (!directoryHandle) throw new DOMException(`Directory not found: ${dirName}`, "NotFoundError");
|
|
243
262
|
return directoryHandle;
|
|
244
263
|
},
|
|
245
264
|
removeEntry: async (entryName, options) => {
|
|
265
|
+
await checkPermission("readwrite");
|
|
246
266
|
if (files.has(entryName)) {
|
|
247
267
|
files.delete(entryName);
|
|
248
268
|
return;
|
|
@@ -256,20 +276,27 @@ const fileSystemDirectoryHandleFactory = (name) => {
|
|
|
256
276
|
throw new DOMException(`No such file or directory: ${entryName}`, "NotFoundError");
|
|
257
277
|
},
|
|
258
278
|
[Symbol.asyncIterator]: async function* () {
|
|
279
|
+
await checkPermission("read");
|
|
259
280
|
const entries = getJoinedMaps();
|
|
260
281
|
for (const [n, h] of entries) yield [n, h];
|
|
261
282
|
return void 0;
|
|
262
283
|
},
|
|
263
284
|
entries: async function* () {
|
|
264
|
-
|
|
285
|
+
await checkPermission("read");
|
|
286
|
+
const joinedMaps = getJoinedMaps();
|
|
287
|
+
for (const [n, h] of joinedMaps.entries()) yield [n, h];
|
|
265
288
|
},
|
|
266
289
|
keys: async function* () {
|
|
290
|
+
await checkPermission("read");
|
|
267
291
|
yield* getJoinedMaps().keys();
|
|
268
292
|
},
|
|
269
293
|
values: async function* () {
|
|
270
|
-
|
|
294
|
+
await checkPermission("read");
|
|
295
|
+
const joinedMaps = getJoinedMaps();
|
|
296
|
+
for (const h of joinedMaps.values()) yield h;
|
|
271
297
|
},
|
|
272
298
|
resolve: async function(possibleDescendant) {
|
|
299
|
+
await checkPermission("read");
|
|
273
300
|
const traverseDirectory = async (directory, target, path = []) => {
|
|
274
301
|
if (await directory.isSameEntry(target)) return path;
|
|
275
302
|
for await (const [nm, h] of directory.entries()) if (isDirectoryHandle(h)) {
|
|
@@ -283,12 +310,15 @@ const fileSystemDirectoryHandleFactory = (name) => {
|
|
|
283
310
|
return traverseDirectory(this, possibleDescendant);
|
|
284
311
|
}
|
|
285
312
|
};
|
|
313
|
+
return handle;
|
|
286
314
|
};
|
|
287
|
-
|
|
288
315
|
//#endregion
|
|
289
316
|
//#region src/index.ts
|
|
290
|
-
const storageFactory = ({ usage = 0, quota = 1024 ** 3 } = {}) => {
|
|
291
|
-
const root = fileSystemDirectoryHandleFactory("root"
|
|
317
|
+
const storageFactory = ({ usage = 0, quota = 1024 ** 3, queryPermission, requestPermission } = {}) => {
|
|
318
|
+
const root = fileSystemDirectoryHandleFactory("root", {
|
|
319
|
+
queryPermission,
|
|
320
|
+
requestPermission
|
|
321
|
+
});
|
|
292
322
|
return {
|
|
293
323
|
estimate: async () => {
|
|
294
324
|
return {
|
|
@@ -317,14 +347,16 @@ const mockOPFS = () => {
|
|
|
317
347
|
writable: true
|
|
318
348
|
});
|
|
319
349
|
};
|
|
320
|
-
const resetMockOPFS = () => {
|
|
321
|
-
const root = fileSystemDirectoryHandleFactory("root"
|
|
350
|
+
const resetMockOPFS = (options = {}) => {
|
|
351
|
+
const root = fileSystemDirectoryHandleFactory("root", {
|
|
352
|
+
queryPermission: options.queryPermission,
|
|
353
|
+
requestPermission: options.requestPermission
|
|
354
|
+
});
|
|
322
355
|
Object.defineProperty(globalThis.navigator.storage, "getDirectory", {
|
|
323
356
|
value: () => root,
|
|
324
357
|
writable: true
|
|
325
358
|
});
|
|
326
359
|
};
|
|
327
360
|
if (typeof globalThis !== "undefined") mockOPFS();
|
|
328
|
-
|
|
329
361
|
//#endregion
|
|
330
|
-
export { mockOPFS, resetMockOPFS, storageFactory };
|
|
362
|
+
export { mockOPFS, resetMockOPFS, storageFactory };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opfs-mock",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Mock all origin private file system APIs for your Jest or Vitest tests",
|
|
6
6
|
"author": "Jure Rotar <hello@jurerotar.com>",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"lint": "npx @biomejs/biome lint --fix",
|
|
52
52
|
"format:check": "npx @biomejs/biome format",
|
|
53
53
|
"format": "npx @biomejs/biome format --write",
|
|
54
|
-
"type-check": "
|
|
54
|
+
"type-check": "tsgo",
|
|
55
55
|
"test": "npm run test:node && npm run test:happy-dom",
|
|
56
56
|
"test:node": "vitest --environment=node",
|
|
57
57
|
"test:jsdom": "vitest --environment=jsdom",
|
|
@@ -60,13 +60,14 @@
|
|
|
60
60
|
"release": "npm publish --access public"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@biomejs/biome": "2.
|
|
64
|
-
"@
|
|
63
|
+
"@biomejs/biome": "2.4.14",
|
|
64
|
+
"@typescript/native-preview": "7.0.0-dev.20260505.1",
|
|
65
|
+
"@vitest/coverage-v8": "4.1.5",
|
|
65
66
|
"@web-std/file": "3.0.3",
|
|
66
|
-
"happy-dom": "20.0
|
|
67
|
-
"jsdom": "
|
|
68
|
-
"tsdown": "0.
|
|
69
|
-
"typescript": "
|
|
70
|
-
"vitest": "4.
|
|
67
|
+
"happy-dom": "20.9.0",
|
|
68
|
+
"jsdom": "29.1.1",
|
|
69
|
+
"tsdown": "0.21.10",
|
|
70
|
+
"typescript": "6.0.3",
|
|
71
|
+
"vitest": "4.1.5"
|
|
71
72
|
}
|
|
72
73
|
}
|