opfs-mock 2.5.1 → 2.6.0
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/dist/index.d.mts +32 -4
- package/dist/index.mjs +55 -25
- package/package.json +9 -8
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,25 @@ 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* () {
|
|
285
|
+
await checkPermission("read");
|
|
264
286
|
yield* getJoinedMaps().entries();
|
|
265
287
|
},
|
|
266
288
|
keys: async function* () {
|
|
289
|
+
await checkPermission("read");
|
|
267
290
|
yield* getJoinedMaps().keys();
|
|
268
291
|
},
|
|
269
292
|
values: async function* () {
|
|
293
|
+
await checkPermission("read");
|
|
270
294
|
yield* getJoinedMaps().values();
|
|
271
295
|
},
|
|
272
296
|
resolve: async function(possibleDescendant) {
|
|
297
|
+
await checkPermission("read");
|
|
273
298
|
const traverseDirectory = async (directory, target, path = []) => {
|
|
274
299
|
if (await directory.isSameEntry(target)) return path;
|
|
275
300
|
for await (const [nm, h] of directory.entries()) if (isDirectoryHandle(h)) {
|
|
@@ -283,12 +308,15 @@ const fileSystemDirectoryHandleFactory = (name) => {
|
|
|
283
308
|
return traverseDirectory(this, possibleDescendant);
|
|
284
309
|
}
|
|
285
310
|
};
|
|
311
|
+
return handle;
|
|
286
312
|
};
|
|
287
|
-
|
|
288
313
|
//#endregion
|
|
289
314
|
//#region src/index.ts
|
|
290
|
-
const storageFactory = ({ usage = 0, quota = 1024 ** 3 } = {}) => {
|
|
291
|
-
const root = fileSystemDirectoryHandleFactory("root"
|
|
315
|
+
const storageFactory = ({ usage = 0, quota = 1024 ** 3, queryPermission, requestPermission } = {}) => {
|
|
316
|
+
const root = fileSystemDirectoryHandleFactory("root", {
|
|
317
|
+
queryPermission,
|
|
318
|
+
requestPermission
|
|
319
|
+
});
|
|
292
320
|
return {
|
|
293
321
|
estimate: async () => {
|
|
294
322
|
return {
|
|
@@ -317,14 +345,16 @@ const mockOPFS = () => {
|
|
|
317
345
|
writable: true
|
|
318
346
|
});
|
|
319
347
|
};
|
|
320
|
-
const resetMockOPFS = () => {
|
|
321
|
-
const root = fileSystemDirectoryHandleFactory("root"
|
|
348
|
+
const resetMockOPFS = (options = {}) => {
|
|
349
|
+
const root = fileSystemDirectoryHandleFactory("root", {
|
|
350
|
+
queryPermission: options.queryPermission,
|
|
351
|
+
requestPermission: options.requestPermission
|
|
352
|
+
});
|
|
322
353
|
Object.defineProperty(globalThis.navigator.storage, "getDirectory", {
|
|
323
354
|
value: () => root,
|
|
324
355
|
writable: true
|
|
325
356
|
});
|
|
326
357
|
};
|
|
327
358
|
if (typeof globalThis !== "undefined") mockOPFS();
|
|
328
|
-
|
|
329
359
|
//#endregion
|
|
330
|
-
export { mockOPFS, resetMockOPFS, storageFactory };
|
|
360
|
+
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.0",
|
|
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.6",
|
|
64
|
+
"@typescript/native-preview": "7.0.0-dev.20260309.1",
|
|
65
|
+
"@vitest/coverage-v8": "4.0.18",
|
|
65
66
|
"@web-std/file": "3.0.3",
|
|
66
|
-
"happy-dom": "20.
|
|
67
|
-
"jsdom": "
|
|
68
|
-
"tsdown": "0.
|
|
67
|
+
"happy-dom": "20.8.3",
|
|
68
|
+
"jsdom": "28.1.0",
|
|
69
|
+
"tsdown": "0.21.1",
|
|
69
70
|
"typescript": "5.9.3",
|
|
70
|
-
"vitest": "4.0.
|
|
71
|
+
"vitest": "4.0.18"
|
|
71
72
|
}
|
|
72
73
|
}
|