refactor(compiler-sfc): split more logic

This commit is contained in:
Evan You 2023-04-11 15:20:55 +08:00
parent 0232c00e11
commit c52157c87d
8 changed files with 363 additions and 493 deletions

View File

@ -1,30 +1,21 @@
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 { parse as _parse, ParserPlugin } from '@babel/parser'
import { generateCodeFrame } from '@vue/shared'
import {
Node,
Declaration,
ObjectPattern,
ObjectExpression,
ArrayPattern,
Identifier,
ExportSpecifier,
TSType,
ArrayExpression,
Statement,
CallExpression,
AwaitExpression,
@ -41,7 +32,6 @@ import {
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
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'
@ -57,23 +47,16 @@ import {
DEFINE_EMITS
} from './script/defineEmits'
import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
import {
resolveObjectKey,
UNKNOWN_TYPE,
isLiteralNode,
unwrapTSNode,
isCallOf
} from './script/utils'
import { isLiteralNode, unwrapTSNode, isCallOf } from './script/utils'
import { inferRuntimeType } from './script/resolveType'
import { analyzeScriptBindings } from './script/analyzeScriptBindings'
import { isImportUsed } from './script/importUsageCheck'
// Special compiler macros
const DEFINE_EXPOSE = 'defineExpose'
const DEFINE_OPTIONS = 'defineOptions'
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 {
/**
* Scope ID for prefixing injected CSS variables.
@ -968,32 +951,10 @@ export function compileScript(
}
// 7. analyze binding metadata
// `defineProps` & `defineModel` also register props bindings
if (scriptAst) {
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(
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[] {
const types = new Set<string>()
for (const m of node.members) {
@ -1708,251 +1519,3 @@ function isStaticNode(node: Node): boolean {
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
}

View File

@ -11,7 +11,8 @@ import { RawSourceMap, SourceMapGenerator } from 'source-map-js'
import { TemplateCompiler } from './compileTemplate'
import { parseCssVars } from './style/cssVars'
import { createCache } from './cache'
import { hmrShouldReload, ImportBinding } from './compileScript'
import { ImportBinding } from './compileScript'
import { isImportUsed } from './script/importUsageCheck'
export const DEFAULT_FILENAME = 'anonymous.vue'
@ -429,3 +430,32 @@ function isEmpty(node: ElementNode) {
}
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
}

View File

@ -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 []
}

View File

@ -8,6 +8,7 @@ import {
toRuntimeTypeString,
unwrapTSNode
} from './utils'
import { BindingTypes } from '@vue/compiler-dom'
export const DEFINE_MODEL = 'defineModel'
@ -51,6 +52,8 @@ export function processDefineModel(
options: optionsString,
identifier: declId && declId.type === 'Identifier' ? declId.name : undefined
}
// register binding type
ctx.bindingMetadata[modelName] = BindingTypes.PROPS
let runtimeOptions = ''
if (options) {

View File

@ -1,7 +1,6 @@
import {
Node,
LVal,
Identifier,
TSTypeLiteral,
TSInterfaceBody,
ObjectProperty,
@ -23,6 +22,8 @@ import {
toRuntimeTypeString
} from './utils'
import { genModelProps } from './defineModel'
import { getObjectOrArrayExpressionKeys } from './analyzeScriptBindings'
import { processPropsDestructure } from './definePropsDestructure'
export const DEFINE_PROPS = 'defineProps'
export const WITH_DEFAULTS = 'withDefaults'
@ -57,9 +58,15 @@ export function processDefineProps(
ctx.error(`duplicate ${DEFINE_PROPS}() call`, node)
}
ctx.hasDefinePropsCall = true
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
if (node.typeParameters) {
if (ctx.propsRuntimeDecl) {
@ -88,48 +95,7 @@ export function processDefineProps(
if (declId) {
// handle props destructure
if (declId.type === 'ObjectPattern') {
ctx.propsDestructureDecl = declId
for (const prop of declId.properties) {
if (prop.type === 'ObjectProperty') {
const propKey = resolveObjectKey(prop.key, prop.computed)
if (!propKey) {
ctx.error(
`${DEFINE_PROPS}() destructure cannot use computed key.`,
prop.key
)
}
if (prop.value.type === 'AssignmentPattern') {
// default value { foo = 123 }
const { left, right } = prop.value
if (left.type !== 'Identifier') {
ctx.error(
`${DEFINE_PROPS}() destructure does not support nested patterns.`,
left
)
}
// store default value
ctx.propsDestructuredBindings[propKey] = {
local: left.name,
default: right
}
} else if (prop.value.type === 'Identifier') {
// simple destructure
ctx.propsDestructuredBindings[propKey] = {
local: prop.value.name
}
} else {
ctx.error(
`${DEFINE_PROPS}() destructure does not support nested patterns.`,
prop.value
)
}
} else {
// rest spread
ctx.propsDestructureRestId = (prop.argument as Identifier).name
}
}
processPropsDestructure(ctx, declId)
} else {
ctx.propsIdentifier = ctx.getString(declId)
}
@ -176,6 +142,7 @@ function processWithDefaults(
export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
let propsDecls: undefined | string
if (ctx.propsRuntimeDecl) {
propsDecls = ctx.getString(ctx.propsRuntimeDecl).trim()
if (ctx.propsDestructureDecl) {

View File

@ -3,20 +3,83 @@ import {
Identifier,
BlockStatement,
Program,
VariableDeclaration
VariableDeclaration,
ObjectPattern,
Expression
} from '@babel/types'
import { walk } from 'estree-walker'
import {
BindingTypes,
extractIdentifiers,
isFunctionType,
isInDestructureAssignment,
isReferencedIdentifier,
isStaticProperty,
walkFunctionParams
} from '@vue/compiler-core'
} from '@vue/compiler-dom'
import { genPropsAccessExp } from '@vue/shared'
import { isCallOf, unwrapTSNode } from './utils'
import { isCallOf, resolveObjectKey, unwrapTSNode } from './utils'
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

View File

@ -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 ''
}

View File

@ -9,7 +9,7 @@ export function resolveObjectKey(node: Node, computed: boolean) {
switch (node.type) {
case 'StringLiteral':
case 'NumericLiteral':
return node.value
return String(node.value)
case 'Identifier':
if (!computed) return node.name
}