Skip to content

Module system

The kit ships as the full repo; the setup wizard prunes the modules you don’t keep. What makes that safe is a contract: every prunable module carries a module.json manifest declaring everything it owns (code, npm dependencies, env vars, manifest permissions, docs). One declaration removes all of it together.

Manifests live at the package root for workspace packages (packages/gate/module.json) or under apps/extension/modules/<id>/module.json for app-level modules, validated against tooling/config/module.schema.json.

{
"$schema": "../../tooling/config/module.schema.json",
"id": "billing",
"title": "Billing & entitlements",
"description": "Stripe checkout/portal routes, webhook-written entitlements, useEntitlement('paid').",
"files": ["packages/core-billing/**", "backend/functions/src/billing/**"],
"dependsOn": ["auth"],
"npmDependencies": { "@extensionstart/core-billing": "workspace:*" },
"env": [{ "name": "WXT_API_URL", "description": "deployed Functions base URL" }],
"permissions": [],
"wiring": ["apps/extension/entrypoints/background/index.ts"],
"docs": []
}
  • files globs are repo-root-relative; a module can own files outside its package.
  • dependsOn is by module ID. The resolver keeps dependencies of any kept module and drops dependents of any dropped one.
  • Core modules set "removable": false; the wizard never offers to prune them.
  • Env vars and permissions listed by several modules are pruned only when no kept module lists them.
  • permissions is why the generated manifest shrinks when you prune: each module declares the chrome.* permissions it needs, and a smaller permission surface means a faster store review.

A module’s code often touches shared files it doesn’t own: the background import order, the messaging protocol, surface roots. Those touchpoints carry marker comments so the pruner can strip them:

  • Line marker: a // module:<id> suffix (or {/* module:<id> */} in JSX) removes that line when <id> is dropped.
  • Block marker: everything from module:<id>:start through module:<id>:end (inclusive) is removed. Blocks of different modules may nest.

Each manifest lists the shared files carrying its markers under wiring. The pass criterion: any prune combination leaves pnpm typecheck and pnpm lint green with zero dangling imports.

Manifests list backend files as documentation of ownership, but the pruner leaves backend/** in place. The Hono app is one self-contained function and unused routes are harmless. Delete them manually if you want a minimal backend.

  1. Create the manifest (apps/extension/modules/<id>/module.json for an app-level feature) with id, title, description, and files globs for everything the module owns.

  2. Tag each touchpoint in shared files with a // module:<id> line marker or a module:<id>:start / end block, and list those files under wiring.

  3. Declare npmDependencies, env (names must exist in apps/extension/.env.example), permissions, and dependsOn.

  4. Keep the manifest in sync: adding a file, dependency, env var, or permission means updating module.json in the same change.

  5. Prove it prunes cleanly:

    Terminal window
    pnpm create extstart --dry-run --keep none # your module in the plan?

    Then, on a scratch branch, run a real prune that drops your module and check pnpm typecheck and pnpm lint stay green.