From: Evan You Date: Wed, 22 Nov 2023 07:19:06 +0000 (+0800) Subject: wip: parser v2 compat X-Git-Tag: v3.4.0-alpha.2~21 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=3ee343928a4b14496d6ad448c10feabd7cd4455d;p=thirdparty%2Fvuejs%2Fcore.git wip: parser v2 compat --- diff --git a/packages/compiler-core/src/compat/compatConfig.ts b/packages/compiler-core/src/compat/compatConfig.ts index b643e80111..1ca59b5b31 100644 --- a/packages/compiler-core/src/compat/compatConfig.ts +++ b/packages/compiler-core/src/compat/compatConfig.ts @@ -1,7 +1,6 @@ import { SourceLocation } from '../ast' import { CompilerError } from '../errors' -// @ts-expect-error TODO -import { ParserContext } from '../parse' +import { MergedParserOptions } from '../parser' import { TransformContext } from '../transform' export type CompilerCompatConfig = Partial< @@ -17,7 +16,6 @@ export interface CompilerCompatOptions { export const enum CompilerDeprecationTypes { COMPILER_IS_ON_ELEMENT = 'COMPILER_IS_ON_ELEMENT', COMPILER_V_BIND_SYNC = 'COMPILER_V_BIND_SYNC', - COMPILER_V_BIND_PROP = 'COMPILER_V_BIND_PROP', COMPILER_V_BIND_OBJECT_ORDER = 'COMPILER_V_BIND_OBJECT_ORDER', COMPILER_V_ON_NATIVE = 'COMPILER_V_ON_NATIVE', COMPILER_V_IF_V_FOR_PRECEDENCE = 'COMPILER_V_IF_V_FOR_PRECEDENCE', @@ -48,12 +46,6 @@ const deprecationData: Record = { link: `https://v3-migration.vuejs.org/breaking-changes/v-model.html` }, - [CompilerDeprecationTypes.COMPILER_V_BIND_PROP]: { - message: - `.prop modifier for v-bind has been removed and no longer necessary. ` + - `Vue 3 will automatically set a binding as DOM property when appropriate.` - }, - [CompilerDeprecationTypes.COMPILER_V_BIND_OBJECT_ORDER]: { message: `v-bind="obj" usage is now order sensitive and behaves like JavaScript ` + @@ -101,12 +93,9 @@ const deprecationData: Record = { function getCompatValue( key: CompilerDeprecationTypes | 'MODE', - context: ParserContext | TransformContext + { compatConfig }: MergedParserOptions | TransformContext ) { - const config = (context as ParserContext).options - ? (context as ParserContext).options.compatConfig - : (context as TransformContext).compatConfig - const value = config && config[key] + const value = compatConfig && compatConfig[key] if (key === 'MODE') { return value || 3 // compiler defaults to v3 behavior } else { @@ -116,7 +105,7 @@ function getCompatValue( export function isCompatEnabled( key: CompilerDeprecationTypes, - context: ParserContext | TransformContext + context: MergedParserOptions | TransformContext ) { const mode = getCompatValue('MODE', context) const value = getCompatValue(key, context) @@ -127,7 +116,7 @@ export function isCompatEnabled( export function checkCompatEnabled( key: CompilerDeprecationTypes, - context: ParserContext | TransformContext, + context: MergedParserOptions | TransformContext, loc: SourceLocation | null, ...args: any[] ): boolean { @@ -140,7 +129,7 @@ export function checkCompatEnabled( export function warnDeprecation( key: CompilerDeprecationTypes, - context: ParserContext | TransformContext, + context: MergedParserOptions | TransformContext, loc: SourceLocation | null, ...args: any[] ) { diff --git a/packages/compiler-core/src/parser/index.ts b/packages/compiler-core/src/parser/index.ts index 1aec4c04bd..72886fbbbd 100644 --- a/packages/compiler-core/src/parser/index.ts +++ b/packages/compiler-core/src/parser/index.ts @@ -24,7 +24,13 @@ import Tokenizer, { isWhitespace, toCharCodes } from './Tokenizer' -import { CompilerCompatOptions } from '../compat/compatConfig' +import { + CompilerCompatOptions, + CompilerDeprecationTypes, + checkCompatEnabled, + isCompatEnabled, + warnDeprecation +} from '../compat/compatConfig' import { NO, extend } from '@vue/shared' import { ErrorCodes, @@ -32,7 +38,7 @@ import { defaultOnError, defaultOnWarn } from '../errors' -import { forAliasRE, isCoreComponent } from '../utils' +import { forAliasRE, isCoreComponent, isStaticArgOf } from '../utils' import { decodeHTML } from 'entities/lib/decode.js' type OptionalOptions = @@ -42,7 +48,10 @@ type OptionalOptions = | 'isBuiltInComponent' | keyof CompilerCompatOptions -type MergedParserOptions = Omit, OptionalOptions> & +export type MergedParserOptions = Omit< + Required, + OptionalOptions +> & Pick export const defaultParserOptions: MergedParserOptions = { @@ -63,7 +72,7 @@ let currentRoot: RootNode | null = null // parser state let currentInput = '' -let currentElement: ElementNode | null = null +let currentOpenTag: ElementNode | null = null let currentProp: AttributeNode | DirectiveNode | null = null let currentAttrValue = '' let currentAttrStartIndex = -1 @@ -118,7 +127,7 @@ const tokenizer = new Tokenizer(stack, { const startIndex = tokenizer.inSFCRoot ? end + fastForward(end, CharCodes.Gt) + 1 : start - 1 - currentElement = { + currentOpenTag = { type: NodeTypes.ELEMENT, tag: name, ns: currentOptions.getNamespace(name, stack[0], currentOptions.ns), @@ -159,7 +168,7 @@ const tokenizer = new Tokenizer(stack, { }, onselfclosingtag(end) { - const name = currentElement!.tag + const name = currentOpenTag!.tag endOpenTag(end) if (stack[0]?.tag === name) { onCloseTag(stack.shift()!, end) @@ -213,9 +222,9 @@ const tokenizer = new Tokenizer(stack, { } if (name === 'pre') { inVPre = true - currentVPreBoundary = currentElement + currentVPreBoundary = currentOpenTag // convert dirs before this one to attributes - const props = currentElement!.props + const props = currentOpenTag!.props for (let i = 0; i < props.length; i++) { if (props[i].type === NodeTypes.DIRECTIVE) { props[i] = dirToAttr(props[i] as DirectiveNode) @@ -279,7 +288,7 @@ const tokenizer = new Tokenizer(stack, { } // check duplicate attrs if ( - currentElement!.props.some( + currentOpenTag!.props.some( p => (p.type === NodeTypes.DIRECTIVE ? p.rawName : p.name) === name ) ) { @@ -288,7 +297,10 @@ const tokenizer = new Tokenizer(stack, { }, onattribend(quote, end) { - if (currentElement && currentProp) { + if (currentOpenTag && currentProp) { + // finalize end pos + currentProp.loc.end = tokenizer.getPos(end) + if (quote !== QuoteType.NoValue) { if (__BROWSER__ && currentAttrValue.includes('&')) { currentAttrValue = currentOptions.decodeEntities!( @@ -296,6 +308,7 @@ const tokenizer = new Tokenizer(stack, { true ) } + if (currentProp.type === NodeTypes.ATTRIBUTE) { // assign value @@ -318,7 +331,7 @@ const tokenizer = new Tokenizer(stack, { } if ( tokenizer.inSFCRoot && - currentElement.tag === 'template' && + currentOpenTag.tag === 'template' && currentProp.name === 'lang' && currentAttrValue && currentAttrValue !== 'html' @@ -338,14 +351,29 @@ const tokenizer = new Tokenizer(stack, { if (currentProp.name === 'for') { currentProp.forParseResult = parseForExpression(currentProp.exp) } + // 2.x compat v-bind:foo.sync -> v-model:foo + let syncIndex = -1 + if ( + __COMPAT__ && + currentProp.name === 'bind' && + (syncIndex = currentProp.modifiers.indexOf('sync')) > -1 && + checkCompatEnabled( + CompilerDeprecationTypes.COMPILER_V_BIND_SYNC, + currentOptions, + currentProp.loc, + currentProp.rawName + ) + ) { + currentProp.name = 'model' + currentProp.modifiers.splice(syncIndex, 1) + } } } - currentProp.loc.end = tokenizer.getPos(end) if ( currentProp.type !== NodeTypes.DIRECTIVE || currentProp.name !== 'pre' ) { - currentElement.props.push(currentProp) + currentOpenTag.props.push(currentProp) } } currentAttrValue = '' @@ -503,20 +531,20 @@ function getSlice(start: number, end: number) { } function endOpenTag(end: number) { - addNode(currentElement!) - const { tag, ns } = currentElement! + addNode(currentOpenTag!) + const { tag, ns } = currentOpenTag! if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) { inPre++ } if (currentOptions.isVoidTag(tag)) { - onCloseTag(currentElement!, end) + onCloseTag(currentOpenTag!, end) } else { - stack.unshift(currentElement!) + stack.unshift(currentOpenTag!) if (ns === Namespaces.SVG || ns === Namespaces.MATH_ML) { tokenizer.inXML = true } } - currentElement = null + currentOpenTag = null } function onText(content: string, start: number, end: number) { @@ -586,6 +614,81 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) { ) { tokenizer.inXML = false } + + // 2.x compat / deprecation checks + if (__COMPAT__) { + const props = el.props + if ( + __DEV__ && + isCompatEnabled( + CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE, + currentOptions + ) + ) { + let hasIf = false + let hasFor = false + for (let i = 0; i < props.length; i++) { + const p = props[i] + if (p.type === NodeTypes.DIRECTIVE) { + if (p.name === 'if') { + hasIf = true + } else if (p.name === 'for') { + hasFor = true + } + } + if (hasIf && hasFor) { + warnDeprecation( + CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE, + currentOptions, + el.loc + ) + break + } + } + } + + if ( + isCompatEnabled( + CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE, + currentOptions + ) && + el.tag === 'template' && + !isFragmentTemplate(el) + ) { + __DEV__ && + warnDeprecation( + CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE, + currentOptions, + el.loc + ) + // unwrap + const parent = stack[0] || currentRoot + const index = parent.children.indexOf(el) + parent.children.splice(index, 1, ...el.children) + } + + const inlineTemplateProp = props.find( + p => p.type === NodeTypes.ATTRIBUTE && p.name === 'inline-template' + ) as AttributeNode + if ( + inlineTemplateProp && + checkCompatEnabled( + CompilerDeprecationTypes.COMPILER_INLINE_TEMPLATE, + currentOptions, + inlineTemplateProp.loc + ) && + el.children.length + ) { + inlineTemplateProp.value = { + type: NodeTypes.TEXT, + content: getSlice( + el.children[0].loc.start.offset, + el.children[el.children.length - 1].loc.end.offset + ), + loc: inlineTemplateProp.loc + } + } + } } function fastForward(start: number, c: number) { @@ -641,32 +744,30 @@ function isComponent({ tag, props }: ElementNode): boolean { if (p.name === 'is' && p.value) { if (p.value.content.startsWith('vue:')) { return true + } else if ( + __COMPAT__ && + checkCompatEnabled( + CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT, + currentOptions, + p.loc + ) + ) { + return true } - // TODO else if ( - // __COMPAT__ && - // checkCompatEnabled( - // CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT, - // context, - // p.loc - // ) - // ) { - // return true - // } } + } else if ( + __COMPAT__ && + // :is on plain element - only treat as component in compat mode + p.name === 'bind' && + isStaticArgOf(p.arg, 'is') && + checkCompatEnabled( + CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT, + currentOptions, + p.loc + ) + ) { + return true } - // TODO else if ( - // __COMPAT__ && - // // :is on plain element - only treat as component in compat mode - // p.name === 'bind' && - // isStaticArgOf(p.arg, 'is') && - // checkCompatEnabled( - // CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT, - // context, - // p.loc - // ) - // ) { - // return true - // } } return false } @@ -818,7 +919,7 @@ function emitError(code: ErrorCodes, index: number) { function reset() { tokenizer.reset() - currentElement = null + currentOpenTag = null currentProp = null currentAttrValue = '' currentAttrStartIndex = -1 @@ -829,7 +930,17 @@ function reset() { export function baseParse(input: string, options?: ParserOptions): RootNode { reset() currentInput = input - currentOptions = extend({}, defaultParserOptions, options) + currentOptions = extend({}, defaultParserOptions) + + if (options) { + let key: keyof ParserOptions + for (key in options) { + if (options[key] != null) { + // @ts-ignore + currentOptions[key] = options[key] + } + } + } if (__DEV__) { if (!__BROWSER__ && currentOptions.decodeEntities) { diff --git a/packages/vue-compat/__tests__/compiler.spec.ts b/packages/vue-compat/__tests__/compiler.spec.ts index c20af972da..1f2f9ae247 100644 --- a/packages/vue-compat/__tests__/compiler.spec.ts +++ b/packages/vue-compat/__tests__/compiler.spec.ts @@ -1,6 +1,6 @@ import Vue from '@vue/compat' import { nextTick } from '@vue/runtime-core' -import { CompilerDeprecationTypes } from '../../compiler-core/src' +import { CompilerDeprecationTypes } from '@vue/compiler-core' import { toggleDeprecationWarning } from '../../runtime-core/src/compat/compatConfig' import { triggerEvent } from './utils' @@ -81,16 +81,6 @@ test('COMPILER_V_BIND_SYNC', async () => { expect(CompilerDeprecationTypes.COMPILER_V_BIND_SYNC).toHaveBeenWarned() }) -test('COMPILER_V_BIND_PROP', () => { - const vm = new Vue({ - template: `
` - }).$mount() - - expect(vm.$el).toBeInstanceOf(HTMLDivElement) - expect(vm.$el.id).toBe('foo') - expect(CompilerDeprecationTypes.COMPILER_V_BIND_PROP).toHaveBeenWarned() -}) - test('COMPILER_V_BIND_OBJECT_ORDER', () => { const vm = new Vue({ template: `
`