├── .eslintignore ├── src ├── app │ ├── index.ts │ └── view.tsx ├── tabs │ ├── files │ │ ├── index.ts │ │ ├── model.ts │ │ └── view.tsx │ ├── effects │ │ ├── index.ts │ │ └── view.tsx │ ├── events │ │ ├── index.ts │ │ └── view.tsx │ ├── stores │ │ ├── index.ts │ │ └── view.tsx │ ├── log │ │ ├── index.ts │ │ ├── model.ts │ │ └── view.tsx │ └── trace │ │ ├── index.ts │ │ ├── model.ts │ │ └── view.tsx ├── types.d.ts ├── entities │ ├── effects │ │ ├── model.ts │ │ └── index.tsx │ ├── events │ │ ├── model.ts │ │ └── index.tsx │ ├── stores │ │ ├── model.ts │ │ └── index.tsx │ ├── files │ │ └── index.ts │ └── units │ │ └── index.ts ├── shared │ ├── configs │ │ └── options │ │ │ └── index.ts │ ├── ui │ │ ├── forms │ │ │ ├── checkbox.tsx │ │ │ └── index.ts │ │ ├── templates │ │ │ └── template.tsx │ │ ├── button │ │ │ └── index.tsx │ │ ├── styles │ │ │ └── global.tsx │ │ └── values │ │ │ └── index.tsx │ └── lib │ │ ├── domains.ts │ │ ├── setting.ts │ │ └── use-dragable.ts ├── types.h.ts └── index.ts ├── .vscode └── settings.json ├── .commitlintrc.json ├── .husky ├── pre-commit └── commit-msg ├── .editorconfig ├── .babelrc ├── .lintstagedrc ├── .stylelintrc ├── usage ├── index.html ├── another.ts └── index.ts ├── .github ├── workflows │ ├── release-drafter.yml │ ├── test.yml │ ├── publish.yml │ └── codeql-analysis.yml └── release-drafter.yml ├── vite.config.ts ├── .prettierrc ├── tsconfig.json ├── .eslintrc.json ├── rollup.config.js ├── babel.config.js ├── package.json ├── .gitignore └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export {App} from './view'; 2 | -------------------------------------------------------------------------------- /src/tabs/files/index.ts: -------------------------------------------------------------------------------- 1 | export {Files} from './view'; 2 | -------------------------------------------------------------------------------- /src/tabs/effects/index.ts: -------------------------------------------------------------------------------- 1 | export {Effect} from './view'; 2 | -------------------------------------------------------------------------------- /src/tabs/events/index.ts: -------------------------------------------------------------------------------- 1 | export {Events} from './view'; 2 | -------------------------------------------------------------------------------- /src/tabs/stores/index.ts: -------------------------------------------------------------------------------- 1 | export {Stores} from './view'; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /src/tabs/log/index.ts: -------------------------------------------------------------------------------- 1 | export {Logs} from './view'; 2 | export {createLogRecordFx} from './model'; 3 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit 5 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ramda.clone' { 2 | function clone(value: T): T; 3 | 4 | export default clone; 5 | } 6 | -------------------------------------------------------------------------------- /src/entities/effects/model.ts: -------------------------------------------------------------------------------- 1 | import {createStore} from 'effector'; 2 | 3 | import {EffectMeta} from '../../types.h'; 4 | 5 | export const $effects = createStore>({}, {serialize: 'ignore'}); 6 | -------------------------------------------------------------------------------- /src/entities/events/model.ts: -------------------------------------------------------------------------------- 1 | import {createStore} from 'effector'; 2 | 3 | import {EventMeta} from '../../types.h'; 4 | 5 | export const $events = createStore>({}, {serialize: 'ignore'}); 6 | -------------------------------------------------------------------------------- /src/entities/stores/model.ts: -------------------------------------------------------------------------------- 1 | import {createStore} from 'effector'; 2 | 3 | import {StoreMeta} from '../../types.h'; 4 | 5 | export const $stores = createStore>({}, {serialize: 'ignore'}); 6 | -------------------------------------------------------------------------------- /src/tabs/trace/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | traceEffectRun, 3 | traceEventTrigger, 4 | traceStoreChange, 5 | $isTraceEnabled, 6 | $traces, 7 | traceCleared, 8 | traceEnableToggled, 9 | } from './model'; 10 | export {Trace} from './view'; 11 | -------------------------------------------------------------------------------- /src/entities/files/index.ts: -------------------------------------------------------------------------------- 1 | import {createStore} from 'effector'; 2 | 3 | type FileName = string; 4 | export type FilesMap = Record>; 5 | 6 | export const $files = createStore({}, {serialize: 'ignore'}); 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 80 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "babel-preset-solid", 5 | "@babel/preset-typescript" 6 | ], 7 | "plugins": [ 8 | [ 9 | "effector/babel-plugin", 10 | { 11 | "addLoc": true, 12 | "addNames": true 13 | } 14 | ] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,tsx,js,jsx,mjs}": [ 3 | "eslint --fix", 4 | "stylelint --fix", 5 | "prettier --write" 6 | ], 7 | "*.css": [ 8 | "stylelint --fix" 9 | ], 10 | "*.md": [ 11 | "prettier --write" 12 | ], 13 | "*.mdx": [ 14 | "prettier --write --parser mdx" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/shared/configs/options/index.ts: -------------------------------------------------------------------------------- 1 | import {createEvent, createStore} from 'effector'; 2 | 3 | type Options = { 4 | trimDomain?: string; 5 | }; 6 | 7 | export const $options = createStore({}); 8 | export const setOptions = createEvent(); 9 | 10 | $options.on(setOptions, (_, options) => options); 11 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-rational-order" 5 | ], 6 | "rules": { 7 | "plugin/rational-order": [ 8 | true, 9 | { 10 | "empty-line-between-groups": true 11 | } 12 | ], 13 | "rule-empty-line-before": "always-multi-line" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/entities/events/index.tsx: -------------------------------------------------------------------------------- 1 | import {useTrimDomain} from '../../shared/lib/domains'; 2 | import {Unit, UnitName} from '../units'; 3 | 4 | export function EventView(props: {name: string}) { 5 | const displayName = useTrimDomain(props.name); 6 | return ( 7 | 8 | {displayName} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /usage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Effector Inspector Usage App 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: release-drafter/release-drafter@v5 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /src/shared/ui/forms/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import {styled} from 'solid-styled-components'; 2 | 3 | export function Checkbox(props: {value: boolean; onClick: () => void; label: string}) { 4 | return ( 5 | 9 | ); 10 | } 11 | 12 | const Label = styled.label` 13 | display: flex; 14 | align-items: center; 15 | flex-shrink: 0; 16 | `; 17 | -------------------------------------------------------------------------------- /src/shared/lib/domains.ts: -------------------------------------------------------------------------------- 1 | import {useUnit} from 'effector-solid'; 2 | 3 | import {$options} from '../configs/options'; 4 | 5 | export function trimDomainFromName(name: string, domainName: string) { 6 | return name.replace(`${domainName}/`, ''); 7 | } 8 | 9 | export function useTrimDomain(name: string): string { 10 | const options = useUnit($options); 11 | const trimDomain = options().trimDomain; 12 | return trimDomain ? trimDomainFromName(name, trimDomain) : name; 13 | } 14 | -------------------------------------------------------------------------------- /src/tabs/effects/view.tsx: -------------------------------------------------------------------------------- 1 | import {useUnit} from 'effector-solid'; 2 | import {For} from 'solid-js'; 3 | 4 | import {EffectView} from '../../entities/effects'; 5 | import {$effects} from '../../entities/effects/model'; 6 | 7 | const $effectsIds = $effects.map((effects) => Object.keys(effects)); 8 | 9 | export function Effect() { 10 | const effectsIds = useUnit($effectsIds); 11 | 12 | return ( 13 | <> 14 | {(id) => } 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/tabs/events/view.tsx: -------------------------------------------------------------------------------- 1 | import {useUnit} from 'effector-solid'; 2 | import {For} from 'solid-js'; 3 | 4 | import {EventView} from '../../entities/events'; 5 | import {$events} from '../../entities/events/model'; 6 | 7 | const $eventsNames = $events.map((events) => Object.keys(events)); 8 | 9 | export function Events() { 10 | const eventsNames = useUnit($eventsNames); 11 | 12 | return ( 13 | <> 14 | {(name) => } 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import legacy from '@vitejs/plugin-legacy'; 3 | import {defineConfig} from 'vite'; 4 | import solid from 'vite-plugin-solid'; 5 | 6 | export default defineConfig({ 7 | root: './usage', 8 | plugins: [ 9 | babel({extensions: ['.ts', '.tsx'], babelrc: true}) as any, 10 | legacy({ 11 | targets: ['last 4 versions', 'not IE 11'], 12 | }), 13 | solid(), 14 | ], 15 | resolve: { 16 | alias: [{find: '~', replacement: './src'}], 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/shared/ui/forms/index.ts: -------------------------------------------------------------------------------- 1 | import {styled} from 'solid-styled-components'; 2 | 3 | export {Checkbox} from './checkbox'; 4 | 5 | export const Input = styled.input` 6 | display: flex; 7 | flex-shrink: 0; 8 | padding: 0 0.5rem; 9 | 10 | border: 1px solid var(--border); 11 | border-radius: 0.2rem; 12 | 13 | &:focus { 14 | border-color: var(--primary); 15 | outline: 0; 16 | box-shadow: 0 0 0 1px var(--primary); 17 | } 18 | `; 19 | 20 | export const Search = styled(Input)` 21 | line-height: 2rem; 22 | `; 23 | 24 | export const Select = styled.select``; 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "semi": true, 7 | "arrowParens": "always", 8 | "bracketSpacing": false, 9 | "printWidth": 100, 10 | "endOfLine": "lf", 11 | "plugins": ["@trivago/prettier-plugin-sort-imports"], 12 | "importOrder": [ 13 | "", 14 | "^../", 15 | "^[./]" 16 | ], 17 | "importOrderSeparation": true, 18 | "importOrderSortSpecifiers": true, 19 | "importOrderGroupNamespaceSpecifiers": true, 20 | "importOrderCaseInsensitive": true 21 | } 22 | -------------------------------------------------------------------------------- /src/entities/units/index.ts: -------------------------------------------------------------------------------- 1 | import {styled} from 'solid-styled-components'; 2 | 3 | export const UnitName = styled.pre` 4 | display: flex; 5 | margin: 0 0; 6 | 7 | color: var(--code-var); 8 | font-family: 'JetBrains Mono', hasklig, monofur, monospace; 9 | `; 10 | 11 | export const UnitContent = styled.pre` 12 | margin: 0 0; 13 | color: var(--code-func); 14 | font-family: 'JetBrains Mono', hasklig, monofur, monospace; 15 | `; 16 | 17 | export const Unit = styled.li` 18 | display: flex; 19 | margin: 0 0; 20 | padding: 6px 10px; 21 | 22 | font-size: 12px; 23 | line-height: 1.3; 24 | `; 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test-package: 7 | runs-on: ubuntu-22.04 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | 12 | - name: Setup pnpm 13 | uses: pnpm/action-setup@v2 14 | 15 | - name: Use Node.js 18.x 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 18.x 19 | 20 | - name: Install dependencies 21 | run: pnpm install 22 | 23 | - name: Build 24 | run: pnpm build 25 | 26 | - name: Run tests 27 | run: pnpm test 28 | env: 29 | CI: true 30 | -------------------------------------------------------------------------------- /src/entities/stores/index.tsx: -------------------------------------------------------------------------------- 1 | import {useStoreMap} from 'effector-solid'; 2 | 3 | import {useTrimDomain} from '../../shared/lib/domains'; 4 | import {ValueView} from '../../shared/ui/values'; 5 | import {Unit, UnitContent, UnitName} from '../units'; 6 | 7 | import {$stores} from './model'; 8 | 9 | export function StoreView(props: {name: string}) { 10 | const store = useStoreMap($stores, (stores) => stores[props.name]); 11 | const displayName = useTrimDomain(props.name); 12 | 13 | return ( 14 | 15 | {displayName}: 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/entities/effects/index.tsx: -------------------------------------------------------------------------------- 1 | import {useStoreMap} from 'effector-solid'; 2 | 3 | import {useTrimDomain} from '../../shared/lib/domains'; 4 | import {Number, String} from '../../shared/ui/values'; 5 | import {Unit, UnitContent, UnitName} from '../units'; 6 | 7 | import {$effects} from './model'; 8 | 9 | export function EffectView(props: {id: string}) { 10 | const effect = useStoreMap($effects, (effects) => effects[props.id]); 11 | const displayName = useTrimDomain(effect().name); 12 | 13 | return ( 14 | 15 | {displayName}: 16 | 17 | 18 | {'{'} 19 | "inFlight": {effect().inFlight} 20 | {'}'} 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "preserve", 9 | "jsxImportSource": "solid-js", 10 | "lib": ["dom", "dom.iterable", "ESNext"], 11 | "module": "ESNext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "skipLibCheck": true, 15 | "strict": true, 16 | "target": "ESNext", 17 | "declaration": true, 18 | "emitDeclarationOnly": true, 19 | "declarationDir": "./dist", 20 | "baseUrl": "./", 21 | "rootDir": "./src", 22 | "outDir": "./dist", 23 | "typeRoots": ["./node_modules/@types"], 24 | "plugins": [{ "name": "styled-components" }] 25 | }, 26 | "exclude": ["./node_modules", "./dist"], 27 | "include": ["./src"] 28 | } 29 | -------------------------------------------------------------------------------- /src/shared/ui/templates/template.tsx: -------------------------------------------------------------------------------- 1 | import {JSX} from 'solid-js'; 2 | import {styled} from 'solid-styled-components'; 3 | 4 | export function TabTemplate(props: {header: JSX.Element; content: JSX.Element}) { 5 | return ( 6 | 7 |
{props.header}
8 | {props.content} 9 |
10 | ); 11 | } 12 | 13 | export const Header = styled.header` 14 | display: flex; 15 | align-items: center; 16 | gap: 1rem; 17 | padding: 1rem; 18 | width: fit-content; 19 | min-width: 100%; 20 | box-sizing: border-box; 21 | 22 | background-color: var(--content-bg); 23 | 24 | position: sticky; 25 | top: 0; 26 | `; 27 | 28 | export const Content = styled.section` 29 | flex: 1; 30 | width: max-content; 31 | `; 32 | 33 | export const TabTemplateRoot = styled.div` 34 | display: flex; 35 | flex-direction: column; 36 | position: relative; 37 | `; 38 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: '⚠️ Breaking changes' 3 | label: 'BREAKING CHANGES' 4 | 5 | - title: '🚀 Features' 6 | labels: 7 | - 'feature' 8 | - 'enhancement' 9 | 10 | - title: '🐛 Bug Fixes' 11 | labels: 12 | - 'fix' 13 | - 'bugfix' 14 | - 'bug' 15 | 16 | - title: '🧰 Maintenance' 17 | labels: 18 | - 'chore' 19 | - 'dependencies' 20 | 21 | - title: '📚 Documentation' 22 | label: 'documentation' 23 | 24 | - title: '🧪 Tests' 25 | label: 'tests' 26 | 27 | - title: '🏎 Optimizations' 28 | label: 'optimizations' 29 | 30 | version-resolver: 31 | major: 32 | labels: 33 | - 'BREAKING CHANGES' 34 | minor: 35 | labels: 36 | - 'feature' 37 | - 'enhancement' 38 | patch: 39 | labels: 40 | - 'fix' 41 | default: patch 42 | 43 | name-template: 'v$RESOLVED_VERSION' 44 | tag-template: 'v$RESOLVED_VERSION' 45 | 46 | change-template: '- $TITLE #$NUMBER (@$AUTHOR)' 47 | template: | 48 | $CHANGES 49 | -------------------------------------------------------------------------------- /usage/another.ts: -------------------------------------------------------------------------------- 1 | import {createStore} from 'effector'; 2 | 3 | export const $fn1 = createStore(function demo() { 4 | /* */ 5 | }); 6 | export const $fn2 = createStore(() => 5); 7 | const op = (a: number, b: number) => a + b; 8 | export const $fn3 = createStore(op); 9 | export const $setOfFns = createStore({ 10 | ref: new Set([ 11 | function demo() { 12 | return 0; 13 | }, 14 | () => 5, 15 | (a: number, b: number) => a + b, 16 | ]), 17 | }); 18 | export const $args = createStore( 19 | (function (a, b, c, d) { 20 | return arguments; // eslint-disable-line prefer-rest-params 21 | })(1, 5, {}, () => 0), 22 | ); 23 | export const $error = createStore(new Error('random')); 24 | export const $errorType = createStore(new TypeError('random')); 25 | 26 | class CustomError extends Error { 27 | demo = 123; 28 | hello = ''; 29 | name = 'Custom'; 30 | 31 | constructor(message: string) { 32 | super(message); 33 | this.hello = message; 34 | } 35 | } 36 | 37 | export const $errorCustom = createStore(new CustomError('message')); 38 | export const $window = createStore(window); 39 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "es2017": true, 6 | "node": true 7 | }, 8 | "extends": ["eslint:recommended"], 9 | "rules": { 10 | "import/extensions": "off", 11 | "@typescript-eslint/explicit-function-return-type": "off", 12 | "no-unused-vars": "warn" 13 | }, 14 | "overrides": [ 15 | { 16 | "files": ["*.ts", "*.tsx"], 17 | "extends": [ 18 | "plugin:@typescript-eslint/eslint-recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 21 | ], 22 | "parser": "@typescript-eslint/parser", 23 | "parserOptions": { 24 | "project": "tsconfig.json", 25 | "createDefaultProgram": true, 26 | "tsconfigRootDir": "./" 27 | }, 28 | "plugins": ["@typescript-eslint"], 29 | "rules": { 30 | "@typescript-eslint/explicit-function-return-type": "off", 31 | "@typescript-eslint/no-use-before-define": "off", 32 | "@typescript-eslint/ban-ts-ignore": "off" 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/shared/ui/button/index.tsx: -------------------------------------------------------------------------------- 1 | import {styled} from 'solid-styled-components'; 2 | 3 | export const Button = styled.button` 4 | white-space: nowrap; 5 | margin: 0; 6 | padding: 0.2rem 0.4rem; 7 | 8 | color: var(--primary-text); 9 | font-family: 'JetBrains Mono', hasklig, monofur, monospace; 10 | 11 | background-color: var(--primary); 12 | border: var(--primary); 13 | border-radius: 4px; 14 | 15 | &:focus { 16 | outline: 0; 17 | box-shadow: 0 0 0 1px var(--primary-dark), 0 0 3px 0 var(--primary-dark); 18 | } 19 | 20 | &:hover { 21 | background-color: var(--primary-dark); 22 | } 23 | `; 24 | const playSymbol = String.fromCharCode(parseInt('25B6', 16)); 25 | const pauseSymbol = String.fromCharCode(parseInt('25A0', 16)); 26 | 27 | const SwitchButton = styled(Button)` 28 | white-space: nowrap; 29 | `; 30 | 31 | export function RunButton(props: {onClick: () => void}) { 32 | return {playSymbol} Run; 33 | } 34 | 35 | export function PauseButton(props: {onClick: () => void}) { 36 | return {pauseSymbol} Pause; 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish CI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish-to-npm: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | 14 | - name: Setup pnpm 15 | uses: pnpm/action-setup@v2 16 | 17 | - name: Use Node.js 18.x 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18.x 21 | 22 | - name: Install dependencies 23 | run: pnpm install 24 | 25 | - name: Build 26 | run: pnpm build 27 | 28 | - name: Run tests 29 | run: pnpm test 30 | env: 31 | CI: true 32 | 33 | - name: Extract version 34 | id: version 35 | uses: olegtarasov/get-tag@v2.1 36 | with: 37 | tagRegex: 'v(.*)' 38 | 39 | - name: Set version from release 40 | uses: reedyuk/npm-version@1.0.1 41 | with: 42 | version: ${{ steps.version.outputs.tag }} 43 | git-tag-version: false 44 | 45 | - name: Create NPM config 46 | run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 47 | env: 48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | 50 | - name: Publish package 51 | run: npm publish 52 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const pluginResolve = require('@rollup/plugin-node-resolve'); 3 | const terser = require('@rollup/plugin-terser'); 4 | const { babel } = require('@rollup/plugin-babel'); 5 | const commonjs = require('@rollup/plugin-commonjs'); 6 | const typescript = require('rollup-plugin-typescript2'); 7 | const babelConfig = require('./babel.config'); 8 | 9 | const extensions = ['.tsx', '.ts', '.js', '.json']; 10 | 11 | function createBuild(input, format) { 12 | return { 13 | input: resolve(__dirname, `src/${input}.ts`), 14 | output: { 15 | file: `${input}.${format === 'esm' ? 'mjs' : 'js'}`, 16 | format, 17 | plugins: [terser()], 18 | sourcemap: true, 19 | }, 20 | plugins: [ 21 | commonjs(), 22 | pluginResolve({ extensions }), 23 | typescript({ useTsconfigDeclarationDir: true }), 24 | babel({ 25 | exclude: 'node_modules/**', 26 | babelHelpers: 'bundled', 27 | extensions, 28 | skipPreflightCheck: true, 29 | babelrc: false, 30 | ...babelConfig.generateConfig({ 31 | isEsm: format === 'esm', 32 | }), 33 | }), 34 | ], 35 | }; 36 | } 37 | 38 | const inputs = ['index']; 39 | const formats = ['cjs', 'esm']; 40 | 41 | const configs = inputs.map((input) => formats.map((format) => createBuild(input, format))).flat(); 42 | 43 | module.exports = configs; 44 | -------------------------------------------------------------------------------- /src/shared/lib/setting.ts: -------------------------------------------------------------------------------- 1 | import {createEvent} from 'effector'; 2 | 3 | type StorageType = 'local' | 'session'; 4 | 5 | const PREFIX = (0xeffec ** 2).toString(36); 6 | 7 | function getStorage(type: StorageType) { 8 | return type === 'session' ? sessionStorage : localStorage; 9 | } 10 | 11 | function read(name: string, defaultValue: string, storageType: StorageType = 'local'): string { 12 | return getStorage(storageType).getItem(`${PREFIX}-${name}`) ?? defaultValue; 13 | } 14 | 15 | function write(name: string, value: string, storageType: StorageType = 'local'): string { 16 | getStorage(storageType).setItem(`${PREFIX}-${name}`, value); 17 | return value; 18 | } 19 | 20 | export function createSetting(name: string, defaultValue: string) { 21 | const save = createEvent(); 22 | save.watch((value) => write(name, value)); 23 | return { 24 | read: () => read(name, defaultValue), 25 | write: (value: string) => write(name, value), 26 | save, 27 | }; 28 | } 29 | 30 | export function createJsonSetting( 31 | name: string, 32 | defaultValue: T, 33 | storageType: StorageType = 'local', 34 | ) { 35 | const save = createEvent(); 36 | save.watch((value) => write(name, JSON.stringify(value), storageType)); 37 | return { 38 | read: (): T => JSON.parse(read(name, JSON.stringify(defaultValue), storageType)), 39 | write: (value: T): T => { 40 | write(name, JSON.stringify(value), storageType); 41 | return value; 42 | }, 43 | save, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/tabs/stores/view.tsx: -------------------------------------------------------------------------------- 1 | import {combine, createEvent, createStore} from 'effector'; 2 | import {useUnit} from 'effector-solid'; 3 | import {For} from 'solid-js'; 4 | import {styled} from 'solid-styled-components'; 5 | 6 | import {StoreView} from '../../entities/stores'; 7 | import {$stores} from '../../entities/stores/model'; 8 | import {Search} from '../../shared/ui/forms'; 9 | 10 | const $list = $stores.map((map) => Object.entries(map).map(([name, meta]) => ({name, ...meta}))); 11 | 12 | const filterChanged = createEvent(); 13 | const $filter = createStore('', {serialize: 'ignore'}); 14 | $filter.on(filterChanged, (_, filter) => filter); 15 | 16 | const $filteredList = combine($list, $filter, (list, searchWord) => 17 | list.filter((item) => item.name.includes(searchWord)), 18 | ); 19 | 20 | const searchChanged = filterChanged.prepend( 21 | (e: Event | KeyboardEvent) => (e.currentTarget as HTMLInputElement)?.value, 22 | ); 23 | 24 | const $filteredName = $filteredList.map((list) => list.map((item) => item.name)); 25 | 26 | export function Stores() { 27 | const [stores, filter] = useUnit([$filteredName, $filter]); 28 | return ( 29 | <> 30 |
31 | 32 |
33 | {(store) => } 34 | 35 | ); 36 | } 37 | 38 | const Header = styled.div` 39 | padding: 6px; 40 | 41 | ${Search.class} { 42 | width: 100%; 43 | box-sizing: border-box; 44 | margin: 0; 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /src/tabs/files/model.ts: -------------------------------------------------------------------------------- 1 | import {combine, createEvent, createStore} from 'effector'; 2 | 3 | import {$files} from '../../entities/files'; 4 | import {$stores} from '../../entities/stores/model'; 5 | 6 | export const fileSelected = createEvent(); 7 | export const fileCleanup = createEvent(); 8 | export const filterChanged = createEvent(); 9 | 10 | export const $selectedFile = createStore('', {serialize: 'ignore'}); 11 | export const $filter = createStore(''); 12 | 13 | export const $filesList = $files.map((files) => Object.keys(files)); 14 | export const $filteredFiles = combine($filter, $filesList, (searchWord, list) => 15 | list.filter((file) => file.includes(searchWord)), 16 | ); 17 | 18 | $selectedFile.on(fileSelected, (_, file) => file).reset(fileCleanup); 19 | $filter.on(filterChanged, (_, value) => value); 20 | 21 | export const $storesFromFile = combine($selectedFile, $files, (current, files) => { 22 | if (current === '' || !files[current]) { 23 | return []; 24 | } 25 | return files[current].filter(({kind}) => kind === 'store').map(({name}) => name); 26 | }); 27 | 28 | export const $EventsFromFile = combine($selectedFile, $files, (current, files) => { 29 | if (current === '' || !files[current]) { 30 | return []; 31 | } 32 | return files[current].filter(({kind}) => kind === 'event').map(({name}) => name); 33 | }); 34 | 35 | export const $EffectFromFile = combine($selectedFile, $files, (current, files) => { 36 | if (current === '' || !files[current]) { 37 | return []; 38 | } 39 | return files[current].filter(({kind}) => kind === 'effect').map(({name}) => name); 40 | }); 41 | -------------------------------------------------------------------------------- /src/types.h.ts: -------------------------------------------------------------------------------- 1 | import {Effect, Event, Store} from 'effector'; 2 | 3 | export interface Options { 4 | trimDomain?: string; 5 | visible?: boolean; 6 | } 7 | 8 | export interface StoreCreator { 9 | store: Store; 10 | name: string; 11 | mapped: boolean; 12 | file?: string; 13 | } 14 | 15 | export interface EventCreator { 16 | event: Event; 17 | name: string; 18 | mapped: boolean; 19 | file?: string; 20 | } 21 | 22 | export interface EffectCreator { 23 | effect: Effect; 24 | sid: string; 25 | name: string; 26 | attached: boolean; 27 | file?: string; 28 | } 29 | 30 | export interface StoreMeta { 31 | value: any; 32 | mapped: boolean; 33 | } 34 | 35 | export interface EventMeta { 36 | mapped: boolean; 37 | history: any[]; 38 | } 39 | 40 | export interface EffectMeta { 41 | inFlight: number; 42 | name: string; 43 | effect: Effect; 44 | } 45 | 46 | export type Kind = 'event' | 'store' | 'effect'; 47 | 48 | export interface LogMeta { 49 | kind: Kind; 50 | name: string; 51 | payload: any; 52 | id: string; 53 | datetime: Date; 54 | } 55 | 56 | export interface Inspector { 57 | root: HTMLElement; 58 | } 59 | 60 | type Loc = { 61 | file: string; 62 | line: number; 63 | col: number; 64 | }; 65 | 66 | export type TraceStoreChange = { 67 | type: 'store'; 68 | name: string; 69 | loc?: Loc; 70 | before: any; 71 | current: any; 72 | }; 73 | 74 | export type TraceEventTrigger = { 75 | type: 'event'; 76 | name: string; 77 | loc?: Loc; 78 | argument: any; 79 | }; 80 | 81 | export type TraceEffectRun = { 82 | type: 'effect'; 83 | name: string; 84 | loc?: Loc; 85 | argument: any; 86 | }; 87 | 88 | export type Trace = TraceStoreChange | TraceEventTrigger | TraceEffectRun; 89 | export type StackTrace = {time: number; traces: Trace[]}; 90 | -------------------------------------------------------------------------------- /src/tabs/trace/model.ts: -------------------------------------------------------------------------------- 1 | import {createEvent, createStore, guard, merge, sample} from 'effector'; 2 | 3 | import {createJsonSetting} from '../../shared/lib/setting'; 4 | import {StackTrace, TraceEffectRun, TraceEventTrigger, TraceStoreChange} from '../../types.h'; 5 | 6 | export const traceStoreChange = createEvent(); 7 | export const traceEventTrigger = createEvent(); 8 | export const traceEffectRun = createEvent(); 9 | const traceAdd = createEvent(); 10 | const traceFinished = createEvent(); 11 | export const traceCleared = createEvent(); 12 | 13 | export const $traces = createStore([]); 14 | const $currentTrace = createStore({time: 0, traces: []}); 15 | 16 | const traceSetting = createJsonSetting('trace-enabled', false, 'session'); 17 | export const $isTraceEnabled = createStore(traceSetting.read()); 18 | $isTraceEnabled.watch(traceSetting.write); 19 | 20 | export const traceEnableToggled = createEvent(); 21 | 22 | $isTraceEnabled.on(traceEnableToggled, (value) => !value); 23 | $traces.on([traceCleared], () => []); 24 | 25 | $currentTrace.on(traceAdd, ({time, traces}, trace) => ({ 26 | time: time ? time : Date.now(), 27 | traces: [...traces, trace], 28 | })); 29 | 30 | guard({ 31 | clock: merge([traceStoreChange, traceEventTrigger, traceEffectRun]), 32 | filter: $isTraceEnabled, 33 | target: traceAdd, 34 | }); 35 | 36 | guard({ 37 | source: $currentTrace, 38 | clock: traceAdd, 39 | filter: ({traces}) => traces.length === 1, 40 | }).watch(() => queueMicrotask(traceFinished)); 41 | 42 | const moveTrace = sample({ 43 | clock: traceFinished, 44 | source: $currentTrace, 45 | }); 46 | 47 | $traces.on(moveTrace, (stackTraces, newTrace) => [...stackTraces, newTrace]); 48 | $currentTrace.reset(moveTrace); 49 | -------------------------------------------------------------------------------- /src/shared/ui/styles/global.tsx: -------------------------------------------------------------------------------- 1 | import {ParentProps} from 'solid-js'; 2 | import {styled} from 'solid-styled-components'; 3 | 4 | export function ThemeProvider(props: ParentProps<{}>) { 5 | return {props.children}; 6 | } 7 | 8 | const Styles = styled('div')` 9 | --primary: #ff8c00; 10 | --primary-light: #ffb152; 11 | --primary-dark: #c86e00; 12 | --primary-text: #fff; 13 | 14 | --text: #404040; 15 | --border: #dadada; 16 | --shadow: 0 4px 20px 4px rgba(0, 0, 0, 0.1); 17 | 18 | --scrollbar: var(--primary-light); 19 | 20 | --tabs-shadow: 0 2px 6px rgba(0, 0, 0, 0.06); 21 | 22 | --tab-bg: #fff; 23 | --tab-text: #606060; 24 | --tab-text-active: var(--primary); 25 | --tab-shadow-active: var(--primary); 26 | 27 | --content-bg: #f9f9f9; 28 | 29 | --code-var: #ff8c00; 30 | --code-func: #249ec6; 31 | --code-string: #00a153; 32 | --code-bool: #ff62d3; 33 | --code-number: #7a70f3; 34 | --code-date: #333; 35 | --code-regexp: #95b70e; 36 | 37 | @media (prefers-color-scheme: dark) { 38 | --text: #ddd; 39 | --border: #111; 40 | --shadow: 0 4px 20px 4px rgba(0, 0, 0, 0.1); 41 | 42 | --scrollbar: var(--primary); 43 | 44 | --tabs-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 45 | 46 | --tab-bg: #444; 47 | --tab-text: #ddd; 48 | --tab-text-active: var(--primary); 49 | --tab-shadow-active: var(--primary); 50 | 51 | --content-bg: #333; 52 | 53 | --code-var: #ff8c00; 54 | --code-func: #a5d4e2; 55 | --code-string: #2cb472; 56 | --code-bool: #ff62d3; 57 | --code-number: #9990ff; 58 | --code-date: #fff; 59 | --code-regexp: #e5ff7e; 60 | } 61 | 62 | ::-webkit-scrollbar-thumb { 63 | background-color: var(--scrollbar); 64 | } 65 | 66 | ::-webkit-scrollbar { 67 | width: 6px; 68 | } 69 | 70 | ::-webkit-scrollbar:horizontal { 71 | height: 6px; 72 | } 73 | `; 74 | -------------------------------------------------------------------------------- /src/shared/lib/use-dragable.ts: -------------------------------------------------------------------------------- 1 | import {Accessor, createSignal} from 'solid-js'; 2 | 3 | export function useDragable(params: { 4 | onDown?: DownCallback; 5 | onMove: MoveCallback; 6 | onUp?: () => void; 7 | }): [(event: MouseEvent) => void, Accessor] { 8 | const [isDrag, setIsDrag] = createSignal(false); 9 | 10 | function handler(mouseDownEvent: MouseEvent) { 11 | mouseDownEvent.stopPropagation(); 12 | const element = mouseDownEvent.currentTarget as HTMLElement; 13 | 14 | const startPoint = getPointFromEvent(mouseDownEvent, element); 15 | setIsDrag(true); 16 | params.onDown?.(startPoint); 17 | 18 | let startX = mouseDownEvent.clientX, 19 | startY = mouseDownEvent.clientY; 20 | 21 | function mouseMoveHandler(moveEvent: MouseEvent) { 22 | params.onMove({ 23 | coords: getPointFromEvent(moveEvent, element), 24 | shift: [moveEvent.clientX - startX, startY - moveEvent.clientY], 25 | }); 26 | 27 | startX = moveEvent.clientX; 28 | startY = moveEvent.clientY; 29 | } 30 | 31 | document.addEventListener('mousemove', mouseMoveHandler); 32 | document.addEventListener( 33 | 'mouseup', 34 | () => { 35 | setIsDrag(false); 36 | params.onUp?.(); 37 | document.removeEventListener('mousemove', mouseMoveHandler); 38 | }, 39 | { 40 | once: true, 41 | }, 42 | ); 43 | } 44 | 45 | return [handler, isDrag]; 46 | } 47 | 48 | type Vector = [number, number]; 49 | type Point = Vector; 50 | 51 | type DownCallback = (start: Point) => void; 52 | type MoveCallback = (param: {coords: Point; shift: Vector}) => void; 53 | 54 | function getPointFromEvent(event: MouseEvent, element: Element): Point { 55 | const clientRect = element.getBoundingClientRect(); 56 | 57 | const x = event.clientX - clientRect.x; 58 | const y = event.clientY - clientRect.y; 59 | return [x, y]; 60 | } 61 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api && api.cache && api.cache.never && api.cache.never(); 3 | // const env = api.cache(() => process.env.NODE_ENV) 4 | return generateConfig(meta, babelConfig); 5 | }; 6 | 7 | const meta = { 8 | isEsm: true, 9 | }; 10 | 11 | function generateConfig(meta, config = babelConfig) { 12 | const result = {}; 13 | for (const key in config) { 14 | const value = config[key]; 15 | result[key] = typeof value === 'function' ? value(meta) : value; 16 | } 17 | return result; 18 | } 19 | 20 | module.exports.generateConfig = generateConfig; 21 | 22 | const aliases = { 23 | effector: { 24 | esm: 'effector/effector.mjs', 25 | }, 26 | foliage: { 27 | esm: 'foliage/index.mjs', 28 | }, 29 | forest: { 30 | esm: 'forest/forest.mjs', 31 | }, 32 | }; 33 | 34 | const babelConfig = { 35 | presets: ['@babel/preset-env', '@babel/preset-typescript', 'solid'], 36 | plugins(meta) { 37 | const alias = parseAliases(meta, aliases); 38 | return [ 39 | ['effector/babel-plugin', { addLoc: true }], 40 | [ 41 | 'babel-plugin-module-resolver', 42 | { 43 | alias, 44 | loglevel: 'silent', 45 | }, 46 | ], 47 | ]; 48 | }, 49 | }; 50 | 51 | function parseAliases(meta, object) { 52 | const result = {}; 53 | for (const key in object) { 54 | const value = object[key]; 55 | if (typeof value === 'function') { 56 | const name = value(meta); 57 | if (name === undefined || name === null) continue; 58 | result[key] = name; 59 | } else if (typeof value === 'object' && value !== null) { 60 | const name = applyPaths(value); 61 | if (name === undefined || name === null) continue; 62 | result[key] = name; 63 | } else { 64 | const name = value; 65 | if (name === undefined || name === null) continue; 66 | result[key] = name; 67 | } 68 | } 69 | return result; 70 | 71 | function applyPaths(paths) { 72 | if (meta.isEsm) return paths.esm; 73 | return paths.default; 74 | } 75 | } 76 | 77 | module.exports.getAliases = (metadata = meta) => parseAliases(metadata, aliases); 78 | -------------------------------------------------------------------------------- /src/tabs/log/model.ts: -------------------------------------------------------------------------------- 1 | import {combine, createEffect, createEvent, createStore, guard} from 'effector'; 2 | 3 | import {createJsonSetting, createSetting} from '../../shared/lib/setting'; 4 | import {Kind, LogMeta} from '../../types.h'; 5 | 6 | const log = createEvent(); 7 | export const isLogEnabledToggle = createEvent(); 8 | export const logCleared = createEvent(); 9 | 10 | export const $logs = createStore([], {serialize: 'ignore'}); 11 | const logsSetting = createJsonSetting('logs-enabled', false, 'session'); 12 | export const $isLogEnabled = createStore(logsSetting.read()); 13 | $isLogEnabled.watch(logsSetting.write); 14 | 15 | type CreateRecord = Pick; 16 | 17 | let id = 1e3; 18 | const nextId = () => (++id).toString(36); 19 | 20 | export const createLogRecordFx = createEffect({ 21 | handler({name, kind, payload}) { 22 | return { 23 | id: nextId(), 24 | kind, 25 | name, 26 | payload, 27 | datetime: new Date(), 28 | }; 29 | }, 30 | }); 31 | 32 | $isLogEnabled.on(isLogEnabledToggle, (value) => !value); 33 | $logs.on(log, (logs, record) => [...logs, record]).reset(logCleared); 34 | 35 | guard({ 36 | clock: createLogRecordFx.doneData, 37 | filter: $isLogEnabled, 38 | target: log, 39 | }); 40 | 41 | export const toggleKind = createEvent(); 42 | const defaultKinds: Kind[] = ['event', 'store']; 43 | export const kindSetting = createJsonSetting('filter-kinds', defaultKinds); 44 | export const textSetting = createSetting('filter-text', ''); 45 | export const filterChanged = createEvent(); 46 | export const $kinds = createStore(kindSetting.read(), {serialize: 'ignore'}); 47 | export const $filterText = createStore(textSetting.read(), {serialize: 'ignore'}); 48 | 49 | $filterText.on(filterChanged, (_, filterText) => filterText); 50 | 51 | $kinds 52 | .on(toggleKind, (exist, toggled) => 53 | exist.includes(toggled) ? exist.filter((i) => i !== toggled) : [...exist, toggled], 54 | ) 55 | .watch(kindSetting.write); 56 | $filterText.watch(textSetting.write); 57 | 58 | export const $filteredLogs = combine($logs, $filterText, $kinds, (logs, text, kinds) => 59 | logs.filter((log) => log.name.includes(text) && kinds.includes(log.kind)), 60 | ); 61 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 2 * * 2' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['javascript'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v1 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v1 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "effector-inspector", 3 | "version": "0.0.0-real-version-will-be-set-on-ci", 4 | "main": "index.js", 5 | "module": "index.mjs", 6 | "exports": { 7 | ".": { 8 | "import": "./index.mjs", 9 | "require": "./index.js", 10 | "default": "./index.mjs" 11 | }, 12 | "./index.mjs": "./index.mjs" 13 | }, 14 | "packageManager": "pnpm@7.16.0", 15 | "types": "dist/index.d.ts", 16 | "license": "MIT", 17 | "files": [ 18 | "index.js", 19 | "index.js.map", 20 | "index.mjs", 21 | "index.mjs.map", 22 | "dist" 23 | ], 24 | "scripts": { 25 | "build": "rollup --config rollup.config.js", 26 | "lint": "eslint --ext .ts,.tsx src", 27 | "commit": "git-cz", 28 | "test": "echo no tests", 29 | "prepublishOnly": "yarn build", 30 | "start": "parcel serve ./usage/index.html", 31 | "lint:style": "stylelint src/**/*.{js,css,ts,tsx} --fix", 32 | "format": "prettier --write 'src/**/*.{js,css,ts,tsx}'", 33 | "prepare": "husky install" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.20.2", 37 | "@babel/preset-env": "^7.20.2", 38 | "@babel/preset-typescript": "^7.18.6", 39 | "@commitlint/cli": "17.2.0", 40 | "@commitlint/config-conventional": "17.2.0", 41 | "@rollup/plugin-babel": "^6.0.2", 42 | "@rollup/plugin-commonjs": "^23.0.2", 43 | "@rollup/plugin-node-resolve": "^15.0.1", 44 | "@rollup/plugin-terser": "^0.1.0", 45 | "@trivago/prettier-plugin-sort-imports": "^3.4.0", 46 | "@types/babel__core": "^7.1.20", 47 | "@types/node": "^18.11.9", 48 | "@typescript-eslint/eslint-plugin": "^2.16.0", 49 | "@typescript-eslint/parser": "^2.16.0", 50 | "@vitejs/plugin-legacy": "^2.3.1", 51 | "babel-plugin-module-resolver": "^4.1.0", 52 | "babel-preset-solid": "^1.6.2", 53 | "commitizen": "4.0.3", 54 | "cz-conventional-changelog": "3.0.2", 55 | "effector": "^22.4.0", 56 | "effector-solid": "^0.22.6", 57 | "eslint": "^6.6.0", 58 | "eslint-plugin-prettier": "^3.1.2", 59 | "eslint-plugin-react": "^7.17.0", 60 | "husky": "^8.0.2", 61 | "lint-staged": "^10.5.4", 62 | "prettier": "^2.7.1", 63 | "ramda.clone": "^0.26.1", 64 | "rollup": "^3.3.0", 65 | "rollup-plugin-typescript2": "^0.34.1", 66 | "solid-js": "^1.6.2", 67 | "solid-styled-components": "^0.28.5", 68 | "stylelint": "^14.15.0", 69 | "stylelint-config-rational-order": "^0.1.2", 70 | "stylelint-config-recommended": "^9.0.0", 71 | "terser": "^5.4.0", 72 | "typescript": "^4.2.3", 73 | "typescript-plugin-styled-components": "^1.4.4", 74 | "vite": "^3.2.3", 75 | "vite-plugin-solid": "^2.4.0" 76 | }, 77 | "config": { 78 | "commitizen": { 79 | "path": "cz-conventional-changelog" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Git ### 2 | # Created by git for backups. To disable backups in Git: 3 | # $ git config --global mergetool.keepBackup false 4 | *.orig 5 | 6 | # Created by git when using merge tools for conflicts 7 | *.BACKUP.* 8 | *.BASE.* 9 | *.LOCAL.* 10 | *.REMOTE.* 11 | *_BACKUP_*.txt 12 | *_BASE_*.txt 13 | *_LOCAL_*.txt 14 | *_REMOTE_*.txt 15 | 16 | ### macOS ### 17 | # General 18 | .DS_Store 19 | .AppleDouble 20 | .LSOverride 21 | 22 | # Icon must end with two \r 23 | Icon 24 | 25 | # Thumbnails 26 | ._* 27 | 28 | # Files that might appear in the root of a volume 29 | .DocumentRevisions-V100 30 | .fseventsd 31 | .Spotlight-V100 32 | .TemporaryItems 33 | .Trashes 34 | .VolumeIcon.icns 35 | .com.apple.timemachine.donotpresent 36 | 37 | # Directories potentially created on remote AFP share 38 | .AppleDB 39 | .AppleDesktop 40 | Network Trash Folder 41 | Temporary Items 42 | .apdisk 43 | 44 | ### Node ### 45 | # Logs 46 | logs 47 | *.log 48 | npm-debug.log* 49 | yarn-debug.log* 50 | yarn-error.log* 51 | lerna-debug.log* 52 | 53 | # Diagnostic reports (https://nodejs.org/api/report.html) 54 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 55 | 56 | # Runtime data 57 | pids 58 | *.pid 59 | *.seed 60 | *.pid.lock 61 | 62 | # Directory for instrumented libs generated by jscoverage/JSCover 63 | lib-cov 64 | 65 | # Coverage directory used by tools like istanbul 66 | coverage 67 | *.lcov 68 | 69 | # nyc test coverage 70 | .nyc_output 71 | 72 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 73 | .grunt 74 | 75 | # Bower dependency directory (https://bower.io/) 76 | bower_components 77 | 78 | # node-waf configuration 79 | .lock-wscript 80 | 81 | # Compiled binary addons (https://nodejs.org/api/addons.html) 82 | build/Release 83 | 84 | # Dependency directories 85 | node_modules/ 86 | jspm_packages/ 87 | 88 | # TypeScript v1 declaration files 89 | typings/ 90 | 91 | # TypeScript cache 92 | *.tsbuildinfo 93 | 94 | # Optional npm cache directory 95 | .npm 96 | 97 | # Optional eslint cache 98 | .eslintcache 99 | 100 | # Optional REPL history 101 | .node_repl_history 102 | 103 | # Output of 'npm pack' 104 | *.tgz 105 | 106 | # Yarn Integrity file 107 | .yarn-integrity 108 | 109 | # dotenv environment variables file 110 | .env 111 | .env.test 112 | 113 | # parcel-bundler cache (https://parceljs.org/) 114 | .cache 115 | 116 | # next.js build output 117 | .next 118 | 119 | # nuxt.js build output 120 | .nuxt 121 | 122 | # react / gatsby 123 | public/ 124 | 125 | # vuepress build output 126 | .vuepress/dist 127 | 128 | # Serverless directories 129 | .serverless/ 130 | 131 | # FuseBox cache 132 | .fusebox/ 133 | 134 | # DynamoDB Local files 135 | .dynamodb/ 136 | 137 | ### react ### 138 | .DS_* 139 | **/*.backup.* 140 | **/*.back.* 141 | 142 | node_modules 143 | bower_componets 144 | 145 | *.sublime* 146 | 147 | psd 148 | thumb 149 | sketch 150 | 151 | ### VisualStudioCode ### 152 | .vscode/* 153 | !.vscode/settings.json 154 | !.vscode/tasks.json 155 | !.vscode/launch.json 156 | !.vscode/extensions.json 157 | 158 | ### VisualStudioCode Patch ### 159 | # Ignore all local history of files 160 | .history 161 | 162 | storybook-static 163 | dist 164 | 165 | .idea 166 | 167 | # End of https://www.gitignore.io/api/git,node,react,macos,visualstudiocode 168 | 169 | 170 | # Built package artifacts 171 | index.js 172 | index.js.map 173 | index.mjs 174 | index.mjs.map 175 | -------------------------------------------------------------------------------- /src/tabs/log/view.tsx: -------------------------------------------------------------------------------- 1 | import {useUnit} from 'effector-solid'; 2 | import {For, Show} from 'solid-js'; 3 | import {styled} from 'solid-styled-components'; 4 | 5 | import {UnitContent} from '../../entities/units'; 6 | import {Button, PauseButton, RunButton} from '../../shared/ui/button'; 7 | import {Checkbox, Input} from '../../shared/ui/forms'; 8 | import {TabTemplate} from '../../shared/ui/templates/template'; 9 | import {ValueView} from '../../shared/ui/values'; 10 | 11 | import { 12 | $filteredLogs, 13 | $filterText, 14 | $isLogEnabled, 15 | $kinds, 16 | filterChanged, 17 | isLogEnabledToggle, 18 | logCleared, 19 | toggleKind, 20 | } from './model'; 21 | 22 | export function Logs() { 23 | const [isLogEnabled, logs, filterText, kinds] = useUnit([ 24 | $isLogEnabled, 25 | $filteredLogs, 26 | $filterText, 27 | $kinds, 28 | ]); 29 | 30 | return ( 31 | 34 | 35 | Show: 36 | toggleKind('event')} 39 | label="Event" 40 | /> 41 | toggleKind('store')} 44 | label="Store" 45 | /> 46 | toggleKind('effect')} 49 | label="Effect" 50 | /> 51 | 52 | 53 | Filter: 54 | filterChanged(e.currentTarget.value)} 57 | /> 58 | 59 | 60 | 61 | isLogEnabledToggle()} />} 64 | > 65 | isLogEnabledToggle()} /> 66 | 67 | 68 | 69 | } 70 | content={ 71 | 72 | 73 | {(log) => { 74 | const textMatched = log.name.includes(filterText()); 75 | 76 | if (!textMatched) { 77 | return null; 78 | } 79 | 80 | const time = log.datetime.toLocaleTimeString(); 81 | return ( 82 | 83 | {time} ▸ 84 | {log.kind} 85 | «{log.name}» 86 | 87 | 88 | 89 | 90 | ); 91 | }} 92 | 93 | 94 | } 95 | /> 96 | ); 97 | } 98 | 99 | const FilterInput = styled(Input)` 100 | line-height: 1.5rem; 101 | `; 102 | 103 | const PanelSection = styled.div` 104 | display: flex; 105 | gap: 0.5rem; 106 | `; 107 | 108 | const LogTitle = styled.pre` 109 | display: flex; 110 | margin: 0 0; 111 | 112 | color: var(--code-var); 113 | font-family: 'JetBrains Mono', hasklig, monofur, monospace; 114 | `; 115 | 116 | const LogItem = styled.li` 117 | display: flex; 118 | margin: 0 0; 119 | padding: 6px 10px; 120 | 121 | font-size: 12px; 122 | line-height: 1.3; 123 | `; 124 | 125 | const LogList = styled.ul` 126 | display: flex; 127 | flex-direction: column; 128 | flex-grow: 1; 129 | margin: 0 0; 130 | padding: 0 0; 131 | width: 100%; 132 | 133 | list-style-type: none; 134 | `; 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Effector Inspector 2 | 3 | | Dark theme and unit tracing | Units in files | 4 | | ----------------------------------------------- | ----------------------------------------- | 5 | | ![Dark-Traces](https://i.imgur.com/m9arc8u.png) | ![Units](https://i.imgur.com/VFki78R.png) | 6 | 7 | ## Installation 8 | 9 | ## Standalone 10 | 11 | 1. Install **effector-inspector** 12 | 13 | ```bash 14 | npm install --dev effector-inspector 15 | ``` 16 | 17 | or 18 | 19 | ```bash 20 | yarn add -D effector-inspector 21 | ``` 22 | 23 | 2. Make sure, that you have either [`effector/babel-plugin`](https://effector.dev/docs/api/effector/babel-plugin/) or [`@effector/swc-plugin`](https://github.com/effector/swc-plugin) set up in your project. These plugins add metadata to all effector's units, which is then used by effector-inspector. 24 | 25 | Check out the documentation of [`effector/babel-plugin`](https://effector.dev/docs/api/effector/babel-plugin/) or [`@effector/swc-plugin`](https://github.com/effector/swc-plugin). 26 | 27 | 3. Initialize `inspector` in your application's entrypoint (something like `index.ts` or `client.tsx`). 28 | 29 | ```ts 30 | import {createInspector} from 'effector-inspector'; 31 | 32 | createInspector(); 33 | ``` 34 | 35 | 4. After that inspector is ready to work, but it does not know about any units yet. You also need to attach inspector to units. 36 | 37 | One way to do it is to attach inspector to units manually: 38 | 39 | ```ts 40 | import {attachInspector} from 'effector-inspector'; 41 | 42 | // single units 43 | attachInspector($store); 44 | attachInspector(event); 45 | attachInspector(effectFx); 46 | // or list of them 47 | attachInspector([ 48 | $store, 49 | event, 50 | effectFx, 51 | // any number of units in the list 52 | ]); 53 | // or by domain 54 | attachInspector(someDomain); 55 | ``` 56 | 57 | ### effector-root 58 | 59 | The `effector-root` library can be used for convenience, as it provides common root domain for all units. 60 | 61 | ```ts 62 | // index.ts 63 | import {attachInspector, createInspector} from 'effector-inspector'; 64 | import {root} from 'effector-root'; 65 | 66 | createInspector(); 67 | attachInspector(root); 68 | ``` 69 | 70 | Check out `effector-root` [documentation here](https://github.com/effector/root#how-to-auto-replace-all-imports-of-effector-to-effector-root-using-babel-plugin). 71 | 72 | ## As a part of effector-logger 73 | 74 | 1. Install effector, logger and **inspector** 75 | 76 | ```bash 77 | npm install effector 78 | npm install --dev effector-logger effector-inspector 79 | ``` 80 | 81 | or yarn 82 | 83 | ```bash 84 | yarn add effector 85 | yarn add -D effector-logger effector-inspector 86 | ``` 87 | 88 | 2. Follow instructions for [effector-logger](https://github.com/sergeysova/effector-logger#installation) 89 | 90 | - Setup babel plugin 91 | - Replace `effector` to `effector-logger` 92 | 93 | 3. Open your root application file (something like `client.tsx` or `index.tsx`) 94 | 95 | Initialize effector logger in it first lines. 96 | 97 | ```ts 98 | import {createInspector} from 'effector-inspector'; 99 | 100 | createInspector(); 101 | ``` 102 | 103 | 4. Press hot keys to open inspector 104 | 105 | By default: `CTRL+B` in your application 106 | 107 | 5. Watch your stores and its values 108 | 109 | ## Release process 110 | 111 | 1. Check out the [draft release](https://github.com/effector/inspector/releases). 112 | 1. All PRs should have correct labels and useful titles. You can [review available labels here](https://github.com/effector/inspector/blob/master/.github/release-drafter.yml). 113 | 1. Update labels for PRs and titles, next [manually run the release drafter action](https://github.com/effector/inspector/actions/workflows/release-drafter.yml) to regenerate the draft release. 114 | 1. Review the new version and press "Publish" 115 | 1. If required check "Create discussion for this release" 116 | -------------------------------------------------------------------------------- /src/tabs/files/view.tsx: -------------------------------------------------------------------------------- 1 | import {useUnit} from 'effector-solid'; 2 | import {For, Show} from 'solid-js'; 3 | import {styled} from 'solid-styled-components'; 4 | 5 | import {EffectView} from '../../entities/effects'; 6 | import {EventView} from '../../entities/events'; 7 | import {StoreView} from '../../entities/stores'; 8 | import {Button} from '../../shared/ui/button'; 9 | import {Search, Select} from '../../shared/ui/forms'; 10 | 11 | import { 12 | $EffectFromFile, 13 | $EventsFromFile, 14 | $filesList, 15 | $filter, 16 | $filteredFiles, 17 | $selectedFile, 18 | $storesFromFile, 19 | fileCleanup, 20 | fileSelected, 21 | filterChanged, 22 | } from './model'; 23 | 24 | export function Files() { 25 | const [selectedFile, storesFromFile, eventsFromFile, effectFromFile, filter] = useUnit([ 26 | $selectedFile, 27 | $storesFromFile, 28 | $EventsFromFile, 29 | $EffectFromFile, 30 | $filter, 31 | ]); 32 | 33 | return ( 34 | 35 | 36 | 37 | Please, select file from the list or type the name 38 | filterChanged(e.currentTarget.value)} 41 | placeholder="Type a part of the file name" 42 | > 43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | File: 52 | 53 | 54 | 55 | 56 | {(eventName) => } 57 | 58 | 59 | 60 | 61 | {(storeName) => } 62 | 63 | 64 | 65 | 66 | {(storeName) => } 67 | 68 |
69 |
70 |
71 | ); 72 | } 73 | 74 | function FileList() { 75 | const [filesList] = useUnit([$filteredFiles]); 76 | return ( 77 | <> 78 | 79 | 80 | {(fileName) => fileSelected(fileName)}>{fileName}} 81 | 82 | 83 | 84 | ); 85 | } 86 | 87 | function FileSelector() { 88 | const [filesList, selectedFile] = useUnit([$filesList, $selectedFile]); 89 | return ( 90 | 93 | ); 94 | } 95 | 96 | const Column = styled.div` 97 | display: flex; 98 | flex-direction: column; 99 | width: 100%; 100 | `; 101 | 102 | const Title = styled.h4` 103 | margin-top: 0; 104 | `; 105 | 106 | const FileHeader = styled.div` 107 | display: flex; 108 | gap: 0.5rem; 109 | `; 110 | 111 | const FileItem = styled.button` 112 | color: var(--text); 113 | font-family: 'JetBrains Mono', hasklig, monofur, monospace; 114 | font-size: 14px; 115 | text-align: left; 116 | 117 | border: var(--primary); 118 | padding: 0.2rem 0.4rem; 119 | 120 | cursor: pointer; 121 | 122 | &:hover { 123 | background-color: var(--primary-dark) !important; 124 | } 125 | 126 | &:focus { 127 | outline: none; 128 | box-shadow: inset 0 0 0 2px var(--primary-dark); 129 | } 130 | `; 131 | 132 | const List = styled.div` 133 | display: flex; 134 | flex-direction: column; 135 | flex-grow: 1; 136 | margin: 0 0; 137 | padding: 0 0; 138 | overflow-x: auto; 139 | align-items: stretch; 140 | 141 | list-style-type: none; 142 | 143 | :nth-child(2n) { 144 | background-color: rgba(0, 0, 0, 0.1); 145 | } 146 | `; 147 | 148 | export const Panel = styled.div` 149 | display: flex; 150 | flex-shrink: 0; 151 | padding: 1rem; 152 | `; 153 | 154 | const NodeList = styled.ul` 155 | display: flex; 156 | flex-direction: column; 157 | flex-grow: 1; 158 | margin: 0 0; 159 | padding: 0 0; 160 | overflow-x: auto; 161 | 162 | list-style-type: none; 163 | `; 164 | -------------------------------------------------------------------------------- /src/tabs/trace/view.tsx: -------------------------------------------------------------------------------- 1 | import {useUnit} from 'effector-solid'; 2 | import {For, Match, Show, Switch} from 'solid-js'; 3 | import {styled} from 'solid-styled-components'; 4 | 5 | import {UnitContent} from '../../entities/units'; 6 | import {Button, PauseButton, RunButton} from '../../shared/ui/button'; 7 | import {TabTemplate} from '../../shared/ui/templates/template'; 8 | import {ValueView} from '../../shared/ui/values'; 9 | import {TraceEffectRun, TraceEventTrigger, TraceStoreChange} from '../../types.h'; 10 | 11 | import {$isTraceEnabled, $traces, traceCleared, traceEnableToggled} from './model'; 12 | 13 | export function Trace() { 14 | const [isTraceEnabled, traces] = useUnit([$isTraceEnabled, $traces]); 15 | 16 | return ( 17 | 20 | 21 | traceEnableToggled()} />} 24 | > 25 | traceEnableToggled()} /> 26 | 27 | 28 | } 29 | content={ 30 | 31 | 32 | {(trace) => ( 33 | <> 34 | 35 | 36 | 37 | 38 | {(line) => ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | )} 53 | 54 | 55 | )} 56 | 57 | 58 | } 59 | /> 60 | ); 61 | } 62 | 63 | function TraceEvent(props: {trace: TraceEventTrigger}) { 64 | return ( 65 | <> 66 | 67 | 68 | Event {props.trace.name} triggered with 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ); 77 | } 78 | 79 | function TraceStore(props: {trace: TraceStoreChange}) { 80 | return ( 81 | <> 82 | 83 | 84 | Store {props.trace.name} changed from 85 | 86 | 87 | 88 | 89 | 90 | 91 | to 92 | 93 | 94 | 95 | 96 | ); 97 | } 98 | 99 | function TraceEffect(props: {trace: TraceEffectRun}) { 100 | return ( 101 | <> 102 | 103 | 104 | Effect {props.trace.name} triggered with 105 | 106 | 107 | 108 | 109 | 110 | 111 | ); 112 | } 113 | 114 | const Actions = styled.div` 115 | display: flex; 116 | gap: 0.5rem; 117 | flex-shrink: 0; 118 | `; 119 | 120 | const TraceList = styled.ul` 121 | display: flex; 122 | flex-direction: column; 123 | flex-grow: 1; 124 | margin: 0 0; 125 | padding: 0 0; 126 | 127 | list-style-type: none; 128 | `; 129 | 130 | const TraceTitle = styled.div` 131 | font-size: 0.8rem; 132 | margin-top: 1rem; 133 | margin-left: 0.5rem; 134 | `; 135 | 136 | const TraceLine = styled.div` 137 | display: flex; 138 | flex-shrink: 0; 139 | font-family: 'JetBrains Mono', hasklig, monofur, monospace; 140 | margin: 0 0.5rem; 141 | 142 | .event { 143 | color: var(--code-var); 144 | } 145 | 146 | .store { 147 | color: var(--code-string); 148 | } 149 | 150 | .effect { 151 | color: var(--code-number); 152 | } 153 | `; 154 | 155 | const Node = styled.li` 156 | display: flex; 157 | margin: 0 0; 158 | padding: 6px 10px; 159 | 160 | font-size: 12px; 161 | line-height: 1.3; 162 | `; 163 | -------------------------------------------------------------------------------- /usage/index.ts: -------------------------------------------------------------------------------- 1 | import {createDomain, createEffect, createEvent, createStore} from 'effector'; 2 | 3 | import * as inspector from '../src'; 4 | import { 5 | $args, 6 | $error, 7 | $errorCustom, 8 | $errorType, 9 | $fn1, 10 | $fn2, 11 | $fn3, 12 | $setOfFns, 13 | $window, // @ts-ignore 14 | } from './another'; 15 | 16 | const emptyEvent = createEvent(); 17 | const event = createEvent<{count: number}>(); 18 | const just = createEvent(); 19 | 20 | const $foo = createStore('hello'); 21 | const $bar = $foo.map((foo) => foo.length); 22 | 23 | const $deep = createStore({ 24 | demo: {baz: 1, baf: 'hello', naf: false}, 25 | }); 26 | 27 | const veryRootDomain = createDomain(); 28 | 29 | const anotherInsideDeepDarkDomainForRoot = veryRootDomain.createDomain(); 30 | 31 | const $number = anotherInsideDeepDarkDomainForRoot.createStore(0); 32 | const $anotherNumber = createStore(0); 33 | const $numberInf = createStore(Infinity); 34 | const $numberNot = createStore(NaN); 35 | const $bigint = createStore(BigInt(498)); 36 | const $bool = createStore(false); 37 | const $bool2 = createStore(true); 38 | const $null = createStore(null); 39 | const $date = createStore(new Date()); 40 | const $symbol = createStore(Symbol.asyncIterator); 41 | 42 | const domain = createDomain(); 43 | 44 | const $example = domain.createStore(100); 45 | 46 | const $set = createStore(new Set(['a', 2, false, null, undefined, new Date()])); 47 | 48 | const $setWrapped = createStore({ 49 | ref: new Set(['a', 2, false, null, undefined, new Date()]), 50 | }); 51 | 52 | const $map = createStore( 53 | new Map([ 54 | ['a', 2], 55 | ['b', false], 56 | ]), 57 | ); 58 | 59 | const $mapWrapped = createStore({ 60 | ref: new Map([ 61 | ['a', 2], 62 | ['b', false], 63 | ]), 64 | }); 65 | 66 | const $setInMap = createStore(new Map([['hello', new Set(['a', 2, false, null, undefined])]])); 67 | 68 | const $mapInSet = createStore(new Set([new Map([['hello', new Set(['b', 12])]])])); 69 | 70 | const $array = createStore([ 71 | false, 72 | 5, 73 | 900e50, 74 | 'hello', 75 | BigInt(720587) * BigInt(44), 76 | new Map([['hello', new Set(['a', 2, false, null, undefined])]]), 77 | new Set([new Map([['hello', new Set(['b', 12])]])]), 78 | { 79 | ref: new Set(['a', 2, false, null, undefined, new Date()]), 80 | }, 81 | ]); 82 | 83 | const $uint = createStore(new Uint32Array([0, 5, 1, 2])); 84 | const $weakSet = createStore(new WeakSet([{a: 1}, {b: 2}, {c: 3}])); 85 | 86 | const $iterators = createStore([ 87 | new Set(['a', 2, false, null, undefined, new Date()]).entries(), 88 | ['a', 2, false, null, undefined, new Date()].entries(), 89 | new Map([ 90 | ['a', 2], 91 | ['b', false], 92 | ]).entries(), 93 | ]); 94 | 95 | const $regexp1 = createStore(/[\w\s]+/gi); 96 | const $regexp2 = createStore(new RegExp('[\\w\\s]+', 'gi')); 97 | 98 | const $promise = createStore(new Promise((resolve) => setTimeout(resolve, 5000))); 99 | const $promiseResolved = createStore(Promise.resolve(1)); 100 | const $promiseRejected = createStore(Promise.reject(1)); 101 | 102 | const cdFirst = {}; 103 | // @ts-ignore 104 | cdFirst.cdFirst = cdFirst; 105 | 106 | const $circularObject = createStore(cdFirst); 107 | const circular = createEvent>(); 108 | 109 | const exampleFx = createEffect({ 110 | handler() { 111 | return new Promise((resolve) => setTimeout(resolve, 1000)); 112 | }, 113 | }); 114 | 115 | const exampleFx2 = createEffect({ 116 | handler() { 117 | return new Promise((resolve) => setTimeout(resolve, 3000)); 118 | }, 119 | }); 120 | 121 | const trimDomain = createDomain(); 122 | 123 | const trimDomainStore = trimDomain.createStore('No Domain in name'); 124 | const trimDomainEvent = trimDomain.createEvent(); 125 | const trimDomainEffect = trimDomain.createEffect(); 126 | 127 | inspector.attachInspector(domain); 128 | inspector.attachInspector([ 129 | exampleFx, 130 | exampleFx2, 131 | emptyEvent, 132 | event, 133 | just, 134 | $args, 135 | $array, 136 | $bar, 137 | $bigint, 138 | $bool, 139 | $bool2, 140 | $date, 141 | $deep, 142 | $error, 143 | $errorCustom, 144 | $errorType, 145 | $fn1, 146 | $fn2, 147 | $fn3, 148 | $foo, 149 | $iterators, 150 | $map, 151 | $mapInSet, 152 | $mapWrapped, 153 | $null, 154 | $number, 155 | $numberInf, 156 | $numberNot, 157 | $promise, 158 | $promiseRejected, 159 | $promiseResolved, 160 | $regexp1, 161 | $regexp2, 162 | $set, 163 | $setInMap, 164 | $setOfFns, 165 | $setWrapped, 166 | $symbol, 167 | $uint, 168 | $weakSet, 169 | $window, 170 | $anotherNumber, 171 | $circularObject, 172 | trimDomainStore, 173 | trimDomainEvent, 174 | trimDomainEffect, 175 | ]); 176 | 177 | inspector.createInspector({visible: true, trimDomain: 'trimDomain'}); 178 | let incrementor = 0; 179 | setInterval(() => emptyEvent(), 2000); 180 | setInterval(() => event({count: incrementor++}), 2000); 181 | setTimeout(() => just('hello'), 0); 182 | setInterval(() => { 183 | exampleFx(); 184 | }, 1500); 185 | 186 | setInterval(() => { 187 | exampleFx2(); 188 | }, 4000); 189 | setInterval(() => { 190 | exampleFx2(); 191 | }, 3500); 192 | setTimeout(() => { 193 | const cdSecond = {}; 194 | // @ts-ignore 195 | cdSecond.cdSecond = cdSecond; 196 | 197 | circular(cdSecond); 198 | }, 1000) 199 | 200 | setTimeout(() => { 201 | const cdThird = { 202 | purple: true, 203 | }; 204 | // @ts-ignore 205 | cdThird.cdThird = cdThird; 206 | 207 | circular(cdThird); 208 | }, 3000) 209 | 210 | $anotherNumber.on(event, (counter) => counter + 1); 211 | $date.on(event, () => new Date()); 212 | $foo.on(just, (s, n) => s + n); 213 | $example.on(event, () => Math.random() * 100); 214 | $circularObject.on(circular, (value, nextValue) => ({ 215 | ...value, 216 | ...nextValue, 217 | })); 218 | -------------------------------------------------------------------------------- /src/shared/ui/values/index.tsx: -------------------------------------------------------------------------------- 1 | import {createSignal, For, Show} from 'solid-js'; 2 | import {styled} from 'solid-styled-components'; 3 | 4 | const typeRegexp = /\[object ([\w\s]+)\]/; 5 | 6 | export function getType(value: unknown): 'unknown' | string { 7 | const typeString = Object.prototype.toString.call(value); 8 | const match = typeRegexp.exec(typeString); 9 | return match ? match[1] : 'unknown'; 10 | } 11 | 12 | export const Boolean = styled.span` 13 | color: var(--code-bool); 14 | font-style: italic; 15 | `; 16 | 17 | export const Number = styled.span` 18 | color: var(--code-number); 19 | `; 20 | 21 | export const String = styled.span` 22 | color: var(--code-string); 23 | `; 24 | 25 | export const Keyword = styled.span` 26 | color: var(--code-number); 27 | font-weight: bold; 28 | `; 29 | 30 | export const Date = styled.span` 31 | color: var(--code-date); 32 | `; 33 | 34 | export const Symbol = styled.span` 35 | /* nothing here */ 36 | `; 37 | 38 | export const Regexp = styled.span` 39 | color: var(--code-regexp); 40 | `; 41 | 42 | export const ListItem = styled.span` 43 | display: inline-block; 44 | 45 | [data-opened='true'] > & { 46 | display: block; 47 | padding-left: 8px; 48 | } 49 | 50 | &[data-hidden='folded'] { 51 | display: none; 52 | 53 | [data-opened='true'] > & { 54 | display: block; 55 | } 56 | } 57 | 58 | &[data-hidden='expanded'] { 59 | display: inline-block; 60 | 61 | [data-opened='true'] > & { 62 | display: none; 63 | } 64 | } 65 | 66 | &:not(:last-child)::after { 67 | content: ', '; 68 | } 69 | `; 70 | 71 | export function ValueView(props: {value: unknown; opened?: boolean}) { 72 | const type = getType(props.value); 73 | 74 | const [opened, setOpened] = createSignal(false); 75 | 76 | const localOpened = () => 77 | props.opened === undefined || props.opened === true ? opened() : false; 78 | const openable = () => props.opened === undefined || props.opened === true; 79 | 80 | function toggleOpened() { 81 | setOpened(!opened()); 82 | } 83 | 84 | const renderArrayLikeObject = (title: string) => (value: []) => { 85 | return ( 86 | <> 87 | 88 | 89 | {title} 90 | 91 | {' ['} 92 | 96 | ... 97 | 98 | } 99 | > 100 | 101 | {(item) => ( 102 | 103 | 104 | 105 | )} 106 | 107 | 108 | ] 109 | 110 | 111 | ); 112 | }; 113 | 114 | const mapByTypes = { 115 | String: (value: string) => "{value}", 116 | Number: (value: number) => {value}, 117 | Boolean: () => {JSON.stringify(props.value)}, 118 | Null: () => null, 119 | Undefined: () => undefined, 120 | Symbol: (value: number) => {value.toString()}, 121 | BigInt: (value: BigInt) => {value.toString()}n, 122 | RegExp: (value: RegExp) => {`/${value.source}/${value.flags}`}, 123 | Function: (value: Function) => ( 124 | <> 125 | function 126 | {`${value.name ? ` ${value.name} ` : ''}`} 127 | () 128 | 129 | ), 130 | AsyncFunction: (value: Function) => ( 131 | <> 132 | async function 133 | {`${value.name ? ` ${value.name} ` : ''}`} 134 | () 135 | 136 | ), 137 | Date: (value: Date) => {value.toISOString?.()}, 138 | Array: renderArrayLikeObject('Array'), 139 | Arguments: renderArrayLikeObject('Arguments'), 140 | Set: renderArrayLikeObject('Set'), 141 | Map: (value: Map) => { 142 | return ( 143 | <> 144 | 145 | 146 | Map 147 | 148 | {' {'} 149 | 150 | {([key, mapValue]) => ( 151 | 152 | "{key}" 153 | {` => `} 154 | 155 | 156 | )} 157 | 158 | {'}'} 159 | 160 | 161 | ); 162 | }, 163 | Error: (error: Error) => ( 164 | 165 | 166 | {error.name} 167 | 168 | {' {'} 169 | 170 | "message" : 171 | "" 172 | 173 | 174 | "stack" : 175 | 176 | 177 | 178 | {([key, objValue], index) => ( 179 | 180 | "{key}": 181 | 182 | )} 183 | 184 | {'}'} 185 | 186 | ), 187 | Window: () => Window {'{...}'}, 188 | __default: (value: object) => ( 189 | <> 190 | 191 | 0} 193 | onClick={toggleOpened} 194 | > 195 | {type} 196 | 197 | {' {'} 198 | 202 | ... 203 | 204 | } 205 | > 206 | 207 | {([key, objValue]) => ( 208 | 209 | "{key}": 210 | 211 | )} 212 | 213 | 214 | {'}'} 215 | 216 | 217 | ), 218 | }; 219 | 220 | // @ts-ignore 221 | if (mapByTypes[type]) { 222 | // @ts-ignore 223 | return <>{mapByTypes[type](props.value)}; 224 | } 225 | 226 | return <>{mapByTypes['__default'](props.value as object)}; 227 | } 228 | 229 | const Openedable = styled.span` 230 | &[data-active='true'] { 231 | cursor: pointer; 232 | &:hover { 233 | text-decoration: underline; 234 | } 235 | } 236 | `; 237 | -------------------------------------------------------------------------------- /src/app/view.tsx: -------------------------------------------------------------------------------- 1 | import {createEvent, createStore, sample} from 'effector'; 2 | import {useUnit} from 'effector-solid'; 3 | import {For, Match, Show, Switch} from 'solid-js'; 4 | import {createGlobalStyles, styled} from 'solid-styled-components'; 5 | 6 | import {createJsonSetting, createSetting} from '../shared/lib/setting'; 7 | import {useDragable} from '../shared/lib/use-dragable'; 8 | import {ThemeProvider} from '../shared/ui/styles/global'; 9 | import {Effect} from '../tabs/effects'; 10 | import {Events} from '../tabs/events'; 11 | import {Files} from '../tabs/files'; 12 | import {Logs} from '../tabs/log'; 13 | import {Stores} from '../tabs/stores'; 14 | import {Trace} from '../tabs/trace'; 15 | 16 | const Tabs = { 17 | files: { 18 | title: 'Files', 19 | Component: Files, 20 | }, 21 | stores: { 22 | title: 'Stores', 23 | Component: Stores, 24 | }, 25 | effects: { 26 | title: 'Effects', 27 | Component: Effect, 28 | }, 29 | events: { 30 | title: 'Events', 31 | Component: Events, 32 | }, 33 | traces: { 34 | title: 'Traces', 35 | Component: Trace, 36 | }, 37 | logs: { 38 | title: 'Logs', 39 | Component: Logs, 40 | }, 41 | }; 42 | type Keys = Array; 43 | type Key = keyof typeof Tabs; 44 | const tabList = Object.keys(Tabs) as Keys; 45 | const firstTab = tabList[0]; 46 | 47 | const lastTab = createSetting('last-tab', firstTab); 48 | const savedTab = lastTab.read() as Key; 49 | const initialTab = tabList.includes(savedTab) ? savedTab : firstTab; 50 | const changeTab = createEvent(); 51 | const $tab = createStore(initialTab); 52 | 53 | $tab.on(changeTab, (_, tab) => tab); 54 | $tab.watch(lastTab.write); 55 | 56 | const KEY_B = 2; 57 | const KEY_L = 12; 58 | 59 | const visibleSettings = createJsonSetting('visible', false); 60 | const $isVisible = createStore(visibleSettings.read(), {serialize: 'ignore'}); 61 | const togglePressed = createEvent(); 62 | const clearPressed = createEvent(); 63 | const showInspector = createEvent(); 64 | 65 | if (typeof document === 'object') { 66 | document.addEventListener('keydown', (event) => { 67 | if (event.ctrlKey) { 68 | if (event.key === 'l' || event.keyCode === KEY_L) { 69 | clearPressed(); 70 | } 71 | if (event.key === 'b' || event.keyCode === KEY_B) { 72 | togglePressed(); 73 | } 74 | } 75 | }); 76 | } 77 | 78 | $isVisible.on(togglePressed, (visible) => !visible).on(showInspector, () => true); 79 | $isVisible.watch(visibleSettings.write); 80 | 81 | const widthChanged = createEvent(); 82 | const resizeStopped = createEvent(); 83 | const positionChanged = createEvent<{top: number; right: number}>(); 84 | const positionChangedStopped = createEvent(); 85 | 86 | const widthSetting = createJsonSetting('width', 736); 87 | const $width = createStore(widthSetting.read(), {serialize: 'ignore'}); 88 | 89 | const positionSettings = createJsonSetting('position', {top: 64, right: 64}); 90 | const $position = createStore(positionSettings.read(), {serialize: 'ignore'}); 91 | 92 | $width.on(widthChanged, (width, change) => width - change); 93 | $position.on(positionChanged, (position, changes) => ({ 94 | top: position.top - changes.top, 95 | right: position.right - changes.right, 96 | })); 97 | 98 | sample({ 99 | clock: resizeStopped, 100 | source: $width, 101 | target: widthSetting.save, 102 | }); 103 | 104 | sample({ 105 | clock: positionChangedStopped, 106 | source: $position, 107 | target: positionSettings.save, 108 | }); 109 | 110 | export function App() { 111 | const [currentTab, isVisible, width, position] = useUnit([$tab, $isVisible, $width, $position]); 112 | 113 | const [onDown, isDrag] = useDragable({ 114 | onMove({shift}) { 115 | widthChanged(shift[0]); 116 | }, 117 | onUp: resizeStopped, 118 | }); 119 | 120 | const [onHeaderMouseDown] = useDragable({ 121 | onMove({shift}) { 122 | positionChanged({ 123 | top: shift[1], 124 | right: shift[0], 125 | }); 126 | }, 127 | onUp: positionChangedStopped, 128 | }); 129 | 130 | const styles = () => ({ 131 | width: `${width()}px`, 132 | top: `${position().top}px`, 133 | right: `${position().right}px`, 134 | }); 135 | 136 | return ( 137 | 138 | {/* @ts-ignore */} 139 | 140 | 141 | 142 | 143 | ... 144 | 145 | 146 |
147 | ☄️ 148 | Loading...}> 149 | {([key, tab]) => ( 150 | e.stopPropagation()} 152 | data-active={currentTab() === key} 153 | onClick={() => changeTab(key as Key)} 154 | > 155 | {tab.title} 156 | 157 | )} 158 | 159 |
160 |
161 | 162 | Not Found}> 163 | Loading...}> 164 | {([key, tab]) => ( 165 | 166 | 167 | 168 | )} 169 | 170 | 171 | 172 |
173 |
174 |
175 |
176 | ); 177 | } 178 | 179 | const Tab = styled.div` 180 | padding: 8px; 181 | 182 | color: var(--tab-text); 183 | 184 | border-radius: inherit; 185 | border-top-right-radius: 0; 186 | cursor: pointer; 187 | 188 | &:hover { 189 | box-shadow: inset 0 -2px 0 0 var(--tab-shadow-active); 190 | } 191 | 192 | &:not(:first-child) { 193 | border-top-left-radius: 0; 194 | } 195 | 196 | &[data-active='true'] { 197 | color: var(--tab-text-active); 198 | 199 | box-shadow: inset 0 -2px 0 0 var(--tab-shadow-active); 200 | } 201 | `; 202 | 203 | const TabsHeader = styled.div` 204 | position: sticky; 205 | top: 0; 206 | right: 0; 207 | left: 0; 208 | 209 | display: flex; 210 | 211 | font-weight: 500; 212 | font-size: 16px; 213 | line-height: 20px; 214 | 215 | background-color: var(--tab-bg); 216 | border-bottom: 1px solid var(--border); 217 | border-radius: inherit; 218 | border-bottom-right-radius: 0; 219 | border-bottom-left-radius: 0; 220 | box-shadow: var(--tabs-shadow); 221 | `; 222 | 223 | const Logo = styled.div` 224 | display: flex; 225 | align-items: center; 226 | justify-content: center; 227 | width: 34px; 228 | `; 229 | 230 | const Header = styled.div` 231 | display: flex; 232 | width: 100%; 233 | cursor: grab; 234 | `; 235 | 236 | const SectionContent = styled.div` 237 | display: flex; 238 | flex-direction: column; 239 | flex-grow: 1; 240 | overflow-y: auto; 241 | 242 | background-color: var(--content-bg); 243 | `; 244 | 245 | const TabsContainer = styled.section` 246 | position: relative; 247 | display: flex; 248 | flex-flow: column; 249 | width: 100%; 250 | border-radius: inherit; 251 | box-shadow: var(--shadow); 252 | `; 253 | 254 | export const DragHandler = styled.div` 255 | display: flex; 256 | flex-direction: column; 257 | justify-content: center; 258 | 259 | width: 8px; 260 | height: 80%; 261 | margin: auto; 262 | 263 | color: var(--primary); 264 | font-size: 14px; 265 | font-family: monospace; 266 | line-height: 6px; 267 | word-break: break-word; 268 | 269 | background-color: var(--bg); 270 | border-top-left-radius: 8px; 271 | border-bottom-left-radius: 8px; 272 | user-select: none; 273 | cursor: col-resize; 274 | 275 | &:hover, 276 | &[data-active='true'] { 277 | color: var(--bg); 278 | background-color: var(--primary); 279 | } 280 | `; 281 | 282 | const InspectorRoot = styled.div` 283 | :global { 284 | } 285 | 286 | position: fixed; 287 | right: 48px; 288 | top: 48px; 289 | height: 80vh; 290 | z-index: 1000; 291 | 292 | display: flex; 293 | justify-content: center; 294 | 295 | width: 736px; 296 | min-width: 410px; 297 | max-width: 90%; 298 | 299 | color: var(--text); 300 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Apple Color Emoji', 301 | 'Segoe UI Emoji', 'Segoe UI Symbol', 'PT Sans', Helvetica, Arial, sans-serif; 302 | line-height: 1.5; 303 | 304 | border-radius: 8px; 305 | 306 | user-select: none; 307 | 308 | color-scheme: light dark; 309 | -ms-text-size-adjust: 100%; 310 | -webkit-text-size-adjust: 100%; 311 | 312 | @media screen and (max-width: 700px) { 313 | max-width: 480px; 314 | } 315 | `; 316 | 317 | // @ts-ignore 318 | const BodyCursor = createGlobalStyles<{isDrag: boolean}>` 319 | body { 320 | cursor: ${(props: {isDrag: boolean}) => (props.isDrag ? 'col-resize' : 'auto')}; 321 | } 322 | `; 323 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompositeName, 3 | createEvent, 4 | createNode, 5 | Domain, 6 | Effect, 7 | Event, 8 | forward, 9 | is, 10 | Node, 11 | Stack, 12 | step, 13 | Store, 14 | Unit, 15 | } from 'effector'; 16 | import {render} from 'solid-js/web'; 17 | import clone from 'ramda.clone'; 18 | 19 | import {App} from './app'; 20 | import {$effects} from './entities/effects/model'; 21 | import {$events} from './entities/events/model'; 22 | import {$files} from './entities/files'; 23 | import {$stores} from './entities/stores/model'; 24 | import {setOptions} from './shared/configs/options'; 25 | import {createLogRecordFx} from './tabs/log'; 26 | import {traceEffectRun, traceEventTrigger, traceStoreChange} from './tabs/trace'; 27 | 28 | import './types.d'; 29 | import {EffectCreator, EventCreator, Inspector, Kind, Options, StoreCreator} from './types.h'; 30 | 31 | const storeAdd = createEvent(); 32 | const storeUpdated = createEvent<{name: string; value: any}>(); 33 | 34 | const eventAdd = createEvent(); 35 | const eventTriggered = createEvent<{name: string; params: any}>(); 36 | 37 | const effectAdd = createEvent(); 38 | const effectTriggered = createEvent<{sid: string}>(); 39 | 40 | $stores 41 | .on(storeAdd, (map, payload) => ({ 42 | ...map, 43 | [payload.name]: { 44 | value: payload.store.getState(), 45 | mapped: payload.mapped, 46 | }, 47 | })) 48 | .on(storeUpdated, (map, {name, value}) => { 49 | // should not change the order of fields 50 | map[name] = {...map[name], value}; 51 | return {...map}; 52 | }); 53 | 54 | $files.on(storeAdd, (map, {name, file}) => { 55 | if (file) { 56 | if (map[file]) { 57 | const list = map[file]; 58 | return {...map, [file]: [...list, {kind: 'store', name}]}; 59 | } 60 | 61 | return {...map, [file]: [{kind: 'store', name}]}; 62 | } 63 | 64 | return map; 65 | }); 66 | 67 | $events 68 | .on(eventAdd, (map, payload) => ({ 69 | ...map, 70 | [payload.name]: { 71 | mapped: payload.mapped, 72 | history: [], 73 | }, 74 | })) 75 | .on(eventTriggered, (map, {name, params}) => { 76 | // should not change the order of fields 77 | const safeParams = params === undefined ? undefined : clone(params); 78 | map[name] = { 79 | ...map[name], 80 | history: [safeParams, ...map[name].history], 81 | }; 82 | return {...map}; 83 | }); 84 | 85 | $files.on(eventAdd, (map, {name, file}) => { 86 | if (file) { 87 | if (map[file]) { 88 | const list = map[file]; 89 | return {...map, [file]: [...list, {kind: 'event', name}]}; 90 | } 91 | 92 | return {...map, [file]: [{kind: 'event', name}]}; 93 | } 94 | 95 | return map; 96 | }); 97 | 98 | $effects 99 | .on(effectAdd, (map, effect) => ({ 100 | ...map, 101 | [effect.sid]: { 102 | name: effect.name, 103 | effect: effect.effect, 104 | inFlight: effect.effect.inFlight.getState(), 105 | }, 106 | })) 107 | .on(effectTriggered, (map, {sid}) => { 108 | const fx = map[sid]; 109 | map[sid] = { 110 | ...fx, 111 | inFlight: fx.effect.inFlight.getState(), 112 | }; 113 | return {...map}; 114 | }); 115 | 116 | $files.on(effectAdd, (map, {sid: name, file}) => { 117 | if (file) { 118 | if (map[file]) { 119 | const list = map[file]; 120 | return {...map, [file]: [...list, {kind: 'effect', name}]}; 121 | } 122 | 123 | return {...map, [file]: [{kind: 'effect', name}]}; 124 | } 125 | return map; 126 | }); 127 | 128 | forward({ 129 | from: eventTriggered, 130 | to: createLogRecordFx.prepend(({name, params}) => ({ 131 | kind: 'event', 132 | name, 133 | payload: params, 134 | })), 135 | }); 136 | 137 | forward({ 138 | from: storeUpdated, 139 | to: createLogRecordFx.prepend(({name, value}) => ({ 140 | kind: 'store', 141 | name, 142 | payload: value, 143 | })), 144 | }); 145 | 146 | function graphite(unit: Unit): Node { 147 | return (unit as any).graphite; 148 | } 149 | 150 | function traceEffect(effect: Effect) { 151 | const name = createName(effect); 152 | graphite(effect).seq.unshift( 153 | step.compute({ 154 | fn(data, scope, stack) { 155 | traceEffectRun({type: 'effect', name, argument: data?.param}); 156 | return data; 157 | }, 158 | }), 159 | ); 160 | traceEvent(effect.doneData, `${name}.doneData`); 161 | traceEvent(effect.failData, `${name}.failData`); 162 | } 163 | 164 | function traceEvent(event: Event, name = createName(event)) { 165 | graphite(event).seq.unshift( 166 | step.compute({ 167 | fn(data, scope, stack) { 168 | traceEventTrigger({type: 'event', name, argument: data}); 169 | return data; 170 | }, 171 | }), 172 | ); 173 | } 174 | 175 | function traceStore($store: Store) { 176 | const name = createName($store); 177 | 178 | let before: unknown; 179 | 180 | graphite($store).seq.unshift( 181 | step.compute({ 182 | fn(data, scope) { 183 | before = clone(scope.state.current); 184 | return data; 185 | }, 186 | }), 187 | ); 188 | 189 | createNode({ 190 | parent: [$store], 191 | meta: {op: 'watch'}, 192 | family: {owners: $store}, 193 | regional: true, 194 | node: [ 195 | step.run({ 196 | fn(data: any, scope: any, _stack: Stack) { 197 | traceStoreChange({ 198 | type: 'store', 199 | name, 200 | before: before, 201 | current: data, 202 | }); 203 | }, 204 | }), 205 | ], 206 | }); 207 | } 208 | 209 | export function createInspector(options: Options = {}): Inspector | undefined { 210 | const solidRoot = typeof document === 'object' && document.createElement('div'); 211 | if (!solidRoot) return undefined; 212 | setOptions(options); 213 | solidRoot.classList.add('effector-inspector-solid'); 214 | document.body.append(solidRoot); 215 | render(App, solidRoot); 216 | 217 | console.info( 218 | '%c[effector-inspector] %cPress %cCTRL+B %cto open Inspector', 219 | 'color: gray; font-size: 1rem;', 220 | 'color: currentColor; font-size: 1rem;', 221 | 'color: deepskyblue; font-family: monospace; font-size: 1rem;', 222 | 'color: currentColor; font-size: 1rem;', 223 | ); 224 | } 225 | 226 | function getLocFile(unit: Unit): string | undefined { 227 | return (unit as any).defaultConfig?.loc?.file; 228 | } 229 | 230 | export function addStore(store: Store, options: {mapped?: boolean; name?: string} = {}): void { 231 | const name = options.name || createName(store); 232 | const mapped = isDerived(store); 233 | 234 | storeAdd({ 235 | store, 236 | name, 237 | mapped: options.mapped || mapped, 238 | file: getLocFile(store), 239 | }); 240 | 241 | traceStore(store); 242 | 243 | forward({ 244 | from: store.updates.map((value) => ({name, value})), 245 | to: storeUpdated, 246 | }); 247 | } 248 | 249 | export function addEvent(event: Event, options: {mapped?: boolean; name?: string} = {}): void { 250 | const name = options.name || createName(event); 251 | const mapped = isDerived(event); 252 | 253 | eventAdd({ 254 | event, 255 | name, 256 | mapped: options.mapped || mapped, 257 | file: getLocFile(event), 258 | }); 259 | 260 | traceEvent(event); 261 | 262 | forward({ 263 | from: event.map((params) => ({ 264 | name, 265 | params, 266 | })), 267 | to: eventTriggered, 268 | }); 269 | } 270 | 271 | export function addEffect( 272 | effect: Effect, 273 | options: {attached?: boolean; sid?: string} = {}, 274 | ) { 275 | const name = createName(effect); 276 | const attached = isAttached(effect); 277 | const sid = options.sid || effect.sid || name; 278 | 279 | effectAdd({ 280 | effect, 281 | name, 282 | sid, 283 | attached: options.attached || attached, 284 | file: getLocFile(effect), 285 | }); 286 | 287 | traceEffect(effect); 288 | 289 | forward({ 290 | from: [effect, effect.finally], 291 | to: effectTriggered.prepend(() => ({sid})), 292 | }); 293 | 294 | const effectRun = effect.map((params) => ({ 295 | kind: 'effect' as Kind, 296 | name, 297 | payload: params, 298 | })); 299 | 300 | const effectDone = effect.done.map((params) => ({ 301 | kind: 'effect' as Kind, 302 | name: name + '.done', 303 | payload: params, 304 | })); 305 | 306 | const effectFail = effect.fail.map((params) => ({ 307 | kind: 'effect' as Kind, 308 | name: name + '.fail', 309 | payload: params, 310 | })); 311 | 312 | forward({ 313 | from: [effectRun, effectDone, effectFail], 314 | to: createLogRecordFx, 315 | }); 316 | } 317 | 318 | /** 319 | * 320 | * Attaches inspector to provided units 321 | * 322 | * @param root {Unit[]} - units to attach inspector to 323 | */ 324 | export function attachInspector(targets: Unit[]): void; 325 | /** 326 | * 327 | * Attaches inspector to provided effect. 328 | * 329 | * @param root {Store} - effect to attach inspector to 330 | */ 331 | export function attachInspector(target: Effect): void; 332 | /** 333 | * 334 | * Attaches inspector to provided event. 335 | * 336 | * @param root {Event} - event to attach inspector to 337 | */ 338 | export function attachInspector(target: Event): void; 339 | /** 340 | * 341 | * Attaches inspector to provided store. 342 | * 343 | * @param root {Store} - store to attach inspector to 344 | */ 345 | export function attachInspector(target: Store): void; 346 | /** 347 | * 348 | * Attaches inspector to provided domain - all units inside will be tracked 349 | * 350 | * @param root {Domain} - domain to attach inspector to 351 | */ 352 | export function attachInspector(root: Domain): void; 353 | export function attachInspector(entries: Unit | Array>) { 354 | const targets = Array.isArray(entries) ? entries : [entries]; 355 | 356 | targets.forEach((unit) => { 357 | if (is.domain(unit)) { 358 | unit.onCreateStore(addStore); 359 | unit.onCreateEvent(addEvent); 360 | unit.onCreateEffect(addEffect); 361 | } 362 | if (is.store(unit)) { 363 | addStore(unit); 364 | } 365 | if (is.effect(unit)) { 366 | addEffect(unit); 367 | } 368 | if (is.event(unit)) { 369 | addEvent(unit); 370 | } 371 | }); 372 | } 373 | 374 | function createName(unit: T): string { 375 | return unit.compositeName.path.join('/'); 376 | } 377 | 378 | function isDerived(store: Store | Event): boolean { 379 | return Boolean((store as any).graphite.meta.derived); 380 | } 381 | 382 | function isAttached(effect: Effect): boolean { 383 | return Boolean((effect as any).graphite.meta.attached); 384 | } 385 | --------------------------------------------------------------------------------