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 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 20-25.
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
- }?: StorageEstimate) => StorageManager;
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
- return "granted";
27
- },
28
- requestPermission: async () => {
29
- return "granted";
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
- return {
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) files.set(fileName, fileSystemFileHandleFactory(fileName, {
227
- content: new Uint8Array(),
228
- lastModified: Date.now(),
229
- id: Symbol("file")
230
- }, () => files.has(fileName)));
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
- const dir = fileSystemDirectoryHandleFactory(dirName);
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
- yield* getJoinedMaps().entries();
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
- yield* getJoinedMaps().values();
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.5.1",
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": "tsc --noEmit",
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.3.4",
64
- "@vitest/coverage-v8": "4.0.7",
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.10",
67
- "jsdom": "27.1.0",
68
- "tsdown": "0.16.0",
69
- "typescript": "5.9.3",
70
- "vitest": "4.0.7"
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
  }