@unshared/fs 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- package/LICENSE.md +21 -0
- package/dist/createTemporaryDirectory.cjs +11 -0
- package/dist/createTemporaryDirectory.cjs.map +1 -0
- package/dist/createTemporaryDirectory.d.ts +35 -0
- package/dist/createTemporaryDirectory.js +14 -0
- package/dist/createTemporaryDirectory.js.map +1 -0
- package/dist/createTemporaryFile.cjs +12 -0
- package/dist/createTemporaryFile.cjs.map +1 -0
- package/dist/createTemporaryFile.d.ts +44 -0
- package/dist/createTemporaryFile.js +15 -0
- package/dist/createTemporaryFile.js.map +1 -0
- package/dist/findAncestor.cjs +16 -0
- package/dist/findAncestor.cjs.map +1 -0
- package/dist/findAncestor.d.ts +18 -0
- package/dist/findAncestor.js +19 -0
- package/dist/findAncestor.js.map +1 -0
- package/dist/findAncestors.cjs +20 -0
- package/dist/findAncestors.cjs.map +1 -0
- package/dist/findAncestors.d.ts +21 -0
- package/dist/findAncestors.js +24 -0
- package/dist/findAncestors.js.map +1 -0
- package/dist/glob.cjs +28 -0
- package/dist/glob.cjs.map +1 -0
- package/dist/glob.d.ts +71 -0
- package/dist/glob.js +33 -0
- package/dist/glob.js.map +1 -0
- package/dist/index.cjs +25 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/loadObject.cjs +187 -0
- package/dist/loadObject.cjs.map +1 -0
- package/dist/loadObject.d.ts +222 -0
- package/dist/loadObject.js +195 -0
- package/dist/loadObject.js.map +1 -0
- package/dist/touch.cjs +15 -0
- package/dist/touch.cjs.map +1 -0
- package/dist/touch.d.ts +36 -0
- package/dist/touch.js +17 -0
- package/dist/touch.js.map +1 -0
- package/dist/updateFile.cjs +8 -0
- package/dist/updateFile.cjs.map +1 -0
- package/dist/updateFile.d.ts +32 -0
- package/dist/updateFile.js +9 -0
- package/dist/updateFile.js.map +1 -0
- package/dist/withTemporaryDirectories.cjs +17 -0
- package/dist/withTemporaryDirectories.cjs.map +1 -0
- package/dist/withTemporaryDirectories.d.ts +18 -0
- package/dist/withTemporaryDirectories.js +18 -0
- package/dist/withTemporaryDirectories.js.map +1 -0
- package/dist/withTemporaryFiles.cjs +17 -0
- package/dist/withTemporaryFiles.cjs.map +1 -0
- package/dist/withTemporaryFiles.d.ts +19 -0
- package/dist/withTemporaryFiles.js +18 -0
- package/dist/withTemporaryFiles.js.map +1 -0
- package/package.json +90 -0
package/dist/index.js
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
import { createTemporaryDirectory } from "./createTemporaryDirectory.js";
|
2
|
+
import { createTemporaryFile } from "./createTemporaryFile.js";
|
3
|
+
import { findAncestor } from "./findAncestor.js";
|
4
|
+
import { findAncestors } from "./findAncestors.js";
|
5
|
+
import { glob } from "./glob.js";
|
6
|
+
import { FSObject, loadObject } from "./loadObject.js";
|
7
|
+
import { touch } from "./touch.js";
|
8
|
+
import { updateFile } from "./updateFile.js";
|
9
|
+
import { withTemporaryDirectories } from "./withTemporaryDirectories.js";
|
10
|
+
import { withTemporaryFiles } from "./withTemporaryFiles.js";
|
11
|
+
import "node:path";
|
12
|
+
import "node:os";
|
13
|
+
import "node:fs/promises";
|
14
|
+
import "node:process";
|
15
|
+
import "@unshared/functions/awaitable";
|
16
|
+
import "@unshared/string/createPattern";
|
17
|
+
import "node:fs";
|
18
|
+
import "node:events";
|
19
|
+
import "@unshared/reactivity/reactive";
|
20
|
+
import "@unshared/functions/garbageCollected";
|
21
|
+
import "@unshared/collection/overwrite";
|
22
|
+
export {
|
23
|
+
FSObject,
|
24
|
+
createTemporaryDirectory,
|
25
|
+
createTemporaryFile,
|
26
|
+
findAncestor,
|
27
|
+
findAncestors,
|
28
|
+
glob,
|
29
|
+
loadObject,
|
30
|
+
touch,
|
31
|
+
updateFile,
|
32
|
+
withTemporaryDirectories,
|
33
|
+
withTemporaryFiles
|
34
|
+
};
|
35
|
+
//# sourceMappingURL=index.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;"}
|
@@ -0,0 +1,187 @@
|
|
1
|
+
"use strict";
|
2
|
+
var node_path = require("node:path"), promises = require("node:fs/promises"), node_fs = require("node:fs"), node_events = require("node:events"), reactive = require("@unshared/reactivity/reactive"), garbageCollected = require("@unshared/functions/garbageCollected"), awaitable = require("@unshared/functions/awaitable"), overwrite = require("@unshared/collection/overwrite");
|
3
|
+
class FSObject extends node_events.EventEmitter {
|
4
|
+
/**
|
5
|
+
* Load a JSON file and keep it synchronized with it's source file.
|
6
|
+
*
|
7
|
+
* @param path The path or file descriptor of the file to load.
|
8
|
+
* @param options Options for the watcher.
|
9
|
+
* @throws If the file is not a JSON object.
|
10
|
+
*/
|
11
|
+
constructor(path, options = {}) {
|
12
|
+
super(), this.path = path, this.options = options;
|
13
|
+
const callback = async () => {
|
14
|
+
this.isBusy || this.options.ignoreObjectChanges || await this.commit();
|
15
|
+
};
|
16
|
+
this.object = reactive.reactive(this.options.initialValue ?? {}, {
|
17
|
+
callbacks: [callback],
|
18
|
+
deep: !0,
|
19
|
+
hooks: ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"],
|
20
|
+
...this.options
|
21
|
+
}), garbageCollected.garbageCollected(this).then(() => this.destroy());
|
22
|
+
}
|
23
|
+
/** Flag to signal the file is synchronized with the object. */
|
24
|
+
isCommitting = !1;
|
25
|
+
/** Flag to signal the instance has been destroyed. */
|
26
|
+
isDestroyed = !1;
|
27
|
+
/** Flag to signal the object is synchronized with the file. */
|
28
|
+
isLoading = !1;
|
29
|
+
/** The current content of the file. */
|
30
|
+
object;
|
31
|
+
/** The current status of the file. */
|
32
|
+
stats;
|
33
|
+
/** A watcher that will update the object when the file changes. */
|
34
|
+
watcher;
|
35
|
+
/**
|
36
|
+
* Create an awaitable instance of `FSObject` that resolves when the file
|
37
|
+
* is synchronized with the object and the object is synchronized with the file.
|
38
|
+
*
|
39
|
+
* This function is a shorthand for creating a new `FSObject` instance and
|
40
|
+
* calling the `access`, `load` and `watch` methods in sequence. This allows
|
41
|
+
* fast and easy access to the file and object in a single call.
|
42
|
+
*
|
43
|
+
* @param path The path or file descriptor of the file to load.
|
44
|
+
* @param options Options to pass to the `FSObject` constructor.
|
45
|
+
* @returns An awaitable instance of `FSObject`.
|
46
|
+
* @example
|
47
|
+
* const fsObject = FSObject.from('file.json')
|
48
|
+
* const object = await fsObject
|
49
|
+
*
|
50
|
+
* // Change the file and check the object.
|
51
|
+
* writeFileSync('file.json', '{"foo":"bar"}')
|
52
|
+
* await fsObject.untilLoaded
|
53
|
+
* object // => { foo: 'bar' }
|
54
|
+
*
|
55
|
+
* // Change the object and check the file.
|
56
|
+
* object.foo = 'baz'
|
57
|
+
* await fsObject.untilCommitted
|
58
|
+
* readFileSync('file.json', 'utf8') // => { "foo": "baz" }
|
59
|
+
*/
|
60
|
+
static from(path, options = {}) {
|
61
|
+
const fsObject = new FSObject(path, options);
|
62
|
+
return awaitable.awaitable(fsObject, () => fsObject.load().then(() => fsObject.watch().object));
|
63
|
+
}
|
64
|
+
/**
|
65
|
+
* Commit the current state of the object to the file. This function
|
66
|
+
* **will** write the object to the file and emit a `commit` event.
|
67
|
+
*
|
68
|
+
* @param writeObject The object to write to the file.
|
69
|
+
* @returns A promise that resolves when the file has been written.
|
70
|
+
*/
|
71
|
+
async commit(writeObject = this.object) {
|
72
|
+
this.isCommitting = !0;
|
73
|
+
const { serialize = (object) => JSON.stringify(object, void 0, 2) } = this.options, writeJson = serialize(writeObject), pathString = this.path.toString(), pathDirectory = node_path.dirname(pathString);
|
74
|
+
await promises.mkdir(pathDirectory, { recursive: !0 }), await promises.writeFile(this.path, `${writeJson}
|
75
|
+
`, "utf8"), overwrite.overwrite(this.object, writeObject), this.stats = await promises.stat(this.path), this.emit("commit", writeObject), this.isCommitting = !1;
|
76
|
+
}
|
77
|
+
/**
|
78
|
+
* Close the file and stop watching the file and object for changes.
|
79
|
+
* If the file has been created as a temporary file, it will be deleted.
|
80
|
+
*/
|
81
|
+
async destroy() {
|
82
|
+
this.isLoading = !1, this.isCommitting = !1, this.watcher && this.watcher.close(), this.options.deleteOnDestroy && await promises.rm(this.path, { force: !0 }), this.watcher = void 0, this.isDestroyed = !0, this.emit("destroy");
|
83
|
+
}
|
84
|
+
/**
|
85
|
+
* Load the file and update the object.
|
86
|
+
*
|
87
|
+
* @returns The loaded object.
|
88
|
+
*/
|
89
|
+
async load() {
|
90
|
+
this.isLoading = !0, this.isDestroyed = !1;
|
91
|
+
const accessError = await promises.access(this.path, node_fs.constants.F_OK | node_fs.constants.R_OK).catch((error) => error);
|
92
|
+
if (accessError && this.options.createIfNotExists) {
|
93
|
+
await this.commit(), this.isLoading = !1, this.emit("load", this.object);
|
94
|
+
return;
|
95
|
+
}
|
96
|
+
if (accessError && !this.options.createIfNotExists)
|
97
|
+
throw accessError;
|
98
|
+
const newStats = await promises.stat(this.path);
|
99
|
+
if (!newStats.isFile())
|
100
|
+
throw new Error(`Expected ${this.path.toString()} to be a file`);
|
101
|
+
if (this.object && this.stats && newStats.mtimeMs < this.stats.mtimeMs)
|
102
|
+
return;
|
103
|
+
this.stats = newStats;
|
104
|
+
const { parse = JSON.parse } = this.options, newJson = await promises.readFile(this.path, "utf8"), newObject = parse(newJson);
|
105
|
+
if (typeof newObject != "object" || newObject === null)
|
106
|
+
throw new Error(`Expected ${this.path.toString()} to be a JSON object`);
|
107
|
+
overwrite.overwrite(this.object, newObject), this.isLoading = !1, this.emit("load", newObject);
|
108
|
+
}
|
109
|
+
/**
|
110
|
+
* Start watching the file for changes and update the object if the content
|
111
|
+
* of the file changes.
|
112
|
+
*
|
113
|
+
* @returns The current instance for chaining.
|
114
|
+
* @example
|
115
|
+
* const object = new FSObject('file.json').watch()
|
116
|
+
*
|
117
|
+
* // Change the file and check the object.
|
118
|
+
* writeFileSync('file.json', '{"foo":"bar"}')
|
119
|
+
*
|
120
|
+
* // Wait until the object is updated.
|
121
|
+
* await object.untilLoaded
|
122
|
+
*
|
123
|
+
* // Check the object.
|
124
|
+
* expect(object.object).toStrictEqual({ foo: 'bar' })
|
125
|
+
*/
|
126
|
+
watch() {
|
127
|
+
return this.watcher ? this : (this.watcher = node_fs.watch(this.path, { persistent: !1, ...this.options }, (event) => {
|
128
|
+
this.isBusy || this.options.ignoreFileChanges || event === "change" && this.load();
|
129
|
+
}), this);
|
130
|
+
}
|
131
|
+
/**
|
132
|
+
* Flag to signal the instance is busy doing a commit or a load operation.
|
133
|
+
*
|
134
|
+
* @returns `true` if the instance is busy, `false` otherwise.
|
135
|
+
*/
|
136
|
+
get isBusy() {
|
137
|
+
return this.isLoading || this.isCommitting || this.isDestroyed;
|
138
|
+
}
|
139
|
+
/**
|
140
|
+
* A promise that resolves when the file is synchronized with the object.
|
141
|
+
*
|
142
|
+
* @returns A promise that resolves when the file is synchronized.
|
143
|
+
* @example
|
144
|
+
* const object = new FSObject('file.json')
|
145
|
+
* object.commit()
|
146
|
+
*
|
147
|
+
* // Wait until the file is synchronized.
|
148
|
+
* await object.untilCommitted
|
149
|
+
*/
|
150
|
+
get untilCommitted() {
|
151
|
+
return this.isCommitting ? new Promise((resolve) => this.prependOnceListener("commit", () => resolve())) : Promise.resolve();
|
152
|
+
}
|
153
|
+
/**
|
154
|
+
* A promise that resolves when the object is destroyed.
|
155
|
+
*
|
156
|
+
* @returns A promise that resolves when the object is destroyed.
|
157
|
+
* @example
|
158
|
+
* const object = new FSObject('file.json')
|
159
|
+
* object.destroy()
|
160
|
+
*
|
161
|
+
* // Wait until the object is destroyed.
|
162
|
+
* await object.untilDestroyed
|
163
|
+
*/
|
164
|
+
get untilDestroyed() {
|
165
|
+
return this.isDestroyed ? Promise.resolve() : new Promise((resolve) => this.prependOnceListener("destroy", resolve));
|
166
|
+
}
|
167
|
+
/**
|
168
|
+
* A promise that resolves when the object is synchronized with the file.
|
169
|
+
*
|
170
|
+
* @returns A promise that resolves when the file is synchronized.
|
171
|
+
* @example
|
172
|
+
* const object = new FSObject('file.json')
|
173
|
+
* object.load()
|
174
|
+
*
|
175
|
+
* // Wait until the object is synchronized.
|
176
|
+
* await object.untilLoaded
|
177
|
+
*/
|
178
|
+
get untilLoaded() {
|
179
|
+
return this.isLoading ? new Promise((resolve) => this.prependOnceListener("load", () => resolve())) : Promise.resolve();
|
180
|
+
}
|
181
|
+
}
|
182
|
+
function loadObject(path, options = {}) {
|
183
|
+
return FSObject.from(path, options);
|
184
|
+
}
|
185
|
+
exports.FSObject = FSObject;
|
186
|
+
exports.loadObject = loadObject;
|
187
|
+
//# sourceMappingURL=loadObject.cjs.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"loadObject.cjs","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":["EventEmitter","reactive","garbageCollected","awaitable","dirname","mkdir","writeFile","overwrite","stat","rm","access","constants","readFile","watch"],"mappings":"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"}
|
@@ -0,0 +1,222 @@
|
|
1
|
+
import { WatchOptions, PathLike, Stats, FSWatcher } from 'node:fs';
|
2
|
+
import { EventEmitter } from 'node:events';
|
3
|
+
import { ReactiveOptions, Reactive } from '@unshared/reactivity/reactive';
|
4
|
+
import { Awaitable } from '@unshared/functions/awaitable';
|
5
|
+
|
6
|
+
interface FSObjectOptions<T extends object> extends ReactiveOptions<T>, WatchOptions {
|
7
|
+
/**
|
8
|
+
* If set to `true` and the file does not exist, the file will be created
|
9
|
+
* if it does not exist and the object will be initialized with an empty
|
10
|
+
* object.
|
11
|
+
*
|
12
|
+
* @default false
|
13
|
+
*/
|
14
|
+
createIfNotExists?: boolean;
|
15
|
+
/**
|
16
|
+
* If set to `true`, the file will be deleted when the instance is destroyed.
|
17
|
+
* Allowing you to create temporary files that will be deleted when the
|
18
|
+
* instance is garbage collected.
|
19
|
+
*/
|
20
|
+
deleteOnDestroy?: boolean;
|
21
|
+
/**
|
22
|
+
* If set to `true`, changes on the file will not be reflected in the object.
|
23
|
+
* You can use this to prevent the object from being updated when you are
|
24
|
+
* making changes to the file.
|
25
|
+
*
|
26
|
+
* @default false
|
27
|
+
*/
|
28
|
+
ignoreFileChanges?: boolean;
|
29
|
+
/**
|
30
|
+
* If set to `true`, changes on the object will be reflected in the file.
|
31
|
+
* You can set this to `false` if you want to make multiple changes to the
|
32
|
+
* object without triggering multiple file updates.
|
33
|
+
*
|
34
|
+
* @default false
|
35
|
+
*/
|
36
|
+
ignoreObjectChanges?: boolean;
|
37
|
+
/**
|
38
|
+
* The initial value of the object. If the file does not exist, the object
|
39
|
+
* will be initialized with this value.
|
40
|
+
*
|
41
|
+
* @default {}
|
42
|
+
*/
|
43
|
+
initialValue?: T;
|
44
|
+
/**
|
45
|
+
* The parser function to use when reading the file. If not set, the file
|
46
|
+
* will be parsed as JSON using the native `JSON.parse` function.
|
47
|
+
*
|
48
|
+
* @default JSON.parse
|
49
|
+
*/
|
50
|
+
parse?: (json: string) => T;
|
51
|
+
/**
|
52
|
+
* The stringifier function to use when writing the file. If not set, the
|
53
|
+
* object will be stringified as JSON using the native `JSON.stringify` function.
|
54
|
+
*
|
55
|
+
* @default JSON.stringify
|
56
|
+
*/
|
57
|
+
serialize?: (object: T) => string;
|
58
|
+
}
|
59
|
+
interface FSObjectEventMap<T extends object> {
|
60
|
+
commit: [T];
|
61
|
+
destroy: [];
|
62
|
+
load: [T];
|
63
|
+
lock: [];
|
64
|
+
unlock: [];
|
65
|
+
}
|
66
|
+
declare class FSObject<T extends object> extends EventEmitter<FSObjectEventMap<T>> {
|
67
|
+
path: PathLike;
|
68
|
+
options: FSObjectOptions<T>;
|
69
|
+
/** Flag to signal the file is synchronized with the object. */
|
70
|
+
isCommitting: boolean;
|
71
|
+
/** Flag to signal the instance has been destroyed. */
|
72
|
+
isDestroyed: boolean;
|
73
|
+
/** Flag to signal the object is synchronized with the file. */
|
74
|
+
isLoading: boolean;
|
75
|
+
/** The current content of the file. */
|
76
|
+
object: Reactive<T>;
|
77
|
+
/** The current status of the file. */
|
78
|
+
stats: Stats | undefined;
|
79
|
+
/** A watcher that will update the object when the file changes. */
|
80
|
+
watcher: FSWatcher | undefined;
|
81
|
+
/**
|
82
|
+
* Load a JSON file and keep it synchronized with it's source file.
|
83
|
+
*
|
84
|
+
* @param path The path or file descriptor of the file to load.
|
85
|
+
* @param options Options for the watcher.
|
86
|
+
* @throws If the file is not a JSON object.
|
87
|
+
*/
|
88
|
+
constructor(path: PathLike, options?: FSObjectOptions<T>);
|
89
|
+
/**
|
90
|
+
* Create an awaitable instance of `FSObject` that resolves when the file
|
91
|
+
* is synchronized with the object and the object is synchronized with the file.
|
92
|
+
*
|
93
|
+
* This function is a shorthand for creating a new `FSObject` instance and
|
94
|
+
* calling the `access`, `load` and `watch` methods in sequence. This allows
|
95
|
+
* fast and easy access to the file and object in a single call.
|
96
|
+
*
|
97
|
+
* @param path The path or file descriptor of the file to load.
|
98
|
+
* @param options Options to pass to the `FSObject` constructor.
|
99
|
+
* @returns An awaitable instance of `FSObject`.
|
100
|
+
* @example
|
101
|
+
* const fsObject = FSObject.from('file.json')
|
102
|
+
* const object = await fsObject
|
103
|
+
*
|
104
|
+
* // Change the file and check the object.
|
105
|
+
* writeFileSync('file.json', '{"foo":"bar"}')
|
106
|
+
* await fsObject.untilLoaded
|
107
|
+
* object // => { foo: 'bar' }
|
108
|
+
*
|
109
|
+
* // Change the object and check the file.
|
110
|
+
* object.foo = 'baz'
|
111
|
+
* await fsObject.untilCommitted
|
112
|
+
* readFileSync('file.json', 'utf8') // => { "foo": "baz" }
|
113
|
+
*/
|
114
|
+
static from<T extends object>(path: PathLike, options?: FSObjectOptions<T>): Awaitable<FSObject<T>, Reactive<T>>;
|
115
|
+
/**
|
116
|
+
* Commit the current state of the object to the file. This function
|
117
|
+
* **will** write the object to the file and emit a `commit` event.
|
118
|
+
*
|
119
|
+
* @param writeObject The object to write to the file.
|
120
|
+
* @returns A promise that resolves when the file has been written.
|
121
|
+
*/
|
122
|
+
commit(writeObject?: T): Promise<void>;
|
123
|
+
/**
|
124
|
+
* Close the file and stop watching the file and object for changes.
|
125
|
+
* If the file has been created as a temporary file, it will be deleted.
|
126
|
+
*/
|
127
|
+
destroy(): Promise<void>;
|
128
|
+
/**
|
129
|
+
* Load the file and update the object.
|
130
|
+
*
|
131
|
+
* @returns The loaded object.
|
132
|
+
*/
|
133
|
+
load(): Promise<void>;
|
134
|
+
/**
|
135
|
+
* Start watching the file for changes and update the object if the content
|
136
|
+
* of the file changes.
|
137
|
+
*
|
138
|
+
* @returns The current instance for chaining.
|
139
|
+
* @example
|
140
|
+
* const object = new FSObject('file.json').watch()
|
141
|
+
*
|
142
|
+
* // Change the file and check the object.
|
143
|
+
* writeFileSync('file.json', '{"foo":"bar"}')
|
144
|
+
*
|
145
|
+
* // Wait until the object is updated.
|
146
|
+
* await object.untilLoaded
|
147
|
+
*
|
148
|
+
* // Check the object.
|
149
|
+
* expect(object.object).toStrictEqual({ foo: 'bar' })
|
150
|
+
*/
|
151
|
+
watch(): this;
|
152
|
+
/**
|
153
|
+
* Flag to signal the instance is busy doing a commit or a load operation.
|
154
|
+
*
|
155
|
+
* @returns `true` if the instance is busy, `false` otherwise.
|
156
|
+
*/
|
157
|
+
get isBusy(): boolean;
|
158
|
+
/**
|
159
|
+
* A promise that resolves when the file is synchronized with the object.
|
160
|
+
*
|
161
|
+
* @returns A promise that resolves when the file is synchronized.
|
162
|
+
* @example
|
163
|
+
* const object = new FSObject('file.json')
|
164
|
+
* object.commit()
|
165
|
+
*
|
166
|
+
* // Wait until the file is synchronized.
|
167
|
+
* await object.untilCommitted
|
168
|
+
*/
|
169
|
+
get untilCommitted(): Promise<void>;
|
170
|
+
/**
|
171
|
+
* A promise that resolves when the object is destroyed.
|
172
|
+
*
|
173
|
+
* @returns A promise that resolves when the object is destroyed.
|
174
|
+
* @example
|
175
|
+
* const object = new FSObject('file.json')
|
176
|
+
* object.destroy()
|
177
|
+
*
|
178
|
+
* // Wait until the object is destroyed.
|
179
|
+
* await object.untilDestroyed
|
180
|
+
*/
|
181
|
+
get untilDestroyed(): Promise<void>;
|
182
|
+
/**
|
183
|
+
* A promise that resolves when the object is synchronized with the file.
|
184
|
+
*
|
185
|
+
* @returns A promise that resolves when the file is synchronized.
|
186
|
+
* @example
|
187
|
+
* const object = new FSObject('file.json')
|
188
|
+
* object.load()
|
189
|
+
*
|
190
|
+
* // Wait until the object is synchronized.
|
191
|
+
* await object.untilLoaded
|
192
|
+
*/
|
193
|
+
get untilLoaded(): Promise<void>;
|
194
|
+
}
|
195
|
+
/**
|
196
|
+
* Create an awaitable instance of `FSObject` that resolves when the file
|
197
|
+
* is synchronized with the object and the object is synchronized with the file.
|
198
|
+
*
|
199
|
+
* This function is a shorthand for creating a new `FSObject` instance and
|
200
|
+
* calling the `access`, `load` and `watch` methods in sequence. This allows
|
201
|
+
* fast and easy access to the file and object in a single call.
|
202
|
+
*
|
203
|
+
* @param path The path or file descriptor of the file to load.
|
204
|
+
* @param options Options to pass to the `FSObject` constructor.
|
205
|
+
* @returns An awaitable instance of `FSObject`.
|
206
|
+
* @example
|
207
|
+
* const fsObject = loadObject('file.json')
|
208
|
+
* const object = await fsObject
|
209
|
+
*
|
210
|
+
* // Change the file and check the object.
|
211
|
+
* writeFileSync('file.json', '{"foo":"bar"}')
|
212
|
+
* await fsObject.untilLoaded
|
213
|
+
* object // => { foo: 'bar' }
|
214
|
+
*
|
215
|
+
* // Change the object and check the file.
|
216
|
+
* object.foo = 'baz'
|
217
|
+
* await fsObject.untilCommitted
|
218
|
+
* readFileSync('file.json', 'utf8') // => { "foo": "baz" }
|
219
|
+
*/
|
220
|
+
declare function loadObject<T extends object>(path: PathLike, options?: FSObjectOptions<T>): Awaitable<FSObject<T>, Reactive<T>>;
|
221
|
+
|
222
|
+
export { FSObject, type FSObjectEventMap, type FSObjectOptions, loadObject };
|