opfs-mock 2.5.0 → 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 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,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.5.0",
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>",
@@ -26,14 +26,18 @@
26
26
  "publishConfig": {
27
27
  "access": "public"
28
28
  },
29
+ "module": "./dist/index.mjs",
30
+ "types": "./dist/index.d.mts",
29
31
  "exports": {
30
- ".": {
31
- "types": "./dist/index.d.ts",
32
- "import": "./dist/index.js"
32
+ "module-sync": {
33
+ "types": "./dist/index.d.mts",
34
+ "default": "./dist/index.mjs"
35
+ },
36
+ "import": {
37
+ "types": "./dist/index.d.mts",
38
+ "default": "./dist/index.mjs"
33
39
  }
34
40
  },
35
- "main": "dist/index.js",
36
- "module": "dist/index.js",
37
41
  "files": [
38
42
  "dist"
39
43
  ],
@@ -47,7 +51,7 @@
47
51
  "lint": "npx @biomejs/biome lint --fix",
48
52
  "format:check": "npx @biomejs/biome format",
49
53
  "format": "npx @biomejs/biome format --write",
50
- "type-check": "tsc --noEmit",
54
+ "type-check": "tsgo",
51
55
  "test": "npm run test:node && npm run test:happy-dom",
52
56
  "test:node": "vitest --environment=node",
53
57
  "test:jsdom": "vitest --environment=jsdom",
@@ -56,13 +60,14 @@
56
60
  "release": "npm publish --access public"
57
61
  },
58
62
  "devDependencies": {
59
- "@biomejs/biome": "2.3.4",
60
- "@vitest/coverage-v8": "4.0.7",
63
+ "@biomejs/biome": "2.4.6",
64
+ "@typescript/native-preview": "7.0.0-dev.20260309.1",
65
+ "@vitest/coverage-v8": "4.0.18",
61
66
  "@web-std/file": "3.0.3",
62
- "happy-dom": "20.0.10",
63
- "jsdom": "27.1.0",
64
- "tsdown": "0.16.0",
67
+ "happy-dom": "20.8.3",
68
+ "jsdom": "28.1.0",
69
+ "tsdown": "0.21.1",
65
70
  "typescript": "5.9.3",
66
- "vitest": "4.0.7"
71
+ "vitest": "4.0.18"
67
72
  }
68
73
  }