pkgprn 0.1.0 → 0.2.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.
package/prune.js CHANGED
@@ -1,18 +1,40 @@
1
- import {
2
- access,
3
- mkdir,
4
- readdir,
5
- readFile,
6
- rename,
7
- rm,
8
- stat,
9
- writeFile,
10
- } from 'node:fs/promises';
1
+ import { access, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
11
2
  import path from 'node:path';
12
3
 
13
4
  /**
14
- * @typedef {Object} Logger
15
- * @property {function(string): void} update
5
+ * Files always included by npm regardless of the `files` array.
6
+ * README & LICENSE/LICENCE are matched case-insensitively by basename (without extension).
7
+ */
8
+ const alwaysIncludedExact = ['package.json'];
9
+ const alwaysIncludedBasenames = ['README', 'LICENSE', 'LICENCE'];
10
+
11
+ /**
12
+ * Files/directories always ignored by npm by default.
13
+ */
14
+ const alwaysIgnored = ['.DS_Store', '.hg', '.lock-wscript', '.svn', 'CVS', 'config.gypi', 'npm-debug.log'];
15
+
16
+ /**
17
+ * Glob-like patterns for always-ignored files.
18
+ * Each entry has a `test` function that checks whether a basename matches.
19
+ */
20
+ const alwaysIgnoredPatterns = [
21
+ /** `*.orig` */
22
+ { test: (/** @type {string} */ basename) => basename.endsWith('.orig') },
23
+ /** `.*.swp` */
24
+ { test: (/** @type {string} */ basename) => basename.startsWith('.') && basename.endsWith('.swp') },
25
+ /** `._*` */
26
+ { test: (/** @type {string} */ basename) => basename.startsWith('._') },
27
+ /** `.wafpickle-N` */
28
+ { test: (/** @type {string} */ basename) => /^\.wafpickle-\d+$/.test(basename) },
29
+ ];
30
+
31
+ /**
32
+ * Subset of always-ignored that can never be included, even if listed in `files`.
33
+ */
34
+ const hardIgnored = new Set(['.git', '.npmrc', 'node_modules', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb']);
35
+
36
+ /**
37
+ * @typedef {import('@niceties/logger').Logger} Logger
16
38
  */
17
39
 
18
40
  /**
@@ -23,9 +45,9 @@ import path from 'node:path';
23
45
  * @property {string} [main]
24
46
  * @property {string|Object.<string, string>} [bin]
25
47
  * @property {Array<string>} [files]
26
- * @property {Object} [directories]
27
- * @property {Object} [exports]
28
- * @property {Object} [typesVersions]
48
+ * @property {Record<string, unknown>} [directories]
49
+ * @property {Record<string, unknown>} [exports]
50
+ * @property {Record<string, unknown>} [typesVersions]
29
51
  */
30
52
 
31
53
  /**
@@ -34,6 +56,7 @@ import path from 'node:path';
34
56
  * @property {string|boolean} flatten
35
57
  * @property {boolean} removeSourcemaps
36
58
  * @property {boolean} optimizeFiles
59
+ * @property {boolean} cleanupFiles
37
60
  */
38
61
 
39
62
  /**
@@ -45,8 +68,7 @@ import path from 'node:path';
45
68
  export async function prunePkg(pkg, options, logger) {
46
69
  const scriptsToKeep = getScriptsData();
47
70
 
48
- const keys =
49
- scriptsToKeep[/** @type {'library'|'app'} */ (options.profile)];
71
+ const keys = scriptsToKeep[/** @type {'library'|'app'} */ (options.profile)];
50
72
 
51
73
  if (!keys) {
52
74
  throw new Error(`unknown profile ${options.profile}`);
@@ -67,14 +89,18 @@ export async function prunePkg(pkg, options, logger) {
67
89
  }
68
90
  }
69
91
 
92
+ if (options.cleanupFiles) {
93
+ await removeJunkFiles('.');
94
+ } else if (options.flatten) {
95
+ logger('cleanup is disabled, junk files may cause flatten to fail', 2);
96
+ }
97
+
70
98
  if (options.flatten) {
71
99
  await flatten(pkg, options.flatten, logger);
72
100
  }
73
101
 
74
102
  if (options.removeSourcemaps) {
75
- const sourceMaps = await walkDir('.', ['node_modules']).then((files) =>
76
- files.filter((file) => file.endsWith('.map'))
77
- );
103
+ const sourceMaps = await walkDir('.', ['node_modules']).then(files => files.filter(file => file.endsWith('.map')));
78
104
  for (const sourceMap of sourceMaps) {
79
105
  // find corresponding file
80
106
  const sourceFile = sourceMap.slice(0, -4);
@@ -92,19 +118,7 @@ export async function prunePkg(pkg, options, logger) {
92
118
  }
93
119
 
94
120
  if (pkg.files && Array.isArray(pkg.files) && options.optimizeFiles) {
95
- const filterFiles = ['package.json'];
96
- const specialFiles = ['README', 'LICENSE', 'LICENCE'];
97
- if (pkg.main && typeof pkg.main === 'string') {
98
- filterFiles.push(normalizePath(pkg.main));
99
- }
100
- if (pkg.bin) {
101
- if (typeof pkg.bin === 'string') {
102
- filterFiles.push(normalizePath(pkg.bin));
103
- }
104
- if (typeof pkg.bin === 'object' && pkg.bin !== null) {
105
- filterFiles.push(...Object.values(pkg.bin).map(normalizePath));
106
- }
107
- }
121
+ const filterFiles = getAlwaysIncludedFiles(pkg);
108
122
 
109
123
  const depthToFiles = new Map();
110
124
 
@@ -136,9 +150,7 @@ export async function prunePkg(pkg, options, logger) {
136
150
  // find out real content of the directory
137
151
  const realFiles = await readdir(dirname);
138
152
  // check if all files in the directory are in the filesInDir
139
- const allFilesInDir =
140
- realFiles.every((file) => filesInDir.includes(file)) ||
141
- realFiles.length === 0;
153
+ const allFilesInDir = realFiles.every(file => filesInDir.includes(file)) || realFiles.length === 0;
142
154
  if (allFilesInDir && dirname !== '.') {
143
155
  if (!depthToFiles.has(depth - 1)) {
144
156
  depthToFiles.set(depth - 1, [dirname]);
@@ -149,10 +161,7 @@ export async function prunePkg(pkg, options, logger) {
149
161
  depthToFiles.set(
150
162
  depth,
151
163
  thisDepth.filter((/** @type {string} */ file) =>
152
- filesInDir.every(
153
- (/** @type {string} */ fileInDir) =>
154
- path.join(dirname, fileInDir) !== file
155
- )
164
+ filesInDir.every((/** @type {string} */ fileInDir) => path.join(dirname, fileInDir) !== file)
156
165
  )
157
166
  );
158
167
  }
@@ -164,13 +173,10 @@ export async function prunePkg(pkg, options, logger) {
164
173
  pkg.files = pkg.files.filter((/** @type {string} */ file) => {
165
174
  const fileNormalized = normalizePath(file);
166
175
  const dirname = path.dirname(fileNormalized);
167
- const basenameWithoutExtension = path
168
- .basename(fileNormalized, path.extname(fileNormalized))
169
- .toUpperCase();
176
+ const basenameWithoutExtension = path.basename(fileNormalized, path.extname(fileNormalized)).toUpperCase();
170
177
  return (
171
178
  !filterFiles.includes(fileNormalized) &&
172
- ((dirname !== '' && dirname !== '.') ||
173
- !specialFiles.includes(basenameWithoutExtension))
179
+ ((dirname !== '' && dirname !== '.') || !alwaysIncludedBasenames.includes(basenameWithoutExtension))
174
180
  );
175
181
  });
176
182
 
@@ -193,16 +199,21 @@ export async function prunePkg(pkg, options, logger) {
193
199
  }
194
200
  }
195
201
 
196
- pkg.files = pkg.files.filter((dir) => !ignoreDirs.includes(dir));
202
+ pkg.files = pkg.files.filter(dir => !ignoreDirs.includes(dir));
197
203
 
198
204
  if (pkg.files.length === 0) {
199
205
  pkg.files = undefined;
200
206
  }
201
207
  }
208
+
209
+ if (pkg.files && Array.isArray(pkg.files) && options.cleanupFiles) {
210
+ await cleanupDir(pkg, logger);
211
+ }
202
212
  }
203
213
 
204
214
  /**
205
215
  * Flattens the dist directory and updates package.json references.
216
+ * Supports multiple directories (comma-separated when passed as a string).
206
217
  * @param {PackageJson} pkg
207
218
  * @param {string|true} flatten
208
219
  * @param {Logger} logger
@@ -212,11 +223,11 @@ async function flatten(pkg, flatten, logger) {
212
223
 
213
224
  // find out where is the dist folder
214
225
 
215
- const expression = jsonata(
216
- '[bin, bin.*, main, module, unpkg, umd, types, typings, exports[].*.*, typesVersions.*.*, directories.bin]'
217
- );
226
+ const expression = jsonata('[bin, bin.*, main, module, unpkg, umd, types, typings, exports[].*.*, typesVersions.*.*, directories.bin]');
218
227
  const allReferences = await expression.evaluate(pkg);
219
- let distDir;
228
+
229
+ /** @type {string[]} */
230
+ let distDirs;
220
231
 
221
232
  // at this point we requested directories.bin, but it is the only one that is directory and not a file
222
233
  // later when we get dirname we can't flatten directories.bin completely
@@ -233,9 +244,7 @@ async function flatten(pkg, flatten, logger) {
233
244
 
234
245
  const dirname = path.dirname(entry);
235
246
 
236
- const cleanedSegments = dirname
237
- .split('/')
238
- .filter((path) => path && path !== '.');
247
+ const cleanedSegments = dirname.split('/').filter(path => path && path !== '.');
239
248
  if (!commonSegments) {
240
249
  commonSegments = cleanedSegments;
241
250
  } else {
@@ -247,29 +256,55 @@ async function flatten(pkg, flatten, logger) {
247
256
  }
248
257
  }
249
258
  }
250
- distDir = commonSegments?.join('/');
259
+ const distDir = commonSegments?.join('/');
260
+ if (!distDir) {
261
+ throw new Error('could not find dist folder');
262
+ }
263
+ distDirs = [distDir];
251
264
  } else {
252
- distDir = normalizePath(flatten);
265
+ // split on comma to support multiple directories
266
+ distDirs = flatten
267
+ .split(',')
268
+ .map(d => normalizePath(d.trim()))
269
+ .filter(Boolean);
253
270
  }
254
271
 
255
- if (!distDir) {
256
- throw new Error('could not find dist folder');
257
- }
272
+ logger.update(`flattening ${distDirs.join(', ')}...`);
258
273
 
259
- logger.update(`flattening ${distDir}...`);
274
+ // collect files from all dist directories
260
275
 
261
- // check if dist can be flattened
276
+ /** @type {Map<string, { distDir: string, relativeDistDir: string, files: string[] }>} */
277
+ const distDirInfo = new Map();
262
278
 
263
- const relativeDistDir = `./${distDir}`;
279
+ for (const distDir of distDirs) {
280
+ const relativeDistDir = `./${distDir}`;
281
+ const files = await walkDir(relativeDistDir);
282
+ distDirInfo.set(distDir, { distDir, relativeDistDir, files });
283
+ }
264
284
 
285
+ // check for conflicts: files already existing in root AND cross-directory collisions
286
+
287
+ /** @type {Map<string, string>} */
288
+ const destinationToSource = new Map();
265
289
  const existsPromises = [];
290
+ /** @type {string[]} */
291
+ const existsKeys = [];
292
+
293
+ for (const [distDir, info] of distDirInfo) {
294
+ for (const file of info.files) {
295
+ const relativePath = path.relative(info.relativeDistDir, file);
266
296
 
267
- const filesInDist = await walkDir(relativeDistDir);
297
+ // check for cross-directory conflicts
298
+ if (destinationToSource.has(relativePath)) {
299
+ const otherDir = destinationToSource.get(relativePath);
300
+ throw new Error(`cannot flatten because '${relativePath}' exists in both '${otherDir}' and '${distDir}'`);
301
+ }
302
+ destinationToSource.set(relativePath, distDir);
268
303
 
269
- for (const file of filesInDist) {
270
- // check file is not in root dir
271
- const relativePath = path.relative(relativeDistDir, file);
272
- existsPromises.push(isExists(relativePath));
304
+ // check if file already exists in root
305
+ existsKeys.push(relativePath);
306
+ existsPromises.push(isExists(relativePath));
307
+ }
273
308
  }
274
309
 
275
310
  const exists = await Promise.all(existsPromises);
@@ -277,44 +312,42 @@ async function flatten(pkg, flatten, logger) {
277
312
  const filesAlreadyExist = exists.filter(Boolean);
278
313
 
279
314
  if (filesAlreadyExist.length) {
280
- throw new Error(
281
- `dist folder cannot be flattened because files already exist: ${filesAlreadyExist.join(', ')}`
282
- );
283
- }
284
-
285
- if (
286
- typeof flatten === 'string' &&
287
- 'directories' in pkg &&
288
- pkg.directories != null &&
289
- typeof pkg.directories === 'object' &&
290
- 'bin' in pkg.directories &&
291
- typeof pkg.directories.bin === 'string' &&
292
- normalizePath(pkg.directories.bin) === normalizePath(flatten)
293
- ) {
294
- // biome-ignore lint/performance/noDelete: <explanation>
295
- delete pkg.directories.bin;
296
- if (Object.keys(pkg.directories).length === 0) {
297
- pkg.directories = undefined;
298
- }
299
- const files = await readdir(flatten);
300
- if (files.length === 1) {
301
- pkg.bin = files[0];
302
- } else {
303
- pkg.bin = {};
304
- for (const file of files) {
305
- pkg.bin[path.basename(file, path.extname(file))] = file;
315
+ throw new Error(`dist folder cannot be flattened because files already exist: ${filesAlreadyExist.join(', ')}`);
316
+ }
317
+
318
+ // handle directories.bin special case for each dist dir
319
+ for (const distDir of distDirs) {
320
+ if (
321
+ 'directories' in pkg &&
322
+ pkg.directories != null &&
323
+ typeof pkg.directories === 'object' &&
324
+ 'bin' in pkg.directories &&
325
+ typeof pkg.directories.bin === 'string' &&
326
+ normalizePath(pkg.directories.bin) === distDir
327
+ ) {
328
+ delete pkg.directories.bin;
329
+ if (Object.keys(pkg.directories).length === 0) {
330
+ pkg.directories = undefined;
331
+ }
332
+ const files = await readdir(distDir);
333
+ if (files.length === 1) {
334
+ pkg.bin = files[0];
335
+ } else {
336
+ pkg.bin = {};
337
+ for (const file of files) {
338
+ pkg.bin[path.basename(file, path.extname(file))] = file;
339
+ }
306
340
  }
307
341
  }
308
342
  }
309
343
 
310
344
  // create new directory structure
311
345
  const mkdirPromises = [];
312
- for (const file of filesInDist) {
313
- // check file is not in root dir
314
- const relativePath = path.relative(relativeDistDir, file);
315
- mkdirPromises.push(
316
- mkdir(path.dirname(relativePath), { recursive: true })
317
- );
346
+ for (const [, info] of distDirInfo) {
347
+ for (const file of info.files) {
348
+ const relativePath = path.relative(info.relativeDistDir, file);
349
+ mkdirPromises.push(mkdir(path.dirname(relativePath), { recursive: true }));
350
+ }
318
351
  }
319
352
 
320
353
  await Promise.all(mkdirPromises);
@@ -323,45 +356,55 @@ async function flatten(pkg, flatten, logger) {
323
356
  const renamePromises = [];
324
357
  const newFiles = [];
325
358
 
326
- for (const file of filesInDist) {
327
- // check file is not in root dir
328
- const relativePath = path.relative(relativeDistDir, file);
329
- newFiles.push(relativePath);
330
- renamePromises.push(rename(file, relativePath));
359
+ for (const [, info] of distDirInfo) {
360
+ for (const file of info.files) {
361
+ const relativePath = path.relative(info.relativeDistDir, file);
362
+ newFiles.push(relativePath);
363
+ renamePromises.push(rename(file, relativePath));
364
+ }
331
365
  }
332
366
 
333
367
  await Promise.all(renamePromises);
334
368
 
335
- let cleanedDir = relativeDistDir;
336
- while (await isEmptyDir(cleanedDir)) {
337
- await rm(cleanedDir, { recursive: true, force: true });
338
- const parentDir = path.dirname(cleanedDir);
339
- if (parentDir === '.') {
340
- break;
369
+ // clean up empty source directories
370
+ /** @type {string[]} */
371
+ const cleanedDirs = [];
372
+ for (const [, info] of distDirInfo) {
373
+ let cleanedDir = info.relativeDistDir;
374
+ while (await isEmptyDir(cleanedDir)) {
375
+ await rm(cleanedDir, { recursive: true, force: true });
376
+ const parentDir = path.dirname(cleanedDir);
377
+ if (parentDir === '.') {
378
+ break;
379
+ }
380
+ cleanedDir = parentDir;
341
381
  }
342
- cleanedDir = parentDir;
382
+ cleanedDirs.push(normalizePath(cleanedDir));
343
383
  }
344
384
 
345
- const normalizedCleanDir = normalizePath(cleanedDir);
346
-
347
385
  const allReferencesSet = new Set(allReferences);
348
386
 
349
- // update package.json
350
- const stringToReplace = `${distDir}/`; // we append / to remove in from the middle of the string
351
- const pkgClone = cloneAndUpdate(pkg, (value) =>
352
- allReferencesSet.has(value) ? value.replace(stringToReplace, '') : value
353
- );
387
+ // update package.json - replace each distDir prefix in references
388
+ const stringsToReplace = distDirs.map(d => `${d}/`);
389
+ const pkgClone = cloneAndUpdate(pkg, value => {
390
+ if (!allReferencesSet.has(value)) {
391
+ return value;
392
+ }
393
+ for (const stringToReplace of stringsToReplace) {
394
+ if (value.includes(stringToReplace)) {
395
+ return value.replace(stringToReplace, '');
396
+ }
397
+ }
398
+ return value;
399
+ });
354
400
  Object.assign(pkg, pkgClone);
355
401
 
356
402
  // update files
357
403
  let files = pkg.files;
358
404
  if (files) {
359
- files = files.filter((file) => {
405
+ files = files.filter(file => {
360
406
  const fileNormalized = normalizePath(file);
361
- return (
362
- !isSubDirectory(cleanedDir, fileNormalized) &&
363
- fileNormalized !== normalizedCleanDir
364
- );
407
+ return !cleanedDirs.some(cleanedDir => isSubDirectory(cleanedDir, fileNormalized) || fileNormalized === cleanedDir);
365
408
  });
366
409
  files.push(...newFiles);
367
410
  pkg.files = [...files];
@@ -410,16 +453,13 @@ function cloneAndUpdate(pkg, updater) {
410
453
  return updater(pkg);
411
454
  }
412
455
  if (Array.isArray(pkg)) {
413
- return pkg.map((value) => cloneAndUpdate(value, updater));
456
+ return pkg.map(value => cloneAndUpdate(value, updater));
414
457
  }
415
458
  if (typeof pkg === 'object' && pkg !== null) {
416
459
  /** @type {Record<string, unknown>} */
417
460
  const clone = {};
418
461
  for (const key of Object.keys(pkg)) {
419
- clone[key] = cloneAndUpdate(
420
- /** @type {Record<string, unknown>} */ (pkg)[key],
421
- updater
422
- );
462
+ clone[key] = cloneAndUpdate(/** @type {Record<string, unknown>} */ (pkg)[key], updater);
423
463
  }
424
464
  return clone;
425
465
  }
@@ -441,7 +481,7 @@ function isSubDirectory(parent, child) {
441
481
  */
442
482
  async function isEmptyDir(dir) {
443
483
  const entries = await readdir(dir, { withFileTypes: true });
444
- return entries.filter((entry) => !entry.isDirectory()).length === 0;
484
+ return entries.filter(entry => !entry.isDirectory()).length === 0;
445
485
  }
446
486
 
447
487
  /**
@@ -459,26 +499,30 @@ async function isDirectory(file) {
459
499
  * @returns {Promise<Array<string>>}
460
500
  */
461
501
  async function walkDir(dir, ignoreDirs = []) {
462
- const entries = await readdir(dir, {
463
- recursive: true,
464
- withFileTypes: true,
465
- });
502
+ const entries = await readdir(dir, { withFileTypes: true });
503
+ /**
504
+ * @type {string[]}
505
+ */
466
506
  const files = [];
467
507
 
508
+ // Process files first
468
509
  for (const entry of entries) {
469
- if (entry.isFile()) {
470
- const childPath = entry.parentPath
471
- ? path.join(entry.parentPath, entry.name)
472
- : entry.name;
473
-
474
- // Check if any part of the path contains ignored directories
475
- const pathParts = path.relative(dir, childPath).split(path.sep);
476
- const shouldIgnore = pathParts.some((part) =>
477
- ignoreDirs.includes(part)
478
- );
510
+ if (!entry.isDirectory()) {
511
+ const childPath = path.join(entry.parentPath, entry.name);
512
+ files.push(childPath);
513
+ }
514
+ }
479
515
 
480
- if (!shouldIgnore) {
481
- files.push(childPath);
516
+ // Then process directories
517
+ for (const entry of entries) {
518
+ if (entry.isDirectory()) {
519
+ const childPath = path.join(entry.parentPath, entry.name);
520
+ const relativePath = path.relative(dir, childPath);
521
+ const topLevelDir = relativePath.split(path.sep)[0];
522
+
523
+ if (!ignoreDirs.includes(topLevelDir)) {
524
+ const childFiles = await walkDir(childPath);
525
+ files.push(...childFiles);
482
526
  }
483
527
  }
484
528
  }
@@ -493,12 +537,7 @@ async function isExists(file) {
493
537
  try {
494
538
  await access(file);
495
539
  } catch (e) {
496
- if (
497
- typeof e === 'object' &&
498
- e != null &&
499
- 'code' in e &&
500
- e.code === 'ENOENT'
501
- ) {
540
+ if (typeof e === 'object' && e != null && 'code' in e && e.code === 'ENOENT') {
502
541
  return false;
503
542
  }
504
543
  throw e;
@@ -506,16 +545,171 @@ async function isExists(file) {
506
545
  return file;
507
546
  }
508
547
 
548
+ /**
549
+ * Returns the list of files always included by npm for a given package.
550
+ * This includes `package.json`, the `main` entry, and all `bin` entries.
551
+ * @param {PackageJson} pkg
552
+ * @returns {string[]}
553
+ */
554
+ function getAlwaysIncludedFiles(pkg) {
555
+ const files = [...alwaysIncludedExact];
556
+ if (pkg.main && typeof pkg.main === 'string') {
557
+ files.push(normalizePath(pkg.main));
558
+ }
559
+ if (pkg.bin) {
560
+ if (typeof pkg.bin === 'string') {
561
+ files.push(normalizePath(pkg.bin));
562
+ }
563
+ if (typeof pkg.bin === 'object' && pkg.bin !== null) {
564
+ files.push(...Object.values(pkg.bin).map(normalizePath));
565
+ }
566
+ }
567
+ return files;
568
+ }
569
+
570
+ /**
571
+ * Checks whether a file or directory name matches the always-ignored patterns.
572
+ * @param {string} basename - The basename of the file or directory.
573
+ * @returns {boolean}
574
+ */
575
+ function isAlwaysIgnored(basename) {
576
+ if (alwaysIgnored.includes(basename)) {
577
+ return true;
578
+ }
579
+ return alwaysIgnoredPatterns.some(pattern => pattern.test(basename));
580
+ }
581
+
582
+ /**
583
+ * Recursively removes junk files (always-ignored by npm) from a directory tree.
584
+ * @param {string} dir
585
+ */
586
+ async function removeJunkFiles(dir) {
587
+ const entries = await readdir(dir, { withFileTypes: true });
588
+ for (const entry of entries) {
589
+ if (hardIgnored.has(entry.name)) {
590
+ continue;
591
+ }
592
+ const fullPath = path.join(dir, entry.name);
593
+ if (isAlwaysIgnored(entry.name)) {
594
+ await rm(fullPath, { recursive: true, force: true });
595
+ } else if (entry.isDirectory()) {
596
+ await removeJunkFiles(fullPath);
597
+ }
598
+ }
599
+ }
600
+
601
+ /**
602
+ * Checks whether a root-level file is always included by npm (case-insensitive basename match).
603
+ * @param {string} file - The file path relative to the package root.
604
+ * @returns {boolean}
605
+ */
606
+ function isAlwaysIncludedByBasename(file) {
607
+ const dir = path.dirname(file);
608
+ if (dir !== '' && dir !== '.') {
609
+ return false;
610
+ }
611
+ const basenameWithoutExtension = path.basename(file, path.extname(file)).toUpperCase();
612
+ return alwaysIncludedBasenames.includes(basenameWithoutExtension);
613
+ }
614
+
615
+ /**
616
+ * Removes files from the working directory that are not included in the `files` array
617
+ * or the always-included list, then drops the `files` array from package.json.
618
+ * @param {PackageJson} pkg
619
+ * @param {Logger} logger
620
+ */
621
+ async function cleanupDir(pkg, logger) {
622
+ logger.update('cleaning up files...');
623
+
624
+ const alwaysIncludedFiles = getAlwaysIncludedFiles(pkg);
625
+ const filesEntries = /** @type {string[]} */ (pkg.files).map(normalizePath);
626
+
627
+ const entries = await readdir('.');
628
+
629
+ for (const entry of entries) {
630
+ if (hardIgnored.has(entry)) {
631
+ continue;
632
+ }
633
+
634
+ const normalized = normalizePath(entry);
635
+
636
+ // check if matched by files entries (exact or parent directory)
637
+ if (filesEntries.some(f => normalized === f || normalized.startsWith(`${f}/`))) {
638
+ continue;
639
+ }
640
+
641
+ // check if any files entry is under this directory
642
+ if (filesEntries.some(f => f.startsWith(`${normalized}/`))) {
643
+ // need to recurse into this directory for granular cleanup
644
+ await cleanupSubDir(normalized, filesEntries, alwaysIncludedFiles);
645
+ continue;
646
+ }
647
+
648
+ // check if always-included by exact path
649
+ if (alwaysIncludedFiles.includes(normalized)) {
650
+ continue;
651
+ }
652
+
653
+ // check if always-included by basename (root level)
654
+ if (isAlwaysIncludedByBasename(normalized)) {
655
+ continue;
656
+ }
657
+
658
+ // not matched - remove
659
+ await rm(entry, { recursive: true, force: true });
660
+ }
661
+
662
+ pkg.files = undefined;
663
+ }
664
+
665
+ /**
666
+ * Recursively cleans up a subdirectory, keeping only files matched by the files entries
667
+ * or always-included files.
668
+ * @param {string} dir
669
+ * @param {string[]} filesEntries
670
+ * @param {string[]} alwaysIncludedFiles
671
+ */
672
+ async function cleanupSubDir(dir, filesEntries, alwaysIncludedFiles) {
673
+ const entries = await readdir(dir);
674
+
675
+ for (const entry of entries) {
676
+ if (hardIgnored.has(entry)) {
677
+ continue;
678
+ }
679
+
680
+ const fullPath = path.join(dir, entry);
681
+
682
+ const normalized = normalizePath(fullPath);
683
+
684
+ // check if matched by files entries
685
+ if (filesEntries.some(f => normalized === f || normalized.startsWith(`${f}/`))) {
686
+ continue;
687
+ }
688
+
689
+ // check if any files entry is under this path
690
+ if (filesEntries.some(f => f.startsWith(`${normalized}/`))) {
691
+ await cleanupSubDir(normalized, filesEntries, alwaysIncludedFiles);
692
+ continue;
693
+ }
694
+
695
+ // check if always-included by exact path
696
+ if (alwaysIncludedFiles.includes(normalized)) {
697
+ continue;
698
+ }
699
+
700
+ // not matched - remove
701
+ await rm(fullPath, { recursive: true, force: true });
702
+ }
703
+
704
+ // remove the directory if it's now empty
705
+ const remaining = await readdir(dir);
706
+ if (remaining.length === 0) {
707
+ await rm(dir, { recursive: true, force: true });
708
+ }
709
+ }
710
+
509
711
  function getScriptsData() {
510
- const libraryScripts = new Set([
511
- 'preinstall',
512
- 'install',
513
- 'postinstall',
514
- 'prepublish',
515
- 'preprepare',
516
- 'prepare',
517
- 'postprepare',
518
- ]);
712
+ const libraryScripts = new Set(['preinstall', 'install', 'postinstall', 'prepublish', 'preprepare', 'prepare', 'postprepare']);
519
713
 
520
714
  const appScripts = new Set([
521
715
  ...libraryScripts,