refactor(compiler-sfc): split all macros

This commit is contained in:
Evan You 2023-04-11 15:35:05 +08:00
parent c52157c87d
commit 3da1bb36b1
7 changed files with 222 additions and 192 deletions

View File

@ -18,8 +18,6 @@ import {
ExportSpecifier, ExportSpecifier,
Statement, Statement,
CallExpression, CallExpression,
AwaitExpression,
LVal,
TSEnumDeclaration TSEnumDeclaration
} from '@babel/types' } from '@babel/types'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
@ -46,16 +44,15 @@ import {
genRuntimeEmits, genRuntimeEmits,
DEFINE_EMITS DEFINE_EMITS
} from './script/defineEmits' } from './script/defineEmits'
import { DEFINE_EXPOSE, processDefineExpose } from './script/defineExpose'
import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions'
import { processDefineSlots } from './script/defineSlots'
import { DEFINE_MODEL, processDefineModel } from './script/defineModel' import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
import { isLiteralNode, unwrapTSNode, isCallOf } from './script/utils' import { isLiteralNode, unwrapTSNode, isCallOf } from './script/utils'
import { inferRuntimeType } from './script/resolveType' import { inferRuntimeType } from './script/resolveType'
import { analyzeScriptBindings } from './script/analyzeScriptBindings' import { analyzeScriptBindings } from './script/analyzeScriptBindings'
import { isImportUsed } from './script/importUsageCheck' import { isImportUsed } from './script/importUsageCheck'
import { processAwait } from './script/topLevelAwait'
// Special compiler macros
const DEFINE_EXPOSE = 'defineExpose'
const DEFINE_OPTIONS = 'defineOptions'
const DEFINE_SLOTS = 'defineSlots'
export interface SFCScriptCompileOptions { export interface SFCScriptCompileOptions {
/** /**
@ -250,30 +247,15 @@ 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 optionsRuntimeDecl: Node | undefined
let hasAwait = false let hasAwait = false
let hasInlinedSsrRenderFn = false let hasInlinedSsrRenderFn = false
// magic-string state // string offsets
const startOffset = scriptSetup.loc.start.offset const startOffset = ctx.startOffset!
const endOffset = scriptSetup.loc.end.offset const endOffset = ctx.endOffset!
const scriptStartOffset = script && script.loc.start.offset const scriptStartOffset = script && script.loc.start.offset
const scriptEndOffset = script && script.loc.end.offset const scriptEndOffset = script && script.loc.end.offset
function error(
msg: string,
node: Node,
end: number = node.end! + startOffset
): never {
throw new Error(
`[@vue/compiler-sfc] ${msg}\n\n${sfc.filename}\n${generateCodeFrame(
source,
node.start! + startOffset,
end
)}`
)
}
function hoistNode(node: Statement) { function hoistNode(node: Statement) {
const start = node.start! + startOffset const start = node.start! + startOffset
let end = node.end! + startOffset let end = node.end! + startOffset
@ -324,108 +306,12 @@ export function compileScript(
} }
} }
function processDefineSlots(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, DEFINE_SLOTS)) {
return false
}
if (ctx.hasDefineSlotsCall) {
error(`duplicate ${DEFINE_SLOTS}() call`, node)
}
ctx.hasDefineSlotsCall = true
if (node.arguments.length > 0) {
error(`${DEFINE_SLOTS}() cannot accept arguments`, node)
}
if (declId) {
ctx.s.overwrite(
startOffset + node.start!,
startOffset + node.end!,
`${ctx.helper('useSlots')}()`
)
}
return true
}
function processDefineOptions(node: Node): boolean {
if (!isCallOf(node, DEFINE_OPTIONS)) {
return false
}
if (ctx.hasDefineOptionsCall) {
error(`duplicate ${DEFINE_OPTIONS}() call`, node)
}
if (node.typeParameters) {
error(`${DEFINE_OPTIONS}() cannot accept type arguments`, node)
}
if (!node.arguments[0]) return true
ctx.hasDefineOptionsCall = true
optionsRuntimeDecl = unwrapTSNode(node.arguments[0])
let propsOption = undefined
let emitsOption = undefined
let exposeOption = undefined
let slotsOption = undefined
if (optionsRuntimeDecl.type === 'ObjectExpression') {
for (const prop of optionsRuntimeDecl.properties) {
if (
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
prop.key.type === 'Identifier'
) {
if (prop.key.name === 'props') propsOption = prop
if (prop.key.name === 'emits') emitsOption = prop
if (prop.key.name === 'expose') exposeOption = prop
if (prop.key.name === 'slots') slotsOption = prop
}
}
}
if (propsOption) {
error(
`${DEFINE_OPTIONS}() cannot be used to declare props. Use ${DEFINE_PROPS}() instead.`,
propsOption
)
}
if (emitsOption) {
error(
`${DEFINE_OPTIONS}() cannot be used to declare emits. Use ${DEFINE_EMITS}() instead.`,
emitsOption
)
}
if (exposeOption) {
error(
`${DEFINE_OPTIONS}() cannot be used to declare expose. Use ${DEFINE_EXPOSE}() instead.`,
exposeOption
)
}
if (slotsOption) {
error(
`${DEFINE_OPTIONS}() cannot be used to declare slots. Use ${DEFINE_SLOTS}() instead.`,
slotsOption
)
}
return true
}
function processDefineExpose(node: Node): boolean {
if (isCallOf(node, DEFINE_EXPOSE)) {
if (ctx.hasDefineExposeCall) {
error(`duplicate ${DEFINE_EXPOSE}() call`, node)
}
ctx.hasDefineExposeCall = true
return true
}
return false
}
function checkInvalidScopeReference(node: Node | undefined, method: string) { function checkInvalidScopeReference(node: Node | undefined, method: string) {
if (!node) return if (!node) return
walkIdentifiers(node, id => { walkIdentifiers(node, id => {
const binding = setupBindings[id.name] const binding = setupBindings[id.name]
if (binding && binding !== BindingTypes.LITERAL_CONST) { if (binding && binding !== BindingTypes.LITERAL_CONST) {
error( ctx.error(
`\`${method}()\` in <script setup> cannot reference locally ` + `\`${method}()\` 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 require initialization ` + `setup() function. If your component options require initialization ` +
@ -437,56 +323,6 @@ export function compileScript(
}) })
} }
/**
* await foo()
* -->
* ;(
* ([__temp,__restore] = withAsyncContext(() => foo())),
* await __temp,
* __restore()
* )
*
* const a = await foo()
* -->
* const a = (
* ([__temp, __restore] = withAsyncContext(() => foo())),
* __temp = await __temp,
* __restore(),
* __temp
* )
*/
function processAwait(
node: AwaitExpression,
needSemi: boolean,
isStatement: boolean
) {
const argumentStart =
node.argument.extra && node.argument.extra.parenthesized
? (node.argument.extra.parenStart as number)
: node.argument.start!
const argumentStr = source.slice(
argumentStart + startOffset,
node.argument.end! + startOffset
)
const containsNestedAwait = /\bawait\b/.test(argumentStr)
ctx.s.overwrite(
node.start! + startOffset,
argumentStart + startOffset,
`${needSemi ? `;` : ``}(\n ([__temp,__restore] = ${ctx.helper(
`withAsyncContext`
)}(${containsNestedAwait ? `async ` : ``}() => `
)
ctx.s.appendLeft(
node.end! + startOffset,
`)),\n ${isStatement ? `` : `__temp = `}await __temp,\n __restore()${
isStatement ? `` : `,\n __temp`
}\n)`
)
}
const scriptAst = ctx.scriptAst const scriptAst = ctx.scriptAst
const scriptSetupAst = ctx.scriptSetupAst! const scriptSetupAst = ctx.scriptSetupAst!
@ -556,7 +392,10 @@ export function compileScript(
// already imported in <script setup>, dedupe // already imported in <script setup>, dedupe
removeSpecifier(i) removeSpecifier(i)
} else { } else {
error(`different imports aliased to same local name.`, specifier) ctx.error(
`different imports aliased to same local name.`,
specifier
)
} }
} else { } else {
registerUserImport( registerUserImport(
@ -725,7 +564,7 @@ export function compileScript(
node.label.name === 'ref' && node.label.name === 'ref' &&
node.body.type === 'ExpressionStatement' node.body.type === 'ExpressionStatement'
) { ) {
error( ctx.error(
`ref sugar using the label syntax was an experimental proposal and ` + `ref sugar using the label syntax was an experimental proposal and ` +
`has been dropped based on community feedback. Please check out ` + `has been dropped based on community feedback. Please check out ` +
`the new proposal at https://github.com/vuejs/rfcs/discussions/369`, `the new proposal at https://github.com/vuejs/rfcs/discussions/369`,
@ -739,11 +578,11 @@ export function compileScript(
if ( if (
processDefineProps(ctx, expr) || processDefineProps(ctx, expr) ||
processDefineEmits(ctx, expr) || processDefineEmits(ctx, expr) ||
processDefineOptions(expr) || processDefineOptions(ctx, expr) ||
processDefineSlots(expr) processDefineSlots(ctx, expr)
) { ) {
ctx.s.remove(node.start! + startOffset, node.end! + startOffset) ctx.s.remove(node.start! + startOffset, node.end! + startOffset)
} else if (processDefineExpose(expr)) { } else if (processDefineExpose(ctx, expr)) {
// defineExpose({}) -> expose({}) // defineExpose({}) -> expose({})
const callee = (expr as CallExpression).callee const callee = (expr as CallExpression).callee
ctx.s.overwrite( ctx.s.overwrite(
@ -765,8 +604,8 @@ export function compileScript(
const decl = node.declarations[i] const decl = node.declarations[i]
const init = decl.init && unwrapTSNode(decl.init) const init = decl.init && unwrapTSNode(decl.init)
if (init) { if (init) {
if (processDefineOptions(init)) { if (processDefineOptions(ctx, init)) {
error( ctx.error(
`${DEFINE_OPTIONS}() has no returning value, it cannot be assigned.`, `${DEFINE_OPTIONS}() has no returning value, it cannot be assigned.`,
node node
) )
@ -777,7 +616,7 @@ export function compileScript(
const isDefineEmits = const isDefineEmits =
!isDefineProps && processDefineEmits(ctx, init, decl.id) !isDefineProps && processDefineEmits(ctx, init, decl.id)
!isDefineEmits && !isDefineEmits &&
(processDefineSlots(init, decl.id) || (processDefineSlots(ctx, init, decl.id) ||
processDefineModel(ctx, init, decl.id)) processDefineModel(ctx, init, decl.id))
if (isDefineProps || isDefineEmits) { if (isDefineProps || isDefineEmits) {
@ -858,6 +697,7 @@ export function compileScript(
) )
}) })
processAwait( processAwait(
ctx,
child, child,
needsSemi, needsSemi,
parent.type === 'ExpressionStatement' parent.type === 'ExpressionStatement'
@ -875,7 +715,7 @@ export function compileScript(
node.type === 'ExportAllDeclaration' || node.type === 'ExportAllDeclaration' ||
node.type === 'ExportDefaultDeclaration' node.type === 'ExportDefaultDeclaration'
) { ) {
error( ctx.error(
`<script setup> cannot contain ES module exports. ` + `<script setup> cannot contain ES module exports. ` +
`If you are using a previous version of <script setup>, please ` + `If you are using a previous version of <script setup>, please ` +
`consult the updated RFC at https://github.com/vuejs/rfcs/pull/227.`, `consult the updated RFC at https://github.com/vuejs/rfcs/pull/227.`,
@ -929,7 +769,7 @@ export function compileScript(
checkInvalidScopeReference(ctx.propsRuntimeDefaults, DEFINE_PROPS) checkInvalidScopeReference(ctx.propsRuntimeDefaults, DEFINE_PROPS)
checkInvalidScopeReference(ctx.propsDestructureDecl, DEFINE_PROPS) checkInvalidScopeReference(ctx.propsDestructureDecl, DEFINE_PROPS)
checkInvalidScopeReference(ctx.emitsRuntimeDecl, DEFINE_EMITS) checkInvalidScopeReference(ctx.emitsRuntimeDecl, DEFINE_EMITS)
checkInvalidScopeReference(optionsRuntimeDecl, DEFINE_OPTIONS) checkInvalidScopeReference(ctx.optionsRuntimeDecl, DEFINE_OPTIONS)
// 6. remove non-script content // 6. remove non-script content
if (script) { if (script) {
@ -1171,9 +1011,9 @@ export function compileScript(
if (emitsDecl) runtimeOptions += `\n emits: ${emitsDecl},` if (emitsDecl) runtimeOptions += `\n emits: ${emitsDecl},`
let definedOptions = '' let definedOptions = ''
if (optionsRuntimeDecl) { if (ctx.optionsRuntimeDecl) {
definedOptions = scriptSetup.content definedOptions = scriptSetup.content
.slice(optionsRuntimeDecl.start!, optionsRuntimeDecl.end!) .slice(ctx.optionsRuntimeDecl.start!, ctx.optionsRuntimeDecl.end!)
.trim() .trim()
} }

View File

@ -19,8 +19,6 @@ export class ScriptCompileContext {
s = new MagicString(this.descriptor.source) s = new MagicString(this.descriptor.source)
startOffset = this.descriptor.scriptSetup?.loc.start.offset startOffset = this.descriptor.scriptSetup?.loc.start.offset
endOffset = this.descriptor.scriptSetup?.loc.end.offset endOffset = this.descriptor.scriptSetup?.loc.end.offset
scriptStartOffset = this.descriptor.script?.loc.start.offset
scriptEndOffset = this.descriptor.script?.loc.end.offset
declaredTypes: Record<string, string[]> = Object.create(null) declaredTypes: Record<string, string[]> = Object.create(null)
@ -51,6 +49,9 @@ export class ScriptCompileContext {
// defineModel // defineModel
modelDecls: Record<string, ModelDecl> = {} modelDecls: Record<string, ModelDecl> = {}
// defineOptions
optionsRuntimeDecl: Node | undefined
// codegen // codegen
bindingMetadata: BindingMetadata = {} bindingMetadata: BindingMetadata = {}
@ -125,7 +126,7 @@ export class ScriptCompileContext {
plugins, plugins,
sourceType: 'module' sourceType: 'module'
}, },
this.scriptStartOffset! this.descriptor.script.loc.start.offset
) )
this.scriptSetupAst = this.scriptSetupAst =

View File

@ -0,0 +1,19 @@
import { Node } from '@babel/types'
import { isCallOf } from './utils'
import { ScriptCompileContext } from './context'
export const DEFINE_EXPOSE = 'defineExpose'
export function processDefineExpose(
ctx: ScriptCompileContext,
node: Node
): boolean {
if (isCallOf(node, DEFINE_EXPOSE)) {
if (ctx.hasDefineExposeCall) {
ctx.error(`duplicate ${DEFINE_EXPOSE}() call`, node)
}
ctx.hasDefineExposeCall = true
return true
}
return false
}

View File

@ -0,0 +1,73 @@
import { Node } from '@babel/types'
import { ScriptCompileContext } from './context'
import { isCallOf, unwrapTSNode } from './utils'
import { DEFINE_PROPS } from './defineProps'
import { DEFINE_EMITS } from './defineEmits'
import { DEFINE_EXPOSE } from './defineExpose'
import { DEFINE_SLOTS } from './defineSlots'
export const DEFINE_OPTIONS = 'defineOptions'
export function processDefineOptions(
ctx: ScriptCompileContext,
node: Node
): boolean {
if (!isCallOf(node, DEFINE_OPTIONS)) {
return false
}
if (ctx.hasDefineOptionsCall) {
ctx.error(`duplicate ${DEFINE_OPTIONS}() call`, node)
}
if (node.typeParameters) {
ctx.error(`${DEFINE_OPTIONS}() cannot accept type arguments`, node)
}
if (!node.arguments[0]) return true
ctx.hasDefineOptionsCall = true
ctx.optionsRuntimeDecl = unwrapTSNode(node.arguments[0])
let propsOption = undefined
let emitsOption = undefined
let exposeOption = undefined
let slotsOption = undefined
if (ctx.optionsRuntimeDecl.type === 'ObjectExpression') {
for (const prop of ctx.optionsRuntimeDecl.properties) {
if (
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
prop.key.type === 'Identifier'
) {
if (prop.key.name === 'props') propsOption = prop
if (prop.key.name === 'emits') emitsOption = prop
if (prop.key.name === 'expose') exposeOption = prop
if (prop.key.name === 'slots') slotsOption = prop
}
}
}
if (propsOption) {
ctx.error(
`${DEFINE_OPTIONS}() cannot be used to declare props. Use ${DEFINE_PROPS}() instead.`,
propsOption
)
}
if (emitsOption) {
ctx.error(
`${DEFINE_OPTIONS}() cannot be used to declare emits. Use ${DEFINE_EMITS}() instead.`,
emitsOption
)
}
if (exposeOption) {
ctx.error(
`${DEFINE_OPTIONS}() cannot be used to declare expose. Use ${DEFINE_EXPOSE}() instead.`,
exposeOption
)
}
if (slotsOption) {
ctx.error(
`${DEFINE_OPTIONS}() cannot be used to declare slots. Use ${DEFINE_SLOTS}() instead.`,
slotsOption
)
}
return true
}

View File

@ -0,0 +1,33 @@
import { LVal, Node } from '@babel/types'
import { isCallOf } from './utils'
import { ScriptCompileContext } from './context'
export const DEFINE_SLOTS = 'defineSlots'
export function processDefineSlots(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal
): boolean {
if (!isCallOf(node, DEFINE_SLOTS)) {
return false
}
if (ctx.hasDefineSlotsCall) {
ctx.error(`duplicate ${DEFINE_SLOTS}() call`, node)
}
ctx.hasDefineSlotsCall = true
if (node.arguments.length > 0) {
ctx.error(`${DEFINE_SLOTS}() cannot accept arguments`, node)
}
if (declId) {
ctx.s.overwrite(
ctx.startOffset! + node.start!,
ctx.startOffset! + node.end!,
`${ctx.helper('useSlots')}()`
)
}
return true
}

View File

@ -8,11 +8,6 @@ import {
import { FromNormalScript, UNKNOWN_TYPE } from './utils' import { FromNormalScript, UNKNOWN_TYPE } from './utils'
import { ScriptCompileContext } from './context' import { ScriptCompileContext } from './context'
/**
* Resolve a type Node into
*/
export function resolveType() {}
export function resolveQualifiedType( export function resolveQualifiedType(
ctx: ScriptCompileContext, ctx: ScriptCompileContext,
node: Node, node: Node,

View File

@ -0,0 +1,69 @@
import { AwaitExpression } from '@babel/types'
import { ScriptCompileContext } from './context'
/**
* Support context-persistence between top-level await expressions:
*
* ```js
* const instance = getCurrentInstance()
* await foo()
* expect(getCurrentInstance()).toBe(instance)
* ```
*
* In the future we can potentially get rid of this when Async Context
* becomes generally available: https://github.com/tc39/proposal-async-context
*
* ```js
* // input
* await foo()
* // output
* ;(
* ([__temp,__restore] = withAsyncContext(() => foo())),
* await __temp,
* __restore()
* )
*
* // input
* const a = await foo()
* // output
* const a = (
* ([__temp, __restore] = withAsyncContext(() => foo())),
* __temp = await __temp,
* __restore(),
* __temp
* )
* ```
*/
export function processAwait(
ctx: ScriptCompileContext,
node: AwaitExpression,
needSemi: boolean,
isStatement: boolean
) {
const argumentStart =
node.argument.extra && node.argument.extra.parenthesized
? (node.argument.extra.parenStart as number)
: node.argument.start!
const startOffset = ctx.startOffset!
const argumentStr = ctx.descriptor.source.slice(
argumentStart + startOffset,
node.argument.end! + startOffset
)
const containsNestedAwait = /\bawait\b/.test(argumentStr)
ctx.s.overwrite(
node.start! + startOffset,
argumentStart + startOffset,
`${needSemi ? `;` : ``}(\n ([__temp,__restore] = ${ctx.helper(
`withAsyncContext`
)}(${containsNestedAwait ? `async ` : ``}() => `
)
ctx.s.appendLeft(
node.end! + startOffset,
`)),\n ${isStatement ? `` : `__temp = `}await __temp,\n __restore()${
isStatement ? `` : `,\n __temp`
}\n)`
)
}