Skip to content

Commit 00a05cd

Browse files
ryanbas21claude
andcommitted
docs(treeshake-check): add README for @wolfcola/treeshake-check
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5ec1022 commit 00a05cd

2 files changed

Lines changed: 167 additions & 0 deletions

File tree

packages/treeshake-check/README.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# @wolfcola/treeshake-check
2+
3+
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.
4+
5+
Built on [Effect](https://effect.website), [@effect/cli](https://www.npmjs.com/package/@effect/cli), and [Rollup](https://rollupjs.org).
6+
7+
## Why this exists
8+
9+
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.
10+
11+
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.
12+
13+
The technique is the same one used by Rich Harris's [agadoo](https://www.npmjs.com/package/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.
14+
15+
## Installation
16+
17+
```bash
18+
pnpm add -D @wolfcola/treeshake-check
19+
```
20+
21+
## Usage
22+
23+
### Quickstart
24+
25+
From any package directory:
26+
27+
```bash
28+
pnpm treeshake-check
29+
```
30+
31+
You'll get one of two outcomes:
32+
33+
- **Fully tree-shakeable** — ASCII tree celebration plus any recommendations for `package.json` improvements.
34+
- **Has side effects** — a per-module breakdown of what survived, with diagnostic info for each file.
35+
36+
### Flags
37+
38+
| Flag | Alias | Description |
39+
| ---------------- | ----- | ------------------------------------------------------------------------------- |
40+
| `--cwd <path>` | `-C` | Directory containing `package.json`. Defaults to the current working directory. |
41+
| `--entry <path>` | `-e` | Analyze a specific entry file directly, skipping `package.json` resolution. |
42+
| `--json` | | Emit machine-readable JSON instead of human output. |
43+
| `--quiet` | `-q` | Suppress all output; rely on the exit code only. |
44+
| `--top <n>` | | Show only the N modules with the largest surviving byte count. |
45+
46+
Plus the standard `--help`, `--version`, `--wizard`, and `--completions <shell>` flags from `@effect/cli`.
47+
48+
### Examples
49+
50+
Check the current package:
51+
52+
```bash
53+
pnpm treeshake-check
54+
```
55+
56+
Check a different package in the workspace:
57+
58+
```bash
59+
pnpm treeshake-check --cwd packages/my-sdk
60+
```
61+
62+
Check a specific built file directly:
63+
64+
```bash
65+
pnpm treeshake-check --entry dist/index.js
66+
```
67+
68+
Show only the worst 5 offenders:
69+
70+
```bash
71+
pnpm treeshake-check --top 5
72+
```
73+
74+
JSON output for CI tooling:
75+
76+
```bash
77+
pnpm treeshake-check --json | jq '.modules[] | {id, renderedLength, suspectedCauses}'
78+
```
79+
80+
## Detected causes
81+
82+
The analyzer uses AST-based heuristics to classify surviving code:
83+
84+
| Cause | Description |
85+
| ----------------------- | --------------------------------------------------------- |
86+
| `EnumPattern` | TypeScript enum compiled to IIFE |
87+
| `CommonJsContamination` | `require()`, `module.exports`, `__esModule` markers |
88+
| `PrototypeMutation` | `Object.defineProperty`, `.prototype.x = ...` |
89+
| `GlobalAssignment` | Assignment to `window`, `globalThis`, `self`, or `global` |
90+
| `UnannotatedCall` | Top-level function call without `/*#__PURE__*/` |
91+
| `TopLevelSideEffect` | Top-level statement with observable effects |
92+
| `Unknown` | None of the above patterns matched |
93+
94+
Labels are heuristic — a starting point for investigation, not a verdict.
95+
96+
## Programmatic usage
97+
98+
```typescript
99+
import { Effect } from 'effect';
100+
import { NodeContext } from '@effect/platform-node';
101+
import { checkPackage } from '@wolfcola/treeshake-check';
102+
103+
const program = Effect.gen(function* () {
104+
const result = yield* checkPackage('./packages/sdk');
105+
106+
if (result._tag === 'FullyTreeshakeable') {
107+
return true;
108+
}
109+
110+
for (const m of result.modules) {
111+
console.log(`${m.id}: ${m.renderedLength}/${m.originalLength} bytes`);
112+
console.log(` causes: ${m.suspectedCauses.join(', ')}`);
113+
}
114+
return false;
115+
});
116+
117+
const isShakeable = await Effect.runPromise(program.pipe(Effect.provide(NodeContext.layer)));
118+
```
119+
120+
## CI integration
121+
122+
`treeshake-check` exits with code 1 when a package isn't fully shakeable, so it composes naturally as a quality gate.
123+
124+
### GitHub Actions
125+
126+
```yaml
127+
- name: Tree-shake check
128+
run: pnpm -r --filter "./packages/*" exec treeshake-check --top 5
129+
```
130+
131+
### As a pre-publish hook
132+
133+
```json
134+
{
135+
"scripts": {
136+
"prepublishOnly": "treeshake-check --quiet"
137+
}
138+
}
139+
```
140+
141+
### Across a monorepo
142+
143+
```bash
144+
pnpm -r --parallel exec treeshake-check --top 3
145+
```
146+
147+
## How it works
148+
149+
1. Reads `package.json` from the target directory and resolves the entry point (`exports``module``main`).
150+
2. Constructs a synthetic Rollup entry that imports the target as a side-effect-only import: `import "/absolute/path/to/entry.js"`.
151+
3. Runs Rollup with default tree-shaking enabled.
152+
4. Inspects `chunk.modules` for per-module `renderedLength`, `renderedExports`, and `removedExports`.
153+
5. Classifies surviving code by AST analysis (with regex fallback for unparseable output).
154+
6. Reports per-module statistics, surviving code, and `package.json` recommendations.
155+
156+
## Prior art
157+
158+
- [agadoo](https://www.npmjs.com/package/agadoo) by Rich Harris — same technique, the original implementation. This package adds richer diagnostics, structured output, and Effect-based composition.
159+
- [bundlephobia](https://bundlephobia.com) — measures the post-shake size from a consumer's perspective rather than analyzing why shaking succeeds or fails.

packages/treeshake-check/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
"description": "Check whether a package can be fully tree-shaken by Rollup",
55
"license": "MIT",
66
"type": "module",
7+
"repository": {
8+
"type": "git",
9+
"url": "https://github.com/ryanbas21/devtools.git",
10+
"directory": "packages/treeshake-check"
11+
},
12+
"publishConfig": {
13+
"access": "public"
14+
},
715
"exports": {
816
".": {
917
"types": "./dist/index.d.ts",

0 commit comments

Comments
 (0)