|
| 1 | +--- |
| 2 | +name: trimming-bundle |
| 3 | +description: For repos that ship a built bundle, finds unused code paths in dist/ and iteratively stubs them via the bundler's stub plugin. Each candidate stub goes through stub → rebuild → test loop; only paths that pass the loop are kept. Today the only supported bundler is rolldown (createLibStubPlugin); the skill shape generalizes to other bundlers if the fleet adopts them. Use after a bundler migration, before publishing a new version, or whenever bundle size grows unexpectedly. |
| 4 | +user-invocable: true |
| 5 | +allowed-tools: Read, Edit, Grep, Glob, AskUserQuestion, Bash(pnpm:*), Bash(node:*), Bash(grep:*), Bash(rg:*), Bash(find:*), Bash(ls:*), Bash(wc:*), Bash(du:*), Bash(stat:*), Bash(git status:*), Bash(git diff:*) |
| 6 | +--- |
| 7 | + |
| 8 | +# trimming-bundle |
| 9 | + |
| 10 | +Iteratively stub heavyweight modules that the bundler statically pulls in but the runtime never reaches. Apply on repos that ship a built bundle. Today: rolldown only (socket-packageurl-js, socket-sdk-js — any repo with `.config/rolldown.config.mts`). The skill is named generically because the dead-path-stubbing pattern applies to any bundler; today the only fleet bundler is rolldown. |
| 11 | + |
| 12 | +## When to invoke |
| 13 | + |
| 14 | +- After the rolldown migration lands (replacing esbuild) — the static-analyzer behavior differs and unused-path detection needs a fresh pass. |
| 15 | +- Before publishing a new version where bundle size matters (npm-published packages). |
| 16 | +- When `dist/index.js` grows by more than ~10% between releases without a corresponding feature addition. |
| 17 | +- As a follow-up step after `scanning-quality` flags `bundle-trim` candidates (the quality scan reads dist/ but doesn't mutate it; this skill does the trim loop). |
| 18 | + |
| 19 | +## Skip when |
| 20 | + |
| 21 | +- The repo doesn't build a rolldown bundle (no `.config/rolldown.config.mts`). |
| 22 | +- The bundle is consumed by code that uses dynamic feature detection (rare; flagged by the rolldown plugin's `moduleSideEffects: false` annotation). |
| 23 | +- Tests aren't running (`pnpm test` fails before any trim) — fix tests first; trim depends on the test signal. |
| 24 | + |
| 25 | +## Required: rolldown/lib-stub.mts |
| 26 | + |
| 27 | +🚨 This skill **REQUIRES** `.config/rolldown/lib-stub.mts` to be present and to export `createLibStubPlugin`. The file is fleet-canonical (cascades from `socket-repo-template/template/.config/rolldown/lib-stub.mts` via sync-scaffolding) and must NOT be edited locally per the no-fleet-fork rule. |
| 28 | + |
| 29 | +Before doing anything else: |
| 30 | + |
| 31 | +```bash |
| 32 | +[ -f .config/rolldown/lib-stub.mts ] || { |
| 33 | + echo "ERROR: .config/rolldown/lib-stub.mts is missing." |
| 34 | + echo "Cascade it from socket-repo-template:" |
| 35 | + echo " cd /Users/<user>/projects/socket-repo-template &&" # socket-hook: allow cross-repo |
| 36 | + echo " node scripts/sync-scaffolding/main.mts --target <this-repo> --fix" |
| 37 | + exit 1 |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +If the file is missing, STOP and run the cascade. Do NOT inline a copy of the plugin — it must be the fleet-canonical version. |
| 42 | + |
| 43 | +Verify the rolldown config imports it: |
| 44 | + |
| 45 | +```bash |
| 46 | +grep -q "createLibStubPlugin" .config/rolldown.config.mts || { |
| 47 | + echo "ERROR: .config/rolldown.config.mts doesn't import createLibStubPlugin." |
| 48 | + echo "Add: import { createLibStubPlugin } from './rolldown/lib-stub.mts'" |
| 49 | + echo "And: plugins: [createLibStubPlugin({ stubPattern: /...regex.../ })]" |
| 50 | + exit 1 |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +## Inputs |
| 55 | + |
| 56 | +- `dist/` — the most recent build output (run `pnpm build` first if missing or stale). |
| 57 | +- `.config/rolldown.config.mts` — already imports `createLibStubPlugin` from `.config/rolldown/lib-stub.mts` (fleet-canonical; cascaded via sync-scaffolding). |
| 58 | +- `pnpm test` — must pass at start; the trim loop's signal is "tests still pass after stub." |
| 59 | + |
| 60 | +## Process |
| 61 | + |
| 62 | +### Phase 1: Baseline |
| 63 | + |
| 64 | +```bash |
| 65 | +pnpm build |
| 66 | +ls -lah dist/ |
| 67 | +pnpm test |
| 68 | +``` |
| 69 | + |
| 70 | +Record: |
| 71 | +- Current bundle size (sum of `dist/*.js`). |
| 72 | +- Current test pass count. |
| 73 | +- Any pre-existing test failures (do NOT proceed if tests were already failing — fix first). |
| 74 | + |
| 75 | +### Phase 2: Identify candidates |
| 76 | + |
| 77 | +Read `dist/index.js` (or the primary entry) and grep for module imports / requires. The static analyzer keeps modules that are statically reachable from any export. Candidates for stubbing are modules whose entire surface area is: |
| 78 | + |
| 79 | +- **Touch-only**: imported but never called via the published API (e.g. `globs` imported by a deprecated helper that's no longer in the entry chain). |
| 80 | +- **Dev-only**: present because of a side-effect import that doesn't matter at runtime (e.g. node:fs/promises pulled in by a build-time helper). |
| 81 | +- **Conditional-dead**: behind a flag that the published bundle never sets (e.g. `if (DEBUG_MODE)` where DEBUG_MODE is `false` in the build). |
| 82 | + |
| 83 | +How to identify, in priority order: |
| 84 | + |
| 85 | +1. **Heuristic**: `rg "from '@socketsecurity/lib/(globs|sorts|http-request|.*)'" dist/` — note which lib subpaths show up. Cross-reference against published API surface (`src/index.ts` exports). Anything imported by the bundle that's not transitively reached from `src/index.ts` is a candidate. |
| 86 | +2. **Bundle size scan**: `du -bc dist/*.js | sort -rn | head -10` — identifies the largest bundle outputs. If `dist/index.js` is unexpectedly large, the heaviest unused dep is usually the culprit. |
| 87 | +3. **Plugin echo**: temporarily set `verbose: true` (if added) on `createLibStubPlugin` to log every resolved module. The list of resolved paths NOT under your repo's src/ is the candidate set. |
| 88 | + |
| 89 | +For each candidate, record: |
| 90 | +- The absolute resolved path or path-pattern (`/.../@socketsecurity/lib/dist/globs.js`). |
| 91 | +- The size impact (run `du -b` on the file). |
| 92 | +- The reason the runtime can't reach it. |
| 93 | + |
| 94 | +### Phase 3: Verify reachability claim |
| 95 | + |
| 96 | +🚨 Stubbing a file that IS reached at runtime gives runtime crashes, not bundle-time errors. Verify each candidate before stubbing: |
| 97 | + |
| 98 | +```bash |
| 99 | +# 1. Search the published API surface for direct imports. |
| 100 | +rg --no-heading "from .*<candidate-name>" src/ |
| 101 | + |
| 102 | +# 2. Search transitively reachable code for indirect imports. |
| 103 | +rg --no-heading "<candidate-name>" src/ |
| 104 | + |
| 105 | +# 3. Confirm the candidate is NOT reached from any test. |
| 106 | +rg --no-heading "<candidate-name>" test/ |
| 107 | +``` |
| 108 | + |
| 109 | +If any of these find a hit, the candidate is reachable — skip it. Only candidates with zero hits across all three queries proceed to Phase 4. |
| 110 | + |
| 111 | +### Phase 4: Stub one candidate |
| 112 | + |
| 113 | +Edit `.config/rolldown.config.mts` to extend the `stubPattern` regex: |
| 114 | + |
| 115 | +```ts |
| 116 | +const stubPattern = /(?:globs|sorts|<new-candidate>)\.js$/ |
| 117 | +``` |
| 118 | + |
| 119 | +Pattern matches the absolute resolved path. Use the file's basename or a unique path fragment — whatever's stable across pnpm hoisting. |
| 120 | + |
| 121 | +Then: |
| 122 | + |
| 123 | +```bash |
| 124 | +pnpm build |
| 125 | +pnpm test |
| 126 | +``` |
| 127 | + |
| 128 | +Three outcomes: |
| 129 | + |
| 130 | +- **Tests pass + bundle smaller** → keep the stub. Move to next candidate. |
| 131 | +- **Tests pass + bundle same size** → the stub didn't trigger; the regex doesn't match the resolved path. Inspect the build output to see why (run with `--logLevel debug`), adjust the pattern, retry. |
| 132 | +- **Tests fail** → the candidate IS reached. Revert the stub. The Phase 3 verification missed an import path; investigate. |
| 133 | + |
| 134 | +Iterate one candidate at a time. Multi-candidate stubs make failure attribution painful — keep the loop tight. |
| 135 | + |
| 136 | +### Phase 5: Document the kept stubs |
| 137 | + |
| 138 | +For each candidate that survived the loop, add a one-line comment in the `stubPattern` definition explaining WHY it's safe to stub (which import path it's on, why runtime never reaches it). Future maintainers need to know the chain of reasoning, not just the regex. |
| 139 | + |
| 140 | +### Phase 6: Verify |
| 141 | + |
| 142 | +```bash |
| 143 | +pnpm build |
| 144 | +pnpm test |
| 145 | +pnpm exec oxlint |
| 146 | +pnpm exec tsgo -p tsconfig.check.json |
| 147 | +``` |
| 148 | + |
| 149 | +All four must pass before committing. |
| 150 | + |
| 151 | +### Phase 7: Commit |
| 152 | + |
| 153 | +```bash |
| 154 | +git add .config/rolldown.config.mts |
| 155 | +git commit -m "perf(bundle): stub <N> unused lib internals (<size> saved)" |
| 156 | +``` |
| 157 | + |
| 158 | +The commit message states the count + size delta. If the trim is significant (say >50KB), also update `docs/rolldown-migration.md` with the new baseline. |
| 159 | + |
| 160 | +## Reference |
| 161 | + |
| 162 | +- `.config/rolldown/lib-stub.mts` — fleet-canonical plugin (cascade via sync-scaffolding; never edit locally per the no-fleet-fork rule). |
| 163 | +- `docs/rolldown-migration.md` — repo-specific (in repos that ran the migration). Records baseline numbers from before/after the esbuild → rolldown switch. |
| 164 | +- `socket-packageurl-js/.config/rolldown.config.mts` — the worked example of `createLibStubPlugin` use, with a populated `stubPattern`. |
| 165 | + |
| 166 | +## Companion: scanning-quality |
| 167 | + |
| 168 | +The `bundle-trim` scan in `scanning-quality/scans/bundle-trim.md` runs the discovery half of this skill (Phase 1–3) and reports candidates. It does NOT mutate the repo. Use this skill for the actual trim loop. |
| 169 | + |
| 170 | +## Failure modes |
| 171 | + |
| 172 | +- **Tests pass but the stubbed dep is dynamically required at runtime via `await import()`** — the static analyzer flags it as unreachable but the runtime path needs it. Add the dep back to the entry's static imports OR remove the dynamic import. |
| 173 | +- **The `stubPattern` matches more paths than intended** — too-broad regex. Tighten to a specific basename or a unique path segment. The plugin matches against the absolute resolved path, so `node_modules/.pnpm/@socketsecurity+lib@.../dist/globs.js` is what you're matching. |
| 174 | +- **Bundle size grows after a stub** — the empty-CJS replacement is heavier than the dependency's tree-shaken form. Check the rolldown output: usually means the dep was already mostly tree-shaken and the stub overhead exceeds what's saved. |
0 commit comments