Skip to content

feat(css): export PostCSS config type for type-safe configs#22792

Open
linyiru wants to merge 4 commits into
vitejs:mainfrom
linyiru:feat/postcss-user-config-type
Open

feat(css): export PostCSS config type for type-safe configs#22792
linyiru wants to merge 4 commits into
vitejs:mainfrom
linyiru:feat/postcss-user-config-type

Conversation

@linyiru

@linyiru linyiru commented Jun 27, 2026

Copy link
Copy Markdown

Description

Re-exports the PostCSS user config shape as PostcssUserConfig from vite, so PostCSS configs can be typed against the same definition Vite loads them with:

import type { PostcssUserConfig } from 'vite'

const config: PostcssUserConfig = { plugins: [] }
export default config

Closes #19109 (approved by @bluwy in the issue). This continues the work of #22525, which was closed by its author; the only outstanding review feedback there (from @sapphi-red) was "We should use the types from postcss-load-config" instead of hand-writing the shape — this PR addresses exactly that.

Why it mirrors the type instead of re-exporting it

A literal re-export turned out not to be possible with the current type-bundling setup, for two compounding reasons:

  • postcss-load-config is a bundled devDependency, not a runtime dependency — so it can't be referenced as an external import('postcss-load-config') in the published .d.ts (consumers don't have it installed).
  • Its declarations use CommonJS export = syntax, which rolldown-plugin-dts cannot re-export through the bundled .d.ts. A named re-export fails codegen (Export 'PostcssUserConfig' is not defined), and resolving the type inline pulls in non-externalized postcss/lib/* subpaths and fails on their export = declarations.

So PostcssUserConfig is defined by mirroring postcss-load-config's Config using the already-public postcss types. To make sure the mirror can never silently drift, a type-level test asserts it stays structurally identical to the upstream Config:

export type cases = [ExpectTrue<Equal<PostcssUserConfig, PostcssLoadConfig>>]

This also caught a subtle bug in the previous attempt: it used PostCSS.TransformCallback where Config actually uses Transformer (which additionally requires postcssPlugin/postcssVersion).

Tests

  • packages/vite/src/node/__tests_dts__/postcss.ts — a type-only test that (1) locks PostcssUserConfig to postcss-load-config's Config via Equal, and (2) checks both the array and object plugin formats are accepted and an invalid value is rejected. It runs as part of pnpm typecheck (tsc -p src/node/__tests_dts__).
  • Verified locally: pnpm typecheck, pnpm build-types (roll + check), eslint, and oxfmt --check all pass.

Notes for reviewers

  • The mirror + Equal lock is the approach I landed on given the export = / bundled-dep constraints above. If you'd prefer a literal reference (e.g. by also externalizing postcss subpaths in rolldown.dts.config.ts, which leaks import ... from "postcss/lib/processor" into the public .d.ts), happy to switch — let me know.
  • No docs change included: the type carries inline JSDoc with a usage example. Glad to add a docs note if you'd like one.

@linyiru linyiru marked this pull request as draft June 27, 2026 20:29
@linyiru linyiru force-pushed the feat/postcss-user-config-type branch 3 times, most recently from f3fc791 to ec6bbc4 Compare June 27, 2026 20:48
@linyiru linyiru marked this pull request as ready for review June 27, 2026 20:52
@linyiru linyiru force-pushed the feat/postcss-user-config-type branch from ec6bbc4 to 28d864f Compare June 30, 2026 02:42
@sapphi-red

Copy link
Copy Markdown
Member

Can we replace export = with export default in a plugin in rolldown.dts.config.ts?

@sapphi-red sapphi-red added feat: css p2-nice-to-have Not breaking anything but nice to have (priority) labels Jun 30, 2026
bluwy
bluwy previously approved these changes Jun 30, 2026
Comment on lines +124 to +130
* ```
*
* `postcss-load-config` is bundled into Vite and its types are declared with
* CommonJS `export =` syntax, so they cannot be re-exported through the bundled
* `.d.ts`. This mirrors its `Config` type instead, and a type-level test in
* `__tests_dts__/postcss.ts` asserts the two stay structurally identical.
*/

@bluwy bluwy Jun 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this part of the comment is suitable for the public API, so we should remove it

Suggested change
* ```
*
* `postcss-load-config` is bundled into Vite and its types are declared with
* CommonJS `export =` syntax, so they cannot be re-exported through the bundled
* `.d.ts`. This mirrors its `Config` type instead, and a type-level test in
* `__tests_dts__/postcss.ts` asserts the two stay structurally identical.
*/
* ```
*/

