prod-files 0.1.4 → 0.2.0

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 +70 -20
  2. package/index.mjs +75 -75
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -36,8 +36,7 @@ Examples:
36
36
  arguments, so don't use equals signs:
37
37
  $ pf -i "**/foo" -e "**/*tsconfig*.json" node_modules/.pnpm
38
38
 
39
- Also with short-hand args the space between the key and the value can be
40
- omitted:
39
+ In short-hand args the space between the key and the value can be omitted:
41
40
  $ pf -i"**/foo" node_modules/.pnpm
42
41
 
43
42
  Usage:
@@ -51,16 +50,22 @@ Arguments:
51
50
  - yarn: 'node_modules' or 'node_modules/.store'
52
51
 
53
52
  Flags:
54
- -i, --include Glob patterns of extra files to be removed. Uses node's
55
- path.matchesGlob(), with one exception: patterns ending with
56
- slash '**/foo/' are marked as directories.
53
+ -i, --include Extra custom glob pattern. Uses node's path.matchesGlob(),
54
+ with one exception: patterns ending with slash '**/foo/' are
55
+ marked as directories. Can have multiple.
57
56
 
58
57
  -e, --exclude Exclude existing glob patterns if the script is too
59
- aggressive. Must be exact match.
58
+ aggressive. Must be exact match. Can have multiple.
59
+
60
+ -d, --dryRun Nothing is removed and the paths are printed out.
60
61
 
61
62
  -h, --help Prints out the help.
62
63
 
63
- -g, --globs Prints out the default globs.
64
+ -g, --showGlobs
65
+ Prints out the default globs.
66
+
67
+ --noGlobs Disable default glob patterns, only use patterns from
68
+ --include.
64
69
 
65
70
  -n, --noSize Skips the size calculation.
66
71
 
@@ -88,6 +93,43 @@ Different package manager `node_modules` paths:
88
93
  | yarn | pnpm | `node_modules/.store` | same as pnpm |
89
94
  | yarn | pnp | no-op | no node_modules |
90
95
 
96
+ ### Provide your own globs
97
+
98
+ The default globs can de disabled with `--noGlobs` flag, and only globs in
99
+ `--include` are matched:
100
+
101
+ ```sh
102
+ pnpm prod-files --noGlobs -i "**/custom/" -i "**/*.html" node_modules/.pnpm
103
+ ```
104
+
105
+ ### Dy run
106
+
107
+ The `--dry-run` flag does not delete anything and prints out the paths:
108
+
109
+ ```sh
110
+ pnpm prod-files --dryRun node_modules/.pnpm
111
+ ```
112
+
113
+ ### Use as a search
114
+
115
+ You can use `prod-files` to search any dir if you set `--dryRun` and
116
+ `--noGlobs`, and provide the search term in `--include`:
117
+
118
+ ```sh
119
+ pnpm prod-files --noGlobs --dryRun --include="**/bower.json" node_modules/.pnpm
120
+
121
+ Pruning (--dryRun, nothing deleted): node_modules/.pnpm
122
+ node_modules/.pnpm/less@4.3.0/node_modules/less/bower.json
123
+ node_modules/.pnpm/papaparse@5.5.3/node_modules/papaparse/bower.json
124
+ node_modules/.pnpm/reflux@0.4.1_react@19.2.3/node_modules/reflux/bower.json
125
+ node_modules/.pnpm/sprintf-js@1.0.3/node_modules/sprintf-js/bower.json
126
+ ```
127
+
128
+ Sentry's `node_modules` has 4 bower config files :)
129
+
130
+ > [!CAUTION]\
131
+ > You're wielding `rm -rf` here, always remember to set `--dryRun`!
132
+
91
133
  ### Dockerfile example
92
134
 
93
135
  Simple yet somewhat realistic example usage in Dockerfile for an app named `foo`
@@ -101,25 +143,27 @@ RUN pnpm fetch
101
143
  COPY . ./
102
144
  RUN pnpm i --offline --frozen-lockfile
103
145
  RUN pnpm build
104
- RUN pnpm -F=foo --prod deploy /my-app/foo
105
- # Run it as the last command of the build step. NOTE: if you installed with
106
- # --prod flag, prod-files needs to be a prod dep. Or use pnpx/npx/yarn dlx
107
- RUN pnpm prod-files my-app/foo/node_modules/.pnpm
146
+ RUN pnpm -F=foo --prod deploy /foo
147
+ # Run it as the last command of the build step.
148
+ # NOTE: with --prod, the script needs to be a prod dep. Or use pnpx/npx/yarn dlx
149
+ WORKDIR /foo
150
+ RUN pnpm prod-files node_modules/.pnpm --noSize
108
151
 
