@unshared/fs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. package/LICENSE.md +21 -0
  2. package/dist/createTemporaryDirectory.cjs +11 -0
  3. package/dist/createTemporaryDirectory.cjs.map +1 -0
  4. package/dist/createTemporaryDirectory.d.ts +35 -0
  5. package/dist/createTemporaryDirectory.js +14 -0
  6. package/dist/createTemporaryDirectory.js.map +1 -0
  7. package/dist/createTemporaryFile.cjs +12 -0
  8. package/dist/createTemporaryFile.cjs.map +1 -0
  9. package/dist/createTemporaryFile.d.ts +44 -0
  10. package/dist/createTemporaryFile.js +15 -0
  11. package/dist/createTemporaryFile.js.map +1 -0
  12. package/dist/findAncestor.cjs +16 -0
  13. package/dist/findAncestor.cjs.map +1 -0
  14. package/dist/findAncestor.d.ts +18 -0
  15. package/dist/findAncestor.js +19 -0
  16. package/dist/findAncestor.js.map +1 -0
  17. package/dist/findAncestors.cjs +20 -0
  18. package/dist/findAncestors.cjs.map +1 -0
  19. package/dist/findAncestors.d.ts +21 -0
  20. package/dist/findAncestors.js +24 -0
  21. package/dist/findAncestors.js.map +1 -0
  22. package/dist/glob.cjs +28 -0
  23. package/dist/glob.cjs.map +1 -0
  24. package/dist/glob.d.ts +71 -0
  25. package/dist/glob.js +33 -0
  26. package/dist/glob.js.map +1 -0
  27. package/dist/index.cjs +25 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.ts +16 -0
  30. package/dist/index.js +35 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/loadObject.cjs +187 -0
  33. package/dist/loadObject.cjs.map +1 -0
  34. package/dist/loadObject.d.ts +222 -0
  35. package/dist/loadObject.js +195 -0
  36. package/dist/loadObject.js.map +1 -0
  37. package/dist/touch.cjs +15 -0
  38. package/dist/touch.cjs.map +1 -0
  39. package/dist/touch.d.ts +36 -0
  40. package/dist/touch.js +17 -0
  41. package/dist/touch.js.map +1 -0
  42. package/dist/updateFile.cjs +8 -0
  43. package/dist/updateFile.cjs.map +1 -0
  44. package/dist/updateFile.d.ts +32 -0
  45. package/dist/updateFile.js +9 -0
  46. package/dist/updateFile.js.map +1 -0
  47. package/dist/withTemporaryDirectories.cjs +17 -0
  48. package/dist/withTemporaryDirectories.cjs.map +1 -0
  49. package/dist/withTemporaryDirectories.d.ts +18 -0
  50. package/dist/withTemporaryDirectories.js +18 -0
  51. package/dist/withTemporaryDirectories.js.map +1 -0
  52. package/dist/withTemporaryFiles.cjs +17 -0
  53. package/dist/withTemporaryFiles.cjs.map +1 -0
  54. package/dist/withTemporaryFiles.d.ts +19 -0
  55. package/dist/withTemporaryFiles.js +18 -0
  56. package/dist/withTemporaryFiles.js.map +1 -0
  57. package/package.json +90 -0
