Skip to content

Commit fa216a0

Browse files
committed
feat(compiler-sfc): built-in support for css modules
1 parent 20d425f commit fa216a0

File tree

4 files changed

+333
-52
lines changed

4 files changed

+333
-52
lines changed

packages/compiler-sfc/__tests__/compileStyle.spec.ts

+81-46
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,121 @@
1-
import { compileStyle } from '../src/compileStyle'
1+
import { compileStyle, compileStyleAsync } from '../src/compileStyle'
22
import { mockWarn } from '@vue/shared'
33

4-
function compile(source: string): string {
5-
const res = compileStyle({
6-
source,
7-
filename: 'test.css',
8-
id: 'test'
9-
})
10-
if (res.errors.length) {
11-
res.errors.forEach(err => {
12-
console.error(err)
13-
})
14-
expect(res.errors.length).toBe(0)
15-
}
16-
return res.code
17-
}
18-
194
describe('SFC scoped CSS', () => {
205
mockWarn()
216

7+
function compileScoped(source: string): string {
8+
const res = compileStyle({
9+
source,
10+
filename: 'test.css',
11+
id: 'test',
12+
scoped: true
13+
})
14+
if (res.errors.length) {
15+
res.errors.forEach(err => {
16+
console.error(err)
17+
})
18+
expect(res.errors.length).toBe(0)
19+
}
20+
return res.code
21+
}
22+
2223
test('simple selectors', () => {
23-
expect(compile(`h1 { color: red; }`)).toMatch(`h1[test] { color: red;`)
24-
expect(compile(`.foo { color: red; }`)).toMatch(`.foo[test] { color: red;`)
24+
expect(compileScoped(`h1 { color: red; }`)).toMatch(
25+
`h1[test] { color: red;`
26+
)
27+
expect(compileScoped(`.foo { color: red; }`)).toMatch(
28+
`.foo[test] { color: red;`
29+
)
2530
})
2631

2732
test('descendent selector', () => {
28-
expect(compile(`h1 .foo { color: red; }`)).toMatch(
33+
expect(compileScoped(`h1 .foo { color: red; }`)).toMatch(
2934
`h1 .foo[test] { color: red;`
3035
)
3136
})
3237

3338
test('multiple selectors', () => {
34-
expect(compile(`h1 .foo, .bar, .baz { color: red; }`)).toMatch(
39+
expect(compileScoped(`h1 .foo, .bar, .baz { color: red; }`)).toMatch(
3540
`h1 .foo[test], .bar[test], .baz[test] { color: red;`
3641
)
3742
})
3843

3944
test('pseudo class', () => {
40-
expect(compile(`.foo:after { color: red; }`)).toMatch(
45+
expect(compileScoped(`.foo:after { color: red; }`)).toMatch(
4146
`.foo[test]:after { color: red;`
4247
)
4348
})
4449

4550
test('pseudo element', () => {
46-
expect(compile(`::selection { display: none; }`)).toMatch(
51+
expect(compileScoped(`::selection { display: none; }`)).toMatch(
4752
'[test]::selection {'
4853
)
4954
})
5055

5156
test('spaces before pseudo element', () => {
52-
const code = compile(`.abc, ::selection { color: red; }`)
57+
const code = compileScoped(`.abc, ::selection { color: red; }`)
5358
expect(code).toMatch('.abc[test],')
5459
expect(code).toMatch('[test]::selection {')
5560
})
5661

5762
test('::v-deep', () => {
58-
expect(compile(`::v-deep(.foo) { color: red; }`)).toMatchInlineSnapshot(`
63+
expect(compileScoped(`::v-deep(.foo) { color: red; }`))
64+
.toMatchInlineSnapshot(`
5965
"[test] .foo { color: red;
6066
}"
6167
`)
62-
expect(compile(`::v-deep(.foo .bar) { color: red; }`))
68+
expect(compileScoped(`::v-deep(.foo .bar) { color: red; }`))
6369
.toMatchInlineSnapshot(`
6470
"[test] .foo .bar { color: red;
6571
}"
6672
`)
67-
expect(compile(`.baz .qux ::v-deep(.foo .bar) { color: red; }`))
73+
expect(compileScoped(`.baz .qux ::v-deep(.foo .bar) { color: red; }`))
6874
.toMatchInlineSnapshot(`
6975
".baz .qux[test] .foo .bar { color: red;
7076
}"
7177
`)
7278
})
7379

7480
test('::v-slotted', () => {
75-
expect(compile(`::v-slotted(.foo) { color: red; }`)).toMatchInlineSnapshot(`
81+
expect(compileScoped(`::v-slotted(.foo) { color: red; }`))
82+
.toMatchInlineSnapshot(`
7683
".foo[test-s] { color: red;
7784
}"
7885
`)
79-
expect(compile(`::v-slotted(.foo .bar) { color: red; }`))
86+
expect(compileScoped(`::v-slotted(.foo .bar) { color: red; }`))
8087
.toMatchInlineSnapshot(`
8188
".foo .bar[test-s] { color: red;
8289
}"
8390
`)
84-
expect(compile(`.baz .qux ::v-slotted(.foo .bar) { color: red; }`))
91+
expect(compileScoped(`.baz .qux ::v-slotted(.foo .bar) { color: red; }`))
8592
.toMatchInlineSnapshot(`
8693
".baz .qux .foo .bar[test-s] { color: red;
8794
}"
8895
`)
8996
})
9097

9198
test('::v-global', () => {
92-
expect(compile(`::v-global(.foo) { color: red; }`)).toMatchInlineSnapshot(`
99+
expect(compileScoped(`::v-global(.foo) { color: red; }`))
100+
.toMatchInlineSnapshot(`
93101
".foo { color: red;
94102
}"
95103
`)
96-
expect(compile(`::v-global(.foo .bar) { color: red; }`))
104+
expect(compileScoped(`::v-global(.foo .bar) { color: red; }`))
97105
.toMatchInlineSnapshot(`
98106
".foo .bar { color: red;
99107
}"
100108
`)
101109
// global ignores anything before it
102-
expect(compile(`.baz .qux ::v-global(.foo .bar) { color: red; }`))
110+
expect(compileScoped(`.baz .qux ::v-global(.foo .bar) { color: red; }`))
103111
.toMatchInlineSnapshot(`
104112
".foo .bar { color: red;
105113
}"
106114
`)
107115
})
108116

109117
test('media query', () => {
110-
expect(compile(`@media print { .foo { color: red }}`))
118+
expect(compileScoped(`@media print { .foo { color: red }}`))
111119
.toMatchInlineSnapshot(`
112120
"@media print {
113121
.foo[test] { color: red
@@ -116,7 +124,7 @@ describe('SFC scoped CSS', () => {
116124
})
117125

118126
test('supports query', () => {
119-
expect(compile(`@supports(display: grid) { .foo { display: grid }}`))
127+
expect(compileScoped(`@supports(display: grid) { .foo { display: grid }}`))
120128
.toMatchInlineSnapshot(`
121129
"@supports(display: grid) {
122130
.foo[test] { display: grid
@@ -125,7 +133,7 @@ describe('SFC scoped CSS', () => {
125133
})
126134

127135
test('scoped keyframes', () => {
128-
const style = compile(`
136+
const style = compileScoped(`
129137
.anim {
130138
animation: color 5s infinite, other 5s;
131139
}
@@ -184,25 +192,20 @@ describe('SFC scoped CSS', () => {
184192

185193
// vue-loader/#1370
186194
test('spaces after selector', () => {
187-
const { code } = compileStyle({
188-
source: `.foo , .bar { color: red; }`,
189-
filename: 'test.css',
190-
id: 'test'
191-
})
192-
193-
expect(code).toMatchInlineSnapshot(`
195+
expect(compileScoped(`.foo , .bar { color: red; }`)).toMatchInlineSnapshot(`
194196
".foo[test], .bar[test] { color: red;
195197
}"
196198
`)
197199
})
198200

199201
describe('deprecated syntax', () => {
200202
test('::v-deep as combinator', () => {
201-
expect(compile(`::v-deep .foo { color: red; }`)).toMatchInlineSnapshot(`
203+
expect(compileScoped(`::v-deep .foo { color: red; }`))
204+
.toMatchInlineSnapshot(`
202205
"[test] .foo { color: red;
203206
}"
204207
`)
205-
expect(compile(`.bar ::v-deep .foo { color: red; }`))
208+
expect(compileScoped(`.bar ::v-deep .foo { color: red; }`))
206209
.toMatchInlineSnapshot(`
207210
".bar[test] .foo { color: red;
208211
}"
@@ -213,7 +216,7 @@ describe('SFC scoped CSS', () => {
213216
})
214217

215218
test('>>> (deprecated syntax)', () => {
216-
const code = compile(`>>> .foo { color: red; }`)
219+
const code = compileScoped(`>>> .foo { color: red; }`)
217220
expect(code).toMatchInlineSnapshot(`
218221
"[test] .foo { color: red;
219222
}"
@@ -224,7 +227,7 @@ describe('SFC scoped CSS', () => {
224227
})
225228

226229
test('/deep/ (deprecated syntax)', () => {
227-
const code = compile(`/deep/ .foo { color: red; }`)
230+
const code = compileScoped(`/deep/ .foo { color: red; }`)
228231
expect(code).toMatchInlineSnapshot(`
229232
"[test] .foo { color: red;
230233
}"
@@ -235,3 +238,35 @@ describe('SFC scoped CSS', () => {
235238
})
236239
})
237240
})
241+
242+
describe('SFC CSS modules', () => {
243+
test('should include resulting classes object in result', async () => {
244+
const result = await compileStyleAsync({
245+
source: `.red { color: red }\n.green { color: green }\n:global(.blue) { color: blue }`,
246+
filename: `test.css`,
247+
id: 'test',
248+
modules: true
249+
})
250+
expect(result.modules).toBeDefined()
251+
expect(result.modules!.red).toMatch('_red_')
252+
expect(result.modules!.green).toMatch('_green_')
253+
expect(result.modules!.blue).toBeUndefined()
254+
})
255+
256+
test('postcss-modules options', async () => {
257+
const result = await compileStyleAsync({
258+
source: `:local(.foo-bar) { color: red }\n.baz-qux { color: green }`,
259+
filename: `test.css`,
260+
id: 'test',
261+
modules: true,
262+
modulesOptions: {
263+
scopeBehaviour: 'global',
264+
generateScopedName: `[name]__[local]__[hash:base64:5]`,
265+
localsConvention: 'camelCaseOnly'
266+
}
267+
})
268+
expect(result.modules).toBeDefined()
269+
expect(result.modules!.fooBar).toMatch('__foo-bar__')
270+
expect(result.modules!.bazQux).toBeUndefined()
271+
})
272+
})

packages/compiler-sfc/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,16 @@
3030
"vue": "3.0.0-beta.3"
3131
},
3232
"dependencies": {
33-
"@vue/shared": "3.0.0-beta.3",
3433
"@vue/compiler-core": "3.0.0-beta.3",
3534
"@vue/compiler-dom": "3.0.0-beta.3",
3635
"@vue/compiler-ssr": "3.0.0-beta.3",
36+
"@vue/shared": "3.0.0-beta.3",
3737
"consolidate": "^0.15.1",
3838
"hash-sum": "^2.0.0",
3939
"lru-cache": "^5.1.1",
4040
"merge-source-map": "^1.1.0",
41-
"postcss": "^7.0.21",
41+
"postcss": "^7.0.27",
42+
"postcss-modules": "^2.0.0",
4243
"postcss-selector-parser": "^6.0.2",
4344
"source-map": "^0.6.1"
4445
},

packages/compiler-sfc/src/compileStyle.ts

+37-2
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,28 @@ export interface SFCStyleCompileOptions {
2525

2626
export interface SFCAsyncStyleCompileOptions extends SFCStyleCompileOptions {
2727
isAsync?: boolean
28+
// css modules support, note this requires async so that we can get the
29+
// resulting json
30+
modules?: boolean
31+
// maps to postcss-modules options
32+
// https://github.com/css-modules/postcss-modules
33+
modulesOptions?: {
34+
scopeBehaviour?: 'global' | 'local'
35+
globalModulePaths?: string[]
36+
generateScopedName?:
37+
| string
38+
| ((name: string, filename: string, css: string) => string)
39+
hashPrefix?: string
40+
localsConvention?: 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly'
41+
}
2842
}
2943

3044
export interface SFCStyleCompileResults {
3145
code: string
3246
map: RawSourceMap | undefined
3347
rawResult: LazyResult | Result | undefined
3448
errors: Error[]
49+
modules?: Record<string, string>
3550
}
3651

3752
export function compileStyle(
@@ -44,7 +59,7 @@ export function compileStyle(
4459
}
4560

4661
export function compileStyleAsync(
47-
options: SFCStyleCompileOptions
62+
options: SFCAsyncStyleCompileOptions
4863
): Promise<SFCStyleCompileResults> {
4964
return doCompileStyle({ ...options, isAsync: true }) as Promise<
5065
SFCStyleCompileResults
@@ -57,8 +72,10 @@ export function doCompileStyle(
5772
const {
5873
filename,
5974
id,
60-
scoped = true,
75+
scoped = false,
6176
trim = true,
77+
modules = false,
78+
modulesOptions = {},
6279
preprocessLang,
6380
postcssOptions,
6481
postcssPlugins
@@ -75,6 +92,23 @@ export function doCompileStyle(
7592
if (scoped) {
7693
plugins.push(scopedPlugin(id))
7794
}
95+
let cssModules: Record<string, string> | undefined
96+
if (modules) {
97+
if (options.isAsync) {
98+
plugins.push(
99+
require('postcss-modules')({
100+
...modulesOptions,
101+
getJSON: (cssFileName: string, json: Record<string, string>) => {
102+
cssModules = json
103+
}
104+
})
105+
)
106+
} else {
107+
throw new Error(
108+
'`modules` option can only be used with compileStyleAsync().'
109+
)
110+
}
111+
}
78112

79113
const postCSSOptions: ProcessOptions = {
80114
...postcssOptions,
@@ -108,6 +142,7 @@ export function doCompileStyle(
108142
code: result.css || '',
109143
map: result.map && (result.map.toJSON() as any),
110144
errors,
145+
modules: cssModules,
111146
rawResult: result
112147
}))
113148
.catch(error => ({

0 commit comments

Comments
 (0)