109
152
  # Enjoy your new slimmer image
110
153
  FROM node:lts-alpine3.19 AS foo
111
- COPY --from=base /my-app/foo /my-app/foo
112
- WORKDIR /myapp/foo
154
+ COPY --from=base foo/build /foo/build
155
+ COPY --from=base foo/node_modules /foo/node_modules
156
+ WORKDIR /foo
113
157
  CMD node build/server.js
114
158
  ```
115
159
 
116
- Or use wget in if you don't have a package manager in your env (there are
117
- certain risks involved when you execute files downloaded from the net, if I get
160
+ Or use `wget` if you don't have a package manager in your env (there are certain
161
+ risks involved when you execute files downloaded from the net, if I get
118
162
  comprised that file can have anything):
119
163
 
120
164
  ```dockerfile
121
- RUN wget -O pf.js https://raw.githubusercontent.com/hilja/prod-files/refs/heads/main/index.mjs
122
- RUN node pf.js my-app/foo/node_modules/.pnpm
165
+ RUN wget -O pf.mjs https://raw.githubusercontent.com/hilja/prod-files/refs/heads/main/index.mjs
166
+ RUN node pf.mjs /foo/node_modules/.pnpm
123
167
  ```
124
168
 
125
169
  ## Development
@@ -138,7 +182,7 @@ pnpm test
138
182
 
139
183
  ### End to end tests
140
184
 
141
- In `test-project` directory has Sentry's `package.json`. You can run the script
185
+ The `test-project` directory has Sentry's `package.json`. You can run the script
142
186
  against it to see how it does in real-world use and get some timing data.
143
187
 
144
188
  ```sh
@@ -148,13 +192,19 @@ pnpm test:e2e
148
192
  pnpm test:e2e --noSize
149
193
  ```
150
194
 
151
- The nuke command removes `node_modules` and prunes the store:
195
+ If you're testing `--dryRun`, use `test:e2e:run`, it does not reinstall:
196
+
197
+ ```sh
198
+ pnpm test:r2e:run
199
+ ```
200
+
201
+ The nuke command removes `test-project/node_modules` and prunes the store:
152
202
 
153
203
  ```sh
154
204
  pnpm test:e2e:nuke
155
205
  ```
156
206
 
157
- There's also a simple script to print the weight of `test-project/node_modules/`
207
+ There's also a simple script to print the weight of `test-project/node_modules`
158
208
  using `du`. You can run it before and after to see more detailed results:
159
209
 
160
210
  ```sh
