mirror of https://github.com/vuejs/core.git
feat(compiler-sfc): analyze import usage in template via AST (#9729)
close #8897 close nuxt/nuxt#22416
This commit is contained in:
parent
f8b74dcf29
commit
e8bbc946cb
|
@ -14,6 +14,7 @@ import {
|
|||
} from '../src/ast'
|
||||
|
||||
import { baseParse } from '../src/parser'
|
||||
import { Program } from '@babel/types'
|
||||
|
||||
/* eslint jest/no-disabled-tests: "off" */
|
||||
|
||||
|
@ -2170,6 +2171,63 @@ describe('compiler: parse', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('expression parsing', () => {
|
||||
test('interpolation', () => {
|
||||
const ast = baseParse(`{{ a + b }}`, { prefixIdentifiers: true })
|
||||
// @ts-ignore
|
||||
expect((ast.children[0] as InterpolationNode).content.ast?.type).toBe(
|
||||
'BinaryExpression'
|
||||
)
|
||||
})
|
||||
|
||||
test('v-bind', () => {
|
||||
const ast = baseParse(`<div :[key+1]="foo()" />`, {
|
||||
prefixIdentifiers: true
|
||||
})
|
||||
const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
|
||||
// @ts-ignore
|
||||
expect(dir.arg?.ast?.type).toBe('BinaryExpression')
|
||||
// @ts-ignore
|
||||
expect(dir.exp?.ast?.type).toBe('CallExpression')
|
||||
})
|
||||
|
||||
test('v-on multi statements', () => {
|
||||
const ast = baseParse(`<div @click="a++;b++" />`, {
|
||||
prefixIdentifiers: true
|
||||
})
|
||||
const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
|
||||
// @ts-ignore
|
||||
expect(dir.exp?.ast?.type).toBe('Program')
|
||||
expect((dir.exp?.ast as Program).body).toMatchObject([
|
||||
{ type: 'ExpressionStatement' },
|
||||
{ type: 'ExpressionStatement' }
|
||||
])
|
||||
})
|
||||
|
||||
test('v-slot', () => {
|
||||
const ast = baseParse(`<Comp #foo="{ a, b }" />`, {
|
||||
prefixIdentifiers: true
|
||||
})
|
||||
const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
|
||||
// @ts-ignore
|
||||
expect(dir.exp?.ast?.type).toBe('ArrowFunctionExpression')
|
||||
})
|
||||
|
||||
test('v-for', () => {
|
||||
const ast = baseParse(`<div v-for="({ a, b }, key, index) of a.b" />`, {
|
||||
prefixIdentifiers: true
|
||||
})
|
||||
const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
|
||||
const { source, value, key, index } = dir.forParseResult!
|
||||
// @ts-ignore
|
||||
expect(source.ast?.type).toBe('MemberExpression')
|
||||
// @ts-ignore
|
||||
expect(value?.ast?.type).toBe('ArrowFunctionExpression')
|
||||
expect(key?.ast).toBeNull() // simple ident
|
||||
expect(index?.ast).toBeNull() // simple ident
|
||||
})
|
||||
})
|
||||
|
||||
describe('Errors', () => {
|
||||
// HTML parsing errors as specified at
|
||||
// https://html.spec.whatwg.org/multipage/parsing.html#parse-errors
|
||||
|
|
|
@ -18,7 +18,7 @@ function parseWithExpressionTransform(
|
|||
template: string,
|
||||
options: CompilerOptions = {}
|
||||
) {
|
||||
const ast = parse(template)
|
||||
const ast = parse(template, options)
|
||||
transform(ast, {
|
||||
prefixIdentifiers: true,
|
||||
nodeTransforms: [transformIf, transformExpression],
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from './runtimeHelpers'
|
||||
import { PropsExpression } from './transforms/transformElement'
|
||||
import { ImportItem, TransformContext } from './transform'
|
||||
import { Node as BabelNode } from '@babel/types'
|
||||
|
||||
// Vue template is a platform-agnostic superset of HTML (syntax only).
|
||||
// More namespaces can be declared by platform specific compilers.
|
||||
|
@ -226,6 +227,12 @@ export interface SimpleExpressionNode extends Node {
|
|||
content: string
|
||||
isStatic: boolean
|
||||
constType: ConstantTypes
|
||||
/**
|
||||
* - `null` means the expression is a simple identifier that doesn't need
|
||||
* parsing
|
||||
* - `false` means there was a parsing error
|
||||
*/
|
||||
ast?: BabelNode | null | false
|
||||
/**
|
||||
* Indicates this is an identifier for a hoist vnode call and points to the
|
||||
* hoisted node.
|
||||
|
@ -246,6 +253,12 @@ export interface InterpolationNode extends Node {
|
|||
|
||||
export interface CompoundExpressionNode extends Node {
|
||||
type: NodeTypes.COMPOUND_EXPRESSION
|
||||
/**
|
||||
* - `null` means the expression is a simple identifier that doesn't need
|
||||
* parsing
|
||||
* - `false` means there was a parsing error
|
||||
*/
|
||||
ast?: BabelNode | null | false
|
||||
children: (
|
||||
| SimpleExpressionNode
|
||||
| CompoundExpressionNode
|
||||
|
|
|
@ -28,9 +28,9 @@ export function walkIdentifiers(
|
|||
}
|
||||
|
||||
const rootExp =
|
||||
root.type === 'Program' &&
|
||||
root.body[0].type === 'ExpressionStatement' &&
|
||||
root.body[0].expression
|
||||
root.type === 'Program'
|
||||
? root.body[0].type === 'ExpressionStatement' && root.body[0].expression
|
||||
: root
|
||||
|
||||
walk(root, {
|
||||
enter(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
|
||||
|
|
|
@ -86,6 +86,17 @@ export interface ParserOptions
|
|||
* This defaults to `true` in development and `false` in production builds.
|
||||
*/
|
||||
comments?: boolean
|
||||
/**
|
||||
* Parse JavaScript expressions with Babel.
|
||||
* @default false
|
||||
*/
|
||||
prefixIdentifiers?: boolean
|
||||
/**
|
||||
* A list of parser plugins to enable for `@babel/parser`, which is used to
|
||||
* parse expressions in bindings and interpolations.
|
||||
* https://babeljs.io/docs/en/next/babel-parser#plugins
|
||||
*/
|
||||
expressionPlugins?: ParserPlugin[]
|
||||
}
|
||||
|
||||
export type HoistTransform = (
|
||||
|
|
|
@ -38,14 +38,25 @@ import {
|
|||
defaultOnError,
|
||||
defaultOnWarn
|
||||
} from './errors'
|
||||
import { forAliasRE, isCoreComponent, isStaticArgOf } from './utils'
|
||||
import {
|
||||
forAliasRE,
|
||||
isCoreComponent,
|
||||
isSimpleIdentifier,
|
||||
isStaticArgOf
|
||||
} from './utils'
|
||||
import { decodeHTML } from 'entities/lib/decode.js'
|
||||
import {
|
||||
parse,
|
||||
parseExpression,
|
||||
type ParserOptions as BabelOptions
|
||||
} from '@babel/parser'
|
||||
|
||||
type OptionalOptions =
|
||||
| 'decodeEntities'
|
||||
| 'whitespace'
|
||||
| 'isNativeTag'
|
||||
| 'isBuiltInComponent'
|
||||
| 'expressionPlugins'
|
||||
| keyof CompilerCompatOptions
|
||||
|
||||
export type MergedParserOptions = Omit<
|
||||
|
@ -64,7 +75,8 @@ export const defaultParserOptions: MergedParserOptions = {
|
|||
isCustomElement: NO,
|
||||
onError: defaultOnError,
|
||||
onWarn: defaultOnWarn,
|
||||
comments: __DEV__
|
||||
comments: __DEV__,
|
||||
prefixIdentifiers: false
|
||||
}
|
||||
|
||||
let currentOptions: MergedParserOptions = defaultParserOptions
|
||||
|
@ -116,7 +128,7 @@ const tokenizer = new Tokenizer(stack, {
|
|||
}
|
||||
addNode({
|
||||
type: NodeTypes.INTERPOLATION,
|
||||
content: createSimpleExpression(exp, false, getLoc(innerStart, innerEnd)),
|
||||
content: createExp(exp, false, getLoc(innerStart, innerEnd)),
|
||||
loc: getLoc(start, end)
|
||||
})
|
||||
},
|
||||
|
@ -245,7 +257,7 @@ const tokenizer = new Tokenizer(stack, {
|
|||
setLocEnd((currentProp as AttributeNode).nameLoc, end)
|
||||
} else {
|
||||
const isStatic = arg[0] !== `[`
|
||||
;(currentProp as DirectiveNode).arg = createSimpleExpression(
|
||||
;(currentProp as DirectiveNode).arg = createExp(
|
||||
isStatic ? arg : arg.slice(1, -1),
|
||||
isStatic,
|
||||
getLoc(start, end),
|
||||
|
@ -346,10 +358,25 @@ const tokenizer = new Tokenizer(stack, {
|
|||
}
|
||||
} else {
|
||||
// directive
|
||||
currentProp.exp = createSimpleExpression(
|
||||
let expParseMode = ExpParseMode.Normal
|
||||
if (!__BROWSER__) {
|
||||
if (currentProp.name === 'for') {
|
||||
expParseMode = ExpParseMode.Skip
|
||||
} else if (currentProp.name === 'slot') {
|
||||
expParseMode = ExpParseMode.Params
|
||||
} else if (
|
||||
currentProp.name === 'on' &&
|
||||
currentAttrValue.includes(';')
|
||||
) {
|
||||
expParseMode = ExpParseMode.Statements
|
||||
}
|
||||
}
|
||||
currentProp.exp = createExp(
|
||||
currentAttrValue,
|
||||
false,
|
||||
getLoc(currentAttrStartIndex, currentAttrEndIndex)
|
||||
getLoc(currentAttrStartIndex, currentAttrEndIndex),
|
||||
ConstantTypes.NOT_CONSTANT,
|
||||
expParseMode
|
||||
)
|
||||
if (currentProp.name === 'for') {
|
||||
currentProp.forParseResult = parseForExpression(currentProp.exp)
|
||||
|
@ -477,10 +504,20 @@ function parseForExpression(
|
|||
|
||||
const [, LHS, RHS] = inMatch
|
||||
|
||||
const createAliasExpression = (content: string, offset: number) => {
|
||||
const createAliasExpression = (
|
||||
content: string,
|
||||
offset: number,
|
||||
asParam = false
|
||||
) => {
|
||||
const start = loc.start.offset + offset
|
||||
const end = start + content.length
|
||||
return createSimpleExpression(content, false, getLoc(start, end))
|
||||
return createExp(
|
||||
content,
|
||||
false,
|
||||
getLoc(start, end),
|
||||
ConstantTypes.NOT_CONSTANT,
|
||||
asParam ? ExpParseMode.Params : ExpParseMode.Normal
|
||||
)
|
||||
}
|
||||
|
||||
const result: ForParseResult = {
|
||||
|
@ -502,7 +539,7 @@ function parseForExpression(
|
|||
let keyOffset: number | undefined
|
||||
if (keyContent) {
|
||||
keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
|
||||
result.key = createAliasExpression(keyContent, keyOffset)
|
||||
result.key = createAliasExpression(keyContent, keyOffset, true)
|
||||
}
|
||||
|
||||
if (iteratorMatch[2]) {
|
||||
|
@ -516,14 +553,15 @@ function parseForExpression(
|
|||
result.key
|
||||
? keyOffset! + keyContent.length
|
||||
: trimmedOffset + valueContent.length
|
||||
)
|
||||
),
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (valueContent) {
|
||||
result.value = createAliasExpression(valueContent, trimmedOffset)
|
||||
result.value = createAliasExpression(valueContent, trimmedOffset, true)
|
||||
}
|
||||
|
||||
return result
|
||||
|
@ -929,8 +967,58 @@ function dirToAttr(dir: DirectiveNode): AttributeNode {
|
|||
return attr
|
||||
}
|
||||
|
||||
function emitError(code: ErrorCodes, index: number) {
|
||||
currentOptions.onError(createCompilerError(code, getLoc(index, index)))
|
||||
enum ExpParseMode {
|
||||
Normal,
|
||||
Params,
|
||||
Statements,
|
||||
Skip
|
||||
}
|
||||
|
||||
function createExp(
|
||||
content: SimpleExpressionNode['content'],
|
||||
isStatic: SimpleExpressionNode['isStatic'] = false,
|
||||
loc: SourceLocation,
|
||||
constType: ConstantTypes = ConstantTypes.NOT_CONSTANT,
|
||||
parseMode = ExpParseMode.Normal
|
||||
) {
|
||||
const exp = createSimpleExpression(content, isStatic, loc, constType)
|
||||
if (
|
||||
!__BROWSER__ &&
|
||||
!isStatic &&
|
||||
currentOptions.prefixIdentifiers &&
|
||||
parseMode !== ExpParseMode.Skip &&
|
||||
content.trim()
|
||||
) {
|
||||
if (isSimpleIdentifier(content)) {
|
||||
exp.ast = null // fast path
|
||||
return exp
|
||||
}
|
||||
try {
|
||||
const plugins = currentOptions.expressionPlugins
|
||||
const options: BabelOptions = {
|
||||
plugins: plugins ? [...plugins, 'typescript'] : ['typescript']
|
||||
}
|
||||
if (parseMode === ExpParseMode.Statements) {
|
||||
// v-on with multi-inline-statements, pad 1 char
|
||||
exp.ast = parse(` ${content} `, options).program
|
||||
} else if (parseMode === ExpParseMode.Params) {
|
||||
exp.ast = parseExpression(`(${content})=>{}`, options)
|
||||
} else {
|
||||
// normal exp, wrap with parens
|
||||
exp.ast = parseExpression(`(${content})`, options)
|
||||
}
|
||||
} catch (e: any) {
|
||||
exp.ast = false // indicate an error
|
||||
emitError(ErrorCodes.X_INVALID_EXPRESSION, loc.start.offset, e.message)
|
||||
}
|
||||
}
|
||||
return exp
|
||||
}
|
||||
|
||||
function emitError(code: ErrorCodes, index: number, message?: string) {
|
||||
currentOptions.onError(
|
||||
createCompilerError(code, getLoc(index, index), undefined, message)
|
||||
)
|
||||
}
|
||||
|
||||
function reset() {
|
||||
|
|
|
@ -223,7 +223,14 @@ export function processExpression(
|
|||
// bail constant on parens (function invocation) and dot (member access)
|
||||
const bailConstant = constantBailRE.test(rawExp)
|
||||
|
||||
if (isSimpleIdentifier(rawExp)) {
|
||||
let ast = node.ast
|
||||
|
||||
if (ast === false) {
|
||||
// ast being false means it has caused an error already during parse phase
|
||||
return node
|
||||
}
|
||||
|
||||
if (ast === null || (!ast && isSimpleIdentifier(rawExp))) {
|
||||
const isScopeVarReference = context.identifiers[rawExp]
|
||||
const isAllowedGlobal = isGloballyAllowed(rawExp)
|
||||
const isLiteral = isLiteralWhitelisted(rawExp)
|
||||
|
@ -249,29 +256,30 @@ export function processExpression(
|
|||
return node
|
||||
}
|
||||
|
||||
let ast: any
|
||||
// exp needs to be parsed differently:
|
||||
// 1. Multiple inline statements (v-on, with presence of `;`): parse as raw
|
||||
// exp, but make sure to pad with spaces for consistent ranges
|
||||
// 2. Expressions: wrap with parens (for e.g. object expressions)
|
||||
// 3. Function arguments (v-for, v-slot): place in a function argument position
|
||||
const source = asRawStatements
|
||||
? ` ${rawExp} `
|
||||
: `(${rawExp})${asParams ? `=>{}` : ``}`
|
||||
try {
|
||||
ast = parse(source, {
|
||||
plugins: context.expressionPlugins
|
||||
}).program
|
||||
} catch (e: any) {
|
||||
context.onError(
|
||||
createCompilerError(
|
||||
ErrorCodes.X_INVALID_EXPRESSION,
|
||||
node.loc,
|
||||
undefined,
|
||||
e.message
|
||||
if (!ast) {
|
||||
// exp needs to be parsed differently:
|
||||
// 1. Multiple inline statements (v-on, with presence of `;`): parse as raw
|
||||
// exp, but make sure to pad with spaces for consistent ranges
|
||||
// 2. Expressions: wrap with parens (for e.g. object expressions)
|
||||
// 3. Function arguments (v-for, v-slot): place in a function argument position
|
||||
const source = asRawStatements
|
||||
? ` ${rawExp} `
|
||||
: `(${rawExp})${asParams ? `=>{}` : ``}`
|
||||
try {
|
||||
ast = parse(source, {
|
||||
plugins: context.expressionPlugins
|
||||
}).program
|
||||
} catch (e: any) {
|
||||
context.onError(
|
||||
createCompilerError(
|
||||
ErrorCodes.X_INVALID_EXPRESSION,
|
||||
node.loc,
|
||||
undefined,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
)
|
||||
return node
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
type QualifiedId = Identifier & PrefixMeta
|
||||
|
@ -351,6 +359,7 @@ export function processExpression(
|
|||
let ret
|
||||
if (children.length) {
|
||||
ret = createCompoundExpression(children, node.loc)
|
||||
ret.ast = ast
|
||||
} else {
|
||||
ret = node
|
||||
ret.constType = bailConstant
|
||||
|
|
|
@ -748,6 +748,51 @@ return { get FooBaz() { return FooBaz }, get Last() { return Last } }
|
|||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > dev mode import usage check > property access (whitespace) 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
import { Foo, Bar, Baz } from './foo'
|
||||
|
||||
export default /*#__PURE__*/_defineComponent({
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
|
||||
return { get Foo() { return Foo } }
|
||||
}
|
||||
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > dev mode import usage check > property access 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
import { Foo, Bar, Baz } from './foo'
|
||||
|
||||
export default /*#__PURE__*/_defineComponent({
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
|
||||
return { get Foo() { return Foo } }
|
||||
}
|
||||
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > dev mode import usage check > spread operator 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
import { Foo, Bar, Baz } from './foo'
|
||||
|
||||
export default /*#__PURE__*/_defineComponent({
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
|
||||
return { get Foo() { return Foo } }
|
||||
}
|
||||
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > dev mode import usage check > template ref 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
import { foo, bar, Baz } from './foo'
|
||||
|
|
|
@ -243,7 +243,7 @@ describe('SFC compile <script setup>', () => {
|
|||
import { useCssVars, ref } from 'vue'
|
||||
const msg = ref()
|
||||
</script>
|
||||
|
||||
|
||||
<style>
|
||||
.foo {
|
||||
color: v-bind(msg)
|
||||
|
@ -518,6 +518,46 @@ describe('SFC compile <script setup>', () => {
|
|||
)
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
// https://github.com/nuxt/nuxt/issues/22416
|
||||
test('property access', () => {
|
||||
const { content } = compile(`
|
||||
<script setup lang="ts">
|
||||
import { Foo, Bar, Baz } from './foo'
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ Foo.Bar.Baz }}</div>
|
||||
</template>
|
||||
`)
|
||||
expect(content).toMatch('return { get Foo() { return Foo } }')
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
test('spread operator', () => {
|
||||
const { content } = compile(`
|
||||
<script setup lang="ts">
|
||||
import { Foo, Bar, Baz } from './foo'
|
||||
</script>
|
||||
<template>
|
||||
<div v-bind="{ ...Foo.Bar.Baz }"></div>
|
||||
</template>
|
||||
`)
|
||||
expect(content).toMatch('return { get Foo() { return Foo } }')
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
test('property access (whitespace)', () => {
|
||||
const { content } = compile(`
|
||||
<script setup lang="ts">
|
||||
import { Foo, Bar, Baz } from './foo'
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ Foo . Bar . Baz }}</div>
|
||||
</template>
|
||||
`)
|
||||
expect(content).toMatch('return { get Foo() { return Foo } }')
|
||||
assertCode(content)
|
||||
})
|
||||
})
|
||||
|
||||
describe('inlineTemplate mode', () => {
|
||||
|
|
|
@ -13,7 +13,10 @@ export function compileSFCScript(
|
|||
options?: Partial<SFCScriptCompileOptions>,
|
||||
parseOptions?: SFCParseOptions
|
||||
) {
|
||||
const { descriptor } = parse(src, parseOptions)
|
||||
const { descriptor, errors } = parse(src, parseOptions)
|
||||
if (errors.length) {
|
||||
console.warn(errors[0])
|
||||
}
|
||||
return compileScript(descriptor, {
|
||||
...options,
|
||||
id: mockId
|
||||
|
|
|
@ -24,6 +24,7 @@ export interface SFCParseOptions {
|
|||
pad?: boolean | 'line' | 'space'
|
||||
ignoreEmpty?: boolean
|
||||
compiler?: TemplateCompiler
|
||||
parseExpressions?: boolean
|
||||
}
|
||||
|
||||
export interface SFCBlock {
|
||||
|
@ -104,7 +105,8 @@ export function parse(
|
|||
sourceRoot = '',
|
||||
pad = false,
|
||||
ignoreEmpty = true,
|
||||
compiler = CompilerDOM
|
||||
compiler = CompilerDOM,
|
||||
parseExpressions = true
|
||||
}: SFCParseOptions = {}
|
||||
): SFCParseResult {
|
||||
const sourceKey =
|
||||
|
@ -130,6 +132,7 @@ export function parse(
|
|||
const errors: (CompilerError | SyntaxError)[] = []
|
||||
const ast = compiler.parse(source, {
|
||||
parseMode: 'sfc',
|
||||
prefixIdentifiers: parseExpressions,
|
||||
onError: e => {
|
||||
errors.push(e)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { parseExpression } from '@babel/parser'
|
||||
import { SFCDescriptor } from '../parse'
|
||||
import {
|
||||
NodeTypes,
|
||||
SimpleExpressionNode,
|
||||
forAliasRE,
|
||||
parserOptions,
|
||||
walkIdentifiers,
|
||||
TemplateChildNode
|
||||
TemplateChildNode,
|
||||
ExpressionNode
|
||||
} from '@vue/compiler-dom'
|
||||
import { createCache } from '../cache'
|
||||
import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
|
||||
|
@ -17,14 +16,10 @@ import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
|
|||
* 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))
|
||||
return resolveTemplateUsageCheckString(sfc).has(local)
|
||||
}
|
||||
|
||||
const templateUsageCheckCache = createCache<string>()
|
||||
const templateUsageCheckCache = createCache<Set<string>>()
|
||||
|
||||
function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
|
||||
const { content, ast } = sfc.template!
|
||||
|
@ -33,7 +28,7 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
|
|||
return cached
|
||||
}
|
||||
|
||||
let code = ''
|
||||
const ids = new Set<string>()
|
||||
|
||||
ast!.children.forEach(walk)
|
||||
|
||||
|
@ -44,27 +39,25 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
|
|||
!parserOptions.isNativeTag!(node.tag) &&
|
||||
!parserOptions.isBuiltInComponent!(node.tag)
|
||||
) {
|
||||
code += `,${camelize(node.tag)},${capitalize(camelize(node.tag))}`
|
||||
ids.add(camelize(node.tag))
|
||||
ids.add(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))}`
|
||||
ids.add(`v${capitalize(camelize(prop.name))}`)
|
||||
}
|
||||
|
||||
// process dynamic directive arguments
|
||||
if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) {
|
||||
code += `,${stripStrings(
|
||||
(prop.arg as SimpleExpressionNode).content
|
||||
)}`
|
||||
extractIdentifiers(ids, prop.arg)
|
||||
}
|
||||
|
||||
if (prop.exp) {
|
||||
code += `,${processExp(
|
||||
(prop.exp as SimpleExpressionNode).content,
|
||||
prop.name
|
||||
)}`
|
||||
if (prop.name === 'for') {
|
||||
extractIdentifiers(ids, prop.forParseResult!.source)
|
||||
} else if (prop.exp) {
|
||||
extractIdentifiers(ids, prop.exp)
|
||||
}
|
||||
}
|
||||
if (
|
||||
|
@ -72,58 +65,25 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
|
|||
prop.name === 'ref' &&
|
||||
prop.value?.content
|
||||
) {
|
||||
code += `,${prop.value.content}`
|
||||
ids.add(prop.value.content)
|
||||
}
|
||||
}
|
||||
node.children.forEach(walk)
|
||||
break
|
||||
case NodeTypes.INTERPOLATION:
|
||||
code += `,${processExp((node.content as SimpleExpressionNode).content)}`
|
||||
extractIdentifiers(ids, node.content)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
code += ';'
|
||||
templateUsageCheckCache.set(content, code)
|
||||
return code
|
||||
templateUsageCheckCache.set(content, ids)
|
||||
return ids
|
||||
}
|
||||
|
||||
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) {
|
||||
let [, LHS, RHS] = inMatch
|
||||
// #6088
|
||||
LHS = LHS.trim().replace(/^\(|\)$/g, '')
|
||||
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
|
||||
function extractIdentifiers(ids: Set<string>, node: ExpressionNode) {
|
||||
if (node.ast) {
|
||||
walkIdentifiers(node.ast, n => ids.add(n.name))
|
||||
} else if (node.ast === null) {
|
||||
ids.add((node as SimpleExpressionNode).content)
|
||||
}
|
||||
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 ''
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue