From 7d0c63ff4361e59e820441a24bf4fb2a93335a1e Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 14 Nov 2022 16:20:12 +0800 Subject: [PATCH] fix(custom-elements): use strict number casting close #4946 close #2598 close #2604 This commit also refactors internal usage of previous loose implementation of `toNumber` to the stricter version where applicable. Use of `looseToNumber` is preserved for `v-model.number` modifier to ensure backwards compatibility and consistency with Vue 2 behavior. --- packages/runtime-core/src/compat/instance.ts | 6 +++--- packages/runtime-core/src/componentEmits.ts | 6 +++--- .../runtime-core/src/components/Suspense.ts | 11 ++++++++++- packages/runtime-core/src/index.ts | 2 +- packages/runtime-core/src/warning.ts | 12 ++++++++++++ .../runtime-dom/src/components/Transition.ts | 18 ++---------------- packages/runtime-dom/src/directives/vModel.ts | 11 +++++++---- packages/shared/src/index.ts | 14 +++++++++++++- 8 files changed, 51 insertions(+), 29 deletions(-) diff --git a/packages/runtime-core/src/compat/instance.ts b/packages/runtime-core/src/compat/instance.ts index 01e6618d45b..141f0bf0a0f 100644 --- a/packages/runtime-core/src/compat/instance.ts +++ b/packages/runtime-core/src/compat/instance.ts @@ -2,9 +2,9 @@ import { extend, looseEqual, looseIndexOf, + looseToNumber, NOOP, - toDisplayString, - toNumber + toDisplayString } from '@vue/shared' import { ComponentPublicInstance, @@ -148,7 +148,7 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) { $createElement: () => compatH, _c: () => compatH, _o: () => legacyMarkOnce, - _n: () => toNumber, + _n: () => looseToNumber, _s: () => toDisplayString, _l: () => renderList, _t: i => legacyRenderSlot.bind(null, i), diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index 9a9fe050150..7568741e24e 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -10,8 +10,8 @@ import { isObject, isString, isOn, - toNumber, - UnionToIntersection + UnionToIntersection, + looseToNumber } from '@vue/shared' import { ComponentInternalInstance, @@ -126,7 +126,7 @@ export function emit( args = rawArgs.map(a => (isString(a) ? a.trim() : a)) } if (number) { - args = rawArgs.map(toNumber) + args = rawArgs.map(looseToNumber) } } diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index baf57088626..6ccca1e26b8 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -22,7 +22,12 @@ import { } from '../renderer' import { queuePostFlushCb } from '../scheduler' import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils' -import { pushWarningContext, popWarningContext, warn } from '../warning' +import { + pushWarningContext, + popWarningContext, + warn, + assertNumber +} from '../warning' import { handleError, ErrorCodes } from '../errorHandling' export interface SuspenseProps { @@ -419,6 +424,10 @@ function createSuspenseBoundary( } = rendererInternals const timeout = toNumber(vnode.props && vnode.props.timeout) + if (__DEV__) { + assertNumber(timeout, `Suspense timeout`) + } + const suspense: SuspenseBoundary = { vnode, parent, diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 7f822489bdc..086beaa6a97 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -104,7 +104,7 @@ export { useSSRContext, ssrContextKey } from './helpers/useSsrContext' export { createRenderer, createHydrationRenderer } from './renderer' export { queuePostFlushCb } from './scheduler' -export { warn } from './warning' +export { warn, assertNumber } from './warning' export { handleError, callWithErrorHandling, diff --git a/packages/runtime-core/src/warning.ts b/packages/runtime-core/src/warning.ts index 9b793ab5148..b314985b771 100644 --- a/packages/runtime-core/src/warning.ts +++ b/packages/runtime-core/src/warning.ts @@ -162,3 +162,15 @@ function formatProp(key: string, value: unknown, raw?: boolean): any { return raw ? value : [`${key}=`, value] } } + +/** + * @internal + */ +export function assertNumber(val: unknown, type: string) { + if (!__DEV__) return + if (typeof val !== 'number') { + warn(`${type} is not a valid number - ` + `got ${JSON.stringify(val)}.`) + } else if (isNaN(val)) { + warn(`${type} is NaN - ` + 'the duration expression might be incorrect.') + } +} diff --git a/packages/runtime-dom/src/components/Transition.ts b/packages/runtime-dom/src/components/Transition.ts index 2c483c76e72..205bea9668f 100644 --- a/packages/runtime-dom/src/components/Transition.ts +++ b/packages/runtime-dom/src/components/Transition.ts @@ -2,7 +2,7 @@ import { BaseTransition, BaseTransitionProps, h, - warn, + assertNumber, FunctionalComponent, compatUtils, DeprecationTypes @@ -283,24 +283,10 @@ function normalizeDuration( function NumberOf(val: unknown): number { const res = toNumber(val) - if (__DEV__) validateDuration(res) + if (__DEV__) assertNumber(res, ' explicit duration') return res } -function validateDuration(val: unknown) { - if (typeof val !== 'number') { - warn( - ` explicit duration is not a valid number - ` + - `got ${JSON.stringify(val)}.` - ) - } else if (isNaN(val)) { - warn( - ` explicit duration is NaN - ` + - 'the duration expression might be incorrect.' - ) - } -} - export function addTransitionClass(el: Element, cls: string) { cls.split(/\s+/).forEach(c => c && el.classList.add(c)) ;( diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 722b4d9b44c..2cf5f4cfc16 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -11,7 +11,7 @@ import { looseEqual, looseIndexOf, invokeArrayFns, - toNumber, + looseToNumber, isSet } from '@vue/shared' @@ -54,7 +54,7 @@ export const vModelText: ModelDirective< domValue = domValue.trim() } if (castToNumber) { - domValue = toNumber(domValue) + domValue = looseToNumber(domValue) } el._assign(domValue) }) @@ -88,7 +88,10 @@ export const vModelText: ModelDirective< if (trim && el.value.trim() === value) { return } - if ((number || el.type === 'number') && toNumber(el.value) === value) { + if ( + (number || el.type === 'number') && + looseToNumber(el.value) === value + ) { return } } @@ -182,7 +185,7 @@ export const vModelSelect: ModelDirective = { const selectedVal = Array.prototype.filter .call(el.options, (o: HTMLOptionElement) => o.selected) .map((o: HTMLOptionElement) => - number ? toNumber(getValue(o)) : getValue(o) + number ? looseToNumber(getValue(o)) : getValue(o) ) el._assign( el.multiple diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index ff302503819..5c1629d5bc4 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -153,11 +153,23 @@ export const def = (obj: object, key: string | symbol, value: any) => { }) } -export const toNumber = (val: any): any => { +/** + * "123-foo" will be parsed to 123 + * This is used for the .number modifier in v-model + */ +export const looseToNumber = (val: any): any => { const n = parseFloat(val) return isNaN(n) ? val : n } +/** + * "123-foo" will be returned as-is + */ +export const toNumber = (val: any): any => { + const n = Number(val) + return isNaN(n) ? val : n +} + let _globalThis: any export const getGlobalThis = (): any => { return (