package/index.mjs CHANGED
@@ -21,13 +21,16 @@ const defaultGlobs = [
21
21
  '**/*.md',
22
22
  '**/*.map',
23
23
  '**/*.{,m,c}ts',
24
- '**/*.tsx',
24
+ '**/*.{j,t}sx',
25
25
  '**/doc{,s}/',
26
26
 
27
27
  // Types
28
28
  '**/*tsconfig*.json',
29
29
  '**/*.tsbuildinfo',
30
30
  '**/flow-typed/',
31
+ '**/.flowconfig',
32
+ '**/*.flow',
33
+ '**/__typings__/',
31
34
 
32
35
  // Sensitive
33
36
  '**/.env*',
@@ -39,6 +42,8 @@ const defaultGlobs = [
39
42
  '**/yarn.lock',
40
43
  '**/bun.lock',
41
44
  '**/bunfig.toml',
45
+ '**/bower.json',
46
+ '**/node_modules/.bin/',
42
47
 
43
48
  // IDE
44
49
  '**/.idea/',
@@ -54,6 +59,7 @@ const defaultGlobs = [
54
59
  '**/contributing',
55
60
  '**/CONTRIBUTORS',
56
61
  '**/contributors',
62
+ '**/node_modules/**/man/',
57
63
 
58
64
  // CI/CD
59
65
  '**/.github/',
@@ -69,6 +75,7 @@ const defaultGlobs = [
69
75
  // Tests
70
76
  '**/test{,s}/',
71
77
  '**/spec{,s}/',
78
+ '**/**.{test,spec}.{js,mjs}',
72
79
  '**/__{mocks,tests}__/',
73
80
  '**/jest.*.{js,ts}',
74
81
  '**/vitest.*.ts',
@@ -77,6 +84,8 @@ const defaultGlobs = [
77
84
  '**/wallaby.{js,ts}',
78
85
  '**/playwright.config.{js,ts}',
79
86
  '**/.mocharc*',
87
+ '**/.zuul.yml',
88
+ '**/.coveralls.yml',
80
89
 
81
90
  // Build/bundle config
82
91
  '**/{rollup,rolldown,vite}.config.{js,ts,mjs}',
@@ -100,8 +109,9 @@ const defaultGlobs = [
100
109
  '**/*.png',
101
110
 
102
111
  // Linters and formatters
103
- '**/eslint*.{json,jsonc,ts}',
104
- '**/.eslintrc',
112
+ '**/eslint*.{json,jsonc,ts,js,mjs}',
113
+ '**/.eslintrc*',
114
+ '**/.eslintignore',
105
115
  '**/prettier.config*',
106
116
  '**/.prettier*',
107
117
  '**/.ox{lint,fmt}rc.json{,c}',
@@ -149,6 +159,11 @@ const defaultGlobs = [
149
159
  '**/*.coffee',
150
160
 
151
161
  // Misc
162
+ '**/KEYS',
163
+ '**/.spmignore',
164
+ '**/.editorconfig',
165
+ '**/component.json',
166
+ '**/*.jison',
152
167
  '**/.jscpd',
153
168
  '**/*.jst',
154
169
  '**/*.log',
@@ -189,8 +204,7 @@ function usage() {
189
204
  arguments, so don't use equals signs:
190
205
  $ pf -i "**/foo" -e "**/*tsconfig*.json" node_modules/.pnpm
191
206
 
192
- Also with short-hand args the space between the key and the value can be
193
- omitted:
207
+ In short-hand args the space between the key and the value can be omitted:
194
208
  $ pf -i"**/foo" node_modules/.pnpm
195
209
 
196
210
  Usage:
@@ -204,16 +218,22 @@ function usage() {
204
218
  - yarn: 'node_modules' or 'node_modules/.store'
205
219
 
206
220
  Flags:
207
- -i, --include Glob patterns of extra files to be removed. Uses node's
208
- path.matchesGlob(), with one exception: patterns ending with
209
- slash '**/foo/' are marked as directories.
221
+ -i, --include Extra custom glob pattern. Uses node's path.matchesGlob(),
222
+ with one exception: patterns ending with slash '**/foo/' are
223
+ marked as directories. Can have multiple.
210
224
 
211
225
  -e, --exclude Exclude existing glob patterns if the script is too
212
- aggressive. Must be exact match.
226
+ aggressive. Must be exact match. Can have multiple.
227
+
228
+ -d, --dryRun Nothing is removed and the paths are printed out.
213
229
 
214
230
  -h, --help Prints out the help.
215
231
 
216
- -g, --globs Prints out the default globs.
232
+ -g, --showGlobs
233
+ Prints out the default globs.
234
+
235
+ --noGlobs Disable default glob patterns, only use patterns from
236
+ --include.
217
237
 
218
238
  -n, --noSize Skips the size calculation.
219
239
 
@@ -264,8 +284,7 @@ const style = (color, args) => args.map(a => styleText(color, String(a)))
264
284
  * @type {Logger}
265
285
  */
266
286
  export const log = {
267
- error: (...a) =>
268
- quiet ? undefined : console.error.apply(console, style('red', a)),
287
+ error: (...a) => console.error.apply(console, style('red', a)),
269
288
  info: (...a) =>
270
289
  quiet ? undefined : console.info.apply(console, style('blue', a)),
271
290
  log: (...a) => (quiet ? undefined : console.log.apply(console, a)),
@@ -312,10 +331,10 @@ async function treeSize(path) {
312
331
  * @param {number} originalSize
313
332
  * @param {number} prunedSize
314
333
  */
315
- function calcSize(originalSize, prunedSize) {
334
+ export function calcSize(originalSize, prunedSize) {
316
335
  const diff = originalSize - prunedSize
317
336
  const diffMb = `${(diff / 1024).toFixed(1)} MB`
318
- const diffPercent = `${((diff / prunedSize) * 100).toFixed(1)}%`
337
+ const diffPercent = `${((diff / originalSize) * 100).toFixed(1)}%`
319
338
 
320
339
  return `${diffPercent} (${diffMb})`
321
340
  }
@@ -352,9 +371,11 @@ export function printDiff({
352
371
  * @property {string} [path] - Path to node_modules
353
372
  * @property {string[]} include - New glob pattern
354
373
  * @property {string[]} exclude - Existing glob pattern
374
+ * @property {boolean} dryRun - Print out the files to be removed, no deletion
355
375
  * @property {boolean} help - Prints help
376
+ * @property {boolean} showGlobs - Prints globs
377
+ * @property {boolean} noGlobs - Disable default glob patterns
356
378
  * @property {boolean} noSize - Don't show size savings
357
- * @property {boolean} globs - Prints globs
358
379
  * @property {boolean} quiet - Suppress console.log output
359
380
  */
360
381
 
@@ -362,7 +383,6 @@ export function printDiff({
362
383
  * Parse the command-line arguments into an object
363
384
  * @returns {Args}
364
385
  */
365
-
366
386
  function handleArgs() {
367
387
  try {
368
388
  const {
@@ -373,8 +393,10 @@ function handleArgs() {
373
393
  options: {
374
394
  include: { type: 'string', short: 'i', default: [], multiple: true },
375
395
  exclude: { type: 'string', short: 'e', default: [], multiple: true },
396
+ dryRun: { type: 'boolean', short: 'd', default: false },
376
397
  help: { type: 'boolean', short: 'h', default: false },
377
- globs: { type: 'boolean', short: 'g', default: false },
398
+ showGlobs: { type: 'boolean', short: 'g', default: false },
399
+ noGlobs: { type: 'boolean', default: false },
378
400
  noSize: { type: 'boolean', short: 'n', default: false },
379
401
  quiet: { type: 'boolean', short: 'q', default: false },
380
402
  },
@@ -685,36 +707,6 @@ export function findJunkFiles(files, compiledGlobs = defaultCompiledGlobs) {
685
707
  return junkFiles
686
708
  }
687
709
 
688
- /**
689
- * Removes descendant paths when an ancestor path is already present
690
- * @param {string[]} paths - Paths to compact
691
- * @returns {string[]} A sorted list without redundant child paths
692
- */
693
- export function compactPaths(paths) {
694
- /** @type {Set<string>} */
695
- const seen = new Set()
696
- /** @type {string[]} */
697
- const compact = []
698
-
699
- // Sorting guarantees parents are encountered before their nested children
700
- for (const path of paths.toSorted()) {
701
- let i = path.lastIndexOf('/')
702
-
703
- while (i > 0) {
704
- if (seen.has(path.slice(0, i))) break
705
- i = path.lastIndexOf('/', i - 1)
706
- }
707
-
708
- // Skip this path if one of its ancestors has already been kept
709
- if (i > 0) continue
710
-
711
- seen.add(path)
712
- compact.push(path)
713
- }
714
-
715
- return compact
716
- }
717
-
718
710
  /**
719
711
  * @typedef {object} WalkResult
720
712
  * @property {string[]} removed - Compacted list of removed paths
@@ -725,12 +717,11 @@ export function compactPaths(paths) {
725
717
  * Parallel walker that finds junk, removes it, and cleans empty dirs in one
726
718
  * pass. Skips recursing into junk directories (implicit path compacting) and
727
719
  * removes empty ancestors bottom-up as the recursion unwinds.
728
- * @param {string} rootDir - The directory to walk
729
720
  * @param {CompiledGlobs} compiledGlobs - Precompiled glob matchers
730
- * @param {boolean} trackSize - Whether to collect byte sizes of removed items
721
+ * @param {ArgsWithPath} opts - The args object
731
722
  * @returns {Promise<WalkResult>}
732
723
  */
733
- async function walkAndPrune(rootDir, compiledGlobs, trackSize) {
724
+ async function walkAndPrune(compiledGlobs, opts) {
734
725
  /** @type {string[]} */
735
726
  const removed = []
736
727
  let removedBlocks = 0
@@ -790,8 +781,9 @@ async function walkAndPrune(rootDir, compiledGlobs, trackSize) {
790
781
  const [junkSizes, walkResults] = await Promise.all([
791
782
  Promise.all(
792
783
  junkPaths.map(async p => {
793
- const size = trackSize ? await treeSize(p) : 0
794
- await fs.rm(p, { recursive: true, force: true })
784
+ const size = opts.noSize ? 0 : await treeSize(p)
785
+ // No rm if --dryRun
786
+ if (!opts.dryRun) await fs.rm(p, { recursive: true, force: true })
795
787
  return size
796
788
  })
797
789
  ),
@@ -803,50 +795,55 @@ async function walkAndPrune(rootDir, compiledGlobs, trackSize) {
803
795
  // Subdirs that became empty after pruning their contents
804
796
  const emptyDirs = keptDirPaths.filter((_, i) => !walkResults[i])
805
797
  if (emptyDirs.length > 0) {
806
- await Promise.all(emptyDirs.map(d => fs.rmdir(d)))
798
+ // No rm if --dryRun
799
+ if (!opts.dryRun) await Promise.all(emptyDirs.map(d => fs.rmdir(d)))
807
800
  }
808
801
 
809
802
  return keptFiles + keptDirPaths.length - emptyDirs.length > 0
810
803
  }
811
804
 
812
- await walkDir(rootDir)
805
+ await walkDir(opts.path)
813
806
  return { removed, removedBlocks }
814
807
  }
815
808
 
816
809
  /**
817
- * @typedef {Args & { path: string }} ArgsWithRequiredPath
810
+ * @typedef {Args & { path: string }} ArgsWithPath
818
811
  */
819
812
 
820
813
  /**
821
814
  * Removes unneeded files from node_modules
822
- * @param {ArgsWithRequiredPath} opts
815
+ * @param {ArgsWithPath} opts
823
816
  */
824
817
  export async function prune(opts) {
825
818
  const startTime = Date.now()
826
- log.info('Pruning:', opts.path)
819
+ const dryRunMsg = opts.dryRun ? ' (--dryRun, nothing deleted)' : ''
820
+ log.info(`Pruning${dryRunMsg}:`, opts.path)
827
821
 
828
822
  // Fire early so du runs concurrently with the walk
829
823
  const sizePromise = opts.noSize ? undefined : getSize(opts.path)
830
824
  const excludedGlobs = new Set(opts.exclude)
831
- const activeGlobs = [...defaultGlobs, ...opts.include].filter(
832
- glob => !excludedGlobs.has(glob)
833
- )
825
+ const activeGlobs = [
826
+ ...(opts.noGlobs ? [] : defaultGlobs),
827
+ ...opts.include,
828
+ ].filter(glob => !excludedGlobs.has(glob))
834
829
  const compiledGlobs = compileGlobs(activeGlobs)
835
830
 
836
831
  /** @type {WalkResult} */
837
832
  let result
838
833
  try {
839
- result = await walkAndPrune(opts.path, compiledGlobs, !opts.noSize)
834
+ result = await walkAndPrune(compiledGlobs, opts)
840
835
  } catch (err) {
841
836
  throw bail(undefined, err)
842
837
  }
843
838
 
844
- printDiff({
845
- itemCount: result.removed.length,
846
- removedBytes: opts.noSize ? undefined : result.removedBlocks,
847
- originalSize: sizePromise ? await sizePromise : undefined,
848
- startTime,
849
- })
839
+ if (!opts.dryRun) {
840
+ printDiff({
841
+ itemCount: result.removed.length,
842
+ removedBytes: opts.noSize ? undefined : result.removedBlocks,
843
+ originalSize: sizePromise ? await sizePromise : undefined,
844
+ startTime,
845
+ })
846
+ }
850
847
 
851
848
  return result.removed
852
849
  }
@@ -865,21 +862,24 @@ if (runAsScript) {
865
862
  process.exit(0)
866
863
  }
867
864
 
868
- if (args.globs) {
865
+ if (args.showGlobs) {
869
866
  log.log(JSON.stringify(defaultGlobs, null, 2))
870
867
  process.exit(0)
871
868
  }
872
869
 
873
- // Should have a path by now
870
+ // From this point forward we should have a path
874
871
  if (!args.path) {
875
- throw bail(
876
- undefined,
877
- 'Path not defined. Usage: prod-files <path-to-node-modules>'
878
- )
872
+ throw bail(undefined, 'Path not defined. Usage: prod-files <path>')
879
873
  }
880
874
 
881
- const argsWithPath = /** @type {ArgsWithRequiredPath} */ (args)
875
+ const argsWithPath = /** @type {ArgsWithPath} */ (args)
882
876
 
883
877
  await validateNodeModulesPath(argsWithPath.path)
884
- await prune(argsWithPath)
878
+ const pruned = await prune(argsWithPath)
879
+
880
+ // Print out paths if --dryRun
881
+ if (args.dryRun) {
882
+ if (pruned.length === 0) log.log('No results')
883
+ for (const item of pruned) log.log(item)
884
+ }
885
885
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prod-files",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Keep only prod files by pruning non-prod files from node_modules before deploying",
5
5
  "keywords": [
6
6
  "clean",
@@ -42,6 +42,7 @@
42
42
  "test": "node --test",
43
43
  "test:e2e": "cd test-project && bash prepare_test.sh && time node ../index.mjs node_modules/.pnpm",
44
44
  "test:e2e:nuke": "trash ./test-project/node_modules ./test-project/pnpm-lock.yaml && pnpm store prune",
45
+ "test:e2e:run": "cd test-project && node ../index.mjs node_modules/.pnpm",
45
46
  "test:e2e:weight": "du -sh ./test-project/node_modules/.pnpm | sort -h"
46
47
  }
47
48
  }