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 {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
case 'StringLiteral':
|
||||
case 'NumericLiteral':
|
||||
return node.value
|
||||
return String(node.value)
|
||||
case 'Identifier':
|
||||
if (!computed) return node.name
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue