wip: filters compat

This commit is contained in:
Evan You 2021-04-19 12:08:26 -04:00
parent 467076361a
commit 7dc681c196
18 changed files with 349 additions and 41 deletions

View File

@ -109,6 +109,9 @@ export interface RootNode extends Node {
temps: number temps: number
ssrHelpers?: symbol[] ssrHelpers?: symbol[]
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement codegenNode?: TemplateChildNode | JSChildNode | BlockStatement
// v2 compat only
filters?: string[]
} }
export type ElementNode = export type ElementNode =

View File

@ -50,7 +50,8 @@ import {
CREATE_BLOCK, CREATE_BLOCK,
OPEN_BLOCK, OPEN_BLOCK,
CREATE_STATIC, CREATE_STATIC,
WITH_CTX WITH_CTX,
RESOLVE_FILTER
} from './runtimeHelpers' } from './runtimeHelpers'
import { ImportItem } from './transform' import { ImportItem } from './transform'
@ -274,6 +275,12 @@ export function generate(
newline() newline()
} }
} }
if (__COMPAT__ && ast.filters && ast.filters.length) {
newline()
genAssets(ast.filters, 'filter', context)
newline()
}
if (ast.temps > 0) { if (ast.temps > 0) {
push(`let `) push(`let `)
for (let i = 0; i < ast.temps; i++) { for (let i = 0; i < ast.temps; i++) {
@ -458,11 +465,15 @@ function genModulePreamble(
function genAssets( function genAssets(
assets: string[], assets: string[],
type: 'component' | 'directive', type: 'component' | 'directive' | 'filter',
{ helper, push, newline }: CodegenContext { helper, push, newline }: CodegenContext
) { ) {
const resolver = helper( 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++) { for (let i = 0; i < assets.length; i++) {
let id = assets[i] let id = assets[i]

View File

@ -22,7 +22,7 @@ export const enum CompilerDeprecationTypes {
COMPILER_V_IF_V_FOR_PRECEDENCE = 'COMPILER_V_IF_V_FOR_PRECEDENCE', COMPILER_V_IF_V_FOR_PRECEDENCE = 'COMPILER_V_IF_V_FOR_PRECEDENCE',
COMPILER_NATIVE_TEMPLATE = 'COMPILER_NATIVE_TEMPLATE', COMPILER_NATIVE_TEMPLATE = 'COMPILER_NATIVE_TEMPLATE',
COMPILER_INLINE_TEMPLATE = 'COMPILER_INLINE_TEMPLATE', COMPILER_INLINE_TEMPLATE = 'COMPILER_INLINE_TEMPLATE',
COMPILER_FILTER = 'COMPILER_FILTER' COMPILER_FILTERS = 'COMPILER_FILTER'
} }
type DeprecationData = { type DeprecationData = {
@ -89,8 +89,11 @@ const deprecationData: Record<CompilerDeprecationTypes, DeprecationData> = {
link: `https://v3.vuejs.org/guide/migration/inline-template-attribute.html` link: `https://v3.vuejs.org/guide/migration/inline-template-attribute.html`
}, },
[CompilerDeprecationTypes.COMPILER_FILTER]: { [CompilerDeprecationTypes.COMPILER_FILTERS]: {
message: `filters have been removed in Vue 3.`, 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` link: `https://v3.vuejs.org/guide/migration/filters.html`
} }
} }

View File

@ -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
}`
}
}

View File

@ -15,6 +15,7 @@ import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
import { transformText } from './transforms/transformText' import { transformText } from './transforms/transformText'
import { transformOnce } from './transforms/vOnce' import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel' import { transformModel } from './transforms/vModel'
import { transformFilter } from './compat/transformFilter'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors' import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
export type TransformPreset = [ export type TransformPreset = [
@ -30,6 +31,7 @@ export function getBaseTransformPreset(
transformOnce, transformOnce,
transformIf, transformIf,
transformFor, transformFor,
...(__COMPAT__ ? [transformFilter] : []),
...(!__BROWSER__ && prefixIdentifiers ...(!__BROWSER__ && prefixIdentifiers
? [ ? [
// order is important // order is important

View File

@ -14,6 +14,7 @@ export const RESOLVE_DYNAMIC_COMPONENT = Symbol(
__DEV__ ? `resolveDynamicComponent` : `` __DEV__ ? `resolveDynamicComponent` : ``
) )
export const RESOLVE_DIRECTIVE = Symbol(__DEV__ ? `resolveDirective` : ``) export const RESOLVE_DIRECTIVE = Symbol(__DEV__ ? `resolveDirective` : ``)
export const RESOLVE_FILTER = Symbol(__DEV__ ? `resolveFilter` : ``)
export const WITH_DIRECTIVES = Symbol(__DEV__ ? `withDirectives` : ``) export const WITH_DIRECTIVES = Symbol(__DEV__ ? `withDirectives` : ``)
export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``) export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``)
export const RENDER_SLOT = Symbol(__DEV__ ? `renderSlot` : ``) export const RENDER_SLOT = Symbol(__DEV__ ? `renderSlot` : ``)
@ -50,6 +51,7 @@ export const helperNameMap: any = {
[RESOLVE_COMPONENT]: `resolveComponent`, [RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`, [RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`, [RESOLVE_DIRECTIVE]: `resolveDirective`,
[RESOLVE_FILTER]: `resolveFilter`,
[WITH_DIRECTIVES]: `withDirectives`, [WITH_DIRECTIVES]: `withDirectives`,
[RENDER_LIST]: `renderList`, [RENDER_LIST]: `renderList`,
[RENDER_SLOT]: `renderSlot`, [RENDER_SLOT]: `renderSlot`,

View File

@ -118,6 +118,9 @@ export interface TransformContext
hoist(exp: JSChildNode): SimpleExpressionNode hoist(exp: JSChildNode): SimpleExpressionNode
cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
constantCache: Map<TemplateChildNode, ConstantTypes> constantCache: Map<TemplateChildNode, ConstantTypes>
// 2.x Compat only
filters?: Set<string>
} }
export function createTransformContext( export function createTransformContext(
@ -289,6 +292,10 @@ export function createTransformContext(
} }
} }
if (__COMPAT__) {
context.filters = new Set()
}
function addId(id: string) { function addId(id: string) {
const { identifiers } = context const { identifiers } = context
if (identifiers[id] === undefined) { if (identifiers[id] === undefined) {
@ -321,6 +328,10 @@ export function transform(root: RootNode, options: TransformOptions) {
root.hoists = context.hoists root.hoists = context.hoists
root.temps = context.temps root.temps = context.temps
root.cached = context.cached root.cached = context.cached
if (__COMPAT__) {
root.filters = [...context.filters!]
}
} }
function createRootCodegen(root: RootNode, context: TransformContext) { function createRootCodegen(root: RootNode, context: TransformContext) {

View File

@ -254,6 +254,11 @@ export function processExpression(
parent && parentStack.push(parent) parent && parentStack.push(parent)
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
if (!isDuplicate(node)) { if (!isDuplicate(node)) {
// v2 wrapped filter call
if (__COMPAT__ && node.name.startsWith('_filter_')) {
return
}
const needPrefix = shouldPrefix(node, parent!, parentStack) const needPrefix = shouldPrefix(node, parent!, parentStack)
if (!knownIds[node.name] && needPrefix) { if (!knownIds[node.name] && needPrefix) {
if (isStaticProperty(parent!) && parent.shorthand) { if (isStaticProperty(parent!) && parent.shorthand) {

View File

@ -271,7 +271,7 @@ export function injectProp(
export function toValidAssetId( export function toValidAssetId(
name: string, name: string,
type: 'component' | 'directive' type: 'component' | 'directive' | 'filter'
): string { ): string {
return `_${type}_${name.replace(/[^\w]/g, '_')}` return `_${type}_${name.replace(/[^\w]/g, '_')}`
} }

View File

@ -17,6 +17,7 @@ import { isFunction, NO, isObject } from '@vue/shared'
import { version } from '.' import { version } from '.'
import { installCompatMount } from './compat/global' import { installCompatMount } from './compat/global'
import { installLegacyConfigProperties } from './compat/globalConfig' import { installLegacyConfigProperties } from './compat/globalConfig'
import { installGlobalFilterMethod } from './compat/filter'
export interface App<HostElement = any> { export interface App<HostElement = any> {
version: string version: string
@ -43,7 +44,13 @@ export interface App<HostElement = any> {
_context: AppContext _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 _createRoot?(options: ComponentOptions): ComponentPublicInstance
} }
@ -92,6 +99,11 @@ export interface AppContext {
* @internal * @internal
*/ */
reload?: () => void reload?: () => void
/**
* v2 compat only
* @internal
*/
filters?: Record<string, Function>
} }
type PluginInstallFunction = (app: App, ...options: any[]) => any type PluginInstallFunction = (app: App, ...options: any[]) => any
@ -307,6 +319,7 @@ export function createAppAPI<HostElement>(
if (__COMPAT__) { if (__COMPAT__) {
installCompatMount(app, context, render, hydrate) installCompatMount(app, context, render, hydrate)
installGlobalFilterMethod(app, context)
if (__DEV__) installLegacyConfigProperties(app.config) if (__DEV__) installLegacyConfigProperties(app.config)
} }

View File

@ -56,7 +56,9 @@ export const enum DeprecationTypes {
COMPONENT_FUNCTIONAL = 'COMPONENT_FUNCTIONAL', COMPONENT_FUNCTIONAL = 'COMPONENT_FUNCTIONAL',
COMPONENT_V_MODEL = 'COMPONENT_V_MODEL', COMPONENT_V_MODEL = 'COMPONENT_V_MODEL',
RENDER_FUNCTION = 'RENDER_FUNCTION' RENDER_FUNCTION = 'RENDER_FUNCTION',
FILTERS = 'FILTERS'
} }
type DeprecationData = { type DeprecationData = {
@ -392,6 +394,14 @@ const deprecationData: Record<DeprecationTypes, DeprecationData> = {
}: false })\n` + }: false })\n` +
`\n (This can also be done per-component via the "compatConfig" option.)`, `\n (This can also be done per-component via the "compatConfig" option.)`,
link: `https://v3.vuejs.org/guide/migration/render-function-api.html` 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`
} }
} }

