feat(compiler-sfc): upgrade to postcss 8 (#2710)

This commit is contained in:
Evan You
2021-02-04 08:37:55 -05:00
committed by GitHub
parent a89d985375
commit 49bc2e4db5
6 changed files with 294 additions and 258 deletions

View File

@@ -1,9 +1,9 @@
import postcss, {
ProcessOptions,
LazyResult,
Result,
ResultMap,
ResultMessage
SourceMap,
Message,
LazyResult
} from 'postcss'
import trimPlugin from './stylePluginTrim'
import scopedPlugin from './stylePluginScoped'
@@ -35,28 +35,33 @@ export interface SFCStyleCompileOptions {
map?: RawSourceMap
}
/**
* Aligns with postcss-modules
* https://github.com/css-modules/postcss-modules
*/
export interface CSSModulesOptions {
scopeBehaviour?: 'global' | 'local'
generateScopedName?:
| string
| ((name: string, filename: string, css: string) => string)
hashPrefix?: string
localsConvention?: 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly'
exportGlobals?: boolean
globalModulePaths?: string[]
}
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'
}
modulesOptions?: CSSModulesOptions
}
export interface SFCStyleCompileResults {
code: string
map: RawSourceMap | undefined
rawResult: LazyResult | Result | undefined
rawResult: Result | LazyResult | undefined
errors: Error[]
modules?: Record<string, string>
dependencies: Set<string>
@@ -149,7 +154,7 @@ export function doCompileStyle(
let result: LazyResult | undefined
let code: string | undefined
let outMap: ResultMap | undefined
let outMap: SourceMap | undefined
// stylus output include plain css. so need remove the repeat item
const dependencies = new Set(
preProcessedSource ? preProcessedSource.dependencies : []
@@ -162,7 +167,7 @@ export function doCompileStyle(
errors.push(...preProcessedSource.errors)
}
const recordPlainCssDependencies = (messages: ResultMessage[]) => {
const recordPlainCssDependencies = (messages: Message[]) => {
messages.forEach(msg => {
if (msg.type === 'dependency') {
// postcss output path is absolute position path
@@ -226,7 +231,7 @@ function preprocess(
return preprocessor(
options.source,
options.map,
options.inMap || options.map,
{
filename: options.filename,
...options.preprocessOptions

View File

@@ -8,7 +8,7 @@ import {
BindingMetadata
} from '@vue/compiler-dom'
import { SFCDescriptor } from './parse'
import postcss, { Root } from 'postcss'
import { PluginCreator } from 'postcss'
import hash from 'hash-sum'
export const CSS_VARS_HELPER = `useCssVars`
@@ -49,20 +49,21 @@ export interface CssVarsPluginOptions {
isProd: boolean
}
export const cssVarsPlugin = postcss.plugin<CssVarsPluginOptions>(
'vue-scoped',
opts => (root: Root) => {
const { id, isProd } = opts!
root.walkDecls(decl => {
export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
const { id, isProd } = opts!
return {
postcssPlugin: 'vue-sfc-vars',
Declaration(decl) {
// rewrite CSS variables
if (cssVarRE.test(decl.value)) {
decl.value = decl.value.replace(cssVarRE, (_, $1, $2, $3) => {
return `var(--${genVarName(id, $1 || $2 || $3, isProd)})`
})
}
})
}
}
)
}
cssVarsPlugin.postcss = true
export function genCssVarsCode(
vars: string[],

View File

@@ -1,181 +1,202 @@
import postcss, { Root } from 'postcss'
import selectorParser, { Node, Selector } from 'postcss-selector-parser'
import { PluginCreator, Rule } from 'postcss'
import selectorParser from 'postcss-selector-parser'
import { warn } from './warn'
const animationNameRE = /^(-\w+-)?animation-name$/
const animationRE = /^(-\w+-)?animation$/
export default postcss.plugin('vue-scoped', (id: any) => (root: Root) => {
const scopedPlugin: PluginCreator<string> = (id = '') => {
const keyframes = Object.create(null)
const shortId = id.replace(/^data-v-/, '')
root.each(function rewriteSelectors(node) {
if (node.type !== 'rule') {
// handle media queries
if (node.type === 'atrule') {
if (node.name === 'media' || node.name === 'supports') {
node.each(rewriteSelectors)
} else if (/-?keyframes$/.test(node.name)) {
// register keyframes
keyframes[node.params] = node.params = node.params + '-' + shortId
}
return {
postcssPlugin: 'vue-sfc-scoped',
Rule(rule) {
processRule(id, rule)
},
AtRule(node) {
if (
/-?keyframes$/.test(node.name) &&
!node.params.endsWith(`-${shortId}`)
) {
// register keyframes
keyframes[node.params] = node.params = node.params + '-' + shortId
}
return
}
node.selector = selectorParser(selectors => {
function rewriteSelector(selector: Selector, slotted?: boolean) {
let node: Node | null = null
let shouldInject = true
// find the last child node to insert attribute selector
selector.each(n => {
// DEPRECATED ">>>" and "/deep/" combinator
if (
n.type === 'combinator' &&
(n.value === '>>>' || n.value === '/deep/')
) {
n.value = ' '
n.spaces.before = n.spaces.after = ''
warn(
`the >>> and /deep/ combinators have been deprecated. ` +
`Use :deep() instead.`
)
return false
},
OnceExit(root) {
if (Object.keys(keyframes).length) {
// If keyframes are found in this <style>, find and rewrite animation names
// in declarations.
// Caveat: this only works for keyframes and animation rules in the same
// <style> element.
// individual animation-name declaration
root.walkDecls(decl => {
if (animationNameRE.test(decl.prop)) {
decl.value = decl.value
.split(',')
.map(v => keyframes[v.trim()] || v.trim())
.join(',')
}
if (n.type === 'pseudo') {
const { value } = n
// deep: inject [id] attribute at the node before the ::v-deep
// combinator.
if (value === ':deep' || value === '::v-deep') {
if (n.nodes.length) {
// .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar
// replace the current node with ::v-deep's inner selector
let last: Selector['nodes'][0] = n
n.nodes[0].each(ss => {
selector.insertAfter(last, ss)
last = ss
})
// 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: ' '
})
)
// shorthand
if (animationRE.test(decl.prop)) {
decl.value = decl.value
.split(',')
.map(v => {
const vals = v.trim().split(/\s+/)
const i = vals.findIndex(val => keyframes[val])
if (i !== -1) {
vals.splice(i, 1, keyframes[vals[i]])
return vals.join(' ')
} else {
return v
}
selector.removeChild(n)
} else {
// DEPRECATED usage
// .foo ::v-deep .bar -> .foo[xxxxxxx] .bar
warn(
`::v-deep usage as a combinator has ` +
`been deprecated. Use :deep(<inner-selector>) instead.`
)
const prev = selector.at(selector.index(n) - 1)
if (prev && isSpaceCombinator(prev)) {
selector.removeChild(prev)
}
selector.removeChild(n)
}
return false
}
// slot: use selector inside `::v-slotted` and inject [id + '-s']
// instead.
// ::v-slotted(.foo) -> .foo[xxxxxxx-s]
if (value === ':slotted' || value === '::v-slotted') {
rewriteSelector(n.nodes[0], true /* slotted */)
let last: Selector['nodes'][0] = n
n.nodes[0].each(ss => {
selector.insertAfter(last, ss)
last = ss
})
// selector.insertAfter(n, n.nodes[0])
selector.removeChild(n)
// since slotted attribute already scopes the selector there's no
// need for the non-slot attribute.
shouldInject = false
return false
}
// global: replace with inner selector and do not inject [id].
// ::v-global(.foo) -> .foo
if (value === ':global' || value === '::v-global') {
selectors.insertAfter(selector, n.nodes[0])
selectors.removeChild(selector)
return false
}
}
if (n.type !== 'pseudo' && n.type !== 'combinator') {
node = n
.join(',')
}
})
if (node) {
;(node as Node).spaces.after = ''
} else {
// For deep selectors & standalone pseudo selectors,
// the attribute selectors are prepended rather than appended.
// So all leading spaces must be eliminated to avoid problems.
selector.first.spaces.before = ''
}
if (shouldInject) {
const idToAdd = slotted ? id + '-s' : id
selector.insertAfter(
// 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,
raws: {},
quoteMark: `"`
})
)
}
}
selectors.each(selector => rewriteSelector(selector))
}).processSync(node.selector)
}
}
}
const processedRules = new WeakSet<Rule>()
function processRule(id: string, rule: Rule) {
if (processedRules.has(rule)) {
return
}
processedRules.add(rule)
rule.selector = selectorParser(selectorRoot => {
selectorRoot.each(selector => {
rewriteSelector(id, selector, selectorRoot)
})
}).processSync(rule.selector)
}
function rewriteSelector(
id: string,
selector: selectorParser.Selector,
selectorRoot: selectorParser.Root,
slotted = false
) {
let node: selectorParser.Node | null = null
let shouldInject = true
// find the last child node to insert attribute selector
selector.each(n => {
// DEPRECATED ">>>" and "/deep/" combinator
if (
n.type === 'combinator' &&
(n.value === '>>>' || n.value === '/deep/')
) {
n.value = ' '
n.spaces.before = n.spaces.after = ''
warn(
`the >>> and /deep/ combinators have been deprecated. ` +
`Use :deep() instead.`
)
return false
}
if (n.type === 'pseudo') {
const { value } = n
// deep: inject [id] attribute at the node before the ::v-deep
// combinator.
if (value === ':deep' || value === '::v-deep') {
if (n.nodes.length) {
// .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar
// replace the current node with ::v-deep's inner selector
let last: selectorParser.Selector['nodes'][0] = n
n.nodes[0].each(ss => {
selector.insertAfter(last, ss)
last = ss
})
// 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
warn(
`::v-deep usage as a combinator has ` +
`been deprecated. Use :deep(<inner-selector>) instead.`
)
const prev = selector.at(selector.index(n) - 1)
if (prev && isSpaceCombinator(prev)) {
selector.removeChild(prev)
}
selector.removeChild(n)
}
return false
}
// slot: use selector inside `::v-slotted` and inject [id + '-s']
// instead.
// ::v-slotted(.foo) -> .foo[xxxxxxx-s]
if (value === ':slotted' || value === '::v-slotted') {
rewriteSelector(id, n.nodes[0], selectorRoot, true /* slotted */)
let last: selectorParser.Selector['nodes'][0] = n
n.nodes[0].each(ss => {
selector.insertAfter(last, ss)
last = ss
})
// selector.insertAfter(n, n.nodes[0])
selector.removeChild(n)
// since slotted attribute already scopes the selector there's no
// need for the non-slot attribute.
shouldInject = false
return false
}
// global: replace with inner selector and do not inject [id].
// ::v-global(.foo) -> .foo
if (value === ':global' || value === '::v-global') {
selectorRoot.insertAfter(selector, n.nodes[0])
selectorRoot.removeChild(selector)
return false
}
}
if (n.type !== 'pseudo' && n.type !== 'combinator') {
node = n
}
})
if (Object.keys(keyframes).length) {
// If keyframes are found in this <style>, find and rewrite animation names
// in declarations.
// Caveat: this only works for keyframes and animation rules in the same
// <style> element.
// individual animation-name declaration
root.walkDecls(decl => {
if (animationNameRE.test(decl.prop)) {
decl.value = decl.value
.split(',')
.map(v => keyframes[v.trim()] || v.trim())
.join(',')
}
// shorthand
if (animationRE.test(decl.prop)) {
decl.value = decl.value
.split(',')
.map(v => {
const vals = v.trim().split(/\s+/)
const i = vals.findIndex(val => keyframes[val])
if (i !== -1) {
vals.splice(i, 1, keyframes[vals[i]])
return vals.join(' ')
} else {
return v
}
})
.join(',')
}
})
if (node) {
;(node as selectorParser.Node).spaces.after = ''
} else {
// For deep selectors & standalone pseudo selectors,
// the attribute selectors are prepended rather than appended.
// So all leading spaces must be eliminated to avoid problems.
selector.first.spaces.before = ''
}
})
function isSpaceCombinator(node: Node) {
if (shouldInject) {
const idToAdd = slotted ? id + '-s' : id
selector.insertAfter(
// 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,
raws: {},
quoteMark: `"`
})
)
}
}
function isSpaceCombinator(node: selectorParser.Node) {
return node.type === 'combinator' && /^\s+$/.test(node.value)
}
scopedPlugin.postcss = true
export default scopedPlugin

View File

@@ -1,10 +1,18 @@
import postcss, { Root } from 'postcss'
import { PluginCreator } from 'postcss'
export default postcss.plugin('trim', () => (css: Root) => {
css.walk(({ type, raws }) => {
if (type === 'rule' || type === 'atrule') {
if (raws.before) raws.before = '\n'
if (raws.after) raws.after = '\n'
const trimPlugin: PluginCreator<{}> = () => {
return {
postcssPlugin: 'vue-sfc-trim',
Once(root) {
root.walk(({ type, raws }) => {
if (type === 'rule' || type === 'atrule') {
if (raws.before) raws.before = '\n'
if ('after' in raws && raws.after) raws.after = '\n'
}
})
}
})
})
}
}
trimPlugin.postcss = true
export default trimPlugin