}
const tagFromConst = checkType(BindingTypes.CONST)
if (tagFromConst) {
- // constant setup bindings (e.g. imports) can be used as-is
- return tagFromConst
+ return context.inline
+ ? // in inline mode, const setup bindings (e.g. imports) can be used as-is
+ tagFromConst
+ : `$setup[${JSON.stringify(tagFromConst)}]`
}
}
const { inline, bindingMetadata } = context
// 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
return node
}
const prefix = (raw: string) => {
const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
- if (type === BindingTypes.CONST) {
- return raw
- }
if (inline) {
// setup inline mode
- if (type === BindingTypes.SETUP) {
+ if (type === BindingTypes.CONST) {
+ return raw
+ } else if (type === BindingTypes.SETUP) {
return `${context.helperString(UNREF)}(${raw})`
} else if (type === BindingTypes.PROPS) {
// use __props which is generated by compileScript so in ts mode
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.
// 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`] = `
"const __default__ = { setup() {} }
import { useCssVars as __useCssVars__ } from 'vue'
exports[`SFC compile <script setup> CSS vars injection w/ <script setup> 1`] = `
"import { useCssVars } from 'vue'
-export function setup() {
+export default {
+ setup() {
const color = 'red'
__useCssVars__(_ctx => ({ color }))
return { color }
}
-export default { setup }"
+}"
`;
-exports[`SFC compile <script setup> errors should allow export default referencing imported binding 1`] = `
-"import { bar } from './bar'
-
-export function setup() {
+exports[`SFC compile <script setup> defineContext() 1`] = `
+"export default {
+ props: {
+ 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: {
default: () => bar
}
- }
- }
- __default__.setup = setup
-export default __default__"
-`;
+ },
+ setup() {
-exports[`SFC compile <script setup> errors should allow export default referencing scope var 1`] = `
-"export function setup() {
-
- const bar = 1
+
return { bar }
}
-const __default__ = {
- props: {
+}"
+`;
+
+exports[`SFC compile <script setup> errors should allow defineContext() referencing scope var 1`] = `
+"export default {
+ props: {
foo: {
default: bar => bar + 1
}
- }
- }
- __default__.setup = setup
-export default __default__"
-`;
+ },
+ setup() {
-exports[`SFC compile <script setup> explicit setup signature 1`] = `
-"export function setup(props, { emit }) {
-emit('foo')
-return { }
+ const bar = 1
+
+
+return { bar }
}
-export default { setup }"
+}"
`;
exports[`SFC compile <script setup> imports dedupe between user & helper 1`] = `
"import { ref } from 'vue'
-
-export function setup() {
+
+export default {
+ setup() {
- const foo = ref(1)
-
-return { ref, foo }
+ const foo = ref(1)
+
+return { foo, ref }
}
-export default { setup }"
+}"
`;
exports[`SFC compile <script setup> imports import dedupe between <script> and <script setup> 1`] = `
"import { x } from './x'
-export function setup() {
+export default {
+ setup() {
x()
return { x }
}
-export default { setup }"
+}"
`;
exports[`SFC compile <script setup> imports should extract comment for import or type declarations 1`] = `
"import a from 'a' // comment
- import b from 'b'
-
-export function setup() {
+ import b from 'b'
+
+export default {
+ setup() {
-
+
return { a, b }
}
-export default { setup }"
+}"
`;
exports[`SFC compile <script setup> imports should hoist and expose imports 1`] = `
"import { ref } from 'vue'
-export function setup() {
+export default {
+ setup() {
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`] = `
"import { ref } from 'vue'
-export function setup() {
+export default {
+ setup() {
const a = ref(1)
console.log(a.value)
return { a, get }
}
-export default { setup }"
+}"
`;
exports[`SFC compile <script setup> ref: syntax sugar array destructure 1`] = `
"import { ref } from 'vue'
-export function setup() {
+export default {
+ setup() {
const n = ref(1), [__a, __b = 1, ...__c] = useFoo()
const a = ref(__a);
return { n, a, b, c }
}
-export default { setup }"
+}"
`;
exports[`SFC compile <script setup> ref: syntax sugar convert ref declarations 1`] = `
"import { ref } from 'vue'
-export function setup() {
+export default {
+ setup() {
const foo = ref()
const a = ref(1)
return { foo, a, b, c, d }
}
-export default { setup }"
+}"
`;
exports[`SFC compile <script setup> ref: syntax sugar multi ref declarations 1`] = `
"import { ref } from 'vue'
-export function setup() {
+export default {
+ setup() {
const a = ref(1), b = ref(2), c = ref({
count: 0
return { a, b, c }
}
-export default { setup }"
+}"
`;
exports[`SFC compile <script setup> ref: syntax sugar mutating ref binding 1`] = `
"import { ref } from 'vue'
-export function setup() {
+export default {
+ setup() {
const a = ref(1)
const b = ref({ count: 0 })
return { a, b, inc }
}
-export default { setup }"
+}"
`;
exports[`SFC compile <script setup> ref: syntax sugar nested destructure 1`] = `
"import { ref } from 'vue'
-export function setup() {
+export default {
+ setup() {
const [{ a: { b: __b }}] = useFoo()
const b = ref(__b);
return { b, d, e }
}
-export default { setup }"
+}"
`;
exports[`SFC compile <script setup> ref: syntax sugar object destructure 1`] = `
"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 a = ref(__a);
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`] = `
-"export function setup() {
+"export default {
+ setup() {
foo: a = 1, b = 2, c = {
count: 0
return { }
}
-export default { setup }"
+}"
`;
exports[`SFC compile <script setup> ref: syntax sugar using ref binding in property shorthand 1`] = `
"import { ref } from 'vue'
-export function setup() {
+export default {
+ setup() {
const a = ref(1)
const b = { a: a.value }
return { a, b, test }
}
-export default { setup }"
+}"
`;
exports[`SFC compile <script setup> should expose top level declarations 1`] = `
"import { x } from './x'
-export function setup() {
+export default {
+ setup() {
let a = 1
const b = 2
function c() {}
class d {}
-return { x, a, b, c, d }
+return { a, b, c, d, x }
+}
+
+}"
+`;
+
+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 { }
}
-export default { setup }"
+})"
+`;
+
+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 { }
+}
+
+})"
`;
}
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', () => {
const { content } = compile(`
<script setup>
</script>
`)
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', () => {
test('should extract comment for import or type declarations', () => {
assertCode(
- compile(`<script setup>
- import a from 'a' // comment
- import b from 'b'
- </script>`).content
+ compile(`
+ <script setup>
+ import a from 'a' // comment
+ import b from 'b'
+ </script>
+ `).content
)
})
test('dedupe between user & helper', () => {
- const { content } = compile(`<script setup>
- import { ref } from 'vue'
- ref: foo = 1
- </script>`)
+ const { content } = compile(`
+ <script setup>
+ import { ref } from 'vue'
+ ref: foo = 1
+ </script>
+ `)
assertCode(content)
expect(content).toMatch(`import { ref } from 'vue'`)
})
})
})
- 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', () => {
const { content } = compile(`
<script setup lang="ts">
assertCode(content)
})
- test('extract props', () => {
+ test('defineContext w/ runtime options', () => {
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 {}
type Alias = number[]
- declare const 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 & {}
- }
+ defineContext<{
+ props: {
+ 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 & {}
+ }
+ }>()
</script>`)
assertCode(content)
expect(content).toMatch(`string: { type: String, required: true }`)
`literalUnionMixed: { type: [String, Number, Boolean], 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(`
- <script setup="_, { emit: myEmit }" lang="ts">
- declare function myEmit(e: 'foo' | 'bar'): void
- declare function myEmit(e: 'baz', id: number): void
+ <script setup lang="ts">
+ import { defineContext } from 'vue'
+ 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>
`)
assertCode(content)
expect(content).toMatch(
- `declare function __emit__(e: 'foo' | 'bar'): void`
- )
- expect(content).toMatch(
- `declare function __emit__(e: 'baz', id: number): void`
+ `props: {},\n emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),`
)
expect(content).toMatch(
`emits: ["foo", "bar", "baz"] as unknown as undefined`
describe('async/await detection', () => {
function assertAwaitDetection(code: string, shouldAsync = true) {
const { content } = compile(`<script setup>${code}</script>`)
- expect(content).toMatch(
- `export ${shouldAsync ? `async ` : ``}function setup`
- )
+ expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup()`)
}
test('expression statement', () => {
).toThrow(`<script> and <script setup> must have the same language type`)
})
+ const moduleErrorMsg = `cannot contain ES module exports`
+
test('non-type named exports', () => {
expect(() =>
compile(`<script setup>
export const a = 1
</script>`)
- ).toThrow(`cannot contain non-type named or * exports`)
+ ).toThrow(moduleErrorMsg)
expect(() =>
compile(`<script setup>
export * from './foo'
</script>`)
- ).toThrow(`cannot contain non-type named or * exports`)
+ ).toThrow(moduleErrorMsg)
expect(() =>
compile(`<script setup>
const bar = 1
export { bar as default }
</script>`)
- ).toThrow(`cannot contain non-type named or * exports`)
+ ).toThrow(moduleErrorMsg)
})
test('ref: non-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(() =>
compile(`<script setup>
- const bar = 1
- export default {
- props: {
- foo: {
- default: () => bar
- }
+ import { defineContext } from 'vue'
+ const bar = 1
+ defineContext({
+ props: {
+ foo: {
+ default: () => bar
}
}
+ })
</script>`)
).toThrow(`cannot reference locally declared variables`)
})
- test('export default referencing ref declarations', () => {
+ test('defineContext() referencing ref declarations', () => {
expect(() =>
compile(`<script setup>
+ import { defineContext } from 'vue'
ref: bar = 1
- export default {
- props: bar
- }
+ defineContext({
+ props: { bar }
+ })
</script>`)
).toThrow(`cannot reference locally declared variables`)
})
- test('should allow export default referencing scope var', () => {
+ test('should allow defineContext() referencing scope var', () => {
assertCode(
compile(`<script setup>
+ import { defineContext } from 'vue'
const bar = 1
- export default {
+ defineContext({
props: {
foo: {
default: bar => bar + 1
}
}
- }
+ })
</script>`).content
)
})
- test('should allow export default referencing imported binding', () => {
+ test('should allow defineContext() referencing imported binding', () => {
assertCode(
compile(`<script setup>
+ import { defineContext } from 'vue'
import { bar } from './bar'
- export default {
+ defineContext({
props: {
foo: {
default: () => bar
}
}
- }
+ })
</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`)
- })
})
})
it('works for script setup', () => {
const { bindings } = compile(`
<script setup>
- export default {
- props: {
- foo: String,
- },
+ import { defineContext } from 'vue'
+ defineContext({
+ props: {
+ foo: String,
}
+ })
</script>
`)
expect(bindings).toStrictEqual({
TSType,
TSTypeLiteral,
TSFunctionType,
- TSDeclareFunction,
ObjectProperty,
ArrayExpression,
Statement,
Expression,
- LabeledStatement
+ LabeledStatement,
+ TSUnionType
} from '@babel/types'
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
const refIdentifiers: Set<Identifier> = new Set()
const enableRefSugar = options.refSugar !== false
let defaultExport: Node | undefined
+ let hasContextCall = false
let setupContextExp: string | undefined
let setupContextArg: ObjectExpression | undefined
let setupContextType: TSTypeLiteral | undefined
)
}
+ 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) {
if (exp.type === 'AssignmentExpression') {
helperImports.add('ref')
}
}
+ if (
+ node.type === 'ExpressionStatement' &&
+ processContextCall(node.expression)
+ ) {
+ s.remove(node.start! + startOffset, node.end! + startOffset)
+ }
+
if (node.type === 'VariableDeclaration' && !node.declare) {
for (const decl of node.declarations) {
- if (
- 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)
- }
+ if (decl.init && processContextCall(decl.init)) {
setupContextExp = scriptSetup.content.slice(
decl.id.start!,
decl.id.end!
)
- const optsArg = decl.init.arguments[0]
- if (optsArg.type === 'ObjectExpression') {
- setupContextArg = optsArg
+ if (node.declarations.length === 1) {
+ s.remove(node.start! + startOffset, node.end! + startOffset)
} else {
- error(
- `${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
- )
- }
+ s.remove(decl.start! + startOffset, decl.end! + startOffset)
}
}
}
typeNode.start!,
typeNode.end!
)
- if (m.key.name === 'props') {
+ const key = m.key.name
+ if (key === 'props') {
propsType = typeString
if (typeNode.type === 'TSTypeLiteral') {
extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
// TODO be able to trace references
error(`props type must be an object literal type`, typeNode)
}
- } else if (m.key.name === 'emit') {
+ } else if (key === 'emit') {
emitType = typeString
- if (typeNode.type === 'TSFunctionType') {
+ if (
+ typeNode.type === 'TSFunctionType' ||
+ typeNode.type === 'TSUnionType'
+ ) {
extractRuntimeEmits(typeNode, typeDeclaredEmits)
} else {
// TODO be able to trace references
error(`emit type must be a function type`, typeNode)
}
- } else if (m.key.name === 'attrs') {
+ } else if (key === 'attrs') {
attrsType = typeString
- } else if (m.key.name === 'slots') {
+ } else if (key === 'slots') {
slotsType = typeString
+ } else {
+ error(`invalid setup context property: "${key}"`, m.key)
}
}
}
if (setupContextArg) {
Object.assign(bindingMetadata, analyzeBindingsFromOptions(setupContextArg))
}
- if (options.inlineTemplate) {
- for (const [key, { source }] of Object.entries(userImports)) {
- bindingMetadata[key] = source.endsWith('.vue')
- ? BindingTypes.CONST
- : BindingTypes.SETUP
- }
- for (const key in setupBindings) {
- bindingMetadata[key] = setupBindings[key]
- }
- } else {
- for (const key in allBindings) {
- bindingMetadata[key] = BindingTypes.SETUP
- }
+ for (const [key, { source }] of Object.entries(userImports)) {
+ bindingMetadata[key] = source.endsWith('.vue')
+ ? BindingTypes.CONST
+ : BindingTypes.SETUP
+ }
+ for (const key in setupBindings) {
+ bindingMetadata[key] = setupBindings[key]
}
// 11. generate return statement
}
function extractRuntimeEmits(
- node: TSFunctionType | TSDeclareFunction,
+ node: TSFunctionType | TSUnionType,
emits: Set<string>
) {
- const eventName =
- node.type === 'TSDeclareFunction' ? node.params[0] : node.parameters[0]
+ if (node.type === 'TSUnionType') {
+ 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 (
eventName.type === 'Identifier' &&
eventName.typeAnnotation &&