wip: defineOptions -> defineProps + defineEmit + useContext
This commit is contained in:
parent
ae2caad740
commit
47d73c23e1
@ -33,38 +33,55 @@ return { x }
|
||||
export const n = 1"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> defineOptions() 1`] = `
|
||||
exports[`SFC compile <script setup> defineEmit() 1`] = `
|
||||
"export default {
|
||||
expose: [],
|
||||
props: {
|
||||
foo: String
|
||||
},
|
||||
emit: ['a', 'b'],
|
||||
setup(__props, { props, emit }) {
|
||||
emits: ['foo', 'bar'],
|
||||
setup(__props, { emit: myEmit }) {
|
||||
|
||||
|
||||
|
||||
const bar = 1
|
||||
|
||||
return { props, emit, bar }
|
||||
return { myEmit }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> errors should allow defineOptions() referencing imported binding 1`] = `
|
||||
exports[`SFC compile <script setup> defineProps() 1`] = `
|
||||
"export default {
|
||||
expose: [],
|
||||
props: {
|
||||
foo: String
|
||||
},
|
||||
setup(__props) {
|
||||
|
||||
const props = __props
|
||||
|
||||
const bar = 1
|
||||
|
||||
return { props, bar }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> errors should allow defineProps/Emit() referencing imported binding 1`] = `
|
||||
"import { bar } from './bar'
|
||||
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
props: {
|
||||
foo: {
|
||||
default: () => bar
|
||||
}
|
||||
},
|
||||
foo: {
|
||||
default: () => bar
|
||||
}
|
||||
},
|
||||
emits: {
|
||||
foo: () => bar > 1
|
||||
},
|
||||
setup(__props) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { bar }
|
||||
}
|
||||
@ -72,18 +89,22 @@ return { bar }
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> errors should allow defineOptions() referencing scope var 1`] = `
|
||||
exports[`SFC compile <script setup> errors should allow defineProps/Emit() referencing scope var 1`] = `
|
||||
"export default {
|
||||
expose: [],
|
||||
props: {
|
||||
foo: {
|
||||
default: bar => bar + 1
|
||||
}
|
||||
},
|
||||
foo: {
|
||||
default: bar => bar + 1
|
||||
}
|
||||
},
|
||||
emits: {
|
||||
foo: bar => bar > 1
|
||||
},
|
||||
setup(__props) {
|
||||
|
||||
const bar = 1
|
||||
|
||||
|
||||
|
||||
return { bar }
|
||||
}
|
||||
@ -594,37 +615,18 @@ return { a, b, c, d, x }
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> with TypeScript defineOptions w/ runtime options 1`] = `
|
||||
exports[`SFC compile <script setup> with TypeScript defineEmit w/ type (union) 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
|
||||
|
||||
export default _defineComponent({
|
||||
expose: [],
|
||||
props: { foo: String },
|
||||
emits: ['a', 'b'],
|
||||
setup(__props, { props, emit }) {
|
||||
|
||||
|
||||
|
||||
return { props, emit }
|
||||
}
|
||||
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract emits (union) 1`] = `
|
||||
"import { Slots as _Slots, defineComponent as _defineComponent } from 'vue'
|
||||
|
||||
|
||||
export default _defineComponent({
|
||||
expose: [],
|
||||
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>
|
||||
}) {
|
||||
emit: (((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)),
|
||||
slots: any,
|
||||
attrs: any
|
||||
}) {
|
||||
|
||||
|
||||
|
||||
@ -634,19 +636,18 @@ return { emit }
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract emits 1`] = `
|
||||
"import { Slots as _Slots, defineComponent as _defineComponent } from 'vue'
|
||||
exports[`SFC compile <script setup> with TypeScript defineEmit w/ type 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
|
||||
|
||||
export default _defineComponent({
|
||||
expose: [],
|
||||
emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
|
||||
setup(__props, { emit }: {
|
||||
props: {},
|
||||
emit: (e: 'foo' | 'bar') => void,
|
||||
slots: Slots,
|
||||
attrs: Record<string, any>
|
||||
}) {
|
||||
emit: ((e: 'foo' | 'bar') => void),
|
||||
slots: any,
|
||||
attrs: any
|
||||
}) {
|
||||
|
||||
|
||||
|
||||
@ -656,7 +657,7 @@ return { emit }
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract props 1`] = `
|
||||
exports[`SFC compile <script setup> with TypeScript defineProps w/ type 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
|
||||
interface Test {}
|
||||
@ -689,7 +690,30 @@ export default _defineComponent({
|
||||
literalUnionMixed: { type: [String, Number, Boolean], required: true },
|
||||
intersection: { type: Object, required: true }
|
||||
} as unknown as undefined,
|
||||
setup(__props) {
|
||||
setup(__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 & {}
|
||||
}) {
|
||||
|
||||
|
||||
|
||||
@ -699,6 +723,26 @@ return { }
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> with TypeScript defineProps/Emit w/ runtime options 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
|
||||
|
||||
export default _defineComponent({
|
||||
expose: [],
|
||||
props: { foo: String },
|
||||
emits: ['a', 'b'],
|
||||
setup(__props, { emit }) {
|
||||
|
||||
const props = __props
|
||||
|
||||
|
||||
|
||||
return { props, emit }
|
||||
}
|
||||
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> with TypeScript hoist type declarations 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
export interface Foo {}
|
||||
|
@ -86,8 +86,8 @@ import { ref } from 'vue'
|
||||
export default {
|
||||
expose: [],
|
||||
props: {
|
||||
foo: String
|
||||
},
|
||||
foo: String
|
||||
},
|
||||
setup(__props) {
|
||||
|
||||
_useCssVars(_ctx => ({
|
||||
|
@ -16,17 +16,13 @@ describe('SFC compile <script setup>', () => {
|
||||
expect(content).toMatch('return { a, b, c, d, x }')
|
||||
})
|
||||
|
||||
test('defineOptions()', () => {
|
||||
test('defineProps()', () => {
|
||||
const { content, bindings } = compile(`
|
||||
<script setup>
|
||||
import { defineOptions } from 'vue'
|
||||
const { props, emit } = defineOptions({
|
||||
props: {
|
||||
foo: String
|
||||
},
|
||||
emit: ['a', 'b']
|
||||
import { defineProps } from 'vue'
|
||||
const props = defineProps({
|
||||
foo: String
|
||||
})
|
||||
|
||||
const bar = 1
|
||||
</script>
|
||||
`)
|
||||
@ -36,21 +32,42 @@ const bar = 1
|
||||
expect(bindings).toStrictEqual({
|
||||
foo: BindingTypes.PROPS,
|
||||
bar: BindingTypes.SETUP_CONST,
|
||||
props: BindingTypes.SETUP_CONST,
|
||||
emit: BindingTypes.SETUP_CONST
|
||||
props: BindingTypes.SETUP_CONST
|
||||
})
|
||||
|
||||
// should remove defineOptions import and call
|
||||
expect(content).not.toMatch('defineOptions')
|
||||
expect(content).not.toMatch('defineProps')
|
||||
// should generate correct setup signature
|
||||
expect(content).toMatch(`setup(__props, { props, emit }) {`)
|
||||
expect(content).toMatch(`setup(__props) {`)
|
||||
// should assign user identifier to it
|
||||
expect(content).toMatch(`const props = __props`)
|
||||
// should include context options in default export
|
||||
expect(content).toMatch(`export default {
|
||||
expose: [],
|
||||
props: {
|
||||
foo: String
|
||||
},
|
||||
emit: ['a', 'b'],`)
|
||||
foo: String
|
||||
},`)
|
||||
})
|
||||
|
||||
test('defineEmit()', () => {
|
||||
const { content, bindings } = compile(`
|
||||
<script setup>
|
||||
import { defineEmit } from 'vue'
|
||||
const myEmit = defineEmit(['foo', 'bar'])
|
||||
</script>
|
||||
`)
|
||||
assertCode(content)
|
||||
expect(bindings).toStrictEqual({
|
||||
myEmit: BindingTypes.SETUP_CONST
|
||||
})
|
||||
// should remove defineOptions import and call
|
||||
expect(content).not.toMatch('defineEmit')
|
||||
// should generate correct setup signature
|
||||
expect(content).toMatch(`setup(__props, { emit: myEmit }) {`)
|
||||
// should include context options in default export
|
||||
expect(content).toMatch(`export default {
|
||||
expose: [],
|
||||
emits: ['foo', 'bar'],`)
|
||||
})
|
||||
|
||||
describe('<script> and <script setup> co-usage', () => {
|
||||
@ -174,7 +191,7 @@ const bar = 1
|
||||
// function, const, component import
|
||||
const { content } = compile(
|
||||
`<script setup>
|
||||
import { ref, defineOptions } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import Foo from './Foo.vue'
|
||||
import other from './util'
|
||||
const count = ref(0)
|
||||
@ -360,14 +377,12 @@ const bar = 1
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
test('defineOptions w/ runtime options', () => {
|
||||
test('defineProps/Emit w/ runtime options', () => {
|
||||
const { content } = compile(`
|
||||
<script setup lang="ts">
|
||||
import { defineOptions } from 'vue'
|
||||
const { props, emit } = defineOptions({
|
||||
props: { foo: String },
|
||||
emits: ['a', 'b']
|
||||
})
|
||||
import { defineProps, defineEmit } from 'vue'
|
||||
const props = defineProps({ foo: String })
|
||||
const emit = defineEmit(['a', 'b'])
|
||||
</script>
|
||||
`)
|
||||
assertCode(content)
|
||||
@ -375,42 +390,40 @@ const { props, emit } = defineOptions({
|
||||
expose: [],
|
||||
props: { foo: String },
|
||||
emits: ['a', 'b'],
|
||||
setup(__props, { props, emit }) {`)
|
||||
setup(__props, { emit }) {`)
|
||||
})
|
||||
|
||||
test('defineOptions w/ type / extract props', () => {
|
||||
test('defineProps w/ type', () => {
|
||||
const { content, bindings } = compile(`
|
||||
<script setup lang="ts">
|
||||
import { defineOptions } from 'vue'
|
||||
import { defineProps } from 'vue'
|
||||
interface Test {}
|
||||
|
||||
type Alias = number[]
|
||||
|
||||
defineOptions<{
|
||||
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
|
||||
defineProps<{
|
||||
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 & {}
|
||||
}
|
||||
union: string | number
|
||||
literalUnion: 'foo' | 'bar'
|
||||
literalUnionMixed: 'foo' | 1 | boolean
|
||||
intersection: Test & {}
|
||||
}>()
|
||||
</script>`)
|
||||
assertCode(content)
|
||||
@ -466,33 +479,28 @@ const { props, emit } = defineOptions({
|
||||
})
|
||||
})
|
||||
|
||||
test('defineOptions w/ type / extract emits', () => {
|
||||
test('defineEmit w/ type', () => {
|
||||
const { content } = compile(`
|
||||
<script setup lang="ts">
|
||||
import { defineOptions } from 'vue'
|
||||
const { emit } = defineOptions<{
|
||||
emit: (e: 'foo' | 'bar') => void
|
||||
}>()
|
||||
import { defineEmit } from 'vue'
|
||||
const emit = defineEmit<(e: 'foo' | 'bar') => void>()
|
||||
</script>
|
||||
`)
|
||||
assertCode(content)
|
||||
expect(content).toMatch(`props: {},\n emit: (e: 'foo' | 'bar') => void,`)
|
||||
expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`)
|
||||
expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
|
||||
})
|
||||
|
||||
test('defineOptions w/ type / extract emits (union)', () => {
|
||||
test('defineEmit w/ type (union)', () => {
|
||||
const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)`
|
||||
const { content } = compile(`
|
||||
<script setup lang="ts">
|
||||
import { defineOptions } from 'vue'
|
||||
const { emit } = defineOptions<{
|
||||
emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)
|
||||
}>()
|
||||
import { defineEmit } from 'vue'
|
||||
const emit = defineEmit<${type}>()
|
||||
</script>
|
||||
`)
|
||||
assertCode(content)
|
||||
expect(content).toMatch(
|
||||
`props: {},\n emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),`
|
||||
)
|
||||
expect(content).toMatch(`emit: (${type}),`)
|
||||
expect(content).toMatch(
|
||||
`emits: ["foo", "bar", "baz"] as unknown as undefined`
|
||||
)
|
||||
@ -774,71 +782,96 @@ const { props, emit } = defineOptions({
|
||||
).toThrow(`ref: statements can only contain assignment expressions`)
|
||||
})
|
||||
|
||||
test('defineOptions() w/ both type and non-type args', () => {
|
||||
test('defineProps/Emit() w/ both type and non-type args', () => {
|
||||
expect(() => {
|
||||
compile(`<script setup lang="ts">
|
||||
import { defineOptions } from 'vue'
|
||||
defineOptions<{}>({})
|
||||
import { defineProps } from 'vue'
|
||||
defineProps<{}>({})
|
||||
</script>`)
|
||||
}).toThrow(`cannot accept both type and non-type arguments`)
|
||||
|
||||
expect(() => {
|
||||
compile(`<script setup lang="ts">
|
||||
import { defineEmit } from 'vue'
|
||||
defineEmit<{}>({})
|
||||
</script>`)
|
||||
}).toThrow(`cannot accept both type and non-type arguments`)
|
||||
})
|
||||
|
||||
test('defineOptions() referencing local var', () => {
|
||||
test('defineProps/Emit() referencing local var', () => {
|
||||
expect(() =>
|
||||
compile(`<script setup>
|
||||
import { defineOptions } from 'vue'
|
||||
import { defineProps } from 'vue'
|
||||
const bar = 1
|
||||
defineOptions({
|
||||
props: {
|
||||
foo: {
|
||||
default: () => bar
|
||||
}
|
||||
defineProps({
|
||||
foo: {
|
||||
default: () => bar
|
||||
}
|
||||
})
|
||||
</script>`)
|
||||
).toThrow(`cannot reference locally declared variables`)
|
||||
})
|
||||
|
||||
test('defineOptions() referencing ref declarations', () => {
|
||||
expect(() =>
|
||||
compile(`<script setup>
|
||||
import { defineOptions } from 'vue'
|
||||
import { defineEmit } from 'vue'
|
||||
const bar = 'hello'
|
||||
defineEmit([bar])
|
||||
</script>`)
|
||||
).toThrow(`cannot reference locally declared variables`)
|
||||
})
|
||||
|
||||
test('defineProps/Emit() referencing ref declarations', () => {
|
||||
expect(() =>
|
||||
compile(`<script setup>
|
||||
import { defineProps } from 'vue'
|
||||
ref: bar = 1
|
||||
defineOptions({
|
||||
props: { bar }
|
||||
defineProps({
|
||||
bar
|
||||
})
|
||||
</script>`)
|
||||
).toThrow(`cannot reference locally declared variables`)
|
||||
|
||||
expect(() =>
|
||||
compile(`<script setup>
|
||||
import { defineEmit } from 'vue'
|
||||
ref: bar = 1
|
||||
defineEmit({
|
||||
bar
|
||||
})
|
||||
</script>`)
|
||||
).toThrow(`cannot reference locally declared variables`)
|
||||
})
|
||||
|
||||
test('should allow defineOptions() referencing scope var', () => {
|
||||
test('should allow defineProps/Emit() referencing scope var', () => {
|
||||
assertCode(
|
||||
compile(`<script setup>
|
||||
import { defineOptions } from 'vue'
|
||||
import { defineProps, defineEmit } from 'vue'
|
||||
const bar = 1
|
||||
defineOptions({
|
||||
props: {
|
||||
foo: {
|
||||
default: bar => bar + 1
|
||||
}
|
||||
defineProps({
|
||||
foo: {
|
||||
default: bar => bar + 1
|
||||
}
|
||||
})
|
||||
defineEmit({
|
||||
foo: bar => bar > 1
|
||||
})
|
||||
</script>`).content
|
||||
)
|
||||
})
|
||||
|
||||
test('should allow defineOptions() referencing imported binding', () => {
|
||||
test('should allow defineProps/Emit() referencing imported binding', () => {
|
||||
assertCode(
|
||||
compile(`<script setup>
|
||||
import { defineOptions } from 'vue'
|
||||
import { bar } from './bar'
|
||||
defineOptions({
|
||||
props: {
|
||||
foo: {
|
||||
default: () => bar
|
||||
}
|
||||
}
|
||||
})
|
||||
import { defineProps, defineEmit } from 'vue'
|
||||
import { bar } from './bar'
|
||||
defineProps({
|
||||
foo: {
|
||||
default: () => bar
|
||||
}
|
||||
})
|
||||
defineEmit({
|
||||
foo: () => bar > 1
|
||||
})
|
||||
</script>`).content
|
||||
)
|
||||
})
|
||||
@ -1063,11 +1096,9 @@ describe('SFC analyze <script> bindings', () => {
|
||||
it('works for script setup', () => {
|
||||
const { bindings } = compile(`
|
||||
<script setup>
|
||||
import { defineOptions, ref as r } from 'vue'
|
||||
defineOptions({
|
||||
props: {
|
||||
foo: String,
|
||||
}
|
||||
import { defineProps, ref as r } from 'vue'
|
||||
defineProps({
|
||||
foo: String
|
||||
})
|
||||
|
||||
const a = r(1)
|
||||
|
@ -20,13 +20,11 @@ describe('CSS vars injection', () => {
|
||||
test('w/ <script setup> binding analysis', () => {
|
||||
const { content } = compileSFCScript(
|
||||
`<script setup>
|
||||
import { defineOptions, ref } from 'vue'
|
||||
import { defineProps, ref } from 'vue'
|
||||
const color = 'red'
|
||||
const size = ref('10px')
|
||||
defineOptions({
|
||||
props: {
|
||||
foo: String
|
||||
}
|
||||
defineProps({
|
||||
foo: String
|
||||
})
|
||||
</script>\n` +
|
||||
`<style>
|
||||
|
@ -29,7 +29,8 @@ import { CSS_VARS_HELPER, genCssVarsCode, injectCssVarsCalls } from './cssVars'
|
||||
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
|
||||
import { warnExperimental, warnOnce } from './warn'
|
||||
|
||||
const DEFINE_OPTIONS = 'defineOptions'
|
||||
const DEFINE_PROPS = 'defineProps'
|
||||
const DEFINE_EMIT = 'defineEmit'
|
||||
|
||||
export interface SFCScriptCompileOptions {
|
||||
/**
|
||||
@ -162,17 +163,16 @@ export function compileScript(
|
||||
const refIdentifiers: Set<Identifier> = new Set()
|
||||
const enableRefSugar = options.refSugar !== false
|
||||
let defaultExport: Node | undefined
|
||||
let hasOptionsCall = false
|
||||
let optionsExp: string | undefined
|
||||
let optionsArg: ObjectExpression | undefined
|
||||
let optionsType: TSTypeLiteral | undefined
|
||||
let hasDefinePropsCall = false
|
||||
let hasDefineEmitCall = false
|
||||
let propsRuntimeDecl: Node | undefined
|
||||
let propsTypeDecl: TSTypeLiteral | undefined
|
||||
let propsIdentifier: string | undefined
|
||||
let emitRuntimeDecl: Node | undefined
|
||||
let emitTypeDecl: TSFunctionType | TSUnionType | undefined
|
||||
let emitIdentifier: string | undefined
|
||||
let hasAwait = false
|
||||
let hasInlinedSsrRenderFn = false
|
||||
// context types to generate
|
||||
let propsType = `{}`
|
||||
let emitType = `(e: string, ...args: any[]) => void`
|
||||
let slotsType = `Slots`
|
||||
let attrsType = `Record<string, any>`
|
||||
// props/emits declared via types
|
||||
const typeDeclaredProps: Record<string, PropTypeData> = {}
|
||||
const typeDeclaredEmits: Set<string> = new Set()
|
||||
@ -236,38 +236,28 @@ export function compileScript(
|
||||
}
|
||||
}
|
||||
|
||||
function processDefineOptions(node: Node): boolean {
|
||||
if (isCallOf(node, DEFINE_OPTIONS)) {
|
||||
if (hasOptionsCall) {
|
||||
error(`duplicate ${DEFINE_OPTIONS}() call`, node)
|
||||
}
|
||||
hasOptionsCall = true
|
||||
const optsArg = node.arguments[0]
|
||||
if (optsArg) {
|
||||
if (optsArg.type === 'ObjectExpression') {
|
||||
optionsArg = optsArg
|
||||
} else {
|
||||
error(
|
||||
`${DEFINE_OPTIONS}() argument must be an object literal.`,
|
||||
optsArg
|
||||
)
|
||||
}
|
||||
function processDefineProps(node: Node): boolean {
|
||||
if (isCallOf(node, DEFINE_PROPS)) {
|
||||
if (hasDefinePropsCall) {
|
||||
error(`duplicate ${DEFINE_PROPS}() call`, node)
|
||||
}
|
||||
hasDefinePropsCall = true
|
||||
propsRuntimeDecl = node.arguments[0]
|
||||
// context call has type parameters - infer runtime types from it
|
||||
if (node.typeParameters) {
|
||||
if (optionsArg) {
|
||||
if (propsRuntimeDecl) {
|
||||
error(
|
||||
`${DEFINE_OPTIONS}() cannot accept both type and non-type arguments ` +
|
||||
`${DEFINE_PROPS}() 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') {
|
||||
optionsType = typeArg
|
||||
propsTypeDecl = typeArg
|
||||
} else {
|
||||
error(
|
||||
`type argument passed to ${DEFINE_OPTIONS}() must be a literal type.`,
|
||||
`type argument passed to ${DEFINE_PROPS}() must be a literal type.`,
|
||||
typeArg
|
||||
)
|
||||
}
|
||||
@ -277,6 +267,56 @@ export function compileScript(
|
||||
return false
|
||||
}
|
||||
|
||||
function processDefineEmit(node: Node): boolean {
|
||||
if (isCallOf(node, DEFINE_EMIT)) {
|
||||
if (hasDefineEmitCall) {
|
||||
error(`duplicate ${DEFINE_EMIT}() call`, node)
|
||||
}
|
||||
hasDefineEmitCall = true
|
||||
emitRuntimeDecl = node.arguments[0]
|
||||
if (node.typeParameters) {
|
||||
if (emitRuntimeDecl) {
|
||||
error(
|
||||
`${DEFINE_EMIT}() 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 === 'TSFunctionType' ||
|
||||
typeArg.type === 'TSUnionType'
|
||||
) {
|
||||
emitTypeDecl = typeArg
|
||||
} else {
|
||||
error(
|
||||
`type argument passed to ${DEFINE_EMIT}() must be a function type ` +
|
||||
`or a union of function types.`,
|
||||
typeArg
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function checkInvalidScopeReference(node: Node | undefined, method: string) {
|
||||
if (!node) return
|
||||
walkIdentifiers(node, id => {
|
||||
if (setupBindings[id.name]) {
|
||||
error(
|
||||
`\`${method}()\` in <script setup> cannot reference locally ` +
|
||||
`declared variables because it will be hoisted outside of the ` +
|
||||
`setup() function. If your component options requires initialization ` +
|
||||
`in the module scope, use a separate normal <script> to export ` +
|
||||
`the options instead.`,
|
||||
id
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function processRefExpression(exp: Expression, statement: LabeledStatement) {
|
||||
if (exp.type === 'AssignmentExpression') {
|
||||
const { left, right } = exp
|
||||
@ -562,7 +602,10 @@ export function compileScript(
|
||||
specifier.imported.name
|
||||
const source = node.source.value
|
||||
const existing = userImports[local]
|
||||
if (source === 'vue' && imported === DEFINE_OPTIONS) {
|
||||
if (
|
||||
source === 'vue' &&
|
||||
(imported === DEFINE_PROPS || imported === DEFINE_EMIT)
|
||||
) {
|
||||
removeSpecifier(specifier)
|
||||
} else if (existing) {
|
||||
if (existing.source === source && existing.imported === imported) {
|
||||
@ -585,22 +628,37 @@ export function compileScript(
|
||||
}
|
||||
}
|
||||
|
||||
// process `defineProps` and `defineEmit` calls
|
||||
if (
|
||||
node.type === 'ExpressionStatement' &&
|
||||
processDefineOptions(node.expression)
|
||||
(processDefineProps(node.expression) ||
|
||||
processDefineEmit(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 && processDefineOptions(decl.init)) {
|
||||
optionsExp = scriptSetup.content.slice(decl.id.start!, decl.id.end!)
|
||||
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) {
|
||||
const isDefineProps = processDefineProps(decl.init)
|
||||
if (isDefineProps) {
|
||||
propsIdentifier = scriptSetup.content.slice(
|
||||
decl.id.start!,
|
||||
decl.id.end!
|
||||
)
|
||||
}
|
||||
const isDefineEmit = processDefineEmit(decl.init)
|
||||
if (isDefineEmit) {
|
||||
emitIdentifier = scriptSetup.content.slice(
|
||||
decl.id.start!,
|
||||
decl.id.end!
|
||||
)
|
||||
}
|
||||
if (isDefineProps || isDefineEmit)
|
||||
if (node.declarations.length === 1) {
|
||||
s.remove(node.start! + startOffset, node.end! + startOffset)
|
||||
} else {
|
||||
s.remove(decl.start! + startOffset, decl.end! + startOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -691,61 +749,17 @@ export function compileScript(
|
||||
}
|
||||
|
||||
// 4. extract runtime props/emits code from setup context type
|
||||
if (optionsType) {
|
||||
for (const m of optionsType.members) {
|
||||
if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
|
||||
const typeNode = m.typeAnnotation!.typeAnnotation
|
||||
const typeString = scriptSetup.content.slice(
|
||||
typeNode.start!,
|
||||
typeNode.end!
|
||||
)
|
||||
const key = m.key.name
|
||||
if (key === 'props') {
|
||||
propsType = typeString
|
||||
if (typeNode.type === 'TSTypeLiteral') {
|
||||
extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
|
||||
} else {
|
||||
// TODO be able to trace references
|
||||
error(`props type must be an object literal type`, typeNode)
|
||||
}
|
||||
} else if (key === 'emit') {
|
||||
emitType = typeString
|
||||
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 (key === 'attrs') {
|
||||
attrsType = typeString
|
||||
} else if (key === 'slots') {
|
||||
slotsType = typeString
|
||||
} else {
|
||||
error(`invalid setup context property: "${key}"`, m.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (propsTypeDecl) {
|
||||
extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
|
||||
}
|
||||
if (emitTypeDecl) {
|
||||
extractRuntimeEmits(emitTypeDecl, typeDeclaredEmits)
|
||||
}
|
||||
|
||||
// 5. check useOptions args to make sure it doesn't reference setup scope
|
||||
// variables
|
||||
if (optionsArg) {
|
||||
walkIdentifiers(optionsArg, id => {
|
||||
if (setupBindings[id.name]) {
|
||||
error(
|
||||
`\`${DEFINE_OPTIONS}()\` in <script setup> cannot reference locally ` +
|
||||
`declared variables because it will be hoisted outside of the ` +
|
||||
`setup() function. If your component options requires initialization ` +
|
||||
`in the module scope, use a separate normal <script> to export ` +
|
||||
`the options instead.`,
|
||||
id
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
|
||||
checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS)
|
||||
|
||||
// 6. remove non-script content
|
||||
if (script) {
|
||||
@ -766,31 +780,17 @@ export function compileScript(
|
||||
s.remove(endOffset, source.length)
|
||||
}
|
||||
|
||||
// 7. finalize setup argument signature.
|
||||
let args = optionsExp ? `__props, ${optionsExp}` : `__props`
|
||||
if (optionsExp && optionsType) {
|
||||
if (slotsType === 'Slots') {
|
||||
helperImports.add('Slots')
|
||||
}
|
||||
args += `: {
|
||||
props: ${propsType},
|
||||
emit: ${emitType},
|
||||
slots: ${slotsType},
|
||||
attrs: ${attrsType}
|
||||
}`
|
||||
}
|
||||
|
||||
// 8. analyze binding metadata
|
||||
// 7. analyze binding metadata
|
||||
if (scriptAst) {
|
||||
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
|
||||
}
|
||||
if (optionsType) {
|
||||
for (const key in typeDeclaredProps) {
|
||||
if (propsRuntimeDecl) {
|
||||
for (const key of getObjectOrArrayExpressionKeys(propsRuntimeDecl)) {
|
||||
bindingMetadata[key] = BindingTypes.PROPS
|
||||
}
|
||||
}
|
||||
if (optionsArg) {
|
||||
Object.assign(bindingMetadata, analyzeBindingsFromOptions(optionsArg))
|
||||
for (const key in typeDeclaredProps) {
|
||||
bindingMetadata[key] = BindingTypes.PROPS
|
||||
}
|
||||
for (const [key, { isType, source }] of Object.entries(userImports)) {
|
||||
if (isType) continue
|
||||
@ -803,7 +803,7 @@ export function compileScript(
|
||||
bindingMetadata[key] = setupBindings[key]
|
||||
}
|
||||
|
||||
// 9. inject `useCssVars` calls
|
||||
// 8. inject `useCssVars` calls
|
||||
if (cssVars.length) {
|
||||
helperImports.add(CSS_VARS_HELPER)
|
||||
helperImports.add('unref')
|
||||
@ -818,6 +818,35 @@ export function compileScript(
|
||||
)
|
||||
}
|
||||
|
||||
// 9. finalize setup() argument signature
|
||||
let args = `__props`
|
||||
if (propsTypeDecl) {
|
||||
args += `: ${scriptSetup.content.slice(
|
||||
propsTypeDecl.start!,
|
||||
propsTypeDecl.end!
|
||||
)}`
|
||||
}
|
||||
// inject user assignment of props
|
||||
// we use a default __props so that template expressions referencing props
|
||||
// can use it directly
|
||||
if (propsIdentifier) {
|
||||
s.prependRight(startOffset, `\nconst ${propsIdentifier} = __props`)
|
||||
}
|
||||
if (emitIdentifier) {
|
||||
args +=
|
||||
emitIdentifier === `emit` ? `, { emit }` : `, { emit: ${emitIdentifier} }`
|
||||
if (emitTypeDecl) {
|
||||
args += `: {
|
||||
emit: (${scriptSetup.content.slice(
|
||||
emitTypeDecl.start!,
|
||||
emitTypeDecl.end!
|
||||
)}),
|
||||
slots: any,
|
||||
attrs: any
|
||||
}`
|
||||
}
|
||||
}
|
||||
|
||||
// 10. generate return statement
|
||||
let returned
|
||||
if (options.inlineTemplate) {
|
||||
@ -896,13 +925,19 @@ export function compileScript(
|
||||
if (hasInlinedSsrRenderFn) {
|
||||
runtimeOptions += `\n __ssrInlineRender: true,`
|
||||
}
|
||||
if (optionsArg) {
|
||||
runtimeOptions += `\n ${scriptSetup.content
|
||||
.slice(optionsArg.start! + 1, optionsArg.end! - 1)
|
||||
if (propsRuntimeDecl) {
|
||||
runtimeOptions += `\n props: ${scriptSetup.content
|
||||
.slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
|
||||
.trim()},`
|
||||
} else if (optionsType) {
|
||||
runtimeOptions +=
|
||||
genRuntimeProps(typeDeclaredProps) + genRuntimeEmits(typeDeclaredEmits)
|
||||
} else if (propsTypeDecl) {
|
||||
runtimeOptions += genRuntimeProps(typeDeclaredProps)
|
||||
}
|
||||
if (emitRuntimeDecl) {
|
||||
runtimeOptions += `\n emits: ${scriptSetup.content
|
||||
.slice(emitRuntimeDecl.start!, emitRuntimeDecl.end!)
|
||||
.trim()},`
|
||||
} else if (emitTypeDecl) {
|
||||
runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
|
||||
}
|
||||
if (isTS) {
|
||||
// for TS, make sure the exported type is still valid type with
|
||||
@ -975,13 +1010,16 @@ function walkDeclaration(
|
||||
const isConst = node.kind === 'const'
|
||||
// export const foo = ...
|
||||
for (const { id, init } of node.declarations) {
|
||||
const isUseOptionsCall = !!(isConst && isCallOf(init, DEFINE_OPTIONS))
|
||||
const isDefineCall = !!(
|
||||
isConst &&
|
||||
(isCallOf(init, DEFINE_PROPS) || isCallOf(init, DEFINE_EMIT))
|
||||
)
|
||||
if (id.type === 'Identifier') {
|
||||
let bindingType
|
||||
if (
|
||||
// if a declaration is a const literal, we can mark it so that
|
||||
// the generated render fn code doesn't need to unref() it
|
||||
isUseOptionsCall ||
|
||||
isDefineCall ||
|
||||
(isConst &&
|
||||
canNeverBeRef(init!, userImportAlias['reactive'] || 'reactive'))
|
||||
) {
|
||||
@ -997,9 +1035,9 @@ function walkDeclaration(
|
||||
}
|
||||
bindings[id.name] = bindingType
|
||||
} else if (id.type === 'ObjectPattern') {
|
||||
walkObjectPattern(id, bindings, isConst, isUseOptionsCall)
|
||||
walkObjectPattern(id, bindings, isConst, isDefineCall)
|
||||
} else if (id.type === 'ArrayPattern') {
|
||||
walkArrayPattern(id, bindings, isConst, isUseOptionsCall)
|
||||
walkArrayPattern(id, bindings, isConst, isDefineCall)
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
@ -1016,7 +1054,7 @@ function walkObjectPattern(
|
||||
node: ObjectPattern,
|
||||
bindings: Record<string, BindingTypes>,
|
||||
isConst: boolean,
|
||||
isUseOptionsCall = false
|
||||
isDefineCall = false
|
||||
) {
|
||||
for (const p of node.properties) {
|
||||
if (p.type === 'ObjectProperty') {
|
||||
@ -1024,13 +1062,13 @@ function walkObjectPattern(
|
||||
if (p.key.type === 'Identifier') {
|
||||
if (p.key === p.value) {
|
||||
// const { x } = ...
|
||||
bindings[p.key.name] = isUseOptionsCall
|
||||
bindings[p.key.name] = isDefineCall
|
||||
? BindingTypes.SETUP_CONST
|
||||
: isConst
|
||||
? BindingTypes.SETUP_MAYBE_REF
|
||||
: BindingTypes.SETUP_LET
|
||||
} else {
|
||||
walkPattern(p.value, bindings, isConst, isUseOptionsCall)
|
||||
walkPattern(p.value, bindings, isConst, isDefineCall)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -1047,10 +1085,10 @@ function walkArrayPattern(
|
||||
node: ArrayPattern,
|
||||
bindings: Record<string, BindingTypes>,
|
||||
isConst: boolean,
|
||||
isUseOptionsCall = false
|
||||
isDefineCall = false
|
||||
) {
|
||||
for (const e of node.elements) {
|
||||
e && walkPattern(e, bindings, isConst, isUseOptionsCall)
|
||||
e && walkPattern(e, bindings, isConst, isDefineCall)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1058,10 +1096,10 @@ function walkPattern(
|
||||
node: Node,
|
||||
bindings: Record<string, BindingTypes>,
|
||||
isConst: boolean,
|
||||
isUseOptionsCall = false
|
||||
isDefineCall = false
|
||||
) {
|
||||
if (node.type === 'Identifier') {
|
||||
bindings[node.name] = isUseOptionsCall
|
||||
bindings[node.name] = isDefineCall
|
||||
? BindingTypes.SETUP_CONST
|
||||
: isConst
|
||||
? BindingTypes.SETUP_MAYBE_REF
|
||||
@ -1077,7 +1115,7 @@ function walkPattern(
|
||||
walkArrayPattern(node, bindings, isConst)
|
||||
} else if (node.type === 'AssignmentPattern') {
|
||||
if (node.left.type === 'Identifier') {
|
||||
bindings[node.left.name] = isUseOptionsCall
|
||||
bindings[node.left.name] = isDefineCall
|
||||
? BindingTypes.SETUP_CONST
|
||||
: isConst
|
||||
? BindingTypes.SETUP_MAYBE_REF
|
||||
@ -1418,43 +1456,6 @@ function isFunction(node: Node): node is FunctionNode {
|
||||
return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
|
||||
}
|
||||
|
||||
function getObjectExpressionKeys(node: ObjectExpression): string[] {
|
||||
const keys = []
|
||||
for (const prop of node.properties) {
|
||||
if (
|
||||
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
|
||||
!prop.computed
|
||||
) {
|
||||
if (prop.key.type === 'Identifier') {
|
||||
keys.push(prop.key.name)
|
||||
} else if (prop.key.type === 'StringLiteral') {
|
||||
keys.push(prop.key.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
function getArrayExpressionKeys(node: ArrayExpression): string[] {
|
||||
const keys = []
|
||||
for (const element of node.elements) {
|
||||
if (element && element.type === 'StringLiteral') {
|
||||
keys.push(element.value)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
function getObjectOrArrayExpressionKeys(property: ObjectProperty): string[] {
|
||||
if (property.value.type === 'ArrayExpression') {
|
||||
return getArrayExpressionKeys(property.value)
|
||||
}
|
||||
if (property.value.type === 'ObjectExpression') {
|
||||
return getObjectExpressionKeys(property.value)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function isCallOf(node: Node | null, name: string): node is CallExpression {
|
||||
return !!(
|
||||
node &&
|
||||
@ -1542,7 +1543,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
|
||||
if (property.key.name === 'props') {
|
||||
// props: ['foo']
|
||||
// props: { foo: ... }
|
||||
for (const key of getObjectOrArrayExpressionKeys(property)) {
|
||||
for (const key of getObjectOrArrayExpressionKeys(property.value)) {
|
||||
bindings[key] = BindingTypes.PROPS
|
||||
}
|
||||
}
|
||||
@ -1551,7 +1552,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
|
||||
else if (property.key.name === 'inject') {
|
||||
// inject: ['foo']
|
||||
// inject: { foo: {} }
|
||||
for (const key of getObjectOrArrayExpressionKeys(property)) {
|
||||
for (const key of getObjectOrArrayExpressionKeys(property.value)) {
|
||||
bindings[key] = BindingTypes.OPTIONS
|
||||
}
|
||||
}
|
||||
@ -1599,3 +1600,40 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
function getObjectExpressionKeys(node: ObjectExpression): string[] {
|
||||
const keys = []
|
||||
for (const prop of node.properties) {
|
||||
if (
|
||||
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
|
||||
!prop.computed
|
||||
) {
|
||||
if (prop.key.type === 'Identifier') {
|
||||
keys.push(prop.key.name)
|
||||
} else if (prop.key.type === 'StringLiteral') {
|
||||
keys.push(prop.key.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
function getArrayExpressionKeys(node: ArrayExpression): string[] {
|
||||
const keys = []
|
||||
for (const element of node.elements) {
|
||||
if (element && element.type === 'StringLiteral') {
|
||||
keys.push(element.value)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
function getObjectOrArrayExpressionKeys(value: Node): string[] {
|
||||
if (value.type === 'ArrayExpression') {
|
||||
return getArrayExpressionKeys(value)
|
||||
}
|
||||
if (value.type === 'ObjectExpression') {
|
||||
return getObjectExpressionKeys(value)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
@ -1,91 +0,0 @@
|
||||
import { EmitFn, EmitsOptions } from './componentEmits'
|
||||
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
|
||||
import { Slots } from './componentSlots'
|
||||
import { Directive } from './directives'
|
||||
import { warn } from './warning'
|
||||
|
||||
interface DefaultContext {
|
||||
props: {}
|
||||
attrs: Record<string, unknown>
|
||||
emit: (...args: any[]) => void
|
||||
slots: Slots
|
||||
}
|
||||
|
||||
interface InferredContext<P, E> {
|
||||
props: Readonly<P>
|
||||
attrs: Record<string, unknown>
|
||||
emit: EmitFn<E>
|
||||
slots: Slots
|
||||
}
|
||||
|
||||
type InferContext<T extends Partial<DefaultContext>, P, E> = {
|
||||
[K in keyof DefaultContext]: T[K] extends {} ? T[K] : InferredContext<P, E>[K]
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a subset of full options that are still useful in the context of
|
||||
* <script setup>. Technically, other options can be used too, but are
|
||||
* discouraged - if using TypeScript, we nudge users away from doing so by
|
||||
* disallowing them in types.
|
||||
*/
|
||||
interface Options<E extends EmitsOptions, EE extends string> {
|
||||
emits?: E | EE[]
|
||||
name?: string
|
||||
inhertiAttrs?: boolean
|
||||
directives?: Record<string, Directive>
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile-time-only helper used for declaring options and retrieving props
|
||||
* and the setup context inside `<script setup>`.
|
||||
* This is stripped away in the compiled code and should never be actually
|
||||
* called at runtime.
|
||||
*/
|
||||
// overload 1: no props
|
||||
export function defineOptions<
|
||||
T extends Partial<DefaultContext> = {},
|
||||
E extends EmitsOptions = EmitsOptions,
|
||||
EE extends string = string
|
||||
>(
|
||||
options?: Options<E, EE> & {
|
||||
props?: undefined
|
||||
}
|
||||
): InferContext<T, {}, E>
|
||||
|
||||
// overload 2: object props
|
||||
export function defineOptions<
|
||||
T extends Partial<DefaultContext> = {},
|
||||
E extends EmitsOptions = EmitsOptions,
|
||||
EE extends string = string,
|
||||
PP extends string = string,
|
||||
P = Readonly<{ [key in PP]?: any }>
|
||||
>(
|
||||
options?: Options<E, EE> & {
|
||||
props?: PP[]
|
||||
}
|
||||
): InferContext<T, P, E>
|
||||
|
||||
// overload 3: object props
|
||||
export function defineOptions<
|
||||
T extends Partial<DefaultContext> = {},
|
||||
E extends EmitsOptions = EmitsOptions,
|
||||
EE extends string = string,
|
||||
PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
|
||||
P = ExtractPropTypes<PP>
|
||||
>(
|
||||
options?: Options<E, EE> & {
|
||||
props?: PP
|
||||
}
|
||||
): InferContext<T, P, E>
|
||||
|
||||
// implementation
|
||||
export function defineOptions() {
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
`defineContext() is a compiler-hint helper that is only usable inside ` +
|
||||
`<script setup> of a single file component. It will be compiled away ` +
|
||||
`and should not be used in final distributed code.`
|
||||
)
|
||||
}
|
||||
return 0 as any
|
||||
}
|
60
packages/runtime-core/src/apiSetupHelpers.ts
Normal file
60
packages/runtime-core/src/apiSetupHelpers.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { shallowReadonly } from '@vue/reactivity'
|
||||
import { getCurrentInstance, SetupContext } from './component'
|
||||
import { EmitFn, EmitsOptions } from './componentEmits'
|
||||
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
|
||||
import { warn } from './warning'
|
||||
|
||||
/**
|
||||
* Compile-time-only helper used for declaring props inside `<script setup>`.
|
||||
* This is stripped away in the compiled code and should never be actually
|
||||
* called at runtime.
|
||||
*/
|
||||
// overload 1: string props
|
||||
export function defineProps<
|
||||
TypeProps = undefined,
|
||||
PropNames extends string = string,
|
||||
InferredProps = { [key in PropNames]?: any }
|
||||
>(
|
||||
props?: PropNames[]
|
||||
): Readonly<TypeProps extends undefined ? InferredProps : TypeProps>
|
||||
// overload 2: object props
|
||||
export function defineProps<
|
||||
TypeProps = undefined,
|
||||
PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
|
||||
InferredProps = ExtractPropTypes<PP>
|
||||
>(props?: PP): Readonly<TypeProps extends undefined ? InferredProps : TypeProps>
|
||||
// implementation
|
||||
export function defineProps(props?: any) {
|
||||
if (__DEV__ && props) {
|
||||
warn(
|
||||
`defineProps() is a compiler-hint helper that is only usable inside ` +
|
||||
`<script setup> of a single file component. Its arguments should be ` +
|
||||
`compiled away and passing it at runtime has no effect.`
|
||||
)
|
||||
}
|
||||
return __DEV__
|
||||
? shallowReadonly(getCurrentInstance()!.props)
|
||||
: getCurrentInstance()!.props
|
||||
}
|
||||
|
||||
export function defineEmit<
|
||||
TypeEmit = undefined,
|
||||
E extends EmitsOptions = EmitsOptions,
|
||||
EE extends string = string,
|
||||
InferredEmit = EmitFn<E>
|
||||
>(emitOptions?: E | EE[]): TypeEmit extends undefined ? InferredEmit : TypeEmit
|
||||
// implementation
|
||||
export function defineEmit(emitOptions?: any) {
|
||||
if (__DEV__ && emitOptions) {
|
||||
warn(
|
||||
`defineEmit() is a compiler-hint helper that is only usable inside ` +
|
||||
`<script setup> of a single file component. Its arguments should be ` +
|
||||
`compiled away and passing it at runtime has no effect.`
|
||||
)
|
||||
}
|
||||
return getCurrentInstance()!.emit
|
||||
}
|
||||
|
||||
export function useContext(): SetupContext {
|
||||
return getCurrentInstance()!.setupContext!
|
||||
}
|
@ -105,7 +105,7 @@ export interface ComponentInternalOptions {
|
||||
export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
|
||||
extends ComponentInternalOptions {
|
||||
// use of any here is intentional so it can be a valid JSX Element constructor
|
||||
(props: P, ctx: Omit<SetupContext<E, P>, 'expose'>): any
|
||||
(props: P, ctx: Omit<SetupContext<E>, 'expose'>): any
|
||||
props?: ComponentPropsOptions<P>
|
||||
emits?: E | (keyof E)[]
|
||||
inheritAttrs?: boolean
|
||||
@ -167,8 +167,7 @@ export const enum LifecycleHooks {
|
||||
ERROR_CAPTURED = 'ec'
|
||||
}
|
||||
|
||||
export interface SetupContext<E = EmitsOptions, P = Data> {
|
||||
props: P
|
||||
export interface SetupContext<E = EmitsOptions> {
|
||||
attrs: Data
|
||||
slots: Slots
|
||||
emit: EmitFn<E>
|
||||
@ -775,7 +774,6 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
|
||||
})
|
||||
} else {
|
||||
return {
|
||||
props: instance.props,
|
||||
attrs: instance.attrs,
|
||||
slots: instance.slots,
|
||||
emit: instance.emit,
|
||||
|
@ -98,7 +98,7 @@ export interface ComponentOptionsBase<
|
||||
setup?: (
|
||||
this: void,
|
||||
props: Props,
|
||||
ctx: SetupContext<E, Props>
|
||||
ctx: SetupContext<E>
|
||||
) => Promise<RawBindings> | RawBindings | RenderFunction | void
|
||||
name?: string
|
||||
template?: string | object // can be a direct DOM node
|
||||
|
@ -95,7 +95,6 @@ export function renderComponentRoot(
|
||||
props,
|
||||
__DEV__
|
||||
? {
|
||||
props,
|
||||
get attrs() {
|
||||
markAttrsAccessed()
|
||||
return attrs
|
||||
@ -103,7 +102,7 @@ export function renderComponentRoot(
|
||||
slots,
|
||||
emit
|
||||
}
|
||||
: { props, attrs, slots, emit }
|
||||
: { attrs, slots, emit }
|
||||
)
|
||||
: render(props, null as any /* we know it doesn't need it */)
|
||||
)
|
||||
|
@ -43,7 +43,7 @@ export { provide, inject } from './apiInject'
|
||||
export { nextTick } from './scheduler'
|
||||
export { defineComponent } from './apiDefineComponent'
|
||||
export { defineAsyncComponent } from './apiAsyncComponent'
|
||||
export { defineOptions } from './apiDefineOptions'
|
||||
export { defineProps, defineEmit, useContext } from './apiSetupHelpers'
|
||||
|
||||
// Advanced API ----------------------------------------------------------------
|
||||
|
||||
|
@ -1,96 +0,0 @@
|
||||
import { expectType, defineOptions, Slots, describe } from './index'
|
||||
|
||||
describe('no args', () => {
|
||||
const { props, attrs, emit, slots } = defineOptions()
|
||||
expectType<{}>(props)
|
||||
expectType<Record<string, unknown>>(attrs)
|
||||
expectType<(...args: any[]) => void>(emit)
|
||||
expectType<Slots>(slots)
|
||||
|
||||
// @ts-expect-error
|
||||
props.foo
|
||||
// should be able to emit anything
|
||||
emit('foo')
|
||||
emit('bar')
|
||||
})
|
||||
|
||||
describe('with type arg', () => {
|
||||
const { props, attrs, emit, slots } = defineOptions<{
|
||||
props: {
|
||||
foo: string
|
||||
}
|
||||
emit: (e: 'change') => void
|
||||
}>()
|
||||
|
||||
// explicitly declared type should be refined
|
||||
expectType<string>(props.foo)
|
||||
// @ts-expect-error
|
||||
props.bar
|
||||
|
||||
emit('change')
|
||||
// @ts-expect-error
|
||||
emit()
|
||||
// @ts-expect-error
|
||||
emit('bar')
|
||||
|
||||
// non explicitly declared type should fallback to default type
|
||||
expectType<Record<string, unknown>>(attrs)
|
||||
expectType<Slots>(slots)
|
||||
})
|
||||
|
||||
// with runtime arg
|
||||
describe('with runtime arg (array syntax)', () => {
|
||||
const { props, emit } = defineOptions({
|
||||
props: ['foo', 'bar'],
|
||||
emits: ['foo', 'bar']
|
||||
})
|
||||
|
||||
expectType<{
|
||||
foo?: any
|
||||
bar?: any
|
||||
}>(props)
|
||||
// @ts-expect-error
|
||||
props.baz
|
||||
|
||||
emit('foo')
|
||||
emit('bar', 123)
|
||||
// @ts-expect-error
|
||||
emit('baz')
|
||||
})
|
||||
|
||||
describe('with runtime arg (object syntax)', () => {
|
||||
const { props, emit } = defineOptions({
|
||||
props: {
|
||||
foo: String,
|
||||
bar: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
baz: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: {
|
||||
foo: () => {},
|
||||
bar: null
|
||||
}
|
||||
})
|
||||
|
||||
expectType<{
|
||||
foo?: string
|
||||
bar: number
|
||||
baz: unknown[]
|
||||
}>(props)
|
||||
|
||||
props.foo && props.foo + 'bar'
|
||||
props.bar + 1
|
||||
// @ts-expect-error should be readonly
|
||||
props.bar++
|
||||
props.baz.push(1)
|
||||
|
||||
emit('foo')
|
||||
emit('bar')
|
||||
// @ts-expect-error
|
||||
emit('baz')
|
||||
})
|
89
test-dts/setupHelpers.test-d.ts
Normal file
89
test-dts/setupHelpers.test-d.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import {
|
||||
expectType,
|
||||
defineProps,
|
||||
defineEmit,
|
||||
useContext,
|
||||
Slots,
|
||||
describe
|
||||
} from './index'
|
||||
|
||||
describe('defineProps w/ type declaration', () => {
|
||||
// type declaration
|
||||
const props = defineProps<{
|
||||
foo: string
|
||||
}>()
|
||||
// explicitly declared type should be refined
|
||||
expectType<string>(props.foo)
|
||||
// @ts-expect-error
|
||||
props.bar
|
||||
})
|
||||
|
||||
describe('defineProps w/ runtime declaration', () => {
|
||||
// runtime declaration
|
||||
const props = defineProps({
|
||||
foo: String,
|
||||
bar: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
baz: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
expectType<{
|
||||
foo?: string
|
||||
bar: number
|
||||
baz: unknown[]
|
||||
}>(props)
|
||||
|
||||
props.foo && props.foo + 'bar'
|
||||
props.bar + 1
|
||||
// @ts-expect-error should be readonly
|
||||
props.bar++
|
||||
props.baz.push(1)
|
||||
|
||||
const props2 = defineProps(['foo', 'bar'])
|
||||
props2.foo + props2.bar
|
||||
// @ts-expect-error
|
||||
props2.baz
|
||||
})
|
||||
|
||||
describe('defineEmit w/ type declaration', () => {
|
||||
const emit = defineEmit<(e: 'change') => void>()
|
||||
emit('change')
|
||||
// @ts-expect-error
|
||||
emit()
|
||||
// @ts-expect-error
|
||||
emit('bar')
|
||||
})
|
||||
|
||||
describe('defineEmit w/ runtime declaration', () => {
|
||||
const emit = defineEmit({
|
||||
foo: () => {},
|
||||
bar: null
|
||||
})
|
||||
emit('foo')
|
||||
emit('bar', 123)
|
||||
// @ts-expect-error
|
||||
emit('baz')
|
||||
|
||||
const emit2 = defineEmit(['foo', 'bar'])
|
||||
emit2('foo')
|
||||
emit2('bar', 123)
|
||||
// @ts-expect-error
|
||||
emit2('baz')
|
||||
})
|
||||
|
||||
describe('useContext', () => {
|
||||
const { attrs, emit, slots } = useContext()
|
||||
expectType<Record<string, unknown>>(attrs)
|
||||
expectType<(...args: any[]) => void>(emit)
|
||||
expectType<Slots>(slots)
|
||||
|
||||
// @ts-expect-error
|
||||
props.foo
|
||||
// should be able to emit anything
|
||||
emit('foo')
|
||||
emit('bar')
|
||||
})
|
Loading…
Reference in New Issue
Block a user