wip: defineContext -> useOptions

This commit is contained in:
Evan You 2020-11-12 22:51:40 -05:00
parent 292a657861
commit 001f8ce993
5 changed files with 131 additions and 128 deletions

View File

@ -56,25 +56,7 @@ return { color }
}" }"
`; `;
exports[`SFC compile <script setup> defineContext() 1`] = ` exports[`SFC compile <script setup> errors should allow useOptions() referencing imported binding 1`] = `
"export default {
props: {
foo: String
},
emit: ['a', 'b'],
setup(__props, { props, emit }) {
const bar = 1
return { props, emit, bar }
}
}"
`;
exports[`SFC compile <script setup> errors should allow defineContext() referencing imported binding 1`] = `
"import { bar } from './bar' "import { bar } from './bar'
export default { export default {
@ -93,7 +75,7 @@ return { bar }
}" }"
`; `;
exports[`SFC compile <script setup> errors should allow defineContext() referencing scope var 1`] = ` exports[`SFC compile <script setup> errors should allow useOptions() referencing scope var 1`] = `
"export default { "export default {
props: { props: {
foo: { foo: {
@ -393,7 +375,40 @@ return { a, b, c, d, x }
}" }"
`; `;
exports[`SFC compile <script setup> with TypeScript defineContext w/ runtime options 1`] = ` exports[`SFC compile <script setup> useOptions() 1`] = `
"export default {
props: {
foo: String
},
emit: ['a', 'b'],
setup(__props, { props, emit }) {
const bar = 1
return { props, emit, bar }
}
}"
`;
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 { }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript useOptions w/ runtime options 1`] = `
"import { defineComponent } from 'vue' "import { defineComponent } from 'vue'
@ -410,7 +425,7 @@ return { props, emit }
})" })"
`; `;
exports[`SFC compile <script setup> with TypeScript defineContext w/ type / extract emits (union) 1`] = ` exports[`SFC compile <script setup> with TypeScript useOptions w/ type / extract emits (union) 1`] = `
"import { Slots, defineComponent } from 'vue' "import { Slots, defineComponent } from 'vue'
@ -431,7 +446,7 @@ return { emit }
})" })"
`; `;
exports[`SFC compile <script setup> with TypeScript defineContext w/ type / extract emits 1`] = ` exports[`SFC compile <script setup> with TypeScript useOptions w/ type / extract emits 1`] = `
"import { Slots, defineComponent } from 'vue' "import { Slots, defineComponent } from 'vue'
@ -452,7 +467,7 @@ return { emit }
})" })"
`; `;
exports[`SFC compile <script setup> with TypeScript defineContext w/ type / extract props 1`] = ` exports[`SFC compile <script setup> with TypeScript useOptions w/ type / extract props 1`] = `
"import { defineComponent } from 'vue' "import { defineComponent } from 'vue'
interface Test {} interface Test {}
@ -488,21 +503,6 @@ export default defineComponent({
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 { } return { }
} }

View File

@ -36,11 +36,11 @@ describe('SFC compile <script setup>', () => {
expect(content).toMatch('return { a, b, c, d, x }') expect(content).toMatch('return { a, b, c, d, x }')
}) })
test('defineContext()', () => { test('useOptions()', () => {
const { content, bindings } = compile(` const { content, bindings } = compile(`
<script setup> <script setup>
import { defineContext } from 'vue' import { useOptions } from 'vue'
const { props, emit } = defineContext({ const { props, emit } = useOptions({
props: { props: {
foo: String foo: String
}, },
@ -60,8 +60,8 @@ const bar = 1
emit: 'const' emit: 'const'
}) })
// should remove defineContext import and call // should remove useOptions import and call
expect(content).not.toMatch('defineContext') expect(content).not.toMatch('useOptions')
// should generate correct setup signature // should generate correct setup signature
expect(content).toMatch(`setup(__props, { props, emit }) {`) expect(content).toMatch(`setup(__props, { props, emit }) {`)
// should include context options in default export // should include context options in default export
@ -143,7 +143,7 @@ const bar = 1
const { content } = compile( const { content } = compile(
` `
<script setup> <script setup>
import { ref, defineContext } from 'vue' import { ref, useOptions } from 'vue'
import Foo from './Foo.vue' import Foo from './Foo.vue'
import other from './util' import other from './util'
const count = ref(0) const count = ref(0)
@ -183,11 +183,11 @@ const bar = 1
assertCode(content) assertCode(content)
}) })
test('defineContext w/ runtime options', () => { test('useOptions w/ runtime options', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { defineContext } from 'vue' import { useOptions } from 'vue'
const { props, emit } = defineContext({ const { props, emit } = useOptions({
props: { foo: String }, props: { foo: String },
emits: ['a', 'b'] emits: ['a', 'b']
}) })
@ -200,15 +200,15 @@ const { props, emit } = defineContext({
setup(__props, { props, emit }) {`) setup(__props, { props, emit }) {`)
}) })
test('defineContext w/ type / extract props', () => { test('useOptions w/ type / extract props', () => {
const { content, bindings } = compile(` const { content, bindings } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { defineContext } from 'vue' import { useOptions } from 'vue'
interface Test {} interface Test {}
type Alias = number[] type Alias = number[]
defineContext<{ useOptions<{
props: { props: {
string: string string: string
number: number number: number
@ -288,11 +288,11 @@ const { props, emit } = defineContext({
}) })
}) })
test('defineContext w/ type / extract emits', () => { test('useOptions w/ type / extract emits', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { defineContext } from 'vue' import { useOptions } from 'vue'
const { emit } = defineContext<{ const { emit } = useOptions<{
emit: (e: 'foo' | 'bar') => void emit: (e: 'foo' | 'bar') => void
}>() }>()
</script> </script>
@ -302,11 +302,11 @@ const { props, emit } = defineContext({
expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`) expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
}) })
test('defineContext w/ type / extract emits (union)', () => { test('useOptions w/ type / extract emits (union)', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { defineContext } from 'vue' import { useOptions } from 'vue'
const { emit } = defineContext<{ const { emit } = useOptions<{
emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void) emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)
}>() }>()
</script> </script>
@ -633,21 +633,21 @@ const { props, emit } = defineContext({
).toThrow(`ref: statements can only contain assignment expressions`) ).toThrow(`ref: statements can only contain assignment expressions`)
}) })
test('defineContext() w/ both type and non-type args', () => { test('useOptions() w/ both type and non-type args', () => {
expect(() => { expect(() => {
compile(`<script setup lang="ts"> compile(`<script setup lang="ts">
import { defineContext } from 'vue' import { useOptions } from 'vue'
defineContext<{}>({}) useOptions<{}>({})
</script>`) </script>`)
}).toThrow(`cannot accept both type and non-type arguments`) }).toThrow(`cannot accept both type and non-type arguments`)
}) })
test('defineContext() referencing local var', () => { test('useOptions() referencing local var', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
import { defineContext } from 'vue' import { useOptions } from 'vue'
const bar = 1 const bar = 1
defineContext({ useOptions({
props: { props: {
foo: { foo: {
default: () => bar default: () => bar
@ -658,24 +658,24 @@ const { props, emit } = defineContext({
).toThrow(`cannot reference locally declared variables`) ).toThrow(`cannot reference locally declared variables`)
}) })
test('defineContext() referencing ref declarations', () => { test('useOptions() referencing ref declarations', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
import { defineContext } from 'vue' import { useOptions } from 'vue'
ref: bar = 1 ref: bar = 1
defineContext({ useOptions({
props: { bar } props: { bar }
}) })
</script>`) </script>`)
).toThrow(`cannot reference locally declared variables`) ).toThrow(`cannot reference locally declared variables`)
}) })
test('should allow defineContext() referencing scope var', () => { test('should allow useOptions() referencing scope var', () => {
assertCode( assertCode(
compile(`<script setup> compile(`<script setup>
import { defineContext } from 'vue' import { useOptions } from 'vue'
const bar = 1 const bar = 1
defineContext({ useOptions({
props: { props: {
foo: { foo: {
default: bar => bar + 1 default: bar => bar + 1
@ -686,12 +686,12 @@ const { props, emit } = defineContext({
) )
}) })
test('should allow defineContext() referencing imported binding', () => { test('should allow useOptions() referencing imported binding', () => {
assertCode( assertCode(
compile(`<script setup> compile(`<script setup>
import { defineContext } from 'vue' import { useOptions } from 'vue'
import { bar } from './bar' import { bar } from './bar'
defineContext({ useOptions({
props: { props: {
foo: { foo: {
default: () => bar default: () => bar
@ -901,8 +901,8 @@ 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>
import { defineContext } from 'vue' import { useOptions } from 'vue'
defineContext({ useOptions({
props: { props: {
foo: String, foo: String,
} }

View File

@ -28,7 +28,7 @@ import { genCssVarsCode, injectCssVarsCalls } from './genCssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate' import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
import { BindingTypes } from 'packages/compiler-core/src/options' import { BindingTypes } from 'packages/compiler-core/src/options'
const CTX_FN_NAME = 'defineContext' const USE_OPTIONS = 'useOptions'
export interface SFCScriptCompileOptions { export interface SFCScriptCompileOptions {
/** /**
@ -143,10 +143,10 @@ 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 hasOptionsCall = false
let setupContextExp: string | undefined let optionsExp: string | undefined
let setupContextArg: ObjectExpression | undefined let optionsArg: ObjectExpression | undefined
let setupContextType: TSTypeLiteral | undefined let optionsType: TSTypeLiteral | undefined
let hasAwait = false let hasAwait = false
const s = new MagicString(source) const s = new MagicString(source)
@ -183,39 +183,39 @@ export function compileScript(
) )
} }
function processContextCall(node: Node): boolean { function processUseOptions(node: Node): boolean {
if ( if (
node.type === 'CallExpression' && node.type === 'CallExpression' &&
node.callee.type === 'Identifier' && node.callee.type === 'Identifier' &&
node.callee.name === CTX_FN_NAME node.callee.name === USE_OPTIONS
) { ) {
if (hasContextCall) { if (hasOptionsCall) {
error('duplicate defineContext() call', node) error(`duplicate ${USE_OPTIONS}() call`, node)
} }
hasContextCall = true hasOptionsCall = true
const optsArg = node.arguments[0] const optsArg = node.arguments[0]
if (optsArg) { if (optsArg) {
if (optsArg.type === 'ObjectExpression') { if (optsArg.type === 'ObjectExpression') {
setupContextArg = optsArg optionsArg = optsArg
} else { } else {
error(`${CTX_FN_NAME}() argument must be an object literal.`, optsArg) error(`${USE_OPTIONS}() argument must be an object literal.`, optsArg)
} }
} }
// context call has type parameters - infer runtime types from it // context call has type parameters - infer runtime types from it
if (node.typeParameters) { if (node.typeParameters) {
if (setupContextArg) { if (optionsArg) {
error( error(
`${CTX_FN_NAME}() cannot accept both type and non-type arguments ` + `${USE_OPTIONS}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`, `at the same time. Use one or the other.`,
node node
) )
} }
const typeArg = node.typeParameters.params[0] const typeArg = node.typeParameters.params[0]
if (typeArg.type === 'TSTypeLiteral') { if (typeArg.type === 'TSTypeLiteral') {
setupContextType = typeArg optionsType = typeArg
} else { } else {
error( error(
`type argument passed to ${CTX_FN_NAME}() must be a literal type.`, `type argument passed to ${USE_OPTIONS}() must be a literal type.`,
typeArg typeArg
) )
} }
@ -513,7 +513,7 @@ export function compileScript(
specifier.imported.name specifier.imported.name
const source = node.source.value const source = node.source.value
const existing = userImports[local] const existing = userImports[local]
if (source === 'vue' && imported === CTX_FN_NAME) { if (source === 'vue' && imported === USE_OPTIONS) {
removed++ removed++
s.remove( s.remove(
prev ? prev.end! + startOffset : specifier.start! + startOffset, prev ? prev.end! + startOffset : specifier.start! + startOffset,
@ -545,18 +545,15 @@ export function compileScript(
if ( if (
node.type === 'ExpressionStatement' && node.type === 'ExpressionStatement' &&
processContextCall(node.expression) processUseOptions(node.expression)
) { ) {
s.remove(node.start! + startOffset, node.end! + startOffset) 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 (decl.init && processContextCall(decl.init)) { if (decl.init && processUseOptions(decl.init)) {
setupContextExp = scriptSetup.content.slice( optionsExp = scriptSetup.content.slice(decl.id.start!, decl.id.end!)
decl.id.start!,
decl.id.end!
)
if (node.declarations.length === 1) { if (node.declarations.length === 1) {
s.remove(node.start! + startOffset, node.end! + startOffset) s.remove(node.start! + startOffset, node.end! + startOffset)
} else { } else {
@ -649,8 +646,8 @@ export function compileScript(
} }
// 5. extract runtime props/emits code from setup context type // 5. extract runtime props/emits code from setup context type
if (setupContextType) { if (optionsType) {
for (const m of setupContextType.members) { for (const m of optionsType.members) {
if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') { if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
const typeNode = m.typeAnnotation!.typeAnnotation const typeNode = m.typeAnnotation!.typeAnnotation
const typeString = scriptSetup.content.slice( const typeString = scriptSetup.content.slice(
@ -688,13 +685,13 @@ export function compileScript(
} }
} }
// 5. check useSetupContext args to make sure it doesn't reference setup scope // 5. check useOptions args to make sure it doesn't reference setup scope
// variables // variables
if (setupContextArg) { if (optionsArg) {
walkIdentifiers(setupContextArg, id => { walkIdentifiers(optionsArg, id => {
if (setupBindings[id.name]) { if (setupBindings[id.name]) {
error( error(
`\`${CTX_FN_NAME}()\` in <script setup> cannot reference locally ` + `\`${USE_OPTIONS}()\` in <script setup> cannot reference locally ` +
`declared variables because it will be hoisted outside of the ` + `declared variables because it will be hoisted outside of the ` +
`setup() function. If your component options requires initialization ` + `setup() function. If your component options requires initialization ` +
`in the module scope, use a separate normal <script> to export ` + `in the module scope, use a separate normal <script> to export ` +
@ -725,8 +722,8 @@ export function compileScript(
} }
// 7. finalize setup argument signature. // 7. finalize setup argument signature.
let args = setupContextExp ? `__props, ${setupContextExp}` : `` let args = optionsExp ? `__props, ${optionsExp}` : ``
if (setupContextExp && setupContextType) { if (optionsExp && optionsType) {
if (slotsType === 'Slots') { if (slotsType === 'Slots') {
helperImports.add('Slots') helperImports.add('Slots')
} }
@ -761,13 +758,13 @@ export function compileScript(
if (scriptAst) { if (scriptAst) {
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst)) Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
} }
if (setupContextType) { if (optionsType) {
for (const key in typeDeclaredProps) { for (const key in typeDeclaredProps) {
bindingMetadata[key] = BindingTypes.PROPS bindingMetadata[key] = BindingTypes.PROPS
} }
} }
if (setupContextArg) { if (optionsArg) {
Object.assign(bindingMetadata, analyzeBindingsFromOptions(setupContextArg)) Object.assign(bindingMetadata, analyzeBindingsFromOptions(optionsArg))
} }
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')
@ -818,11 +815,11 @@ export function compileScript(
// 12. finalize default export // 12. finalize default export
let runtimeOptions = `` let runtimeOptions = ``
if (setupContextArg) { if (optionsArg) {
runtimeOptions = `\n ${scriptSetup.content runtimeOptions = `\n ${scriptSetup.content
.slice(setupContextArg.start! + 1, setupContextArg.end! - 1) .slice(optionsArg.start! + 1, optionsArg.end! - 1)
.trim()},` .trim()},`
} else if (setupContextType) { } else if (optionsType) {
runtimeOptions = runtimeOptions =
genRuntimeProps(typeDeclaredProps) + genRuntimeEmits(typeDeclaredEmits) genRuntimeProps(typeDeclaredProps) + genRuntimeEmits(typeDeclaredEmits)
} }
@ -896,18 +893,18 @@ function walkDeclaration(
const isConst = node.kind === 'const' const isConst = node.kind === 'const'
// export const foo = ... // export const foo = ...
for (const { id, init } of node.declarations) { for (const { id, init } of node.declarations) {
const isContextCall = !!( const isUseOptionsCall = !!(
isConst && isConst &&
init && init &&
init.type === 'CallExpression' && init.type === 'CallExpression' &&
init.callee.type === 'Identifier' && init.callee.type === 'Identifier' &&
init.callee.name === CTX_FN_NAME init.callee.name === USE_OPTIONS
) )
if (id.type === 'Identifier') { if (id.type === 'Identifier') {
bindings[id.name] = bindings[id.name] =
// if a declaration is a const literal, we can mark it so that // if a declaration is a const literal, we can mark it so that
// the generated render fn code doesn't need to unref() it // the generated render fn code doesn't need to unref() it
isContextCall || isUseOptionsCall ||
(isConst && (isConst &&
init!.type !== 'Identifier' && // const a = b init!.type !== 'Identifier' && // const a = b
init!.type !== 'CallExpression' && // const a = ref() init!.type !== 'CallExpression' && // const a = ref()
@ -915,9 +912,9 @@ function walkDeclaration(
? BindingTypes.CONST ? BindingTypes.CONST
: BindingTypes.SETUP : BindingTypes.SETUP
} else if (id.type === 'ObjectPattern') { } else if (id.type === 'ObjectPattern') {
walkObjectPattern(id, bindings, isConst, isContextCall) walkObjectPattern(id, bindings, isConst, isUseOptionsCall)
} else if (id.type === 'ArrayPattern') { } else if (id.type === 'ArrayPattern') {
walkArrayPattern(id, bindings, isConst, isContextCall) walkArrayPattern(id, bindings, isConst, isUseOptionsCall)
} }
} }
} else if ( } else if (
@ -934,7 +931,7 @@ function walkObjectPattern(
node: ObjectPattern, node: ObjectPattern,
bindings: Record<string, BindingTypes>, bindings: Record<string, BindingTypes>,
isConst: boolean, isConst: boolean,
isContextCall = false isUseOptionsCall = false
) { ) {
for (const p of node.properties) { for (const p of node.properties) {
if (p.type === 'ObjectProperty') { if (p.type === 'ObjectProperty') {
@ -942,11 +939,11 @@ function walkObjectPattern(
if (p.key.type === 'Identifier') { if (p.key.type === 'Identifier') {
if (p.key === p.value) { if (p.key === p.value) {
// const { x } = ... // const { x } = ...
bindings[p.key.name] = isContextCall bindings[p.key.name] = isUseOptionsCall
? BindingTypes.CONST ? BindingTypes.CONST
: BindingTypes.SETUP : BindingTypes.SETUP
} else { } else {
walkPattern(p.value, bindings, isConst, isContextCall) walkPattern(p.value, bindings, isConst, isUseOptionsCall)
} }
} }
} else { } else {
@ -963,10 +960,10 @@ function walkArrayPattern(
node: ArrayPattern, node: ArrayPattern,
bindings: Record<string, BindingTypes>, bindings: Record<string, BindingTypes>,
isConst: boolean, isConst: boolean,
isContextCall = false isUseOptionsCall = false
) { ) {
for (const e of node.elements) { for (const e of node.elements) {
e && walkPattern(e, bindings, isConst, isContextCall) e && walkPattern(e, bindings, isConst, isUseOptionsCall)
} }
} }
@ -974,10 +971,10 @@ function walkPattern(
node: Node, node: Node,
bindings: Record<string, BindingTypes>, bindings: Record<string, BindingTypes>,
isConst: boolean, isConst: boolean,
isContextCall = false isUseOptionsCall = false
) { ) {
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
bindings[node.name] = isContextCall bindings[node.name] = isUseOptionsCall
? BindingTypes.CONST ? BindingTypes.CONST
: BindingTypes.SETUP : BindingTypes.SETUP
} else if (node.type === 'RestElement') { } else if (node.type === 'RestElement') {
@ -991,7 +988,7 @@ function walkPattern(
walkArrayPattern(node, bindings, isConst) walkArrayPattern(node, bindings, isConst)
} else if (node.type === 'AssignmentPattern') { } else if (node.type === 'AssignmentPattern') {
if (node.left.type === 'Identifier') { if (node.left.type === 'Identifier') {
bindings[node.left.name] = isContextCall bindings[node.left.name] = isUseOptionsCall
? BindingTypes.CONST ? BindingTypes.CONST
: BindingTypes.SETUP : BindingTypes.SETUP
} else { } else {

View File

@ -1,5 +1,5 @@
import { Slots } from './componentSlots' import { Slots } from '../componentSlots'
import { warn } from './warning' import { warn } from '../warning'
interface DefaultContext { interface DefaultContext {
props: Record<string, unknown> props: Record<string, unknown>
@ -8,7 +8,13 @@ interface DefaultContext {
slots: Slots slots: Slots
} }
export function defineContext<T extends Partial<DefaultContext> = {}>( /**
* 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.
*/
export function useOptions<T extends Partial<DefaultContext> = {}>(
opts?: any // TODO infer opts?: any // TODO infer
): { [K in keyof DefaultContext]: T[K] extends {} ? T[K] : DefaultContext[K] } { ): { [K in keyof DefaultContext]: T[K] extends {} ? T[K] : DefaultContext[K] } {
if (__DEV__) { if (__DEV__) {

View File

@ -43,7 +43,7 @@ export { provide, inject } from './apiInject'
export { nextTick } from './scheduler' export { nextTick } from './scheduler'
export { defineComponent } from './apiDefineComponent' export { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent' export { defineAsyncComponent } from './apiAsyncComponent'
export { defineContext } from './apiDefineContext' export { useOptions } from './helpers/useOptions'
// Advanced API ---------------------------------------------------------------- // Advanced API ----------------------------------------------------------------