--- /dev/null
+import { compileStyle } from '../src/compileStyle'
+import { mockWarn } from '@vue/runtime-test'
+
+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()
+
+ test('simple selectors', () => {
+ expect(compile(`h1 { color: red; }`)).toMatch(`h1[test] { color: red;`)
+ expect(compile(`.foo { color: red; }`)).toMatch(`.foo[test] { color: red;`)
+ })
+
+ test('descendent selector', () => {
+ expect(compile(`h1 .foo { color: red; }`)).toMatch(
+ `h1 .foo[test] { color: red;`
+ )
+ })
+
+ test('multiple selectors', () => {
+ expect(compile(`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(
+ `.foo[test]:after { color: red;`
+ )
+ })
+
+ test('pseudo element', () => {
+ expect(compile(`::selection { display: none; }`)).toMatch(
+ '[test]::selection {'
+ )
+ })
+
+ test('spaces before pseudo element', () => {
+ const code = compile(`.abc, ::selection { color: red; }`)
+ expect(code).toMatch('.abc[test],')
+ expect(code).toMatch('[test]::selection {')
+ })
+
+ test('::v-deep', () => {
+ expect(compile(`::v-deep(.foo) { color: red; }`)).toMatch(
+ `[test] .foo { color: red;`
+ )
+ expect(compile(`::v-deep(.foo .bar) { color: red; }`)).toMatch(
+ `[test] .foo .bar { color: red;`
+ )
+ expect(compile(`.baz .qux ::v-deep(.foo .bar) { color: red; }`)).toMatch(
+ `.baz .qux[test] .foo .bar { color: red;`
+ )
+ })
+
+ test('::v-slotted', () => {
+ expect(compile(`::v-slotted(.foo) { color: red; }`)).toMatch(
+ `.foo[test-s] { color: red;`
+ )
+ expect(compile(`::v-slotted(.foo .bar) { color: red; }`)).toMatch(
+ `.foo .bar[test-s] { color: red;`
+ )
+ expect(compile(`.baz .qux ::v-slotted(.foo .bar) { color: red; }`)).toMatch(
+ `.baz .qux[test] .foo .bar[test-s] { color: red;`
+ )
+ })
+
+ test('::v-global', () => {
+ expect(compile(`::v-global(.foo) { color: red; }`)).toMatch(
+ `.foo { color: red;`
+ )
+ expect(compile(`::v-global(.foo .bar) { color: red; }`)).toMatch(
+ `.foo .bar { color: red;`
+ )
+ // global ignores anything before it
+ expect(compile(`.baz .qux ::v-global(.foo .bar) { color: red; }`)).toMatch(
+ `.foo .bar { color: red;`
+ )
+ })
+
+ test('scoped keyframes', () => {
+ const style = compile(`
+.anim {
+ animation: color 5s infinite, other 5s;
+}
+.anim-2 {
+ animation-name: color;
+ animation-duration: 5s;
+}
+.anim-3 {
+ animation: 5s color infinite, 5s other;
+}
+.anim-multiple {
+ animation: color 5s infinite, opacity 2s;
+}
+.anim-multiple-2 {
+ animation-name: color, opacity;
+ animation-duration: 5s, 2s;
+}
+
+@keyframes color {
+ from { color: red; }
+ to { color: green; }
+}
+@-webkit-keyframes color {
+ from { color: red; }
+ to { color: green; }
+}
+@keyframes opacity {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+@-webkit-keyframes opacity {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+ `)
+
+ expect(style).toContain(
+ `.anim[test] {\n animation: color-test 5s infinite, other 5s;`
+ )
+ expect(style).toContain(`.anim-2[test] {\n animation-name: color-test`)
+ expect(style).toContain(
+ `.anim-3[test] {\n animation: 5s color-test infinite, 5s other;`
+ )
+ expect(style).toContain(`@keyframes color-test {`)
+ expect(style).toContain(`@-webkit-keyframes color-test {`)
+
+ expect(style).toContain(
+ `.anim-multiple[test] {\n animation: color-test 5s infinite,opacity-test 2s;`
+ )
+ expect(style).toContain(
+ `.anim-multiple-2[test] {\n animation-name: color-test,opacity-test;`
+ )
+ expect(style).toContain(`@keyframes opacity-test {`)
+ expect(style).toContain(`@-webkit-keyframes opacity-test {`)
+ })
+
+ // vue-loader/#1370
+ test('spaces after selector', () => {
+ const { code } = compileStyle({
+ source: `.foo , .bar { color: red; }`,
+ filename: 'test.css',
+ id: 'test'
+ })
+
+ expect(code).toMatch(`.foo[test], .bar[test] { color: red;`)
+ })
+
+ describe('deprecated syntax', () => {
+ test('::v-deep as combinator', () => {
+ expect(compile(`::v-deep .foo { color: red; }`)).toMatch(
+ `[test] .foo { color: red;`
+ )
+ expect(compile(`.bar ::v-deep .foo { color: red; }`)).toMatch(
+ `.bar[test] .foo { color: red;`
+ )
+ expect(
+ `::v-deep usage as a combinator has been deprecated.`
+ ).toHaveBeenWarned()
+ })
+
+ test('>>> (deprecated syntax)', () => {
+ const code = compile(`>>> .foo { color: red; }`)
+ expect(code).toMatch(`[test] .foo { color: red;`)
+ expect(
+ `the >>> and /deep/ combinators have been deprecated.`
+ ).toHaveBeenWarned()
+ })
+
+ test('/deep/ (deprecated syntax)', () => {
+ const code = compile(`/deep/ .foo { color: red; }`)
+ expect(code).toMatch(`[test] .foo { color: red;`)
+ expect(
+ `the >>> and /deep/ combinators have been deprecated.`
+ ).toHaveBeenWarned()
+ })
+ })
+})
import postcss, { Root } from 'postcss'
-import selectorParser from 'postcss-selector-parser'
+import selectorParser, { Node, Selector } from 'postcss-selector-parser'
-export default postcss.plugin('add-id', (options: any) => (root: Root) => {
+export default postcss.plugin('vue-scoped', (options: any) => (root: Root) => {
const id: string = options
const keyframes = Object.create(null)
- root.each(function rewriteSelectors(node: any) {
- if (!node.selector) {
+ root.each(function rewriteSelectors(node) {
+ if (node.type !== 'rule') {
// handle media queries
if (node.type === 'atrule') {
if (node.name === 'media' || node.name === 'supports') {
return
}
- node.selector = selectorParser((selectors: any) => {
- selectors.each(function rewriteSelector(
- selector: any,
- _i: number,
- slotted?: boolean
- ) {
- let node: any = null
+ node.selector = selectorParser(selectors => {
+ function rewriteSelector(selector: Selector, slotted?: boolean) {
+ let node: Node | null = null
// find the last child node to insert attribute selector
- selector.each((n: any) => {
+ selector.each(n => {
+ // DEPRECATED ">>>" and "/deep/" combinator
+ if (
+ n.type === 'combinator' &&
+ (n.value === '>>>' || n.value === '/deep/')
+ ) {
+ n.value = ' '
+ n.spaces.before = n.spaces.after = ''
+ console.warn(
+ `[@vue/compiler-sfc] the >>> and /deep/ combinators have ` +
+ `been deprecated. Use ::v-deep instead.`
+ )
+ return false
+ }
+
if (n.type === 'pseudo') {
// deep: inject [id] attribute at the node before the ::v-deep
// combinator.
- // .foo ::v-deep .bar -> .foo[xxxxxxx] .bar
if (n.value === '::v-deep') {
- n.value = n.spaces.before = n.spaces.after = ''
+ if (n.nodes.length) {
+ // .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar
+ // replace the current node with ::v-deep's inner selector
+ selector.insertAfter(n, n.nodes[0])
+ // insert a space combinator before if it doesn't already have one
+ const prev = selector.at(selector.index(n) - 1)
+ if (!prev || !isSpaceCombinator(prev)) {
+ selector.insertAfter(
+ n,
+ selectorParser.combinator({
+ value: ' '
+ })
+ )
+ }
+ selector.removeChild(n)
+ } else {
+ // DEPRECATED usage
+ // .foo ::v-deep .bar -> .foo[xxxxxxx] .bar
+ console.warn(
+ `[@vue/compiler-sfc] ::v-deep usage as a combinator has ` +
+ `been deprecated. Use ::v-deep(<inner-selector>) instead.`
+ )
+ const prev = selector.at(selector.index(n) - 1)
+ if (prev && isSpaceCombinator(prev)) {
+ selector.removeChild(prev)
+ }
+ selector.removeChild(n)
+ }
return false
}
// instead.
// ::v-slotted(.foo) -> .foo[xxxxxxx-s]
if (n.value === '::v-slotted') {
- rewriteSelector(n.nodes[0], 0, true /* slotted */)
- selectors.insertAfter(selector, n.nodes[0])
- selectors.removeChild(selector)
+ rewriteSelector(n.nodes[0] as Selector, true /* slotted */)
+ selector.insertAfter(n, n.nodes[0])
+ selector.removeChild(n)
return false
}
})
if (node) {
- node.spaces.after = ''
+ ;(node as Node).spaces.after = ''
} else {
// For deep selectors & standalone pseudo selectors,
// the attribute selectors are prepended rather than appended.
const idToAdd = slotted ? id + '-s' : id
selector.insertAfter(
- node,
+ // If node is null it means we need to inject [id] at the start
+ // insertAfter can handle `null` here
+ node as any,
selectorParser.attribute({
attribute: idToAdd,
value: idToAdd,
quoteMark: `"`
})
)
- })
+ }
+ selectors.each(selector => rewriteSelector(selector as Selector))
}).processSync(node.selector)
})
})
}
})
+
+function isSpaceCombinator(node: Node) {
+ return node.type === 'combinator' && /^\s+$/.test(node.value)
+}