From fdad044ba8bb6402756d54ecf30e667e3321f79a Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 27 Oct 2021 14:11:02 +0800 Subject: [PATCH] fork @vue-reactivity/watch to fix bundler issue @vue-reactivity/watch use tsup as its bundler, which create .mjs file as the ES module output. But webpack4 do not like .mjs extension which cause tools like create-react-app failed when using meta-ui. Even users can add some trick to let webpack4 bundle .mjs file, the watch function still not working. Refs: 1. https://github.com/facebook/create-react-app/issues/10356 2. https://github.com/formatjs/formatjs/issues/1395#issuecomment-518823361 --- .eslintrc.json | 6 +- package.json | 1 + packages/runtime/package.json | 4 +- .../src/components/chakra-ui/Form/Form.tsx | 2 +- .../components/chakra-ui/Form/FormControl.tsx | 2 +- .../src/components/core/GridLayout.tsx | 2 +- .../runtime/src/services/DebugComponents.tsx | 2 +- packages/runtime/src/services/ImplWrapper.tsx | 2 +- packages/runtime/src/services/stateStore.ts | 14 +- packages/runtime/src/utils/watchReactivity.ts | 266 ++++++++++++++++++ yarn.lock | 10 +- 11 files changed, 288 insertions(+), 23 deletions(-) create mode 100644 packages/runtime/src/utils/watchReactivity.ts diff --git a/.eslintrc.json b/.eslintrc.json index 0488eb278..31fe8bd3f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -6,7 +6,8 @@ "extends": [ "eslint:recommended", "plugin:react/recommended", - "plugin:@typescript-eslint/recommended" + "plugin:@typescript-eslint/recommended", + "prettier" ], "parser": "@typescript-eslint/parser", "parserOptions": { @@ -18,10 +19,7 @@ }, "plugins": ["react", "@typescript-eslint"], "rules": { - "indent": ["error", 2, { "flatTernaryExpressions": true, "SwitchCase": 1 }], "linebreak-style": ["error", "unix"], - "quotes": ["error", "single"], - "semi": ["error", "always"], "react/prop-types": "off", "no-case-declarations": "off", "@typescript-eslint/explicit-module-boundary-types": "off", diff --git a/package.json b/package.json index 5e79e85ae..07076e410 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/parser": "^4.31.1", "eslint": "^7.32.0", + "eslint-config-prettier": "^8.3.0", "eslint-plugin-react": "^7.25.1", "husky": "^6.0.0", "lerna": "^4.0.0", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 77ed1fbac..cf552bbb5 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -23,7 +23,7 @@ "scripts": { "dev": "vite", "test": "jest", - "build": "tsup src/index.ts --format cjs,esm,iife --legacy-output --inject ./react-import.js", + "build": "tsup src/index.ts --format cjs,esm,iife --legacy-output --inject ./react-import.js --clean --no-splitting --sourcemap", "typings": "tsc --emitDeclarationOnly --declarationDir typings", "lint": "eslint src --ext .ts", "prepublish": "npm run build && npm run typings" @@ -35,8 +35,8 @@ "@emotion/styled": "^11", "@meta-ui/core": "^0.2.1", "@sinclair/typebox": "^0.20.5", - "@vue-reactivity/watch": "^0.1.6", "@vue/reactivity": "^3.1.5", + "@vue/shared": "^3.2.20", "copy-to-clipboard": "^3.3.1", "dayjs": "^1.10.6", "framer-motion": "^4", diff --git a/packages/runtime/src/components/chakra-ui/Form/Form.tsx b/packages/runtime/src/components/chakra-ui/Form/Form.tsx index 472e3b821..acc885429 100644 --- a/packages/runtime/src/components/chakra-ui/Form/Form.tsx +++ b/packages/runtime/src/components/chakra-ui/Form/Form.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/react'; import { Type, Static } from '@sinclair/typebox'; import { createComponent } from '@meta-ui/core'; import { Button, VStack } from '@chakra-ui/react'; -import { watch } from '@vue-reactivity/watch'; +import { watch } from '../../../utils/watchReactivity'; import { ComponentImplementation } from '../../../services/registry'; import Slot from '../../_internal/Slot'; diff --git a/packages/runtime/src/components/chakra-ui/Form/FormControl.tsx b/packages/runtime/src/components/chakra-ui/Form/FormControl.tsx index fb74e7c97..33c445151 100644 --- a/packages/runtime/src/components/chakra-ui/Form/FormControl.tsx +++ b/packages/runtime/src/components/chakra-ui/Form/FormControl.tsx @@ -9,7 +9,7 @@ import { FormLabel, Text, } from '@chakra-ui/react'; -import { watch } from '@vue-reactivity/watch'; +import { watch } from '../../../utils/watchReactivity'; import { FormControlContentCSS, FormControlCSS, diff --git a/packages/runtime/src/components/core/GridLayout.tsx b/packages/runtime/src/components/core/GridLayout.tsx index a80d1970d..85f52568c 100644 --- a/packages/runtime/src/components/core/GridLayout.tsx +++ b/packages/runtime/src/components/core/GridLayout.tsx @@ -5,7 +5,7 @@ import { getSlots } from '../_internal/Slot'; import { Static, Type } from '@sinclair/typebox'; import { partial } from 'lodash'; -const BaseGridLayout = React.lazy(() => import('../../components/_internal/GridLayout')); +const BaseGridLayout = React.lazy(() => import('../_internal/GridLayout')); const GridLayout: ComponentImplementation> = ({ slotsMap, diff --git a/packages/runtime/src/services/DebugComponents.tsx b/packages/runtime/src/services/DebugComponents.tsx index 1df6398a5..4027784be 100644 --- a/packages/runtime/src/services/DebugComponents.tsx +++ b/packages/runtime/src/services/DebugComponents.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { StateManager } from './stateStore'; import { ApiService } from './apiService'; -import { watch } from '@vue-reactivity/watch'; +import { watch } from '../utils/watchReactivity'; import copy from 'copy-to-clipboard'; export const DebugStore: React.FC<{ stateManager: StateManager }> = ({ diff --git a/packages/runtime/src/services/ImplWrapper.tsx b/packages/runtime/src/services/ImplWrapper.tsx index 39b392c28..27f5456b1 100644 --- a/packages/runtime/src/services/ImplWrapper.tsx +++ b/packages/runtime/src/services/ImplWrapper.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { watch } from '@vue-reactivity/watch'; +import { watch } from '../utils/watchReactivity'; import { merge } from 'lodash'; import { RuntimeApplicationComponent, diff --git a/packages/runtime/src/services/stateStore.ts b/packages/runtime/src/services/stateStore.ts index 7f41a6427..0958afbe0 100644 --- a/packages/runtime/src/services/stateStore.ts +++ b/packages/runtime/src/services/stateStore.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; import { reactive } from '@vue/reactivity'; -import { watch } from '@vue-reactivity/watch'; +import { watch } from '../utils/watchReactivity'; import { LIST_ITEM_EXP, LIST_ITEM_INDEX_EXP } from '../constants'; dayjs.extend(relativeTime); @@ -125,13 +125,13 @@ export class StateManager { return _.mapValues(obj, (val, key) => { return _.isArray(val) ? val.map((innerVal, idx) => { - return _.isPlainObject(innerVal) - ? this.mapValuesDeep(innerVal, fn, path.concat(key, idx)) - : fn({ value: innerVal, key, obj, path: path.concat(key, idx) }); - }) + return _.isPlainObject(innerVal) + ? this.mapValuesDeep(innerVal, fn, path.concat(key, idx)) + : fn({ value: innerVal, key, obj, path: path.concat(key, idx) }); + }) : _.isPlainObject(val) - ? this.mapValuesDeep(val, fn, path.concat(key)) - : fn({ value: val, key, obj, path: path.concat(key) }); + ? this.mapValuesDeep(val, fn, path.concat(key)) + : fn({ value: val, key, obj, path: path.concat(key) }); }); } diff --git a/packages/runtime/src/utils/watchReactivity.ts b/packages/runtime/src/utils/watchReactivity.ts new file mode 100644 index 000000000..3258c5544 --- /dev/null +++ b/packages/runtime/src/utils/watchReactivity.ts @@ -0,0 +1,266 @@ +// forked from https://github.com/vue-reactivity/watch/blob/master/src/index.ts by Anthony Fu +// ported from https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/apiWatch.ts by Evan You + +/* eslint-disable @typescript-eslint/ban-types */ + +import { + ComputedRef, + effect, + Ref, + ReactiveEffectOptions, + isReactive, + isRef, + stop, +} from '@vue/reactivity'; +import { hasChanged, isArray, isFunction, isObject, NOOP, isPromise } from '@vue/shared'; + +export function callWithErrorHandling(fn: Function, type: string, args?: unknown[]) { + let res; + try { + res = args ? fn(...args) : fn(); + } catch (err) { + handleError(err, type); + } + return res; +} + +export function callWithAsyncErrorHandling( + fn: Function | Function[], + type: string, + args?: unknown[] +): any[] { + if (isFunction(fn)) { + const res = callWithErrorHandling(fn, type, args); + if (res && isPromise(res)) { + res.catch(err => { + handleError(err, type); + }); + } + return res; + } + + const values = []; + for (let i = 0; i < fn.length; i++) + values.push(callWithAsyncErrorHandling(fn[i], type, args)); + + return values; +} + +function handleError(err: unknown, type: String) { + console.error(new Error(`[@vue-reactivity/watch]: ${type}`)); + console.error(err); +} + +export function warn(message: string) { + console.warn(createError(message)); +} + +function createError(message: string) { + return new Error(`[reactivue]: ${message}`); +} + +export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void; + +export type WatchSource = Ref | ComputedRef | (() => T); + +export type WatchCallback = ( + value: V, + oldValue: OV, + onInvalidate: InvalidateCbRegistrator +) => any; + +export type WatchStopHandle = () => void; + +type MapSources = { + [K in keyof T]: T[K] extends WatchSource + ? V + : T[K] extends object + ? T[K] + : never; +}; + +type MapOldSources = { + [K in keyof T]: T[K] extends WatchSource + ? Immediate extends true + ? V | undefined + : V + : T[K] extends object + ? Immediate extends true + ? T[K] | undefined + : T[K] + : never; +}; + +type InvalidateCbRegistrator = (cb: () => void) => void; +const invoke = (fn: Function) => fn(); +const INITIAL_WATCHER_VALUE = {}; + +export interface WatchOptionsBase { + /** + * @depreacted ignored in `@vue-reactivity/watch` and will always be `sync` + */ + flush?: 'sync' | 'pre' | 'post'; + onTrack?: ReactiveEffectOptions['onTrack']; + onTrigger?: ReactiveEffectOptions['onTrigger']; +} + +export interface WatchOptions extends WatchOptionsBase { + immediate?: Immediate; + deep?: boolean; +} + +// Simple effect. +export function watchEffect( + effect: WatchEffect, + options?: WatchOptionsBase +): WatchStopHandle { + return doWatch(effect, null, options); +} + +// 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 | object>>, + Immediate extends Readonly = false +>( + sources: T, + 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 { + return doWatch(source, cb, options); +} + +function doWatch( + source: WatchSource | WatchSource[] | WatchEffect, + cb: WatchCallback | null, + { immediate, deep, onTrack, onTrigger }: WatchOptions = {} +): WatchStopHandle { + let getter: () => any; + if (isArray(source) && !isReactive(source)) { + getter = () => + // eslint-disable-next-line array-callback-return + source.map(s => { + if (isRef(s)) return s.value; + else if (isReactive(s)) return traverse(s); + else if (isFunction(s)) return callWithErrorHandling(s, 'watch getter'); + else warn('invalid source'); + }); + } else if (isRef(source)) { + getter = () => source.value; + } else if (isReactive(source)) { + getter = () => source; + deep = true; + } else if (isFunction(source)) { + if (cb) { + // getter with cb + getter = () => callWithErrorHandling(source, 'watch getter'); + } else { + // no cb -> simple effect + getter = () => { + if (cleanup) cleanup(); + + return callWithErrorHandling(source, 'watch callback', [onInvalidate]); + }; + } + } else { + getter = NOOP; + } + + if (cb && deep) { + const baseGetter = getter; + getter = () => traverse(baseGetter()); + } + + let cleanup: () => void; + const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => { + cleanup = (runner as any).options.onStop = () => { + callWithErrorHandling(fn, 'watch cleanup'); + }; + }; + + let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE; + const applyCb = cb + ? () => { + const newValue = runner(); + if (deep || hasChanged(newValue, oldValue)) { + // cleanup before running cb again + if (cleanup) cleanup(); + + callWithAsyncErrorHandling(cb, 'watch callback', [ + newValue, + // pass undefined as the old value when it's changed for the first time + oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, + onInvalidate, + ]); + oldValue = newValue; + } + } + : undefined; + + const scheduler = invoke; + + const runner = effect(getter, { + lazy: true, + onTrack, + onTrigger, + scheduler: applyCb ? () => scheduler(applyCb) : scheduler, + }); + + // initial run + if (applyCb) { + if (immediate) applyCb(); + else oldValue = runner(); + } else { + runner(); + } + + const stopWatcher = function () { + stop(runner); + }; + stopWatcher.effect = runner; + return stopWatcher; +} + +function traverse(value: unknown, seen: Set = new Set()) { + if (!isObject(value) || seen.has(value)) return value; + + seen.add(value); + if (isArray(value)) { + for (let i = 0; i < value.length; i++) traverse(value[i], seen); + } else if (value instanceof Map) { + value.forEach((_, key) => { + // to register mutation dep for existing keys + traverse(value.get(key), seen); + }); + } else if (value instanceof Set) { + value.forEach(v => { + traverse(v, seen); + }); + } else { + for (const key of Object.keys(value)) traverse(value[key], seen); + } + return value; +} diff --git a/yarn.lock b/yarn.lock index 7b6041bf9..dced5762b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3168,11 +3168,6 @@ react-refresh "^0.10.0" resolve "^1.20.0" -"@vue-reactivity/watch@^0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@vue-reactivity/watch/-/watch-0.1.6.tgz#631c91319dee62724eb020e89dc2f868b480cb6d" - integrity sha512-Se4D+1LAnn8B49MuRzWyZv2syb+9FTuxXKA8FF6gjZD08PdVwoVZG3ncZzqFFG2lVlzx03+rjOCtIJb+VkD3Qg== - "@vue/reactivity@^3.1.5": version "3.2.16" resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.16.tgz#0d4253443d580c906508b0b05b2cd136d46bf4a2" @@ -3185,6 +3180,11 @@ resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.16.tgz#a7f5e37e07ac68d4b7ea8ebeba515b46d205c524" integrity sha512-zpv8lxuatl3ruCJCsGzrO/F4+IlLug4jbu3vaIi/wJVZKQgnsW1R/xSRJMQS6K57cl4fT/2zkrYsWh1/6H7Esw== +"@vue/shared@^3.2.20": + version "3.2.20" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.20.tgz#53746961f731a8ea666e3316271e944238dc31db" + integrity sha512-FbpX+hD5BvXCQerEYO7jtAGHlhAkhTQ4KIV73kmLWNlawWhTiVuQxizgVb0BOkX5oG9cIRZ42EG++d/k/Efp0w== + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"