diff --git a/.eslintrc.js b/.eslintrc.js index 0732923e..fc169abe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,7 +41,7 @@ module.exports = { }, // Packages targeting DOM { - files: ['packages/{vue,runtime-dom}/**'], + files: ['packages/{vue,vue-compat,runtime-dom}/**'], rules: { 'no-restricted-globals': ['error', ...NodeGlobals] } diff --git a/jest.config.js b/jest.config.js index e8507b5d..306ed5f0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,7 +12,8 @@ module.exports = { __NODE_JS__: true, __FEATURE_OPTIONS_API__: true, __FEATURE_SUSPENSE__: true, - __FEATURE_PROD_DEVTOOLS__: false + __FEATURE_PROD_DEVTOOLS__: false, + __COMPAT__: true }, coverageDirectory: 'coverage', coverageReporters: ['html', 'lcov', 'text'], @@ -34,6 +35,7 @@ module.exports = { watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/.git/'], moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], moduleNameMapper: { + '@vue/compat': '/packages/vue-compat/src', '^@vue/(.*?)$': '/packages/$1/src', vue: '/packages/vue/src' }, diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts index 790123ac..1c7c8785 100644 --- a/packages/compiler-core/__tests__/parse.spec.ts +++ b/packages/compiler-core/__tests__/parse.spec.ts @@ -1736,20 +1736,26 @@ foo }) }) - describe('whitespace management', () => { + describe('whitespace management when adopting strategy condense', () => { + const parse = (content: string, options?: ParserOptions) => + baseParse(content, { + whitespace: 'condense', + ...options + }) + it('should remove whitespaces at start/end inside an element', () => { - const ast = baseParse(`
`) + const ast = parse(`
`) expect((ast.children[0] as ElementNode).children.length).toBe(1) }) it('should remove whitespaces w/ newline between elements', () => { - const ast = baseParse(`
\n
\n
`) + const ast = parse(`
\n
\n
`) expect(ast.children.length).toBe(3) expect(ast.children.every(c => c.type === NodeTypes.ELEMENT)).toBe(true) }) it('should remove whitespaces adjacent to comments', () => { - const ast = baseParse(`
\n
`) + const ast = parse(`
\n
`) expect(ast.children.length).toBe(3) expect(ast.children[0].type).toBe(NodeTypes.ELEMENT) expect(ast.children[1].type).toBe(NodeTypes.COMMENT) @@ -1757,7 +1763,7 @@ foo }) it('should remove whitespaces w/ newline between comments and elements', () => { - const ast = baseParse(`
\n \n
`) + const ast = parse(`
\n \n
`) expect(ast.children.length).toBe(3) expect(ast.children[0].type).toBe(NodeTypes.ELEMENT) expect(ast.children[1].type).toBe(NodeTypes.COMMENT) @@ -1765,7 +1771,7 @@ foo }) it('should NOT remove whitespaces w/ newline between interpolations', () => { - const ast = baseParse(`{{ foo }} \n {{ bar }}`) + const ast = parse(`{{ foo }} \n {{ bar }}`) expect(ast.children.length).toBe(3) expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION) expect(ast.children[1]).toMatchObject({ @@ -1776,7 +1782,7 @@ foo }) it('should NOT remove whitespaces w/o newline between elements', () => { - const ast = baseParse(`
`) + const ast = parse(`
`) expect(ast.children.length).toBe(5) expect(ast.children.map(c => c.type)).toMatchObject([ NodeTypes.ELEMENT, @@ -1788,7 +1794,7 @@ foo }) it('should condense consecutive whitespaces in text', () => { - const ast = baseParse(` foo \n bar baz `) + const ast = parse(` foo \n bar baz `) expect((ast.children[0] as TextNode).content).toBe(` foo bar baz `) }) @@ -1824,6 +1830,84 @@ foo }) }) + describe('whitespace management when adopting strategy preserve', () => { + const parse = (content: string, options?: ParserOptions) => + baseParse(content, { + whitespace: 'preserve', + ...options + }) + + it('should still remove whitespaces at start/end inside an element', () => { + const ast = parse(`
`) + expect((ast.children[0] as ElementNode).children.length).toBe(1) + }) + + it('should preserve whitespaces w/ newline between elements', () => { + const ast = parse(`
\n
\n
`) + expect(ast.children.length).toBe(5) + expect(ast.children.map(c => c.type)).toMatchObject([ + NodeTypes.ELEMENT, + NodeTypes.TEXT, + NodeTypes.ELEMENT, + NodeTypes.TEXT, + NodeTypes.ELEMENT + ]) + }) + + it('should preserve whitespaces adjacent to comments', () => { + const ast = parse(`
\n
`) + expect(ast.children.length).toBe(5) + expect(ast.children.map(c => c.type)).toMatchObject([ + NodeTypes.ELEMENT, + NodeTypes.TEXT, + NodeTypes.COMMENT, + NodeTypes.TEXT, + NodeTypes.ELEMENT + ]) + }) + + it('should preserve whitespaces w/ newline between comments and elements', () => { + const ast = parse(`
\n \n
`) + expect(ast.children.length).toBe(5) + expect(ast.children.map(c => c.type)).toMatchObject([ + NodeTypes.ELEMENT, + NodeTypes.TEXT, + NodeTypes.COMMENT, + NodeTypes.TEXT, + NodeTypes.ELEMENT + ]) + }) + + it('should preserve whitespaces w/ newline between interpolations', () => { + const ast = parse(`{{ foo }} \n {{ bar }}`) + expect(ast.children.length).toBe(3) + expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION) + expect(ast.children[1]).toMatchObject({ + type: NodeTypes.TEXT, + content: ' ' + }) + expect(ast.children[2].type).toBe(NodeTypes.INTERPOLATION) + }) + + it('should preserve whitespaces w/o newline between elements', () => { + const ast = parse(`
`) + expect(ast.children.length).toBe(5) + expect(ast.children.map(c => c.type)).toMatchObject([ + NodeTypes.ELEMENT, + NodeTypes.TEXT, + NodeTypes.ELEMENT, + NodeTypes.TEXT, + NodeTypes.ELEMENT + ]) + }) + + it('should preserve consecutive whitespaces in text', () => { + const content = ` foo \n bar baz ` + const ast = parse(content) + expect((ast.children[0] as TextNode).content).toBe(content) + }) + }) + describe('Errors', () => { const patterns: { [key: string]: Array<{ diff --git a/packages/compiler-core/__tests__/transforms/vIf.spec.ts b/packages/compiler-core/__tests__/transforms/vIf.spec.ts index f1b91cc4..2024cdd3 100644 --- a/packages/compiler-core/__tests__/transforms/vIf.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vIf.spec.ts @@ -306,6 +306,7 @@ describe('compiler: v-if', () => { code: ErrorCodes.X_V_IF_SAME_KEY } ]) + expect('unnecessary key usage on v-if').toHaveBeenWarned() }) }) diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index 8adc7a6d..0297bc14 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -11,6 +11,7 @@ ], "buildOptions": { "name": "VueCompilerCore", + "compat": true, "formats": [ "esm-bundler", "cjs" diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 4a2b610f..901f00ce 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -109,6 +109,9 @@ export interface RootNode extends Node { temps: number ssrHelpers?: symbol[] codegenNode?: TemplateChildNode | JSChildNode | BlockStatement + + // v2 compat only + filters?: string[] } export type ElementNode = diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index e3dbbc82..d2322f41 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -50,7 +50,8 @@ import { CREATE_BLOCK, OPEN_BLOCK, CREATE_STATIC, - WITH_CTX + WITH_CTX, + RESOLVE_FILTER } from './runtimeHelpers' import { ImportItem } from './transform' @@ -274,6 +275,12 @@ export function generate( newline() } } + if (__COMPAT__ && ast.filters && ast.filters.length) { + newline() + genAssets(ast.filters, 'filter', context) + newline() + } + if (ast.temps > 0) { push(`let `) for (let i = 0; i < ast.temps; i++) { @@ -458,11 +465,15 @@ function genModulePreamble( function genAssets( assets: string[], - type: 'component' | 'directive', + type: 'component' | 'directive' | 'filter', { helper, push, newline }: CodegenContext ) { const resolver = helper( - type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE + __COMPAT__ && type === 'filter' + ? RESOLVE_FILTER + : type === 'component' + ? RESOLVE_COMPONENT + : RESOLVE_DIRECTIVE ) for (let i = 0; i < assets.length; i++) { let id = assets[i] @@ -727,13 +738,11 @@ function genExpressionAsPropertyKey( } function genComment(node: CommentNode, context: CodegenContext) { - if (__DEV__) { - const { push, helper, pure } = context - if (pure) { - push(PURE_ANNOTATION) - } - push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node) + const { push, helper, pure } = context + if (pure) { + push(PURE_ANNOTATION) } + push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node) } function genVNodeCall(node: VNodeCall, context: CodegenContext) { diff --git a/packages/compiler-core/src/compat/compatConfig.ts b/packages/compiler-core/src/compat/compatConfig.ts new file mode 100644 index 00000000..d43c441d --- /dev/null +++ b/packages/compiler-core/src/compat/compatConfig.ts @@ -0,0 +1,167 @@ +import { SourceLocation } from '../ast' +import { CompilerError } from '../errors' +import { ParserContext } from '../parse' +import { TransformContext } from '../transform' + +export type CompilerCompatConfig = Partial< + Record +> & { + MODE?: 2 | 3 +} + +export interface CompilerCompatOptions { + compatConfig?: CompilerCompatConfig +} + +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', + COMPILER_V_FOR_REF = 'COMPILER_V_FOR_REF', + COMPILER_NATIVE_TEMPLATE = 'COMPILER_NATIVE_TEMPLATE', + COMPILER_INLINE_TEMPLATE = 'COMPILER_INLINE_TEMPLATE', + COMPILER_FILTERS = 'COMPILER_FILTER' +} + +type DeprecationData = { + message: string | ((...args: any[]) => string) + link?: string +} + +const deprecationData: Record = { + [CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT]: { + message: + `Platform-native elements with "is" prop will no longer be ` + + `treated as components in Vue 3 unless the "is" value is explicitly ` + + `prefixed with "vue:".`, + link: `https://v3.vuejs.org/guide/migration/custom-elements-interop.html` + }, + + [CompilerDeprecationTypes.COMPILER_V_BIND_SYNC]: { + message: key => + `.sync modifier for v-bind has been removed. Use v-model with ` + + `argument instead. \`v-bind:${key}.sync\` should be changed to ` + + `\`v-model:${key}\`.`, + link: `https://v3.vuejs.org/guide/migration/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 ` + + `object spread: it will now overwrite an existing non-mergeable attribute ` + + `that appears before v-bind in the case of conflict. ` + + `To retain 2.x behavior, move v-bind to make it the first attribute. ` + + `You can also suppress this warning if the usage is intended.`, + link: `https://v3.vuejs.org/guide/migration/v-bind.html` + }, + + [CompilerDeprecationTypes.COMPILER_V_ON_NATIVE]: { + message: `.native modifier for v-on has been removed as is no longer necessary.`, + link: `https://v3.vuejs.org/guide/migration/v-on-native-modifier-removed.html` + }, + + [CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE]: { + message: + `v-if / v-for precedence when used on the same element has changed ` + + `in Vue 3: v-if now takes higher precedence and will no longer have ` + + `access to v-for scope variables. It is best to avoid the ambiguity ` + + `with