Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(server-renderer): respect compilerOptions during runtime template compilation #4631

Merged
merged 2 commits into from Sep 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
148 changes: 148 additions & 0 deletions packages/server-renderer/__tests__/ssrCompilerOptions.spec.ts
@@ -0,0 +1,148 @@
/**
* @jest-environment node
*/

import { createApp } from 'vue'
import { renderToString } from '../src/renderToString'

describe('ssr: compiler options', () => {
test('config.isCustomElement (deprecated)', async () => {
const app = createApp({
template: `<div><x-button/></div>`
})
app.config.isCustomElement = tag => tag.startsWith('x-')
expect(await renderToString(app)).toBe(`<div><x-button></x-button></div>`)
})

test('config.compilerOptions.isCustomElement', async () => {
const app = createApp({
template: `<div><x-panel/></div>`
})
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('x-')
expect(await renderToString(app)).toBe(`<div><x-panel></x-panel></div>`)
})

test('component.compilerOptions.isCustomElement', async () => {
const app = createApp({
template: `<div><x-card/><y-child/></div>`,
compilerOptions: {
isCustomElement: (tag: string) => tag.startsWith('x-')
},
components: {
YChild: {
template: `<div><y-button/></div>`
}
}
})
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('y-')
expect(await renderToString(app)).toBe(
`<div><x-card></x-card><div><y-button></y-button></div></div>`
)
})

test('component.delimiters (deprecated)', async () => {
const app = createApp({
template: `<div>[[ 1 + 1 ]]</div>`,
delimiters: ['[[', ']]']
})
expect(await renderToString(app)).toBe(`<div>2</div>`)
})

test('config.compilerOptions.delimiters', async () => {
const app = createApp({
template: `<div>[( 1 + 1 )]</div>`
})
app.config.compilerOptions.delimiters = ['[(', ')]']
expect(await renderToString(app)).toBe(`<div>2</div>`)
})

test('component.compilerOptions.delimiters', async () => {
const app = createApp({
template: `<div>[[ 1 + 1 ]]<ChildComponent/></div>`,
compilerOptions: {
delimiters: ['[[', ']]']
},
components: {
ChildComponent: {
template: `<div>(( 2 + 2 ))</div>`
}
}
})
app.config.compilerOptions.delimiters = ['((', '))']
expect(await renderToString(app)).toBe(`<div>2<div>4</div></div>`)
})

test('compilerOptions.whitespace', async () => {
const app = createApp({
template: `<div><span>Hello world</span><ChildComponent/></div>`,
compilerOptions: {
whitespace: 'condense'
},
components: {
ChildComponent: {
template: `<span>Hello world</span>`
}
}
})
app.config.compilerOptions.whitespace = 'preserve'
expect(await renderToString(app)).toBe(
`<div><span>Hello world</span><span>Hello world</span></div>`
)
})

test('caching with compilerOptions', async () => {
const template = `<div>{{1 + 1}} [[1 + 1]]</div>`

const app = createApp({
template: `<div><ChildOne/><ChildTwo/><ChildThree/></div>`,
components: {
ChildOne: {
template
},
ChildTwo: {
template,
compilerOptions: {
whitespace: 'preserve'
}
},
ChildThree: {
template,
compilerOptions: {
delimiters: ['[[', ']]']
}
}
}
})
expect(await renderToString(app)).toBe(
`<div><div>2 [[1 + 1]]</div><div>2 [[1 + 1]]</div><div>{{1 + 1}} 2</div></div>`
)
})

test('caching with isCustomElement', async () => {
const template = `<div><MyChild/></div>`

const app = createApp({
template,
// No compilerOptions on the root
components: {
MyChild: {
template,
compilerOptions: {
isCustomElement: tag => tag.startsWith('x-')
},
components: {
MyChild: {
template,
compilerOptions: {
isCustomElement: tag => tag.startsWith('My')
}
}
}
}
}
})
expect(await renderToString(app)).toBe(
`<div><div><div><MyChild></MyChild></div></div></div>`
)
})
})
74 changes: 51 additions & 23 deletions packages/server-renderer/src/helpers/ssrCompile.ts
@@ -1,7 +1,7 @@
import { ComponentInternalInstance, warn } from 'vue'
import { ComponentInternalInstance, ComponentOptions, warn } from 'vue'
import { compile } from '@vue/compiler-ssr'
import { generateCodeFrame, NO } from '@vue/shared'
import { CompilerError } from '@vue/compiler-core'
import { extend, generateCodeFrame, isFunction, NO } from '@vue/shared'
import { CompilerError, CompilerOptions } from '@vue/compiler-core'
import { PushFn } from '../render'

type SSRRenderFunction = (
Expand All @@ -24,29 +24,57 @@ export function ssrCompile(
)
}

const cached = compileCache[template]
// TODO: This is copied from runtime-core/src/component.ts and should probably be refactored
const Component = instance.type as ComponentOptions
const { isCustomElement, compilerOptions } = instance.appContext.config
const { delimiters, compilerOptions: componentCompilerOptions } = Component

const finalCompilerOptions: CompilerOptions = extend(
extend(
{
isCustomElement,
delimiters
},
compilerOptions
),
componentCompilerOptions
)

finalCompilerOptions.isCustomElement =
finalCompilerOptions.isCustomElement || NO
finalCompilerOptions.isNativeTag = finalCompilerOptions.isNativeTag || NO

const cacheKey = JSON.stringify(
{
template,
compilerOptions: finalCompilerOptions
},
(key, value) => {
return isFunction(value) ? value.toString() : value
}
)

const cached = compileCache[cacheKey]
if (cached) {
return cached
}

const { code } = compile(template, {
isCustomElement: instance.appContext.config.isCustomElement || NO,
isNativeTag: instance.appContext.config.isNativeTag || NO,
onError(err: CompilerError) {
if (__DEV__) {
const message = `[@vue/server-renderer] Template compilation error: ${err.message}`
const codeFrame =
err.loc &&
generateCodeFrame(
template as string,
err.loc.start.offset,
err.loc.end.offset
)
warn(codeFrame ? `${message}\n${codeFrame}` : message)
} else {
throw err
}
finalCompilerOptions.onError = (err: CompilerError) => {
if (__DEV__) {
const message = `[@vue/server-renderer] Template compilation error: ${err.message}`
const codeFrame =
err.loc &&
generateCodeFrame(
template as string,
err.loc.start.offset,
err.loc.end.offset
)
warn(codeFrame ? `${message}\n${codeFrame}` : message)
} else {
throw err
}
})
return (compileCache[template] = Function('require', code)(require))
}

const { code } = compile(template, finalCompilerOptions)
return (compileCache[cacheKey] = Function('require', code)(require))
}