@@ -0,0 +1,195 @@
1
+ import { dirname } from "node:path";
2
+ import { mkdir, writeFile, stat, rm, access, readFile } from "node:fs/promises";
3
+ import { constants, watch } from "node:fs";
4
+ import { EventEmitter } from "node:events";
5
+ import { reactive } from "@unshared/reactivity/reactive";
6
+ import { garbageCollected } from "@unshared/functions/garbageCollected";
7
+ import { awaitable } from "@unshared/functions/awaitable";
8
+ import { overwrite } from "@unshared/collection/overwrite";
9
+ class FSObject extends EventEmitter {
10
+ /**
11
+ * Load a JSON file and keep it synchronized with it's source file.
12
+ *
13
+ * @param path The path or file descriptor of the file to load.
14
+ * @param options Options for the watcher.
15
+ * @throws If the file is not a JSON object.
16
+ */
17
+ constructor(path, options = {}) {
18
+ super(), this.path = path, this.options = options;
19
+ const callback = async () => {
20
+ this.isBusy || this.options.ignoreObjectChanges || await this.commit();
21
+ };
22
+ this.object = reactive(this.options.initialValue ?? {}, {
23
+ callbacks: [callback],
24
+ deep: !0,
25
+ hooks: ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"],
26
+ ...this.options
27
+ }), garbageCollected(this).then(() => this.destroy());
28
+ }
29
+ /** Flag to signal the file is synchronized with the object. */
30
+ isCommitting = !1;
31
+ /** Flag to signal the instance has been destroyed. */
32
+ isDestroyed = !1;
33
+ /** Flag to signal the object is synchronized with the file. */
34
+ isLoading = !1;
35
+ /** The current content of the file. */
36
+ object;
37
+ /** The current status of the file. */
38
+ stats;
39
+ /** A watcher that will update the object when the file changes. */
40
+ watcher;
41
+ /**
42
+ * Create an awaitable instance of `FSObject` that resolves when the file
43
+ * is synchronized with the object and the object is synchronized with the file.
44
+ *
45
+ * This function is a shorthand for creating a new `FSObject` instance and
46
+ * calling the `access`, `load` and `watch` methods in sequence. This allows
47
+ * fast and easy access to the file and object in a single call.
48
+ *
49
+ * @param path The path or file descriptor of the file to load.
50
+ * @param options Options to pass to the `FSObject` constructor.
51
+ * @returns An awaitable instance of `FSObject`.
52
+ * @example
53
+ * const fsObject = FSObject.from('file.json')
54
+ * const object = await fsObject
55
+ *
56
+ * // Change the file and check the object.
57
+ * writeFileSync('file.json', '{"foo":"bar"}')
58
+ * await fsObject.untilLoaded
59
+ * object // => { foo: 'bar' }
60
+ *
61
+ * // Change the object and check the file.
62
+ * object.foo = 'baz'
63
+ * await fsObject.untilCommitted
64
+ * readFileSync('file.json', 'utf8') // => { "foo": "baz" }
65
+ */
66
+ static from(path, options = {}) {
67
+ const fsObject = new FSObject(path, options);
68
+ return awaitable(fsObject, () => fsObject.load().then(() => fsObject.watch().object));
69
+ }
70
+ /**
71
+ * Commit the current state of the object to the file. This function
72
+ * **will** write the object to the file and emit a `commit` event.
73
+ *
74
+ * @param writeObject The object to write to the file.
75
+ * @returns A promise that resolves when the file has been written.
76
+ */
77
+ async commit(writeObject = this.object) {
78
+ this.isCommitting = !0;
79
+ const { serialize = (object) => JSON.stringify(object, void 0, 2) } = this.options, writeJson = serialize(writeObject), pathString = this.path.toString(), pathDirectory = dirname(pathString);
80
+ await mkdir(pathDirectory, { recursive: !0 }), await writeFile(this.path, `${writeJson}
81
+ `, "utf8"), overwrite(this.object, writeObject), this.stats = await stat(this.path), this.emit("commit", writeObject), this.isCommitting = !1;
82
+ }
83
+ /**
84
+ * Close the file and stop watching the file and object for changes.
85
+ * If the file has been created as a temporary file, it will be deleted.
86
+ */
87
+ async destroy() {
88
+ this.isLoading = !1, this.isCommitting = !1, this.watcher && this.watcher.close(), this.options.deleteOnDestroy && await rm(this.path, { force: !0 }), this.watcher = void 0, this.isDestroyed = !0, this.emit("destroy");
89
+ }
90
+ /**
91
+ * Load the file and update the object.
92
+ *
93
+ * @returns The loaded object.
94
+ */
95
+ async load() {
96
+ this.isLoading = !0, this.isDestroyed = !1;
97
+ const accessError = await access(this.path, constants.F_OK | constants.R_OK).catch((error) => error);
98
+ if (accessError && this.options.createIfNotExists) {
99
+ await this.commit(), this.isLoading = !1, this.emit("load", this.object);
100
+ return;
101
+ }
102
+ if (accessError && !this.options.createIfNotExists)
103
+ throw accessError;
104
+ const newStats = await stat(this.path);
105
+ if (!newStats.isFile())
106
+ throw new Error(`Expected ${this.path.toString()} to be a file`);
107
+ if (this.object && this.stats && newStats.mtimeMs < this.stats.mtimeMs)
108
+ return;
109
+ this.stats = newStats;
110
+ const { parse = JSON.parse } = this.options, newJson = await readFile(this.path, "utf8"), newObject = parse(newJson);
111
+ if (typeof newObject != "object" || newObject === null)
112
+ throw new Error(`Expected ${this.path.toString()} to be a JSON object`);
113
+ overwrite(this.object, newObject), this.isLoading = !1, this.emit("load", newObject);
114
+ }
115
+ /**
116
+ * Start watching the file for changes and update the object if the content
117
+ * of the file changes.
118
+ *
119
+ * @returns The current instance for chaining.
120
+ * @example
121
+ * const object = new FSObject('file.json').watch()
122
+ *
123
+ * // Change the file and check the object.
124
+ * writeFileSync('file.json', '{"foo":"bar"}')
125
+ *
126
+ * // Wait until the object is updated.
127
+ * await object.untilLoaded
128
+ *
129
+ * // Check the object.
130
+ * expect(object.object).toStrictEqual({ foo: 'bar' })
131
+ */
132
+ watch() {
133
+ return this.watcher ? this : (this.watcher = watch(this.path, { persistent: !1, ...this.options }, (event) => {
134
+ this.isBusy || this.options.ignoreFileChanges || event === "change" && this.load();
135
+ }), this);
136
+ }
137
+ /**
138
+ * Flag to signal the instance is busy doing a commit or a load operation.
139
+ *
140
+ * @returns `true` if the instance is busy, `false` otherwise.
141
+ */
142
+ get isBusy() {
143
+ return this.isLoading || this.isCommitting || this.isDestroyed;
144
+ }
145
+ /**
146
+ * A promise that resolves when the file is synchronized with the object.
147
+ *
148
+ * @returns A promise that resolves when the file is synchronized.
149
+ * @example
150
+ * const object = new FSObject('file.json')
151
+ * object.commit()
152
+ *
153
+ * // Wait until the file is synchronized.
154
+ * await object.untilCommitted
155
+ */
156
+ get untilCommitted() {
157
+ return this.isCommitting ? new Promise((resolve) => this.prependOnceListener("commit", () => resolve())) : Promise.resolve();
158
+ }
159
+ /**
160
+ * A promise that resolves when the object is destroyed.
161
+ *
162
+ * @returns A promise that resolves when the object is destroyed.
163
+ * @example
164
+ * const object = new FSObject('file.json')
165
+ * object.destroy()
166
+ *
167
+ * // Wait until the object is destroyed.
168
+ * await object.untilDestroyed
169
+ */
170
+ get untilDestroyed() {
171
+ return this.isDestroyed ? Promise.resolve() : new Promise((resolve) => this.prependOnceListener("destroy", resolve));
172
+ }
173
+ /**
174
+ * A promise that resolves when the object is synchronized with the file.
175
+ *
176
+ * @returns A promise that resolves when the file is synchronized.
177
+ * @example
178
+ * const object = new FSObject('file.json')
179
+ * object.load()
180
+ *
181
+ * // Wait until the object is synchronized.
182
+ * await object.untilLoaded
183
+ */
184
+ get untilLoaded() {
185
+ return this.isLoading ? new Promise((resolve) => this.prependOnceListener("load", () => resolve())) : Promise.resolve();
186
+ }
187
+ }
188
+ function loadObject(path, options = {}) {
189
+ return FSObject.from(path, options);
190
+ }
191
+ export {
192
+ FSObject,
193
+ loadObject
194
+ };
195
+ //# sourceMappingURL=loadObject.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadObject.js","sources":["../loadObject.ts"],"sourcesContent":["import { dirname } from 'node:path'\nimport { access, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'\nimport { FSWatcher, PathLike, Stats, WatchOptions, constants, existsSync, readFileSync, watch, writeFileSync } from 'node:fs'\nimport { EventEmitter } from 'node:events'\nimport { Reactive, ReactiveOptions, reactive } from '@unshared/reactivity/reactive'\nimport { garbageCollected } from '@unshared/functions/garbageCollected'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\nimport { overwrite } from '@unshared/collection/overwrite'\n\nexport interface FSObjectOptions<T extends object> extends ReactiveOptions<T>, WatchOptions {\n /**\n * If set to `true` and the file does not exist, the file will be created\n * if it does not exist and the object will be initialized with an empty\n * object.\n *\n * @default false\n */\n createIfNotExists?: boolean\n /**\n * If set to `true`, the file will be deleted when the instance is destroyed.\n * Allowing you to create temporary files that will be deleted when the\n * instance is garbage collected.\n */\n deleteOnDestroy?: boolean\n /**\n * If set to `true`, changes on the file will not be reflected in the object.\n * You can use this to prevent the object from being updated when you are\n * making changes to the file.\n *\n * @default false\n */\n ignoreFileChanges?: boolean\n /**\n * If set to `true`, changes on the object will be reflected in the file.\n * You can set this to `false` if you want to make multiple changes to the\n * object without triggering multiple file updates.\n *\n * @default false\n */\n ignoreObjectChanges?: boolean\n /**\n * The initial value of the object. If the file does not exist, the object\n * will be initialized with this value.\n *\n * @default {}\n */\n initialValue?: T\n /**\n * The parser function to use when reading the file. If not set, the file\n * will be parsed as JSON using the native `JSON.parse` function.\n *\n * @default JSON.parse\n */\n parse?: (json: string) => T\n /**\n * The stringifier function to use when writing the file. If not set, the\n * object will be stringified as JSON using the native `JSON.stringify` function.\n *\n * @default JSON.stringify\n */\n serialize?: (object: T) => string\n}\n\nexport interface FSObjectEventMap<T extends object> {\n commit: [T]\n destroy: []\n load: [T]\n lock: []\n unlock: []\n}\n\n// eslint-disable-next-line unicorn/prefer-event-target\nexport class FSObject<T extends object> extends EventEmitter<FSObjectEventMap<T>> {\n /** Flag to signal the file is synchronized with the object. */\n public isCommitting = false\n\n /** Flag to signal the instance has been destroyed. */\n public isDestroyed = false\n\n /** Flag to signal the object is synchronized with the file. */\n public isLoading = false\n\n /** The current content of the file. */\n public object: Reactive<T>\n\n /** The current status of the file. */\n public stats: Stats | undefined\n\n /** A watcher that will update the object when the file changes. */\n public watcher: FSWatcher | undefined\n\n /**\n * Load a JSON file and keep it synchronized with it's source file.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options for the watcher.\n * @throws If the file is not a JSON object.\n */\n constructor(public path: PathLike, public options: FSObjectOptions<T> = {}) {\n super()\n\n // --- The callback that will be called when the object changes.\n // --- This callback is wrapped in a debounce function to prevent\n // --- multiple writes in a short period of time.\n const callback = async() => {\n if (this.isBusy) return\n if (this.options.ignoreObjectChanges) return\n await this.commit()\n }\n\n // --- Create the reactive object. Each time a nested property is\n // --- changed, the callback will be called with the new object.\n this.object = reactive(this.options.initialValue ?? {} as T, {\n callbacks: [callback],\n deep: true,\n hooks: ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'],\n ...this.options,\n })\n\n // --- Destroy the object once this instance is garbage collected.\n // --- This will also delete the file if it was created as a temporary file.\n void garbageCollected(this).then(() => this.destroy())\n }\n /**\n * Create an awaitable instance of `FSObject` that resolves when the file\n * is synchronized with the object and the object is synchronized with the file.\n *\n * This function is a shorthand for creating a new `FSObject` instance and\n * calling the `access`, `load` and `watch` methods in sequence. This allows\n * fast and easy access to the file and object in a single call.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options to pass to the `FSObject` constructor.\n * @returns An awaitable instance of `FSObject`.\n * @example\n * const fsObject = FSObject.from('file.json')\n * const object = await fsObject\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n * await fsObject.untilLoaded\n * object // => { foo: 'bar' }\n *\n * // Change the object and check the file.\n * object.foo = 'baz'\n * await fsObject.untilCommitted\n * readFileSync('file.json', 'utf8') // => { \"foo\": \"baz\" }\n */\n static from<T extends object>(path: PathLike, options: FSObjectOptions<T> = {}): Awaitable<FSObject<T>, Reactive<T>> {\n const fsObject = new FSObject<T>(path, options)\n const createPromise = () => fsObject.load().then(() => fsObject.watch().object)\n return awaitable(fsObject, createPromise)\n }\n\n /**\n * Commit the current state of the object to the file. This function\n * **will** write the object to the file and emit a `commit` event.\n *\n * @param writeObject The object to write to the file.\n * @returns A promise that resolves when the file has been written.\n */\n public async commit(writeObject = this.object as T): Promise<void> {\n this.isCommitting = true\n\n // --- Stringify the object and write it to disk.\n const { serialize = (object: unknown) => JSON.stringify(object, undefined, 2) } = this.options\n const writeJson = serialize(writeObject)\n const pathString = this.path.toString()\n const pathDirectory = dirname(pathString)\n await mkdir(pathDirectory, { recursive: true })\n await writeFile(this.path, `${writeJson}\\n`, 'utf8')\n overwrite(this.object, writeObject)\n this.stats = await stat(this.path)\n\n this.emit('commit', writeObject)\n this.isCommitting = false\n }\n\n /**\n * Close the file and stop watching the file and object for changes.\n * If the file has been created as a temporary file, it will be deleted.\n */\n public async destroy(): Promise<void> {\n this.isLoading = false\n this.isCommitting = false\n if (this.watcher) this.watcher.close()\n if (this.options.deleteOnDestroy) await rm(this.path, { force: true })\n this.watcher = undefined\n this.isDestroyed = true\n this.emit('destroy')\n }\n\n /**\n * Load the file and update the object.\n *\n * @returns The loaded object.\n */\n public async load(): Promise<void> {\n this.isLoading = true\n this.isDestroyed = false\n\n // --- If the file does not exist, and the `createIfNotExists` option is\n // --- set to `true`, create the file and initialize the object with the\n // --- `initialValue` option.\n const accessError = await access(this.path, constants.F_OK | constants.R_OK).catch((error: Error) => error)\n if (accessError && this.options.createIfNotExists) {\n await this.commit()\n this.isLoading = false\n this.emit('load', this.object)\n return\n }\n\n // --- If the file does not exist, throw an error.\n if (accessError && !this.options.createIfNotExists) throw accessError\n\n // --- Assert the path points to a file.\n const newStats = await stat(this.path)\n const newIsFile = newStats.isFile()\n if (!newIsFile) throw new Error(`Expected ${this.path.toString()} to be a file`)\n\n // --- If the file has not changed, return the current object.\n if (this.object && this.stats && newStats.mtimeMs < this.stats.mtimeMs) return\n this.stats = newStats\n\n // --- Read and parse the file.\n const { parse = JSON.parse } = this.options\n const newJson = await readFile(this.path, 'utf8')\n const newObject = parse(newJson) as T\n\n // --- Assert JSON is an object.\n if (typeof newObject !== 'object' || newObject === null)\n throw new Error(`Expected ${this.path.toString()} to be a JSON object`)\n\n // --- Update the object by overwriting it's properties.\n overwrite(this.object, newObject)\n this.isLoading = false\n this.emit('load', newObject)\n }\n\n /**\n * Start watching the file for changes and update the object if the content\n * of the file changes.\n *\n * @returns The current instance for chaining.\n * @example\n * const object = new FSObject('file.json').watch()\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n *\n * // Wait until the object is updated.\n * await object.untilLoaded\n *\n * // Check the object.\n * expect(object.object).toStrictEqual({ foo: 'bar' })\n */\n public watch(): this {\n if (this.watcher) return this\n\n // --- Try to watch the file for changes. If an error occurs, the file\n // --- is likely not accessible. In this case, just set the `isWatching`\n // --- flag to `true` and retry watching the file when it becomes accessible.\n this.watcher = watch(this.path, { persistent: false, ...this.options }, (event) => {\n if (this.isBusy) return\n if (this.options.ignoreFileChanges) return\n if (event === 'change') void this.load()\n })\n\n // --- Return the instance for chaining.\n return this\n }\n\n /**\n * Flag to signal the instance is busy doing a commit or a load operation.\n *\n * @returns `true` if the instance is busy, `false` otherwise.\n */\n get isBusy() {\n return this.isLoading || this.isCommitting || this.isDestroyed\n }\n\n /**\n * A promise that resolves when the file is synchronized with the object.\n *\n * @returns A promise that resolves when the file is synchronized.\n * @example\n * const object = new FSObject('file.json')\n * object.commit()\n *\n * // Wait until the file is synchronized.\n * await object.untilCommitted\n */\n get untilCommitted(): Promise<void> {\n if (!this.isCommitting) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('commit', () => resolve()))\n }\n\n /**\n * A promise that resolves when the object is destroyed.\n *\n * @returns A promise that resolves when the object is destroyed.\n * @example\n * const object = new FSObject('file.json')\n * object.destroy()\n *\n * // Wait until the object is destroyed.\n * await object.untilDestroyed\n */\n get untilDestroyed(): Promise<void> {\n if (this.isDestroyed) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('destroy', resolve))\n }\n\n /**\n * A promise that resolves when the object is synchronized with the file.\n *\n * @returns A promise that resolves when the file is synchronized.\n * @example\n * const object = new FSObject('file.json')\n * object.load()\n *\n * // Wait until the object is synchronized.\n * await object.untilLoaded\n */\n get untilLoaded(): Promise<void> {\n if (!this.isLoading) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('load', () => resolve()))\n }\n}\n\n/**\n * Create an awaitable instance of `FSObject` that resolves when the file\n * is synchronized with the object and the object is synchronized with the file.\n *\n * This function is a shorthand for creating a new `FSObject` instance and\n * calling the `access`, `load` and `watch` methods in sequence. This allows\n * fast and easy access to the file and object in a single call.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options to pass to the `FSObject` constructor.\n * @returns An awaitable instance of `FSObject`.\n * @example\n * const fsObject = loadObject('file.json')\n * const object = await fsObject\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n * await fsObject.untilLoaded\n * object // => { foo: 'bar' }\n *\n * // Change the object and check the file.\n * object.foo = 'baz'\n * await fsObject.untilCommitted\n * readFileSync('file.json', 'utf8') // => { \"foo\": \"baz\" }\n */\nexport function loadObject<T extends object>(path: PathLike, options: FSObjectOptions<T> = {}): Awaitable<FSObject<T>, Reactive<T>> {\n return FSObject.from(path, options)\n}\n\n/* v8 ignore start */\n/* eslint-disable sonarjs/no-duplicate-string */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n describe('loadObject', () => {\n it('should return an instance of `FSObject`', () => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = loadObject('/app/packages.json')\n expect(result).toBeInstanceOf(FSObject)\n expect(result).toBeInstanceOf(EventEmitter)\n expect(result).toHaveProperty('path', '/app/packages.json')\n expect(result).toHaveProperty('object', reactive({}))\n })\n\n it('should expose the options as properties', () => {\n const options = { initialValue: { foo: 'bar' } }\n const result = loadObject('/app/packages.json', options)\n expect(result.options).toBe(options)\n })\n\n it('should resolve the parsed JSON file', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = await loadObject('/app/packages.json')\n expect(result).toMatchObject({ foo: 'bar' })\n })\n\n it('should create the file if it does not exist and the `createIfNotExists` option is set to `true`', async() => {\n const result = await loadObject('/app/packages.json', { createIfNotExists: true })\n expect(result).toMatchObject({})\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('{}\\n')\n })\n\n it('should reject if the file is not a JSON object', async() => {\n vol.fromJSON({ 'file.json': '\"foo\": \"bar\"' })\n const shouldReject = async() => await loadObject('file.json')\n await expect(shouldReject).rejects.toThrow('Unexpected non-whitespace character after JSON at position 5')\n })\n })\n\n describe('load', () => {\n it('should load the file when the `load` method is called', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json')\n const loaded = await result.load()\n expect(loaded).toBeUndefined()\n expect(result.object).toMatchObject({ foo: 'bar' })\n })\n\n it('should set the `isLoading` flag to `true` when loading', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json')\n expect(result.isLoading).toBeFalsy()\n const loaded = result.load()\n expect(result.isLoading).toBeTruthy()\n await loaded\n expect(result.isLoading).toBeFalsy()\n })\n\n it('should call the `load` event when the file is loaded', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const fn = vi.fn()\n const result = new FSObject('/app/packages.json')\n result.addListener('load', fn)\n await result.load()\n expect(fn).toHaveBeenCalledOnce()\n expect(fn).toHaveBeenCalledWith({ foo: 'bar' })\n })\n\n it('should resolve the `untilLoaded` property once the file is loaded', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = loadObject('/app/packages.json')\n void result.load()\n expect(result.isLoading).toBeTruthy()\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBeFalsy()\n })\n\n it('should resolve the `untilLoaded` property immediately if the file is already loaded', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json')\n await result.load()\n expect(result.isLoading).toBeFalsy()\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBeFalsy()\n })\n\n it('should create the file if it does not exist and the `createIfNotExists` option is set to `true`', async() => {\n const result = new FSObject('/app/packages.json', { createIfNotExists: true })\n await result.load()\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('{}\\n')\n expect(result.object).toMatchObject({})\n })\n\n it('should create with initial value if the file does not exist and the `createIfNotExists` option is set to `true`', async() => {\n const result = new FSObject('/app/packages.json', { createIfNotExists: true, initialValue: { foo: 'bar' } })\n await result.load()\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('{\\n \"foo\": \"bar\"\\n}\\n')\n expect(result.object).toMatchObject({ foo: 'bar' })\n })\n\n it('should use the provided `parse` function to parse the file', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const parse = vi.fn((json: string) => ({ json }))\n const result = new FSObject('/app/packages.json', { parse })\n await result.load()\n expect(result.object).toMatchObject({ json: '{\"foo\":\"bar\"}' })\n expect(parse).toHaveBeenCalledOnce()\n expect(parse).toHaveBeenCalledWith('{\"foo\":\"bar\"}')\n })\n\n it('should reject if the file does not exist', async() => {\n const result = new FSObject('/app/packages.json')\n const shouldReject = () => result.load()\n await expect(shouldReject).rejects.toThrow('ENOENT')\n })\n })\n\n describe('watch', () => {\n it('should return the current instance', () => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json')\n const watch = result.watch()\n expect(watch).toBe(result)\n })\n\n it('should watch for changes on the file', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const fn = vi.fn()\n const result = new FSObject('/app/packages.json')\n result.addListener('load', fn)\n result.watch()\n writeFileSync('/app/packages.json', '{\"bar\":\"baz\"}')\n await result.untilLoaded\n expect(fn).toHaveBeenCalledOnce()\n expect(fn).toHaveBeenCalledWith({ bar: 'baz' })\n })\n\n it('should not watch for changes on the file when `ignoreFileChanges` is `true`', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const fn = vi.fn()\n const result = new FSObject('/app/packages.json', { ignoreFileChanges: true })\n result.watch()\n result.addListener('load', fn)\n writeFileSync('/app/packages.json', '{\"bar\":\"baz\"}')\n await new Promise(resolve => setTimeout(resolve, 10))\n expect(fn).not.toHaveBeenCalled()\n })\n\n it('should throw an error if the file does not exist', () => {\n const result = new FSObject('/app/packages.json')\n const shouldThrow = () => result.watch()\n expect(shouldThrow).toThrow('ENOENT')\n })\n })\n\n describe('commit', () => {\n it('should commit the object to the file when the `commit` method is called', async() => {\n const result = new FSObject('/app/packages.json')\n const commited = await result.commit()\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(commited).toBeUndefined()\n expect(fileContent).toBe('{}\\n')\n })\n\n it('should set the `isCommitting` flag to `true` when committing', () => {\n const result = new FSObject('/app/packages.json')\n expect(result.isCommitting).toBeFalsy()\n void result.commit()\n expect(result.isCommitting).toBeTruthy()\n })\n\n it('should call the `commit` event when the file is isCommitting', async() => {\n const result = new FSObject('/app/packages.json', { initialValue: { foo: 'bar' } })\n const fn = vi.fn()\n result.addListener('commit', fn)\n await result.commit()\n expect(fn).toHaveBeenCalledOnce()\n expect(fn).toHaveBeenCalledWith({ foo: 'bar' })\n })\n\n it('should commit the given object to the file', async() => {\n const result = new FSObject('/app/packages.json')\n await result.commit({ foo: 'bar' })\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('{\\n \"foo\": \"bar\"\\n}\\n')\n })\n\n it('should resolve the `untilCommitted` promise once the file is committed', async() => {\n const result = new FSObject('/app/packages.json')\n expect(result.isCommitting).toBeFalsy()\n void result.commit()\n expect(result.isCommitting).toBeTruthy()\n await expect(result.untilCommitted).resolves.toBeUndefined()\n expect(result.isCommitting).toBeFalsy()\n })\n\n it('should resolve the `untilCommitted` promise immediately if the file is already committed', async() => {\n const result = new FSObject('/app/packages.json', { initialValue: { foo: 'bar' } })\n await result.commit()\n const untilCommitted = result.untilCommitted\n await expect(untilCommitted).resolves.toBeUndefined()\n })\n\n it('should commit the object to the file when the object changes', async() => {\n const fn = vi.fn()\n const result = new FSObject('/app/packages.json', { initialValue: { foo: 'bar' } })\n result.addListener('commit', fn)\n result.object.foo = 'baz'\n await result.untilCommitted\n expect(fn).toHaveBeenCalledOnce()\n expect(fn).toHaveBeenCalledWith({ foo: 'baz' })\n })\n\n it('should use the provided `serialize` function to serialize the object', async() => {\n const serialize = vi.fn(String)\n const result = new FSObject('/app/packages.json', { initialValue: { foo: 'bar' }, serialize })\n await result.commit()\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('[object Object]\\n')\n expect(serialize).toHaveBeenCalledOnce()\n expect(serialize).toHaveBeenCalledWith({ foo: 'bar' })\n })\n\n it('should not commit the object to the file when the `ignoreObjectChanges` option is set to `true`', async() => {\n const fn = vi.fn()\n const result = new FSObject<{ foo: string }>('/app/packages.json', { ignoreObjectChanges: true })\n result.addListener('commit', fn)\n result.object.foo = 'baz'\n await new Promise(resolve => setTimeout(resolve, 10))\n expect(fn).not.toHaveBeenCalled()\n })\n })\n\n describe('destroy', () => {\n it('should set the `isDestroyed` flag to `true` when destroyed', async() => {\n const result = new FSObject('/app/packages.json')\n expect(result.isDestroyed).toBeFalsy()\n await result.destroy()\n expect(result.isDestroyed).toBeTruthy()\n })\n\n it('should emit the `destroy` event when the object is destroyed', async() => {\n const result = new FSObject('/app/packages.json')\n const fn = vi.fn()\n result.addListener('destroy', fn)\n await result.destroy()\n expect(fn).toHaveBeenCalledOnce()\n })\n\n it('should resolve the `untilDestroyed` promise when the object is destroyed', async() => {\n const result = new FSObject('/app/packages.json')\n expect(result.isDestroyed).toBeFalsy()\n const untilDestroyed = result.untilDestroyed\n void result.destroy()\n expect(result.isDestroyed).toBeTruthy()\n await expect(untilDestroyed).resolves.toBeUndefined()\n expect(result.isDestroyed).toBeTruthy()\n })\n\n it('should resolve the `untilDestroyed` promise immediately if the object is already destroyed', async() => {\n const result = new FSObject('/app/packages.json')\n await result.destroy()\n const untilDestroyed = result.untilDestroyed\n await expect(untilDestroyed).resolves.toBeUndefined()\n })\n\n it('should delete the file when the `deleteOnDestroy` option is set to `true`', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json', { deleteOnDestroy: true })\n await result.destroy()\n const fileExists = existsSync('/app/packages.json')\n expect(fileExists).toBeFalsy()\n })\n })\n}\n"],"names":[],"mappings":";;;;;;;;AAwEO,MAAM,iBAAmC,aAAkC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0BhF,YAAmB,MAAuB,UAA8B,IAAI;AACpE,aADW,KAAA,OAAA,MAAuB,KAAA,UAAA;AAMxC,UAAM,WAAW,YAAW;AACtB,WAAK,UACL,KAAK,QAAQ,uBACjB,MAAM,KAAK;IAAO;AAKpB,SAAK,SAAS,SAAS,KAAK,QAAQ,gBAAgB,IAAS;AAAA,MAC3D,WAAW,CAAC,QAAQ;AAAA,MACpB,MAAM;AAAA,MACN,OAAO,CAAC,QAAQ,OAAO,SAAS,WAAW,UAAU,QAAQ,SAAS;AAAA,MACtE,GAAG,KAAK;AAAA,IAAA,CACT,GAII,iBAAiB,IAAI,EAAE,KAAK,MAAM,KAAK,QAAA,CAAS;AAAA,EACvD;AAAA;AAAA,EAhDO,eAAe;AAAA;AAAA,EAGf,cAAc;AAAA;AAAA,EAGd,YAAY;AAAA;AAAA,EAGZ;AAAA;AAAA,EAGA;AAAA;AAAA,EAGA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA2DP,OAAO,KAAuB,MAAgB,UAA8B,IAAyC;AACnH,UAAM,WAAW,IAAI,SAAY,MAAM,OAAO;AAE9C,WAAO,UAAU,UADK,MAAM,SAAS,KAAK,EAAE,KAAK,MAAM,SAAS,QAAQ,MAAM,CACtC;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAa,OAAO,cAAc,KAAK,QAA4B;AACjE,SAAK,eAAe;AAGd,UAAA,EAAE,YAAY,CAAC,WAAoB,KAAK,UAAU,QAAQ,QAAW,CAAC,EAAA,IAAM,KAAK,SACjF,YAAY,UAAU,WAAW,GACjC,aAAa,KAAK,KAAK,YACvB,gBAAgB,QAAQ,UAAU;AACxC,UAAM,MAAM,eAAe,EAAE,WAAW,GAAK,CAAC,GAC9C,MAAM,UAAU,KAAK,MAAM,GAAG,SAAS;AAAA,GAAM,MAAM,GACnD,UAAU,KAAK,QAAQ,WAAW,GAClC,KAAK,QAAQ,MAAM,KAAK,KAAK,IAAI,GAEjC,KAAK,KAAK,UAAU,WAAW,GAC/B,KAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAa,UAAyB;AACpC,SAAK,YAAY,IACjB,KAAK,eAAe,IAChB,KAAK,WAAS,KAAK,QAAQ,MAAA,GAC3B,KAAK,QAAQ,mBAAiB,MAAM,GAAG,KAAK,MAAM,EAAE,OAAO,GAAM,CAAA,GACrE,KAAK,UAAU,QACf,KAAK,cAAc,IACnB,KAAK,KAAK,SAAS;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAa,OAAsB;AAC5B,SAAA,YAAY,IACjB,KAAK,cAAc;AAKnB,UAAM,cAAc,MAAM,OAAO,KAAK,MAAM,UAAU,OAAO,UAAU,IAAI,EAAE,MAAM,CAAC,UAAiB,KAAK;AACtG,QAAA,eAAe,KAAK,QAAQ,mBAAmB;AAC3C,YAAA,KAAK,UACX,KAAK,YAAY,IACjB,KAAK,KAAK,QAAQ,KAAK,MAAM;AAC7B;AAAA,IACF;AAGI,QAAA,eAAe,CAAC,KAAK,QAAQ;AAAyB,YAAA;AAG1D,UAAM,WAAW,MAAM,KAAK,KAAK,IAAI;AAEjC,QAAA,CADc,SAAS,OAAO;AAClB,YAAM,IAAI,MAAM,YAAY,KAAK,KAAK,SAAA,CAAU,eAAe;AAG/E,QAAI,KAAK,UAAU,KAAK,SAAS,SAAS,UAAU,KAAK,MAAM;AAAS;AACxE,SAAK,QAAQ;AAGb,UAAM,EAAE,QAAQ,KAAK,MAAM,IAAI,KAAK,SAC9B,UAAU,MAAM,SAAS,KAAK,MAAM,MAAM,GAC1C,YAAY,MAAM,OAAO;AAG3B,QAAA,OAAO,aAAc,YAAY,cAAc;AACjD,YAAM,IAAI,MAAM,YAAY,KAAK,KAAK,SAAA,CAAU,sBAAsB;AAG9D,cAAA,KAAK,QAAQ,SAAS,GAChC,KAAK,YAAY,IACjB,KAAK,KAAK,QAAQ,SAAS;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBO,QAAc;AACnB,WAAI,KAAK,UAAgB,QAKzB,KAAK,UAAU,MAAM,KAAK,MAAM,EAAE,YAAY,IAAO,GAAG,KAAK,QAAQ,GAAG,CAAC,UAAU;AAC7E,WAAK,UACL,KAAK,QAAQ,qBACb,UAAU,YAAe,KAAK;IAAK,CACxC,GAGM;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,SAAS;AACX,WAAO,KAAK,aAAa,KAAK,gBAAgB,KAAK;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,IAAI,iBAAgC;AAClC,WAAK,KAAK,eACH,IAAI,QAAc,aAAW,KAAK,oBAAoB,UAAU,MAAM,QAAS,CAAA,CAAC,IADxD,QAAQ,QAAQ;AAAA,EAEjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,IAAI,iBAAgC;AAClC,WAAI,KAAK,cAAoB,QAAQ,QAC9B,IAAA,IAAI,QAAc,CAAA,YAAW,KAAK,oBAAoB,WAAW,OAAO,CAAC;AAAA,EAClF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,IAAI,cAA6B;AAC/B,WAAK,KAAK,YACH,IAAI,QAAc,aAAW,KAAK,oBAAoB,QAAQ,MAAM,QAAS,CAAA,CAAC,IADzD,QAAQ,QAAQ;AAAA,EAE9C;AACF;AA2BO,SAAS,WAA6B,MAAgB,UAA8B,IAAyC;AAC3H,SAAA,SAAS,KAAK,MAAM,OAAO;AACpC;"}
package/dist/touch.cjs ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ var node_path = require("node:path"), promises = require("node:fs/promises");
3
+ async function touch(path, options = {}) {
4
+ const {
5
+ accessTime = Date.now(),
6
+ modifiedTime = Date.now()
7
+ } = options;
8
+ if (!await promises.stat(path).then(() => !0).catch(() => !1)) {
9
+ const fileDirectory = node_path.dirname(path);
10
+ await promises.mkdir(fileDirectory, { recursive: !0 }), await promises.writeFile(path, []);
11
+ }
12
+ await promises.utimes(path, accessTime, modifiedTime);
13
+ }
14
+ exports.touch = touch;
15
+ //# sourceMappingURL=touch.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"touch.cjs","sources":["../touch.ts"],"sourcesContent":["import { dirname } from 'node:path'\nimport { mkdir, stat, utimes, writeFile } from 'node:fs/promises'\nimport { TimeLike } from 'node:fs'\n\nexport interface TouchOptions {\n /**\n * The time to set as the file's last access time.\n *\n * @default Date.now()\n */\n accessTime?: TimeLike\n /**\n * The time to set as the file's last modified time.\n *\n * @default Date.now()\n */\n modifiedTime?: TimeLike\n}\n\n/**\n * Touch a file at the given path. You can optionally specify the access and modified times\n * to set on the file. If the file does not exists, an empty file and any missing parent\n * folders will be created.\n *\n * @param path The path to the file to touch.\n * @param options The access and modified times to set on the file.\n * @returns A promise that resolves when the file has been touched.\n * @example\n * // Touch a file with a specific access and modified time.\n * await touch('/foo/bar.txt', { accessTime: 1000, modifiedTime: 2000 })\n *\n * // Check the file's access and modified times.\n * const stats = await stat('/foo/bar.txt')\n * expect(stats.atimeMs).toStrictEqual(1000)\n * expect(stats.mtimeMs).toStrictEqual(2000)\n */\nexport async function touch(path: string, options: TouchOptions = {}): Promise<void> {\n const {\n accessTime = Date.now(),\n modifiedTime = Date.now(),\n } = options\n\n // --- If the path does not exist, then create it.\n const fileExists = await stat(path)\n .then(() => true)\n .catch(() => false)\n\n if (!fileExists) {\n const fileDirectory = dirname(path)\n await mkdir(fileDirectory, { recursive: true })\n await writeFile(path, [])\n }\n\n // --- Update the file's access and modified times.\n await utimes(path, accessTime, modifiedTime)\n}\n\n/* v8 ignore next */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n beforeAll(() => {\n vi.useFakeTimers()\n })\n\n test('should create a file if it does not exist', async() => {\n await touch('/foo.txt')\n const now = Date.now() * 1000\n const stats = await stat('/foo.txt')\n expect(stats.atimeMs).toStrictEqual(now)\n expect(stats.mtimeMs).toStrictEqual(now)\n })\n\n test('should create a nested file if the parent folder does not exist', async() => {\n await touch('/foo/bar.txt')\n const now = Date.now() * 1000\n const stats = await stat('/foo/bar.txt')\n expect(stats.atimeMs).toStrictEqual(now)\n expect(stats.mtimeMs).toStrictEqual(now)\n })\n\n test('should update the access and modified times of an existing file', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await touch('/foo.txt', { accessTime: 1000 })\n const stats = await stat('/foo.txt')\n expect(stats.atimeMs).toStrictEqual(1000 * 1000)\n })\n\n test('should update the modified time of an existing file', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await touch('/foo.txt', { modifiedTime: 1000 })\n const stats = await stat('/foo.txt')\n expect(stats.mtimeMs).toStrictEqual(1000 * 1000)\n })\n}\n"],"names":["stat","dirname","mkdir","writeFile","utimes"],"mappings":";;AAoCA,eAAsB,MAAM,MAAc,UAAwB,IAAmB;AAC7E,QAAA;AAAA,IACJ,aAAa,KAAK,IAAI;AAAA,IACtB,eAAe,KAAK,IAAI;AAAA,EACtB,IAAA;AAOJ,MAAI,CAJe,MAAMA,SAAAA,KAAK,IAAI,EAC/B,KAAK,MAAM,EAAI,EACf,MAAM,MAAM,EAAK,GAEH;AACT,UAAA,gBAAgBC,kBAAQ,IAAI;AAC5B,UAAAC,eAAM,eAAe,EAAE,WAAW,IAAM,GAC9C,MAAMC,SAAA,UAAU,MAAM,CAAA,CAAE;AAAA,EAC1B;AAGM,QAAAC,gBAAO,MAAM,YAAY,YAAY;AAC7C;;"}
@@ -0,0 +1,36 @@
1
+ import { TimeLike } from 'node:fs';
2
+
3
+ interface TouchOptions {
4
+ /**
5
+ * The time to set as the file's last access time.
6
+ *
7
+ * @default Date.now()
8
+ */
9
+ accessTime?: TimeLike;
10
+ /**
11
+ * The time to set as the file's last modified time.
12
+ *
13
+ * @default Date.now()
14
+ */
15
+ modifiedTime?: TimeLike;
16
+ }
17
+ /**
18
+ * Touch a file at the given path. You can optionally specify the access and modified times
19
+ * to set on the file. If the file does not exists, an empty file and any missing parent
20
+ * folders will be created.
21
+ *
22
+ * @param path The path to the file to touch.
23
+ * @param options The access and modified times to set on the file.
24
+ * @returns A promise that resolves when the file has been touched.
25
+ * @example
26
+ * // Touch a file with a specific access and modified time.
27
+ * await touch('/foo/bar.txt', { accessTime: 1000, modifiedTime: 2000 })
28
+ *
29
+ * // Check the file's access and modified times.
30
+ * const stats = await stat('/foo/bar.txt')
31
+ * expect(stats.atimeMs).toStrictEqual(1000)
32
+ * expect(stats.mtimeMs).toStrictEqual(2000)
33
+ */
34
+ declare function touch(path: string, options?: TouchOptions): Promise<void>;
35
+
36
+ export { type TouchOptions, touch };
package/dist/touch.js ADDED
@@ -0,0 +1,17 @@
1
+ import { dirname } from "node:path";
2
+ import { stat, mkdir, writeFile, utimes } from "node:fs/promises";
3
+ async function touch(path, options = {}) {
4
+ const {
5
+ accessTime = Date.now(),
6
+ modifiedTime = Date.now()
7
+ } = options;
8
+ if (!await stat(path).then(() => !0).catch(() => !1)) {
9
+ const fileDirectory = dirname(path);
10
+ await mkdir(fileDirectory, { recursive: !0 }), await writeFile(path, []);
11
+ }
12
+ await utimes(path, accessTime, modifiedTime);
13
+ }
14
+ export {
15
+ touch
16
+ };
17
+ //# sourceMappingURL=touch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"touch.js","sources":["../touch.ts"],"sourcesContent":["import { dirname } from 'node:path'\nimport { mkdir, stat, utimes, writeFile } from 'node:fs/promises'\nimport { TimeLike } from 'node:fs'\n\nexport interface TouchOptions {\n /**\n * The time to set as the file's last access time.\n *\n * @default Date.now()\n */\n accessTime?: TimeLike\n /**\n * The time to set as the file's last modified time.\n *\n * @default Date.now()\n */\n modifiedTime?: TimeLike\n}\n\n/**\n * Touch a file at the given path. You can optionally specify the access and modified times\n * to set on the file. If the file does not exists, an empty file and any missing parent\n * folders will be created.\n *\n * @param path The path to the file to touch.\n * @param options The access and modified times to set on the file.\n * @returns A promise that resolves when the file has been touched.\n * @example\n * // Touch a file with a specific access and modified time.\n * await touch('/foo/bar.txt', { accessTime: 1000, modifiedTime: 2000 })\n *\n * // Check the file's access and modified times.\n * const stats = await stat('/foo/bar.txt')\n * expect(stats.atimeMs).toStrictEqual(1000)\n * expect(stats.mtimeMs).toStrictEqual(2000)\n */\nexport async function touch(path: string, options: TouchOptions = {}): Promise<void> {\n const {\n accessTime = Date.now(),\n modifiedTime = Date.now(),\n } = options\n\n // --- If the path does not exist, then create it.\n const fileExists = await stat(path)\n .then(() => true)\n .catch(() => false)\n\n if (!fileExists) {\n const fileDirectory = dirname(path)\n await mkdir(fileDirectory, { recursive: true })\n await writeFile(path, [])\n }\n\n // --- Update the file's access and modified times.\n await utimes(path, accessTime, modifiedTime)\n}\n\n/* v8 ignore next */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n beforeAll(() => {\n vi.useFakeTimers()\n })\n\n test('should create a file if it does not exist', async() => {\n await touch('/foo.txt')\n const now = Date.now() * 1000\n const stats = await stat('/foo.txt')\n expect(stats.atimeMs).toStrictEqual(now)\n expect(stats.mtimeMs).toStrictEqual(now)\n })\n\n test('should create a nested file if the parent folder does not exist', async() => {\n await touch('/foo/bar.txt')\n const now = Date.now() * 1000\n const stats = await stat('/foo/bar.txt')\n expect(stats.atimeMs).toStrictEqual(now)\n expect(stats.mtimeMs).toStrictEqual(now)\n })\n\n test('should update the access and modified times of an existing file', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await touch('/foo.txt', { accessTime: 1000 })\n const stats = await stat('/foo.txt')\n expect(stats.atimeMs).toStrictEqual(1000 * 1000)\n })\n\n test('should update the modified time of an existing file', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await touch('/foo.txt', { modifiedTime: 1000 })\n const stats = await stat('/foo.txt')\n expect(stats.mtimeMs).toStrictEqual(1000 * 1000)\n })\n}\n"],"names":[],"mappings":";;AAoCA,eAAsB,MAAM,MAAc,UAAwB,IAAmB;AAC7E,QAAA;AAAA,IACJ,aAAa,KAAK,IAAI;AAAA,IACtB,eAAe,KAAK,IAAI;AAAA,EACtB,IAAA;AAOJ,MAAI,CAJe,MAAM,KAAK,IAAI,EAC/B,KAAK,MAAM,EAAI,EACf,MAAM,MAAM,EAAK,GAEH;AACT,UAAA,gBAAgB,QAAQ,IAAI;AAC5B,UAAA,MAAM,eAAe,EAAE,WAAW,IAAM,GAC9C,MAAM,UAAU,MAAM,CAAA,CAAE;AAAA,EAC1B;AAGM,QAAA,OAAO,MAAM,YAAY,YAAY;AAC7C;"}
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ var promises = require("node:fs/promises");
3
+ async function updateFile(path, callback, encoding) {
4
+ const fileHandle = await promises.open(path, "r+"), fileContents = await promises.readFile(fileHandle, encoding), newFileContents = await callback(fileContents);
5
+ await promises.writeFile(fileHandle, newFileContents), await fileHandle.close();
6
+ }
7
+ exports.updateFile = updateFile;
8
+ //# sourceMappingURL=updateFile.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updateFile.cjs","sources":["../updateFile.ts"],"sourcesContent":["/* eslint-disable jsdoc/check-param-names */\nimport { open, readFile, writeFile } from 'node:fs/promises'\nimport { PathLike } from 'node:fs'\nimport { MaybePromise } from '@unshared/types'\n\n/**\n * A callback that updates a file's contents.\n *\n * @template T The type of the file's contents.\n * @example UpdateFileCallback<string> // (content: string) => Promise<string> | string\n */\nexport type UpdateFileCallback<T extends Buffer | string> = (content: T) => MaybePromise<Buffer | string>\n\n/**\n * Open a file, update its contents using the provided callback, and close it. The file\n * must exist before calling this function or an error will be thrown.\n *\n * @param path The path to the file to update.\n * @param callback A callback that updates the file's contents.\n * @param encoding The encoding to use when reading the file.\n * @returns A promise that resolves when the file is updated.\n * @example\n * // Create a file.\n * await writeFile('/path/to/file.txt', 'foo')\n *\n * // Update a file's contents using a transform function.\n * await updateFile('/path/to/file.txt', toUpperCase, 'utf8')\n *\n * // Check the file's contents.\n * await readFile('/path/to/file.txt', 'utf8') // 'FOO'\n */\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<Buffer>): Promise<void>\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<string>, encoding: BufferEncoding): Promise<void>\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<any>, encoding?: BufferEncoding): Promise<void> {\n const fileHandle = await open(path, 'r+')\n const fileContents = await readFile(fileHandle, encoding)\n const newFileContents = await callback(fileContents)\n await writeFile(fileHandle, newFileContents)\n await fileHandle.close()\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n test('should update a file with a callback using buffer', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await updateFile('/foo.txt', content => content.toString('utf8').toUpperCase())\n const result = await readFile('/foo.txt', 'utf8')\n expect(result).toBe('HELLO, WORLD!')\n })\n\n test('should update a file with a callback using utf8 encoding', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await updateFile('/foo.txt', content => content.toUpperCase(), 'utf8')\n const result = await readFile('/foo.txt', 'utf8')\n expect(result).toBe('HELLO, WORLD!')\n })\n\n test('should throw an error if the file does not exist', async() => {\n const shouldThrow = updateFile('/foo.txt', content => content)\n await expect(shouldThrow).rejects.toThrow('ENOENT: no such file or directory, open')\n })\n}\n"],"names":["open","readFile","writeFile"],"mappings":";;AAiCsB,eAAA,WAAW,MAAgB,UAAmC,UAA0C;AAC5H,QAAM,aAAa,MAAMA,SAAK,KAAA,MAAM,IAAI,GAClC,eAAe,MAAMC,SAAA,SAAS,YAAY,QAAQ,GAClD,kBAAkB,MAAM,SAAS,YAAY;AACnD,QAAMC,SAAAA,UAAU,YAAY,eAAe,GAC3C,MAAM,WAAW;AACnB;;"}
@@ -0,0 +1,32 @@
1
+ import { PathLike } from 'node:fs';
2
+ import { MaybePromise } from '@unshared/types';
3
+
4
+ /**
5
+ * A callback that updates a file's contents.
6
+ *
7
+ * @template T The type of the file's contents.
8
+ * @example UpdateFileCallback<string> // (content: string) => Promise<string> | string
9
+ */
10
+ type UpdateFileCallback<T extends Buffer | string> = (content: T) => MaybePromise<Buffer | string>;
11
+ /**
12
+ * Open a file, update its contents using the provided callback, and close it. The file
13
+ * must exist before calling this function or an error will be thrown.
14
+ *
15
+ * @param path The path to the file to update.
16
+ * @param callback A callback that updates the file's contents.
17
+ * @param encoding The encoding to use when reading the file.
18
+ * @returns A promise that resolves when the file is updated.
19
+ * @example
20
+ * // Create a file.
21
+ * await writeFile('/path/to/file.txt', 'foo')
22
+ *
23
+ * // Update a file's contents using a transform function.
24
+ * await updateFile('/path/to/file.txt', toUpperCase, 'utf8')
25
+ *
26
+ * // Check the file's contents.
27
+ * await readFile('/path/to/file.txt', 'utf8') // 'FOO'
28
+ */
29
+ declare function updateFile(path: PathLike, callback: UpdateFileCallback<Buffer>): Promise<void>;
30
+ declare function updateFile(path: PathLike, callback: UpdateFileCallback<string>, encoding: BufferEncoding): Promise<void>;
31
+
32
+ export { type UpdateFileCallback, updateFile };
@@ -0,0 +1,9 @@
1
+ import { open, readFile, writeFile } from "node:fs/promises";
2
+ async function updateFile(path, callback, encoding) {
3
+ const fileHandle = await open(path, "r+"), fileContents = await readFile(fileHandle, encoding), newFileContents = await callback(fileContents);
4
+ await writeFile(fileHandle, newFileContents), await fileHandle.close();
5
+ }
6
+ export {
7
+ updateFile
8
+ };
9
+ //# sourceMappingURL=updateFile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updateFile.js","sources":["../updateFile.ts"],"sourcesContent":["/* eslint-disable jsdoc/check-param-names */\nimport { open, readFile, writeFile } from 'node:fs/promises'\nimport { PathLike } from 'node:fs'\nimport { MaybePromise } from '@unshared/types'\n\n/**\n * A callback that updates a file's contents.\n *\n * @template T The type of the file's contents.\n * @example UpdateFileCallback<string> // (content: string) => Promise<string> | string\n */\nexport type UpdateFileCallback<T extends Buffer | string> = (content: T) => MaybePromise<Buffer | string>\n\n/**\n * Open a file, update its contents using the provided callback, and close it. The file\n * must exist before calling this function or an error will be thrown.\n *\n * @param path The path to the file to update.\n * @param callback A callback that updates the file's contents.\n * @param encoding The encoding to use when reading the file.\n * @returns A promise that resolves when the file is updated.\n * @example\n * // Create a file.\n * await writeFile('/path/to/file.txt', 'foo')\n *\n * // Update a file's contents using a transform function.\n * await updateFile('/path/to/file.txt', toUpperCase, 'utf8')\n *\n * // Check the file's contents.\n * await readFile('/path/to/file.txt', 'utf8') // 'FOO'\n */\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<Buffer>): Promise<void>\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<string>, encoding: BufferEncoding): Promise<void>\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<any>, encoding?: BufferEncoding): Promise<void> {\n const fileHandle = await open(path, 'r+')\n const fileContents = await readFile(fileHandle, encoding)\n const newFileContents = await callback(fileContents)\n await writeFile(fileHandle, newFileContents)\n await fileHandle.close()\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n test('should update a file with a callback using buffer', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await updateFile('/foo.txt', content => content.toString('utf8').toUpperCase())\n const result = await readFile('/foo.txt', 'utf8')\n expect(result).toBe('HELLO, WORLD!')\n })\n\n test('should update a file with a callback using utf8 encoding', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await updateFile('/foo.txt', content => content.toUpperCase(), 'utf8')\n const result = await readFile('/foo.txt', 'utf8')\n expect(result).toBe('HELLO, WORLD!')\n })\n\n test('should throw an error if the file does not exist', async() => {\n const shouldThrow = updateFile('/foo.txt', content => content)\n await expect(shouldThrow).rejects.toThrow('ENOENT: no such file or directory, open')\n })\n}\n"],"names":[],"mappings":";AAiCsB,eAAA,WAAW,MAAgB,UAAmC,UAA0C;AAC5H,QAAM,aAAa,MAAM,KAAK,MAAM,IAAI,GAClC,eAAe,MAAM,SAAS,YAAY,QAAQ,GAClD,kBAAkB,MAAM,SAAS,YAAY;AACnD,QAAM,UAAU,YAAY,eAAe,GAC3C,MAAM,WAAW;AACnB;"}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var createTemporaryDirectory = require("./createTemporaryDirectory.cjs");
3
+ require("node:path");
4
+ require("node:os");
5
+ require("node:fs/promises");
6
+ async function withTemporaryDirectories(options, fn) {
7
+ typeof options == "number" && (options = Array.from({ length: options }, () => ({}))), Array.isArray(options) || (options = [options]);
8
+ const pathsPromises = options.map(createTemporaryDirectory.createTemporaryDirectory), pathsInstances = await Promise.all(pathsPromises), paths = pathsInstances.map((x) => x[0]);
9
+ try {
10
+ return await fn(...paths);
11
+ } finally {
12
+ const promises = pathsInstances.map((x) => x[1]());
13
+ await Promise.all(promises);
14
+ }
15
+ }
16
+ exports.withTemporaryDirectories = withTemporaryDirectories;
17
+ //# sourceMappingURL=withTemporaryDirectories.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withTemporaryDirectories.cjs","sources":["../withTemporaryDirectories.ts"],"sourcesContent":["import { Function, MaybeArray, Tuple, TupleLength } from '@unshared/types'\nimport { CreateTemporaryDirectoryOptions, createTemporaryDirectory } from './createTemporaryDirectory'\n\ntype Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U\n\n/**\n * Wrap a function that will create a temporary directory and\n * recursively remove it after the function has been executed,\n * regardless of whether the function throws an error or not.\n *\n * @param count The number of temporary directories to create.\n * @param fn The function to wrap that takes the temporary directory path(s) as arguments.\n * @returns A promise that resolves to the result of the function.\n */\nexport async function withTemporaryDirectories<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>\nexport async function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>\nexport async function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions>(option: T, fn: Callback<U, 1>): Promise<U>\nexport async function withTemporaryDirectories(options: MaybeArray<CreateTemporaryDirectoryOptions> | number, fn: Function<unknown>): Promise<unknown> {\n\n // --- Normalize the arguments.\n if (typeof options === 'number') options = Array.from({ length: options }, () => ({}))\n if (!Array.isArray(options)) options = [options]\n\n // --- Create temporary files.\n const pathsPromises = options.map(createTemporaryDirectory)\n const pathsInstances = await Promise.all(pathsPromises)\n const paths = pathsInstances.map(x => x[0])\n\n try {\n return await fn(...paths)\n }\n finally {\n const promises = pathsInstances.map(x => x[1]())\n await Promise.all(promises)\n }\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { existsSync } = await import('node:fs')\n\n test('should call a function with one temporary directory', async() => {\n await withTemporaryDirectories(1, (path) => {\n const exists = existsSync(path)\n expect(exists).toBeTruthy()\n })\n })\n\n test('should call a function with two temporary directories', async() => {\n await withTemporaryDirectories(2, (path1, path2) => {\n const exists1 = existsSync(path1)\n const exists2 = existsSync(path2)\n expect(exists1).toBeTruthy()\n expect(exists2).toBeTruthy()\n })\n })\n\n test('should remove the temporary directories after calling the function', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryDirectories(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n })\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBeFalsy()\n expect(exists2).toBeFalsy()\n })\n\n test('should remove the temporary directories even if the function throws an error', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryDirectories(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n throw new Error('Test error')\n }).catch(() => {})\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBeFalsy()\n expect(exists2).toBeFalsy()\n })\n\n test('should call a function with a temporary file in the specified directory', async() => {\n await withTemporaryDirectories({ directory: '/cache' }, (path) => {\n expect(path).toMatch(/^\\/cache\\/[\\da-z]+$/)\n })\n })\n\n test('should call a function with a temporary file with the given random function', async() => {\n await withTemporaryDirectories({ random: () => 'foo' }, (path) => {\n expect(path).toMatch(/^\\/tmp\\/foo$/)\n })\n })\n\n test('should call a function with multiple temporary files with different options', async() => {\n await withTemporaryDirectories(\n [{ directory: '/cache' }, { random: () => 'foo' }],\n (path1, path2) => {\n expect(path1).toMatch(/^\\/cache\\/[\\da-z]+$/)\n expect(path2).toMatch(/^\\/tmp\\/foo$/)\n },\n )\n })\n\n test('should return the result of the function', async() => {\n const result = await withTemporaryDirectories(1, () => 42)\n expect(result).toBe(42)\n })\n\n test('should throw an error if the function throws an error', async() => {\n const shouldReject = withTemporaryDirectories(1, () => { throw new Error('Test error') })\n await expect(shouldReject).rejects.toThrow('Test error')\n })\n}\n"],"names":["createTemporaryDirectory"],"mappings":";;;;;AAiBsB,eAAA,yBAAyB,SAA+D,IAAyC;AAGjJ,SAAO,WAAY,aAAU,UAAU,MAAM,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC,EAAE,IAChF,MAAM,QAAQ,OAAO,MAAG,UAAU,CAAC,OAAO;AAG/C,QAAM,gBAAgB,QAAQ,IAAIA,yBAAwB,wBAAA,GACpD,iBAAiB,MAAM,QAAQ,IAAI,aAAa,GAChD,QAAQ,eAAe,IAAI,CAAK,MAAA,EAAE,CAAC,CAAC;AAEtC,MAAA;AACK,WAAA,MAAM,GAAG,GAAG,KAAK;AAAA,EAAA,UAE1B;AACE,UAAM,WAAW,eAAe,IAAI,OAAK,EAAE,CAAC,GAAG;AACzC,UAAA,QAAQ,IAAI,QAAQ;AAAA,EAC5B;AACF;;"}
@@ -0,0 +1,18 @@
1
+ import { TupleLength, Tuple } from '@unshared/types';
2
+ import { CreateTemporaryDirectoryOptions } from './createTemporaryDirectory.js';
3
+
4
+ type Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U;
5
+ /**
6
+ * Wrap a function that will create a temporary directory and
7
+ * recursively remove it after the function has been executed,
8
+ * regardless of whether the function throws an error or not.
9
+ *
10
+ * @param count The number of temporary directories to create.
11
+ * @param fn The function to wrap that takes the temporary directory path(s) as arguments.
12
+ * @returns A promise that resolves to the result of the function.
13
+ */
14
+ declare function withTemporaryDirectories<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>;
15
+ declare function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>;
16
+ declare function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions>(option: T, fn: Callback<U, 1>): Promise<U>;
17
+
18
+ export { withTemporaryDirectories };
@@ -0,0 +1,18 @@
1
+ import { createTemporaryDirectory } from "./createTemporaryDirectory.js";
2
+ import "node:path";
3
+ import "node:os";
4
+ import "node:fs/promises";
5
+ async function withTemporaryDirectories(options, fn) {
6
+ typeof options == "number" && (options = Array.from({ length: options }, () => ({}))), Array.isArray(options) || (options = [options]);
7
+ const pathsPromises = options.map(createTemporaryDirectory), pathsInstances = await Promise.all(pathsPromises), paths = pathsInstances.map((x) => x[0]);
8
+ try {
9
+ return await fn(...paths);
10
+ } finally {
11
+ const promises = pathsInstances.map((x) => x[1]());
12
+ await Promise.all(promises);
13
+ }
14
+ }
15
+ export {
16
+ withTemporaryDirectories
17
+ };
18
+ //# sourceMappingURL=withTemporaryDirectories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withTemporaryDirectories.js","sources":["../withTemporaryDirectories.ts"],"sourcesContent":["import { Function, MaybeArray, Tuple, TupleLength } from '@unshared/types'\nimport { CreateTemporaryDirectoryOptions, createTemporaryDirectory } from './createTemporaryDirectory'\n\ntype Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U\n\n/**\n * Wrap a function that will create a temporary directory and\n * recursively remove it after the function has been executed,\n * regardless of whether the function throws an error or not.\n *\n * @param count The number of temporary directories to create.\n * @param fn The function to wrap that takes the temporary directory path(s) as arguments.\n * @returns A promise that resolves to the result of the function.\n */\nexport async function withTemporaryDirectories<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>\nexport async function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>\nexport async function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions>(option: T, fn: Callback<U, 1>): Promise<U>\nexport async function withTemporaryDirectories(options: MaybeArray<CreateTemporaryDirectoryOptions> | number, fn: Function<unknown>): Promise<unknown> {\n\n // --- Normalize the arguments.\n if (typeof options === 'number') options = Array.from({ length: options }, () => ({}))\n if (!Array.isArray(options)) options = [options]\n\n // --- Create temporary files.\n const pathsPromises = options.map(createTemporaryDirectory)\n const pathsInstances = await Promise.all(pathsPromises)\n const paths = pathsInstances.map(x => x[0])\n\n try {\n return await fn(...paths)\n }\n finally {\n const promises = pathsInstances.map(x => x[1]())\n await Promise.all(promises)\n }\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { existsSync } = await import('node:fs')\n\n test('should call a function with one temporary directory', async() => {\n await withTemporaryDirectories(1, (path) => {\n const exists = existsSync(path)\n expect(exists).toBeTruthy()\n })\n })\n\n test('should call a function with two temporary directories', async() => {\n await withTemporaryDirectories(2, (path1, path2) => {\n const exists1 = existsSync(path1)\n const exists2 = existsSync(path2)\n expect(exists1).toBeTruthy()\n expect(exists2).toBeTruthy()\n })\n })\n\n test('should remove the temporary directories after calling the function', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryDirectories(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n })\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBeFalsy()\n expect(exists2).toBeFalsy()\n })\n\n test('should remove the temporary directories even if the function throws an error', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryDirectories(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n throw new Error('Test error')\n }).catch(() => {})\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBeFalsy()\n expect(exists2).toBeFalsy()\n })\n\n test('should call a function with a temporary file in the specified directory', async() => {\n await withTemporaryDirectories({ directory: '/cache' }, (path) => {\n expect(path).toMatch(/^\\/cache\\/[\\da-z]+$/)\n })\n })\n\n test('should call a function with a temporary file with the given random function', async() => {\n await withTemporaryDirectories({ random: () => 'foo' }, (path) => {\n expect(path).toMatch(/^\\/tmp\\/foo$/)\n })\n })\n\n test('should call a function with multiple temporary files with different options', async() => {\n await withTemporaryDirectories(\n [{ directory: '/cache' }, { random: () => 'foo' }],\n (path1, path2) => {\n expect(path1).toMatch(/^\\/cache\\/[\\da-z]+$/)\n expect(path2).toMatch(/^\\/tmp\\/foo$/)\n },\n )\n })\n\n test('should return the result of the function', async() => {\n const result = await withTemporaryDirectories(1, () => 42)\n expect(result).toBe(42)\n })\n\n test('should throw an error if the function throws an error', async() => {\n const shouldReject = withTemporaryDirectories(1, () => { throw new Error('Test error') })\n await expect(shouldReject).rejects.toThrow('Test error')\n })\n}\n"],"names":[],"mappings":";;;;AAiBsB,eAAA,yBAAyB,SAA+D,IAAyC;AAGjJ,SAAO,WAAY,aAAU,UAAU,MAAM,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC,EAAE,IAChF,MAAM,QAAQ,OAAO,MAAG,UAAU,CAAC,OAAO;AAG/C,QAAM,gBAAgB,QAAQ,IAAI,wBAAwB,GACpD,iBAAiB,MAAM,QAAQ,IAAI,aAAa,GAChD,QAAQ,eAAe,IAAI,CAAK,MAAA,EAAE,CAAC,CAAC;AAEtC,MAAA;AACK,WAAA,MAAM,GAAG,GAAG,KAAK;AAAA,EAAA,UAE1B;AACE,UAAM,WAAW,eAAe,IAAI,OAAK,EAAE,CAAC,GAAG;AACzC,UAAA,QAAQ,IAAI,QAAQ;AAAA,EAC5B;AACF;"}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var createTemporaryFile = require("./createTemporaryFile.cjs");
3
+ require("node:path");
4
+ require("node:os");
5
+ require("node:fs/promises");
6
+ async function withTemporaryFiles(options, fn) {
7
+ typeof options == "number" && (options = Array.from({ length: options }, () => ({}))), Array.isArray(options) || (options = [options]);
8
+ const pathsPromises = options.map((option) => createTemporaryFile.createTemporaryFile(void 0, option)), pathsInstances = await Promise.all(pathsPromises), paths = pathsInstances.map((x) => x[0]);
9
+ try {
10
+ return await fn(...paths);
11
+ } finally {
12
+ const promises = pathsInstances.map((x) => x[1]());
13
+ await Promise.all(promises);
14
+ }
15
+ }
16
+ exports.withTemporaryFiles = withTemporaryFiles;
17
+ //# sourceMappingURL=withTemporaryFiles.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withTemporaryFiles.cjs","sources":["../withTemporaryFiles.ts"],"sourcesContent":["import { Function, MaybeArray, Tuple, TupleLength } from '@unshared/types'\nimport { CreateTemporaryFileOptions, createTemporaryFile } from './createTemporaryFile'\n\ntype Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U\n\n/**\n * Wrap a function that will create one or more temporary files and\n * remove them after the function has been executed, regardless of\n * whether the function throws an error or not.\n *\n * @param count The number of temporary files to create.\n * @param fn The function to wrap that takes the temporary directory path(s) as arguments.\n * @returns A promise that resolves to the result of the function.\n */\nexport async function withTemporaryFiles<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>\nexport async function withTemporaryFiles<U, T extends CreateTemporaryFileOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>\nexport async function withTemporaryFiles<U, T extends CreateTemporaryFileOptions>(option: T, fn: Callback<U, 1>): Promise<U>\nexport async function withTemporaryFiles(options: MaybeArray<CreateTemporaryFileOptions> | number, fn: Function<unknown>): Promise<unknown> {\n\n // --- Normalize the arguments.\n if (typeof options === 'number') options = Array.from({ length: options }, () => ({}))\n if (!Array.isArray(options)) options = [options]\n\n // --- Create temporary files.\n const pathsPromises = options.map(option => createTemporaryFile(undefined, option))\n const pathsInstances = await Promise.all(pathsPromises)\n const paths = pathsInstances.map(x => x[0])\n\n try {\n return await fn(...paths)\n }\n finally {\n const promises = pathsInstances.map(x => x[1]())\n await Promise.all(promises)\n }\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { existsSync } = await import('node:fs')\n\n test('should call a function with one temporary file', async() => {\n await withTemporaryFiles(1, (path) => {\n const exists = existsSync(path)\n expect(exists).toBeTruthy()\n })\n })\n\n test('should call a function with two temporary files', async() => {\n await withTemporaryFiles(2, (path1, path2) => {\n const exists1 = existsSync(path1)\n const exists2 = existsSync(path2)\n expect(exists1).toBeTruthy()\n expect(exists2).toBeTruthy()\n })\n })\n\n test('should remove the temporary files after calling the function', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryFiles(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n })\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBeFalsy()\n expect(exists2).toBeFalsy()\n })\n\n test('should remove the temporary files after the function throws an error', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryFiles(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n throw new Error('Test error')\n }).catch(() => {})\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBeFalsy()\n expect(exists2).toBeFalsy()\n })\n\n test('should call a function with a temporary file in the specified directory', async() => {\n await withTemporaryFiles({ directory: '/cache' }, (path) => {\n expect(path).toMatch(/^\\/cache\\/[\\da-z]+$/)\n })\n })\n\n test('should call a function with a temporary file with the specified extension', async() => {\n await withTemporaryFiles({ extension: 'txt' }, (path) => {\n expect(path).toMatch(/^\\/tmp\\/[\\da-z]+\\.txt$/)\n })\n })\n\n test('should call a function with a temporary file with the given random function', async() => {\n await withTemporaryFiles({ random: () => 'foo' }, (path) => {\n expect(path).toMatch(/^\\/tmp\\/foo$/)\n })\n })\n\n test('should call a function with multiple temporary files with different options', async() => {\n await withTemporaryFiles([{ directory: '/cache' }, { extension: 'txt' }, { random: () => 'foo' }], (path1, path2, path3) => {\n expect(path1).toMatch(/^\\/cache\\/[\\da-z]+$/)\n expect(path2).toMatch(/^\\/tmp\\/[\\da-z]+\\.txt$/)\n expect(path3).toMatch(/^\\/tmp\\/foo$/)\n })\n })\n\n test('should return the result of the function', async() => {\n const result = await withTemporaryFiles(1, () => 42)\n expect(result).toBe(42)\n })\n\n test('should throw an error if the function throws an error', async() => {\n const shouldReject = withTemporaryFiles(1, () => { throw new Error('Test error') })\n await expect(shouldReject).rejects.toThrow('Test error')\n })\n}\n"],"names":["createTemporaryFile"],"mappings":";;;;;AAiBsB,eAAA,mBAAmB,SAA0D,IAAyC;AAGtI,SAAO,WAAY,aAAU,UAAU,MAAM,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC,EAAE,IAChF,MAAM,QAAQ,OAAO,MAAG,UAAU,CAAC,OAAO;AAGzC,QAAA,gBAAgB,QAAQ,IAAI,CAAA,WAAUA,wCAAoB,QAAW,MAAM,CAAC,GAC5E,iBAAiB,MAAM,QAAQ,IAAI,aAAa,GAChD,QAAQ,eAAe,IAAI,CAAA,MAAK,EAAE,CAAC,CAAC;AAEtC,MAAA;AACK,WAAA,MAAM,GAAG,GAAG,KAAK;AAAA,EAAA,UAE1B;AACE,UAAM,WAAW,eAAe,IAAI,OAAK,EAAE,CAAC,GAAG;AACzC,UAAA,QAAQ,IAAI,QAAQ;AAAA,EAC5B;AACF;;"}
@@ -0,0 +1,19 @@
1
+ import { TupleLength, Tuple } from '@unshared/types';
2
+ import { CreateTemporaryFileOptions } from './createTemporaryFile.js';
3
+ import 'node:fs/promises';
4
+
5
+ type Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U;
6
+ /**
7
+ * Wrap a function that will create one or more temporary files and
8
+ * remove them after the function has been executed, regardless of
9
+ * whether the function throws an error or not.
10
+ *
11
+ * @param count The number of temporary files to create.
12
+ * @param fn The function to wrap that takes the temporary directory path(s) as arguments.
13
+ * @returns A promise that resolves to the result of the function.
14
+ */
15
+ declare function withTemporaryFiles<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>;
16
+ declare function withTemporaryFiles<U, T extends CreateTemporaryFileOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>;
17
+ declare function withTemporaryFiles<U, T extends CreateTemporaryFileOptions>(option: T, fn: Callback<U, 1>): Promise<U>;
18
+
19
+ export { withTemporaryFiles };
@@ -0,0 +1,18 @@
1
+ import { createTemporaryFile } from "./createTemporaryFile.js";
2
+ import "node:path";
3
+ import "node:os";
4
+ import "node:fs/promises";
5
+ async function withTemporaryFiles(options, fn) {
6
+ typeof options == "number" && (options = Array.from({ length: options }, () => ({}))), Array.isArray(options) || (options = [options]);
7
+ const pathsPromises = options.map((option) => createTemporaryFile(void 0, option)), pathsInstances = await Promise.all(pathsPromises), paths = pathsInstances.map((x) => x[0]);
8
+ try {
9
+ return await fn(...paths);
10
+ } finally {
11
+ const promises = pathsInstances.map((x) => x[1]());
12
+ await Promise.all(promises);
13
+ }
14
+ }
15
+ export {
16
+ withTemporaryFiles
17
+ };
18
+ //# sourceMappingURL=withTemporaryFiles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withTemporaryFiles.js","sources":["../withTemporaryFiles.ts"],"sourcesContent":["import { Function, MaybeArray, Tuple, TupleLength } from '@unshared/types'\nimport { CreateTemporaryFileOptions, createTemporaryFile } from './createTemporaryFile'\n\ntype Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U\n\n/**\n * Wrap a function that will create one or more temporary files and\n * remove them after the function has been executed, regardless of\n * whether the function throws an error or not.\n *\n * @param count The number of temporary files to create.\n * @param fn The function to wrap that takes the temporary directory path(s) as arguments.\n * @returns A promise that resolves to the result of the function.\n */\nexport async function withTemporaryFiles<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>\nexport async function withTemporaryFiles<U, T extends CreateTemporaryFileOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>\nexport async function withTemporaryFiles<U, T extends CreateTemporaryFileOptions>(option: T, fn: Callback<U, 1>): Promise<U>\nexport async function withTemporaryFiles(options: MaybeArray<CreateTemporaryFileOptions> | number, fn: Function<unknown>): Promise<unknown> {\n\n // --- Normalize the arguments.\n if (typeof options === 'number') options = Array.from({ length: options }, () => ({}))\n if (!Array.isArray(options)) options = [options]\n\n // --- Create temporary files.\n const pathsPromises = options.map(option => createTemporaryFile(undefined, option))\n const pathsInstances = await Promise.all(pathsPromises)\n const paths = pathsInstances.map(x => x[0])\n\n try {\n return await fn(...paths)\n }\n finally {\n const promises = pathsInstances.map(x => x[1]())\n await Promise.all(promises)\n }\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { existsSync } = await import('node:fs')\n\n test('should call a function with one temporary file', async() => {\n await withTemporaryFiles(1, (path) => {\n const exists = existsSync(path)\n expect(exists).toBeTruthy()\n })\n })\n\n test('should call a function with two temporary files', async() => {\n await withTemporaryFiles(2, (path1, path2) => {\n const exists1 = existsSync(path1)\n const exists2 = existsSync(path2)\n expect(exists1).toBeTruthy()\n expect(exists2).toBeTruthy()\n })\n })\n\n test('should remove the temporary files after calling the function', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryFiles(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n })\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBeFalsy()\n expect(exists2).toBeFalsy()\n })\n\n test('should remove the temporary files after the function throws an error', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryFiles(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n throw new Error('Test error')\n }).catch(() => {})\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBeFalsy()\n expect(exists2).toBeFalsy()\n })\n\n test('should call a function with a temporary file in the specified directory', async() => {\n await withTemporaryFiles({ directory: '/cache' }, (path) => {\n expect(path).toMatch(/^\\/cache\\/[\\da-z]+$/)\n })\n })\n\n test('should call a function with a temporary file with the specified extension', async() => {\n await withTemporaryFiles({ extension: 'txt' }, (path) => {\n expect(path).toMatch(/^\\/tmp\\/[\\da-z]+\\.txt$/)\n })\n })\n\n test('should call a function with a temporary file with the given random function', async() => {\n await withTemporaryFiles({ random: () => 'foo' }, (path) => {\n expect(path).toMatch(/^\\/tmp\\/foo$/)\n })\n })\n\n test('should call a function with multiple temporary files with different options', async() => {\n await withTemporaryFiles([{ directory: '/cache' }, { extension: 'txt' }, { random: () => 'foo' }], (path1, path2, path3) => {\n expect(path1).toMatch(/^\\/cache\\/[\\da-z]+$/)\n expect(path2).toMatch(/^\\/tmp\\/[\\da-z]+\\.txt$/)\n expect(path3).toMatch(/^\\/tmp\\/foo$/)\n })\n })\n\n test('should return the result of the function', async() => {\n const result = await withTemporaryFiles(1, () => 42)\n expect(result).toBe(42)\n })\n\n test('should throw an error if the function throws an error', async() => {\n const shouldReject = withTemporaryFiles(1, () => { throw new Error('Test error') })\n await expect(shouldReject).rejects.toThrow('Test error')\n })\n}\n"],"names":[],"mappings":";;;;AAiBsB,eAAA,mBAAmB,SAA0D,IAAyC;AAGtI,SAAO,WAAY,aAAU,UAAU,MAAM,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC,EAAE,IAChF,MAAM,QAAQ,OAAO,MAAG,UAAU,CAAC,OAAO;AAGzC,QAAA,gBAAgB,QAAQ,IAAI,CAAA,WAAU,oBAAoB,QAAW,MAAM,CAAC,GAC5E,iBAAiB,MAAM,QAAQ,IAAI,aAAa,GAChD,QAAQ,eAAe,IAAI,CAAA,MAAK,EAAE,CAAC,CAAC;AAEtC,MAAA;AACK,WAAA,MAAM,GAAG,GAAG,KAAK;AAAA,EAAA,UAE1B;AACE,UAAM,WAAW,eAAe,IAAI,OAAK,EAAE,CAAC,GAAG;AACzC,UAAA,QAAQ,IAAI,QAAQ;AAAA,EAC5B;AACF;"}