View File

@ -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
}
}

View File

@ -268,6 +268,11 @@ export interface ComponentInternalInstance {
* @internal * @internal
*/ */
directives: Record<string, Directive> | null directives: Record<string, Directive> | null
/**
* Resolved filters registry, v2 compat only
* @internal
*/
filters?: Record<string, Function>
/** /**
* resolved props options * resolved props options
* @internal * @internal

View File

@ -72,6 +72,12 @@ import {
isCompatEnabled, isCompatEnabled,
softAssertCompatEnabled softAssertCompatEnabled
} from './compat/compatConfig' } from './compat/compatConfig'
import {
AssetTypes,
COMPONENTS,
DIRECTIVES,
FILTERS
} from './helpers/resolveAssets'
/** /**
* Interface for declaring custom options. * Interface for declaring custom options.
@ -413,6 +419,9 @@ interface LegacyOptions<
provide?: Data | Function provide?: Data | Function
inject?: ComponentInjectOptions inject?: ComponentInjectOptions
// assets
filters?: Record<string, Function>
// composition // composition
mixins?: Mixin[] mixins?: Mixin[]
extends?: Extends extends?: Extends
@ -510,9 +519,6 @@ export function applyOptions(
watch: watchOptions, watch: watchOptions,
provide: provideOptions, provide: provideOptions,
inject: injectOptions, inject: injectOptions,
// assets
components,
directives,
// lifecycle // lifecycle
beforeMount, beforeMount,
mounted, mounted,
@ -721,25 +727,10 @@ export function applyOptions(
// To reduce memory usage, only components with mixins or extends will have // To reduce memory usage, only components with mixins or extends will have
// resolved asset registry attached to instance. // resolved asset registry attached to instance.
if (asMixin) { if (asMixin) {
if (components) { resolveInstanceAssets(instance, options, COMPONENTS)
extend( resolveInstanceAssets(instance, options, DIRECTIVES)
instance.components || if (__COMPAT__ && isCompatEnabled(DeprecationTypes.FILTERS, instance)) {
(instance.components = extend( resolveInstanceAssets(instance, options, FILTERS)
{},
(instance.type as ComponentOptions).components
) as Record<string, ConcreteComponent>),
components
)
}
if (directives) {
extend(
instance.directives ||
(instance.directives = extend(
{},
(instance.type as ComponentOptions).directives
)),
directives
)
} }
} }
@ -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( export function resolveInjections(
injectOptions: ComponentInjectOptions, injectOptions: ComponentInjectOptions,
ctx: any, ctx: any,

View File

@ -10,8 +10,11 @@ import { camelize, capitalize, isString } from '@vue/shared'
import { warn } from '../warning' import { warn } from '../warning'
import { VNodeTypes } from '../vnode' import { VNodeTypes } from '../vnode'
const COMPONENTS = 'components' export const COMPONENTS = 'components'
const DIRECTIVES = 'directives' export const DIRECTIVES = 'directives'
export const FILTERS = 'filters'
export type AssetTypes = typeof COMPONENTS | typeof DIRECTIVES | typeof FILTERS
/** /**
* @private * @private
@ -44,6 +47,14 @@ export function resolveDirective(name: string): Directive | undefined {
return resolveAsset(DIRECTIVES, name) return resolveAsset(DIRECTIVES, name)
} }
/**
* v2 compat only
* @internal
*/
export function resolveFilter(name: string): Function | undefined {
return resolveAsset(FILTERS, name)
}
/** /**
* @private * @private
* overload 1: components * overload 1: components
@ -60,8 +71,11 @@ function resolveAsset(
name: string name: string
): Directive | undefined ): Directive | undefined
// implementation // implementation
// overload 3: filters (compat only)
function resolveAsset(type: typeof FILTERS, name: string): Function | undefined
// implementation
function resolveAsset( function resolveAsset(
type: typeof COMPONENTS | typeof DIRECTIVES, type: AssetTypes,
name: string, name: string,
warnMissing = true, warnMissing = true,
maybeSelfReference = false maybeSelfReference = false

View File

@ -293,6 +293,12 @@ import {
checkCompatEnabled, checkCompatEnabled,
softAssertCompatEnabled softAssertCompatEnabled
} from './compat/compatConfig' } from './compat/compatConfig'
import { resolveFilter as _resolveFilter } from './helpers/resolveAssets'
/**
* @internal only exposed in compat builds
*/
export const resolveFilter = __COMPAT__ ? _resolveFilter : null
const _compatUtils = { const _compatUtils = {
warnDeprecation, warnDeprecation,

View File

@ -6,6 +6,7 @@
"formats": [ "formats": [
"global" "global"
], ],
"compat": true,
"env": "development", "env": "development",
"enableNonBrowserBranches": true "enableNonBrowserBranches": true
}, },

View File

@ -8,12 +8,12 @@ export const compilerOptions: CompilerOptions = reactive({
mode: 'module', mode: 'module',
filename: 'Foo.vue', filename: 'Foo.vue',
prefixIdentifiers: false, prefixIdentifiers: false,
optimizeImports: false,
hoistStatic: false, hoistStatic: false,
cacheHandlers: false, cacheHandlers: false,
scopeId: null, scopeId: null,
inline: false, inline: false,
ssrCssVars: `{ color }`, ssrCssVars: `{ color }`,
compatConfig: { MODE: 3 },
bindingMetadata: { bindingMetadata: {
TestComponent: BindingTypes.SETUP_CONST, TestComponent: BindingTypes.SETUP_CONST,
setupRef: BindingTypes.SETUP_REF, setupRef: BindingTypes.SETUP_REF,
@ -170,18 +170,20 @@ const App = {
h('label', { for: 'inline' }, 'inline') h('label', { for: 'inline' }, 'inline')
]), ]),
// toggle optimizeImports // compat mode
h('li', [ h('li', [
h('input', { h('input', {
type: 'checkbox', type: 'checkbox',
id: 'optimize-imports', id: 'compat',
disabled: !isModule || isSSR, checked: compilerOptions.compatConfig!.MODE === 2,
checked: isModule && !isSSR && compilerOptions.optimizeImports,
onChange(e: Event) { 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')
]) ])
]) ])
]) ])