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/CHANGELOG.md +17 -0
- package/README.md +217 -1
- package/biome.json +62 -0
- package/index.js +15 -11
- package/package.json +10 -2
- package/pnpm-workspace.yaml +3 -1
- package/prune.js +353 -159
- package/renovate.json +6 -0
- package/tests/test.js +1 -1
- package/tests/tests.json +334 -32
- package/tsconfig.json +17 -0
- package/.prettierrc +0 -6
- package/jsconfig.json +0 -12
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
|
-
*
|
|
15
|
-
*
|
|
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 {
|
|
27
|
-
* @property {
|
|
28
|
-
* @property {
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
throw new Error('could not find dist folder');
|
|
257
|
-
}
|
|
272
|
+
logger.update(`flattening ${distDirs.join(', ')}...`);
|
|
258
273
|
|
|
259
|
-
|
|
274
|
+
// collect files from all dist directories
|
|
260
275
|
|
|
261
|
-
|
|
276
|
+
/** @type {Map<string, { distDir: string, relativeDistDir: string, files: string[] }>} */
|
|
277
|
+
const distDirInfo = new Map();
|
|
262
278
|
|
|
263
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
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
|
|
351
|
-
const pkgClone = cloneAndUpdate(pkg,
|
|
352
|
-
allReferencesSet.has(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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
464
|
-
|
|
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.
|
|
470
|
-
const childPath = entry.parentPath
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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,
|