@unshared/fs 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 };
|