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..cfe345f6 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] diff --git a/packages/compiler-core/src/compat/compatConfig.ts b/packages/compiler-core/src/compat/compatConfig.ts index e73c8a1b..717b8dc4 100644 --- a/packages/compiler-core/src/compat/compatConfig.ts +++ b/packages/compiler-core/src/compat/compatConfig.ts @@ -22,7 +22,7 @@ export const enum CompilerDeprecationTypes { COMPILER_V_IF_V_FOR_PRECEDENCE = 'COMPILER_V_IF_V_FOR_PRECEDENCE', COMPILER_NATIVE_TEMPLATE = 'COMPILER_NATIVE_TEMPLATE', COMPILER_INLINE_TEMPLATE = 'COMPILER_INLINE_TEMPLATE', - COMPILER_FILTER = 'COMPILER_FILTER' + COMPILER_FILTERS = 'COMPILER_FILTER' } type DeprecationData = { @@ -89,8 +89,11 @@ const deprecationData: Record = { link: `https://v3.vuejs.org/guide/migration/inline-template-attribute.html` }, - [CompilerDeprecationTypes.COMPILER_FILTER]: { - message: `filters have been removed in Vue 3.`, + [CompilerDeprecationTypes.COMPILER_FILTERS]: { + message: + `filters have been removed in Vue 3. ` + + `The "|" symbol will be treated as native JavaScript bitwise OR operator. ` + + `Use method calls or computed properties instead.`, link: `https://v3.vuejs.org/guide/migration/filters.html` } } diff --git a/packages/compiler-core/src/compat/transformFilter.ts b/packages/compiler-core/src/compat/transformFilter.ts new file mode 100644 index 00000000..bd6fd384 --- /dev/null +++ b/packages/compiler-core/src/compat/transformFilter.ts @@ -0,0 +1,194 @@ +import { RESOLVE_FILTER } from '../runtimeHelpers' +import { + AttributeNode, + DirectiveNode, + NodeTransform, + NodeTypes, + SimpleExpressionNode, + toValidAssetId, + TransformContext +} from '@vue/compiler-core' +import { + CompilerDeprecationTypes, + isCompatEnabled, + warnDeprecation +} from './compatConfig' +import { ExpressionNode } from '../ast' + +const validDivisionCharRE = /[\w).+\-_$\]]/ + +export const transformFilter: NodeTransform = (node, context) => { + if (!isCompatEnabled(CompilerDeprecationTypes.COMPILER_FILTERS, context)) { + return + } + + if (node.type === NodeTypes.INTERPOLATION) { + // filter rewrite is applied before expression transform so only + // simple expressions are possible at this stage + rewriteFilter(node.content, context) + } + + if (node.type === NodeTypes.ELEMENT) { + node.props.forEach((prop: AttributeNode | DirectiveNode) => { + if ( + prop.type === NodeTypes.DIRECTIVE && + prop.name !== 'for' && + prop.exp + ) { + rewriteFilter(prop.exp, context) + } + }) + } +} + +function rewriteFilter(node: ExpressionNode, context: TransformContext) { + if (node.type === NodeTypes.SIMPLE_EXPRESSION) { + parseFilter(node, context) + } else { + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i] + if (typeof child !== 'object') continue + if (child.type === NodeTypes.SIMPLE_EXPRESSION) { + parseFilter(child, context) + } else if (child.type === NodeTypes.COMPOUND_EXPRESSION) { + rewriteFilter(node, context) + } else if (child.type === NodeTypes.INTERPOLATION) { + rewriteFilter(child.content, context) + } + } + } +} + +function parseFilter(node: SimpleExpressionNode, context: TransformContext) { + const exp = node.content + let inSingle = false + let inDouble = false + let inTemplateString = false + let inRegex = false + let curly = 0 + let square = 0 + let paren = 0 + let lastFilterIndex = 0 + let c, + prev, + i: number, + expression, + filters: string[] = [] + + for (i = 0; i < exp.length; i++) { + prev = c + c = exp.charCodeAt(i) + if (inSingle) { + if (c === 0x27 && prev !== 0x5c) inSingle = false + } else if (inDouble) { + if (c === 0x22 && prev !== 0x5c) inDouble = false + } else if (inTemplateString) { + if (c === 0x60 && prev !== 0x5c) inTemplateString = false + } else if (inRegex) { + if (c === 0x2f && prev !== 0x5c) inRegex = false + } else if ( + c === 0x7c && // pipe + exp.charCodeAt(i + 1) !== 0x7c && + exp.charCodeAt(i - 1) !== 0x7c && + !curly && + !square && + !paren + ) { + if (expression === undefined) { + // first filter, end of expression + lastFilterIndex = i + 1 + expression = exp.slice(0, i).trim() + } else { + pushFilter() + } + } else { + switch (c) { + case 0x22: + inDouble = true + break // " + case 0x27: + inSingle = true + break // ' + case 0x60: + inTemplateString = true + break // ` + case 0x28: + paren++ + break // ( + case 0x29: + paren-- + break // ) + case 0x5b: + square++ + break // [ + case 0x5d: + square-- + break // ] + case 0x7b: + curly++ + break // { + case 0x7d: + curly-- + break // } + } + if (c === 0x2f) { + // / + let j = i - 1 + let p + // find first non-whitespace prev char + for (; j >= 0; j--) { + p = exp.charAt(j) + if (p !== ' ') break + } + if (!p || !validDivisionCharRE.test(p)) { + inRegex = true + } + } + } + } + + if (expression === undefined) { + expression = exp.slice(0, i).trim() + } else if (lastFilterIndex !== 0) { + pushFilter() + } + + function pushFilter() { + filters.push(exp.slice(lastFilterIndex, i).trim()) + lastFilterIndex = i + 1 + } + + if ( + filters.length && + warnDeprecation( + CompilerDeprecationTypes.COMPILER_FILTERS, + context, + node.loc + ) + ) { + for (i = 0; i < filters.length; i++) { + expression = wrapFilter(expression, filters[i], context) + } + node.content = expression + } +} + +function wrapFilter( + exp: string, + filter: string, + context: TransformContext +): string { + context.helper(RESOLVE_FILTER) + const i = filter.indexOf('(') + if (i < 0) { + context.filters!.add(filter) + return `${toValidAssetId(filter, 'filter')}(${exp})` + } else { + const name = filter.slice(0, i) + const args = filter.slice(i + 1) + context.filters!.add(name) + return `${toValidAssetId(name, 'filter')}(${exp}${ + args !== ')' ? ',' + args : args + }` + } +} diff --git a/packages/compiler-core/src/compile.ts b/packages/compiler-core/src/compile.ts index e3b6260b..78a6cb62 100644 --- a/packages/compiler-core/src/compile.ts +++ b/packages/compiler-core/src/compile.ts @@ -15,6 +15,7 @@ import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot' import { transformText } from './transforms/transformText' import { transformOnce } from './transforms/vOnce' import { transformModel } from './transforms/vModel' +import { transformFilter } from './compat/transformFilter' import { defaultOnError, createCompilerError, ErrorCodes } from './errors' export type TransformPreset = [ @@ -30,6 +31,7 @@ export function getBaseTransformPreset( transformOnce, transformIf, transformFor, + ...(__COMPAT__ ? [transformFilter] : []), ...(!__BROWSER__ && prefixIdentifiers ? [ // order is important diff --git a/packages/compiler-core/src/runtimeHelpers.ts b/packages/compiler-core/src/runtimeHelpers.ts index f40c94c3..94ce1ebc 100644 --- a/packages/compiler-core/src/runtimeHelpers.ts +++ b/packages/compiler-core/src/runtimeHelpers.ts @@ -14,6 +14,7 @@ export const RESOLVE_DYNAMIC_COMPONENT = Symbol( __DEV__ ? `resolveDynamicComponent` : `` ) export const RESOLVE_DIRECTIVE = Symbol(__DEV__ ? `resolveDirective` : ``) +export const RESOLVE_FILTER = Symbol(__DEV__ ? `resolveFilter` : ``) export const WITH_DIRECTIVES = Symbol(__DEV__ ? `withDirectives` : ``) export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``) export const RENDER_SLOT = Symbol(__DEV__ ? `renderSlot` : ``) @@ -50,6 +51,7 @@ export const helperNameMap: any = { [RESOLVE_COMPONENT]: `resolveComponent`, [RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`, [RESOLVE_DIRECTIVE]: `resolveDirective`, + [RESOLVE_FILTER]: `resolveFilter`, [WITH_DIRECTIVES]: `withDirectives`, [RENDER_LIST]: `renderList`, [RENDER_SLOT]: `renderSlot`, diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 36f7f2e0..add7ea13 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -118,6 +118,9 @@ export interface TransformContext hoist(exp: JSChildNode): SimpleExpressionNode cache(exp: T, isVNode?: boolean): CacheExpression | T constantCache: Map + + // 2.x Compat only + filters?: Set } export function createTransformContext( @@ -289,6 +292,10 @@ export function createTransformContext( } } + if (__COMPAT__) { + context.filters = new Set() + } + function addId(id: string) { const { identifiers } = context if (identifiers[id] === undefined) { @@ -321,6 +328,10 @@ export function transform(root: RootNode, options: TransformOptions) { root.hoists = context.hoists root.temps = context.temps root.cached = context.cached + + if (__COMPAT__) { + root.filters = [...context.filters!] + } } function createRootCodegen(root: RootNode, context: TransformContext) { diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 1b746111..4ebfbbe7 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -254,6 +254,11 @@ export function processExpression( parent && parentStack.push(parent) if (node.type === 'Identifier') { if (!isDuplicate(node)) { + // v2 wrapped filter call + if (__COMPAT__ && node.name.startsWith('_filter_')) { + return + } + const needPrefix = shouldPrefix(node, parent!, parentStack) if (!knownIds[node.name] && needPrefix) { if (isStaticProperty(parent!) && parent.shorthand) { diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index b65c812a..370a9ac6 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -271,7 +271,7 @@ export function injectProp( export function toValidAssetId( name: string, - type: 'component' | 'directive' + type: 'component' | 'directive' | 'filter' ): string { return `_${type}_${name.replace(/[^\w]/g, '_')}` } diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index bda1501b..53bb6b1a 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -17,6 +17,7 @@ import { isFunction, NO, isObject } from '@vue/shared' import { version } from '.' import { installCompatMount } from './compat/global' import { installLegacyConfigProperties } from './compat/globalConfig' +import { installGlobalFilterMethod } from './compat/filter' export interface App { version: string @@ -43,7 +44,13 @@ export interface App { _context: AppContext /** - * @internal 2.x compat only + * v2 compat only + */ + filter?(name: string): Function | undefined + filter?(name: string, filter: Function): this + + /** + * @internal v3 compat only */ _createRoot?(options: ComponentOptions): ComponentPublicInstance } @@ -92,6 +99,11 @@ export interface AppContext { * @internal */ reload?: () => void + /** + * v2 compat only + * @internal + */ + filters?: Record } type PluginInstallFunction = (app: App, ...options: any[]) => any @@ -307,6 +319,7 @@ export function createAppAPI( if (__COMPAT__) { installCompatMount(app, context, render, hydrate) + installGlobalFilterMethod(app, context) if (__DEV__) installLegacyConfigProperties(app.config) } diff --git a/packages/runtime-core/src/compat/compatConfig.ts b/packages/runtime-core/src/compat/compatConfig.ts index 290aa711..5ebe0770 100644 --- a/packages/runtime-core/src/compat/compatConfig.ts +++ b/packages/runtime-core/src/compat/compatConfig.ts @@ -56,7 +56,9 @@ export const enum DeprecationTypes { COMPONENT_FUNCTIONAL = 'COMPONENT_FUNCTIONAL', COMPONENT_V_MODEL = 'COMPONENT_V_MODEL', - RENDER_FUNCTION = 'RENDER_FUNCTION' + RENDER_FUNCTION = 'RENDER_FUNCTION', + + FILTERS = 'FILTERS' } type DeprecationData = { @@ -392,6 +394,14 @@ const deprecationData: Record = { }: false })\n` + `\n (This can also be done per-component via the "compatConfig" option.)`, link: `https://v3.vuejs.org/guide/migration/render-function-api.html` + }, + + [DeprecationTypes.FILTERS]: { + message: + `filters have been removed in Vue 3. ` + + `The "|" symbol will be treated as native JavaScript bitwise OR operator. ` + + `Use method calls or computed properties instead.`, + link: `https://v3.vuejs.org/guide/migration/filters.html` } } diff --git a/packages/runtime-core/src/compat/filter.ts b/packages/runtime-core/src/compat/filter.ts new file mode 100644 index 00000000..29a5a3fc --- /dev/null +++ b/packages/runtime-core/src/compat/filter.ts @@ -0,0 +1,18 @@ +import { App, AppContext } from '../apiCreateApp' +import { warn } from '../warning' +import { assertCompatEnabled, DeprecationTypes } from './compatConfig' + +export function installGlobalFilterMethod(app: App, context: AppContext) { + context.filters = {} + app.filter = (name: string, filter?: Function): any => { + assertCompatEnabled(DeprecationTypes.FILTERS, null) + if (!filter) { + return context.filters![name] + } + if (__DEV__ && context.filters![name]) { + warn(`Filter "${name}" has already been registered.`) + } + context.filters![name] = filter + return app + } +} diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 0c6c2ca8..83ee02ea 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -268,6 +268,11 @@ export interface ComponentInternalInstance { * @internal */ directives: Record | null + /** + * Resolved filters registry, v2 compat only + * @internal + */ + filters?: Record /** * resolved props options * @internal diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index be38504b..2e729bfe 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -72,6 +72,12 @@ import { isCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig' +import { + AssetTypes, + COMPONENTS, + DIRECTIVES, + FILTERS +} from './helpers/resolveAssets' /** * Interface for declaring custom options. @@ -413,6 +419,9 @@ interface LegacyOptions< provide?: Data | Function inject?: ComponentInjectOptions + // assets + filters?: Record + // composition mixins?: Mixin[] extends?: Extends @@ -510,9 +519,6 @@ export function applyOptions( watch: watchOptions, provide: provideOptions, inject: injectOptions, - // assets - components, - directives, // lifecycle beforeMount, mounted, @@ -721,25 +727,10 @@ export function applyOptions( // To reduce memory usage, only components with mixins or extends will have // resolved asset registry attached to instance. if (asMixin) { - if (components) { - extend( - instance.components || - (instance.components = extend( - {}, - (instance.type as ComponentOptions).components - ) as Record), - components - ) - } - if (directives) { - extend( - instance.directives || - (instance.directives = extend( - {}, - (instance.type as ComponentOptions).directives - )), - directives - ) + resolveInstanceAssets(instance, options, COMPONENTS) + resolveInstanceAssets(instance, options, DIRECTIVES) + if (__COMPAT__ && isCompatEnabled(DeprecationTypes.FILTERS, instance)) { + resolveInstanceAssets(instance, options, FILTERS) } } @@ -818,6 +809,23 @@ export function applyOptions( } } +function resolveInstanceAssets( + instance: ComponentInternalInstance, + mixin: ComponentOptions, + type: AssetTypes +) { + if (mixin[type]) { + extend( + instance[type] || + (instance[type] = extend( + {}, + (instance.type as ComponentOptions)[type] + ) as any), + mixin[type] + ) + } +} + export function resolveInjections( injectOptions: ComponentInjectOptions, ctx: any, diff --git a/packages/runtime-core/src/helpers/resolveAssets.ts b/packages/runtime-core/src/helpers/resolveAssets.ts index e867cc51..119bdbe6 100644 --- a/packages/runtime-core/src/helpers/resolveAssets.ts +++ b/packages/runtime-core/src/helpers/resolveAssets.ts @@ -10,8 +10,11 @@ import { camelize, capitalize, isString } from '@vue/shared' import { warn } from '../warning' import { VNodeTypes } from '../vnode' -const COMPONENTS = 'components' -const DIRECTIVES = 'directives' +export const COMPONENTS = 'components' +export const DIRECTIVES = 'directives' +export const FILTERS = 'filters' + +export type AssetTypes = typeof COMPONENTS | typeof DIRECTIVES | typeof FILTERS /** * @private @@ -44,6 +47,14 @@ export function resolveDirective(name: string): Directive | undefined { return resolveAsset(DIRECTIVES, name) } +/** + * v2 compat only + * @internal + */ +export function resolveFilter(name: string): Function | undefined { + return resolveAsset(FILTERS, name) +} + /** * @private * overload 1: components @@ -60,8 +71,11 @@ function resolveAsset( name: string ): Directive | undefined // implementation +// overload 3: filters (compat only) +function resolveAsset(type: typeof FILTERS, name: string): Function | undefined +// implementation function resolveAsset( - type: typeof COMPONENTS | typeof DIRECTIVES, + type: AssetTypes, name: string, warnMissing = true, maybeSelfReference = false diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 42c5481b..3b4a1656 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -293,6 +293,12 @@ import { checkCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig' +import { resolveFilter as _resolveFilter } from './helpers/resolveAssets' + +/** + * @internal only exposed in compat builds + */ +export const resolveFilter = __COMPAT__ ? _resolveFilter : null const _compatUtils = { warnDeprecation, diff --git a/packages/template-explorer/package.json b/packages/template-explorer/package.json index f7afa8a5..60117ed7 100644 --- a/packages/template-explorer/package.json +++ b/packages/template-explorer/package.json @@ -6,6 +6,7 @@ "formats": [ "global" ], + "compat": true, "env": "development", "enableNonBrowserBranches": true }, diff --git a/packages/template-explorer/src/options.ts b/packages/template-explorer/src/options.ts index e8669bc7..e7b56fcc 100644 --- a/packages/template-explorer/src/options.ts +++ b/packages/template-explorer/src/options.ts @@ -8,12 +8,12 @@ export const compilerOptions: CompilerOptions = reactive({ mode: 'module', filename: 'Foo.vue', prefixIdentifiers: false, - optimizeImports: false, hoistStatic: false, cacheHandlers: false, scopeId: null, inline: false, ssrCssVars: `{ color }`, + compatConfig: { MODE: 3 }, bindingMetadata: { TestComponent: BindingTypes.SETUP_CONST, setupRef: BindingTypes.SETUP_REF, @@ -170,18 +170,20 @@ const App = { h('label', { for: 'inline' }, 'inline') ]), - // toggle optimizeImports + // compat mode h('li', [ h('input', { type: 'checkbox', - id: 'optimize-imports', - disabled: !isModule || isSSR, - checked: isModule && !isSSR && compilerOptions.optimizeImports, + id: 'compat', + checked: compilerOptions.compatConfig!.MODE === 2, onChange(e: Event) { - compilerOptions.optimizeImports = (e.target as HTMLInputElement).checked + compilerOptions.compatConfig!.MODE = (e.target as HTMLInputElement) + .checked + ? 2 + : 3 } }), - h('label', { for: 'optimize-imports' }, 'optimizeImports') + h('label', { for: 'compat' }, 'v2 compat mode') ]) ]) ])