pkgprn 0.5.0 → 0.5.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.
@@ -4,75 +4,28 @@ import path from 'node:path';
4
4
  import { extractReferences } from './extract-references.js';
5
5
  import { adjustSourcemapLineMappings, isStrippableFile, parseCommentTypes, stripCommentsWithLineMap } from './strip-comments.js';
6
6
 
7
- /**
8
- * Files always included by npm regardless of the `files` array.
9
- * README & LICENSE/LICENCE are matched case-insensitively by basename (without extension).
10
- */
11
7
  const alwaysIncludedExact = ['package.json'];
12
8
  const alwaysIncludedBasenames = ['README', 'LICENSE', 'LICENCE'];
13
9
 
14
- /**
15
- * Files/directories always ignored by npm by default.
16
- */
17
10
  const alwaysIgnored = ['.DS_Store', '.hg', '.lock-wscript', '.svn', 'CVS', 'config.gypi', 'npm-debug.log'];
18
11
 
19
- /**
20
- * Glob-like patterns for always-ignored files.
21
- * Each entry has a `test` function that checks whether a basename matches.
22
- */
23
12
  const alwaysIgnoredPatterns = [
24
- /** `*.orig` */
25
- { test: (/** @type {string} */ basename) => basename.endsWith('.orig') },
26
- /** `.*.swp` */
27
- { test: (/** @type {string} */ basename) => basename.startsWith('.') && basename.endsWith('.swp') },
28
- /** `._*` */
29
- { test: (/** @type {string} */ basename) => basename.startsWith('._') },
30
- /** `.wafpickle-N` */
31
- { test: (/** @type {string} */ basename) => /^\.wafpickle-\d+$/.test(basename) },
13
+
14
+ { test: ( basename) => basename.endsWith('.orig') },
15
+
16
+ { test: ( basename) => basename.startsWith('.') && basename.endsWith('.swp') },
17
+
18
+ { test: ( basename) => basename.startsWith('._') },
19
+
20
+ { test: ( basename) => /^\.wafpickle-\d+$/.test(basename) },
32
21
  ];
33
22
 
