import { useStore } from 'effector-react' import * as effector from 'effector' import { combine } from 'effector' import { pathOr } from 'ramda' import React, { useLayoutEffect, useRef } from 'react' import ReactDOM from 'react-dom' import { chromeDark, ObjectInspector, ObjectLabel } from 'react-inspector' import { throttle } from 'lodash' import './effector-addon.css' const trackCreateStore = effector.createEvent('trackCreateStore') const trackCreateEvent = effector.createEvent('trackCreateEvent') const trackCreateEffect = effector.createEvent('trackCreateEffect') export function createStore(...params) { const store = effector.createStore(...params) process.env.NODE_ENV === 'development' && trackCreateStore({ store, params, }) return store } export function createEvent(...params) { // console.log('createEvent', params) const event = effector.createEvent(...params) const name = params[0] const file = params[1].loc.file const module = file.split('/').slice(-1)[0].split('.')[0] process.env.NODE_ENV === 'development' && trackCreateEvent({ event, name, module, }) return event } export function createEffect(...params) { // console.log('createEffect', params) const effect = effector.createEffect(...params) process.env.NODE_ENV === 'development' && trackCreateEffect({ effect, params, }) return effect } const toggleVisibility = effector.createEvent('toggleVisibility') const visibility = effector.createStore(true) .on(toggleVisibility, state => !state) const $winParams = effector.createStoreObject({ visibility, }) const getNewPromise = () => new Promise((resolve) => resolver = resolve) let resolver = null let sync = Promise.resolve() const $eventMap = effector.createStore({}) .on(trackCreateEvent, (state, { event, name, module }) => { event.watch(async () => { await sync sync = getNewPromise() debouncedTimeSlice() addEvent({ type: 'event', module, name, fullname: `${module}.${name}` }) resolver && resolver() }) return { ...state, [module]: [ ...pathOr([], [module], state), `${module}.${name}`, ], } }) const $storeMap = effector.createStore({}) .on(trackCreateStore, (state, { store, params }) => { store.updates.watch(async () => { await sync sync = getNewPromise() debouncedTimeSlice() addEvent({ type: 'store', name: store.shortName }) resolver && resolver() }) return { ...state, [store.shortName]: store, } }) const addEvent = effector.createEvent() const eventCall = addEvent.filter({ fn: (params) => params.type === 'event', }) const timeSlice = effector.createEvent() const debouncedTimeSlice = throttle(timeSlice, 1000, { leading: true, trailing: false }) let eventCounter = 0 const clearStack = effector.createEvent() const $eventCallStack = effector.createStore([]) .reset(clearStack) .on(addEvent, (state, event) => { if (event.type === 'store') { const lastEvent = state.slice(-1)[0] if (lastEvent) { return [...state.slice(0, -1), { ...lastEvent, store: event.name }] } } return [ ...state.slice(-100), { ...event, index: event.type === 'event' ? ++eventCounter : 0 }, ] }) .on(timeSlice, (state) => [ ...state, { type: 'time', time: Date.now() }, ]) const $eventCalls = effector.createStore({}) .reset(clearStack) .on(eventCall, (state, { module, name }) => { return ({ ...state, [`${module}.${name}`]: pathOr(0, [`${module}.${name}`], state) + 1, }) }) const toggleEventFilter = effector.createEvent() const toggleEventGroupFilter = effector.createEvent() const $eventFilter = effector.createStore({}) .reset(clearStack) .on(trackCreateEvent, (state, { event, name, module }) => ({ ...state, [module]: { checked: pathOr(true, [module, 'checked'], state), data: { ...pathOr({}, [module, 'data'], state), [`${module}.${name}`]: true, }, }, })) .on(toggleEventFilter, (state, name) => { const module = name.split('.')[0] const checked = !pathOr(true, [module, 'data', name], state) const newState = { ...state, [module]: { // checked: moduleChecked, data: { ...pathOr({}, [module, 'data'], state), [name]: checked, }, }, } const moduleChecked = checked ? Object.values(pathOr({}, [module, 'data'], newState)).every(item => item) : false newState[module].checked = moduleChecked return newState }) .on(toggleEventGroupFilter, (state, module) => { const checked = !pathOr(true, [module, 'checked'], state) return ({ ...state, [module]: { checked, data: Object.keys(pathOr({}, [module, 'data'], state)) .reduce((acc, item) => Object.assign(acc, { [item]: checked }), {}), }, }) }) const $filteredCallStack = combine($eventCallStack, $eventFilter, (eventCallStack, eventFilter) => { return eventCallStack.filter(event => { if (event.type !== 'event') return event return eventFilter[event.module].data[event.fullname] }) }) function formatDate(date) { let diff = new Date() - date // the difference in milliseconds if (diff < 1000) { // less than 1 second return 'right now' } let sec = Math.floor(diff / 1000) // convert diff to seconds if (sec < 60) { return sec + ' sec. ago' } let min = Math.floor(diff / 60000) // convert diff to minutes if (min < 60) { return min + ' min. ago' } // format the date // add leading zeroes to single-digit day/month/hours/minutes let d = date d = [ '0' + d.getDate(), '0' + (d.getMonth() + 1), '' + d.getFullYear(), '0' + d.getHours(), '0' + d.getMinutes(), ].map(component => component.slice(-2)) // take last 2 digits of every component // join the components into date return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':') } const storeNodeRenderer = ({ depth, name, data, isNonenumerable, expanded }) => { switch (depth) { case 0: return Store list case 1: return typeof data === 'object' || Array.isArray(data) ? {name} : default: return } } const eventNodeRenderer = (eventCalls, eventFilter) => ({ depth, name, data, isNonenumerable, expanded }) => { switch (depth) { case 0: return case 1: { return ( { e.preventDefault() e.stopPropagation() }} onChange={() => toggleEventGroupFilter(name)} style={{ height: 12, width: 12, marginLeft: 2, marginRight: 5, // float: 'left', }} /> {name} ) } case 2: { const splitPath = data.split('.') const module = splitPath[0] const eventName = splitPath.slice(-1) return ( ) } default: return null } } const EventStackItemRenderer = ({ eventCallStack, item, index }) => { switch (item.type) { case 'store': return (
{item.name || ''}
) case 'event': return (
{item.index}. {item.name || ''}
{item.store && (
)}
{item.store}
) case 'time': return null return (
{formatDate(item.time)}
) default: return (
Unknown event
) } } // const tick = effector.createEvent() // const $refresher = effector.createStore(Date.now()) // .on(tick, state => Date.now()) // // setInterval(tick, 5000) export const DevToolWindow = () => { const winParams = useStore($winParams) const eventCallStack = useStore($filteredCallStack) const eventCalls = useStore($eventCalls) const storeMap = useStore($storeMap) const eventMap = useStore($eventMap) const storeList = Object.keys(storeMap) const eventFilter = useStore($eventFilter) // const refresher = useStore($refresher) const ref = useRef(null) useLayoutEffect(() => { if (ref.current) { ref.current.scrollTop = ref.current.scrollHeight } }, [eventCallStack[eventCallStack.length - 1]]) // if (!winParams.visibility) return null const storeObject = storeList.reduce((acc, storeName) => Object.assign(acc, { [storeName]: storeMap[storeName].getState() }), {}, ) return (
e.stopPropagation()} onMouseUp={e => e.stopPropagation()} onClick={e => e.stopPropagation()} >
Effector Inspector
Events stack:
Clear
{eventCallStack.map((item, index) => ( ))}
) } export function bindEffectorInspectorHotKey({ alt = false, ctrl = false, shift = false, key = '`' } = {}) { window.addEventListener('keydown', (e) => { e.key === key && e.altKey === alt && e.ctrlKey === ctrl && e.shiftKey === shift && toggleVisibility() }) } export const effectorInspector = (hotKeyOptions) => { bindEffectorInspectorHotKey(hotKeyOptions) const devtool = document.createElement('div') ReactDOM.render(, devtool) document.body.appendChild(devtool) }