ncc collapses a Node module into one file, and its blind spot is anything dynamic
Compile a Node.js project into a single file. Supports TypeScript, binary addons, dynamic requires.
At a glance
- What is it?
- ncc is Vercel's zero-configuration CLI for compiling a Node.js module and its whole dependency tree into a single gcc-style file. It handles TypeScript, binary addons, and dynamic requires, and it does it through static analysis, which is also the single place where it can fail you.
- Who is it for?
- Reach for ncc when you are publishing a package, shipping app code to a serverless environment, or bundling a CLI into one file, and you want the whole tree in one file without writing bundler configuration. Do not reach for it for a browser bundle or a library target, because the stated design goal is Node.js programs in and Node.js out, and it does not attempt anything else.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 93 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- vercel/ncc9,844 ★ · +6
| Repository | 2026-09-22 | 2026-11-13 |
|---|---|---|
| vercel/ncc | 9.8k | 9.8k |
6 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
gcc-style means one command and one output file
The whole install is a global npm flag:
npm i -g @vercel/nccand the invocation follows the shape of a compiler rather than a bundler config:
$ ncc <cmd> <opts>$ ncc build input.js -o distThat writes a compact build of input.js into dist/index.js, defaulting to dist when -o is left out. The comparison the project draws is to gcc, and the design goals explain why that is the right mental model: zero configuration, TypeScript built in, support for all Node.js patterns and npm modules. The motivation list is narrower than the design goals, though, and it says so: publish minimal packages to npm, only ship relevant app code to serverless environments, don't waste time configuring bundlers, faster bootup and less I/O overhead, and a compiled language-like experience in the shape of go. That is a tool for shrinking a server side artifact, not for authoring a frontend.
Every flag changes one thing, and -e is the escape hatch
The option surface is small enough to read in full, which is unusual for a bundler:
-o, --out [dir] Output directory for build (defaults to dist)
-m, --minify Minify output
-C, --no-cache Skip build cache population
-s, --source-map Generate source map
-a, --asset-builds Build nested JS assets recursively, useful for
when code is loaded as an asset eg for workers.
--no-source-map-register Skip source-map-register source map support
-e, --external [mod] Skip bundling 'mod'. Can be used many times
-q, --quiet Disable build summaries / non-error outputs
-w, --watch-e is the one to understand first. It leaves a module out of the bundle as a plain require, and it can be repeated, so a dependency that fails to bundle or that you would rather resolve at runtime gets named on the command line instead of being fought over. -a is the second, and it exists for code loaded as an asset rather than imported, which is the pattern a worker environment tends to hit. There is also a cache subcommand with clean, dir, and size, so you can inspect or drop the build cache without hunting for it, and -C skips populating it in the first place when a stale cache would be worse than a slow build.
The static evaluator is where dynamic asset loads break
There is one caveat in the project, and it is worth reading as a hard boundary rather than a footnote. Files and assets are relocated based on a static evaluator, which the README links to the webpack asset relocator loader, and dynamic non-statically analyzable asset loads may not work out correctly.
That sentence is the whole risk profile of the tool. ncc decides where your files land by reading your require calls ahead of time, not by watching what the program actually opens at runtime. So a path computed from a variable, a file name assembled from a config value, or an asset resolved by a runtime lookup is exactly the case the project declines to promise anything about. The README only says such loads may not work out correctly. If your project loads anything by a computed path, the honest move is to find it in package-support.md, or to exclude it with -e and accept the runtime require. Treat a green build as no evidence at all on this point.
The .mjs and .cjs extension rules decide what comes out
Module output is not something you configure; it follows the input and the nearest package boundary. If the input is an .mjs file, or a .js file inside a type module package boundary, an ES module output is created automatically. If the input file uses a .cjs extension, the corresponding output file uses .cjs as well.
The second rule exists for a specific reason: packages that want to use .js files as modules in native Node.js with a type module entry in their package.json. If you are porting such a package, feeding a .cjs entry to ncc preserves the extension and therefore preserves the semantics, instead of producing a .js file that Node would then try to load as an ES module and choke on. The practical consequence for a reader is that renaming your entry point to claim an output format is a mistake: ncc derives the format from the file you point it at and the package.json above it, and there is no flag in the option list to override that.
Zero configuration stops at the tsconfig.json
TypeScript needs no plugin and no separate build step. Point ncc at .ts or .tsx files and that is the entire requirement, with one exception: a tsconfig.json file is necessary. Most likely you want to indicate es2015 support:
{
"compilerOptions": {
"target": "es2015",
"moduleResolution": "node"
}
}So the zero configuration promise holds for the bundler and not for the compiler. Whatever target and resolution mode your tsconfig.json declares is what the emit honours, which means two projects that both say they use zero configuration can produce different output from identical source. The second constraint is about versions: if typescript is found in devDependencies, that version will be used. You cannot pick the compiler version from the ncc command line, and a global ncc install will happily borrow whichever TypeScript happens to sit in the project's devDependencies, so a lockfile that has drifted from what CI installs is a real source of build differences between machines.
Node.js programs in, Node.js programs out
One of the four design goals states the scope more plainly than any amount of feature description: only Node.js programs as input, only Node.js programs as output. That is a deliberate narrowing, and it is the reason there is no browser target, no library mode, and no format negotiation in the option list. All nine flags concern output directory, minification, cache, source maps, assets, externals, quiet mode, and watching.
The consequence for a reader is worth stating plainly. If your goal is a single file that Node runs, ncc is aimed at exactly that, and the examples directory backs it up with a hello-world, a multiple-bundles case, a programmatic case, and a typescript case. If your goal is a browser bundle, an ESM library for other bundlers to consume, or anything targeting a runtime with no node_modules and no require, then no flag here will move you there, and the design goal is telling you the tool was never going to. Choosing ncc and discovering that later costs more than reading the four bullets before you install.
The programmatic API hands you a watcher object, not a promise
For use inside a build script, ncc is required as a module and called with an input path and an options object. The options are the flags by another name: cache takes a custom path or false to disable it, externals is an array of modules to leave as requires of the build, filterAssetBase sets a directory outside of which assets are never emitted and defaults to process.cwd(), minify, sourceMap, and assetBuilds are booleans defaulting to false, and sourceMapBasePrefix defaults to treating sources as output relative. There is also a switch that adds source-map-support into the output file, at the cost of a larger artifact.
Watch mode is the part that surprises people. When watch: true is set, the build object is not a promise. It is a plain object with three members:
{
// handler re-run on each build completion
// watch errors are reported on "err"
handler (({ err, code, map, assets }) => { ... })
// handler re-run on each rebuild start
rebuild (() => {})
// close the watcher
void close ();
}So await ncc(...) will not work in watch mode, errors arrive on the handler's err rather than through a rejected promise, and a build script that forgets to call close leaves the process alive.
Only dist/ is published, and the test suite asks for 4 GB of heap
The published surface is narrow. package.json names dist as the only entry in files, points main at ./dist/ncc/index.js, and maps the bin entry to dist/ncc/cli.js, so the src/ tree you read on GitHub is not what you install. Inside the repository the version field reads 0.0.0-development, which is a placeholder replaced by the release tooling rather than a version you will ever see. The build runs through node scripts/build.js, and prepublishOnly runs that same script with --no-cache, so the artifact that ships is built without warm cache.
The test setup tells you what the tool is up against. Tests run jest with --expose-gc and a 4096 MB old space cap, and there is a separate build-test-binary step that runs node-gyp rebuild against a native test package and copies the resulting hello.node into the integration fixtures. The devDependency list includes native-addon packages such as canvas, plus cloud clients, firebase, auth0, slack, sentry and bugsnag alongside express and browserify-middleware.
Editorial conclusion
Reach for ncc when you are publishing a package, shipping app code to a serverless environment, or bundling a CLI into one file, and you want the whole tree in one file without writing bundler configuration. Do not reach for it for a browser bundle or a library target, because the stated design goal is Node.js programs in and Node.js out, and it does not attempt anything else. Before you commit, check the packages in your dependency list against package-support.md, then run the build and grep the output for the asset paths you load at runtime: anything that escapes static analysis is the case the project itself flags as not guaranteed.
Frequently asked questions
what is vercel ncc
ncc is a simple CLI from Vercel for compiling a Node.js module into a single file together with all its dependencies, in a gcc-style way. Design goals are zero configuration, built-in TypeScript, Node.js programs as both input and output, and support for all Node.js patterns and npm modules.
What is Vercel used for in this project?
The motivation section names two uses: publish minimal packages to npm, and only ship relevant app code to serverless environments. It also lists faster bootup time, less I/O overhead, and avoiding bundler configuration as reasons to compile with ncc.
How do I install and run ncc?
Install globally with `npm i -g @vercel/ncc`, then run `ncc build input.js -o dist` to write a compact build to dist/index.js. Use `ncc run input.js` to build into a temporary directory and execute it with full source map support, and `ncc help` or `ncc version` for the rest.
Does ncc support TypeScript?
Yes, built in. Point ncc at .ts or .tsx files, and a tsconfig.json is necessary, with target es2015 and moduleResolution node given as the likely starting point. If typescript is found in devDependencies, that version is the one used.
What can go wrong when ncc bundles an asset?
Files and assets are relocated based on a static evaluator, and dynamic non-statically analyzable asset loads may not work out correctly. Packages that need extra options for the static analysis are listed in package-support.md.
How do I exclude a dependency from an ncc bundle?
Use -e, --external [mod] to skip bundling a module, and it can be used many times. The programmatic form is the externals array, which leaves those packages as requires of the build.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/vercel-ncc)Star history chart for your README
[](https://hysenlabs.com/projects/vercel-ncc)Was this useful?