Skip to content

Commit b566f6d

Browse files
sxzzlazerg
andcommitted
feat(css): support function form for css.modules.localsConvention
Closes #1034 Co-authored-by: lazerg <lazerg2@gmail.com>
1 parent ebee509 commit b566f6d

15 files changed

Lines changed: 221 additions & 16 deletions

docs/options/css.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,26 @@ Controls how class names are exported in JavaScript:
386386
| `'dashes'` | `foo-bar` | `foo-bar`, `fooBar` |
387387
| `'dashesOnly'` | `foo-bar` | `fooBar` |
388388

389+
You can also provide a function to generate a custom export name. It receives
390+
the original class name, generated scoped class name, and input file:
391+
392+
```ts
393+
export default defineConfig({
394+
css: {
395+
modules: {
396+
localsConvention: (originalClassName, generatedClassName, inputFile) => {
397+
return originalClassName.replaceAll(/-([a-z0-9])/g, (_, character) =>
398+
character.toUpperCase(),
399+
)
400+
},
401+
},
402+
},
403+
})
404+
```
405+
406+
The function form works with both CSS transformers. It changes the exported
407+
JavaScript key only; `generatedClassName` remains the value of that export.
408+
389409
### `generateScopedName`
390410

391411
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]'`).

docs/zh-CN/options/css.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,25 @@ export default defineConfig({
386386
| `'dashes'` | `foo-bar` | `foo-bar``fooBar` |
387387
| `'dashesOnly'` | `foo-bar` | `fooBar` |
388388

389+
你也可以通过函数生成自定义导出名称。该函数接收原始类名、生成的作用域类名和输入文件:
390+
391+
```ts
392+
export default defineConfig({
393+
css: {
394+
modules: {
395+
localsConvention: (originalClassName, generatedClassName, inputFile) => {
396+
return originalClassName.replaceAll(/-([a-z0-9])/g, (_, character) =>
397+
character.toUpperCase(),
398+
)
399+
},
400+
},
401+
},
402+
})
403+
```
404+
405+
函数形式适用于两种 CSS 转换器。它仅修改导出的 JavaScript 键;
406+
`generatedClassName` 仍作为该导出的值。
407+
389408
### `generateScopedName`
390409

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
"pkg-types": "catalog:dev",
166166
"postcss": "catalog:dev",
167167
"postcss-import": "catalog:dev",
168+
"postcss-modules": "catalog:dev",
168169
"prettier": "catalog:dev",
169170
"publint": "catalog:peer",
170171
"rolldown-plugin-require-cjs": "catalog:dev",

packages/css/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { resolveCssOptions } from './options.ts'
22
export { CssPlugin } from './plugin.ts'
33
export type {
4+
CSSModulesLocalsConvention,
45
CSSModulesOptions,
56
CssOptions,
67
LessPreprocessorOptions,

packages/css/src/modules.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from 'vitest'
2-
import { modulesToEsm } from './modules.ts'
2+
import { applyLocalsConvention, modulesToEsm } from './modules.ts'
33

44
describe('modulesToEsm', () => {
55
test('exports arbitrary module keys via aliased bindings', () => {
@@ -24,3 +24,36 @@ describe('modulesToEsm', () => {
2424
)
2525
})
2626
})
27+
28+
describe('applyLocalsConvention', () => {
29+
test('camelCaseOnly exports only the camelized key', () => {
30+
const result = applyLocalsConvention(
31+
{ 'foo-bar': 'a', baz: 'b' },
32+
'camelCaseOnly',
33+
'style.module.css',
34+
)
35+
expect(result).toEqual({ fooBar: 'a', baz: 'b' })
36+
})
37+
38+
test('function convention maps each key through the callback', () => {
39+
const result = applyLocalsConvention(
40+
{ 'foo-bar': 'a', baz: 'b' },
41+
(name) => name.toUpperCase(),
42+
'style.module.css',
43+
)
44+
expect(result).toEqual({ 'FOO-BAR': 'a', BAZ: 'b' })
45+
})
46+
47+
test('function convention receives class name, scoped name, and filename', () => {
48+
const calls: Array<[string, string, string]> = []
49+
applyLocalsConvention(
50+
{ 'foo-bar': 'scoped_foo_bar' },
51+
(original, generated, file) => {
52+
calls.push([original, generated, file])
53+
return original
54+
},
55+
'style.module.css',
56+
)
57+
expect(calls).toEqual([['foo-bar', 'scoped_foo_bar', 'style.module.css']])
58+
})
59+
})

packages/css/src/modules.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { CSSModulesLocalsConvention } from './options.ts'
12
import type { CSSModuleExports } from 'lightningcss'
23

34
export function modulesToEsm(modules: Record<string, string>): string {
@@ -23,10 +24,16 @@ function dashToCamel(str: string): string {
2324

2425
export function applyLocalsConvention(
2526
modules: Record<string, string>,
26-
convention: 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly',
27+
convention: CSSModulesLocalsConvention,
28+
inputFile: string,
2729
): Record<string, string> {
2830
const result: Record<string, string> = {}
2931
for (const [key, value] of Object.entries(modules)) {
32+
if (typeof convention === 'function') {
33+
result[convention(key, value, inputFile)] = value
34+
continue
35+
}
36+
3037
const camelized = dashToCamel(key)
3138
switch (convention) {
3239
case 'camelCase':

packages/css/src/options.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ export type LightningCSSOptions = Omit<
1212
'filename' | 'code'
1313
>
1414

15+
export type CSSModulesLocalsConvention =
16+
| 'camelCase'
17+
| 'camelCaseOnly'
18+
| 'dashes'
19+
| 'dashesOnly'
20+
| ((
21+
originalClassName: string,
22+
generatedClassName: string,
23+
inputFile: string,
24+
) => string)
25+
1526
export interface CSSModulesOptions {
1627
/**
1728
* Controls the scoping behavior.
@@ -39,7 +50,7 @@ export interface CSSModulesOptions {
3950
/**
4051
* Transform convention for exported class names.
4152
*/
42-
localsConvention?: 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly'
53+
localsConvention?: CSSModulesLocalsConvention
4354

4455
/**
4556
* Whether to include global class names in the export.

packages/css/src/plugin.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,16 @@ export function CssPlugin(
174174
typeof cssConfig.css.modules === 'object'
175175
? cssConfig.css.modules
176176
: undefined
177-
if (modulesConfig?.localsConvention) {
178-
modules = applyLocalsConvention(
179-
modules,
180-
modulesConfig.localsConvention,
181-
)
177+
if (cssConfig.css.transformer === 'lightningcss') {
178+
if (modulesConfig?.localsConvention) {
179+
modules = applyLocalsConvention(
180+
modules,
181+
modulesConfig.localsConvention,
182+
cleanId,
183+
)
184+
}
185+
modulesConfig?.getJSON?.(cleanId, modules, cleanId)
182186
}
183-
modulesConfig?.getJSON?.(cleanId, modules, cleanId)
184187
modulesMap.set(id, modules)
185188
}
186189

packages/css/src/postcss.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,10 @@ export async function processWithPostCSS(
8181

8282
if (modulesOptions?.isModule) {
8383
const postcssModules: any = await importWithError('postcss-modules')
84-
const {
85-
localsConvention: _,
86-
getJSON: userGetJSON,
87-
...rest
88-
} = modulesOptions.config ?? {}
84+
const userGetJSON = modulesOptions.config?.getJSON
8985
plugins.push(
9086
(postcssModules.default ?? postcssModules)({
91-
...rest,
87+
...modulesOptions.config,
9288
getJSON(
9389
cssFileName: string,
9490
json: Record<string, string>,

pnpm-lock.yaml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)