Skip to content

feat: vite-plugin-solid-three — build-time tree-shaking for createT(THREE)#61

Closed
bigmistqke wants to merge 21 commits into
solidjs-community:nextfrom
bigmistqke:vite-plugin-solid-three
Closed

feat: vite-plugin-solid-three — build-time tree-shaking for createT(THREE)#61
bigmistqke wants to merge 21 commits into
solidjs-community:nextfrom
bigmistqke:vite-plugin-solid-three

Conversation

@bigmistqke
Copy link
Copy Markdown
Contributor

Summary

A new Vite plugin, vite-plugin-solid-three, that makes createT(THREE) tree-shakeable. createT returns a runtime proxy, and passing the whole THREE namespace into it defeats the bundler's tree-shaking — so all of three ships. This plugin narrows the call to only the classes your app actually reaches through T.

It lets the bundler do the analysis, in two passes on production builds:

  • Measure: a nested build temporarily turns the catalogue into a namespace; the bundler's own tree-shaking reports (via renderedExports) exactly which keys survive — across files, dynamic imports, and code-split chunks.
  • Emit: createT(THREE) is rewritten to createT({ Mesh: THREE.Mesh, … }) with only the measured keys. createT stays in the output, so runtime and debugging are unchanged.

Soundness is structural: any access the bundler can't resolve statically (T[expr], T escaping into a function, {...T}) deopts to keeping the whole catalogue; dynamic catalogue sources (createT(store), computed keys) are left untouched. No static cross-file analysis, no alias replication, no resolution guard — the bundler is the analyzer, so the result is exact rather than over-approximate. Dev is untouched (the runtime proxy is used).

This supersedes the earlier ts-morph/unplugin approach (#60), which reconstructed cross-file analysis the bundler already performs.

What's covered

Fixtures build small consumer projects through the plugin and assert drop/keep:

  • namespace createT(THREE), custom object literals, spread + override precedence (last-write-wins), verbatim getters
  • dynamic-usage bail, whole-value escape, {...T} spread → full catalogue kept (sound)
  • cross-file union, lazy chunks, template-literal dynamic imports → reached classes survive
  • a browser soundness oracle: every class the rendered scene accesses through T is present in the narrowed catalogue

Test plan

  • pnpm --filter vite-plugin-solid-three test (30 Node tests)
  • pnpm --filter vite-plugin-solid-three test:oracle (browser soundness gate)
  • pnpm --filter vite-plugin-solid-three build (clean dist)
  • Try on a real solid-three app with a non-trivial Vite config and a custom catalogue

Notes / deferred

  • Two-pass ≈ 2× production build cost; the measure pass can be made cheaper (build-until-tree-shake, skip output stages). Design rationale and the build-cost trilemma are in docs/superpowers/specs/2026-06-01-createt-narrowing-design.md.
  • Vite-only for now. Cross-bundler is no longer architecturally blocked (the bundler does its own resolution and tree-shaking) and is a documented follow-up.

🤖 Generated with Claude Code

bigmistqke added 19 commits June 1, 2026 16:29
A Vite plugin that makes createT(THREE) tree-shakeable by letting the bundler itself compute the used-set: pass 1 rewrites the catalogue binding to a throwaway namespace scaffold and reads renderedExports after tree-shaking; pass 2 emits createT({ ...used keys with locally-resolved providers }), keeping createT in the output.

Replaces the earlier ts-morph / alias-resolution-host / soundness-guard / unplugin approach, which reconstructed cross-file analysis the bundler already performs. Mechanism (cross-file union, sound deopt, custom literals, spread/override precedence) and the two-pass orchestration are spike-validated.
Bite-sized TDD tasks for the bundler-as-analyzer design: single-file createT analysis (parse with ts.createSourceFile, no Program), provider-map with last-write-wins, throwaway measurement scaffold, and the two-pass measure/emit orchestration. Pure layers (analyze/providers/rewrite/scaffold) are unit-tested; the orchestration and catalogue shapes are covered by real vite-build fixtures; the browser oracle is the release gate.
Also adds packages/vite-plugin-solid-three to the pnpm workspace so its
magic-string dependency is installed and resolvable by vitest.
Getter/method catalogue entries carried their full member text, but emit prefixed it with `key:` — producing invalid syntax (`createT({ Foo: get Foo(){…} })`) and a failing build. The entry source now records `verbatim` and emit pushes such members as-is. Adds a getter fixture (the emit side was untested) and an escape fixture proving the whole-value-escape deopt keeps the full catalogue. Also clears coordination state per build and documents the one-build-per-process limitation.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 1, 2026

commit: 3144780

@bigmistqke
Copy link
Copy Markdown
Contributor Author

Closing this one. After living with it for a bit, the two-pass "let the bundler measure usage" approach — rewriting createT(THREE) into a scaffold namespace, running a throwaway nested build to read Rollup's surviving exports, then narrowing — works, but it's too hacky to commit to: it leans on a nested build coordinated by process-global state (one build per process, user plugins run twice), and it's tightly coupled to Rollup internals (renderedExports).

For the cost and the moving parts, the immediate payoff just isn't there to justify carrying it. Leaving the branch up for reference in case we want to revisit the idea later, but not pursuing this path for now.

@bigmistqke bigmistqke closed this Jun 1, 2026
@bigmistqke bigmistqke deleted the vite-plugin-solid-three branch June 3, 2026 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant