Skip to content

Commit

Permalink
fork @vue-reactivity/watch to fix bundler issue
Browse files Browse the repository at this point in the history
@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. facebook/create-react-app#10356
2. formatjs/formatjs#1395 (comment)
  • Loading branch information
Yuyz0112 committed Oct 27, 2021
1 parent aa6a643 commit fd86d9c
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 23 deletions.
6 changes: 2 additions & 4 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
"plugin:@typescript-eslint/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/components/chakra-ui/Form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/components/core/GridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/services/DebugComponents.tsx
Original file line number Diff line number Diff line change
@@ -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 }> = ({
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/services/ImplWrapper.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
14 changes: 7 additions & 7 deletions packages/runtime/src/services/stateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) });
});
}

Expand Down
266 changes: 266 additions & 0 deletions packages/runtime/src/utils/watchReactivity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit fd86d9c

Please sign in to comment.