diff --git a/.eslintrc.js b/.eslintrc.js index 7d394d8d..c3ba8c3e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,7 +2,8 @@ module.exports = { globals: {}, parser: '@typescript-eslint/parser', parserOptions: { - project: './tsconfig.json', + project: ['./tsconfig.base.json', './tsconfig.tsnode.json'], + files: ['src/**/*', 'config/**/*'], extraFileExtensions: ['.md', '.mdx'], }, root: true, diff --git a/.storybook/main.js b/.storybook/main.js index 2e80655f..c9bc5c82 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,23 +1,6 @@ -module.exports = { - stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], - addons: [ - '@storybook/addon-links', - '@storybook/addon-essentials', - '@storybook/addon-interactions', - '@storybook/addon-jest', - '@storybook/addon-a11y', // TODO: vite fix https://github.com/storybookjs/storybook/pull/17997 - 'storybook-addon-designs', - ], - staticDirs: ['../public'], - framework: '@storybook/react', - core: { - builder: '@storybook/builder-vite', - }, - async viteFinal(config, { configType }) { - console.log('env', process.env.BASE_URL, process.env.STORYBOOK_FIGMA_ACCESS_TOKEN) - config.base = process.env.BASE_URL || config.base - - // return the customized config - return config - }, -} +const register = require('ts-node-register') +register({ + target: 'node16', + project: 'tsconfig.tsnode.json', +}) +module.exports = require('./main.ts') diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 00000000..e2d02cd9 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,29 @@ +import { StorybookConfig, Options } from '@storybook/core-common' +import { UserConfig } from 'vite' +interface ExtendedConfig extends StorybookConfig { + viteFinal?: (config: UserConfig, options: Options) => Promise +} +const config: ExtendedConfig = { + stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-interactions', + '@storybook/addon-jest', + '@storybook/addon-a11y', // TODO: vite fix https://github.com/storybookjs/storybook/pull/17997 + 'storybook-addon-designs', + ], + framework: '@storybook/react', + core: { + builder: '@storybook/builder-vite', + }, + async viteFinal(config) { + console.log('env', process.env.BASE_URL, process.env.STORYBOOK_FIGMA_ACCESS_TOKEN) + config.base = process.env.BASE_URL || config.base + + // return the customized config + return config + }, +} + +export default config diff --git a/.storybook/preview.js b/.storybook/preview.ts similarity index 76% rename from .storybook/preview.js rename to .storybook/preview.ts index 1b194fa9..44b0d98b 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.ts @@ -1,8 +1,9 @@ import { withDesign } from 'storybook-addon-designs' import { withThemeProvider } from '../src/helpers' -import AVAILABLE_THEMES from '../src/lib/shared/themes' +import AvailableThemes from '../src/lib/shared/themes' +import { Parameters } from '@storybook/addons' import order from './order.json' -export const parameters = { +export const parameters: Parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { @@ -21,9 +22,9 @@ export const globalTypes = { theme: { name: 'Theme', description: 'Switch theme for preview', - defaultValue: AVAILABLE_THEMES[0].key, + defaultValue: AvailableThemes[0].key, toolbar: { - items: AVAILABLE_THEMES.map((theme) => { + items: AvailableThemes.map((theme) => { return { value: theme.key, title: theme.title, diff --git a/figma/lib/helpers.ts b/figma/lib/helpers.ts index fbf63ccb..bcb26b34 100644 --- a/figma/lib/helpers.ts +++ b/figma/lib/helpers.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ +// eslint-disable @typescript-eslint/no-explicit-any import { Node } from 'figma-api' export function rgbToHex(r: number, g: number, b: number) { const color = '#' + ((1 << 24) + ((r * 255) << 16) + ((g * 255) << 8) + b * 255).toString(16).slice(1) diff --git a/package.json b/package.json index 9004559b..57d5c0de 100644 --- a/package.json +++ b/package.json @@ -36,14 +36,16 @@ "prepare": "husky install" }, "dependencies": { - "react": "^17.0.2", - "react-dom": "^17.0.2" + "classnames": "^2.3.1" }, "peerDependencies": { "react": "^17.0.2", - "react-dom": "^17.0.2" + "rxjs": "^7.5.5", + "react-dom": "^17.0.2", + "remotedev": "^0.2.9" }, "devDependencies": { + "remotedev": "^0.2.9", "@babel/core": "^7.17.9", "@storybook/addon-a11y": "^6.4.22", "@storybook/addon-actions": "^6.4.21", @@ -58,6 +60,7 @@ "@swc/jest": "^0.2.21", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.5", + "@testing-library/react-hooks": "^8.0.0", "@types/jest": "^27.5.1", "@types/prettier": "^2.6.1", "@types/react": "^17.0.10", @@ -88,6 +91,9 @@ "lint-staged": ">=10", "postcss-apply": "^0.12.0", "postcss-nesting": "^10.1.7", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "rxjs": "^7.5.5", "storybook-addon-designs": "^6.2.1", "style-dictionary": "^3.7.0", "stylelint": "^14.8.1", @@ -97,6 +103,7 @@ "through2": "^4.0.2", "ts-jest": "^28.0.2", "ts-node": "^10.7.0", + "ts-node-register": "^1.0.0", "typescript": "^4.5.4", "vite": "^2.9.0", "vite-plugin-dts": "^1.0.5", diff --git a/src/common.d.ts b/src/common.d.ts index d33567f9..c5ed5237 100644 --- a/src/common.d.ts +++ b/src/common.d.ts @@ -1,19 +1,19 @@ -declare module '*.svg' { - const content: any - export default content -} -declare module '*.jpg' { - const content: any - export default content -} -declare module '*.png' { - const content: any - export default content -} -declare module '*.css' { - interface IClassNames { - [className: string]: string - } - const classNames: IClassNames - export = classNames -} +// declare module '*.svg' { +// const content: any +// export default content +// } +// declare module '*.jpg' { +// const content: any +// export default content +// } +// declare module '*.png' { +// const content: any +// export default content +// } +// declare module '*.css' { +// interface IClassNames { +// [className: string]: string +// } +// const classNames: IClassNames +// export = classNames +// } diff --git a/src/helpers/theme_provider.tsx b/src/helpers/theme_provider.tsx index af5ed149..40383d12 100644 --- a/src/helpers/theme_provider.tsx +++ b/src/helpers/theme_provider.tsx @@ -1,6 +1,7 @@ import { ReactFramework, StoryContext } from '@storybook/react' import React, { ReactNode, useState } from 'react' import '../../build/css/globals.css' +import tokens from '../../config/tokens.json' import AVAILABLE_THEMES from '../lib/shared/themes' const ThemeKeys = AVAILABLE_THEMES.map((theme) => theme.key) type Theme = typeof ThemeKeys[number] @@ -9,20 +10,28 @@ type ThemeProviderProps = { theme: Theme children: ReactNode } +type Tokens = typeof tokens +export type ThemeProviderContext = { + theme?: Theme + tokens?: { + global: Tokens['global'] + theme: Tokens[Exclude] + } +} -export const ThemeContext = React.createContext('org') +export const ThemeContext = React.createContext({}) -export const ThemeProvider = (props: ThemeProviderProps) => { +export const ThemeProvider = ({ theme, children }: ThemeProviderProps) => { const [themeVars, setThemeVars] = useState('') - import(`../../build/css/themes/${props.theme}.css`).then((styles) => { + import(`../../build/css/themes/${theme}.css`).then((styles) => { setThemeVars(styles.default) }) return ( - + - {props.children} + {children} ) } diff --git a/src/lib/shared/form/actions.ts b/src/lib/shared/form/actions.ts new file mode 100644 index 00000000..dba4e952 --- /dev/null +++ b/src/lib/shared/form/actions.ts @@ -0,0 +1,97 @@ +import { HTMLInputTypeAttribute } from 'react' +import { catchError, debounceTime, delay, filter, from, mergeMap, Observable, of, switchMap } from 'rxjs' +import { createActionCreator, isActionOf } from '../store' +import { ActionType, Effect } from '../store/types' +import { Values, UseFormProps, FormState } from './types' + +const ActionTypes = { + Submit: 'Form/Submit', + SubmitSuccess: 'Form/SubmitSuccess', + SubmitError: 'Form/SubmitError', + Validate: 'Form/Validate', + ValidateSuccess: 'Form/ValidateSuccess', + ValidateError: 'Form/ValidateError', + Reset: 'Form/Reset', + SetValue: 'Form/SetValue', + SetValues: 'Form/SetValues', + SetTouched: 'Form/SetTouched', + RegisterField: 'Form/RegisterField', + UnregisterField: 'Form/UnregisterField', +} as const + +export class ActionFactory { + Submit = createActionCreator(ActionTypes.Submit)() + SubmitSuccess = createActionCreator(ActionTypes.SubmitSuccess)() + + SubmitError = createActionCreator(ActionTypes.SubmitError)() + + Validate = createActionCreator(ActionTypes.Validate)() + + ValidateSuccess = createActionCreator(ActionTypes.ValidateSuccess)<{ [key in keyof T]?: string }>() + + ValidateError = createActionCreator(ActionTypes.ValidateError)() + + Reset = createActionCreator(ActionTypes.Reset)() + + SetValue = createActionCreator(ActionTypes.SetValue)<{ key: keyof T; value: T[keyof T]; internal?: boolean }>() + + SetValues = createActionCreator(ActionTypes.SetValues)>() + + SetTouched = createActionCreator(ActionTypes.SetTouched)<{ key: keyof T; touched: boolean }>() + + RegisterField = createActionCreator(ActionTypes.RegisterField)<{ + key: keyof T + removeValueOnUnmount?: boolean + type?: HTMLInputTypeAttribute + }>() + UnregisterField = createActionCreator(ActionTypes.UnregisterField)<{ key: keyof T; type?: HTMLInputTypeAttribute }>() +} + +export type FormActions = ActionType[keyof ActionFactory]> +export type ActionDispatch = (value: FormActions) => void + +export type GetFormEffectsProps = Omit, 'onValidate'> & { + onValidate: (values: T) => Observable<{ [key in keyof T]?: string | undefined }> +} +export const getFormEffects = ( + actions: ActionFactory, + propsRef: React.RefObject> +): Array, FormActions>> => [ + (action$) => + action$.pipe( + filter(() => typeof propsRef.current?.onSubmit === 'function'), + isActionOf(actions.Submit), + switchMap(([_, __, { values }]) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return from(propsRef.current!.onSubmit!(values)).pipe( + mergeMap((values) => of(actions.SubmitSuccess(values))), + catchError((err) => of(actions.SubmitError(err))) + ) + }) + ), + (action$) => + action$.pipe( + isActionOf(actions.Validate), + switchMap(([_, __, { values }]) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return from(propsRef.current!.onValidate!(values)).pipe( + mergeMap((values) => of(actions.ValidateSuccess(values))), + catchError((err) => of(actions.ValidateError(err))) + ) + }) + ), + (action$) => + action$.pipe( + isActionOf(actions.SetValue), + debounceTime(33), + delay(33), + switchMap((_) => of(actions.Validate(undefined))) + ), + (action$) => + action$.pipe( + isActionOf(actions.RegisterField), + debounceTime(33), + delay(33), + switchMap((_) => of(actions.Validate(undefined))) + ), +] diff --git a/src/lib/shared/form/form.stories.tsx b/src/lib/shared/form/form.stories.tsx new file mode 100644 index 00000000..6e883168 --- /dev/null +++ b/src/lib/shared/form/form.stories.tsx @@ -0,0 +1,108 @@ +import React, { useCallback } from 'react' +import { ComponentStory, ComponentMeta } from '@storybook/react' +import { FormProvider, useFieldProps, useForm } from './form' +import { Values, FieldValidatorFn, KeysMatching } from './types' +type MyFormValues = { foo: string; bar: Date; baz: boolean; faz: string } +const MyForm = (_: {}) => { + const onSubmit = useCallback((values: MyFormValues) => { + return new Promise((resolve) => { + setTimeout(() => resolve(values), 1000) + }) + }, []) + const form = useForm({ onSubmit, initialValues: { foo: '' } }) + + return ( + +
+ name="foo" /> + name="bar" /> + name="baz" /> + name="faz" /> + +

+ {JSON.stringify(form.fieldErrors)} +

+ +
+ ) +} + +const MyField = ({ name }: { name: Extract }) => { + const validate: FieldValidatorFn = useCallback( + (value, _) => + new Promise((resolve) => + setTimeout( + () => resolve(!value ? 'Required' : (value as unknown as string).length < 3 ? 'Too short' : undefined), + 300 + ) + ), + [] + ) + const props = useFieldProps({ name, validate, type: 'text' }) + return +} +const old = new Date().getTime() + +const MyDateField = ({ name }: { name: KeysMatching }) => { + const validate: FieldValidatorFn = useCallback( + (value, _) => + new Promise((resolve) => setTimeout(() => resolve((value?.getTime() ?? 0) < old ? 'Invalid' : undefined), 300)), + [] + ) + const props = useFieldProps({ + name, + validate, + type: 'date', + parse: (v) => (v ? new Date(v) : undefined), + format: (v) => v?.toISOString().split('T')[0] ?? '', + }) + return +} + +const MyCheckboxField = ({ name }: { name: KeysMatching }) => { + const validate: FieldValidatorFn = useCallback( + (value, _) => new Promise((resolve) => setTimeout(() => resolve(!value ? 'Required' : undefined), 300)), + [] + ) + const props = useFieldProps({ + name, + validate, + type: 'checkbox', + }) + + return +} + +const MySelectField = ({ name }: { name: KeysMatching }) => { + const validate: FieldValidatorFn = useCallback( + (value, _) => new Promise((resolve) => setTimeout(() => resolve(!value ? 'Required' : undefined), 300)), + [] + ) + const props = useFieldProps({ + name, + validate, + }) + return ( + + ) +} + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default { + title: 'Lib/Form', + component: MyForm, + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + // argTypes: {}, +} as ComponentMeta + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +const Template: ComponentStory = (args) => + +export const Default = Template.bind({}) +// More on args: https://storybook.js.org/docs/react/writing-stories/args +Default.args = {} diff --git a/src/lib/shared/form/form.test.tsx b/src/lib/shared/form/form.test.tsx new file mode 100644 index 00000000..efd25611 --- /dev/null +++ b/src/lib/shared/form/form.test.tsx @@ -0,0 +1,133 @@ +/** + * @jest-environment jsdom + */ +import React from 'react' +import { renderHook } from '@testing-library/react-hooks' +import '@testing-library/jest-dom' +import '@testing-library/jest-dom/extend-expect' +import { promisify } from './utils' +import { useValidator } from './form' +import { firstValueFrom } from 'rxjs' +import FormReducer, { getInitialState } from './reducer' +import { ActionFactory } from './actions' + +describe('Form', () => { + describe('Utils', () => { + describe('promisify', () => { + it('should wrap a non-promise value in a promise', () => { + const value = 'value' + const res = promisify(value) + expect(res).toBeInstanceOf(Promise) + }) + it('should return a promise value as is', () => { + const value = Promise.resolve('value') + const res = promisify(value) + expect(res).toBe(value) + }) + }) + }) + describe('useValidator', () => { + it('should return an empty validator if no global validator is provided', async () => { + const { result } = renderHook(() => useValidator({}, {})) + const res = await firstValueFrom(result.current({ foo: 'value' })) + expect(res).toEqual({}) + }) + + it('should return an empty validator, if global validator is empty', async () => { + const { result } = renderHook(() => useValidator({}, {}, (_) => ({}))) + const res = await firstValueFrom(result.current({ foo: 'value' })) + expect(res).toEqual({}) + }) + + it('should return a valid validator out of global one provided (sync)', async () => { + const { result } = renderHook(() => useValidator({}, {}, (_) => ({ foo: 'Error' }))) + const res = await firstValueFrom(result.current({ foo: 'value' })) + expect(res.foo).toBe('Error') + }) + + it('should return a valid validator out of global one provided (async)', async () => { + const { result } = renderHook(() => + useValidator({}, {}, (_) => new Promise((resolve) => setTimeout(() => resolve({ foo: 'Error' }), 33))) + ) + const res = await firstValueFrom(result.current({ foo: 'value' })) + expect(res.foo).toBe('Error') + }) + + it('should return a valid merged validator out of the global one and enabled field validators provided (async)', async () => { + const values = { foo: 'value', baz: 2, bar: new Date('2019-09-09') } + const { result } = renderHook(() => + useValidator( + { + foo: (value) => Promise.resolve(value.length > 4 ? 'Too long' : undefined), + baz: (value) => Promise.resolve(value < 3 ? 'Too small' : undefined), + bar: (_) => 'Error1', + }, + { + foo: true, + baz: true, + }, + (_) => new Promise((resolve) => setTimeout(() => resolve({ foo: 'Error', bar: 'Error2' }), 33)) + ) + ) + const res = await firstValueFrom(result.current(values)) + expect(res.foo).toBe('Too long') + expect(res.bar).toBe('Error2') + expect(res.baz).toBe('Too small') + }) + }) + describe('Reducer', () => { + describe('Register Field', () => { + it('should enable fields that are registered', () => { + const dispatch = new ActionFactory<{ foo: string }>() + let next = FormReducer<{ foo: string }>(getInitialState(), dispatch.RegisterField({ key: 'foo' })) + expect(next.mounted.foo).toBeTruthy() + next = FormReducer<{ foo: string }>( + getInitialState({ values: { foo: 'bar' } }), + dispatch.RegisterField({ key: 'foo', removeValueOnUnmount: true }) + ) + expect(next.mounted.foo).toBeTruthy() + expect(next.removeValueOnUnmount.foo).toBeTruthy() + }) + + it('should initialise field value if it does not exist', () => { + const dispatch = new ActionFactory<{ foo: string }>() + const next = FormReducer<{ foo: string }>(getInitialState(), dispatch.RegisterField({ key: 'foo' })) + expect(next.values.foo).toBe(null) + }) + + it('should not reset value if it already exists ', () => { + const dispatch = new ActionFactory<{ foo: string }>() + const next = FormReducer<{ foo: string }>( + getInitialState({ values: { foo: '' } }), + dispatch.RegisterField({ key: 'foo' }) + ) + expect(next.values.foo).toBe('') + }) + + describe('Unregister field', () => { + it('should remove field value if it was mounted (aka registered) and the removeValueOnUnmount flag is set', () => { + const dispatch = new ActionFactory<{ foo: string }>() + let next = FormReducer<{ foo: string }>( + getInitialState({ values: { foo: '' }, mounted: { foo: true } }), + dispatch.UnregisterField({ key: 'foo' }) + ) + expect(next.values.foo).toBe('') + expect(next.removeValueOnUnmount.foo).toBe(undefined) + next = FormReducer<{ foo: string }>( + getInitialState({ values: { foo: '' }, mounted: { foo: false } }), + dispatch.UnregisterField({ key: 'foo' }) + ) + expect(next.values.foo).toBe('') + expect(next.removeValueOnUnmount.foo).toBe(undefined) + + next = FormReducer<{ foo: string }>( + getInitialState({ values: { foo: '' }, mounted: { foo: true }, removeValueOnUnmount: { foo: true } }), + dispatch.UnregisterField({ key: 'foo' }) + ) + expect(next.values.foo).toBe(undefined) + expect(next.removeValueOnUnmount.foo).toBe(undefined) + }) + }) + }) + }) +}) diff --git a/src/lib/shared/form/form.tsx b/src/lib/shared/form/form.tsx new file mode 100644 index 00000000..1a9b7ee5 --- /dev/null +++ b/src/lib/shared/form/form.tsx @@ -0,0 +1,266 @@ +import React, { + HTMLInputTypeAttribute, + MutableRefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import { from, map, forkJoin, Observable } from 'rxjs' +import { ActionDispatch, ActionFactory, FormActions, getFormEffects } from './actions' +import reducer, { getInitialState } from './reducer' +import Store from '../store' +import { + Values, + UseFormProps, + UseFormReturn, + FieldValidatorFn, + FormState, + RegisterFn, + UseFieldProps, + GlobalValidatorFn, + Errors, + UnpackRef, + RegisterFnOptions, + NullableTransformFn, + SetValidatorFn, +} from './types' +import { promisify } from './utils' +type ObservableValidatorFn = (values: T) => Observable> + +function mergeErrors>(source: T, target: T): T { + const res = { ...target } + Object.entries(source).forEach(([key, error]) => { + const k = key as keyof T + if (res[k] !== undefined && res[k] !== null) return + res[k] = error as T[keyof T] + }) + return res +} + +export function useValidator( + fieldValidators: { [K in keyof T]?: FieldValidatorFn }, + validatorEnabled: { [key in keyof T]?: boolean }, + global: GlobalValidatorFn | undefined = (_) => ({}) +): ObservableValidatorFn { + const validators: Array> = useMemo(() => { + const globalised = Object.entries(fieldValidators) + .filter(([key]) => validatorEnabled[key]) + .map(([key, fn]) => (values: T) => { + return from(promisify(fn?.(values[key] as T[keyof T], values))).pipe( + map((res) => ({ [key as keyof T]: res } as Errors)) + ) + }) + globalised.push((values: T) => from(promisify(global(values) ?? {})) as Observable>) + + return globalised + }, [fieldValidators, validatorEnabled, global]) + const res: ObservableValidatorFn = useCallback( + (values) => + forkJoin(validators.map((fn) => fn(values))).pipe( + map((errors) => errors.reduce((agg, errors) => mergeErrors(errors, agg), {} as Errors)) + ), + [validators] + ) + return res +} + +function useRegisterFn( + setValidator: SetValidatorFn, + dispatch: ActionDispatch, + actions: ActionFactory +) { + const onInstanceChange = useCallback( + (key: keyof T, type: HTMLInputTypeAttribute | undefined, instance: Element | null | undefined) => { + setTimeout(() => { + if (!instance) return dispatch(actions.UnregisterField({ key, type })) + dispatch(actions.RegisterField({ key, type })) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, 0) + }, + [actions, dispatch] + ) + + const register: RegisterFn = useCallback( + ({ + name: key, + validate, + parse, + type, + }: RegisterFnOptions) => { + setTimeout(() => setValidator(key, validate as FieldValidatorFn), 0) + return { + ref: onInstanceChange.bind(undefined, key, type), + onChange: (evt: React.ChangeEvent) => { + const value = + type !== 'checkbox' + ? (parse.current ?? (String as unknown as NullableTransformFn))( + (evt.target as HTMLInputElement).value as unknown as Source + ) + : ((evt.target as HTMLInputElement).checked as unknown as V) + dispatch( + actions.SetValue({ + key, + value: value as T[K], + }) + ) + }, + onBlur: () => dispatch(actions.SetTouched({ key, touched: true })), + name: key as string, + type, + } + }, + [actions, onInstanceChange, setValidator, dispatch] + ) + return register +} + +function useMappedStoreDispatch(dispatch: ActionDispatch, actionFactory: ActionFactory) { + const mapped = useMemo>( + () => + ({ + submit: (evt) => { + evt?.preventDefault() + dispatch(actionFactory.Submit(undefined)) + }, + validate: () => dispatch(actionFactory.Validate(undefined)), + setTouched: (key, touched) => dispatch(actionFactory.SetTouched({ key, touched })), + setValue: (key, value) => dispatch(actionFactory.SetValue({ key, value: value as T[keyof T] })), + setValues: (values) => dispatch(actionFactory.SetValues(values)), + reset: () => dispatch(actionFactory.Reset(undefined)), + } as UseFormReturn), + [actionFactory, dispatch] + ) + return mapped +} + +export function useForm(props: UseFormProps): UseFormReturn { + const [validators, setValidator_] = useState<{ [K in keyof T]?: FieldValidatorFn }>({}) + const setValidator: SetValidatorFn = useCallback((key, validate) => { + setValidator_((old) => { + if (old[key] === validate) return old + return { ...old, [key]: validate } + }) + }, []) + + const { onValidate: onValidate_, ...props_ } = props + + const initialFormValueRef = useRef(getInitialState({ values: { ...((props.initialValues ?? {}) as T) } })) + const actionFactoryRef = useRef(new ActionFactory()) + + const [state, setState] = useState(initialFormValueRef.current) + const onValidate = useValidator(validators, state.mounted, props.onValidate) + const propsRef = useRef({ ...props_, onValidate }) + const effects = useMemo(() => getFormEffects(actionFactoryRef.current, propsRef), []) + + const storeRef = useRef( + new Store, FormActions>(initialFormValueRef.current, reducer, effects, 'Form') + ) + + useEffect(() => { + propsRef.current = { ...props_, onValidate } + }, [onValidate, props_]) + useEffect(() => { + const sub = storeRef.current.subscribe({ + next: setState, + error: console.error, + }) + return sub.unsubscribe + }, []) + + const register = useRegisterFn(setValidator, storeRef.current.next, actionFactoryRef.current) + const dispatch = useMappedStoreDispatch(storeRef.current.next, actionFactoryRef.current) + const res = useMemo>( + () => ({ + ...state, + ...dispatch, + register, + }), + [register, state, dispatch] + ) + return res +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const FormContext = React.createContext | null>(null) + +export function FormProvider(props: UseFormReturn & { children?: React.ReactNode }) { + const { children, ...data } = props + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return }>{props.children} +} + +export function useFormContext(): UseFormReturn { + return React.useContext(FormContext) as unknown as UseFormReturn +} + +export function useParseRef( + fromString: UnpackRef['parse']> +) { + const ref = useRef['parse']>>(fromString) + useEffect(() => { + ref.current = fromString + }, [fromString]) + return ref +} + +function useFormatRef( + asString: UnpackRef['format']> +) { + const ref = useRef['format']>>(asString) + useEffect(() => { + ref.current = asString + }, [asString]) + return ref +} + +function useFieldValue( + { name, type }: Pick, 'type' | 'name'>, + formatRef: MutableRefObject | undefined> +) { + const { values } = useFormContext() + const value = useMemo(() => { + const value = values[name] + const defaultCtor = (type === 'checkbox' ? Boolean : String) as unknown as NullableTransformFn + + return (formatRef.current ?? defaultCtor)((value ?? undefined) as V) + }, [values, name, type, formatRef]) + return value +} + +export function useFieldError({ name }: Pick, 'name'>) { + const { fieldErrors: errors } = useFormContext() + const error = useMemo(() => errors[name], [errors, name]) + return error +} + +export function useFieldProps({ + validate, + parse, + format, + type, + name, +}: UseFieldProps) { + const parseRef = useParseRef(parse) + const formatRef = useFormatRef(format) + const { register } = useFormContext() + // const setValue = useCallback((value: V) => setValue_.bind(undefined, name), [setValue_, name]) + const fieldProps_ = useMemo( + () => register({ name, validate, type, parse: parseRef }), + [register, name, validate, type, parseRef] + ) + const error = useFieldError({ name }) + const value = useFieldValue({ name, type }, formatRef) + + const fieldProps = useMemo( + () => ({ + ...fieldProps_, + ...(type === 'checkbox' ? { checked: !!value as boolean } : { value }), + type, + ...(error ? { ['data-error']: error, ['data-invalid']: !!error } : {}), + }), + [fieldProps_, value, type, error] + ) + return fieldProps +} diff --git a/src/lib/shared/form/index.ts b/src/lib/shared/form/index.ts new file mode 100644 index 00000000..22dfd096 --- /dev/null +++ b/src/lib/shared/form/index.ts @@ -0,0 +1,3 @@ +export * from './actions' +export * from './form' +export * from './types' diff --git a/src/lib/shared/form/reducer.ts b/src/lib/shared/form/reducer.ts new file mode 100644 index 00000000..ec8c2a6d --- /dev/null +++ b/src/lib/shared/form/reducer.ts @@ -0,0 +1,135 @@ +import { ActionType } from '../store/types' +import { ActionFactory, FormActions } from './actions' +import { Values, FormState } from './types' + +const FormReducer = (state: FormState, action: FormActions): FormState => { + switch (action.type) { + case 'Form/Submit': + return { ...state, isSubmitting: true, error: undefined } + case 'Form/SubmitError': + return { + ...state, + isSubmitting: false, + error: action.payload, + } + case 'Form/SubmitSuccess': + case 'Form/SetValues': + return { + ...state, + isSubmitting: false, + error: undefined, + values: { ...state.values, ...action.payload }, + touched: {}, + isDirty: false, + } + case 'Form/SetValue': + return { + ...state, + values: { ...state.values, [action.payload.key]: action.payload.value }, + touched: { + ...state.touched, + [action.payload.key]: action.payload.internal && action.payload.value !== state.values[action.payload.key], + }, + isDirty: + (action.payload.internal && action.payload.value !== state.values[action.payload.key]) || state.isDirty, + } + case 'Form/Validate': + return { + ...state, + isValidating: true, + error: undefined, + } + case 'Form/ValidateSuccess': + return { + ...state, + isValidating: false, + error: undefined, + fieldErrors: action.payload, + isValid: !Object.values(action.payload).filter(Boolean).length, + } + case 'Form/ValidateError': + return { + ...state, + isValidating: false, + error: action.payload, + } + case 'Form/SetTouched': + return { + ...state, + touched: { + ...state.touched, + [action.payload.key]: action.payload.touched, + }, + isDirty: action.payload.touched || state.isDirty, + } + case 'Form/RegisterField': + return onRegisterField(state, action) + case 'Form/UnregisterField': { + return onUnregisterField(state, action) + } + default: + return state + } +} + +export const getInitialState = >( + initialiseWith: Partial> = {} +): FormState => + ({ + isValidating: false, + isSubmitting: false, + values: {}, + fieldErrors: {}, + error: undefined, + touched: {}, + mounted: {}, + removeValueOnUnmount: {}, + isDirty: false, + isValid: true, + fieldKeys: {}, + ...initialiseWith, + } as FormState) + +export default FormReducer + +function onUnregisterField( + state: FormState, + { payload: { key } }: ActionType['UnregisterField']> +) { + if (!state.mounted[key]) return state + let values = state.values + let mounted = state.mounted + let removeValueOnUnmount = state.removeValueOnUnmount + + if (state.removeValueOnUnmount[key]) { + values = { ...values } + mounted = { ...state.mounted } + removeValueOnUnmount = { ...state.removeValueOnUnmount } + delete values[key] + delete mounted[key] + delete removeValueOnUnmount[key] + } + return { + ...state, + values, + mounted, + removeValueOnUnmount, + } +} + +function onRegisterField( + state: FormState, + { payload: { key, removeValueOnUnmount: remove } }: ActionType['RegisterField']> +) { + let value = state.values[key] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (typeof value === 'undefined') value = null as any + const mounted = { ...state.mounted, [key]: true } + const removeValueOnUnmount = { + ...state.removeValueOnUnmount, + [key]: remove, + } + let values = state.values + values = value !== values[key] ? { ...values, [key]: value } : values + return { ...state, values, mounted, removeValueOnUnmount } +} diff --git a/src/lib/shared/form/types.ts b/src/lib/shared/form/types.ts new file mode 100644 index 00000000..a10d69fc --- /dev/null +++ b/src/lib/shared/form/types.ts @@ -0,0 +1,89 @@ +import React, { HTMLInputTypeAttribute } from 'react' + +export type Values = Record + +export type TransformFn = (input: V) => U + +export type NullableTransformFn = TransformFn +export type ValueToKeyTransform = NullableTransformFn ? R : V, string> + +export type UnpackRef = T extends React.MutableRefObject ? R : T +export type UnpackRefFields = { [K in keyof T]: UnpackRef } + +export type UnpackArr = T extends Array ? V : never + +export type KeysMatching = Extract< + NonNullable< + { + [K in keyof T]: [V] extends [T[K]] ? (T[K] extends V ? K : never) : never + }[keyof T] + >, + string +> + +export type FieldValidatorFn = ( + value: V, + values: T +) => (string | undefined) | Promise + +export type GlobalValidatorFn = (values: T) => Errors | undefined | Promise | undefined> +export type Errors = { [key in keyof T]?: string | undefined } +export interface FormDispatch { + validate(): void + setValue(key: K, value: V): void + setValues(values: Partial): void + setTouched(key: K, touched: boolean): void + submit(evt?: React.FormEvent): void + reset(): void + register: RegisterFn +} +export interface FormState { + values: T + isValid: boolean + touched: { [key in keyof T]?: boolean } + mounted: { [key in keyof T]?: boolean } + removeValueOnUnmount: { [key in keyof T]?: boolean } + fieldErrors: Errors + isDirty: boolean + isSubmitting: boolean + isValidating: boolean + error?: Error + className?: string +} + +export interface UseFormProps { + onValidate?: GlobalValidatorFn + onSubmit?: (values: T) => Promise + initialValues?: Partial +} +export interface RegisterFnOptions { + name: Extract + type?: HTMLInputTypeAttribute + parse: React.MutableRefObject | undefined> + validate?: FieldValidatorFn +} + +export interface UseFieldProps { + name: Extract + type?: Exclude + parse?: NullableTransformFn + format?: NullableTransformFn + validate?: FieldValidatorFn + removeValueOnUnmount?: boolean +} + +export type RegisterFn = ( + props: RegisterFnOptions +) => { + name: string + onChange: React.ChangeEventHandler + onBlur: React.FocusEventHandler + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ref: React.Ref +} + +// export type GetValueFn = (props: UseFieldProps) => string + +export type UseFormReturn = FormState & FormDispatch + +export type SetValidatorFn = (key: keyof T, validate?: FieldValidatorFn) => void diff --git a/src/lib/shared/form/utils.ts b/src/lib/shared/form/utils.ts new file mode 100644 index 00000000..460e2ce2 --- /dev/null +++ b/src/lib/shared/form/utils.ts @@ -0,0 +1,9 @@ +export function isPromise(value: T | Promise): value is Promise { + if (typeof (value as Promise).then === 'function') return true + return false +} + +export function promisify(value: T | Promise): Promise { + if (!isPromise(value)) return Promise.resolve(value) + return value +} diff --git a/src/lib/shared/store/index.ts b/src/lib/shared/store/index.ts new file mode 100644 index 00000000..d202bed4 --- /dev/null +++ b/src/lib/shared/store/index.ts @@ -0,0 +1,5 @@ +import Store from './store' +export * from './types' +export * from './utils' + +export default Store diff --git a/src/lib/shared/store/store.ts b/src/lib/shared/store/store.ts new file mode 100644 index 00000000..52a6e3da --- /dev/null +++ b/src/lib/shared/store/store.ts @@ -0,0 +1,81 @@ +import { + merge, + Observer, + scan, + Subject, + Subscribable, + Unsubscribable, + delay, + observeOn, + queueScheduler, + shareReplay, + Observable, +} from 'rxjs' +import { connectViaExtension, RemoteDev } from 'remotedev' + +import { Action, Effect, Reducer } from './types' + +class Store implements Subscribable, Observer { + protected complete$: Subject = new Subject() + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected error$: Subject = new Subject() + protected state$: Observable + protected _action$: Subject = new Subject() + + protected action$: Observable + protected remotedev: RemoteDev + + protected _sideEffect$: Subject<[A, S, S]> = new Subject() + pipe: typeof this.state$.pipe + constructor(initialState: S, reducer: Reducer, effects: Array>, label = 'Store') { + this.remotedev = connectViaExtension({ maxAge: 30, instanceId: `${label}_${Date.now()}` }) + this.action$ = merge( + this._action$, + merge(...effects.map((effect) => effect(this._sideEffect$))).pipe(delay(33)) + ) + + this.state$ = this.action$.pipe( + observeOn(queueScheduler), + scan((state, action) => { + const newState = reducer(state, action) + this._sideEffect$.next([action, state, newState]) + this.remotedev.send(action, newState) + return newState + }, initialState), + shareReplay(1) + ) + this.pipe = this.state$.pipe + + this._action$.next({ type: '@@init', payload: undefined } as A) + } + + subscribe = (observer: Partial>): Unsubscribable => { + const sub = this.state$.subscribe({ + ...observer, + complete: () => { + observer.complete?.() + this._sideEffect$.complete() + this._action$.complete() + }, + }) + return { + unsubscribe: () => { + sub.unsubscribe() + }, + } + } + next: (value: A) => void = (value) => { + this._action$.next(value) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error: (err: any) => void = (err) => { + this._action$.next(err) + } + complete: () => void = () => { + this._action$.complete() + this._sideEffect$.complete() + } +} + +export default Store diff --git a/src/lib/shared/store/types.ts b/src/lib/shared/store/types.ts new file mode 100644 index 00000000..9f543ae5 --- /dev/null +++ b/src/lib/shared/store/types.ts @@ -0,0 +1,9 @@ +import { Observable } from 'rxjs' +export type Action = { type: string; payload?: unknown } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ActionCreator = (payload?: any) => Action +export type Reducer = (state: S, action: A) => S +export type Effect = (input: Observable<[A, S, S]>) => Observable + +export type ActionType = ReturnType diff --git a/src/lib/shared/store/utils.ts b/src/lib/shared/store/utils.ts new file mode 100644 index 00000000..7137d98b --- /dev/null +++ b/src/lib/shared/store/utils.ts @@ -0,0 +1,17 @@ +import { filter, Observable } from 'rxjs' +import { Action, ActionCreator } from './types' + +export function isActionOf(creator: AC) { + const type = creator().type + return (source$: Observable<[A, S, S]>) => + source$.pipe(filter(isActionOfType(type))) as Observable<[ReturnType, S, S]> +} + +export function isActionOfType(type: T) { + return ([action]: [A, unknown, unknown]) => action.type === type +} + +export function createActionCreator(type: T) { + return

() => + (payload: P) => ({ type, payload }) +} diff --git a/src/remotedev.d.ts b/src/remotedev.d.ts new file mode 100644 index 00000000..9fa6f692 --- /dev/null +++ b/src/remotedev.d.ts @@ -0,0 +1,16 @@ +declare module 'remotedev' { + export interface RemoteDev { + init(state: any, actionType: string): void + send(actionType: any, state: any): void + subscribe(message: any): void + } + export interface RemoteDevConfig { + features?: object | boolean + name?: string + instanceId?: string + maxAge?: number + serialize?: boolean + } + export function connectViaExtension(options?: RemoteDevConfig): RemoteDev + export function extractState(message: any): any +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json index 15e2bf7e..3a0734d1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,4 @@ { - "extends": "./tsconfig.base", - "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.tsnode.json" }] + "extends": "./tsconfig.base.json", + "references": [{ "path": "./tsconfig.tsnode.json" }] } diff --git a/tsconfig.node.json b/tsconfig.node.json deleted file mode 100644 index 887d754b..00000000 --- a/tsconfig.node.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "module": "esnext", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "skipLibCheck": true, - "resolveJsonModule": true - }, - "include": ["vite.config.ts"] -} diff --git a/yarn.lock b/yarn.lock index 054db37c..fd6f9f85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1260,6 +1260,13 @@ dependencies: "@cspotcode/source-map-consumer" "0.8.0" +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@csstools/selector-specificity@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-1.0.0.tgz#91c560df2ed8d9700e4c7ed4ac21a3a322c9d975" @@ -1726,7 +1733,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== -"@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9": +"@jridgewell/trace-mapping@0.3.9", "@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== @@ -3247,6 +3254,14 @@ lodash "^4.17.15" redent "^3.0.0" +"@testing-library/react-hooks@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.0.tgz#7d0164bffce4647f506039de0a97f6fcbd20f4bf" + integrity sha512-uZqcgtcUUtw7Z9N32W13qQhVAD+Xki2hxbTR461MKax8T6Jr8nsUvZB+vcBTkzY2nFvsUet434CsgF0ncW2yFw== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-boundary "^3.1.0" + "@testing-library/react@^12.1.5": version "12.1.5" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" @@ -4514,6 +4529,11 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + async@^2.6.1, async@^2.6.2: version "2.6.4" resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" @@ -4773,6 +4793,11 @@ balanced-match@^2.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== +base-64@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" + integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== + base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -5531,6 +5556,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +classnames@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + clean-css@^4.2.3: version "4.2.4" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178" @@ -5676,6 +5706,11 @@ clone-stats@^1.0.0: resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= +clone@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb" + integrity sha512-h5FLmEMFHeuzqmpVRcDayNlVZ+k4uK1niyKQN6oUMe7ieJihv44Vc3dY/kDnnWX4PDQSwes48s965PG/D4GntQ== + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -5848,6 +5883,11 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= +component-emitter@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -10372,6 +10412,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsan@^3.1.3: + version "3.1.14" + resolved "https://registry.yarnpkg.com/jsan/-/jsan-3.1.14.tgz#197fee2d260b85acacb049c1ffa41bd09fb1f213" + integrity sha512-wStfgOJqMv4QKktuH273f5fyi3D3vy2pHOiSDGPvpcS/q+wb/M7AK3vkCcaHbkZxDOlDU/lDJgccygKSG2OhtA== + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -10645,6 +10690,11 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linked-list@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/linked-list/-/linked-list-0.1.0.tgz#798b0ff97d1b92a4fd08480f55aea4e9d49d37bf" + integrity sha512-Zr4ovrd0ODzF3ut2TWZMdHIxb8iFdJc/P3QM4iCJdlxxGHXo69c9hGIHzLo8/FtuR9E6WUZc5irKhtPUgOKMAg== + lint-staged@>=10: version "12.4.1" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.4.1.tgz#63fa27bfc8a33515f6902f63f6670864f1fb233c" @@ -13281,6 +13331,13 @@ react-element-to-jsx-string@^14.3.4: is-plain-object "5.0.0" react-is "17.0.2" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-fast-compare@^3.0.1, react-fast-compare@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" @@ -13725,6 +13782,16 @@ remark-stringify@^8.1.1: unherit "^1.0.4" xtend "^4.0.1" +remotedev@^0.2.9: + version "0.2.9" + resolved "https://registry.yarnpkg.com/remotedev/-/remotedev-0.2.9.tgz#a5f148704bc6bf6e46e93640cabb7b4ed3f7fc12" + integrity sha512-W8dHOv9BcFnetFEd08yNb5O9Hd+zkTFFnf9FRjNCkb4u+JgQ/U152Aw4q83AmY3m34d6KZwhK5ip/Qc331+4vA== + dependencies: + jsan "^3.1.3" + querystring "^0.2.0" + rn-host-detect "^1.0.1" + socketcluster-client "^13.0.0" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -13931,6 +13998,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +rn-host-detect@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/rn-host-detect/-/rn-host-detect-1.2.0.tgz#8b0396fc05631ec60c1cb8789e5070cdb04d0da0" + integrity sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A== + roarr@^2.15.3: version "2.15.4" resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd" @@ -14050,6 +14122,23 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" +sc-channel@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/sc-channel/-/sc-channel-1.2.0.tgz#d9209f3a91e3fa694c66b011ce55c4ad8c3087d9" + integrity sha512-M3gdq8PlKg0zWJSisWqAsMmTVxYRTpVRqw4CWAdKBgAfVKumFcTjoCV0hYu7lgUXccCtCD8Wk9VkkE+IXCxmZA== + dependencies: + component-emitter "1.2.1" + +sc-errors@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sc-errors/-/sc-errors-1.4.1.tgz#53e80030fe647e133d73b51eaa7d2b0f7591fd5b" + integrity sha512-dBn92iIonpChTxYLgKkIT/PCApvmYT6EPIbRvbQKTgY6tbEbIy8XVUv4pGyKwEK4nCmvX4TKXcN0iXC6tNW6rQ== + +sc-formatter@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sc-formatter/-/sc-formatter-3.0.3.tgz#caeb1e9bf3145dc77b7128b2a8abbb14bad3162e" + integrity sha512-lYI/lTs1u1c0geKElcj+bmEUfcP/HuKg2iDeTijPSjiTNFzN3Cf8Qh6tVd65oi7Qn+2/oD7LP4s6GC13v/9NiQ== + scheduler@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" @@ -14391,6 +14480,22 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socketcluster-client@^13.0.0: + version "13.0.1" + resolved "https://registry.yarnpkg.com/socketcluster-client/-/socketcluster-client-13.0.1.tgz#920e0a2437c228555e84f8ef0c9c5eb720e6067e" + integrity sha512-hxiE2xz6mgaBlhXbtBa4POgWVEvIcjCoHzf5LTUVhI9IL8V2ltV3Ze8pQsi9egqTjSz4RHPfyrJ7BiETe5Kthw== + dependencies: + base-64 "0.1.0" + clone "2.1.1" + component-emitter "1.2.1" + linked-list "0.1.0" + querystring "0.2.0" + sc-channel "^1.2.0" + sc-errors "^1.4.0" + sc-formatter "^3.0.1" + uuid "3.2.1" + ws "5.1.1" + socks-proxy-agent@^6.0.0, socks-proxy-agent@^6.1.1: version "6.2.0" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.0.tgz#f6b5229cc0cbd6f2f202d9695f09d871e951c85e" @@ -15490,6 +15595,32 @@ ts-morph@^14.0.0: "@ts-morph/common" "~0.13.0" code-block-writer "^11.0.0" +ts-node-register@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ts-node-register/-/ts-node-register-1.0.0.tgz#ea0c2ac787a323466a523ce4676f9423e858b286" + integrity sha512-0i5qFowh0t1phylWB0Y0l6+K2bv1He0ncYhM+jLSzOWGYM37qjDQJQV5yER63+97q7VFETC5mkPXZH+4JgZlng== + dependencies: + ts-node ">=0.9.0" + +ts-node@>=0.9.0: + version "10.8.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.0.tgz#3ceb5ac3e67ae8025c1950626aafbdecb55d82ce" + integrity sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + ts-node@^10.7.0: version "10.7.0" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" @@ -16013,6 +16144,11 @@ uuid-browser@^3.1.0: resolved "https://registry.yarnpkg.com/uuid-browser/-/uuid-browser-3.1.0.tgz#0f05a40aef74f9e5951e20efbf44b11871e56410" integrity sha1-DwWkCu90+eWVHiDvv0SxGHHlZBA= +uuid@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA== + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -16023,7 +16159,7 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -v8-compile-cache-lib@^3.0.0: +v8-compile-cache-lib@^3.0.0, v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== @@ -16545,6 +16681,13 @@ write-file-atomic@^4.0.0, write-file-atomic@^4.0.1: imurmurhash "^0.1.4" signal-exit "^3.0.7" +ws@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.1.1.tgz#1d43704689711ac1942fd2f283e38f825c4b8b95" + integrity sha512-bOusvpCb09TOBLbpMKszd45WKC2KPtxiyiHanv+H2DE3Az+1db5a/L7sVJZVDPUC1Br8f0SKRr1KjLpD1U/IAw== + dependencies: + async-limiter "~1.0.0" + ws@^8.2.3: version "8.5.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f"