Keep only production related files in node_modules, remove files which are not
needed to run the app in production, so your final Docker images is smaller and
you spend less time and resources zooting ballast over internet.
Cuts anything from 10 to 70+ percent of weight, largely depending on how many source map files you have, which is usually the bulk of the weight. Comes handy if you’re dealing with limited resources or work at a scale of thousands of projects, or you’re just obsessed with small deployments.
It's relatively fast, prunes
Sentry's node_modules
in 1.8s (M2 MacBook). Prod deps only though, installed with pnpm i --prod, but
that's the common use-case anyway.
pnpm add prod-filesIt’s a single JavaScript file with no deps, so you can easily copy it to your project if you don’t want to install it.
Examples:
Basic usage:
$ prod-files node_modules/.pnpm
Short:
$ pf node_modules/.pnpm
Since we’re just raw-dogging parseArgs, the short args don’t support inline
arguments, so don't use equals signs:
$ pf -i "**/foo" -e "**/*tsconfig*.json" node_modules/.pnpm
In short-hand args the space between the key and the value can be omitted:
$ pf -i"**/foo" node_modules/.pnpm
Usage:
prod-files [flags] path
pf [flags] path
Arguments:
path Relative or absolute path to node_modules directory:
- pnpm: 'node_modules/.pnpm'
- npm: 'node_modules'
- yarn: 'node_modules' or 'node_modules/.store'
Flags:
-i, --include Extra custom glob pattern. Uses node's path.matchesGlob(),
with one exception: patterns ending with slash '**/foo/' are
marked as directories. Can have multiple.
-e, --exclude Exclude existing glob patterns if the script is too
aggressive. Must be exact match. Can have multiple.
-d, --dryRun Nothing is removed and the paths are printed out.
-h, --help Prints out the help.
-g, --showGlobs
Prints out the default globs.
--noGlobs Disable default glob patterns, only use patterns from
--include.
-n, --noSize Skips the size calculation.
-q, --quiet Quiet output, suppresses stdout.
With a package manager:
pnpm prod-files node_modules/.pnpm
# Short
pnpm pf node_modules/.pnpm
# pnpx/npx
pnpx prod-files node_modules/.pnpmDifferent package manager node_modules paths:
| Manager | Linker | Path | Description |
|---|---|---|---|
| pnpm | - | node_modules/.pnpm |
hard-linked |
| npm | - | node_modules |
the good old |
| yarn v1 | - | node_modules |
the good old |
| yarn | node-modules | node_modules |
the good old |
| yarn | pnpm | node_modules/.store |
same as pnpm |
| yarn | pnp | no-op | no node_modules |
The default globs can de disabled with --noGlobs flag, and only globs in
--include are matched:
pnpm prod-files --noGlobs -i "**/custom/" -i "**/*.html" node_modules/.pnpmThe --dry-run flag does not delete anything and prints out the paths:
pnpm prod-files --dryRun node_modules/.pnpmYou can use prod-files to search any dir if you set --dryRun and
--noGlobs, and provide the search term in --include:
pnpm prod-files --noGlobs --dryRun --include="**/bower.json" node_modules/.pnpm
Pruning (--dryRun, nothing deleted): node_modules/.pnpm
node_modules/.pnpm/less@4.3.0/node_modules/less/bower.json
node_modules/.pnpm/papaparse@5.5.3/node_modules/papaparse/bower.json
node_modules/.pnpm/reflux@0.4.1_react@19.2.3/node_modules/reflux/bower.json
node_modules/.pnpm/sprintf-js@1.0.3/node_modules/sprintf-js/bower.jsonSentry's node_modules has 4 bower config files :)
Caution
You're wielding rm -rf here, always remember to set --dryRun!
Simple yet somewhat realistic example usage in Dockerfile for an app named foo
using pnpm:
FROM node:lts-alpine3.19 AS base
WORKDIR /usr/src/app
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm fetch
COPY . ./
RUN pnpm i --offline --frozen-lockfile
RUN pnpm build
RUN pnpm -F=foo --prod deploy /foo
# Run it as the last command of the build step.
# NOTE: with --prod, the script needs to be a prod dep. Or use pnpx/npx/yarn dlx
WORKDIR /foo
RUN pnpm prod-files node_modules/.pnpm --noSize
# Enjoy your new slimmer image
FROM node:lts-alpine3.19 AS foo
COPY --from=base foo/build /foo/build
COPY --from=base foo/node_modules /foo/node_modules
WORKDIR /foo
CMD node build/server.jsOr use wget if you don't have a package manager in your env (there are certain
risks involved when you execute files downloaded from the net, if I get
comprised that file can have anything):
RUN wget -O pf.mjs https://raw.githubusercontent.com/hilja/prod-files/refs/heads/main/index.mjs
RUN node pf.mjs /foo/node_modules/.pnpmpnpm iUnit tests are written with node's test utils.
pnpm testThe test-project directory has Sentry's package.json. You can run the script
against it to see how it does in real-world use and get some timing data.
# Re-installs the packages and runs the script on it
pnpm test:e2e
# Disable size reportings since it adds 200-300ms
pnpm test:e2e --noSizeIf you're testing --dryRun, use test:e2e:run, it does not reinstall:
pnpm test:r2e:runThe nuke command removes test-project/node_modules and prunes the store:
pnpm test:e2e:nukeThere's also a simple script to print the weight of test-project/node_modules
using du. You can run it before and after to see more detailed results:
pnpm test:e2e:weight- npmprune (bash)
- node-prune (go)
- clean-modules (node)