|
|
@@ -4,7 +4,8 @@ import { readdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
|
import { basename, dirname, relative, resolve } from 'node:path';
|
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
|
|
-const EXCLUDED_DIRECTORIES = new Set(['.git', '.hg', '.svn', 'node_modules', 'vendor']);
|
|
|
+const EXCLUDED_DIRECTORIES = new Set(['.git', '.hg', '.svn', 'node_modules', 'vendor', 'dist', 'target', '__pycache__']);
|
|
|
+const utf8Decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
|
const argumentsList = process.argv.slice(2);
|
|
|
const applyChanges = argumentsList.at(-1) === '--apply';
|
|
|
|
|
|
@@ -124,8 +125,33 @@ async function collectTextChange(path, oldValue, newValue, changes) {
|
|
|
process.exit(1);
|
|
|
}
|
|
|
|
|
|
- if (!contents.includes(0) && contents.includes(oldValue)) {
|
|
|
- changes.set(path, contents.toString().replaceAll(oldValue, newValue));
|
|
|
+ if (contents.includes(0) || !contents.includes(oldValue)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const text = decodeUtf8(contents);
|
|
|
+ if (text === null) {
|
|
|
+ console.warn(`Skipped non-UTF-8 file: ${displayPath(path)}.`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ changes.set(path, text.replaceAll(oldValue, newValue));
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Decodes a buffer as strict UTF-8, preserving any BOM.
|
|
|
+ *
|
|
|
+ * @param {Buffer} contents
|
|
|
+ * @returns {string|null} Decoded text, or null when the bytes are not valid UTF-8.
|
|
|
+ */
|
|
|
+function decodeUtf8(contents) {
|
|
|
+ try {
|
|
|
+ return utf8Decoder.decode(contents);
|
|
|
+ } catch (error) {
|
|
|
+ if (error instanceof TypeError) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -143,7 +169,10 @@ function validatePathChanges(changes) {
|
|
|
}
|
|
|
|
|
|
for (const [source, target] of changes) {
|
|
|
- if (existsSync(target) && !changes.has(target)) {
|
|
|
+
|
|
|
+ // Case-insensitive filesystems (NTFS, APFS) resolve a case-only target to the source itself.
|
|
|
+ const isCaseOnlyRename = source.toLowerCase() === target.toLowerCase();
|
|
|
+ if (existsSync(target) && !changes.has(target) && !isCaseOnlyRename) {
|
|
|
console.error(`Target path already exists: ${displayPath(target)}.`);
|
|
|
process.exit(1);
|
|
|
}
|