Skip to content

hilja/prod-files

Repository files navigation

prod-files

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.

Install

pnpm add prod-files

It’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.

Usage

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/.pnpm

Different 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

Provide your own globs

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/.pnpm

Dy run

The --dry-run flag does not delete anything and prints out the paths:

pnpm prod-files --dryRun node_modules/.pnpm

Use as a search

You 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.json

Sentry's node_modules has 4 bower config files :)

Caution

You're wielding rm -rf here, always remember to set --dryRun!

Dockerfile example

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.js

Or 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/.pnpm

Development

pnpm i

Unit tests

Unit tests are written with node's test utils.

pnpm test

End to end tests

The 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 --noSize

If you're testing --dryRun, use test:e2e:run, it does not reinstall:

pnpm test:r2e:run

The nuke command removes test-project/node_modules and prunes the store:

pnpm test:e2e:nuke

There'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

Prior art

About

Prunes node_modules of non-prod files, fast!

Resources

License

Stars

Watchers

Forks

Contributors