npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vue-shaker

v0.2.0

Published

Source-level tree-shaker for Vue 3 SFCs — removes unreachable v-if branches and scoped CSS rules a bundler can't see

Readme

▶ Try it in the browser: https://baseballyama.github.io/vue-shaker/ — an interactive playground that runs the engine entirely client-side.

Rollup tree-shakes JS modules, but it can't see inside a .vue; Vue compiles one generic render function per component and prunes no scoped CSS at all. So the .btn-danger rule you never use, the v-if="loading" arm you never trigger, and the props you never pass ship in every app that imports the component. vue-shaker is a sound, whole-program tree-shaker for Vue 3 SFCs (<script setup> + defineProps) that closes that gap.

It runs in your app's production build, before the Vue compiler, and slims each .vue file by partially evaluating it against how the whole app actually uses it: props that are never passed (or always passed the same value) are folded to their constant, the dead v-if arms behind them are deleted, those props are dropped from the defineProps signature, and the now-pointless attributes are removed at every call site. The Vue compiler then only sees the code your app can actually reach.

It is sound first: it never changes what the user sees. When it cannot prove a transform is safe, it leaves the code untouched (bails).

See docs/ARCHITECTURE.md for the full design.

Why this exists (and why a JS bundler can't do it)

Design-system components carry lots of props (Button with variant / size / loading / icon / iconPosition / fullWidth / rounded / href …), but any one app uses only a few. The code behind the unused props — template branches, class computation, computed values, imports, CSS — is effectively dead for that app, yet it ships anyway.

It cannot be removed after Vue compiles, because Vue emits one generic render function per component, shared by every caller. In that output the prop values flow through the runtime (props.loading, $props), so loading / variant are not static JS constants — terser/esbuild/Rollup cannot fold if (loading) to if (false), and the single module has no whole-program information to know which props this particular app never uses.

vue-shaker works one step earlier, on the pre-compile .vue source, where the prop's value (its default, or the literal at the call site) is still visible and the template structure is intact. It is essentially a whole-program partial evaluator + dead-code eliminator that understands Vue, driven by every call site in the app.

The CSS differentiator (what a bundler genuinely can't reach)

Given :class="['btn', \btn-${variant}`]"where the app only ever passesvariant ∈ {primary, secondary}, the class btn-dangercan never exist at runtime. But the class only appears as a runtime string, so Rollup/terser can't touch it (the class isn't in the JS at all), and — unlike Svelte — **Vue does not prune unusedrules at all**. Sobtn-danger` ships no matter what.

vue-shaker computes the reachable value set of variant, proves the .btn-danger / .btn-ghost rules can never match any element this component renders, and removes those <style scoped> rules — while keeping .btn, .btn-primary, .btn-secondary. Because Vue has no unused-CSS pruning of its own, this is an even bigger win for Vue than for Svelte.

Install

pnpm add -D vue-shaker
# or: npm i -D vue-shaker / yarn add -D vue-shaker

Requires vue@^3.

Usage (Vite)

Add the plugin before vue() so it hands already-slimmed source to the Vue compiler. It is build-only by design — dev is a pass-through.

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { shaker } from 'vue-shaker/vite';

export default defineConfig({
  plugins: [
    // `include` must cover EVERY call site in the app, or prop elimination
    // would be unsound. Defaults to the Vite root.
    shaker({ include: ['src'] }),
    vue(),
  ],
});

There is also a plain-Rollup plugin (rollup-plugin-vue-shaker) for non-Vite pipelines; the Vite plugin is preferred for apps.

Options

shaker({
  include: ['src'], // dirs (relative to root) holding every .vue call site
  level: 1, //  0 | 1 | 2 — default 1 (L0/L1/L1.5 always on). 2 = opt-in L2.
  monomorphize: false, // L2 tuning; only consulted when level: 2.
});

What it does

| Level | What it removes | Default | | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | L0 | Props no call site ever passes → fold to the default, drop from defineProps, strip the attribute at call sites | on | | L1 | Props that collapse to one constant app-wide → fold + drop + strip every call site's attribute | on | | L1.5 | Value-set narrowing: with variant ∈ {primary, secondary}, delete provably-dead v-if/v-else-if arms (prop stays in the signature) | on | | CSS | <style scoped> rules whose class can never be produced given the value sets — the bundler-can't differentiator (and Vue can't either) | on | | L2 | Per-call-site monomorphization: specialize a component per prop shape (deduped by residual, capped by maxVariants) | opt-in |

Soundness

The whole point is to never change observable behavior.

  • Differential-SSR verified. Tests server-render the original and the shaken component (via @vue/server-renderer, comments/scope-ids stripped, whitespace normalized) and assert the HTML is identical for every value the app passes.
  • Conservative bail. When a transform can't be proven safe, the code is left as-is. Whole-component bails: a component rendered through <component :is>, that escapes as a value, or is reached only through a barrel/named re-export (its call sites aren't enumerable). Per-prop bails: a v-bind="…" spread that could overwrite it, a name shadowed by v-for / scoped-slot props, or used in a way the engine can't follow.
  • Side effects preserved. A call-site attribute is only stripped if its value has no side effects; a value's code is removed only when it is provably pure and unused.
  • Whole-program fixpoint. Call sites inside a folded-away v-if don't count toward a child's prop profile; analysis iterates to a fixpoint so cascades are consistent with what the transform actually deletes.

Limitations

  • <script setup> + defineProps only. Options API and plain <script> (defineComponent({ props })) are out of scope and silently passed through.
  • Needs .vue source. Libraries shipping compiled JS can't be shaken (the source has to be visible — that's the whole premise). Anything it can't resolve is silently passed through.
  • Build only. It runs in vite build, not in dev/HMR — whole-program analysis is fundamentally incompatible with HMR's locality. Dev is always a pass-through (unoptimized but always correct).
  • include must cover the whole app. A call site outside the scanned dirs is invisible, so soundness requires every consumer of a prop to be in scope.

Architecture & status

The engine is split into an environment-free Engine (Vue-aware analysis + transform) behind a stable IR, and a thin Shell (the Vite/Rollup plugin) that owns file IO and module resolution — so the core can later be ported to Rust on vize (the Rust Vue toolchain). See docs/ARCHITECTURE.md and docs/RUST-MIGRATION.md.

License

MIT