})"
`;
+exports[`SFC compile <script setup> CSS vars injection <script> w/ default export 1`] = `
+"const __default__ = { setup() {} }
+import { useCSSVars as __useCSSVars__ } from 'vue'
+const __injectCSSVars__ = () => {
+__useCSSVars__(_ctx => ({ color: _ctx.color }))
+}
+const __setup__ = __default__.setup
+__default__.setup = __setup__
+ ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
+ : __injectCSSVars__
+export default __default__"
+`;
+
+exports[`SFC compile <script setup> CSS vars injection <script> w/ default export in strings/comments 1`] = `
+"
+ // export default {}
+ const __default__ = {}
+
+import { useCSSVars as __useCSSVars__ } from 'vue'
+const __injectCSSVars__ = () => {
+__useCSSVars__(_ctx => ({ color: _ctx.color }))
+}
+const __setup__ = __default__.setup
+__default__.setup = __setup__
+ ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
+ : __injectCSSVars__
+export default __default__"
+`;
+
+exports[`SFC compile <script setup> CSS vars injection <script> w/ no default export 1`] = `
+"const a = 1
+const __default__ = {}
+import { useCSSVars as __useCSSVars__ } from 'vue'
+const __injectCSSVars__ = () => {
+__useCSSVars__(_ctx => ({ color: _ctx.color }))
+}
+const __setup__ = __default__.setup
+__default__.setup = __setup__
+ ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
+ : __injectCSSVars__
+export default __default__"
+`;
+
+exports[`SFC compile <script setup> CSS vars injection w/ <script setup> 1`] = `
+"import { useCSSVars as __useCSSVars__ } from 'vue'
+
+export function setup() {
+const color = 'red'
+__useCSSVars__(_ctx => ({ color }))
+return { color }
+}
+
+export default { setup }"
+`;
+
exports[`SFC compile <script setup> errors should allow export default referencing imported binding 1`] = `
"import { bar } from './bar'
)
})
+ test('async/await detection', () => {
+ // TODO
+ })
+
describe('exports', () => {
test('export const x = ...', () => {
const { content, bindings } = compile(
})
})
+ describe('CSS vars injection', () => {
+ test('<script> w/ no default export', () => {
+ assertCode(
+ compile(
+ `<script>const a = 1</script>\n` +
+ `<style vars="{ color }">div{ color: var(--color); }</style>`
+ ).content
+ )
+ })
+
+ test('<script> w/ default export', () => {
+ assertCode(
+ compile(
+ `<script>export default { setup() {} }</script>\n` +
+ `<style vars="{ color }">div{ color: var(--color); }</style>`
+ ).content
+ )
+ })
+
+ test('<script> w/ default export in strings/comments', () => {
+ assertCode(
+ compile(
+ `<script>
+ // export default {}
+ export default {}
+ </script>\n` +
+ `<style vars="{ color }">div{ color: var(--color); }</style>`
+ ).content
+ )
+ })
+
+ test('w/ <script setup>', () => {
+ assertCode(
+ compile(
+ `<script setup>export const color = 'red'</script>\n` +
+ `<style vars="{ color }">div{ color: var(--color); }</style>`
+ ).content
+ )
+ })
+ })
+
describe('errors', () => {
test('<script> and <script setup> must have same lang', () => {
expect(
).toHaveBeenWarned()
})
})
+
+ describe('<style vars>', () => {
+ test('should rewrite CSS vars in scoped mode', () => {
+ const code = compileScoped(`.foo {
+ color: var(--color);
+ font-size: var(--global:font);
+ }`)
+ expect(code).toMatchInlineSnapshot(`
+ ".foo[test] {
+ color: var(--test-color);
+ font-size: var(--font);
+ }"
+ `)
+ })
+ })
})
describe('SFC CSS modules', () => {
} from '@babel/types'
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
+import { genCssVarsCode, injectCssVarsCalls } from './genCssVars'
export interface SFCScriptCompileOptions {
/**
)
}
- const { script, scriptSetup, source, filename } = sfc
+ const { script, scriptSetup, styles, source, filename } = sfc
+ const hasCssVars = styles.some(s => typeof s.attrs.vars === 'string')
+
+ const isTS =
+ (script && script.lang === 'ts') ||
+ (scriptSetup && scriptSetup.lang === 'ts')
+
+ const plugins: ParserPlugin[] = [
+ ...(options.babelParserPlugins || []),
+ ...babelParserDefautPlugins,
+ ...(isTS ? (['typescript'] as const) : [])
+ ]
+
if (!scriptSetup) {
if (!script) {
throw new Error(`SFC contains no <script> tags.`)
}
return {
...script,
+ content: hasCssVars ? injectCssVarsCalls(sfc, plugins) : script.content,
bindings: analyzeScriptBindings(script)
}
}
const scriptStartOffset = script && script.loc.start.offset
const scriptEndOffset = script && script.loc.end.offset
- const isTS = scriptSetup.lang === 'ts'
- const plugins: ParserPlugin[] = [
- ...(options.babelParserPlugins || []),
- ...babelParserDefautPlugins,
- ...(isTS ? (['typescript'] as const) : [])
- ]
-
// 1. process normal <script> first if it exists
if (script) {
// import dedupe between <script> and <script setup>
// 6. wrap setup code with function.
// export the content of <script setup> as a named export, `setup`.
// this allows `import { setup } from '*.vue'` for testing purposes.
- s.appendLeft(startOffset, `\nexport function setup(${args}) {\n`)
+ s.prependLeft(startOffset, `\nexport function setup(${args}) {\n`)
// generate return statement
let returned = `{ ${Object.keys(setupExports).join(', ')} }`
returned = `Object.assign(\n ${returned}\n)`
}
+ // inject `useCSSVars` calls
+ if (hasCssVars) {
+ s.prepend(`import { useCSSVars as __useCSSVars__ } from 'vue'\n`)
+ for (const style of styles) {
+ const vars = style.attrs.vars
+ if (typeof vars === 'string') {
+ s.prependRight(
+ endOffset,
+ `\n${genCssVarsCode(vars, !!style.scoped, setupExports)}`
+ )
+ }
+ }
+ }
+
s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
// 7. finalize default export
--- /dev/null
+import {
+ processExpression,
+ createTransformContext,
+ createSimpleExpression,
+ createRoot,
+ NodeTypes,
+ SimpleExpressionNode
+} from '@vue/compiler-dom'
+import { SFCDescriptor } from './parse'
+import { rewriteDefault } from './rewriteDefault'
+import { ParserPlugin } from '@babel/parser'
+
+export function genCssVarsCode(
+ varsExp: string,
+ scoped: boolean,
+ knownBindings?: Record<string, boolean>
+) {
+ const exp = createSimpleExpression(varsExp, false)
+ const context = createTransformContext(createRoot([]), {
+ prefixIdentifiers: true
+ })
+ 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
+ ? transformed.content
+ : transformed.children
+ .map(c => {
+ return typeof c === 'string'
+ ? c
+ : (c as SimpleExpressionNode).content
+ })
+ .join('')
+
+ return `__useCSSVars__(_ctx => (${transformedString})${
+ scoped ? `, true` : ``
+ })`
+}
+
+// <script setup> already gets the calls injected as part of the transform
+// this is only for single normal <script>
+export function injectCssVarsCalls(
+ sfc: SFCDescriptor,
+ parserPlugins: ParserPlugin[]
+): string {
+ const script = rewriteDefault(
+ sfc.script!.content,
+ `__default__`,
+ 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 { useCSSVars as __useCSSVars__ } from 'vue'\n` +
+ `const __injectCSSVars__ = () => {\n${calls}}\n` +
+ `const __setup__ = __default__.setup\n` +
+ `__default__.setup = __setup__\n` +
+ ` ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }\n` +
+ ` : __injectCSSVars__\n` +
+ `export default __default__`
+ )
+}
export { compileTemplate } from './compileTemplate'
export { compileStyle, compileStyleAsync } from './compileStyle'
export { compileScript, analyzeScriptBindings } from './compileScript'
+export { rewriteDefault } from './rewriteDefault'
// Types
export {
--- /dev/null
+import { parse, ParserPlugin } from '@babel/parser'
+import MagicString from 'magic-string'
+
+const defaultExportRE = /((?:^|\n|;)\s*)export default/
+
+/**
+ * Utility for rewriting `export default` in a script block into a varaible
+ * declaration so that we can inject things into it
+ */
+export function rewriteDefault(
+ input: string,
+ as: string,
+ parserPlugins?: ParserPlugin[]
+): string {
+ if (!defaultExportRE.test(input)) {
+ return input + `\nconst ${as} = {}`
+ }
+
+ const replaced = input.replace(defaultExportRE, `$1const ${as} =`)
+ if (!defaultExportRE.test(replaced)) {
+ return replaced
+ }
+
+ // if the script somehow still contains `default export`, it probably has
+ // multi-line comments or template strings. fallback to a full parse.
+ const s = new MagicString(input)
+ const ast = parse(input, {
+ plugins: parserPlugins
+ }).program.body
+ ast.forEach(node => {
+ if (node.type === 'ExportDefaultDeclaration') {
+ s.overwrite(node.start!, node.declaration.start!, `const ${as} = `)
+ }
+ })
+ return s.toString()
+}
import postcss, { Root } from 'postcss'
import selectorParser, { Node, Selector } from 'postcss-selector-parser'
+const animationNameRE = /^(-\w+-)?animation-name$/
+const animationRE = /^(-\w+-)?animation$/
+const cssVarRE = /\bvar\(--(global:)?([^)]+)\)/g
+
export default postcss.plugin('vue-scoped', (options: any) => (root: Root) => {
const id: string = options
const keyframes = Object.create(null)
}).processSync(node.selector)
})
- // 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.
- if (Object.keys(keyframes).length) {
- root.walkDecls(decl => {
+ const hasKeyframes = Object.keys(keyframes).length
+ root.walkDecls(decl => {
+ // 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.
+ if (hasKeyframes) {
// individual animation-name declaration
- if (/^(-\w+-)?animation-name$/.test(decl.prop)) {
+ if (animationNameRE.test(decl.prop)) {
decl.value = decl.value
.split(',')
.map(v => keyframes[v.trim()] || v.trim())
.join(',')
}
// shorthand
- if (/^(-\w+-)?animation$/.test(decl.prop)) {
+ if (animationRE.test(decl.prop)) {
decl.value = decl.value
.split(',')
.map(v => {
})
.join(',')
}
- })
- }
+ }
+
+ // rewrite CSS variables
+ if (cssVarRE.test(decl.value)) {
+ decl.value = decl.value.replace(cssVarRE, (_, $1, $2) => {
+ return $1 ? `var(--${$2})` : `var(--${id}-${$2})`
+ })
+ }
+ })
})
function isSpaceCombinator(node: Node) {