feat(compiler-sfc): new SFC css varaible injection implementation
ref: https://github.com/vuejs/rfcs/pull/231
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import MagicString from 'magic-string'
|
||||
import { BindingMetadata, BindingTypes } from '@vue/compiler-core'
|
||||
import { BindingMetadata, BindingTypes, UNREF } from '@vue/compiler-core'
|
||||
import { SFCDescriptor, SFCScriptBlock } from './parse'
|
||||
import { parse as _parse, ParserOptions, ParserPlugin } from '@babel/parser'
|
||||
import { babelParserDefaultPlugins, generateCodeFrame } from '@vue/shared'
|
||||
@@ -26,14 +26,20 @@ import { walk } from 'estree-walker'
|
||||
import { RawSourceMap } from 'source-map'
|
||||
import {
|
||||
CSS_VARS_HELPER,
|
||||
parseCssVars,
|
||||
genCssVarsCode,
|
||||
injectCssVarsCalls
|
||||
} from './genCssVars'
|
||||
} from './cssVars'
|
||||
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
|
||||
|
||||
const DEFINE_OPTIONS = 'defineOptions'
|
||||
|
||||
export interface SFCScriptCompileOptions {
|
||||
/**
|
||||
* Scope ID for prefixing injected CSS varialbes.
|
||||
* This must be consistent with the `id` passed to `compileStyle`.
|
||||
*/
|
||||
id: string
|
||||
/**
|
||||
* https://babeljs.io/docs/en/babel-parser#plugins
|
||||
*/
|
||||
@@ -52,7 +58,7 @@ export interface SFCScriptCompileOptions {
|
||||
* from being hot-reloaded separately from component state.
|
||||
*/
|
||||
inlineTemplate?: boolean
|
||||
templateOptions?: SFCTemplateCompileOptions
|
||||
templateOptions?: Partial<SFCTemplateCompileOptions>
|
||||
}
|
||||
|
||||
const hasWarned: Record<string, boolean> = {}
|
||||
@@ -71,19 +77,33 @@ function warnOnce(msg: string) {
|
||||
*/
|
||||
export function compileScript(
|
||||
sfc: SFCDescriptor,
|
||||
options: SFCScriptCompileOptions = {}
|
||||
options: SFCScriptCompileOptions
|
||||
): SFCScriptBlock {
|
||||
const { script, scriptSetup, styles, source, filename } = sfc
|
||||
const { script, scriptSetup, source, filename } = sfc
|
||||
|
||||
if (__DEV__ && !__TEST__ && scriptSetup) {
|
||||
warnOnce(
|
||||
`<script setup> is still an experimental proposal.\n` +
|
||||
`Follow its status at https://github.com/vuejs/rfcs/pull/227.`
|
||||
`Follow its status at https://github.com/vuejs/rfcs/pull/227.\n` +
|
||||
`It's also recommended to pin your vue dependencies to exact versions ` +
|
||||
`to avoid breakage.`
|
||||
)
|
||||
}
|
||||
|
||||
const hasCssVars = styles.some(s => typeof s.attrs.vars === 'string')
|
||||
// for backwards compat
|
||||
if (!options) {
|
||||
options = { id: '' }
|
||||
}
|
||||
if (!options.id) {
|
||||
warnOnce(
|
||||
`compileScript now requires passing the \`id\` option.\n` +
|
||||
`Upgrade your vite or vue-loader version for compatibility with ` +
|
||||
`the latest experimental proposals.`
|
||||
)
|
||||
}
|
||||
|
||||
const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
|
||||
const cssVars = parseCssVars(sfc)
|
||||
const scriptLang = script && script.lang
|
||||
const scriptSetupLang = scriptSetup && scriptSetup.lang
|
||||
const isTS = scriptLang === 'ts' || scriptSetupLang === 'ts'
|
||||
@@ -104,10 +124,13 @@ export function compileScript(
|
||||
plugins,
|
||||
sourceType: 'module'
|
||||
}).program.body
|
||||
const bindings = analyzeScriptBindings(scriptAst)
|
||||
return {
|
||||
...script,
|
||||
content: hasCssVars ? injectCssVarsCalls(sfc, plugins) : script.content,
|
||||
bindings: analyzeScriptBindings(scriptAst),
|
||||
content: cssVars.length
|
||||
? injectCssVarsCalls(sfc, cssVars, bindings, scopeId, plugins)
|
||||
: script.content,
|
||||
bindings,
|
||||
scriptAst
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -491,7 +514,9 @@ export function compileScript(
|
||||
warnOnce(
|
||||
`ref: sugar is still an experimental proposal and is not ` +
|
||||
`guaranteed to be a part of <script setup>.\n` +
|
||||
`Follow its status at https://github.com/vuejs/rfcs/pull/228.`
|
||||
`Follow its status at https://github.com/vuejs/rfcs/pull/228.\n` +
|
||||
`It's also recommended to pin your vue dependencies to exact versions ` +
|
||||
`to avoid breakage.`
|
||||
)
|
||||
s.overwrite(
|
||||
node.label.start! + startOffset,
|
||||
@@ -512,10 +537,22 @@ export function compileScript(
|
||||
if (node.type === 'ImportDeclaration') {
|
||||
// import declarations are moved to top
|
||||
s.move(start, end, 0)
|
||||
|
||||
// dedupe imports
|
||||
let prev
|
||||
let removed = 0
|
||||
for (const specifier of node.specifiers) {
|
||||
let prev: Node | undefined, next: Node | undefined
|
||||
const removeSpecifier = (node: Node) => {
|
||||
removed++
|
||||
s.remove(
|
||||
prev ? prev.end! + startOffset : node.start! + startOffset,
|
||||
next ? next.start! + startOffset : node.end! + startOffset
|
||||
)
|
||||
}
|
||||
|
||||
for (let i = 0; i < node.specifiers.length; i++) {
|
||||
const specifier = node.specifiers[i]
|
||||
prev = node.specifiers[i - 1]
|
||||
next = node.specifiers[i + 1]
|
||||
const local = specifier.local.name
|
||||
const imported =
|
||||
specifier.type === 'ImportSpecifier' &&
|
||||
@@ -524,19 +561,11 @@ export function compileScript(
|
||||
const source = node.source.value
|
||||
const existing = userImports[local]
|
||||
if (source === 'vue' && imported === DEFINE_OPTIONS) {
|
||||
removed++
|
||||
s.remove(
|
||||
prev ? prev.end! + startOffset : specifier.start! + startOffset,
|
||||
specifier.end! + startOffset
|
||||
)
|
||||
removeSpecifier(specifier)
|
||||
} else if (existing) {
|
||||
if (existing.source === source && existing.imported === imported) {
|
||||
// already imported in <script setup>, dedupe
|
||||
removed++
|
||||
s.remove(
|
||||
prev ? prev.end! + startOffset : specifier.start! + startOffset,
|
||||
specifier.end! + startOffset
|
||||
)
|
||||
removeSpecifier(specifier)
|
||||
} else {
|
||||
error(`different imports aliased to same local name.`, specifier)
|
||||
}
|
||||
@@ -546,7 +575,6 @@ export function compileScript(
|
||||
source: node.source.value
|
||||
}
|
||||
}
|
||||
prev = specifier
|
||||
}
|
||||
if (removed === node.specifiers.length) {
|
||||
s.remove(node.start! + startOffset, node.end! + startOffset)
|
||||
@@ -732,7 +760,7 @@ export function compileScript(
|
||||
}
|
||||
|
||||
// 7. finalize setup argument signature.
|
||||
let args = optionsExp ? `__props, ${optionsExp}` : ``
|
||||
let args = optionsExp ? `__props, ${optionsExp}` : `__props`
|
||||
if (optionsExp && optionsType) {
|
||||
if (slotsType === 'Slots') {
|
||||
helperImports.add('Slots')
|
||||
@@ -745,26 +773,7 @@ export function compileScript(
|
||||
}`
|
||||
}
|
||||
|
||||
const allBindings: Record<string, any> = { ...setupBindings }
|
||||
for (const key in userImports) {
|
||||
allBindings[key] = true
|
||||
}
|
||||
|
||||
// 8. inject `useCssVars` calls
|
||||
if (hasCssVars) {
|
||||
helperImports.add(CSS_VARS_HELPER)
|
||||
for (const style of styles) {
|
||||
const vars = style.attrs.vars
|
||||
if (typeof vars === 'string') {
|
||||
s.prependRight(
|
||||
endOffset,
|
||||
`\n${genCssVarsCode(vars, !!style.scoped, allBindings)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 9. analyze binding metadata
|
||||
// 8. analyze binding metadata
|
||||
if (scriptAst) {
|
||||
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
|
||||
}
|
||||
@@ -785,13 +794,23 @@ export function compileScript(
|
||||
bindingMetadata[key] = setupBindings[key]
|
||||
}
|
||||
|
||||
// 9. inject `useCssVars` calls
|
||||
if (cssVars.length) {
|
||||
helperImports.add(CSS_VARS_HELPER)
|
||||
helperImports.add('unref')
|
||||
s.prependRight(
|
||||
startOffset,
|
||||
`\n${genCssVarsCode(cssVars, bindingMetadata, scopeId)}\n`
|
||||
)
|
||||
}
|
||||
|
||||
// 10. generate return statement
|
||||
let returned
|
||||
if (options.inlineTemplate) {
|
||||
if (sfc.template) {
|
||||
// inline render function mode - we are going to compile the template and
|
||||
// inline it right here
|
||||
const { code, preamble, tips, errors } = compileTemplate({
|
||||
const { code, ast, preamble, tips, errors } = compileTemplate({
|
||||
...options.templateOptions,
|
||||
filename,
|
||||
source: sfc.template.content,
|
||||
@@ -813,12 +832,22 @@ export function compileScript(
|
||||
if (preamble) {
|
||||
s.prepend(preamble)
|
||||
}
|
||||
// avoid duplicated unref import
|
||||
// as this may get injected by the render function preamble OR the
|
||||
// css vars codegen
|
||||
if (ast && ast.helpers.includes(UNREF)) {
|
||||
helperImports.delete('unref')
|
||||
}
|
||||
returned = code
|
||||
} else {
|
||||
returned = `() => {}`
|
||||
}
|
||||
} else {
|
||||
// return bindings from setup
|
||||
const allBindings: Record<string, any> = { ...setupBindings }
|
||||
for (const key in userImports) {
|
||||
allBindings[key] = true
|
||||
}
|
||||
returned = `{ ${Object.keys(allBindings).join(', ')} }`
|
||||
}
|
||||
s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
|
||||
|
||||
@@ -7,7 +7,6 @@ import postcss, {
|
||||
} from 'postcss'
|
||||
import trimPlugin from './stylePluginTrim'
|
||||
import scopedPlugin from './stylePluginScoped'
|
||||
import scopedVarsPlugin from './stylePluginScopedVars'
|
||||
import {
|
||||
processors,
|
||||
StylePreprocessor,
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
PreprocessLang
|
||||
} from './stylePreprocessors'
|
||||
import { RawSourceMap } from 'source-map'
|
||||
import { cssVarsPlugin } from './cssVars'
|
||||
|
||||
export interface SFCStyleCompileOptions {
|
||||
source: string
|
||||
@@ -22,7 +22,6 @@ export interface SFCStyleCompileOptions {
|
||||
id: string
|
||||
map?: RawSourceMap
|
||||
scoped?: boolean
|
||||
vars?: boolean
|
||||
trim?: boolean
|
||||
preprocessLang?: PreprocessLang
|
||||
preprocessOptions?: any
|
||||
@@ -82,7 +81,6 @@ export function doCompileStyle(
|
||||
filename,
|
||||
id,
|
||||
scoped = false,
|
||||
vars = false,
|
||||
trim = true,
|
||||
modules = false,
|
||||
modulesOptions = {},
|
||||
@@ -96,11 +94,7 @@ export function doCompileStyle(
|
||||
const source = preProcessedSource ? preProcessedSource.code : options.source
|
||||
|
||||
const plugins = (postcssPlugins || []).slice()
|
||||
if (vars && scoped) {
|
||||
// vars + scoped, only applies to raw source before other transforms
|
||||
// #1623
|
||||
plugins.unshift(scopedVarsPlugin(id))
|
||||
}
|
||||
plugins.unshift(cssVarsPlugin(id))
|
||||
if (trim) {
|
||||
plugins.push(trimPlugin())
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface TemplateCompiler {
|
||||
|
||||
export interface SFCTemplateCompileResults {
|
||||
code: string
|
||||
ast?: RootNode
|
||||
preamble?: string
|
||||
source: string
|
||||
tips: string[]
|
||||
@@ -169,7 +170,7 @@ function doCompileTemplate({
|
||||
nodeTransforms = [transformAssetUrl, transformSrcset]
|
||||
}
|
||||
|
||||
let { code, preamble, map } = compiler.compile(source, {
|
||||
let { code, ast, preamble, map } = compiler.compile(source, {
|
||||
mode: 'module',
|
||||
prefixIdentifiers: true,
|
||||
hoistStatic: true,
|
||||
@@ -193,7 +194,7 @@ function doCompileTemplate({
|
||||
}
|
||||
}
|
||||
|
||||
return { code, preamble, source, errors, tips: [], map }
|
||||
return { code, ast, preamble, source, errors, tips: [], map }
|
||||
}
|
||||
|
||||
function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
|
||||
|
||||
@@ -4,30 +4,62 @@ import {
|
||||
createSimpleExpression,
|
||||
createRoot,
|
||||
NodeTypes,
|
||||
SimpleExpressionNode
|
||||
SimpleExpressionNode,
|
||||
BindingMetadata
|
||||
} from '@vue/compiler-dom'
|
||||
import { SFCDescriptor } from './parse'
|
||||
import { rewriteDefault } from './rewriteDefault'
|
||||
import { ParserPlugin } from '@babel/parser'
|
||||
import postcss, { Root } from 'postcss'
|
||||
|
||||
export const CSS_VARS_HELPER = `useCssVars`
|
||||
export const cssVarRE = /\bvar\(--(?:v-bind)?:([^)]+)\)/g
|
||||
|
||||
export function convertCssVarCasing(raw: string): string {
|
||||
return raw.replace(/([^\w-])/g, '_')
|
||||
}
|
||||
|
||||
export function parseCssVars(sfc: SFCDescriptor): string[] {
|
||||
const vars: string[] = []
|
||||
sfc.styles.forEach(style => {
|
||||
let match
|
||||
while ((match = cssVarRE.exec(style.content))) {
|
||||
vars.push(match[1])
|
||||
}
|
||||
})
|
||||
return vars
|
||||
}
|
||||
|
||||
// for compileStyle
|
||||
export const cssVarsPlugin = postcss.plugin(
|
||||
'vue-scoped',
|
||||
(id: any) => (root: Root) => {
|
||||
const shortId = id.replace(/^data-v-/, '')
|
||||
root.walkDecls(decl => {
|
||||
// rewrite CSS variables
|
||||
if (cssVarRE.test(decl.value)) {
|
||||
decl.value = decl.value.replace(cssVarRE, (_, $1) => {
|
||||
return `var(--${shortId}-${convertCssVarCasing($1)})`
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export function genCssVarsCode(
|
||||
varsExp: string,
|
||||
scoped: boolean,
|
||||
knownBindings?: Record<string, any>
|
||||
vars: string[],
|
||||
bindings: BindingMetadata,
|
||||
id: string
|
||||
) {
|
||||
const varsExp = `{\n ${vars
|
||||
.map(v => `${convertCssVarCasing(v)}: (${v})`)
|
||||
.join(',\n ')}\n}`
|
||||
const exp = createSimpleExpression(varsExp, false)
|
||||
const context = createTransformContext(createRoot([]), {
|
||||
prefixIdentifiers: true
|
||||
prefixIdentifiers: true,
|
||||
inline: true,
|
||||
bindingMetadata: bindings
|
||||
})
|
||||
if (knownBindings) {
|
||||
// when compiling <script setup> we already know what bindings are exposed
|
||||
// so we can avoid prefixing them from the ctx.
|
||||
for (const key in knownBindings) {
|
||||
context.identifiers[key] = 1
|
||||
}
|
||||
}
|
||||
const transformed = processExpression(exp, context)
|
||||
const transformedString =
|
||||
transformed.type === NodeTypes.SIMPLE_EXPRESSION
|
||||
@@ -40,15 +72,16 @@ export function genCssVarsCode(
|
||||
})
|
||||
.join('')
|
||||
|
||||
return `_${CSS_VARS_HELPER}(_ctx => (${transformedString})${
|
||||
scoped ? `, true` : ``
|
||||
})`
|
||||
return `_${CSS_VARS_HELPER}(_ctx => (${transformedString}), "${id}")`
|
||||
}
|
||||
|
||||
// <script setup> already gets the calls injected as part of the transform
|
||||
// this is only for single normal <script>
|
||||
export function injectCssVarsCalls(
|
||||
sfc: SFCDescriptor,
|
||||
cssVars: string[],
|
||||
bindings: BindingMetadata,
|
||||
id: string,
|
||||
parserPlugins: ParserPlugin[]
|
||||
): string {
|
||||
const script = rewriteDefault(
|
||||
@@ -57,18 +90,14 @@ export function injectCssVarsCalls(
|
||||
parserPlugins
|
||||
)
|
||||
|
||||
let calls = ``
|
||||
for (const style of sfc.styles) {
|
||||
const vars = style.attrs.vars
|
||||
if (typeof vars === 'string') {
|
||||
calls += genCssVarsCode(vars, !!style.scoped) + '\n'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
script +
|
||||
`\nimport { ${CSS_VARS_HELPER} as _${CSS_VARS_HELPER} } from 'vue'\n` +
|
||||
`const __injectCSSVars__ = () => {\n${calls}}\n` +
|
||||
`const __injectCSSVars__ = () => {\n${genCssVarsCode(
|
||||
cssVars,
|
||||
bindings,
|
||||
id
|
||||
)}}\n` +
|
||||
`const __setup__ = __default__.setup\n` +
|
||||
`__default__.setup = __setup__\n` +
|
||||
` ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }\n` +
|
||||
@@ -45,7 +45,6 @@ export interface SFCScriptBlock extends SFCBlock {
|
||||
export interface SFCStyleBlock extends SFCBlock {
|
||||
type: 'style'
|
||||
scoped?: boolean
|
||||
vars?: string
|
||||
module?: string | boolean
|
||||
}
|
||||
|
||||
@@ -269,8 +268,6 @@ function createBlock(
|
||||
} else if (type === 'style') {
|
||||
if (p.name === 'scoped') {
|
||||
;(block as SFCStyleBlock).scoped = true
|
||||
} else if (p.name === 'vars' && typeof attrs.vars === 'string') {
|
||||
;(block as SFCStyleBlock).vars = attrs.vars
|
||||
} else if (p.name === 'module') {
|
||||
;(block as SFCStyleBlock).module = attrs[p.name]
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import postcss, { Root } from 'postcss'
|
||||
|
||||
const cssVarRE = /\bvar\(--(global:)?([^)]+)\)/g
|
||||
|
||||
export default postcss.plugin('vue-scoped', (id: any) => (root: Root) => {
|
||||
const shortId = id.replace(/^data-v-/, '')
|
||||
root.walkDecls(decl => {
|
||||
// rewrite CSS variables
|
||||
if (cssVarRE.test(decl.value)) {
|
||||
decl.value = decl.value.replace(cssVarRE, (_, $1, $2) => {
|
||||
return $1 ? `var(--${$2})` : `var(--${shortId}-${$2})`
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user