Alternatively, can be moved as a normal comment if you prefer.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 1e8c087. The mirrored type is gone in favor of the literal re-export, and that paragraph went with it. The JSDoc now only has the description and the usage example; the bundling details moved to the plugin comment in rolldown.dts.config.ts.

@bluwy bluwy dismissed their stale review June 30, 2026 08:30

Didn't mean to approve

@linyiru linyiru force-pushed the feat/postcss-user-config-type branch from 28d864f to febafaf Compare July 8, 2026 04:14
@linyiru

linyiru commented Jul 8, 2026

Copy link
Copy Markdown
Author

Done, switched to your suggested approach in the latest commit. Two things turned out to be needed in the plugin besides the export = to export default replacement:

  1. Redirecting the deep import Processor from 'postcss/lib/processor' to the postcss package entry. The postcss/lib/*.d.ts files declare namespace members that are not named exports, so bundling them fails with MISSING_EXPORT errors. The package entry is an external dependency, so the reference stays as a plain import.
  2. Unwrapping declare namespace postcssrc { ... } into top-level named exports. A namespace is a single declaration and cannot be partially tree-shaken, so without this the whole namespace plus the postcssrc function ends up inlined in the public d.ts. With it, tree shaking keeps only Config and ConfigPlugin (9 lines, referencing the external postcss types).

With the module rewritten to plain ESM, css.ts can use the literal named re-export: export type { Config as PostcssUserConfig } from 'postcss-load-config'. The mirrored type is gone.

The plugin verifies the dts shape it rewrites and fails the build with a clear message if a future postcss-load-config update changes it. The inlined postcss import collides with Vite's own Plugin type, handled with an identifierReplacements entry (Plugin$2 to PostCSS.Plugin), same mechanism as the existing Rolldown.Plugin entry.

Resulting public d.ts:

type ConfigPlugin = Transformer | PostCSS.Plugin | Processor;
interface Config {
  parser?: string | ProcessOptions['parser'] | false;
  stringifier?: string | ProcessOptions['stringifier'] | false;
  syntax?: string | ProcessOptions['syntax'] | false;
  map?: string | false;
  from?: string;
  to?: string;
  plugins?: Array<ConfigPlugin | false> | Record<string, object | false>;
}

Verified locally: build-types (including the patchTypes guards and lib check), typecheck, lint and format all pass.

@linyiru linyiru requested a review from bluwy July 8, 2026 04:17
linyiru added 2 commits July 8, 2026 21:02
Re-exports the PostCSS user config shape as `PostcssUserConfig` from
`vite`, so PostCSS configs can be typed against the same definition Vite
loads them with:

```ts
import type { PostcssUserConfig } from 'vite'

const config: PostcssUserConfig = { plugins: [] }
export default config
```

The type mirrors `postcss-load-config`'s `Config`. `postcss-load-config`
is bundled into Vite and its declarations use CommonJS `export =` syntax,
which `rolldown-plugin-dts` cannot re-export through the bundled `.d.ts`,
so the shape is mirrored from `postcss` types instead. A type-level test
asserts the mirror stays structurally identical to the upstream `Config`,
so it cannot silently drift.

Closes vitejs#19109
Replaces the mirrored PostcssUserConfig type with a literal re-export of
postcss-load-config's Config, per review feedback. A new plugin in
rolldown.dts.config.ts rewrites postcss-load-config's dts to plain ESM:

- redirects the deep `postcss/lib/processor` import to the `postcss`
  package entry (external), whose `export =` declarations otherwise fail
  to bundle
- unwraps the `declare namespace postcssrc` block into top-level named
  exports so tree shaking keeps only `Config` and `ConfigPlugin`
- replaces `export = postcssrc` with `export default postcssrc`

The plugin verifies the dts shape it rewrites and fails the build if a
postcss-load-config update changes it. The inlined postcss import
collides with Vite's own `Plugin` type, handled by an
`identifierReplacements` entry (`Plugin$2` -> `PostCSS.Plugin`).
@linyiru linyiru force-pushed the feat/postcss-user-config-type branch from 1e8c087 to 3615f3c Compare July 9, 2026 02:02
Comment thread packages/vite/src/node/__tests_dts__/postcss.ts Outdated

@sapphi-red sapphi-red left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat: css p2-nice-to-have Not breaking anything but nice to have (priority)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Re-export PostCSS config type from postcss-load-config

3 participants