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
This commit is contained in:
Yanzhen Yu 2021-10-27 14:11:02 +08:00
parent aa6a643bf3
commit fd86d9cc0e
11 changed files with 288 additions and 23 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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';

View File

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

View File

@ -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<Static<typeof PropsSchema>> = ({
slotsMap,

View File

@ -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 }> = ({

View File

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

View File

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

View File

@ -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<T = any> = Ref<T> | ComputedRef<T> | (() => T);
export type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onInvalidate: InvalidateCbRegistrator
) => any;
export type WatchStopHandle = () => void;
type MapSources<T> = {
[K in keyof T]: T[K] extends WatchSource<infer V>
? V
: T[K] extends object
? T[K]
: never;
};
type MapOldSources<T, Immediate> = {
[K in keyof T]: T[K] extends WatchSource<infer V>
? 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<Immediate = boolean> 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<Array<WatchSource<unknown> | object>>,
Immediate extends Readonly<boolean> = false
>(
sources: T,
cb: WatchCallback<MapSources<T>, MapOldSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle;
// overload #2: single source + cb
export function watch<T, Immediate extends Readonly<boolean> = false>(
source: WatchSource<T>,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle;
// overload #3: watching reactive object w/ cb
export function watch<T extends object, Immediate extends Readonly<boolean> = false>(
source: T,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle;
// implementation
export function watch<T = any>(
source: WatchSource<T> | WatchSource<T>[],
cb: WatchCallback<T>,
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<unknown> = 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;
}

View File

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