wip: more consistent compiler-sfc usage + inline mode for ssr

This commit is contained in:
Evan You
2020-11-20 14:22:51 -05:00
parent 58227e88e9
commit 8ac2241b22
18 changed files with 213 additions and 93 deletions

View File

@@ -201,6 +201,40 @@ return (_ctx, _cache) => {
}"
`;
exports[`SFC compile <script setup> inlineTemplate mode ssr codegen 1`] = `
"import { useCssVars as _useCssVars, unref as _unref } from 'vue'
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from \\"@vue/server-renderer\\"
import { ref } from 'vue'
export default {
expose: [],
__ssrInlineRender: true,
setup(__props) {
_useCssVars(_ctx => ({
\\"xxxxxxxx-count\\": (count.value)
}))
const count = ref(0)
return (_ctx, _push, _parent, _attrs) => {
const _cssVars = { style: {
\\"xxxxxxxx-count\\": (count.value)
}}
_push(\`<!--[--><div\${
_ssrRenderAttrs(_cssVars)
}>\${
_ssrInterpolate(count.value)
}</div><div\${
_ssrRenderAttrs(_cssVars)
}>static</div><!--]-->\`)
}
}
}"
`;
exports[`SFC compile <script setup> inlineTemplate mode template assignment expression codegen 1`] = `
"import { createVNode as _createVNode, isRef as _isRef, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"

View File

@@ -1,4 +1,4 @@
import { BindingTypes } from '@vue/compiler-dom/src'
import { BindingTypes } from '@vue/compiler-dom'
import { compileSFCScript as compile, assertCode } from './utils'
describe('SFC compile <script setup>', () => {
@@ -297,6 +297,34 @@ const bar = 1
expect(content).toMatch(`{ lett: lett } = val`)
assertCode(content)
})
test('ssr codegen', () => {
const { content } = compile(
`
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div>{{ count }}</div>
<div>static</div>
</template>
<style>
div { color: v-bind(count) }
</style>
`,
{
inlineTemplate: true,
templateOptions: {
ssr: true
}
}
)
expect(content).toMatch(`\n __ssrInlineRender: true,\n`)
expect(content).toMatch(`return (_ctx, _push`)
expect(content).toMatch(`ssrInterpolate`)
assertCode(content)
})
})
describe('with TypeScript', () => {

View File

@@ -16,7 +16,7 @@ export function compileScoped(
const res = compileStyle({
source,
filename: 'test.css',
id: 'test',
id: 'data-v-test',
scoped: true,
...options
})
@@ -32,61 +32,61 @@ export function compileScoped(
describe('SFC scoped CSS', () => {
test('simple selectors', () => {
expect(compileScoped(`h1 { color: red; }`)).toMatch(
`h1[test] { color: red;`
`h1[data-v-test] { color: red;`
)
expect(compileScoped(`.foo { color: red; }`)).toMatch(
`.foo[test] { color: red;`
`.foo[data-v-test] { color: red;`
)
})
test('descendent selector', () => {
expect(compileScoped(`h1 .foo { color: red; }`)).toMatch(
`h1 .foo[test] { color: red;`
`h1 .foo[data-v-test] { color: red;`
)
})
test('multiple selectors', () => {
expect(compileScoped(`h1 .foo, .bar, .baz { color: red; }`)).toMatch(
`h1 .foo[test], .bar[test], .baz[test] { color: red;`
`h1 .foo[data-v-test], .bar[data-v-test], .baz[data-v-test] { color: red;`
)
})
test('pseudo class', () => {
expect(compileScoped(`.foo:after { color: red; }`)).toMatch(
`.foo[test]:after { color: red;`
`.foo[data-v-test]:after { color: red;`
)
})
test('pseudo element', () => {
expect(compileScoped(`::selection { display: none; }`)).toMatch(
'[test]::selection {'
'[data-v-test]::selection {'
)
})
test('spaces before pseudo element', () => {
const code = compileScoped(`.abc, ::selection { color: red; }`)
expect(code).toMatch('.abc[test],')
expect(code).toMatch('[test]::selection {')
expect(code).toMatch('.abc[data-v-test],')
expect(code).toMatch('[data-v-test]::selection {')
})
test('::v-deep', () => {
expect(compileScoped(`:deep(.foo) { color: red; }`)).toMatchInlineSnapshot(`
"[test] .foo { color: red;
"[data-v-test] .foo { color: red;
}"
`)
expect(compileScoped(`::v-deep(.foo) { color: red; }`))
.toMatchInlineSnapshot(`
"[test] .foo { color: red;
"[data-v-test] .foo { color: red;
}"
`)
expect(compileScoped(`::v-deep(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
"[test] .foo .bar { color: red;
"[data-v-test] .foo .bar { color: red;
}"
`)
expect(compileScoped(`.baz .qux ::v-deep(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".baz .qux[test] .foo .bar { color: red;
".baz .qux[data-v-test] .foo .bar { color: red;
}"
`)
})
@@ -94,22 +94,22 @@ describe('SFC scoped CSS', () => {
test('::v-slotted', () => {
expect(compileScoped(`:slotted(.foo) { color: red; }`))
.toMatchInlineSnapshot(`
".foo[test-s] { color: red;
".foo[data-v-test-s] { color: red;
}"
`)
expect(compileScoped(`::v-slotted(.foo) { color: red; }`))
.toMatchInlineSnapshot(`
".foo[test-s] { color: red;
".foo[data-v-test-s] { color: red;
}"
`)
expect(compileScoped(`::v-slotted(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".foo .bar[test-s] { color: red;
".foo .bar[data-v-test-s] { color: red;
}"
`)
expect(compileScoped(`.baz .qux ::v-slotted(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".baz .qux .foo .bar[test-s] { color: red;
".baz .qux .foo .bar[data-v-test-s] { color: red;
}"
`)
})
@@ -142,7 +142,7 @@ describe('SFC scoped CSS', () => {
expect(compileScoped(`@media print { .foo { color: red }}`))
.toMatchInlineSnapshot(`
"@media print {
.foo[test] { color: red
.foo[data-v-test] { color: red
}}"
`)
})
@@ -151,7 +151,7 @@ describe('SFC scoped CSS', () => {
expect(compileScoped(`@supports(display: grid) { .foo { display: grid }}`))
.toMatchInlineSnapshot(`
"@supports(display: grid) {
.foo[test] { display: grid
.foo[data-v-test] { display: grid
}}"
`)
})
@@ -222,7 +222,7 @@ describe('SFC scoped CSS', () => {
// vue-loader/#1370
test('spaces after selector', () => {
expect(compileScoped(`.foo , .bar { color: red; }`)).toMatchInlineSnapshot(`
".foo[test], .bar[test] { color: red;
".foo[data-v-test], .bar[data-v-test] { color: red;
}"
`)
})
@@ -231,12 +231,12 @@ describe('SFC scoped CSS', () => {
test('::v-deep as combinator', () => {
expect(compileScoped(`::v-deep .foo { color: red; }`))
.toMatchInlineSnapshot(`
"[test] .foo { color: red;
"[data-v-test] .foo { color: red;
}"
`)
expect(compileScoped(`.bar ::v-deep .foo { color: red; }`))
.toMatchInlineSnapshot(`
".bar[test] .foo { color: red;
".bar[data-v-test] .foo { color: red;
}"
`)
expect(
@@ -247,7 +247,7 @@ describe('SFC scoped CSS', () => {
test('>>> (deprecated syntax)', () => {
const code = compileScoped(`>>> .foo { color: red; }`)
expect(code).toMatchInlineSnapshot(`
"[test] .foo { color: red;
"[data-v-test] .foo { color: red;
}"
`)
expect(
@@ -258,7 +258,7 @@ describe('SFC scoped CSS', () => {
test('/deep/ (deprecated syntax)', () => {
const code = compileScoped(`/deep/ .foo { color: red; }`)
expect(code).toMatchInlineSnapshot(`
"[test] .foo { color: red;
"[data-v-test] .foo { color: red;
}"
`)
expect(

View File

@@ -1,10 +1,20 @@
import { compileTemplate } from '../src/compileTemplate'
import {
compileTemplate,
SFCTemplateCompileOptions
} from '../src/compileTemplate'
import { parse, SFCTemplateBlock } from '../src/parse'
function compile(opts: Omit<SFCTemplateCompileOptions, 'id'>) {
return compileTemplate({
...opts,
id: ''
})
}
test('should work', () => {
const source = `<div><p>{{ render }}</p></div>`
const result = compileTemplate({ filename: 'example.vue', source })
const result = compile({ filename: 'example.vue', source })
expect(result.errors.length).toBe(0)
expect(result.source).toBe(source)
@@ -25,7 +35,7 @@ body
{ filename: 'example.vue', sourceMap: true }
).descriptor.template as SFCTemplateBlock
const result = compileTemplate({
const result = compile({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang
@@ -40,7 +50,7 @@ test('warn missing preprocessor', () => {
sourceMap: true
}).descriptor.template as SFCTemplateBlock
const result = compileTemplate({
const result = compile({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang
@@ -52,7 +62,7 @@ test('warn missing preprocessor', () => {
test('transform asset url options', () => {
const input = { source: `<foo bar="~baz"/>`, filename: 'example.vue' }
// Object option
const { code: code1 } = compileTemplate({
const { code: code1 } = compile({
...input,
transformAssetUrls: {
tags: { foo: ['bar'] }
@@ -61,7 +71,7 @@ test('transform asset url options', () => {
expect(code1).toMatch(`import _imports_0 from 'baz'\n`)
// legacy object option (direct tags config)
const { code: code2 } = compileTemplate({
const { code: code2 } = compile({
...input,
transformAssetUrls: {
foo: ['bar']
@@ -70,7 +80,7 @@ test('transform asset url options', () => {
expect(code2).toMatch(`import _imports_0 from 'baz'\n`)
// false option
const { code: code3 } = compileTemplate({
const { code: code3 } = compile({
...input,
transformAssetUrls: false
})
@@ -87,7 +97,7 @@ test('source map', () => {
{ filename: 'example.vue', sourceMap: true }
).descriptor.template as SFCTemplateBlock
const result = compileTemplate({
const result = compile({
filename: 'example.vue',
source: template.content
})
@@ -96,7 +106,7 @@ test('source map', () => {
})
test('template errors', () => {
const result = compileTemplate({
const result = compile({
filename: 'example.vue',
source: `<div :foo
:bar="a[" v-model="baz"/>`
@@ -114,7 +124,7 @@ test('preprocessor errors', () => {
{ filename: 'example.vue', sourceMap: true }
).descriptor.template as SFCTemplateBlock
const result = compileTemplate({
const result = compile({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang

View File

@@ -167,6 +167,7 @@ export function compileScript(
let optionsArg: ObjectExpression | undefined
let optionsType: TSTypeLiteral | undefined
let hasAwait = false
let hasInlinedSsrRenderFn = false
// context types to generate
let propsType = `{}`
let emitType = `(e: string, ...args: any[]) => void`
@@ -820,15 +821,24 @@ export function compileScript(
// 10. generate return statement
let returned
if (options.inlineTemplate) {
if (sfc.template) {
if (sfc.template && !sfc.template.src) {
if (options.templateOptions && options.templateOptions.ssr) {
hasInlinedSsrRenderFn = true
}
// inline render function mode - we are going to compile the template and
// inline it right here
const { code, ast, preamble, tips, errors } = compileTemplate({
...options.templateOptions,
filename,
source: sfc.template.content,
inMap: sfc.template.map,
...options.templateOptions,
id: scopeId,
scoped: sfc.styles.some(s => s.scoped),
isProd: options.isProd,
ssrCssVars: sfc.cssVars,
compilerOptions: {
...(options.templateOptions &&
options.templateOptions.compilerOptions),
inline: true,
isTS,
bindingMetadata
@@ -883,6 +893,9 @@ export function compileScript(
// 11. finalize default export
// expose: [] makes <script setup> components "closed" by default.
let runtimeOptions = `\n expose: [],`
if (hasInlinedSsrRenderFn) {
runtimeOptions += `\n __ssrInlineRender: true,`
}
if (optionsArg) {
runtimeOptions += `\n ${scriptSetup.content
.slice(optionsArg.start! + 1, optionsArg.end! - 1)

View File

@@ -20,15 +20,19 @@ export interface SFCStyleCompileOptions {
source: string
filename: string
id: string
map?: RawSourceMap
scoped?: boolean
trim?: boolean
isProd?: boolean
inMap?: RawSourceMap
preprocessLang?: PreprocessLang
preprocessOptions?: any
preprocessCustomRequire?: (id: string) => any
postcssOptions?: any
postcssPlugins?: any[]
/**
* @deprecated
*/
map?: RawSourceMap
}
export interface SFCAsyncStyleCompileOptions extends SFCStyleCompileOptions {
@@ -92,16 +96,21 @@ export function doCompileStyle(
} = options
const preprocessor = preprocessLang && processors[preprocessLang]
const preProcessedSource = preprocessor && preprocess(options, preprocessor)
const map = preProcessedSource ? preProcessedSource.map : options.map
const map = preProcessedSource
? preProcessedSource.map
: options.inMap || options.map
const source = preProcessedSource ? preProcessedSource.code : options.source
const shortId = id.replace(/^data-v-/, '')
const longId = `data-v-${shortId}`
const plugins = (postcssPlugins || []).slice()
plugins.unshift(cssVarsPlugin({ id, isProd }))
plugins.unshift(cssVarsPlugin({ id: shortId, isProd }))
if (trim) {
plugins.push(trimPlugin())
}
if (scoped) {
plugins.push(scopedPlugin(id))
plugins.push(scopedPlugin(longId))
}
let cssModules: Record<string, string> | undefined
if (modules) {

View File

@@ -23,6 +23,7 @@ import * as CompilerDOM from '@vue/compiler-dom'
import * as CompilerSSR from '@vue/compiler-ssr'
import consolidate from 'consolidate'
import { warnOnce } from './warn'
import { genCssVarsFromList } from './cssVars'
export interface TemplateCompiler {
compile(template: string, options: CompilerOptions): CodegenResult
@@ -42,7 +43,11 @@ export interface SFCTemplateCompileResults {
export interface SFCTemplateCompileOptions {
source: string
filename: string
id: string
scoped?: boolean
isProd?: boolean
ssr?: boolean
ssrCssVars?: string[]
inMap?: RawSourceMap
compiler?: TemplateCompiler
compilerOptions?: CompilerOptions
@@ -151,9 +156,13 @@ export function compileTemplate(
function doCompileTemplate({
filename,
id,
scoped,
inMap,
source,
ssr = false,
ssrCssVars,
isProd = false,
compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM,
compilerOptions = {},
transformAssetUrls
@@ -171,19 +180,30 @@ function doCompileTemplate({
nodeTransforms = [transformAssetUrl, transformSrcset]
}
if (ssr && compilerOptions.ssrCssVars == null) {
if (ssr && !ssrCssVars) {
warnOnce(
`compileTemplate is called with \`ssr: true\` but no ` +
`corresponding \`ssrCssVars\` option. The value can be generated by ` +
`calling \`generateCssVars(sfcDescriptor, scopeId, isProduction)\`.`
`corresponding \`cssVars\` option.\`.`
)
}
if (!id) {
warnOnce(`compileTemplate now requires the \`id\` option.\`.`)
id = ''
}
const shortId = id.replace(/^data-v-/, '')
const longId = `data-v-${shortId}`
let { code, ast, preamble, map } = compiler.compile(source, {
mode: 'module',
prefixIdentifiers: true,
hoistStatic: true,
cacheHandlers: true,
ssrCssVars:
ssr && ssrCssVars && ssrCssVars.length
? genCssVarsFromList(ssrCssVars, shortId, isProd)
: '',
scopeId: scoped ? longId : undefined,
...compilerOptions,
nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
filename,

View File

@@ -16,26 +16,13 @@ import hash from 'hash-sum'
export const CSS_VARS_HELPER = `useCssVars`
export const cssVarRE = /\bv-bind\(\s*(?:'([^']+)'|"([^"]+)"|([^'"][^)]*))\s*\)/g
/**
* Given an SFC descriptor, generate the CSS variables object string that can be
* passed to `compileTemplate` as `compilerOptions.ssrCssVars`.
* @public
*/
export function generateCssVars(
sfc: SFCDescriptor,
id: string,
isProd: boolean
): string {
return sfc.cssVars.length ? genCssVarsFromList(sfc.cssVars, id, isProd) : ''
}
function genCssVarsFromList(
export function genCssVarsFromList(
vars: string[],
id: string,
isProd: boolean
): string {
return `{\n ${vars
.map(v => `"${genVarName(id, v, isProd)}": (${v})`)
.map(key => `"${genVarName(id, key, isProd)}": (${key})`)
.join(',\n ')}\n}`
}
@@ -68,12 +55,11 @@ export const cssVarsPlugin = postcss.plugin<CssVarsPluginOptions>(
'vue-scoped',
opts => (root: Root) => {
const { id, isProd } = opts!
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, $3) => {
return `var(--${genVarName(shortId, $1 || $2 || $3, isProd)})`
return `var(--${genVarName(id, $1 || $2 || $3, isProd)})`
})
}
})

View File

@@ -5,7 +5,6 @@ export { compileStyle, compileStyleAsync } from './compileStyle'
export { compileScript } from './compileScript'
export { rewriteDefault } from './rewriteDefault'
export { generateCodeFrame } from '@vue/compiler-core'
export { generateCssVars } from './cssVars'
// Types
export {