wip: defineContext()

This commit is contained in:
Evan You 2020-11-12 14:10:39 -05:00
parent dc098c7f81
commit 6a9b56ca60
10 changed files with 200 additions and 142 deletions

View File

@ -62,7 +62,7 @@ export type HoistTransform = (
) => void ) => void
export interface BindingMetadata { export interface BindingMetadata {
[key: string]: 'data' | 'props' | 'setup' | 'options' | 'component-import' [key: string]: 'data' | 'props' | 'setup' | 'options' | 'setup-raw'
} }
interface SharedTransformCodegenOptions { interface SharedTransformCodegenOptions {

View File

@ -270,9 +270,9 @@ export function resolveComponentType(
`${context.helperString(UNREF)}(${tagFromSetup})` `${context.helperString(UNREF)}(${tagFromSetup})`
: `$setup[${JSON.stringify(tagFromSetup)}]` : `$setup[${JSON.stringify(tagFromSetup)}]`
} }
const tagFromImport = checkType('component-import') const tagFromImport = checkType('setup-raw')
if (tagFromImport) { if (tagFromImport) {
// imports can be used as-is // raw setup bindings (e.g. imports) can be used as-is
return tagFromImport return tagFromImport
} }
} }

View File

@ -105,7 +105,11 @@ export function processExpression(
// setup inline mode // setup inline mode
if (type === 'setup') { if (type === 'setup') {
return `${context.helperString(UNREF)}(${raw})` return `${context.helperString(UNREF)}(${raw})`
} else if (type === 'component-import') { } else if (type === 'props') {
// use __props which is generated by compileScript so in ts mode
// it gets correct type
return `__props.${raw}`
} else if (type === 'setup-raw') {
return raw return raw
} }
} }

View File

