mirror of https://github.com/vuejs/core.git
refactor(compiler-sfc): split more logic
This commit is contained in:
parent
0232c00e11
commit
c52157c87d
|
@ -1,30 +1,21 @@
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import {
|
import {
|
||||||
BindingMetadata,
|
|
||||||
BindingTypes,
|
BindingTypes,
|
||||||
createRoot,
|
|
||||||
NodeTypes,
|
|
||||||
transform,
|
|
||||||
parserOptions,
|
|
||||||
UNREF,
|
UNREF,
|
||||||
SimpleExpressionNode,
|
|
||||||
isFunctionType,
|
isFunctionType,
|
||||||
walkIdentifiers,
|
walkIdentifiers,
|
||||||
getImportedName
|
getImportedName
|
||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
|
import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
|
||||||
import { parse as _parse, parseExpression, ParserPlugin } from '@babel/parser'
|
import { parse as _parse, ParserPlugin } from '@babel/parser'
|
||||||
import { camelize, capitalize, generateCodeFrame, makeMap } from '@vue/shared'
|
import { generateCodeFrame } from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
Node,
|
Node,
|
||||||
Declaration,
|
Declaration,
|
||||||
ObjectPattern,
|
ObjectPattern,
|
||||||
ObjectExpression,
|
|
||||||
ArrayPattern,
|
ArrayPattern,
|
||||||
Identifier,
|
Identifier,
|
||||||
ExportSpecifier,
|
ExportSpecifier,
|
||||||
TSType,
|
|
||||||
ArrayExpression,
|
|
||||||
Statement,
|
Statement,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
AwaitExpression,
|
AwaitExpression,
|
||||||
|
@ -41,7 +32,6 @@ import {
|
||||||
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
|
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
|
||||||
import { warnOnce } from './warn'
|
import { warnOnce } from './warn'
|
||||||
import { rewriteDefaultAST } from './rewriteDefault'
|
import { rewriteDefaultAST } from './rewriteDefault'
|
||||||
import { createCache } from './cache'
|
|
||||||
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
|
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
|
||||||
import { transformDestructuredProps } from './script/definePropsDestructure'
|
import { transformDestructuredProps } from './script/definePropsDestructure'
|
||||||
import { ScriptCompileContext } from './script/context'
|
import { ScriptCompileContext } from './script/context'
|
||||||
|
@ -57,23 +47,16 @@ import {
|
||||||
DEFINE_EMITS
|
DEFINE_EMITS
|
||||||
} from './script/defineEmits'
|
} from './script/defineEmits'
|
||||||
import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
|
import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
|
||||||
import {
|
import { isLiteralNode, unwrapTSNode, isCallOf } from './script/utils'
|
||||||
resolveObjectKey,
|
import { inferRuntimeType } from './script/resolveType'
|
||||||
UNKNOWN_TYPE,
|
import { analyzeScriptBindings } from './script/analyzeScriptBindings'
|
||||||
isLiteralNode,
|
import { isImportUsed } from './script/importUsageCheck'
|
||||||
unwrapTSNode,
|
|
||||||
isCallOf
|
|
||||||
} from './script/utils'
|
|
||||||
|
|
||||||
// Special compiler macros
|
// Special compiler macros
|
||||||
const DEFINE_EXPOSE = 'defineExpose'
|
const DEFINE_EXPOSE = 'defineExpose'
|
||||||
const DEFINE_OPTIONS = 'defineOptions'
|
const DEFINE_OPTIONS = 'defineOptions'
|
||||||
const DEFINE_SLOTS = 'defineSlots'
|
const DEFINE_SLOTS = 'defineSlots'
|
||||||
|
|
||||||
const isBuiltInDir = makeMap(
|
|
||||||
`once,memo,if,for,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
|
|
||||||
)
|
|
||||||
|
|
||||||
export interface SFCScriptCompileOptions {
|
export interface SFCScriptCompileOptions {
|
||||||
/**
|
/**
|
||||||
* Scope ID for prefixing injected CSS variables.
|
* Scope ID for prefixing injected CSS variables.
|
||||||
|
@ -968,32 +951,10 @@ export function compileScript(
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. analyze binding metadata
|
// 7. analyze binding metadata
|
||||||
|
// `defineProps` & `defineModel` also register props bindings
|
||||||
if (scriptAst) {
|
if (scriptAst) {
|
||||||
Object.assign(ctx.bindingMetadata, analyzeScriptBindings(scriptAst.body))
|
Object.assign(ctx.bindingMetadata, analyzeScriptBindings(scriptAst.body))
|
||||||
}
|
}
|
||||||
if (ctx.propsRuntimeDecl) {
|
|
||||||
for (const key of getObjectOrArrayExpressionKeys(ctx.propsRuntimeDecl)) {
|
|
||||||
ctx.bindingMetadata[key] = BindingTypes.PROPS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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(
|
for (const [key, { isType, imported, source }] of Object.entries(
|
||||||
userImports
|
userImports
|
||||||
)) {
|
)) {
|
||||||
|
@ -1478,156 +1439,6 @@ function recordType(node: Node, declaredTypes: Record<string, string[]>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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':
|
|
||||||
// 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[] {
|
function inferEnumType(node: TSEnumDeclaration): string[] {
|
||||||
const types = new Set<string>()
|
const types = new Set<string>()
|
||||||
for (const m of node.members) {
|
for (const m of node.members) {
|
||||||
|
@ -1708,251 +1519,3 @@ function isStaticNode(node: Node): boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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'
|
|
||||||
) {
|
|
||||||
return analyzeBindingsFromOptions(node.declaration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
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)) {
|
|
||||||
bindings[key] = BindingTypes.PROPS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// inject
|
|
||||||
else if (property.key.name === 'inject') {
|
|
||||||
// inject: ['foo']
|
|
||||||
// inject: { foo: {} }
|
|
||||||
for (const key of getObjectOrArrayExpressionKeys(property.value)) {
|
|
||||||
bindings[key] = BindingTypes.OPTIONS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)) {
|
|
||||||
bindings[key] = BindingTypes.OPTIONS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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'
|
|
||||||
) {
|
|
||||||
for (const key of getObjectExpressionKeys(bodyItem.argument)) {
|
|
||||||
bindings[key] =
|
|
||||||
property.key.name === 'setup'
|
|
||||||
? BindingTypes.SETUP_MAYBE_REF
|
|
||||||
: BindingTypes.DATA
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bindings
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,7 +11,8 @@ import { RawSourceMap, SourceMapGenerator } from 'source-map-js'
|
||||||
import { TemplateCompiler } from './compileTemplate'
|
import { TemplateCompiler } from './compileTemplate'
|
||||||
import { parseCssVars } from './style/cssVars'
|
import { parseCssVars } from './style/cssVars'
|
||||||
import { createCache } from './cache'
|
import { createCache } from './cache'
|
||||||
import { hmrShouldReload, ImportBinding } from './compileScript'
|
import { ImportBinding } from './compileScript'
|
||||||
|
import { isImportUsed } from './script/importUsageCheck'
|
||||||
|
|
||||||
export const DEFAULT_FILENAME = 'anonymous.vue'
|
export const DEFAULT_FILENAME = 'anonymous.vue'
|
||||||
|
|
||||||
|
@ -429,3 +430,32 @@ function isEmpty(node: ElementNode) {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
import {
|
||||||
|
ArrayExpression,
|
||||||
|
Node,
|
||||||
|
ObjectExpression,
|
||||||
|
Statement
|
||||||
|
} from '@babel/types'
|
||||||
|
import { BindingMetadata, BindingTypes } from '@vue/compiler-dom'
|
||||||
|
import { resolveObjectKey } from './utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
export function analyzeScriptBindings(ast: Statement[]): BindingMetadata {
|
||||||
|
for (const node of ast) {
|
||||||
|
if (
|
||||||
|
node.type === 'ExportDefaultDeclaration' &&
|
||||||
|
node.declaration.type === 'ObjectExpression'
|
||||||
|
) {
|
||||||
|
return analyzeBindingsFromOptions(node.declaration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
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)) {
|
||||||
|
bindings[key] = BindingTypes.PROPS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inject
|
||||||
|
else if (property.key.name === 'inject') {
|
||||||
|
// inject: ['foo']
|
||||||
|
// inject: { foo: {} }
|
||||||
|
for (const key of getObjectOrArrayExpressionKeys(property.value)) {
|
||||||
|
bindings[key] = BindingTypes.OPTIONS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)) {
|
||||||
|
bindings[key] = BindingTypes.OPTIONS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
) {
|
||||||
|
for (const key of getObjectExpressionKeys(bodyItem.argument)) {
|
||||||
|
bindings[key] =
|
||||||
|
property.key.name === 'setup'
|
||||||
|
? BindingTypes.SETUP_MAYBE_REF
|
||||||
|
: BindingTypes.DATA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getObjectOrArrayExpressionKeys(value: Node): string[] {
|
||||||
|
if (value.type === 'ArrayExpression') {
|
||||||
|
return getArrayExpressionKeys(value)
|
||||||
|
}
|
||||||
|
if (value.type === 'ObjectExpression') {
|
||||||
|
return getObjectExpressionKeys(value)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import {
|
||||||
toRuntimeTypeString,
|
toRuntimeTypeString,
|
||||||
unwrapTSNode
|
unwrapTSNode
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
import { BindingTypes } from '@vue/compiler-dom'
|
||||||
|
|
||||||
export const DEFINE_MODEL = 'defineModel'
|
export const DEFINE_MODEL = 'defineModel'
|
||||||
|
|
||||||
|
@ -51,6 +52,8 @@ export function processDefineModel(
|
||||||
options: optionsString,
|
options: optionsString,
|
||||||
identifier: declId && declId.type === 'Identifier' ? declId.name : undefined
|
identifier: declId && declId.type === 'Identifier' ? declId.name : undefined
|
||||||
}
|
}
|
||||||
|
// register binding type
|
||||||
|
ctx.bindingMetadata[modelName] = BindingTypes.PROPS
|
||||||
|
|
||||||
let runtimeOptions = ''
|
let runtimeOptions = ''
|
||||||
if (options) {
|
if (options) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {
|
import {
|
||||||
Node,
|
Node,
|
||||||
LVal,
|
LVal,
|
||||||
Identifier,
|
|
||||||
TSTypeLiteral,
|
TSTypeLiteral,
|
||||||
TSInterfaceBody,
|
TSInterfaceBody,
|
||||||
ObjectProperty,
|
ObjectProperty,
|
||||||
|
@ -23,6 +22,8 @@ import {
|
||||||
toRuntimeTypeString
|
toRuntimeTypeString
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { genModelProps } from './defineModel'
|
import { genModelProps } from './defineModel'
|
||||||
|
import { getObjectOrArrayExpressionKeys } from './analyzeScriptBindings'
|
||||||
|
import { processPropsDestructure } from './definePropsDestructure'
|
||||||
|
|
||||||
export const DEFINE_PROPS = 'defineProps'
|
export const DEFINE_PROPS = 'defineProps'
|
||||||
export const WITH_DEFAULTS = 'withDefaults'
|
export const WITH_DEFAULTS = 'withDefaults'
|
||||||
|
@ -57,9 +58,15 @@ export function processDefineProps(
|
||||||
ctx.error(`duplicate ${DEFINE_PROPS}() call`, node)
|
ctx.error(`duplicate ${DEFINE_PROPS}() call`, node)
|
||||||
}
|
}
|
||||||
ctx.hasDefinePropsCall = true
|
ctx.hasDefinePropsCall = true
|
||||||
|
|
||||||
ctx.propsRuntimeDecl = node.arguments[0]
|
ctx.propsRuntimeDecl = node.arguments[0]
|
||||||
|
|
||||||
|
// register bindings
|
||||||
|
if (ctx.propsRuntimeDecl) {
|
||||||
|
for (const key of getObjectOrArrayExpressionKeys(ctx.propsRuntimeDecl)) {
|
||||||
|
ctx.bindingMetadata[key] = BindingTypes.PROPS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// call has type parameters - infer runtime types from it
|
// call has type parameters - infer runtime types from it
|
||||||
if (node.typeParameters) {
|
if (node.typeParameters) {
|
||||||
if (ctx.propsRuntimeDecl) {
|
if (ctx.propsRuntimeDecl) {
|
||||||
|
@ -88,48 +95,7 @@ export function processDefineProps(
|
||||||
if (declId) {
|
if (declId) {
|
||||||
// handle props destructure
|
// handle props destructure
|
||||||
if (declId.type === 'ObjectPattern') {
|
if (declId.type === 'ObjectPattern') {
|
||||||
ctx.propsDestructureDecl = declId
|
processPropsDestructure(ctx, 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 {
|
} else {
|
||||||
ctx.propsIdentifier = ctx.getString(declId)
|
ctx.propsIdentifier = ctx.getString(declId)
|
||||||
}
|
}
|
||||||
|
@ -176,6 +142,7 @@ function processWithDefaults(
|
||||||
|
|
||||||
export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
|
export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
|
||||||
let propsDecls: undefined | string
|
let propsDecls: undefined | string
|
||||||
|
|
||||||
if (ctx.propsRuntimeDecl) {
|
if (ctx.propsRuntimeDecl) {
|
||||||
propsDecls = ctx.getString(ctx.propsRuntimeDecl).trim()
|
propsDecls = ctx.getString(ctx.propsRuntimeDecl).trim()
|
||||||
if (ctx.propsDestructureDecl) {
|
if (ctx.propsDestructureDecl) {
|
||||||
|
|
|
@ -3,20 +3,83 @@ import {
|
||||||
Identifier,
|
Identifier,
|
||||||
BlockStatement,
|
BlockStatement,
|
||||||
Program,
|
Program,
|
||||||
VariableDeclaration
|
VariableDeclaration,
|
||||||
|
ObjectPattern,
|
||||||
|
Expression
|
||||||
} from '@babel/types'
|
} from '@babel/types'
|
||||||
import { walk } from 'estree-walker'
|
import { walk } from 'estree-walker'
|
||||||
import {
|
import {
|
||||||
|
BindingTypes,
|
||||||
extractIdentifiers,
|
extractIdentifiers,
|
||||||
isFunctionType,
|
isFunctionType,
|
||||||
isInDestructureAssignment,
|
isInDestructureAssignment,
|
||||||
isReferencedIdentifier,
|
isReferencedIdentifier,
|
||||||
isStaticProperty,
|
isStaticProperty,
|
||||||
walkFunctionParams
|
walkFunctionParams
|
||||||
} from '@vue/compiler-core'
|
} from '@vue/compiler-dom'
|
||||||
import { genPropsAccessExp } from '@vue/shared'
|
import { genPropsAccessExp } from '@vue/shared'
|
||||||
import { isCallOf, unwrapTSNode } from './utils'
|
import { isCallOf, resolveObjectKey, unwrapTSNode } from './utils'
|
||||||
import { ScriptCompileContext } from './context'
|
import { ScriptCompileContext } from './context'
|
||||||
|
import { DEFINE_PROPS } from './defineProps'
|
||||||
|
|
||||||
|
export function processPropsDestructure(
|
||||||
|
ctx: ScriptCompileContext,
|
||||||
|
declId: ObjectPattern
|
||||||
|
) {
|
||||||
|
ctx.propsDestructureDecl = declId
|
||||||
|
|
||||||
|
const registerBinding = (
|
||||||
|
key: string,
|
||||||
|
local: string,
|
||||||
|
defaultValue?: Expression
|
||||||
|
) => {
|
||||||
|
ctx.propsDestructuredBindings[key] = { local, default: defaultValue }
|
||||||
|
if (local !== key) {
|
||||||
|
ctx.bindingMetadata[local] = BindingTypes.PROPS_ALIASED
|
||||||
|
;(ctx.bindingMetadata.__propsAliases ||
|
||||||
|
(ctx.bindingMetadata.__propsAliases = {}))[local] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
registerBinding(propKey, left.name, right)
|
||||||
|
} else if (prop.value.type === 'Identifier') {
|
||||||
|
// simple destructure
|
||||||
|
registerBinding(propKey, 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
|
||||||
|
// register binding
|
||||||
|
ctx.bindingMetadata[ctx.propsDestructureRestId] =
|
||||||
|
BindingTypes.SETUP_REACTIVE_CONST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* true -> prop binding
|
* true -> prop binding
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { parseExpression } from '@babel/parser'
|
||||||
|
import { SFCDescriptor } from '../parse'
|
||||||
|
import {
|
||||||
|
NodeTypes,
|
||||||
|
SimpleExpressionNode,
|
||||||
|
createRoot,
|
||||||
|
parserOptions,
|
||||||
|
transform,
|
||||||
|
walkIdentifiers
|
||||||
|
} from '@vue/compiler-dom'
|
||||||
|
import { createCache } from '../cache'
|
||||||
|
import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an import is used in the SFC's template. This is used to determine
|
||||||
|
* the properties that should be included in the object returned from setup()
|
||||||
|
* when not using inline mode.
|
||||||
|
*/
|
||||||
|
export 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (!isBuiltInDirective(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 ''
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ export function resolveObjectKey(node: Node, computed: boolean) {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'StringLiteral':
|
case 'StringLiteral':
|
||||||
case 'NumericLiteral':
|
case 'NumericLiteral':
|
||||||
return node.value
|
return String(node.value)
|
||||||
case 'Identifier':
|
case 'Identifier':
|
||||||
if (!computed) return node.name
|
if (!computed) return node.name
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue