Skip to content

Commit 72a99bb

Browse files
committed
chore(sync): cascade conditional-files gate + sort-equality-disjunctions rule from socket-repo-template
1 parent f249a3a commit 72a99bb

6 files changed

Lines changed: 491 additions & 1 deletion

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Bundle-trim scan
2+
3+
Identifies unused module paths the rolldown bundler statically pulls into `dist/` but that the runtime never reaches. Reports candidates only — does NOT mutate the repo. The active trim loop (stub → rebuild → tests pass → keep) lives in the `trimming-bundle` skill.
4+
5+
## Mission
6+
7+
For each repo that ships a rolldown bundle, look at `dist/index.js` (or the primary entry) and compare the set of statically-resolved imports against the set of imports actually reachable from the published API surface. The delta is the candidate set — modules the bundler kept that the runtime can't reach.
8+
9+
## Inputs
10+
11+
- `dist/` — the most recent build output. If missing or stale, the scan flags "build first" and skips.
12+
- `.config/rolldown.config.mts` — required (signal that this repo uses rolldown).
13+
- `.config/rolldown/lib-stub.mts` — required (the canonical plugin the trim skill uses). If missing, the scan flags "cascade missing canonical plugin" and skips.
14+
- `src/index.ts` (or the entry declared in `package.json` `exports`) — the published API surface.
15+
16+
## Skip when
17+
18+
- `.config/rolldown.config.mts` doesn't exist (repo doesn't use rolldown).
19+
- `.config/rolldown/lib-stub.mts` doesn't exist (cascade gap; surface as a separate finding).
20+
- `dist/` doesn't exist (run `pnpm build` first; surface as a separate finding).
21+
22+
## Method
23+
24+
1. **Survey resolved imports**: `rg --no-heading "from '@socketsecurity/lib/[^']+'" dist/` — list of every lib subpath the bundle imported.
25+
2. **Survey published surface**: read `src/index.ts` (or `package.json` `exports`-pointed entry) end-to-end and collect every transitively-reached lib subpath. Walk re-exports.
26+
3. **Compute delta**: subpaths in (1) but not in (2) are candidates.
27+
4. **Verify reachability claim** (cheap pass; the trim skill does the deep verification before stubbing): for each candidate, `rg --no-heading "<subpath-name>" src/` should return zero hits in src. Hits mean the subpath IS reached and the candidate is a false positive.
28+
5. **Estimate size impact**: `du -b dist/<file>` for the heaviest candidates.
29+
30+
## Output shape
31+
32+
```
33+
### Bundle Trim
34+
35+
Bundle: dist/index.js (current size: <N> KB)
36+
Plugin status: createLibStubPlugin imported (current stubPattern: /<regex>/)
37+
38+
Candidates (sorted by size, heaviest first):
39+
- @socketsecurity/lib/<subpath> — <KB> potential savings
40+
Reason: imported by bundle, not reached from src/index.ts
41+
Verify: src/ has zero hits for `<subpath-name>`
42+
Confidence: HIGH | MEDIUM | LOW
43+
Action: hand to trimming-bundle skill for stub loop
44+
45+
If 0 candidates:
46+
✓ No unreachable lib subpaths detected. Bundle is tree-shaken cleanly.
47+
```
48+
49+
Confidence levels:
50+
51+
- **HIGH** — subpath is in the import survey, has zero hits in `src/`, and the trim skill's Phase 3 verify would pass.
52+
- **MEDIUM** — subpath is in the survey, has hits in `src/` but only inside files that aren't reached from the entry. The trim skill needs to walk the reachability graph to confirm.
53+
- **LOW** — subpath is in the survey but the static analysis is ambiguous. Skip in the report or leave for manual investigation.
54+
55+
## When to escalate
56+
57+
If candidates total >50KB and the repo is npm-published (consumers bear the bundle weight), prioritize handing off to the `trimming-bundle` skill before the next release. Bundle bloat is a quality issue users feel.
58+
59+
## Cross-references
60+
61+
- `trimming-bundle` skill — the active trim loop. This scan reports; that skill mutates.
62+
- `.config/rolldown/lib-stub.mts` — the canonical plugin. Both scan and skill require it to exist.
63+
- `socket-packageurl-js/docs/rolldown-migration.md` — worked example of bundle-size baseline tracking.
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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.

.config/oxlint-plugin/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import preferNodeBuiltinImports from './rules/prefer-node-builtin-imports.js'
3131
import preferSafeDelete from './rules/prefer-safe-delete.js'
3232
import preferUndefinedOverNull from './rules/prefer-undefined-over-null.js'
3333
import socketApiTokenEnv from './rules/socket-api-token-env.js'
34+
import sortEqualityDisjunctions from './rules/sort-equality-disjunctions.js'
3435
import sortNamedImports from './rules/sort-named-imports.js'
3536
import sortRegexAlternations from './rules/sort-regex-alternations.js'
3637
import sortSetArgs from './rules/sort-set-args.js'
@@ -61,6 +62,7 @@ const plugin = {
6162
'prefer-safe-delete': preferSafeDelete,
6263
'prefer-undefined-over-null': preferUndefinedOverNull,
6364
'socket-api-token-env': socketApiTokenEnv,
65+
'sort-equality-disjunctions': sortEqualityDisjunctions,
6466
'sort-named-imports': sortNamedImports,
6567
'sort-regex-alternations': sortRegexAlternations,
6668
'sort-set-args': sortSetArgs,

0 commit comments

Comments
 (0)