@ -134,13 +134,13 @@ export function compileScript(
source: string source: string
} }
> = Object.create(null) > = Object.create(null)
const setupBindings: Record<string, boolean> = Object.create(null) const setupBindings: Record<string, 'var' | 'const'> = Object.create(null)
const refBindings: Record<string, boolean> = Object.create(null) const refBindings: Record<string, 'var'> = Object.create(null)
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 setupContextExp: string | undefined let setupContextExp: string | undefined
let setupContextArg: Node | undefined let setupContextArg: ObjectExpression | undefined
let setupContextType: TSTypeLiteral | undefined let setupContextType: TSTypeLiteral | undefined
let hasAwait = false let hasAwait = false
@ -222,7 +222,7 @@ export function compileScript(
if (id.name[0] === '$') { if (id.name[0] === '$') {
error(`ref variable identifiers cannot start with $.`, id) error(`ref variable identifiers cannot start with $.`, id)
} }
refBindings[id.name] = setupBindings[id.name] = true refBindings[id.name] = setupBindings[id.name] = 'var'
refIdentifiers.add(id) refIdentifiers.add(id)
} }
@ -513,10 +513,25 @@ export function compileScript(
decl.id.start!, decl.id.start!,
decl.id.end! decl.id.end!
) )
setupContextArg = decl.init.arguments[0] const optsArg = decl.init.arguments[0]
if (optsArg.type === 'ObjectExpression') {
setupContextArg = optsArg
} else {
error(
`${CTX_FN_NAME}() argument must be an object literal.`,
optsArg
)
}
// useSetupContext() has type parameters - infer runtime types from it // useSetupContext() has type parameters - infer runtime types from it
if (decl.init.typeParameters) { 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] const typeArg = decl.init.typeParameters.params[0]
if (typeArg.type === 'TSTypeLiteral') { if (typeArg.type === 'TSTypeLiteral') {
setupContextType = typeArg setupContextType = typeArg
@ -685,7 +700,7 @@ export function compileScript(
// 7. finalize setup argument signature. // 7. finalize setup argument signature.
let args = setupContextExp ? `__props, ${setupContextExp}` : `` let args = setupContextExp ? `__props, ${setupContextExp}` : ``
if (isTS) { if (setupContextExp && setupContextType) {
if (slotsType === 'Slots') { if (slotsType === 'Slots') {
helperImports.add('Slots') helperImports.add('Slots')
} }
@ -695,32 +710,11 @@ export function compileScript(
slots: ${slotsType}, slots: ${slotsType},
attrs: ${attrsType} attrs: ${attrsType}
}` }`
// if (hasExplicitSignature) {
// // inject types to user signature
// args = setupValue as string
// const ss = new MagicString(args)
// if (propsASTNode) {
// // compensate for () wraper offset
// ss.appendRight(propsASTNode.end! - 1, `: ${propsType}`)
// }
// if (setupCtxASTNode) {
// ss.appendRight(setupCtxASTNode.end! - 1!, `: ${ctxType}`)
// }
// args = ss.toString()
// }
} }
// 8. wrap setup code with function.
// export the content of <script setup> as a named export, `setup`.
// this allows `import { setup } from '*.vue'` for testing purposes.
s.prependLeft(
startOffset,
`\nexport ${hasAwait ? `async ` : ``}function setup(${args}) {\n`
)
const allBindings = { ...setupBindings } const allBindings = { ...setupBindings }
for (const key in userImports) { for (const key in userImports) {
allBindings[key] = true allBindings[key] = 'var'
} }
// 9. inject `useCssVars` calls // 9. inject `useCssVars` calls
@ -741,23 +735,27 @@ export function compileScript(
if (scriptAst) { if (scriptAst) {
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst)) Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
} }
if (setupContextType) {
for (const key in typeDeclaredProps) {
bindingMetadata[key] = 'props'
}
}
if (setupContextArg) {
Object.assign(bindingMetadata, analyzeBindingsFromOptions(setupContextArg))
}
if (options.inlineTemplate) { if (options.inlineTemplate) {
for (const [key, { source }] of Object.entries(userImports)) { for (const [key, { source }] of Object.entries(userImports)) {
bindingMetadata[key] = source.endsWith('.vue') bindingMetadata[key] = source.endsWith('.vue') ? 'setup-raw' : 'setup'
? 'component-import'
: 'setup'
} }
for (const key in setupBindings) { for (const key in setupBindings) {
bindingMetadata[key] = 'setup' bindingMetadata[key] =
setupBindings[key] === 'var' ? 'setup' : 'setup-raw'
} }
} else { } else {
for (const key in allBindings) { for (const key in allBindings) {
bindingMetadata[key] = 'setup' bindingMetadata[key] = 'setup'
} }
} }
for (const key in typeDeclaredProps) {
bindingMetadata[key] = 'props'
}
// 11. generate return statement // 11. generate return statement
let returned let returned
@ -798,6 +796,16 @@ export function compileScript(
s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`) s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
// 12. finalize default export // 12. finalize default export
let runtimeOptions = ``
if (setupContextArg) {
runtimeOptions = `\n ${scriptSetup.content
.slice(setupContextArg.start! + 1, setupContextArg.end! - 1)
.trim()},`
} else if (setupContextType) {
runtimeOptions =
genRuntimeProps(typeDeclaredProps) + genRuntimeEmits(typeDeclaredEmits)
}
if (isTS) { if (isTS) {
// for TS, make sure the exported type is still valid type with // for TS, make sure the exported type is still valid type with
// correct props information // correct props information
@ -805,18 +813,34 @@ export function compileScript(
// we have to use object spread for types to be merged properly // we have to use object spread for types to be merged properly
// user's TS setting should compile it down to proper targets // user's TS setting should compile it down to proper targets
const def = defaultExport ? `\n ...${defaultTempVar},` : `` const def = defaultExport ? `\n ...${defaultTempVar},` : ``
const runtimeProps = genRuntimeProps(typeDeclaredProps) // wrap setup code with function.
const runtimeEmits = genRuntimeEmits(typeDeclaredEmits) // export the content of <script setup> as a named export, `setup`.
s.append( // this allows `import { setup } from '*.vue'` for testing purposes.
`export default defineComponent({${def}${runtimeProps}${runtimeEmits}\n setup\n})` s.prependLeft(
startOffset,
`\nexport default defineComponent({${def}${runtimeOptions}\n ${
hasAwait ? `async ` : ``
}setup(${args}) {\n`
) )
s.append(`})`)
} else { } else {
if (defaultExport) { if (defaultExport) {
// can't rely on spread operator in non ts mode
s.prependLeft(
startOffset,
`\n${hasAwait ? `async ` : ``}function setup(${args}) {\n`
)
s.append( s.append(
`${defaultTempVar}.setup = setup\nexport default ${defaultTempVar}` `/*#__PURE__*/ Object.assign(${defaultTempVar}, {${runtimeOptions}\n setup\n})\n` +
`export default ${defaultTempVar}`
) )
} else { } else {
s.append(`export default { setup }`) s.prependLeft(
startOffset,
`\nexport default {${runtimeOptions}\n ` +
`${hasAwait ? `async ` : ``}setup(${args}) {\n`
)
s.append(`}`)
} }
} }
@ -843,16 +867,25 @@ export function compileScript(
} }
} }
function walkDeclaration(node: Declaration, bindings: Record<string, boolean>) { function walkDeclaration(node: Declaration, bindings: Record<string, string>) {
if (node.type === 'VariableDeclaration') { if (node.type === 'VariableDeclaration') {
const isConst = node.kind === 'const'
// export const foo = ... // export const foo = ...
for (const { id } of node.declarations) { for (const { id, init } of node.declarations) {
if (id.type === 'Identifier') { if (id.type === 'Identifier') {
bindings[id.name] = true bindings[id.name] =
// if a declaration is a const literal, we can mark it so that
// the generated render fn code doesn't need to unref() it
isConst &&
init!.type !== 'Identifier' && // const a = b
init!.type !== 'CallExpression' && // const a = ref()
init!.type !== 'MemberExpression' // const a = b.c
? 'const'
: 'var'
} else if (id.type === 'ObjectPattern') { } else if (id.type === 'ObjectPattern') {
walkObjectPattern(id, bindings) walkObjectPattern(id, bindings, isConst)
} else if (id.type === 'ArrayPattern') { } else if (id.type === 'ArrayPattern') {
walkArrayPattern(id, bindings) walkArrayPattern(id, bindings, isConst)
} }
} }
} else if ( } else if (
@ -861,13 +894,14 @@ function walkDeclaration(node: Declaration, bindings: Record<string, boolean>) {
) { ) {
// export function foo() {} / export class Foo {} // export function foo() {} / export class Foo {}
// export declarations must be named. // export declarations must be named.
bindings[node.id!.name] = true bindings[node.id!.name] = 'const'
} }
} }
function walkObjectPattern( function walkObjectPattern(
node: ObjectPattern, node: ObjectPattern,
bindings: Record<string, boolean> bindings: Record<string, string>,
isConst: boolean
) { ) {
for (const p of node.properties) { for (const p of node.properties) {
if (p.type === 'ObjectProperty') { if (p.type === 'ObjectProperty') {
@ -875,43 +909,48 @@ 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] = true bindings[p.key.name] = 'var'
} else { } else {
walkPattern(p.value, bindings) walkPattern(p.value, bindings, isConst)
} }
} }
} else { } else {
// ...rest // ...rest
// argument can only be identifer when destructuring // argument can only be identifer when destructuring
bindings[(p.argument as Identifier).name] = true bindings[(p.argument as Identifier).name] = isConst ? 'const' : 'var'
} }
} }
} }
function walkArrayPattern( function walkArrayPattern(
node: ArrayPattern, node: ArrayPattern,
bindings: Record<string, boolean> bindings: Record<string, string>,
isConst: boolean
) { ) {
for (const e of node.elements) { for (const e of node.elements) {
e && walkPattern(e, bindings) e && walkPattern(e, bindings, isConst)
} }
} }
function walkPattern(node: Node, bindings: Record<string, boolean>) { function walkPattern(
node: Node,
bindings: Record<string, string>,
isConst: boolean
) {
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
bindings[node.name] = true bindings[node.name] = 'var'
} else if (node.type === 'RestElement') { } else if (node.type === 'RestElement') {
// argument can only be identifer when destructuring // argument can only be identifer when destructuring
bindings[(node.argument as Identifier).name] = true bindings[(node.argument as Identifier).name] = isConst ? 'const' : 'var'
} else if (node.type === 'ObjectPattern') { } else if (node.type === 'ObjectPattern') {
walkObjectPattern(node, bindings) walkObjectPattern(node, bindings, isConst)
} else if (node.type === 'ArrayPattern') { } else if (node.type === 'ArrayPattern') {
walkArrayPattern(node, bindings) 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] = true bindings[node.left.name] = 'var'
} else { } else {
walkPattern(node.left, bindings) walkPattern(node.left, bindings, isConst)
} }
} }
} }
@ -1273,14 +1312,20 @@ function getObjectOrArrayExpressionKeys(property: ObjectProperty): string[] {
* compilation process so this should only be used on single `<script>` SFCs. * compilation process so this should only be used on single `<script>` SFCs.
*/ */
function analyzeScriptBindings(ast: Statement[]): BindingMetadata { function analyzeScriptBindings(ast: Statement[]): BindingMetadata {
const bindings: BindingMetadata = {}
for (const node of ast) { for (const node of ast) {
if ( if (
node.type === 'ExportDefaultDeclaration' && node.type === 'ExportDefaultDeclaration' &&
node.declaration.type === 'ObjectExpression' node.declaration.type === 'ObjectExpression'
) { ) {
for (const property of node.declaration.properties) { return analyzeBindingsFromOptions(node.declaration)
}
}
return {}
}
function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
const bindings: BindingMetadata = {}
for (const property of node.properties) {
if ( if (
property.type === 'ObjectProperty' && property.type === 'ObjectProperty' &&
!property.computed && !property.computed &&
@ -1307,8 +1352,7 @@ function analyzeScriptBindings(ast: Statement[]): BindingMetadata {
// computed & methods // computed & methods
else if ( else if (
property.value.type === 'ObjectExpression' && property.value.type === 'ObjectExpression' &&
(property.key.name === 'computed' || (property.key.name === 'computed' || property.key.name === 'methods')
property.key.name === 'methods')
) { ) {
// methods: { foo() {} } // methods: { foo() {} }
// computed: { foo() {} } // computed: { foo() {} }
@ -1342,8 +1386,6 @@ function analyzeScriptBindings(ast: Statement[]): BindingMetadata {
} }
} }
} }
}
}
return bindings return bindings
} }

View File

@ -0,0 +1,22 @@
import { Slots } from './componentSlots'
import { warn } from './warning'
interface DefaultContext {
props: Record<string, unknown>
attrs: Record<string, unknown>
emit: (...args: any[]) => void
slots: Slots
}
export function defineContext<T extends Partial<DefaultContext> = {}>(
opts?: any // TODO infer
): { [K in keyof DefaultContext]: T[K] extends {} ? T[K] : DefaultContext[K] } {
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 null as any
}

View File

@ -105,7 +105,7 @@ export interface ComponentInternalOptions {
export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}> export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
extends ComponentInternalOptions { extends ComponentInternalOptions {
// use of any here is intentional so it can be a valid JSX Element constructor // use of any here is intentional so it can be a valid JSX Element constructor
(props: P, ctx: SetupContext<E>): any (props: P, ctx: SetupContext<E, P>): any
props?: ComponentPropsOptions<P> props?: ComponentPropsOptions<P>
emits?: E | (keyof E)[] emits?: E | (keyof E)[]
inheritAttrs?: boolean inheritAttrs?: boolean
@ -167,7 +167,8 @@ export const enum LifecycleHooks {
ERROR_CAPTURED = 'ec' ERROR_CAPTURED = 'ec'
} }
export interface SetupContext<E = EmitsOptions> { export interface SetupContext<E = EmitsOptions, P = Data> {
props: P
attrs: Data attrs: Data
slots: Slots slots: Slots
emit: EmitFn<E> emit: EmitFn<E>
@ -735,6 +736,9 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
// We use getters in dev in case libs like test-utils overwrite instance // We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod) // properties (overwrites should not be done in prod)
return Object.freeze({ return Object.freeze({
get props() {
return instance.props
},
get attrs() { get attrs() {
return new Proxy(instance.attrs, attrHandlers) return new Proxy(instance.attrs, attrHandlers)
}, },
@ -747,6 +751,7 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
}) })
} else { } else {
return { return {
props: instance.props,
attrs: instance.attrs, attrs: instance.attrs,
slots: instance.slots, slots: instance.slots,
emit: instance.emit emit: instance.emit

View File

@ -96,7 +96,7 @@ export interface ComponentOptionsBase<
setup?: ( setup?: (
this: void, this: void,
props: Props, props: Props,
ctx: SetupContext<E> ctx: SetupContext<E, Props>
) => Promise<RawBindings> | RawBindings | RenderFunction | void ) => Promise<RawBindings> | RawBindings | RenderFunction | void
name?: string name?: string
template?: string | object // can be a direct DOM node template?: string | object // can be a direct DOM node

View File

@ -95,6 +95,7 @@ export function renderComponentRoot(
props, props,
__DEV__ __DEV__
? { ? {
props,
get attrs() { get attrs() {
markAttrsAccessed() markAttrsAccessed()
return attrs return attrs
@ -102,7 +103,7 @@ export function renderComponentRoot(
slots, slots,
emit emit
} }
: { attrs, slots, emit } : { props, attrs, slots, emit }
) )
: render(props, null as any /* we know it doesn't need it */) : render(props, null as any /* we know it doesn't need it */)
) )

View File

@ -1,15 +0,0 @@
import { EMPTY_OBJ } from '@vue/shared'
import { Slots } from '../componentSlots'
interface DefaultContext {
props: Record<string, unknown>
attrs: Record<string, unknown>
emit: (...args: any[]) => void
slots: Slots
}
export function useSetupContext<T extends Partial<DefaultContext> = {}>(
opts?: any // TODO infer
): { [K in keyof DefaultContext]: T[K] extends {} ? T[K] : DefaultContext[K] } {
return EMPTY_OBJ as any
}

View File

@ -43,6 +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'
// Advanced API ---------------------------------------------------------------- // Advanced API ----------------------------------------------------------------
@ -261,8 +262,6 @@ import {
setCurrentRenderingInstance setCurrentRenderingInstance
} from './componentRenderUtils' } from './componentRenderUtils'
import { isVNode, normalizeVNode } from './vnode' import { isVNode, normalizeVNode } from './vnode'
import { Slots } from './componentSlots'
import { EMPTY_OBJ } from '@vue/shared/src'
const _ssrUtils = { const _ssrUtils = {
createComponentInstance, createComponentInstance,