vue3-core/packages/compiler-sfc/src/compileScript.ts

1966 lines
58 KiB
TypeScript
Raw Normal View History

import MagicString from 'magic-string'
import {
BindingMetadata,
BindingTypes,
createRoot,
NodeTypes,
transform,
parserOptions,
UNREF,
SimpleExpressionNode,
isFunctionType,
walkIdentifiers,
getImportedName
} from '@vue/compiler-dom'
import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
import { parse as _parse, parseExpression, ParserPlugin } from '@babel/parser'
import { camelize, capitalize, generateCodeFrame, makeMap } from '@vue/shared'
import {
Node,
Declaration,
ObjectPattern,
ObjectExpression,
ArrayPattern,
Identifier,
2020-07-09 05:21:39 +08:00
ExportSpecifier,
TSType,
ArrayExpression,
Statement,
CallExpression,
AwaitExpression,
LVal,
TSEnumDeclaration
} from '@babel/types'
import { walk } from 'estree-walker'
2023-04-09 16:50:00 +08:00
import { RawSourceMap } from 'source-map-js'
import {
CSS_VARS_HELPER,
genCssVarsCode,
genNormalScriptCssVarsCode
} from './style/cssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
2021-08-24 22:26:38 +08:00
import { warnOnce } from './warn'
import { rewriteDefaultAST } from './rewriteDefault'
import { createCache } from './cache'
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
import { transformDestructuredProps } from './script/definePropsDestructure'
import { ScriptCompileContext } from './script/context'
import {
processDefineProps,
genRuntimeProps,
DEFINE_PROPS,
WITH_DEFAULTS
} from './script/defineProps'
import {
processDefineEmits,
genRuntimeEmits,
DEFINE_EMITS
} from './script/defineEmits'
import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
import {
resolveObjectKey,
UNKNOWN_TYPE,
isLiteralNode,
unwrapTSNode,
isCallOf
} from './script/utils'
2020-07-07 03:56:24 +08:00
// Special compiler macros
2021-06-26 01:14:49 +08:00
const DEFINE_EXPOSE = 'defineExpose'
const DEFINE_OPTIONS = 'defineOptions'
const DEFINE_SLOTS = 'defineSlots'
2021-06-27 09:11:57 +08:00
const isBuiltInDir = makeMap(
`once,memo,if,for,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
)
export interface SFCScriptCompileOptions {
/**
* Scope ID for prefixing injected CSS variables.
* This must be consistent with the `id` passed to `compileStyle`.
*/
id: string
2020-11-18 04:58:46 +08:00
/**
* Production mode. Used to determine whether to generate hashed CSS variables
*/
isProd?: boolean
/**
* Enable/disable source map. Defaults to true.
*/
sourceMap?: boolean
2020-07-10 11:06:11 +08:00
/**
* https://babeljs.io/docs/en/babel-parser#plugins
*/
babelParserPlugins?: ParserPlugin[]
/**
* (Experimental) Enable syntax transform for using refs without `.value` and
* using destructured props with reactivity
* @deprecated the Reactivity Transform proposal has been dropped. This
* feature will be removed from Vue core in 3.4. If you intend to continue
* using it, disable this and switch to the [Vue Macros implementation](https://vue-macros.sxzz.moe/features/reactivity-transform.html).
*/
reactivityTransform?: boolean
/**
* Compile the template and inline the resulting render function
* directly inside setup().
* - Only affects `<script setup>`
* - This should only be used in production because it prevents the template
* from being hot-reloaded separately from component state.
*/
inlineTemplate?: boolean
/**
* Generate the final component as a variable instead of default export.
* This is useful in e.g. @vitejs/plugin-vue where the script needs to be
* placed inside the main module.
*/
genDefaultAs?: string
/**
* Options for template compilation when inlining. Note these are options that
* would normally be passed to `compiler-sfc`'s own `compileTemplate()`, not
* options passed to `compiler-dom`.
*/
templateOptions?: Partial<SFCTemplateCompileOptions>
/**
* Hoist <script setup> static constants.
* - Only enables when one `<script setup>` exists.
* @default true
*/
hoistStatic?: boolean
/**
* (**Experimental**) Enable macro `defineModel`
*/
defineModel?: boolean
}
export interface ImportBinding {
isType: boolean
imported: string
local: string
source: string
isFromSetup: boolean
isUsedInTemplate: boolean
}
2020-07-07 03:56:24 +08:00
/**
* Compile `<script setup>`
* It requires the whole SFC descriptor because we need to handle and merge
* normal `<script>` + `<script setup>` if both are present.
*/
export function compileScript(
2020-07-07 03:56:24 +08:00
sfc: SFCDescriptor,
options: SFCScriptCompileOptions
): SFCScriptBlock {
let { script, scriptSetup, source, filename } = sfc
// feature flags
// TODO remove in 3.4
const enableReactivityTransform = !!options.reactivityTransform
const isProd = !!options.isProd
const genSourceMap = options.sourceMap !== false
const hoistStatic = options.hoistStatic !== false && !script
let refBindings: string[] | undefined
if (!options.id) {
warnOnce(
`compileScript now requires passing the \`id\` option.\n` +
`Upgrade your vite or vue-loader version for compatibility with ` +
`the latest experimental proposals.`
)
}
const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
const cssVars = sfc.cssVars
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
const genDefaultAs = options.genDefaultAs
? `const ${options.genDefaultAs} =`
: `export default`
const normalScriptDefaultVar = `__default__`
const ctx = new ScriptCompileContext(sfc, options)
const { isTS } = ctx
2020-07-07 03:56:24 +08:00
if (!scriptSetup) {
if (!script) {
throw new Error(`[@vue/compiler-sfc] SFC contains no <script> tags.`)
}
if (scriptLang && !ctx.isJS && !ctx.isTS) {
// do not process non js/ts script blocks
return script
}
// normal <script> only
try {
let content = script.content
let map = script.map
const scriptAst = ctx.scriptAst!
const bindings = analyzeScriptBindings(scriptAst.body)
if (enableReactivityTransform && shouldTransform(content)) {
const s = new MagicString(source)
const startOffset = script.loc.start.offset
const endOffset = script.loc.end.offset
const { importedHelpers } = transformAST(scriptAst, s, startOffset)
if (importedHelpers.length) {
s.prepend(
`import { ${importedHelpers
.map(h => `${h} as _${h}`)
.join(', ')} } from 'vue'\n`
)
}
s.remove(0, startOffset)
s.remove(endOffset, source.length)
content = s.toString()
if (genSourceMap) {
map = s.generateMap({
source: filename,
hires: true,
includeContent: true
}) as unknown as RawSourceMap
}
}
if (cssVars.length || options.genDefaultAs) {
const defaultVar = options.genDefaultAs || normalScriptDefaultVar
const s = new MagicString(content)
rewriteDefaultAST(ctx.scriptAst!.body, s, defaultVar)
content = s.toString()
if (cssVars.length) {
content += genNormalScriptCssVarsCode(
cssVars,
bindings,
scopeId,
isProd,
defaultVar
)
}
if (!options.genDefaultAs) {
content += `\nexport default ${defaultVar}`
}
}
return {
...script,
content,
map,
bindings,
scriptAst: scriptAst.body
}
} catch (e: any) {
// silently fallback if parse fails since user may be using custom
// babel syntax
return script
}
2020-07-07 03:56:24 +08:00
}
if (script && scriptLang !== scriptSetupLang) {
2020-07-07 03:56:24 +08:00
throw new Error(
`[@vue/compiler-sfc] <script> and <script setup> must have the same ` +
`language type.`
2020-07-07 03:56:24 +08:00
)
}
if (scriptSetupLang && !ctx.isJS && !ctx.isTS) {
// do not process non js/ts script blocks
return scriptSetup
}
// metadata that needs to be returned
// const ctx.bindingMetadata: BindingMetadata = {}
const userImports: Record<string, ImportBinding> = Object.create(null)
const scriptBindings: Record<string, BindingTypes> = Object.create(null)
const setupBindings: Record<string, BindingTypes> = Object.create(null)
let defaultExport: Node | undefined
let optionsRuntimeDecl: Node | undefined
let hasAwait = false
let hasInlinedSsrRenderFn = false
2020-07-09 09:11:57 +08:00
2020-11-14 06:38:28 +08:00
// magic-string state
2020-07-07 03:56:24 +08:00
const startOffset = scriptSetup.loc.start.offset
const endOffset = scriptSetup.loc.end.offset
const scriptStartOffset = script && script.loc.start.offset
const scriptEndOffset = script && script.loc.end.offset
2020-07-07 03:56:24 +08:00
function error(
msg: string,
node: Node,
end: number = node.end! + startOffset
): never {
throw new Error(
2020-11-20 01:17:39 +08:00
`[@vue/compiler-sfc] ${msg}\n\n${sfc.filename}\n${generateCodeFrame(
source,
node.start! + startOffset,
end
)}`
)
}
function hoistNode(node: Statement) {
const start = node.start! + startOffset
let end = node.end! + startOffset
// locate comment
if (node.trailingComments && node.trailingComments.length > 0) {
const lastCommentNode =
node.trailingComments[node.trailingComments.length - 1]
2023-02-03 05:47:40 +08:00
end = lastCommentNode.end! + startOffset
}
// locate the end of whitespace between this statement and the next
while (end <= source.length) {
if (!/\s/.test(source.charAt(end))) {
break
}
end++
}
ctx.s.move(start, end, 0)
}
function registerUserImport(
source: string,
local: string,
imported: string,
isType: boolean,
isFromSetup: boolean,
needTemplateUsageCheck: boolean
) {
// template usage check is only needed in non-inline mode, so we can skip
// the work if inlineTemplate is true.
let isUsedInTemplate = needTemplateUsageCheck
if (
needTemplateUsageCheck &&
isTS &&
sfc.template &&
!sfc.template.src &&
!sfc.template.lang
) {
isUsedInTemplate = isImportUsed(local, sfc)
}
userImports[local] = {
2020-11-20 01:17:39 +08:00
isType,
imported,
local,
source,
isFromSetup,
isUsedInTemplate
}
}
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
}
2021-06-26 01:14:49 +08:00
function processDefineExpose(node: Node): boolean {
if (isCallOf(node, DEFINE_EXPOSE)) {
if (ctx.hasDefineExposeCall) {
2021-06-26 01:14:49 +08:00
error(`duplicate ${DEFINE_EXPOSE}() call`, node)
}
ctx.hasDefineExposeCall = true
2021-06-26 01:14:49 +08:00
return true
}
return false
}
function checkInvalidScopeReference(node: Node | undefined, method: string) {
if (!node) return
walkIdentifiers(node, id => {
const binding = setupBindings[id.name]
if (binding && binding !== BindingTypes.LITERAL_CONST) {
error(
`\`${method}()\` in <script setup> cannot reference locally ` +
`declared variables because it will be hoisted outside of the ` +
`setup() function. If your component options require initialization ` +
`in the module scope, use a separate normal <script> to export ` +
`the options instead.`,
id
)
}
})
}
/**
* 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 scriptSetupAst = ctx.scriptSetupAst!
// 1.1 walk import delcarations of <script>
if (scriptAst) {
for (const node of scriptAst.body) {
if (node.type === 'ImportDeclaration') {
// record imports for dedupe
2020-11-12 08:40:27 +08:00
for (const specifier of node.specifiers) {
const imported = getImportedName(specifier)
2020-11-20 01:17:39 +08:00
registerUserImport(
node.source.value,
specifier.local.name,
imported,
node.importKind === 'type' ||
(specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type'),
false,
!options.inlineTemplate
2020-11-20 01:17:39 +08:00
)
}
}
}
}
// 1.2 walk import declarations of <script setup>
for (const node of scriptSetupAst.body) {
if (node.type === 'ImportDeclaration') {
// import declarations are moved to top
hoistNode(node)
// dedupe imports
let removed = 0
const removeSpecifier = (i: number) => {
const removeLeft = i > removed
removed++
const current = node.specifiers[i]
const next = node.specifiers[i + 1]
ctx.s.remove(
removeLeft
? node.specifiers[i - 1].end! + startOffset
: current.start! + startOffset,
next && !removeLeft
? next.start! + startOffset
: current.end! + startOffset
)
}
for (let i = 0; i < node.specifiers.length; i++) {
const specifier = node.specifiers[i]
const local = specifier.local.name
const imported = getImportedName(specifier)
const source = node.source.value
const existing = userImports[local]
if (
source === 'vue' &&
(imported === DEFINE_PROPS ||
imported === DEFINE_EMITS ||
imported === DEFINE_EXPOSE)
) {
warnOnce(
`\`${imported}\` is a compiler macro and no longer needs to be imported.`
)
removeSpecifier(i)
} else if (existing) {
if (existing.source === source && existing.imported === imported) {
// already imported in <script setup>, dedupe
removeSpecifier(i)
} else {
error(`different imports aliased to same local name.`, specifier)
}
} else {
registerUserImport(
source,
local,
imported,
node.importKind === 'type' ||
(specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type'),
true,
!options.inlineTemplate
)
}
}
if (node.specifiers.length && removed === node.specifiers.length) {
ctx.s.remove(node.start! + startOffset, node.end! + startOffset)
}
}
}
// 1.3 resolve possible user import alias of `ref` and `reactive`
const vueImportAliases: Record<string, string> = {}
for (const key in userImports) {
const { source, imported, local } = userImports[key]
if (source === 'vue') vueImportAliases[imported] = local
}
// 2.1 process normal <script> body
if (script && scriptAst) {
for (const node of scriptAst.body) {
if (node.type === 'ExportDefaultDeclaration') {
// export default
defaultExport = node
// check if user has manually specified `name` or 'render` option in
// export default
// if has name, skip name inference
// if has render and no template, generate return object instead of
// empty render function (#4980)
let optionProperties
if (defaultExport.declaration.type === 'ObjectExpression') {
optionProperties = defaultExport.declaration.properties
} else if (
defaultExport.declaration.type === 'CallExpression' &&
defaultExport.declaration.arguments[0] &&
defaultExport.declaration.arguments[0].type === 'ObjectExpression'
) {
optionProperties = defaultExport.declaration.arguments[0].properties
}
if (optionProperties) {
for (const p of optionProperties) {
if (
p.type === 'ObjectProperty' &&
p.key.type === 'Identifier' &&
p.key.name === 'name'
) {
ctx.hasDefaultExportName = true
}
if (
(p.type === 'ObjectMethod' || p.type === 'ObjectProperty') &&
p.key.type === 'Identifier' &&
p.key.name === 'render'
) {
// TODO warn when we provide a better way to do it?
ctx.hasDefaultExportRender = true
}
}
}
// export default { ... } --> const __default__ = { ... }
const start = node.start! + scriptStartOffset!
const end = node.declaration.start! + scriptStartOffset!
ctx.s.overwrite(start, end, `const ${normalScriptDefaultVar} = `)
} else if (node.type === 'ExportNamedDeclaration') {
2020-07-09 05:21:39 +08:00
const defaultSpecifier = node.specifiers.find(
s => s.exported.type === 'Identifier' && s.exported.name === 'default'
2020-07-09 05:21:39 +08:00
) as ExportSpecifier
if (defaultSpecifier) {
defaultExport = node
// 1. remove specifier
if (node.specifiers.length > 1) {
ctx.s.remove(
2020-07-09 05:21:39 +08:00
defaultSpecifier.start! + scriptStartOffset!,
defaultSpecifier.end! + scriptStartOffset!
)
} else {
ctx.s.remove(
2020-07-09 05:21:39 +08:00
node.start! + scriptStartOffset!,
node.end! + scriptStartOffset!
)
}
if (node.source) {
// export { x as default } from './x'
// rewrite to `import { x as __default__ } from './x'` and
// add to top
ctx.s.prepend(
`import { ${defaultSpecifier.local.name} as ${normalScriptDefaultVar} } from '${node.source.value}'\n`
2020-07-09 05:21:39 +08:00
)
} else {
// export { x as default }
// rewrite to `const __default__ = x` and move to end
ctx.s.appendLeft(
scriptEndOffset!,
`\nconst ${normalScriptDefaultVar} = ${defaultSpecifier.local.name}\n`
2020-07-10 11:06:11 +08:00
)
2020-07-09 05:21:39 +08:00
}
}
if (node.declaration) {
walkDeclaration(
'script',
node.declaration,
scriptBindings,
vueImportAliases,
hoistStatic
)
}
} else if (
(node.type === 'VariableDeclaration' ||
node.type === 'FunctionDeclaration' ||
node.type === 'ClassDeclaration' ||
node.type === 'TSEnumDeclaration') &&
!node.declare
) {
walkDeclaration(
'script',
node,
scriptBindings,
vueImportAliases,
hoistStatic
)
}
}
// apply reactivity transform
// TODO remove in 3.4
if (enableReactivityTransform && shouldTransform(script.content)) {
const { rootRefs, importedHelpers } = transformAST(
scriptAst,
ctx.s,
scriptStartOffset!
)
refBindings = rootRefs
for (const h of importedHelpers) {
ctx.helperImports.add(h)
}
}
// <script> after <script setup>
// we need to move the block up so that `const __default__` is
// declared before being used in the actual component definition
if (scriptStartOffset! > startOffset) {
2022-05-13 11:04:04 +08:00
// if content doesn't end with newline, add one
if (!/\n$/.test(script.content.trim())) {
ctx.s.appendLeft(scriptEndOffset!, `\n`)
}
ctx.s.move(scriptStartOffset!, scriptEndOffset!, 0)
}
}
// 2.2 process <script setup> body
for (const node of scriptSetupAst.body) {
// (Dropped) `ref: x` bindings
// TODO remove when out of experimental
if (
node.type === 'LabeledStatement' &&
node.label.name === 'ref' &&
node.body.type === 'ExpressionStatement'
) {
error(
`ref sugar using the label syntax was an experimental proposal and ` +
`has been dropped based on community feedback. Please check out ` +
`the new proposal at https://github.com/vuejs/rfcs/discussions/369`,
node
)
}
2021-06-26 01:14:49 +08:00
if (node.type === 'ExpressionStatement') {
const expr = unwrapTSNode(node.expression)
2021-06-26 01:14:49 +08:00
// process `defineProps` and `defineEmit(s)` calls
if (
processDefineProps(ctx, expr) ||
processDefineEmits(ctx, expr) ||
processDefineOptions(expr) ||
processDefineSlots(expr)
2021-06-26 01:14:49 +08:00
) {
ctx.s.remove(node.start! + startOffset, node.end! + startOffset)
} else if (processDefineExpose(expr)) {
2021-06-26 01:14:49 +08:00
// defineExpose({}) -> expose({})
const callee = (expr as CallExpression).callee
ctx.s.overwrite(
2021-06-26 01:14:49 +08:00
callee.start! + startOffset,
callee.end! + startOffset,
'__expose'
2021-06-26 01:14:49 +08:00
)
} else {
processDefineModel(ctx, expr)
2021-06-26 01:14:49 +08:00
}
2020-11-13 07:11:25 +08:00
}
2021-06-26 01:14:49 +08:00
2020-11-12 08:40:27 +08:00
if (node.type === 'VariableDeclaration' && !node.declare) {
const total = node.declarations.length
let left = total
let lastNonRemoved: number | undefined
for (let i = 0; i < total; i++) {
const decl = node.declarations[i]
const init = decl.init && unwrapTSNode(decl.init)
if (init) {
if (processDefineOptions(init)) {
error(
`${DEFINE_OPTIONS}() has no returning value, it cannot be assigned.`,
node
)
}
// defineProps / defineEmits
const isDefineProps = processDefineProps(ctx, init, decl.id)
const isDefineEmits =
!isDefineProps && processDefineEmits(ctx, init, decl.id)
!isDefineEmits &&
(processDefineSlots(init, decl.id) ||
processDefineModel(ctx, init, decl.id))
if (isDefineProps || isDefineEmits) {
if (left === 1) {
ctx.s.remove(node.start! + startOffset, node.end! + startOffset)
} else {
let start = decl.start! + startOffset
let end = decl.end! + startOffset
if (i === total - 1) {
// last one, locate the end of the last one that is not removed
// if we arrive at this branch, there must have been a
// non-removed decl before us, so lastNonRemoved is non-null.
start = node.declarations[lastNonRemoved!].end! + startOffset
} else {
// not the last one, locate the start of the next
end = node.declarations[i + 1].start! + startOffset
}
ctx.s.remove(start, end)
left--
}
} else {
lastNonRemoved = i
}
2020-11-12 08:40:27 +08:00
}
}
}
let isAllLiteral = false
// walk declarations to record declared bindings
if (
(node.type === 'VariableDeclaration' ||
node.type === 'FunctionDeclaration' ||
node.type === 'ClassDeclaration' ||
node.type === 'TSEnumDeclaration') &&
!node.declare
) {
isAllLiteral = walkDeclaration(
'scriptSetup',
node,
setupBindings,
vueImportAliases,
hoistStatic
)
}
// hoist literal constants
if (hoistStatic && isAllLiteral) {
hoistNode(node)
}
// walk statements & named exports / variable declarations for top level
// await
if (
2020-11-12 08:40:27 +08:00
(node.type === 'VariableDeclaration' && !node.declare) ||
node.type.endsWith('Statement')
) {
2022-05-13 15:40:53 +08:00
const scope: Statement[][] = [scriptSetupAst.body]
;(walk as any)(node, {
enter(child: Node, parent: Node) {
if (isFunctionType(child)) {
this.skip()
}
2022-05-13 15:40:53 +08:00
if (child.type === 'BlockStatement') {
scope.push(child.body)
}
if (child.type === 'AwaitExpression') {
hasAwait = true
2022-05-13 15:40:53 +08:00
// if the await expression is an expression statement and
// - is in the root scope
// - or is not the first statement in a nested block scope
// then it needs a semicolon before the generated code.
const currentScope = scope[scope.length - 1]
const needsSemi = currentScope.some((n, i) => {
return (
(scope.length === 1 || i > 0) &&
n.type === 'ExpressionStatement' &&
n.start === child.start
)
})
processAwait(
child,
needsSemi,
parent.type === 'ExpressionStatement'
)
}
2022-05-13 15:40:53 +08:00
},
exit(node: Node) {
if (node.type === 'BlockStatement') scope.pop()
}
})
}
2020-11-12 08:40:27 +08:00
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
)
}
if (isTS) {
// move all Type declarations to outer scope
if (
node.type.startsWith('TS') ||
(node.type === 'ExportNamedDeclaration' &&
node.exportKind === 'type') ||
(node.type === 'VariableDeclaration' && node.declare)
) {
recordType(node, ctx.declaredTypes)
if (node.type !== 'TSEnumDeclaration') {
hoistNode(node)
}
}
}
}
// 3 props destructure transform
if (ctx.propsDestructureDecl) {
transformDestructuredProps(
scriptSetupAst,
ctx.s,
startOffset,
ctx.propsDestructuredBindings,
error,
vueImportAliases
)
}
// 4. Apply reactivity transform
// TODO remove in 3.4
if (
enableReactivityTransform &&
// normal <script> had ref bindings that maybe used in <script setup>
(refBindings || shouldTransform(scriptSetup.content))
) {
const { rootRefs, importedHelpers } = transformAST(
scriptSetupAst,
ctx.s,
startOffset,
refBindings
)
refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
for (const h of importedHelpers) {
ctx.helperImports.add(h)
}
}
// 5. check macro args to make sure it doesn't reference setup scope
// variables
checkInvalidScopeReference(ctx.propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(ctx.propsRuntimeDefaults, DEFINE_PROPS)
checkInvalidScopeReference(ctx.propsDestructureDecl, DEFINE_PROPS)
checkInvalidScopeReference(ctx.emitsRuntimeDecl, DEFINE_EMITS)
checkInvalidScopeReference(optionsRuntimeDecl, DEFINE_OPTIONS)
2020-07-07 03:56:24 +08:00
// 6. remove non-script content
2020-07-07 03:56:24 +08:00
if (script) {
if (startOffset < scriptStartOffset!) {
2020-07-07 03:56:24 +08:00
// <script setup> before <script>
ctx.s.remove(0, startOffset)
ctx.s.remove(endOffset, scriptStartOffset!)
ctx.s.remove(scriptEndOffset!, source.length)
2020-07-07 03:56:24 +08:00
} else {
// <script> before <script setup>
ctx.s.remove(0, scriptStartOffset!)
ctx.s.remove(scriptEndOffset!, startOffset)
ctx.s.remove(endOffset, source.length)
2020-07-07 03:56:24 +08:00
}
} else {
// only <script setup>
ctx.s.remove(0, startOffset)
ctx.s.remove(endOffset, source.length)
2020-07-07 03:56:24 +08:00
}
// 7. analyze binding metadata
if (scriptAst) {
Object.assign(ctx.bindingMetadata, analyzeScriptBindings(scriptAst.body))
}
if (ctx.propsRuntimeDecl) {
for (const key of getObjectOrArrayExpressionKeys(ctx.propsRuntimeDecl)) {
ctx.bindingMetadata[key] = BindingTypes.PROPS
2020-11-13 03:10:39 +08:00
}
}
for (const key in ctx.modelDecls) {
ctx.bindingMetadata[key] = BindingTypes.PROPS
}
// props aliases
if (ctx.propsDestructureDecl) {
if (ctx.propsDestructureRestId) {
ctx.bindingMetadata[ctx.propsDestructureRestId] =
BindingTypes.SETUP_REACTIVE_CONST
}
for (const key in ctx.propsDestructuredBindings) {
const { local } = ctx.propsDestructuredBindings[key]
if (local !== key) {
ctx.bindingMetadata[local] = BindingTypes.PROPS_ALIASED
;(ctx.bindingMetadata.__propsAliases ||
(ctx.bindingMetadata.__propsAliases = {}))[local] = key
}
}
}
for (const [key, { isType, imported, source }] of Object.entries(
userImports
)) {
2020-11-20 01:17:39 +08:00
if (isType) continue
ctx.bindingMetadata[key] =
imported === '*' ||
(imported === 'default' && source.endsWith('.vue')) ||
source === 'vue'
? BindingTypes.SETUP_CONST
: BindingTypes.SETUP_MAYBE_REF
2020-11-13 07:11:25 +08:00
}
for (const key in scriptBindings) {
ctx.bindingMetadata[key] = scriptBindings[key]
}
2020-11-13 07:11:25 +08:00
for (const key in setupBindings) {
ctx.bindingMetadata[key] = setupBindings[key]
}
// known ref bindings
if (refBindings) {
for (const key of refBindings) {
ctx.bindingMetadata[key] = BindingTypes.SETUP_REF
}
}
// 8. inject `useCssVars` calls
if (
cssVars.length &&
// no need to do this when targeting SSR
!(options.inlineTemplate && options.templateOptions?.ssr)
) {
ctx.helperImports.add(CSS_VARS_HELPER)
ctx.helperImports.add('unref')
ctx.s.prependLeft(
startOffset,
`\n${genCssVarsCode(cssVars, ctx.bindingMetadata, scopeId, isProd)}\n`
)
}
// 9. finalize setup() argument signature
let args = `__props`
if (ctx.propsTypeDecl) {
// mark as any and only cast on assignment
// since the user defined complex types may be incompatible with the
// inferred type from generated runtime declarations
args += `: any`
}
// inject user assignment of props
// we use a default __props so that template expressions referencing props
// can use it directly
if (ctx.propsIdentifier) {
ctx.s.prependLeft(
startOffset,
`\nconst ${ctx.propsIdentifier} = __props;\n`
)
}
if (ctx.propsDestructureRestId) {
ctx.s.prependLeft(
startOffset,
`\nconst ${ctx.propsDestructureRestId} = ${ctx.helper(
`createPropsRestProxy`
)}(__props, ${JSON.stringify(
Object.keys(ctx.propsDestructuredBindings)
)});\n`
)
}
// inject temp variables for async context preservation
if (hasAwait) {
const any = isTS ? `: any` : ``
ctx.s.prependLeft(startOffset, `\nlet __temp${any}, __restore${any}\n`)
}
2021-06-26 01:14:49 +08:00
const destructureElements =
ctx.hasDefineExposeCall || !options.inlineTemplate
? [`expose: __expose`]
: []
if (ctx.emitIdentifier) {
2021-06-26 01:14:49 +08:00
destructureElements.push(
ctx.emitIdentifier === `emit` ? `emit` : `emit: ${ctx.emitIdentifier}`
2021-06-26 01:14:49 +08:00
)
}
if (destructureElements.length) {
args += `, { ${destructureElements.join(', ')} }`
}
2020-11-14 06:38:28 +08:00
// 10. generate return statement
let returned
if (
!options.inlineTemplate ||
(!sfc.template && ctx.hasDefaultExportRender)
) {
// non-inline mode, or has manual render in normal <script>
// return bindings from script and script setup
const allBindings: Record<string, any> = {
...scriptBindings,
...setupBindings
}
for (const key in userImports) {
if (!userImports[key].isType && userImports[key].isUsedInTemplate) {
allBindings[key] = true
}
}
returned = `{ `
for (const key in allBindings) {
if (
allBindings[key] === true &&
userImports[key].source !== 'vue' &&
!userImports[key].source.endsWith('.vue')
) {
// generate getter for import bindings
// skip vue imports since we know they will never change
returned += `get ${key}() { return ${key} }, `
} else if (ctx.bindingMetadata[key] === BindingTypes.SETUP_LET) {
// local let binding, also add setter
const setArg = key === 'v' ? `_v` : `v`
returned +=
`get ${key}() { return ${key} }, ` +
`set ${key}(${setArg}) { ${key} = ${setArg} }, `
} else {
returned += `${key}, `
}
}
returned = returned.replace(/, $/, '') + ` }`
} else {
// inline mode
if (sfc.template && !sfc.template.src) {
if (options.templateOptions && options.templateOptions.ssr) {
hasInlinedSsrRenderFn = true
}
// inline render function mode - we are going to compile the template and
// inline it right here
const { code, ast, preamble, tips, errors } = compileTemplate({
filename,
source: sfc.template.content,
inMap: sfc.template.map,
...options.templateOptions,
id: scopeId,
scoped: sfc.styles.some(s => s.scoped),
isProd: options.isProd,
ssrCssVars: sfc.cssVars,
compilerOptions: {
...(options.templateOptions &&
options.templateOptions.compilerOptions),
inline: true,
isTS,
bindingMetadata: ctx.bindingMetadata
}
})
if (tips.length) {
tips.forEach(warnOnce)
}
const err = errors[0]
if (typeof err === 'string') {
throw new Error(err)
} else if (err) {
if (err.loc) {
err.message +=
2020-11-20 01:17:39 +08:00
`\n\n` +
sfc.filename +
'\n' +
generateCodeFrame(
source,
err.loc.start.offset,
err.loc.end.offset
) +
`\n`
}
throw err
}
if (preamble) {
ctx.s.prepend(preamble)
}
// avoid duplicated unref import
// as this may get injected by the render function preamble OR the
// css vars codegen
if (ast && ast.helpers.has(UNREF)) {
ctx.helperImports.delete('unref')
}
returned = code
} else {
returned = `() => {}`
}
}
if (!options.inlineTemplate && !__TEST__) {
// in non-inline mode, the `__isScriptSetup: true` flag is used by
// componentPublicInstance proxy to allow properties that start with $ or _
ctx.s.appendRight(
endOffset,
`\nconst __returned__ = ${returned}\n` +
`Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })\n` +
`return __returned__` +
`\n}\n\n`
)
} else {
ctx.s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
}
2020-11-14 06:38:28 +08:00
// 11. finalize default export
2021-06-26 01:14:49 +08:00
let runtimeOptions = ``
if (!ctx.hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) {
const match = filename.match(/([^/\\]+)\.\w+$/)
if (match) {
runtimeOptions += `\n __name: '${match[1]}',`
}
}
if (hasInlinedSsrRenderFn) {
runtimeOptions += `\n __ssrInlineRender: true,`
}
const propsDecl = genRuntimeProps(ctx)
if (propsDecl) runtimeOptions += `\n props: ${propsDecl},`
const emitsDecl = genRuntimeEmits(ctx)
if (emitsDecl) runtimeOptions += `\n emits: ${emitsDecl},`
2021-06-26 01:14:49 +08:00
let definedOptions = ''
if (optionsRuntimeDecl) {
definedOptions = scriptSetup.content
.slice(optionsRuntimeDecl.start!, optionsRuntimeDecl.end!)
.trim()
}
2021-06-26 01:14:49 +08:00
// <script setup> components are closed by default. If the user did not
// explicitly call `defineExpose`, call expose() with no args.
const exposeCall =
ctx.hasDefineExposeCall || options.inlineTemplate ? `` : ` __expose();\n`
// wrap setup code with function.
if (isTS) {
// for TS, make sure the exported type is still valid type with
// correct props information
// we have to use object spread for types to be merged properly
// user's TS setting should compile it down to proper targets
// export default defineComponent({ ...__default__, ... })
const def =
(defaultExport ? `\n ...${normalScriptDefaultVar},` : ``) +
(definedOptions ? `\n ...${definedOptions},` : '')
ctx.s.prependLeft(
startOffset,
`\n${genDefaultAs} /*#__PURE__*/${ctx.helper(
`defineComponent`
)}({${def}${runtimeOptions}\n ${
hasAwait ? `async ` : ``
}setup(${args}) {\n${exposeCall}`
)
ctx.s.appendRight(endOffset, `})`)
} else {
if (defaultExport || definedOptions) {
// without TS, can't rely on rest spread, so we use Object.assign
// export default Object.assign(__default__, { ... })
ctx.s.prependLeft(
2020-11-13 03:10:39 +08:00
startOffset,
`\n${genDefaultAs} /*#__PURE__*/Object.assign(${
defaultExport ? `${normalScriptDefaultVar}, ` : ''
}${definedOptions ? `${definedOptions}, ` : ''}{${runtimeOptions}\n ` +
`${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`
2020-07-10 11:06:11 +08:00
)
ctx.s.appendRight(endOffset, `})`)
} else {
ctx.s.prependLeft(
2020-11-13 03:10:39 +08:00
startOffset,
`\n${genDefaultAs} {${runtimeOptions}\n ` +
2021-06-26 01:14:49 +08:00
`${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`
2020-11-13 03:10:39 +08:00
)
ctx.s.appendRight(endOffset, `}`)
}
}
2020-07-07 03:56:24 +08:00
2020-11-14 06:38:28 +08:00
// 12. finalize Vue helper imports
if (ctx.helperImports.size > 0) {
ctx.s.prepend(
`import { ${[...ctx.helperImports]
.map(h => `${h} as _${h}`)
.join(', ')} } from 'vue'\n`
)
}
ctx.s.trim()
2020-07-07 03:56:24 +08:00
return {
...scriptSetup,
bindings: ctx.bindingMetadata,
imports: userImports,
content: ctx.s.toString(),
map: genSourceMap
? (ctx.s.generateMap({
source: filename,
hires: true,
includeContent: true
}) as unknown as RawSourceMap)
: undefined,
scriptAst: scriptAst?.body,
scriptSetupAst: scriptSetupAst?.body
2020-07-07 03:56:24 +08:00
}
}
function registerBinding(
bindings: Record<string, BindingTypes>,
node: Identifier,
type: BindingTypes
) {
bindings[node.name] = type
}
2020-11-13 05:11:14 +08:00
function walkDeclaration(
from: 'script' | 'scriptSetup',
2020-11-13 05:11:14 +08:00
node: Declaration,
bindings: Record<string, BindingTypes>,
userImportAliases: Record<string, string>,
hoistStatic: boolean
): boolean {
let isAllLiteral = false
if (node.type === 'VariableDeclaration') {
2020-11-13 03:10:39 +08:00
const isConst = node.kind === 'const'
isAllLiteral =
isConst &&
node.declarations.every(
decl => decl.id.type === 'Identifier' && isStaticNode(decl.init!)
)
// export const foo = ...
for (const { id, init: _init } of node.declarations) {
const init = _init && unwrapTSNode(_init)
const isDefineCall = !!(
isConst &&
2021-06-27 09:11:57 +08:00
isCallOf(
init,
c => c === DEFINE_PROPS || c === DEFINE_EMITS || c === WITH_DEFAULTS
2021-06-27 09:11:57 +08:00
)
)
if (id.type === 'Identifier') {
let bindingType
const userReactiveBinding = userImportAliases['reactive']
if (
(hoistStatic || from === 'script') &&
(isAllLiteral || (isConst && isStaticNode(init!)))
) {
bindingType = BindingTypes.LITERAL_CONST
} else if (isCallOf(init, userReactiveBinding)) {
// treat reactive() calls as let since it's meant to be mutable
bindingType = isConst
? BindingTypes.SETUP_REACTIVE_CONST
: BindingTypes.SETUP_LET
} else if (
2020-11-13 03:10:39 +08:00
// if a declaration is a const literal, we can mark it so that
// the generated render fn code doesn't need to unref() it
isDefineCall ||
(isConst && canNeverBeRef(init!, userReactiveBinding))
) {
bindingType = isCallOf(init, DEFINE_PROPS)
? BindingTypes.SETUP_REACTIVE_CONST
: BindingTypes.SETUP_CONST
} else if (isConst) {
if (
isCallOf(init, userImportAliases['ref']) ||
isCallOf(init, DEFINE_MODEL)
) {
bindingType = BindingTypes.SETUP_REF
} else {
bindingType = BindingTypes.SETUP_MAYBE_REF
}
} else {
bindingType = BindingTypes.SETUP_LET
}
registerBinding(bindings, id, bindingType)
} else {
if (isCallOf(init, DEFINE_PROPS)) {
continue
}
if (id.type === 'ObjectPattern') {
walkObjectPattern(id, bindings, isConst, isDefineCall)
} else if (id.type === 'ArrayPattern') {
walkArrayPattern(id, bindings, isConst, isDefineCall)
}
}
}
} else if (node.type === 'TSEnumDeclaration') {
isAllLiteral = node.members.every(
member => !member.initializer || isStaticNode(member.initializer)
)
bindings[node.id!.name] = isAllLiteral
? BindingTypes.LITERAL_CONST
: BindingTypes.SETUP_CONST
} else if (
node.type === 'FunctionDeclaration' ||
node.type === 'ClassDeclaration'
) {
// export function foo() {} / export class Foo {}
// export declarations must be named.
bindings[node.id!.name] = BindingTypes.SETUP_CONST
}
return isAllLiteral
}
function walkObjectPattern(
node: ObjectPattern,
bindings: Record<string, BindingTypes>,
2020-11-13 05:11:14 +08:00
isConst: boolean,
isDefineCall = false
) {
for (const p of node.properties) {
if (p.type === 'ObjectProperty') {
if (p.key.type === 'Identifier' && p.key === p.value) {
// shorthand: const { x } = ...
const type = isDefineCall
? BindingTypes.SETUP_CONST
: isConst
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
registerBinding(bindings, p.key, type)
} else {
walkPattern(p.value, bindings, isConst, isDefineCall)
}
} else {
// ...rest
// argument can only be identifier when destructuring
const type = isConst ? BindingTypes.SETUP_CONST : BindingTypes.SETUP_LET
registerBinding(bindings, p.argument as Identifier, type)
}
}
}
function walkArrayPattern(
node: ArrayPattern,
bindings: Record<string, BindingTypes>,
2020-11-13 05:11:14 +08:00
isConst: boolean,
isDefineCall = false
) {
for (const e of node.elements) {
e && walkPattern(e, bindings, isConst, isDefineCall)
}
}
2020-11-13 03:10:39 +08:00
function walkPattern(
node: Node,
bindings: Record<string, BindingTypes>,
2020-11-13 05:11:14 +08:00
isConst: boolean,
isDefineCall = false
2020-11-13 03:10:39 +08:00
) {
if (node.type === 'Identifier') {
const type = isDefineCall
? BindingTypes.SETUP_CONST
: isConst
2021-07-20 06:24:18 +08:00
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
registerBinding(bindings, node, type)
} else if (node.type === 'RestElement') {
// argument can only be identifier when destructuring
const type = isConst ? BindingTypes.SETUP_CONST : BindingTypes.SETUP_LET
registerBinding(bindings, node.argument as Identifier, type)
} else if (node.type === 'ObjectPattern') {
2020-11-13 03:10:39 +08:00
walkObjectPattern(node, bindings, isConst)
} else if (node.type === 'ArrayPattern') {
2020-11-13 03:10:39 +08:00
walkArrayPattern(node, bindings, isConst)
} else if (node.type === 'AssignmentPattern') {
if (node.left.type === 'Identifier') {
const type = isDefineCall
? BindingTypes.SETUP_CONST
: isConst
2021-07-20 06:24:18 +08:00
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
registerBinding(bindings, node.left, type)
} else {
2020-11-13 03:10:39 +08:00
walkPattern(node.left, bindings, isConst)
}
}
}
function recordType(node: Node, declaredTypes: Record<string, string[]>) {
if (node.type === 'TSInterfaceDeclaration') {
declaredTypes[node.id.name] = [`Object`]
} else if (node.type === 'TSTypeAliasDeclaration') {
declaredTypes[node.id.name] = inferRuntimeType(
node.typeAnnotation,
declaredTypes
)
} else if (node.type === 'ExportNamedDeclaration' && node.declaration) {
recordType(node.declaration, declaredTypes)
} else if (node.type === 'TSEnumDeclaration') {
declaredTypes[node.id.name] = inferEnumType(node)
}
}
function inferRuntimeType(
node: TSType,
declaredTypes: Record<string, string[]>
): string[] {
switch (node.type) {
case 'TSStringKeyword':
return ['String']
case 'TSNumberKeyword':
return ['Number']
case 'TSBooleanKeyword':
return ['Boolean']
case 'TSObjectKeyword':
return ['Object']
case 'TSNullKeyword':
return ['null']
case 'TSTypeLiteral': {
// TODO (nice to have) generate runtime property validation
const types = new Set<string>()
for (const m of node.members) {
if (
m.type === 'TSCallSignatureDeclaration' ||
m.type === 'TSConstructSignatureDeclaration'
) {
types.add('Function')
} else {
types.add('Object')
}
}
return types.size ? Array.from(types) : ['Object']
}
case 'TSFunctionType':
return ['Function']
case 'TSArrayType':
case 'TSTupleType':
2020-07-17 23:24:53 +08:00
// TODO (nice to have) generate runtime element type/length checks
return ['Array']
case 'TSLiteralType':
switch (node.literal.type) {
case 'StringLiteral':
return ['String']
case 'BooleanLiteral':
return ['Boolean']
case 'NumericLiteral':
case 'BigIntLiteral':
return ['Number']
default:
return [UNKNOWN_TYPE]
}
case 'TSTypeReference':
if (node.typeName.type === 'Identifier') {
if (declaredTypes[node.typeName.name]) {
return declaredTypes[node.typeName.name]
}
switch (node.typeName.name) {
case 'Array':
case 'Function':
case 'Object':
case 'Set':
case 'Map':
case 'WeakSet':
case 'WeakMap':
case 'Date':
case 'Promise':
return [node.typeName.name]
// TS built-in utility types
// https://www.typescriptlang.org/docs/handbook/utility-types.html
case 'Partial':
case 'Required':
case 'Readonly':
case 'Record':
case 'Pick':
case 'Omit':
case 'InstanceType':
return ['Object']
case 'Uppercase':
case 'Lowercase':
case 'Capitalize':
case 'Uncapitalize':
return ['String']
case 'Parameters':
case 'ConstructorParameters':
return ['Array']
case 'NonNullable':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
node.typeParameters.params[0],
declaredTypes
).filter(t => t !== 'null')
}
break
case 'Extract':
if (node.typeParameters && node.typeParameters.params[1]) {
return inferRuntimeType(
node.typeParameters.params[1],
declaredTypes
)
}
break
case 'Exclude':
case 'OmitThisParameter':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
node.typeParameters.params[0],
declaredTypes
)
}
break
}
}
// cannot infer, fallback to UNKNOWN: ThisParameterType
return [UNKNOWN_TYPE]
case 'TSParenthesizedType':
return inferRuntimeType(node.typeAnnotation, declaredTypes)
case 'TSUnionType':
return flattenTypes(node.types, declaredTypes)
case 'TSIntersectionType': {
return flattenTypes(node.types, declaredTypes).filter(
t => t !== UNKNOWN_TYPE
)
}
case 'TSSymbolKeyword':
return ['Symbol']
default:
return [UNKNOWN_TYPE] // no runtime check
}
}
function flattenTypes(
types: TSType[],
declaredTypes: Record<string, string[]>
): string[] {
return [
...new Set(
([] as string[]).concat(
...types.map(t => inferRuntimeType(t, declaredTypes))
)
)
]
}
function inferEnumType(node: TSEnumDeclaration): string[] {
const types = new Set<string>()
for (const m of node.members) {
if (m.initializer) {
switch (m.initializer.type) {
case 'StringLiteral':
types.add('String')
break
case 'NumericLiteral':
types.add('Number')
break
}
}
}
return types.size ? [...types] : ['Number']
}
function canNeverBeRef(node: Node, userReactiveImport?: string): boolean {
if (isCallOf(node, userReactiveImport)) {
return true
}
switch (node.type) {
case 'UnaryExpression':
case 'BinaryExpression':
case 'ArrayExpression':
case 'ObjectExpression':
case 'FunctionExpression':
case 'ArrowFunctionExpression':
case 'UpdateExpression':
case 'ClassExpression':
case 'TaggedTemplateExpression':
return true
case 'SequenceExpression':
return canNeverBeRef(
node.expressions[node.expressions.length - 1],
userReactiveImport
)
default:
if (isLiteralNode(node)) {
return true
}
return false
}
}
function isStaticNode(node: Node): boolean {
switch (node.type) {
case 'UnaryExpression': // void 0, !true
return isStaticNode(node.argument)
case 'LogicalExpression': // 1 > 2
case 'BinaryExpression': // 1 + 2
return isStaticNode(node.left) && isStaticNode(node.right)
case 'ConditionalExpression': {
// 1 ? 2 : 3
return (
isStaticNode(node.test) &&
isStaticNode(node.consequent) &&
isStaticNode(node.alternate)
)
}
case 'SequenceExpression': // (1, 2)
case 'TemplateLiteral': // `foo${1}`
return node.expressions.every(expr => isStaticNode(expr))
case 'ParenthesizedExpression': // (1)
case 'TSNonNullExpression': // 1!
case 'TSAsExpression': // 1 as number
case 'TSTypeAssertion': // (<number>2)
return isStaticNode(node.expression)
default:
if (isLiteralNode(node)) {
return true
}
return false
}
}
2020-07-07 03:56:24 +08:00
/**
* Analyze bindings in normal `<script>`
* Note that `compileScriptSetup` already analyzes bindings as part of its
* compilation process so this should only be used on single `<script>` SFCs.
*/
function analyzeScriptBindings(ast: Statement[]): BindingMetadata {
for (const node of ast) {
if (
node.type === 'ExportDefaultDeclaration' &&
node.declaration.type === 'ObjectExpression'
) {
2020-11-13 03:10:39 +08:00
return analyzeBindingsFromOptions(node.declaration)
}
}
return {}
}
2020-11-13 03:10:39 +08:00
function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
const bindings: BindingMetadata = {}
// #3270, #3275
// mark non-script-setup so we don't resolve components/directives from these
Object.defineProperty(bindings, '__isScriptSetup', {
enumerable: false,
value: false
})
2020-11-13 03:10:39 +08:00
for (const property of node.properties) {
if (
property.type === 'ObjectProperty' &&
!property.computed &&
property.key.type === 'Identifier'
) {
// props
if (property.key.name === 'props') {
// props: ['foo']
// props: { foo: ... }
for (const key of getObjectOrArrayExpressionKeys(property.value)) {
2020-11-13 05:11:14 +08:00
bindings[key] = BindingTypes.PROPS
2020-11-13 03:10:39 +08:00
}
}
2020-11-13 03:10:39 +08:00
// inject
else if (property.key.name === 'inject') {
// inject: ['foo']
// inject: { foo: {} }
for (const key of getObjectOrArrayExpressionKeys(property.value)) {
2020-11-13 05:11:14 +08:00
bindings[key] = BindingTypes.OPTIONS
2020-11-13 03:10:39 +08:00
}
}
// computed & methods
else if (
property.value.type === 'ObjectExpression' &&
(property.key.name === 'computed' || property.key.name === 'methods')
) {
// methods: { foo() {} }
// computed: { foo() {} }
for (const key of getObjectExpressionKeys(property.value)) {
2020-11-13 05:11:14 +08:00
bindings[key] = BindingTypes.OPTIONS
}
2020-11-13 03:10:39 +08:00
}
}
2020-11-13 03:10:39 +08:00
// setup & data
else if (
property.type === 'ObjectMethod' &&
property.key.type === 'Identifier' &&
(property.key.name === 'setup' || property.key.name === 'data')
) {
for (const bodyItem of property.body.body) {
// setup() {
// return {
// foo: null
// }
// }
if (
bodyItem.type === 'ReturnStatement' &&
bodyItem.argument &&
bodyItem.argument.type === 'ObjectExpression'
) {
2020-11-13 03:10:39 +08:00
for (const key of getObjectExpressionKeys(bodyItem.argument)) {
2020-11-13 05:11:14 +08:00
bindings[key] =
property.key.name === 'setup'
? BindingTypes.SETUP_MAYBE_REF
2020-11-13 05:11:14 +08:00
: BindingTypes.DATA
}
}
}
}
}
return bindings
2020-07-07 03:56:24 +08:00
}
function getObjectExpressionKeys(node: ObjectExpression): string[] {
const keys = []
for (const prop of node.properties) {
if (prop.type === 'SpreadElement') continue
const key = resolveObjectKey(prop.key, prop.computed)
if (key) keys.push(String(key))
}
return keys
}
function getArrayExpressionKeys(node: ArrayExpression): string[] {
const keys = []
for (const element of node.elements) {
if (element && element.type === 'StringLiteral') {
keys.push(element.value)
}
}
return keys
}
function getObjectOrArrayExpressionKeys(value: Node): string[] {
if (value.type === 'ArrayExpression') {
return getArrayExpressionKeys(value)
}
if (value.type === 'ObjectExpression') {
return getObjectExpressionKeys(value)
}
return []
}
const templateUsageCheckCache = createCache<string>()
function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
const { content, ast } = sfc.template!
const cached = templateUsageCheckCache.get(content)
if (cached) {
return cached
}
let code = ''
transform(createRoot([ast]), {
nodeTransforms: [
node => {
if (node.type === NodeTypes.ELEMENT) {
if (
!parserOptions.isNativeTag!(node.tag) &&
!parserOptions.isBuiltInComponent!(node.tag)
) {
code += `,${camelize(node.tag)},${capitalize(camelize(node.tag))}`
}
for (let i = 0; i < node.props.length; i++) {
const prop = node.props[i]
if (prop.type === NodeTypes.DIRECTIVE) {
if (!isBuiltInDir(prop.name)) {
code += `,v${capitalize(camelize(prop.name))}`
}
if (prop.exp) {
code += `,${processExp(
(prop.exp as SimpleExpressionNode).content,
prop.name
)}`
}
}
}
} else if (node.type === NodeTypes.INTERPOLATION) {
code += `,${processExp(
(node.content as SimpleExpressionNode).content
)}`
}
}
]
})
code += ';'
templateUsageCheckCache.set(content, code)
return code
}
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
function processExp(exp: string, dir?: string): string {
if (/ as\s+\w|<.*>|:/.test(exp)) {
if (dir === 'slot') {
exp = `(${exp})=>{}`
} else if (dir === 'on') {
exp = `()=>{return ${exp}}`
} else if (dir === 'for') {
const inMatch = exp.match(forAliasRE)
if (inMatch) {
const [, LHS, RHS] = inMatch
return processExp(`(${LHS})=>{}`) + processExp(RHS)
}
}
let ret = ''
// has potential type cast or generic arguments that uses types
const ast = parseExpression(exp, { plugins: ['typescript'] })
walkIdentifiers(ast, node => {
ret += `,` + node.name
})
return ret
}
return stripStrings(exp)
}
function stripStrings(exp: string) {
return exp
.replace(/'[^']*'|"[^"]*"/g, '')
.replace(/`[^`]+`/g, stripTemplateString)
}
function stripTemplateString(str: string): string {
const interpMatch = str.match(/\${[^}]+}/g)
if (interpMatch) {
return interpMatch.map(m => m.slice(2, -1)).join(',')
}
return ''
}
function isImportUsed(local: string, sfc: SFCDescriptor): boolean {
return new RegExp(
// #4274 escape $ since it's a special char in regex
// (and is the only regex special char that is valid in identifiers)
`[^\\w$_]${local.replace(/\$/g, '\\$')}[^\\w$_]`
).test(resolveTemplateUsageCheckString(sfc))
}
/**
* Note: this comparison assumes the prev/next script are already identical,
* and only checks the special case where <script setup lang="ts"> unused import
* pruning result changes due to template changes.
*/
export function hmrShouldReload(
prevImports: Record<string, ImportBinding>,
next: SFCDescriptor
): boolean {
if (
!next.scriptSetup ||
(next.scriptSetup.lang !== 'ts' && next.scriptSetup.lang !== 'tsx')
) {
return false
}
// for each previous import, check if its used status remain the same based on
// the next descriptor's template
for (const key in prevImports) {
// if an import was previous unused, but now is used, we need to force
// reload so that the script now includes that import.
if (!prevImports[key].isUsedInTemplate && isImportUsed(key, next)) {
return true
}
}
return false
}