diff --git a/package.json b/package.json index 3167b0b8..46cdb8a7 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ }, "homepage": "https://github.com/vuejs/composition-api#readme", "devDependencies": { - "@types/jest": "^24.0.13", + "@types/jest": "^25.2.1", "@types/node": "^12.0.2", "cross-env": "^5.2.0", "husky": "^2.7.0", @@ -76,6 +76,9 @@ }, "jest": { "verbose": true, + "globals": { + "__DEV__": true + }, "setupFiles": [ "/test/setupTest.js" ], diff --git a/rollup.config.js b/rollup.config.js index 12edbef9..4a758ba6 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -56,7 +56,15 @@ function genConfig({ outFile, format, mode }) { typescript: require('typescript'), }), resolve(), - replace({ 'process.env.NODE_ENV': JSON.stringify(isProd ? 'production' : 'development') }), + replace({ + 'process.env.NODE_ENV': JSON.stringify(isProd ? 'production' : 'development'), + __DEV__: + format === 'es' + ? // preserve to be handled by bundlers + `(__DEV__)` + : // hard coded dev/prod builds + !isProd, + }), isProd && terser(), ].filter(Boolean), }; diff --git a/src/apis/computed.ts b/src/apis/computed.ts index 6bbcfee9..861e7f9f 100644 --- a/src/apis/computed.ts +++ b/src/apis/computed.ts @@ -8,14 +8,20 @@ interface Option { set: (value: T) => void; } +export interface ComputedRef extends WritableComputedRef { + readonly value: T; +} + +export interface WritableComputedRef extends Ref {} + // read-only -export function computed(getter: Option['get']): Readonly>>; +export function computed(getter: Option['get']): ComputedRef; // writable -export function computed(options: Option): Ref>; +export function computed(options: Option): WritableComputedRef; // implement export function computed( options: Option['get'] | Option -): Readonly>> | Ref> { +): ComputedRef | WritableComputedRef { const vm = getCurrentVM(); let get: Option['get'], set: Option['set'] | undefined; if (typeof options === 'function') { @@ -37,7 +43,7 @@ export function computed( return createRef({ get: () => (computedHost as any).$$state, set: (v: T) => { - if (process.env.NODE_ENV !== 'production' && !set) { + if (__DEV__ && !set) { warn('Computed property was assigned to but it has no setter.', vm!); return; } diff --git a/src/apis/inject.ts b/src/apis/inject.ts index a059e9a7..fa6f60de 100644 --- a/src/apis/inject.ts +++ b/src/apis/inject.ts @@ -1,11 +1,12 @@ import { ComponentInstance } from '../component'; -import { ensureCurrentVMInFn } from '../helper'; +import { currentVMInFn } from '../helper'; import { hasOwn, warn } from '../utils'; +import { getCurrentVM } from '../runtimeContext'; const NOT_FOUND = {}; export interface InjectionKey extends Symbol {} -function resolveInject(provideKey: InjectionKey, vm: ComponentInstance): any { +function resolveInject(provideKey: InjectionKey | string, vm: ComponentInstance): any { let source = vm; while (source) { // @ts-ignore @@ -20,7 +21,9 @@ function resolveInject(provideKey: InjectionKey, vm: ComponentInstance): an } export function provide(key: InjectionKey | string, value: T): void { - const vm: any = ensureCurrentVMInFn('provide'); + const vm: any = currentVMInFn('provide'); + if (!vm) return; + if (!vm._provided) { const provideCache = {}; Object.defineProperty(vm, '_provided', { @@ -34,19 +37,23 @@ export function provide(key: InjectionKey | string, value: T): void { export function inject(key: InjectionKey | string): T | undefined; export function inject(key: InjectionKey | string, defaultValue: T): T; -export function inject(key: InjectionKey | string, defaultValue?: T): T | undefined { +export function inject(key: InjectionKey | string, defaultValue?: unknown) { if (!key) { return defaultValue; } - const vm = ensureCurrentVMInFn('inject'); - const val = resolveInject(key as InjectionKey, vm); - if (val !== NOT_FOUND) { - return val; - } else { - if (defaultValue === undefined && process.env.NODE_ENV !== 'production') { - warn(`Injection "${String(key)}" not found`, vm); + const vm = getCurrentVM(); + if (vm) { + const val = resolveInject(key, vm); + if (val !== NOT_FOUND) { + return val; + } else { + if (defaultValue === undefined && process.env.NODE_ENV !== 'production') { + warn(`Injection "${String(key)}" not found`, vm); + } + return defaultValue; } - return defaultValue; + } else { + warn(`inject() can only be used inside setup() or functional components.`); } } diff --git a/src/apis/lifecycle.ts b/src/apis/lifecycle.ts index 5d44ad03..d6f6651e 100644 --- a/src/apis/lifecycle.ts +++ b/src/apis/lifecycle.ts @@ -1,20 +1,34 @@ import { VueConstructor } from 'vue'; import { ComponentInstance } from '../component'; -import { getCurrentVue } from '../runtimeContext'; -import { ensureCurrentVMInFn } from '../helper'; +import { getCurrentVue, setCurrentVM, getCurrentVM } from '../runtimeContext'; +import { currentVMInFn } from '../helper'; const genName = (name: string) => `on${name[0].toUpperCase() + name.slice(1)}`; function createLifeCycle(lifeCyclehook: string) { return (callback: Function) => { - const vm = ensureCurrentVMInFn(genName(lifeCyclehook)); - injectHookOption(getCurrentVue(), vm, lifeCyclehook, callback); + const vm = currentVMInFn(genName(lifeCyclehook)); + if (vm) { + injectHookOption(getCurrentVue(), vm, lifeCyclehook, callback); + } }; } function injectHookOption(Vue: VueConstructor, vm: ComponentInstance, hook: string, val: Function) { const options = vm.$options as any; const mergeFn = Vue.config.optionMergeStrategies[hook]; - options[hook] = mergeFn(options[hook], val); + options[hook] = mergeFn(options[hook], wrapHookCall(vm, val)); +} + +function wrapHookCall(vm: ComponentInstance, fn: Function) { + return (...args: any) => { + let preVm = getCurrentVM(); + setCurrentVM(vm); + try { + return fn(...args); + } finally { + setCurrentVM(preVm); + } + }; } // export const onCreated = createLifeCycle('created'); diff --git a/src/apis/state.ts b/src/apis/state.ts index dc6e2856..15b21d1e 100644 --- a/src/apis/state.ts +++ b/src/apis/state.ts @@ -1 +1,17 @@ -export { reactive, ref, Ref, isRef, toRefs, set } from '../reactivity'; +export { + reactive, + ref, + Ref, + isRef, + toRefs, + set, + toRef, + isReactive, + UnwrapRef, + markRaw, + unref, + shallowReactive, + toRaw, + shallowRef, + triggerRef, +} from '../reactivity'; diff --git a/src/apis/watch.ts b/src/apis/watch.ts index 2e874ba9..a860e045 100644 --- a/src/apis/watch.ts +++ b/src/apis/watch.ts @@ -1,30 +1,46 @@ import { ComponentInstance } from '../component'; -import { Ref, isRef } from '../reactivity'; -import { assert, logError, noopFn, warn } from '../utils'; +import { Ref, isRef, isReactive } from '../reactivity'; +import { assert, logError, noopFn, warn, isFunction } from '../utils'; import { defineComponentInstance } from '../helper'; import { getCurrentVM, getCurrentVue } from '../runtimeContext'; import { WatcherPreFlushQueueKey, WatcherPostFlushQueueKey } from '../symbols'; +import { ComputedRef } from './computed'; -type CleanupRegistrator = (invalidate: () => void) => void; +export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void; -type SimpleEffect = (onCleanup: CleanupRegistrator) => void; +export type WatchSource = Ref | ComputedRef | (() => T); -type StopHandle = () => void; - -type WatcherCallBack = (newVal: T, oldVal: T, onCleanup: CleanupRegistrator) => void; - -type WatcherSource = Ref | (() => T); +export type WatchCallback = ( + value: V, + oldValue: OV, + onInvalidate: InvalidateCbRegistrator +) => any; type MapSources = { - [K in keyof T]: T[K] extends WatcherSource ? V : never; + [K in keyof T]: T[K] extends WatchSource ? V : never; +}; + +type MapOldSources = { + [K in keyof T]: T[K] extends WatchSource + ? Immediate extends true + ? (V | undefined) + : V + : never; }; -type FlushMode = 'pre' | 'post' | 'sync'; +export interface WatchOptionsBase { + flush?: FlushMode; + // onTrack?: ReactiveEffectOptions['onTrack']; + // onTrigger?: ReactiveEffectOptions['onTrigger']; +} + +type InvalidateCbRegistrator = (cb: () => void) => void; + +export type FlushMode = 'pre' | 'post' | 'sync'; -interface WatcherOption { - lazy: boolean; // whether or not to delay callcack invoking - deep: boolean; - flush: FlushMode; +export interface WatchOptions extends WatchOptionsBase { + immediate?: Immediate; + deep?: boolean; } export interface VueWatcher { @@ -33,6 +49,8 @@ export interface VueWatcher { teardown(): void; } +export type WatchStopHandle = () => void; + let fallbackVM: ComponentInstance; function flushPreQueue(this: any) { @@ -54,10 +72,21 @@ function installWatchEnv(vm: any) { vm.$on('hook:updated', flushPostQueue); } -function getWatcherOption(options?: Partial): WatcherOption { +function getWatcherOption(options?: Partial): WatchOptions { + return { + ...{ + immediate: false, + deep: false, + flush: 'post', + }, + ...options, + }; +} + +function getWatchEffectOption(options?: Partial): WatchOptions { return { ...{ - lazy: false, + immediate: true, deep: false, flush: 'post', }, @@ -151,14 +180,14 @@ function patchWatcherTeardown(watcher: VueWatcher, runCleanup: () => void) { function createWatcher( vm: ComponentInstance, - source: WatcherSource | WatcherSource[] | SimpleEffect, - cb: WatcherCallBack | null, - options: WatcherOption + source: WatchSource | WatchSource[] | WatchEffect, + cb: WatchCallback | null, + options: WatchOptions ): () => void { const flushMode = options.flush; const isSync = flushMode === 'sync'; let cleanup: (() => void) | null; - const registerCleanup: CleanupRegistrator = (fn: () => void) => { + const registerCleanup: InvalidateCbRegistrator = (fn: () => void) => { cleanup = () => { try { fn(); @@ -190,10 +219,9 @@ function createWatcher( // effect watch if (cb === null) { - const getter = () => (source as SimpleEffect)(registerCleanup); + const getter = () => (source as WatchEffect)(registerCleanup); const watcher = createVueWatcher(vm, getter, noopFn, { - noRun: true, // take control the initial gettet invoking - deep: options.deep, + deep: options.deep || false, sync: isSync, before: runCleanup, }); @@ -202,13 +230,9 @@ function createWatcher( // enable the watcher update watcher.lazy = false; - const originGet = watcher.get.bind(watcher); - if (isSync) { - watcher.get(); - } else { - vm.$nextTick(originGet); - } + + // always run watchEffect watcher.get = createScheduler(originGet); return () => { @@ -216,13 +240,25 @@ function createWatcher( }; } + let deep = options.deep; + let getter: () => any; if (Array.isArray(source)) { getter = () => source.map(s => (isRef(s) ? s.value : s())); } else if (isRef(source)) { getter = () => source.value; - } else { + } else if (isReactive(source)) { + getter = () => source; + deep = true; + } else if (isFunction(source)) { getter = source as () => any; + } else { + getter = noopFn; + warn( + `Invalid watch source: ${JSON.stringify(source)}. + A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.`, + vm + ); } const applyCb = (n: any, o: any) => { @@ -231,7 +267,7 @@ function createWatcher( cb(n, o, registerCleanup); }; let callback = createScheduler(applyCb); - if (!options.lazy) { + if (options.immediate) { const originalCallbck = callback; // `shiftCallback` is used to handle the first sync effect run. // The subsequent callbacks will redirect to `callback`. @@ -246,8 +282,8 @@ function createWatcher( // @ts-ignore: use undocumented option "sync" const stop = vm.$watch(getter, callback, { - immediate: !options.lazy, - deep: options.deep, + immediate: options.immediate, + deep: deep, sync: isSync, }); @@ -260,48 +296,59 @@ function createWatcher( }; } -export function watchEffect( - effect: SimpleEffect, - options?: Omit, 'lazy'> -): StopHandle { - const opts = getWatcherOption(options); +export function watchEffect(effect: WatchEffect, options?: WatchOptionsBase): WatchStopHandle { + const opts = getWatchEffectOption(options); const vm = getWatcherVM(); return createWatcher(vm, effect, null, opts); } -export function watch( - source: SimpleEffect, - options?: Omit, 'lazy'> -): StopHandle; -export function watch( - source: WatcherSource, - cb: WatcherCallBack, - options?: Partial -): StopHandle; -export function watch[]>( +// overload #1: array of multiple sources + cb +// Readonly constraint helps the callback to correctly infer value types based +// on position in the source array. Otherwise the values will get a union type +// of all possible value types. +export function watch< + T extends Readonly[]>, + Immediate extends Readonly = false +>( sources: T, - cb: (newValues: MapSources, oldValues: MapSources, onCleanup: CleanupRegistrator) => any, - options?: Partial -): StopHandle; -export function watch( - source: WatcherSource | WatcherSource[] | SimpleEffect, - cb?: Partial | WatcherCallBack, - options?: Partial -): StopHandle { - let callback: WatcherCallBack | null = null; + cb: WatchCallback, MapOldSources>, + options?: WatchOptions +): WatchStopHandle; + +// overload #2: single source + cb +export function watch = false>( + source: WatchSource, + cb: WatchCallback, + options?: WatchOptions +): WatchStopHandle; + +// overload #3: watching reactive object w/ cb +export function watch = false>( + source: T, + cb: WatchCallback, + options?: WatchOptions +): WatchStopHandle; + +// implementation +export function watch( + source: WatchSource | WatchSource[], + cb: WatchCallback, + options?: WatchOptions +): WatchStopHandle { + let callback: WatchCallback | null = null; if (typeof cb === 'function') { // source watch - callback = cb as WatcherCallBack; + callback = cb as WatchCallback; } else { // effect watch - if (process.env.NODE_ENV !== 'production') { + if (__DEV__) { warn( `\`watch(fn, options?)\` signature has been moved to a separate API. ` + `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` + `supports \`watch(source, cb, options?) signature.` ); } - options = cb as Partial; + options = cb as Partial; callback = null; } diff --git a/src/component/component.ts b/src/component/component.ts index b32f5c26..a60121fe 100644 --- a/src/component/component.ts +++ b/src/component/component.ts @@ -116,7 +116,7 @@ export function createComponent< ): VueProxy; // implementation, deferring to defineComponent, but logging a warning in dev mode export function createComponent(options: any) { - if (process.env.NODE_ENV !== 'production') { + if (__DEV__) { Vue.util.warn('`createComponent` has been renamed to `defineComponent`.'); } return defineComponent(options); diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..85b4eae4 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,2 @@ +// Global compile-time constants +declare var __DEV__: boolean; diff --git a/src/helper.ts b/src/helper.ts index 14b0aad7..ee33f2d4 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,14 +1,18 @@ import Vue, { VNode, ComponentOptions, VueConstructor } from 'vue'; import { ComponentInstance } from './component'; import { currentVue, getCurrentVM } from './runtimeContext'; -import { assert, warn } from './utils'; +import { warn } from './utils'; -export function ensureCurrentVMInFn(hook: string): ComponentInstance { +export function currentVMInFn(hook: string): ComponentInstance | null { const vm = getCurrentVM(); - if (process.env.NODE_ENV !== 'production') { - assert(vm, `"${hook}" get called outside of "setup()"`); + if (__DEV__ && !vm) { + warn( + `${hook} is called when there is no active component instance to be ` + + `associated with. ` + + `Lifecycle injection APIs can only be used during execution of setup().` + ); } - return vm!; + return vm; } export function defineComponentInstance( diff --git a/src/install.ts b/src/install.ts index bf0c7479..252a16f8 100644 --- a/src/install.ts +++ b/src/install.ts @@ -36,7 +36,7 @@ function mergeData(from: AnyObject, to: AnyObject): Object { export function install(Vue: VueConstructor, _install: (Vue: VueConstructor) => void) { if (currentVue && currentVue === Vue) { - if (process.env.NODE_ENV !== 'production') { + if (__DEV__) { assert(false, 'already installed. Vue.use(plugin) should be called only once'); } return; diff --git a/src/reactivity/index.ts b/src/reactivity/index.ts index d44c0943..514ca889 100644 --- a/src/reactivity/index.ts +++ b/src/reactivity/index.ts @@ -1,3 +1,14 @@ -export { reactive, isReactive, nonReactive } from './reactive'; -export { ref, isRef, Ref, createRef, UnwrapRef, toRefs } from './ref'; +export { reactive, isReactive, markRaw, shallowReactive, toRaw, isRaw } from './reactive'; +export { + ref, + isRef, + Ref, + createRef, + UnwrapRef, + toRefs, + toRef, + unref, + shallowRef, + triggerRef, +} from './ref'; export { set } from './set'; diff --git a/src/reactivity/reactive.ts b/src/reactivity/reactive.ts index 2962c245..610027f4 100644 --- a/src/reactivity/reactive.ts +++ b/src/reactivity/reactive.ts @@ -5,23 +5,25 @@ import { isComponentInstance, defineComponentInstance } from '../helper'; import { AccessControlIdentifierKey, ReactiveIdentifierKey, - NonReactiveIdentifierKey, + RawIdentifierKey, RefKey, } from '../symbols'; import { isRef, UnwrapRef } from './ref'; const AccessControlIdentifier = {}; const ReactiveIdentifier = {}; -const NonReactiveIdentifier = {}; +const RawIdentifier = {}; -function isNonReactive(obj: any): boolean { - return ( - hasOwn(obj, NonReactiveIdentifierKey) && obj[NonReactiveIdentifierKey] === NonReactiveIdentifier - ); +export function isRaw(obj: any): boolean { + return hasOwn(obj, RawIdentifierKey) && obj[RawIdentifierKey] === RawIdentifier; } export function isReactive(obj: any): boolean { - return hasOwn(obj, ReactiveIdentifierKey) && obj[ReactiveIdentifierKey] === ReactiveIdentifier; + return ( + Object.isExtensible(obj) && + hasOwn(obj, ReactiveIdentifierKey) && + obj[ReactiveIdentifierKey] === ReactiveIdentifier + ); } /** @@ -31,7 +33,7 @@ export function isReactive(obj: any): boolean { function setupAccessControl(target: AnyObject): void { if ( !isPlainObject(target) || - isNonReactive(target) || + isRaw(target) || Array.isArray(target) || isRef(target) || isComponentInstance(target) @@ -123,22 +125,111 @@ function observe(obj: T): T { return observed; } + +export function shallowReactive(obj: T): T { + if (__DEV__ && !obj) { + warn('"shallowReactive()" is called without provide an "object".'); + // @ts-ignore + return; + } + + if (!isPlainObject(obj) || isReactive(obj) || isRaw(obj) || !Object.isExtensible(obj)) { + return obj as any; + } + + const observed = observe({}); + markReactive(observed, true); + setupAccessControl(observed); + + const ob = (observed as any).__ob__; + + for (const key of Object.keys(obj)) { + let val = obj[key]; + let getter: (() => any) | undefined; + let setter: ((x: any) => void) | undefined; + const property = Object.getOwnPropertyDescriptor(obj, key); + if (property) { + if (property.configurable === false) { + continue; + } + getter = property.get; + setter = property.set; + if ((!getter || setter) /* not only have getter */ && arguments.length === 2) { + val = obj[key]; + } + } + + // setupAccessControl(val); + Object.defineProperty(observed, key, { + enumerable: true, + configurable: true, + get: function getterHandler() { + const value = getter ? getter.call(obj) : val; + ob.dep.depend(); + return value; + }, + set: function setterHandler(newVal) { + if (getter && !setter) return; + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + ob.dep.notify(); + }, + }); + } + return (observed as unknown) as T; +} + +export function markReactive(target: any, shallow = false) { + if ( + !isPlainObject(target) || + isRaw(target) || + Array.isArray(target) || + isRef(target) || + isComponentInstance(target) + ) { + return; + } + + if ( + hasOwn(target, ReactiveIdentifierKey) && + target[ReactiveIdentifierKey] === ReactiveIdentifier + ) { + return; + } + + if (Object.isExtensible(target)) { + def(target, ReactiveIdentifierKey, ReactiveIdentifier); + } + + if (shallow) { + return; + } + const keys = Object.keys(target); + for (let i = 0; i < keys.length; i++) { + markReactive(target[keys[i]]); + } +} + /** * Make obj reactivity */ -export function reactive(obj: T): UnwrapRef { - if (process.env.NODE_ENV !== 'production' && !obj) { +export function reactive(obj: T): UnwrapRef { + if (__DEV__ && !obj) { warn('"reactive()" is called without provide an "object".'); // @ts-ignore return; } - if (!isPlainObject(obj) || isReactive(obj) || isNonReactive(obj) || !Object.isExtensible(obj)) { + if (!isPlainObject(obj) || isReactive(obj) || isRaw(obj) || !Object.isExtensible(obj)) { return obj as any; } const observed = observe(obj); - def(observed, ReactiveIdentifierKey, ReactiveIdentifier); + // def(obj, ReactiveIdentifierKey, ReactiveIdentifier); + markReactive(obj); setupAccessControl(observed); return observed as UnwrapRef; } @@ -146,15 +237,23 @@ export function reactive(obj: T): UnwrapRef { /** * Make sure obj can't be a reactive */ -export function nonReactive(obj: T): T { +export function markRaw(obj: T): T { if (!isPlainObject(obj)) { return obj; } // set the vue observable flag at obj def(obj, '__ob__', (observe({}) as any).__ob__); - // mark as nonReactive - def(obj, NonReactiveIdentifierKey, NonReactiveIdentifier); + // mark as Raw + def(obj, RawIdentifierKey, RawIdentifier); return obj; } + +export function toRaw(observed: T): T { + if (isRaw(observe) || !Object.isExtensible(observed)) { + return observed; + } + + return (observed as any).__ob__.value || observed; +} diff --git a/src/reactivity/ref.ts b/src/reactivity/ref.ts index 49763bef..3a8818d6 100644 --- a/src/reactivity/ref.ts +++ b/src/reactivity/ref.ts @@ -1,92 +1,61 @@ import { Data } from '../component'; import { RefKey } from '../symbols'; -import { proxy, isPlainObject } from '../utils'; +import { proxy, isPlainObject, warn } from '../utils'; import { HasDefined } from '../types/basic'; -import { reactive } from './reactive'; - -type BailTypes = Function | Map | Set | WeakMap | WeakSet | Element; - -// corner case when use narrows type -// Ex. type RelativePath = string & { __brand: unknown } -// RelativePath extends object -> true -type BaseTypes = string | number | boolean; +import { reactive, isReactive, shallowReactive } from './reactive'; +import { ComputedRef } from '../apis/computed'; declare const _refBrand: unique symbol; -export interface Ref { +export interface Ref { readonly [_refBrand]: true; value: T; } -// prettier-ignore -// Recursively unwraps nested value bindings. -// Unfortunately TS cannot do recursive types, but this should be enough for -// practical use cases... -export type UnwrapRef = T extends Ref - ? UnwrapRef2 - : T extends BailTypes | BaseTypes - ? T // bail out on types that shouldn't be unwrapped - : T extends object ? { [K in keyof T]: UnwrapRef2 } : T - -// prettier-ignore -type UnwrapRef2 = T extends Ref - ? UnwrapRef3 - : T extends BailTypes | BaseTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapRef3 } : T - -// prettier-ignore -type UnwrapRef3 = T extends Ref - ? UnwrapRef4 - : T extends BailTypes | BaseTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapRef4 } : T - -// prettier-ignore -type UnwrapRef4 = T extends Ref - ? UnwrapRef5 - : T extends BailTypes | BaseTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapRef5 } : T - -// prettier-ignore -type UnwrapRef5 = T extends Ref - ? UnwrapRef6 - : T extends BailTypes | BaseTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapRef6 } : T - -// prettier-ignore -type UnwrapRef6 = T extends Ref - ? UnwrapRef7 - : T extends BailTypes | BaseTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapRef7 } : T - -// prettier-ignore -type UnwrapRef7 = T extends Ref - ? UnwrapRef8 - : T extends BailTypes | BaseTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapRef8 } : T - -// prettier-ignore -type UnwrapRef8 = T extends Ref - ? UnwrapRef9 - : T extends BailTypes | BaseTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapRef9 } : T - -// prettier-ignore -type UnwrapRef9 = T extends Ref - ? UnwrapRef10 - : T extends BailTypes | BaseTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapRef10 } : T - -// prettier-ignore -type UnwrapRef10 = T extends Ref - ? V // stop recursion - : T +export type ToRefs = { [K in keyof T]: Ref }; + +export type CollectionTypes = IterableCollections | WeakCollections; + +type IterableCollections = Map | Set; +type WeakCollections = WeakMap | WeakSet; + +// corner case when use narrows type +// Ex. type RelativePath = string & { __brand: unknown } +// RelativePath extends object -> true +type BaseTypes = string | number | boolean | Node | Window; + +export type UnwrapRef = T extends ComputedRef + ? UnwrapRefSimple + : T extends Ref + ? UnwrapRefSimple + : UnwrapRefSimple; + +type UnwrapRefSimple = T extends Function | CollectionTypes | BaseTypes | Ref + ? T + : T extends Array + ? T + : T extends object + ? UnwrappedObject + : T; + +// Extract all known symbols from an object +// when unwrapping Object the symbols are not `in keyof`, this should cover all the +// known symbols +type SymbolExtract = (T extends { [Symbol.asyncIterator]: infer V } + ? { [Symbol.asyncIterator]: V } + : {}) & + (T extends { [Symbol.hasInstance]: infer V } ? { [Symbol.hasInstance]: V } : {}) & + (T extends { [Symbol.isConcatSpreadable]: infer V } ? { [Symbol.isConcatSpreadable]: V } : {}) & + (T extends { [Symbol.iterator]: infer V } ? { [Symbol.iterator]: V } : {}) & + (T extends { [Symbol.match]: infer V } ? { [Symbol.match]: V } : {}) & + (T extends { [Symbol.replace]: infer V } ? { [Symbol.replace]: V } : {}) & + (T extends { [Symbol.search]: infer V } ? { [Symbol.search]: V } : {}) & + (T extends { [Symbol.species]: infer V } ? { [Symbol.species]: V } : {}) & + (T extends { [Symbol.split]: infer V } ? { [Symbol.split]: V } : {}) & + (T extends { [Symbol.toPrimitive]: infer V } ? { [Symbol.toPrimitive]: V } : {}) & + (T extends { [Symbol.toStringTag]: infer V } ? { [Symbol.toStringTag]: V } : {}) & + (T extends { [Symbol.unscopables]: infer V } ? { [Symbol.unscopables]: V } : {}); + +type UnwrappedObject = { [P in keyof T]: UnwrapRef } & SymbolExtract; interface RefOption { get(): T; @@ -105,7 +74,7 @@ class RefImpl implements Ref { export function createRef(options: RefOption) { // seal the ref, this could prevent ref from being observed - // It's safe to seal the ref, since we really shoulnd't extend it. + // It's safe to seal the ref, since we really shouldn't extend it. // related issues: #79 return Object.seal(new RefImpl(options)); } @@ -114,7 +83,7 @@ type RefValue = T extends Ref ? V : UnwrapRef; // without init value, explicit typed: a = ref<{ a: number }>() // typeof a will be Ref<{ a: number } | undefined> -export function ref(): Ref; +export function ref(): Ref; // with null as init value: a = ref<{ a: number }>(null); // typeof a will be Ref<{ a: number } | null> export function ref(raw: null): Ref; @@ -128,6 +97,9 @@ export function ref(raw?: any): any { // if (isRef(raw)) { // return {} as any; // } + if (isRef(raw)) { + return raw; + } const value = reactive({ [RefKey]: raw }); return createRef({ @@ -140,29 +112,50 @@ export function isRef(value: any): value is Ref { return value instanceof RefImpl; } -// prettier-ignore -type Refs = { - [K in keyof Data]: Data[K] extends Ref - ? Ref - : Ref +export function unref(ref: T): T extends Ref ? V : T { + return isRef(ref) ? (ref.value as any) : ref; } -export function toRefs(obj: T): Refs { +export function toRefs(obj: T): ToRefs { if (!isPlainObject(obj)) return obj as any; - const res: Refs = {} as any; - Object.keys(obj).forEach(key => { - let val: any = obj[key]; - // use ref to proxy the property - if (!isRef(val)) { - val = createRef({ - get: () => obj[key], - set: v => (obj[key as keyof T] = v), - }); - } - // todo - res[key as keyof T] = val; + if (__DEV__ && !isReactive(obj)) { + warn(`toRefs() expects a reactive object but received a plain one.`); + } + + const ret: any = {}; + for (const key in obj) { + ret[key] = toRef(obj, key); + } + + return ret; +} + +export function toRef(object: T, key: K): Ref { + const v = object[key]; + if (isRef(v)) return v; + + return createRef({ + get: () => object[key], + set: v => (object[key] = v), + }); +} + +export function shallowRef(value: T): T extends Ref ? T : Ref; +export function shallowRef(): Ref; +export function shallowRef(raw?: unknown) { + if (isRef(raw)) { + return raw; + } + const value = shallowReactive({ [RefKey]: raw }); + return createRef({ + get: () => value[RefKey] as any, + set: v => ((value[RefKey] as any) = v), }); +} + +export function triggerRef(value: any) { + if (!isRef(value)) return; - return res; + value.value = value.value; } diff --git a/src/reactivity/set.ts b/src/reactivity/set.ts index bf47c8d1..3e542a72 100644 --- a/src/reactivity/set.ts +++ b/src/reactivity/set.ts @@ -1,6 +1,6 @@ import { getCurrentVue } from '../runtimeContext'; import { isArray } from '../utils'; -import { defineAccessControl } from './reactive'; +import { defineAccessControl, markReactive } from './reactive'; function isUndef(v: any): boolean { return v === undefined || v === null; @@ -29,7 +29,7 @@ function isValidArrayIndex(val: any): boolean { export function set(target: any, key: any, val: T): T { const Vue = getCurrentVue(); const { warn, defineReactive } = Vue.util; - if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target))) { + if (__DEV__ && (isUndef(target) || isPrimitive(target))) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${target}`); } if (isArray(target) && isValidArrayIndex(key)) { @@ -43,7 +43,7 @@ export function set(target: any, key: any, val: T): T { } const ob = target.__ob__; if (target._isVue || (ob && ob.vmCount)) { - process.env.NODE_ENV !== 'production' && + __DEV__ && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' @@ -57,6 +57,8 @@ export function set(target: any, key: any, val: T): T { defineReactive(ob.value, key, val); // IMPORTANT: define access control before trigger watcher defineAccessControl(target, key, val); + markReactive(ob.value[key]); + ob.dep.notify(); return val; } diff --git a/src/runtimeContext.ts b/src/runtimeContext.ts index 77f195cb..dba03b3d 100644 --- a/src/runtimeContext.ts +++ b/src/runtimeContext.ts @@ -6,7 +6,7 @@ let currentVue: VueConstructor | null = null; let currentVM: ComponentInstance | null = null; export function getCurrentVue(): VueConstructor { - if (process.env.NODE_ENV !== 'production') { + if (__DEV__) { assert(currentVue, `must call Vue.use(plugin) before using any function.`); } diff --git a/src/setup.ts b/src/setup.ts index b8b46963..09584d3d 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -1,6 +1,6 @@ import { VueConstructor } from 'vue'; import { ComponentInstance, SetupContext, SetupFunction, Data } from './component'; -import { Ref, isRef, isReactive, nonReactive } from './reactivity'; +import { Ref, isRef, isReactive, markRaw } from './reactivity'; import { getCurrentVM, setCurrentVM } from './runtimeContext'; import { resolveSlots, createSlotProxy } from './helper'; import { hasOwn, isPlainObject, assert, proxy, warn, isFunction } from './utils'; @@ -17,7 +17,7 @@ function asVmProperty(vm: ComponentInstance, propName: string, propValue: Ref { @@ -29,7 +29,7 @@ function asVmProperty(vm: ComponentInstance, propName: string, propValue: Ref Object.prototype.toString.call(x); -export function isNative (Ctor: any): boolean { - return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) +export function isNative(Ctor: any): boolean { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()); } export const hasSymbol = - typeof Symbol !== 'undefined' && isNative(Symbol) && - typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys) + typeof Symbol !== 'undefined' && + isNative(Symbol) && + typeof Reflect !== 'undefined' && + isNative(Reflect.ownKeys); export const noopFn: any = (_: any) => _; @@ -64,7 +66,7 @@ export function warn(msg: string, vm?: Vue) { } export function logError(err: Error, vm: Vue, info: string) { - if (process.env.NODE_ENV !== 'production') { + if (__DEV__) { warn(`Error in ${info}: "${err.toString()}"`, vm); } if (typeof window !== 'undefined' && typeof console !== 'undefined') { diff --git a/test/apis/lifecycle.spec.js b/test/apis/lifecycle.spec.js index af7fb246..9fa7e438 100644 --- a/test/apis/lifecycle.spec.js +++ b/test/apis/lifecycle.spec.js @@ -7,6 +7,7 @@ const { onBeforeUnmount, onUnmounted, onErrorCaptured, + getCurrentInstance, } = require('../../src'); describe('Hooks lifecycle', () => { @@ -100,6 +101,40 @@ describe('Hooks lifecycle', () => { }).$mount(); expect(calls).toEqual(['nested', 'child', 'parent']); }); + + it('getCurrentInstance should be available', () => { + const parent = new Vue(); + let instance; + new Vue({ + parent, + template: '
', + setup() { + onMounted(() => { + instance = getCurrentInstance(); + }); + }, + }).$mount(); + expect(instance).toBeDefined(); + }); + + it('getCurrentInstance should not be available on promised hook', () => { + const parent = new Vue(); + let instance; + let promisedInstance; + new Vue({ + parent, + template: '
', + setup() { + onMounted(async () => { + instance = getCurrentInstance(); + await Promise.resolve(); + promisedInstance = getCurrentInstance(); + }); + }, + }).$mount(); + expect(instance).toBeDefined(); + expect(promisedInstance).not.toBeDefined(); + }); }); describe('beforeUpdate', () => { diff --git a/test/apis/state.spec.js b/test/apis/state.spec.js index 718c935b..9e10a6bf 100644 --- a/test/apis/state.spec.js +++ b/test/apis/state.spec.js @@ -1,5 +1,5 @@ const Vue = require('vue/dist/vue.common.js'); -const { reactive, ref, watch, set, toRefs, computed } = require('../../src'); +const { reactive, ref, watch, set, toRefs, computed, unref } = require('../../src'); describe('api/ref', () => { it('should work with array', () => { @@ -24,9 +24,13 @@ describe('api/ref', () => { it('should be reactive', done => { const a = ref(1); let dummy; - watch(a, () => { - dummy = a.value; - }); + watch( + a, + () => { + dummy = a.value; + }, + { immediate: true } + ); expect(dummy).toBe(1); a.value = 2; waitForUpdate(() => { @@ -44,7 +48,7 @@ describe('api/ref', () => { () => { dummy = a.value.count; }, - { deep: true } + { deep: true, immediate: true } ); expect(dummy).toBe(1); a.value.count = 2; @@ -86,6 +90,7 @@ describe('api/reactive', () => { expect(warn.mock.calls[1][0]).toMatch( '[Vue warn]: "reactive()" is called without provide an "object".' ); + expect(warn).toBeCalledTimes(2); warn.mockRestore(); }); }); @@ -102,7 +107,8 @@ describe('api/toRefs', () => { () => state, () => { dummy = state.foo; - } + }, + { immediate: true } ); const stateAsRefs = toRefs(state); expect(dummy).toBe(1); @@ -122,6 +128,7 @@ describe('api/toRefs', () => { }); it('should proxy plain object but not make it a reactive', () => { + warn = jest.spyOn(global.console, 'error').mockImplementation(() => null); const spy = jest.fn(); const state = { foo: 1, @@ -131,6 +138,10 @@ describe('api/toRefs', () => { watch(() => state, spy, { flush: 'sync', lazy: true }); const stateAsRefs = toRefs(state); + expect(warn.mock.calls[0][0]).toMatch( + '[Vue warn]: toRefs() expects a reactive object but received a plain one.' + ); + expect(stateAsRefs.foo.value).toBe(1); expect(stateAsRefs.bar.value).toBe(2); state.foo++; @@ -140,6 +151,8 @@ describe('api/toRefs', () => { expect(state.foo).toBe(3); expect(spy).not.toHaveBeenCalled(); + expect(warn).toBeCalledTimes(1); + warn.mockRestore(); }); }); @@ -155,7 +168,7 @@ describe('unwrapping', () => { () => { dummy = obj.a; }, - { deep: true, flush: 'sync' } + { deep: true, flush: 'sync', immediate: true } ); expect(dummy).toBe(0); expect(obj.a).toBe(0); @@ -170,7 +183,7 @@ describe('unwrapping', () => { const a = ref(0); const b = ref(a); expect(a.value).toBe(0); - expect(b.value).toBe(a); + expect(b).toBe(a); }); it('should not unwrap a ref when re-assign', () => { @@ -194,7 +207,7 @@ describe('unwrapping', () => { it('should unwrap when re-assign', () => { const a = ref(); const b = ref(a); - expect(b.value).toBe(a); + expect(b.value).toBe(a.value); const c = ref(0); b.value = { count: c, @@ -220,7 +233,7 @@ describe('unwrapping', () => { dummy1 = obj.a; dummy2 = obj.b.c; }, - { deep: true, flush: 'sync' } + { deep: true, flush: 'sync', immediate: true } ); expect(dummy1).toBe(1); expect(dummy2).toBe(1); @@ -250,7 +263,7 @@ describe('unwrapping', () => { dummy1 = obj.a; dummy2 = obj.b.c; }, - { deep: true, flush: 'sync' } + { deep: true, flush: 'sync', immediate: true } ); expect(dummy1).toBe(1); expect(dummy2).toBe(1); @@ -330,6 +343,12 @@ describe('unwrapping', () => { expect(state.list[0].value).toBe(0); }); + it('should unrwap ref', () => { + expect(unref(0)).toBe(0); + expect(unref(ref(0))).toBe(0); + expect(unref({ value: 1 })).toStrictEqual({ value: 1 }); + }); + it('should now unwrap plain object when using set at Array', () => { const state = reactive({ list: [], diff --git a/test/apis/watch.spec.js b/test/apis/watch.spec.js index 9f74cc5d..6d2bb1ec 100644 --- a/test/apis/watch.spec.js +++ b/test/apis/watch.spec.js @@ -17,10 +17,14 @@ describe('api/watch', () => { const vm = new Vue({ setup() { const a = ref(1); - watch(a, (n, o, _onCleanup) => { - spy(n, o, _onCleanup); - _onCleanup(onCleanupSpy); - }); + watch( + a, + (n, o, _onCleanup) => { + spy(n, o, _onCleanup); + _onCleanup(onCleanupSpy); + }, + { immediate: true } + ); return { a, }; @@ -50,7 +54,7 @@ describe('api/watch', () => { const vm = new Vue({ setup() { const a = ref(1); - watch(a, (n, o) => spy(n, o), { flush: 'pre' }); + watch(a, (n, o) => spy(n, o), { flush: 'pre', immediate: true }); return { a, @@ -72,7 +76,7 @@ describe('api/watch', () => { const vm = new Vue({ setup() { const a = ref(1); - watch(() => a.value, (n, o) => spy(n, o)); + watch(() => a.value, (n, o) => spy(n, o), { immediate: true }); return { a, @@ -159,7 +163,7 @@ describe('api/watch', () => { .then(done); }); - it('should flush after render (lazy=true)', done => { + it('should flush after render (immediate=false)', done => { let rerenderedText; const vm = new Vue({ setup() { @@ -189,17 +193,21 @@ describe('api/watch', () => { }).then(done); }); - it('should flush after render (lazy=false)', done => { + it('should flush after render (immediate=true)', done => { let rerenderedText; var vm = new Vue({ setup() { const a = ref(1); - watch(a, (newVal, oldVal) => { - spy(newVal, oldVal); - if (vm) { - rerenderedText = vm.$el.textContent; - } - }); + watch( + a, + (newVal, oldVal) => { + spy(newVal, oldVal); + if (vm) { + rerenderedText = vm.$el.textContent; + } + }, + { immediate: true } + ); return { a, }; @@ -294,7 +302,7 @@ describe('api/watch', () => { new Vue({ setup() { const count = ref(0); - watch(count, (n, o) => spy(n, o), { flush: 'sync' }); + watch(count, (n, o) => spy(n, o), { flush: 'sync', immediate: true }); count.value++; }, }); @@ -317,11 +325,11 @@ describe('api/watch', () => { watchEffect(() => { void x.value; result.push('post effect'); }, { flush: 'post' }); // prettier-ignore - watch(x, () => { result.push('sync callback') }, { flush: 'sync' }) + watch(x, () => { result.push('sync callback') }, { flush: 'sync', immediate: true }) // prettier-ignore - watch(x, () => { result.push('pre callback') }, { flush: 'pre' }) + watch(x, () => { result.push('pre callback') }, { flush: 'pre', immediate: true }) // prettier-ignore - watch(x, () => { result.push('post callback') }, { flush: 'post' }) + watch(x, () => { result.push('post callback') }, { flush: 'post', immediate: true }) const inc = () => { result.push('before inc'); @@ -333,11 +341,18 @@ describe('api/watch', () => { }, template: `
{{x}}
`, }).$mount(); - expect(result).toEqual(['sync effect', 'sync callback', 'pre callback', 'post callback']); + expect(result).toEqual([ + 'sync effect', + 'pre effect', + 'post effect', + 'sync callback', + 'pre callback', + 'post callback', + ]); result.length = 0; waitForUpdate(() => { - expect(result).toEqual(['pre effect', 'post effect']); + expect(result).toEqual([]); result.length = 0; vm.inc(); @@ -358,7 +373,6 @@ describe('api/watch', () => { }); describe('simple effect', () => { - let renderedText; it('should work', done => { let onCleanup; const onCleanupSpy = jest.fn(); @@ -369,7 +383,6 @@ describe('api/watch', () => { onCleanup = _onCleanup; _onCleanup(onCleanupSpy); spy(count.value); - renderedText = vm.$el.textContent; }); return { @@ -380,16 +393,14 @@ describe('api/watch', () => { return h('div', this.count); }, }).$mount(); - expect(spy).not.toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); waitForUpdate(() => { expect(onCleanup).toEqual(anyFn); expect(onCleanupSpy).toHaveBeenCalledTimes(0); - expect(renderedText).toBe('0'); expect(spy).toHaveBeenLastCalledWith(0); vm.count++; }) .then(() => { - expect(renderedText).toBe('1'); expect(spy).toHaveBeenLastCalledWith(1); expect(onCleanupSpy).toHaveBeenCalledTimes(1); vm.$destroy(); @@ -431,7 +442,7 @@ describe('api/watch', () => { setup() { obj1 = reactive({ a: 1 }); obj2 = reactive({ a: 2 }); - watch([() => obj1.a, () => obj2.a], (n, o) => spy(n, o)); + watch([() => obj1.a, () => obj2.a], (n, o) => spy(n, o), { immediate: true }); return { obj1, obj2, @@ -459,12 +470,12 @@ describe('api/watch', () => { .then(done); }); - it('basic usage(lazy=false, flush=none-sync)', done => { + it('basic usage(immediate=true, flush=none-sync)', done => { const vm = new Vue({ setup() { const a = ref(1); const b = ref(1); - watch([a, b], (n, o) => spy(n, o), { lazy: false, flush: 'post' }); + watch([a, b], (n, o) => spy(n, o), { flush: 'post', immediate: true }); return { a, @@ -490,12 +501,12 @@ describe('api/watch', () => { .then(done); }); - it('basic usage(lazy=true, flush=none-sync)', done => { + it('basic usage(immediate=false, flush=none-sync)', done => { const vm = new Vue({ setup() { const a = ref(1); const b = ref(1); - watch([a, b], (n, o) => spy(n, o), { lazy: true, flush: 'post' }); + watch([a, b], (n, o) => spy(n, o), { immediate: false, flush: 'post' }); return { a, @@ -519,12 +530,12 @@ describe('api/watch', () => { .then(done); }); - it('basic usage(lazy=false, flush=sync)', () => { + it('basic usage(immediate=true, flush=sync)', () => { const vm = new Vue({ setup() { const a = ref(1); const b = ref(1); - watch([a, b], (n, o) => spy(n, o), { lazy: false, flush: 'sync' }); + watch([a, b], (n, o) => spy(n, o), { immediate: true, flush: 'sync' }); return { a, @@ -544,7 +555,7 @@ describe('api/watch', () => { expect(spy).toHaveBeenNthCalledWith(4, [3, 3], [3, 1]); }); - it('basic usage(lazy=true, flush=sync)', () => { + it('basic usage(immediate=false, flush=sync)', () => { const vm = new Vue({ setup() { const a = ref(1); @@ -572,7 +583,7 @@ describe('api/watch', () => { describe('Out of setup', () => { it('should work', done => { const obj = reactive({ a: 1 }); - watch(() => obj.a, (n, o) => spy(n, o)); + watch(() => obj.a, (n, o) => spy(n, o), { immediate: true }); expect(spy).toHaveBeenLastCalledWith(1, undefined); obj.a = 2; waitForUpdate(() => { @@ -584,7 +595,7 @@ describe('api/watch', () => { it('simple effect', done => { const obj = reactive({ a: 1 }); watchEffect(() => spy(obj.a)); - expect(spy).not.toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); waitForUpdate(() => { expect(spy).toBeCalledTimes(1); expect(spy).toHaveBeenLastCalledWith(1); @@ -658,10 +669,14 @@ describe('api/watch', () => { const id = ref(1); const spy = jest.fn(); const cleanup = jest.fn(); - const stop = watch(id, (value, oldValue, onCleanup) => { - spy(value); - onCleanup(cleanup); - }); + const stop = watch( + id, + (value, oldValue, onCleanup) => { + spy(value); + onCleanup(cleanup); + }, + { immediate: true } + ); expect(spy).toHaveBeenCalledWith(1); stop(); @@ -696,13 +711,17 @@ describe('api/watch', () => { it('work with callback ', done => { const id = ref(1); const promises = []; - watch(id, (newVal, oldVal, onCleanup) => { - const val = getAsyncValue(newVal); - promises.push(val); - onCleanup(() => { - val.cancel(); - }); - }); + watch( + id, + (newVal, oldVal, onCleanup) => { + const val = getAsyncValue(newVal); + promises.push(val); + onCleanup(() => { + val.cancel(); + }); + }, + { immediate: true } + ); id.value = 2; waitForUpdate() .thenWaitFor(async next => { diff --git a/test/globals.d.ts b/test/globals.d.ts new file mode 100644 index 00000000..e1df07af --- /dev/null +++ b/test/globals.d.ts @@ -0,0 +1,5 @@ +declare function waitForUpdate(cb: Function): Promise; + +declare interface Window { + waitForUpdate(cb: Function): Promise; +} diff --git a/test/helpers/mockWarn.ts b/test/helpers/mockWarn.ts new file mode 100644 index 00000000..17bacc32 --- /dev/null +++ b/test/helpers/mockWarn.ts @@ -0,0 +1,100 @@ +declare global { + namespace jest { + interface Matchers { + toHaveBeenWarned(): R; + toHaveBeenWarnedLast(): R; + toHaveBeenWarnedTimes(n: number): R; + } + } +} + +export const mockError = () => mockWarn(true); + +export function mockWarn(asError = false) { + expect.extend({ + toHaveBeenWarned(received: string) { + asserted.add(received); + const passed = warn.mock.calls.some(args => args[0].indexOf(received) > -1); + if (passed) { + return { + pass: true, + message: () => `expected "${received}" not to have been warned.`, + }; + } else { + const msgs = warn.mock.calls.map(args => args[0]).join('\n - '); + return { + pass: false, + message: () => + `expected "${received}" to have been warned.\n\nActual messages:\n\n - ${msgs}`, + }; + } + }, + + toHaveBeenWarnedLast(received: string) { + asserted.add(received); + const passed = warn.mock.calls[warn.mock.calls.length - 1][0].indexOf(received) > -1; + if (passed) { + return { + pass: true, + message: () => `expected "${received}" not to have been warned last.`, + }; + } else { + const msgs = warn.mock.calls.map(args => args[0]).join('\n - '); + return { + pass: false, + message: () => + `expected "${received}" to have been warned last.\n\nActual messages:\n\n - ${msgs}`, + }; + } + }, + + toHaveBeenWarnedTimes(received: string, n: number) { + asserted.add(received); + let found = 0; + warn.mock.calls.forEach(args => { + if (args[0].indexOf(received) > -1) { + found++; + } + }); + + if (found === n) { + return { + pass: true, + message: () => `expected "${received}" to have been warned ${n} times.`, + }; + } else { + return { + pass: false, + message: () => `expected "${received}" to have been warned ${n} times but got ${found}.`, + }; + } + }, + }); + + let warn: jest.SpyInstance; + const asserted: Set = new Set(); + + beforeEach(() => { + asserted.clear(); + warn = jest.spyOn(console, asError ? 'error' : 'warn'); + warn.mockImplementation(() => {}); + }); + + afterEach(() => { + const assertedArray = Array.from(asserted); + const nonAssertedWarnings = warn.mock.calls + .map(args => args[0]) + .filter(received => { + return !assertedArray.some(assertedMsg => { + return received.indexOf(assertedMsg) > -1; + }); + }); + warn.mockRestore(); + if (nonAssertedWarnings.length) { + nonAssertedWarnings.forEach(warning => { + console.warn(warning); + }); + throw new Error(`test case threw unexpected warnings.`); + } + }); +} diff --git a/test/helpers/utils.ts b/test/helpers/utils.ts new file mode 100644 index 00000000..e1941e20 --- /dev/null +++ b/test/helpers/utils.ts @@ -0,0 +1,5 @@ +const Vue = require('vue/dist/vue.common.js'); + +export function nextTick(): Promise { + return Vue.nextTick(); +} diff --git a/test/helpers/wait-for-update.js b/test/helpers/wait-for-update.js index cecdca63..a0f84544 100644 --- a/test/helpers/wait-for-update.js +++ b/test/helpers/wait-for-update.js @@ -12,6 +12,7 @@ const Vue = require('vue'); // // more assertions... // }) // .then(done) + window.waitForUpdate = initialCb => { let end; const queue = initialCb ? [initialCb] : []; @@ -68,6 +69,8 @@ window.waitForUpdate = initialCb => { return chainer; }; +exports.waitForUpdate = window.waitForUpdate; + function timeout(n) { return next => setTimeout(next, n); } diff --git a/test/templateRefs.spec.js b/test/templateRefs.spec.js index 260e9d4d..262633ed 100644 --- a/test/templateRefs.spec.js +++ b/test/templateRefs.spec.js @@ -1,5 +1,5 @@ const Vue = require('vue/dist/vue.common.js'); -const { ref, watchEffect, createElement: h } = require('../src'); +const { ref, watchEffect, watch, createElement: h } = require('../src'); describe('ref', () => { it('should work', done => { @@ -25,9 +25,11 @@ describe('ref', () => { }, }, }).$mount(); - waitForUpdate(() => { - expect(dummy).toBe(vm.$refs.bar); - }).then(done); + vm.$nextTick() + .then(() => { + expect(dummy).toBe(vm.$refs.bar); + }) + .then(done); }); it('should dynamically update refs', done => { @@ -48,11 +50,12 @@ describe('ref', () => { }, template: '
', }).$mount(); - waitForUpdate(() => { - expect(dummy1).toBe(vm.$refs.bar); - expect(dummy2).toBe(null); - vm.value = 'foo'; - }) + waitForUpdate(() => {}) + .then(() => { + expect(dummy1).toBe(vm.$refs.bar); + expect(dummy2).toBe(null); + vm.value = 'foo'; + }) .then(() => { // vm updated. ref update occures after updated; }) diff --git a/test/v3/reactivity/computed.spec.ts b/test/v3/reactivity/computed.spec.ts new file mode 100644 index 00000000..9287de33 --- /dev/null +++ b/test/v3/reactivity/computed.spec.ts @@ -0,0 +1,186 @@ +import { computed, reactive, ref, watchEffect } from '../../../src'; +import { mockWarn } from '../../helpers/mockWarn'; +import { nextTick } from '../../helpers/utils'; + +describe('reactivity/computed', () => { + mockWarn(); + + it('should return updated value', async () => { + const value = reactive<{ foo?: number }>({ foo: undefined }); + const cValue = computed(() => value.foo); + expect(cValue.value).toBe(undefined); + value.foo = 1; + await nextTick(); + + expect(cValue.value).toBe(1); + }); + + it('should compute lazily', () => { + const value = reactive<{ foo?: number }>({ foo: undefined }); + const getter = jest.fn(() => value.foo); + const cValue = computed(getter); + + // lazy + expect(getter).not.toHaveBeenCalled(); + + expect(cValue.value).toBe(undefined); + expect(getter).toHaveBeenCalledTimes(1); + + // should not compute again + cValue.value; + expect(getter).toHaveBeenCalledTimes(1); + + // should not compute until needed + value.foo = 1; + expect(getter).toHaveBeenCalledTimes(1); + + // now it should compute + expect(cValue.value).toBe(1); + expect(getter).toHaveBeenCalledTimes(2); + + // should not compute again + cValue.value; + expect(getter).toHaveBeenCalledTimes(2); + }); + + it('should trigger effect', () => { + const value = reactive<{ foo?: number }>({ foo: undefined }); + const cValue = computed(() => value.foo); + let dummy; + watchEffect( + () => { + dummy = cValue.value; + }, + { flush: 'sync' } + ); + expect(dummy).toBe(undefined); + value.foo = 1; + expect(dummy).toBe(1); + }); + + it('should work when chained', () => { + const value = reactive({ foo: 0 }); + const c1 = computed(() => value.foo); + const c2 = computed(() => c1.value + 1); + expect(c2.value).toBe(1); + expect(c1.value).toBe(0); + value.foo++; + expect(c2.value).toBe(2); + expect(c1.value).toBe(1); + }); + + it('should trigger effect when chained', () => { + const value = reactive({ foo: 0 }); + const getter1 = jest.fn(() => value.foo); + const getter2 = jest.fn(() => { + return c1.value + 1; + }); + const c1 = computed(getter1); + const c2 = computed(getter2); + + let dummy; + watchEffect( + () => { + dummy = c2.value; + }, + { flush: 'sync' } + ); + expect(dummy).toBe(1); + expect(getter1).toHaveBeenCalledTimes(1); + expect(getter2).toHaveBeenCalledTimes(1); + value.foo++; + expect(dummy).toBe(2); + // should not result in duplicate calls + expect(getter1).toHaveBeenCalledTimes(2); + expect(getter2).toHaveBeenCalledTimes(2); + }); + + it('should trigger effect when chained (mixed invocations)', async () => { + const value = reactive({ foo: 0 }); + const getter1 = jest.fn(() => value.foo); + const getter2 = jest.fn(() => { + return c1.value + 1; + }); + const c1 = computed(getter1); + const c2 = computed(getter2); + + let dummy; + watchEffect(() => { + dummy = c1.value + c2.value; + }); + await nextTick(); + expect(dummy).toBe(1); + + expect(getter1).toHaveBeenCalledTimes(1); + expect(getter2).toHaveBeenCalledTimes(1); + value.foo++; + + await nextTick(); + + expect(dummy).toBe(3); + // should not result in duplicate calls + expect(getter1).toHaveBeenCalledTimes(2); + expect(getter2).toHaveBeenCalledTimes(2); + }); + + // it('should no longer update when stopped', () => { + // const value = reactive<{ foo?: number }>({}); + // const cValue = computed(() => value.foo); + // let dummy; + // effect(() => { + // dummy = cValue.value; + // }); + // expect(dummy).toBe(undefined); + // value.foo = 1; + // expect(dummy).toBe(1); + // stop(cValue.effect); + // value.foo = 2; + // expect(dummy).toBe(1); + // }); + + it('should support setter', () => { + const n = ref(1); + const plusOne = computed({ + get: () => n.value + 1, + set: val => { + n.value = val - 1; + }, + }); + + expect(plusOne.value).toBe(2); + n.value++; + expect(plusOne.value).toBe(3); + + plusOne.value = 0; + expect(n.value).toBe(-1); + }); + + it('should trigger effect w/ setter', async () => { + const n = ref(1); + const plusOne = computed({ + get: () => n.value + 1, + set: val => { + n.value = val - 1; + }, + }); + + let dummy; + watchEffect(() => { + dummy = n.value; + }); + expect(dummy).toBe(1); + + plusOne.value = 0; + await nextTick(); + expect(dummy).toBe(-1); + }); + + // it('should warn if trying to set a readonly computed', async () => { + // const n = ref(1); + // const plusOne = computed(() => n.value + 1); + // (plusOne as WritableComputedRef).value++; // Type cast to prevent TS from preventing the error + // await nextTick(); + + // expect('Write operation failed: computed value is readonly').toHaveBeenWarnedLast(); + // }); +}); diff --git a/test/v3/reactivity/reactive.spec.ts b/test/v3/reactivity/reactive.spec.ts new file mode 100644 index 00000000..7d4f12a6 --- /dev/null +++ b/test/v3/reactivity/reactive.spec.ts @@ -0,0 +1,207 @@ +import { + ref, + isRef, + reactive, + isReactive, + computed, + toRaw, + shallowReactive, + set, + markRaw, +} from '../../../src'; + +describe('reactivity/reactive', () => { + let warn: jest.SpyInstance; + beforeEach(() => { + warn = jest.spyOn(global.console, 'error').mockImplementation(() => null); + warn.mockReset(); + }); + afterEach(() => { + expect(warn).not.toBeCalled(); + warn.mockRestore(); + }); + + test('Object', () => { + const original = { foo: 1 }; + const observed = reactive(original); + expect(observed).toBe(original); + expect(isReactive(observed)).toBe(true); + expect(isReactive(original)).toBe(true); // this is false in v3 but true in v2 + // get + expect(observed.foo).toBe(1); + // has + expect('foo' in observed).toBe(true); + // ownKeys + expect(Object.keys(observed)).toEqual(['foo']); + }); + + test('proto', () => { + const obj = {}; + const reactiveObj = reactive(obj); + expect(isReactive(reactiveObj)).toBe(true); + // read prop of reactiveObject will cause reactiveObj[prop] to be reactive + // @ts-ignore + const prototype = reactiveObj['__proto__']; + const otherObj = { data: ['a'] }; + expect(isReactive(otherObj)).toBe(false); + const reactiveOther = reactive(otherObj); + expect(isReactive(reactiveOther)).toBe(true); + expect(reactiveOther.data[0]).toBe('a'); + }); + test('nested reactives', () => { + const original = { + nested: { + foo: 1, + }, + array: [{ bar: 2 }], + }; + const observed = reactive(original); + expect(isReactive(observed.nested)).toBe(true); + // expect(isReactive(observed.array)).toBe(true); //not supported by vue2 + // expect(isReactive(observed.array[0])).toBe(true); //not supported by vue2 + }); + + test('observed value should proxy mutations to original (Object)', () => { + const original: any = { foo: 1 }; + const observed = reactive(original); + // set + observed.bar = 1; + expect(observed.bar).toBe(1); + expect(original.bar).toBe(1); + // delete + delete observed.foo; + expect('foo' in observed).toBe(false); + expect('foo' in original).toBe(false); + }); + + test('setting a property with an unobserved value should wrap with reactive', () => { + const observed = reactive<{ foo?: object }>({}); + const raw = {}; + set(observed, 'foo', raw); // v2 limitation + + expect(observed.foo).toBe(raw); // v2 limitation + expect(isReactive(observed.foo)).toBe(true); + }); + + test('observing already observed value should return same Proxy', () => { + const original = { foo: 1 }; + const observed = reactive(original); + const observed2 = reactive(observed); + expect(observed2).toBe(observed); + }); + + test('observing the same value multiple times should return same Proxy', () => { + const original = { foo: 1 }; + const observed = reactive(original); + const observed2 = reactive(original); + expect(observed2).toBe(observed); + }); + + test('should not pollute original object with Proxies', () => { + const original: any = { foo: 1 }; + const original2 = { bar: 2 }; + const observed = reactive(original); + const observed2 = reactive(original2); + observed.bar = observed2; + expect(observed.bar).toBe(observed2); + expect(original.bar).toBe(original2); + }); + + test('unwrap', () => { + // vue2 mutates the original object + const original = { foo: 1 }; + const observed = reactive(original); + expect(toRaw(observed)).toBe(original); + expect(toRaw(original)).toBe(original); + }); + + test('should not unwrap Ref', () => { + const observedNumberRef = reactive(ref(1)); + const observedObjectRef = reactive(ref({ foo: 1 })); + + expect(isRef(observedNumberRef)).toBe(true); + expect(isRef(observedObjectRef)).toBe(true); + }); + + test('should unwrap computed refs', () => { + // readonly + const a = computed(() => 1); + // writable + const b = computed({ + get: () => 1, + set: () => {}, + }); + const obj = reactive({ a, b }); + // check type + obj.a + 1; + obj.b + 1; + expect(typeof obj.a).toBe(`number`); + expect(typeof obj.b).toBe(`number`); + }); + + test('non-observable values', () => { + const assertValue = (value: any) => { + expect(isReactive(reactive(value))).toBe(false); + // expect(warnSpy).toHaveBeenLastCalledWith(`value cannot be made reactive: ${String(value)}`); + }; + + // number + assertValue(1); + // string + assertValue('foo'); + // boolean + assertValue(false); + // null + assertValue(null); + // undefined + assertValue(undefined); + // symbol + const s = Symbol(); + assertValue(s); + + // built-ins should work and return same value + const p = Promise.resolve(); + expect(reactive(p)).toBe(p); + const r = new RegExp(''); + expect(reactive(r)).toBe(r); + const d = new Date(); + expect(reactive(d)).toBe(d); + + expect(warn).toBeCalledTimes(3); + expect( + warn.mock.calls.map(call => { + expect(call[0]).toBe('[Vue warn]: "reactive()" is called without provide an "object".'); + }) + ); + warn.mockReset(); + }); + + test('markRaw', () => { + const obj = reactive({ + foo: { a: 1 }, + bar: markRaw({ b: 2 }), + }); + expect(isReactive(obj.foo)).toBe(true); + expect(isReactive(obj.bar)).toBe(false); + }); + + test('should not observe frozen objects', () => { + const obj = reactive({ + foo: Object.freeze({ a: 1 }), + }); + expect(isReactive(obj.foo)).toBe(false); + }); + + describe('shallowReactive', () => { + test('should not make non-reactive properties reactive', () => { + const props = shallowReactive({ n: { foo: 1 } }); + expect(isReactive(props.n)).toBe(false); + }); + + test('should keep reactive properties reactive', () => { + const props: any = shallowReactive({ n: reactive({ foo: 1 }) }); + props.n = reactive({ foo: 2 }); + expect(isReactive(props.n)).toBe(true); + }); + }); +}); diff --git a/test/v3/reactivity/ref.spec.ts b/test/v3/reactivity/ref.spec.ts new file mode 100644 index 00000000..17dc3a1c --- /dev/null +++ b/test/v3/reactivity/ref.spec.ts @@ -0,0 +1,341 @@ +import { + ref, + reactive, + isRef, + toRef, + toRefs, + Ref, + computed, + triggerRef, + watchEffect, + unref, + isReactive, + shallowRef, +} from '../../../src'; + +describe('reactivity/ref', () => { + it('should hold a value', () => { + const a = ref(1); + expect(a.value).toBe(1); + a.value = 2; + expect(a.value).toBe(2); + }); + + it('should be reactive', () => { + const a = ref(1); + let dummy; + let calls = 0; + watchEffect( + () => { + calls++; + dummy = a.value; + }, + { flush: 'sync' } + ); + expect(calls).toBe(1); + expect(dummy).toBe(1); + + a.value = 2; + expect(calls).toBe(2); + expect(dummy).toBe(2); + // same value should not trigger + a.value = 2; + expect(calls).toBe(2); + expect(dummy).toBe(2); + }); + + it('should make nested properties reactive', () => { + const a = ref({ + count: 1, + }); + let dummy; + watchEffect( + () => { + dummy = a.value.count; + }, + { flush: 'sync' } + ); + expect(dummy).toBe(1); + a.value.count = 2; + expect(dummy).toBe(2); + }); + + it('should work without initial value', () => { + const a = ref(); + let dummy; + watchEffect( + () => { + dummy = a.value; + }, + { flush: 'sync' } + ); + expect(dummy).toBe(undefined); + a.value = 2; + expect(dummy).toBe(2); + }); + + it('should work like a normal property when nested in a reactive object', () => { + const a = ref(1); + const obj = reactive({ + a, + b: { + c: a, + }, + }); + + let dummy1: number; + let dummy2: number; + + watchEffect( + () => { + dummy1 = obj.a; + dummy2 = obj.b.c; + }, + { flush: 'sync' } + ); + + const assertDummiesEqualTo = (val: number) => + [dummy1, dummy2].forEach(dummy => expect(dummy).toBe(val)); + + assertDummiesEqualTo(1); + a.value++; + assertDummiesEqualTo(2); + obj.a++; + assertDummiesEqualTo(3); + obj.b.c++; + assertDummiesEqualTo(4); + }); + + it('should unwrap nested ref in types', () => { + const a = ref(0); + const b = ref(a); + + expect(typeof (b.value + 1)).toBe('number'); + }); + + it('should unwrap nested values in types', () => { + const a = { + b: ref(0), + }; + + const c = ref(a); + + expect(typeof (c.value.b + 1)).toBe('number'); + }); + + it('should NOT unwrap ref types nested inside arrays', () => { + const arr = ref([1, ref(1)]).value; + (arr[0] as number)++; + (arr[1] as Ref).value++; + + const arr2 = ref([1, new Map(), ref('1')]).value; + const value = arr2[0]; + if (isRef(value)) { + value + 'foo'; + } else if (typeof value === 'number') { + value + 1; + } else { + // should narrow down to Map type + // and not contain any Ref type + value.has('foo'); + } + }); + + it('should keep tuple types', () => { + const tuple: [number, string, { a: number }, () => number, Ref] = [ + 0, + '1', + { a: 1 }, + () => 0, + ref(0), + ]; + const tupleRef = ref(tuple); + + tupleRef.value[0]++; + expect(tupleRef.value[0]).toBe(1); + tupleRef.value[1] += '1'; + expect(tupleRef.value[1]).toBe('11'); + tupleRef.value[2].a++; + expect(tupleRef.value[2].a).toBe(2); + expect(tupleRef.value[3]()).toBe(0); + tupleRef.value[4].value++; + expect(tupleRef.value[4].value).toBe(1); + }); + + it('should keep symbols', () => { + const customSymbol = Symbol(); + const obj = { + [Symbol.asyncIterator]: { a: 1 }, + [Symbol.unscopables]: { b: '1' }, + [customSymbol]: { c: [1, 2, 3] }, + }; + + const objRef = ref(obj); + + expect(objRef.value[Symbol.asyncIterator]).toBe(obj[Symbol.asyncIterator]); + expect(objRef.value[Symbol.unscopables]).toBe(obj[Symbol.unscopables]); + expect(objRef.value[customSymbol]).toStrictEqual(obj[customSymbol]); + }); + + test('unref', () => { + expect(unref(1)).toBe(1); + expect(unref(ref(1))).toBe(1); + }); + + test('shallowRef', () => { + const sref = shallowRef({ a: 1 }); + expect(isReactive(sref.value)).toBe(false); + + let dummy; + watchEffect( + () => { + dummy = sref.value.a; + }, + { flush: 'sync' } + ); + expect(dummy).toBe(1); + + sref.value = { a: 2 }; + expect(isReactive(sref.value)).toBe(false); + expect(dummy).toBe(2); + + sref.value.a = 3; + expect(dummy).toBe(2); + }); + + test('shallowRef force trigger', () => { + const sref = shallowRef({ a: 1 }); + let dummy; + watchEffect( + () => { + dummy = sref.value.a; + }, + { flush: 'sync' } + ); + expect(dummy).toBe(1); + + sref.value.a = 2; + expect(dummy).toBe(1); // should not trigger yet + + // force trigger + // sref.value = sref.value; + triggerRef(sref); + expect(dummy).toBe(2); + }); + + test('isRef', () => { + expect(isRef(ref(1))).toBe(true); + expect(isRef(computed(() => 1))).toBe(true); + + expect(isRef(0)).toBe(false); + expect(isRef(1)).toBe(false); + // an object that looks like a ref isn't necessarily a ref + expect(isRef({ value: 0 })).toBe(false); + }); + + test('toRef', () => { + const a = reactive({ + x: 1, + }); + const x = toRef(a, 'x'); + expect(isRef(x)).toBe(true); + expect(x.value).toBe(1); + + // source -> proxy + a.x = 2; + expect(x.value).toBe(2); + + // proxy -> source + x.value = 3; + expect(a.x).toBe(3); + + // reactivity + let dummyX; + watchEffect( + () => { + dummyX = x.value; + }, + { flush: 'sync' } + ); + expect(dummyX).toBe(x.value); + + // mutating source should trigger watchEffect using the proxy refs + a.x = 4; + expect(dummyX).toBe(4); + }); + + test('toRefs', () => { + const a = reactive({ + x: 1, + y: 2, + }); + + const { x, y } = toRefs(a); + + expect(isRef(x)).toBe(true); + expect(isRef(y)).toBe(true); + expect(x.value).toBe(1); + expect(y.value).toBe(2); + + // source -> proxy + a.x = 2; + a.y = 3; + expect(x.value).toBe(2); + expect(y.value).toBe(3); + + // proxy -> source + x.value = 3; + y.value = 4; + expect(a.x).toBe(3); + expect(a.y).toBe(4); + + // reactivity + let dummyX, dummyY; + watchEffect( + () => { + dummyX = x.value; + dummyY = y.value; + }, + { flush: 'sync' } + ); + expect(dummyX).toBe(x.value); + expect(dummyY).toBe(y.value); + + // mutating source should trigger watchEffect using the proxy refs + a.x = 4; + a.y = 5; + expect(dummyX).toBe(4); + expect(dummyY).toBe(5); + }); + + // test('customRef', () => { + // let value = 1; + // let _trigger: () => void; + + // const custom = customRef((track, trigger) => ({ + // get() { + // track(); + // return value; + // }, + // set(newValue: number) { + // value = newValue; + // _trigger = trigger; + // }, + // })); + + // expect(isRef(custom)).toBe(true); + + // let dummy; + // watchEffect(() => { + // dummy = custom.value; + // }, {flush: 'sync'}); + // expect(dummy).toBe(1); + + // custom.value = 2; + // // should not trigger yet + // expect(dummy).toBe(1); + + // _trigger!(); + // expect(dummy).toBe(2); + // }); +}); diff --git a/test/v3/runtime-core/apiLifecycle.spec.ts b/test/v3/runtime-core/apiLifecycle.spec.ts new file mode 100644 index 00000000..b7a9629c --- /dev/null +++ b/test/v3/runtime-core/apiLifecycle.spec.ts @@ -0,0 +1,354 @@ +import { VueConstructor } from 'vue'; +const Vue = require('vue/dist/vue.common.js') as VueConstructor; +import { + onBeforeMount, + onMounted, + ref, + createElement as h, + onBeforeUpdate, + onUpdated, + onBeforeUnmount, + onUnmounted, +} from '../../../src'; +import { nextTick } from '../../helpers/utils'; + +// reference: https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks + +describe('api: lifecycle hooks', () => { + it('onBeforeMount', () => { + const root = document.createElement('div'); + const fn = jest.fn(() => { + // should be called before inner div is rendered + expect(root.innerHTML).toBe(``); + }); + + const Comp = { + setup() { + onBeforeMount(fn); + return () => h('div'); + }, + }; + new Vue(Comp).$mount(root); + //render(h(Comp), root); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('onMounted', () => { + const root = document.createElement('div'); + const fn = jest.fn(() => { + // should be called after inner div is rendered + expect(root.outerHTML).toBe(`
`); + }); + + const Comp = { + setup() { + onMounted(fn); + return () => h('div'); + }, + }; + new Vue(Comp).$mount(root); + //render(h(Comp), root); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('onBeforeUpdate', async () => { + const count = ref(0); + // const root = document.createElement('div'); + const fn = jest.fn(() => { + // should be called before inner div is updated + expect(vm.$el.outerHTML).toBe(`
0
`); + }); + + const Comp = { + setup() { + onBeforeUpdate(fn); + return () => h('div', (count.value as unknown) as string); + }, + }; + const vm = new Vue(Comp).$mount(); + //render(h(Comp), root); + + count.value = 1; + await nextTick(); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('onUpdated', async () => { + const count = ref(0); + // const root = document.createElement('div'); + const fn = jest.fn(() => { + // should be called after inner div is updated + expect(vm.$el.outerHTML).toBe(`
1
`); + }); + + const Comp = { + setup() { + onUpdated(fn); + return () => h('div', (count.value as unknown) as string); + }, + }; + const vm = new Vue(Comp).$mount(); + //render(h(Comp), root); + + count.value++; + await nextTick(); + expect(fn).toHaveBeenCalledTimes(1); + }); + + // it('onBeforeUnmount', async () => { + // const toggle = ref(true); + // const root = document.createElement('div'); + // const fn = jest.fn(() => { + // // should be called before inner div is removed + // expect(root.outerHTML).toBe(`
`); + // }); + + // const Comp = { + // setup() { + // return () => (toggle.value ? h(Child) : null); + // }, + // }; + + // const Child = { + // setup() { + // onBeforeUnmount(fn); + // return () => h('div'); + // }, + // }; + + // new Vue(Comp).$mount(root); + // //render(h(Comp), root); + + // toggle.value = false; + // await nextTick(); + // expect(fn).toHaveBeenCalledTimes(1); + // }); + + // it('onUnmounted', async () => { + // const toggle = ref(true); + // const root = document.createElement('div'); + // const fn = jest.fn(() => { + // // should be called after inner div is removed + // expect(root.outerHTML).toBe(``); + // }); + + // const Comp = { + // setup() { + // return () => (toggle.value ? h(Child) : null); + // }, + // }; + + // const Child = { + // setup() { + // onUnmounted(fn); + // return () => h('div'); + // }, + // }; + + // new Vue(Comp).$mount(root); + // //render(h(Comp), root); + + // toggle.value = false; + // await nextTick(); + // expect(fn).toHaveBeenCalledTimes(1); + // }); + + it('onBeforeUnmount in onMounted', async () => { + const toggle = ref(true); + const root = document.createElement('div'); + const fn = jest.fn(() => { + // should be called before inner div is removed + expect(root.outerHTML).toBe(`
`); + }); + + const Comp = { + setup() { + return () => (toggle.value ? h(Child) : null); + }, + }; + + const Child = { + setup() { + onMounted(() => { + onBeforeUnmount(fn); + }); + return () => h('div'); + }, + }; + + new Vue(Comp).$mount(root); + //render(h(Comp), root); + + toggle.value = false; + await nextTick(); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('lifecycle call order', async () => { + const count = ref(0); + const calls: string[] = []; + + const Root = { + setup() { + onBeforeMount(() => calls.push('root onBeforeMount')); + onMounted(() => calls.push('root onMounted')); + onBeforeUpdate(() => calls.push('root onBeforeUpdate')); + onUpdated(() => calls.push('root onUpdated')); + onBeforeUnmount(() => calls.push('root onBeforeUnmount')); + onUnmounted(() => calls.push('root onUnmounted')); + return () => h(Mid, { props: { count: count.value } }); + }, + }; + const Mid = { + props: { + count: Number, + }, + setup(props: any) { + onBeforeMount(() => calls.push('mid onBeforeMount')); + onMounted(() => calls.push('mid onMounted')); + onBeforeUpdate(() => calls.push('mid onBeforeUpdate')); + onUpdated(() => calls.push('mid onUpdated')); + onBeforeUnmount(() => calls.push('mid onBeforeUnmount')); + onUnmounted(() => calls.push('mid onUnmounted')); + return () => h(Child, { props: { count: props.count } }); + }, + }; + + const Child = { + props: { + count: Number, + }, + setup(props: any) { + onBeforeMount(() => calls.push('child onBeforeMount')); + onMounted(() => calls.push('child onMounted')); + onBeforeUpdate(() => calls.push('child onBeforeUpdate')); + onUpdated(() => calls.push('child onUpdated')); + onBeforeUnmount(() => calls.push('child onBeforeUnmount')); + onUnmounted(() => calls.push('child onUnmounted')); + return () => h('div', props.count); + }, + }; + + // mount + // render(h(Root), root); + const vm = new Vue(Root); + vm.$mount(); + expect(calls).toEqual([ + 'root onBeforeMount', + 'mid onBeforeMount', + 'child onBeforeMount', + 'child onMounted', + 'mid onMounted', + 'root onMounted', + ]); + + calls.length = 0; + + // update + count.value++; + await nextTick(); + await nextTick(); + await nextTick(); + expect(calls).toEqual([ + 'root onBeforeUpdate', + 'mid onBeforeUpdate', + 'child onBeforeUpdate', + 'child onUpdated', + 'mid onUpdated', + 'root onUpdated', + ]); + + calls.length = 0; + + // unmount + // render(null, root); + vm.$destroy(); + expect(calls).toEqual([ + 'root onBeforeUnmount', + 'mid onBeforeUnmount', + 'child onBeforeUnmount', + 'child onUnmounted', + 'mid onUnmounted', + 'root onUnmounted', + ]); + }); + + // it('onRenderTracked', () => { + // const events: DebuggerEvent[] = []; + // const onTrack = jest.fn((e: DebuggerEvent) => { + // events.push(e); + // }); + // const obj = reactive({ foo: 1, bar: 2 }); + + // const Comp = { + // setup() { + // onRenderTracked(onTrack); + // return () => h('div', [obj.foo, 'bar' in obj, Object.keys(obj).join('')]); + // }, + // }; + + // render(h(Comp), document.createElement('div')); + // expect(onTrack).toHaveBeenCalledTimes(3); + // expect(events).toMatchObject([ + // { + // target: obj, + // type: TrackOpTypes.GET, + // key: 'foo', + // }, + // { + // target: obj, + // type: TrackOpTypes.HAS, + // key: 'bar', + // }, + // { + // target: obj, + // type: TrackOpTypes.ITERATE, + // key: ITERATE_KEY, + // }, + // ]); + // }); + + // it('onRenderTriggered', async () => { + // const events: DebuggerEvent[] = []; + // const onTrigger = jest.fn((e: DebuggerEvent) => { + // events.push(e); + // }); + // const obj = reactive({ foo: 1, bar: 2 }); + + // const Comp = { + // setup() { + // onRenderTriggered(onTrigger); + // return () => h('div', [obj.foo, 'bar' in obj, Object.keys(obj).join('')]); + // }, + // }; + + // render(h(Comp), document.createElement('div')); + + // obj.foo++; + // await nextTick(); + // expect(onTrigger).toHaveBeenCalledTimes(1); + // expect(events[0]).toMatchObject({ + // type: TriggerOpTypes.SET, + // key: 'foo', + // oldValue: 1, + // newValue: 2, + // }); + + // delete obj.bar; + // await nextTick(); + // expect(onTrigger).toHaveBeenCalledTimes(2); + // expect(events[1]).toMatchObject({ + // type: TriggerOpTypes.DELETE, + // key: 'bar', + // oldValue: 2, + // }); + // (obj as any).baz = 3; + // await nextTick(); + // expect(onTrigger).toHaveBeenCalledTimes(3); + // expect(events[2]).toMatchObject({ + // type: TriggerOpTypes.ADD, + // key: 'baz', + // newValue: 3, + // }); + // }); +}); diff --git a/test/v3/runtime-core/apiWatch.spec.ts b/test/v3/runtime-core/apiWatch.spec.ts new file mode 100644 index 00000000..fc5f58cc --- /dev/null +++ b/test/v3/runtime-core/apiWatch.spec.ts @@ -0,0 +1,495 @@ +import { watch, watchEffect, computed, reactive, ref, set, shallowReactive } from '../../../src'; +import { nextTick } from '../../helpers/utils'; +import Vue from 'vue'; + +// reference: https://vue-composition-api-rfc.netlify.com/api.html#watch + +describe('api: watch', () => { + // const warnSpy = jest.spyOn(console, 'warn'); + const warnSpy = jest.spyOn((Vue as any).util, 'warn'); + + beforeEach(() => { + warnSpy.mockReset(); + }); + + it('effect', async () => { + const state = reactive({ count: 0 }); + let dummy; + watchEffect(() => { + dummy = state.count; + }); + expect(dummy).toBe(0); + + state.count++; + await nextTick(); + expect(dummy).toBe(1); + }); + + it('watching single source: getter', async () => { + const state = reactive({ count: 0 }); + let dummy; + watch( + () => state.count, + (count, prevCount) => { + dummy = [count, prevCount]; + // assert types + count + 1; + if (prevCount) { + prevCount + 1; + } + } + ); + state.count++; + await nextTick(); + expect(dummy).toMatchObject([1, 0]); + }); + + it('watching single source: ref', async () => { + const count = ref(0); + let dummy; + watch(count, (count, prevCount) => { + dummy = [count, prevCount]; + // assert types + count + 1; + if (prevCount) { + prevCount + 1; + } + }); + count.value++; + await nextTick(); + expect(dummy).toMatchObject([1, 0]); + }); + + it('watching single source: computed ref', async () => { + const count = ref(0); + const plus = computed(() => count.value + 1); + let dummy; + watch(plus, (count, prevCount) => { + dummy = [count, prevCount]; + // assert types + count + 1; + if (prevCount) { + prevCount + 1; + } + }); + count.value++; + await nextTick(); + expect(dummy).toMatchObject([2, 1]); + }); + + it('watching primitive with deep: true', async () => { + const count = ref(0); + let dummy; + watch( + count, + (c, prevCount) => { + dummy = [c, prevCount]; + }, + { + deep: true, + } + ); + count.value++; + await nextTick(); + expect(dummy).toMatchObject([1, 0]); + }); + + it('directly watching reactive object (with automatic deep: true)', async () => { + const src = reactive({ + count: 0, + }); + let dummy; + watch(src, ({ count }) => { + dummy = count; + }); + src.count++; + await nextTick(); + expect(dummy).toBe(1); + }); + + it('watching multiple sources', async () => { + const state = reactive({ count: 1 }); + const count = ref(1); + const plus = computed(() => count.value + 1); + + let dummy; + watch([() => state.count, count, plus], (vals, oldVals) => { + dummy = [vals, oldVals]; + // assert types + vals.concat(1); + oldVals.concat(1); + }); + + state.count++; + count.value++; + await nextTick(); + expect(dummy).toMatchObject([[2, 2, 3], [1, 1, 2]]); + }); + + it('watching multiple sources: readonly array', async () => { + const state = reactive({ count: 1 }); + const status = ref(false); + + let dummy; + watch([() => state.count, status] as const, (vals, oldVals) => { + dummy = [vals, oldVals]; + const [count] = vals; + const [, oldStatus] = oldVals; + // assert types + count + 1; + oldStatus === true; + }); + + state.count++; + status.value = true; + await nextTick(); + expect(dummy).toMatchObject([[2, true], [1, false]]); + }); + + it('warn invalid watch source', () => { + // @ts-ignore + watch(1, () => {}); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Invalid watch source'), + expect.anything() + ); + }); + + it('stopping the watcher (effect)', async () => { + const state = reactive({ count: 0 }); + let dummy; + const stop = watchEffect(() => { + dummy = state.count; + }); + expect(dummy).toBe(0); + + stop(); + state.count++; + await nextTick(); + // should not update + expect(dummy).toBe(0); + }); + + it('stopping the watcher (with source)', async () => { + const state = reactive({ count: 0 }); + let dummy; + const stop = watch( + () => state.count, + count => { + dummy = count; + } + ); + + state.count++; + await nextTick(); + expect(dummy).toBe(1); + + stop(); + state.count++; + await nextTick(); + // should not update + expect(dummy).toBe(1); + }); + + it('cleanup registration (effect)', async () => { + const state = reactive({ count: 0 }); + const cleanup = jest.fn(); + let dummy; + const stop = watchEffect(onCleanup => { + onCleanup(cleanup); + dummy = state.count; + }); + expect(dummy).toBe(0); + + state.count++; + await nextTick(); + expect(cleanup).toHaveBeenCalledTimes(1); + expect(dummy).toBe(1); + + stop(); + expect(cleanup).toHaveBeenCalledTimes(2); + }); + + it('cleanup registration (with source)', async () => { + const count = ref(0); + const cleanup = jest.fn(); + let dummy; + const stop = watch(count, (count, prevCount, onCleanup) => { + onCleanup(cleanup); + dummy = count; + }); + + count.value++; + await nextTick(); + expect(cleanup).toHaveBeenCalledTimes(0); + expect(dummy).toBe(1); + + count.value++; + await nextTick(); + expect(cleanup).toHaveBeenCalledTimes(1); + expect(dummy).toBe(2); + + stop(); + expect(cleanup).toHaveBeenCalledTimes(2); + }); + + // it('flush timing: post (default)', async () => { + // const count = ref(0); + // let callCount = 0; + // const assertion = jest.fn(count => { + // callCount++; + // // on mount, the watcher callback should be called before DOM render + // // on update, should be called after the count is updated + // const expectedDOM = callCount === 1 ? `` : `${count}`; + // expect(serializeInner(root)).toBe(expectedDOM); + // }); + + // const Comp = { + // setup() { + // watchEffect(() => { + // assertion(count.value); + // }); + // return () => count.value; + // }, + // }; + // const root = nodeOps.createElement('div'); + // render(h(Comp), root); + // expect(assertion).toHaveBeenCalledTimes(1); + + // count.value++; + // await nextTick(); + // expect(assertion).toHaveBeenCalledTimes(2); + // }); + + // it('flush timing: pre', async () => { + // const count = ref(0); + // const count2 = ref(0); + + // let callCount = 0; + // const assertion = jest.fn((count, count2Value) => { + // callCount++; + // // on mount, the watcher callback should be called before DOM render + // // on update, should be called before the count is updated + // const expectedDOM = callCount === 1 ? `` : `${count - 1}`; + // expect(serializeInner(root)).toBe(expectedDOM); + + // // in a pre-flush callback, all state should have been updated + // const expectedState = callCount === 1 ? 0 : 1; + // expect(count2Value).toBe(expectedState); + // }); + + // const Comp = { + // setup() { + // watchEffect( + // () => { + // assertion(count.value, count2.value); + // }, + // { + // flush: 'pre', + // } + // ); + // return () => count.value; + // }, + // }; + // const root = nodeOps.createElement('div'); + // render(h(Comp), root); + // expect(assertion).toHaveBeenCalledTimes(1); + + // count.value++; + // count2.value++; + // await nextTick(); + // // two mutations should result in 1 callback execution + // expect(assertion).toHaveBeenCalledTimes(2); + // }); + + // it('flush timing: sync', async () => { + // const count = ref(0); + // const count2 = ref(0); + + // let callCount = 0; + // const assertion = jest.fn(count => { + // callCount++; + // // on mount, the watcher callback should be called before DOM render + // // on update, should be called before the count is updated + // const expectedDOM = callCount === 1 ? `` : `${count - 1}`; + // expect(serializeInner(root)).toBe(expectedDOM); + + // // in a sync callback, state mutation on the next line should not have + // // executed yet on the 2nd call, but will be on the 3rd call. + // const expectedState = callCount < 3 ? 0 : 1; + // expect(count2.value).toBe(expectedState); + // }); + + // const Comp = { + // setup() { + // watchEffect( + // () => { + // assertion(count.value); + // }, + // { + // flush: 'sync', + // } + // ); + // return () => count.value; + // }, + // }; + // const root = nodeOps.createElement('div'); + // render(h(Comp), root); + // expect(assertion).toHaveBeenCalledTimes(1); + + // count.value++; + // count2.value++; + // await nextTick(); + // expect(assertion).toHaveBeenCalledTimes(3); + // }); + + it('deep', async () => { + const state = reactive({ + nested: { + count: ref(0), + }, + array: [1, 2, 3], + map: new Map([['a', 1], ['b', 2]]), + set: new Set([1, 2, 3]), + }); + + let dummy; + watch( + () => state, + state => { + dummy = [state.nested.count, state.array[0], state.map.get('a'), state.set.has(1)]; + }, + { deep: true } + ); + + state.nested.count++; + await nextTick(); + expect(dummy).toEqual([1, 1, 1, true]); + + // nested array mutation + set(state.array, '0', 2); + await nextTick(); + expect(dummy).toEqual([1, 2, 1, true]); + + // NOT supported by Vue.observe :( + // // nested map mutation + // state.map.set('a', 2); + // await nextTick(); + // expect(dummy).toEqual([1, 2, 2, true]); + + // // nested set mutation + // state.set.delete(1); + // await nextTick(); + // expect(dummy).toEqual([1, 2, 2, false]); + }); + + it('immediate', async () => { + const count = ref(0); + const cb = jest.fn(); + watch(count, cb, { immediate: true }); + expect(cb).toHaveBeenCalledTimes(1); + count.value++; + await nextTick(); + expect(cb).toHaveBeenCalledTimes(2); + }); + + it('immediate: triggers when initial value is null', async () => { + const state = ref(null); + const spy = jest.fn(); + watch(() => state.value, spy, { immediate: true }); + expect(spy).toHaveBeenCalled(); + }); + + it('immediate: triggers when initial value is undefined', async () => { + const state = ref(); + const spy = jest.fn(); + watch(() => state.value, spy, { immediate: true }); + expect(spy).toHaveBeenCalled(); + state.value = 3; + await nextTick(); + expect(spy).toHaveBeenCalledTimes(2); + // testing if undefined can trigger the watcher + state.value = undefined; + await nextTick(); + expect(spy).toHaveBeenCalledTimes(3); + // it shouldn't trigger if the same value is set + state.value = undefined; + await nextTick(); + expect(spy).toHaveBeenCalledTimes(3); + }); + + it('shallow reactive effect', async () => { + const state = shallowReactive({ count: 0 }); + let dummy; + watch( + () => state.count, + () => { + dummy = state.count; + }, + { immediate: true } + ); + expect(dummy).toBe(0); + + state.count++; + await nextTick(); + expect(dummy).toBe(1); + }); + + it('shallow reactive object', async () => { + const state = shallowReactive({ a: { count: 0 } }); + let dummy; + watch( + () => state.a, + () => { + dummy = state.a.count; + }, + { immediate: true } + ); + expect(dummy).toBe(0); + + state.a.count++; + await nextTick(); + expect(dummy).toBe(0); + + state.a = { count: 5 }; + await nextTick(); + + expect(dummy).toBe(5); + }); + + // it('warn immediate option when using effect', async () => { + // const count = ref(0); + // let dummy; + // watchEffect( + // () => { + // dummy = count.value; + // }, + // // @ts-ignore + // { immediate: false } + // ); + // expect(dummy).toBe(0); + // expect(warnSpy).toHaveBeenCalledWith(`"immediate" option is only respected`); + + // count.value++; + // await nextTick(); + // expect(dummy).toBe(1); + // }); + + // it('warn and not respect deep option when using effect', async () => { + // const arr = ref([1, [2]]); + // const spy = jest.fn(); + // watchEffect( + // () => { + // spy(); + // return arr; + // }, + // // @ts-ignore + // { deep: true } + // ); + // expect(spy).toHaveBeenCalledTimes(1); + // (arr.value[1] as Array)[0] = 3; + // await nextTick(); + // expect(spy).toHaveBeenCalledTimes(1); + // expect(warnSpy).toHaveBeenCalledWith(`"deep" option is only respected`); + // }); +}); diff --git a/yarn.lock b/yarn.lock index 9d144118..8d1bd5cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -292,6 +292,16 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^12.0.9" +"@jest/types@^25.3.0": + version "25.3.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.3.0.tgz#88f94b277a1d028fd7117bc1f74451e0fc2131e7" + integrity sha512-UkaDNewdqXAmCDbN2GlUM6amDKS78eCqiw/UmF5nE0mmLTd6moJkiZJML/X52Ke3LH7Swhw883IRXq8o9nWjVw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^15.0.0" + chalk "^3.0.0" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.0" resolved "https://registry.npm.taobao.org/@samverschueren/stream-to-observable/download/@samverschueren/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" @@ -332,6 +342,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.npm.taobao.org/@types/estree/download/@types/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -357,17 +372,13 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest-diff@*": - version "20.0.1" - resolved "https://registry.npm.taobao.org/@types/jest-diff/download/@types/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" - integrity sha1-NcwVucTzChjvIYUuJV/bAvbVm4k= - -"@types/jest@^24.0.13": - version "24.0.13" - resolved "https://registry.npm.taobao.org/@types/jest/download/@types/jest-24.0.13.tgz#10f50b64cb05fb02411fbba49e9042a3a11da3f9" - integrity sha1-EPULZMsF+wJBH7uknpBCo6Edo/k= +"@types/jest@^25.2.1": + version "25.2.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.2.1.tgz#9544cd438607955381c1bdbdb97767a249297db5" + integrity sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA== dependencies: - "@types/jest-diff" "*" + jest-diff "^25.2.1" + pretty-format "^25.2.1" "@types/node@*", "@types/node@^12.0.2", "@types/node@^12.0.3": version "12.0.4" @@ -391,11 +402,23 @@ resolved "https://registry.npm.taobao.org/@types/stack-utils/download/@types/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha1-CoUdO9lkmPolwzq3J47TvWXwbD4= +"@types/yargs-parser@*": + version "15.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" + integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== + "@types/yargs@^12.0.2", "@types/yargs@^12.0.9": version "12.0.12" resolved "https://registry.npm.taobao.org/@types/yargs/download/@types/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916" integrity sha1-Rd0dBjjoyPFT6H0paQdlkpaHORY= +"@types/yargs@^15.0.0": + version "15.0.4" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299" + integrity sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg== + dependencies: + "@types/yargs-parser" "*" + abab@^2.0.0: version "2.0.0" resolved "https://registry.npm.taobao.org/abab/download/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" @@ -459,6 +482,11 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0: resolved "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc= +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-2.2.1.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fansi-styles%2Fdownload%2Fansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -471,6 +499,14 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + any-observable@^0.3.0: version "0.3.0" resolved "https://registry.npm.taobao.org/any-observable/download/any-observable-0.3.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fany-observable%2Fdownload%2Fany-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" @@ -796,6 +832,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chownr@^1.1.1: version "1.1.1" resolved "https://registry.npm.taobao.org/chownr/download/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" @@ -865,11 +909,23 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.npm.taobao.org/color-name/download/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.npm.taobao.org/combined-stream/download/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -1088,6 +1144,11 @@ diff-sequences@^24.3.0: resolved "https://registry.npm.taobao.org/diff-sequences/download/diff-sequences-24.3.0.tgz#0f20e8a1df1abddaf4d9c226680952e64118b975" integrity sha1-DyDood8avdr02cImaAlS5kEYuXU= +diff-sequences@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" + integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== + domexception@^1.0.1: version "1.0.1" resolved "https://registry.npm.taobao.org/domexception/download/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" @@ -1535,6 +1596,11 @@ has-flag@^3.0.0: resolved "https://registry.npm.taobao.org/has-flag/download/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.npm.taobao.org/has-symbols/download/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" @@ -2046,6 +2112,16 @@ jest-diff@^24.8.0: jest-get-type "^24.8.0" pretty-format "^24.8.0" +jest-diff@^25.2.1: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.3.0.tgz#0d7d6f5d6171e5dacde9e05be47b3615e147c26f" + integrity sha512-vyvs6RPoVdiwARwY4kqFWd4PirPLm2dmmkNzKqo38uZOzJvLee87yzDjIZLmY1SjM3XR5DwsUH+cdQ12vgqi1w== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.2.6" + jest-get-type "^25.2.6" + pretty-format "^25.3.0" + jest-docblock@^24.3.0: version "24.3.0" resolved "https://registry.npm.taobao.org/jest-docblock/download/jest-docblock-24.3.0.tgz#b9c32dac70f72e4464520d2ba4aec02ab14db5dd" @@ -2092,6 +2168,11 @@ jest-get-type@^24.8.0: resolved "https://registry.npm.taobao.org/jest-get-type/download/jest-get-type-24.8.0.tgz#a7440de30b651f5a70ea3ed7ff073a32dfe646fc" integrity sha1-p0QN4wtlH1pw6j7X/wc6Mt/mRvw= +jest-get-type@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" + integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== + jest-haste-map@^24.8.0: version "24.8.0" resolved "https://registry.npm.taobao.org/jest-haste-map/download/jest-haste-map-24.8.0.tgz#51794182d877b3ddfd6e6d23920e3fe72f305800" @@ -3305,6 +3386,16 @@ pretty-format@^24.8.0: ansi-styles "^3.2.0" react-is "^16.8.4" +pretty-format@^25.2.1, pretty-format@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.3.0.tgz#d0a4f988ff4a6cd350342fdabbb809aeb4d49ad5" + integrity sha512-wToHwF8bkQknIcFkBqNfKu4+UZqnrLn/Vr+wwKQwwvPzkBfDDKp/qIabFqdgtoi5PEnM8LFByVsOrHoa3SpTVA== + dependencies: + "@jest/types" "^25.3.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.npm.taobao.org/process-nextick-args/download/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" @@ -3361,6 +3452,11 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-is@^16.12.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react-is@^16.8.4: version "16.8.6" resolved "https://registry.npm.taobao.org/react-is/download/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" @@ -4035,6 +4131,13 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + symbol-observable@^1.1.0: version "1.2.0" resolved "https://registry.npm.taobao.org/symbol-observable/download/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"