34
- /**
35
- * Subset of always-ignored that can never be included, even if listed in `files`.
36
- */
37
23
  const hardIgnored = new Set(['.git', '.npmrc', 'node_modules', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb']);
38
24
 
39
- /**
40
- * @typedef {import('@niceties/logger').Logger} Logger
41
- */
42
-
43
- /**
44
- * @typedef {Object} PackageJson
45
- * @property {Object.<string, string>} [scripts]
46
- * @property {Object.<string, string>} [devDependencies]
47
- * @property {string} [packageManager]
48
- * @property {string} [main]
49
- * @property {string|Object.<string, string>} [bin]
50
- * @property {Array<string>} [files]
51
- * @property {Record<string, unknown>} [directories]
52
- * @property {Record<string, unknown>} [exports]
53
- * @property {Record<string, unknown>} [typesVersions]
54
- */
55
-
56
- /**
57
- * @typedef {Object} PruneOptions
58
- * @property {string} profile
59
- * @property {string|boolean} flatten
60
- * @property {boolean} removeSourcemaps
61
- * @property {string|boolean} stripComments
62
- * @property {boolean} optimizeFiles
63
- * @property {boolean} cleanupFiles
64
- */
65
-
66
- /**
67
- * Prunes a package.json object according to the given options.
68
- * @param {PackageJson} pkg
69
- * @param {PruneOptions} options
70
- * @param {Logger} logger
71
- */
72
25
  export async function prunePkg(pkg, options, logger) {
73
26
  const scriptsToKeep = getScriptsData();
74
27
 
75
- const keys = scriptsToKeep[/** @type {'library'|'app'} */ (options.profile)];
28
+ const keys = scriptsToKeep[ (options.profile)];
76
29
 
77
30
  if (!keys) {
78
31
  throw new Error(`unknown profile ${options.profile}`);
@@ -106,30 +59,28 @@ export async function prunePkg(pkg, options, logger) {
106
59
  if (options.removeSourcemaps) {
107
60
  const sourceMaps = await walkDir('.', ['node_modules']).then(files => files.filter(file => file.endsWith('.map')));
108
61
  for (const sourceMap of sourceMaps) {
109
- // find corresponding file
62
+
110
63
  const sourceFile = sourceMap.slice(0, -4);
111
- // load file
64
+
112
65
  const sourceFileContent = await readFile(sourceFile, 'utf8');
113
- // find sourceMappingURL
66
+
114
67
  const sourceMappingUrl = `\n//# sourceMappingURL=${path.basename(sourceMap)}`;
115
- // remove sourceMappingURL
68
+
116
69
  const newContent = sourceFileContent.replace(sourceMappingUrl, '');
117
- // write file
70
+
118
71
  await writeFile(sourceFile, newContent, 'utf8');
119
- // remove sourceMap
72
+
120
73
  await rm(sourceMap);
121
74
  }
122
75
  }
123
76
 
124
77
  if (options.stripComments) {
125
- const typesToStrip = parseCommentTypes(/** @type {string | true} */ (options.stripComments));
78
+ const typesToStrip = parseCommentTypes( (options.stripComments));
126
79
  logger.update('stripping comments...');
127
80
  const allFiles = await walkDir('.', ['node_modules']);
128
81
  const jsFiles = allFiles.filter(isStrippableFile);
129
82
  const dtsMapFiles = allFiles.filter(f => f.endsWith('.d.ts.map'));
130
83
 
131
- // Strip comments from JS files and collect line maps keyed by file path.
132
- /** @type {Map<string, Int32Array>} */
133
84
  const lineMaps = new Map();
134
85
  for (const file of jsFiles) {
135
86
  const content = await readFile(file, 'utf8');
@@ -140,7 +91,6 @@ export async function prunePkg(pkg, options, logger) {
140
91
  }
141
92
  }
142
93
 
143
- // Adjust .d.ts.map files that reference any of the stripped JS files.
144
94
  if (lineMaps.size > 0 && dtsMapFiles.length > 0) {
145
95
  for (const mapFile of dtsMapFiles) {
146
96
  const mapContent = await readFile(mapFile, 'utf8');
@@ -184,7 +134,6 @@ export async function prunePkg(pkg, options, logger) {
184
134
  }
185
135
  }
186
136
 
187
- // walk depth keys from the highest to the lowest
188
137
  const maxDepth = Math.max(...depthToFiles.keys());
189
138
  for (let depth = maxDepth; depth > 0; --depth) {
190
139
  const files = depthToFiles.get(depth);
@@ -199,9 +148,9 @@ export async function prunePkg(pkg, options, logger) {
199
148
  }
200
149
  }
201
150
  for (const [dirname, filesInDir] of mapDirToFiles) {
202
- // find out real content of the directory
151
+
203
152
  const realFiles = await readdir(dirname);
204
- // check if all files in the directory are in the filesInDir
153
+
205
154
  const allFilesInDir = realFiles.every(file => filesInDir.includes(file)) || realFiles.length === 0;
206
155
  if (allFilesInDir && dirname !== '.') {
207
156
  if (!depthToFiles.has(depth - 1)) {
@@ -212,8 +161,8 @@ export async function prunePkg(pkg, options, logger) {
212
161
  const thisDepth = depthToFiles.get(depth);
213
162
  depthToFiles.set(
214
163
  depth,
215
- thisDepth.filter((/** @type {string} */ file) =>
216
- filesInDir.every((/** @type {string} */ fileInDir) => path.join(dirname, fileInDir) !== file)
164
+ thisDepth.filter(( file) =>
165
+ filesInDir.every(( fileInDir) => path.join(dirname, fileInDir) !== file)
217
166
  )
218
167
  );
219
168
  }
@@ -222,7 +171,7 @@ export async function prunePkg(pkg, options, logger) {
222
171
 
223
172
  pkg.files = [...new Set(Array.from(depthToFiles.values()).flat())];
224
173
 
225
- pkg.files = pkg.files.filter((/** @type {string} */ file) => {
174
+ pkg.files = pkg.files.filter(( file) => {
226
175
  const fileNormalized = normalizePath(file);
227
176
  const dirname = path.dirname(fileNormalized);
228
177
  const basenameWithoutExtension = path.basename(fileNormalized, path.extname(fileNormalized)).toUpperCase();
@@ -232,16 +181,13 @@ export async function prunePkg(pkg, options, logger) {
232
181
  );
233
182
  });
234
183
 
235
- /**
236
- * @type {string[]}
237
- */
238
184
  const ignoreDirs = [];
239
185
 
240
186
  for (const fileOrDir of pkg.files) {
241
187
  if (await isDirectory(fileOrDir)) {
242
188
  const allFiles = await walkDir(fileOrDir);
243
189
  if (
244
- allFiles.every((/** @type {string} */ file) => {
190
+ allFiles.every(( file) => {
245
191
  const fileNormalized = normalizePath(file);
246
192
  return filterFiles.includes(fileNormalized);
247
193
  })
@@ -263,26 +209,12 @@ export async function prunePkg(pkg, options, logger) {
263
209
  }
264
210
  }
265
211
 
266
- /**
267
- * Flattens the dist directory and updates package.json references.
268
- * Supports multiple directories (comma-separated when passed as a string).
269
- * @param {PackageJson} pkg
270
- * @param {string|true} flatten
271
- * @param {Logger} logger
272
- */
273
212
  async function flatten(pkg, flatten, logger) {
274
- // find out where is the dist folder
275
213
 
276
214
  const allReferences = extractReferences(pkg);
277
215
 
278
- /** @type {string[]} */
279
216
  let distDirs;
280
217
 
281
- // at this point we requested directories.bin, but it is the only one that is directory and not a file
282
- // later when we get dirname we can't flatten directories.bin completely
283
- // it is easy to fix by checking element is a directory but it is kind of good
284
- // to have it as a separate directory, but user still can flatten it by specifying the directory
285
-
286
218
  if (flatten === true) {
287
219
  let commonSegments;
288
220
 
@@ -311,7 +243,7 @@ async function flatten(pkg, flatten, logger) {
311
243
  }
312
244
  distDirs = [distDir];
313
245
  } else {
314
- // split on comma to support multiple directories
246
+
315
247
  distDirs = flatten
316
248
  .split(',')
317
249
  .map(d => normalizePath(d.trim()))
@@ -320,9 +252,6 @@ async function flatten(pkg, flatten, logger) {
320
252
 
321
253
  logger.update(`flattening ${distDirs.join(', ')}...`);
322
254
 
323
- // collect files from all dist directories
324
-
325
- /** @type {Map<string, { distDir: string, relativeDistDir: string, files: string[] }>} */
326
255
  const distDirInfo = new Map();
327
256
 
328
257
  for (const distDir of distDirs) {
@@ -331,26 +260,21 @@ async function flatten(pkg, flatten, logger) {
331
260
  distDirInfo.set(distDir, { distDir, relativeDistDir, files });
332
261
  }
333
262
 
334
- // check for conflicts: files already existing in root AND cross-directory collisions
335
-
336
- /** @type {Map<string, string>} */
337
263
  const destinationToSource = new Map();
338
264
  const existsPromises = [];
339
- /** @type {string[]} */
265
+
340
266
  const existsKeys = [];
341
267
 
342
268
  for (const [distDir, info] of distDirInfo) {
343
269
  for (const file of info.files) {
344
270
  const relativePath = path.relative(info.relativeDistDir, file);
345
271
 
346
- // check for cross-directory conflicts
347
272
  if (destinationToSource.has(relativePath)) {
348
273
  const otherDir = destinationToSource.get(relativePath);
349
274
  throw new Error(`cannot flatten because '${relativePath}' exists in both '${otherDir}' and '${distDir}'`);
350
275
  }
351
276
  destinationToSource.set(relativePath, distDir);
352
277
 
353
- // check if file already exists in root
354
278
  existsKeys.push(relativePath);
355
279
  existsPromises.push(isExists(relativePath));
356
280
  }
@@ -364,7 +288,6 @@ async function flatten(pkg, flatten, logger) {
364
288
  throw new Error(`dist folder cannot be flattened because files already exist: ${filesAlreadyExist.join(', ')}`);
365
289
  }
366
290
 
367
- // handle directories.bin special case for each dist dir
368
291
  for (const distDir of distDirs) {
369
292
  if (
370
293
  'directories' in pkg &&
@@ -390,7 +313,6 @@ async function flatten(pkg, flatten, logger) {
390
313
  }
391
314
  }
392
315
 
393
- // create new directory structure
394
316
  const mkdirPromises = [];
395
317
  for (const [, info] of distDirInfo) {
396
318
  for (const file of info.files) {
@@ -401,11 +323,9 @@ async function flatten(pkg, flatten, logger) {
401
323
 
402
324
  await Promise.all(mkdirPromises);
403
325
 
404
- // move files to root dir (rename)
405
326
  const renamePromises = [];
406
327
  const newFiles = [];
407
328
 
408
- /** @type {Map<string, string>} maps new path -> old path */
409
329
  const movedFiles = new Map();
410
330
 
411
331
  for (const [, info] of distDirInfo) {
@@ -419,12 +339,8 @@ async function flatten(pkg, flatten, logger) {
419
339
 
420
340
  await Promise.all(renamePromises);
421
341
 
422
- // adjust sourcemap paths for explicit flatten only
423
- // (automatic flatten is safe because the common prefix is derived from package.json references)
424
342
  if (typeof flatten === 'string') {
425
- // build reverse map: normalized old path -> new path
426
- // so we can fix sources that point to files which themselves moved
427
- /** @type {Map<string, string>} */
343
+
428
344
  const oldToNew = new Map();
429
345
  for (const [newPath, oldPath] of movedFiles) {
430
346
  oldToNew.set(path.normalize(oldPath), newPath);
@@ -439,8 +355,6 @@ async function flatten(pkg, flatten, logger) {
439
355
  }
440
356
  }
441
357
 
442
- // clean up empty source directories
443
- /** @type {string[]} */
444
358
  const cleanedDirs = [];
445
359
  for (const [, info] of distDirInfo) {
446
360
  let cleanedDir = info.relativeDistDir;
@@ -457,7 +371,6 @@ async function flatten(pkg, flatten, logger) {
457
371
 
458
372
  const allReferencesSet = new Set(allReferences);
459
373
 
460
- // update package.json - replace each distDir prefix in references
461
374
  const stringsToReplace = distDirs.map(d => `${d}/`);
462
375
  const pkgClone = cloneAndUpdate(pkg, value => {
463
376
  if (!allReferencesSet.has(value)) {
@@ -472,7 +385,6 @@ async function flatten(pkg, flatten, logger) {
472
385
  });
473
386
  Object.assign(pkg, pkgClone);
474
387
 
475
- // update files
476
388
  let files = pkg.files;
477
389
  if (files) {
478
390
  files = files.filter(file => {
@@ -483,7 +395,6 @@ async function flatten(pkg, flatten, logger) {
483
395
  pkg.files = [...files];
484
396
  }
485
397
 
486
- // remove extra directories with package.json
487
398
  const exports = pkg.exports ? Object.keys(pkg.exports) : [];
488
399
  for (const key of exports) {
489
400
  if (key === '.') {
@@ -493,7 +404,7 @@ async function flatten(pkg, flatten, logger) {
493
404
  if (isDir) {
494
405
  const pkgPath = path.join(key, 'package.json');
495
406
  const pkgExists = await isExists(pkgPath);
496
- // ensure nothing else is in the directory
407
+
497
408
  const files = await readdir(key);
498
409
  if (files.length === 1 && pkgExists) {
499
410
  await rm(key, { recursive: true, force: true });
@@ -502,25 +413,15 @@ async function flatten(pkg, flatten, logger) {
502
413
  }
503
414
  }
504
415
 
505
- /**
506
- * @param {string} file
507
- * @returns {string}
508
- */
509
416
  function normalizePath(file) {
510
417
  let fileNormalized = path.normalize(file);
511
418
  if (fileNormalized.endsWith('/') || fileNormalized.endsWith('\\')) {
512
- // remove trailing slash
419
+
513
420
  fileNormalized = fileNormalized.slice(0, -1);
514
421
  }
515
422
  return fileNormalized;
516
423
  }
517
424
 
518
- /**
519
- * Deep clones an object/array and updates all string values using the updater function
520
- * @param {unknown} pkg
521
- * @param {(value: string) => string} updater
522
- * @returns {unknown}
523
- */
524
425
  function cloneAndUpdate(pkg, updater) {
525
426
  if (typeof pkg === 'string') {
526
427
  return updater(pkg);
@@ -529,24 +430,16 @@ function cloneAndUpdate(pkg, updater) {
529
430
  return pkg.map(value => cloneAndUpdate(value, updater));
530
431
  }
531
432
  if (typeof pkg === 'object' && pkg !== null) {
532
- /** @type {Record<string, unknown>} */
433
+
533
434
  const clone = {};
534
435
  for (const key of Object.keys(pkg)) {
535
- clone[key] = cloneAndUpdate(/** @type {Record<string, unknown>} */ (pkg)[key], updater);
436
+ clone[key] = cloneAndUpdate( (pkg)[key], updater);
536
437
  }
537
438
  return clone;
538
439
  }
539
440
  return pkg;
540
441
  }
541
442
 
542
- /**
543
- * Adjusts the `sources` (and `sourceRoot`) in a v3 sourcemap file after it has been moved.
544
- * Resolves each source against the old location, then makes it relative to the new location.
545
- * If a source target was itself moved during flatten, the new location is used instead.
546
- * @param {string} newMapPath - The new path of the .map file (relative to project root).
547
- * @param {string} oldMapPath - The old path of the .map file (relative to project root).
548
- * @param {Map<string, string>} oldToNew - Map from normalized old file paths to their new paths.
549
- */
550
443
  async function adjustSourcemapPaths(newMapPath, oldMapPath, oldToNew) {
551
444
  const content = await readFile(newMapPath, 'utf8');
552
445
 
@@ -554,7 +447,7 @@ async function adjustSourcemapPaths(newMapPath, oldMapPath, oldToNew) {
554
447
  try {
555
448
  map = JSON.parse(content);
556
449
  } catch {
557
- return; // not valid JSON, skip
450
+ return;
558
451
  }
559
452
 
560
453
  if (map.version !== 3 || !Array.isArray(map.sources)) {
@@ -565,18 +458,17 @@ async function adjustSourcemapPaths(newMapPath, oldMapPath, oldToNew) {
565
458
  const newDir = path.dirname(newMapPath) || '.';
566
459
  const sourceRoot = map.sourceRoot || '';
567
460
 
568
- map.sources = map.sources.map((/** @type {string} */ source) => {
569
- // Resolve source against old map location (incorporating sourceRoot)
461
+ map.sources = map.sources.map(( source) => {
462
+
570
463
  const resolved = path.normalize(path.join(oldDir, sourceRoot, source));
571
- // If the resolved source was itself moved, use its new location
464
+
572
465
  const effective = oldToNew.get(resolved) ?? resolved;
573
- // Make relative to new map location
466
+
574
467
  const newRelative = path.relative(newDir, effective);
575
- // Sourcemaps always use forward slashes
468
+
576
469
  return newRelative.split(path.sep).join('/');
577
470
  });
578
471
 
579
- // sourceRoot has been incorporated into the individual source paths
580
472
  if (map.sourceRoot !== undefined) {
581
473
  delete map.sourceRoot;
582
474
  }
@@ -584,47 +476,26 @@ async function adjustSourcemapPaths(newMapPath, oldMapPath, oldToNew) {
584
476
  await writeFile(newMapPath, `${JSON.stringify(map, null, 2)}\n`, 'utf8');
585
477
  }
586
478
 
587
- /**
588
- * @param {string} parent
589
- * @param {string} child
590
- * @returns {boolean}
591
- */
592
479
  function isSubDirectory(parent, child) {
593
480
  const rel = path.relative(parent, child);
594
481
  return rel !== '' && !rel.startsWith('..');
595
482
  }
596
483
 
597
- /**
598
- * @param {string} dir
599
- * @returns {Promise<boolean>}
600
- */
601
484
  async function isEmptyDir(dir) {
602
485
  const entries = await readdir(dir, { withFileTypes: true });
603
486
  return entries.filter(entry => !entry.isDirectory()).length === 0;
604
487
  }
605
488
 
606
- /**
607
- * @param {string} file
608
- * @returns {Promise<boolean>}
609
- */
610
489
  async function isDirectory(file) {
611
490
  const fileStat = await stat(file);
612
491
  return fileStat.isDirectory();
613
492
  }
614
493
 
615
- /**
616
- * @param {string} dir
617
- * @param {Array<string>} [ignoreDirs=[]]
618
- * @returns {Promise<Array<string>>}
619
- */
620
494
  async function walkDir(dir, ignoreDirs = []) {
621
495
  const entries = await readdir(dir, { withFileTypes: true });
622
- /**
623
- * @type {string[]}
624
- */
496
+
625
497
  const files = [];
626
498
 
627
- // Process files first
628
499
  for (const entry of entries) {
629
500
  if (!entry.isDirectory()) {
630
501
  const childPath = path.join(entry.parentPath, entry.name);
@@ -632,7 +503,6 @@ async function walkDir(dir, ignoreDirs = []) {
632
503
  }
633
504
  }
634
505
 
635
- // Then process directories
636
506
  for (const entry of entries) {
637
507
  if (entry.isDirectory()) {
638
508
  const childPath = path.join(entry.parentPath, entry.name);
@@ -649,9 +519,6 @@ async function walkDir(dir, ignoreDirs = []) {
649
519
  return files;
650
520
  }
651
521
 
652
- /**
653
- * @param {string} file
654
- */
655
522
  async function isExists(file) {
656
523
  try {
657
524
  await access(file);
@@ -664,12 +531,6 @@ async function isExists(file) {
664
531
  return file;
665
532
  }
666
533
 
667
- /**
668
- * Returns the list of files always included by npm for a given package.
669
- * This includes `package.json`, the `main` entry, and all `bin` entries.
670
- * @param {PackageJson} pkg
671
- * @returns {string[]}
672
- */
673
534
  function getAlwaysIncludedFiles(pkg) {
674
535
  const files = [...alwaysIncludedExact];
675
536
  if (pkg.main && typeof pkg.main === 'string') {
@@ -686,11 +547,6 @@ function getAlwaysIncludedFiles(pkg) {
686
547
  return files;
687
548
  }
688
549
 
689
- /**
690
- * Checks whether a file or directory name matches the always-ignored patterns.
691
- * @param {string} basename - The basename of the file or directory.
692
- * @returns {boolean}
693
- */
694
550
  function isAlwaysIgnored(basename) {
695
551
  if (alwaysIgnored.includes(basename)) {
696
552
  return true;
@@ -698,10 +554,6 @@ function isAlwaysIgnored(basename) {
698
554
  return alwaysIgnoredPatterns.some(pattern => pattern.test(basename));
699
555
  }
700
556
 
701
- /**
702
- * Recursively removes junk files (always-ignored by npm) from a directory tree.
703
- * @param {string} dir
704
- */
705
557
  async function removeJunkFiles(dir) {
706
558
  const entries = await readdir(dir, { withFileTypes: true });
707
559
  for (const entry of entries) {
@@ -717,11 +569,6 @@ async function removeJunkFiles(dir) {
717
569
  }
718
570
  }
719
571
 
720
- /**
721
- * Checks whether a root-level file is always included by npm (case-insensitive basename match).
722
- * @param {string} file - The file path relative to the package root.
723
- * @returns {boolean}
724
- */
725
572
  function isAlwaysIncludedByBasename(file) {
726
573
  const dir = path.dirname(file);
727
574
  if (dir !== '' && dir !== '.') {
@@ -731,17 +578,11 @@ function isAlwaysIncludedByBasename(file) {
731
578
  return alwaysIncludedBasenames.includes(basenameWithoutExtension);
732
579
  }
733
580
 
734
- /**
735
- * Removes files from the working directory that are not included in the `files` array
736
- * or the always-included list, then drops the `files` array from package.json.
737
- * @param {PackageJson} pkg
738
- * @param {Logger} logger
739
- */
740
581
  async function cleanupDir(pkg, logger) {
741
582
  logger.update('cleaning up files...');
742
583
 
743
584
  const alwaysIncludedFiles = getAlwaysIncludedFiles(pkg);
744
- const filesEntries = /** @type {string[]} */ (pkg.files).map(normalizePath);
585
+ const filesEntries = (pkg.files).map(normalizePath);
745
586
 
746
587
  const entries = await readdir('.');
747
588
 
@@ -752,42 +593,30 @@ async function cleanupDir(pkg, logger) {
752
593
 
753
594
  const normalized = normalizePath(entry);
754
595
 
755
- // check if matched by files entries (exact or parent directory)
756
596
  if (filesEntries.some(f => normalized === f || normalized.startsWith(`${f}/`))) {
757
597
  continue;
758
598
  }
759
599
 
760
- // check if any files entry is under this directory
761
600
  if (filesEntries.some(f => f.startsWith(`${normalized}/`))) {
762
- // need to recurse into this directory for granular cleanup
601
+
763
602
  await cleanupSubDir(normalized, filesEntries, alwaysIncludedFiles);
764
603
  continue;
765
604
  }
766
605
 
767
- // check if always-included by exact path
768
606
  if (alwaysIncludedFiles.includes(normalized)) {
769
607
  continue;
770
608
  }
771
609
 
772
- // check if always-included by basename (root level)
773
610
  if (isAlwaysIncludedByBasename(normalized)) {
774
611
  continue;
775
612
  }
776
613
 
777
- // not matched - remove
778
614
  await rm(entry, { recursive: true, force: true });
779
615
  }
780
616
 
781
617
  pkg.files = undefined;
782
618
  }
783
619
 
784
- /**
785
- * Recursively cleans up a subdirectory, keeping only files matched by the files entries
786
- * or always-included files.
787
- * @param {string} dir
788
- * @param {string[]} filesEntries
789
- * @param {string[]} alwaysIncludedFiles
790
- */
791
620
  async function cleanupSubDir(dir, filesEntries, alwaysIncludedFiles) {
792
621
  const entries = await readdir(dir);
793
622
 
@@ -800,27 +629,22 @@ async function cleanupSubDir(dir, filesEntries, alwaysIncludedFiles) {
800
629
 
801
630
  const normalized = normalizePath(fullPath);
802
631
 
803
- // check if matched by files entries
804
632
  if (filesEntries.some(f => normalized === f || normalized.startsWith(`${f}/`))) {
805
633
  continue;
806
634
  }
807
635
 
808
- // check if any files entry is under this path
809
636
  if (filesEntries.some(f => f.startsWith(`${normalized}/`))) {
810
637
  await cleanupSubDir(normalized, filesEntries, alwaysIncludedFiles);
811
638
  continue;
812
639
  }
813
640
 
814
- // check if always-included by exact path
815
641
  if (alwaysIncludedFiles.includes(normalized)) {
816
642
  continue;
817
643
  }
818
644
 
819
- // not matched - remove
820
645
  await rm(fullPath, { recursive: true, force: true });
821
646
  }
822
647
 
823
- // remove the directory if it's now empty
824
648
  const remaining = await readdir(dir);
825
649
  if (remaining.length === 0) {
826
650
  await rm(dir, { recursive: true, force: true });