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'
|
} from '../src/ast'
|
||||||
|
|
||||||
import { baseParse } from '../src/parser'
|
import { baseParse } from '../src/parser'
|
||||||
|
import { Program } from '@babel/types'
|
||||||
|
|
||||||
/* eslint jest/no-disabled-tests: "off" */
|
/* 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', () => {
|
describe('Errors', () => {
|
||||||
// HTML parsing errors as specified at
|
// HTML parsing errors as specified at
|
||||||
// https://html.spec.whatwg.org/multipage/parsing.html#parse-errors
|
// https://html.spec.whatwg.org/multipage/parsing.html#parse-errors
|
||||||
|
|
|
@ -18,7 +18,7 @@ function parseWithExpressionTransform(
|
||||||
template: string,
|
template: string,
|
||||||
options: CompilerOptions = {}
|
options: CompilerOptions = {}
|
||||||
) {
|
) {
|
||||||
const ast = parse(template)
|
const ast = parse(template, options)
|
||||||
transform(ast, {
|
transform(ast, {
|
||||||
prefixIdentifiers: true,
|
prefixIdentifiers: true,
|
||||||
nodeTransforms: [transformIf, transformExpression],
|
nodeTransforms: [transformIf, transformExpression],
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
} from './runtimeHelpers'
|
} from './runtimeHelpers'
|
||||||
import { PropsExpression } from './transforms/transformElement'
|
import { PropsExpression } from './transforms/transformElement'
|
||||||
import { ImportItem, TransformContext } from './transform'
|
import { ImportItem, TransformContext } from './transform'
|
||||||
|
import { Node as BabelNode } from '@babel/types'
|
||||||
|
|
||||||
// Vue template is a platform-agnostic superset of HTML (syntax only).
|
// Vue template is a platform-agnostic superset of HTML (syntax only).
|
||||||
// More namespaces can be declared by platform specific compilers.
|
// More namespaces can be declared by platform specific compilers.
|
||||||
|
@ -226,6 +227,12 @@ export interface SimpleExpressionNode extends Node {
|
||||||
content: string
|
content: string
|
||||||
isStatic: boolean
|
isStatic: boolean
|
||||||
constType: ConstantTypes
|
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
|
* Indicates this is an identifier for a hoist vnode call and points to the
|
||||||
* hoisted node.
|
* hoisted node.
|
||||||
|
@ -246,6 +253,12 @@ export interface InterpolationNode extends Node {
|
||||||
|
|
||||||
export interface CompoundExpressionNode extends Node {
|
export interface CompoundExpressionNode extends Node {
|
||||||
type: NodeTypes.COMPOUND_EXPRESSION
|
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: (
|
children: (
|
||||||
| SimpleExpressionNode
|
| SimpleExpressionNode
|
||||||
| CompoundExpressionNode
|
| CompoundExpressionNode
|
||||||
|
|
|
@ -28,9 +28,9 @@ export function walkIdentifiers(
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootExp =
|
const rootExp =
|
||||||
root.type === 'Program' &&
|
root.type === 'Program'
|
||||||
root.body[0].type === 'ExpressionStatement' &&
|
? root.body[0].type === 'ExpressionStatement' && root.body[0].expression
|
||||||
root.body[0].expression
|
: root
|
||||||
|
|
||||||
walk(root, {
|
walk(root, {
|
||||||
enter(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
|
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.
|
* This defaults to `true` in development and `false` in production builds.
|
||||||
*/
|
*/
|
||||||
comments?: boolean
|
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 = (
|
export type HoistTransform = (
|
||||||
|
|
|
@ -38,14 +38,25 @@ import {
|
||||||
defaultOnError,
|
defaultOnError,
|
||||||
defaultOnWarn
|
defaultOnWarn
|
||||||
} from './errors'
|
} from './errors'
|
||||||
import { forAliasRE, isCoreComponent, isStaticArgOf } from './utils'
|
import {
|
||||||
|
forAliasRE,
|
||||||
|
isCoreComponent,
|
||||||
|
isSimpleIdentifier,
|
||||||
|
isStaticArgOf
|
||||||
|
} from './utils'
|
||||||
import { decodeHTML } from 'entities/lib/decode.js'
|
import { decodeHTML } from 'entities/lib/decode.js'
|
||||||
|
import {
|
||||||
|
parse,
|
||||||
|
parseExpression,
|
||||||
|
type ParserOptions as BabelOptions
|
||||||
|
} from '@babel/parser'
|
||||||
|
|
||||||
type OptionalOptions =
|
type OptionalOptions =
|
||||||
| 'decodeEntities'
|
| 'decodeEntities'
|
||||||
| 'whitespace'
|
| 'whitespace'
|
||||||
| 'isNativeTag'
|
| 'isNativeTag'
|
||||||
| 'isBuiltInComponent'
|
| 'isBuiltInComponent'
|
||||||
|
| 'expressionPlugins'
|
||||||
| keyof CompilerCompatOptions
|
| keyof CompilerCompatOptions
|
||||||
|
|
||||||
export type MergedParserOptions = Omit<
|
export type MergedParserOptions = Omit<
|
||||||
|
@ -64,7 +75,8 @@ export const defaultParserOptions: MergedParserOptions = {
|
||||||
isCustomElement: NO,
|
isCustomElement: NO,
|
||||||
onError: defaultOnError,
|
onError: defaultOnError,
|
||||||
onWarn: defaultOnWarn,
|
onWarn: defaultOnWarn,
|
||||||
comments: __DEV__
|
comments: __DEV__,
|
||||||
|
prefixIdentifiers: false
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentOptions: MergedParserOptions = defaultParserOptions
|
let currentOptions: MergedParserOptions = defaultParserOptions
|
||||||
|
@ -116,7 +128,7 @@ const tokenizer = new Tokenizer(stack, {
|
||||||
}
|
}
|
||||||
addNode({
|
addNode({
|
||||||
type: NodeTypes.INTERPOLATION,
|
type: NodeTypes.INTERPOLATION,
|
||||||
content: createSimpleExpression(exp, false, getLoc(innerStart, innerEnd)),
|
content: createExp(exp, false, getLoc(innerStart, innerEnd)),
|
||||||
loc: getLoc(start, end)
|
loc: getLoc(start, end)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -245,7 +257,7 @@ const tokenizer = new Tokenizer(stack, {
|
||||||
setLocEnd((currentProp as AttributeNode).nameLoc, end)
|
setLocEnd((currentProp as AttributeNode).nameLoc, end)
|
||||||
} else {
|
} else {
|
||||||
const isStatic = arg[0] !== `[`
|
const isStatic = arg[0] !== `[`
|
||||||
;(currentProp as DirectiveNode).arg = createSimpleExpression(
|
;(currentProp as DirectiveNode).arg = createExp(
|
||||||
isStatic ? arg : arg.slice(1, -1),
|
isStatic ? arg : arg.slice(1, -1),
|
||||||
isStatic,
|
isStatic,
|
||||||
getLoc(start, end),
|
getLoc(start, end),
|
||||||
|
@ -346,10 +358,25 @@ const tokenizer = new Tokenizer(stack, {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// directive
|
// 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,
|
currentAttrValue,
|
||||||
false,
|
false,
|
||||||
getLoc(currentAttrStartIndex, currentAttrEndIndex)
|
getLoc(currentAttrStartIndex, currentAttrEndIndex),
|
||||||
|
ConstantTypes.NOT_CONSTANT,
|
||||||
|
expParseMode
|
||||||
)
|
)
|
||||||
if (currentProp.name === 'for') {
|
if (currentProp.name === 'for') {
|
||||||
currentProp.forParseResult = parseForExpression(currentProp.exp)
|
currentProp.forParseResult = parseForExpression(currentProp.exp)
|
||||||
|
@ -477,10 +504,20 @@ function parseForExpression(
|
||||||
|
|
||||||
const [, LHS, RHS] = inMatch
|
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 start = loc.start.offset + offset
|
||||||
const end = start + content.length
|
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 = {
|
const result: ForParseResult = {
|
||||||
|
@ -502,7 +539,7 @@ function parseForExpression(
|
||||||
let keyOffset: number | undefined
|
let keyOffset: number | undefined
|
||||||
if (keyContent) {
|
if (keyContent) {
|
||||||
keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
|
keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
|
||||||
result.key = createAliasExpression(keyContent, keyOffset)
|
result.key = createAliasExpression(keyContent, keyOffset, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iteratorMatch[2]) {
|
if (iteratorMatch[2]) {
|
||||||
|
@ -516,14 +553,15 @@ function parseForExpression(
|
||||||
result.key
|
result.key
|
||||||
? keyOffset! + keyContent.length
|
? keyOffset! + keyContent.length
|
||||||
: trimmedOffset + valueContent.length
|
: trimmedOffset + valueContent.length
|
||||||
)
|
),
|
||||||
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valueContent) {
|
if (valueContent) {
|
||||||
result.value = createAliasExpression(valueContent, trimmedOffset)
|
result.value = createAliasExpression(valueContent, trimmedOffset, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -929,8 +967,58 @@ function dirToAttr(dir: DirectiveNode): AttributeNode {
|
||||||
return attr
|
return attr
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitError(code: ErrorCodes, index: number) {
|
enum ExpParseMode {
|
||||||
currentOptions.onError(createCompilerError(code, getLoc(index, index)))
|
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() {
|
function reset() {
|
||||||
|
|
|
@ -223,7 +223,14 @@ export function processExpression(
|
||||||
// bail constant on parens (function invocation) and dot (member access)
|
// bail constant on parens (function invocation) and dot (member access)
|
||||||
const bailConstant = constantBailRE.test(rawExp)
|
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 isScopeVarReference = context.identifiers[rawExp]
|
||||||
const isAllowedGlobal = isGloballyAllowed(rawExp)
|
const isAllowedGlobal = isGloballyAllowed(rawExp)
|
||||||
const isLiteral = isLiteralWhitelisted(rawExp)
|
const isLiteral = isLiteralWhitelisted(rawExp)
|
||||||
|
@ -249,7 +256,7 @@ export function processExpression(
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
let ast: any
|
if (!ast) {
|
||||||
// exp needs to be parsed differently:
|
// exp needs to be parsed differently:
|
||||||
// 1. Multiple inline statements (v-on, with presence of `;`): parse as raw
|
// 1. Multiple inline statements (v-on, with presence of `;`): parse as raw
|
||||||
// exp, but make sure to pad with spaces for consistent ranges
|
// exp, but make sure to pad with spaces for consistent ranges
|
||||||
|
@ -273,6 +280,7 @@ export function processExpression(
|
||||||
)
|
)
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type QualifiedId = Identifier & PrefixMeta
|
type QualifiedId = Identifier & PrefixMeta
|
||||||
const ids: QualifiedId[] = []
|
const ids: QualifiedId[] = []
|
||||||
|
@ -351,6 +359,7 @@ export function processExpression(
|
||||||
let ret
|
let ret
|
||||||
if (children.length) {
|
if (children.length) {
|
||||||
ret = createCompoundExpression(children, node.loc)
|
ret = createCompoundExpression(children, node.loc)
|
||||||
|
ret.ast = ast
|
||||||
} else {
|
} else {
|
||||||
ret = node
|
ret = node
|
||||||
ret.constType = bailConstant
|
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`] = `
|
exports[`SFC compile <script setup> > dev mode import usage check > template ref 1`] = `
|
||||||
"import { defineComponent as _defineComponent } from 'vue'
|
"import { defineComponent as _defineComponent } from 'vue'
|
||||||
import { foo, bar, Baz } from './foo'
|
import { foo, bar, Baz } from './foo'
|
||||||
|
|
|
@ -518,6 +518,46 @@ describe('SFC compile <script setup>', () => {
|
||||||
)
|
)
|
||||||
assertCode(content)
|
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', () => {
|
describe('inlineTemplate mode', () => {
|
||||||
|
|
|
@ -13,7 +13,10 @@ export function compileSFCScript(
|
||||||
options?: Partial<SFCScriptCompileOptions>,
|
options?: Partial<SFCScriptCompileOptions>,
|
||||||
parseOptions?: SFCParseOptions
|
parseOptions?: SFCParseOptions
|
||||||
) {
|
) {
|
||||||
const { descriptor } = parse(src, parseOptions)
|
const { descriptor, errors } = parse(src, parseOptions)
|
||||||
|
if (errors.length) {
|
||||||
|
console.warn(errors[0])
|
||||||
|
}
|
||||||
return compileScript(descriptor, {
|
return compileScript(descriptor, {
|
||||||
...options,
|
...options,
|
||||||
id: mockId
|
id: mockId
|
||||||
|
|
|
@ -24,6 +24,7 @@ export interface SFCParseOptions {
|
||||||
pad?: boolean | 'line' | 'space'
|
pad?: boolean | 'line' | 'space'
|
||||||
ignoreEmpty?: boolean
|
ignoreEmpty?: boolean
|
||||||
compiler?: TemplateCompiler
|
compiler?: TemplateCompiler
|
||||||
|
parseExpressions?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SFCBlock {
|
export interface SFCBlock {
|
||||||
|
@ -104,7 +105,8 @@ export function parse(
|
||||||
sourceRoot = '',
|
sourceRoot = '',
|
||||||
pad = false,
|
pad = false,
|
||||||
ignoreEmpty = true,
|
ignoreEmpty = true,
|
||||||
compiler = CompilerDOM
|
compiler = CompilerDOM,
|
||||||
|
parseExpressions = true
|
||||||
}: SFCParseOptions = {}
|
}: SFCParseOptions = {}
|
||||||
): SFCParseResult {
|
): SFCParseResult {
|
||||||
const sourceKey =
|
const sourceKey =
|
||||||
|
@ -130,6 +132,7 @@ export function parse(
|
||||||
const errors: (CompilerError | SyntaxError)[] = []
|
const errors: (CompilerError | SyntaxError)[] = []
|
||||||
const ast = compiler.parse(source, {
|
const ast = compiler.parse(source, {
|
||||||
parseMode: 'sfc',
|
parseMode: 'sfc',
|
||||||
|
prefixIdentifiers: parseExpressions,
|
||||||
onError: e => {
|
onError: e => {
|
||||||
errors.push(e)
|
errors.push(e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { parseExpression } from '@babel/parser'
|
|
||||||
import { SFCDescriptor } from '../parse'
|
import { SFCDescriptor } from '../parse'
|
||||||
import {
|
import {
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
SimpleExpressionNode,
|
SimpleExpressionNode,
|
||||||
forAliasRE,
|
|
||||||
parserOptions,
|
parserOptions,
|
||||||
walkIdentifiers,
|
walkIdentifiers,
|
||||||
TemplateChildNode
|
TemplateChildNode,
|
||||||
|
ExpressionNode
|
||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
import { createCache } from '../cache'
|
import { createCache } from '../cache'
|
||||||
import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
|
import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
|
||||||
|
@ -17,14 +16,10 @@ import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
|
||||||
* when not using inline mode.
|
* when not using inline mode.
|
||||||
*/
|
*/
|
||||||
export function isImportUsed(local: string, sfc: SFCDescriptor): boolean {
|
export function isImportUsed(local: string, sfc: SFCDescriptor): boolean {
|
||||||
return new RegExp(
|
return resolveTemplateUsageCheckString(sfc).has(local)
|
||||||
// #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>()
|
const templateUsageCheckCache = createCache<Set<string>>()
|
||||||
|
|
||||||
function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
|
function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
|
||||||
const { content, ast } = sfc.template!
|
const { content, ast } = sfc.template!
|
||||||
|
@ -33,7 +28,7 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
|
||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
|
|
||||||
let code = ''
|
const ids = new Set<string>()
|
||||||
|
|
||||||
ast!.children.forEach(walk)
|
ast!.children.forEach(walk)
|
||||||
|
|
||||||
|
@ -44,27 +39,25 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
|
||||||
!parserOptions.isNativeTag!(node.tag) &&
|
!parserOptions.isNativeTag!(node.tag) &&
|
||||||
!parserOptions.isBuiltInComponent!(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++) {
|
for (let i = 0; i < node.props.length; i++) {
|
||||||
const prop = node.props[i]
|
const prop = node.props[i]
|
||||||
if (prop.type === NodeTypes.DIRECTIVE) {
|
if (prop.type === NodeTypes.DIRECTIVE) {
|
||||||
if (!isBuiltInDirective(prop.name)) {
|
if (!isBuiltInDirective(prop.name)) {
|
||||||
code += `,v${capitalize(camelize(prop.name))}`
|
ids.add(`v${capitalize(camelize(prop.name))}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// process dynamic directive arguments
|
// process dynamic directive arguments
|
||||||
if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) {
|
if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) {
|
||||||
code += `,${stripStrings(
|
extractIdentifiers(ids, prop.arg)
|
||||||
(prop.arg as SimpleExpressionNode).content
|
|
||||||
)}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop.exp) {
|
if (prop.name === 'for') {
|
||||||
code += `,${processExp(
|
extractIdentifiers(ids, prop.forParseResult!.source)
|
||||||
(prop.exp as SimpleExpressionNode).content,
|
} else if (prop.exp) {
|
||||||
prop.name
|
extractIdentifiers(ids, prop.exp)
|
||||||
)}`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
@ -72,58 +65,25 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
|
||||||
prop.name === 'ref' &&
|
prop.name === 'ref' &&
|
||||||
prop.value?.content
|
prop.value?.content
|
||||||
) {
|
) {
|
||||||
code += `,${prop.value.content}`
|
ids.add(prop.value.content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
node.children.forEach(walk)
|
node.children.forEach(walk)
|
||||||
break
|
break
|
||||||
case NodeTypes.INTERPOLATION:
|
case NodeTypes.INTERPOLATION:
|
||||||
code += `,${processExp((node.content as SimpleExpressionNode).content)}`
|
extractIdentifiers(ids, node.content)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
code += ';'
|
templateUsageCheckCache.set(content, ids)
|
||||||
templateUsageCheckCache.set(content, code)
|
return ids
|
||||||
return code
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function processExp(exp: string, dir?: string): string {
|
function extractIdentifiers(ids: Set<string>, node: ExpressionNode) {
|
||||||
if (/ as\s+\w|<.*>|:/.test(exp)) {
|
if (node.ast) {
|
||||||
if (dir === 'slot') {
|
walkIdentifiers(node.ast, n => ids.add(n.name))
|
||||||
exp = `(${exp})=>{}`
|
} else if (node.ast === null) {
|
||||||
} else if (dir === 'on') {
|
ids.add((node as SimpleExpressionNode).content)
|
||||||
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
|
|
||||||
}
|
|
||||||
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