gadget-client-trim
v0.1.0
Published
Trim unused models from a generated Gadget API client to shrink your bundle in dev and production.
Maintainers
Readme
gadget-client-trim
Trim unused models, namespaces, and actions from a generated Gadget API client to shrink your bundle — in dev and in production.
A Gadget app's generated client (@gadget-client/<app>) instantiates every model manager, namespace,
and global action in its Client constructor. Because the constructor references all of them, nothing
tree-shakes: all of that code ships to the browser and is fetched in dev, even the parts your frontend
never touches.
gadget-client-trim is a small, framework-agnostic CLI that scans your frontend source for what you
actually use, then removes the rest from the generated client on disk. It trims four kinds of
members — models, namespaces, global actions, and computed views — by stripping their references
from Client.js (ESM and CJS), deleting orphaned model/namespace modules, and pruning the
declarations from Client.d.ts so accidental use becomes a compile error. (The generic api.view and
session/currentSession are always kept.)
Because it edits the client on disk, it works with any bundler that reads it — Vite, the Shopify CLI (UI extensions), webpack, esbuild — not just one.
What it does / does not do
- ✅ Removes unused models, namespaces, global actions, and computed views so your bundler drops them.
- ✅ Works for any consumer of the client (web app, Shopify UI extensions, SSR).
- ✅ Fails open (keeps anything it's unsure about) and safe (skips anything whose codegen shape it doesn't recognize) — see Safety.
- ❌ Does not generate your client, run
ggt, or touch any secrets/tokens. You generate the client (e.g.ggt dev/ggt pull); this tool only trims a client that already exists on disk. - ❌ Does not ship a GitHub Action or bundler plugin — just a CLI you wire into your own scripts/CI (recipes below).
Install
npm i -D gadget-client-trim
# or: yarn add -D gadget-client-trim / pnpm add -D gadget-client-trimUsage
# Apply the trim (default command):
gadget-client-trim
# Preview without changing anything:
gadget-client-trim --report
# CI gate — exits non-zero if the client isn't trimmed or its shape is unrecognized:
gadget-client-trim --check
# Re-apply automatically whenever the client is regenerated (dev):
gadget-client-trim --watchYou don't tell it where your client is. It finds .gadget/client automatically by walking up from
the current directory (and also recognizes a single node_modules/@gadget-client/*), so it works from
the repo root or any subdirectory. The only thing you configure is which folders to scan for
api.<model> usage — that's what determines which models/actions are kept vs. removed. The default scan
is web/, extensions/, and shared/.
Options
| Option | Description |
| ------------------------ | --------------------------------------------------------------------------------------- |
| --scan <a,b,c> | Folders to scan for api.<member> usage (default: web,extensions,shared). |
| --keep <a,b> | Extra members to always keep. |
| --force-trim <a,b> | Remove these even if used (never overrides session/currentSession). |
| --kinds <a,b> | Member kinds to trim (default: all): model,namespace,globalAction,computedView. |
| --no-delete | Strip references but keep model files on disk. |
| --no-prune-types | Don't edit Client.d.ts. |
| --config <path> | Config file (default: gadget-trim.config.{json,js,mjs} or package.json#gadgetTrim). |
| --report / --dry-run | Print the plan, change nothing. |
| --check | CI gate (see recipes). |
| --watch | Re-apply on regeneration. |
| --client-dir <path> | Advanced. Override client auto-detection (unusual monorepo layouts). |
| --cwd <path> | Advanced. Directory to start searching from. |
Config file
gadget-trim.config.json, gadget-trim.config.js, or gadget-trim.config.mjs (default export), or a
gadgetTrim key in package.json. The first one found in the current directory wins, in that order:
{
"scan": ["web", "extensions", "shared"],
"keep": [],
"forceTrim": [],
"deleteFiles": true,
"pruneTypes": true
}When to run it
The generated client is regenerated by ggt (and overwritten on every sync), so run the trim after
the client is generated and before your bundler reads it — never after the bundle is built.
- Dev: run
gadget-client-trim --watchalongside your dev server. It re-applies after every regeneration. - Build / CI: run
gadget-client-trimas an explicit step before your build. Don't rely on npm'sprebuildlifecycle hook — it won't fire for the Shopify CLI or for build pipelines that invoke your bundler directly.
ℹ️ You are responsible for generating the client first (e.g.
ggt pull) and for any auth/secrets that requires. This tool neither needs nor reads them.
Integration recipes
These are examples — copy what fits. See examples/ for full files.
// package.json
{
"scripts": {
"dev": "run-p dev:server trim:watch",
"trim:watch": "gadget-client-trim --watch",
},
}gadget-client-trim && your-build-command# after the client exists (e.g. ggt pull) and deps are installed:
gadget-client-trim
shopify app deploy# inside a job that has already generated the client (your ggt step):
- run: npx gadget-client-trim # trim
- run: npx gadget-client-trim --check # fail the build if not trimmed / shape changedSafety
- Fail-open: if the usage scan can't read a source file, it keeps everything (only
forceTrimmodels are removed). Wrongly keeping a model costs a little size; wrongly removing one would break at runtime — so it always biases toward keeping. - Fail-safe: the client is matched against a known set of anchor lines per model. If a model's shape
isn't exactly recognized (e.g. a future Gadget codegen change), that model is skipped with a
warning rather than partially edited.
--checkturns that into a CI failure so you notice. - All-or-nothing: a model is trimmed only if it can be cleanly removed from every target file
(ESM, CJS, and the
.d.ts), so the outputs never drift out of sync. session/currentSessionare always kept (auth), even via--force-trim.- Types stay consistent: only the trimmed managers' fields/imports are pruned from
Client.d.ts; the large cross-referential type graph (types.ts) is never touched, and record/selection type re-exports for trimmed models are preserved.
Programmatic API
import { analyze, applyPlan } from "gadget-client-trim";
const { plan } = analyze({
clientDir: ".gadget/client",
scan: ["web", "extensions"],
});
console.log(plan.trim); // ["shopifyFile", ...]
if (process.env.WRITE) applyPlan(plan);How it works
Members are discovered generically by parsing the client (no hardcoded list), so it tracks your schema
automatically. For each unused member it removes the relevant declarations from Client.js (ESM + CJS)
and Client.d.ts:
- models — manager import, constructor instantiation, internal-manager entry, and selection
re-export; deletes the orphaned
models/<Model>.js(record/selection type re-exports are kept). - namespaces — import + constructor; deletes the orphaned
namespaces/<name>.js. - global actions / computed views — the inline
buildGlobalAction/buildComputedViewblock (removed via string-aware, brace-balanced matching) and the correspondingClient.d.tsfield.
A member is only trimmed if it can be cleanly removed from every target file; otherwise it's skipped with a warning (see Safety).
License
MIT
