@ix-xs/node-comfort 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +150 -0
- package/README.md +32 -30
- package/index.js +3 -3
- package/package.json +8 -7
- package/src/Arr.js +222 -58
- package/src/Async.js +322 -90
- package/src/Cache.js +148 -44
- package/src/Checker.js +257 -26
- package/src/Cli.js +404 -145
- package/src/Color.js +224 -83
- package/src/Crypto.js +481 -106
- package/src/Emitter.js +104 -37
- package/src/Env.js +176 -42
- package/src/FS.js +203 -60
- package/src/Func.js +226 -76
- package/src/Http.js +311 -149
- package/src/Id.js +281 -96
- package/src/Logger.js +302 -78
- package/src/Num.js +182 -60
- package/src/Obj.js +340 -125
- package/src/SQLite.js +426 -105
- package/src/Schema.js +602 -87
- package/src/Str.js +316 -86
- package/src/Sys.js +197 -54
- package/src/Time.js +572 -317
- package/src/Utils.js +62 -41
- package/src/errors.js +54 -11
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +59 -17
- package/types/src/Arr.d.ts +78 -0
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +52 -1
- package/types/src/Async.d.ts.map +1 -1
- package/types/src/Cache.d.ts +33 -7
- package/types/src/Cache.d.ts.map +1 -1
- package/types/src/Checker.d.ts +155 -2
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +97 -15
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +7 -1
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +99 -13
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +36 -17
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +31 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +36 -5
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +147 -19
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +113 -43
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts +60 -8
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +53 -2
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +48 -7
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +67 -4
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +123 -14
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +256 -7
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +49 -1
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +51 -6
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +130 -34
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +15 -11
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +27 -3
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
package/src/FS.js
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
const fs = require("node:fs");
|
|
22
22
|
const os = require("node:os");
|
|
23
23
|
const nodePath = require("node:path");
|
|
24
|
+
const util = require("node:util");
|
|
24
25
|
const { fileURLToPath } = require("node:url");
|
|
26
|
+
const { sleepSync, toMs } = require("./internal/shared.js");
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Options for `copyFoldersIn()` and `moveFoldersIn()`.
|
|
@@ -75,7 +77,8 @@ const { fileURLToPath } = require("node:url");
|
|
|
75
77
|
* @typedef {object} WriteFileOptions
|
|
76
78
|
* @property {boolean} [atomic] Write to a temporary file and rename it, so nobody reads a half-written file and a crash can't corrupt it. Defaults to `true`.
|
|
77
79
|
* @property {BufferEncoding} [encoding] Defaults to `"utf8"`.
|
|
78
|
-
* @property {number} [mode] Permissions, like `0o600` for secrets.
|
|
80
|
+
* @property {number} [mode] Permissions, like `0o600` for secrets. A file that already exists keeps the ones it has.
|
|
81
|
+
* @property {boolean} [fsync] Wait until the data is on the disk before returning. Slower, for files that must survive a power cut.
|
|
79
82
|
* @property {number} [spaces] Indentation when writing an object as JSON. Defaults to `2`.
|
|
80
83
|
*/
|
|
81
84
|
|
|
@@ -85,6 +88,8 @@ const { fileURLToPath } = require("node:url");
|
|
|
85
88
|
* @property {number} [spaces] Defaults to `2`.
|
|
86
89
|
* @property {boolean} [atomic] Defaults to `true`.
|
|
87
90
|
* @property {(key: string, value: unknown) => unknown} [replacer] Passed to `JSON.stringify`.
|
|
91
|
+
* @property {number} [mode] Permissions, like `0o600` for secrets. A file that already exists keeps the ones it has.
|
|
92
|
+
* @property {boolean} [fsync] Wait until the data is on the disk before returning.
|
|
88
93
|
*/
|
|
89
94
|
|
|
90
95
|
/**
|
|
@@ -126,13 +131,13 @@ const { fileURLToPath } = require("node:url");
|
|
|
126
131
|
* @property {string} [path] What to watch. Defaults to the working directory.
|
|
127
132
|
* @property {boolean} [recursive] Watch nested folders too.
|
|
128
133
|
* @property {(event: "rename" | "change", file: string) => boolean} [filter] Return `false` to ignore an event.
|
|
129
|
-
* @property {number} [debounce] Merge events on the same file within this
|
|
134
|
+
* @property {number | string} [debounce] Merge events on the same file within this long, in ms or like `"100ms"`. Editors often save in several steps.
|
|
130
135
|
*/
|
|
131
136
|
|
|
132
137
|
/**
|
|
133
138
|
* Returned by `watch()`.
|
|
134
139
|
* @typedef {object} Watcher
|
|
135
|
-
* @property {<E extends "change" | "rename" | "all">(event: E, callback: E extends "all" ? (event: "change" | "rename", file: string) => void : (file: string) => void) => Watcher} on Listens for changes. `"all"` also gets the event name.
|
|
140
|
+
* @property {<E extends "change" | "rename" | "all" | "error">(event: E, callback: E extends "all" ? (event: "change" | "rename", file: string) => void : E extends "error" ? (error: Error) => void : (file: string) => void) => Watcher} on Listens for changes. `"all"` also gets the event name. `"error"` tells you the watch broke, for instance because the folder was deleted.
|
|
136
141
|
* @property {() => void} stop Stops watching.
|
|
137
142
|
* @property {() => Watcher} pause Ignores events until `resume()`.
|
|
138
143
|
* @property {() => Watcher} resume
|
|
@@ -146,20 +151,8 @@ const INDEX_FILE = nodePath.join(__dirname, "..", "index.js");
|
|
|
146
151
|
* @returns {string}
|
|
147
152
|
*/
|
|
148
153
|
const _callerDir = () => {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
/** @type {NodeJS.CallSite[] | undefined} */
|
|
152
|
-
let stack;
|
|
153
|
-
try {
|
|
154
|
-
Error.stackTraceLimit = 50;
|
|
155
|
-
Error.prepareStackTrace = (_, frames) => frames;
|
|
156
|
-
stack = /** @type {any} */ (new Error()).stack;
|
|
157
|
-
} finally {
|
|
158
|
-
Error.prepareStackTrace = originalPrepare;
|
|
159
|
-
Error.stackTraceLimit = originalLimit;
|
|
160
|
-
}
|
|
161
|
-
for (const frame of stack ?? []) {
|
|
162
|
-
let file = frame.getFileName();
|
|
154
|
+
for (const frame of util.getCallSites(50)) {
|
|
155
|
+
let file = frame.scriptName;
|
|
163
156
|
if (!file) continue;
|
|
164
157
|
if (file.startsWith("file://")) file = fileURLToPath(file);
|
|
165
158
|
if (file.startsWith("node:") || file.includes("node_modules")) continue;
|
|
@@ -209,25 +202,36 @@ const _existing = (input, type) => {
|
|
|
209
202
|
const _walk = (root, type, options) => {
|
|
210
203
|
/** @type {string[]} */
|
|
211
204
|
const out = [];
|
|
205
|
+
_walkInto(root, type, options, new Set(options.ignore), out);
|
|
206
|
+
return out;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {string} dir
|
|
211
|
+
* @param {"file" | "folder"} type
|
|
212
|
+
* @param {Required<ListOptions>} options
|
|
213
|
+
* @param {Set<string>} ignore
|
|
214
|
+
* @param {string[]} out Filled in place: no array is built and thrown away at each level.
|
|
215
|
+
*/
|
|
216
|
+
const _walkInto = (dir, type, options, ignore, out) => {
|
|
212
217
|
/** @type {fs.Dirent[]} */
|
|
213
218
|
let entries;
|
|
214
219
|
try {
|
|
215
|
-
entries = fs.readdirSync(
|
|
220
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
216
221
|
} catch {
|
|
217
|
-
return
|
|
222
|
+
return;
|
|
218
223
|
}
|
|
219
224
|
for (const entry of entries) {
|
|
220
225
|
if (!options.hidden && entry.name.startsWith(".")) continue;
|
|
221
|
-
const full = nodePath.join(
|
|
226
|
+
const full = nodePath.join(dir, entry.name);
|
|
222
227
|
if (entry.isDirectory()) {
|
|
223
|
-
if (
|
|
228
|
+
if (ignore.has(entry.name)) continue;
|
|
224
229
|
if (type === "folder") out.push(full);
|
|
225
|
-
if (options.recursive)
|
|
230
|
+
if (options.recursive) _walkInto(full, type, options, ignore, out);
|
|
226
231
|
} else if (type === "file" && (entry.isFile() || entry.isSymbolicLink())) {
|
|
227
232
|
out.push(full);
|
|
228
233
|
}
|
|
229
234
|
}
|
|
230
|
-
return out;
|
|
231
235
|
};
|
|
232
236
|
|
|
233
237
|
/**
|
|
@@ -252,6 +256,26 @@ const _content = (data, spaces) => {
|
|
|
252
256
|
return JSON.stringify(data, null, spaces);
|
|
253
257
|
};
|
|
254
258
|
|
|
259
|
+
const _RETRY_CODES = new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Renames a file. On Windows, an antivirus or an indexer can hold the
|
|
263
|
+
* target for a few milliseconds: a refused rename is tried again.
|
|
264
|
+
* @param {string} from
|
|
265
|
+
* @param {string} to
|
|
266
|
+
*/
|
|
267
|
+
const _rename = (from, to) => {
|
|
268
|
+
for (let attempt = 0; ; attempt++) {
|
|
269
|
+
try {
|
|
270
|
+
fs.renameSync(from, to);
|
|
271
|
+
return;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (process.platform !== "win32" || attempt >= 8 || !_RETRY_CODES.has(/** @type {NodeJS.ErrnoException} */ (error).code ?? "")) throw error;
|
|
274
|
+
sleepSync(5 * (attempt + 1));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
255
279
|
/**
|
|
256
280
|
* @param {string} target
|
|
257
281
|
* @param {string | Buffer} content
|
|
@@ -259,17 +283,31 @@ const _content = (data, spaces) => {
|
|
|
259
283
|
*/
|
|
260
284
|
const _write = (target, content, options) => {
|
|
261
285
|
fs.mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
262
|
-
|
|
263
|
-
const writeOptions = { encoding: options.encoding ?? "utf8" };
|
|
264
|
-
if (options.mode !== undefined) writeOptions.mode = options.mode;
|
|
286
|
+
const encoding = options.encoding ?? "utf8";
|
|
265
287
|
if (options.atomic === false) {
|
|
266
|
-
fs.
|
|
288
|
+
/** @type {fs.WriteFileOptions} */
|
|
289
|
+
const direct = { encoding, flush: options.fsync ?? false };
|
|
290
|
+
if (options.mode !== undefined) direct.mode = options.mode;
|
|
291
|
+
fs.writeFileSync(target, content, direct);
|
|
267
292
|
return;
|
|
268
293
|
}
|
|
294
|
+
// The new file takes the place of the old one, so it must take its permissions too:
|
|
295
|
+
// a secret readable by its owner only stays that way.
|
|
296
|
+
let mode = options.mode;
|
|
297
|
+
if (mode === undefined) {
|
|
298
|
+
try {
|
|
299
|
+
mode = fs.statSync(target).mode & 0o777;
|
|
300
|
+
} catch {
|
|
301
|
+
mode = undefined;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
269
304
|
const temp = `${target}.${process.pid}.${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
270
305
|
try {
|
|
306
|
+
/** @type {fs.WriteFileOptions} */
|
|
307
|
+
const writeOptions = { encoding, flush: options.fsync ?? false };
|
|
308
|
+
if (mode !== undefined) writeOptions.mode = mode;
|
|
271
309
|
fs.writeFileSync(temp, content, writeOptions);
|
|
272
|
-
|
|
310
|
+
_rename(temp, target);
|
|
273
311
|
} catch (error) {
|
|
274
312
|
fs.rmSync(temp, { force: true });
|
|
275
313
|
throw error;
|
|
@@ -315,7 +353,7 @@ const _stripJSONComments = (text) => {
|
|
|
315
353
|
* @param {boolean} dot
|
|
316
354
|
* @returns {RegExp}
|
|
317
355
|
*/
|
|
318
|
-
const
|
|
356
|
+
const _compileGlob = (pattern, dot) => {
|
|
319
357
|
const noDot = dot ? "" : "(?!\\.)";
|
|
320
358
|
let re = "";
|
|
321
359
|
let i = 0;
|
|
@@ -377,6 +415,26 @@ const _globToRegExp = (pattern, dot) => {
|
|
|
377
415
|
return new RegExp(`^${re}$`);
|
|
378
416
|
};
|
|
379
417
|
|
|
418
|
+
/** @type {Map<string, RegExp>} */
|
|
419
|
+
const _globs = new Map();
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* A glob pattern as a regular expression, compiled once.
|
|
423
|
+
* @param {string} pattern Forward slashes only.
|
|
424
|
+
* @param {boolean} dot Whether `*` matches names starting with a dot.
|
|
425
|
+
* @returns {RegExp}
|
|
426
|
+
*/
|
|
427
|
+
const _globToRegExp = (pattern, dot) => {
|
|
428
|
+
const key = dot ? `.${pattern}` : `/${pattern}`;
|
|
429
|
+
let regex = _globs.get(key);
|
|
430
|
+
if (!regex) {
|
|
431
|
+
regex = _compileGlob(pattern, dot);
|
|
432
|
+
if (_globs.size >= 500) _globs.clear();
|
|
433
|
+
_globs.set(key, regex);
|
|
434
|
+
}
|
|
435
|
+
return regex;
|
|
436
|
+
};
|
|
437
|
+
|
|
380
438
|
/**
|
|
381
439
|
* @param {string} path
|
|
382
440
|
* @returns {string} The real path, symlinks and short names resolved.
|
|
@@ -544,8 +602,11 @@ const _globIn = (cwd, patterns, options, absolute) => {
|
|
|
544
602
|
const matchers = patterns.map((p) => _globToRegExp(p, options.dot ?? false));
|
|
545
603
|
const ignores = (options.ignore ?? ["**/node_modules/**", "**/.git/**"]).map((p) => _globToRegExp(p.replace(/\\/g, "/"), true));
|
|
546
604
|
const type = options.type ?? "files";
|
|
605
|
+
// Without "**", a pattern can't match deeper than it has parts: no need to read further down.
|
|
606
|
+
let maxDepth = 0;
|
|
547
607
|
const bases = patterns.map((p) => {
|
|
548
608
|
const segments = p.split("/");
|
|
609
|
+
maxDepth = p.includes("**") ? Infinity : Math.max(maxDepth, segments.length);
|
|
549
610
|
const staticParts = [];
|
|
550
611
|
for (const segment of segments.slice(0, -1)) {
|
|
551
612
|
if (/[*?[{]/.test(segment)) break;
|
|
@@ -558,8 +619,11 @@ const _globIn = (cwd, patterns, options, absolute) => {
|
|
|
558
619
|
/** @type {Set<string>} */
|
|
559
620
|
const visited = new Set();
|
|
560
621
|
|
|
561
|
-
/**
|
|
562
|
-
|
|
622
|
+
/**
|
|
623
|
+
* @param {string} relDir
|
|
624
|
+
* @param {number} depth How many parts `relDir` has.
|
|
625
|
+
*/
|
|
626
|
+
const visit = (relDir, depth) => {
|
|
563
627
|
if (visited.has(relDir)) return;
|
|
564
628
|
visited.add(relDir);
|
|
565
629
|
/** @type {fs.Dirent[]} */
|
|
@@ -575,10 +639,10 @@ const _globIn = (cwd, patterns, options, absolute) => {
|
|
|
575
639
|
if (ignores.some((re) => re.test(rel) || (isDir && re.test(`${rel}/`)))) continue;
|
|
576
640
|
const wanted = type === "all" || (type === "folders" ? isDir : !isDir);
|
|
577
641
|
if (wanted && matchers.some((re) => re.test(rel))) results.add(rel);
|
|
578
|
-
if (isDir) visit(rel);
|
|
642
|
+
if (isDir && depth + 1 < maxDepth) visit(rel, depth + 1);
|
|
579
643
|
}
|
|
580
644
|
};
|
|
581
|
-
for (const base of new Set(bases)) visit(base);
|
|
645
|
+
for (const base of new Set(bases)) visit(base, base ? base.split("/").length : 0);
|
|
582
646
|
return [...results].sort().map((rel) => (absolute ? nodePath.join(cwd, rel) : rel));
|
|
583
647
|
};
|
|
584
648
|
|
|
@@ -596,10 +660,10 @@ const _globIn = (cwd, patterns, options, absolute) => {
|
|
|
596
660
|
*/
|
|
597
661
|
function find(name, options = {}) {
|
|
598
662
|
const type = options.type ?? "file";
|
|
599
|
-
const ignore = options.ignore ?? ["node_modules", ".git"];
|
|
663
|
+
const ignore = new Set(options.ignore ?? ["node_modules", ".git"]);
|
|
600
664
|
const queue = [_abs(options.cwd)];
|
|
601
|
-
|
|
602
|
-
const dir =
|
|
665
|
+
for (let next = 0; next < queue.length; next++) {
|
|
666
|
+
const dir = queue[next];
|
|
603
667
|
/** @type {fs.Dirent[]} */
|
|
604
668
|
let entries;
|
|
605
669
|
try {
|
|
@@ -611,11 +675,41 @@ function find(name, options = {}) {
|
|
|
611
675
|
const isDir = entry.isDirectory();
|
|
612
676
|
if (entry.name === name && (type === "any" || (type === "folder" ? isDir : !isDir))) return nodePath.join(dir, entry.name);
|
|
613
677
|
}
|
|
614
|
-
for (const entry of entries) if (entry.isDirectory() && !ignore.
|
|
678
|
+
for (const entry of entries) if (entry.isDirectory() && !ignore.has(entry.name)) queue.push(nodePath.join(dir, entry.name));
|
|
615
679
|
}
|
|
616
680
|
return undefined;
|
|
617
681
|
}
|
|
618
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Looks for a file or folder in a folder, then in its parent, and so on up
|
|
685
|
+
* to the root. It's how you find the `package.json` or the `.git` a file
|
|
686
|
+
* belongs to.
|
|
687
|
+
*
|
|
688
|
+
* @example
|
|
689
|
+
* nc.findUp("package.json"); // the closest one above the working directory
|
|
690
|
+
* nc.findUp(".git", { type: "folder", cwd: __dirname });
|
|
691
|
+
* nc.findUp(["pnpm-lock.yaml", "yarn.lock", "package-lock.json"]);
|
|
692
|
+
*
|
|
693
|
+
* @param {string | string[]} name One name, or several to try in each folder, in this order.
|
|
694
|
+
* @param {{ cwd?: string, type?: "file" | "folder" | "any", stopAt?: string }} [options] Where to start (the working directory by default), what to look for (`"any"` by default), and a folder not to go above.
|
|
695
|
+
* @returns {string | undefined} The absolute path.
|
|
696
|
+
*/
|
|
697
|
+
function findUp(name, options = {}) {
|
|
698
|
+
const names = Array.isArray(name) ? name : [name];
|
|
699
|
+
const type = options.type ?? "any";
|
|
700
|
+
const stopAt = options.stopAt === undefined ? undefined : _abs(options.stopAt);
|
|
701
|
+
let dir = _abs(options.cwd);
|
|
702
|
+
for (;;) {
|
|
703
|
+
for (const candidate of names) {
|
|
704
|
+
const found = _existing(nodePath.join(dir, candidate), type);
|
|
705
|
+
if (found) return found;
|
|
706
|
+
}
|
|
707
|
+
const parent = nodePath.dirname(dir);
|
|
708
|
+
if (parent === dir || dir === stopAt) return undefined;
|
|
709
|
+
dir = parent;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
619
713
|
/**
|
|
620
714
|
* Reads a file as text, or as a Buffer with `"buffer"`.
|
|
621
715
|
*
|
|
@@ -642,11 +736,11 @@ function find(name, options = {}) {
|
|
|
642
736
|
* @returns {string | Buffer | undefined}
|
|
643
737
|
*/
|
|
644
738
|
function readFile(path, encoding = "utf8") {
|
|
645
|
-
|
|
646
|
-
if (!target) return undefined;
|
|
739
|
+
if (!path) return undefined;
|
|
647
740
|
try {
|
|
648
|
-
return encoding === "buffer" ? fs.readFileSync(
|
|
741
|
+
return encoding === "buffer" ? fs.readFileSync(_abs(path)) : fs.readFileSync(_abs(path), encoding);
|
|
649
742
|
} catch {
|
|
743
|
+
// missing, a folder, or unreadable
|
|
650
744
|
return undefined;
|
|
651
745
|
}
|
|
652
746
|
}
|
|
@@ -729,7 +823,7 @@ function writeJSON(path, data, options = {}) {
|
|
|
729
823
|
try {
|
|
730
824
|
const text = JSON.stringify(data, /** @type {any} */ (options.replacer), options.spaces ?? 2);
|
|
731
825
|
if (text === undefined) return false;
|
|
732
|
-
_write(_abs(path), `${text}\n`, { atomic: options.atomic });
|
|
826
|
+
_write(_abs(path), `${text}\n`, { atomic: options.atomic, mode: options.mode, fsync: options.fsync });
|
|
733
827
|
return true;
|
|
734
828
|
} catch {
|
|
735
829
|
return false;
|
|
@@ -749,8 +843,14 @@ function writeJSON(path, data, options = {}) {
|
|
|
749
843
|
function appendFile(path, data) {
|
|
750
844
|
try {
|
|
751
845
|
const target = _abs(path);
|
|
752
|
-
|
|
753
|
-
|
|
846
|
+
const content = Buffer.isBuffer(data) ? data : String(data);
|
|
847
|
+
try {
|
|
848
|
+
fs.appendFileSync(target, content);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
if (/** @type {NodeJS.ErrnoException} */ (error).code !== "ENOENT") throw error;
|
|
851
|
+
fs.mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
852
|
+
fs.appendFileSync(target, content);
|
|
853
|
+
}
|
|
754
854
|
return true;
|
|
755
855
|
} catch {
|
|
756
856
|
return false;
|
|
@@ -1026,17 +1126,23 @@ function move(source, destination, options = {}) {
|
|
|
1026
1126
|
if (!src) return false;
|
|
1027
1127
|
const dest = _abs(destination);
|
|
1028
1128
|
try {
|
|
1029
|
-
if (fs.existsSync(dest))
|
|
1030
|
-
if (options.overwrite === false) return false;
|
|
1031
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
1032
|
-
}
|
|
1129
|
+
if (options.overwrite === false && fs.existsSync(dest)) return false;
|
|
1033
1130
|
fs.mkdirSync(nodePath.dirname(dest), { recursive: true });
|
|
1034
1131
|
try {
|
|
1132
|
+
// A rename replaces a file in one step: the destination is never missing, not even for an instant.
|
|
1035
1133
|
fs.renameSync(src, dest);
|
|
1036
1134
|
} catch (error) {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1135
|
+
const code = /** @type {NodeJS.ErrnoException} */ (error).code;
|
|
1136
|
+
if (code === "EXDEV") {
|
|
1137
|
+
// another drive: copy, then delete
|
|
1138
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
1139
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
1140
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
1141
|
+
} else {
|
|
1142
|
+
// a folder in the way, or a file where a folder goes
|
|
1143
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
1144
|
+
_rename(src, dest);
|
|
1145
|
+
}
|
|
1040
1146
|
}
|
|
1041
1147
|
return true;
|
|
1042
1148
|
} catch {
|
|
@@ -1191,8 +1297,8 @@ function moveFilesIn(options) {
|
|
|
1191
1297
|
const root = _existing(options.path, "folder");
|
|
1192
1298
|
if (!root || !options.dest) return undefined;
|
|
1193
1299
|
const filter = typeof options.filter === "function" ? options.filter : () => true;
|
|
1194
|
-
const files = (getFilesIn(root, options.recursive ?? true) ?? []).filter(filter);
|
|
1195
|
-
const copied = copyFilesIn({ ...options, path: root, filter: (
|
|
1300
|
+
const files = new Set((getFilesIn(root, options.recursive ?? true) ?? []).filter(filter));
|
|
1301
|
+
const copied = copyFilesIn({ ...options, path: root, filter: (file) => files.has(file) });
|
|
1196
1302
|
for (const file of files) fs.rmSync(file, { force: true });
|
|
1197
1303
|
return copied;
|
|
1198
1304
|
}
|
|
@@ -1270,8 +1376,13 @@ function stat(path) {
|
|
|
1270
1376
|
* @returns {number | undefined} `undefined` if it doesn't exist.
|
|
1271
1377
|
*/
|
|
1272
1378
|
function fileSize(path) {
|
|
1273
|
-
|
|
1274
|
-
|
|
1379
|
+
if (!path) return undefined;
|
|
1380
|
+
try {
|
|
1381
|
+
const stats = fs.statSync(_abs(path));
|
|
1382
|
+
return stats.isFile() ? stats.size : undefined;
|
|
1383
|
+
} catch {
|
|
1384
|
+
return undefined;
|
|
1385
|
+
}
|
|
1275
1386
|
}
|
|
1276
1387
|
|
|
1277
1388
|
/**
|
|
@@ -1355,7 +1466,17 @@ function sanitizeFilename(name, options = {}) {
|
|
|
1355
1466
|
let clean = String(name).replace(/[<>:"/\\|?*\u0000-\u001f\u007f]/g, replacement).replace(/[. ]+$/, "");
|
|
1356
1467
|
if (/^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i.test(clean)) clean = `_${clean}`;
|
|
1357
1468
|
if (clean === "." || clean === "..") clean = replacement;
|
|
1358
|
-
|
|
1469
|
+
if (Buffer.byteLength(clean) > 255) {
|
|
1470
|
+
// File systems count bytes, and a character cut in two would be broken.
|
|
1471
|
+
let bytes = 0;
|
|
1472
|
+
let kept = "";
|
|
1473
|
+
for (const char of clean) {
|
|
1474
|
+
bytes += Buffer.byteLength(char);
|
|
1475
|
+
if (bytes > 255) break;
|
|
1476
|
+
kept += char;
|
|
1477
|
+
}
|
|
1478
|
+
clean = kept.replace(/[. ]+$/, "");
|
|
1479
|
+
}
|
|
1359
1480
|
return clean || "_";
|
|
1360
1481
|
}
|
|
1361
1482
|
|
|
@@ -1375,8 +1496,9 @@ function watch(options = {}) {
|
|
|
1375
1496
|
const target = _existing(options.path, "any");
|
|
1376
1497
|
if (!target) return undefined;
|
|
1377
1498
|
let paused = false;
|
|
1499
|
+
const debounce = options.debounce === undefined ? 0 : toMs(options.debounce, "debounce");
|
|
1378
1500
|
/** @type {Record<string, Function[]>} */
|
|
1379
|
-
const listeners = { change: [], rename: [], all: [] };
|
|
1501
|
+
const listeners = { change: [], rename: [], all: [], error: [] };
|
|
1380
1502
|
/** @type {Map<string, NodeJS.Timeout>} */
|
|
1381
1503
|
const pending = new Map();
|
|
1382
1504
|
|
|
@@ -1394,18 +1516,38 @@ function watch(options = {}) {
|
|
|
1394
1516
|
const isDir = fs.statSync(target).isDirectory();
|
|
1395
1517
|
// Watching a short (8.3) or symlinked path crashes libuv on Windows, so
|
|
1396
1518
|
// watch the real one and keep reporting paths under the one you passed.
|
|
1397
|
-
const
|
|
1519
|
+
const real = _realPath(target);
|
|
1520
|
+
let gone = false;
|
|
1521
|
+
const watcher = fs.watch(real, { recursive: options.recursive ?? false }, (event, fileName) => {
|
|
1522
|
+
if (gone) return;
|
|
1398
1523
|
const file = fileName && isDir ? nodePath.join(target, String(fileName)) : target;
|
|
1399
1524
|
const kind = event === "rename" ? "rename" : "change";
|
|
1400
|
-
|
|
1525
|
+
// A folder that was deleted keeps sending events on some systems, without
|
|
1526
|
+
// end and without an error: say it once, then stop listening.
|
|
1527
|
+
if (kind === "rename" && isDir && !fs.existsSync(real)) {
|
|
1528
|
+
gone = true;
|
|
1529
|
+
controller.stop();
|
|
1530
|
+
const error = Object.assign(new Error(`The watched folder no longer exists: ${target}`), { code: "ENOENT", path: target });
|
|
1531
|
+
for (const cb of listeners.error) cb(error);
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
if (!(debounce > 0)) return dispatch(kind, file);
|
|
1401
1535
|
const key = `${kind}:${file}`;
|
|
1402
1536
|
clearTimeout(pending.get(key));
|
|
1403
1537
|
pending.set(key, setTimeout(() => {
|
|
1404
1538
|
pending.delete(key);
|
|
1405
1539
|
dispatch(kind, file);
|
|
1406
|
-
},
|
|
1540
|
+
}, debounce));
|
|
1541
|
+
});
|
|
1542
|
+
watcher.on("error", (error) => {
|
|
1543
|
+
if (gone) return;
|
|
1544
|
+
// Some systems report the deleted folder here: it is the same news, told once.
|
|
1545
|
+
if (isDir && !fs.existsSync(real)) {
|
|
1546
|
+
gone = true;
|
|
1547
|
+
controller.stop();
|
|
1548
|
+
}
|
|
1549
|
+
for (const cb of listeners.error) cb(error);
|
|
1407
1550
|
});
|
|
1408
|
-
watcher.on("error", () => {});
|
|
1409
1551
|
|
|
1410
1552
|
/** @type {Watcher} */
|
|
1411
1553
|
const controller = {
|
|
@@ -1443,6 +1585,7 @@ module.exports = {
|
|
|
1443
1585
|
getFilesIn,
|
|
1444
1586
|
glob,
|
|
1445
1587
|
find,
|
|
1588
|
+
findUp,
|
|
1446
1589
|
readFile,
|
|
1447
1590
|
readLines,
|
|
1448
1591
|
readJSON,
|