feat(compiler-sfc): built-in support for css modules
This commit is contained in:
@@ -1,70 +1,76 @@
|
||||
import { compileStyle } from '../src/compileStyle'
|
||||
import { compileStyle, compileStyleAsync } from '../src/compileStyle'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
function compile(source: string): string {
|
||||
const res = compileStyle({
|
||||
source,
|
||||
filename: 'test.css',
|
||||
id: 'test'
|
||||
})
|
||||
if (res.errors.length) {
|
||||
res.errors.forEach(err => {
|
||||
console.error(err)
|
||||
})
|
||||
expect(res.errors.length).toBe(0)
|
||||
}
|
||||
return res.code
|
||||
}
|
||||
|
||||
describe('SFC scoped CSS', () => {
|
||||
mockWarn()
|
||||
|
||||
function compileScoped(source: string): string {
|
||||
const res = compileStyle({
|
||||
source,
|
||||
filename: 'test.css',
|
||||
id: 'test',
|
||||
scoped: true
|
||||
})
|
||||
if (res.errors.length) {
|
||||
res.errors.forEach(err => {
|
||||
console.error(err)
|
||||
})
|
||||
expect(res.errors.length).toBe(0)
|
||||
}
|
||||
return res.code
|
||||
}
|
||||
|
||||
test('simple selectors', () => {
|
||||
expect(compile(`h1 { color: red; }`)).toMatch(`h1[test] { color: red;`)
|
||||
expect(compile(`.foo { color: red; }`)).toMatch(`.foo[test] { color: red;`)
|
||||
expect(compileScoped(`h1 { color: red; }`)).toMatch(
|
||||
`h1[test] { color: red;`
|
||||
)
|
||||
expect(compileScoped(`.foo { color: red; }`)).toMatch(
|
||||
`.foo[test] { color: red;`
|
||||
)
|
||||
})
|
||||
|
||||
test('descendent selector', () => {
|
||||
expect(compile(`h1 .foo { color: red; }`)).toMatch(
|
||||
expect(compileScoped(`h1 .foo { color: red; }`)).toMatch(
|
||||
`h1 .foo[test] { color: red;`
|
||||
)
|
||||
})
|
||||
|
||||
test('multiple selectors', () => {
|
||||
expect(compile(`h1 .foo, .bar, .baz { color: red; }`)).toMatch(
|
||||
expect(compileScoped(`h1 .foo, .bar, .baz { color: red; }`)).toMatch(
|
||||
`h1 .foo[test], .bar[test], .baz[test] { color: red;`
|
||||
)
|
||||
})
|
||||
|
||||
test('pseudo class', () => {
|
||||
expect(compile(`.foo:after { color: red; }`)).toMatch(
|
||||
expect(compileScoped(`.foo:after { color: red; }`)).toMatch(
|
||||
`.foo[test]:after { color: red;`
|
||||
)
|
||||
})
|
||||
|
||||
test('pseudo element', () => {
|
||||
expect(compile(`::selection { display: none; }`)).toMatch(
|
||||
expect(compileScoped(`::selection { display: none; }`)).toMatch(
|
||||
'[test]::selection {'
|
||||
)
|
||||
})
|
||||
|
||||
test('spaces before pseudo element', () => {
|
||||
const code = compile(`.abc, ::selection { color: red; }`)
|
||||
const code = compileScoped(`.abc, ::selection { color: red; }`)
|
||||
expect(code).toMatch('.abc[test],')
|
||||
expect(code).toMatch('[test]::selection {')
|
||||
})
|
||||
|
||||
test('::v-deep', () => {
|
||||
expect(compile(`::v-deep(.foo) { color: red; }`)).toMatchInlineSnapshot(`
|
||||
expect(compileScoped(`::v-deep(.foo) { color: red; }`))
|
||||
.toMatchInlineSnapshot(`
|
||||
"[test] .foo { color: red;
|
||||
}"
|
||||
`)
|
||||
expect(compile(`::v-deep(.foo .bar) { color: red; }`))
|
||||
expect(compileScoped(`::v-deep(.foo .bar) { color: red; }`))
|
||||
.toMatchInlineSnapshot(`
|
||||
"[test] .foo .bar { color: red;
|
||||
}"
|
||||
`)
|
||||
expect(compile(`.baz .qux ::v-deep(.foo .bar) { color: red; }`))
|
||||
expect(compileScoped(`.baz .qux ::v-deep(.foo .bar) { color: red; }`))
|
||||
.toMatchInlineSnapshot(`
|
||||
".baz .qux[test] .foo .bar { color: red;
|
||||
}"
|
||||
@@ -72,16 +78,17 @@ describe('SFC scoped CSS', () => {
|
||||
})
|
||||
|
||||
test('::v-slotted', () => {
|
||||
expect(compile(`::v-slotted(.foo) { color: red; }`)).toMatchInlineSnapshot(`
|
||||
expect(compileScoped(`::v-slotted(.foo) { color: red; }`))
|
||||
.toMatchInlineSnapshot(`
|
||||
".foo[test-s] { color: red;
|
||||
}"
|
||||
`)
|
||||
expect(compile(`::v-slotted(.foo .bar) { color: red; }`))
|
||||
expect(compileScoped(`::v-slotted(.foo .bar) { color: red; }`))
|
||||
.toMatchInlineSnapshot(`
|
||||
".foo .bar[test-s] { color: red;
|
||||
}"
|
||||
`)
|
||||
expect(compile(`.baz .qux ::v-slotted(.foo .bar) { color: red; }`))
|
||||
expect(compileScoped(`.baz .qux ::v-slotted(.foo .bar) { color: red; }`))
|
||||
.toMatchInlineSnapshot(`
|
||||
".baz .qux .foo .bar[test-s] { color: red;
|
||||
}"
|
||||
@@ -89,17 +96,18 @@ describe('SFC scoped CSS', () => {
|
||||
})
|
||||
|
||||
test('::v-global', () => {
|
||||
expect(compile(`::v-global(.foo) { color: red; }`)).toMatchInlineSnapshot(`
|
||||
expect(compileScoped(`::v-global(.foo) { color: red; }`))
|
||||
.toMatchInlineSnapshot(`
|
||||
".foo { color: red;
|
||||
}"
|
||||
`)
|
||||
expect(compile(`::v-global(.foo .bar) { color: red; }`))
|
||||
expect(compileScoped(`::v-global(.foo .bar) { color: red; }`))
|
||||
.toMatchInlineSnapshot(`
|
||||
".foo .bar { color: red;
|
||||
}"
|
||||
`)
|
||||
// global ignores anything before it
|
||||
expect(compile(`.baz .qux ::v-global(.foo .bar) { color: red; }`))
|
||||
expect(compileScoped(`.baz .qux ::v-global(.foo .bar) { color: red; }`))
|
||||
.toMatchInlineSnapshot(`
|
||||
".foo .bar { color: red;
|
||||
}"
|
||||
@@ -107,7 +115,7 @@ describe('SFC scoped CSS', () => {
|
||||
})
|
||||
|
||||
test('media query', () => {
|
||||
expect(compile(`@media print { .foo { color: red }}`))
|
||||
expect(compileScoped(`@media print { .foo { color: red }}`))
|
||||
.toMatchInlineSnapshot(`
|
||||
"@media print {
|
||||
.foo[test] { color: red
|
||||
@@ -116,7 +124,7 @@ describe('SFC scoped CSS', () => {
|
||||
})
|
||||
|
||||
test('supports query', () => {
|
||||
expect(compile(`@supports(display: grid) { .foo { display: grid }}`))
|
||||
expect(compileScoped(`@supports(display: grid) { .foo { display: grid }}`))
|
||||
.toMatchInlineSnapshot(`
|
||||
"@supports(display: grid) {
|
||||
.foo[test] { display: grid
|
||||
@@ -125,7 +133,7 @@ describe('SFC scoped CSS', () => {
|
||||
})
|
||||
|
||||
test('scoped keyframes', () => {
|
||||
const style = compile(`
|
||||
const style = compileScoped(`
|
||||
.anim {
|
||||
animation: color 5s infinite, other 5s;
|
||||
}
|
||||
@@ -184,13 +192,7 @@ describe('SFC scoped CSS', () => {
|
||||
|
||||
// vue-loader/#1370
|
||||
test('spaces after selector', () => {
|
||||
const { code } = compileStyle({
|
||||
source: `.foo , .bar { color: red; }`,
|
||||
filename: 'test.css',
|
||||
id: 'test'
|
||||
})
|
||||
|
||||
expect(code).toMatchInlineSnapshot(`
|
||||
expect(compileScoped(`.foo , .bar { color: red; }`)).toMatchInlineSnapshot(`
|
||||
".foo[test], .bar[test] { color: red;
|
||||
}"
|
||||
`)
|
||||
@@ -198,11 +200,12 @@ describe('SFC scoped CSS', () => {
|
||||
|
||||
describe('deprecated syntax', () => {
|
||||
test('::v-deep as combinator', () => {
|
||||
expect(compile(`::v-deep .foo { color: red; }`)).toMatchInlineSnapshot(`
|
||||
expect(compileScoped(`::v-deep .foo { color: red; }`))
|
||||
.toMatchInlineSnapshot(`
|
||||
"[test] .foo { color: red;
|
||||
}"
|
||||
`)
|
||||
expect(compile(`.bar ::v-deep .foo { color: red; }`))
|
||||
expect(compileScoped(`.bar ::v-deep .foo { color: red; }`))
|
||||
.toMatchInlineSnapshot(`
|
||||
".bar[test] .foo { color: red;
|
||||
}"
|
||||
@@ -213,7 +216,7 @@ describe('SFC scoped CSS', () => {
|
||||
})
|
||||
|
||||
test('>>> (deprecated syntax)', () => {
|
||||
const code = compile(`>>> .foo { color: red; }`)
|
||||
const code = compileScoped(`>>> .foo { color: red; }`)
|
||||
expect(code).toMatchInlineSnapshot(`
|
||||
"[test] .foo { color: red;
|
||||
}"
|
||||
@@ -224,7 +227,7 @@ describe('SFC scoped CSS', () => {
|
||||
})
|
||||
|
||||
test('/deep/ (deprecated syntax)', () => {
|
||||
const code = compile(`/deep/ .foo { color: red; }`)
|
||||
const code = compileScoped(`/deep/ .foo { color: red; }`)
|
||||
expect(code).toMatchInlineSnapshot(`
|
||||
"[test] .foo { color: red;
|
||||
}"
|
||||
@@ -235,3 +238,35 @@ describe('SFC scoped CSS', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SFC CSS modules', () => {
|
||||
test('should include resulting classes object in result', async () => {
|
||||
const result = await compileStyleAsync({
|
||||
source: `.red { color: red }\n.green { color: green }\n:global(.blue) { color: blue }`,
|
||||
filename: `test.css`,
|
||||
id: 'test',
|
||||
modules: true
|
||||
})
|
||||
expect(result.modules).toBeDefined()
|
||||
expect(result.modules!.red).toMatch('_red_')
|
||||
expect(result.modules!.green).toMatch('_green_')
|
||||
expect(result.modules!.blue).toBeUndefined()
|
||||
})
|
||||
|
||||
test('postcss-modules options', async () => {
|
||||
const result = await compileStyleAsync({
|
||||
source: `:local(.foo-bar) { color: red }\n.baz-qux { color: green }`,
|
||||
filename: `test.css`,
|
||||
id: 'test',
|
||||
modules: true,
|
||||
modulesOptions: {
|
||||
scopeBehaviour: 'global',
|
||||
generateScopedName: `[name]__[local]__[hash:base64:5]`,
|
||||
localsConvention: 'camelCaseOnly'
|
||||
}
|
||||
})
|
||||
expect(result.modules).toBeDefined()
|
||||
expect(result.modules!.fooBar).toMatch('__foo-bar__')
|
||||
expect(result.modules!.bazQux).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,15 +30,16 @@
|
||||
"vue": "3.0.0-beta.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.0.0-beta.3",
|
||||
"@vue/compiler-core": "3.0.0-beta.3",
|
||||
"@vue/compiler-dom": "3.0.0-beta.3",
|
||||
"@vue/compiler-ssr": "3.0.0-beta.3",
|
||||
"@vue/shared": "3.0.0-beta.3",
|
||||
"consolidate": "^0.15.1",
|
||||
"hash-sum": "^2.0.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"merge-source-map": "^1.1.0",
|
||||
"postcss": "^7.0.21",
|
||||
"postcss": "^7.0.27",
|
||||
"postcss-modules": "^2.0.0",
|
||||
"postcss-selector-parser": "^6.0.2",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
|
||||
@@ -25,6 +25,20 @@ export interface SFCStyleCompileOptions {
|
||||
|
||||
export interface SFCAsyncStyleCompileOptions extends SFCStyleCompileOptions {
|
||||
isAsync?: boolean
|
||||
// css modules support, note this requires async so that we can get the
|
||||
// resulting json
|
||||
modules?: boolean
|
||||
// maps to postcss-modules options
|
||||
// https://github.com/css-modules/postcss-modules
|
||||
modulesOptions?: {
|
||||
scopeBehaviour?: 'global' | 'local'
|
||||
globalModulePaths?: string[]
|
||||
generateScopedName?:
|
||||
| string
|
||||
| ((name: string, filename: string, css: string) => string)
|
||||
hashPrefix?: string
|
||||
localsConvention?: 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly'
|
||||
}
|
||||
}
|
||||
|
||||
export interface SFCStyleCompileResults {
|
||||
@@ -32,6 +46,7 @@ export interface SFCStyleCompileResults {
|
||||
map: RawSourceMap | undefined
|
||||
rawResult: LazyResult | Result | undefined
|
||||
errors: Error[]
|
||||
modules?: Record<string, string>
|
||||
}
|
||||
|
||||
export function compileStyle(
|
||||
@@ -44,7 +59,7 @@ export function compileStyle(
|
||||
}
|
||||
|
||||
export function compileStyleAsync(
|
||||
options: SFCStyleCompileOptions
|
||||
options: SFCAsyncStyleCompileOptions
|
||||
): Promise<SFCStyleCompileResults> {
|
||||
return doCompileStyle({ ...options, isAsync: true }) as Promise<
|
||||
SFCStyleCompileResults
|
||||
@@ -57,8 +72,10 @@ export function doCompileStyle(
|
||||
const {
|
||||
filename,
|
||||
id,
|
||||
scoped = true,
|
||||
scoped = false,
|
||||
trim = true,
|
||||
modules = false,
|
||||
modulesOptions = {},
|
||||
preprocessLang,
|
||||
postcssOptions,
|
||||
postcssPlugins
|
||||
@@ -75,6 +92,23 @@ export function doCompileStyle(
|
||||
if (scoped) {
|
||||
plugins.push(scopedPlugin(id))
|
||||
}
|
||||
let cssModules: Record<string, string> | undefined
|
||||
if (modules) {
|
||||
if (options.isAsync) {
|
||||
plugins.push(
|
||||
require('postcss-modules')({
|
||||
...modulesOptions,
|
||||
getJSON: (cssFileName: string, json: Record<string, string>) => {
|
||||
cssModules = json
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
throw new Error(
|
||||
'`modules` option can only be used with compileStyleAsync().'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const postCSSOptions: ProcessOptions = {
|
||||
...postcssOptions,
|
||||
@@ -108,6 +142,7 @@ export function doCompileStyle(
|
||||
code: result.css || '',
|
||||
map: result.map && (result.map.toJSON() as any),
|
||||
errors,
|
||||
modules: cssModules,
|
||||
rawResult: result
|
||||
}))
|
||||
.catch(error => ({
|
||||
|
||||
Reference in New Issue
Block a user