wip: defineContext

This commit is contained in:
Evan You 2020-11-11 19:40:27 -05:00
parent 2a4fc32d15
commit dc098c7f81
3 changed files with 171 additions and 164 deletions

View File

@ -10,8 +10,6 @@ import {
ObjectExpression, ObjectExpression,
ArrayPattern, ArrayPattern,
Identifier, Identifier,
ExpressionStatement,
ArrowFunctionExpression,
ExportSpecifier, ExportSpecifier,
Function as FunctionNode, Function as FunctionNode,
TSType, TSType,
@ -29,6 +27,8 @@ import { RawSourceMap } from 'source-map'
import { genCssVarsCode, injectCssVarsCalls } from './genCssVars' import { genCssVarsCode, injectCssVarsCalls } from './genCssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate' import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
const CTX_FN_NAME = 'defineContext'
export interface SFCScriptCompileOptions { export interface SFCScriptCompileOptions {
/** /**
* https://babeljs.io/docs/en/babel-parser#plugins * https://babeljs.io/docs/en/babel-parser#plugins
@ -127,13 +127,21 @@ export function compileScript(
const defaultTempVar = `__default__` const defaultTempVar = `__default__`
const bindingMetadata: BindingMetadata = {} const bindingMetadata: BindingMetadata = {}
const helperImports: Set<string> = new Set() const helperImports: Set<string> = new Set()
const userImports: Record<string, string> = Object.create(null) const userImports: Record<
string,
{
imported: string | null
source: string
}
> = Object.create(null)
const setupBindings: Record<string, boolean> = Object.create(null) const setupBindings: Record<string, boolean> = Object.create(null)
const refBindings: Record<string, boolean> = Object.create(null) const refBindings: Record<string, boolean> = 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 needDefaultExportRefCheck = false let setupContextExp: string | undefined
let setupContextArg: Node | undefined
let setupContextType: TSTypeLiteral | undefined
let hasAwait = false let hasAwait = false
const s = new MagicString(source) const s = new MagicString(source)
@ -314,10 +322,16 @@ export function compileScript(
for (const node of scriptAst) { for (const node of scriptAst) {
if (node.type === 'ImportDeclaration') { if (node.type === 'ImportDeclaration') {
// record imports for dedupe // record imports for dedupe
for (const { for (const specifier of node.specifiers) {
local: { name } const name = specifier.local.name
} of node.specifiers) { const imported =
userImports[name] = node.source.value specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name
userImports[name] = {
imported: imported || null,
source: node.source.value
}
} }
} else if (node.type === 'ExportDefaultDeclaration') { } else if (node.type === 'ExportDefaultDeclaration') {
// export default // export default
@ -367,75 +381,17 @@ export function compileScript(
} }
} }
// 2. check <script setup="xxx"> function signature
const setupValue = scriptSetup.setup
const hasExplicitSignature = typeof setupValue === 'string'
let propsIdentifier: string | undefined
let emitIdentifier: string | undefined
let slotsIdentifier: string | undefined
let attrsIdentifier: string | undefined
let propsType = `{}` let propsType = `{}`
let emitType = `(e: string, ...args: any[]) => void` let emitType = `(e: string, ...args: any[]) => void`
let slotsType = `Slots` let slotsType = `Slots`
let attrsType = `Record<string, any>` let attrsType = `Record<string, any>`
let propsASTNode
let setupCtxASTNode
// props/emits declared via types // props/emits declared via types
const typeDeclaredProps: Record<string, PropTypeData> = {} const typeDeclaredProps: Record<string, PropTypeData> = {}
const typeDeclaredEmits: Set<string> = new Set() const typeDeclaredEmits: Set<string> = new Set()
// record declared types for runtime props type generation // record declared types for runtime props type generation
const declaredTypes: Record<string, string[]> = {} const declaredTypes: Record<string, string[]> = {}
// <script setup="xxx">
if (hasExplicitSignature) {
let signatureAST
try {
signatureAST = _parse(`(${setupValue})=>{}`, { plugins }).program.body[0]
} catch (e) {
throw new Error(
`[@vue/compiler-sfc] Invalid <script setup> signature: ${setupValue}\n\n${generateCodeFrame(
source,
startOffset - 1,
startOffset
)}`
)
}
if (isTS) {
// <script setup="xxx" lang="ts">
// parse the signature to extract the identifiers users are assigning to
// the arguments. They are needed for matching type delcarations.
const params = ((signatureAST as ExpressionStatement)
.expression as ArrowFunctionExpression).params
if (params[0] && params[0].type === 'Identifier') {
propsASTNode = params[0]
propsIdentifier = propsASTNode.name
}
if (params[1] && params[1].type === 'ObjectPattern') {
setupCtxASTNode = params[1]
for (const p of params[1].properties) {
if (
p.type === 'ObjectProperty' &&
p.key.type === 'Identifier' &&
p.value.type === 'Identifier'
) {
if (p.key.name === 'emit') {
emitIdentifier = p.value.name
} else if (p.key.name === 'slots') {
slotsIdentifier = p.value.name
} else if (p.key.name === 'attrs') {
attrsIdentifier = p.value.name
}
}
}
}
}
}
// 3. parse <script setup> and walk over top level statements // 3. parse <script setup> and walk over top level statements
const scriptSetupAst = parse( const scriptSetupAst = parse(
scriptSetup.content, scriptSetup.content,
@ -503,15 +459,35 @@ export function compileScript(
let prev let prev
let removed = 0 let removed = 0
for (const specifier of node.specifiers) { for (const specifier of node.specifiers) {
if (userImports[specifier.local.name]) { const local = specifier.local.name
// already imported in <script setup>, dedupe const imported =
specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name
const source = node.source.value
const existing = userImports[local]
if (source === 'vue' && imported === CTX_FN_NAME) {
removed++ removed++
s.remove( s.remove(
prev ? prev.end! + startOffset : specifier.start! + startOffset, prev ? prev.end! + startOffset : specifier.start! + startOffset,
specifier.end! + startOffset specifier.end! + startOffset
) )
} else if (existing) {
if (existing.source === source && existing.imported === imported) {
// already imported in <script setup>, dedupe
removed++
s.remove(
prev ? prev.end! + startOffset : specifier.start! + startOffset,
specifier.end! + startOffset
)
} else {
error(`different imports aliased to same local name.`, specifier)
}
} else { } else {
userImports[specifier.local.name] = node.source.value userImports[local] = {
imported: imported || null,
source: node.source.value
}
} }
prev = specifier prev = specifier
} }
@ -520,37 +496,42 @@ export function compileScript(
} }
} }
if ( if (node.type === 'VariableDeclaration' && !node.declare) {
(node.type === 'ExportNamedDeclaration' && node.exportKind !== 'type') || for (const decl of node.declarations) {
node.type === 'ExportAllDeclaration' if (
) { decl.init &&
error( decl.init.type === 'CallExpression' &&
`<script setup> cannot contain non-type named or * exports. ` + decl.init.callee.type === 'Identifier' &&
`If you are using a previous version of <script setup>, please ` + decl.init.callee.name === CTX_FN_NAME
`consult the updated RFC at https://github.com/vuejs/rfcs/pull/227.`, ) {
node if (node.declarations.length === 1) {
) s.remove(node.start! + startOffset, node.end! + startOffset)
} } else {
s.remove(decl.start! + startOffset, decl.end! + startOffset)
}
setupContextExp = scriptSetup.content.slice(
decl.id.start!,
decl.id.end!
)
setupContextArg = decl.init.arguments[0]
if (node.type === 'ExportDefaultDeclaration') { // useSetupContext() has type parameters - infer runtime types from it
if (defaultExport) { if (decl.init.typeParameters) {
// <script> already has export default const typeArg = decl.init.typeParameters.params[0]
error( if (typeArg.type === 'TSTypeLiteral') {
`Default export is already declared in normal <script>.`, setupContextType = typeArg
node, } else {
node.start! + startOffset + `export default`.length error(
) `type argument passed to ${CTX_FN_NAME}() must be a literal type.`,
typeArg
)
}
}
}
} }
// export default {} inside <script setup>
// this should be kept in module scope - move it to the end
s.move(start, end, source.length)
s.overwrite(start, start + `export default`.length, `const __default__ =`)
// save it for analysis when all imports and variable declarations have
// been recorded
defaultExport = node
needDefaultExportRefCheck = true
} }
// walk decalrations to record declared bindings
if ( if (
(node.type === 'VariableDeclaration' || (node.type === 'VariableDeclaration' ||
node.type === 'FunctionDeclaration' || node.type === 'FunctionDeclaration' ||
@ -563,47 +544,6 @@ export function compileScript(
// Type declarations // Type declarations
if (node.type === 'VariableDeclaration' && node.declare) { if (node.type === 'VariableDeclaration' && node.declare) {
s.remove(start, end) s.remove(start, end)
for (const { id } of node.declarations) {
if (id.type === 'Identifier') {
if (
id.typeAnnotation &&
id.typeAnnotation.type === 'TSTypeAnnotation'
) {
const typeNode = id.typeAnnotation.typeAnnotation
const typeString = source.slice(
typeNode.start! + startOffset,
typeNode.end! + startOffset
)
if (typeNode.type === 'TSTypeLiteral') {
if (id.name === propsIdentifier) {
propsType = typeString
extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
} else if (id.name === slotsIdentifier) {
slotsType = typeString
} else if (id.name === attrsIdentifier) {
attrsType = typeString
}
} else if (
id.name === emitIdentifier &&
typeNode.type === 'TSFunctionType'
) {
emitType = typeString
extractRuntimeEmits(typeNode, typeDeclaredEmits)
}
}
}
}
}
if (
node.type === 'TSDeclareFunction' &&
node.id &&
node.id.name === emitIdentifier
) {
const index = node.id.start! + startOffset
s.overwrite(index, index + emitIdentifier.length, '__emit__')
emitType = `typeof __emit__`
extractRuntimeEmits(node, typeDeclaredEmits)
} }
// move all type declarations to outer scope // move all type declarations to outer scope
@ -618,7 +558,7 @@ export function compileScript(
// walk statements & named exports / variable declarations for top level // walk statements & named exports / variable declarations for top level
// await // await
if ( if (
node.type === 'VariableDeclaration' || (node.type === 'VariableDeclaration' && !node.declare) ||
node.type.endsWith('Statement') node.type.endsWith('Statement')
) { ) {
;(walk as any)(node, { ;(walk as any)(node, {
@ -632,6 +572,19 @@ export function compileScript(
} }
}) })
} }
if (
(node.type === 'ExportNamedDeclaration' && node.exportKind !== 'type') ||
node.type === 'ExportAllDeclaration' ||
node.type === 'ExportDefaultDeclaration'
) {
error(
`<script setup> cannot contain ES module exports. ` +
`If you are using a previous version of <script setup>, please ` +
`consult the updated RFC at https://github.com/vuejs/rfcs/pull/227.`,
node
)
}
} }
// 4. Do a full walk to rewrite identifiers referencing let exports with ref // 4. Do a full walk to rewrite identifiers referencing let exports with ref
@ -660,13 +613,47 @@ export function compileScript(
} }
} }
// 5. check default export to make sure it doesn't reference setup scope // 5. extract runtime props/emits code from setup context type
if (setupContextType) {
for (const m of setupContextType.members) {
if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
const typeNode = m.typeAnnotation!.typeAnnotation
const typeString = scriptSetup.content.slice(
typeNode.start!,
typeNode.end!
)
if (m.key.name === '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 (m.key.name === 'emit') {
emitType = typeString
if (typeNode.type === 'TSFunctionType') {
extractRuntimeEmits(typeNode, typeDeclaredEmits)
} else {
// TODO be able to trace references
error(`emit type must be a function type`, typeNode)
}
} else if (m.key.name === 'attrs') {
attrsType = typeString
} else if (m.key.name === 'slots') {
slotsType = typeString
}
}
}
}
// 5. check useSetupContext args to make sure it doesn't reference setup scope
// variables // variables
if (needDefaultExportRefCheck) { if (setupContextArg) {
walkIdentifiers(defaultExport!, id => { walkIdentifiers(setupContextArg, id => {
if (setupBindings[id.name]) { if (setupBindings[id.name]) {
error( error(
`\`export default\` in <script setup> cannot reference locally ` + `\`${CTX_FN_NAME}()\` 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 ` +
@ -697,31 +684,30 @@ export function compileScript(
} }
// 7. finalize setup argument signature. // 7. finalize setup argument signature.
let args = `` let args = setupContextExp ? `__props, ${setupContextExp}` : ``
if (isTS) { if (isTS) {
if (slotsType === 'Slots') { if (slotsType === 'Slots') {
helperImports.add('Slots') helperImports.add('Slots')
} }
const ctxType = `{ args += `: {
props: ${propsType},
emit: ${emitType}, emit: ${emitType},
slots: ${slotsType}, slots: ${slotsType},
attrs: ${attrsType} attrs: ${attrsType}
}` }`
if (hasExplicitSignature) { // if (hasExplicitSignature) {
// inject types to user signature // // inject types to user signature
args = setupValue as string // args = setupValue as string
const ss = new MagicString(args) // const ss = new MagicString(args)
if (propsASTNode) { // if (propsASTNode) {
// compensate for () wraper offset // // compensate for () wraper offset
ss.appendRight(propsASTNode.end! - 1, `: ${propsType}`) // ss.appendRight(propsASTNode.end! - 1, `: ${propsType}`)
} // }
if (setupCtxASTNode) { // if (setupCtxASTNode) {
ss.appendRight(setupCtxASTNode.end! - 1!, `: ${ctxType}`) // ss.appendRight(setupCtxASTNode.end! - 1!, `: ${ctxType}`)
} // }
args = ss.toString() // args = ss.toString()
} // }
} else if (hasExplicitSignature) {
args = setupValue as string
} }
// 8. wrap setup code with function. // 8. wrap setup code with function.
@ -732,7 +718,10 @@ export function compileScript(
`\nexport ${hasAwait ? `async ` : ``}function setup(${args}) {\n` `\nexport ${hasAwait ? `async ` : ``}function setup(${args}) {\n`
) )
const allBindings = { ...userImports, ...setupBindings } const allBindings = { ...setupBindings }
for (const key in userImports) {
allBindings[key] = true
}
// 9. inject `useCssVars` calls // 9. inject `useCssVars` calls
if (hasCssVars) { if (hasCssVars) {
@ -753,8 +742,8 @@ export function compileScript(
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst)) Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
} }
if (options.inlineTemplate) { if (options.inlineTemplate) {
for (const [key, value] of Object.entries(userImports)) { for (const [key, { source }] of Object.entries(userImports)) {
bindingMetadata[key] = value.endsWith('.vue') bindingMetadata[key] = source.endsWith('.vue')
? 'component-import' ? 'component-import'
: 'setup' : 'setup'
} }
@ -769,7 +758,6 @@ export function compileScript(
for (const key in typeDeclaredProps) { for (const key in typeDeclaredProps) {
bindingMetadata[key] = 'props' bindingMetadata[key] = 'props'
} }
Object.assign(bindingMetadata, analyzeScriptBindings(scriptSetupAst))
// 11. generate return statement // 11. generate return statement
let returned let returned
@ -833,7 +821,9 @@ export function compileScript(
} }
// 13. finalize Vue helper imports // 13. finalize Vue helper imports
const helpers = [...helperImports].filter(i => userImports[i] !== 'vue') // TODO account for cases where user imports a helper with the same name
// from a non-vue source
const helpers = [...helperImports].filter(i => !userImports[i])
if (helpers.length) { if (helpers.length) {
s.prepend(`import { ${helpers.join(', ')} } from 'vue'\n`) s.prepend(`import { ${helpers.join(', ')} } from 'vue'\n`)
} }

View File

@ -0,0 +1,15 @@
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

@ -261,6 +261,8 @@ 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,