wip: tests for defineContext()

This commit is contained in:
Evan You 2020-11-12 18:11:25 -05:00
parent 128621d6a0
commit 0ca9137188
5 changed files with 592 additions and 350 deletions

View File

@ -272,8 +272,10 @@ export function resolveComponentType(
} }
const tagFromConst = checkType(BindingTypes.CONST) const tagFromConst = checkType(BindingTypes.CONST)
if (tagFromConst) { if (tagFromConst) {
// constant setup bindings (e.g. imports) can be used as-is return context.inline
return tagFromConst ? // in inline mode, const setup bindings (e.g. imports) can be used as-is
tagFromConst
: `$setup[${JSON.stringify(tagFromConst)}]`
} }
} }

View File

@ -102,19 +102,18 @@ export function processExpression(
const { inline, bindingMetadata } = context const { inline, bindingMetadata } = context
// const bindings exposed from setup - we know they never change // const bindings exposed from setup - we know they never change
if (inline && bindingMetadata[node.content] === BindingTypes.CONST) { if (bindingMetadata[node.content] === BindingTypes.CONST) {
node.isRuntimeConstant = true node.isRuntimeConstant = true
return node return node
} }
const prefix = (raw: string) => { const prefix = (raw: string) => {
const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw] const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
if (type === BindingTypes.CONST) {
return raw
}
if (inline) { if (inline) {
// setup inline mode // setup inline mode
if (type === BindingTypes.SETUP) { if (type === BindingTypes.CONST) {
return raw
} else if (type === BindingTypes.SETUP) {
return `${context.helperString(UNREF)}(${raw})` return `${context.helperString(UNREF)}(${raw})`
} else if (type === BindingTypes.PROPS) { } else if (type === BindingTypes.PROPS) {
// use __props which is generated by compileScript so in ts mode // use __props which is generated by compileScript so in ts mode
@ -122,8 +121,16 @@ export function processExpression(
return `__props.${raw}` return `__props.${raw}`
} }
} }
// fallback to normal
return `${type ? `$${type}` : `_ctx`}.${raw}` if (type === BindingTypes.CONST) {
// setup const binding in non-inline mode
return `$setup.${raw}`
} else if (type) {
return `$${type}.${raw}`
} else {
// fallback to ctx
return `_ctx.${raw}`
}
} }
// fast path if expression is a simple identifier. // fast path if expression is a simple identifier.

View File

@ -1,106 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SFC compile <script setup> <script setup lang="ts"> extract emits 1`] = `
"import { Slots, defineComponent } from 'vue'
declare function __emit__(e: 'foo' | 'bar'): void
declare function __emit__(e: 'baz', id: number): void
export function setup(_: {}, { emit: myEmit }: {
emit: typeof __emit__,
slots: Slots,
attrs: Record<string, any>
}) {
return { }
}
export default defineComponent({
emits: [\\"foo\\", \\"bar\\", \\"baz\\"] as unknown as undefined,
setup
})"
`;
exports[`SFC compile <script setup> <script setup lang="ts"> extract props 1`] = `
"import { Slots, defineComponent } from 'vue'
interface Test {}
type Alias = number[]
export function setup(myProps: {
string: string
number: number
boolean: boolean
object: object
objectLiteral: { a: number }
fn: (n: number) => void
functionRef: Function
objectRef: Object
array: string[]
arrayRef: Array<any>
tuple: [number, number]
set: Set<string>
literal: 'foo'
optional?: any
recordRef: Record<string, null>
interface: Test
alias: Alias
union: string | number
literalUnion: 'foo' | 'bar'
literalUnionMixed: 'foo' | 1 | boolean
intersection: Test & {}
}) {
return { }
}
export default defineComponent({
props: {
string: { type: String, required: true },
number: { type: Number, required: true },
boolean: { type: Boolean, required: true },
object: { type: Object, required: true },
objectLiteral: { type: Object, required: true },
fn: { type: Function, required: true },
functionRef: { type: Function, required: true },
objectRef: { type: Object, required: true },
array: { type: Array, required: true },
arrayRef: { type: Array, required: true },
tuple: { type: Array, required: true },
set: { type: Set, required: true },
literal: { type: String, required: true },
optional: { type: null, required: false },
recordRef: { type: Object, required: true },
interface: { type: Object, required: true },
alias: { type: Array, required: true },
union: { type: [String, Number], required: true },
literalUnion: { type: [String, String], required: true },
literalUnionMixed: { type: [String, Number, Boolean], required: true },
intersection: { type: Object, required: true }
} as unknown as undefined,
setup
})"
`;
exports[`SFC compile <script setup> <script setup lang="ts"> hoist type declarations 1`] = `
"import { Slots, defineComponent } from 'vue'
export interface Foo {}
type Bar = {}
export function setup() {
return { }
}
export default defineComponent({
setup
})"
`;
exports[`SFC compile <script setup> CSS vars injection <script> w/ default export 1`] = ` exports[`SFC compile <script setup> CSS vars injection <script> w/ default export 1`] = `
"const __default__ = { setup() {} } "const __default__ = { setup() {} }
import { useCssVars as __useCssVars__ } from 'vue' import { useCssVars as __useCssVars__ } from 'vue'
@ -147,116 +46,177 @@ export default __default__"
exports[`SFC compile <script setup> CSS vars injection w/ <script setup> 1`] = ` exports[`SFC compile <script setup> CSS vars injection w/ <script setup> 1`] = `
"import { useCssVars } from 'vue' "import { useCssVars } from 'vue'
export function setup() { export default {
setup() {
const color = 'red' const color = 'red'
__useCssVars__(_ctx => ({ color })) __useCssVars__(_ctx => ({ color }))
return { color } return { color }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> errors should allow export default referencing imported binding 1`] = ` exports[`SFC compile <script setup> defineContext() 1`] = `
"import { bar } from './bar' "export default {
props: {
export function setup() { foo: String
},
emit: ['a', 'b'],
setup(__props, { props, emit }) {
return { bar }
const bar = 1
return { props, emit, bar }
} }
const __default__ = { }"
props: { `;
exports[`SFC compile <script setup> errors should allow defineContext() referencing imported binding 1`] = `
"import { bar } from './bar'
export default {
props: {
foo: { foo: {
default: () => bar default: () => bar
} }
} },
} setup() {
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> errors should allow export default referencing scope var 1`] = `
"export function setup() {
const bar = 1
return { bar } return { bar }
} }
const __default__ = { }"
props: { `;
exports[`SFC compile <script setup> errors should allow defineContext() referencing scope var 1`] = `
"export default {
props: {
foo: { foo: {
default: bar => bar + 1 default: bar => bar + 1
} }
} },
} setup() {
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> explicit setup signature 1`] = ` const bar = 1
"export function setup(props, { emit }) {
emit('foo')
return { } return { bar }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> imports dedupe between user & helper 1`] = ` exports[`SFC compile <script setup> imports dedupe between user & helper 1`] = `
"import { ref } from 'vue' "import { ref } from 'vue'
export function setup() { export default {
setup() {
const foo = ref(1) const foo = ref(1)
return { ref, foo } return { foo, ref }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> imports import dedupe between <script> and <script setup> 1`] = ` exports[`SFC compile <script setup> imports import dedupe between <script> and <script setup> 1`] = `
"import { x } from './x' "import { x } from './x'
export function setup() { export default {
setup() {
x() x()
return { x } return { x }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> imports should extract comment for import or type declarations 1`] = ` exports[`SFC compile <script setup> imports should extract comment for import or type declarations 1`] = `
"import a from 'a' // comment "import a from 'a' // comment
import b from 'b' import b from 'b'
export function setup() { export default {
setup() {
return { a, b } return { a, b }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> imports should hoist and expose imports 1`] = ` exports[`SFC compile <script setup> imports should hoist and expose imports 1`] = `
"import { ref } from 'vue' "import { ref } from 'vue'
export function setup() { export default {
setup() {
return { ref } return { ref }
} }
export default { setup }" }"
`;
exports[`SFC compile <script setup> inlineTemplate mode avoid unref() when necessary 1`] = `
"import { createVNode as _createVNode, unref as _unref, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
import { ref } from 'vue'
import Foo from './Foo.vue'
import other from './util'
export default {
setup() {
const count = ref(0)
const constant = {}
function fn() {}
return (_ctx, _cache, $props, $setup, $data, $options) => {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(Foo),
_createVNode(\\"div\\", { onClick: fn }, _toDisplayString(_unref(count)) + \\" \\" + _toDisplayString(constant) + \\" \\" + _toDisplayString(_unref(other)), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
}
}"
`;
exports[`SFC compile <script setup> inlineTemplate mode should work 1`] = `
"import { unref as _unref, toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
const _hoisted_1 = /*#__PURE__*/_createVNode(\\"div\\", null, \\"static\\", -1 /* HOISTED */)
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return (_ctx, _cache, $props, $setup, $data, $options) => {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\", null, _toDisplayString(_unref(count)), 1 /* TEXT */),
_hoisted_1
], 64 /* STABLE_FRAGMENT */))
}
}
}"
`; `;
exports[`SFC compile <script setup> ref: syntax sugar accessing ref binding 1`] = ` exports[`SFC compile <script setup> ref: syntax sugar accessing ref binding 1`] = `
"import { ref } from 'vue' "import { ref } from 'vue'
export function setup() { export default {
setup() {
const a = ref(1) const a = ref(1)
console.log(a.value) console.log(a.value)
@ -267,13 +227,14 @@ export function setup() {
return { a, get } return { a, get }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> ref: syntax sugar array destructure 1`] = ` exports[`SFC compile <script setup> ref: syntax sugar array destructure 1`] = `
"import { ref } from 'vue' "import { ref } from 'vue'
export function setup() { export default {
setup() {
const n = ref(1), [__a, __b = 1, ...__c] = useFoo() const n = ref(1), [__a, __b = 1, ...__c] = useFoo()
const a = ref(__a); const a = ref(__a);
@ -284,13 +245,14 @@ const c = ref(__c);
return { n, a, b, c } return { n, a, b, c }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> ref: syntax sugar convert ref declarations 1`] = ` exports[`SFC compile <script setup> ref: syntax sugar convert ref declarations 1`] = `
"import { ref } from 'vue' "import { ref } from 'vue'
export function setup() { export default {
setup() {
const foo = ref() const foo = ref()
const a = ref(1) const a = ref(1)
@ -303,13 +265,14 @@ export function setup() {
return { foo, a, b, c, d } return { foo, a, b, c, d }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> ref: syntax sugar multi ref declarations 1`] = ` exports[`SFC compile <script setup> ref: syntax sugar multi ref declarations 1`] = `
"import { ref } from 'vue' "import { ref } from 'vue'
export function setup() { export default {
setup() {
const a = ref(1), b = ref(2), c = ref({ const a = ref(1), b = ref(2), c = ref({
count: 0 count: 0
@ -318,13 +281,14 @@ export function setup() {
return { a, b, c } return { a, b, c }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> ref: syntax sugar mutating ref binding 1`] = ` exports[`SFC compile <script setup> ref: syntax sugar mutating ref binding 1`] = `
"import { ref } from 'vue' "import { ref } from 'vue'
export function setup() { export default {
setup() {
const a = ref(1) const a = ref(1)
const b = ref({ count: 0 }) const b = ref({ count: 0 })
@ -338,13 +302,14 @@ export function setup() {
return { a, b, inc } return { a, b, inc }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> ref: syntax sugar nested destructure 1`] = ` exports[`SFC compile <script setup> ref: syntax sugar nested destructure 1`] = `
"import { ref } from 'vue' "import { ref } from 'vue'
export function setup() { export default {
setup() {
const [{ a: { b: __b }}] = useFoo() const [{ a: { b: __b }}] = useFoo()
const b = ref(__b); const b = ref(__b);
@ -356,13 +321,14 @@ const e = ref(__e);
return { b, d, e } return { b, d, e }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> ref: syntax sugar object destructure 1`] = ` exports[`SFC compile <script setup> ref: syntax sugar object destructure 1`] = `
"import { ref } from 'vue' "import { ref } from 'vue'
export function setup() { export default {
setup() {
const n = ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = useFoo() const n = ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = useFoo()
const a = ref(__a); const a = ref(__a);
@ -375,11 +341,12 @@ const g = ref(__g);
return { n, a, c, d, f, g } return { n, a, c, d, f, g }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> ref: syntax sugar should not convert non ref labels 1`] = ` exports[`SFC compile <script setup> ref: syntax sugar should not convert non ref labels 1`] = `
"export function setup() { "export default {
setup() {
foo: a = 1, b = 2, c = { foo: a = 1, b = 2, c = {
count: 0 count: 0
@ -388,13 +355,14 @@ exports[`SFC compile <script setup> ref: syntax sugar should not convert non ref
return { } return { }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> ref: syntax sugar using ref binding in property shorthand 1`] = ` exports[`SFC compile <script setup> ref: syntax sugar using ref binding in property shorthand 1`] = `
"import { ref } from 'vue' "import { ref } from 'vue'
export function setup() { export default {
setup() {
const a = ref(1) const a = ref(1)
const b = { a: a.value } const b = { a: a.value }
@ -405,21 +373,138 @@ export function setup() {
return { a, b, test } return { a, b, test }
} }
export default { setup }" }"
`; `;
exports[`SFC compile <script setup> should expose top level declarations 1`] = ` exports[`SFC compile <script setup> should expose top level declarations 1`] = `
"import { x } from './x' "import { x } from './x'
export function setup() { export default {
setup() {
let a = 1 let a = 1
const b = 2 const b = 2
function c() {} function c() {}
class d {} class d {}
return { x, a, b, c, d } return { a, b, c, d, x }
} }
export default { setup }" }"
`;
exports[`SFC compile <script setup> with TypeScript defineContext w/ runtime options 1`] = `
"import { defineComponent } from 'vue'
export default defineComponent({
props: { foo: String },
emits: ['a', 'b'],
setup(__props, { props, emit }) {
return { props, emit }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript defineContext w/ type / extract emits (union) 1`] = `
"import { Slots, defineComponent } from 'vue'
export default defineComponent({
emits: [\\"foo\\", \\"bar\\", \\"baz\\"] as unknown as undefined,
setup(__props, { emit }: {
props: {},
emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),
slots: Slots,
attrs: Record<string, any>
}) {
return { emit }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript defineContext w/ type / extract emits 1`] = `
"import { Slots, defineComponent } from 'vue'
export default defineComponent({
emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
setup(__props, { emit }: {
props: {},
emit: (e: 'foo' | 'bar') => void,
slots: Slots,
attrs: Record<string, any>
}) {
return { emit }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript defineContext w/ type / extract props 1`] = `
"import { defineComponent } from 'vue'
interface Test {}
type Alias = number[]
export default defineComponent({
props: {
string: { type: String, required: true },
number: { type: Number, required: true },
boolean: { type: Boolean, required: true },
object: { type: Object, required: true },
objectLiteral: { type: Object, required: true },
fn: { type: Function, required: true },
functionRef: { type: Function, required: true },
objectRef: { type: Object, required: true },
array: { type: Array, required: true },
arrayRef: { type: Array, required: true },
tuple: { type: Array, required: true },
set: { type: Set, required: true },
literal: { type: String, required: true },
optional: { type: null, required: false },
recordRef: { type: Object, required: true },
interface: { type: Object, required: true },
alias: { type: Array, required: true },
union: { type: [String, Number], required: true },
literalUnion: { type: [String, String], required: true },
literalUnionMixed: { type: [String, Number, Boolean], required: true },
intersection: { type: Object, required: true }
} as unknown as undefined,
setup() {
return { }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript hoist type declarations 1`] = `
"import { defineComponent } from 'vue'
export interface Foo {}
type Bar = {}
export default defineComponent({
setup() {
return { }
}
})"
`; `;

View File

@ -22,12 +22,6 @@ function assertCode(code: string) {
} }
describe('SFC compile <script setup>', () => { describe('SFC compile <script setup>', () => {
test('explicit setup signature', () => {
assertCode(
compile(`<script setup="props, { emit }">emit('foo')</script>`).content
)
})
test('should expose top level declarations', () => { test('should expose top level declarations', () => {
const { content } = compile(` const { content } = compile(`
<script setup> <script setup>
@ -39,7 +33,43 @@ describe('SFC compile <script setup>', () => {
</script> </script>
`) `)
assertCode(content) assertCode(content)
expect(content).toMatch('return { x, a, b, c, d }') expect(content).toMatch('return { a, b, c, d, x }')
})
test('defineContext()', () => {
const { content, bindings } = compile(`
<script setup>
import { defineContext } from 'vue'
const { props, emit } = defineContext({
props: {
foo: String
},
emit: ['a', 'b']
})
const bar = 1
</script>
`)
// should generate working code
assertCode(content)
// should anayze bindings
expect(bindings).toStrictEqual({
foo: 'props',
bar: 'const',
props: 'const',
emit: 'const'
})
// should remove defineContext import and call
expect(content).not.toMatch('defineContext')
// should generate correct setup signature
expect(content).toMatch(`setup(__props, { props, emit }) {`)
// should include context options in default export
expect(content).toMatch(`export default {
props: {
foo: String
},
emit: ['a', 'b'],`)
}) })
describe('imports', () => { describe('imports', () => {
@ -51,18 +81,22 @@ describe('SFC compile <script setup>', () => {
test('should extract comment for import or type declarations', () => { test('should extract comment for import or type declarations', () => {
assertCode( assertCode(
compile(`<script setup> compile(`
import a from 'a' // comment <script setup>
import b from 'b' import a from 'a' // comment
</script>`).content import b from 'b'
</script>
`).content
) )
}) })
test('dedupe between user & helper', () => { test('dedupe between user & helper', () => {
const { content } = compile(`<script setup> const { content } = compile(`
import { ref } from 'vue' <script setup>
ref: foo = 1 import { ref } from 'vue'
</script>`) ref: foo = 1
</script>
`)
assertCode(content) assertCode(content)
expect(content).toMatch(`import { ref } from 'vue'`) expect(content).toMatch(`import { ref } from 'vue'`)
}) })
@ -84,7 +118,62 @@ describe('SFC compile <script setup>', () => {
}) })
}) })
describe('<script setup lang="ts">', () => { describe('inlineTemplate mode', () => {
test('should work', () => {
const { content } = compile(
`
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div>{{ count }}</div>
<div>static</div>
</template>
`,
{ inlineTemplate: true }
)
// check snapshot and make sure helper imports and
// hoists are placed correctly.
assertCode(content)
})
test('avoid unref() when necessary', () => {
// function, const, component import
const { content } = compile(
`
<script setup>
import { ref, defineContext } from 'vue'
import Foo from './Foo.vue'
import other from './util'
const count = ref(0)
const constant = {}
function fn() {}
</script>
<template>
<Foo/>
<div @click="fn">{{ count }} {{ constant }} {{ other }}</div>
</template>
`,
{ inlineTemplate: true }
)
assertCode(content)
// no need to unref vue component import
expect(content).toMatch(`createVNode(Foo)`)
// should unref other imports
expect(content).toMatch(`unref(other)`)
// no need to unref constant literals
expect(content).not.toMatch(`unref(constant)`)
// should unref const w/ call init (e.g. ref())
expect(content).toMatch(`unref(count)`)
// no need to unref function declarations
expect(content).toMatch(`{ onClick: fn }`)
// no need to mark constant fns in patch flag
expect(content).not.toMatch(`PROPS`)
})
})
describe('with TypeScript', () => {
test('hoist type declarations', () => { test('hoist type declarations', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
@ -94,37 +183,57 @@ describe('SFC compile <script setup>', () => {
assertCode(content) assertCode(content)
}) })
test('extract props', () => { test('defineContext w/ runtime options', () => {
const { content } = compile(` const { content } = compile(`
<script setup="myProps" lang="ts"> <script setup lang="ts">
import { defineContext } from 'vue'
const { props, emit } = defineContext({
props: { foo: String },
emits: ['a', 'b']
})
</script>
`)
assertCode(content)
expect(content).toMatch(`export default defineComponent({
props: { foo: String },
emits: ['a', 'b'],
setup(__props, { props, emit }) {`)
})
test('defineContext w/ type / extract props', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
import { defineContext } from 'vue'
interface Test {} interface Test {}
type Alias = number[] type Alias = number[]
declare const myProps: { defineContext<{
string: string props: {
number: number string: string
boolean: boolean number: number
object: object boolean: boolean
objectLiteral: { a: number } object: object
fn: (n: number) => void objectLiteral: { a: number }
functionRef: Function fn: (n: number) => void
objectRef: Object functionRef: Function
array: string[] objectRef: Object
arrayRef: Array<any> array: string[]
tuple: [number, number] arrayRef: Array<any>
set: Set<string> tuple: [number, number]
literal: 'foo' set: Set<string>
optional?: any literal: 'foo'
recordRef: Record<string, null> optional?: any
interface: Test recordRef: Record<string, null>
alias: Alias interface: Test
alias: Alias
union: string | number union: string | number
literalUnion: 'foo' | 'bar' literalUnion: 'foo' | 'bar'
literalUnionMixed: 'foo' | 1 | boolean literalUnionMixed: 'foo' | 1 | boolean
intersection: Test & {} intersection: Test & {}
} }
}>()
</script>`) </script>`)
assertCode(content) assertCode(content)
expect(content).toMatch(`string: { type: String, required: true }`) expect(content).toMatch(`string: { type: String, required: true }`)
@ -154,21 +263,57 @@ describe('SFC compile <script setup>', () => {
`literalUnionMixed: { type: [String, Number, Boolean], required: true }` `literalUnionMixed: { type: [String, Number, Boolean], required: true }`
) )
expect(content).toMatch(`intersection: { type: Object, required: true }`) expect(content).toMatch(`intersection: { type: Object, required: true }`)
expect(bindings).toStrictEqual({
string: 'props',
number: 'props',
boolean: 'props',
object: 'props',
objectLiteral: 'props',
fn: 'props',
functionRef: 'props',
objectRef: 'props',
array: 'props',
arrayRef: 'props',
tuple: 'props',
set: 'props',
literal: 'props',
optional: 'props',
recordRef: 'props',
interface: 'props',
alias: 'props',
union: 'props',
literalUnion: 'props',
literalUnionMixed: 'props',
intersection: 'props'
})
}) })
test('extract emits', () => { test('defineContext w/ type / extract emits', () => {
const { content } = compile(` const { content } = compile(`
<script setup="_, { emit: myEmit }" lang="ts"> <script setup lang="ts">
declare function myEmit(e: 'foo' | 'bar'): void import { defineContext } from 'vue'
declare function myEmit(e: 'baz', id: number): void const { emit } = defineContext<{
emit: (e: 'foo' | 'bar') => void
}>()
</script>
`)
assertCode(content)
expect(content).toMatch(`props: {},\n emit: (e: 'foo' | 'bar') => void,`)
expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
})
test('defineContext w/ type / extract emits (union)', () => {
const { content } = compile(`
<script setup lang="ts">
import { defineContext } from 'vue'
const { emit } = defineContext<{
emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)
}>()
</script> </script>
`) `)
assertCode(content) assertCode(content)
expect(content).toMatch( expect(content).toMatch(
`declare function __emit__(e: 'foo' | 'bar'): void` `props: {},\n emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),`
)
expect(content).toMatch(
`declare function __emit__(e: 'baz', id: number): void`
) )
expect(content).toMatch( expect(content).toMatch(
`emits: ["foo", "bar", "baz"] as unknown as undefined` `emits: ["foo", "bar", "baz"] as unknown as undefined`
@ -220,9 +365,7 @@ describe('SFC compile <script setup>', () => {
describe('async/await detection', () => { describe('async/await detection', () => {
function assertAwaitDetection(code: string, shouldAsync = true) { function assertAwaitDetection(code: string, shouldAsync = true) {
const { content } = compile(`<script setup>${code}</script>`) const { content } = compile(`<script setup>${code}</script>`)
expect(content).toMatch( expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup()`)
`export ${shouldAsync ? `async ` : ``}function setup`
)
} }
test('expression statement', () => { test('expression statement', () => {
@ -459,25 +602,27 @@ describe('SFC compile <script setup>', () => {
).toThrow(`<script> and <script setup> must have the same language type`) ).toThrow(`<script> and <script setup> must have the same language type`)
}) })
const moduleErrorMsg = `cannot contain ES module exports`
test('non-type named exports', () => { test('non-type named exports', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
export const a = 1 export const a = 1
</script>`) </script>`)
).toThrow(`cannot contain non-type named or * exports`) ).toThrow(moduleErrorMsg)
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
export * from './foo' export * from './foo'
</script>`) </script>`)
).toThrow(`cannot contain non-type named or * exports`) ).toThrow(moduleErrorMsg)
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
const bar = 1 const bar = 1
export { bar as default } export { bar as default }
</script>`) </script>`)
).toThrow(`cannot contain non-type named or * exports`) ).toThrow(moduleErrorMsg)
}) })
test('ref: non-assignment expressions', () => { test('ref: non-assignment expressions', () => {
@ -488,97 +633,74 @@ describe('SFC compile <script setup>', () => {
).toThrow(`ref: statements can only contain assignment expressions`) ).toThrow(`ref: statements can only contain assignment expressions`)
}) })
test('export default referencing local var', () => { test('defineContext() w/ both type and non-type args', () => {
expect(() => {
compile(`<script setup lang="ts">
import { defineContext } from 'vue'
defineContext<{}>({})
</script>`)
}).toThrow(`cannot accept both type and non-type arguments`)
})
test('defineContext() referencing local var', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
const bar = 1 import { defineContext } from 'vue'
export default { const bar = 1
props: { defineContext({
foo: { props: {
default: () => bar foo: {
} default: () => bar
} }
} }
})
</script>`) </script>`)
).toThrow(`cannot reference locally declared variables`) ).toThrow(`cannot reference locally declared variables`)
}) })
test('export default referencing ref declarations', () => { test('defineContext() referencing ref declarations', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
import { defineContext } from 'vue'
ref: bar = 1 ref: bar = 1
export default { defineContext({
props: bar props: { bar }
} })
</script>`) </script>`)
).toThrow(`cannot reference locally declared variables`) ).toThrow(`cannot reference locally declared variables`)
}) })
test('should allow export default referencing scope var', () => { test('should allow defineContext() referencing scope var', () => {
assertCode( assertCode(
compile(`<script setup> compile(`<script setup>
import { defineContext } from 'vue'
const bar = 1 const bar = 1
export default { defineContext({
props: { props: {
foo: { foo: {
default: bar => bar + 1 default: bar => bar + 1
} }
} }
} })
</script>`).content </script>`).content
) )
}) })
test('should allow export default referencing imported binding', () => { test('should allow defineContext() referencing imported binding', () => {
assertCode( assertCode(
compile(`<script setup> compile(`<script setup>
import { defineContext } from 'vue'
import { bar } from './bar' import { bar } from './bar'
export default { defineContext({
props: { props: {
foo: { foo: {
default: () => bar default: () => bar
} }
} }
} })
</script>`).content </script>`).content
) )
}) })
test('error on duplicated default export', () => {
expect(() =>
compile(`
<script>
export default {}
</script>
<script setup>
export default {}
</script>
`)
).toThrow(`Default export is already declared`)
expect(() =>
compile(`
<script>
export { x as default } from './y'
</script>
<script setup>
export default {}
</script>
`)
).toThrow(`Default export is already declared`)
expect(() =>
compile(`
<script>
const x = {}
export { x as default }
</script>
<script setup>
export default {}
</script>
`)
).toThrow(`Default export is already declared`)
})
}) })
}) })
@ -779,11 +901,12 @@ describe('SFC analyze <script> bindings', () => {
it('works for script setup', () => { it('works for script setup', () => {
const { bindings } = compile(` const { bindings } = compile(`
<script setup> <script setup>
export default { import { defineContext } from 'vue'
props: { defineContext({
foo: String, props: {
}, foo: String,
} }
})
</script> </script>
`) `)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({

View File

@ -15,12 +15,12 @@ import {
TSType, TSType,
TSTypeLiteral, TSTypeLiteral,
TSFunctionType, TSFunctionType,
TSDeclareFunction,
ObjectProperty, ObjectProperty,
ArrayExpression, ArrayExpression,
Statement, Statement,
Expression, Expression,
LabeledStatement LabeledStatement,
TSUnionType
} from '@babel/types' } from '@babel/types'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map' import { RawSourceMap } from 'source-map'
@ -143,6 +143,7 @@ export function compileScript(
const refIdentifiers: Set<Identifier> = new Set() const refIdentifiers: Set<Identifier> = new Set()
const enableRefSugar = options.refSugar !== false const enableRefSugar = options.refSugar !== false
let defaultExport: Node | undefined let defaultExport: Node | undefined
let hasContextCall = false
let setupContextExp: string | undefined let setupContextExp: string | undefined
let setupContextArg: ObjectExpression | undefined let setupContextArg: ObjectExpression | undefined
let setupContextType: TSTypeLiteral | undefined let setupContextType: TSTypeLiteral | undefined
@ -182,6 +183,48 @@ export function compileScript(
) )
} }
function processContextCall(node: Node): boolean {
if (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === CTX_FN_NAME
) {
if (hasContextCall) {
error('duplicate defineContext() call', node)
}
hasContextCall = true
const optsArg = node.arguments[0]
if (optsArg) {
if (optsArg.type === 'ObjectExpression') {
setupContextArg = optsArg
} else {
error(`${CTX_FN_NAME}() argument must be an object literal.`, optsArg)
}
}
// context call has type parameters - infer runtime types from it
if (node.typeParameters) {
if (setupContextArg) {
error(
`${CTX_FN_NAME}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`,
node
)
}
const typeArg = node.typeParameters.params[0]
if (typeArg.type === 'TSTypeLiteral') {
setupContextType = typeArg
} else {
error(
`type argument passed to ${CTX_FN_NAME}() must be a literal type.`,
typeArg
)
}
}
return true
}
return false
}
function processRefExpression(exp: Expression, statement: LabeledStatement) { function processRefExpression(exp: Expression, statement: LabeledStatement) {
if (exp.type === 'AssignmentExpression') { if (exp.type === 'AssignmentExpression') {
helperImports.add('ref') helperImports.add('ref')
@ -500,51 +543,24 @@ export function compileScript(
} }
} }
if (
node.type === 'ExpressionStatement' &&
processContextCall(node.expression)
) {
s.remove(node.start! + startOffset, node.end! + startOffset)
}
if (node.type === 'VariableDeclaration' && !node.declare) { if (node.type === 'VariableDeclaration' && !node.declare) {
for (const decl of node.declarations) { for (const decl of node.declarations) {
if ( if (decl.init && processContextCall(decl.init)) {
decl.init &&
decl.init.type === 'CallExpression' &&
decl.init.callee.type === 'Identifier' &&
decl.init.callee.name === CTX_FN_NAME
) {
if (node.declarations.length === 1) {
s.remove(node.start! + startOffset, node.end! + startOffset)
} else {
s.remove(decl.start! + startOffset, decl.end! + startOffset)
}
setupContextExp = scriptSetup.content.slice( setupContextExp = scriptSetup.content.slice(
decl.id.start!, decl.id.start!,
decl.id.end! decl.id.end!
) )
const optsArg = decl.init.arguments[0] if (node.declarations.length === 1) {
if (optsArg.type === 'ObjectExpression') { s.remove(node.start! + startOffset, node.end! + startOffset)
setupContextArg = optsArg
} else { } else {
error( s.remove(decl.start! + startOffset, decl.end! + startOffset)
`${CTX_FN_NAME}() argument must be an object literal.`,
optsArg
)
}
// useSetupContext() has type parameters - infer runtime types from it
if (decl.init.typeParameters) {
if (setupContextArg) {
error(
`${CTX_FN_NAME}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`,
decl.init
)
}
const typeArg = decl.init.typeParameters.params[0]
if (typeArg.type === 'TSTypeLiteral') {
setupContextType = typeArg
} else {
error(
`type argument passed to ${CTX_FN_NAME}() must be a literal type.`,
typeArg
)
}
} }
} }
} }
@ -641,7 +657,8 @@ export function compileScript(
typeNode.start!, typeNode.start!,
typeNode.end! typeNode.end!
) )
if (m.key.name === 'props') { const key = m.key.name
if (key === 'props') {
propsType = typeString propsType = typeString
if (typeNode.type === 'TSTypeLiteral') { if (typeNode.type === 'TSTypeLiteral') {
extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes) extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
@ -649,18 +666,23 @@ export function compileScript(
// TODO be able to trace references // TODO be able to trace references
error(`props type must be an object literal type`, typeNode) error(`props type must be an object literal type`, typeNode)
} }
} else if (m.key.name === 'emit') { } else if (key === 'emit') {
emitType = typeString emitType = typeString
if (typeNode.type === 'TSFunctionType') { if (
typeNode.type === 'TSFunctionType' ||
typeNode.type === 'TSUnionType'
) {
extractRuntimeEmits(typeNode, typeDeclaredEmits) extractRuntimeEmits(typeNode, typeDeclaredEmits)
} else { } else {
// TODO be able to trace references // TODO be able to trace references
error(`emit type must be a function type`, typeNode) error(`emit type must be a function type`, typeNode)
} }
} else if (m.key.name === 'attrs') { } else if (key === 'attrs') {
attrsType = typeString attrsType = typeString
} else if (m.key.name === 'slots') { } else if (key === 'slots') {
slotsType = typeString slotsType = typeString
} else {
error(`invalid setup context property: "${key}"`, m.key)
} }
} }
} }
@ -747,19 +769,13 @@ export function compileScript(
if (setupContextArg) { if (setupContextArg) {
Object.assign(bindingMetadata, analyzeBindingsFromOptions(setupContextArg)) Object.assign(bindingMetadata, analyzeBindingsFromOptions(setupContextArg))
} }
if (options.inlineTemplate) { for (const [key, { source }] of Object.entries(userImports)) {
for (const [key, { source }] of Object.entries(userImports)) { bindingMetadata[key] = source.endsWith('.vue')
bindingMetadata[key] = source.endsWith('.vue') ? BindingTypes.CONST
? BindingTypes.CONST : BindingTypes.SETUP
: BindingTypes.SETUP }
} for (const key in setupBindings) {
for (const key in setupBindings) { bindingMetadata[key] = setupBindings[key]
bindingMetadata[key] = setupBindings[key]
}
} else {
for (const key in allBindings) {
bindingMetadata[key] = BindingTypes.SETUP
}
} }
// 11. generate return statement // 11. generate return statement
@ -1135,11 +1151,20 @@ function toRuntimeTypeString(types: string[]) {
} }
function extractRuntimeEmits( function extractRuntimeEmits(
node: TSFunctionType | TSDeclareFunction, node: TSFunctionType | TSUnionType,
emits: Set<string> emits: Set<string>
) { ) {
const eventName = if (node.type === 'TSUnionType') {
node.type === 'TSDeclareFunction' ? node.params[0] : node.parameters[0] for (let t of node.types) {
if (t.type === 'TSParenthesizedType') t = t.typeAnnotation
if (t.type === 'TSFunctionType') {
extractRuntimeEmits(t, emits)
}
}
return
}
const eventName = node.parameters[0]
if ( if (
eventName.type === 'Identifier' && eventName.type === 'Identifier' &&
eventName.typeAnnotation && eventName.typeAnnotation &&