A tree-shakeability analyzer for npm packages. Tells you whether your package can be fully tree-shaken by Rollup, and when it can't, points at the specific files, exports, and likely causes preventing it.
Built on Effect, @effect/cli, and Rollup.
When you publish a library, consumers' bundlers (webpack, Rollup, Vite, esbuild) try to eliminate unused exports from your package — that's tree-shaking. If your package isn't shakeable, every consumer who imports a single function pulls in your entire library, inflating their bundle size.
Tree-shakeability isn't visible from the outside. You can ship what looks like a clean ESM library and still have it be unshakeable due to a single Object.defineProperty call at module scope, a missing "sideEffects": false in package.json, or a transitive CJS dependency. This tool surfaces those problems.
The technique is the same one used by Rich Harris's agadoo: bundle your package as a side-effect-only import (import "your-package" with no bindings used) and see what Rollup couldn't eliminate. Anything that survives is what's preventing tree-shaking.
pnpm add -D @wolfcola/treeshake-checkFrom any package directory:
pnpm treeshake-checkYou'll get one of two outcomes:
- Fully tree-shakeable — ASCII tree celebration plus any recommendations for
package.jsonimprovements. - Has side effects — a per-module breakdown of what survived, with diagnostic info for each file.
| Flag | Alias | Description |
|---|---|---|
--cwd <path> |
-C |
Directory containing package.json. Defaults to the current working directory. |
--entry <path> |
-e |
Analyze a specific entry file directly, skipping package.json resolution. |
--json |
Emit machine-readable JSON instead of human output. | |
--quiet |
-q |
Suppress all output; rely on the exit code only. |
--top <n> |
Show only the N modules with the largest surviving byte count. |
Plus the standard --help, --version, --wizard, and --completions <shell> flags from @effect/cli.
Check the current package:
pnpm treeshake-checkCheck a different package in the workspace:
pnpm treeshake-check --cwd packages/my-sdkCheck a specific built file directly:
pnpm treeshake-check --entry dist/index.jsShow only the worst 5 offenders:
pnpm treeshake-check --top 5JSON output for CI tooling:
pnpm treeshake-check --json | jq '.modules[] | {id, renderedLength, suspectedCauses}'The analyzer uses AST-based heuristics to classify surviving code:
| Cause | Description |
|---|---|
EnumPattern |
TypeScript enum compiled to IIFE |
CommonJsContamination |
require(), module.exports, __esModule markers |
PrototypeMutation |
Object.defineProperty, .prototype.x = ... |
GlobalAssignment |
Assignment to window, globalThis, self, or global |
UnannotatedCall |
Top-level function call without /*#__PURE__*/ |
TopLevelSideEffect |
Top-level statement with observable effects |
Unknown |
None of the above patterns matched |
Labels are heuristic — a starting point for investigation, not a verdict.
import { Effect } from 'effect';
import { NodeContext } from '@effect/platform-node';
import { checkPackage } from '@wolfcola/treeshake-check';
const program = Effect.gen(function* () {
const result = yield* checkPackage('./packages/sdk');
if (result._tag === 'FullyTreeshakeable') {
return true;
}
for (const m of result.modules) {
console.log(`${m.id}: ${m.renderedLength}/${m.originalLength} bytes`);
console.log(` causes: ${m.suspectedCauses.join(', ')}`);
}
return false;
});
const isShakeable = await Effect.runPromise(program.pipe(Effect.provide(NodeContext.layer)));treeshake-check exits with code 1 when a package isn't fully shakeable, so it composes naturally as a quality gate.
- name: Tree-shake check
run: pnpm -r --filter "./packages/*" exec treeshake-check --top 5{
"scripts": {
"prepublishOnly": "treeshake-check --quiet"
}
}pnpm -r --parallel exec treeshake-check --top 3- Reads
package.jsonfrom the target directory and resolves the entry point (exports→module→main). - Constructs a synthetic Rollup entry that imports the target as a side-effect-only import:
import "/absolute/path/to/entry.js". - Runs Rollup with default tree-shaking enabled.
- Inspects
chunk.modulesfor per-modulerenderedLength,renderedExports, andremovedExports. - Classifies surviving code by AST analysis (with regex fallback for unparseable output).
- Reports per-module statistics, surviving code, and
package.jsonrecommendations.
- agadoo by Rich Harris — same technique, the original implementation. This package adds richer diagnostics, structured output, and Effect-based composition.
- bundlephobia — measures the post-shake size from a consumer's perspective rather than analyzing why shaking succeeds or fails.