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 1 commit
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
92 changes: 92 additions & 0 deletions packages/server-renderer/__tests__/ssrCompilerOptions.spec.ts
@@ -0,0 +1,92 @@
/**
* @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>`
)
})
})
61 changes: 40 additions & 21 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, NO } from '@vue/shared'
import { CompilerError, CompilerOptions } from '@vue/compiler-core'
import { PushFn } from '../render'

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

// TODO: The cache does not take the compilerOptions into account
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runtime compiler options are limited and largely serializable. We can call fn.toString() if isCustomElement is present, and isNativeTag is in fact constant since server-renderer implies runtime-dom.

So we can resolve the options first, serialize it to get a cache key that takes options into account.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I think that should be working now.

There are some potential edge cases around using fn.toString(), e.g. if someone uses a utility function to create isCustomElement functions they will likely have the same toString but different values caught in their closures. This probably isn't worth worrying about given the templates also have to be a perfect match. If someone did encounter that extreme edge case, it should be possible for them to work around it by passing a cache-buster in the compilerOptions.

const cached = compileCache[template]
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
}
// 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

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
}
})
}

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