Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions docs/options/css.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,94 @@ export function greet() {

This is useful for component libraries where you want CSS to be automatically included when users import your components.

## CSS Modules

Files with the `.module.css` extension (and preprocessor variants like `.module.scss`, `.module.less`, etc.) are treated as [CSS modules](https://github.com/css-modules/css-modules). Class names are automatically scoped and exported as a JavaScript object:

```ts
// src/index.ts
import styles from './app.module.css'

console.log(styles.title) // "scoped_title_hash"
```

```css
/* app.module.css */
.title {
color: red;
}
.content {
font-size: 14px;
}
```

The CSS is emitted with scoped class names, and the JS output exports the mapping from original to scoped names.

### Configuration

Configure CSS modules behavior via `css.modules`:

```ts
export default defineConfig({
css: {
modules: {
// Scoping behavior: 'local' (default) or 'global'
scopeBehaviour: 'local',

// Pattern for scoped class names (Lightning CSS pattern syntax)
generateScopedName: '[hash]_[local]',

// Transform class name convention in JS exports
localsConvention: 'camelCase',
},
},
})
```

Set `css.modules: false` to disable CSS modules entirely — `.module.css` files will be treated as regular CSS.

### `localsConvention`

Controls how class names are exported in JavaScript:

| Value | Input | Exports |
| ----------------- | --------- | ------------------- |
| _(not set)_ | `foo-bar` | `foo-bar` |
| `'camelCase'` | `foo-bar` | `foo-bar`, `fooBar` |
| `'camelCaseOnly'` | `foo-bar` | `fooBar` |
| `'dashes'` | `foo-bar` | `foo-bar`, `fooBar` |
| `'dashesOnly'` | `foo-bar` | `fooBar` |

### `generateScopedName`

When using `transformer: 'lightningcss'` (default), this accepts a Lightning CSS [pattern string](https://lightningcss.dev/css-modules.html#custom-naming-conventions) (e.g., `'[hash]_[local]'`).

When using `transformer: 'postcss'`, this also accepts a function:

```ts
export default defineConfig({
css: {
transformer: 'postcss',
modules: {
generateScopedName: (name, filename, css) => {
return `my-lib_${name}`
},
},
},
})
```

> [!NOTE]
> Function-form `generateScopedName` is only supported with `transformer: 'postcss'`. The Lightning CSS transformer only supports string patterns.

### Optional Dependencies

When using `transformer: 'postcss'` with CSS modules, install [`postcss-modules`](https://github.com/css-modules/postcss-modules):

```bash
npm install -D postcss postcss-modules
```

## CSS Code Splitting

### Merged Mode (Default)
Expand Down Expand Up @@ -372,6 +460,22 @@ dist/
async-abc123.css ← CSS from async chunk
```

## PostCSS Optional Peer Dependencies

When using `transformer: 'postcss'`, the following packages may need to be installed depending on the features you use:

| Package | Purpose | Required When |
| ------------------------------------------------------------------- | ---------------------------------------- | -------------------------------------- |
| [`postcss`](https://github.com/postcss/postcss) | Core PostCSS engine | Always (with `transformer: 'postcss'`) |
| [`postcss-import`](https://github.com/postcss/postcss-import) | Resolve and inline `@import` statements | CSS files use `@import` |
| [`postcss-modules`](https://github.com/css-modules/postcss-modules) | CSS modules support (scoped class names) | Using `.module.css` files |

```bash
npm install -D postcss postcss-import postcss-modules
```

All three are declared as optional peer dependencies of `@tsdown/css` and only loaded when needed.

## Options Reference

| Option | Type | Default | Description |
Expand All @@ -380,6 +484,7 @@ dist/
| `css.splitting` | `boolean` | `false` | Enable CSS code splitting per chunk |
| `css.fileName` | `string` | `'style.css'` | File name for the merged CSS file (when `splitting: false`) |
| `css.minify` | `boolean` | `false` | Enable CSS minification |
| `css.modules` | `object \| false` | `{}` | CSS modules configuration, or `false` to disable |
| `css.target` | `string \| string[] \| false` | _from `target`_ | CSS-specific syntax lowering target |
| `css.postcss` | `string \| object` | — | PostCSS config path or inline options |
| `css.preprocessorOptions` | `object` | — | Options for CSS preprocessors |
Expand Down
105 changes: 105 additions & 0 deletions docs/zh-CN/options/css.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,94 @@ export function greet() {

这对于组件库非常有用,可以确保用户导入组件时自动包含对应的 CSS。

## CSS Modules

扩展名为 `.module.css` 的文件(以及预处理器变体如 `.module.scss`、`.module.less` 等)会被视为 [CSS Modules](https://github.com/css-modules/css-modules)。类名会自动添加作用域,并作为 JavaScript 对象导出:

```ts
// src/index.ts
import styles from './app.module.css'

console.log(styles.title) // "scoped_title_hash"
```

```css
/* app.module.css */
.title {
color: red;
}
.content {
font-size: 14px;
}
```

CSS 会以作用域化的类名输出,JS 输出导出原始类名到作用域化类名的映射。

### 配置

通过 `css.modules` 配置 CSS modules 行为:

```ts
export default defineConfig({
css: {
modules: {
// 作用域行为:'local'(默认)或 'global'
scopeBehaviour: 'local',

// 作用域类名模式(Lightning CSS 模式语法)
generateScopedName: '[hash]_[local]',

// JS 导出中的类名转换约定
localsConvention: 'camelCase',
},
},
})
```

设置 `css.modules: false` 可完全禁用 CSS modules——`.module.css` 文件将被视为普通 CSS。

### `localsConvention`

控制类名在 JavaScript 中的导出方式:

| 值 | 输入 | 导出 |
| ----------------- | --------- | ------------------- |
| _(未设置)_ | `foo-bar` | `foo-bar` |
| `'camelCase'` | `foo-bar` | `foo-bar`、`fooBar` |
| `'camelCaseOnly'` | `foo-bar` | `fooBar` |
| `'dashes'` | `foo-bar` | `foo-bar`、`fooBar` |
| `'dashesOnly'` | `foo-bar` | `fooBar` |

### `generateScopedName`

使用 `transformer: 'lightningcss'`(默认)时,接受 Lightning CSS [模式字符串](https://lightningcss.dev/css-modules.html#custom-naming-conventions)(如 `'[hash]_[local]'`)。

使用 `transformer: 'postcss'` 时,还支持函数形式:

```ts
export default defineConfig({
css: {
transformer: 'postcss',
modules: {
generateScopedName: (name, filename, css) => {
return `my-lib_${name}`
},
},
},
})
```

> [!NOTE]
> 函数形式的 `generateScopedName` 仅在 `transformer: 'postcss'` 时支持。Lightning CSS 转换器仅支持字符串模式。

### 可选依赖

使用 `transformer: 'postcss'` 配合 CSS modules 时,需安装 [`postcss-modules`](https://github.com/css-modules/postcss-modules):

```bash
npm install -D postcss postcss-modules
```

## CSS 代码分割

### 合并模式(默认)
Expand Down Expand Up @@ -372,6 +460,22 @@ dist/
async-abc123.css ← 异步 chunk 的 CSS
```

## PostCSS 可选依赖

使用 `transformer: 'postcss'` 时,根据使用的功能可能需要安装以下包:

| 包 | 用途 | 何时需要 |
| ------------------------------------------------------------------- | ------------------------------ | -------------------------------------------- |
| [`postcss`](https://github.com/postcss/postcss) | PostCSS 核心引擎 | 始终需要(使用 `transformer: 'postcss'` 时) |
| [`postcss-import`](https://github.com/postcss/postcss-import) | 解析和内联 `@import` 语句 | CSS 文件使用 `@import` 时 |
| [`postcss-modules`](https://github.com/css-modules/postcss-modules) | CSS modules 支持(作用域类名) | 使用 `.module.css` 文件时 |

```bash
npm install -D postcss postcss-import postcss-modules
```

这三个包都声明为 `@tsdown/css` 的可选 peer dependencies,仅在需要时加载。

## 选项参考

| 选项 | 类型 | 默认值 | 描述 |
Expand All @@ -380,6 +484,7 @@ dist/
| `css.splitting` | `boolean` | `false` | 启用按 chunk 的 CSS 代码分割 |
| `css.fileName` | `string` | `'style.css'` | 合并 CSS 的文件名(当 `splitting: false` 时) |
| `css.minify` | `boolean` | `false` | 启用 CSS 压缩 |
| `css.modules` | `object \| false` | `{}` | CSS modules 配置,或 `false` 禁用 |
| `css.target` | `string \| string[] \| false` | _继承 `target`_ | CSS 专用语法降级目标 |
| `css.postcss` | `string \| object` | — | PostCSS 配置路径或内联选项 |
| `css.preprocessorOptions` | `object` | — | CSS 预处理器选项 |
Expand Down
4 changes: 4 additions & 0 deletions packages/css/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"peerDependencies": {
"postcss": "^8.4.0",
"postcss-import": "^16.0.0",
"postcss-modules": "^6.0.0",
"sass": "*",
"sass-embedded": "*",
"tsdown": "workspace:*"
Expand All @@ -61,6 +62,9 @@
"postcss-import": {
"optional": true
},
"postcss-modules": {
"optional": true
},
"sass": {
"optional": true
},
Expand Down
1 change: 1 addition & 0 deletions packages/css/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { resolveCssOptions } from './options.ts'
export { CssPlugin } from './plugin.ts'
export type {
CSSModulesOptions,
CssOptions,
LessPreprocessorOptions,
LightningCSSOptions,
Expand Down
34 changes: 29 additions & 5 deletions packages/css/src/lightningcss.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { extractLightningCssModuleExports } from './modules.ts'
import { compilePreprocessor, getPreprocessorLang } from './preprocessors.ts'
import { getCssResolver, resolveWithResolver } from './resolve.ts'
import type { LightningCSSOptions, PreprocessorOptions } from './options.ts'
import type { Targets } from 'lightningcss'
import type { CSSModulesConfig, Targets } from 'lightningcss'
import type { Logger } from 'tsdown/internal'

const encoder = new TextEncoder()
Expand All @@ -13,31 +14,44 @@ export interface TransformCssOptions {
target?: string[]
lightningcss?: LightningCSSOptions
minify?: boolean
cssModules?: boolean | CSSModulesConfig
}

export interface TransformCssResult {
code: string
modules?: Record<string, string>
}

export interface BundleCssOptions {
target?: string[]
lightningcss?: LightningCSSOptions
minify?: boolean
cssModules?: boolean | CSSModulesConfig
preprocessorOptions?: PreprocessorOptions
logger: Logger
}

export interface BundleCssResult {
code: string
deps: string[]
modules?: Record<string, string>
}

export async function transformWithLightningCSS(
code: string,
filename: string,
options: TransformCssOptions,
): Promise<string> {
): Promise<TransformCssResult> {
const targets =
options.lightningcss?.targets ??
(options.target ? esbuildTargetToLightningCSS(options.target) : undefined)
if (!targets && !options.lightningcss && !options.minify) {
return code
if (
!targets &&
!options.lightningcss &&
!options.minify &&
!options.cssModules
) {
return { code }
}

const { transform } = await import('lightningcss')
Expand All @@ -47,9 +61,15 @@ export async function transformWithLightningCSS(
...options.lightningcss,
targets,
minify: options.minify,
cssModules: options.cssModules,
})

return decoder.decode(result.code)
return {
code: decoder.decode(result.code),
modules: result.exports
? extractLightningCssModuleExports(result.exports)
: undefined,
}
}

export async function bundleWithLightningCSS(
Expand All @@ -69,6 +89,7 @@ export async function bundleWithLightningCSS(
...options.lightningcss,
targets,
minify: options.minify,
cssModules: options.cssModules,
resolver: {
async read(filePath: string) {
let fileCode: string
Expand Down Expand Up @@ -108,6 +129,9 @@ export async function bundleWithLightningCSS(
return {
code: new TextDecoder().decode(result.code),
deps,
modules: result.exports
? extractLightningCssModuleExports(result.exports)
: undefined,
}
}

Expand Down
Loading
Loading