refactor(compiler-sfc): (wip) split compileScript logic, use context, move defineProps

This commit is contained in:
Evan You 2023-04-11 13:06:04 +08:00
parent b16866d56b
commit acd7eb22cf
6 changed files with 541 additions and 299 deletions

View File

@ -15,12 +15,7 @@ import {
isCallOf isCallOf
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse' import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
import { import { parse as _parse, parseExpression, ParserPlugin } from '@babel/parser'
parse as _parse,
parseExpression,
ParserOptions,
ParserPlugin
} from '@babel/parser'
import { camelize, capitalize, generateCodeFrame, makeMap } from '@vue/shared' import { camelize, capitalize, generateCodeFrame, makeMap } from '@vue/shared'
import { import {
Node, Node,
@ -41,7 +36,6 @@ import {
TSInterfaceBody, TSInterfaceBody,
TSTypeElement, TSTypeElement,
AwaitExpression, AwaitExpression,
Program,
ObjectMethod, ObjectMethod,
LVal, LVal,
Expression, Expression,
@ -59,13 +53,19 @@ import { warnOnce } from './warn'
import { rewriteDefaultAST } from './rewriteDefault' import { rewriteDefaultAST } from './rewriteDefault'
import { createCache } from './cache' import { createCache } from './cache'
import { shouldTransform, transformAST } from '@vue/reactivity-transform' import { shouldTransform, transformAST } from '@vue/reactivity-transform'
import { transformDestructuredProps } from './script/propsDestructure' import { transformDestructuredProps } from './script/definePropsDestructure'
import { resolveObjectKey, FromNormalScript } from './script/utils'
import { ScriptCompileContext } from './script/context'
import {
processDefineProps,
DEFINE_PROPS,
WITH_DEFAULTS,
PropsDeclType
} from './script/defineProps'
// Special compiler macros // Special compiler macros
const DEFINE_PROPS = 'defineProps'
const DEFINE_EMITS = 'defineEmits' const DEFINE_EMITS = 'defineEmits'
const DEFINE_EXPOSE = 'defineExpose' const DEFINE_EXPOSE = 'defineExpose'
const WITH_DEFAULTS = 'withDefaults'
const DEFINE_OPTIONS = 'defineOptions' const DEFINE_OPTIONS = 'defineOptions'
const DEFINE_SLOTS = 'defineSlots' const DEFINE_SLOTS = 'defineSlots'
const DEFINE_MODEL = 'defineModel' const DEFINE_MODEL = 'defineModel'
@ -149,8 +149,6 @@ export type PropsDestructureBindings = Record<
} }
> >
type FromNormalScript<T> = T & { __fromNormalScript?: boolean | null }
type PropsDeclType = FromNormalScript<TSTypeLiteral | TSInterfaceBody>
type EmitsDeclType = FromNormalScript< type EmitsDeclType = FromNormalScript<
TSFunctionType | TSTypeLiteral | TSInterfaceBody TSFunctionType | TSTypeLiteral | TSInterfaceBody
> >
@ -195,41 +193,15 @@ export function compileScript(
? `const ${options.genDefaultAs} =` ? `const ${options.genDefaultAs} =`
: `export default` : `export default`
const normalScriptDefaultVar = `__default__` const normalScriptDefaultVar = `__default__`
const isJS =
scriptLang === 'js' ||
scriptLang === 'jsx' ||
scriptSetupLang === 'js' ||
scriptSetupLang === 'jsx'
const isTS =
scriptLang === 'ts' ||
scriptLang === 'tsx' ||
scriptSetupLang === 'ts' ||
scriptSetupLang === 'tsx'
// resolve parser plugins const ctx = new ScriptCompileContext(sfc, options)
const plugins: ParserPlugin[] = [] const { isTS } = ctx
if (!isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') {
plugins.push('jsx')
} else {
// If don't match the case of adding jsx, should remove the jsx from the babelParserPlugins
if (options.babelParserPlugins)
options.babelParserPlugins = options.babelParserPlugins.filter(
n => n !== 'jsx'
)
}
if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
if (isTS) {
plugins.push('typescript')
if (!plugins.includes('decorators')) {
plugins.push('decorators-legacy')
}
}
if (!scriptSetup) { if (!scriptSetup) {
if (!script) { if (!script) {
throw new Error(`[@vue/compiler-sfc] SFC contains no <script> tags.`) throw new Error(`[@vue/compiler-sfc] SFC contains no <script> tags.`)
} }
if (scriptLang && !isJS && !isTS) { if (scriptLang && !ctx.isJS && !ctx.isTS) {
// do not process non js/ts script blocks // do not process non js/ts script blocks
return script return script
} }
@ -237,10 +209,7 @@ export function compileScript(
try { try {
let content = script.content let content = script.content
let map = script.map let map = script.map
const scriptAst = _parse(content, { const scriptAst = ctx.scriptAst!
plugins,
sourceType: 'module'
}).program
const bindings = analyzeScriptBindings(scriptAst.body) const bindings = analyzeScriptBindings(scriptAst.body)
if (enableReactivityTransform && shouldTransform(content)) { if (enableReactivityTransform && shouldTransform(content)) {
const s = new MagicString(source) const s = new MagicString(source)
@ -268,7 +237,7 @@ export function compileScript(
if (cssVars.length || options.genDefaultAs) { if (cssVars.length || options.genDefaultAs) {
const defaultVar = options.genDefaultAs || normalScriptDefaultVar const defaultVar = options.genDefaultAs || normalScriptDefaultVar
const s = new MagicString(content) const s = new MagicString(content)
rewriteDefaultAST(scriptAst.body, s, defaultVar) rewriteDefaultAST(ctx.scriptAst!.body, s, defaultVar)
content = s.toString() content = s.toString()
if (cssVars.length) { if (cssVars.length) {
content += genNormalScriptCssVarsCode( content += genNormalScriptCssVarsCode(
@ -304,7 +273,7 @@ export function compileScript(
) )
} }
if (scriptSetupLang && !isJS && !isTS) { if (scriptSetupLang && !ctx.isJS && !ctx.isTS) {
// do not process non js/ts script blocks // do not process non js/ts script blocks
return scriptSetup return scriptSetup
} }
@ -317,21 +286,7 @@ export function compileScript(
const setupBindings: Record<string, BindingTypes> = Object.create(null) const setupBindings: Record<string, BindingTypes> = Object.create(null)
let defaultExport: Node | undefined let defaultExport: Node | undefined
let hasDefinePropsCall = false // let propsRuntimeDefaults: Node | undefined
let hasDefineEmitCall = false
let hasDefineExposeCall = false
let hasDefaultExportName = false
let hasDefaultExportRender = false
let hasDefineOptionsCall = false
let hasDefineSlotsCall = false
let hasDefineModelCall = false
let propsRuntimeDecl: Node | undefined
let propsRuntimeDefaults: Node | undefined
let propsDestructureDecl: Node | undefined
let propsDestructureRestId: string | undefined
let propsTypeDecl: PropsDeclType | undefined
let propsTypeDeclRaw: Node | undefined
let propsIdentifier: string | undefined
let emitsRuntimeDecl: Node | undefined let emitsRuntimeDecl: Node | undefined
let emitsTypeDecl: EmitsDeclType | undefined let emitsTypeDecl: EmitsDeclType | undefined
let emitIdentifier: string | undefined let emitIdentifier: string | undefined
@ -344,9 +299,6 @@ export function compileScript(
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[]> = {}
// props destructure data
const propsDestructuredBindings: PropsDestructureBindings =
Object.create(null)
// magic-string state // magic-string state
const s = new MagicString(source) const s = new MagicString(source)
@ -360,21 +312,6 @@ export function compileScript(
return `_${key}` return `_${key}`
} }
function parse(
input: string,
options: ParserOptions,
offset: number
): Program {
try {
return _parse(input, options).program
} catch (e: any) {
e.message = `[@vue/compiler-sfc] ${e.message}\n\n${
sfc.filename
}\n${generateCodeFrame(source, e.pos + offset, e.pos + offset + 1)}`
throw e
}
}
function error( function error(
msg: string, msg: string,
node: Node, node: Node,
@ -439,136 +376,14 @@ export function compileScript(
} }
} }
function processDefineProps(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, DEFINE_PROPS)) {
return false
}
if (hasDefinePropsCall) {
error(`duplicate ${DEFINE_PROPS}() call`, node)
}
hasDefinePropsCall = true
propsRuntimeDecl = node.arguments[0]
// call has type parameters - infer runtime types from it
if (node.typeParameters) {
if (propsRuntimeDecl) {
error(
`${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`,
node
)
}
propsTypeDeclRaw = node.typeParameters.params[0]
propsTypeDecl = resolveQualifiedType(
propsTypeDeclRaw,
node => node.type === 'TSTypeLiteral'
) as PropsDeclType | undefined
if (!propsTypeDecl) {
error(
`type argument passed to ${DEFINE_PROPS}() must be a literal type, ` +
`or a reference to an interface or literal type.`,
propsTypeDeclRaw
)
}
}
if (declId) {
// handle props destructure
if (declId.type === 'ObjectPattern') {
propsDestructureDecl = declId
for (const prop of declId.properties) {
if (prop.type === 'ObjectProperty') {
const propKey = resolveObjectKey(prop.key, prop.computed)
if (!propKey) {
error(
`${DEFINE_PROPS}() destructure cannot use computed key.`,
prop.key
)
}
if (prop.value.type === 'AssignmentPattern') {
// default value { foo = 123 }
const { left, right } = prop.value
if (left.type !== 'Identifier') {
error(
`${DEFINE_PROPS}() destructure does not support nested patterns.`,
left
)
}
// store default value
propsDestructuredBindings[propKey] = {
local: left.name,
default: right
}
} else if (prop.value.type === 'Identifier') {
// simple destructure
propsDestructuredBindings[propKey] = {
local: prop.value.name
}
} else {
error(
`${DEFINE_PROPS}() destructure does not support nested patterns.`,
prop.value
)
}
} else {
// rest spread
propsDestructureRestId = (prop.argument as Identifier).name
}
}
} else {
propsIdentifier = scriptSetup!.content.slice(declId.start!, declId.end!)
}
}
return true
}
function processWithDefaults(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, WITH_DEFAULTS)) {
return false
}
if (processDefineProps(node.arguments[0], declId)) {
if (propsRuntimeDecl) {
error(
`${WITH_DEFAULTS} can only be used with type-based ` +
`${DEFINE_PROPS} declaration.`,
node
)
}
if (propsDestructureDecl) {
error(
`${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
`Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...).`,
node.callee
)
}
propsRuntimeDefaults = node.arguments[1]
if (!propsRuntimeDefaults) {
error(`The 2nd argument of ${WITH_DEFAULTS} is required.`, node)
}
} else {
error(
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
node.arguments[0] || node
)
}
return true
}
function processDefineEmits(node: Node, declId?: LVal): boolean { function processDefineEmits(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, DEFINE_EMITS)) { if (!isCallOf(node, DEFINE_EMITS)) {
return false return false
} }
if (hasDefineEmitCall) { if (ctx.hasDefineEmitCall) {
error(`duplicate ${DEFINE_EMITS}() call`, node) error(`duplicate ${DEFINE_EMITS}() call`, node)
} }
hasDefineEmitCall = true ctx.hasDefineEmitCall = true
emitsRuntimeDecl = node.arguments[0] emitsRuntimeDecl = node.arguments[0]
if (node.typeParameters) { if (node.typeParameters) {
if (emitsRuntimeDecl) { if (emitsRuntimeDecl) {
@ -608,10 +423,10 @@ export function compileScript(
if (!isCallOf(node, DEFINE_SLOTS)) { if (!isCallOf(node, DEFINE_SLOTS)) {
return false return false
} }
if (hasDefineSlotsCall) { if (ctx.hasDefineSlotsCall) {
error(`duplicate ${DEFINE_SLOTS}() call`, node) error(`duplicate ${DEFINE_SLOTS}() call`, node)
} }
hasDefineSlotsCall = true ctx.hasDefineSlotsCall = true
if (node.arguments.length > 0) { if (node.arguments.length > 0) {
error(`${DEFINE_SLOTS}() cannot accept arguments`, node) error(`${DEFINE_SLOTS}() cannot accept arguments`, node)
@ -632,7 +447,7 @@ export function compileScript(
if (!enableDefineModel || !isCallOf(node, DEFINE_MODEL)) { if (!enableDefineModel || !isCallOf(node, DEFINE_MODEL)) {
return false return false
} }
hasDefineModelCall = true ctx.hasDefineModelCall = true
const type = const type =
(node.typeParameters && node.typeParameters.params[0]) || undefined (node.typeParameters && node.typeParameters.params[0]) || undefined
@ -781,7 +596,7 @@ export function compileScript(
if (!isCallOf(node, DEFINE_OPTIONS)) { if (!isCallOf(node, DEFINE_OPTIONS)) {
return false return false
} }
if (hasDefineOptionsCall) { if (ctx.hasDefineOptionsCall) {
error(`duplicate ${DEFINE_OPTIONS}() call`, node) error(`duplicate ${DEFINE_OPTIONS}() call`, node)
} }
if (node.typeParameters) { if (node.typeParameters) {
@ -789,7 +604,7 @@ export function compileScript(
} }
if (!node.arguments[0]) return true if (!node.arguments[0]) return true
hasDefineOptionsCall = true ctx.hasDefineOptionsCall = true
optionsRuntimeDecl = unwrapTSNode(node.arguments[0]) optionsRuntimeDecl = unwrapTSNode(node.arguments[0])
let propsOption = undefined let propsOption = undefined
@ -875,10 +690,10 @@ export function compileScript(
function processDefineExpose(node: Node): boolean { function processDefineExpose(node: Node): boolean {
if (isCallOf(node, DEFINE_EXPOSE)) { if (isCallOf(node, DEFINE_EXPOSE)) {
if (hasDefineExposeCall) { if (ctx.hasDefineExposeCall) {
error(`duplicate ${DEFINE_EXPOSE}() call`, node) error(`duplicate ${DEFINE_EXPOSE}() call`, node)
} }
hasDefineExposeCall = true ctx.hasDefineExposeCall = true
return true return true
} }
return false return false
@ -958,9 +773,9 @@ export function compileScript(
*/ */
function hasStaticWithDefaults() { function hasStaticWithDefaults() {
return ( return (
propsRuntimeDefaults && ctx.propsRuntimeDefaults &&
propsRuntimeDefaults.type === 'ObjectExpression' && ctx.propsRuntimeDefaults.type === 'ObjectExpression' &&
propsRuntimeDefaults.properties.every( ctx.propsRuntimeDefaults.properties.every(
node => node =>
node.type !== 'SpreadElement' && node.type !== 'SpreadElement' &&
(!node.computed || node.key.type.endsWith('Literal')) (!node.computed || node.key.type.endsWith('Literal'))
@ -993,7 +808,7 @@ export function compileScript(
}` }`
} else if (hasStaticDefaults) { } else if (hasStaticDefaults) {
const prop = ( const prop = (
propsRuntimeDefaults as ObjectExpression ctx.propsRuntimeDefaults as ObjectExpression
).properties.find(node => { ).properties.find(node => {
if (node.type === 'SpreadElement') return false if (node.type === 'SpreadElement') return false
return resolveObjectKey(node.key, node.computed) === key return resolveObjectKey(node.key, node.computed) === key
@ -1045,10 +860,10 @@ export function compileScript(
}) })
.join(',\n ')}\n }` .join(',\n ')}\n }`
if (propsRuntimeDefaults && !hasStaticDefaults) { if (ctx.propsRuntimeDefaults && !hasStaticDefaults) {
propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice( propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice(
propsRuntimeDefaults.start! + startOffset, ctx.propsRuntimeDefaults.start! + startOffset,
propsRuntimeDefaults.end! + startOffset ctx.propsRuntimeDefaults.end! + startOffset
)})` )})`
} }
@ -1056,7 +871,7 @@ export function compileScript(
} }
function genModels() { function genModels() {
if (!hasDefineModelCall) return if (!ctx.hasDefineModelCall) return
let modelPropsDecl = '' let modelPropsDecl = ''
for (const [name, { type, options }] of Object.entries(modelDecls)) { for (const [name, { type, options }] of Object.entries(modelDecls)) {
@ -1088,7 +903,7 @@ export function compileScript(
let decl: string let decl: string
if (runtimeType && options) { if (runtimeType && options) {
decl = isTS decl = ctx.isTS
? `{ ${codegenOptions}, ...${options} }` ? `{ ${codegenOptions}, ...${options} }`
: `Object.assign({ ${codegenOptions} }, ${options})` : `Object.assign({ ${codegenOptions} }, ${options})`
} else { } else {
@ -1100,13 +915,13 @@ export function compileScript(
} }
let propsDecls: undefined | string let propsDecls: undefined | string
if (propsRuntimeDecl) { if (ctx.propsRuntimeDecl) {
propsDecls = scriptSetup!.content propsDecls = scriptSetup!.content
.slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!) .slice(ctx.propsRuntimeDecl.start!, ctx.propsRuntimeDecl.end!)
.trim() .trim()
if (propsDestructureDecl) { if (ctx.propsDestructureDecl) {
const defaults: string[] = [] const defaults: string[] = []
for (const key in propsDestructuredBindings) { for (const key in ctx.propsDestructuredBindings) {
const d = genDestructuredDefaultValue(key) const d = genDestructuredDefaultValue(key)
if (d) if (d)
defaults.push( defaults.push(
@ -1121,7 +936,7 @@ export function compileScript(
)}(${propsDecls}, {\n ${defaults.join(',\n ')}\n})` )}(${propsDecls}, {\n ${defaults.join(',\n ')}\n})`
} }
} }
} else if (propsTypeDecl) { } else if (ctx.propsTypeDecl) {
propsDecls = genPropsFromTS() propsDecls = genPropsFromTS()
} }
@ -1143,7 +958,7 @@ export function compileScript(
needSkipFactory: boolean needSkipFactory: boolean
} }
| undefined { | undefined {
const destructured = propsDestructuredBindings[key] const destructured = ctx.propsDestructuredBindings[key]
const defaultVal = destructured && destructured.default const defaultVal = destructured && destructured.default
if (defaultVal) { if (defaultVal) {
const value = scriptSetup!.content.slice( const value = scriptSetup!.content.slice(
@ -1204,13 +1019,15 @@ export function compileScript(
m.key.type === 'Identifier' m.key.type === 'Identifier'
) { ) {
if ( if (
(propsRuntimeDefaults as ObjectExpression).properties.some(p => { (ctx.propsRuntimeDefaults as ObjectExpression).properties.some(
p => {
if (p.type === 'SpreadElement') return false if (p.type === 'SpreadElement') return false
return ( return (
resolveObjectKey(p.key, p.computed) === resolveObjectKey(p.key, p.computed) ===
(m.key as Identifier).name (m.key as Identifier).name
) )
}) }
)
) { ) {
res += res +=
m.key.name + m.key.name +
@ -1248,7 +1065,7 @@ export function compileScript(
} else if (emitsTypeDecl) { } else if (emitsTypeDecl) {
emitsDecl = genEmitsFromTS() emitsDecl = genEmitsFromTS()
} }
if (hasDefineModelCall) { if (ctx.hasDefineModelCall) {
let modelEmitsDecl = `[${Object.keys(modelDecls) let modelEmitsDecl = `[${Object.keys(modelDecls)
.map(n => JSON.stringify(`update:${n}`)) .map(n => JSON.stringify(`update:${n}`))
.join(', ')}]` .join(', ')}]`
@ -1260,29 +1077,32 @@ export function compileScript(
} }
// 0. parse both <script> and <script setup> blocks // 0. parse both <script> and <script setup> blocks
const scriptAst = // const scriptAst =
script && // script &&
parse( // parse(
script.content, // script.content,
{ // {
plugins, // plugins,
sourceType: 'module' // sourceType: 'module'
}, // },
scriptStartOffset! // scriptStartOffset!
) // )
const scriptSetupAst = parse( // const scriptSetupAst = parse(
scriptSetup.content, // scriptSetup.content,
{ // {
plugins: [ // plugins: [
...plugins, // ...plugins,
// allow top level await but only inside <script setup> // // allow top level await but only inside <script setup>
'topLevelAwait' // 'topLevelAwait'
], // ],
sourceType: 'module' // sourceType: 'module'
}, // },
startOffset // startOffset
) // )
const scriptAst = ctx.scriptAst
const scriptSetupAst = ctx.scriptSetupAst!
// 1.1 walk import delcarations of <script> // 1.1 walk import delcarations of <script>
if (scriptAst) { if (scriptAst) {
@ -1407,7 +1227,7 @@ export function compileScript(
s.key.type === 'Identifier' && s.key.type === 'Identifier' &&
s.key.name === 'name' s.key.name === 'name'
) { ) {
hasDefaultExportName = true ctx.hasDefaultExportName = true
} }
if ( if (
(s.type === 'ObjectMethod' || s.type === 'ObjectProperty') && (s.type === 'ObjectMethod' || s.type === 'ObjectProperty') &&
@ -1415,7 +1235,7 @@ export function compileScript(
s.key.name === 'render' s.key.name === 'render'
) { ) {
// TODO warn when we provide a better way to do it? // TODO warn when we provide a better way to do it?
hasDefaultExportRender = true ctx.hasDefaultExportRender = true
} }
} }
} }
@ -1531,10 +1351,9 @@ export function compileScript(
const expr = unwrapTSNode(node.expression) const expr = unwrapTSNode(node.expression)
// process `defineProps` and `defineEmit(s)` calls // process `defineProps` and `defineEmit(s)` calls
if ( if (
processDefineProps(expr) || processDefineProps(ctx, expr) ||
processDefineEmits(expr) || processDefineEmits(expr) ||
processDefineOptions(expr) || processDefineOptions(expr) ||
processWithDefaults(expr) ||
processDefineSlots(expr) processDefineSlots(expr)
) { ) {
s.remove(node.start! + startOffset, node.end! + startOffset) s.remove(node.start! + startOffset, node.end! + startOffset)
@ -1568,9 +1387,7 @@ export function compileScript(
} }
// defineProps / defineEmits // defineProps / defineEmits
const isDefineProps = const isDefineProps = processDefineProps(ctx, init, decl.id)
processDefineProps(init, decl.id) ||
processWithDefaults(init, decl.id)
const isDefineEmits = const isDefineEmits =
!isDefineProps && processDefineEmits(init, decl.id) !isDefineProps && processDefineEmits(init, decl.id)
!isDefineEmits && !isDefineEmits &&
@ -1697,12 +1514,12 @@ export function compileScript(
} }
// 3.1 props destructure transform // 3.1 props destructure transform
if (propsDestructureDecl) { if (ctx.propsDestructureDecl) {
transformDestructuredProps( transformDestructuredProps(
scriptSetupAst, scriptSetupAst,
s, s,
startOffset, startOffset,
propsDestructuredBindings, ctx.propsDestructuredBindings,
error, error,
vueImportAliases vueImportAliases
) )
@ -1728,8 +1545,8 @@ export function compileScript(
} }
// 4. extract runtime props/emits code from setup context type // 4. extract runtime props/emits code from setup context type
if (propsTypeDecl) { if (ctx.propsTypeDecl) {
extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes) extractRuntimeProps(ctx.propsTypeDecl, typeDeclaredProps, declaredTypes)
} }
if (emitsTypeDecl) { if (emitsTypeDecl) {
extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits, error) extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits, error)
@ -1737,9 +1554,9 @@ export function compileScript(
// 5. check macro args to make sure it doesn't reference setup scope // 5. check macro args to make sure it doesn't reference setup scope
// variables // variables
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS) checkInvalidScopeReference(ctx.propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS) checkInvalidScopeReference(ctx.propsRuntimeDefaults, DEFINE_PROPS)
checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS) checkInvalidScopeReference(ctx.propsDestructureDecl, DEFINE_PROPS)
checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_EMITS) checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_EMITS)
checkInvalidScopeReference(optionsRuntimeDecl, DEFINE_OPTIONS) checkInvalidScopeReference(optionsRuntimeDecl, DEFINE_OPTIONS)
@ -1766,8 +1583,8 @@ export function compileScript(
if (scriptAst) { if (scriptAst) {
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst.body)) Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst.body))
} }
if (propsRuntimeDecl) { if (ctx.propsRuntimeDecl) {
for (const key of getObjectOrArrayExpressionKeys(propsRuntimeDecl)) { for (const key of getObjectOrArrayExpressionKeys(ctx.propsRuntimeDecl)) {
bindingMetadata[key] = BindingTypes.PROPS bindingMetadata[key] = BindingTypes.PROPS
} }
} }
@ -1778,13 +1595,13 @@ export function compileScript(
bindingMetadata[key] = BindingTypes.PROPS bindingMetadata[key] = BindingTypes.PROPS
} }
// props aliases // props aliases
if (propsDestructureDecl) { if (ctx.propsDestructureDecl) {
if (propsDestructureRestId) { if (ctx.propsDestructureRestId) {
bindingMetadata[propsDestructureRestId] = bindingMetadata[ctx.propsDestructureRestId] =
BindingTypes.SETUP_REACTIVE_CONST BindingTypes.SETUP_REACTIVE_CONST
} }
for (const key in propsDestructuredBindings) { for (const key in ctx.propsDestructuredBindings) {
const { local } = propsDestructuredBindings[key] const { local } = ctx.propsDestructuredBindings[key]
if (local !== key) { if (local !== key) {
bindingMetadata[local] = BindingTypes.PROPS_ALIASED bindingMetadata[local] = BindingTypes.PROPS_ALIASED
;(bindingMetadata.__propsAliases || ;(bindingMetadata.__propsAliases ||
@ -1832,7 +1649,7 @@ export function compileScript(
// 9. finalize setup() argument signature // 9. finalize setup() argument signature
let args = `__props` let args = `__props`
if (propsTypeDecl) { if (ctx.propsTypeDecl) {
// mark as any and only cast on assignment // mark as any and only cast on assignment
// since the user defined complex types may be incompatible with the // since the user defined complex types may be incompatible with the
// inferred type from generated runtime declarations // inferred type from generated runtime declarations
@ -1841,20 +1658,22 @@ export function compileScript(
// inject user assignment of props // inject user assignment of props
// we use a default __props so that template expressions referencing props // we use a default __props so that template expressions referencing props
// can use it directly // can use it directly
if (propsIdentifier) { if (ctx.propsIdentifier) {
s.prependLeft( s.prependLeft(
startOffset, startOffset,
`\nconst ${propsIdentifier} = __props${ `\nconst ${ctx.propsIdentifier} = __props${
propsTypeDecl ? ` as ${genSetupPropsType(propsTypeDecl)}` : `` ctx.propsTypeDecl ? ` as ${genSetupPropsType(ctx.propsTypeDecl)}` : ``
};\n` };\n`
) )
} }
if (propsDestructureRestId) { if (ctx.propsDestructureRestId) {
s.prependLeft( s.prependLeft(
startOffset, startOffset,
`\nconst ${propsDestructureRestId} = ${helper( `\nconst ${ctx.propsDestructureRestId} = ${helper(
`createPropsRestProxy` `createPropsRestProxy`
)}(__props, ${JSON.stringify(Object.keys(propsDestructuredBindings))});\n` )}(__props, ${JSON.stringify(
Object.keys(ctx.propsDestructuredBindings)
)});\n`
) )
} }
// inject temp variables for async context preservation // inject temp variables for async context preservation
@ -1864,7 +1683,9 @@ export function compileScript(
} }
const destructureElements = const destructureElements =
hasDefineExposeCall || !options.inlineTemplate ? [`expose: __expose`] : [] ctx.hasDefineExposeCall || !options.inlineTemplate
? [`expose: __expose`]
: []
if (emitIdentifier) { if (emitIdentifier) {
destructureElements.push( destructureElements.push(
emitIdentifier === `emit` ? `emit` : `emit: ${emitIdentifier}` emitIdentifier === `emit` ? `emit` : `emit: ${emitIdentifier}`
@ -1876,7 +1697,10 @@ export function compileScript(
// 10. generate return statement // 10. generate return statement
let returned let returned
if (!options.inlineTemplate || (!sfc.template && hasDefaultExportRender)) { if (
!options.inlineTemplate ||
(!sfc.template && ctx.hasDefaultExportRender)
) {
// non-inline mode, or has manual render in normal <script> // non-inline mode, or has manual render in normal <script>
// return bindings from script and script setup // return bindings from script and script setup
const allBindings: Record<string, any> = { const allBindings: Record<string, any> = {
@ -1986,7 +1810,7 @@ export function compileScript(
// 11. finalize default export // 11. finalize default export
let runtimeOptions = `` let runtimeOptions = ``
if (!hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) { if (!ctx.hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) {
const match = filename.match(/([^/\\]+)\.\w+$/) const match = filename.match(/([^/\\]+)\.\w+$/)
if (match) { if (match) {
runtimeOptions += `\n __name: '${match[1]}',` runtimeOptions += `\n __name: '${match[1]}',`
@ -2012,7 +1836,7 @@ export function compileScript(
// <script setup> components are closed by default. If the user did not // <script setup> components are closed by default. If the user did not
// explicitly call `defineExpose`, call expose() with no args. // explicitly call `defineExpose`, call expose() with no args.
const exposeCall = const exposeCall =
hasDefineExposeCall || options.inlineTemplate ? `` : ` __expose();\n` ctx.hasDefineExposeCall || options.inlineTemplate ? `` : ` __expose();\n`
// wrap setup code with function. // wrap setup code with function.
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
@ -2893,14 +2717,3 @@ export function hmrShouldReload(
return false return false
} }
export function resolveObjectKey(node: Node, computed: boolean) {
switch (node.type) {
case 'StringLiteral':
case 'NumericLiteral':
return node.value
case 'Identifier':
if (!computed) return node.name
}
return undefined
}

View File

@ -0,0 +1,153 @@
import { Expression, Node, ObjectPattern, Program } from '@babel/types'
import { SFCDescriptor } from '../parse'
import { generateCodeFrame } from '@vue/shared'
import { PropsDeclType } from './defineProps'
import { parse as babelParse, ParserOptions, ParserPlugin } from '@babel/parser'
import { SFCScriptCompileOptions } from '../compileScript'
import MagicString from 'magic-string'
export type PropsDestructureBindings = Record<
string, // public prop key
{
local: string // local identifier, may be different
default?: Expression
}
>
export class ScriptCompileContext {
isJS: boolean
isTS: boolean
scriptAst: Program | null
scriptSetupAst: Program | null
s = new MagicString(this.descriptor.source)
startOffset = this.descriptor.scriptSetup?.loc.start.offset
endOffset = this.descriptor.scriptSetup?.loc.end.offset
scriptStartOffset = this.descriptor.script?.loc.start.offset
scriptEndOffset = this.descriptor.script?.loc.end.offset
// macros presence check
hasDefinePropsCall = false
hasDefineEmitCall = false
hasDefineExposeCall = false
hasDefaultExportName = false
hasDefaultExportRender = false
hasDefineOptionsCall = false
hasDefineSlotsCall = false
hasDefineModelCall = false
// defineProps
propsIdentifier: string | undefined
propsRuntimeDecl: Node | undefined
propsTypeDecl: PropsDeclType | undefined
propsDestructureDecl: ObjectPattern | undefined
propsDestructuredBindings: PropsDestructureBindings = Object.create(null)
propsDestructureRestId: string | undefined
propsRuntimeDefaults: Node | undefined
constructor(
public descriptor: SFCDescriptor,
public options: SFCScriptCompileOptions
) {
const { script, scriptSetup } = descriptor
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
this.isJS =
scriptLang === 'js' ||
scriptLang === 'jsx' ||
scriptSetupLang === 'js' ||
scriptSetupLang === 'jsx'
this.isTS =
scriptLang === 'ts' ||
scriptLang === 'tsx' ||
scriptSetupLang === 'ts' ||
scriptSetupLang === 'tsx'
// resolve parser plugins
const plugins: ParserPlugin[] = []
if (!this.isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') {
plugins.push('jsx')
} else {
// If don't match the case of adding jsx, should remove the jsx from the babelParserPlugins
if (options.babelParserPlugins)
options.babelParserPlugins = options.babelParserPlugins.filter(
n => n !== 'jsx'
)
}
if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
if (this.isTS) {
plugins.push('typescript')
if (!plugins.includes('decorators')) {
plugins.push('decorators-legacy')
}
}
function parse(
input: string,
options: ParserOptions,
offset: number
): Program {
try {
return babelParse(input, options).program
} catch (e: any) {
e.message = `[@vue/compiler-sfc] ${e.message}\n\n${
descriptor.filename
}\n${generateCodeFrame(
descriptor.source,
e.pos + offset,
e.pos + offset + 1
)}`
throw e
}
}
this.scriptAst =
this.descriptor.script &&
parse(
this.descriptor.script.content,
{
plugins,
sourceType: 'module'
},
this.scriptStartOffset!
)
this.scriptSetupAst =
this.descriptor.scriptSetup &&
parse(
this.descriptor.scriptSetup!.content,
{
plugins: [...plugins, 'topLevelAwait'],
sourceType: 'module'
},
this.startOffset!
)
}
getString(node: Node, scriptSetup = true): string {
const block = scriptSetup
? this.descriptor.scriptSetup!
: this.descriptor.script!
return block.content.slice(node.start!, node.end!)
}
error(
msg: string,
node: Node,
end: number = node.end! + this.startOffset!
): never {
throw new Error(
`[@vue/compiler-sfc] ${msg}\n\n${
this.descriptor.filename
}\n${generateCodeFrame(
this.descriptor.source,
node.start! + this.startOffset!,
end
)}`
)
}
}

View File

@ -0,0 +1,148 @@
import {
Node,
LVal,
Identifier,
TSTypeLiteral,
TSInterfaceBody
} from '@babel/types'
import { isCallOf } from '@vue/compiler-dom'
import { ScriptCompileContext } from './context'
import { resolveObjectKey } from './utils'
import { resolveQualifiedType } from './resolveType'
export const DEFINE_PROPS = 'defineProps'
export const WITH_DEFAULTS = 'withDefaults'
export type PropsDeclType = (TSTypeLiteral | TSInterfaceBody) & {
__fromNormalScript?: boolean | null
}
export function processDefineProps(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal
) {
if (!isCallOf(node, DEFINE_PROPS)) {
return processWithDefaults(ctx, node, declId)
}
if (ctx.hasDefinePropsCall) {
ctx.error(`duplicate ${DEFINE_PROPS}() call`, node)
}
ctx.hasDefinePropsCall = true
ctx.propsRuntimeDecl = node.arguments[0]
// call has type parameters - infer runtime types from it
if (node.typeParameters) {
if (ctx.propsRuntimeDecl) {
ctx.error(
`${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`,
node
)
}
const rawDecl = node.typeParameters.params[0]
ctx.propsTypeDecl = resolveQualifiedType(
ctx,
rawDecl,
node => node.type === 'TSTypeLiteral'
) as PropsDeclType | undefined
if (!ctx.propsTypeDecl) {
ctx.error(
`type argument passed to ${DEFINE_PROPS}() must be a literal type, ` +
`or a reference to an interface or literal type.`,
rawDecl
)
}
}
if (declId) {
// handle props destructure
if (declId.type === 'ObjectPattern') {
ctx.propsDestructureDecl = declId
for (const prop of declId.properties) {
if (prop.type === 'ObjectProperty') {
const propKey = resolveObjectKey(prop.key, prop.computed)
if (!propKey) {
ctx.error(
`${DEFINE_PROPS}() destructure cannot use computed key.`,
prop.key
)
}
if (prop.value.type === 'AssignmentPattern') {
// default value { foo = 123 }
const { left, right } = prop.value
if (left.type !== 'Identifier') {
ctx.error(
`${DEFINE_PROPS}() destructure does not support nested patterns.`,
left
)
}
// store default value
ctx.propsDestructuredBindings[propKey] = {
local: left.name,
default: right
}
} else if (prop.value.type === 'Identifier') {
// simple destructure
ctx.propsDestructuredBindings[propKey] = {
local: prop.value.name
}
} else {
ctx.error(
`${DEFINE_PROPS}() destructure does not support nested patterns.`,
prop.value
)
}
} else {
// rest spread
ctx.propsDestructureRestId = (prop.argument as Identifier).name
}
}
} else {
ctx.propsIdentifier = ctx.getString(declId)
}
}
return true
}
function processWithDefaults(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal
): boolean {
if (!isCallOf(node, WITH_DEFAULTS)) {
return false
}
if (processDefineProps(ctx, node.arguments[0], declId)) {
if (ctx.propsRuntimeDecl) {
ctx.error(
`${WITH_DEFAULTS} can only be used with type-based ` +
`${DEFINE_PROPS} declaration.`,
node
)
}
if (ctx.propsDestructureDecl) {
ctx.error(
`${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
`Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...).`,
node.callee
)
}
ctx.propsRuntimeDefaults = node.arguments[1]
if (!ctx.propsRuntimeDefaults) {
ctx.error(`The 2nd argument of ${WITH_DEFAULTS} is required.`, node)
}
} else {
ctx.error(
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
node.arguments[0] || node
)
}
return true
}

View File

@ -0,0 +1,114 @@
import { Node, Statement, TSInterfaceBody, TSTypeElement } from '@babel/types'
import { FromNormalScript } from './utils'
import { ScriptCompileContext } from './context'
/**
* Resolve a type Node into
*/
export function resolveType() {}
export function resolveQualifiedType(
ctx: ScriptCompileContext,
node: Node,
qualifier: (node: Node) => boolean
): Node | undefined {
if (qualifier(node)) {
return node
}
if (node.type === 'TSTypeReference' && node.typeName.type === 'Identifier') {
const refName = node.typeName.name
const { scriptAst, scriptSetupAst } = ctx
const body = scriptAst
? [...scriptSetupAst!.body, ...scriptAst.body]
: scriptSetupAst!.body
for (let i = 0; i < body.length; i++) {
const node = body[i]
let qualified = isQualifiedType(
node,
qualifier,
refName
) as TSInterfaceBody
if (qualified) {
const extendsTypes = resolveExtendsType(body, node, qualifier)
if (extendsTypes.length) {
const bodies: TSTypeElement[] = [...qualified.body]
filterExtendsType(extendsTypes, bodies)
qualified.body = bodies
}
;(qualified as FromNormalScript<Node>).__fromNormalScript =
scriptAst && i >= scriptSetupAst!.body.length
return qualified
}
}
}
}
function isQualifiedType(
node: Node,
qualifier: (node: Node) => boolean,
refName: String
): Node | undefined {
if (node.type === 'TSInterfaceDeclaration' && node.id.name === refName) {
return node.body
} else if (
node.type === 'TSTypeAliasDeclaration' &&
node.id.name === refName &&
qualifier(node.typeAnnotation)
) {
return node.typeAnnotation
} else if (node.type === 'ExportNamedDeclaration' && node.declaration) {
return isQualifiedType(node.declaration, qualifier, refName)
}
}
function resolveExtendsType(
body: Statement[],
node: Node,
qualifier: (node: Node) => boolean,
cache: Array<Node> = []
): Array<Node> {
if (node.type === 'TSInterfaceDeclaration' && node.extends) {
node.extends.forEach(extend => {
if (
extend.type === 'TSExpressionWithTypeArguments' &&
extend.expression.type === 'Identifier'
) {
for (const node of body) {
const qualified = isQualifiedType(
node,
qualifier,
extend.expression.name
)
if (qualified) {
cache.push(qualified)
resolveExtendsType(body, node, qualifier, cache)
return cache
}
}
}
})
}
return cache
}
// filter all extends types to keep the override declaration
function filterExtendsType(extendsTypes: Node[], bodies: TSTypeElement[]) {
extendsTypes.forEach(extend => {
const body = (extend as TSInterfaceBody).body
body.forEach(newBody => {
if (
newBody.type === 'TSPropertySignature' &&
newBody.key.type === 'Identifier'
) {
const name = newBody.key.name
const hasOverride = bodies.some(
seenBody =>
seenBody.type === 'TSPropertySignature' &&
seenBody.key.type === 'Identifier' &&
seenBody.key.name === name
)
if (!hasOverride) bodies.push(newBody)
}
})
})
}

View File

@ -0,0 +1,14 @@
import { Node } from '@babel/types'
export type FromNormalScript<T> = T & { __fromNormalScript?: boolean | null }
export function resolveObjectKey(node: Node, computed: boolean) {
switch (node.type) {
case 'StringLiteral':
case 'NumericLiteral':
return node.value
case 'Identifier':
if (!computed) return node.name
}
return undefined
}