# Contributing
`carbon-preprocess-svelte` ships Svelte preprocessors and build plugins that make [Carbon Design System](https://github.com/carbon-design-system/carbon-components-svelte) apps smaller and faster. Two separate problems:
- **`optimizeImports`**, a Svelte _script_ preprocessor that rewrites barrel imports (`import { Button } from "carbon-components-svelte"`) into direct path imports (`import Button from "carbon-components-svelte/src/Button/Button.svelte"`) so bundlers tree-shake and HMR stays fast.
- **`optimizeCss` / `OptimizeCssPlugin`**, build plugins (Vite/Rollup and Webpack) that strip unused Carbon CSS rules from production output.
Option shapes, usage per bundler, and what each export does are in [README.md](README.md). That file is the source of truth for what the package supports. This file is how the code is built and changed.
If you're not sure what to build or how to approach a change, [file an issue](https://github.com/carbon-design-system/carbon-preprocess-svelte/issues) before opening a PR.
## Prerequisites
[Bun](https://bun.sh/) is the package manager, test runner, and bundler. There is no separate Node toolchain for development. Run package scripts with `bun run `, parsed, walked for `ImportDeclaration`s, rewritten with `MagicString`, then the wrapper tags are stripped back off.
- Carbon component names resolve through the index `path`. Names missing from the index get an _optimistic_ `src/Name/Name.svelte` path **only if PascalCase**. camelCase utilities stay on the barrel so we never point at a `.svelte` file that is not there. Icons and pictograms map to `lib/Name.svelte`.
- **Type imports stay on the barrel.** `import type { … }` statements are left alone. In `import { type X, Y }`, `X` stays on the barrel and only `Y` is rewritten. Recent fixes ([#138](https://github.com/carbon-design-system/carbon-preprocess-svelte/pull/138), [#133](https://github.com/carbon-design-system/carbon-preprocess-svelte/pull/133)) live here. Add a fixture in [`tests/optimize-imports.test.ts`](tests/optimize-imports.test.ts) for any import-shape change.
### `optimizeCss` (Vite/Rollup) and `OptimizeCssPlugin` (Webpack)
Both plugins do the same job through different bundler hooks, then call the same optimizer.
- [`src/plugins/optimize-css.ts`](src/plugins/optimize-css.ts) is the Vite plugin (`apply: "build"`, `enforce: "post"`). It collects Carbon component ids in the `transform` hook (it transforms nothing, just records ids), then rewrites CSS assets in `generateBundle` by mutating `file.source` in place. Synchronous PostCSS.
- [`src/plugins/OptimizeCssPlugin.ts`](src/plugins/OptimizeCssPlugin.ts) is the Webpack plugin, production-only. It collects ids from `NormalModule` `beforeSnapshot` `fileDependencies`, processes CSS assets at `PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE` (before minification). Async PostCSS, all assets in parallel via `Promise.all`.
The shared core is [`src/plugins/create-optimized-css.ts`](src/plugins/create-optimized-css.ts). It builds an **allowlist** of `.bx--*` classes from the bundled components' index entries, plus `ALWAYS_ON_CLASSES` and any `content`-scanned tokens, then runs a PostCSS pipeline (the Carbon rule/at-rule visitor + `postcss-discard-empty`). It exposes sync (`optimizeCssWithReport`) and async (`…Async`) variants because Vite and Webpack differ. Keep the two in lockstep when you change behavior. The `report.removed` count suppresses the size-diff log when nothing was pruned ([#131](https://github.com/carbon-design-system/carbon-preprocess-svelte/pull/131)).
Two pruning modes:
- **Default.** Keep a rule if any of its comma-separated selectors matches the allowlist. Coarse but safe.
- **`experimental.strict`**, implemented in [`src/plugins/strict-css-optimizer.ts`](src/plugins/strict-css-optimizer.ts). Prunes individual selectors out of comma lists. Every Carbon class in a same-element compound must match. Descendant selectors split into ancestors + subject (subject must fully match; ancestors may match `CONTEXT_ANCESTORS` without being imported). Strips `:not(...)` before matching. Drops flatpickr/legacy `bx-` rules unless DatePicker is bundled. Parenthesis-aware for `:is()`. Most CSS-correctness work happens here.
Supporting modules: [`safelist.ts`](src/plugins/safelist.ts) (string = literal class-token match, RegExp = whole-selector match), [`scan-content.ts`](src/plugins/scan-content.ts) (glob source files for literal `bx--` tokens, for runtime-built class names like `` `bx--btn--${kind}` ``), [`print-diff.ts`](src/plugins/print-diff.ts) (the before/after size log the e2e tests parse). Safelist and `content` are the user-facing overrides added in [#140](https://github.com/carbon-design-system/carbon-preprocess-svelte/pull/140).
## Conventions
Biome enforces most of this in CI (`biome ci --error-on-warnings`). Config: [`biome.json`](biome.json), space indent, multiline attributes, imports auto-organized. The recommended preset is on. The rules below are **errors**, so a violation fails the build:
- **Hoist regexes to module scope.** `useTopLevelRegex` is an error. A regex literal inside a function fails lint. Declare it as a named top-level `const` (see the `*_REGEX` / `CARBON_*` constants at the top of nearly every module).
- **No `await` in loops, no `forEach`.** `noAwaitInLoops` and `noForEach` are errors. Build the work and `await Promise.all(...)`, or use `for...of` with hoisted awaits. The two intentional sequential-await loops in [`tests/test-e2e.ts`](tests/test-e2e.ts) carry `// biome-ignore` comments explaining why. Match that pattern if you genuinely need ordering.
- **No namespace imports, no barrel re-exports, no import cycles.** `noNamespaceImport`, `noReExportAll`, `noImportCycles`. Export named bindings explicitly, as [`src/index.ts`](src/index.ts) does.
- **No `delete`, no accumulating spread, prefer arrow functions and literal keys.** `noDelete`, `noAccumulatingSpread`, `useArrowFunction`, `useLiteralKeys`.
- **No `!important` in authored styles** (`noImportantStyles`) and **no `bun:test` imports**. The Bun test globals (`describe`, `test`, `expect`) are ambient. Importing them is an error.
- **Use `node:` import specifiers**, e.g. `import path from "node:path"`, as the scripts and plugins do.
TypeScript ([`tsconfig.json`](tsconfig.json)) runs `strict` with `noUnusedLocals`, `noUnusedParameters`, and `erasableSyntaxOnly` (no runtime-emitting TS syntax: enums, parameter properties, etc.). The `carbon-preprocess-svelte` path alias resolves to `src/` so tests and fixtures import the package by name. `estree-walker`'s loose AST types are tightened in [`src/global.d.ts`](src/global.d.ts). Extend that module declaration instead of reaching for `any` when you walk new node types.
Comment the _why_ behind non-obvious parser, hook-ordering, and CSS-matching logic. The existing modules are heavily annotated; match that density. Skip comments that restate the code.
## Testing
Tests use the Bun test runner and live in [`tests/`](tests), mostly one `*.test.ts` per `src/` module.
```sh
bun run test # everything, in parallel
bun test optimize-imports # filter by file-path substring
bun test tests/utils.test.ts # a single file
bun test --watch # watch mode
```
Three layers, smallest to largest:
### Unit tests
Per-module behavior: [`utils.test.ts`](tests/utils.test.ts), [`scan-content.test.ts`](tests/scan-content.test.ts), [`print-diff.test.ts`](tests/print-diff.test.ts), [`extract-selectors.test.ts`](tests/extract-selectors.test.ts), [`optimize-imports.test.ts`](tests/optimize-imports.test.ts), [`create-optimized-css.test.ts`](tests/create-optimized-css.test.ts), [`OptimizeCssPlugin.test.ts`](tests/OptimizeCssPlugin.test.ts). Add a focused case here for any logic change.
### CSS optimization fixtures
[`tests/optimize-css-fixtures.test.ts`](tests/optimize-css-fixtures.test.ts) is the main regression check for the optimizer. It runs `createOptimizedCss` against Carbon's compiled stylesheet (`carbon-components-svelte/css/white.css`) for ~35 scenarios. Each scenario is a set of imported component ids in default or strict mode. Output is compared to committed baselines under [`tests/fixtures/optimize-css/`](tests/fixtures/optimize-css).
Each scenario has two files:
- `.css`, the pruned, pretty-printed output. **Gitignored**, regenerated every run. Open it to see what survived.
- `.report.json`, the **committed** baseline: `reduction_percent`, `kept_rules`, byte counts, and `leaked_classes` (Carbon classes still present that the import allowlist cannot explain). Strict scenarios target `leaked_count: 0`.
Beyond byte baselines, the test also checks the index: no over-prune (a selector that should survive keeps its classes), no foreign survivor in strict mode, correct multi-class strict pruning. See [`tests/fixtures/optimize-css/README.md`](tests/fixtures/optimize-css/README.md) for the scenario catalog and when to add a fixture (new `MANUAL_OVERRIDES` entry, new typical multi-import bundle, suspected leak/over-prune regression, not a duplicate import set).
After an optimizer change or a Carbon bump, regenerate and **review the `.report.json` diff**. A baseline change is a behavior change:
```sh
bun run test:fixtures:update
```
Shared helpers (`buildAllowlist`, `matchesAllowlist`, `shouldKeepStrictSelector`, `resolveCarbonCss`, `prettifyCss`) live in [`tests/helpers/carbon-css.ts`](tests/helpers/carbon-css.ts). They mirror production matching logic so the test can re-derive what the optimizer should have done.
### End-to-end tests
[`tests/test-e2e.ts`](tests/test-e2e.ts) (`bun run test:e2e`) builds the package, `bun link`s it into each project under [`examples/`](examples), builds every example, parses the `printDiff` output, and compares CSS reduction to [`tests/__snapshots__/e2e.json`](tests/__snapshots__/e2e.json) within 0.01 tolerance. Unit tests will not catch a plugin that silently no-ops inside a real Vite or Webpack build. This suite will.
The examples are real downstream consumers, each linking the package via `"carbon-preprocess-svelte": "link:carbon-preprocess-svelte"`:
- `examples/rollup`, `examples/vite`, `examples/vite@svelte-5`, `examples/sveltekit`: Vite/Rollup plugin path
- `examples/webpack`, `examples/webpack@svelte-5`: Webpack plugin path
Other Svelte frameworks (Astro, Routify, …) run Vite under the hood, so the Vite examples cover them. When you change output shape or reduction behavior, update snapshots and read the diff:
```sh
bun run test:e2e:update
```
`bun run upgrade-examples` bumps each example's dependencies.
## Build
[`scripts/build.ts`](scripts/build.ts) (`bun run build`) removes `dist/`, regenerates the component index (the `prebuild` step runs `index:components` with `BUILD=true`, which writes the index as minified JSON), bundles `src/index.ts` with `Bun.build` (minified ESM, Node target), then runs `tsc --project tsconfig.build.json` ([`tsconfig.build.json`](tsconfig.build.json)) to emit declarations. `dist/` is gitignored and never committed. `bun run build -w` rebuilds on changes under `src/` and keeps linked examples current.
`bun run build:prune-package` (`bunx culls`) trims `package.json` for publishing and runs in the release workflow.
## Continuous integration
[`.github/workflows/test.yml`](.github/workflows/test.yml) runs on every PR and on pushes to `main` (macOS runner):
1. `bun ci`
2. `bunx biome ci --error-on-warnings`
3. `bun run typecheck`
4. `bun run test`
5. `bun run test:e2e`
Run those locally before pushing. The e2e suite runs in CI, so a snapshot you forgot to update will fail the build.
## Commit messages
Use [Conventional Commits](https://www.conventionalcommits.org/):
```
():
```
- Common types: `fix`, `feat`, `perf`, `chore`, `test`, `docs`, `refactor`.
- Scope is the area touched: `optimize-css`, `optimize-imports`, `index`, `components`, `examples`, `e2e`, `deps-dev`, `ci`.
- Imperative mood, one concise line. Put detail in the body and reference issues with `Fixes #N`.
Examples from the log:
```
feat(optimize-css): add safelist and content escape hatches
perf(optimize-imports): skip parse for files without carbon- imports
fix(index): automate runtime and CSS context classes
chore(components): re-index using v0.109.0
```
## Submit a pull request
Sync your fork with upstream first:
```sh
git fetch upstream
git checkout main
git merge upstream/main
```
Push your branch and open a PR comparing your feature branch to `origin/main`. Keep PRs focused. Include regenerated artifacts your change needs: the component index, fixture baselines, and e2e snapshots. Those diffs are part of the review.
## Maintainer guide
The following applies only to maintainers.
### Release
[`.github/workflows/release.yml`](.github/workflows/release.yml) publishes to NPM with [provenance](https://docs.npmjs.com/generating-provenance-statements) when a tag starting with `v` is pushed. It installs, runs `bun run build` and `bun run build:prune-package`, then `npm publish --provenance --access public`.
To cut a release:
1. Bump `version` in [`package.json`](package.json) and update [`CHANGELOG.md`](CHANGELOG.md).
2. Commit with the version as the message, tag, and push the tag:
```sh
git commit -am "v0.11.37"
git tag v0.11.37
git push origin v0.11.37
```
A successful workflow publishes the new version to NPM.
### Post-release
1. Create a [new release](https://github.com/carbon-design-system/carbon-preprocess-svelte/releases/new) on GitHub and publish it as the latest release.
2. Close out any issues resolved in the release.