Merge remote-tracking branch 'github/master' into changing_unwrap_ref

# Conflicts:
#	packages/reactivity/src/ref.ts
#	packages/runtime-core/__tests__/apiTemplateRef.spec.ts
#	packages/runtime-core/src/apiWatch.ts
This commit is contained in:
pikax
2020-04-08 21:21:04 +01:00
339 changed files with 26645 additions and 8965 deletions

View File

@@ -2,8 +2,8 @@
exports[`compiler: codegen ArrayExpression 1`] = `
"
return function render() {
with (this) {
return function render(_ctx, _cache) {
with (_ctx) {
return [
foo,
bar(baz)
@@ -14,22 +14,18 @@ return function render() {
exports[`compiler: codegen CacheExpression 1`] = `
"
export default function render() {
const _ctx = this
const _cache = _ctx.$cache
export function render(_ctx, _cache) {
return _cache[1] || (_cache[1] = foo)
}"
`;
exports[`compiler: codegen CacheExpression w/ isVNode: true 1`] = `
"
export default function render() {
const _ctx = this
const _cache = _ctx.$cache
export function render(_ctx, _cache) {
return _cache[1] || (
setBlockTracking(-1),
_setBlockTracking(-1),
_cache[1] = foo,
setBlockTracking(1),
_setBlockTracking(1),
_cache[1]
)
}"
@@ -37,8 +33,8 @@ export default function render() {
exports[`compiler: codegen ConditionalExpression 1`] = `
"
return function render() {
with (this) {
return function render(_ctx, _cache) {
with (_ctx) {
return ok
? foo()
: orNot
@@ -50,8 +46,8 @@ return function render() {
exports[`compiler: codegen Element (callExpression + objectExpression + TemplateChildNode[]) 1`] = `
"
return function render() {
with (this) {
return function render(_ctx, _cache) {
with (_ctx) {
return _createVNode(\\"div\\", {
id: \\"foo\\",
[prop]: bar,
@@ -63,24 +59,17 @@ return function render() {
}"
`;
exports[`compiler: codegen SequenceExpression 1`] = `
exports[`compiler: codegen assets + temps 1`] = `
"
return function render() {
with (this) {
return (foo, bar(baz))
}
}"
`;
exports[`compiler: codegen assets 1`] = `
"
return function render() {
with (this) {
return function render(_ctx, _cache) {
with (_ctx) {
const _component_Foo = _resolveComponent(\\"Foo\\")
const _component_bar_baz = _resolveComponent(\\"bar-baz\\")
const _component_barbaz = _resolveComponent(\\"barbaz\\")
const _directive_my_dir = _resolveDirective(\\"my_dir\\")
const _directive_my_dir_0 = _resolveDirective(\\"my_dir_0\\")
const _directive_my_dir_1 = _resolveDirective(\\"my_dir_1\\")
let _temp0, _temp1, _temp2
return null
}
}"
@@ -88,8 +77,8 @@ return function render() {
exports[`compiler: codegen comment 1`] = `
"
return function render() {
with (this) {
return function render(_ctx, _cache) {
with (_ctx) {
return _createCommentVNode(\\"foo\\")
}
}"
@@ -97,18 +86,18 @@ return function render() {
exports[`compiler: codegen compound expression 1`] = `
"
return function render() {
with (this) {
return _ctx.foo + _toString(bar)
return function render(_ctx, _cache) {
with (_ctx) {
return _ctx.foo + _toDisplayString(bar) + nested
}
}"
`;
exports[`compiler: codegen forNode 1`] = `
"
return function render() {
with (this) {
return (foo, bar)
return function render(_ctx, _cache) {
with (_ctx) {
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(), 1))
}
}"
`;
@@ -116,20 +105,19 @@ return function render() {
exports[`compiler: codegen function mode preamble 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, resolveDirective: _resolveDirective } = _Vue
return null
}
}"
`;
exports[`compiler: codegen function mode preamble w/ prefixIdentifiers: true 1`] = `
"const { createVNode, resolveDirective } = Vue
"const { createVNode: _createVNode, resolveDirective: _resolveDirective } = Vue
return function render() {
const _ctx = this
return function render(_ctx, _cache) {
return null
}"
`;
@@ -139,8 +127,8 @@ exports[`compiler: codegen hoists 1`] = `
const _hoisted_1 = hello
const _hoisted_2 = { id: \\"foo\\" }
return function render() {
with (this) {
return function render(_ctx, _cache) {
with (_ctx) {
return null
}
}"
@@ -148,44 +136,59 @@ return function render() {
exports[`compiler: codegen ifNode 1`] = `
"
return function render() {
with (this) {
return (foo, bar)
return function render(_ctx, _cache) {
with (_ctx) {
return foo
? bar
: baz
}
}"
`;
exports[`compiler: codegen interpolation 1`] = `
"
return function render() {
with (this) {
return _toString(hello)
return function render(_ctx, _cache) {
with (_ctx) {
return _toDisplayString(hello)
}
}"
`;
exports[`compiler: codegen module mode preamble 1`] = `
"import { createVNode, resolveDirective } from \\"vue\\"
"import { createVNode as _createVNode, resolveDirective as _resolveDirective } from \\"vue\\"
export default function render() {
const _ctx = this
export function render(_ctx, _cache) {
return null
}"
`;
exports[`compiler: codegen prefixIdentifiers: true should inject _ctx statement 1`] = `
"
return function render() {
const _ctx = this
exports[`compiler: codegen module mode preamble w/ optimizeBindings: true 1`] = `
"import { createVNode, resolveDirective } from \\"vue\\"
// Binding optimization for webpack code-split
const _createVNode = createVNode, _resolveDirective = resolveDirective
export function render(_ctx, _cache) {
return null
}"
`;
exports[`compiler: codegen static text 1`] = `
"
return function render() {
with (this) {
return function render(_ctx, _cache) {
with (_ctx) {
return \\"hello\\"
}
}"
`;
exports[`compiler: codegen temps 1`] = `
"
return function render(_ctx, _cache) {
with (_ctx) {
let _temp0, _temp1, _temp2
return null
}
}"
`;

View File

@@ -3,21 +3,23 @@
exports[`compiler: integration tests function mode 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { toString: _toString, openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode, Fragment: _Fragment, renderList: _renderList, createTextVNode: _createTextVNode } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, renderList: _renderList } = _Vue
return (_openBlock(), _createBlock(\\"div\\", {
id: \\"foo\\",
class: bar.baz
}, [
_createTextVNode(_toString(world.burn()) + \\" \\", 1 /* TEXT */),
(_openBlock(), ok
? _createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: _createBlock(_Fragment, { key: 1 }, [\\"no\\"])),
(_openBlock(false), _createBlock(_Fragment, null, _renderList(list, (value, index) => {
_createTextVNode(_toDisplayString(world.burn()) + \\" \\", 1 /* TEXT */),
ok
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }, \\"yes\\"))
: (_openBlock(), _createBlock(_Fragment, { key: 1 }, [
_createTextVNode(\\"no\\")
], 64 /* STABLE_FRAGMENT */)),
(_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (value, index) => {
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"span\\", null, _toString(value + index), 1 /* TEXT */)
_createVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */)
]))
}), 256 /* UNKEYED_FRAGMENT */))
], 2 /* CLASS */))
@@ -26,21 +28,22 @@ return function render() {
`;
exports[`compiler: integration tests function mode w/ prefixIdentifiers: true 1`] = `
"const { toString, openBlock, createVNode, createBlock, createCommentVNode, Fragment, renderList, createTextVNode } = Vue
"const { toDisplayString: _toDisplayString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, renderList: _renderList } = Vue
return function render() {
const _ctx = this
return (openBlock(), createBlock(\\"div\\", {
return function render(_ctx, _cache) {
return (_openBlock(), _createBlock(\\"div\\", {
id: \\"foo\\",
class: _ctx.bar.baz
}, [
createTextVNode(toString(_ctx.world.burn()) + \\" \\", 1 /* TEXT */),
(openBlock(), (_ctx.ok)
? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: createBlock(Fragment, { key: 1 }, [\\"no\\"])),
(openBlock(false), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => {
return (openBlock(), createBlock(\\"div\\", null, [
createVNode(\\"span\\", null, toString(value + index), 1 /* TEXT */)
_createTextVNode(_toDisplayString(_ctx.world.burn()) + \\" \\", 1 /* TEXT */),
(_ctx.ok)
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }, \\"yes\\"))
: (_openBlock(), _createBlock(_Fragment, { key: 1 }, [
_createTextVNode(\\"no\\")
], 64 /* STABLE_FRAGMENT */)),
(_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.list, (value, index) => {
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */)
]))
}), 256 /* UNKEYED_FRAGMENT */))
], 2 /* CLASS */))
@@ -48,21 +51,22 @@ return function render() {
`;
exports[`compiler: integration tests module mode 1`] = `
"import { toString, openBlock, createVNode, createBlock, createCommentVNode, Fragment, renderList, createTextVNode } from \\"vue\\"
"import { toDisplayString as _toDisplayString, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock, createCommentVNode as _createCommentVNode, createTextVNode as _createTextVNode, Fragment as _Fragment, renderList as _renderList } from \\"vue\\"
export default function render() {
const _ctx = this
return (openBlock(), createBlock(\\"div\\", {
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock(\\"div\\", {
id: \\"foo\\",
class: _ctx.bar.baz
}, [
createTextVNode(toString(_ctx.world.burn()) + \\" \\", 1 /* TEXT */),
(openBlock(), (_ctx.ok)
? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: createBlock(Fragment, { key: 1 }, [\\"no\\"])),
(openBlock(false), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => {
return (openBlock(), createBlock(\\"div\\", null, [
createVNode(\\"span\\", null, toString(value + index), 1 /* TEXT */)
_createTextVNode(_toDisplayString(_ctx.world.burn()) + \\" \\", 1 /* TEXT */),
(_ctx.ok)
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }, \\"yes\\"))
: (_openBlock(), _createBlock(_Fragment, { key: 1 }, [
_createTextVNode(\\"no\\")
], 64 /* STABLE_FRAGMENT */)),
(_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.list, (value, index) => {
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */)
]))
}), 256 /* UNKEYED_FRAGMENT */))
], 2 /* CLASS */))

View File

@@ -0,0 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`scopeId compiler support should push scopeId for hoisted nodes 1`] = `
"import { createVNode as _createVNode, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from \\"vue\\"
const _withId = _withScopeId(\\"test\\")
_pushScopeId(\\"test\\")
const _hoisted_1 = _createVNode(\\"div\\", null, \\"hello\\", -1 /* HOISTED */)
const _hoisted_2 = _createVNode(\\"div\\", null, \\"world\\", -1 /* HOISTED */)
_popScopeId()
export const render = _withId(function render(_ctx, _cache) {
return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1,
_createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */),
_hoisted_2
]))
})"
`;
exports[`scopeId compiler support should wrap default slot 1`] = `
"import { createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\"
const _withId = _withScopeId(\\"test\\")
export const render = _withId(function render(_ctx, _cache) {
const _component_Child = _resolveComponent(\\"Child\\")
return (_openBlock(), _createBlock(_component_Child, null, {
default: _withId(() => [
_createVNode(\\"div\\")
]),
_: 1
}))
})"
`;
exports[`scopeId compiler support should wrap dynamic slots 1`] = `
"import { createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, renderList as _renderList, createSlots as _createSlots, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\"
const _withId = _withScopeId(\\"test\\")
export const render = _withId(function render(_ctx, _cache) {
const _component_Child = _resolveComponent(\\"Child\\")
return (_openBlock(), _createBlock(_component_Child, null, _createSlots({ _: 1 }, [
(_ctx.ok)
? {
name: \\"foo\\",
fn: _withId(() => [
_createVNode(\\"div\\")
])
}
: undefined,
_renderList(_ctx.list, (i) => {
return {
name: i,
fn: _withId(() => [
_createVNode(\\"div\\")
])
}
})
]), 1024 /* DYNAMIC_SLOTS */))
})"
`;
exports[`scopeId compiler support should wrap named slots 1`] = `
"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\"
const _withId = _withScopeId(\\"test\\")
export const render = _withId(function render(_ctx, _cache) {
const _component_Child = _resolveComponent(\\"Child\\")
return (_openBlock(), _createBlock(_component_Child, null, {
foo: _withId(({ msg }) => [
_createTextVNode(_toDisplayString(msg), 1 /* TEXT */)
]),
bar: _withId(() => [
_createVNode(\\"div\\")
]),
_: 1
}))
})"
`;
exports[`scopeId compiler support should wrap render function 1`] = `
"import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\"
const _withId = _withScopeId(\\"test\\")
export const render = _withId(function render(_ctx, _cache) {
return (_openBlock(), _createBlock(\\"div\\"))
})"
`;

View File

@@ -9,20 +9,28 @@ import {
createArrayExpression,
createCompoundExpression,
createInterpolation,
createSequenceExpression,
createCallExpression,
createConditionalExpression,
IfCodegenNode,
ForCodegenNode,
createCacheExpression
createCacheExpression,
createTemplateLiteral,
createBlockStatement,
createIfStatement,
createAssignmentExpression,
IfConditionalExpression,
createVNodeCall,
VNodeCall,
DirectiveArguments
} from '../src'
import {
CREATE_VNODE,
TO_STRING,
TO_DISPLAY_STRING,
RESOLVE_DIRECTIVE,
helperNameMap,
RESOLVE_COMPONENT,
CREATE_COMMENT
CREATE_COMMENT,
FRAGMENT,
RENDER_LIST
} from '../src/runtimeHelpers'
import { createElementWithCodegen } from './testUtils'
import { PatchFlags } from '@vue/shared'
@@ -37,6 +45,7 @@ function createRoot(options: Partial<RootNode> = {}): RootNode {
imports: [],
hoists: [],
cached: 0,
temps: 0,
codegenNode: createSimpleExpression(`null`, false),
loc: locStub,
...options
@@ -49,11 +58,33 @@ describe('compiler: codegen', () => {
helpers: [CREATE_VNODE, RESOLVE_DIRECTIVE]
})
const { code } = generate(root, { mode: 'module' })
expect(code).toMatch(
`import { ${helperNameMap[CREATE_VNODE]} as _${
helperNameMap[CREATE_VNODE]
}, ${helperNameMap[RESOLVE_DIRECTIVE]} as _${
helperNameMap[RESOLVE_DIRECTIVE]
} } from "vue"`
)
expect(code).toMatchSnapshot()
})
test('module mode preamble w/ optimizeBindings: true', () => {
const root = createRoot({
helpers: [CREATE_VNODE, RESOLVE_DIRECTIVE]
})
const { code } = generate(root, { mode: 'module', optimizeBindings: true })
expect(code).toMatch(
`import { ${helperNameMap[CREATE_VNODE]}, ${
helperNameMap[RESOLVE_DIRECTIVE]
} } from "vue"`
)
expect(code).toMatch(
`const _${helperNameMap[CREATE_VNODE]} = ${
helperNameMap[CREATE_VNODE]
}, _${helperNameMap[RESOLVE_DIRECTIVE]} = ${
helperNameMap[RESOLVE_DIRECTIVE]
}`
)
expect(code).toMatchSnapshot()
})
@@ -83,17 +114,20 @@ describe('compiler: codegen', () => {
})
expect(code).not.toMatch(`const _Vue = Vue`)
expect(code).toMatch(
`const { ${helperNameMap[CREATE_VNODE]}, ${
`const { ${helperNameMap[CREATE_VNODE]}: _${
helperNameMap[CREATE_VNODE]
}, ${helperNameMap[RESOLVE_DIRECTIVE]}: _${
helperNameMap[RESOLVE_DIRECTIVE]
} } = Vue`
)
expect(code).toMatchSnapshot()
})
test('assets', () => {
test('assets + temps', () => {
const root = createRoot({
components: [`Foo`, `bar-baz`, `barbaz`],
directives: [`my_dir`]
directives: [`my_dir_0`, `my_dir_1`],
temps: 3
})
const { code } = generate(root, { mode: 'function' })
expect(code).toMatch(
@@ -110,10 +144,16 @@ describe('compiler: codegen', () => {
}("barbaz")\n`
)
expect(code).toMatch(
`const _directive_my_dir = _${
`const _directive_my_dir_0 = _${
helperNameMap[RESOLVE_DIRECTIVE]
}("my_dir")\n`
}("my_dir_0")\n`
)
expect(code).toMatch(
`const _directive_my_dir_1 = _${
helperNameMap[RESOLVE_DIRECTIVE]
}("my_dir_1")\n`
)
expect(code).toMatch(`let _temp0, _temp1, _temp2`)
expect(code).toMatchSnapshot()
})
@@ -138,9 +178,12 @@ describe('compiler: codegen', () => {
expect(code).toMatchSnapshot()
})
test('prefixIdentifiers: true should inject _ctx statement', () => {
const { code } = generate(createRoot(), { prefixIdentifiers: true })
expect(code).toMatch(`const _ctx = this\n`)
test('temps', () => {
const root = createRoot({
temps: 3
})
const { code } = generate(root)
expect(code).toMatch(`let _temp0, _temp1, _temp2`)
expect(code).toMatchSnapshot()
})
@@ -164,7 +207,7 @@ describe('compiler: codegen', () => {
codegenNode: createInterpolation(`hello`, locStub)
})
)
expect(code).toMatch(`return _${helperNameMap[TO_STRING]}(hello)`)
expect(code).toMatch(`return _${helperNameMap[TO_DISPLAY_STRING]}(hello)`)
expect(code).toMatchSnapshot()
})
@@ -193,11 +236,15 @@ describe('compiler: codegen', () => {
type: NodeTypes.INTERPOLATION,
loc: locStub,
content: createSimpleExpression(`bar`, false, locStub)
}
},
// nested compound
createCompoundExpression([` + `, `nested`])
])
})
)
expect(code).toMatch(`return _ctx.foo + _${helperNameMap[TO_STRING]}(bar)`)
expect(code).toMatch(
`return _ctx.foo + _${helperNameMap[TO_DISPLAY_STRING]}(bar) + nested`
)
expect(code).toMatchSnapshot()
})
@@ -208,14 +255,15 @@ describe('compiler: codegen', () => {
type: NodeTypes.IF,
loc: locStub,
branches: [],
codegenNode: createSequenceExpression([
codegenNode: createConditionalExpression(
createSimpleExpression('foo', false),
createSimpleExpression('bar', false)
]) as IfCodegenNode
createSimpleExpression('bar', false),
createSimpleExpression('baz', false)
) as IfConditionalExpression
}
})
)
expect(code).toMatch(`return (foo, bar)`)
expect(code).toMatch(/return foo\s+\? bar\s+: baz/)
expect(code).toMatchSnapshot()
})
@@ -230,21 +278,30 @@ describe('compiler: codegen', () => {
keyAlias: undefined,
objectIndexAlias: undefined,
children: [],
codegenNode: createSequenceExpression([
createSimpleExpression('foo', false),
createSimpleExpression('bar', false)
]) as ForCodegenNode
parseResult: {} as any,
codegenNode: {
type: NodeTypes.VNODE_CALL,
tag: FRAGMENT,
isBlock: true,
isForBlock: true,
props: undefined,
children: createCallExpression(RENDER_LIST),
patchFlag: '1',
dynamicProps: undefined,
directives: undefined,
loc: locStub
} as ForCodegenNode
}
})
)
expect(code).toMatch(`return (foo, bar)`)
expect(code).toMatch(`openBlock(true)`)
expect(code).toMatchSnapshot()
})
test('Element (callExpression + objectExpression + TemplateChildNode[])', () => {
const { code } = generate(
createRoot({
codegenNode: createElementWithCodegen([
codegenNode: createElementWithCodegen(
// string
`"div"`,
// ObjectExpression
@@ -275,7 +332,7 @@ describe('compiler: codegen', () => {
),
// ChildNode[]
[
createElementWithCodegen([
createElementWithCodegen(
`"p"`,
createObjectExpression(
[
@@ -287,11 +344,11 @@ describe('compiler: codegen', () => {
],
locStub
)
])
)
],
// flag
PatchFlags.FULL_PROPS + ''
])
)
})
)
expect(code).toMatch(`
@@ -321,19 +378,6 @@ describe('compiler: codegen', () => {
expect(code).toMatchSnapshot()
})
test('SequenceExpression', () => {
const { code } = generate(
createRoot({
codegenNode: createSequenceExpression([
createSimpleExpression(`foo`, false),
createCallExpression(`bar`, [`baz`])
])
})
)
expect(code).toMatch(`return (foo, bar(baz))`)
expect(code).toMatchSnapshot()
})
test('ConditionalExpression', () => {
const { code } = generate(
createRoot({
@@ -372,7 +416,6 @@ describe('compiler: codegen', () => {
prefixIdentifiers: true
}
)
expect(code).toMatch(`const _cache = _ctx.$cache`)
expect(code).toMatch(`_cache[1] || (_cache[1] = foo)`)
expect(code).toMatchSnapshot()
})
@@ -392,17 +435,309 @@ describe('compiler: codegen', () => {
prefixIdentifiers: true
}
)
expect(code).toMatch(`const _cache = _ctx.$cache`)
expect(code).toMatch(
`
_cache[1] || (
setBlockTracking(-1),
_setBlockTracking(-1),
_cache[1] = foo,
setBlockTracking(1),
_setBlockTracking(1),
_cache[1]
)
`.trim()
)
expect(code).toMatchSnapshot()
})
test('TemplateLiteral', () => {
const { code } = generate(
createRoot({
codegenNode: createCallExpression(`_push`, [
createTemplateLiteral([
`foo`,
createCallExpression(`_renderAttr`, ['id', 'foo']),
`bar`
])
])
}),
{ ssr: true, mode: 'module' }
)
expect(code).toMatchInlineSnapshot(`
"
export function ssrRender(_ctx, _push, _parent) {
_push(\`foo\${_renderAttr(id, foo)}bar\`)
}"
`)
})
describe('IfStatement', () => {
test('if', () => {
const { code } = generate(
createRoot({
codegenNode: createBlockStatement([
createIfStatement(
createSimpleExpression('foo', false),
createBlockStatement([createCallExpression(`ok`)])
)
])
}),
{ ssr: true, mode: 'module' }
)
expect(code).toMatchInlineSnapshot(`
"
export function ssrRender(_ctx, _push, _parent) {
if (foo) {
ok()
}
}"
`)
})
test('if/else', () => {
const { code } = generate(
createRoot({
codegenNode: createBlockStatement([
createIfStatement(
createSimpleExpression('foo', false),
createBlockStatement([createCallExpression(`foo`)]),
createBlockStatement([createCallExpression('bar')])
)
])
}),
{ ssr: true, mode: 'module' }
)
expect(code).toMatchInlineSnapshot(`
"
export function ssrRender(_ctx, _push, _parent) {
if (foo) {
foo()
} else {
bar()
}
}"
`)
})
test('if/else-if', () => {
const { code } = generate(
createRoot({
codegenNode: createBlockStatement([
createIfStatement(
createSimpleExpression('foo', false),
createBlockStatement([createCallExpression(`foo`)]),
createIfStatement(
createSimpleExpression('bar', false),
createBlockStatement([createCallExpression(`bar`)])
)
)
])
}),
{ ssr: true, mode: 'module' }
)
expect(code).toMatchInlineSnapshot(`
"
export function ssrRender(_ctx, _push, _parent) {
if (foo) {
foo()
} else if (bar) {
bar()
}
}"
`)
})
test('if/else-if/else', () => {
const { code } = generate(
createRoot({
codegenNode: createBlockStatement([
createIfStatement(
createSimpleExpression('foo', false),
createBlockStatement([createCallExpression(`foo`)]),
createIfStatement(
createSimpleExpression('bar', false),
createBlockStatement([createCallExpression(`bar`)]),
createBlockStatement([createCallExpression('baz')])
)
)
])
}),
{ ssr: true, mode: 'module' }
)
expect(code).toMatchInlineSnapshot(`
"
export function ssrRender(_ctx, _push, _parent) {
if (foo) {
foo()
} else if (bar) {
bar()
} else {
baz()
}
}"
`)
})
})
test('AssignmentExpression', () => {
const { code } = generate(
createRoot({
codegenNode: createAssignmentExpression(
createSimpleExpression(`foo`, false),
createSimpleExpression(`bar`, false)
)
})
)
expect(code).toMatchInlineSnapshot(`
"
return function render(_ctx, _cache) {
with (_ctx) {
return foo = bar
}
}"
`)
})
describe('VNodeCall', () => {
function genCode(node: VNodeCall) {
return generate(
createRoot({
codegenNode: node
})
).code.match(/with \(_ctx\) \{\s+([^]+)\s+\}\s+\}$/)![1]
}
const mockProps = createObjectExpression([
createObjectProperty(`foo`, createSimpleExpression(`bar`, true))
])
const mockChildren = createCompoundExpression(['children'])
const mockDirs = createArrayExpression([
createArrayExpression([`foo`, createSimpleExpression(`bar`, false)])
]) as DirectiveArguments
test('tag only', () => {
expect(genCode(createVNodeCall(null, `"div"`))).toMatchInlineSnapshot(`
"return _createVNode(\\"div\\")
"
`)
expect(genCode(createVNodeCall(null, FRAGMENT))).toMatchInlineSnapshot(`
"return _createVNode(_Fragment)
"
`)
})
test('with props', () => {
expect(genCode(createVNodeCall(null, `"div"`, mockProps)))
.toMatchInlineSnapshot(`
"return _createVNode(\\"div\\", { foo: \\"bar\\" })
"
`)
})
test('with children, no props', () => {
expect(genCode(createVNodeCall(null, `"div"`, undefined, mockChildren)))
.toMatchInlineSnapshot(`
"return _createVNode(\\"div\\", null, children)
"
`)
})
test('with children + props', () => {
expect(genCode(createVNodeCall(null, `"div"`, mockProps, mockChildren)))
.toMatchInlineSnapshot(`
"return _createVNode(\\"div\\", { foo: \\"bar\\" }, children)
"
`)
})
test('with patchFlag and no children/props', () => {
expect(genCode(createVNodeCall(null, `"div"`, undefined, undefined, '1')))
.toMatchInlineSnapshot(`
"return _createVNode(\\"div\\", null, null, 1)
"
`)
})
test('as block', () => {
expect(
genCode(
createVNodeCall(
null,
`"div"`,
mockProps,
mockChildren,
undefined,
undefined,
undefined,
true
)
)
).toMatchInlineSnapshot(`
"return (_openBlock(), _createBlock(\\"div\\", { foo: \\"bar\\" }, children))
"
`)
})
test('as for block', () => {
expect(
genCode(
createVNodeCall(
null,
`"div"`,
mockProps,
mockChildren,
undefined,
undefined,
undefined,
true,
true
)
)
).toMatchInlineSnapshot(`
"return (_openBlock(true), _createBlock(\\"div\\", { foo: \\"bar\\" }, children))
"
`)
})
test('with directives', () => {
expect(
genCode(
createVNodeCall(
null,
`"div"`,
mockProps,
mockChildren,
undefined,
undefined,
mockDirs
)
)
).toMatchInlineSnapshot(`
"return _withDirectives(_createVNode(\\"div\\", { foo: \\"bar\\" }, children), [
[foo, bar]
])
"
`)
})
test('block + directives', () => {
expect(
genCode(
createVNodeCall(
null,
`"div"`,
mockProps,
mockChildren,
undefined,
undefined,
mockDirs,
true
)
)
).toMatchInlineSnapshot(`
"return _withDirectives((_openBlock(), _createBlock(\\"div\\", { foo: \\"bar\\" }, children)), [
[foo, bar]
])
"
`)
})
})
})

View File

@@ -44,7 +44,7 @@ describe('compiler: integration tests', () => {
return res
}
test('function mode', async () => {
test('function mode', () => {
const { code, map } = compile(source, {
sourceMap: true,
filename: `foo.vue`
@@ -54,7 +54,7 @@ describe('compiler: integration tests', () => {
expect(map!.sources).toEqual([`foo.vue`])
expect(map!.sourcesContent).toEqual([source])
const consumer = await new SourceMapConsumer(map as RawSourceMap)
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, `id`))
@@ -109,7 +109,7 @@ describe('compiler: integration tests', () => {
).toMatchObject(getPositionInCode(source, `value + index`))
})
test('function mode w/ prefixIdentifiers: true', async () => {
test('function mode w/ prefixIdentifiers: true', () => {
const { code, map } = compile(source, {
sourceMap: true,
filename: `foo.vue`,
@@ -120,7 +120,7 @@ describe('compiler: integration tests', () => {
expect(map!.sources).toEqual([`foo.vue`])
expect(map!.sourcesContent).toEqual([source])
const consumer = await new SourceMapConsumer(map as RawSourceMap)
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, `id`))
@@ -184,7 +184,7 @@ describe('compiler: integration tests', () => {
).toMatchObject(getPositionInCode(source, `value + index`))
})
test('module mode', async () => {
test('module mode', () => {
const { code, map } = compile(source, {
mode: 'module',
sourceMap: true,
@@ -195,7 +195,7 @@ describe('compiler: integration tests', () => {
expect(map!.sources).toEqual([`foo.vue`])
expect(map!.sourcesContent).toEqual([source])
const consumer = await new SourceMapConsumer(map as RawSourceMap)
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, `id`))

View File

@@ -1,5 +1,5 @@
import { ParserOptions } from '../src/options'
import { parse, TextModes } from '../src/parse'
import { baseParse, TextModes } from '../src/parse'
import { ErrorCodes } from '../src/errors'
import {
CommentNode,
@@ -16,7 +16,7 @@ import {
describe('compiler: parse', () => {
describe('Text', () => {
test('simple text', () => {
const ast = parse('some text')
const ast = baseParse('some text')
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
@@ -31,7 +31,7 @@ describe('compiler: parse', () => {
})
test('simple text with invalid end tag', () => {
const ast = parse('some text</div>', {
const ast = baseParse('some text</div>', {
onError: () => {}
})
const text = ast.children[0] as TextNode
@@ -48,7 +48,7 @@ describe('compiler: parse', () => {
})
test('text with interpolation', () => {
const ast = parse('some {{ foo + bar }} text')
const ast = baseParse('some {{ foo + bar }} text')
const text1 = ast.children[0] as TextNode
const text2 = ast.children[2] as TextNode
@@ -73,7 +73,7 @@ describe('compiler: parse', () => {
})
test('text with interpolation which has `<`', () => {
const ast = parse('some {{ a<b && c>d }} text')
const ast = baseParse('some {{ a<b && c>d }} text')
const text1 = ast.children[0] as TextNode
const text2 = ast.children[2] as TextNode
@@ -98,7 +98,7 @@ describe('compiler: parse', () => {
})
test('text with mix of tags and interpolations', () => {
const ast = parse('some <span>{{ foo < bar + foo }} text</span>')
const ast = baseParse('some <span>{{ foo < bar + foo }} text</span>')
const text1 = ast.children[0] as TextNode
const text2 = (ast.children[1] as ElementNode).children![1] as TextNode
@@ -123,7 +123,7 @@ describe('compiler: parse', () => {
})
test('lonly "<" don\'t separate nodes', () => {
const ast = parse('a < b', {
const ast = baseParse('a < b', {
onError: err => {
if (err.code !== ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME) {
throw err
@@ -144,7 +144,7 @@ describe('compiler: parse', () => {
})
test('lonly "{{" don\'t separate nodes', () => {
const ast = parse('a {{ b', {
const ast = baseParse('a {{ b', {
onError: error => {
if (error.code !== ErrorCodes.X_MISSING_INTERPOLATION_END) {
throw error
@@ -166,7 +166,7 @@ describe('compiler: parse', () => {
test('HTML entities compatibility in text (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => {
const spy = jest.fn()
const ast = parse('&ampersand;', {
const ast = baseParse('&ampersand;', {
namedCharacterReferences: { amp: '&' },
onError: spy
})
@@ -195,7 +195,7 @@ describe('compiler: parse', () => {
test('HTML entities compatibility in attribute (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => {
const spy = jest.fn()
const ast = parse(
const ast = baseParse(
'<div a="&ampersand;" b="&amp;ersand;" c="&amp!"></div>',
{
namedCharacterReferences: { amp: '&', 'amp;': '&' },
@@ -248,7 +248,7 @@ describe('compiler: parse', () => {
test('Some control character reference should be replaced.', () => {
const spy = jest.fn()
const ast = parse('&#x86;', { onError: spy })
const ast = baseParse('&#x86;', { onError: spy })
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
@@ -275,7 +275,7 @@ describe('compiler: parse', () => {
describe('Interpolation', () => {
test('simple interpolation', () => {
const ast = parse('{{message}}')
const ast = baseParse('{{message}}')
const interpolation = ast.children[0] as InterpolationNode
expect(interpolation).toStrictEqual({
@@ -300,7 +300,7 @@ describe('compiler: parse', () => {
})
test('it can have tag-like notation', () => {
const ast = parse('{{ a<b }}')
const ast = baseParse('{{ a<b }}')
const interpolation = ast.children[0] as InterpolationNode
expect(interpolation).toStrictEqual({
@@ -325,7 +325,7 @@ describe('compiler: parse', () => {
})
test('it can have tag-like notation (2)', () => {
const ast = parse('{{ a<b }}{{ c>d }}')
const ast = baseParse('{{ a<b }}{{ c>d }}')
const interpolation1 = ast.children[0] as InterpolationNode
const interpolation2 = ast.children[1] as InterpolationNode
@@ -371,7 +371,7 @@ describe('compiler: parse', () => {
})
test('it can have tag-like notation (3)', () => {
const ast = parse('<div>{{ "</div>" }}</div>')
const ast = baseParse('<div>{{ "</div>" }}</div>')
const element = ast.children[0] as ElementNode
const interpolation = element.children[0] as InterpolationNode
@@ -398,7 +398,7 @@ describe('compiler: parse', () => {
})
test('custom delimiters', () => {
const ast = parse('<p>{msg}</p>', {
const ast = baseParse('<p>{msg}</p>', {
delimiters: ['{', '}']
})
const element = ast.children[0] as ElementNode
@@ -428,7 +428,7 @@ describe('compiler: parse', () => {
describe('Comment', () => {
test('empty comment', () => {
const ast = parse('<!---->')
const ast = baseParse('<!---->')
const comment = ast.children[0] as CommentNode
expect(comment).toStrictEqual({
@@ -443,7 +443,7 @@ describe('compiler: parse', () => {
})
test('simple comment', () => {
const ast = parse('<!--abc-->')
const ast = baseParse('<!--abc-->')
const comment = ast.children[0] as CommentNode
expect(comment).toStrictEqual({
@@ -458,7 +458,7 @@ describe('compiler: parse', () => {
})
test('two comments', () => {
const ast = parse('<!--abc--><!--def-->')
const ast = baseParse('<!--abc--><!--def-->')
const comment1 = ast.children[0] as CommentNode
const comment2 = ast.children[1] as CommentNode
@@ -485,7 +485,7 @@ describe('compiler: parse', () => {
describe('Element', () => {
test('simple div', () => {
const ast = parse('<div>hello</div>')
const ast = baseParse('<div>hello</div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
@@ -516,7 +516,7 @@ describe('compiler: parse', () => {
})
test('empty', () => {
const ast = parse('<div></div>')
const ast = baseParse('<div></div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
@@ -526,7 +526,6 @@ describe('compiler: parse', () => {
tagType: ElementTypes.ELEMENT,
codegenNode: undefined,
props: [],
isSelfClosing: false,
children: [],
loc: {
@@ -538,7 +537,7 @@ describe('compiler: parse', () => {
})
test('self closing', () => {
const ast = parse('<div/>after')
const ast = baseParse('<div/>after')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
@@ -560,7 +559,7 @@ describe('compiler: parse', () => {
})
test('void element', () => {
const ast = parse('<img>after', {
const ast = baseParse('<img>after', {
isVoidTag: tag => tag === 'img'
})
const element = ast.children[0] as ElementNode
@@ -583,8 +582,26 @@ describe('compiler: parse', () => {
})
})
test('template element with directives', () => {
const ast = baseParse('<template v-if="ok"></template>')
const element = ast.children[0]
expect(element).toMatchObject({
type: NodeTypes.ELEMENT,
tagType: ElementTypes.TEMPLATE
})
})
test('template element without directives', () => {
const ast = baseParse('<template></template>')
const element = ast.children[0]
expect(element).toMatchObject({
type: NodeTypes.ELEMENT,
tagType: ElementTypes.ELEMENT
})
})
test('native element with `isNativeTag`', () => {
const ast = parse('<div></div><comp></comp><Comp></Comp>', {
const ast = baseParse('<div></div><comp></comp><Comp></Comp>', {
isNativeTag: tag => tag === 'div'
})
@@ -608,7 +625,7 @@ describe('compiler: parse', () => {
})
test('native element without `isNativeTag`', () => {
const ast = parse('<div></div><comp></comp><Comp></Comp>')
const ast = baseParse('<div></div><comp></comp><Comp></Comp>')
expect(ast.children[0]).toMatchObject({
type: NodeTypes.ELEMENT,
@@ -629,8 +646,57 @@ describe('compiler: parse', () => {
})
})
test('v-is without `isNativeTag`', () => {
const ast = baseParse(
`<div></div><div v-is="'foo'"></div><Comp></Comp>`,
{
isNativeTag: tag => tag === 'div'
}
)
expect(ast.children[0]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.ELEMENT
})
expect(ast.children[1]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.COMPONENT
})
expect(ast.children[2]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'Comp',
tagType: ElementTypes.COMPONENT
})
})
test('v-is with `isNativeTag`', () => {
const ast = baseParse(`<div></div><div v-is="'foo'"></div><Comp></Comp>`)
expect(ast.children[0]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.ELEMENT
})
expect(ast.children[1]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.COMPONENT
})
expect(ast.children[2]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'Comp',
tagType: ElementTypes.COMPONENT
})
})
test('custom element', () => {
const ast = parse('<div></div><comp></comp>', {
const ast = baseParse('<div></div><comp></comp>', {
isNativeTag: tag => tag === 'div',
isCustomElement: tag => tag === 'comp'
})
@@ -649,7 +715,7 @@ describe('compiler: parse', () => {
})
test('attribute with no value', () => {
const ast = parse('<div id></div>')
const ast = baseParse('<div id></div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
@@ -682,7 +748,7 @@ describe('compiler: parse', () => {
})
test('attribute with empty value, double quote', () => {
const ast = parse('<div id=""></div>')
const ast = baseParse('<div id=""></div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
@@ -723,7 +789,7 @@ describe('compiler: parse', () => {
})
test('attribute with empty value, single quote', () => {
const ast = parse("<div id=''></div>")
const ast = baseParse("<div id=''></div>")
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
@@ -764,7 +830,7 @@ describe('compiler: parse', () => {
})
test('attribute with value, double quote', () => {
const ast = parse('<div id=">\'"></div>')
const ast = baseParse('<div id=">\'"></div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
@@ -805,7 +871,7 @@ describe('compiler: parse', () => {
})
test('attribute with value, single quote', () => {
const ast = parse("<div id='>\"'></div>")
const ast = baseParse("<div id='>\"'></div>")
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
@@ -846,7 +912,7 @@ describe('compiler: parse', () => {
})
test('attribute with value, unquoted', () => {
const ast = parse('<div id=a/></div>')
const ast = baseParse('<div id=a/></div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
@@ -887,7 +953,7 @@ describe('compiler: parse', () => {
})
test('multiple attributes', () => {
const ast = parse('<div id=a class="c" inert style=\'\'></div>')
const ast = baseParse('<div id=a class="c" inert style=\'\'></div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
@@ -974,7 +1040,7 @@ describe('compiler: parse', () => {
})
test('directive with no value', () => {
const ast = parse('<div v-if/>')
const ast = baseParse('<div v-if/>')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
@@ -992,7 +1058,7 @@ describe('compiler: parse', () => {
})
test('directive with value', () => {
const ast = parse('<div v-if="a"/>')
const ast = baseParse('<div v-if="a"/>')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
@@ -1020,7 +1086,7 @@ describe('compiler: parse', () => {
})
test('directive with argument', () => {
const ast = parse('<div v-on:click/>')
const ast = baseParse('<div v-on:click/>')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
@@ -1057,7 +1123,7 @@ describe('compiler: parse', () => {
})
test('directive with a modifier', () => {
const ast = parse('<div v-on.enter/>')
const ast = baseParse('<div v-on.enter/>')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
@@ -1075,7 +1141,7 @@ describe('compiler: parse', () => {
})
test('directive with two modifiers', () => {
const ast = parse('<div v-on.enter.exact/>')
const ast = baseParse('<div v-on.enter.exact/>')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
@@ -1093,7 +1159,7 @@ describe('compiler: parse', () => {
})
test('directive with argument and modifiers', () => {
const ast = parse('<div v-on:click.enter.exact/>')
const ast = baseParse('<div v-on:click.enter.exact/>')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
@@ -1130,7 +1196,7 @@ describe('compiler: parse', () => {
})
test('v-bind shorthand', () => {
const ast = parse('<div :a=b />')
const ast = baseParse('<div :a=b />')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
@@ -1178,7 +1244,7 @@ describe('compiler: parse', () => {
})
test('v-bind shorthand with modifier', () => {
const ast = parse('<div :a.sync=b />')
const ast = baseParse('<div :a.sync=b />')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
@@ -1226,7 +1292,7 @@ describe('compiler: parse', () => {
})
test('v-on shorthand', () => {
const ast = parse('<div @a=b />')
const ast = baseParse('<div @a=b />')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
@@ -1274,7 +1340,7 @@ describe('compiler: parse', () => {
})
test('v-on shorthand with modifier', () => {
const ast = parse('<div @a.enter=b />')
const ast = baseParse('<div @a.enter=b />')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
@@ -1322,7 +1388,7 @@ describe('compiler: parse', () => {
})
test('v-slot shorthand', () => {
const ast = parse('<Comp #a="{ b }" />')
const ast = baseParse('<Comp #a="{ b }" />')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
@@ -1369,7 +1435,7 @@ describe('compiler: parse', () => {
})
test('v-pre', () => {
const ast = parse(
const ast = baseParse(
`<div v-pre :id="foo"><Comp/>{{ bar }}</div>\n` +
`<div :id="foo"><Comp/>{{ bar }}</div>`
)
@@ -1451,7 +1517,7 @@ describe('compiler: parse', () => {
})
test('end tags are case-insensitive.', () => {
const ast = parse('<div>hello</DIV>after')
const ast = baseParse('<div>hello</DIV>after')
const element = ast.children[0] as ElementNode
const text = element.children[0] as TextNode
@@ -1468,14 +1534,14 @@ describe('compiler: parse', () => {
})
test('self closing single tag', () => {
const ast = parse('<div :class="{ some: condition }" />')
const ast = baseParse('<div :class="{ some: condition }" />')
expect(ast.children).toHaveLength(1)
expect(ast.children[0]).toMatchObject({ tag: 'div' })
})
test('self closing multiple tag', () => {
const ast = parse(
const ast = baseParse(
`<div :class="{ some: condition }" />\n` +
`<p v-bind:style="{ color: 'red' }"/>`
)
@@ -1488,7 +1554,7 @@ describe('compiler: parse', () => {
})
test('valid html', () => {
const ast = parse(
const ast = baseParse(
`<div :class="{ some: condition }">\n` +
` <p v-bind:style="{ color: 'red' }"/>\n` +
` <!-- a comment with <html> inside it -->\n` +
@@ -1513,11 +1579,11 @@ describe('compiler: parse', () => {
test('invalid html', () => {
expect(() => {
parse(`<div>\n<span>\n</div>\n</span>`)
}).toThrow('End tag was not found. (3:1)')
baseParse(`<div>\n<span>\n</div>\n</span>`)
}).toThrow('Element is missing end tag.')
const spy = jest.fn()
const ast = parse(`<div>\n<span>\n</div>\n</span>`, {
const ast = baseParse(`<div>\n<span>\n</div>\n</span>`, {
onError: spy
})
@@ -1527,8 +1593,8 @@ describe('compiler: parse', () => {
code: ErrorCodes.X_MISSING_END_TAG,
loc: {
start: {
offset: 13,
line: 3,
offset: 6,
line: 2,
column: 1
}
}
@@ -1552,7 +1618,7 @@ describe('compiler: parse', () => {
})
test('parse with correct location info', () => {
const [foo, bar, but, baz] = parse(
const [foo, bar, but, baz] = baseParse(
`
foo
is {{ bar }} but {{ baz }}`.trim()
@@ -1588,7 +1654,7 @@ foo
describe('namedCharacterReferences option', () => {
test('use the given map', () => {
const ast: any = parse('&amp;&cups;', {
const ast: any = baseParse('&amp;&cups;', {
namedCharacterReferences: {
'cups;': '\u222A\uFE00' // UNION with serifs
},
@@ -1603,18 +1669,18 @@ foo
describe('whitespace management', () => {
it('should remove whitespaces at start/end inside an element', () => {
const ast = parse(`<div> <span/> </div>`)
const ast = baseParse(`<div> <span/> </div>`)
expect((ast.children[0] as ElementNode).children.length).toBe(1)
})
it('should remove whitespaces w/ newline between elements', () => {
const ast = parse(`<div/> \n <div/> \n <div/>`)
const ast = baseParse(`<div/> \n <div/> \n <div/>`)
expect(ast.children.length).toBe(3)
expect(ast.children.every(c => c.type === NodeTypes.ELEMENT)).toBe(true)
})
it('should remove whitespaces adjacent to comments', () => {
const ast = parse(`<div/> \n <!--foo--> <div/>`)
const ast = baseParse(`<div/> \n <!--foo--> <div/>`)
expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
@@ -1622,7 +1688,7 @@ foo
})
it('should remove whitespaces w/ newline between comments and elements', () => {
const ast = parse(`<div/> \n <!--foo--> \n <div/>`)
const ast = baseParse(`<div/> \n <!--foo--> \n <div/>`)
expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
@@ -1630,7 +1696,7 @@ foo
})
it('should NOT remove whitespaces w/ newline between interpolations', () => {
const ast = parse(`{{ foo }} \n {{ bar }}`)
const ast = baseParse(`{{ foo }} \n {{ bar }}`)
expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION)
expect(ast.children[1]).toMatchObject({
@@ -1641,7 +1707,7 @@ foo
})
it('should NOT remove whitespaces w/o newline between elements', () => {
const ast = parse(`<div/> <div/> <div/>`)
const ast = baseParse(`<div/> <div/> <div/>`)
expect(ast.children.length).toBe(5)
expect(ast.children.map(c => c.type)).toMatchObject([
NodeTypes.ELEMENT,
@@ -1653,7 +1719,7 @@ foo
})
it('should condense consecutive whitespaces in text', () => {
const ast = parse(` foo \n bar baz `)
const ast = baseParse(` foo \n bar baz `)
expect((ast.children[0] as TextNode).content).toBe(` foo bar baz `)
})
})
@@ -1833,7 +1899,7 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 11, line: 1, column: 12 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -1846,7 +1912,7 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 12, line: 1, column: 13 }
loc: { offset: 0, line: 1, column: 1 }
}
]
}
@@ -1861,11 +1927,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 29, line: 1, column: 30 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 29, line: 1, column: 30 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -1878,11 +1944,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 24, line: 1, column: 25 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 24, line: 1, column: 25 }
loc: { offset: 0, line: 1, column: 1 }
}
]
}
@@ -1897,7 +1963,7 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 21, line: 1, column: 22 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -1910,7 +1976,7 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 14, line: 1, column: 15 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -1925,7 +1991,7 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 12, line: 1, column: 13 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -1938,7 +2004,7 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 13, line: 1, column: 14 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -1951,7 +2017,7 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 15, line: 1, column: 16 }
loc: { offset: 0, line: 1, column: 1 }
}
]
}
@@ -1962,7 +2028,7 @@ foo
errors: [
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 32, line: 1, column: 33 }
loc: { offset: 0, line: 1, column: 1 }
},
{
type: ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT,
@@ -1975,7 +2041,7 @@ foo
errors: [
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 28, line: 1, column: 29 }
loc: { offset: 0, line: 1, column: 1 }
}
]
}
@@ -1990,11 +2056,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 14, line: 1, column: 15 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 14, line: 1, column: 15 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2007,11 +2073,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 15, line: 1, column: 16 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 15, line: 1, column: 16 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2024,11 +2090,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 17, line: 1, column: 18 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 17, line: 1, column: 18 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2041,11 +2107,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 18, line: 1, column: 19 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 18, line: 1, column: 19 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2062,11 +2128,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 19, line: 1, column: 20 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 19, line: 1, column: 20 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2079,11 +2145,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 22, line: 1, column: 23 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 22, line: 1, column: 23 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2096,11 +2162,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 22, line: 1, column: 23 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 22, line: 1, column: 23 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2113,11 +2179,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 23, line: 1, column: 24 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 23, line: 1, column: 24 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2130,11 +2196,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 23, line: 1, column: 24 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 23, line: 1, column: 24 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2147,11 +2213,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 21, line: 1, column: 22 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 21, line: 1, column: 22 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2168,11 +2234,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 24, line: 1, column: 25 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 24, line: 1, column: 25 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2189,11 +2255,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 24, line: 1, column: 25 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 24, line: 1, column: 25 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2210,11 +2276,11 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 23, line: 1, column: 24 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 23, line: 1, column: 24 }
loc: { offset: 0, line: 1, column: 1 }
}
]
}
@@ -2292,7 +2358,7 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 27, line: 1, column: 28 }
loc: { offset: 0, line: 1, column: 1 }
}
]
},
@@ -2429,7 +2495,7 @@ foo
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 19, line: 1, column: 20 }
loc: { offset: 0, line: 1, column: 1 }
}
]
}
@@ -2594,17 +2660,6 @@ foo
]
}
],
UNKNOWN_NAMED_CHARACTER_REFERENCE: [
{
code: '<template>&unknown;</template>',
errors: [
{
type: ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE,
loc: { offset: 10, line: 1, column: 11 }
}
]
}
],
X_INVALID_END_TAG: [
{
code: '<template></div></template>',
@@ -2651,7 +2706,7 @@ foo
errors: [
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 15, line: 1, column: 16 }
loc: { offset: 10, line: 1, column: 11 }
}
]
},
@@ -2660,11 +2715,11 @@ foo
errors: [
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 15, line: 1, column: 16 }
loc: { offset: 10, line: 1, column: 11 }
},
{
type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 15, line: 1, column: 16 }
loc: { offset: 0, line: 1, column: 1 }
}
]
}
@@ -2716,7 +2771,7 @@ foo
),
() => {
const spy = jest.fn()
const ast = parse(code, {
const ast = baseParse(code, {
getNamespace: (tag, parent) => {
const ns = parent ? parent.ns : Namespaces.HTML
if (ns === Namespaces.HTML) {

View File

@@ -0,0 +1,97 @@
import { baseCompile } from '../src/compile'
import {
WITH_SCOPE_ID,
PUSH_SCOPE_ID,
POP_SCOPE_ID
} from '../src/runtimeHelpers'
import { PatchFlags } from '@vue/shared'
import { genFlagText } from './testUtils'
describe('scopeId compiler support', () => {
test('should only work in module mode', () => {
expect(() => {
baseCompile(``, { scopeId: 'test' })
}).toThrow(`"scopeId" option is only supported in module mode`)
})
test('should wrap render function', () => {
const { ast, code } = baseCompile(`<div/>`, {
mode: 'module',
scopeId: 'test'
})
expect(ast.helpers).toContain(WITH_SCOPE_ID)
expect(code).toMatch(`const _withId = _withScopeId("test")`)
expect(code).toMatch(`export const render = _withId(function render(`)
expect(code).toMatchSnapshot()
})
test('should wrap default slot', () => {
const { code } = baseCompile(`<Child><div/></Child>`, {
mode: 'module',
scopeId: 'test'
})
expect(code).toMatch(`default: _withId(() => [`)
expect(code).toMatchSnapshot()
})
test('should wrap named slots', () => {
const { code } = baseCompile(
`<Child>
<template #foo="{ msg }">{{ msg }}</template>
<template #bar><div/></template>
</Child>
`,
{
mode: 'module',
scopeId: 'test'
}
)
expect(code).toMatch(`foo: _withId(({ msg }) => [`)
expect(code).toMatch(`bar: _withId(() => [`)
expect(code).toMatchSnapshot()
})
test('should wrap dynamic slots', () => {
const { code } = baseCompile(
`<Child>
<template #foo v-if="ok"><div/></template>
<template v-for="i in list" #[i]><div/></template>
</Child>
`,
{
mode: 'module',
scopeId: 'test'
}
)
expect(code).toMatch(/name: "foo",\s+fn: _withId\(/)
expect(code).toMatch(/name: i,\s+fn: _withId\(/)
expect(code).toMatchSnapshot()
})
test('should push scopeId for hoisted nodes', () => {
const { ast, code } = baseCompile(
`<div><div>hello</div>{{ foo }}<div>world</div></div>`,
{
mode: 'module',
scopeId: 'test',
hoistStatic: true
}
)
expect(ast.helpers).toContain(PUSH_SCOPE_ID)
expect(ast.helpers).toContain(POP_SCOPE_ID)
expect(ast.hoists.length).toBe(2)
expect(code).toMatch(
[
`_pushScopeId("test")`,
`const _hoisted_1 = _createVNode("div", null, "hello", ${genFlagText(
PatchFlags.HOISTED
)})`,
`const _hoisted_2 = _createVNode("div", null, "world", ${genFlagText(
PatchFlags.HOISTED
)})`,
`_popScopeId()`
].join('\n')
)
expect(code).toMatchSnapshot()
})
})

View File

@@ -4,9 +4,8 @@ import {
locStub,
Namespaces,
ElementTypes,
PlainElementCodegenNode
VNodeCall
} from '../src'
import { CREATE_VNODE } from '../src/runtimeHelpers'
import { isString, PatchFlags, PatchFlagNames, isArray } from '@vue/shared'
const leadingBracketRE = /^\[/
@@ -39,7 +38,11 @@ export function createObjectMatcher(obj: Record<string, any>) {
}
export function createElementWithCodegen(
args: PlainElementCodegenNode['arguments']
tag: VNodeCall['tag'],
props?: VNodeCall['props'],
children?: VNodeCall['children'],
patchFlag?: VNodeCall['patchFlag'],
dynamicProps?: VNodeCall['dynamicProps']
): ElementNode {
return {
type: NodeTypes.ELEMENT,
@@ -51,10 +54,16 @@ export function createElementWithCodegen(
props: [],
children: [],
codegenNode: {
type: NodeTypes.JS_CALL_EXPRESSION,
loc: locStub,
callee: CREATE_VNODE,
arguments: args
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
patchFlag,
dynamicProps,
directives: undefined,
isBlock: false,
isForBlock: false,
loc: locStub
}
}
}

View File

@@ -1,19 +1,17 @@
import { parse } from '../src/parse'
import { baseParse } from '../src/parse'
import { transform, NodeTransform } from '../src/transform'
import {
ElementNode,
NodeTypes,
DirectiveNode,
ExpressionNode
ExpressionNode,
VNodeCall
} from '../src/ast'
import { ErrorCodes, createCompilerError } from '../src/errors'
import {
TO_STRING,
OPEN_BLOCK,
CREATE_BLOCK,
TO_DISPLAY_STRING,
FRAGMENT,
RENDER_SLOT,
WITH_DIRECTIVES,
CREATE_COMMENT
} from '../src/runtimeHelpers'
import { transformIf } from '../src/transforms/vIf'
@@ -26,7 +24,7 @@ import { PatchFlags } from '@vue/shared'
describe('compiler: transform', () => {
test('context state', () => {
const ast = parse(`<div>hello {{ world }}</div>`)
const ast = baseParse(`<div>hello {{ world }}</div>`)
// manually store call arguments because context is mutable and shared
// across calls
@@ -72,7 +70,7 @@ describe('compiler: transform', () => {
})
test('context.replaceNode', () => {
const ast = parse(`<div/><span/>`)
const ast = baseParse(`<div/><span/>`)
const plugin: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
// change the node to <p>
@@ -106,7 +104,7 @@ describe('compiler: transform', () => {
})
test('context.removeNode', () => {
const ast = parse(`<span/><div>hello</div><span/>`)
const ast = baseParse(`<span/><div>hello</div><span/>`)
const c1 = ast.children[0]
const c2 = ast.children[2]
@@ -132,7 +130,7 @@ describe('compiler: transform', () => {
})
test('context.removeNode (prev sibling)', () => {
const ast = parse(`<span/><div/><span/>`)
const ast = baseParse(`<span/><div/><span/>`)
const c1 = ast.children[0]
const c2 = ast.children[2]
@@ -159,7 +157,7 @@ describe('compiler: transform', () => {
})
test('context.removeNode (next sibling)', () => {
const ast = parse(`<span/><div/><span/>`)
const ast = baseParse(`<span/><div/><span/>`)
const c1 = ast.children[0]
const d1 = ast.children[1]
@@ -186,7 +184,7 @@ describe('compiler: transform', () => {
})
test('context.hoist', () => {
const ast = parse(`<div :id="foo"/><div :id="bar"/>`)
const ast = baseParse(`<div :id="foo"/><div :id="bar"/>`)
const hoisted: ExpressionNode[] = []
const mock: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
@@ -204,7 +202,7 @@ describe('compiler: transform', () => {
})
test('onError option', () => {
const ast = parse(`<div/>`)
const ast = baseParse(`<div/>`)
const loc = ast.children[0].loc
const plugin: NodeTransform = (node, context) => {
context.onError(
@@ -225,20 +223,20 @@ describe('compiler: transform', () => {
})
test('should inject toString helper for interpolations', () => {
const ast = parse(`{{ foo }}`)
const ast = baseParse(`{{ foo }}`)
transform(ast, {})
expect(ast.helpers).toContain(TO_STRING)
expect(ast.helpers).toContain(TO_DISPLAY_STRING)
})
test('should inject createVNode and Comment for comments', () => {
const ast = parse(`<!--foo-->`)
const ast = baseParse(`<!--foo-->`)
transform(ast, {})
expect(ast.helpers).toContain(CREATE_COMMENT)
})
describe('root codegenNode', () => {
function transformWithCodegen(template: string) {
const ast = parse(template)
const ast = baseParse(template)
transform(ast, {
nodeTransforms: [
transformIf,
@@ -251,20 +249,19 @@ describe('compiler: transform', () => {
return ast
}
function createBlockMatcher(args: any[]) {
function createBlockMatcher(
tag: VNodeCall['tag'],
props?: VNodeCall['props'],
children?: VNodeCall['children'],
patchFlag?: VNodeCall['patchFlag']
) {
return {
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK
},
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK,
arguments: args
}
]
type: NodeTypes.VNODE_CALL,
isBlock: true,
tag,
props,
children,
patchFlag
}
}
@@ -285,7 +282,7 @@ describe('compiler: transform', () => {
test('single element', () => {
const ast = transformWithCodegen(`<div/>`)
expect(ast.codegenNode).toMatchObject(createBlockMatcher([`"div"`]))
expect(ast.codegenNode).toMatchObject(createBlockMatcher(`"div"`))
})
test('root v-if', () => {
@@ -305,22 +302,8 @@ describe('compiler: transform', () => {
test('root element with custom directive', () => {
const ast = transformWithCodegen(`<div v-foo/>`)
expect(ast.codegenNode).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK
},
{
type: NodeTypes.JS_CALL_EXPRESSION,
// should wrap withDirectives() around createBlock()
callee: WITH_DIRECTIVES,
arguments: [
{ callee: CREATE_BLOCK },
{ type: NodeTypes.JS_ARRAY_EXPRESSION }
]
}
]
type: NodeTypes.VNODE_CALL,
directives: { type: NodeTypes.JS_ARRAY_EXPRESSION }
})
})
@@ -348,15 +331,15 @@ describe('compiler: transform', () => {
test('multiple children', () => {
const ast = transformWithCodegen(`<div/><div/>`)
expect(ast.codegenNode).toMatchObject(
createBlockMatcher([
createBlockMatcher(
FRAGMENT,
`null`,
undefined,
[
{ type: NodeTypes.ELEMENT, tag: `div` },
{ type: NodeTypes.ELEMENT, tag: `div` }
],
] as any,
genFlagText(PatchFlags.STABLE_FRAGMENT)
])
)
)
})
})

View File

@@ -2,14 +2,14 @@
exports[`compiler: hoistStatic transform hoist element with static key 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode } = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode(\\"div\\", { key: \\"foo\\" })
const _hoisted_1 = _createVNode(\\"div\\", { key: \\"foo\\" }, null, -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1
]))
@@ -19,17 +19,17 @@ return function render() {
exports[`compiler: hoistStatic transform hoist nested static tree 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode } = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode(\\"p\\", null, [
_createVNode(\\"span\\"),
_createVNode(\\"span\\")
])
], -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1
]))
@@ -39,16 +39,16 @@ return function render() {
exports[`compiler: hoistStatic transform hoist nested static tree with comments 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = Vue
const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _Vue
const _hoisted_1 = _createVNode(\\"div\\", null, [
_createCommentVNode(\\"comment\\")
])
], -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { createCommentVNode: _createCommentVNode, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return function render() {
with (this) {
const { createCommentVNode: _createCommentVNode, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1
]))
@@ -58,15 +58,15 @@ return function render() {
exports[`compiler: hoistStatic transform hoist siblings with common non-hoistable parent 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode } = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode(\\"span\\")
const _hoisted_2 = _createVNode(\\"div\\")
const _hoisted_1 = _createVNode(\\"span\\", null, null, -1 /* HOISTED */)
const _hoisted_2 = _createVNode(\\"div\\", null, null, -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1,
_hoisted_2
@@ -77,14 +77,14 @@ return function render() {
exports[`compiler: hoistStatic transform hoist simple element 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode } = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode(\\"span\\", { class: \\"inline\\" }, \\"hello\\")
const _hoisted_1 = _createVNode(\\"span\\", { class: \\"inline\\" }, \\"hello\\", -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1
]))
@@ -94,18 +94,18 @@ return function render() {
exports[`compiler: hoistStatic transform hoist static props for elements with directives 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode } = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = { id: \\"foo\\" }
return function render() {
with (this) {
const { createVNode: _createVNode, withDirectives: _withDirectives, resolveDirective: _resolveDirective, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { resolveDirective: _resolveDirective, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _directive_foo = _resolveDirective(\\"foo\\")
return (_openBlock(), _createBlock(\\"div\\", null, [
_withDirectives(_createVNode(\\"div\\", _hoisted_1, null, 32 /* NEED_PATCH */), [
_withDirectives(_createVNode(\\"div\\", _hoisted_1, null, 512 /* NEED_PATCH */), [
[_directive_foo]
])
]))
@@ -115,16 +115,16 @@ return function render() {
exports[`compiler: hoistStatic transform hoist static props for elements with dynamic text children 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode } = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = { id: \\"foo\\" }
return function render() {
with (this) {
const { toString: _toString, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"div\\", _hoisted_1, _toString(hello), 1 /* TEXT */)
_createVNode(\\"div\\", _hoisted_1, _toDisplayString(hello), 1 /* TEXT */)
]))
}
}"
@@ -132,16 +132,16 @@ return function render() {
exports[`compiler: hoistStatic transform hoist static props for elements with unhoistable children 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode } = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = { id: \\"foo\\" }
return function render() {
with (this) {
const { resolveComponent: _resolveComponent, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { resolveComponent: _resolveComponent, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"div\\", _hoisted_1, [
_createVNode(_component_Comp)
@@ -153,16 +153,16 @@ return function render() {
exports[`compiler: hoistStatic transform prefixIdentifiers hoist class with static object value 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode } = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = { class: { foo: true } }
return function render() {
with (this) {
const { toString: _toString, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"span\\", _hoisted_1, _toString(_ctx.bar), 1 /* TEXT */)
_createVNode(\\"span\\", _hoisted_1, _toDisplayString(_ctx.bar), 1 /* TEXT */)
]))
}
}"
@@ -170,19 +170,14 @@ return function render() {
exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static tree with static interpolation 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode } = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode(\\"span\\", null, [
\\"foo \\",
_toString(1),
\\" \\",
_toString(true)
])
const _hoisted_1 = _createVNode(\\"span\\", null, \\"foo \\" + _toDisplayString(1) + \\" \\" + _toDisplayString(true), -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return function render() {
with (this) {
const { toString: _toString, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1
]))
@@ -192,14 +187,14 @@ return function render() {
exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static tree with static prop value 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode } = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode(\\"span\\", { foo: 0 }, _toString(1))
const _hoisted_1 = _createVNode(\\"span\\", { foo: 0 }, _toDisplayString(1), -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return function render() {
with (this) {
const { toString: _toString, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1
]))
@@ -208,14 +203,12 @@ return function render() {
`;
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist elements with cached handlers 1`] = `
"import { createVNode, createBlock, openBlock } from \\"vue\\"
"import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
export default function render() {
const _ctx = this
const _cache = _ctx.$cache
return (openBlock(), createBlock(\\"div\\", null, [
createVNode(\\"div\\", null, [
createVNode(\\"div\\", {
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"div\\", null, [
_createVNode(\\"div\\", {
onClick: _cache[1] || (_cache[1] = $event => (_ctx.foo($event)))
})
])
@@ -226,14 +219,14 @@ export default function render() {
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that refer scope variables (2) 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, toString: _toString, createVNode: _createVNode } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, toDisplayString: _toDisplayString, createVNode: _createVNode } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
(_openBlock(false), _createBlock(_Fragment, null, _renderList(_ctx.list, (o) => {
(_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.list, (o) => {
return (_openBlock(), _createBlock(\\"p\\", null, [
_createVNode(\\"span\\", null, _toString(o + 'foo'), 1 /* TEXT */)
_createVNode(\\"span\\", null, _toDisplayString(o + 'foo'), 1 /* TEXT */)
]))
}), 256 /* UNKEYED_FRAGMENT */))
]))
@@ -244,15 +237,17 @@ return function render() {
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that refer scope variables (v-slot) 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { toString: _toString, resolveComponent: _resolveComponent, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
default: ({ foo }) => [_toString(_ctx.foo)],
_compiled: true
default: _withCtx(({ foo }) => [
_createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */)
]),
_: 1
}))
}
}"
@@ -261,14 +256,14 @@ return function render() {
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that refer scope variables 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, toString: _toString, createVNode: _createVNode } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, toDisplayString: _toDisplayString, createVNode: _createVNode } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
(_openBlock(false), _createBlock(_Fragment, null, _renderList(_ctx.list, (o) => {
(_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.list, (o) => {
return (_openBlock(), _createBlock(\\"p\\", null, [
_createVNode(\\"span\\", null, _toString(o), 1 /* TEXT */)
_createVNode(\\"span\\", null, _toDisplayString(o), 1 /* TEXT */)
]))
}), 256 /* UNKEYED_FRAGMENT */))
]))
@@ -279,12 +274,12 @@ return function render() {
exports[`compiler: hoistStatic transform should NOT hoist components 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { resolveComponent: _resolveComponent, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { resolveComponent: _resolveComponent, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(_component_Comp)
]))
@@ -295,12 +290,12 @@ return function render() {
exports[`compiler: hoistStatic transform should NOT hoist element with dynamic key 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"div\\", { key: foo })
(_openBlock(), _createBlock(\\"div\\", { key: foo }))
]))
}
}"
@@ -309,10 +304,10 @@ return function render() {
exports[`compiler: hoistStatic transform should NOT hoist element with dynamic props 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"])
]))
@@ -323,12 +318,12 @@ return function render() {
exports[`compiler: hoistStatic transform should NOT hoist element with dynamic ref 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"div\\", { ref: foo }, null, 32 /* NEED_PATCH */)
_createVNode(\\"div\\", { ref: foo }, null, 512 /* NEED_PATCH */)
]))
}
}"
@@ -337,10 +332,10 @@ return function render() {
exports[`compiler: hoistStatic transform should NOT hoist root node 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\"))
}
}"
@@ -348,17 +343,17 @@ return function render() {
exports[`compiler: hoistStatic transform should hoist v-for children if static 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode } = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = { id: \\"foo\\" }
const _hoisted_2 = _createVNode(\\"span\\")
const _hoisted_2 = _createVNode(\\"span\\", null, null, -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
(_openBlock(false), _createBlock(_Fragment, null, _renderList(list, (i) => {
(_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createBlock(\\"div\\", _hoisted_1, [
_hoisted_2
]))
@@ -370,24 +365,24 @@ return function render() {
exports[`compiler: hoistStatic transform should hoist v-if props/children if static 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = Vue
const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _Vue
const _hoisted_1 = {
key: 0,
id: \\"foo\\"
}
const _hoisted_2 = _createVNode(\\"span\\")
const _hoisted_2 = _createVNode(\\"span\\", null, null, -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return function render() {
with (this) {
const { openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
(_openBlock(), ok
? _createBlock(\\"div\\", _hoisted_1, [
ok
? (_openBlock(), _createBlock(\\"div\\", _hoisted_1, [
_hoisted_2
])
: _createCommentVNode(\\"v-if\\", true))
]))
: _createCommentVNode(\\"v-if\\", true)
]))
}
}"

View File

@@ -3,11 +3,11 @@
exports[`compiler: transform text <template v-for> 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createTextVNode: _createTextVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(list, (i) => {
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createTextVNode: _createTextVNode } = _Vue
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createBlock(_Fragment, null, [
_createTextVNode(\\"foo\\")
], 64 /* STABLE_FRAGMENT */))
@@ -19,11 +19,11 @@ return function render() {
exports[`compiler: transform text consecutive text 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { toString: _toString } = _Vue
return _toString(foo) + \\" bar \\" + _toString(baz)
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString } = _Vue
return _toDisplayString(foo) + \\" bar \\" + _toDisplayString(baz)
}
}"
`;
@@ -31,13 +31,13 @@ return function render() {
exports[`compiler: transform text consecutive text between elements 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, toString: _toString, createTextVNode: _createTextVNode, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\"),
_createTextVNode(_toString(foo) + \\" bar \\" + _toString(baz), 1 /* TEXT */),
_createTextVNode(_toDisplayString(foo) + \\" bar \\" + _toDisplayString(baz), 1 /* TEXT */),
_createVNode(\\"div\\")
], 64 /* STABLE_FRAGMENT */))
}
@@ -47,13 +47,13 @@ return function render() {
exports[`compiler: transform text consecutive text mixed with elements 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, toString: _toString, createTextVNode: _createTextVNode, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\"),
_createTextVNode(_toString(foo) + \\" bar \\" + _toString(baz), 1 /* TEXT */),
_createTextVNode(_toDisplayString(foo) + \\" bar \\" + _toDisplayString(baz), 1 /* TEXT */),
_createVNode(\\"div\\"),
_createTextVNode(\\"hello\\"),
_createVNode(\\"div\\")
@@ -65,11 +65,11 @@ return function render() {
exports[`compiler: transform text no consecutive text 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { toString: _toString } = _Vue
return _toString(foo)
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString } = _Vue
return _toDisplayString(foo)
}
}"
`;
@@ -77,10 +77,10 @@ return function render() {
exports[`compiler: transform text text between elements (static) 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createTextVNode: _createTextVNode, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\"),
_createTextVNode(\\"hello\\"),
@@ -91,10 +91,9 @@ return function render() {
`;
exports[`compiler: transform text with prefixIdentifiers: true 1`] = `
"const { toString } = Vue
"const { toDisplayString: _toDisplayString } = Vue
return function render() {
const _ctx = this
return toString(_ctx.foo) + \\" bar \\" + toString(_ctx.baz + _ctx.qux)
return function render(_ctx, _cache) {
return _toDisplayString(_ctx.foo) + \\" bar \\" + _toDisplayString(_ctx.baz + _ctx.qux)
}"
`;

View File

@@ -3,11 +3,11 @@
exports[`compiler: v-for codegen basic v-for 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item) => {
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createBlock(\\"span\\"))
}), 256 /* UNKEYED_FRAGMENT */))
}
@@ -17,11 +17,11 @@ return function render() {
exports[`compiler: v-for codegen keyed template v-for 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item) => {
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createBlock(_Fragment, { key: item }, [
\\"hello\\",
_createVNode(\\"span\\")
@@ -34,11 +34,11 @@ return function render() {
exports[`compiler: v-for codegen keyed v-for 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item) => {
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createBlock(\\"span\\", { key: item }))
}), 128 /* KEYED_FRAGMENT */))
}
@@ -48,11 +48,11 @@ return function render() {
exports[`compiler: v-for codegen skipped key 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item, __, index) => {
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item, __, index) => {
return (_openBlock(), _createBlock(\\"span\\"))
}), 256 /* UNKEYED_FRAGMENT */))
}
@@ -62,11 +62,11 @@ return function render() {
exports[`compiler: v-for codegen skipped value & key 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (_, __, index) => {
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (_, __, index) => {
return (_openBlock(), _createBlock(\\"span\\"))
}), 256 /* UNKEYED_FRAGMENT */))
}
@@ -76,11 +76,11 @@ return function render() {
exports[`compiler: v-for codegen skipped value 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (_, key, index) => {
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (_, key, index) => {
return (_openBlock(), _createBlock(\\"span\\"))
}), 256 /* UNKEYED_FRAGMENT */))
}
@@ -90,11 +90,11 @@ return function render() {
exports[`compiler: v-for codegen template v-for 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item) => {
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createBlock(_Fragment, null, [
\\"hello\\",
_createVNode(\\"span\\")
@@ -107,11 +107,11 @@ return function render() {
exports[`compiler: v-for codegen template v-for w/ <slot/> 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, renderSlot: _renderSlot } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item) => {
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, renderSlot: _renderSlot } = _Vue
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
return _renderSlot($slots, \\"default\\")
}), 256 /* UNKEYED_FRAGMENT */))
}
@@ -121,11 +121,11 @@ return function render() {
exports[`compiler: v-for codegen v-for on <slot/> 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, renderSlot: _renderSlot } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item) => {
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, renderSlot: _renderSlot } = _Vue
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
return _renderSlot($slots, \\"default\\")
}), 256 /* UNKEYED_FRAGMENT */))
}
@@ -135,16 +135,16 @@ return function render() {
exports[`compiler: v-for codegen v-for on element with custom directive 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode, withDirectives: _withDirectives, resolveDirective: _resolveDirective } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, resolveDirective: _resolveDirective, createVNode: _createVNode, withDirectives: _withDirectives } = _Vue
const _directive_foo = _resolveDirective(\\"foo\\")
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _withDirectives(_createBlock(\\"div\\", null, null, 32 /* NEED_PATCH */), [
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (i) => {
return _withDirectives((_openBlock(), _createBlock(\\"div\\", null, null, 512 /* NEED_PATCH */)), [
[_directive_foo]
]))
])
}), 256 /* UNKEYED_FRAGMENT */))
}
}"
@@ -153,15 +153,15 @@ return function render() {
exports[`compiler: v-for codegen v-if + v-for 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { openBlock: _openBlock, renderList: _renderList, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok
? _createBlock(_Fragment, { key: 0 }, _renderList(list, (i) => {
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _Vue
return ok
? (_openBlock(true), _createBlock(_Fragment, { key: 0 }, _renderList(list, (i) => {
return (_openBlock(), _createBlock(\\"div\\"))
}), 256 /* UNKEYED_FRAGMENT */)
: _createCommentVNode(\\"v-if\\", true))
}), 256 /* UNKEYED_FRAGMENT */))
: _createCommentVNode(\\"v-if\\", true)
}
}"
`;
@@ -169,11 +169,11 @@ return function render() {
exports[`compiler: v-for codegen value + key + index 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item, key, index) => {
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item, key, index) => {
return (_openBlock(), _createBlock(\\"span\\"))
}), 256 /* UNKEYED_FRAGMENT */))
}

View File

@@ -3,13 +3,13 @@
exports[`compiler: v-if codegen basic v-if 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok
? _createBlock(\\"div\\", { key: 0 })
: _createCommentVNode(\\"v-if\\", true))
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return ok
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
: _createCommentVNode(\\"v-if\\", true)
}
}"
`;
@@ -17,17 +17,17 @@ return function render() {
exports[`compiler: v-if codegen template v-if 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { openBlock: _openBlock, createVNode: _createVNode, Fragment: _Fragment, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok
? _createBlock(_Fragment, { key: 0 }, [
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return ok
? (_openBlock(), _createBlock(_Fragment, { key: 0 }, [
_createVNode(\\"div\\"),
\\"hello\\",
_createVNode(\\"p\\")
])
: _createCommentVNode(\\"v-if\\", true))
], 64 /* STABLE_FRAGMENT */))
: _createCommentVNode(\\"v-if\\", true)
}
}"
`;
@@ -35,13 +35,13 @@ return function render() {
exports[`compiler: v-if codegen template v-if w/ single <slot/> child 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { openBlock: _openBlock, renderSlot: _renderSlot, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok
return function render(_ctx, _cache) {
with (_ctx) {
const { renderSlot: _renderSlot, createCommentVNode: _createCommentVNode } = _Vue
return ok
? _renderSlot($slots, \\"default\\", { key: 0 })
: _createCommentVNode(\\"v-if\\", true))
: _createCommentVNode(\\"v-if\\", true)
}
}"
`;
@@ -49,13 +49,13 @@ return function render() {
exports[`compiler: v-if codegen v-if + v-else 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok
? _createBlock(\\"div\\", { key: 0 })
: _createBlock(\\"p\\", { key: 1 }))
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return ok
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
: (_openBlock(), _createBlock(\\"p\\", { key: 1 }))
}
}"
`;
@@ -63,15 +63,15 @@ return function render() {
exports[`compiler: v-if codegen v-if + v-else-if + v-else 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode, Fragment: _Fragment } = _Vue
return (_openBlock(), ok
? _createBlock(\\"div\\", { key: 0 })
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode, Fragment: _Fragment } = _Vue
return ok
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
: orNot
? _createBlock(\\"p\\", { key: 1 })
: _createBlock(_Fragment, { key: 2 }, [\\"fine\\"]))
? (_openBlock(), _createBlock(\\"p\\", { key: 1 }))
: (_openBlock(), _createBlock(_Fragment, { key: 2 }, [\\"fine\\"], 64 /* STABLE_FRAGMENT */))
}
}"
`;
@@ -79,15 +79,15 @@ return function render() {
exports[`compiler: v-if codegen v-if + v-else-if 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok
? _createBlock(\\"div\\", { key: 0 })
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return ok
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
: orNot
? _createBlock(\\"p\\", { key: 1 })
: _createCommentVNode(\\"v-if\\", true))
? (_openBlock(), _createBlock(\\"p\\", { key: 1 }))
: _createCommentVNode(\\"v-if\\", true)
}
}"
`;
@@ -95,13 +95,27 @@ return function render() {
exports[`compiler: v-if codegen v-if on <slot/> 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { openBlock: _openBlock, renderSlot: _renderSlot, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok
return function render(_ctx, _cache) {
with (_ctx) {
const { renderSlot: _renderSlot, createCommentVNode: _createCommentVNode } = _Vue
return ok
? _renderSlot($slots, \\"default\\", { key: 0 })
: _createCommentVNode(\\"v-if\\", true))
: _createCommentVNode(\\"v-if\\", true)
}
}"
`;
exports[`compiler: v-if codegen v-if with key 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return ok
? (_openBlock(), _createBlock(\\"div\\", { key: \\"some-key\\" }))
: _createCommentVNode(\\"v-if\\", true)
}
}"
`;

View File

@@ -1,11 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler: transform v-model compound expression (with prefixIdentifiers) 1`] = `
"import { createVNode, createBlock, openBlock } from \\"vue\\"
"import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
export default function render() {
const _ctx = this
return (openBlock(), createBlock(\\"input\\", {
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock(\\"input\\", {
modelValue: _ctx.model[_ctx.index],
\\"onUpdate:modelValue\\": $event => (_ctx.model[_ctx.index] = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
@@ -15,10 +14,10 @@ export default function render() {
exports[`compiler: transform v-model compound expression 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"input\\", {
modelValue: model[index],
\\"onUpdate:modelValue\\": $event => (model[index] = $event)
@@ -28,11 +27,10 @@ return function render() {
`;
exports[`compiler: transform v-model simple exprssion (with prefixIdentifiers) 1`] = `
"import { createVNode, createBlock, openBlock } from \\"vue\\"
"import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
export default function render() {
const _ctx = this
return (openBlock(), createBlock(\\"input\\", {
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock(\\"input\\", {
modelValue: _ctx.model,
\\"onUpdate:modelValue\\": $event => (_ctx.model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
@@ -42,10 +40,10 @@ export default function render() {
exports[`compiler: transform v-model simple exprssion 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"input\\", {
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event)
@@ -57,24 +55,23 @@ return function render() {
exports[`compiler: transform v-model with argument 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"input\\", {
value: model,
\\"onUpdate:value\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"value\\", \\"onUpdate:value\\"]))
}, null, 40 /* PROPS, HYDRATE_EVENTS */, [\\"value\\", \\"onUpdate:value\\"]))
}
}"
`;
exports[`compiler: transform v-model with dynamic argument (with prefixIdentifiers) 1`] = `
"import { createVNode, createBlock, openBlock } from \\"vue\\"
"import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
export default function render() {
const _ctx = this
return (openBlock(), createBlock(\\"input\\", {
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock(\\"input\\", {
[_ctx.value]: _ctx.model,
[\\"onUpdate:\\" + _ctx.value]: $event => (_ctx.model = $event)
}, null, 16 /* FULL_PROPS */))
@@ -84,10 +81,10 @@ export default function render() {
exports[`compiler: transform v-model with dynamic argument 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"input\\", {
[value]: model,
[\\"onUpdate:\\" + value]: $event => (model = $event)

View File

@@ -3,11 +3,10 @@
exports[`compiler: v-once transform as root node 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
return function render(_ctx, _cache) {
with (_ctx) {
const { setBlockTracking: _setBlockTracking, createVNode: _createVNode } = _Vue
const _cache = $cache
return _cache[1] || (
_setBlockTracking(-1),
_cache[1] = _createVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]),
@@ -21,13 +20,12 @@ return function render() {
exports[`compiler: v-once transform on component 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { setBlockTracking: _setBlockTracking, resolveComponent: _resolveComponent, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
const _cache = $cache
return function render(_ctx, _cache) {
with (_ctx) {
const { setBlockTracking: _setBlockTracking, resolveComponent: _resolveComponent, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(\\"div\\", null, [
_cache[1] || (
_setBlockTracking(-1),
@@ -43,11 +41,10 @@ return function render() {
exports[`compiler: v-once transform on nested plain element 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { setBlockTracking: _setBlockTracking, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
const _cache = $cache
return function render(_ctx, _cache) {
with (_ctx) {
const { setBlockTracking: _setBlockTracking, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_cache[1] || (
_setBlockTracking(-1),
@@ -63,11 +60,10 @@ return function render() {
exports[`compiler: v-once transform on slot outlet 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { setBlockTracking: _setBlockTracking, renderSlot: _renderSlot, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
const _cache = $cache
return function render(_ctx, _cache) {
with (_ctx) {
const { setBlockTracking: _setBlockTracking, renderSlot: _renderSlot, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_cache[1] || (
_setBlockTracking(-1),
@@ -83,11 +79,10 @@ return function render() {
exports[`compiler: v-once transform with hoistStatic: true 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { setBlockTracking: _setBlockTracking, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
const _cache = $cache
return function render(_ctx, _cache) {
with (_ctx) {
const { setBlockTracking: _setBlockTracking, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_cache[1] || (
_setBlockTracking(-1),

View File

@@ -1,111 +1,93 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler: transform component slots dynamically named slots 1`] = `
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, {
[_ctx.one]: ({ foo }) => [toString(foo), toString(_ctx.bar)],
[_ctx.two]: ({ bar }) => [toString(_ctx.foo), toString(bar)],
_compiled: true
}, 512 /* DYNAMIC_SLOTS */))
}"
`;
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
exports[`compiler: transform component slots explicit default slot 1`] = `
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, {
default: ({ foo }) => [toString(foo), toString(_ctx.bar)],
_compiled: true
}))
return (_openBlock(), _createBlock(_component_Comp, null, {
[_ctx.one]: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
[_ctx.two]: _withCtx(({ bar }) => [_toDisplayString(_ctx.foo), _toDisplayString(bar)]),
_: 1
}, 1024 /* DYNAMIC_SLOTS */))
}"
`;
exports[`compiler: transform component slots implicit default slot 1`] = `
"const { createVNode, resolveComponent, createBlock, openBlock } = Vue
"const { createVNode: _createVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, {
default: () => [
createVNode(\\"div\\")
],
_compiled: true
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
default: _withCtx(() => [
_createVNode(\\"div\\")
]),
_: 1
}))
}"
`;
exports[`compiler: transform component slots named slot with v-for w/ prefixIdentifiers: true 1`] = `
"const { toString, resolveComponent, renderList, createSlots, createVNode, createBlock, openBlock } = Vue
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, renderList: _renderList, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, createSlots({ _compiled: true }, [
renderList(_ctx.list, (name) => {
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 1 }, [
_renderList(_ctx.list, (name) => {
return {
name: name,
fn: () => [toString(name)]
fn: _withCtx(() => [_toDisplayString(name)])
}
})
]), 512 /* DYNAMIC_SLOTS */))
]), 1024 /* DYNAMIC_SLOTS */))
}"
`;
exports[`compiler: transform component slots named slot with v-if + prefixIdentifiers: true 1`] = `
"const { toString, resolveComponent, createSlots, createVNode, createBlock, openBlock } = Vue
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, createSlots({ _compiled: true }, [
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 1 }, [
(_ctx.ok)
? {
name: \\"one\\",
fn: (props) => [toString(props)]
fn: _withCtx((props) => [_toDisplayString(props)])
}
: undefined
]), 512 /* DYNAMIC_SLOTS */))
]), 1024 /* DYNAMIC_SLOTS */))
}"
`;
exports[`compiler: transform component slots named slot with v-if + v-else-if + v-else 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { resolveComponent: _resolveComponent, createSlots: _createSlots, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _compiled: true }, [
return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 1 }, [
ok
? {
name: \\"one\\",
fn: () => [\\"foo\\"]
fn: _withCtx(() => [\\"foo\\"])
}
: orNot
? {
name: \\"two\\",
fn: (props) => [\\"bar\\"]
fn: _withCtx((props) => [\\"bar\\"])
}
: {
name: \\"one\\",
fn: () => [\\"baz\\"]
fn: _withCtx(() => [\\"baz\\"])
}
]), 512 /* DYNAMIC_SLOTS */))
]), 1024 /* DYNAMIC_SLOTS */))
}
}"
`;
@@ -113,59 +95,117 @@ return function render() {
exports[`compiler: transform component slots named slot with v-if 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { resolveComponent: _resolveComponent, createSlots: _createSlots, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _compiled: true }, [
return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 1 }, [
ok
? {
name: \\"one\\",
fn: () => [\\"hello\\"]
fn: _withCtx(() => [\\"hello\\"])
}
: undefined
]), 512 /* DYNAMIC_SLOTS */))
]), 1024 /* DYNAMIC_SLOTS */))
}
}"
`;
exports[`compiler: transform component slots named slots 1`] = `
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
exports[`compiler: transform component slots named slots w/ implicit default slot 1`] = `
"const _Vue = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, {
one: ({ foo }) => [toString(foo), toString(_ctx.bar)],
two: ({ bar }) => [toString(_ctx.foo), toString(bar)],
_compiled: true
}))
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
one: _withCtx(() => [\\"foo\\"]),
default: _withCtx(() => [
\\"bar\\",
_createVNode(\\"span\\")
]),
_: 1
}))
}
}"
`;
exports[`compiler: transform component slots nested slots scoping 1`] = `
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render() {
const _ctx = this
const _component_Inner = resolveComponent(\\"Inner\\")
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, {
default: ({ foo }) => [
createVNode(_component_Inner, null, {
default: ({ bar }) => [toString(foo), toString(bar), toString(_ctx.baz)],
_compiled: true
}, 512 /* DYNAMIC_SLOTS */),
return function render(_ctx, _cache) {
const _component_Inner = _resolveComponent(\\"Inner\\")
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
default: _withCtx(({ foo }) => [
_createVNode(_component_Inner, null, {
default: _withCtx(({ bar }) => [_toDisplayString(foo), _toDisplayString(bar), _toDisplayString(_ctx.baz)]),
_: 1
}, 1024 /* DYNAMIC_SLOTS */),
\\" \\",
toString(foo),
toString(_ctx.bar),
toString(_ctx.baz)
],
_compiled: true
_toDisplayString(foo),
_toDisplayString(_ctx.bar),
_toDisplayString(_ctx.baz)
]),
_: 1
}))
}"
`;
exports[`compiler: transform component slots on component dynamically named slot 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
[_ctx.named]: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
_: 1
}))
}"
`;
exports[`compiler: transform component slots on component named slot 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
named: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
_: 1
}))
}"
`;
exports[`compiler: transform component slots on-component default slot 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
default: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
_: 1
}))
}"
`;
exports[`compiler: transform component slots template named slots 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
one: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
two: _withCtx(({ bar }) => [_toDisplayString(_ctx.foo), _toDisplayString(bar)]),
_: 1
}))
}"
`;

View File

@@ -1,18 +1,15 @@
import {
parse,
baseParse as parse,
transform,
NodeTypes,
generate,
CompilerOptions
CompilerOptions,
VNodeCall,
IfNode,
ElementNode,
ForNode
} from '../../src'
import {
OPEN_BLOCK,
CREATE_BLOCK,
CREATE_VNODE,
WITH_DIRECTIVES,
FRAGMENT,
RENDER_LIST
} from '../../src/runtimeHelpers'
import { FRAGMENT, RENDER_LIST, CREATE_TEXT } from '../../src/runtimeHelpers'
import { transformElement } from '../../src/transforms/transformElement'
import { transformExpression } from '../../src/transforms/transformExpression'
import { transformIf } from '../../src/transforms/vIf'
@@ -20,6 +17,7 @@ import { transformFor } from '../../src/transforms/vFor'
import { transformBind } from '../../src/transforms/vBind'
import { transformOn } from '../../src/transforms/vOn'
import { createObjectMatcher, genFlagText } from '../testUtils'
import { transformText } from '../../src/transforms/transformText'
import { PatchFlags } from '@vue/shared'
function transformWithHoist(template: string, options: CompilerOptions = {}) {
@@ -30,7 +28,8 @@ function transformWithHoist(template: string, options: CompilerOptions = {}) {
transformIf,
transformFor,
...(options.prefixIdentifiers ? [transformExpression] : []),
transformElement
transformElement,
transformText
],
directiveTransforms: {
on: transformOn,
@@ -39,56 +38,43 @@ function transformWithHoist(template: string, options: CompilerOptions = {}) {
...options
})
expect(ast.codegenNode).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK
},
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK
}
]
type: NodeTypes.VNODE_CALL,
isBlock: true
})
return {
root: ast,
args: (ast.codegenNode as any).expressions[1].arguments
}
return ast
}
describe('compiler: hoistStatic transform', () => {
test('should NOT hoist root node', () => {
// if the whole tree is static, the root still needs to be a block
// so that it's patched in optimized mode to skip children
const { root, args } = transformWithHoist(`<div/>`)
const root = transformWithHoist(`<div/>`)
expect(root.hoists.length).toBe(0)
expect(args).toEqual([`"div"`])
expect(root.codegenNode).toMatchObject({
tag: `"div"`
})
expect(generate(root).code).toMatchSnapshot()
})
test('hoist simple element', () => {
const { root, args } = transformWithHoist(
const root = transformWithHoist(
`<div><span class="inline">hello</span></div>`
)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE,
arguments: [
`"span"`,
createObjectMatcher({ class: 'inline' }),
{
type: NodeTypes.TEXT,
content: `hello`
}
]
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
props: createObjectMatcher({ class: 'inline' }),
children: {
type: NodeTypes.TEXT,
content: `hello`
}
}
])
expect(args).toMatchObject([
`"div"`,
`null`,
[
expect(root.codegenNode).toMatchObject({
tag: `"div"`,
props: undefined,
children: [
{
type: NodeTypes.ELEMENT,
codegenNode: {
@@ -97,29 +83,24 @@ describe('compiler: hoistStatic transform', () => {
}
}
]
])
})
expect(generate(root).code).toMatchSnapshot()
})
test('hoist nested static tree', () => {
const { root, args } = transformWithHoist(
`<div><p><span/><span/></p></div>`
)
const root = transformWithHoist(`<div><p><span/><span/></p></div>`)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE,
arguments: [
`"p"`,
`null`,
[
{ type: NodeTypes.ELEMENT, tag: `span` },
{ type: NodeTypes.ELEMENT, tag: `span` }
]
type: NodeTypes.VNODE_CALL,
tag: `"p"`,
props: undefined,
children: [
{ type: NodeTypes.ELEMENT, tag: `span` },
{ type: NodeTypes.ELEMENT, tag: `span` }
]
}
])
expect(args[2]).toMatchObject([
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
codegenNode: {
@@ -132,21 +113,16 @@ describe('compiler: hoistStatic transform', () => {
})
test('hoist nested static tree with comments', () => {
const { root, args } = transformWithHoist(
`<div><div><!--comment--></div></div>`
)
const root = transformWithHoist(`<div><div><!--comment--></div></div>`)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE,
arguments: [
`"div"`,
`null`,
[{ type: NodeTypes.COMMENT, content: `comment` }]
]
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: undefined,
children: [{ type: NodeTypes.COMMENT, content: `comment` }]
}
])
expect(args[2]).toMatchObject([
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
codegenNode: {
@@ -159,20 +135,18 @@ describe('compiler: hoistStatic transform', () => {
})
test('hoist siblings with common non-hoistable parent', () => {
const { root, args } = transformWithHoist(`<div><span/><div/></div>`)
const root = transformWithHoist(`<div><span/><div/></div>`)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE,
arguments: [`"span"`]
type: NodeTypes.VNODE_CALL,
tag: `"span"`
},
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE,
arguments: [`"div"`]
type: NodeTypes.VNODE_CALL,
tag: `"div"`
}
])
expect(args[2]).toMatchObject([
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
codegenNode: {
@@ -192,14 +166,14 @@ describe('compiler: hoistStatic transform', () => {
})
test('should NOT hoist components', () => {
const { root, args } = transformWithHoist(`<div><Comp/></div>`)
const root = transformWithHoist(`<div><Comp/></div>`)
expect(root.hoists.length).toBe(0)
expect(args[2]).toMatchObject([
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
codegenNode: {
callee: CREATE_VNODE,
arguments: [`_component_Comp`]
type: NodeTypes.VNODE_CALL,
tag: `_component_Comp`
}
}
])
@@ -207,22 +181,20 @@ describe('compiler: hoistStatic transform', () => {
})
test('should NOT hoist element with dynamic props', () => {
const { root, args } = transformWithHoist(`<div><div :id="foo"/></div>`)
const root = transformWithHoist(`<div><div :id="foo"/></div>`)
expect(root.hoists.length).toBe(0)
expect(args[2]).toMatchObject([
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
codegenNode: {
callee: CREATE_VNODE,
arguments: [
`"div"`,
createObjectMatcher({
id: `[foo]`
}),
`null`,
genFlagText(PatchFlags.PROPS),
`["id"]`
]
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: createObjectMatcher({
id: `[foo]`
}),
children: undefined,
patchFlag: genFlagText(PatchFlags.PROPS),
dynamicProps: `["id"]`
}
}
])
@@ -230,19 +202,19 @@ describe('compiler: hoistStatic transform', () => {
})
test('hoist element with static key', () => {
const { root, args } = transformWithHoist(`<div><div key="foo"/></div>`)
const root = transformWithHoist(`<div><div key="foo"/></div>`)
expect(root.hoists.length).toBe(1)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE,
arguments: [`"div"`, createObjectMatcher({ key: 'foo' })]
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: createObjectMatcher({ key: 'foo' })
}
])
expect(args).toMatchObject([
`"div"`,
`null`,
[
expect(root.codegenNode).toMatchObject({
tag: `"div"`,
props: undefined,
children: [
{
type: NodeTypes.ELEMENT,
codegenNode: {
@@ -251,24 +223,22 @@ describe('compiler: hoistStatic transform', () => {
}
}
]
])
})
expect(generate(root).code).toMatchSnapshot()
})
test('should NOT hoist element with dynamic key', () => {
const { root, args } = transformWithHoist(`<div><div :key="foo"/></div>`)
const root = transformWithHoist(`<div><div :key="foo"/></div>`)
expect(root.hoists.length).toBe(0)
expect(args[2]).toMatchObject([
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
codegenNode: {
callee: CREATE_VNODE,
arguments: [
`"div"`,
createObjectMatcher({
key: `[foo]`
})
]
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: createObjectMatcher({
key: `[foo]`
})
}
}
])
@@ -276,21 +246,19 @@ describe('compiler: hoistStatic transform', () => {
})
test('should NOT hoist element with dynamic ref', () => {
const { root, args } = transformWithHoist(`<div><div :ref="foo"/></div>`)
const root = transformWithHoist(`<div><div :ref="foo"/></div>`)
expect(root.hoists.length).toBe(0)
expect(args[2]).toMatchObject([
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
codegenNode: {
callee: CREATE_VNODE,
arguments: [
`"div"`,
createObjectMatcher({
ref: `[foo]`
}),
`null`,
genFlagText(PatchFlags.NEED_PATCH)
]
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: createObjectMatcher({
ref: `[foo]`
}),
children: undefined,
patchFlag: genFlagText(PatchFlags.NEED_PATCH)
}
}
])
@@ -298,32 +266,23 @@ describe('compiler: hoistStatic transform', () => {
})
test('hoist static props for elements with directives', () => {
const { root, args } = transformWithHoist(
`<div><div id="foo" v-foo/></div>`
)
const root = transformWithHoist(`<div><div id="foo" v-foo/></div>`)
expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
expect(args[2]).toMatchObject([
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
codegenNode: {
callee: WITH_DIRECTIVES,
arguments: [
{
callee: CREATE_VNODE,
arguments: [
`"div"`,
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`
},
`null`,
genFlagText(PatchFlags.NEED_PATCH)
]
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION
}
]
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`
},
children: undefined,
patchFlag: genFlagText(PatchFlags.NEED_PATCH),
directives: {
type: NodeTypes.JS_ARRAY_EXPRESSION
}
}
}
])
@@ -331,21 +290,19 @@ describe('compiler: hoistStatic transform', () => {
})
test('hoist static props for elements with dynamic text children', () => {
const { root, args } = transformWithHoist(
const root = transformWithHoist(
`<div><div id="foo">{{ hello }}</div></div>`
)
expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
expect(args[2]).toMatchObject([
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
codegenNode: {
callee: CREATE_VNODE,
arguments: [
`"div"`,
{ content: `_hoisted_1` },
{ type: NodeTypes.INTERPOLATION },
genFlagText(PatchFlags.TEXT)
]
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: { content: `_hoisted_1` },
children: { type: NodeTypes.INTERPOLATION },
patchFlag: genFlagText(PatchFlags.TEXT)
}
}
])
@@ -353,20 +310,16 @@ describe('compiler: hoistStatic transform', () => {
})
test('hoist static props for elements with unhoistable children', () => {
const { root, args } = transformWithHoist(
`<div><div id="foo"><Comp/></div></div>`
)
const root = transformWithHoist(`<div><div id="foo"><Comp/></div></div>`)
expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
expect(args[2]).toMatchObject([
expect((root.codegenNode as VNodeCall).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
codegenNode: {
callee: CREATE_VNODE,
arguments: [
`"div"`,
{ content: `_hoisted_1` },
[{ type: NodeTypes.ELEMENT, tag: `Comp` }]
]
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: { content: `_hoisted_1` },
children: [{ type: NodeTypes.ELEMENT, tag: `Comp` }]
}
}
])
@@ -374,7 +327,7 @@ describe('compiler: hoistStatic transform', () => {
})
test('should hoist v-if props/children if static', () => {
const { root, args } = transformWithHoist(
const root = transformWithHoist(
`<div><div v-if="ok" id="foo"><span/></div></div>`
)
expect(root.hoists).toMatchObject([
@@ -383,37 +336,31 @@ describe('compiler: hoistStatic transform', () => {
id: 'foo'
}),
{
callee: CREATE_VNODE,
arguments: [`"span"`]
type: NodeTypes.VNODE_CALL,
tag: `"span"`
}
])
expect(args[2][0].codegenNode).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{ callee: OPEN_BLOCK },
{
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
consequent: {
// blocks should NOT be hoisted
callee: CREATE_BLOCK,
arguments: [
`"div"`,
{ content: `_hoisted_1` },
[
{
codegenNode: { content: `_hoisted_2` }
}
]
]
expect(
((root.children[0] as ElementNode).children[0] as IfNode).codegenNode
).toMatchObject({
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
consequent: {
// blocks should NOT be hoisted
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: { content: `_hoisted_1` },
children: [
{
codegenNode: { content: `_hoisted_2` }
}
}
]
]
}
})
expect(generate(root).code).toMatchSnapshot()
})
test('should hoist v-for children if static', () => {
const { root, args } = transformWithHoist(
const root = transformWithHoist(
`<div><div v-for="i in list" id="foo"><span/></div></div>`
)
expect(root.hoists).toMatchObject([
@@ -421,55 +368,58 @@ describe('compiler: hoistStatic transform', () => {
id: 'foo'
}),
{
callee: CREATE_VNODE,
arguments: [`"span"`]
type: NodeTypes.VNODE_CALL,
tag: `"span"`
}
])
const forBlockCodegen = args[2][0].codegenNode
const forBlockCodegen = ((root.children[0] as ElementNode)
.children[0] as ForNode).codegenNode
expect(forBlockCodegen).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{ callee: OPEN_BLOCK },
{
callee: CREATE_BLOCK,
arguments: [
FRAGMENT,
`null`,
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST
},
genFlagText(PatchFlags.UNKEYED_FRAGMENT)
]
}
]
type: NodeTypes.VNODE_CALL,
tag: FRAGMENT,
props: undefined,
children: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST
},
patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT)
})
const innerBlockCodegen =
forBlockCodegen.expressions[1].arguments[2].arguments[1].returns
expect(innerBlockCodegen).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{ callee: OPEN_BLOCK },
const innerBlockCodegen = forBlockCodegen!.children.arguments[1]
expect(innerBlockCodegen.returns).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: { content: `_hoisted_1` },
children: [
{
callee: CREATE_BLOCK,
arguments: [
`"div"`,
{ content: `_hoisted_1` },
[
{
codegenNode: { content: `_hoisted_2` }
}
]
]
codegenNode: { content: `_hoisted_2` }
}
]
})
expect(generate(root).code).toMatchSnapshot()
})
test('hoist static text node between elements', () => {
const root = transformWithHoist(`<div>static<div>static</div></div>`)
expect(root.hoists).toMatchObject([
{
callee: CREATE_TEXT,
arguments: [
{
type: NodeTypes.TEXT,
content: `static`
}
]
},
{
type: NodeTypes.VNODE_CALL,
tag: `"div"`
}
])
})
describe('prefixIdentifiers', () => {
test('hoist nested static tree with static interpolation', () => {
const { root, args } = transformWithHoist(
const root = transformWithHoist(
`<div><span>foo {{ 1 }} {{ true }}</span></div>`,
{
prefixIdentifiers: true
@@ -477,44 +427,18 @@ describe('compiler: hoistStatic transform', () => {
)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE,
arguments: [
`"span"`,
`null`,
[
{
type: NodeTypes.TEXT,
content: `foo `
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `1`,
isStatic: false,
isConstant: true
}
},
{
type: NodeTypes.TEXT,
content: ` `
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `true`,
isStatic: false,
isConstant: true
}
}
]
]
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
props: undefined,
children: {
type: NodeTypes.COMPOUND_EXPRESSION
}
}
])
expect(args).toMatchObject([
`"div"`,
`null`,
[
expect(root.codegenNode).toMatchObject({
tag: `"div"`,
props: undefined,
children: [
{
type: NodeTypes.ELEMENT,
codegenNode: {
@@ -523,12 +447,12 @@ describe('compiler: hoistStatic transform', () => {
}
}
]
])
})
expect(generate(root).code).toMatchSnapshot()
})
test('hoist nested static tree with static prop value', () => {
const { root, args } = transformWithHoist(
const root = transformWithHoist(
`<div><span :foo="0">{{ 1 }}</span></div>`,
{
prefixIdentifiers: true
@@ -537,26 +461,23 @@ describe('compiler: hoistStatic transform', () => {
expect(root.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE,
arguments: [
`"span"`,
createObjectMatcher({ foo: `[0]` }),
{
type: NodeTypes.INTERPOLATION,
content: {
content: `1`,
isStatic: false,
isConstant: true
}
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
props: createObjectMatcher({ foo: `[0]` }),
children: {
type: NodeTypes.INTERPOLATION,
content: {
content: `1`,
isStatic: false,
isConstant: true
}
]
}
}
])
expect(args).toMatchObject([
`"div"`,
`null`,
[
expect(root.codegenNode).toMatchObject({
tag: `"div"`,
props: undefined,
children: [
{
type: NodeTypes.ELEMENT,
codegenNode: {
@@ -565,12 +486,12 @@ describe('compiler: hoistStatic transform', () => {
}
}
]
])
})
expect(generate(root).code).toMatchSnapshot()
})
test('hoist class with static object value', () => {
const { root, args } = transformWithHoist(
const root = transformWithHoist(
`<div><span :class="{ foo: true }">{{ bar }}</span></div>`,
{
prefixIdentifiers: true
@@ -596,39 +517,37 @@ describe('compiler: hoistStatic transform', () => {
]
}
])
expect(args).toMatchObject([
`"div"`,
`null`,
[
expect(root.codegenNode).toMatchObject({
tag: `"div"`,
props: undefined,
children: [
{
type: NodeTypes.ELEMENT,
codegenNode: {
callee: CREATE_VNODE,
arguments: [
`"span"`,
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.bar`,
isConstant: false,
isStatic: false
}
},
`1 /* TEXT */`
]
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
props: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`
},
children: {
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.bar`,
isConstant: false,
isStatic: false
}
},
patchFlag: `1 /* TEXT */`
}
}
]
])
})
expect(generate(root).code).toMatchSnapshot()
})
test('should NOT hoist expressions that refer scope variables', () => {
const { root } = transformWithHoist(
const root = transformWithHoist(
`<div><p v-for="o in list"><span>{{ o }}</span></p></div>`,
{
prefixIdentifiers: true
@@ -640,7 +559,7 @@ describe('compiler: hoistStatic transform', () => {
})
test('should NOT hoist expressions that refer scope variables (2)', () => {
const { root } = transformWithHoist(
const root = transformWithHoist(
`<div><p v-for="o in list"><span>{{ o + 'foo' }}</span></p></div>`,
{
prefixIdentifiers: true
@@ -652,7 +571,7 @@ describe('compiler: hoistStatic transform', () => {
})
test('should NOT hoist expressions that refer scope variables (v-slot)', () => {
const { root } = transformWithHoist(
const root = transformWithHoist(
`<Comp v-slot="{ foo }">{{ foo }}</Comp>`,
{
prefixIdentifiers: true
@@ -664,7 +583,7 @@ describe('compiler: hoistStatic transform', () => {
})
test('should NOT hoist elements with cached handlers', () => {
const { root } = transformWithHoist(
const root = transformWithHoist(
`<div><div><div @click="foo"/></div></div>`,
{
prefixIdentifiers: true,

View File

@@ -0,0 +1,24 @@
import {
baseParse as parse,
transform,
ElementNode,
noopDirectiveTransform,
VNodeCall
} from '../../src'
import { transformElement } from '../../src/transforms/transformElement'
describe('compiler: noop directive transform', () => {
test('should add no props to DOM', () => {
const ast = parse(`<div v-noop/>`)
transform(ast, {
nodeTransforms: [transformElement],
directiveTransforms: {
noop: noopDirectiveTransform
}
})
const node = ast.children[0] as ElementNode
// As v-noop adds no properties the codegen should be identical to
// rendering a div with no props or reactive data (so just the tag as the arg)
expect((node.codegenNode as VNodeCall).props).toBeUndefined()
})
})

View File

@@ -1,24 +1,28 @@
import { CompilerOptions, parse, transform, ErrorCodes } from '../../src'
import {
CompilerOptions,
baseParse as parse,
transform,
ErrorCodes
} from '../../src'
import {
RESOLVE_COMPONENT,
CREATE_VNODE,
MERGE_PROPS,
RESOLVE_DIRECTIVE,
WITH_DIRECTIVES,
TO_HANDLERS,
helperNameMap,
PORTAL,
TELEPORT,
RESOLVE_DYNAMIC_COMPONENT,
SUSPENSE,
KEEP_ALIVE,
BASE_TRANSITION
} from '../../src/runtimeHelpers'
import {
CallExpression,
NodeTypes,
createObjectProperty,
DirectiveNode,
RootNode
RootNode,
VNodeCall
} from '../../src/ast'
import { transformElement } from '../../src/transforms/transformElement'
import { transformStyle } from '../../../compiler-dom/src/transforms/transformStyle'
@@ -33,7 +37,7 @@ function parseWithElementTransform(
options: CompilerOptions = {}
): {
root: RootNode
node: CallExpression
node: VNodeCall
} {
// wrap raw template in an extra div so that it doesn't get turned into a
// block as root node
@@ -43,8 +47,8 @@ function parseWithElementTransform(
...options
})
const codegenNode = (ast as any).children[0].children[0]
.codegenNode as CallExpression
expect(codegenNode.type).toBe(NodeTypes.JS_CALL_EXPRESSION)
.codegenNode as VNodeCall
expect(codegenNode.type).toBe(NodeTypes.VNODE_CALL)
return {
root: ast,
node: codegenNode
@@ -68,63 +72,63 @@ describe('compiler: element transform', () => {
test('static props', () => {
const { node } = parseWithElementTransform(`<div id="foo" class="bar" />`)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([
`"div"`,
createObjectMatcher({
expect(node).toMatchObject({
tag: `"div"`,
props: createObjectMatcher({
id: 'foo',
class: 'bar'
})
])
}),
children: undefined
})
})
test('props + children', () => {
const { node } = parseWithElementTransform(`<div id="foo"><span/></div>`)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([
`"div"`,
createObjectMatcher({
expect(node).toMatchObject({
tag: `"div"`,
props: createObjectMatcher({
id: 'foo'
}),
[
children: [
{
type: NodeTypes.ELEMENT,
tag: 'span',
codegenNode: {
callee: CREATE_VNODE,
arguments: [`"span"`]
type: NodeTypes.VNODE_CALL,
tag: `"span"`
}
}
]
])
})
})
test('0 placeholder for children with no props', () => {
const { node } = parseWithElementTransform(`<div><span/></div>`)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([
`"div"`,
`null`,
[
expect(node).toMatchObject({
tag: `"div"`,
props: undefined,
children: [
{
type: NodeTypes.ELEMENT,
tag: 'span',
codegenNode: {
callee: CREATE_VNODE,
arguments: [`"span"`]
type: NodeTypes.VNODE_CALL,
tag: `"span"`
}
}
]
])
})
})
test('v-bind="obj"', () => {
const { root, node } = parseWithElementTransform(`<div v-bind="obj" />`)
// single v-bind doesn't need mergeProps
expect(root.helpers).not.toContain(MERGE_PROPS)
expect(node.callee).toBe(CREATE_VNODE)
// should directly use `obj` in props position
expect(node.arguments[1]).toMatchObject({
expect(node.props).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `obj`
})
@@ -135,8 +139,8 @@ describe('compiler: element transform', () => {
`<div id="foo" v-bind="obj" />`
)
expect(root.helpers).toContain(MERGE_PROPS)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments[1]).toMatchObject({
expect(node.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS,
arguments: [
@@ -156,8 +160,8 @@ describe('compiler: element transform', () => {
`<div v-bind="obj" id="foo" />`
)
expect(root.helpers).toContain(MERGE_PROPS)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments[1]).toMatchObject({
expect(node.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS,
arguments: [
@@ -177,8 +181,8 @@ describe('compiler: element transform', () => {
`<div id="foo" v-bind="obj" class="bar" />`
)
expect(root.helpers).toContain(MERGE_PROPS)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments[1]).toMatchObject({
expect(node.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS,
arguments: [
@@ -201,8 +205,8 @@ describe('compiler: element transform', () => {
`<div id="foo" v-on="obj" class="bar" />`
)
expect(root.helpers).toContain(MERGE_PROPS)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments[1]).toMatchObject({
expect(node.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS,
arguments: [
@@ -231,8 +235,8 @@ describe('compiler: element transform', () => {
`<div id="foo" v-on="handlers" v-bind="obj" />`
)
expect(root.helpers).toContain(MERGE_PROPS)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments[1]).toMatchObject({
expect(node.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS,
arguments: [
@@ -259,43 +263,43 @@ describe('compiler: element transform', () => {
test('should handle plain <template> as normal element', () => {
const { node } = parseWithElementTransform(`<template id="foo" />`)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([
`"template"`,
createObjectMatcher({
expect(node).toMatchObject({
tag: `"template"`,
props: createObjectMatcher({
id: 'foo'
})
])
})
})
test('should handle <Portal> with normal children', () => {
test('should handle <Teleport> with normal children', () => {
function assert(tag: string) {
const { root, node } = parseWithElementTransform(
`<${tag} target="#foo"><span /></${tag}>`
)
expect(root.components.length).toBe(0)
expect(root.helpers).toContain(PORTAL)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([
PORTAL,
createObjectMatcher({
expect(root.helpers).toContain(TELEPORT)
expect(node).toMatchObject({
tag: TELEPORT,
props: createObjectMatcher({
target: '#foo'
}),
[
children: [
{
type: NodeTypes.ELEMENT,
tag: 'span',
codegenNode: {
callee: CREATE_VNODE,
arguments: [`"span"`]
type: NodeTypes.VNODE_CALL,
tag: `"span"`
}
}
]
])
})
}
assert(`portal`)
assert(`Portal`)
assert(`teleport`)
assert(`Teleport`)
})
test('should handle <Suspense>', () => {
@@ -305,11 +309,11 @@ describe('compiler: element transform', () => {
)
expect(root.components.length).toBe(0)
expect(root.helpers).toContain(SUSPENSE)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([
SUSPENSE,
`null`,
hasFallback
expect(node).toMatchObject({
tag: SUSPENSE,
props: undefined,
children: hasFallback
? createObjectMatcher({
default: {
type: NodeTypes.JS_FUNCTION_EXPRESSION
@@ -317,15 +321,15 @@ describe('compiler: element transform', () => {
fallback: {
type: NodeTypes.JS_FUNCTION_EXPRESSION
},
_compiled: `[true]`
_: `[1]`
})
: createObjectMatcher({
default: {
type: NodeTypes.JS_FUNCTION_EXPRESSION
},
_compiled: `[true]`
_: `[1]`
})
])
})
}
assert(`suspense`, `foo`)
@@ -339,18 +343,23 @@ describe('compiler: element transform', () => {
test('should handle <KeepAlive>', () => {
function assert(tag: string) {
const { root, node } = parseWithElementTransform(
`<${tag}><span /></${tag}>`
)
const root = parse(`<div><${tag}><span /></${tag}></div>`)
transform(root, {
nodeTransforms: [transformElement, transformText]
})
expect(root.components.length).toBe(0)
expect(root.helpers).toContain(KEEP_ALIVE)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([
KEEP_ALIVE,
`null`,
const node = (root.children[0] as any).children[0].codegenNode
expect(node).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: KEEP_ALIVE,
isBlock: true, // should be forced into a block
props: undefined,
// keep-alive should not compile content to slots
[{ type: NodeTypes.ELEMENT, tag: 'span' }]
])
children: [{ type: NodeTypes.ELEMENT, tag: 'span' }],
// should get a dynamic slots flag to force updates
patchFlag: genFlagText(PatchFlags.DYNAMIC_SLOTS)
})
}
assert(`keep-alive`)
@@ -364,17 +373,17 @@ describe('compiler: element transform', () => {
)
expect(root.components.length).toBe(0)
expect(root.helpers).toContain(BASE_TRANSITION)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([
BASE_TRANSITION,
`null`,
createObjectMatcher({
expect(node).toMatchObject({
tag: BASE_TRANSITION,
props: undefined,
children: createObjectMatcher({
default: {
type: NodeTypes.JS_FUNCTION_EXPRESSION
},
_compiled: `[true]`
_: `[1]`
})
])
})
}
assert(`base-transition`)
@@ -398,14 +407,13 @@ describe('compiler: element transform', () => {
foo(dir) {
_dir = dir
return {
props: [createObjectProperty(dir.arg!, dir.exp!)],
needRuntime: false
props: [createObjectProperty(dir.arg!, dir.exp!)]
}
}
}
})
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments[1]).toMatchObject({
expect(node.props).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
@@ -417,8 +425,8 @@ describe('compiler: element transform', () => {
})
// should factor in props returned by custom directive transforms
// in patchFlag analysis
expect(node.arguments[3]).toMatch(PatchFlags.PROPS + '')
expect(node.arguments[4]).toMatch(`"bar"`)
expect(node.patchFlag).toMatch(PatchFlags.PROPS + '')
expect(node.dynamicProps).toMatch(`"bar"`)
})
test('directiveTransform with needRuntime: true', () => {
@@ -437,20 +445,12 @@ describe('compiler: element transform', () => {
)
expect(root.helpers).toContain(RESOLVE_DIRECTIVE)
expect(root.directives).toContain(`foo`)
expect(node.callee).toBe(WITH_DIRECTIVES)
expect(node.arguments).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE,
arguments: [
`"div"`,
`null`,
`null`,
genFlagText(PatchFlags.NEED_PATCH) // should generate appropriate flag
]
},
{
expect(node).toMatchObject({
tag: `"div"`,
props: undefined,
children: undefined,
patchFlag: genFlagText(PatchFlags.NEED_PATCH), // should generate appropriate flag
directives: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{
@@ -473,7 +473,7 @@ describe('compiler: element transform', () => {
}
]
}
])
})
})
test('directiveTransform with needRuntime: Symbol', () => {
@@ -494,7 +494,7 @@ describe('compiler: element transform', () => {
expect(root.helpers).toContain(CREATE_VNODE)
expect(root.helpers).not.toContain(RESOLVE_DIRECTIVE)
expect(root.directives.length).toBe(0)
expect((node as any).arguments[1].elements[0].elements[0]).toBe(
expect(node.directives!.elements[0].elements[0]).toBe(
`_${helperNameMap[CREATE_VNODE]}`
)
})
@@ -508,12 +508,8 @@ describe('compiler: element transform', () => {
expect(root.directives).toContain(`bar`)
expect(root.directives).toContain(`baz`)
expect(node.callee).toBe(WITH_DIRECTIVES)
expect(node.arguments).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION
},
{
expect(node).toMatchObject({
directives: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{
@@ -583,7 +579,7 @@ describe('compiler: element transform', () => {
}
]
}
])
})
})
test(`props merging: event handlers`, () => {
@@ -595,7 +591,7 @@ describe('compiler: element transform', () => {
}
}
)
expect(node.arguments[1]).toMatchObject({
expect(node.props).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
@@ -627,7 +623,7 @@ describe('compiler: element transform', () => {
test(`props merging: style`, () => {
const { node } = parseWithElementTransform(
`<div style="color: red" :style="{ color: 'red' }" />`,
`<div style="color: green" :style="{ color: 'red' }" />`,
{
nodeTransforms: [transformStyle, transformElement],
directiveTransforms: {
@@ -635,7 +631,7 @@ describe('compiler: element transform', () => {
}
}
)
expect(node.arguments[1]).toMatchObject({
expect(node.props).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
@@ -650,7 +646,7 @@ describe('compiler: element transform', () => {
elements: [
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`,
content: `{"color":"green"}`,
isStatic: false
},
{
@@ -674,7 +670,7 @@ describe('compiler: element transform', () => {
}
}
)
expect(node.arguments[1]).toMatchObject({
expect(node.props).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
@@ -707,113 +703,175 @@ describe('compiler: element transform', () => {
describe('patchFlag analysis', () => {
test('TEXT', () => {
const { node } = parseWithBind(`<div>foo</div>`)
expect(node.arguments.length).toBe(3)
expect(node.patchFlag).toBeUndefined()
const { node: node2 } = parseWithBind(`<div>{{ foo }}</div>`)
expect(node2.arguments.length).toBe(4)
expect(node2.arguments[3]).toBe(genFlagText(PatchFlags.TEXT))
expect(node2.patchFlag).toBe(genFlagText(PatchFlags.TEXT))
// multiple nodes, merged with optimize text
const { node: node3 } = parseWithBind(`<div>foo {{ bar }} baz</div>`)
expect(node3.arguments.length).toBe(4)
expect(node3.arguments[3]).toBe(genFlagText(PatchFlags.TEXT))
expect(node3.patchFlag).toBe(genFlagText(PatchFlags.TEXT))
})
test('CLASS', () => {
const { node } = parseWithBind(`<div :class="foo" />`)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.CLASS))
expect(node.patchFlag).toBe(genFlagText(PatchFlags.CLASS))
})
test('STYLE', () => {
const { node } = parseWithBind(`<div :style="foo" />`)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.STYLE))
expect(node.patchFlag).toBe(genFlagText(PatchFlags.STYLE))
})
test('PROPS', () => {
const { node } = parseWithBind(`<div id="foo" :foo="bar" :baz="qux" />`)
expect(node.arguments.length).toBe(5)
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.PROPS))
expect(node.arguments[4]).toBe(`["foo", "baz"]`)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
expect(node.dynamicProps).toBe(`["foo", "baz"]`)
})
test('CLASS + STYLE + PROPS', () => {
const { node } = parseWithBind(
`<div id="foo" :class="cls" :style="styl" :foo="bar" :baz="qux"/>`
)
expect(node.arguments.length).toBe(5)
expect(node.arguments[3]).toBe(
expect(node.patchFlag).toBe(
genFlagText([PatchFlags.CLASS, PatchFlags.STYLE, PatchFlags.PROPS])
)
expect(node.arguments[4]).toBe(`["foo", "baz"]`)
expect(node.dynamicProps).toBe(`["foo", "baz"]`)
})
test('FULL_PROPS (v-bind)', () => {
const { node } = parseWithBind(`<div v-bind="foo" />`)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS))
expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
})
test('FULL_PROPS (dynamic key)', () => {
const { node } = parseWithBind(`<div :[foo]="bar" />`)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS))
expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
})
test('FULL_PROPS (w/ others)', () => {
const { node } = parseWithBind(
`<div id="foo" v-bind="bar" :class="cls" />`
)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS))
expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
})
test('NEED_PATCH (static ref)', () => {
const { node } = parseWithBind(`<div ref="foo" />`)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
})
test('NEED_PATCH (dynamic ref)', () => {
const { node } = parseWithBind(`<div :ref="foo" />`)
expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
})
test('NEED_PATCH (custom directives)', () => {
const { node } = parseWithBind(`<div v-foo />`)
const vnodeCall = node.arguments[0] as CallExpression
expect(vnodeCall.arguments.length).toBe(4)
expect(vnodeCall.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
})
test('HYDRATE_EVENTS', () => {
// ignore click events (has dedicated fast path)
const { node } = parseWithElementTransform(`<div @click="foo" />`, {
directiveTransforms: {
on: transformOn
}
})
// should only have props flag
expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
const { node: node2 } = parseWithElementTransform(
`<div @keyup="foo" />`,
{
directiveTransforms: {
on: transformOn
}
}
)
expect(node2.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS])
)
})
})
describe('dynamic component', () => {
test('static binding', () => {
const { node, root } = parseWithBind(`<component is="foo" />`)
expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT)
expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
expect(node).toMatchObject({
callee: CREATE_VNODE,
arguments: ['_component_foo']
tag: {
callee: RESOLVE_DYNAMIC_COMPONENT,
arguments: [
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'foo',
isStatic: true
}
]
}
})
})
test('dynamic binding', () => {
const { node, root } = parseWithBind(`<component :is="foo" />`)
expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
expect(node.arguments).toMatchObject([
{
expect(node).toMatchObject({
tag: {
callee: RESOLVE_DYNAMIC_COMPONENT,
arguments: [
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'foo'
},
'$'
content: 'foo',
isStatic: false
}
]
}
])
})
})
test('v-is', () => {
const { node, root } = parseWithBind(`<div v-is="'foo'" />`)
expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
expect(node).toMatchObject({
tag: {
callee: RESOLVE_DYNAMIC_COMPONENT,
arguments: [
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `'foo'`,
isStatic: false
}
]
},
// should skip v-is runtime check
directives: undefined
})
})
})
test('<svg> should be forced into blocks', () => {
const ast = parse(`<div><svg/></div>`)
transform(ast, {
nodeTransforms: [transformElement]
})
expect((ast as any).children[0].children[0].codegenNode).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: `"svg"`,
isBlock: true
})
})
// #938
test('element with dynamic keys should be forced into blocks', () => {
const ast = parse(`<div><div :key="foo" /></div>`)
transform(ast, {
nodeTransforms: [transformElement]
})
expect((ast as any).children[0].children[0].codegenNode).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
isBlock: true
})
})
})

View File

@@ -1,5 +1,5 @@
import {
parse,
baseParse as parse,
transform,
ElementNode,
DirectiveNode,
@@ -317,6 +317,16 @@ describe('compiler: expression transform', () => {
})
})
test('should not duplicate object key with same name as value', () => {
const node = parseWithExpressionTransform(
`{{ { foo: foo } }}`
) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`{ foo: `, { content: `_ctx.foo` }, ` }`]
})
})
test('should prefix a computed object property key', () => {
const node = parseWithExpressionTransform(
`{{ { [foo]: bar } }}`
@@ -380,7 +390,65 @@ describe('compiler: expression transform', () => {
const onError = jest.fn()
parseWithExpressionTransform(`{{ a( }}`, { onError })
expect(onError.mock.calls[0][0].message).toMatch(
`Invalid JavaScript expression. (1:4)`
`Error parsing JavaScript expression: Unexpected token`
)
})
describe('ES Proposals support', () => {
test('bigInt', () => {
const node = parseWithExpressionTransform(
`{{ 13000n }}`
) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `13000n`,
isStatic: false,
isConstant: true
})
})
test('nullish colescing', () => {
const node = parseWithExpressionTransform(
`{{ a ?? b }}`
) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [{ content: `_ctx.a` }, ` ?? `, { content: `_ctx.b` }]
})
})
test('optional chaining', () => {
const node = parseWithExpressionTransform(
`{{ a?.b?.c }}`
) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `_ctx.a` },
`?.`,
{ content: `b` },
`?.`,
{ content: `c` }
]
})
})
test('Enabling additional plugins', () => {
// enabling pipeline operator to replace filters:
const node = parseWithExpressionTransform(`{{ a |> uppercase }}`, {
expressionPlugins: [
[
'pipelineOperator',
{
proposal: 'minimal'
}
]
]
}) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [{ content: `_ctx.a` }, ` |> `, { content: `_ctx.uppercase` }]
})
})
})
})

View File

@@ -1,6 +1,6 @@
import {
CompilerOptions,
parse,
baseParse as parse,
transform,
ElementNode,
NodeTypes,

View File

@@ -1,6 +1,6 @@
import {
CompilerOptions,
parse,
baseParse as parse,
transform,
NodeTypes,
generate,

View File

@@ -1,11 +1,11 @@
import {
parse,
baseParse as parse,
transform,
ElementNode,
ObjectExpression,
CompilerOptions,
ErrorCodes,
CallExpression
VNodeCall
} from '../../src'
import { transformBind } from '../../src/transforms/vBind'
import { transformElement } from '../../src/transforms/transformElement'
@@ -33,8 +33,7 @@ function parseWithVBind(
describe('compiler: transform v-bind', () => {
test('basic', () => {
const node = parseWithVBind(`<div v-bind:id="id"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `id`,
@@ -69,8 +68,7 @@ describe('compiler: transform v-bind', () => {
test('dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[id]="id"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `id`,
@@ -103,8 +101,7 @@ describe('compiler: transform v-bind', () => {
test('.camel modifier', () => {
const node = parseWithVBind(`<div v-bind:foo-bar.camel="id"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `fooBar`,
@@ -119,8 +116,7 @@ describe('compiler: transform v-bind', () => {
test('.camel modifier w/ dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[foo].camel="id"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `_${helperNameMap[CAMELIZE]}(foo)`,
@@ -137,12 +133,11 @@ describe('compiler: transform v-bind', () => {
const node = parseWithVBind(`<div v-bind:[foo(bar)].camel="id"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
children: [
`${helperNameMap[CAMELIZE]}(`,
`_${helperNameMap[CAMELIZE]}(`,
{ content: `_ctx.foo` },
`(`,
{ content: `_ctx.bar` },

View File

@@ -1,4 +1,4 @@
import { parse } from '../../src/parse'
import { baseParse as parse } from '../../src/parse'
import { transform } from '../../src/transform'
import { transformIf } from '../../src/transforms/vIf'
import { transformFor } from '../../src/transforms/vFor'
@@ -12,19 +12,11 @@ import {
SimpleExpressionNode,
ElementNode,
InterpolationNode,
CallExpression,
SequenceExpression
ForCodegenNode
} from '../../src/ast'
import { ErrorCodes } from '../../src/errors'
import { CompilerOptions, generate } from '../../src'
import {
OPEN_BLOCK,
CREATE_BLOCK,
FRAGMENT,
RENDER_LIST,
RENDER_SLOT,
WITH_DIRECTIVES
} from '../../src/runtimeHelpers'
import { FRAGMENT, RENDER_LIST, RENDER_SLOT } from '../../src/runtimeHelpers'
import { PatchFlags } from '@vue/shared'
import { createObjectMatcher, genFlagText } from '../testUtils'
@@ -48,7 +40,7 @@ function parseWithForTransform(
})
return {
root: ast,
node: ast.children[0] as ForNode
node: ast.children[0] as ForNode & { codegenNode: ForCodegenNode }
}
}
@@ -566,64 +558,40 @@ describe('compiler: v-for', () => {
describe('codegen', () => {
function assertSharedCodegen(
node: SequenceExpression,
node: ForCodegenNode,
keyed: boolean = false,
customReturn: boolean = false
) {
expect(node).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK
},
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK,
arguments: [
FRAGMENT,
`null`,
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
arguments: [
{}, // to be asserted by each test
{
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: customReturn
? {}
: {
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK
},
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK
}
]
}
type: NodeTypes.VNODE_CALL,
tag: FRAGMENT,
isForBlock: true,
patchFlag: keyed
? genFlagText(PatchFlags.KEYED_FRAGMENT)
: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
children: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
arguments: [
{}, // to be asserted by each test
{
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: customReturn
? {}
: {
type: NodeTypes.VNODE_CALL,
isBlock: true
}
]
},
keyed
? genFlagText(PatchFlags.KEYED_FRAGMENT)
: genFlagText(PatchFlags.UNKEYED_FRAGMENT)
]
}
]
}
]
}
})
const renderListArgs = ((node.expressions[1] as CallExpression)
.arguments[2] as CallExpression).arguments
const renderListArgs = node.children.arguments
return {
source: renderListArgs[0] as SimpleExpressionNode,
params: (renderListArgs[1] as any).params,
returns: (renderListArgs[1] as any).returns,
blockArgs: customReturn
? null
: (renderListArgs[1] as any).returns.expressions[1].arguments
innerVNodeCall: customReturn ? null : (renderListArgs[1] as any).returns
}
}
@@ -635,7 +603,9 @@ describe('compiler: v-for', () => {
expect(assertSharedCodegen(codegenNode)).toMatchObject({
source: { content: `items` },
params: [{ content: `item` }],
blockArgs: [`"span"`]
innerVNodeCall: {
tag: `"span"`
}
})
expect(generate(root).code).toMatchSnapshot()
})
@@ -698,15 +668,16 @@ describe('compiler: v-for', () => {
expect(assertSharedCodegen(codegenNode)).toMatchObject({
source: { content: `items` },
params: [{ content: `item` }],
blockArgs: [
FRAGMENT,
`null`,
[
innerVNodeCall: {
tag: FRAGMENT,
props: undefined,
isBlock: true,
children: [
{ type: NodeTypes.TEXT, content: `hello` },
{ type: NodeTypes.ELEMENT, tag: `span` }
],
genFlagText(PatchFlags.STABLE_FRAGMENT)
]
patchFlag: genFlagText(PatchFlags.STABLE_FRAGMENT)
}
})
expect(generate(root).code).toMatchSnapshot()
})
@@ -757,12 +728,12 @@ describe('compiler: v-for', () => {
expect(assertSharedCodegen(codegenNode, true)).toMatchObject({
source: { content: `items` },
params: [{ content: `item` }],
blockArgs: [
`"span"`,
createObjectMatcher({
innerVNodeCall: {
tag: `"span"`,
props: createObjectMatcher({
key: `[item]`
})
]
}
})
expect(generate(root).code).toMatchSnapshot()
})
@@ -777,17 +748,17 @@ describe('compiler: v-for', () => {
expect(assertSharedCodegen(codegenNode, true)).toMatchObject({
source: { content: `items` },
params: [{ content: `item` }],
blockArgs: [
FRAGMENT,
createObjectMatcher({
innerVNodeCall: {
tag: FRAGMENT,
props: createObjectMatcher({
key: `[item]`
}),
[
children: [
{ type: NodeTypes.TEXT, content: `hello` },
{ type: NodeTypes.ELEMENT, tag: `span` }
],
genFlagText(PatchFlags.STABLE_FRAGMENT)
]
patchFlag: genFlagText(PatchFlags.STABLE_FRAGMENT)
}
})
expect(generate(root).code).toMatchSnapshot()
})
@@ -798,53 +769,33 @@ describe('compiler: v-for', () => {
node: { codegenNode }
} = parseWithForTransform(`<div v-if="ok" v-for="i in list"/>`)
expect(codegenNode).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test: { content: `ok` },
consequent: {
type: NodeTypes.VNODE_CALL,
props: createObjectMatcher({
key: `[0]`
}),
isBlock: true,
isForBlock: true,
patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
children: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK,
arguments: []
},
{
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test: { content: `ok` },
consequent: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK,
// should optimize v-if + v-for into a single Fragment block
arguments: [
FRAGMENT,
createObjectMatcher({ key: `[0]` }),
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
arguments: [
{ content: `list` },
{
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: [{ content: `i` }],
returns: {
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK
},
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK,
arguments: [`"div"`]
}
]
}
}
]
},
genFlagText(PatchFlags.UNKEYED_FRAGMENT)
]
}
callee: RENDER_LIST,
arguments: [
{ content: `list` },
{
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: [{ content: `i` }],
returns: {
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
isBlock: true
}
}
]
}
]
}
})
expect(generate(root).code).toMatchSnapshot()
})
@@ -856,18 +807,8 @@ describe('compiler: v-for', () => {
} = parseWithForTransform('<div v-for="i in list" v-foo/>')
const { returns } = assertSharedCodegen(codegenNode, false, true)
expect(returns).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{ callee: OPEN_BLOCK },
// should wrap withDirectives() around createBlock()
{
callee: WITH_DIRECTIVES,
arguments: [
{ callee: CREATE_BLOCK },
{ type: NodeTypes.JS_ARRAY_EXPRESSION }
]
}
]
type: NodeTypes.VNODE_CALL,
directives: { type: NodeTypes.JS_ARRAY_EXPRESSION }
})
expect(generate(root).code).toMatchSnapshot()
})

View File

@@ -1,4 +1,4 @@
import { parse } from '../../src/parse'
import { baseParse as parse } from '../../src/parse'
import { transform } from '../../src/transform'
import { transformIf } from '../../src/transforms/vIf'
import { transformElement } from '../../src/transforms/transformElement'
@@ -10,18 +10,16 @@ import {
TextNode,
CommentNode,
SimpleExpressionNode,
SequenceExpression,
ConditionalExpression,
CallExpression
IfConditionalExpression,
VNodeCall,
ElementTypes
} from '../../src/ast'
import { ErrorCodes } from '../../src/errors'
import { CompilerOptions, generate } from '../../src'
import {
OPEN_BLOCK,
CREATE_BLOCK,
FRAGMENT,
MERGE_PROPS,
WITH_DIRECTIVES,
RENDER_SLOT,
CREATE_COMMENT
} from '../../src/runtimeHelpers'
@@ -43,7 +41,9 @@ function parseWithIfTransform(
}
return {
root: ast,
node: ast.children[returnIndex] as IfNode
node: ast.children[returnIndex] as IfNode & {
codegenNode: IfConditionalExpression
}
}
}
@@ -79,6 +79,22 @@ describe('compiler: v-if', () => {
expect((node.branches[0].children[2] as ElementNode).tag).toBe(`p`)
})
test('component v-if', () => {
const { node } = parseWithIfTransform(`<Component v-if="ok"></Component>`)
expect(node.type).toBe(NodeTypes.IF)
expect(node.branches.length).toBe(1)
expect((node.branches[0].children[0] as ElementNode).tag).toBe(
`Component`
)
expect((node.branches[0].children[0] as ElementNode).tagType).toBe(
ElementTypes.COMPONENT
)
expect(
((node.branches[0].children[0] as ElementNode)!
.codegenNode as VNodeCall)!.isBlock
).toBe(false)
})
test('v-if + v-else', () => {
const { node } = parseWithIfTransform(`<div v-if="ok"/><p v-else/>`)
expect(node.type).toBe(NodeTypes.IF)
@@ -266,49 +282,49 @@ describe('compiler: v-if', () => {
describe('codegen', () => {
function assertSharedCodegen(
node: SequenceExpression,
node: IfConditionalExpression,
depth: number = 0,
hasElse: boolean = false
) {
expect(node).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK,
arguments: []
},
{
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test: {
content: `ok`
},
consequent: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK
},
alternate:
depth < 1
? {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: hasElse ? CREATE_BLOCK : CREATE_COMMENT
}
: {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test: {
content: `orNot`
},
consequent: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK
},
alternate: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: hasElse ? CREATE_BLOCK : CREATE_COMMENT
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test: {
content: `ok`
},
consequent: {
type: NodeTypes.VNODE_CALL,
isBlock: true
},
alternate:
depth < 1
? hasElse
? {
type: NodeTypes.VNODE_CALL,
isBlock: true
}
: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_COMMENT
}
: {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test: {
content: `orNot`
},
consequent: {
type: NodeTypes.VNODE_CALL,
isBlock: true
},
alternate: hasElse
? {
type: NodeTypes.VNODE_CALL,
isBlock: true
}
}
}
]
: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_COMMENT
}
}
})
}
@@ -318,15 +334,11 @@ describe('compiler: v-if', () => {
node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok"/>`)
assertSharedCodegen(codegenNode)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1.arguments).toMatchObject([
`"div"`,
createObjectMatcher({ key: `[0]` })
])
const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
.alternate as CallExpression
expect(branch2).toMatchObject({
expect(codegenNode.consequent).toMatchObject({
tag: `"div"`,
props: createObjectMatcher({ key: `[0]` })
})
expect(codegenNode.alternate).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_COMMENT
})
@@ -339,20 +351,16 @@ describe('compiler: v-if', () => {
node: { codegenNode }
} = parseWithIfTransform(`<template v-if="ok"><div/>hello<p/></template>`)
assertSharedCodegen(codegenNode)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1.arguments).toMatchObject([
FRAGMENT,
createObjectMatcher({ key: `[0]` }),
[
expect(codegenNode.consequent).toMatchObject({
tag: FRAGMENT,
props: createObjectMatcher({ key: `[0]` }),
children: [
{ type: NodeTypes.ELEMENT, tag: 'div' },
{ type: NodeTypes.TEXT, content: `hello` },
{ type: NodeTypes.ELEMENT, tag: 'p' }
]
])
const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
.alternate as CallExpression
expect(branch2).toMatchObject({
})
expect(codegenNode.alternate).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_COMMENT
})
@@ -364,10 +372,7 @@ describe('compiler: v-if', () => {
root,
node: { codegenNode }
} = parseWithIfTransform(`<template v-if="ok"><slot/></template>`)
// assertSharedCodegen(codegenNode)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1).toMatchObject({
expect(codegenNode.consequent).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_SLOT,
arguments: ['$slots', '"default"', createObjectMatcher({ key: `[0]` })]
@@ -380,10 +385,7 @@ describe('compiler: v-if', () => {
root,
node: { codegenNode }
} = parseWithIfTransform(`<slot v-if="ok"></slot>`)
// assertSharedCodegen(codegenNode)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1).toMatchObject({
expect(codegenNode.consequent).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_SLOT,
arguments: ['$slots', '"default"', createObjectMatcher({ key: `[0]` })]
@@ -397,18 +399,14 @@ describe('compiler: v-if', () => {
node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok"/><p v-else/>`)
assertSharedCodegen(codegenNode, 0, true)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1.arguments).toMatchObject([
`"div"`,
createObjectMatcher({ key: `[0]` })
])
const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
.alternate as CallExpression
expect(branch2.arguments).toMatchObject([
`"p"`,
createObjectMatcher({ key: `[1]` })
])
expect(codegenNode.consequent).toMatchObject({
tag: `"div"`,
props: createObjectMatcher({ key: `[0]` })
})
expect(codegenNode.alternate).toMatchObject({
tag: `"p"`,
props: createObjectMatcher({ key: `[1]` })
})
expect(generate(root).code).toMatchSnapshot()
})
@@ -418,18 +416,15 @@ describe('compiler: v-if', () => {
node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok"/><p v-else-if="orNot" />`)
assertSharedCodegen(codegenNode, 1)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1.arguments).toMatchObject([
`"div"`,
createObjectMatcher({ key: `[0]` })
])
const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
.alternate as ConditionalExpression
expect((branch2.consequent as CallExpression).arguments).toMatchObject([
`"p"`,
createObjectMatcher({ key: `[1]` })
])
expect(codegenNode.consequent).toMatchObject({
tag: `"div"`,
props: createObjectMatcher({ key: `[0]` })
})
const branch2 = codegenNode.alternate as ConditionalExpression
expect(branch2.consequent).toMatchObject({
tag: `"p"`,
props: createObjectMatcher({ key: `[1]` })
})
expect(generate(root).code).toMatchSnapshot()
})
@@ -441,28 +436,25 @@ describe('compiler: v-if', () => {
`<div v-if="ok"/><p v-else-if="orNot"/><template v-else>fine</template>`
)
assertSharedCodegen(codegenNode, 1, true)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1.arguments).toMatchObject([
`"div"`,
createObjectMatcher({ key: `[0]` })
])
const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
.alternate as ConditionalExpression
expect((branch2.consequent as CallExpression).arguments).toMatchObject([
`"p"`,
createObjectMatcher({ key: `[1]` })
])
expect((branch2.alternate as CallExpression).arguments).toMatchObject([
FRAGMENT,
createObjectMatcher({ key: `[2]` }),
[
expect(codegenNode.consequent).toMatchObject({
tag: `"div"`,
props: createObjectMatcher({ key: `[0]` })
})
const branch2 = codegenNode.alternate as ConditionalExpression
expect(branch2.consequent).toMatchObject({
tag: `"p"`,
props: createObjectMatcher({ key: `[1]` })
})
expect(branch2.alternate).toMatchObject({
tag: FRAGMENT,
props: createObjectMatcher({ key: `[2]` }),
children: [
{
type: NodeTypes.TEXT,
content: `fine`
}
]
])
})
expect(generate(root).code).toMatchSnapshot()
})
@@ -470,9 +462,8 @@ describe('compiler: v-if', () => {
const {
node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok" v-bind="obj"/>`)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1.arguments[1]).toMatchObject({
const branch1 = codegenNode.consequent as VNodeCall
expect(branch1.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS,
arguments: [createObjectMatcher({ key: `[0]` }), { content: `obj` }]
@@ -483,9 +474,8 @@ describe('compiler: v-if', () => {
const {
node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok" id="foo" v-bind="obj"/>`)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1.arguments[1]).toMatchObject({
const branch1 = codegenNode.consequent as VNodeCall
expect(branch1.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS,
arguments: [
@@ -502,9 +492,8 @@ describe('compiler: v-if', () => {
const {
node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok" v-bind="obj" id="foo"/>`)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1.arguments[1]).toMatchObject({
const branch1 = codegenNode.consequent as VNodeCall
expect(branch1.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS,
arguments: [
@@ -521,13 +510,21 @@ describe('compiler: v-if', () => {
const {
node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok" v-foo />`)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1.callee).toBe(WITH_DIRECTIVES)
const realBranch = branch1.arguments[0] as CallExpression
expect(realBranch.arguments[1]).toMatchObject(
createObjectMatcher({ key: `[0]` })
)
const branch1 = codegenNode.consequent as VNodeCall
expect(branch1.directives).not.toBeUndefined()
expect(branch1.props).toMatchObject(createObjectMatcher({ key: `[0]` }))
})
test('v-if with key', () => {
const {
root,
node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok" key="some-key"/>`)
expect(codegenNode.consequent).toMatchObject({
tag: `"div"`,
props: createObjectMatcher({ key: 'some-key' })
})
expect(generate(root).code).toMatchSnapshot()
})
test.todo('with comments')

View File

@@ -1,16 +1,15 @@
import {
parse,
baseParse as parse,
transform,
generate,
ElementNode,
ObjectExpression,
CompilerOptions,
CallExpression,
ForNode,
PlainElementNode,
PlainElementCodegenNode,
ComponentNode,
NodeTypes
NodeTypes,
VNodeCall
} from '../../src'
import { ErrorCodes } from '../../src/errors'
import { transformModel } from '../../src/transforms/vModel'
@@ -43,8 +42,8 @@ describe('compiler: transform v-model', () => {
test('simple exprssion', () => {
const root = parseWithVModel('<input v-model="model" />')
const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression).properties
const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.properties
expect(props[0]).toMatchObject({
key: {
@@ -82,8 +81,8 @@ describe('compiler: transform v-model', () => {
prefixIdentifiers: true
})
const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression).properties
const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.properties
expect(props[0]).toMatchObject({
key: {
@@ -119,8 +118,8 @@ describe('compiler: transform v-model', () => {
test('compound expression', () => {
const root = parseWithVModel('<input v-model="model[index]" />')
const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression).properties
const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.properties
expect(props[0]).toMatchObject({
key: {
@@ -158,8 +157,8 @@ describe('compiler: transform v-model', () => {
prefixIdentifiers: true
})
const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression).properties
const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.properties
expect(props[0]).toMatchObject({
key: {
@@ -191,15 +190,19 @@ describe('compiler: transform v-model', () => {
children: [
'$event => (',
{
content: '_ctx.model',
isStatic: false
children: [
{
content: '_ctx.model',
isStatic: false
},
'[',
{
content: '_ctx.index',
isStatic: false
},
']'
]
},
'[',
{
content: '_ctx.index',
isStatic: false
},
']',
' = $event)'
]
}
@@ -211,9 +214,8 @@ describe('compiler: transform v-model', () => {
test('with argument', () => {
const root = parseWithVModel('<input v-model:value="model" />')
const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression).properties
const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.properties
expect(props[0]).toMatchObject({
key: {
content: 'value',
@@ -248,8 +250,8 @@ describe('compiler: transform v-model', () => {
test('with dynamic argument', () => {
const root = parseWithVModel('<input v-model:[value]="model" />')
const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression).properties
const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.properties
expect(props[0]).toMatchObject({
key: {
@@ -292,8 +294,8 @@ describe('compiler: transform v-model', () => {
prefixIdentifiers: true
})
const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression).properties
const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.properties
expect(props[0]).toMatchObject({
key: {
@@ -338,12 +340,12 @@ describe('compiler: transform v-model', () => {
})
expect(root.cached).toBe(1)
const codegen = (root.children[0] as PlainElementNode)
.codegenNode as PlainElementCodegenNode
.codegenNode as VNodeCall
// should not list cached prop in dynamicProps
expect(codegen.arguments[4]).toBe(`["modelValue"]`)
expect(
(codegen.arguments[1] as ObjectExpression).properties[1].value.type
).toBe(NodeTypes.JS_CACHE_EXPRESSION)
expect(codegen.dynamicProps).toBe(`["modelValue"]`)
expect((codegen.props as ObjectExpression).properties[1].value.type).toBe(
NodeTypes.JS_CACHE_EXPRESSION
)
})
test('should not cache update handler if it refers v-for scope variables', () => {
@@ -356,10 +358,10 @@ describe('compiler: transform v-model', () => {
)
expect(root.cached).toBe(0)
const codegen = ((root.children[0] as ForNode)
.children[0] as PlainElementNode).codegenNode as PlainElementCodegenNode
expect(codegen.arguments[4]).toBe(`["modelValue", "onUpdate:modelValue"]`)
.children[0] as PlainElementNode).codegenNode as VNodeCall
expect(codegen.dynamicProps).toBe(`["modelValue", "onUpdate:modelValue"]`)
expect(
(codegen.arguments[1] as ObjectExpression).properties[1].value.type
(codegen.props as ObjectExpression).properties[1].value.type
).not.toBe(NodeTypes.JS_CACHE_EXPRESSION)
})
@@ -371,18 +373,18 @@ describe('compiler: transform v-model', () => {
}
)
const codegen = ((root.children[0] as ComponentNode)
.children[0] as PlainElementNode).codegenNode as PlainElementCodegenNode
expect(codegen.arguments[4]).toBe(`["modelValue", "onUpdate:modelValue"]`)
.children[0] as PlainElementNode).codegenNode as VNodeCall
expect(codegen.dynamicProps).toBe(`["modelValue", "onUpdate:modelValue"]`)
})
test('should generate modelModifers for component v-model', () => {
const root = parseWithVModel('<Comp v-model.trim.bar-baz="foo" />', {
prefixIdentifiers: true
})
const args = ((root.children[0] as ComponentNode)
.codegenNode as CallExpression).arguments
const vnodeCall = (root.children[0] as ComponentNode)
.codegenNode as VNodeCall
// props
expect(args[1]).toMatchObject({
expect(vnodeCall.props).toMatchObject({
properties: [
{ key: { content: `modelValue` } },
{ key: { content: `onUpdate:modelValue` } },
@@ -394,7 +396,7 @@ describe('compiler: transform v-model', () => {
})
// should NOT include modelModifiers in dynamicPropNames because it's never
// gonna change
expect(args[4]).toBe(`["modelValue", "onUpdate:modelValue"]`)
expect(vnodeCall.dynamicProps).toBe(`["modelValue", "onUpdate:modelValue"]`)
})
test('should generate modelModifers for component v-model with arguments', () => {
@@ -404,10 +406,10 @@ describe('compiler: transform v-model', () => {
prefixIdentifiers: true
}
)
const args = ((root.children[0] as ComponentNode)
.codegenNode as CallExpression).arguments
const vnodeCall = (root.children[0] as ComponentNode)
.codegenNode as VNodeCall
// props
expect(args[1]).toMatchObject({
expect(vnodeCall.props).toMatchObject({
properties: [
{ key: { content: `foo` } },
{ key: { content: `onUpdate:foo` } },
@@ -425,7 +427,9 @@ describe('compiler: transform v-model', () => {
})
// should NOT include modelModifiers in dynamicPropNames because it's never
// gonna change
expect(args[4]).toBe(`["foo", "onUpdate:foo", "bar", "onUpdate:bar"]`)
expect(vnodeCall.dynamicProps).toBe(
`["foo", "onUpdate:foo", "bar", "onUpdate:bar"]`
)
})
describe('errors', () => {

View File

@@ -1,13 +1,12 @@
import {
parse,
baseParse as parse,
transform,
ElementNode,
ObjectExpression,
CompilerOptions,
ErrorCodes,
NodeTypes,
CallExpression,
PlainElementCodegenNode
VNodeCall
} from '../../src'
import { transformOn } from '../../src/transforms/vOn'
import { transformElement } from '../../src/transforms/transformElement'
@@ -31,54 +30,58 @@ function parseWithVOn(template: string, options: CompilerOptions = {}) {
describe('compiler: transform v-on', () => {
test('basic', () => {
const { node } = parseWithVOn(`<div v-on:click="onClick"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `onClick`,
isStatic: true,
loc: {
start: {
line: 1,
column: 11
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: {
content: `onClick`,
isStatic: true,
loc: {
start: {
line: 1,
column: 11
},
end: {
line: 1,
column: 16
}
}
},
end: {
line: 1,
column: 16
value: {
content: `onClick`,
isStatic: false,
loc: {
start: {
line: 1,
column: 18
},
end: {
line: 1,
column: 25
}
}
}
}
},
value: {
content: `onClick`,
isStatic: false,
loc: {
start: {
line: 1,
column: 18
},
end: {
line: 1,
column: 25
}
}
}
]
})
})
test('dynamic arg', () => {
const { node } = parseWithVOn(`<div v-on:[event]="handler"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`"on" + (`, { content: `event` }, `)`]
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `handler`,
isStatic: false
}
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`"on" + (`, { content: `event` }, `)`]
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `handler`,
isStatic: false
}
}
]
})
})
@@ -86,18 +89,20 @@ describe('compiler: transform v-on', () => {
const { node } = parseWithVOn(`<div v-on:[event]="handler"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`"on" + (`, { content: `_ctx.event` }, `)`]
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.handler`,
isStatic: false
}
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`"on" + (`, { content: `_ctx.event` }, `)`]
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.handler`,
isStatic: false
}
}
]
})
})
@@ -105,38 +110,60 @@ describe('compiler: transform v-on', () => {
const { node } = parseWithVOn(`<div v-on:[event(foo)]="handler"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`"on" + (`,
{ content: `_ctx.event` },
`(`,
{ content: `_ctx.foo` },
`)`,
`)`
]
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.handler`,
isStatic: false
}
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`"on" + (`,
{ content: `_ctx.event` },
`(`,
{ content: `_ctx.foo` },
`)`,
`)`
]
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.handler`,
isStatic: false
}
}
]
})
})
test('should wrap as function if expression is inline statement', () => {
const { node } = parseWithVOn(`<div @click="i++"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`$event => (`, { content: `i++` }, `)`]
}
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`$event => (`, { content: `i++` }, `)`]
}
}
]
})
})
test('should handle multiple inline statement', () => {
const { node } = parseWithVOn(`<div @click="foo();bar()"/>`)
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
// should wrap with `{` for multiple statements
// in this case the return value is discarded and the behavior is
// consistent with 2.x
children: [`$event => {`, { content: `foo();bar()` }, `}`]
}
}
]
})
})
@@ -144,48 +171,90 @@ describe('compiler: transform v-on', () => {
const { node } = parseWithVOn(`<div @click="foo($event)"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`$event => (`,
{ content: `_ctx.foo` },
`(`,
// should NOT prefix $event
{ content: `$event` },
`)`,
`)`
]
}
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`$event => (`,
{
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `_ctx.foo` },
`(`,
// should NOT prefix $event
{ content: `$event` },
`)`
]
},
`)`
]
}
}
]
})
})
test('multiple inline statements w/ prefixIdentifiers: true', () => {
const { node } = parseWithVOn(`<div @click="foo($event);bar()"/>`, {
prefixIdentifiers: true
})
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`$event => {`,
{
children: [
{ content: `_ctx.foo` },
`(`,
// should NOT prefix $event
{ content: `$event` },
`);`,
{ content: `_ctx.bar` },
`()`
]
},
`}`
]
}
}
]
})
})
test('should NOT wrap as function if expression is already function expression', () => {
const { node } = parseWithVOn(`<div @click="$event => foo($event)"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: { content: `onClick` },
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `$event => foo($event)`
}
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: { content: `onClick` },
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `$event => foo($event)`
}
}
]
})
})
test('should NOT wrap as function if expression is complex member expression', () => {
const { node } = parseWithVOn(`<div @click="a['b' + c]"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: { content: `onClick` },
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `a['b' + c]`
}
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: { content: `onClick` },
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `a['b' + c]`
}
}
]
})
})
@@ -193,14 +262,21 @@ describe('compiler: transform v-on', () => {
const { node } = parseWithVOn(`<div @click="a['b' + c]"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [{ content: `_ctx.a` }, `['b' + `, { content: `_ctx.c` }, `]`]
}
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `_ctx.a` },
`['b' + `,
{ content: `_ctx.c` },
`]`
]
}
}
]
})
})
@@ -208,21 +284,23 @@ describe('compiler: transform v-on', () => {
const { node } = parseWithVOn(`<div @click="e => foo(e)"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `e` },
` => `,
{ content: `_ctx.foo` },
`(`,
{ content: `e` },
`)`
]
}
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `e` },
` => `,
{ content: `_ctx.foo` },
`(`,
{ content: `e` },
`)`
]
}
}
]
})
})
@@ -250,6 +328,22 @@ describe('compiler: transform v-on', () => {
expect(onError).not.toHaveBeenCalled()
})
test('case conversion for vnode hooks', () => {
const { node } = parseWithVOn(`<div v-on:vnode-mounted="onMount"/>`)
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: {
content: `onVnodeMounted`
},
value: {
content: `onMount`
}
}
]
})
})
describe('cacheHandler', () => {
test('empty handler', () => {
const { root, node } = parseWithVOn(`<div v-on:click.prevent />`, {
@@ -257,10 +351,12 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true
})
expect(root.cached).toBe(1)
const args = (node.codegenNode as PlainElementCodegenNode).arguments
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(args.length).toBe(2)
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({
expect(vnodeCall.patchFlag).toBeUndefined()
expect(
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
@@ -276,10 +372,12 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true
})
expect(root.cached).toBe(1)
const args = (node.codegenNode as PlainElementCodegenNode).arguments
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(args.length).toBe(2)
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({
expect(vnodeCall.patchFlag).toBeUndefined()
expect(
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
@@ -295,10 +393,12 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true
})
expect(root.cached).toBe(1)
const args = (node.codegenNode as PlainElementCodegenNode).arguments
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(args.length).toBe(2)
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({
expect(vnodeCall.patchFlag).toBeUndefined()
expect(
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
@@ -314,15 +414,22 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true
})
expect(root.cached).toBe(1)
const args = (node.codegenNode as PlainElementCodegenNode).arguments
expect(root.cached).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(args.length).toBe(2)
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({
expect(vnodeCall.patchFlag).toBeUndefined()
expect(
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`$event => (`, { content: `_ctx.foo` }, `++`, `)`]
children: [
`$event => (`,
{ children: [{ content: `_ctx.foo` }, `++`] },
`)`
]
}
})
})

View File

@@ -1,5 +1,5 @@
import {
parse,
baseParse as parse,
transform,
NodeTypes,
generate,
@@ -7,11 +7,7 @@ import {
} from '../../src'
import { transformOnce } from '../../src/transforms/vOnce'
import { transformElement } from '../../src/transforms/transformElement'
import {
CREATE_VNODE,
RENDER_SLOT,
SET_BLOCK_TRACKING
} from '../../src/runtimeHelpers'
import { RENDER_SLOT, SET_BLOCK_TRACKING } from '../../src/runtimeHelpers'
import { transformBind } from '../../src/transforms/vBind'
import { transformSlotOutlet } from '../../src/transforms/transformSlotOutlet'
@@ -36,8 +32,8 @@ describe('compiler: v-once transform', () => {
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE
type: NodeTypes.VNODE_CALL,
tag: `"div"`
}
})
expect(generate(root).code).toMatchSnapshot()
@@ -51,8 +47,8 @@ describe('compiler: v-once transform', () => {
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE
type: NodeTypes.VNODE_CALL,
tag: `"div"`
}
})
expect(generate(root).code).toMatchSnapshot()
@@ -66,8 +62,8 @@ describe('compiler: v-once transform', () => {
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE
type: NodeTypes.VNODE_CALL,
tag: `_component_Comp`
}
})
expect(generate(root).code).toMatchSnapshot()
@@ -100,8 +96,8 @@ describe('compiler: v-once transform', () => {
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_VNODE
type: NodeTypes.VNODE_CALL,
tag: `"div"`
}
})
expect(generate(root).code).toMatchSnapshot()

View File

@@ -1,14 +1,15 @@
import {
CompilerOptions,
parse,
baseParse as parse,
transform,
generate,
ElementNode,
NodeTypes,
ErrorCodes,
ForNode,
CallExpression,
ComponentNode
ComponentNode,
VNodeCall,
SlotsExpression
} from '../../src'
import { transformElement } from '../../src/transforms/transformElement'
import { transformOn } from '../../src/transforms/vOn'
@@ -46,7 +47,8 @@ function parseWithSlots(template: string, options: CompilerOptions = {}) {
root: ast,
slots:
ast.children[0].type === NodeTypes.ELEMENT
? (ast.children[0].codegenNode as CallExpression).arguments[2]
? ((ast.children[0].codegenNode as VNodeCall)
.children as SlotsExpression)
: null
}
}
@@ -67,8 +69,8 @@ function createSlotMatcher(obj: Record<string, any>) {
} as any
})
.concat({
key: { content: `_compiled` },
value: { content: `true` }
key: { content: `_` },
value: { content: `1`, isStatic: false }
})
}
}
@@ -95,7 +97,7 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('explicit default slot', () => {
test('on-component default slot', () => {
const { root, slots } = parseWithSlots(
`<Comp v-slot="{ foo }">{{ foo }}{{ bar }}</Comp>`,
{ prefixIdentifiers: true }
@@ -128,7 +130,40 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('named slots', () => {
test('on component named slot', () => {
const { root, slots } = parseWithSlots(
`<Comp v-slot:named="{ foo }">{{ foo }}{{ bar }}</Comp>`,
{ prefixIdentifiers: true }
)
expect(slots).toMatchObject(
createSlotMatcher({
named: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`{ `, { content: `foo` }, ` }`]
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.bar`
}
}
]
}
})
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('template named slots', () => {
const { root, slots } = parseWithSlots(
`<Comp>
<template v-slot:one="{ foo }">
@@ -189,6 +224,76 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('on component dynamically named slot', () => {
const { root, slots } = parseWithSlots(
`<Comp v-slot:[named]="{ foo }">{{ foo }}{{ bar }}</Comp>`,
{ prefixIdentifiers: true }
)
expect(slots).toMatchObject(
createSlotMatcher({
'[_ctx.named]': {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`{ `, { content: `foo` }, ` }`]
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.bar`
}
}
]
}
})
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('named slots w/ implicit default slot', () => {
const { root, slots } = parseWithSlots(
`<Comp>
<template #one>foo</template>bar<span/>
</Comp>`
)
expect(slots).toMatchObject(
createSlotMatcher({
one: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: undefined,
returns: [
{
type: NodeTypes.TEXT,
content: `foo`
}
]
},
default: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: undefined,
returns: [
{
type: NodeTypes.TEXT,
content: `bar`
},
{
type: NodeTypes.ELEMENT,
tag: `span`
}
]
}
})
)
expect(generate(root).code).toMatchSnapshot()
})
test('dynamically named slots', () => {
const { root, slots } = parseWithSlots(
`<Comp>
@@ -274,43 +379,41 @@ describe('compiler: transform component slots', () => {
{
type: NodeTypes.ELEMENT,
codegenNode: {
type: NodeTypes.JS_CALL_EXPRESSION,
arguments: [
`_component_Inner`,
`null`,
createSlotMatcher({
default: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`{ `, { content: `bar` }, ` }`]
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `bar`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.baz`
}
type: NodeTypes.VNODE_CALL,
tag: `_component_Inner`,
props: undefined,
children: createSlotMatcher({
default: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`{ `, { content: `bar` }, ` }`]
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
]
}
}),
// nested slot should be forced dynamic, since scope variables
// are not tracked as dependencies of the slot.
genFlagText(PatchFlags.DYNAMIC_SLOTS)
]
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `bar`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.baz`
}
}
]
}
}),
// nested slot should be forced dynamic, since scope variables
// are not tracked as dependencies of the slot.
patchFlag: genFlagText(PatchFlags.DYNAMIC_SLOTS)
}
},
// test scope
@@ -351,8 +454,8 @@ describe('compiler: transform component slots', () => {
)
const div = ((root.children[0] as ForNode).children[0] as ElementNode)
.codegenNode as any
const comp = div.arguments[2][0]
expect(comp.codegenNode.arguments[3]).toBe(
const comp = div.children[0]
expect(comp.codegenNode.patchFlag).toBe(
genFlagText(PatchFlags.DYNAMIC_SLOTS)
)
})
@@ -364,12 +467,12 @@ describe('compiler: transform component slots', () => {
if (root.children[0].type === NodeTypes.FOR) {
const div = (root.children[0].children[0] as ElementNode)
.codegenNode as any
const comp = div.arguments[2][0]
flag = comp.codegenNode.arguments[3]
const comp = div.children[0]
flag = comp.codegenNode.patchFlag
} else {
const innerComp = (root.children[0] as ComponentNode)
.children[0] as ComponentNode
flag = (innerComp.codegenNode as CallExpression).arguments[3]
flag = (innerComp.codegenNode as VNodeCall).patchFlag
}
if (shouldForce) {
expect(flag).toBe(genFlagText(PatchFlags.DYNAMIC_SLOTS))
@@ -419,7 +522,7 @@ describe('compiler: transform component slots', () => {
callee: CREATE_SLOTS,
arguments: [
createObjectMatcher({
_compiled: `[true]`
_: `[1]`
}),
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
@@ -443,7 +546,7 @@ describe('compiler: transform component slots', () => {
}
]
})
expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
expect((root as any).children[0].codegenNode.patchFlag).toMatch(
PatchFlags.DYNAMIC_SLOTS + ''
)
expect(generate(root).code).toMatchSnapshot()
@@ -461,7 +564,7 @@ describe('compiler: transform component slots', () => {
callee: CREATE_SLOTS,
arguments: [
createObjectMatcher({
_compiled: `[true]`
_: `[1]`
}),
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
@@ -491,7 +594,7 @@ describe('compiler: transform component slots', () => {
}
]
})
expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
expect((root as any).children[0].codegenNode.patchFlag).toMatch(
PatchFlags.DYNAMIC_SLOTS + ''
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
@@ -510,7 +613,7 @@ describe('compiler: transform component slots', () => {
callee: CREATE_SLOTS,
arguments: [
createObjectMatcher({
_compiled: `[true]`
_: `[1]`
}),
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
@@ -551,7 +654,7 @@ describe('compiler: transform component slots', () => {
}
]
})
expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
expect((root as any).children[0].codegenNode.patchFlag).toMatch(
PatchFlags.DYNAMIC_SLOTS + ''
)
expect(generate(root).code).toMatchSnapshot()
@@ -569,7 +672,7 @@ describe('compiler: transform component slots', () => {
callee: CREATE_SLOTS,
arguments: [
createObjectMatcher({
_compiled: `[true]`
_: `[1]`
}),
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
@@ -601,20 +704,20 @@ describe('compiler: transform component slots', () => {
}
]
})
expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
expect((root as any).children[0].codegenNode.patchFlag).toMatch(
PatchFlags.DYNAMIC_SLOTS + ''
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
describe('errors', () => {
test('error on extraneous children w/ named slots', () => {
test('error on extraneous children w/ named default slot', () => {
const onError = jest.fn()
const source = `<Comp><template #default>foo</template>bar</Comp>`
parseWithSlots(source, { onError })
const index = source.indexOf('bar')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
code: ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
loc: {
source: `bar`,
start: {
@@ -699,28 +802,5 @@ describe('compiler: transform component slots', () => {
}
})
})
test('error on named slot on component', () => {
const onError = jest.fn()
const source = `<Comp v-slot:foo>foo</Comp>`
parseWithSlots(source, { onError })
const index = source.indexOf('v-slot')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT,
loc: {
source: `v-slot:foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 10,
line: 1,
column: index + 11
}
}
})
})
})
})

View File

@@ -1,15 +1,16 @@
{
"name": "@vue/compiler-core",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.11",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",
"types": "dist/compiler-core.d.ts",
"files": [
"index.js",
"dist"
],
"types": "dist/compiler-core.d.ts",
"buildOptions": {
"name": "VueCompilerCore",
"formats": [
"esm-bundler",
"cjs"
@@ -17,7 +18,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/vue.git"
"url": "git+https://github.com/vuejs/vue-next.git"
},
"keywords": [
"vue"
@@ -25,12 +26,14 @@
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/vue/issues"
"url": "https://github.com/vuejs/vue-next/issues"
},
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-core#readme",
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-core#readme",
"dependencies": {
"acorn": "^7.1.0",
"@vue/shared": "3.0.0-alpha.11",
"@babel/parser": "^7.8.6",
"@babel/types": "^7.8.6",
"estree-walker": "^0.8.1",
"source-map": "^0.7.3"
"source-map": "^0.6.1"
}
}

View File

@@ -1,17 +1,17 @@
import { isString } from '@vue/shared'
import { ForParseResult } from './transforms/vFor'
import {
CREATE_VNODE,
WITH_DIRECTIVES,
RENDER_SLOT,
CREATE_SLOTS,
RENDER_LIST,
OPEN_BLOCK,
CREATE_BLOCK,
FRAGMENT
FRAGMENT,
CREATE_VNODE,
WITH_DIRECTIVES
} from './runtimeHelpers'
import { PropsExpression } from './transforms/transformElement'
import { ImportItem } from './transform'
import { ImportItem, TransformContext } from './transform'
// Vue template is a platform-agnostic superset of HTML (syntax only).
// More namespaces like SVG and MathML are declared by platform specific
@@ -38,14 +38,22 @@ export const enum NodeTypes {
FOR,
TEXT_CALL,
// codegen
VNODE_CALL,
JS_CALL_EXPRESSION,
JS_OBJECT_EXPRESSION,
JS_PROPERTY,
JS_ARRAY_EXPRESSION,
JS_FUNCTION_EXPRESSION,
JS_SEQUENCE_EXPRESSION,
JS_CONDITIONAL_EXPRESSION,
JS_CACHE_EXPRESSION
JS_CACHE_EXPRESSION,
// ssr codegen
JS_BLOCK_STATEMENT,
JS_TEMPLATE_LITERAL,
JS_IF_STATEMENT,
JS_ASSIGNMENT_EXPRESSION,
JS_SEQUENCE_EXPRESSION,
JS_RETURN_STATEMENT
}
export const enum ElementTypes {
@@ -85,6 +93,7 @@ export type TemplateChildNode =
| TextNode
| CommentNode
| IfNode
| IfBranchNode
| ForNode
| TextCallNode
@@ -97,7 +106,9 @@ export interface RootNode extends Node {
hoists: JSChildNode[]
imports: ImportItem[]
cached: number
codegenNode: TemplateChildNode | JSChildNode | undefined
temps: number
ssrHelpers?: symbol[]
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined
}
export type ElementNode =
@@ -114,35 +125,40 @@ export interface BaseElementNode extends Node {
isSelfClosing: boolean
props: Array<AttributeNode | DirectiveNode>
children: TemplateChildNode[]
codegenNode:
| CallExpression
| SimpleExpressionNode
| CacheExpression
| undefined
}
export interface PlainElementNode extends BaseElementNode {
tagType: ElementTypes.ELEMENT
codegenNode:
| ElementCodegenNode
| undefined
| VNodeCall
| SimpleExpressionNode // when hoisted
| CacheExpression // when cached by v-once
| undefined
ssrCodegenNode?: TemplateLiteral
}
export interface ComponentNode extends BaseElementNode {
tagType: ElementTypes.COMPONENT
codegenNode: ComponentCodegenNode | undefined | CacheExpression // when cached by v-once
codegenNode:
| VNodeCall
| CacheExpression // when cached by v-once
| undefined
ssrCodegenNode?: CallExpression
}
export interface SlotOutletNode extends BaseElementNode {
tagType: ElementTypes.SLOT
codegenNode: SlotOutletCodegenNode | undefined | CacheExpression // when cached by v-once
codegenNode:
| RenderSlotCall
| CacheExpression // when cached by v-once
| undefined
ssrCodegenNode?: CallExpression
}
export interface TemplateNode extends BaseElementNode {
tagType: ElementTypes.TEMPLATE
codegenNode: ElementCodegenNode | undefined | CacheExpression
// TemplateNode is a container type that always gets compiled away
codegenNode: undefined
}
export interface TextNode extends Node {
@@ -190,6 +206,7 @@ export interface CompoundExpressionNode extends Node {
type: NodeTypes.COMPOUND_EXPRESSION
children: (
| SimpleExpressionNode
| CompoundExpressionNode
| InterpolationNode
| TextNode
| string
@@ -202,7 +219,7 @@ export interface CompoundExpressionNode extends Node {
export interface IfNode extends Node {
type: NodeTypes.IF
branches: IfBranchNode[]
codegenNode: IfCodegenNode
codegenNode?: IfConditionalExpression
}
export interface IfBranchNode extends Node {
@@ -217,28 +234,56 @@ export interface ForNode extends Node {
valueAlias: ExpressionNode | undefined
keyAlias: ExpressionNode | undefined
objectIndexAlias: ExpressionNode | undefined
parseResult: ForParseResult
children: TemplateChildNode[]
codegenNode: ForCodegenNode
codegenNode?: ForCodegenNode
}
export interface TextCallNode extends Node {
type: NodeTypes.TEXT_CALL
content: TextNode | InterpolationNode | CompoundExpressionNode
codegenNode: CallExpression
codegenNode: CallExpression | SimpleExpressionNode // when hoisted
}
export type TemplateTextChildNode =
| TextNode
| InterpolationNode
| CompoundExpressionNode
export interface VNodeCall extends Node {
type: NodeTypes.VNODE_CALL
tag: string | symbol | CallExpression
props: PropsExpression | undefined
children:
| TemplateChildNode[] // multiple children
| TemplateTextChildNode // single text child
| SlotsExpression // component slots
| ForRenderListExpression // v-for fragment call
| undefined
patchFlag: string | undefined
dynamicProps: string | undefined
directives: DirectiveArguments | undefined
isBlock: boolean
isForBlock: boolean
}
// JS Node Types ---------------------------------------------------------------
// We also include a number of JavaScript AST nodes for code generation.
// The AST is an intentionally minimal subset just to meet the exact needs of
// Vue render function generation.
export type JSChildNode =
| VNodeCall
| CallExpression
| ObjectExpression
| ArrayExpression
| ExpressionNode
| FunctionExpression
| ConditionalExpression
| SequenceExpression
| CacheExpression
| AssignmentExpression
| SequenceExpression
export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION
@@ -247,6 +292,7 @@ export interface CallExpression extends Node {
| string
| symbol
| JSChildNode
| SSRCodegenNode
| TemplateChildNode
| TemplateChildNode[])[]
}
@@ -269,21 +315,20 @@ export interface ArrayExpression extends Node {
export interface FunctionExpression extends Node {
type: NodeTypes.JS_FUNCTION_EXPRESSION
params: ExpressionNode | ExpressionNode[] | undefined
returns: TemplateChildNode | TemplateChildNode[] | JSChildNode
params: ExpressionNode | string | (ExpressionNode | string)[] | undefined
returns?: TemplateChildNode | TemplateChildNode[] | JSChildNode
body?: BlockStatement | IfStatement
newline: boolean
}
export interface SequenceExpression extends Node {
type: NodeTypes.JS_SEQUENCE_EXPRESSION
expressions: JSChildNode[]
// so that codegen knows it needs to generate ScopeId wrapper
isSlot: boolean
}
export interface ConditionalExpression extends Node {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION
test: ExpressionNode
test: JSChildNode
consequent: JSChildNode
alternate: JSChildNode
newline: boolean
}
export interface CacheExpression extends Node {
@@ -293,59 +338,76 @@ export interface CacheExpression extends Node {
isVNode: boolean
}
// SSR-specific Node Types -----------------------------------------------------
export type SSRCodegenNode =
| BlockStatement
| TemplateLiteral
| IfStatement
| AssignmentExpression
| ReturnStatement
| SequenceExpression
export interface BlockStatement extends Node {
type: NodeTypes.JS_BLOCK_STATEMENT
body: (JSChildNode | IfStatement)[]
}
export interface TemplateLiteral extends Node {
type: NodeTypes.JS_TEMPLATE_LITERAL
elements: (string | JSChildNode)[]
}
export interface IfStatement extends Node {
type: NodeTypes.JS_IF_STATEMENT
test: ExpressionNode
consequent: BlockStatement
alternate: IfStatement | BlockStatement | ReturnStatement | undefined
}
export interface AssignmentExpression extends Node {
type: NodeTypes.JS_ASSIGNMENT_EXPRESSION
left: SimpleExpressionNode
right: JSChildNode
}
export interface SequenceExpression extends Node {
type: NodeTypes.JS_SEQUENCE_EXPRESSION
expressions: JSChildNode[]
}
export interface ReturnStatement extends Node {
type: NodeTypes.JS_RETURN_STATEMENT
returns: TemplateChildNode | TemplateChildNode[] | JSChildNode
}
// Codegen Node Types ----------------------------------------------------------
// createVNode(...)
export interface PlainElementCodegenNode extends CallExpression {
callee: typeof CREATE_VNODE | typeof CREATE_BLOCK
arguments: // tag, props, children, patchFlag, dynamicProps
| [string | symbol]
| [string | symbol, PropsExpression]
| [string | symbol, 'null' | PropsExpression, TemplateChildNode[]]
| [
string | symbol,
'null' | PropsExpression,
'null' | TemplateChildNode[],
string
]
| [
string | symbol,
'null' | PropsExpression,
'null' | TemplateChildNode[],
string,
string
]
export interface DirectiveArguments extends ArrayExpression {
elements: DirectiveArgumentNode[]
}
export type ElementCodegenNode =
| PlainElementCodegenNode
| CodegenNodeWithDirective<PlainElementCodegenNode>
// createVNode(...)
export interface PlainComponentCodegenNode extends CallExpression {
callee: typeof CREATE_VNODE | typeof CREATE_BLOCK
arguments: // Comp, props, slots, patchFlag, dynamicProps
| [string | symbol]
| [string | symbol, PropsExpression]
| [string | symbol, 'null' | PropsExpression, SlotsExpression]
| [
string | symbol,
'null' | PropsExpression,
'null' | SlotsExpression,
string
]
| [
string | symbol,
'null' | PropsExpression,
'null' | SlotsExpression,
string,
string
]
export interface DirectiveArgumentNode extends ArrayExpression {
elements: // dir, exp, arg, modifiers
| [string]
| [string, ExpressionNode]
| [string, ExpressionNode, ExpressionNode]
| [string, ExpressionNode, ExpressionNode, ObjectExpression]
}
export type ComponentCodegenNode =
| PlainComponentCodegenNode
| CodegenNodeWithDirective<PlainComponentCodegenNode>
// renderSlot(...)
export interface RenderSlotCall extends CallExpression {
callee: typeof RENDER_SLOT
arguments: // $slots, name, props, fallback
| [string, string | ExpressionNode]
| [string, string | ExpressionNode, PropsExpression]
| [
string,
string | ExpressionNode,
PropsExpression | '{}',
TemplateChildNode[]
]
}
export type SlotsExpression = SlotsObjectExpression | DynamicSlotsExpression
@@ -397,63 +459,20 @@ export interface DynamicSlotFnProperty extends Property {
value: SlotFunctionExpression
}
// withDirectives(createVNode(...), [
// [_directive_foo, someValue],
// [_directive_bar, someValue, "arg", { mod: true }]
// ])
export interface CodegenNodeWithDirective<T extends CallExpression>
extends CallExpression {
callee: typeof WITH_DIRECTIVES
arguments: [T, DirectiveArguments]
}
export interface DirectiveArguments extends ArrayExpression {
elements: DirectiveArgumentNode[]
}
export interface DirectiveArgumentNode extends ArrayExpression {
elements: // dir, exp, arg, modifiers
| [string]
| [string, ExpressionNode]
| [string, ExpressionNode, ExpressionNode]
| [string, ExpressionNode, ExpressionNode, ObjectExpression]
}
// renderSlot(...)
export interface SlotOutletCodegenNode extends CallExpression {
callee: typeof RENDER_SLOT
arguments: // $slots, name, props, fallback
| [string, string | ExpressionNode]
| [string, string | ExpressionNode, PropsExpression]
| [
string,
string | ExpressionNode,
PropsExpression | '{}',
TemplateChildNode[]
]
}
export type BlockCodegenNode =
| ElementCodegenNode
| ComponentCodegenNode
| SlotOutletCodegenNode
export interface IfCodegenNode extends SequenceExpression {
expressions: [OpenBlockExpression, IfConditionalExpression]
}
export type BlockCodegenNode = VNodeCall | RenderSlotCall
export interface IfConditionalExpression extends ConditionalExpression {
consequent: BlockCodegenNode
alternate: BlockCodegenNode | IfConditionalExpression
}
export interface ForCodegenNode extends SequenceExpression {
expressions: [OpenBlockExpression, ForBlockCodegenNode]
}
export interface ForBlockCodegenNode extends CallExpression {
callee: typeof CREATE_BLOCK
arguments: [typeof FRAGMENT, 'null', ForRenderListExpression, string]
export interface ForCodegenNode extends VNodeCall {
isBlock: true
tag: typeof FRAGMENT
props: undefined
children: ForRenderListExpression
patchFlag: string
isForBlock: true
}
export interface ForRenderListExpression extends CallExpression {
@@ -465,11 +484,6 @@ export interface ForIteratorExpression extends FunctionExpression {
returns: BlockCodegenNode
}
export interface OpenBlockExpression extends CallExpression {
callee: typeof OPEN_BLOCK
arguments: []
}
// AST Utilities ---------------------------------------------------------------
// Some expressions, e.g. sequence and conditional expressions, are never
@@ -481,6 +495,63 @@ export const locStub: SourceLocation = {
end: { line: 1, column: 1, offset: 0 }
}
export function createRoot(
children: TemplateChildNode[],
loc = locStub
): RootNode {
return {
type: NodeTypes.ROOT,
children,
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc
}
}
export function createVNodeCall(
context: TransformContext | null,
tag: VNodeCall['tag'],
props?: VNodeCall['props'],
children?: VNodeCall['children'],
patchFlag?: VNodeCall['patchFlag'],
dynamicProps?: VNodeCall['dynamicProps'],
directives?: VNodeCall['directives'],
isBlock: VNodeCall['isBlock'] = false,
isForBlock: VNodeCall['isForBlock'] = false,
loc = locStub
): VNodeCall {
if (context) {
if (isBlock) {
context.helper(OPEN_BLOCK)
context.helper(CREATE_BLOCK)
} else {
context.helper(CREATE_VNODE)
}
if (directives) {
context.helper(WITH_DIRECTIVES)
}
}
return {
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
isForBlock,
loc
}
}
export function createArrayExpression(
elements: ArrayExpression['elements'],
loc: SourceLocation = locStub
@@ -554,15 +625,9 @@ export function createCompoundExpression(
}
}
type InferCodegenNodeType<T> = T extends
| typeof CREATE_VNODE
| typeof CREATE_BLOCK
? PlainElementCodegenNode | PlainComponentCodegenNode
: T extends typeof WITH_DIRECTIVES
?
| CodegenNodeWithDirective<PlainElementCodegenNode>
| CodegenNodeWithDirective<PlainComponentCodegenNode>
: T extends typeof RENDER_SLOT ? SlotOutletCodegenNode : CallExpression
type InferCodegenNodeType<T> = T extends typeof RENDER_SLOT
? RenderSlotCall
: CallExpression
export function createCallExpression<T extends CallExpression['callee']>(
callee: T,
@@ -579,8 +644,9 @@ export function createCallExpression<T extends CallExpression['callee']>(
export function createFunctionExpression(
params: FunctionExpression['params'],
returns: FunctionExpression['returns'],
returns: FunctionExpression['returns'] = undefined,
newline: boolean = false,
isSlot: boolean = false,
loc: SourceLocation = locStub
): FunctionExpression {
return {
@@ -588,30 +654,23 @@ export function createFunctionExpression(
params,
returns,
newline,
isSlot,
loc
}
}
export function createSequenceExpression(
expressions: SequenceExpression['expressions']
): SequenceExpression {
return {
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions,
loc: locStub
}
}
export function createConditionalExpression(
test: ConditionalExpression['test'],
consequent: ConditionalExpression['consequent'],
alternate: ConditionalExpression['alternate']
alternate: ConditionalExpression['alternate'],
newline = true
): ConditionalExpression {
return {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test,
consequent,
alternate,
newline,
loc: locStub
}
}
@@ -629,3 +688,69 @@ export function createCacheExpression(
loc: locStub
}
}
export function createBlockStatement(
body: BlockStatement['body']
): BlockStatement {
return {
type: NodeTypes.JS_BLOCK_STATEMENT,
body,
loc: locStub
}
}
export function createTemplateLiteral(
elements: TemplateLiteral['elements']
): TemplateLiteral {
return {
type: NodeTypes.JS_TEMPLATE_LITERAL,
elements,
loc: locStub
}
}
export function createIfStatement(
test: IfStatement['test'],
consequent: IfStatement['consequent'],
alternate?: IfStatement['alternate']
): IfStatement {
return {
type: NodeTypes.JS_IF_STATEMENT,
test,
consequent,
alternate,
loc: locStub
}
}
export function createAssignmentExpression(
left: AssignmentExpression['left'],
right: AssignmentExpression['right']
): AssignmentExpression {
return {
type: NodeTypes.JS_ASSIGNMENT_EXPRESSION,
left,
right,
loc: locStub
}
}
export function createSequenceExpression(
expressions: SequenceExpression['expressions']
): SequenceExpression {
return {
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions,
loc: locStub
}
}
export function createReturnStatement(
returns: ReturnStatement['returns']
): ReturnStatement {
return {
type: NodeTypes.JS_RETURN_STATEMENT,
returns,
loc: locStub
}
}

View File

@@ -10,15 +10,21 @@ import {
CallExpression,
ArrayExpression,
ObjectExpression,
SourceLocation,
Position,
InterpolationNode,
CompoundExpressionNode,
SimpleExpressionNode,
FunctionExpression,
SequenceExpression,
ConditionalExpression,
CacheExpression
CacheExpression,
locStub,
SSRCodegenNode,
TemplateLiteral,
IfStatement,
AssignmentExpression,
ReturnStatement,
VNodeCall,
SequenceExpression
} from './ast'
import { SourceMapGenerator, RawSourceMap } from 'source-map'
import {
@@ -31,17 +37,25 @@ import {
import { isString, isArray, isSymbol } from '@vue/shared'
import {
helperNameMap,
TO_STRING,
TO_DISPLAY_STRING,
CREATE_VNODE,
RESOLVE_COMPONENT,
RESOLVE_DIRECTIVE,
SET_BLOCK_TRACKING,
CREATE_COMMENT,
CREATE_TEXT
CREATE_TEXT,
PUSH_SCOPE_ID,
POP_SCOPE_ID,
WITH_SCOPE_ID,
WITH_DIRECTIVES,
CREATE_BLOCK,
OPEN_BLOCK,
CREATE_STATIC,
WITH_CTX
} from './runtimeHelpers'
import { ImportItem } from './transform'
type CodegenNode = TemplateChildNode | JSChildNode
type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode
export interface CodegenResult {
code: string
@@ -58,8 +72,7 @@ export interface CodegenContext extends Required<CodegenOptions> {
indentLevel: number
map?: SourceMapGenerator
helper(key: symbol): string
push(code: string, node?: CodegenNode, openOnly?: boolean): void
resetMapping(loc: SourceLocation): void
push(code: string, node?: CodegenNode): void
indent(): void
deindent(withoutNewLine?: boolean): void
newline(): void
@@ -71,7 +84,12 @@ function createCodegenContext(
mode = 'function',
prefixIdentifiers = mode === 'module',
sourceMap = false,
filename = `template.vue.html`
filename = `template.vue.html`,
scopeId = null,
optimizeBindings = false,
runtimeGlobalName = `Vue`,
runtimeModuleName = `vue`,
ssr = false
}: CodegenOptions
): CodegenContext {
const context: CodegenContext = {
@@ -79,24 +97,22 @@ function createCodegenContext(
prefixIdentifiers,
sourceMap,
filename,
scopeId,
optimizeBindings,
runtimeGlobalName,
runtimeModuleName,
ssr,
source: ast.loc.source,
code: ``,
column: 1,
line: 1,
offset: 0,
indentLevel: 0,
// lazy require source-map implementation, only in non-browser builds!
map:
__BROWSER__ || !sourceMap
? undefined
: new (loadDep('source-map')).SourceMapGenerator(),
map: undefined,
helper(key) {
const name = helperNameMap[key]
return prefixIdentifiers ? name : `_${name}`
return `_${helperNameMap[key]}`
},
push(code, node, openOnly) {
push(code, node) {
context.code += code
if (!__BROWSER__ && context.map) {
if (node) {
@@ -110,16 +126,11 @@ function createCodegenContext(
addMapping(node.loc.start, name)
}
advancePositionWithMutation(context, code)
if (node && !openOnly) {
if (node && node.loc !== locStub) {
addMapping(node.loc.end)
}
}
},
resetMapping(loc: SourceLocation) {
if (!__BROWSER__ && context.map) {
addMapping(loc.start)
}
},
indent() {
newline(++context.indentLevel)
},
@@ -154,9 +165,12 @@ function createCodegenContext(
})
}
if (!__BROWSER__ && context.map) {
context.map.setSourceContent(filename, context.source)
if (!__BROWSER__ && sourceMap) {
// lazy require source-map implementation, only in non-browser builds
context.map = new (loadDep('source-map')).SourceMapGenerator()
context.map!.setSourceContent(filename, context.source)
}
return context
}
@@ -168,63 +182,37 @@ export function generate(
const {
mode,
push,
helper,
prefixIdentifiers,
indent,
deindent,
newline
newline,
scopeId,
ssr
} = context
const hasHelpers = ast.helpers.length > 0
const useWithBlock = !prefixIdentifiers && mode !== 'module'
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
// preambles
if (mode === 'function') {
// Generate const declaration for helpers
// In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the
// with block so it doesn't incur the `in` check cost for every helper access.
if (hasHelpers) {
if (prefixIdentifiers) {
push(`const { ${ast.helpers.map(helper).join(', ')} } = Vue\n`)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = Vue\n`)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
if (ast.hoists.length) {
const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
.filter(helper => ast.helpers.includes(helper))
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')
push(`const { ${staticHelpers} } = Vue\n`)
}
}
}
genHoists(ast.hoists, context)
newline()
push(`return `)
if (!__BROWSER__ && mode === 'module') {
genModulePreamble(ast, context, genScopeId)
} else {
// generate import statements for helpers
if (hasHelpers) {
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
}
if (ast.imports.length) {
genImports(ast.imports, context)
newline()
}
genHoists(ast.hoists, context)
newline()
push(`export default `)
genFunctionPreamble(ast, context)
}
// enter render function
push(`function render() {`)
if (genScopeId && !ssr) {
push(`const render = _withId(`)
}
if (!ssr) {
push(`function render(_ctx, _cache) {`)
} else {
push(`function ssrRender(_ctx, _push, _parent) {`)
}
indent()
if (useWithBlock) {
push(`with (this) {`)
push(`with (_ctx) {`)
indent()
// function mode const declarations should be inside with block
// also they should be renamed to avoid collision with user properties
@@ -234,35 +222,39 @@ export function generate(
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')} } = _Vue`
)
newline()
if (ast.cached > 0) {
push(`const _cache = $cache`)
newline()
}
push(`\n`)
newline()
}
} else {
push(`const _ctx = this`)
if (ast.cached > 0) {
newline()
push(`const _cache = _ctx.$cache`)
}
newline()
}
// generate asset resolution statements
if (ast.components.length) {
genAssets(ast.components, 'component', context)
if (ast.directives.length || ast.temps > 0) {
newline()
}
}
if (ast.directives.length) {
genAssets(ast.directives, 'directive', context)
if (ast.temps > 0) {
newline()
}
}
if (ast.components.length || ast.directives.length) {
if (ast.temps > 0) {
push(`let `)
for (let i = 0; i < ast.temps; i++) {
push(`${i > 0 ? `, ` : ``}_temp${i}`)
}
}
if (ast.components.length || ast.directives.length || ast.temps) {
push(`\n`)
newline()
}
// generate the VNode tree expression
push(`return `)
if (!ssr) {
push(`return `)
}
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
@@ -276,27 +268,164 @@ export function generate(
deindent()
push(`}`)
if (genScopeId && !ssr) {
push(`)`)
}
return {
ast,
code: context.code,
map: context.map ? context.map.toJSON() : undefined
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
}
}
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const {
ssr,
prefixIdentifiers,
push,
newline,
runtimeModuleName,
runtimeGlobalName
} = context
const VueBinding =
!__BROWSER__ && ssr
? `require(${JSON.stringify(runtimeModuleName)})`
: runtimeGlobalName
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
// Generate const declaration for helpers
// In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the
// with block so it doesn't incur the `in` check cost for every helper access.
if (ast.helpers.length > 0) {
if (!__BROWSER__ && prefixIdentifiers) {
push(
`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`
)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = ${VueBinding}\n`)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
if (ast.hoists.length) {
const staticHelpers = [
CREATE_VNODE,
CREATE_COMMENT,
CREATE_TEXT,
CREATE_STATIC
]
.filter(helper => ast.helpers.includes(helper))
.map(aliasHelper)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
}
}
// generate variables for ssr helpers
if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
// ssr guaruntees prefixIdentifier: true
push(
`const { ${ast.ssrHelpers
.map(aliasHelper)
.join(', ')} } = require("@vue/server-renderer")\n`
)
}
genHoists(ast.hoists, context)
newline()
push(`return `)
}
function genModulePreamble(
ast: RootNode,
context: CodegenContext,
genScopeId: boolean
) {
const {
push,
helper,
newline,
scopeId,
optimizeBindings,
runtimeModuleName
} = context
if (genScopeId) {
ast.helpers.push(WITH_SCOPE_ID)
if (ast.hoists.length) {
ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
}
}
// generate import statements for helpers
if (ast.helpers.length) {
if (optimizeBindings) {
// when bundled with webpack with code-split, calling an import binding
// as a function leads to it being wrapped with `Object(a.b)` or `(0,a.b)`,
// incurring both payload size increase and potential perf overhead.
// therefore we assign the imports to vairables (which is a constant ~50b
// cost per-component instead of scaling with template size)
push(
`import { ${ast.helpers
.map(s => helperNameMap[s])
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
)
push(
`\n// Binding optimization for webpack code-split\nconst ${ast.helpers
.map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
.join(', ')}\n`
)
} else {
push(
`import { ${ast.helpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
)
}
}
if (ast.ssrHelpers && ast.ssrHelpers.length) {
push(
`import { ${ast.ssrHelpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from "@vue/server-renderer"\n`
)
}
if (ast.imports.length) {
genImports(ast.imports, context)
newline()
}
if (genScopeId) {
push(`const _withId = ${helper(WITH_SCOPE_ID)}("${scopeId}")`)
newline()
}
genHoists(ast.hoists, context)
newline()
push(`export `)
}
function genAssets(
assets: string[],
type: 'component' | 'directive',
context: CodegenContext
{ helper, push, newline }: CodegenContext
) {
const resolver = context.helper(
const resolver = helper(
type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE
)
for (let i = 0; i < assets.length; i++) {
const id = assets[i]
context.push(
push(
`const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)})`
)
context.newline()
if (i < assets.length - 1) {
newline()
}
}
}
@@ -304,12 +433,27 @@ function genHoists(hoists: JSChildNode[], context: CodegenContext) {
if (!hoists.length) {
return
}
context.newline()
const { push, newline, helper, scopeId, mode } = context
const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function'
newline()
// push scope Id before initilaizing hoisted vnodes so that these vnodes
// get the proper scopeId as well.
if (genScopeId) {
push(`${helper(PUSH_SCOPE_ID)}("${scopeId}")`)
newline()
}
hoists.forEach((exp, i) => {
context.push(`const _hoisted_${i + 1} = `)
push(`const _hoisted_${i + 1} = `)
genNode(exp, context)
context.newline()
newline()
})
if (genScopeId) {
push(`${helper(POP_SCOPE_ID)}()`)
newline()
}
}
function genImports(importsOptions: ImportItem[], context: CodegenContext) {
@@ -351,7 +495,8 @@ function genNodeListAsArray(
function genNodeList(
nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
context: CodegenContext,
multilines: boolean = false
multilines: boolean = false,
comma: boolean = true
) {
const { push, newline } = context
for (let i = 0; i < nodes.length; i++) {
@@ -365,10 +510,10 @@ function genNodeList(
}
if (i < nodes.length - 1) {
if (multilines) {
push(',')
comma && push(',')
newline()
} else {
push(', ')
comma && push(', ')
}
}
}
@@ -413,6 +558,10 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.COMMENT:
genComment(node, context)
break
case NodeTypes.VNODE_CALL:
genVNodeCall(node, context)
break
case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context)
break
@@ -425,16 +574,37 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.JS_FUNCTION_EXPRESSION:
genFunctionExpression(node, context)
break
case NodeTypes.JS_SEQUENCE_EXPRESSION:
genSequenceExpression(node, context)
break
case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context)
break
case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context)
break
// SSR only types
case NodeTypes.JS_BLOCK_STATEMENT:
!__BROWSER__ && genNodeList(node.body, context, true, false)
break
case NodeTypes.JS_TEMPLATE_LITERAL:
!__BROWSER__ && genTemplateLiteral(node, context)
break
case NodeTypes.JS_IF_STATEMENT:
!__BROWSER__ && genIfStatement(node, context)
break
case NodeTypes.JS_ASSIGNMENT_EXPRESSION:
!__BROWSER__ && genAssignmentExpression(node, context)
break
case NodeTypes.JS_SEQUENCE_EXPRESSION:
!__BROWSER__ && genSequenceExpression(node, context)
break
case NodeTypes.JS_RETURN_STATEMENT:
!__BROWSER__ && genReturnStatement(node, context)
break
/* istanbul ignore next */
case NodeTypes.IF_BRANCH:
// noop
break
default:
if (__DEV__) {
assert(false, `unhandled codegen node type: ${(node as any).type}`)
@@ -459,7 +629,7 @@ function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
function genInterpolation(node: InterpolationNode, context: CodegenContext) {
const { push, helper } = context
push(`${helper(TO_STRING)}(`)
push(`${helper(TO_DISPLAY_STRING)}(`)
genNode(node.content, context)
push(`)`)
}
@@ -505,18 +675,60 @@ function genComment(node: CommentNode, context: CodegenContext) {
}
}
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper } = context
const {
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
isForBlock
} = node
if (directives) {
push(helper(WITH_DIRECTIVES) + `(`)
}
if (isBlock) {
push(`(${helper(OPEN_BLOCK)}(${isForBlock ? `true` : ``}), `)
}
push(helper(isBlock ? CREATE_BLOCK : CREATE_VNODE) + `(`, node)
genNodeList(
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
)
push(`)`)
if (isBlock) {
push(`)`)
}
if (directives) {
push(`, `)
genNode(directives, context)
push(`)`)
}
}
function genNullableArgs(args: any[]): CallExpression['arguments'] {
let i = args.length
while (i--) {
if (args[i] != null) break
}
return args.slice(0, i + 1).map(arg => arg || `null`)
}
// JavaScript
function genCallExpression(node: CallExpression, context: CodegenContext) {
const callee = isString(node.callee)
? node.callee
: context.helper(node.callee)
context.push(callee + `(`, node, true)
context.push(callee + `(`, node)
genNodeList(node.arguments, context)
context.push(`)`)
}
function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
const { push, indent, deindent, newline, resetMapping } = context
const { push, indent, deindent, newline } = context
const { properties } = node
if (!properties.length) {
push(`{}`, node)
@@ -529,8 +741,7 @@ function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
push(multilines ? `{` : `{ `)
multilines && indent()
for (let i = 0; i < properties.length; i++) {
const { key, value, loc } = properties[i]
resetMapping(loc) // reset source mapping for every property.
const { key, value } = properties[i]
// key
genExpressionAsPropertyKey(key, context)
push(`: `)
@@ -554,8 +765,17 @@ function genFunctionExpression(
node: FunctionExpression,
context: CodegenContext
) {
const { push, indent, deindent } = context
const { params, returns, newline } = node
const { push, indent, deindent, scopeId, mode } = context
const { params, returns, body, newline, isSlot } = node
// slot functions also need to push scopeId before rendering its content
const genScopeId =
!__BROWSER__ && isSlot && scopeId != null && mode !== 'function'
if (genScopeId) {
push(`_withId(`)
} else if (isSlot) {
push(`_${helperNameMap[WITH_CTX]}(`)
}
push(`(`, node)
if (isArray(params)) {
genNodeList(params, context)
@@ -563,27 +783,36 @@ function genFunctionExpression(
genNode(params, context)
}
push(`) => `)
if (newline) {
if (newline || body) {
push(`{`)
indent()
push(`return `)
}
if (isArray(returns)) {
genNodeListAsArray(returns, context)
} else {
genNode(returns, context)
if (returns) {
if (newline) {
push(`return `)
}
if (isArray(returns)) {
genNodeListAsArray(returns, context)
} else {
genNode(returns, context)
}
} else if (body) {
genNode(body, context)
}
if (newline) {
if (newline || body) {
deindent()
push(`}`)
}
if (genScopeId || isSlot) {
push(`)`)
}
}
function genConditionalExpression(
node: ConditionalExpression,
context: CodegenContext
) {
const { test, consequent, alternate } = node
const { test, consequent, alternate, newline: needNewline } = node
const { push, indent, deindent, newline } = context
if (test.type === NodeTypes.SIMPLE_EXPRESSION) {
const needsParens = !isSimpleIdentifier(test.content)
@@ -592,15 +821,17 @@ function genConditionalExpression(
needsParens && push(`)`)
} else {
push(`(`)
genCompoundExpression(test, context)
genNode(test, context)
push(`)`)
}
indent()
needNewline && indent()
context.indentLevel++
needNewline || push(` `)
push(`? `)
genNode(consequent, context)
context.indentLevel--
newline()
needNewline && newline()
needNewline || push(` `)
push(`: `)
const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
if (!isNested) {
@@ -610,16 +841,7 @@ function genConditionalExpression(
if (!isNested) {
context.indentLevel--
}
deindent(true /* without newline */)
}
function genSequenceExpression(
node: SequenceExpression,
context: CodegenContext
) {
context.push(`(`)
genNodeList(node.expressions, context)
context.push(`)`)
needNewline && deindent(true /* without newline */)
}
function genCacheExpression(node: CacheExpression, context: CodegenContext) {
@@ -642,3 +864,77 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
}
push(`)`)
}
function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) {
const { push, indent, deindent } = context
push('`')
const l = node.elements.length
const multilines = l > 3
for (let i = 0; i < l; i++) {
const e = node.elements[i]
if (isString(e)) {
push(e.replace(/`/g, '\\`'))
} else {
push('${')
if (multilines) indent()
genNode(e, context)
if (multilines) deindent()
push('}')
}
}
push('`')
}
function genIfStatement(node: IfStatement, context: CodegenContext) {
const { push, indent, deindent } = context
const { test, consequent, alternate } = node
push(`if (`)
genNode(test, context)
push(`) {`)
indent()
genNode(consequent, context)
deindent()
push(`}`)
if (alternate) {
push(` else `)
if (alternate.type === NodeTypes.JS_IF_STATEMENT) {
genIfStatement(alternate, context)
} else {
push(`{`)
indent()
genNode(alternate, context)
deindent()
push(`}`)
}
}
}
function genAssignmentExpression(
node: AssignmentExpression,
context: CodegenContext
) {
genNode(node.left, context)
context.push(` = `)
genNode(node.right, context)
}
function genSequenceExpression(
node: SequenceExpression,
context: CodegenContext
) {
context.push(`(`)
genNodeList(node.expressions, context)
context.push(`)`)
}
function genReturnStatement(
{ returns }: ReturnStatement,
context: CodegenContext
) {
context.push(`return `)
if (isArray(returns)) {
genNodeListAsArray(returns, context)
} else {
genNode(returns, context)
}
}

View File

@@ -0,0 +1,100 @@
import { CompilerOptions } from './options'
import { baseParse } from './parse'
import { transform, NodeTransform, DirectiveTransform } from './transform'
import { generate, CodegenResult } from './codegen'
import { RootNode } from './ast'
import { isString } from '@vue/shared'
import { transformIf } from './transforms/vIf'
import { transformFor } from './transforms/vFor'
import { transformExpression } from './transforms/transformExpression'
import { transformSlotOutlet } from './transforms/transformSlotOutlet'
import { transformElement } from './transforms/transformElement'
import { transformOn } from './transforms/vOn'
import { transformBind } from './transforms/vBind'
import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
import { transformText } from './transforms/transformText'
import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
export type TransformPreset = [
NodeTransform[],
Record<string, DirectiveTransform>
]
export function getBaseTransformPreset(
prefixIdentifiers?: boolean
): TransformPreset {
return [
[
transformOnce,
transformIf,
transformFor,
...(!__BROWSER__ && prefixIdentifiers
? [
// order is important
trackVForSlotScopes,
transformExpression
]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
]
}
// we name it `baseCompile` so that higher order compilers like
// @vue/compiler-dom can export `compile` while re-exporting everything else.
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
const onError = options.onError || defaultOnError
const isModuleMode = options.mode === 'module'
/* istanbul ignore if */
if (__BROWSER__) {
if (options.prefixIdentifiers === true) {
onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
} else if (isModuleMode) {
onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
}
}
const prefixIdentifiers =
!__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
if (!prefixIdentifiers && options.cacheHandlers) {
onError(createCompilerError(ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED))
}
if (options.scopeId && !isModuleMode) {
onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
}
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
prefixIdentifiers
)
transform(ast, {
...options,
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: {
...directiveTransforms,
...(options.directiveTransforms || {}) // user transforms
}
})
return generate(ast, {
...options,
prefixIdentifiers
})
}

View File

@@ -16,11 +16,14 @@ export function defaultOnError(error: CompilerError) {
export function createCompilerError<T extends number>(
code: T,
loc?: SourceLocation,
messages?: { [code: number]: string }
messages?: { [code: number]: string },
additionalMessage?: string
): T extends ErrorCodes ? CoreCompilerError : CompilerError {
const msg = __DEV__ || !__BROWSER__ ? (messages || errorMessages)[code] : code
const locInfo = loc ? ` (${loc.start.line}:${loc.start.column})` : ``
const error = new SyntaxError(msg + locInfo) as CompilerError
const msg =
__DEV__ || !__BROWSER__
? (messages || errorMessages)[code] + (additionalMessage || ``)
: code
const error = new SyntaxError(String(msg)) as CompilerError
error.code = code
error.loc = loc
return error as any
@@ -58,7 +61,6 @@ export const enum ErrorCodes {
UNEXPECTED_NULL_CHARACTER,
UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
UNEXPECTED_SOLIDUS_IN_TAG,
UNKNOWN_NAMED_CHARACTER_REFERENCE,
// Vue-specific parse errors
X_INVALID_END_TAG,
@@ -74,19 +76,21 @@ export const enum ErrorCodes {
X_V_BIND_NO_EXPRESSION,
X_V_ON_NO_EXPRESSION,
X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
X_V_SLOT_NAMED_SLOT_ON_COMPONENT,
X_V_SLOT_MIXED_SLOT_USAGE,
X_V_SLOT_DUPLICATE_SLOT_NAMES,
X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
X_V_SLOT_MISPLACED,
X_V_MODEL_NO_EXPRESSION,
X_V_MODEL_MALFORMED_EXPRESSION,
X_V_MODEL_ON_SCOPE_VARIABLE,
X_INVALID_EXPRESSION,
X_KEEP_ALIVE_INVALID_CHILDREN,
// generic errors
X_PREFIX_ID_NOT_SUPPORTED,
X_MODULE_MODE_NOT_SUPPORTED,
X_CACHE_HANDLER_NOT_SUPPORTED,
X_SCOPE_ID_NOT_SUPPORTED,
// Special value for higher-order compilers to pick up the last code
// to avoid collision of error codes. This should always be kept as the last
@@ -140,11 +144,10 @@ export const errorMessages: { [code: number]: string } = {
[ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME]:
"'<?' is allowed only in XML context.",
[ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.",
[ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE]: 'Unknown entity name.',
// Vue-specific parse errors
[ErrorCodes.X_INVALID_END_TAG]: 'Invalid end tag.',
[ErrorCodes.X_MISSING_END_TAG]: 'End tag was not found.',
[ErrorCodes.X_MISSING_END_TAG]: 'Element is missing end tag.',
[ErrorCodes.X_MISSING_INTERPOLATION_END]:
'Interpolation end sign was not found.',
[ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END]:
@@ -159,24 +162,24 @@ export const errorMessages: { [code: number]: string } = {
[ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
[ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression.`,
[ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`,
[ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT]:
`Named v-slot on component. ` +
`Named slots should use <template v-slot> syntax nested inside the component.`,
[ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE]:
`Mixed v-slot usage on both the component and nested <template>.` +
`The default slot should also use <template> syntax when there are other ` +
`named slots to avoid scope ambiguity.`,
`When there are multiple named slots, all slots should use <template> ` +
`syntax to avoid scope ambiguity.`,
[ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES]: `Duplicate slot names found. `,
[ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN]:
`Extraneous children found when component has explicit slots. ` +
`These children will be ignored.`,
[ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN]:
`Extraneous children found when component already has explicitly named ` +
`default slot. These children will be ignored.`,
[ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
[ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
[ErrorCodes.X_V_MODEL_ON_SCOPE_VARIABLE]: `v-model cannot be used on v-for or v-slot scope variables because they are not writable.`,
[ErrorCodes.X_INVALID_EXPRESSION]: `Invalid JavaScript expression.`,
[ErrorCodes.X_INVALID_EXPRESSION]: `Error parsing JavaScript expression: `,
[ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: `<KeepAlive> expects exactly one child component.`,
// generic errors
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
[ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`
[ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`,
[ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED]: `"cacheHandlers" option is only supported when the "prefixIdentifiers" option is enabled.`,
[ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED]: `"scopeId" option is only supported in module mode.`
}

View File

@@ -1,90 +1,20 @@
import { CompilerOptions } from './options'
import { parse } from './parse'
import { transform } from './transform'
import { generate, CodegenResult } from './codegen'
import { RootNode } from './ast'
import { isString } from '@vue/shared'
import { transformIf } from './transforms/vIf'
import { transformFor } from './transforms/vFor'
import { transformExpression } from './transforms/transformExpression'
import { transformSlotOutlet } from './transforms/transformSlotOutlet'
import { transformElement } from './transforms/transformElement'
import { transformOn } from './transforms/vOn'
import { transformBind } from './transforms/vBind'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
import { transformText } from './transforms/transformText'
import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel'
// we name it `baseCompile` so that higher order compilers like @vue/compiler-dom
// can export `compile` while re-exporting everything else.
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
/* istanbul ignore if */
if (__BROWSER__) {
const onError = options.onError || defaultOnError
if (options.prefixIdentifiers === true) {
onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
} else if (options.mode === 'module') {
onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
}
}
const ast = isString(template) ? parse(template, options) : template
const prefixIdentifiers =
!__BROWSER__ &&
(options.prefixIdentifiers === true || options.mode === 'module')
transform(ast, {
...options,
prefixIdentifiers,
nodeTransforms: [
transformOnce,
transformIf,
transformFor,
...(prefixIdentifiers
? [
// order is important
trackVForSlotScopes,
transformExpression
]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: {
on: transformOn,
bind: transformBind,
model: transformModel,
...(options.directiveTransforms || {}) // user transforms
}
})
return generate(ast, {
...options,
prefixIdentifiers
})
}
export { baseCompile } from './compile'
// Also expose lower level APIs & types
export {
CompilerOptions,
ParserOptions,
TransformOptions,
CodegenOptions
CodegenOptions,
HoistTransform
} from './options'
export { parse, TextModes } from './parse'
export { baseParse, TextModes } from './parse'
export {
transform,
createStructuralDirectiveTransform,
TransformContext,
createTransformContext,
traverseNode,
createStructuralDirectiveTransform,
NodeTransform,
StructuralDirectiveTransform,
DirectiveTransform
@@ -96,19 +26,32 @@ export {
CompilerError,
createCompilerError
} from './errors'
export * from './ast'
export * from './utils'
export { registerRuntimeHelpers } from './runtimeHelpers'
export * from './runtimeHelpers'
// expose transforms so higher-order compilers can import and extend them
export { getBaseTransformPreset, TransformPreset } from './compile'
export { transformModel } from './transforms/vModel'
export { transformOn } from './transforms/vOn'
// utility, but need to rewrite typing to avoid dts relying on @vue/shared
import { generateCodeFrame as _genCodeFrame } from '@vue/shared'
const generateCodeFrame = _genCodeFrame as (
source: string,
start?: number,
end?: number
) => string
export { generateCodeFrame }
export { transformBind } from './transforms/vBind'
export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
export { processIf } from './transforms/vIf'
export { processFor, createForLoopParams } from './transforms/vFor'
export {
transformExpression,
processExpression
} from './transforms/transformExpression'
export {
buildSlots,
SlotFnBuilder,
trackVForSlotScopes,
trackSlotScopes
} from './transforms/vSlot'
export {
transformElement,
resolveComponentType,
buildProps
} from './transforms/transformElement'
export { processSlotOutlet } from './transforms/transformSlotOutlet'
export { generateCodeFrame } from '@vue/shared'

View File

@@ -1,16 +1,30 @@
import { ElementNode, Namespace } from './ast'
import { ElementNode, Namespace, JSChildNode, PlainElementNode } from './ast'
import { TextModes } from './parse'
import { CompilerError } from './errors'
import { NodeTransform, DirectiveTransform } from './transform'
import {
NodeTransform,
DirectiveTransform,
TransformContext
} from './transform'
import { ParserPlugin } from '@babel/parser'
export interface ParserOptions {
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
isNativeTag?: (tag: string) => boolean // e.g. loading-indicator in weex
isPreTag?: (tag: string) => boolean // e.g. <pre> where whitespace is intact
isCustomElement?: (tag: string) => boolean
// e.g. platform native elements, e.g. <div> for browsers
isNativeTag?: (tag: string) => boolean
// e.g. native elements that can self-close, e.g. <img>, <br>, <hr>
isVoidTag?: (tag: string) => boolean
// e.g. elements that should preserve whitespace inside, e.g. <pre>
isPreTag?: (tag: string) => boolean
// platform-specific built-in components e.g. <Transition>
isBuiltInComponent?: (tag: string) => symbol | void
// separate option for end users to extend the native elements list
isCustomElement?: (tag: string) => boolean
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
getTextMode?: (tag: string, ns: Namespace) => TextModes
getTextMode?: (
tag: string,
ns: Namespace,
parent: ElementNode | undefined
) => TextModes
delimiters?: [string, string] // ['{{', '}}']
// Map to HTML entities. E.g., `{ "amp;": "&" }`
@@ -19,27 +33,46 @@ export interface ParserOptions {
// this number is based on the map above, but it should be pre-computed
// to avoid the cost on every parse() call.
maxCRNameLength?: number
onError?: (error: CompilerError) => void
}
export type HoistTransform = (
node: PlainElementNode,
context: TransformContext
) => JSChildNode
export interface TransformOptions {
nodeTransforms?: NodeTransform[]
directiveTransforms?: { [name: string]: DirectiveTransform }
directiveTransforms?: Record<string, DirectiveTransform | undefined>
// an optional hook to transform a node being hoisted.
// used by compiler-dom to turn hoisted nodes into stringified HTML vnodes.
transformHoist?: HoistTransform | null
isBuiltInComponent?: (tag: string) => symbol | void
// Transform expressions like {{ foo }} to `_ctx.foo`.
// Default: mode === 'module'
// If this option is false, the generated code will be wrapped in a
// `with (this) { ... }` block.
// - This is force-enabled in module mode, since modules are by default strict
// and cannot use `with`
// - Default: mode === 'module'
prefixIdentifiers?: boolean
// Hoist static VNodes and props objects to `_hoisted_x` constants
// Default: false
// - Default: false
hoistStatic?: boolean
// Cache v-on handlers to avoid creating new inline functions on each render,
// also avoids the need for dynamically patching the handlers by wrapping it.
// e.g `@click="foo"` by default is compiled to `{ onClick: foo }`. With this
// option it's compiled to:
// `{ onClick: _cache[0] || (_cache[0] = e => _ctx.foo(e)) }`
// Default: false
// - Requires "prefixIdentifiers" to be enabled because it relies on scope
// analysis to determine if a handler is safe to cache.
// - Default: false
cacheHandlers?: boolean
// a list of parser plugins to enable for @babel/parser
// https://babeljs.io/docs/en/next/babel-parser#plugins
expressionPlugins?: ParserPlugin[]
// SFC scoped styles ID
scopeId?: string | null
ssr?: boolean
onError?: (error: CompilerError) => void
}
@@ -49,19 +82,26 @@ export interface CodegenOptions {
// - Function mode will generate a single `const { helpers... } = Vue`
// statement and return the render function. It is meant to be used with
// `new Function(code)()` to generate a render function at runtime.
// Default: 'function'
// - Default: 'function'
mode?: 'module' | 'function'
// Prefix suitable identifiers with _ctx.
// If this option is false, the generated code will be wrapped in a
// `with (this) { ... }` block.
// Default: false
prefixIdentifiers?: boolean
// Generate source map?
// Default: false
// - Default: false
sourceMap?: boolean
// Filename for source map generation.
// Default: `template.vue.html`
// - Default: `template.vue.html`
filename?: string
// SFC scoped styles ID
scopeId?: string | null
// we need to know about this to generate proper preambles
prefixIdentifiers?: boolean
// option to optimize helper import bindings via variable assignment
// (only used for webpack code-split)
optimizeBindings?: boolean
// for specifying where to import helpers
runtimeModuleName?: string
runtimeGlobalName?: string
// generate ssr-specific code?
ssr?: boolean
}
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions

View File

@@ -1,5 +1,5 @@
import { ParserOptions } from './options'
import { NO, isArray } from '@vue/shared'
import { NO, isArray, makeMap } from '@vue/shared'
import { ErrorCodes, createCompilerError, defaultOnError } from './errors'
import {
assert,
@@ -21,11 +21,11 @@ import {
SourceLocation,
TextNode,
TemplateChildNode,
InterpolationNode
InterpolationNode,
createRoot
} from './ast'
import { extend } from '@vue/shared'
// `isNativeTag` is optional, others are required
type OptionalOptions = 'isNativeTag' | 'isBuiltInComponent'
type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
Pick<ParserOptions, OptionalOptions>
@@ -64,25 +64,20 @@ interface ParserContext {
offset: number
line: number
column: number
inPre: boolean
inPre: boolean // HTML <pre> tag, preserve whitespaces
inVPre: boolean // v-pre, do not process directives and interpolations
}
export function parse(content: string, options: ParserOptions = {}): RootNode {
export function baseParse(
content: string,
options: ParserOptions = {}
): RootNode {
const context = createParserContext(content, options)
const start = getCursor(context)
return {
type: NodeTypes.ROOT,
children: parseChildren(context, TextModes.DATA, []),
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
codegenNode: undefined,
loc: getSelection(context, start)
}
return createRoot(
parseChildren(context, TextModes.DATA, []),
getSelection(context, start)
)
}
function createParserContext(
@@ -99,7 +94,8 @@ function createParserContext(
offset: 0,
originalSource: content,
source: content,
inPre: false
inPre: false,
inVPre: false
}
}
@@ -117,11 +113,11 @@ function parseChildren(
const s = context.source
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
if (mode === TextModes.DATA) {
if (!context.inPre && startsWith(s, context.options.delimiters[0])) {
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
// '{{'
node = parseInterpolation(context, mode)
} else if (s[0] === '<') {
} else if (mode === TextModes.DATA && s[0] === '<') {
// https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
if (s.length === 1) {
emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
@@ -193,45 +189,51 @@ function parseChildren(
// Whitespace management for more efficient output
// (same as v2 whitespace: 'condense')
let removedWhitespace = false
if (
mode !== TextModes.RAWTEXT &&
(!parent || !context.options.isPreTag(parent.tag))
) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === NodeTypes.TEXT) {
if (!node.content.trim()) {
const prev = nodes[i - 1]
const next = nodes[i + 1]
// If:
// - the whitespace is the first or last node, or:
// - the whitespace is adjacent to a comment, or:
// - the whitespace is between two elements AND contains newline
// Then the whitespace is ignored.
if (
!prev ||
!next ||
prev.type === NodeTypes.COMMENT ||
next.type === NodeTypes.COMMENT ||
(prev.type === NodeTypes.ELEMENT &&
next.type === NodeTypes.ELEMENT &&
/[\r\n]/.test(node.content))
) {
removedWhitespace = true
nodes[i] = null as any
if (mode !== TextModes.RAWTEXT) {
if (!context.inPre) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === NodeTypes.TEXT) {
if (!node.content.trim()) {
const prev = nodes[i - 1]
const next = nodes[i + 1]
// If:
// - the whitespace is the first or last node, or:
// - the whitespace is adjacent to a comment, or:
// - the whitespace is between two elements AND contains newline
// Then the whitespace is ignored.
if (
!prev ||
!next ||
prev.type === NodeTypes.COMMENT ||
next.type === NodeTypes.COMMENT ||
(prev.type === NodeTypes.ELEMENT &&
next.type === NodeTypes.ELEMENT &&
/[\r\n]/.test(node.content))
) {
removedWhitespace = true
nodes[i] = null as any
} else {
// Otherwise, condensed consecutive whitespace inside the text down to
// a single space
node.content = ' '
}
} else {
// Otherwise, condensed consecutive whitespace inside the text down to
// a single space
node.content = ' '
node.content = node.content.replace(/\s+/g, ' ')
}
} else {
node.content = node.content.replace(/\s+/g, ' ')
}
}
} else {
// remove leading newline per html spec
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
const first = nodes[0]
if (first && first.type === NodeTypes.TEXT) {
first.content = first.content.replace(/^\r?\n/, '')
}
}
}
return removedWhitespace ? nodes.filter(node => node !== null) : nodes
return removedWhitespace ? nodes.filter(Boolean) : nodes
}
function pushNode(nodes: TemplateChildNode[], node: TemplateChildNode): void {
@@ -353,9 +355,11 @@ function parseElement(
// Start tag.
const wasInPre = context.inPre
const wasInVPre = context.inVPre
const parent = last(ancestors)
const element = parseTag(context, TagType.Start, parent)
const isPreBoundary = context.inPre && !wasInPre
const isVPreBoundary = context.inVPre && !wasInVPre
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
return element
@@ -363,7 +367,7 @@ function parseElement(
// Children.
ancestors.push(element)
const mode = context.options.getTextMode(element.tag, element.ns)
const mode = context.options.getTextMode(element.tag, element.ns, parent)
const children = parseChildren(context, mode, ancestors)
ancestors.pop()
@@ -373,7 +377,7 @@ function parseElement(
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End, parent)
} else {
emitError(context, ErrorCodes.X_MISSING_END_TAG)
emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start)
if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {
const first = children[0]
if (first && startsWith(first.loc.source, '<!--')) {
@@ -387,6 +391,9 @@ function parseElement(
if (isPreBoundary) {
context.inPre = false
}
if (isVPreBoundary) {
context.inVPre = false
}
return element
}
@@ -395,6 +402,10 @@ const enum TagType {
End
}
const isSpecialTemplateDirective = /*#__PURE__*/ makeMap(
`if,else,else-if,for,slot`
)
/**
* Parse a tag (E.g. `<div id=a>`) with that type (start tag or end tag).
*/
@@ -425,12 +436,17 @@ function parseTag(
// Attributes.
let props = parseAttributes(context, type)
// check <pre> tag
if (context.options.isPreTag(tag)) {
context.inPre = true
}
// check v-pre
if (
!context.inPre &&
!context.inVPre &&
props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
) {
context.inPre = true
context.inVPre = true
// reset context
extend(context, cursor)
context.source = currentSource
@@ -452,20 +468,32 @@ function parseTag(
let tagType = ElementTypes.ELEMENT
const options = context.options
if (!context.inPre && !options.isCustomElement(tag)) {
if (options.isNativeTag) {
if (!context.inVPre && !options.isCustomElement(tag)) {
const hasVIs = props.some(
p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
)
if (options.isNativeTag && !hasVIs) {
if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
} else if (
hasVIs ||
isCoreComponent(tag) ||
(options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
/^[A-Z]/.test(tag)
/^[A-Z]/.test(tag) ||
tag === 'component'
) {
tagType = ElementTypes.COMPONENT
}
if (tag === 'slot') {
tagType = ElementTypes.SLOT
} else if (tag === 'template') {
} else if (
tag === 'template' &&
props.some(p => {
return (
p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
)
})
) {
tagType = ElementTypes.TEMPLATE
}
}
@@ -539,7 +567,7 @@ function parseAttribute(
{
const pattern = /["'<]/g
let m: RegExpExecArray | null
while ((m = pattern.exec(name)) !== null) {
while ((m = pattern.exec(name))) {
emitError(
context,
ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
@@ -570,7 +598,7 @@ function parseAttribute(
}
const loc = getSelection(context, start)
if (!context.inPre && /^(v-|:|@|#)/.test(name)) {
if (!context.inVPre && /^(v-|:|@|#)/.test(name)) {
const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(
name
)!
@@ -688,9 +716,9 @@ function parseAttributeValue(
if (!match) {
return undefined
}
let unexpectedChars = /["'<=`]/g
const unexpectedChars = /["'<=`]/g
let m: RegExpExecArray | null
while ((m = unexpectedChars.exec(match[0])) !== null) {
while ((m = unexpectedChars.exec(match[0]))) {
emitError(
context,
ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
@@ -818,8 +846,8 @@ function parseTextData(
if (head[0] === '&') {
// Named character reference.
let name = '',
value: string | undefined = undefined
let name = ''
let value: string | undefined = undefined
if (/[0-9a-z]/i.test(rawText[1])) {
for (
let length = context.options.maxCRNameLength;
@@ -834,7 +862,7 @@ function parseTextData(
if (
mode === TextModes.ATTRIBUTE_VALUE &&
!semi &&
/[=a-z0-9]/i.test(rawText[1 + name.length] || '')
/[=a-z0-9]/i.test(rawText[name.length + 1] || '')
) {
decodedText += '&' + name
advance(1 + name.length)
@@ -849,7 +877,6 @@ function parseTextData(
}
}
} else {
emitError(context, ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE)
decodedText += '&' + name
advance(1 + name.length)
}
@@ -964,9 +991,9 @@ function getNewPosition(
function emitError(
context: ParserContext,
code: ErrorCodes,
offset?: number
offset?: number,
loc: Position = getCursor(context)
): void {
const loc = getCursor(context)
if (offset) {
loc.offset += offset
loc.column += offset

View File

@@ -1,5 +1,5 @@
export const FRAGMENT = Symbol(__DEV__ ? `Fragment` : ``)
export const PORTAL = Symbol(__DEV__ ? `Portal` : ``)
export const TELEPORT = Symbol(__DEV__ ? `Teleport` : ``)
export const SUSPENSE = Symbol(__DEV__ ? `Suspense` : ``)
export const KEEP_ALIVE = Symbol(__DEV__ ? `KeepAlive` : ``)
export const BASE_TRANSITION = Symbol(__DEV__ ? `BaseTransition` : ``)
@@ -8,6 +8,7 @@ export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
export const CREATE_COMMENT = Symbol(__DEV__ ? `createCommentVNode` : ``)
export const CREATE_TEXT = Symbol(__DEV__ ? `createTextVNode` : ``)
export const CREATE_STATIC = Symbol(__DEV__ ? `createStaticVNode` : ``)
export const RESOLVE_COMPONENT = Symbol(__DEV__ ? `resolveComponent` : ``)
export const RESOLVE_DYNAMIC_COMPONENT = Symbol(
__DEV__ ? `resolveDynamicComponent` : ``
@@ -17,18 +18,22 @@ export const WITH_DIRECTIVES = Symbol(__DEV__ ? `withDirectives` : ``)
export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``)
export const RENDER_SLOT = Symbol(__DEV__ ? `renderSlot` : ``)
export const CREATE_SLOTS = Symbol(__DEV__ ? `createSlots` : ``)
export const TO_STRING = Symbol(__DEV__ ? `toString` : ``)
export const TO_DISPLAY_STRING = Symbol(__DEV__ ? `toDisplayString` : ``)
export const MERGE_PROPS = Symbol(__DEV__ ? `mergeProps` : ``)
export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``)
export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``)
export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``)
export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``)
export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``)
export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``)
export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``)
// Name mapping for runtime helpers that need to be imported from 'vue' in
// generated code. Make sure these are correctly exported in the runtime!
// Using `any` here because TS doesn't allow symbols as index type.
export const helperNameMap: any = {
[FRAGMENT]: `Fragment`,
[PORTAL]: `Portal`,
[TELEPORT]: `Teleport`,
[SUSPENSE]: `Suspense`,
[KEEP_ALIVE]: `KeepAlive`,
[BASE_TRANSITION]: `BaseTransition`,
@@ -37,6 +42,7 @@ export const helperNameMap: any = {
[CREATE_VNODE]: `createVNode`,
[CREATE_COMMENT]: `createCommentVNode`,
[CREATE_TEXT]: `createTextVNode`,
[CREATE_STATIC]: `createStaticVNode`,
[RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`,
@@ -44,11 +50,15 @@ export const helperNameMap: any = {
[RENDER_LIST]: `renderList`,
[RENDER_SLOT]: `renderSlot`,
[CREATE_SLOTS]: `createSlots`,
[TO_STRING]: `toString`,
[TO_DISPLAY_STRING]: `toDisplayString`,
[MERGE_PROPS]: `mergeProps`,
[TO_HANDLERS]: `toHandlers`,
[CAMELIZE]: `camelize`,
[SET_BLOCK_TRACKING]: `setBlockTracking`
[SET_BLOCK_TRACKING]: `setBlockTracking`,
[PUSH_SCOPE_ID]: `pushScopeId`,
[POP_SCOPE_ID]: `popScopeId`,
[WITH_SCOPE_ID]: `withScopeId`,
[WITH_CTX]: `withCtx`
}
export function registerRuntimeHelpers(helpers: any) {

View File

@@ -12,11 +12,10 @@ import {
JSChildNode,
SimpleExpressionNode,
ElementTypes,
ElementCodegenNode,
ComponentCodegenNode,
createCallExpression,
CacheExpression,
createCacheExpression
createCacheExpression,
TemplateLiteral,
createVNodeCall
} from './ast'
import {
isString,
@@ -27,14 +26,14 @@ import {
} from '@vue/shared'
import { defaultOnError } from './errors'
import {
TO_STRING,
TO_DISPLAY_STRING,
FRAGMENT,
helperNameMap,
WITH_DIRECTIVES,
CREATE_BLOCK,
CREATE_COMMENT
CREATE_COMMENT,
OPEN_BLOCK
} from './runtimeHelpers'
import { isVSlot, createBlockExpression } from './utils'
import { isVSlot } from './utils'
import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
// There are two types of transforms:
@@ -61,7 +60,8 @@ export type DirectiveTransform = (
export interface DirectiveTransformResult {
props: Property[]
needRuntime: boolean | symbol
needRuntime?: boolean | symbol
ssrTagParts?: TemplateLiteral['elements']
}
// A structural directive transform is a technically a NodeTransform;
@@ -84,6 +84,7 @@ export interface TransformContext extends Required<TransformOptions> {
directives: Set<string>
hoists: JSChildNode[]
imports: Set<ImportItem>
temps: number
cached: number
identifiers: { [name: string]: number | undefined }
scopes: {
@@ -106,7 +107,7 @@ export interface TransformContext extends Required<TransformOptions> {
cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
}
function createTransformContext(
export function createTransformContext(
root: RootNode,
{
prefixIdentifiers = false,
@@ -114,17 +115,36 @@ function createTransformContext(
cacheHandlers = false,
nodeTransforms = [],
directiveTransforms = {},
transformHoist = null,
isBuiltInComponent = NOOP,
expressionPlugins = [],
scopeId = null,
ssr = false,
onError = defaultOnError
}: TransformOptions
): TransformContext {
const context: TransformContext = {
// options
prefixIdentifiers,
hoistStatic,
cacheHandlers,
nodeTransforms,
directiveTransforms,
transformHoist,
isBuiltInComponent,
expressionPlugins,
scopeId,
ssr,
onError,
// state
root,
helpers: new Set(),
components: new Set(),
directives: new Set(),
hoists: [],
imports: new Set(),
temps: 0,
cached: 0,
identifiers: {},
scopes: {
@@ -133,25 +153,17 @@ function createTransformContext(
vPre: 0,
vOnce: 0
},
prefixIdentifiers,
hoistStatic,
cacheHandlers,
nodeTransforms,
directiveTransforms,
isBuiltInComponent,
onError,
parent: null,
currentNode: root,
childIndex: 0,
// methods
helper(name) {
context.helpers.add(name)
return name
},
helperString(name) {
return (
(context.prefixIdentifiers ? `` : `_`) +
helperNameMap[context.helper(name)]
)
return `_${helperNameMap[context.helper(name)]}`
},
replaceNode(node) {
/* istanbul ignore if */
@@ -251,10 +263,20 @@ export function transform(root: RootNode, options: TransformOptions) {
if (options.hoistStatic) {
hoistStatic(root, context)
}
finalizeRoot(root, context)
if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
}
function finalizeRoot(root: RootNode, context: TransformContext) {
function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context
const { children } = root
const child = children[0]
@@ -263,20 +285,13 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
if (isSingleElementRoot(root, child) && child.codegenNode) {
// single element root is never hoisted so codegenNode will never be
// SimpleExpressionNode
const codegenNode = child.codegenNode as
| ElementCodegenNode
| ComponentCodegenNode
| CacheExpression
if (codegenNode.type !== NodeTypes.JS_CACHE_EXPRESSION) {
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
} else {
codegenNode.callee = helper(CREATE_BLOCK)
}
root.codegenNode = createBlockExpression(codegenNode, context)
} else {
root.codegenNode = codegenNode
const codegenNode = child.codegenNode
if (codegenNode.type === NodeTypes.VNODE_CALL) {
codegenNode.isBlock = true
helper(OPEN_BLOCK)
helper(CREATE_BLOCK)
}
root.codegenNode = codegenNode
} else {
// - single <slot/>, IfNode, ForNode: already blocks.
// - single text node: always patched.
@@ -285,27 +300,21 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
}
} else if (children.length > 1) {
// root has multiple nodes - return a fragment block.
root.codegenNode = createBlockExpression(
createCallExpression(helper(CREATE_BLOCK), [
helper(FRAGMENT),
`null`,
root.children,
`${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */`
]),
context
root.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
root.children,
`${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */`,
undefined,
undefined,
true
)
} else {
// no children = noop. codegen will return null.
}
// finalize meta information
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.cached = context.cached
}
export function traverseChildren(
@@ -319,7 +328,6 @@ export function traverseChildren(
for (; i < parent.children.length; i++) {
const child = parent.children[i]
if (isString(child)) continue
context.currentNode = child
context.parent = parent
context.childIndex = i
context.onNodeRemoved = nodeRemoved
@@ -331,6 +339,7 @@ export function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext
) {
context.currentNode = node
// apply transform plugins
const { nodeTransforms } = context
const exitFns = []
@@ -354,21 +363,26 @@ export function traverseNode(
switch (node.type) {
case NodeTypes.COMMENT:
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
if (!context.ssr) {
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
}
break
case NodeTypes.INTERPOLATION:
// no need to traverse, but we need to inject toString helper
context.helper(TO_STRING)
if (!context.ssr) {
context.helper(TO_DISPLAY_STRING)
}
break
// for container types, further traverse downwards
case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) {
traverseChildren(node.branches[i], context)
traverseNode(node.branches[i], context)
}
break
case NodeTypes.IF_BRANCH:
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:

View File

@@ -8,11 +8,9 @@ import {
ComponentNode,
TemplateNode,
ElementNode,
PlainElementCodegenNode,
CodegenNodeWithDirective
VNodeCall
} from '../ast'
import { TransformContext } from '../transform'
import { WITH_DIRECTIVES } from '../runtimeHelpers'
import { PatchFlags, isString, isSymbol } from '@vue/shared'
import { isSlotOutlet, findProp } from '../utils'
@@ -21,6 +19,8 @@ export function hoistStatic(root: RootNode, context: TransformContext) {
root.children,
context,
new Map(),
// Root node is unfortuantely non-hoistable due to potential parent
// fallthrough attributes.
isSingleElementRoot(root, root.children[0])
)
}
@@ -52,13 +52,18 @@ function walk(
) {
if (!doNotHoistNode && isStaticNode(child, resultCache)) {
// whole tree is static
child.codegenNode = context.hoist(child.codegenNode!)
;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
const hoisted = context.transformHoist
? context.transformHoist(child, context)
: child.codegenNode!
child.codegenNode = context.hoist(hoisted)
continue
} else {
// node may contain dynamic children, but its props may be eligible for
// hoisting.
const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) {
if (codegenNode.type === NodeTypes.VNODE_CALL) {
const flag = getPatchFlag(codegenNode)
if (
(!flag ||
@@ -68,8 +73,8 @@ function walk(
!hasCachedProps(child)
) {
const props = getNodeProps(child)
if (props && props !== `null`) {
getVNodeCall(codegenNode).arguments[1] = context.hoist(props)
if (props) {
codegenNode.props = context.hoist(props)
}
}
}
@@ -86,6 +91,11 @@ function walk(
// Do not hoist v-if single child because it has to be a block
walk(branchChildren, context, resultCache, branchChildren.length === 1)
}
} else if (
child.type === NodeTypes.TEXT_CALL &&
isStaticNode(child.content, resultCache)
) {
child.codegenNode = context.hoist(child.codegenNode)
}
}
}
@@ -104,7 +114,7 @@ export function isStaticNode(
return cached
}
const codegenNode = node.codegenNode!
if (codegenNode.type !== NodeTypes.JS_CALL_EXPRESSION) {
if (codegenNode.type !== NodeTypes.VNODE_CALL) {
return false
}
const flag = getPatchFlag(codegenNode)
@@ -116,6 +126,12 @@ export function isStaticNode(
return false
}
}
// only svg/foreignObject could be block here, however if they are static
// then they don't need to be blocks since there will be no nested
// updates.
if (codegenNode.isBlock) {
codegenNode.isBlock = false
}
resultCache.set(node, true)
return true
} else {
@@ -127,6 +143,7 @@ export function isStaticNode(
return true
case NodeTypes.IF:
case NodeTypes.FOR:
case NodeTypes.IF_BRANCH:
return false
case NodeTypes.INTERPOLATION:
case NodeTypes.TEXT_CALL:
@@ -157,14 +174,20 @@ function hasCachedProps(node: PlainElementNode): boolean {
return false
}
const props = getNodeProps(node)
if (
props &&
props !== 'null' &&
props.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
if (props && props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
const { properties } = props
for (let i = 0; i < properties.length; i++) {
if (properties[i].value.type === NodeTypes.JS_CACHE_EXPRESSION) {
const val = properties[i].value
if (val.type === NodeTypes.JS_CACHE_EXPRESSION) {
return true
}
// merged event handlers
if (
val.type === NodeTypes.JS_ARRAY_EXPRESSION &&
val.elements.some(
e => !isString(e) && e.type === NodeTypes.JS_CACHE_EXPRESSION
)
) {
return true
}
}
@@ -174,30 +197,12 @@ function hasCachedProps(node: PlainElementNode): boolean {
function getNodeProps(node: PlainElementNode) {
const codegenNode = node.codegenNode!
if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) {
return getVNodeArgAt(
codegenNode,
1
) as PlainElementCodegenNode['arguments'][1]
if (codegenNode.type === NodeTypes.VNODE_CALL) {
return codegenNode.props
}
}
type NonCachedCodegenNode =
| PlainElementCodegenNode
| CodegenNodeWithDirective<PlainElementCodegenNode>
function getVNodeArgAt(
node: NonCachedCodegenNode,
index: number
): PlainElementCodegenNode['arguments'][number] {
return getVNodeCall(node).arguments[index]
}
function getVNodeCall(node: NonCachedCodegenNode) {
return node.callee === WITH_DIRECTIVES ? node.arguments[0] : node
}
function getPatchFlag(node: NonCachedCodegenNode): number | undefined {
const flag = getVNodeArgAt(node, 3) as string
function getPatchFlag(node: VNodeCall): number | undefined {
const flag = node.patchFlag
return flag ? parseInt(flag, 10) : undefined
}

View File

@@ -0,0 +1,3 @@
import { DirectiveTransform } from '../transform'
export const noopDirectiveTransform: DirectiveTransform = () => ({ props: [] })

View File

@@ -13,27 +13,31 @@ import {
createObjectProperty,
createSimpleExpression,
createObjectExpression,
Property
Property,
ComponentNode,
VNodeCall,
TemplateTextChildNode,
DirectiveArguments,
createVNodeCall
} from '../ast'
import { PatchFlags, PatchFlagNames, isSymbol } from '@vue/shared'
import { PatchFlags, PatchFlagNames, isSymbol, isOn } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
import {
CREATE_VNODE,
WITH_DIRECTIVES,
RESOLVE_DIRECTIVE,
RESOLVE_COMPONENT,
RESOLVE_DYNAMIC_COMPONENT,
MERGE_PROPS,
TO_HANDLERS,
PORTAL,
TELEPORT,
KEEP_ALIVE
} from '../runtimeHelpers'
import {
getInnerRange,
isVSlot,
toValidAssetId,
findProp,
isCoreComponent
isCoreComponent,
isBindKey,
findDir
} from '../utils'
import { buildSlots } from './vSlot'
import { isStaticNode } from './hoistStatic'
@@ -45,107 +49,97 @@ const directiveImportMap = new WeakMap<DirectiveNode, symbol>()
// generate a JavaScript AST for this element's codegen
export const transformElement: NodeTransform = (node, context) => {
if (
node.type !== NodeTypes.ELEMENT ||
// handled by transformSlotOutlet
node.tagType === ElementTypes.SLOT ||
// <template v-if/v-for> should have already been replaced
// <template v-slot> is handled by buildSlots
(node.tagType === ElementTypes.TEMPLATE && node.props.some(isVSlot))
!(
node.type === NodeTypes.ELEMENT &&
(node.tagType === ElementTypes.ELEMENT ||
node.tagType === ElementTypes.COMPONENT)
)
) {
return
}
// perform the work on exit, after all child expressions have been
// processed and merged.
return function postTransformElement() {
const { tag, tagType, props } = node
const builtInComponentSymbol =
isCoreComponent(tag) || context.isBuiltInComponent(tag)
const isComponent = tagType === ElementTypes.COMPONENT
const { tag, props } = node
const isComponent = node.tagType === ElementTypes.COMPONENT
let hasProps = props.length > 0
// The goal of the transform is to create a codegenNode implementing the
// VNodeCall interface.
const vnodeTag = isComponent
? resolveComponentType(node as ComponentNode, context)
: `"${tag}"`
let vnodeProps: VNodeCall['props']
let vnodeChildren: VNodeCall['children']
let vnodePatchFlag: VNodeCall['patchFlag']
let patchFlag: number = 0
let runtimeDirectives: DirectiveNode[] | undefined
let vnodeDynamicProps: VNodeCall['dynamicProps']
let dynamicPropNames: string[] | undefined
let dynamicComponent: string | CallExpression | undefined
let vnodeDirectives: VNodeCall['directives']
// handle dynamic component
const isProp = findProp(node, 'is')
if (tag === 'component') {
if (isProp) {
// static <component is="foo" />
if (isProp.type === NodeTypes.ATTRIBUTE) {
const tag = isProp.value && isProp.value.content
if (tag) {
context.helper(RESOLVE_COMPONENT)
context.components.add(tag)
dynamicComponent = toValidAssetId(tag, `component`)
}
}
// dynamic <component :is="asdf" />
else if (isProp.exp) {
dynamicComponent = createCallExpression(
context.helper(RESOLVE_DYNAMIC_COMPONENT),
// _ctx.$ exposes the owner instance of current render function
[isProp.exp, context.prefixIdentifiers ? `_ctx.$` : `$`]
let shouldUseBlock =
!isComponent &&
// <svg> and <foreignObject> must be forced into blocks so that block
// updates inside get proper isSVG flag at runtime. (#639, #643)
// This is technically web-specific, but splitting the logic out of core
// leads to too much unnecessary complexity.
(tag === 'svg' ||
tag === 'foreignObject' ||
// #938: elements with dynamic keys should be forced into blocks
findProp(node, 'key', true))
// props
if (props.length > 0) {
const propsBuildResult = buildProps(node, context)
vnodeProps = propsBuildResult.props
patchFlag = propsBuildResult.patchFlag
dynamicPropNames = propsBuildResult.dynamicPropNames
const directives = propsBuildResult.directives
vnodeDirectives =
directives && directives.length
? (createArrayExpression(
directives.map(dir => buildDirectiveArgs(dir, context))
) as DirectiveArguments)
: undefined
}
// children
if (node.children.length > 0) {
if (vnodeTag === KEEP_ALIVE) {
// Although a built-in component, we compile KeepAlive with raw children
// instead of slot functions so that it can be used inside Transition
// or other Transition-wrapping HOCs.
// To ensure correct updates with block optimizations, we need to:
// 1. Force keep-alive into a block. This avoids its children being
// collected by a parent block.
shouldUseBlock = true
// 2. Force keep-alive to always be updated, since it uses raw children.
patchFlag |= PatchFlags.DYNAMIC_SLOTS
if (__DEV__ && node.children.length > 1) {
context.onError(
createCompilerError(ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN, {
start: node.children[0].loc.start,
end: node.children[node.children.length - 1].loc.end,
source: ''
})
)
}
}
}
let nodeType
if (dynamicComponent) {
nodeType = dynamicComponent
} else if (builtInComponentSymbol) {
nodeType = context.helper(builtInComponentSymbol)
} else if (isComponent) {
// user component w/ resolve
context.helper(RESOLVE_COMPONENT)
context.components.add(tag)
nodeType = toValidAssetId(tag, `component`)
} else {
// plain element
nodeType = `"${node.tag}"`
}
const args: CallExpression['arguments'] = [nodeType]
// props
if (hasProps) {
const propsBuildResult = buildProps(
node,
context,
// skip reserved "is" prop <component is>
isProp ? node.props.filter(p => p !== isProp) : node.props
)
patchFlag = propsBuildResult.patchFlag
dynamicPropNames = propsBuildResult.dynamicPropNames
runtimeDirectives = propsBuildResult.directives
if (!propsBuildResult.props) {
hasProps = false
} else {
args.push(propsBuildResult.props)
}
}
// children
const hasChildren = node.children.length > 0
if (hasChildren) {
if (!hasProps) {
args.push(`null`)
}
// Portal & KeepAlive should have normal children instead of slots
// Portal is not a real component has dedicated handling in the renderer
// KeepAlive should not track its own deps so that it can be used inside
// Transition
if (
const shouldBuildAsSlots =
isComponent &&
builtInComponentSymbol !== PORTAL &&
builtInComponentSymbol !== KEEP_ALIVE
) {
// Teleport is not a real component and has dedicated runtime handling
vnodeTag !== TELEPORT &&
// explained above.
vnodeTag !== KEEP_ALIVE
if (shouldBuildAsSlots) {
const { slots, hasDynamicSlots } = buildSlots(node, context)
args.push(slots)
vnodeChildren = slots
if (hasDynamicSlots) {
patchFlag |= PatchFlags.DYNAMIC_SLOTS
}
} else if (node.children.length === 1) {
} else if (node.children.length === 1 && vnodeTag !== TELEPORT) {
const child = node.children[0]
const type = child.type
// check for dynamic text children
@@ -158,65 +152,88 @@ export const transformElement: NodeTransform = (node, context) => {
// pass directly if the only child is a text node
// (plain / interpolation / expression)
if (hasDynamicTextChild || type === NodeTypes.TEXT) {
args.push(child)
vnodeChildren = child as TemplateTextChildNode
} else {
args.push(node.children)
vnodeChildren = node.children
}
} else {
args.push(node.children)
vnodeChildren = node.children
}
}
// patchFlag & dynamicPropNames
if (patchFlag !== 0) {
if (!hasChildren) {
if (!hasProps) {
args.push(`null`)
}
args.push(`null`)
}
if (__DEV__) {
const flagNames = Object.keys(PatchFlagNames)
.map(Number)
.filter(n => n > 0 && patchFlag & n)
.map(n => PatchFlagNames[n])
.join(`, `)
args.push(patchFlag + ` /* ${flagNames} */`)
if (patchFlag < 0) {
// special flags (negative and mutually exclusive)
vnodePatchFlag = patchFlag + ` /* ${PatchFlagNames[patchFlag]} */`
} else {
// bitwise flags
const flagNames = Object.keys(PatchFlagNames)
.map(Number)
.filter(n => n > 0 && patchFlag & n)
.map(n => PatchFlagNames[n])
.join(`, `)
vnodePatchFlag = patchFlag + ` /* ${flagNames} */`
}
} else {
args.push(patchFlag + '')
vnodePatchFlag = String(patchFlag)
}
if (dynamicPropNames && dynamicPropNames.length) {
args.push(stringifyDynamicPropNames(dynamicPropNames))
vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames)
}
}
const { loc } = node
const vnode = createCallExpression(context.helper(CREATE_VNODE), args, loc)
if (runtimeDirectives && runtimeDirectives.length) {
node.codegenNode = createCallExpression(
context.helper(WITH_DIRECTIVES),
[
vnode,
createArrayExpression(
runtimeDirectives.map(dir => buildDirectiveArgs(dir, context)),
loc
)
],
loc
)
} else {
node.codegenNode = vnode
}
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren,
vnodePatchFlag,
vnodeDynamicProps,
vnodeDirectives,
!!shouldUseBlock,
false /* isForBlock */,
node.loc
)
}
}
function stringifyDynamicPropNames(props: string[]): string {
let propsNamesString = `[`
for (let i = 0, l = props.length; i < l; i++) {
propsNamesString += JSON.stringify(props[i])
if (i < l - 1) propsNamesString += ', '
export function resolveComponentType(
node: ComponentNode,
context: TransformContext,
ssr = false
) {
const { tag } = node
// 1. dynamic component
const isProp =
node.tag === 'component' ? findProp(node, 'is') : findDir(node, 'is')
if (isProp) {
const exp =
isProp.type === NodeTypes.ATTRIBUTE
? isProp.value && createSimpleExpression(isProp.value.content, true)
: isProp.exp
if (exp) {
return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [
exp
])
}
}
return propsNamesString + `]`
// 2. built-in components (Teleport, Transition, KeepAlive, Suspense...)
const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
if (builtIn) {
// built-ins are simply fallthroughs / have special handling during ssr
// no we don't need to import their runtime equivalents
if (!ssr) context.helper(builtIn)
return builtIn
}
// 3. user component (resolve)
context.helper(RESOLVE_COMPONENT)
context.components.add(tag)
return toValidAssetId(tag, `component`)
}
export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
@@ -224,14 +241,15 @@ export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
export function buildProps(
node: ElementNode,
context: TransformContext,
props: ElementNode['props'] = node.props
props: ElementNode['props'] = node.props,
ssr = false
): {
props: PropsExpression | undefined
directives: DirectiveNode[]
patchFlag: number
dynamicPropNames: string[]
} {
const elementLoc = node.loc
const { tag, loc: elementLoc } = node
const isComponent = node.tagType === ElementTypes.COMPONENT
let properties: ObjectExpression['properties'] = []
const mergeArgs: PropsExpression[] = []
@@ -242,27 +260,40 @@ export function buildProps(
let hasRef = false
let hasClassBinding = false
let hasStyleBinding = false
let hasHydrationEventBinding = false
let hasDynamicKeys = false
const dynamicPropNames: string[] = []
const analyzePatchFlag = ({ key, value }: Property) => {
if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
const name = key.content
if (
!isComponent &&
isOn(name) &&
// omit the flag for click handlers becaues hydration gives click
// dedicated fast path.
name.toLowerCase() !== 'onclick' &&
// omit v-model handlers
name !== 'onUpdate:modelValue'
) {
hasHydrationEventBinding = true
}
if (
value.type === NodeTypes.JS_CACHE_EXPRESSION ||
((value.type === NodeTypes.SIMPLE_EXPRESSION ||
value.type === NodeTypes.COMPOUND_EXPRESSION) &&
isStaticNode(value))
) {
// skip if the prop is a cached handler or has constant value
return
}
const name = key.content
if (name === 'ref') {
hasRef = true
} else if (name === 'class') {
hasClassBinding = true
} else if (name === 'style') {
hasStyleBinding = true
} else if (name !== 'key') {
} else if (name !== 'key' && !dynamicPropNames.includes(name)) {
dynamicPropNames.push(name)
}
} else {
@@ -278,6 +309,10 @@ export function buildProps(
if (name === 'ref') {
hasRef = true
}
// skip :is on <component>
if (name === 'is' && tag === 'component') {
continue
}
properties.push(
createObjectProperty(
createSimpleExpression(
@@ -295,6 +330,8 @@ export function buildProps(
} else {
// directives
const { name, arg, exp, loc } = prop
const isBind = name === 'bind'
const isOn = name === 'on'
// skip v-slot - it is handled by its dedicated transform.
if (name === 'slot') {
@@ -305,15 +342,23 @@ export function buildProps(
}
continue
}
// skip v-once - it is handled by its dedicated transform.
if (name === 'once') {
continue
}
// skip v-is and :is on <component>
if (
name === 'is' ||
(isBind && tag === 'component' && isBindKey(arg, 'is'))
) {
continue
}
// skip v-on in SSR compilation
if (isOn && ssr) {
continue
}
// special case for v-bind and v-on with no argument
const isBind = name === 'bind'
const isOn = name === 'on'
if (!arg && (isBind || isOn)) {
hasDynamicKeys = true
if (exp) {
@@ -351,7 +396,7 @@ export function buildProps(
if (directiveTransform) {
// has built-in directive transform.
const { props, needRuntime } = directiveTransform(prop, node, context)
props.forEach(analyzePatchFlag)
!ssr && props.forEach(analyzePatchFlag)
properties.push(...props)
if (needRuntime) {
runtimeDirectives.push(prop)
@@ -405,8 +450,14 @@ export function buildProps(
if (dynamicPropNames.length) {
patchFlag |= PatchFlags.PROPS
}
if (hasHydrationEventBinding) {
patchFlag |= PatchFlags.HYDRATE_EVENTS
}
}
if (patchFlag === 0 && (hasRef || runtimeDirectives.length > 0)) {
if (
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) &&
(hasRef || runtimeDirectives.length > 0)
) {
patchFlag |= PatchFlags.NEED_PATCH
}
@@ -437,12 +488,7 @@ function dedupeProperties(properties: Property[]): Property[] {
const name = prop.key.content
const existing = knownProps.get(name)
if (existing) {
if (
name === 'style' ||
name === 'class' ||
name.startsWith('on') ||
name.startsWith('vnode')
) {
if (name === 'style' || name === 'class' || name.startsWith('on')) {
mergeAsArray(existing, prop)
}
// unexpected duplicate, should have emitted error during parse
@@ -472,7 +518,6 @@ function buildDirectiveArgs(
const dirArgs: ArrayExpression['elements'] = []
const runtime = directiveImportMap.get(dir)
if (runtime) {
context.helper(runtime)
dirArgs.push(context.helperString(runtime))
} else {
// inject statement for resolving directive
@@ -507,3 +552,12 @@ function buildDirectiveArgs(
}
return createArrayExpression(dirArgs, dir.loc)
}
function stringifyDynamicPropNames(props: string[]): string {
let propsNamesString = `[`
for (let i = 0, l = props.length; i < l; i++) {
propsNamesString += JSON.stringify(props[i])
if (i < l - 1) propsNamesString += ', '
}
return propsNamesString + `]`
}

View File

@@ -16,7 +16,6 @@ import {
CompoundExpressionNode,
createCompoundExpression
} from '../ast'
import { Node, Function, Identifier, Property } from 'estree'
import {
advancePositionWithClone,
isSimpleIdentifier,
@@ -25,6 +24,7 @@ import {
} from '../utils'
import { isGloballyWhitelisted, makeMap } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
@@ -40,11 +40,15 @@ export const transformExpression: NodeTransform = (node, context) => {
const dir = node.props[i]
// do not process for v-on & v-for since they are special handled
if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') {
const exp = dir.exp as SimpleExpressionNode | undefined
const arg = dir.arg as SimpleExpressionNode | undefined
const exp = dir.exp
const arg = dir.arg
// do not process exp if this is v-on:arg - we need special handling
// for wrapping inline statements.
if (exp && !(dir.name === 'on' && arg)) {
if (
exp &&
exp.type === NodeTypes.SIMPLE_EXPRESSION &&
!(dir.name === 'on' && arg)
) {
dir.exp = processExpression(
exp,
context,
@@ -52,7 +56,7 @@ export const transformExpression: NodeTransform = (node, context) => {
dir.name === 'slot'
)
}
if (arg && !arg.isStatic) {
if (arg && arg.type === NodeTypes.SIMPLE_EXPRESSION && !arg.isStatic) {
dir.arg = processExpression(arg, context)
}
}
@@ -76,7 +80,9 @@ export function processExpression(
context: TransformContext,
// some expressions like v-slot props & v-for aliases should be parsed as
// function params
asParams: boolean = false
asParams = false,
// v-on handler values may contain multiple statements
asRawStatements = false
): ExpressionNode {
if (!context.prefixIdentifiers || !node.content.trim()) {
return node
@@ -84,6 +90,8 @@ export function processExpression(
// fast path if expression is a simple identifier.
const rawExp = node.content
// bail on parens to prevent any possible function invocations.
const bailConstant = rawExp.indexOf(`(`) > -1
if (isSimpleIdentifier(rawExp)) {
if (
!asParams &&
@@ -92,7 +100,7 @@ export function processExpression(
!isLiteralWhitelisted(rawExp)
) {
node.content = `_ctx.${rawExp}`
} else if (!context.identifiers[rawExp]) {
} else if (!context.identifiers[rawExp] && !bailConstant) {
// mark node constant for hoisting unless it's referring a scope variable
node.isConstant = true
}
@@ -100,26 +108,48 @@ export function processExpression(
}
let ast: any
// if the expression is supposed to be used in a function params position
// we need to parse it differently.
const source = `(${rawExp})${asParams ? `=>{}` : ``}`
// exp needs to be parsed differently:
// 1. Multiple inline statements (v-on, with presence of `;`): parse as raw
// exp, but make sure to pad with spaces for consistent ranges
// 2. Expressions: wrap with parens (for e.g. object expressions)
// 3. Function arguments (v-for, v-slot): place in a function argument position
const source = asRawStatements
? ` ${rawExp} `
: `(${rawExp})${asParams ? `=>{}` : ``}`
try {
ast = parseJS(source, { ranges: true })
ast = parseJS(source, {
plugins: [
...context.expressionPlugins,
// by default we enable proposals slated for ES2020.
// full list at https://babeljs.io/docs/en/next/babel-parser#plugins
// this will need to be updated as the spec moves forward.
'bigInt',
'optionalChaining',
'nullishCoalescingOperator'
]
}).program
} catch (e) {
context.onError(
createCompilerError(ErrorCodes.X_INVALID_EXPRESSION, node.loc)
createCompilerError(
ErrorCodes.X_INVALID_EXPRESSION,
node.loc,
undefined,
e.message
)
)
return node
}
const ids: (Identifier & PrefixMeta)[] = []
const knownIds = Object.create(context.identifiers)
const isDuplicate = (node: Node & PrefixMeta): boolean =>
ids.some(id => id.start === node.start)
// walk the AST and look for identifiers that need to be prefixed with `_ctx.`.
walkJS(ast, {
enter(node: Node & PrefixMeta, parent) {
if (node.type === 'Identifier') {
if (!ids.includes(node)) {
if (!isDuplicate(node)) {
const needPrefix = shouldPrefix(node, parent)
if (!knownIds[node.name] && needPrefix) {
if (isPropertyShorthand(node, parent)) {
@@ -128,12 +158,13 @@ export function processExpression(
node.prefix = `${node.name}: `
}
node.name = `_ctx.${node.name}`
node.isConstant = false
ids.push(node)
} else if (!isStaticPropertyKey(node, parent)) {
// The identifier is considered constant unless it's pointing to a
// scope variable (a v-for alias, or a v-slot prop)
node.isConstant = !(needPrefix && knownIds[node.name])
if (!(needPrefix && knownIds[node.name]) && !bailConstant) {
node.isConstant = true
}
// also generate sub-expressions for other identifiers for better
// source map support. (except for property keys which are static)
ids.push(node)
@@ -223,7 +254,7 @@ export function processExpression(
ret = createCompoundExpression(children, node.loc)
} else {
ret = node
ret.isConstant = true
ret.isConstant = !bailConstant
}
ret.identifiers = Object.keys(knownIds)
return ret
@@ -232,17 +263,21 @@ export function processExpression(
const isFunction = (node: Node): node is Function =>
/Function(Expression|Declaration)$/.test(node.type)
const isPropertyKey = (node: Node, parent: Node) =>
parent &&
parent.type === 'Property' &&
parent.key === node &&
!parent.computed
const isStaticProperty = (node: Node): node is ObjectProperty =>
node && node.type === 'ObjectProperty' && !node.computed
const isPropertyShorthand = (node: Node, parent: Node) =>
isPropertyKey(node, parent) && (parent as Property).value === node
const isPropertyShorthand = (node: Node, parent: Node) => {
return (
isStaticProperty(parent) &&
parent.value === node &&
parent.key.type === 'Identifier' &&
parent.key.name === (node as Identifier).name &&
parent.key.start === node.start
)
}
const isStaticPropertyKey = (node: Node, parent: Node) =>
isPropertyKey(node, parent) && (parent as Property).value !== node
isStaticProperty(parent) && parent.key === node
function shouldPrefix(identifier: Identifier, parent: Node) {
if (
@@ -257,7 +292,8 @@ function shouldPrefix(identifier: Identifier, parent: Node) {
!isStaticPropertyKey(identifier, parent) &&
// not a property of a MemberExpression
!(
parent.type === 'MemberExpression' &&
(parent.type === 'MemberExpression' ||
parent.type === 'OptionalMemberExpression') &&
parent.property === identifier &&
!parent.computed
) &&

View File

@@ -1,78 +1,32 @@
import { NodeTransform } from '../transform'
import { NodeTransform, TransformContext } from '../transform'
import {
NodeTypes,
CallExpression,
createCallExpression,
ExpressionNode
ExpressionNode,
SlotOutletNode
} from '../ast'
import { isSlotOutlet } from '../utils'
import { buildProps } from './transformElement'
import { isSlotOutlet, findProp } from '../utils'
import { buildProps, PropsExpression } from './transformElement'
import { createCompilerError, ErrorCodes } from '../errors'
import { RENDER_SLOT } from '../runtimeHelpers'
export const transformSlotOutlet: NodeTransform = (node, context) => {
if (isSlotOutlet(node)) {
const { props, children, loc } = node
const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots`
let slotName: string | ExpressionNode = `"default"`
const { children, loc } = node
const { slotName, slotProps } = processSlotOutlet(node, context)
// check for <slot name="xxx" OR :name="xxx" />
let nameIndex: number = -1
for (let i = 0; i < props.length; i++) {
const prop = props[i]
if (prop.type === NodeTypes.ATTRIBUTE) {
if (prop.name === `name` && prop.value) {
// static name="xxx"
slotName = JSON.stringify(prop.value.content)
nameIndex = i
break
}
} else if (prop.name === `bind`) {
const { arg, exp } = prop
if (
arg &&
exp &&
arg.type === NodeTypes.SIMPLE_EXPRESSION &&
arg.isStatic &&
arg.content === `name`
) {
// dynamic :name="xxx"
slotName = exp
nameIndex = i
break
}
}
}
const slotArgs: CallExpression['arguments'] = [
context.prefixIdentifiers ? `_ctx.$slots` : `$slots`,
slotName
]
const slotArgs: CallExpression['arguments'] = [$slots, slotName]
const propsWithoutName =
nameIndex > -1
? props.slice(0, nameIndex).concat(props.slice(nameIndex + 1))
: props
let hasProps = propsWithoutName.length > 0
if (hasProps) {
const { props: propsExpression, directives } = buildProps(
node,
context,
propsWithoutName
)
if (directives.length) {
context.onError(
createCompilerError(
ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
directives[0].loc
)
)
}
if (propsExpression) {
slotArgs.push(propsExpression)
} else {
hasProps = false
}
if (slotProps) {
slotArgs.push(slotProps)
}
if (children.length) {
if (!hasProps) {
if (!slotProps) {
slotArgs.push(`{}`)
}
slotArgs.push(children)
@@ -85,3 +39,49 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
)
}
}
interface SlotOutletProcessResult {
slotName: string | ExpressionNode
slotProps: PropsExpression | undefined
}
export function processSlotOutlet(
node: SlotOutletNode,
context: TransformContext
): SlotOutletProcessResult {
let slotName: string | ExpressionNode = `"default"`
let slotProps: PropsExpression | undefined = undefined
// check for <slot name="xxx" OR :name="xxx" />
const name = findProp(node, 'name')
if (name) {
if (name.type === NodeTypes.ATTRIBUTE && name.value) {
// static name
slotName = JSON.stringify(name.value.content)
} else if (name.type === NodeTypes.DIRECTIVE && name.exp) {
// dynamic name
slotName = name.exp
}
}
const propsWithoutName = name
? node.props.filter(p => p !== name)
: node.props
if (propsWithoutName.length > 0) {
const { props, directives } = buildProps(node, context, propsWithoutName)
slotProps = props
if (directives.length) {
context.onError(
createCompilerError(
ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
directives[0].loc
)
)
}
}
return {
slotName,
slotProps
}
}

View File

@@ -1,29 +1,23 @@
import { NodeTransform } from '../transform'
import {
NodeTypes,
TemplateChildNode,
TextNode,
InterpolationNode,
CompoundExpressionNode,
createCallExpression,
CallExpression,
ElementTypes
} from '../ast'
import { isText } from '../utils'
import { CREATE_TEXT } from '../runtimeHelpers'
import { PatchFlags, PatchFlagNames } from '@vue/shared'
const isText = (
node: TemplateChildNode
): node is TextNode | InterpolationNode =>
node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
// Merge adjacent text nodes and expressions into a single expression
// e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.
export const transformText: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ROOT ||
node.type === NodeTypes.ELEMENT ||
node.type === NodeTypes.FOR
node.type === NodeTypes.FOR ||
node.type === NodeTypes.IF_BRANCH
) {
// perform the transform on node exit so that all expressions have already
// been processed.
@@ -84,7 +78,7 @@ export const transformText: NodeTransform = (node, context) => {
callArgs.push(child)
}
// mark dynamic text with flag so it gets patched inside a block
if (child.type !== NodeTypes.TEXT) {
if (!context.ssr && child.type !== NodeTypes.TEXT) {
callArgs.push(
`${PatchFlags.TEXT} /* ${PatchFlagNames[PatchFlags.TEXT]} */`
)

View File

@@ -30,7 +30,6 @@ export const transformBind: DirectiveTransform = (dir, node, context) => {
return {
props: [
createObjectProperty(arg!, exp || createSimpleExpression('', true, loc))
],
needRuntime: false
]
}
}

View File

@@ -8,22 +8,28 @@ import {
createSimpleExpression,
SourceLocation,
SimpleExpressionNode,
createSequenceExpression,
createCallExpression,
createFunctionExpression,
ElementTypes,
createObjectExpression,
createObjectProperty,
ForCodegenNode,
ElementCodegenNode,
SlotOutletCodegenNode,
SlotOutletNode
RenderSlotCall,
SlotOutletNode,
ElementNode,
DirectiveNode,
ForNode,
PlainElementNode,
createVNodeCall,
VNodeCall,
ForRenderListExpression,
BlockCodegenNode,
ForIteratorExpression
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import {
getInnerRange,
findProp,
createBlockExpression,
isTemplateNode,
isSlotOutlet,
injectProp
@@ -32,8 +38,7 @@ import {
RENDER_LIST,
OPEN_BLOCK,
CREATE_BLOCK,
FRAGMENT,
WITH_DIRECTIVES
FRAGMENT
} from '../runtimeHelpers'
import { processExpression } from './transformExpression'
import { PatchFlags, PatchFlagNames } from '@vue/shared'
@@ -41,141 +46,163 @@ import { PatchFlags, PatchFlagNames } from '@vue/shared'
export const transformFor = createStructuralDirectiveTransform(
'for',
(node, dir, context) => {
if (!dir.exp) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
)
return
}
const parseResult = parseForExpression(
// can only be simple expression because vFor transform is applied
// before expression transform.
dir.exp as SimpleExpressionNode,
context
)
if (!parseResult) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
)
return
}
const { helper, addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult
// create the loop render function expression now, and add the
// iterator on exit after all children have been traversed
const renderExp = createCallExpression(helper(RENDER_LIST), [source])
const keyProp = findProp(node, `key`)
const fragmentFlag = keyProp
? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT
const codegenNode = createSequenceExpression([
// fragment blocks disable tracking since they always diff their children
createCallExpression(helper(OPEN_BLOCK), [`false`]),
createCallExpression(helper(CREATE_BLOCK), [
const { helper } = context
return processFor(node, dir, context, forNode => {
// create the loop render function expression now, and add the
// iterator on exit after all children have been traversed
const renderExp = createCallExpression(helper(RENDER_LIST), [
forNode.source
]) as ForRenderListExpression
const keyProp = findProp(node, `key`)
const fragmentFlag = keyProp
? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT
forNode.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
`null`,
undefined,
renderExp,
`${fragmentFlag} /* ${PatchFlagNames[fragmentFlag]} */`
])
]) as ForCodegenNode
`${fragmentFlag} /* ${PatchFlagNames[fragmentFlag]} */`,
undefined,
undefined,
true /* isBlock */,
true /* isForBlock */,
node.loc
) as ForCodegenNode
context.replaceNode({
type: NodeTypes.FOR,
loc: dir.loc,
source,
valueAlias: value,
keyAlias: key,
objectIndexAlias: index,
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node],
codegenNode
})
// bookkeeping
scopes.vFor++
if (!__BROWSER__ && context.prefixIdentifiers) {
// scope management
// inject identifiers to context
value && addIdentifiers(value)
key && addIdentifiers(key)
index && addIdentifiers(index)
}
return () => {
scopes.vFor--
if (!__BROWSER__ && context.prefixIdentifiers) {
value && removeIdentifiers(value)
key && removeIdentifiers(key)
index && removeIdentifiers(index)
}
// finish the codegen now that all children have been traversed
let childBlock
const isTemplate = isTemplateNode(node)
const slotOutlet = isSlotOutlet(node)
? node
: isTemplate &&
node.children.length === 1 &&
isSlotOutlet(node.children[0])
? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
return () => {
// finish the codegen now that all children have been traversed
let childBlock: BlockCodegenNode
const isTemplate = isTemplateNode(node)
const { children } = forNode
const needFragmentWrapper =
children.length > 1 || children[0].type !== NodeTypes.ELEMENT
const slotOutlet = isSlotOutlet(node)
? node
: isTemplate &&
node.children.length === 1 &&
isSlotOutlet(node.children[0])
? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
: null
const keyProperty = keyProp
? createObjectProperty(
`key`,
keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!
)
: null
const keyProperty = keyProp
? createObjectProperty(
`key`,
keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!
)
: null
if (slotOutlet) {
// <slot v-for="..."> or <template v-for="..."><slot/></template>
childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode
if (isTemplate && keyProperty) {
// <template v-for="..." :key="..."><slot/></template>
// we need to inject the key to the renderSlot() call.
// the props for renderSlot is passed as the 3rd argument.
injectProp(childBlock, keyProperty, context)
}
} else if (isTemplate) {
// <template v-for="...">
// should generate a fragment block for each loop
childBlock = createBlockExpression(
createCallExpression(helper(CREATE_BLOCK), [
if (slotOutlet) {
// <slot v-for="..."> or <template v-for="..."><slot/></template>
childBlock = slotOutlet.codegenNode as RenderSlotCall
if (isTemplate && keyProperty) {
// <template v-for="..." :key="..."><slot/></template>
// we need to inject the key to the renderSlot() call.
// the props for renderSlot is passed as the 3rd argument.
injectProp(childBlock, keyProperty, context)
}
} else if (needFragmentWrapper) {
// <template v-for="..."> with text or multi-elements
// should generate a fragment block for each loop
childBlock = createVNodeCall(
context,
helper(FRAGMENT),
keyProperty ? createObjectExpression([keyProperty]) : `null`,
keyProperty ? createObjectExpression([keyProperty]) : undefined,
node.children,
`${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */`
]),
context
)
} else {
// Normal element v-for. Directly use the child's codegenNode
// arguments, but replace createVNode() with createBlock()
let codegenNode = node.codegenNode as ElementCodegenNode
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
} */`,
undefined,
undefined,
true
)
} else {
codegenNode.callee = helper(CREATE_BLOCK)
// Normal element v-for. Directly use the child's codegenNode
// but mark it as a block.
childBlock = (children[0] as PlainElementNode)
.codegenNode as VNodeCall
childBlock.isBlock = true
helper(OPEN_BLOCK)
helper(CREATE_BLOCK)
}
childBlock = createBlockExpression(codegenNode, context)
}
renderExp.arguments.push(
createFunctionExpression(
createForLoopParams(parseResult),
renderExp.arguments.push(createFunctionExpression(
createForLoopParams(forNode.parseResult),
childBlock,
true /* force newline */
)
)
}
) as ForIteratorExpression)
}
})
}
)
// target-agnostic transform used for both Client and SSR
export function processFor(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
processCodegen?: (forNode: ForNode) => (() => void) | undefined
) {
if (!dir.exp) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
)
return
}
const parseResult = parseForExpression(
// can only be simple expression because vFor transform is applied
// before expression transform.
dir.exp as SimpleExpressionNode,
context
)
if (!parseResult) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
)
return
}
const { addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult
const forNode: ForNode = {
type: NodeTypes.FOR,
loc: dir.loc,
source,
valueAlias: value,
keyAlias: key,
objectIndexAlias: index,
parseResult,
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node]
}
context.replaceNode(forNode)
// bookkeeping
scopes.vFor++
if (!__BROWSER__ && context.prefixIdentifiers) {
// scope management
// inject identifiers to context
value && addIdentifiers(value)
key && addIdentifiers(key)
index && addIdentifiers(index)
}
const onExit = processCodegen && processCodegen(forNode)
return () => {
scopes.vFor--
if (!__BROWSER__ && context.prefixIdentifiers) {
value && removeIdentifiers(value)
key && removeIdentifiers(key)
index && removeIdentifiers(index)
}
if (onExit) onExit()
}
}
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
// This regex doesn't cover the case if key or index aliases have destructuring,
// but those do not make sense in the first place, so this works in practice.

View File

@@ -1,7 +1,7 @@
import {
createStructuralDirectiveTransform,
traverseChildren,
TransformContext
TransformContext,
traverseNode
} from '../transform'
import {
NodeTypes,
@@ -10,130 +10,139 @@ import {
DirectiveNode,
IfBranchNode,
SimpleExpressionNode,
createSequenceExpression,
createCallExpression,
createConditionalExpression,
ConditionalExpression,
CallExpression,
createSimpleExpression,
createObjectProperty,
createObjectExpression,
IfCodegenNode,
IfConditionalExpression,
BlockCodegenNode,
SlotOutletCodegenNode,
ElementCodegenNode,
ComponentCodegenNode
IfNode,
createVNodeCall
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression'
import {
OPEN_BLOCK,
CREATE_BLOCK,
FRAGMENT,
WITH_DIRECTIVES,
CREATE_VNODE,
CREATE_COMMENT
CREATE_COMMENT,
OPEN_BLOCK,
TELEPORT
} from '../runtimeHelpers'
import { injectProp } from '../utils'
import { PatchFlags, PatchFlagNames } from '@vue/shared'
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
if (
dir.name !== 'else' &&
(!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())
) {
const loc = dir.exp ? dir.exp.loc : node.loc
context.onError(
createCompilerError(ErrorCodes.X_V_IF_NO_EXPRESSION, dir.loc)
)
dir.exp = createSimpleExpression(`true`, false, loc)
}
if (!__BROWSER__ && context.prefixIdentifiers && dir.exp) {
// dir.exp can only be simple expression because vIf transform is applied
// before expression transform.
dir.exp = processExpression(dir.exp as SimpleExpressionNode, context)
}
if (dir.name === 'if') {
const branch = createIfBranch(node, dir)
const codegenNode = createSequenceExpression([
createCallExpression(context.helper(OPEN_BLOCK))
]) as IfCodegenNode
context.replaceNode({
type: NodeTypes.IF,
loc: node.loc,
branches: [branch],
codegenNode
})
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// Exit callback. Complete the codegenNode when all children have been
// transformed.
return () => {
codegenNode.expressions.push(createCodegenNodeForBranch(
branch,
0,
context
) as IfConditionalExpression)
}
} else {
// locate the adjacent v-if
const siblings = context.parent!.children
const comments = []
let i = siblings.indexOf(node)
while (i-- >= -1) {
const sibling = siblings[i]
if (__DEV__ && sibling && sibling.type === NodeTypes.COMMENT) {
context.removeNode(sibling)
comments.unshift(sibling)
continue
}
if (sibling && sibling.type === NodeTypes.IF) {
// move the node to the if node's branches
context.removeNode()
const branch = createIfBranch(node, dir)
if (__DEV__ && comments.length) {
branch.children = [...comments, ...branch.children]
}
sibling.branches.push(branch)
// since the branch was removed, it will not be traversed.
// make sure to traverse here.
traverseChildren(branch, context)
// make sure to reset currentNode after traversal to indicate this
// node has been removed.
context.currentNode = null
// attach this branch's codegen node to the v-if root.
let parentCondition = sibling.codegenNode
.expressions[1] as ConditionalExpression
while (true) {
if (
parentCondition.alternate.type ===
NodeTypes.JS_CONDITIONAL_EXPRESSION
) {
parentCondition = parentCondition.alternate
} else {
parentCondition.alternate = createCodegenNodeForBranch(
branch,
sibling.branches.length - 1,
context
)
break
}
}
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
0,
context
) as IfConditionalExpression
} else {
context.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc)
// attach this branch's codegen node to the v-if root.
let parentCondition = ifNode.codegenNode!
while (
parentCondition.alternate.type ===
NodeTypes.JS_CONDITIONAL_EXPRESSION
) {
parentCondition = parentCondition.alternate
}
parentCondition.alternate = createCodegenNodeForBranch(
branch,
ifNode.branches.length - 1,
context
)
}
break
}
}
})
}
)
// target-agnostic transform used for both Client and SSR
export function processIf(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
processCodegen?: (
node: IfNode,
branch: IfBranchNode,
isRoot: boolean
) => (() => void) | undefined
) {
if (
dir.name !== 'else' &&
(!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())
) {
const loc = dir.exp ? dir.exp.loc : node.loc
context.onError(
createCompilerError(ErrorCodes.X_V_IF_NO_EXPRESSION, dir.loc)
)
dir.exp = createSimpleExpression(`true`, false, loc)
}
if (!__BROWSER__ && context.prefixIdentifiers && dir.exp) {
// dir.exp can only be simple expression because vIf transform is applied
// before expression transform.
dir.exp = processExpression(dir.exp as SimpleExpressionNode, context)
}
if (dir.name === 'if') {
const branch = createIfBranch(node, dir)
const ifNode: IfNode = {
type: NodeTypes.IF,
loc: node.loc,
branches: [branch]
}
context.replaceNode(ifNode)
if (processCodegen) {
return processCodegen(ifNode, branch, true)
}
} else {
// locate the adjacent v-if
const siblings = context.parent!.children
const comments = []
let i = siblings.indexOf(node)
while (i-- >= -1) {
const sibling = siblings[i]
if (__DEV__ && sibling && sibling.type === NodeTypes.COMMENT) {
context.removeNode(sibling)
comments.unshift(sibling)
continue
}
if (sibling && sibling.type === NodeTypes.IF) {
// move the node to the if node's branches
context.removeNode()
const branch = createIfBranch(node, dir)
if (__DEV__ && comments.length) {
branch.children = [...comments, ...branch.children]
}
sibling.branches.push(branch)
const onExit = processCodegen && processCodegen(sibling, branch, false)
// since the branch was removed, it will not be traversed.
// make sure to traverse here.
traverseNode(branch, context)
// call on exit
if (onExit) onExit()
// make sure to reset currentNode after traversal to indicate this
// node has been removed.
context.currentNode = null
} else {
context.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc)
)
}
break
}
}
}
function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
return {
type: NodeTypes.IF_BRANCH,
@@ -160,7 +169,7 @@ function createCodegenNodeForBranch(
])
) as IfConditionalExpression
} else {
return createChildrenCodegenNode(branch, index, context) as BlockCodegenNode
return createChildrenCodegenNode(branch, index, context)
}
}
@@ -168,46 +177,56 @@ function createChildrenCodegenNode(
branch: IfBranchNode,
index: number,
context: TransformContext
): CallExpression {
): BlockCodegenNode {
const { helper } = context
const keyProperty = createObjectProperty(
`key`,
createSimpleExpression(index + '', false)
)
const { children } = branch
const child = children[0]
const firstChild = children[0]
const needFragmentWrapper =
children.length !== 1 || child.type !== NodeTypes.ELEMENT
children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT
if (needFragmentWrapper) {
const blockArgs: CallExpression['arguments'] = [
helper(FRAGMENT),
createObjectExpression([keyProperty]),
children
]
if (children.length === 1 && child.type === NodeTypes.FOR) {
if (children.length === 1 && firstChild.type === NodeTypes.FOR) {
// optimize away nested fragments when child is a ForNode
const forBlockArgs = child.codegenNode.expressions[1].arguments
// directly use the for block's children and patchFlag
blockArgs[2] = forBlockArgs[2]
blockArgs[3] = forBlockArgs[3]
const vnodeCall = firstChild.codegenNode!
injectProp(vnodeCall, keyProperty, context)
return vnodeCall
} else {
return createVNodeCall(
context,
helper(FRAGMENT),
createObjectExpression([keyProperty]),
children,
`${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */`,
undefined,
undefined,
true,
false,
branch.loc
)
}
return createCallExpression(helper(CREATE_BLOCK), blockArgs)
} else {
const childCodegen = (child as ElementNode).codegenNode as
| ElementCodegenNode
| ComponentCodegenNode
| SlotOutletCodegenNode
let vnodeCall = childCodegen
// Element with custom directives. Locate the actual createVNode() call.
if (vnodeCall.callee === WITH_DIRECTIVES) {
vnodeCall = vnodeCall.arguments[0]
}
const vnodeCall = (firstChild as ElementNode)
.codegenNode as BlockCodegenNode
// Change createVNode to createBlock.
if (vnodeCall.callee === CREATE_VNODE) {
vnodeCall.callee = helper(CREATE_BLOCK)
if (
vnodeCall.type === NodeTypes.VNODE_CALL &&
// component vnodes are always tracked and its children are
// compiled into slots so no need to make it a block
((firstChild as ElementNode).tagType !== ElementTypes.COMPONENT ||
// teleport has component type but isn't always tracked
vnodeCall.tag === TELEPORT)
) {
vnodeCall.isBlock = true
helper(OPEN_BLOCK)
helper(CREATE_BLOCK)
}
// inject branch key
injectProp(vnodeCall, keyProperty, context)
return childCodegen
return vnodeCall
}
}

View File

@@ -44,10 +44,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
const eventName = arg
? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic
? `onUpdate:${arg.content}`
: createCompoundExpression([
'"onUpdate:" + ',
...(arg.type === NodeTypes.SIMPLE_EXPRESSION ? [arg] : arg.children)
])
: createCompoundExpression(['"onUpdate:" + ', arg])
: `onUpdate:modelValue`
const props = [
@@ -56,11 +53,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
// "onUpdate:modelValue": $event => (foo = $event)
createObjectProperty(
eventName,
createCompoundExpression([
`$event => (`,
...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
` = $event)`
])
createCompoundExpression([`$event => (`, exp, ` = $event)`])
)
]
@@ -82,12 +75,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
const modifiersKey = arg
? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic
? `${arg.content}Modifiers`
: createCompoundExpression([
...(arg.type === NodeTypes.SIMPLE_EXPRESSION
? [arg]
: arg.children),
' + "Modifiers"'
])
: createCompoundExpression([arg, ' + "Modifiers"'])
: `modelModifiers`
props.push(
createObjectProperty(
@@ -101,5 +89,5 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
}
function createTransformProps(props: Property[] = []) {
return { props, needRuntime: false }
return { props }
}

View File

@@ -8,7 +8,7 @@ import {
createCompoundExpression,
SimpleExpressionNode
} from '../ast'
import { capitalize } from '@vue/shared'
import { capitalize, camelize } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression'
import { isMemberExpression, hasScopeRef } from '../utils'
@@ -26,23 +26,24 @@ export interface VOnDirectiveNode extends DirectiveNode {
}
export const transformOn: DirectiveTransform = (
dir: VOnDirectiveNode,
dir,
node,
context,
augmentor
) => {
const { loc, modifiers, arg } = dir
const { loc, modifiers, arg } = dir as VOnDirectiveNode
if (!dir.exp && !modifiers.length) {
context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc))
}
let eventName: ExpressionNode
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
if (arg.isStatic) {
eventName = createSimpleExpression(
`on${capitalize(arg.content)}`,
true,
arg.loc
)
const rawName = arg.content
// for @vnode-xxx event listeners, auto convert it to camelCase
const normalizedName = rawName.startsWith(`vnode`)
? capitalize(camelize(rawName))
: capitalize(rawName)
eventName = createSimpleExpression(`on${normalizedName}`, true, arg.loc)
} else {
eventName = createCompoundExpression([`"on" + (`, arg, `)`])
}
@@ -54,16 +55,22 @@ export const transformOn: DirectiveTransform = (
}
// handler processing
let exp: ExpressionNode | undefined = dir.exp
let exp: ExpressionNode | undefined = dir.exp as
| SimpleExpressionNode
| undefined
if (exp && !exp.content.trim()) {
exp = undefined
}
let isCacheable: boolean = !exp
if (exp) {
const isMemberExp = isMemberExpression(exp.content)
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
const hasMultipleStatements = exp.content.includes(`;`)
// process the expression since it's been skipped
if (!__BROWSER__ && context.prefixIdentifiers) {
context.addIdentifiers(`$event`)
exp = processExpression(exp, context)
exp = processExpression(exp, context, false, hasMultipleStatements)
context.removeIdentifiers(`$event`)
// with scope analysis, the function is hoistable if it has no reference
// to scope variables.
@@ -85,9 +92,9 @@ export const transformOn: DirectiveTransform = (
if (isInlineStatement || (isCacheable && isMemberExp)) {
// wrap inline statement in a function expression
exp = createCompoundExpression([
`$event => (`,
...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
`)`
`$event => ${hasMultipleStatements ? `{` : `(`}`,
exp,
hasMultipleStatements ? `}` : `)`
])
}
}
@@ -98,8 +105,7 @@ export const transformOn: DirectiveTransform = (
eventName,
exp || createSimpleExpression(`() => {}`, false, loc)
)
],
needRuntime: false
]
}
// apply extended compiler augmentor

View File

@@ -19,12 +19,13 @@ import {
FunctionExpression,
CallExpression,
createCallExpression,
createArrayExpression
createArrayExpression,
SlotsExpression
} from '../ast'
import { TransformContext, NodeTransform } from '../transform'
import { createCompilerError, ErrorCodes } from '../errors'
import { findDir, isTemplateNode, assert, isVSlot, hasScopeRef } from '../utils'
import { CREATE_SLOTS, RENDER_LIST } from '../runtimeHelpers'
import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
import { parseForExpression, createForLoopParams } from './vFor'
const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
@@ -93,19 +94,42 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => {
}
}
export type SlotFnBuilder = (
slotProps: ExpressionNode | undefined,
slotChildren: TemplateChildNode[],
loc: SourceLocation
) => FunctionExpression
const buildClientSlotFn: SlotFnBuilder = (props, children, loc) =>
createFunctionExpression(
props,
children,
false /* newline */,
true /* isSlot */,
children.length ? children[0].loc : loc
)
// Instead of being a DirectiveTransform, v-slot processing is called during
// transformElement to build the slots object for a component.
export function buildSlots(
node: ElementNode,
context: TransformContext
context: TransformContext,
buildSlotFn: SlotFnBuilder = buildClientSlotFn
): {
slots: ObjectExpression | CallExpression
slots: SlotsExpression
hasDynamicSlots: boolean
} {
context.helper(WITH_CTX)
const { children, loc } = node
const slotsProperties: Property[] = []
const dynamicSlots: (ConditionalExpression | CallExpression)[] = []
const buildDefaultSlotProperty = (
props: ExpressionNode | undefined,
children: TemplateChildNode[]
) => createObjectProperty(`default`, buildSlotFn(props, children, loc))
// If the slot is inside a v-for or another v-slot, force it to be dynamic
// since it likely uses a scope variable.
let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0
@@ -115,24 +139,26 @@ export function buildSlots(
hasDynamicSlots = hasScopeRef(node, context.identifiers)
}
// 1. Check for default slot with slotProps on component itself.
// 1. Check for slot with slotProps on component itself.
// <Comp v-slot="{ prop }"/>
const explicitDefaultSlot = findDir(node, 'slot', true)
if (explicitDefaultSlot) {
const { arg, exp, loc } = explicitDefaultSlot
if (arg) {
context.onError(
createCompilerError(ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT, loc)
const onComponentSlot = findDir(node, 'slot', true)
if (onComponentSlot) {
const { arg, exp } = onComponentSlot
slotsProperties.push(
createObjectProperty(
arg || createSimpleExpression('default', true),
buildSlotFn(exp, children, loc)
)
}
slotsProperties.push(buildDefaultSlot(exp, children, loc))
)
}
// 2. Iterate through children and check for template slots
// <template v-slot:foo="{ prop }">
let hasTemplateSlots = false
let extraneousChild: TemplateChildNode | undefined = undefined
let hasNamedDefaultSlot = false
const implicitDefaultChildren: TemplateChildNode[] = []
const seenSlotNames = new Set<string>()
for (let i = 0; i < children.length; i++) {
const slotElement = children[i]
let slotDir
@@ -142,14 +168,14 @@ export function buildSlots(
!(slotDir = findDir(slotElement, 'slot', true))
) {
// not a <template v-slot>, skip.
if (slotElement.type !== NodeTypes.COMMENT && !extraneousChild) {
extraneousChild = slotElement
if (slotElement.type !== NodeTypes.COMMENT) {
implicitDefaultChildren.push(slotElement)
}
continue
}
if (explicitDefaultSlot) {
// already has on-component default slot - this is incorrect usage.
if (onComponentSlot) {
// already has on-component slot - this is incorrect usage.
context.onError(
createCompilerError(ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, slotDir.loc)
)
@@ -172,13 +198,7 @@ export function buildSlots(
hasDynamicSlots = true
}
const slotFunction = createFunctionExpression(
slotProps,
slotChildren,
false,
slotChildren.length ? slotChildren[0].loc : slotLoc
)
const slotFunction = buildSlotFn(slotProps, slotChildren, slotLoc)
// check if this slot is conditional (v-if/v-for)
let vIf: DirectiveNode | undefined
let vElse: DirectiveNode | undefined
@@ -244,7 +264,7 @@ export function buildSlots(
createFunctionExpression(
createForLoopParams(parseResult),
buildDynamicSlot(slotName, slotFunction),
true
true /* force newline */
)
])
)
@@ -266,36 +286,46 @@ export function buildSlots(
continue
}
seenSlotNames.add(staticSlotName)
if (staticSlotName === 'default') {
hasNamedDefaultSlot = true
}
}
slotsProperties.push(createObjectProperty(slotName, slotFunction))
}
}
if (hasTemplateSlots && extraneousChild) {
context.onError(
createCompilerError(
ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
extraneousChild.loc
)
)
if (!onComponentSlot) {
if (!hasTemplateSlots) {
// implicit default slot (on component)
slotsProperties.push(buildDefaultSlotProperty(undefined, children))
} else if (implicitDefaultChildren.length) {
// implicit default slot (mixed with named slots)
if (hasNamedDefaultSlot) {
context.onError(
createCompilerError(
ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
implicitDefaultChildren[0].loc
)
)
} else {
slotsProperties.push(
buildDefaultSlotProperty(undefined, implicitDefaultChildren)
)
}
}
}
if (!explicitDefaultSlot && !hasTemplateSlots) {
// implicit default slot.
slotsProperties.push(buildDefaultSlot(undefined, children, loc))
}
let slots: ObjectExpression | CallExpression = createObjectExpression(
let slots = createObjectExpression(
slotsProperties.concat(
createObjectProperty(`_compiled`, createSimpleExpression(`true`, false))
createObjectProperty(`_`, createSimpleExpression(`1`, false))
),
loc
)
) as SlotsExpression
if (dynamicSlots.length) {
slots = createCallExpression(context.helper(CREATE_SLOTS), [
slots,
createArrayExpression(dynamicSlots)
])
]) as SlotsExpression
}
return {
@@ -304,22 +334,6 @@ export function buildSlots(
}
}
function buildDefaultSlot(
slotProps: ExpressionNode | undefined,
children: TemplateChildNode[],
loc: SourceLocation
): Property {
return createObjectProperty(
`default`,
createFunctionExpression(
slotProps,
children,
false,
children.length ? children[0].loc : loc
)
)
}
function buildDynamicSlot(
name: ExpressionNode,
fn: FunctionExpression

View File

@@ -4,8 +4,6 @@ import {
ElementNode,
NodeTypes,
CallExpression,
SequenceExpression,
createSequenceExpression,
createCallExpression,
DirectiveNode,
ElementTypes,
@@ -17,33 +15,31 @@ import {
createObjectExpression,
SlotOutletNode,
TemplateNode,
BlockCodegenNode,
ElementCodegenNode,
SlotOutletCodegenNode,
ComponentCodegenNode,
RenderSlotCall,
ExpressionNode,
IfBranchNode
IfBranchNode,
TextNode,
InterpolationNode,
VNodeCall
} from './ast'
import { parse } from 'acorn'
import { walk } from 'estree-walker'
import { TransformContext } from './transform'
import {
OPEN_BLOCK,
MERGE_PROPS,
RENDER_SLOT,
PORTAL,
TELEPORT,
SUSPENSE,
KEEP_ALIVE,
BASE_TRANSITION
} from './runtimeHelpers'
import { isString, isFunction, isObject, hyphenate } from '@vue/shared'
import { parse } from '@babel/parser'
import { Node } from '@babel/types'
export const isBuiltInType = (tag: string, expected: string): boolean =>
tag === expected || tag === hyphenate(expected)
export function isCoreComponent(tag: string): symbol | void {
if (isBuiltInType(tag, 'Portal')) {
return PORTAL
if (isBuiltInType(tag, 'Teleport')) {
return TELEPORT
} else if (isBuiltInType(tag, 'Suspense')) {
return SUSPENSE
} else if (isBuiltInType(tag, 'KeepAlive')) {
@@ -57,10 +53,10 @@ export function isCoreComponent(tag: string): symbol | void {
// lazy require dependencies so that they don't end up in rollup's dep graph
// and thus can be tree-shaken in browser builds.
let _parse: typeof parse
let _walk: typeof walk
let _walk: any
export function loadDep(name: string) {
if (typeof process !== 'undefined' && isFunction(require)) {
if (!__BROWSER__ && typeof process !== 'undefined' && isFunction(require)) {
return require(name)
} else {
// This is only used when we are building a dev-only build of the compiler
@@ -74,11 +70,18 @@ export const parseJS: typeof parse = (code, options) => {
!__BROWSER__,
`Expression AST analysis can only be performed in non-browser builds.`
)
const parse = _parse || (_parse = loadDep('acorn').parse)
return parse(code, options)
if (!_parse) {
_parse = loadDep('@babel/parser').parse
}
return _parse(code, options)
}
export const walkJS: typeof walk = (ast, walker) => {
interface Walker {
enter?(node: Node, parent: Node): void
leave?(node: Node): void
}
export const walkJS = (ast: Node, walker: Walker) => {
assert(
!__BROWSER__,
`Expression AST analysis can only be performed in non-browser builds.`
@@ -149,7 +152,7 @@ export function advancePositionWithMutation(
pos.column =
lastNewLinePos === -1
? pos.column + numberOfCharacters
: Math.max(1, numberOfCharacters - lastNewLinePos)
: numberOfCharacters - lastNewLinePos
return pos
}
@@ -181,36 +184,46 @@ export function findDir(
export function findProp(
node: ElementNode,
name: string,
dynamicOnly: boolean = false
dynamicOnly: boolean = false,
allowEmpty: boolean = false
): ElementNode['props'][0] | undefined {
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) {
if (dynamicOnly) continue
if (p.name === name && p.value) {
if (p.name === name && (p.value || allowEmpty)) {
return p
}
} else if (
p.name === 'bind' &&
p.arg &&
p.arg.type === NodeTypes.SIMPLE_EXPRESSION &&
p.arg.isStatic &&
p.arg.content === name &&
p.exp
) {
} else if (p.name === 'bind' && p.exp && isBindKey(p.arg, name)) {
return p
}
}
}
export function createBlockExpression(
blockExp: BlockCodegenNode,
context: TransformContext
): SequenceExpression {
return createSequenceExpression([
createCallExpression(context.helper(OPEN_BLOCK)),
blockExp
])
export function isBindKey(arg: DirectiveNode['arg'], name: string): boolean {
return !!(
arg &&
arg.type === NodeTypes.SIMPLE_EXPRESSION &&
arg.isStatic &&
arg.content === name
)
}
export function hasDynamicKeyVBind(node: ElementNode): boolean {
return node.props.some(
p =>
p.type === NodeTypes.DIRECTIVE &&
p.name === 'bind' &&
(!p.arg || // v-bind="obj"
p.arg.type !== NodeTypes.SIMPLE_EXPRESSION || // v-bind:[_ctx.foo]
!p.arg.isStatic) // v-bind:[foo]
)
}
export function isText(
node: TemplateChildNode
): node is TextNode | InterpolationNode {
return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
}
export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
@@ -232,13 +245,13 @@ export function isSlotOutlet(
}
export function injectProp(
node: ElementCodegenNode | ComponentCodegenNode | SlotOutletCodegenNode,
node: VNodeCall | RenderSlotCall,
prop: Property,
context: TransformContext
) {
let propsWithInjection: ObjectExpression | CallExpression
const props =
node.callee === RENDER_SLOT ? node.arguments[2] : node.arguments[1]
node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
if (props == null || isString(props)) {
propsWithInjection = createObjectExpression([prop])
} else if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
@@ -253,7 +266,19 @@ export function injectProp(
}
propsWithInjection = props
} else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
props.properties.unshift(prop)
let alreadyExists = false
// check existing key to avoid overriding user provided keys
if (prop.key.type === NodeTypes.SIMPLE_EXPRESSION) {
const propKeyName = prop.key.content
alreadyExists = props.properties.some(
p =>
p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
p.key.content === propKeyName
)
}
if (!alreadyExists) {
props.properties.unshift(prop)
}
propsWithInjection = props
} else {
// single v-bind with expression, return a merged replacement
@@ -262,10 +287,10 @@ export function injectProp(
props
])
}
if (node.callee === RENDER_SLOT) {
node.arguments[2] = propsWithInjection
if (node.type === NodeTypes.VNODE_CALL) {
node.props = propsWithInjection
} else {
node.arguments[1] = propsWithInjection
node.arguments[2] = propsWithInjection
}
}

View File

@@ -2,19 +2,16 @@
exports[`compile should contain standard transforms 1`] = `
"const _Vue = Vue
const { createVNode: _createVNode } = Vue
const _hoisted_1 = {}
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\", { textContent: text }, null, 8 /* PROPS */, [\\"textContent\\"]),
_createVNode(\\"div\\", { innerHTML: html }, null, 8 /* PROPS */, [\\"innerHTML\\"]),
_createVNode(\\"div\\", null, \\"test\\"),
_createVNode(\\"div\\", { style: _hoisted_1 }, \\"red\\"),
_createVNode(\\"div\\", { style: {\\"color\\":\\"red\\"} }, \\"red\\"),
_createVNode(\\"div\\", { style: {color: 'green'} }, null, 4 /* STYLE */)
], 64 /* STABLE_FRAGMENT */))
}

View File

@@ -5,7 +5,7 @@ describe('compile', () => {
const { code } = compile(`<div v-text="text"></div>
<div v-html="html"></div>
<div v-cloak>test</div>
<div style="color=red">red</div>
<div style="color:red">red</div>
<div :style="{color: 'green'}"></div>`)
expect(code).toMatchSnapshot()

View File

@@ -1,5 +1,5 @@
import {
parse,
baseParse as parse,
NodeTypes,
ElementNode,
TextNode,
@@ -49,6 +49,22 @@ describe('DOM parser', () => {
})
})
test('textarea support interpolation', () => {
const ast = parse('<textarea><div>{{ foo }}</textarea>', parserOptions)
const element = ast.children[0] as ElementNode
expect(element.children).toMatchObject([
{ type: NodeTypes.TEXT, content: `<div>` },
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `foo`,
isStatic: false
}
}
])
})
test('style handles comments/elements as just a text', () => {
const ast = parse(
'<style>some<div>text</div>and<!--comment--></style>',
@@ -100,11 +116,36 @@ describe('DOM parser', () => {
})
test('<pre> tag should preserve raw whitespace', () => {
const rawText = ` \na b \n c`
const rawText = ` \na <div>foo \n bar</div> \n c`
const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
expect((ast.children[0] as ElementNode).children).toMatchObject([
{
type: NodeTypes.TEXT,
content: ` \na `
},
{
type: NodeTypes.ELEMENT,
children: [
{
type: NodeTypes.TEXT,
content: `foo \n bar`
}
]
},
{
type: NodeTypes.TEXT,
content: ` \n c`
}
])
})
// #908
test('<pre> tag should remove leading newline', () => {
const rawText = `\nhello`
const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
expect((ast.children[0] as ElementNode).children[0]).toMatchObject({
type: NodeTypes.TEXT,
content: rawText
content: rawText.slice(1)
})
})
})

View File

@@ -1,23 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler: transform v-model input w/ dynamic v-bind 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelDynamic: _vModelDynamic, mergeProps: _mergeProps, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock(\\"input\\", _mergeProps(obj, {
\\"onUpdate:modelValue\\": $event => (model = $event)
}), null, 16 /* FULL_PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelDynamic, model]
])
}
}"
`;
exports[`compiler: transform v-model input w/ dynamic v-bind 2`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _directive_bind = _resolveDirective(\\"bind\\")
return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
\\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_directive_bind, val, key],
[_vModelDynamic, model]
])
}
}"
`;
exports[`compiler: transform v-model modifiers .lazy 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", {
modelValue: model,
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
\\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[
_vModelText,
model,
void 0,
{ lazy: true }
]
]))
])
}
}"
`;
@@ -25,21 +59,20 @@ return function render() {
exports[`compiler: transform v-model modifiers .number 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", {
modelValue: model,
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
\\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[
_vModelText,
model,
void 0,
{ number: true }
]
]))
])
}
}"
`;
@@ -47,21 +80,20 @@ return function render() {
exports[`compiler: transform v-model modifiers .trim 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", {
modelValue: model,
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
\\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[
_vModelText,
model,
void 0,
{ trim: true }
]
]))
])
}
}"
`;
@@ -69,16 +101,15 @@ return function render() {
exports[`compiler: transform v-model simple expression 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", {
modelValue: model,
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
\\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelText, model]
]))
])
}
}"
`;
@@ -86,17 +117,16 @@ return function render() {
exports[`compiler: transform v-model simple expression for input (checkbox) 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { vModelCheckbox: _vModelCheckbox, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", {
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelCheckbox: _vModelCheckbox, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
type: \\"checkbox\\",
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelCheckbox, model]
]))
])
}
}"
`;
@@ -104,19 +134,18 @@ return function render() {
exports[`compiler: transform v-model simple expression for input (dynamic type) 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { vModelDynamic: _vModelDynamic, createVNode: _createVNode, withDirectives: _withDirectives, resolveDirective: _resolveDirective, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _directive_bind = _resolveDirective(\\"bind\\")
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", {
modelValue: model,
return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
\\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_directive_bind, foo, \\"type\\"],
[_vModelDynamic, model]
]))
])
}
}"
`;
@@ -124,17 +153,16 @@ return function render() {
exports[`compiler: transform v-model simple expression for input (radio) 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { vModelRadio: _vModelRadio, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", {
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelRadio: _vModelRadio, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
type: \\"radio\\",
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelRadio, model]
]))
])
}
}"
`;
@@ -142,17 +170,16 @@ return function render() {
exports[`compiler: transform v-model simple expression for input (text) 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", {
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
type: \\"text\\",
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelText, model]
]))
])
}
}"
`;
@@ -160,16 +187,15 @@ return function render() {
exports[`compiler: transform v-model simple expression for select 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { vModelSelect: _vModelSelect, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"select\\", {
modelValue: model,
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelSelect: _vModelSelect, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock(\\"select\\", {
\\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelSelect, model]
]))
])
}
}"
`;
@@ -177,16 +203,15 @@ return function render() {
exports[`compiler: transform v-model simple expression for textarea 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"textarea\\", {
modelValue: model,
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock(\\"textarea\\", {
\\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelText, model]
]))
])
}
}"
`;

View File

@@ -3,13 +3,13 @@
exports[`compiler: v-show transform simple expression 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { vShow: _vShow, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"div\\", null, null, 32 /* NEED_PATCH */), [
return function render(_ctx, _cache) {
with (_ctx) {
const { vShow: _vShow, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock(\\"div\\", null, null, 512 /* NEED_PATCH */)), [
[_vShow, a]
]))
])
}
}"
`;

View File

@@ -0,0 +1,124 @@
import { compile, NodeTypes, CREATE_STATIC } from '../../src'
import {
stringifyStatic,
StringifyThresholds
} from '../../src/transforms/stringifyStatic'
describe('stringify static html', () => {
function compileWithStringify(template: string) {
return compile(template, {
hoistStatic: true,
prefixIdentifiers: true,
transformHoist: stringifyStatic
})
}
function repeat(code: string, n: number): string {
return new Array(n)
.fill(0)
.map(() => code)
.join('')
}
test('should bail on non-eligible static trees', () => {
const { ast } = compileWithStringify(
`<div><div><div>hello</div><div>hello</div></div></div>`
)
expect(ast.hoists.length).toBe(1)
// should be a normal vnode call
expect(ast.hoists[0].type).toBe(NodeTypes.VNODE_CALL)
})
test('should work on eligible content (elements with binding > 5)', () => {
const { ast } = compileWithStringify(
`<div><div>${repeat(
`<span class="foo"/>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div></div>`
)
expect(ast.hoists.length).toBe(1)
// should be optimized now
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div>${repeat(
`<span class="foo"></span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div>`
)
]
})
})
test('should work on eligible content (elements > 20)', () => {
const { ast } = compileWithStringify(
`<div><div>${repeat(
`<span/>`,
StringifyThresholds.NODE_COUNT
)}</div></div>`
)
expect(ast.hoists.length).toBe(1)
// should be optimized now
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div>${repeat(
`<span></span>`,
StringifyThresholds.NODE_COUNT
)}</div>`
)
]
})
})
test('serliazing constant bindings', () => {
const { ast } = compileWithStringify(
`<div><div :style="{ color: 'red' }">${repeat(
`<span :class="[{ foo: true }, { bar: true }]">{{ 1 }} + {{ false }}</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div></div>`
)
expect(ast.hoists.length).toBe(1)
// should be optimized now
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div style="color:red;">${repeat(
`<span class="foo bar">1 + false</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div>`
)
]
})
})
test('escape', () => {
const { ast } = compileWithStringify(
`<div><div>${repeat(
`<span :class="'foo' + '&gt;ar'">{{ 1 }} + {{ '<' }}</span>` +
`<span>&amp;</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div></div>`
)
expect(ast.hoists.length).toBe(1)
// should be optimized now
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div>${repeat(
`<span class="foo&gt;ar">1 + &lt;</span>` + `<span>&amp;</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div>`
)
]
})
})
})

View File

@@ -1,10 +1,10 @@
import {
parse,
baseParse as parse,
transform,
CompilerOptions,
ElementNode,
NodeTypes,
CallExpression
VNodeCall
} from '@vue/compiler-core'
import { transformBind } from '../../../compiler-core/src/transforms/vBind'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
@@ -26,17 +26,8 @@ function transformWithStyleTransform(
}
describe('compiler: style transform', () => {
test('should transform into directive node and hoist value', () => {
const { root, node } = transformWithStyleTransform(
`<div style="color: red"/>`
)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `{"color":"red"}`,
isStatic: false
}
])
test('should transform into directive node', () => {
const { node } = transformWithStyleTransform(`<div style="color: red"/>`)
expect(node.props[0]).toMatchObject({
type: NodeTypes.DIRECTIVE,
name: `bind`,
@@ -47,7 +38,7 @@ describe('compiler: style transform', () => {
},
exp: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`,
content: `{"color":"red"}`,
isStatic: false
}
})
@@ -60,7 +51,7 @@ describe('compiler: style transform', () => {
bind: transformBind
}
})
expect((node.codegenNode as CallExpression).arguments[1]).toMatchObject({
expect((node.codegenNode as VNodeCall).props).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
@@ -71,13 +62,13 @@ describe('compiler: style transform', () => {
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`,
content: `{"color":"red"}`,
isStatic: false
}
}
]
})
// should not cause the STYLE patchFlag to be attached
expect((node.codegenNode as CallExpression).arguments.length).toBe(2)
expect((node.codegenNode as VNodeCall).patchFlag).toBeUndefined()
})
})

View File

@@ -1,30 +0,0 @@
import {
parse,
transform,
ElementNode,
CallExpression
} from '@vue/compiler-core'
import { transformCloak } from '../../src/transforms/vCloak'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
function transformWithCloak(template: string) {
const ast = parse(template)
transform(ast, {
nodeTransforms: [transformElement],
directiveTransforms: {
cloak: transformCloak
}
})
return ast.children[0] as ElementNode
}
describe('compiler: v-cloak transform', () => {
test('should add no props to DOM', () => {
const node = transformWithCloak(`<div v-cloak/>`)
const codegenArgs = (node.codegenNode as CallExpression).arguments
// As v-cloak adds no properties the codegen should be identical to
// rendering a div with no props or reactive data (so just the tag as the arg)
expect(codegenArgs.length).toBe(1)
})
})

View File

@@ -1,5 +1,5 @@
import {
parse,
baseParse as parse,
transform,
PlainElementNode,
CompilerOptions
@@ -29,15 +29,13 @@ describe('compiler: v-html transform', () => {
it('should convert v-html to innerHTML', () => {
const ast = transformWithVHtml(`<div v-html="test"/>`)
expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({
arguments: [
`"div"`,
createObjectMatcher({
innerHTML: `[test]`
}),
`null`,
genFlagText(PatchFlags.PROPS),
`["innerHTML"]`
]
tag: `"div"`,
props: createObjectMatcher({
innerHTML: `[test]`
}),
children: undefined,
patchFlag: genFlagText(PatchFlags.PROPS),
dynamicProps: `["innerHTML"]`
})
})
@@ -50,15 +48,13 @@ describe('compiler: v-html transform', () => {
[{ code: DOMErrorCodes.X_V_HTML_WITH_CHILDREN }]
])
expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({
arguments: [
`"div"`,
createObjectMatcher({
innerHTML: `[test]`
}),
`null`, // <-- children should have been removed
genFlagText(PatchFlags.PROPS),
`["innerHTML"]`
]
tag: `"div"`,
props: createObjectMatcher({
innerHTML: `[test]`
}),
children: undefined, // <-- children should have been removed
patchFlag: genFlagText(PatchFlags.PROPS),
dynamicProps: `["innerHTML"]`
})
})

View File

@@ -1,4 +1,9 @@
import { parse, transform, CompilerOptions, generate } from '@vue/compiler-core'
import {
baseParse as parse,
transform,
CompilerOptions,
generate
} from '@vue/compiler-core'
import { transformModel } from '../../src/transforms/vModel'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import { DOMErrorCodes } from '../../src/errors'
@@ -58,6 +63,19 @@ describe('compiler: transform v-model', () => {
expect(generate(root).code).toMatchSnapshot()
})
test('input w/ dynamic v-bind', () => {
const root = transformWithModel('<input v-bind="obj" v-model="model" />')
expect(root.helpers).toContain(V_MODEL_DYNAMIC)
expect(generate(root).code).toMatchSnapshot()
const root2 = transformWithModel(
'<input v-bind:[key]="val" v-model="model" />'
)
expect(root2.helpers).toContain(V_MODEL_DYNAMIC)
expect(generate(root2).code).toMatchSnapshot()
})
test('simple expression for select', () => {
const root = transformWithModel('<select v-model="model" />')

View File

@@ -1,17 +1,21 @@
import {
parse,
baseParse as parse,
transform,
CompilerOptions,
ElementNode,
ObjectExpression,
CallExpression,
NodeTypes
NodeTypes,
VNodeCall
} from '@vue/compiler-core'
import { transformOn } from '../../src/transforms/vOn'
import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../../src/runtimeHelpers'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression'
import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils'
import {
createObjectMatcher,
genFlagText
} from '../../../compiler-core/__tests__/testUtils'
import { PatchFlags } from '@vue/shared'
function parseWithVOn(template: string, options: CompilerOptions = {}) {
const ast = parse(template)
@@ -24,8 +28,8 @@ function parseWithVOn(template: string, options: CompilerOptions = {}) {
})
return {
root: ast,
props: (((ast.children[0] as ElementNode).codegenNode as CallExpression)
.arguments[1] as ObjectExpression).properties
props: (((ast.children[0] as ElementNode).codegenNode as VNodeCall)
.props as ObjectExpression).properties
}
}
@@ -148,6 +152,58 @@ describe('compiler-dom: transform v-on', () => {
})
})
test('should transform click.right', () => {
const {
props: [prop]
} = parseWithVOn(`<div @click.right="test"/>`)
expect(prop.key).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `onContextmenu`
})
// dynamic
const {
props: [prop2]
} = parseWithVOn(`<div @[event].right="test"/>`)
// ("on" + (event)).toLowerCase() === "onclick" ? "onContextmenu" : ("on" + (event))
expect(prop2.key).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`(`,
{ children: [`"on" + (`, { content: 'event' }, `)`] },
`).toLowerCase() === "onclick" ? "onContextmenu" : (`,
{ children: [`"on" + (`, { content: 'event' }, `)`] },
`)`
]
})
})
test('should transform click.middle', () => {
const {
props: [prop]
} = parseWithVOn(`<div @click.middle="test"/>`)
expect(prop.key).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `onMouseup`
})
// dynamic
const {
props: [prop2]
} = parseWithVOn(`<div @[event].middle="test"/>`)
// ("on" + (event)).toLowerCase() === "onclick" ? "onMouseup" : ("on" + (event))
expect(prop2.key).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`(`,
{ children: [`"on" + (`, { content: 'event' }, `)`] },
`).toLowerCase() === "onclick" ? "onMouseup" : (`,
{ children: [`"on" + (`, { content: 'event' }, `)`] },
`)`
]
})
})
test('cache handler w/ modifiers', () => {
const {
root,
@@ -157,8 +213,11 @@ describe('compiler-dom: transform v-on', () => {
cacheHandlers: true
})
expect(root.cached).toBe(1)
// should not treat cached handler as dynamicProp, so no flags
expect((root as any).children[0].codegenNode.arguments.length).toBe(2)
// should not treat cached handler as dynamicProp, so it should have no
// dynamicProps flags and only the hydration flag
expect((root as any).children[0].codegenNode.patchFlag).toBe(
genFlagText(PatchFlags.HYDRATE_EVENTS)
)
expect(prop.value).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,

View File

@@ -1,4 +1,9 @@
import { parse, transform, generate, CompilerOptions } from '@vue/compiler-core'
import {
baseParse as parse,
transform,
generate,
CompilerOptions
} from '@vue/compiler-core'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import { transformShow } from '../../src/transforms/vShow'
import { DOMErrorCodes } from '../../src/errors'

View File

@@ -1,5 +1,5 @@
import {
parse,
baseParse as parse,
transform,
PlainElementNode,
CompilerOptions
@@ -29,15 +29,13 @@ describe('compiler: v-text transform', () => {
it('should convert v-text to textContent', () => {
const ast = transformWithVText(`<div v-text="test"/>`)
expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({
arguments: [
`"div"`,
createObjectMatcher({
textContent: `[test]`
}),
`null`,
genFlagText(PatchFlags.PROPS),
`["textContent"]`
]
tag: `"div"`,
props: createObjectMatcher({
textContent: `[test]`
}),
children: undefined,
patchFlag: genFlagText(PatchFlags.PROPS),
dynamicProps: `["textContent"]`
})
})
@@ -50,15 +48,13 @@ describe('compiler: v-text transform', () => {
[{ code: DOMErrorCodes.X_V_TEXT_WITH_CHILDREN }]
])
expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({
arguments: [
`"div"`,
createObjectMatcher({
textContent: `[test]`
}),
`null`, // <-- children should have been removed
genFlagText(PatchFlags.PROPS),
`["textContent"]`
]
tag: `"div"`,
props: createObjectMatcher({
textContent: `[test]`
}),
children: undefined, // <-- children should have been removed
patchFlag: genFlagText(PatchFlags.PROPS),
dynamicProps: `["textContent"]`
})
})

View File

@@ -0,0 +1,140 @@
import { compile } from '../../src'
describe('compiler warnings', () => {
describe('Transition', () => {
function checkWarning(
template: string,
shouldWarn: boolean,
message = `<Transition> expects exactly one child element or component.`
) {
const spy = jest.fn()
compile(template.trim(), {
hoistStatic: true,
transformHoist: null,
onError: err => {
spy(err.message)
}
})
if (shouldWarn) expect(spy).toHaveBeenCalledWith(message)
else expect(spy).not.toHaveBeenCalled()
}
test('warns if multiple children', () => {
checkWarning(
`
<transition>
<div>hey</div>
<div>hey</div>
</transition>
`,
true
)
})
test('warns with v-for', () => {
checkWarning(
`
<transition>
<div v-for="i in items">hey</div>
</transition>
`,
true
)
})
test('warns with multiple v-if + v-for', () => {
checkWarning(
`
<transition>
<div v-if="a" v-for="i in items">hey</div>
<div v-else v-for="i in items">hey</div>
</transition>
`,
true
)
})
test('warns with template v-if', () => {
checkWarning(
`
<transition>
<template v-if="ok"></template>
</transition>
`,
true
)
})
test('warns with multiple templates', () => {
checkWarning(
`
<transition>
<template v-if="a"></template>
<template v-else></template>
</transition>
`,
true
)
})
test('warns if multiple children with v-if', () => {
checkWarning(
`
<transition>
<div v-if="one">hey</div>
<div v-if="other">hey</div>
</transition>
`,
true
)
})
test('does not warn with regular element', () => {
checkWarning(
`
<transition>
<div>hey</div>
</transition>
`,
false
)
})
test('does not warn with one single v-if', () => {
checkWarning(
`
<transition>
<div v-if="a">hey</div>
</transition>
`,
false
)
})
test('does not warn with v-if v-else-if v-else', () => {
checkWarning(
`
<transition>
<div v-if="a">hey</div>
<div v-else-if="b">hey</div>
<div v-else>hey</div>
</transition>
`,
false
)
})
test('does not warn with v-if v-else', () => {
checkWarning(
`
<transition>
<div v-if="a">hey</div>
<div v-else>hey</div>
</transition>
`,
false
)
})
})
})

View File

@@ -1,18 +1,18 @@
{
"name": "@vue/compiler-dom",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.11",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",
"types": "dist/compiler-dom.d.ts",
"unpkg": "dist/compiler-dom/global.js",
"files": [
"index.js",
"dist"
],
"types": "dist/compiler-dom.d.ts",
"unpkg": "dist/compiler-dom/global.js",
"sideEffects": false,
"buildOptions": {
"name": "VueDOMCompiler",
"name": "VueCompilerDOM",
"formats": [
"esm-bundler",
"cjs",
@@ -22,7 +22,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/vue.git"
"url": "git+https://github.com/vuejs/vue-next.git"
},
"keywords": [
"vue"
@@ -30,10 +30,11 @@
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/vue/issues"
"url": "https://github.com/vuejs/vue-next/issues"
},
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-dom#readme",
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-dom#readme",
"dependencies": {
"@vue/compiler-core": "3.0.0-alpha.0"
"@vue/shared": "3.0.0-alpha.11",
"@vue/compiler-core": "3.0.0-alpha.11"
}
}

View File

@@ -28,7 +28,10 @@ export const enum DOMErrorCodes {
X_V_MODEL_ON_INVALID_ELEMENT,
X_V_MODEL_ARG_ON_ELEMENT,
X_V_MODEL_ON_FILE_INPUT_ELEMENT,
X_V_SHOW_NO_EXPRESSION
X_V_MODEL_UNNECESSARY_VALUE,
X_V_SHOW_NO_EXPRESSION,
X_TRANSITION_INVALID_CHILDREN,
__EXTEND_POINT__
}
export const DOMErrorMessages: { [code: number]: string } = {
@@ -39,5 +42,7 @@ export const DOMErrorMessages: { [code: number]: string } = {
[DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT]: `v-model can only be used on <input>, <textarea> and <select> elements.`,
[DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT]: `v-model argument is not supported on plain elements.`,
[DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT]: `v-model cannot used on file inputs since they are read-only. Use a v-on:change listener instead.`,
[DOMErrorCodes.X_V_SHOW_NO_EXPRESSION]: `v-show is missing expression.`
[DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE]: `Unnecessary value binding used alongside v-model. It will interfere with v-model's behavior.`,
[DOMErrorCodes.X_V_SHOW_NO_EXPRESSION]: `v-show is missing expression.`,
[DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN]: `<Transition> expects exactly one child element or component.`
}

View File

@@ -1,45 +1,67 @@
import {
baseCompile,
baseParse,
CompilerOptions,
CodegenResult,
isBuiltInType
ParserOptions,
RootNode,
noopDirectiveTransform,
NodeTransform,
DirectiveTransform
} from '@vue/compiler-core'
import { parserOptionsMinimal } from './parserOptionsMinimal'
import { parserOptionsStandard } from './parserOptionsStandard'
import { transformStyle } from './transforms/transformStyle'
import { transformCloak } from './transforms/vCloak'
import { transformVHtml } from './transforms/vHtml'
import { transformVText } from './transforms/vText'
import { transformModel } from './transforms/vModel'
import { transformOn } from './transforms/vOn'
import { transformShow } from './transforms/vShow'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { warnTransitionChildren } from './transforms/warnTransitionChildren'
import { stringifyStatic } from './transforms/stringifyStatic'
export const parserOptions = __BROWSER__
? parserOptionsMinimal
: parserOptionsStandard
export const DOMNodeTransforms: NodeTransform[] = [
transformStyle,
...(__DEV__ ? [warnTransitionChildren] : [])
]
export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {
cloak: noopDirectiveTransform,
html: transformVHtml,
text: transformVText,
model: transformModel, // override compiler-core
on: transformOn, // override compiler-core
show: transformShow
}
export function compile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
return baseCompile(template, {
...parserOptions,
...options,
...(__BROWSER__ ? parserOptionsMinimal : parserOptionsStandard),
nodeTransforms: [transformStyle, ...(options.nodeTransforms || [])],
nodeTransforms: [...DOMNodeTransforms, ...(options.nodeTransforms || [])],
directiveTransforms: {
cloak: transformCloak,
html: transformVHtml,
text: transformVText,
model: transformModel, // override compiler-core
on: transformOn,
show: transformShow,
...DOMDirectiveTransforms,
...(options.directiveTransforms || {})
},
isBuiltInComponent: tag => {
if (isBuiltInType(tag, `Transition`)) {
return TRANSITION
} else if (isBuiltInType(tag, `TransitionGroup`)) {
return TRANSITION_GROUP
}
}
transformHoist: __BROWSER__ ? null : stringifyStatic
})
}
export function parse(template: string, options: ParserOptions = {}): RootNode {
return baseParse(template, {
...parserOptions,
...options
})
}
export * from './runtimeHelpers'
export { transformStyle } from './transforms/transformStyle'
export { createDOMCompilerError, DOMErrorCodes } from './errors'
export * from '@vue/compiler-core'

View File

@@ -3,9 +3,11 @@ import {
ParserOptions,
ElementNode,
Namespaces,
NodeTypes
NodeTypes,
isBuiltInType
} from '@vue/compiler-core'
import { makeMap, isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
const isRawTextContainer = /*#__PURE__*/ makeMap(
'style,iframe,script,noscript',
@@ -23,6 +25,14 @@ export const parserOptionsMinimal: ParserOptions = {
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
isPreTag: tag => tag === 'pre',
isBuiltInComponent: (tag: string): symbol | undefined => {
if (isBuiltInType(tag, `Transition`)) {
return TRANSITION
} else if (isBuiltInType(tag, `TransitionGroup`)) {
return TRANSITION_GROUP
}
},
// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {
let ns = parent ? parent.ns : DOMNamespaces.HTML

View File

@@ -0,0 +1,171 @@
import {
NodeTypes,
ElementNode,
TransformContext,
TemplateChildNode,
SimpleExpressionNode,
createCallExpression,
HoistTransform,
CREATE_STATIC,
ExpressionNode
} from '@vue/compiler-core'
import {
isVoidTag,
isString,
isSymbol,
escapeHtml,
toDisplayString,
normalizeClass,
normalizeStyle,
stringifyStyle
} from '@vue/shared'
// Turn eligible hoisted static trees into stringied static nodes, e.g.
// const _hoisted_1 = createStaticVNode(`<div class="foo">bar</div>`)
// This is only performed in non-in-browser compilations.
export const stringifyStatic: HoistTransform = (node, context) => {
if (shouldOptimize(node)) {
return createCallExpression(context.helper(CREATE_STATIC), [
JSON.stringify(stringifyElement(node, context))
])
} else {
return node.codegenNode!
}
}
export const enum StringifyThresholds {
ELEMENT_WITH_BINDING_COUNT = 5,
NODE_COUNT = 20
}
// Opt-in heuristics based on:
// 1. number of elements with attributes > 5.
// 2. OR: number of total nodes > 20
// For some simple trees, the performance can actually be worse.
// it is only worth it when the tree is complex enough
// (e.g. big piece of static content)
function shouldOptimize(node: ElementNode): boolean {
let bindingThreshold = StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
let nodeThreshold = StringifyThresholds.NODE_COUNT
// TODO: check for cases where using innerHTML will result in different
// output compared to imperative node insertions.
// probably only need to check for most common case
// i.e. non-phrasing-content tags inside `<p>`
function walk(node: ElementNode) {
for (let i = 0; i < node.children.length; i++) {
if (--nodeThreshold === 0) {
return true
}
const child = node.children[i]
if (child.type === NodeTypes.ELEMENT) {
if (child.props.length > 0 && --bindingThreshold === 0) {
return true
}
if (walk(child)) {
return true
}
}
}
return false
}
return walk(node)
}
function stringifyElement(
node: ElementNode,
context: TransformContext
): string {
let res = `<${node.tag}`
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) {
res += ` ${p.name}`
if (p.value) {
res += `="${escapeHtml(p.value.content)}"`
}
} else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
// constant v-bind, e.g. :foo="1"
let evaluated = evaluateConstant(p.exp as SimpleExpressionNode)
const arg = p.arg && (p.arg as SimpleExpressionNode).content
if (arg === 'class') {
evaluated = normalizeClass(evaluated)
} else if (arg === 'style') {
evaluated = stringifyStyle(normalizeStyle(evaluated))
}
res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml(
evaluated
)}"`
}
}
if (context.scopeId) {
res += ` ${context.scopeId}`
}
res += `>`
for (let i = 0; i < node.children.length; i++) {
res += stringifyNode(node.children[i], context)
}
if (!isVoidTag(node.tag)) {
res += `</${node.tag}>`
}
return res
}
function stringifyNode(
node: string | TemplateChildNode,
context: TransformContext
): string {
if (isString(node)) {
return node
}
if (isSymbol(node)) {
return ``
}
switch (node.type) {
case NodeTypes.ELEMENT:
return stringifyElement(node, context)
case NodeTypes.TEXT:
return escapeHtml(node.content)
case NodeTypes.COMMENT:
return `<!--${escapeHtml(node.content)}-->`
case NodeTypes.INTERPOLATION:
return escapeHtml(toDisplayString(evaluateConstant(node.content)))
case NodeTypes.COMPOUND_EXPRESSION:
return escapeHtml(evaluateConstant(node))
case NodeTypes.TEXT_CALL:
return stringifyNode(node.content, context)
default:
// static trees will not contain if/for nodes
return ''
}
}
// __UNSAFE__
// Reason: eval.
// It's technically safe to eval because only constant expressions are possible
// here, e.g. `{{ 1 }}` or `{{ 'foo' }}`
// in addition, constant exps bail on presence of parens so you can't even
// run JSFuck in here. But we mark it unsafe for security review purposes.
// (see compiler-core/src/transformExpressions)
function evaluateConstant(exp: ExpressionNode): string {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
return new Function(`return ${exp.content}`)()
} else {
// compound
let res = ``
exp.children.forEach(c => {
if (isString(c) || isSymbol(c)) {
return
}
if (c.type === NodeTypes.TEXT) {
res += c.content
} else if (c.type === NodeTypes.INTERPOLATION) {
res += toDisplayString(evaluateConstant(c.content))
} else {
res += evaluateConstant(c)
}
})
return res
}
}

View File

@@ -1,7 +1,9 @@
import {
NodeTransform,
NodeTypes,
createSimpleExpression
createSimpleExpression,
SimpleExpressionNode,
SourceLocation
} from '@vue/compiler-core'
// Parse inline CSS strings for static style attributes into an object.
@@ -15,13 +17,11 @@ export const transformStyle: NodeTransform = (node, context) => {
node.props.forEach((p, i) => {
if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) {
// replace p with an expression node
const parsed = JSON.stringify(parseInlineCSS(p.value.content))
const exp = context.hoist(createSimpleExpression(parsed, false, p.loc))
node.props[i] = {
type: NodeTypes.DIRECTIVE,
name: `bind`,
arg: createSimpleExpression(`style`, true, p.loc),
exp,
exp: parseInlineCSS(p.value.content, p.loc),
modifiers: [],
loc: p.loc
}
@@ -33,7 +33,10 @@ export const transformStyle: NodeTransform = (node, context) => {
const listDelimiterRE = /;(?![^(]*\))/g
const propertyDelimiterRE = /:(.+)/
function parseInlineCSS(cssText: string): Record<string, string> {
function parseInlineCSS(
cssText: string,
loc: SourceLocation
): SimpleExpressionNode {
const res: Record<string, string> = {}
cssText.split(listDelimiterRE).forEach(item => {
if (item) {
@@ -41,5 +44,5 @@ function parseInlineCSS(cssText: string): Record<string, string> {
tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim())
}
})
return res
return createSimpleExpression(JSON.stringify(res), false, loc, true)
}

View File

@@ -1,5 +0,0 @@
import { DirectiveTransform } from 'packages/compiler-core/src/transform'
export const transformCloak: DirectiveTransform = (node, context) => {
return { props: [], needRuntime: false }
}

View File

@@ -24,7 +24,6 @@ export const transformVHtml: DirectiveTransform = (dir, node, context) => {
createSimpleExpression(`innerHTML`, true, loc),
exp || createSimpleExpression('', true)
)
],
needRuntime: false
]
}
}

View File

@@ -3,7 +3,8 @@ import {
DirectiveTransform,
ElementTypes,
findProp,
NodeTypes
NodeTypes,
hasDynamicKeyVBind
} from '@vue/compiler-core'
import { createDOMCompilerError, DOMErrorCodes } from '../errors'
import {
@@ -16,68 +17,104 @@ import {
export const transformModel: DirectiveTransform = (dir, node, context) => {
const baseResult = baseTransform(dir, node, context)
// base transform has errors
if (!baseResult.props.length) {
// base transform has errors OR component v-model (only need props)
if (!baseResult.props.length || node.tagType === ElementTypes.COMPONENT) {
return baseResult
}
const { tag, tagType } = node
if (tagType === ElementTypes.ELEMENT) {
if (dir.arg) {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
dir.arg.loc
)
if (dir.arg) {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
dir.arg.loc
)
}
)
}
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
let directiveToUse = V_MODEL_TEXT
let isInvalidType = false
if (tag === 'input') {
const type = findProp(node, `type`)
if (type) {
if (type.type === NodeTypes.DIRECTIVE) {
// :type="foo"
directiveToUse = V_MODEL_DYNAMIC
} else if (type.value) {
switch (type.value.content) {
case 'radio':
directiveToUse = V_MODEL_RADIO
break
case 'checkbox':
directiveToUse = V_MODEL_CHECKBOX
break
case 'file':
isInvalidType = true
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
dir.loc
)
)
break
}
}
}
} else if (tag === 'select') {
directiveToUse = V_MODEL_SELECT
}
// inject runtime directive
// by returning the helper symbol via needRuntime
// the import will replaced a resolveDirective call.
if (!isInvalidType) {
baseResult.needRuntime = context.helper(directiveToUse)
}
} else {
function checkDuplicatedValue() {
const value = findProp(node, 'value')
if (value) {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
dir.loc
DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
value.loc
)
)
}
}
const { tag } = node
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
let directiveToUse = V_MODEL_TEXT
let isInvalidType = false
if (tag === 'input') {
const type = findProp(node, `type`)
if (type) {
if (type.type === NodeTypes.DIRECTIVE) {
// :type="foo"
directiveToUse = V_MODEL_DYNAMIC
} else if (type.value) {
switch (type.value.content) {
case 'radio':
directiveToUse = V_MODEL_RADIO
break
case 'checkbox':
directiveToUse = V_MODEL_CHECKBOX
break
case 'file':
isInvalidType = true
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
dir.loc
)
)
break
default:
// text type
__DEV__ && checkDuplicatedValue()
break
}
}
} else if (hasDynamicKeyVBind(node)) {
// element has bindings with dynamic keys, which can possibly contain
// "type".
directiveToUse = V_MODEL_DYNAMIC
} else {
// text type
__DEV__ && checkDuplicatedValue()
}
} else if (tag === 'select') {
directiveToUse = V_MODEL_SELECT
} else if (tag === 'textarea') {
__DEV__ && checkDuplicatedValue()
}
// inject runtime directive
// by returning the helper symbol via needRuntime
// the import will replaced a resolveDirective call.
if (!isInvalidType) {
baseResult.needRuntime = context.helper(directiveToUse)
}
} else {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
dir.loc
)
)
}
// native vmodel doesn't need the `modelValue` props since they are also
// passed to the runtime as `binding.value`. removing it reduces code size.
baseResult.props = baseResult.props.filter(p => {
if (
p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
p.key.content === 'modelValue'
) {
return false
}
return true
})
return baseResult
}

View File

@@ -5,7 +5,9 @@ import {
createCallExpression,
createObjectExpression,
createSimpleExpression,
NodeTypes
NodeTypes,
createCompoundExpression,
ExpressionNode
} from '@vue/compiler-core'
import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../runtimeHelpers'
import { makeMap } from '@vue/shared'
@@ -52,6 +54,24 @@ const generateModifiers = (modifiers: string[]) => {
}
}
const transformClick = (key: ExpressionNode, event: string) => {
const isStaticClick =
key.type === NodeTypes.SIMPLE_EXPRESSION &&
key.isStatic &&
key.content.toLowerCase() === 'onclick'
return isStaticClick
? createSimpleExpression(event, true)
: key.type !== NodeTypes.SIMPLE_EXPRESSION
? createCompoundExpression([
`(`,
key,
`).toLowerCase() === "onclick" ? "${event}" : (`,
key,
`)`
])
: key
}
export const transformOn: DirectiveTransform = (dir, node, context) => {
return baseTransform(dir, node, context, baseResult => {
const { modifiers } = dir
@@ -64,6 +84,14 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
eventOptionModifiers
} = generateModifiers(modifiers)
// normalize click.right and click.middle since they don't actually fire
if (nonKeyModifiers.includes('right')) {
key = transformClick(key, `onContextmenu`)
}
if (nonKeyModifiers.includes('middle')) {
key = transformClick(key, `onMouseup`)
}
if (nonKeyModifiers.length) {
handlerExp = createCallExpression(context.helper(V_ON_WITH_MODIFIERS), [
handlerExp,
@@ -102,8 +130,7 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
}
return {
props: [createObjectProperty(key, handlerExp)],
needRuntime: false
props: [createObjectProperty(key, handlerExp)]
}
})
}

View File

@@ -24,7 +24,6 @@ export const transformVText: DirectiveTransform = (dir, node, context) => {
createSimpleExpression(`textContent`, true, loc),
exp || createSimpleExpression('', true)
)
],
needRuntime: false
]
}
}

View File

@@ -0,0 +1,43 @@
import {
NodeTransform,
NodeTypes,
ElementTypes,
ComponentNode,
IfBranchNode
} from '@vue/compiler-core'
import { TRANSITION } from '../runtimeHelpers'
import { createDOMCompilerError, DOMErrorCodes } from '../errors'
export const warnTransitionChildren: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.COMPONENT
) {
const component = context.isBuiltInComponent(node.tag)
if (component === TRANSITION) {
return () => {
if (node.children.length && hasMultipleChildren(node)) {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN,
{
start: node.children[0].loc.start,
end: node.children[node.children.length - 1].loc.end,
source: ''
}
)
)
}
}
}
}
}
function hasMultipleChildren(node: ComponentNode | IfBranchNode): boolean {
const child = node.children[0]
return (
node.children.length !== 1 ||
child.type === NodeTypes.FOR ||
(child.type === NodeTypes.IF && child.branches.some(hasMultipleChildren))
)
}

View File

@@ -2,7 +2,7 @@
exports[`source map 1`] = `
Object {
"mappings": ";;;;UAAA,aACE;IAAK,gCAAMA,WAAM",
"mappings": ";;;wBACE,aAA8B;IAAzB,aAAmB,4BAAbA,WAAM",
"names": Array [
"render",
],
@@ -20,8 +20,8 @@ Object {
exports[`template errors 1`] = `
Array [
[SyntaxError: Invalid JavaScript expression. (2:13)],
[SyntaxError: v-bind is missing expression. (1:6)],
[SyntaxError: v-model can only be used on <input>, <textarea> and <select> elements. (2:17)],
[SyntaxError: Error parsing JavaScript expression: Unexpected token (1:3)],
[SyntaxError: v-bind is missing expression.],
[SyntaxError: v-model can only be used on <input>, <textarea> and <select> elements.],
]
`;

View File

@@ -1,39 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler sfc: transform asset url support uri fragment 1`] = `
"import { createVNode, createBlock, openBlock } from \\"vue\\"
"import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
import _imports_0 from '@svg/file.svg'
const _hoisted_1 = _imports_0 + '#fragment'
export default function render() {
const _ctx = this
return (openBlock(), createBlock(\\"use\\", { href: _hoisted_1 }))
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock(\\"use\\", { href: _hoisted_1 }))
}"
`;
exports[`compiler sfc: transform asset url support uri is empty 1`] = `
"import { createVNode, createBlock, openBlock } from \\"vue\\"
"import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
export default function render() {
const _ctx = this
return (openBlock(), createBlock(\\"use\\", { href: '' }))
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock(\\"use\\", { href: '' }))
}"
`;
exports[`compiler sfc: transform asset url transform assetUrls 1`] = `
"import { createVNode, createBlock, Fragment, openBlock } from \\"vue\\"
"import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
import _imports_0 from './logo.png'
import _imports_1 from 'fixtures/logo.png'
export default function render() {
const _ctx = this
return (openBlock(), createBlock(Fragment, null, [
createVNode(\\"img\\", { src: _imports_0 }),
createVNode(\\"img\\", { src: _imports_1 }),
createVNode(\\"img\\", { src: _imports_1 })
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"img\\", { src: _imports_0 }),
_createVNode(\\"img\\", { src: _imports_1 }),
_createVNode(\\"img\\", { src: _imports_1 }),
_createVNode(\\"img\\", { src: \\"http://example.com/fixtures/logo.png\\" }),
_createVNode(\\"img\\", { src: \\"/fixtures/logo.png\\" })
], 64 /* STABLE_FRAGMENT */))
}"
`;

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler sfc: transform srcset transform srcset 1`] = `
"import { createVNode, createBlock, Fragment, openBlock } from \\"vue\\"
"import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
import _imports_0 from './logo.png'
@@ -12,37 +12,49 @@ const _hoisted_4 = _imports_0 + ', ' + _imports_0 + '2x'
const _hoisted_5 = _imports_0 + '2x, ' + _imports_0
const _hoisted_6 = _imports_0 + '2x, ' + _imports_0 + '3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + '2x, ' + _imports_0 + '3x'
const _hoisted_8 = \\"/logo.png\\" + ', ' + _imports_0 + '2x'
export default function render() {
const _ctx = this
return (openBlock(), createBlock(Fragment, null, [
createVNode(\\"img\\", {
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_1
}),
createVNode(\\"img\\", {
_createVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_2
}),
createVNode(\\"img\\", {
_createVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_3
}),
createVNode(\\"img\\", {
_createVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_4
}),
createVNode(\\"img\\", {
_createVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_5
}),
createVNode(\\"img\\", {
_createVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_6
}),
createVNode(\\"img\\", {
_createVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_7
}),
_createVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: \\"/logo.png, /logo.png 2x\\"
}),
_createVNode(\\"img\\", {
src: \\"https://example.com/logo.png\\",
srcset: \\"https://example.com/logo.png, https://example.com/logo.png 2x\\"
}),
_createVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: _hoisted_8
})
], 64 /* STABLE_FRAGMENT */))
}"

View File

@@ -0,0 +1,237 @@
import { compileStyle } from '../src/compileStyle'
import { mockWarn } from '@vue/shared'
function compile(source: string): string {
const res = compileStyle({
source,
filename: 'test.css',
id: 'test'
})
if (res.errors.length) {
res.errors.forEach(err => {
console.error(err)
})
expect(res.errors.length).toBe(0)
}
return res.code
}
describe('SFC scoped CSS', () => {
mockWarn()
test('simple selectors', () => {
expect(compile(`h1 { color: red; }`)).toMatch(`h1[test] { color: red;`)
expect(compile(`.foo { color: red; }`)).toMatch(`.foo[test] { color: red;`)
})
test('descendent selector', () => {
expect(compile(`h1 .foo { color: red; }`)).toMatch(
`h1 .foo[test] { color: red;`
)
})
test('multiple selectors', () => {
expect(compile(`h1 .foo, .bar, .baz { color: red; }`)).toMatch(
`h1 .foo[test], .bar[test], .baz[test] { color: red;`
)
})
test('pseudo class', () => {
expect(compile(`.foo:after { color: red; }`)).toMatch(
`.foo[test]:after { color: red;`
)
})
test('pseudo element', () => {
expect(compile(`::selection { display: none; }`)).toMatch(
'[test]::selection {'
)
})
test('spaces before pseudo element', () => {
const code = compile(`.abc, ::selection { color: red; }`)
expect(code).toMatch('.abc[test],')
expect(code).toMatch('[test]::selection {')
})
test('::v-deep', () => {
expect(compile(`::v-deep(.foo) { color: red; }`)).toMatchInlineSnapshot(`
"[test] .foo { color: red;
}"
`)
expect(compile(`::v-deep(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
"[test] .foo .bar { color: red;
}"
`)
expect(compile(`.baz .qux ::v-deep(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".baz .qux[test] .foo .bar { color: red;
}"
`)
})
test('::v-slotted', () => {
expect(compile(`::v-slotted(.foo) { color: red; }`)).toMatchInlineSnapshot(`
".foo[test-s] { color: red;
}"
`)
expect(compile(`::v-slotted(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".foo .bar[test-s] { color: red;
}"
`)
expect(compile(`.baz .qux ::v-slotted(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".baz .qux .foo .bar[test-s] { color: red;
}"
`)
})
test('::v-global', () => {
expect(compile(`::v-global(.foo) { color: red; }`)).toMatchInlineSnapshot(`
".foo { color: red;
}"
`)
expect(compile(`::v-global(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".foo .bar { color: red;
}"
`)
// global ignores anything before it
expect(compile(`.baz .qux ::v-global(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".foo .bar { color: red;
}"
`)
})
test('media query', () => {
expect(compile(`@media print { .foo { color: red }}`))
.toMatchInlineSnapshot(`
"@media print {
.foo[test] { color: red
}}"
`)
})
test('supports query', () => {
expect(compile(`@supports(display: grid) { .foo { display: grid }}`))
.toMatchInlineSnapshot(`
"@supports(display: grid) {
.foo[test] { display: grid
}}"
`)
})
test('scoped keyframes', () => {
const style = compile(`
.anim {
animation: color 5s infinite, other 5s;
}
.anim-2 {
animation-name: color;
animation-duration: 5s;
}
.anim-3 {
animation: 5s color infinite, 5s other;
}
.anim-multiple {
animation: color 5s infinite, opacity 2s;
}
.anim-multiple-2 {
animation-name: color, opacity;
animation-duration: 5s, 2s;
}
@keyframes color {
from { color: red; }
to { color: green; }
}
@-webkit-keyframes color {
from { color: red; }
to { color: green; }
}
@keyframes opacity {
from { opacity: 0; }
to { opacity: 1; }
}
@-webkit-keyframes opacity {
from { opacity: 0; }
to { opacity: 1; }
}
`)
expect(style).toContain(
`.anim[test] {\n animation: color-test 5s infinite, other 5s;`
)
expect(style).toContain(`.anim-2[test] {\n animation-name: color-test`)
expect(style).toContain(
`.anim-3[test] {\n animation: 5s color-test infinite, 5s other;`
)
expect(style).toContain(`@keyframes color-test {`)
expect(style).toContain(`@-webkit-keyframes color-test {`)
expect(style).toContain(
`.anim-multiple[test] {\n animation: color-test 5s infinite,opacity-test 2s;`
)
expect(style).toContain(
`.anim-multiple-2[test] {\n animation-name: color-test,opacity-test;`
)
expect(style).toContain(`@keyframes opacity-test {`)
expect(style).toContain(`@-webkit-keyframes opacity-test {`)
})
// vue-loader/#1370
test('spaces after selector', () => {
const { code } = compileStyle({
source: `.foo , .bar { color: red; }`,
filename: 'test.css',
id: 'test'
})
expect(code).toMatchInlineSnapshot(`
".foo[test], .bar[test] { color: red;
}"
`)
})
describe('deprecated syntax', () => {
test('::v-deep as combinator', () => {
expect(compile(`::v-deep .foo { color: red; }`)).toMatchInlineSnapshot(`
"[test] .foo { color: red;
}"
`)
expect(compile(`.bar ::v-deep .foo { color: red; }`))
.toMatchInlineSnapshot(`
".bar[test] .foo { color: red;
}"
`)
expect(
`::v-deep usage as a combinator has been deprecated.`
).toHaveBeenWarned()
})
test('>>> (deprecated syntax)', () => {
const code = compile(`>>> .foo { color: red; }`)
expect(code).toMatchInlineSnapshot(`
"[test] .foo { color: red;
}"
`)
expect(
`the >>> and /deep/ combinators have been deprecated.`
).toHaveBeenWarned()
})
test('/deep/ (deprecated syntax)', () => {
const code = compile(`/deep/ .foo { color: red; }`)
expect(code).toMatchInlineSnapshot(`
"[test] .foo { color: red;
}"
`)
expect(
`the >>> and /deep/ combinators have been deprecated.`
).toHaveBeenWarned()
})
})
})

View File

@@ -9,7 +9,7 @@ test('should work', () => {
expect(result.errors.length).toBe(0)
expect(result.source).toBe(source)
// should expose render fn
expect(result.code).toMatch(`export default function render()`)
expect(result.code).toMatch(`export function render(`)
})
test('preprocess pug', () => {
@@ -23,7 +23,7 @@ body
</template>
`,
{ filename: 'example.vue', sourceMap: true }
).template as SFCTemplateBlock
).descriptor.template as SFCTemplateBlock
const result = compileTemplate({
filename: 'example.vue',
@@ -35,10 +35,10 @@ body
})
test('warn missing preprocessor', () => {
const template = parse(`<template lang="unknownLang">\n</template>\n`, {
const template = parse(`<template lang="unknownLang">hi</template>\n`, {
filename: 'example.vue',
sourceMap: true
}).template as SFCTemplateBlock
}).descriptor.template as SFCTemplateBlock
const result = compileTemplate({
filename: 'example.vue',
@@ -50,7 +50,7 @@ test('warn missing preprocessor', () => {
})
test('transform asset url options', () => {
const input = { source: `<foo bar="baz"/>`, filename: 'example.vue' }
const input = { source: `<foo bar="~baz"/>`, filename: 'example.vue' }
// Object option
const { code: code1 } = compileTemplate({
...input,
@@ -70,10 +70,10 @@ test('source map', () => {
`
<template>
<div><p>{{ render }}</p></div>
</template>
</template>
`,
{ filename: 'example.vue', sourceMap: true }
).template as SFCTemplateBlock
).descriptor.template as SFCTemplateBlock
const result = compileTemplate({
filename: 'example.vue',
@@ -86,7 +86,7 @@ test('source map', () => {
test('template errors', () => {
const result = compileTemplate({
filename: 'example.vue',
source: `<div :foo
source: `<div :foo
:bar="a[" v-model="baz"/>`
})
expect(result.errors).toMatchSnapshot()
@@ -100,7 +100,7 @@ test('preprocessor errors', () => {
</template>
`,
{ filename: 'example.vue', sourceMap: true }
).template as SFCTemplateBlock
).descriptor.template as SFCTemplateBlock
const result = compileTemplate({
filename: 'example.vue',

View File

@@ -1,21 +1,40 @@
import { parse } from '../src'
import { mockWarn } from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
import { baseParse, baseCompile } from '@vue/compiler-core'
import { SourceMapConsumer } from 'source-map'
describe('compiler:sfc', () => {
mockWarn()
describe('source map', () => {
test('style block', () => {
const style = parse(`<style>\n.color {\n color: red;\n }\n</style>\n`)
.styles[0]
// TODO need to actually test this with SourceMapConsumer
// Padding determines how many blank lines will there be before the style block
const padding = Math.round(Math.random() * 10)
const style = parse(
`${'\n'.repeat(padding)}<style>\n.color {\n color: red;\n }\n</style>\n`
).descriptor.styles[0]
expect(style.map).not.toBeUndefined()
const consumer = new SourceMapConsumer(style.map!)
consumer.eachMapping(mapping => {
expect(mapping.originalLine - mapping.generatedLine).toBe(padding)
})
})
test('script block', () => {
const script = parse(`<script>\nconsole.log(1)\n }\n</script>\n`).script
// TODO need to actually test this with SourceMapConsumer
// Padding determines how many blank lines will there be before the style block
const padding = Math.round(Math.random() * 10)
const script = parse(
`${'\n'.repeat(padding)}<script>\nconsole.log(1)\n }\n</script>\n`
).descriptor.script
expect(script!.map).not.toBeUndefined()
const consumer = new SourceMapConsumer(script!.map!)
consumer.eachMapping(mapping => {
expect(mapping.originalLine - mapping.generatedLine).toBe(padding)
})
})
})
@@ -30,12 +49,12 @@ export default {}
<style>
h1 { color: red }
</style>`
const padFalse = parse(content.trim(), { pad: false })
const padFalse = parse(content.trim(), { pad: false }).descriptor
expect(padFalse.template!.content).toBe('\n<div></div>\n')
expect(padFalse.script!.content).toBe('\nexport default {}\n')
expect(padFalse.styles[0].content).toBe('\nh1 { color: red }\n')
const padTrue = parse(content.trim(), { pad: true })
const padTrue = parse(content.trim(), { pad: true }).descriptor
expect(padTrue.script!.content).toBe(
Array(3 + 1).join('//\n') + '\nexport default {}\n'
)
@@ -43,7 +62,7 @@ h1 { color: red }
Array(6 + 1).join('\n') + '\nh1 { color: red }\n'
)
const padLine = parse(content.trim(), { pad: 'line' })
const padLine = parse(content.trim(), { pad: 'line' }).descriptor
expect(padLine.script!.content).toBe(
Array(3 + 1).join('//\n') + '\nexport default {}\n'
)
@@ -51,7 +70,7 @@ h1 { color: red }
Array(6 + 1).join('\n') + '\nh1 { color: red }\n'
)
const padSpace = parse(content.trim(), { pad: 'space' })
const padSpace = parse(content.trim(), { pad: 'space' }).descriptor
expect(padSpace.script!.content).toBe(
`<template>\n<div></div>\n</template>\n<script>`.replace(/./g, ' ') +
'\nexport default {}\n'
@@ -65,13 +84,55 @@ h1 { color: red }
})
test('should ignore nodes with no content', () => {
expect(parse(`<template/>`).template).toBe(null)
expect(parse(`<script/>`).script).toBe(null)
expect(parse(`<style/>`).styles.length).toBe(0)
expect(parse(`<custom/>`).customBlocks.length).toBe(0)
expect(parse(`<template/>`).descriptor.template).toBe(null)
expect(parse(`<script/>`).descriptor.script).toBe(null)
expect(parse(`<style/>`).descriptor.styles.length).toBe(0)
expect(parse(`<custom/>`).descriptor.customBlocks.length).toBe(0)
})
describe('error', () => {
test('handle empty nodes with src attribute', () => {
const { descriptor } = parse(`<script src="com"/>`)
expect(descriptor.script).toBeTruthy()
expect(descriptor.script!.content).toBeFalsy()
expect(descriptor.script!.attrs['src']).toBe('com')
})
test('nested templates', () => {
const content = `
<template v-if="ok">ok</template>
<div><div></div></div>
`
const { descriptor } = parse(`<template>${content}</template>`)
expect(descriptor.template!.content).toBe(content)
})
test('error tolerance', () => {
const { errors } = parse(`<template>`)
expect(errors.length).toBe(1)
})
test('should parse as DOM by default', () => {
const { errors } = parse(`<template><input></template>`)
expect(errors.length).toBe(0)
})
test('custom compiler', () => {
const { errors } = parse(`<template><input></template>`, {
compiler: {
parse: baseParse,
compile: baseCompile
}
})
expect(errors.length).toBe(1)
})
test('treat custom blocks as raw text', () => {
const { errors, descriptor } = parse(`<foo> <-& </foo>`)
expect(errors.length).toBe(0)
expect(descriptor.customBlocks[0].content).toBe(` <-& `)
})
describe('warnings', () => {
test('should only allow single template element', () => {
parse(`<template><div/></template><template><div/></template>`)
expect(

View File

@@ -1,10 +1,10 @@
import { generate, parse, transform } from '@vue/compiler-core'
import { generate, baseParse, transform } from '@vue/compiler-core'
import { transformAssetUrl } from '../src/templateTransformAssetUrl'
import { transformElement } from '../../compiler-core/src/transforms/transformElement'
import { transformBind } from '../../compiler-core/src/transforms/vBind'
function compileWithAssetUrls(template: string) {
const ast = parse(template)
const ast = baseParse(template)
transform(ast, {
nodeTransforms: [transformAssetUrl, transformElement],
directiveTransforms: {
@@ -20,6 +20,8 @@ describe('compiler sfc: transform asset url', () => {
<img src="./logo.png"/>
<img src="~fixtures/logo.png"/>
<img src="~/fixtures/logo.png"/>
<img src="http://example.com/fixtures/logo.png"/>
<img src="/fixtures/logo.png"/>
`)
expect(result.code).toMatchSnapshot()

View File

@@ -1,10 +1,10 @@
import { generate, parse, transform } from '@vue/compiler-core'
import { generate, baseParse, transform } from '@vue/compiler-core'
import { transformSrcset } from '../src/templateTransformSrcset'
import { transformElement } from '../../compiler-core/src/transforms/transformElement'
import { transformBind } from '../../compiler-core/src/transforms/vBind'
function compileWithSrcset(template: string) {
const ast = parse(template)
const ast = baseParse(template)
transform(ast, {
nodeTransforms: [transformSrcset, transformElement],
directiveTransforms: {
@@ -24,6 +24,9 @@ describe('compiler sfc: transform srcset', () => {
<img src="./logo.png" srcset="./logo.png 2x, ./logo.png"/>
<img src="./logo.png" srcset="./logo.png 2x, ./logo.png 3x"/>
<img src="./logo.png" srcset="./logo.png, ./logo.png 2x, ./logo.png 3x"/>
<img src="/logo.png" srcset="/logo.png, /logo.png 2x"/>
<img src="https://example.com/logo.png" srcset="https://example.com/logo.png, https://example.com/logo.png 2x"/>
<img src="/logo.png" srcset="/logo.png, ./logo.png 2x"/>
`)
expect(result.code).toMatchSnapshot()

View File

@@ -1,12 +1,12 @@
{
"name": "@vue/compiler-sfc",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.11",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"types": "dist/compiler-sfc.d.ts",
"files": [
"dist"
],
"types": "dist/compiler-sfc.d.ts",
"buildOptions": {
"prod": false,
"formats": [
@@ -15,7 +15,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/vue.git"
"url": "git+https://github.com/vuejs/vue-next.git"
},
"keywords": [
"vue"
@@ -23,19 +23,24 @@
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/vue/issues"
"url": "https://github.com/vuejs/vue-next/issues"
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-sfc#readme",
"peerDependencies": {
"vue": "3.0.0-alpha.11"
},
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-sfc#readme",
"dependencies": {
"@vue/compiler-core": "3.0.0-alpha.0",
"@vue/compiler-dom": "3.0.0-alpha.0",
"@vue/shared": "3.0.0-alpha.11",
"@vue/compiler-core": "3.0.0-alpha.11",
"@vue/compiler-dom": "3.0.0-alpha.11",
"@vue/compiler-ssr": "3.0.0-alpha.11",
"consolidate": "^0.15.1",
"hash-sum": "^2.0.0",
"lru-cache": "^5.1.1",
"merge-source-map": "^1.1.0",
"postcss": "^7.0.21",
"postcss-selector-parser": "^6.0.2",
"source-map": "^0.7.3"
"source-map": "^0.6.1"
},
"devDependencies": {
"@types/consolidate": "^0.14.0",

View File

@@ -10,7 +10,7 @@ import {
} from './stylePreprocessors'
import { RawSourceMap } from 'source-map'
export interface StyleCompileOptions {
export interface SFCStyleCompileOptions {
source: string
filename: string
id: string
@@ -23,11 +23,11 @@ export interface StyleCompileOptions {
postcssPlugins?: any[]
}
export interface AsyncStyleCompileOptions extends StyleCompileOptions {
export interface SFCAsyncStyleCompileOptions extends SFCStyleCompileOptions {
isAsync?: boolean
}
export interface StyleCompileResults {
export interface SFCStyleCompileResults {
code: string
map: RawSourceMap | undefined
rawResult: LazyResult | Result | undefined
@@ -35,22 +35,25 @@ export interface StyleCompileResults {
}
export function compileStyle(
options: StyleCompileOptions
): StyleCompileResults {
return doCompileStyle({ ...options, isAsync: false }) as StyleCompileResults
options: SFCStyleCompileOptions
): SFCStyleCompileResults {
return doCompileStyle({
...options,
isAsync: false
}) as SFCStyleCompileResults
}
export function compileStyleAsync(
options: StyleCompileOptions
): Promise<StyleCompileResults> {
options: SFCStyleCompileOptions
): Promise<SFCStyleCompileResults> {
return doCompileStyle({ ...options, isAsync: true }) as Promise<
StyleCompileResults
SFCStyleCompileResults
>
}
export function doCompileStyle(
options: AsyncStyleCompileOptions
): StyleCompileResults | Promise<StyleCompileResults> {
options: SFCAsyncStyleCompileOptions
): SFCStyleCompileResults | Promise<SFCStyleCompileResults> {
const {
filename,
id,
@@ -131,7 +134,7 @@ export function doCompileStyle(
}
function preprocess(
options: StyleCompileOptions,
options: SFCStyleCompileOptions,
preprocessor: StylePreprocessor
): StylePreprocessorResults {
return preprocessor.render(options.source, options.map, {

View File

@@ -2,9 +2,11 @@ import {
CompilerOptions,
CodegenResult,
CompilerError,
NodeTransform
NodeTransform,
ParserOptions,
RootNode
} from '@vue/compiler-core'
import { RawSourceMap } from 'source-map'
import { SourceMapConsumer, SourceMapGenerator, RawSourceMap } from 'source-map'
import {
transformAssetUrl,
AssetURLOptions,
@@ -14,7 +16,12 @@ import { transformSrcset } from './templateTransformSrcset'
import { isObject } from '@vue/shared'
import consolidate from 'consolidate'
export interface TemplateCompileResults {
export interface TemplateCompiler {
compile(template: string, options: CompilerOptions): CodegenResult
parse(template: string, options: ParserOptions): RootNode
}
export interface SFCTemplateCompileResults {
code: string
source: string
tips: string[]
@@ -22,13 +29,11 @@ export interface TemplateCompileResults {
map?: RawSourceMap
}
export interface TemplateCompiler {
compile(template: string, options: CompilerOptions): CodegenResult
}
export interface TemplateCompileOptions {
export interface SFCTemplateCompileOptions {
source: string
filename: string
ssr?: boolean
inMap?: RawSourceMap
compiler?: TemplateCompiler
compilerOptions?: CompilerOptions
preprocessLang?: string
@@ -37,7 +42,7 @@ export interface TemplateCompileOptions {
}
function preprocess(
{ source, filename, preprocessOptions }: TemplateCompileOptions,
{ source, filename, preprocessOptions }: SFCTemplateCompileOptions,
preprocessor: any
): string {
// Consolidate exposes a callback based API, but the callback is in fact
@@ -59,8 +64,8 @@ function preprocess(
}
export function compileTemplate(
options: TemplateCompileOptions
): TemplateCompileResults {
options: SFCTemplateCompileOptions
): SFCTemplateCompileResults {
const { preprocessLang } = options
const preprocessor =
preprocessLang && consolidate[preprocessLang as keyof typeof consolidate]
@@ -100,11 +105,13 @@ export function compileTemplate(
function doCompileTemplate({
filename,
inMap,
source,
compiler = require('@vue/compiler-dom'),
ssr = false,
compiler = ssr ? require('@vue/compiler-ssr') : require('@vue/compiler-dom'),
compilerOptions = {},
transformAssetUrls
}: TemplateCompileOptions): TemplateCompileResults {
}: SFCTemplateCompileOptions): SFCTemplateCompileResults {
const errors: CompilerError[] = []
let nodeTransforms: NodeTransform[] = []
@@ -117,7 +124,7 @@ function doCompileTemplate({
nodeTransforms = [transformAssetUrl, transformSrcset]
}
const { code, map } = compiler.compile(source, {
let { code, map } = compiler.compile(source, {
mode: 'module',
prefixIdentifiers: true,
hoistStatic: true,
@@ -128,5 +135,91 @@ function doCompileTemplate({
sourceMap: true,
onError: e => errors.push(e)
})
// inMap should be the map produced by ./parse.ts which is a simple line-only
// mapping. If it is present, we need to adjust the final map and errors to
// reflect the original line numbers.
if (inMap) {
if (map) {
map = mapLines(inMap, map)
}
if (errors.length) {
patchErrors(errors, source, inMap)
}
}
return { code, source, errors, tips: [], map }
}
function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
if (!oldMap) return newMap
if (!newMap) return oldMap
const oldMapConsumer = new SourceMapConsumer(oldMap)
const newMapConsumer = new SourceMapConsumer(newMap)
const mergedMapGenerator = new SourceMapGenerator()
newMapConsumer.eachMapping(m => {
if (m.originalLine == null) {
return
}
const origPosInOldMap = oldMapConsumer.originalPositionFor({
line: m.originalLine,
column: m.originalColumn
})
if (origPosInOldMap.source == null) {
return
}
mergedMapGenerator.addMapping({
generated: {
line: m.generatedLine,
column: m.generatedColumn
},
original: {
line: origPosInOldMap.line, // map line
// use current column, since the oldMap produced by @vue/compiler-sfc
// does not
column: m.originalColumn
},
source: origPosInOldMap.source,
name: origPosInOldMap.name
})
})
// source-map's type definition is incomplete
const generator = mergedMapGenerator as any
;(oldMapConsumer as any).sources.forEach((sourceFile: string) => {
generator._sources.add(sourceFile)
const sourceContent = oldMapConsumer.sourceContentFor(sourceFile)
if (sourceContent != null) {
mergedMapGenerator.setSourceContent(sourceFile, sourceContent)
}
})
generator._sourceRoot = oldMap.sourceRoot
generator._file = oldMap.file
return generator.toJSON()
}
function patchErrors(
errors: CompilerError[],
source: string,
inMap: RawSourceMap
) {
const originalSource = inMap.sourcesContent![0]
const offset = originalSource.indexOf(source)
const lineOffset = originalSource.slice(0, offset).split(/\r?\n/).length - 1
errors.forEach(err => {
if (err.loc) {
err.loc.start.line += lineOffset
err.loc.start.offset += offset
if (err.loc.end !== err.loc.start) {
err.loc.end.line += lineOffset
err.loc.end.offset += offset
}
}
})
}

View File

@@ -14,8 +14,12 @@ export {
} from './parse'
export {
TemplateCompiler,
TemplateCompileOptions,
TemplateCompileResults
SFCTemplateCompileOptions,
SFCTemplateCompileResults
} from './compileTemplate'
export { StyleCompileOptions, StyleCompileResults } from './compileStyle'
export { CompilerOptions, generateCodeFrame } from '@vue/compiler-core'
export { SFCStyleCompileOptions, SFCStyleCompileResults } from './compileStyle'
export {
CompilerOptions,
CompilerError,
generateCodeFrame
} from '@vue/compiler-core'

View File

@@ -1,20 +1,21 @@
import {
parse as baseParse,
TextModes,
NodeTypes,
TextNode,
ElementNode,
SourceLocation
SourceLocation,
CompilerError,
TextModes
} from '@vue/compiler-core'
import { RawSourceMap, SourceMapGenerator } from 'source-map'
import LRUCache from 'lru-cache'
import { generateCodeFrame } from '@vue/shared'
import { TemplateCompiler } from './compileTemplate'
export interface SFCParseOptions {
filename?: string
sourceMap?: boolean
sourceRoot?: string
pad?: boolean | 'line' | 'space'
compiler?: TemplateCompiler
}
export interface SFCBlock {
@@ -50,24 +51,32 @@ export interface SFCDescriptor {
customBlocks: SFCBlock[]
}
export interface SFCParseResult {
descriptor: SFCDescriptor
errors: CompilerError[]
}
const SFC_CACHE_MAX_SIZE = 500
const sourceToSFC = new LRUCache<string, SFCDescriptor>(SFC_CACHE_MAX_SIZE)
const sourceToSFC = new LRUCache<string, SFCParseResult>(SFC_CACHE_MAX_SIZE)
export function parse(
source: string,
{
sourceMap = true,
filename = 'component.vue',
sourceRoot = '',
pad = false
pad = false,
compiler = require('@vue/compiler-dom')
}: SFCParseOptions = {}
): SFCDescriptor {
const sourceKey = source + sourceMap + filename + sourceRoot + pad
): SFCParseResult {
const sourceKey =
source + sourceMap + filename + sourceRoot + pad + compiler.parse
const cache = sourceToSFC.get(sourceKey)
if (cache) {
return cache
}
const sfc: SFCDescriptor = {
const descriptor: SFCDescriptor = {
filename,
template: null,
script: null,
@@ -75,69 +84,84 @@ export function parse(
customBlocks: []
}
const ast = baseParse(source, {
const errors: CompilerError[] = []
const ast = compiler.parse(source, {
// there are no components at SFC parsing level
isNativeTag: () => true,
getTextMode: () => TextModes.RAWTEXT
// preserve all whitespaces
isPreTag: () => true,
getTextMode: (tag, _ns, parent) => {
// all top level elements except <template> are parsed as raw text
// containers
if (!parent && tag !== 'template') {
return TextModes.RAWTEXT
} else {
return TextModes.DATA
}
},
onError: e => {
errors.push(e)
}
})
ast.children.forEach(node => {
if (node.type !== NodeTypes.ELEMENT) {
return
}
if (!node.children.length) {
if (!node.children.length && !hasSrc(node)) {
return
}
switch (node.tag) {
case 'template':
if (!sfc.template) {
sfc.template = createBlock(node, source, pad) as SFCTemplateBlock
if (!descriptor.template) {
descriptor.template = createBlock(
node,
source,
false
) as SFCTemplateBlock
} else {
warnDuplicateBlock(source, filename, node)
}
break
case 'script':
if (!sfc.script) {
sfc.script = createBlock(node, source, pad) as SFCScriptBlock
if (!descriptor.script) {
descriptor.script = createBlock(node, source, pad) as SFCScriptBlock
} else {
warnDuplicateBlock(source, filename, node)
}
break
case 'style':
sfc.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
break
default:
sfc.customBlocks.push(createBlock(node, source, pad))
descriptor.customBlocks.push(createBlock(node, source, pad))
break
}
})
if (sourceMap) {
if (sfc.script && !sfc.script.src) {
sfc.script.map = generateSourceMap(
filename,
source,
sfc.script.content,
sourceRoot,
pad
)
}
if (sfc.styles) {
sfc.styles.forEach(style => {
if (!style.src) {
style.map = generateSourceMap(
filename,
source,
style.content,
sourceRoot,
pad
)
}
})
const genMap = (block: SFCBlock | null) => {
if (block && !block.src) {
block.map = generateSourceMap(
filename,
source,
block.content,
sourceRoot,
!pad || block.type === 'template' ? block.loc.start.line - 1 : 0
)
}
}
genMap(descriptor.template)
genMap(descriptor.script)
descriptor.styles.forEach(genMap)
}
sourceToSFC.set(sourceKey, sfc)
return sfc
const result = {
descriptor,
errors
}
sourceToSFC.set(sourceKey, result)
return result
}
function warnDuplicateBlock(
@@ -164,15 +188,26 @@ function createBlock(
pad: SFCParseOptions['pad']
): SFCBlock {
const type = node.tag
const text = node.children[0] as TextNode
let { start, end } = node.loc
let content = ''
if (node.children.length) {
start = node.children[0].loc.start
end = node.children[node.children.length - 1].loc.end
content = source.slice(start.offset, end.offset)
}
const loc = {
source: content,
start,
end
}
const attrs: Record<string, string | true> = {}
const block: SFCBlock = {
type,
content: text.content,
loc: text.loc,
content,
loc,
attrs
}
if (node.tag !== 'template' && pad) {
if (pad) {
block.content = padContent(source, block, pad) + block.content
}
node.props.forEach(p => {
@@ -205,27 +240,19 @@ function generateSourceMap(
source: string,
generated: string,
sourceRoot: string,
pad?: SFCParseOptions['pad']
lineOffset: number
): RawSourceMap {
const map = new SourceMapGenerator({
file: filename.replace(/\\/g, '/'),
sourceRoot: sourceRoot.replace(/\\/g, '/')
})
let offset = 0
if (!pad) {
offset =
source
.split(generated)
.shift()!
.split(splitRE).length - 1
}
map.setSourceContent(filename, source)
generated.split(splitRE).forEach((line, index) => {
if (!emptyRE.test(line)) {
map.addMapping({
source: filename,
original: {
line: index + 1 + offset,
line: index + 1 + lineOffset,
column: 0
},
generated: {
@@ -252,3 +279,12 @@ function padContent(
return Array(offset).join(padChar)
}
}
function hasSrc(node: ElementNode) {
return node.props.some(p => {
if (p.type !== NodeTypes.ATTRIBUTE) {
return false
}
return p.name === 'src'
})
}

View File

@@ -1,16 +1,16 @@
import postcss, { Root } from 'postcss'
import selectorParser from 'postcss-selector-parser'
import selectorParser, { Node, Selector } from 'postcss-selector-parser'
export default postcss.plugin('add-id', (options: any) => (root: Root) => {
export default postcss.plugin('vue-scoped', (options: any) => (root: Root) => {
const id: string = options
const keyframes = Object.create(null)
root.each(function rewriteSelector(node: any) {
if (!node.selector) {
root.each(function rewriteSelectors(node) {
if (node.type !== 'rule') {
// handle media queries
if (node.type === 'atrule') {
if (node.name === 'media' || node.name === 'supports') {
node.each(rewriteSelector)
node.each(rewriteSelectors)
} else if (/-?keyframes$/.test(node.name)) {
// register keyframes
keyframes[node.params] = node.params = node.params + '-' + id
@@ -18,27 +18,82 @@ export default postcss.plugin('add-id', (options: any) => (root: Root) => {
}
return
}
node.selector = selectorParser((selectors: any) => {
selectors.each((selector: any) => {
let node: any = null
node.selector = selectorParser(selectors => {
function rewriteSelector(selector: Selector, slotted?: boolean) {
let node: Node | null = null
let shouldInject = true
// find the last child node to insert attribute selector
selector.each((n: any) => {
// ">>>" combinator
// and /deep/ alias for >>>, since >>> doesn't work in SASS
selector.each(n => {
// DEPRECATED ">>>" and "/deep/" combinator
if (
n.type === 'combinator' &&
(n.value === '>>>' || n.value === '/deep/')
) {
n.value = ' '
n.spaces.before = n.spaces.after = ''
console.warn(
`[@vue/compiler-sfc] the >>> and /deep/ combinators have ` +
`been deprecated. Use ::v-deep instead.`
)
return false
}
// in newer versions of sass, /deep/ support is also dropped, so add a ::v-deep alias
if (n.type === 'pseudo' && n.value === '::v-deep') {
n.value = n.spaces.before = n.spaces.after = ''
return false
if (n.type === 'pseudo') {
// deep: inject [id] attribute at the node before the ::v-deep
// combinator.
if (n.value === '::v-deep') {
if (n.nodes.length) {
// .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar
// replace the current node with ::v-deep's inner selector
selector.insertAfter(n, n.nodes[0])
// insert a space combinator before if it doesn't already have one
const prev = selector.at(selector.index(n) - 1)
if (!prev || !isSpaceCombinator(prev)) {
selector.insertAfter(
n,
selectorParser.combinator({
value: ' '
})
)
}
selector.removeChild(n)
} else {
// DEPRECATED usage
// .foo ::v-deep .bar -> .foo[xxxxxxx] .bar
console.warn(
`[@vue/compiler-sfc] ::v-deep usage as a combinator has ` +
`been deprecated. Use ::v-deep(<inner-selector>) instead.`
)
const prev = selector.at(selector.index(n) - 1)
if (prev && isSpaceCombinator(prev)) {
selector.removeChild(prev)
}
selector.removeChild(n)
}
return false
}
// slot: use selector inside `::v-slotted` and inject [id + '-s']
// instead.
// ::v-slotted(.foo) -> .foo[xxxxxxx-s]
if (n.value === '::v-slotted') {
rewriteSelector(n.nodes[0] as Selector, true /* slotted */)
selector.insertAfter(n, n.nodes[0])
selector.removeChild(n)
// since slotted attribute already scopes the selector there's no
// need for the non-slot attribute.
shouldInject = false
return false
}
// global: replace with inner selector and do not inject [id].
// ::v-global(.foo) -> .foo
if (n.value === '::v-global') {
selectors.insertAfter(selector, n.nodes[0])
selectors.removeChild(selector)
return false
}
}
if (n.type !== 'pseudo' && n.type !== 'combinator') {
@@ -47,7 +102,7 @@ export default postcss.plugin('add-id', (options: any) => (root: Root) => {
})
if (node) {
node.spaces.after = ''
;(node as Node).spaces.after = ''
} else {
// For deep selectors & standalone pseudo selectors,
// the attribute selectors are prepended rather than appended.
@@ -55,15 +110,22 @@ export default postcss.plugin('add-id', (options: any) => (root: Root) => {
selector.first.spaces.before = ''
}
selector.insertAfter(
node,
selectorParser.attribute({
attribute: id,
value: id,
raws: {}
})
)
})
if (shouldInject) {
const idToAdd = slotted ? id + '-s' : id
selector.insertAfter(
// If node is null it means we need to inject [id] at the start
// insertAfter can handle `null` here
node as any,
selectorParser.attribute({
attribute: idToAdd,
value: idToAdd,
raws: {},
quoteMark: `"`
})
)
}
}
selectors.each(selector => rewriteSelector(selector as Selector))
}).processSync(node.selector)
})
@@ -99,3 +161,7 @@ export default postcss.plugin('add-id', (options: any) => (root: Root) => {
})
}
})
function isSpaceCombinator(node: Node) {
return node.type === 'combinator' && /^\s+$/.test(node.value)
}

View File

@@ -1,5 +1,4 @@
import {
AttributeNode,
createSimpleExpression,
ExpressionNode,
NodeTransform,
@@ -7,7 +6,7 @@ import {
SourceLocation,
TransformContext
} from '@vue/compiler-core'
import { parseUrl } from './templateUtils'
import { isRelativeUrl, parseUrl } from './templateUtils'
export interface AssetURLOptions {
[name: string]: string[]
@@ -42,10 +41,11 @@ export const transformAssetUrl: NodeTransform = (
if ((tag === '*' || node.tag === tag) && node.props.length) {
const attributes = options[tag]
attributes.forEach(item => {
node.props.forEach((attr: AttributeNode, index) => {
node.props.forEach((attr, index) => {
if (attr.type !== NodeTypes.ATTRIBUTE) return
if (attr.name !== item) return
if (!attr.value) return
if (!isRelativeUrl(attr.value.content)) return
const url = parseUrl(attr.value.content)
const exp = getImportsExpressionExp(
url.path,

View File

@@ -5,7 +5,7 @@ import {
NodeTypes,
SimpleExpressionNode
} from '@vue/compiler-core'
import { parseUrl } from './templateUtils'
import { isRelativeUrl, parseUrl } from './templateUtils'
const srcsetTags = ['img', 'source']
@@ -36,31 +36,44 @@ export const transformSrcset: NodeTransform = (node, context) => {
return { url, descriptor }
})
// When srcset does not contain any relative URLs, skip transforming
if (!imageCandidates.some(({ url }) => isRelativeUrl(url))) return
const compoundExpression = createCompoundExpression([], attr.loc)
imageCandidates.forEach(({ url, descriptor }, index) => {
const { path } = parseUrl(url)
let exp: SimpleExpressionNode
if (path) {
const importsArray = Array.from(context.imports)
const existingImportsIndex = importsArray.findIndex(
i => i.path === path
)
if (existingImportsIndex > -1) {
exp = createSimpleExpression(
`_imports_${existingImportsIndex}`,
false,
attr.loc,
true
if (isRelativeUrl(url)) {
const { path } = parseUrl(url)
let exp: SimpleExpressionNode
if (path) {
const importsArray = Array.from(context.imports)
const existingImportsIndex = importsArray.findIndex(
i => i.path === path
)
} else {
exp = createSimpleExpression(
`_imports_${importsArray.length}`,
false,
attr.loc,
true
)
context.imports.add({ exp, path })
if (existingImportsIndex > -1) {
exp = createSimpleExpression(
`_imports_${existingImportsIndex}`,
false,
attr.loc,
true
)
} else {
exp = createSimpleExpression(
`_imports_${importsArray.length}`,
false,
attr.loc,
true
)
context.imports.add({ exp, path })
}
compoundExpression.children.push(exp)
}
} else {
const exp = createSimpleExpression(
`"${url}"`,
false,
attr.loc,
true
)
compoundExpression.children.push(exp)
}
const isNotLast = imageCandidates.length - 1 > index

View File

@@ -1,15 +1,18 @@
import { UrlWithStringQuery, parse as uriParse } from 'url'
import { isString } from '@vue/shared'
export function isRelativeUrl(url: string): boolean {
const firstChar = url.charAt(0)
return firstChar === '.' || firstChar === '~' || firstChar === '@'
}
// We need an extra transform context API for injecting arbitrary import
// statements.
export function parseUrl(url: string): UrlWithStringQuery {
const firstChar = url.charAt(0)
if (firstChar === '.' || firstChar === '~' || firstChar === '@') {
if (firstChar === '~') {
const secondChar = url.charAt(1)
url = url.slice(secondChar === '/' ? 2 : 1)
}
if (firstChar === '~') {
const secondChar = url.charAt(1)
url = url.slice(secondChar === '/' ? 2 : 1)
}
return parseUriParts(url)
}

View File

@@ -0,0 +1 @@
# @vue/compiler-ssr

View File

@@ -0,0 +1,299 @@
import { compile } from '../src'
describe('ssr: components', () => {
test('basic', () => {
expect(compile(`<foo id="a" :prop="b" />`).code).toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, {
id: \\"a\\",
prop: _ctx.b
}, null, _parent))
}"
`)
})
test('dynamic component', () => {
expect(compile(`<component is="foo" prop="b" />`).code)
.toMatchInlineSnapshot(`
"const { resolveDynamicComponent: _resolveDynamicComponent, withCtx: _withCtx } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_push(_ssrRenderComponent(_resolveDynamicComponent(\\"foo\\"), { prop: \\"b\\" }, null, _parent))
}"
`)
expect(compile(`<component :is="foo" prop="b" />`).code)
.toMatchInlineSnapshot(`
"const { resolveDynamicComponent: _resolveDynamicComponent, withCtx: _withCtx } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_push(_ssrRenderComponent(_resolveDynamicComponent(_ctx.foo), { prop: \\"b\\" }, null, _parent))
}"
`)
})
describe('slots', () => {
test('implicit default slot', () => {
expect(compile(`<foo>hello<div/></foo>`).code).toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, createTextVNode: _createTextVNode } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, null, {
default: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(\`hello<div\${_scopeId}></div>\`)
} else {
return [
_createTextVNode(\\"hello\\"),
_createVNode(\\"div\\")
]
}
}),
_: 1
}, _parent))
}"
`)
})
test('explicit default slot', () => {
expect(compile(`<foo v-slot="{ msg }">{{ msg + outer }}</foo>`).code)
.toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, null, {
default: _withCtx(({ msg }, _push, _parent, _scopeId) => {
if (_push) {
_push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`)
} else {
return [
_createTextVNode(_toDisplayString(msg + _ctx.outer), 1 /* TEXT */)
]
}
}),
_: 1
}, _parent))
}"
`)
})
test('named slots', () => {
expect(
compile(`<foo>
<template v-slot>foo</template>
<template v-slot:named>bar</template>
</foo>`).code
).toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, createTextVNode: _createTextVNode } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, null, {
default: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(\`foo\`)
} else {
return [
_createTextVNode(\\"foo\\")
]
}
}),
named: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(\`bar\`)
} else {
return [
_createTextVNode(\\"bar\\")
]
}
}),
_: 1
}, _parent))
}"
`)
})
test('v-if slot', () => {
expect(
compile(`<foo>
<template v-slot:named v-if="ok">foo</template>
</foo>`).code
).toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, createTextVNode: _createTextVNode, createSlots: _createSlots } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, null, _createSlots({ _: 1 }, [
(_ctx.ok)
? {
name: \\"named\\",
fn: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(\`foo\`)
} else {
return [
_createTextVNode(\\"foo\\")
]
}
})
}
: undefined
]), _parent))
}"
`)
})
test('v-for slot', () => {
expect(
compile(`<foo>
<template v-for="key in names" v-slot:[key]="{ msg }">{{ msg + key + bar }}</template>
</foo>`).code
).toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, renderList: _renderList, createSlots: _createSlots } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, null, _createSlots({ _: 1 }, [
_renderList(_ctx.names, (key) => {
return {
name: key,
fn: _withCtx(({ msg }, _push, _parent, _scopeId) => {
if (_push) {
_push(\`\${_ssrInterpolate(msg + key + _ctx.bar)}\`)
} else {
return [
_createTextVNode(_toDisplayString(msg + _ctx.key + _ctx.bar), 1 /* TEXT */)
]
}
})
}
})
]), _parent))
}"
`)
})
test('nested transform scoping in vnode branch', () => {
expect(
compile(`<foo>
<template v-slot:foo="{ list }">
<div v-if="ok">
<span v-for="i in list"></span>
</div>
</template>
<template v-slot:bar="{ ok }">
<div v-if="ok">
<span v-for="i in list"></span>
</div>
</template>
</foo>`).code
).toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode, createCommentVNode: _createCommentVNode } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, null, {
foo: _withCtx(({ list }, _push, _parent, _scopeId) => {
if (_push) {
if (_ctx.ok) {
_push(\`<div\${_scopeId}><!--[-->\`)
_ssrRenderList(list, (i) => {
_push(\`<span\${_scopeId}></span>\`)
})
_push(\`<!--]--></div>\`)
} else {
_push(\`<!---->\`)
}
} else {
return [
(_ctx.ok)
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }, [
(_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createBlock(\\"span\\"))
}), 256 /* UNKEYED_FRAGMENT */))
]))
: _createCommentVNode(\\"v-if\\", true)
]
}
}),
bar: _withCtx(({ ok }, _push, _parent, _scopeId) => {
if (_push) {
if (ok) {
_push(\`<div\${_scopeId}><!--[-->\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`<span\${_scopeId}></span>\`)
})
_push(\`<!--]--></div>\`)
} else {
_push(\`<!---->\`)
}
} else {
return [
ok
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }, [
(_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.list, (i) => {
return (_openBlock(), _createBlock(\\"span\\"))
}), 256 /* UNKEYED_FRAGMENT */))
]))
: _createCommentVNode(\\"v-if\\", true)
]
}
}),
_: 1
}, _parent))
}"
`)
})
test('built-in fallthroughs', () => {
expect(compile(`<transition><div/></transition>`).code)
.toMatchInlineSnapshot(`
"
return function ssrRender(_ctx, _push, _parent) {
_push(\`<div></div>\`)
}"
`)
expect(compile(`<transition-group><div/></transition-group>`).code)
.toMatchInlineSnapshot(`
"
return function ssrRender(_ctx, _push, _parent) {
_push(\`<!--[--><div></div><!--]-->\`)
}"
`)
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
.toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, null, null, _parent))
}"
`)
})
})
})

View File

@@ -0,0 +1,218 @@
import { getCompiledString } from './utils'
import { compile } from '../src'
describe('ssr: element', () => {
test('basic elements', () => {
expect(getCompiledString(`<div></div>`)).toMatchInlineSnapshot(
`"\`<div></div>\`"`
)
expect(getCompiledString(`<div/>`)).toMatchInlineSnapshot(
`"\`<div></div>\`"`
)
})
test('nested elements', () => {
expect(
getCompiledString(`<div><span></span><span></span></div>`)
).toMatchInlineSnapshot(`"\`<div><span></span><span></span></div>\`"`)
})
test('void element', () => {
expect(getCompiledString(`<input>`)).toMatchInlineSnapshot(`"\`<input>\`"`)
})
describe('children override', () => {
test('v-html', () => {
expect(getCompiledString(`<div v-html="foo"/>`)).toMatchInlineSnapshot(
`"\`<div>\${_ctx.foo}</div>\`"`
)
})
test('v-text', () => {
expect(getCompiledString(`<div v-text="foo"/>`)).toMatchInlineSnapshot(
`"\`<div>\${_ssrInterpolate(_ctx.foo)}</div>\`"`
)
})
test('<textarea> with dynamic value', () => {
expect(
getCompiledString(`<textarea :value="foo"/>`)
).toMatchInlineSnapshot(
`"\`<textarea>\${_ssrInterpolate(_ctx.foo)}</textarea>\`"`
)
})
test('<textarea> with static value', () => {
expect(
getCompiledString(`<textarea value="fo&gt;o"/>`)
).toMatchInlineSnapshot(`"\`<textarea>fo&gt;o</textarea>\`"`)
})
test('<textarea> with dynamic v-bind', () => {
expect(compile(`<textarea v-bind="obj">fallback</textarea>`).code)
.toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
let _temp0
_push(\`<textarea\${
_ssrRenderAttrs(_temp0 = _ctx.obj, \\"textarea\\")
}>\${
_ssrInterpolate((\\"value\\" in _temp0) ? _temp0.value : \\"fallback\\")
}</textarea>\`)
}"
`)
})
test('should pass tag to custom elements w/ dynamic v-bind', () => {
expect(
compile(`<my-foo v-bind="obj"></my-foo>`, {
isCustomElement: () => true
}).code
).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_push(\`<my-foo\${_ssrRenderAttrs(_ctx.obj, \\"my-foo\\")}></my-foo>\`)
}"
`)
})
})
describe('attrs', () => {
test('static attrs', () => {
expect(
getCompiledString(`<div id="foo" class="bar"></div>`)
).toMatchInlineSnapshot(`"\`<div id=\\"foo\\" class=\\"bar\\"></div>\`"`)
})
test('v-bind:class', () => {
expect(
getCompiledString(`<div id="foo" :class="bar"></div>`)
).toMatchInlineSnapshot(
`"\`<div id=\\"foo\\" class=\\"\${_ssrRenderClass(_ctx.bar)}\\"></div>\`"`
)
})
test('static class + v-bind:class', () => {
expect(
getCompiledString(`<div class="foo" :class="bar"></div>`)
).toMatchInlineSnapshot(
`"\`<div class=\\"\${_ssrRenderClass([_ctx.bar, \\"foo\\"])}\\"></div>\`"`
)
})
test('v-bind:style', () => {
expect(
getCompiledString(`<div id="foo" :style="bar"></div>`)
).toMatchInlineSnapshot(
`"\`<div id=\\"foo\\" style=\\"\${_ssrRenderStyle(_ctx.bar)}\\"></div>\`"`
)
})
test('static style + v-bind:style', () => {
expect(
getCompiledString(`<div style="color:red;" :style="bar"></div>`)
).toMatchInlineSnapshot(
`"\`<div style=\\"\${_ssrRenderStyle([{\\"color\\":\\"red\\"}, _ctx.bar])}\\"></div>\`"`
)
})
test('v-bind:key (boolean)', () => {
expect(
getCompiledString(`<input type="checkbox" :checked="checked">`)
).toMatchInlineSnapshot(
`"\`<input type=\\"checkbox\\"\${(_ctx.checked) ? \\" checked\\" : \\"\\"}>\`"`
)
})
test('v-bind:key (non-boolean)', () => {
expect(
getCompiledString(`<div :id="id" class="bar"></div>`)
).toMatchInlineSnapshot(
`"\`<div\${_ssrRenderAttr(\\"id\\", _ctx.id)} class=\\"bar\\"></div>\`"`
)
})
test('v-bind:[key]', () => {
expect(
getCompiledString(`<div v-bind:[key]="value"></div>`)
).toMatchInlineSnapshot(
`"\`<div\${_ssrRenderAttrs({ [_ctx.key]: _ctx.value })}></div>\`"`
)
expect(getCompiledString(`<div class="foo" v-bind:[key]="value"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${_ssrRenderAttrs({
class: \\"foo\\",
[_ctx.key]: _ctx.value
})}></div>\`"
`)
expect(getCompiledString(`<div :id="id" v-bind:[key]="value"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${_ssrRenderAttrs({
id: _ctx.id,
[_ctx.key]: _ctx.value
})}></div>\`"
`)
})
test('v-bind="obj"', () => {
expect(
getCompiledString(`<div v-bind="obj"></div>`)
).toMatchInlineSnapshot(`"\`<div\${_ssrRenderAttrs(_ctx.obj)}></div>\`"`)
expect(
getCompiledString(`<div class="foo" v-bind="obj"></div>`)
).toMatchInlineSnapshot(
`"\`<div\${_ssrRenderAttrs(_mergeProps({ class: \\"foo\\" }, _ctx.obj))}></div>\`"`
)
expect(
getCompiledString(`<div :id="id" v-bind="obj"></div>`)
).toMatchInlineSnapshot(
`"\`<div\${_ssrRenderAttrs(_mergeProps({ id: _ctx.id }, _ctx.obj))}></div>\`"`
)
// dynamic key + v-bind="object"
expect(
getCompiledString(`<div :[key]="id" v-bind="obj"></div>`)
).toMatchInlineSnapshot(
`"\`<div\${_ssrRenderAttrs(_mergeProps({ [_ctx.key]: _ctx.id }, _ctx.obj))}></div>\`"`
)
// should merge class and :class
expect(getCompiledString(`<div class="a" :class="b" v-bind="obj"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${_ssrRenderAttrs(_mergeProps({
class: [\\"a\\", _ctx.b]
}, _ctx.obj))}></div>\`"
`)
// should merge style and :style
expect(
getCompiledString(
`<div style="color:red;" :style="b" v-bind="obj"></div>`
)
).toMatchInlineSnapshot(`
"\`<div\${_ssrRenderAttrs(_mergeProps({
style: [{\\"color\\":\\"red\\"}, _ctx.b]
}, _ctx.obj))}></div>\`"
`)
})
test('should ignore v-on', () => {
expect(
getCompiledString(`<div id="foo" @click="bar"/>`)
).toMatchInlineSnapshot(`"\`<div id=\\"foo\\"></div>\`"`)
expect(
getCompiledString(`<div id="foo" v-on="bar"/>`)
).toMatchInlineSnapshot(`"\`<div id=\\"foo\\"></div>\`"`)
expect(
getCompiledString(`<div v-bind="foo" v-on="bar"/>`)
).toMatchInlineSnapshot(`"\`<div\${_ssrRenderAttrs(_ctx.foo)}></div>\`"`)
})
})
})

View File

@@ -0,0 +1,43 @@
import { compile } from '../src'
describe('ssr compile: teleport', () => {
test('should work', () => {
expect(compile(`<teleport :target="target"><div/></teleport>`).code)
.toMatchInlineSnapshot(`
"const { ssrRenderTeleport: _ssrRenderTeleport } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_ssrRenderTeleport(_push, (_push) => {
_push(\`<div></div>\`)
}, _ctx.target, false, _parent)
}"
`)
})
test('disabled prop handling', () => {
expect(
compile(`<teleport :target="target" disabled><div/></teleport>`).code
).toMatchInlineSnapshot(`
"const { ssrRenderTeleport: _ssrRenderTeleport } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_ssrRenderTeleport(_push, (_push) => {
_push(\`<div></div>\`)
}, _ctx.target, true, _parent)
}"
`)
expect(
compile(`<teleport :target="target" :disabled="foo"><div/></teleport>`)
.code
).toMatchInlineSnapshot(`
"const { ssrRenderTeleport: _ssrRenderTeleport } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_ssrRenderTeleport(_push, (_push) => {
_push(\`<div></div>\`)
}, _ctx.target, _ctx.foo, _parent)
}"
`)
})
})

Some files were not shown because too many files have changed in this diff Show More