├── .eslintignore ├── usage ├── .babelrc ├── index.html ├── api │ └── session.ts ├── pages │ └── home.ts ├── vite.config.js ├── features │ └── session.ts ├── tsconfig.json └── index.ts ├── .babelrc ├── .commitlintrc.json ├── .huskyrc ├── .prettierrc ├── .github ├── pr-labeler.yml ├── workflows │ ├── release-drafter.yml │ ├── test.yml │ └── publish.yml ├── FUNDING.yml └── release-drafter.yml ├── .editorconfig ├── .eslintrc.json ├── .all-contributorsrc ├── tsconfig.json ├── src ├── lib.ts ├── index.ts ├── logger.ts └── basic.test.ts ├── babel.config.js ├── package.json ├── .gitignore ├── scripts └── build.ts ├── __snapshots__ ├── babel-plugin.test.js.snap └── macro.test.js.snap └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /usage/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["effector/babel-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [["effector/babel-plugin", { "addLoc": true, "addNames": true }]] 3 | } 4 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "arrowParens": "always", 7 | "tabWidth": 2, 8 | "useTabs": false 9 | } 10 | -------------------------------------------------------------------------------- /.github/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | feature: ['feature/*', 'feat/*', 'add-', 'create-'] 2 | test: ['test/*', 'tests/*'] 3 | fix: ['fix/*', 'hotfix/*', 'bugfix/*', 'fix-'] 4 | chore: chore/* 5 | refactor: ['refactor/*', 'refactoring/*'] 6 | -------------------------------------------------------------------------------- /usage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Effector Inspector Usage App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /usage/api/session.ts: -------------------------------------------------------------------------------- 1 | import { createDomain } from 'effector'; 2 | 3 | const api = createDomain(); 4 | 5 | export const sessionFetch = api.createEffect({ 6 | handler: (params) => ({ id: 100000 + params }), 7 | }); 8 | -------------------------------------------------------------------------------- /usage/pages/home.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, forward } from 'effector'; 2 | import { sessionFetch } from '../api/session'; 3 | 4 | export const pageMounted = createEvent(); 5 | 6 | forward({ 7 | from: pageMounted, 8 | to: sessionFetch, 9 | }); 10 | -------------------------------------------------------------------------------- /usage/vite.config.js: -------------------------------------------------------------------------------- 1 | import { babel } from '@rollup/plugin-babel'; 2 | import { defineConfig } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [babel({ extensions: ['.ts'], babelHelpers: 'bundled' })], 7 | }); 8 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /usage/features/session.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'effector'; 2 | import { sessionFetch } from '../api/session'; 3 | 4 | export const $session = createStore(null); 5 | 6 | $session.on(sessionFetch.doneData, (_, result) => result); 7 | 8 | setTimeout(() => { 9 | const $asyncLoadedStore = createStore('some string'); 10 | 11 | $asyncLoadedStore.on(sessionFetch.doneData, (result) => `${Math.random()}`); 12 | }, 1500); 13 | -------------------------------------------------------------------------------- /usage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": false, 8 | "jsx": "react", 9 | "lib": ["dom", "dom.iterable", "esnext"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true, 14 | "strict": true, 15 | "baseUrl": ".", 16 | "rootDir": "." 17 | }, 18 | "include": ["./src"] 19 | } 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: sergeysova 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://www.buymeacoffee.com/sergeysova', 'https://vk.com/sovadev'] 13 | -------------------------------------------------------------------------------- /usage/index.ts: -------------------------------------------------------------------------------- 1 | import { fork, scopeBind } from 'effector'; 2 | import { attachLogger } from '../src'; 3 | 4 | import { $session } from './features/session'; 5 | import { pageMounted } from './pages/home'; 6 | 7 | const scope = fork({ 8 | values: [[$session, { id: 'lol' }]], 9 | }); 10 | 11 | attachLogger({ 12 | scope, 13 | }); 14 | 15 | attachLogger({ 16 | name: 'my-cool-app', 17 | }); 18 | 19 | setTimeout(pageMounted, 500, Math.floor(Math.random() * 3000)); 20 | setTimeout(pageMounted, 1000, Math.floor(Math.random() * 3000)); 21 | setTimeout(pageMounted, 1500, Math.floor(Math.random() * 3000)); 22 | setTimeout(pageMounted, 2000, Math.floor(Math.random() * 3000)); 23 | setTimeout(scopeBind(pageMounted, { scope }), 2500, Math.floor(Math.random() * 3000)); 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test-package: 7 | name: Test on Node v${{ matrix.node }} and ${{ matrix.os }} 8 | 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | node: ['16.x', '18.x', '20.x'] 13 | os: [ubuntu-latest, macOS-latest] 14 | 15 | steps: 16 | - name: 🛎️ Checkout 17 | uses: actions/checkout@v3 18 | 19 | - name: 📦 Setup pnpm 20 | uses: pnpm/action-setup@v2 21 | 22 | - name: 🐧 Use Node.js ${{ matrix.node }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node }} 26 | cache: 'pnpm' 27 | 28 | - name: 🔍 Install dependencies 29 | run: pnpm install 30 | 31 | - name: 🔧 Build package 32 | run: pnpm build 33 | 34 | - name: 🧪 Test 35 | run: pnpm test 36 | env: 37 | CI: true 38 | -------------------------------------------------------------------------------- /.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 | "parser": "@babel/eslint-parser", 15 | "overrides": [ 16 | { 17 | "files": ["*.ts", "*.tsx"], 18 | "extends": [ 19 | "plugin:@typescript-eslint/eslint-recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 22 | ], 23 | "parser": "@typescript-eslint/parser", 24 | "parserOptions": { 25 | "project": "tsconfig.json", 26 | "createDefaultProgram": true, 27 | "tsconfigRootDir": "./" 28 | }, 29 | "plugins": ["@typescript-eslint"] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "Laiff", 10 | "name": "Andrey Antropov", 11 | "avatar_url": "https://avatars0.githubusercontent.com/u/575885?v=4", 12 | "profile": "https://github.com/Laiff", 13 | "contributions": [ 14 | "code" 15 | ] 16 | }, 17 | { 18 | "login": "sergeysova", 19 | "name": "Sergey Sova", 20 | "avatar_url": "https://avatars0.githubusercontent.com/u/5620073?v=4", 21 | "profile": "https://sova.dev", 22 | "contributions": [ 23 | "code" 24 | ] 25 | }, 26 | { 27 | "login": "Sozonov", 28 | "name": "Sozonov", 29 | "avatar_url": "https://avatars2.githubusercontent.com/u/1931637?v=4", 30 | "profile": "https://github.com/Sozonov", 31 | "contributions": [ 32 | "code" 33 | ] 34 | } 35 | ], 36 | "contributorsPerLine": 7, 37 | "projectName": "effector-logger", 38 | "projectOwner": "sergeysova", 39 | "repoType": "github", 40 | "repoHost": "https://github.com", 41 | "skipCi": true 42 | } 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": false, 8 | "jsx": "react", 9 | "lib": ["dom", "dom.iterable", "esnext"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true, 14 | "strict": true, 15 | "target": "es6", 16 | "declaration": true, 17 | "emitDeclarationOnly": true, 18 | "declarationDir": "./dist", 19 | "baseUrl": "./", 20 | "rootDir": "./src", 21 | "outDir": "./dist", 22 | "types": ["node", "jest"] 23 | }, 24 | "exclude": ["./dist"], 25 | "include": ["./src/index.ts"], 26 | "ts-node": { 27 | // It is faster to skip typechecking. 28 | // Remove if you want ts-node to do typechecking. 29 | "transpileOnly": true, 30 | 31 | "compilerOptions": { 32 | // compilerOptions specified here will override those declared below, 33 | // but *only* in ts-node. Useful if you want ts-node and tsc to use 34 | // different options with a single tsconfig.json. 35 | "module": "CommonJS" 36 | } 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /.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 v20.x 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: v20.x 21 | cache: 'pnpm' 22 | 23 | - name: 🔍 Install dependencies 24 | run: pnpm install 25 | 26 | - name: 🔧 Build package 27 | run: pnpm build 28 | 29 | - name: 📜 Copy README 30 | run: cp ./README.md ./dist/ 31 | 32 | - name: 🧪 Test 33 | run: pnpm test 34 | env: 35 | CI: true 36 | 37 | - name: 🔦 Extract version 38 | id: version 39 | uses: olegtarasov/get-tag@v2.1 40 | with: 41 | tagRegex: 'v(.*)' 42 | 43 | - name: 🥤 Set version from release 44 | uses: reedyuk/npm-version@1.1.1 45 | with: 46 | version: ${{ steps.version.outputs.tag }} 47 | git-tag-version: false 48 | package: 'dist/' 49 | 50 | - name: 📝 Create NPM config 51 | working-directory: './dist/' 52 | run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 53 | env: 54 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 55 | 56 | - name: 🚀 Publish package 57 | working-directory: './dist/' 58 | run: npm publish 59 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | /* eslint-disable @typescript-eslint/explicit-function-return-type */ 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | /* eslint-disable @typescript-eslint/no-use-before-define */ 5 | import type { Message, Declaration } from 'effector/inspect'; 6 | import type { Unit, Node, Scope } from 'effector'; 7 | 8 | export function getName(m: Message | Declaration, loggerName: string) { 9 | const finalName = m.name || locToString(m.loc) || `unknown_${m.id}`; 10 | 11 | if (!loggerName) return finalName; 12 | 13 | return `(${loggerName}) ${finalName}`; 14 | } 15 | export function locToString(loc: any) { 16 | if (!loc) return null; 17 | return `${loc.file}:${loc.line}:${loc.column}`; 18 | } 19 | export function getNode(unit: Unit) { 20 | return (unit as any).graphite as Node; 21 | } 22 | 23 | export function getStateFromDeclaration(d: Declaration, scope?: Scope) { 24 | if (scope) { 25 | return HACK_readStateFromScope(d, scope); 26 | } 27 | 28 | return d.meta.defaultState; 29 | } 30 | 31 | function HACK_readStateFromScope(d: Declaration, scope: Scope) { 32 | const stateRefId = d.meta.rootStateRefId as any; 33 | 34 | const scopeRef = (scope as any).reg[stateRefId]; 35 | 36 | if (scopeRef) { 37 | return scopeRef.current; 38 | } 39 | 40 | if (stateRefId in (scope as any).values.idMap) { 41 | return { value: (scope as any).values.idMap[stateRefId] }; 42 | } 43 | 44 | const sid = d.meta.sid as any; 45 | 46 | if (sid && sid in (scope as any).values.sidMap) { 47 | return (scope as any).values.sidMap[sid]; 48 | } 49 | 50 | return d.meta.defaultState; 51 | } 52 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const { resolve: resolvePath } = require('path'); 2 | 3 | module.exports = (api) => { 4 | api && api.cache && api.cache.never && api.cache.never(); 5 | // const env = api.cache(() => process.env.NODE_ENV) 6 | return generateConfig(meta, babelConfig); 7 | }; 8 | 9 | const meta = { 10 | isEsm: true, 11 | }; 12 | 13 | function generateConfig(meta, config = babelConfig) { 14 | const result = {}; 15 | for (const key in config) { 16 | const value = config[key]; 17 | result[key] = typeof value === 'function' ? value(meta) : value; 18 | } 19 | return result; 20 | } 21 | 22 | module.exports.generateConfig = generateConfig; 23 | 24 | const aliases = {}; 25 | 26 | const babelConfig = { 27 | plugins(meta) { 28 | const alias = parseAliases(meta, aliases); 29 | const result = [ 30 | ['effector/babel-plugin', { addLoc: true }], 31 | [ 32 | 'babel-plugin-module-resolver', 33 | { 34 | alias, 35 | loglevel: 'silent', 36 | }, 37 | ], 38 | ]; 39 | 40 | return result; 41 | }, 42 | }; 43 | 44 | function parseAliases(meta, obj) { 45 | const result = {}; 46 | for (const key in obj) { 47 | const value = obj[key]; 48 | if (typeof value === 'function') { 49 | const name = value(meta); 50 | if (name === undefined || name === null) continue; 51 | result[key] = name; 52 | } else if (typeof value === 'object' && value !== null) { 53 | const name = applyPaths(value); 54 | if (name === undefined || name === null) continue; 55 | result[key] = name; 56 | } else { 57 | const name = value; 58 | if (name === undefined || name === null) continue; 59 | result[key] = name; 60 | } 61 | } 62 | return result; 63 | 64 | function applyPaths(paths) { 65 | if (meta.isEsm) return paths.esm; 66 | return paths.default; 67 | } 68 | } 69 | 70 | module.exports.getAliases = (metadata = meta) => parseAliases(metadata, aliases); 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "effector-logger", 3 | "version": "0.0.0-real-version-will-be-set-on-ci", 4 | "main": "./index.js", 5 | "module": "./index.mjs", 6 | "typings": "./index.d.ts", 7 | "license": "MIT", 8 | "repository": "github:effector/logger", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "start": "vite ./usage", 14 | "build": "rm -rf ./dist && ts-node ./scripts/build.ts", 15 | "lint": "eslint --ext .ts,.tsx src", 16 | "test": "vitest run", 17 | "commit": "git-cz", 18 | "prepublish": "pnpm build" 19 | }, 20 | "packageManager": "pnpm@8.11.0", 21 | "peerDependencies": { 22 | "effector": "^22.8.8 || ^23.0.0" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.13.10", 26 | "@babel/eslint-parser": "^7.13.10", 27 | "@commitlint/cli": "8.2.0", 28 | "@commitlint/config-conventional": "8.2.0", 29 | "@rollup/plugin-babel": "^5.3.1", 30 | "@rollup/plugin-commonjs": "^20.0.0", 31 | "@rollup/plugin-node-resolve": "^13.0.4", 32 | "@rollup/plugin-typescript": "^8.2.5", 33 | "@types/babel__core": "^7.1.19", 34 | "@types/detect-node": "^2.0.0", 35 | "@types/jest": "^29.4.0", 36 | "@types/node": "^18.14.2", 37 | "@types/set-value": "^2.0.0", 38 | "@typescript-eslint/eslint-plugin": "^2.16.0", 39 | "@typescript-eslint/parser": "^2.16.0", 40 | "babel-plugin-module-resolver": "^4.1.0", 41 | "babel-plugin-tester": "^10.0.0", 42 | "commitizen": "4.0.3", 43 | "cz-conventional-changelog": "3.0.2", 44 | "effector": "^23.0.0", 45 | "eslint": "^6.6.0", 46 | "eslint-plugin-prettier": "^3.1.2", 47 | "husky": "3.1.0", 48 | "jest": "^29.3.1", 49 | "prettier": "^2.1.2", 50 | "rollup": "^2.79.1", 51 | "rollup-plugin-terser": "^7.0.2", 52 | "ts-node": "^10.4.0", 53 | "typescript": "^4.9.5", 54 | "vite": "^3.1.8", 55 | "vitest": "^0.31.0" 56 | }, 57 | "dependencies": { 58 | "detect-node": "2.0.4", 59 | "just-debounce-it": "^1.1.0" 60 | }, 61 | "config": { 62 | "commitizen": { 63 | "path": "cz-conventional-changelog" 64 | } 65 | }, 66 | "files": [ 67 | "index.js", 68 | "index.js.map", 69 | "index.mjs", 70 | "index.mjs.map", 71 | "index.d.ts" 72 | ], 73 | "exports": { 74 | ".": { 75 | "types": "./index.d.ts", 76 | "import": "./index.mjs", 77 | "require": "./index.js", 78 | "default": "./index.mjs" 79 | }, 80 | "./index.mjs": "./index.mjs" 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 | dist-test 165 | ### Jetbrains 166 | 167 | /.idea 168 | 169 | # End of https://www.gitignore.io/api/git,node,react,macos,visualstudiocode 170 | 171 | # Built package artifacts 172 | attach.js 173 | attach.js.map 174 | attach.mjs 175 | attach.mjs.map 176 | index.js 177 | index.js.map 178 | index.mjs 179 | index.mjs.map 180 | inspector.js 181 | inspector.js.map 182 | inspector.mjs 183 | inspector.mjs.map 184 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */ 2 | /* eslint-disable @typescript-eslint/no-use-before-define */ 3 | import fs from 'node:fs'; 4 | import { promisify } from 'util'; 5 | import { rollup, InputOptions, OutputOptions } from 'rollup'; 6 | import { resolve } from 'path'; 7 | import pluginResolve from '@rollup/plugin-node-resolve'; 8 | import { babel } from '@rollup/plugin-babel'; 9 | import commonjs from '@rollup/plugin-commonjs'; 10 | import Package from '../package.json'; 11 | import typescript from '@rollup/plugin-typescript'; 12 | import { generateConfig } from '../babel.config'; 13 | 14 | const DIR = 'dist/'; 15 | const LIB_NAME = process.env.LIB_NAME ?? Package.name; 16 | 17 | async function build(): Promise { 18 | const configs = getAllConfigs(); 19 | 20 | for (const config of configs) { 21 | await buildEntry(config); 22 | } 23 | const packageJson = JSON.parse(JSON.stringify(Package)); 24 | packageJson.name = LIB_NAME; 25 | delete packageJson['scripts']; 26 | delete packageJson['config']; 27 | delete packageJson.devDependencies; 28 | 29 | await saveFile(`${DIR}package.json`, JSON.stringify(packageJson)); 30 | } 31 | 32 | async function buildEntry(config: ReturnType) { 33 | console.log('building: ', `src/${config.name}.ts`, '->', config.format); 34 | const bundle = await rollup(config.input); 35 | console.log('saving: ', config.output.file); 36 | await bundle.write(config.output); 37 | console.log(''); 38 | } 39 | 40 | const entrypoints = ['index'] as const; 41 | const formats = ['cjs', 'esm'] as const; 42 | 43 | function getAllConfigs() { 44 | const configs: ReturnType[] = []; 45 | entrypoints.forEach((name) => { 46 | formats.forEach((format) => { 47 | configs.push( 48 | getConfig({ 49 | name, 50 | format, 51 | base: DIR, 52 | }), 53 | ); 54 | }); 55 | }); 56 | 57 | return configs; 58 | } 59 | 60 | function getConfig(config: { name: string; format: 'esm' | 'cjs'; base: string }) { 61 | const { name, format, base } = config; 62 | const input = getInput(name, format); 63 | const output = getOutput(name, format, base); 64 | 65 | return { 66 | input, 67 | output, 68 | name, 69 | format, 70 | }; 71 | } 72 | 73 | const extensions = ['.tsx', '.ts', '.js', '.json']; 74 | const external = [ 75 | 'forest/forest.mjs', 76 | 'effector/effector.mjs', 77 | 'effector-inspector/index.mjs', 78 | ].concat(Object.keys(Package.peerDependencies), Object.keys(Package.dependencies)); 79 | 80 | function getInput(name: string, format: 'esm' | 'cjs'): InputOptions { 81 | const input = resolve(__dirname, `../src/${name}.ts`); 82 | const plugins = [ 83 | commonjs(), 84 | pluginResolve({ extensions }), 85 | typescript({ 86 | tsconfig: './tsconfig.json', 87 | }), 88 | babel({ 89 | babelHelpers: 'bundled', 90 | extensions, 91 | skipPreflightCheck: true, 92 | babelrc: false, 93 | ...generateConfig({ 94 | isEsm: format === 'esm', 95 | }), 96 | }), 97 | ]; 98 | return { 99 | input, 100 | plugins, 101 | external, 102 | }; 103 | } 104 | 105 | function getOutput(name: string, format: 'esm' | 'cjs', base = ''): OutputOptions { 106 | const file = `${base}${name}.${format === 'esm' ? 'mjs' : 'js'}`; 107 | return { 108 | file, 109 | sourcemap: true, 110 | format, 111 | }; 112 | } 113 | 114 | const saveFile = promisify(fs.writeFile); 115 | 116 | build() 117 | .then(() => { 118 | process.exit(0); 119 | }) 120 | .catch((error) => { 121 | // eslint-disable-next-line no-console 122 | console.error(error); 123 | process.exit(-1); 124 | }); 125 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | /* eslint-disable @typescript-eslint/no-use-before-define */ 4 | import { inspect, Message, inspectGraph } from 'effector/inspect'; 5 | import type { Scope, Unit } from 'effector'; 6 | import { is } from 'effector'; 7 | 8 | import { 9 | storeUpdated, 10 | eventCalled, 11 | effectCalled, 12 | effectDone, 13 | effectFail, 14 | createDeclarationsLogger, 15 | } from './logger'; 16 | import { getNode, getName, locToString } from './lib'; 17 | 18 | const ignored = new Set(); 19 | const forceLog = new Set(); 20 | const fxRunning = new Map(); 21 | 22 | export function attachLogger(config: { scope?: Scope; name?: string } = {}): () => void { 23 | const name = config.name || (config.scope ? `scope: ${getNode(config.scope).id}` : ''); 24 | 25 | const logDeclarations = createDeclarationsLogger({ 26 | name, 27 | scope: config.scope, 28 | }); 29 | 30 | const uninspect = inspect({ 31 | scope: config.scope || undefined, 32 | fn: (m) => { 33 | if ( 34 | /** 35 | * Log only non-derived units by default 36 | */ 37 | (isLoggable(m) && !ignored.has(m.id)) || 38 | /** 39 | * Log any units if they are forced to be logged 40 | */ 41 | forceLog.has(m.id) 42 | ) { 43 | log(m, name); 44 | } 45 | }, 46 | }); 47 | 48 | logDeclarations(); 49 | 50 | const uninspectGraph = inspectGraph({ 51 | fn: () => { 52 | logDeclarations(); 53 | }, 54 | }); 55 | 56 | return () => { 57 | uninspect(); 58 | uninspectGraph(); 59 | }; 60 | } 61 | 62 | export function configure( 63 | unitOrUnits: Unit | Unit[], 64 | config: { log: 'disabled' | 'enabled' }, 65 | ): void { 66 | const units = Array.isArray(unitOrUnits) ? unitOrUnits : [unitOrUnits]; 67 | 68 | if (config.log === 'disabled') { 69 | units.forEach((unit) => { 70 | ignored.add(getNode(unit).id); 71 | 72 | if (is.effect(unit)) { 73 | ignored.add(getNode(unit.finally).id); 74 | } 75 | }); 76 | } 77 | if (config.log === 'enabled') { 78 | units.forEach((unit) => { 79 | forceLog.add(getNode(unit).id); 80 | 81 | if (is.effect(unit)) { 82 | forceLog.add(getNode(unit.finally).id); 83 | } 84 | }); 85 | } 86 | } 87 | 88 | // utils 89 | function isLoggable(m: Message) { 90 | return isEffectFinally(m) || (!m.meta.derived && m.type === 'update'); 91 | } 92 | function isEffectFinally(m: Message) { 93 | return m.kind === 'event' && m.meta.named === 'finally'; 94 | } 95 | function log(m: Message, name: string) { 96 | if (m.kind === 'effect') { 97 | const fxName = getName(m, name); 98 | 99 | effectCalled(fxName, locToString(m.loc) || '', m.value); 100 | fxRunning.set((m.stack as any).fxID, fxName); 101 | 102 | return; 103 | } 104 | if (isEffectFinally(m)) { 105 | const fxName = fxRunning.get((m.stack as any).fxID); 106 | if (fxName) { 107 | fxRunning.delete((m.stack as any).fxID); 108 | 109 | const finallyValue = m.value as { 110 | status: 'fail' | 'done'; 111 | params: any; 112 | result: any; 113 | error: any; 114 | }; 115 | 116 | if (finallyValue.status === 'fail') { 117 | effectFail(fxName, locToString(m.loc) || '', finallyValue.params, finallyValue.error); 118 | } 119 | if (finallyValue.status === 'done') { 120 | effectDone(fxName, locToString(m.loc) || '', finallyValue.params, finallyValue.result); 121 | } 122 | } 123 | 124 | return; 125 | } 126 | if (m.kind === 'store') { 127 | storeUpdated(getName(m, name), locToString(m.loc) || '', m.value); 128 | 129 | return; 130 | } 131 | if (m.kind === 'event') { 132 | eventCalled(getName(m, name), locToString(m.loc) || '', m.value); 133 | 134 | return; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /__snapshots__/babel-plugin.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`effector-logger/babel-plugin adds inspector: adds inspector 1`] = ` 4 | 5 | import { createStore, createEvent, createEffect, attach, restore } from 'effector'; 6 | const fx = createEffect(() => 1); 7 | const $data = createStore(0); 8 | const was = createEvent(); 9 | const anotherFx = attach({ 10 | effect: fx, 11 | source: $data, 12 | mapParams: (a) => a, 13 | }) 14 | const $has = restore(was, ""); 15 | 16 | ↓ ↓ ↓ ↓ ↓ ↓ 17 | 18 | var _effectorFileName = '/babel-plugin.test.js'; 19 | import 'effector-logger/inspector'; 20 | import { createStore, createEvent, createEffect, attach, restore } from 'effector-logger'; 21 | const fx = createEffect(() => 1, { 22 | loc: { 23 | file: _effectorFileName, 24 | line: 2, 25 | column: 11, 26 | }, 27 | name: 'fx', 28 | sid: '-vy4jz4', 29 | }); 30 | const $data = createStore(0, { 31 | loc: { 32 | file: _effectorFileName, 33 | line: 3, 34 | column: 14, 35 | }, 36 | name: '$data', 37 | sid: 'ais0rc', 38 | }); 39 | const was = createEvent({ 40 | loc: { 41 | file: _effectorFileName, 42 | line: 4, 43 | column: 12, 44 | }, 45 | name: 'was', 46 | sid: '-jc3cc', 47 | }); 48 | const anotherFx = attach({ 49 | ɔ: { 50 | effect: fx, 51 | source: $data, 52 | mapParams: (a) => a, 53 | }, 54 | config: { 55 | loc: { 56 | file: _effectorFileName, 57 | line: 5, 58 | column: 18, 59 | }, 60 | name: 'anotherFx', 61 | sid: '-o67sjr', 62 | }, 63 | }); 64 | const $has = restore(was, '', { 65 | loc: { 66 | file: _effectorFileName, 67 | line: 10, 68 | column: 13, 69 | }, 70 | name: '$has', 71 | sid: '3zimrf', 72 | }); 73 | 74 | 75 | `; 76 | 77 | exports[`effector-logger/babel-plugin adds loc and names: adds loc and names 1`] = ` 78 | 79 | import { createStore, createEvent, createEffect, attach, restore } from 'effector'; 80 | const fx = createEffect(() => 1); 81 | const $data = createStore(0); 82 | const was = createEvent(); 83 | const anotherFx = attach({ 84 | effect: fx, 85 | source: $data, 86 | mapParams: (a) => a, 87 | }) 88 | const $has = restore(was, ""); 89 | 90 | ↓ ↓ ↓ ↓ ↓ ↓ 91 | 92 | var _effectorFileName = '/babel-plugin.test.js'; 93 | import { createStore, createEvent, createEffect, attach, restore } from 'effector-logger'; 94 | const fx = createEffect(() => 1, { 95 | loc: { 96 | file: _effectorFileName, 97 | line: 2, 98 | column: 11, 99 | }, 100 | name: 'fx', 101 | sid: '-vy4jz4', 102 | }); 103 | const $data = createStore(0, { 104 | loc: { 105 | file: _effectorFileName, 106 | line: 3, 107 | column: 14, 108 | }, 109 | name: '$data', 110 | sid: 'ais0rc', 111 | }); 112 | const was = createEvent({ 113 | loc: { 114 | file: _effectorFileName, 115 | line: 4, 116 | column: 12, 117 | }, 118 | name: 'was', 119 | sid: '-jc3cc', 120 | }); 121 | const anotherFx = attach({ 122 | ɔ: { 123 | effect: fx, 124 | source: $data, 125 | mapParams: (a) => a, 126 | }, 127 | config: { 128 | loc: { 129 | file: _effectorFileName, 130 | line: 5, 131 | column: 18, 132 | }, 133 | name: 'anotherFx', 134 | sid: '-o67sjr', 135 | }, 136 | }); 137 | const $has = restore(was, '', { 138 | loc: { 139 | file: _effectorFileName, 140 | line: 10, 141 | column: 13, 142 | }, 143 | name: '$has', 144 | sid: '3zimrf', 145 | }); 146 | 147 | 148 | `; 149 | 150 | exports[`effector-logger/babel-plugin replaces imports from effector: replaces imports from effector 1`] = ` 151 | 152 | import { createStore, createEvent, createEffect, attach, restore } from 'effector'; 153 | const fx = createEffect(() => 1); 154 | const $data = createStore(0); 155 | const was = createEvent(); 156 | const anotherFx = attach({ 157 | effect: fx, 158 | source: $data, 159 | mapParams: (a) => a, 160 | }) 161 | const $has = restore(was, ""); 162 | 163 | ↓ ↓ ↓ ↓ ↓ ↓ 164 | 165 | var _effectorFileName = '/babel-plugin.test.js'; 166 | import { createStore, createEvent, createEffect, attach, restore } from 'effector-logger'; 167 | const fx = createEffect(() => 1, { 168 | loc: { 169 | file: _effectorFileName, 170 | line: 2, 171 | column: 11, 172 | }, 173 | name: 'fx', 174 | sid: '-vy4jz4', 175 | }); 176 | const $data = createStore(0, { 177 | loc: { 178 | file: _effectorFileName, 179 | line: 3, 180 | column: 14, 181 | }, 182 | name: '$data', 183 | sid: 'ais0rc', 184 | }); 185 | const was = createEvent({ 186 | loc: { 187 | file: _effectorFileName, 188 | line: 4, 189 | column: 12, 190 | }, 191 | name: 'was', 192 | sid: '-jc3cc', 193 | }); 194 | const anotherFx = attach({ 195 | ɔ: { 196 | effect: fx, 197 | source: $data, 198 | mapParams: (a) => a, 199 | }, 200 | config: { 201 | loc: { 202 | file: _effectorFileName, 203 | line: 5, 204 | column: 18, 205 | }, 206 | name: 'anotherFx', 207 | sid: '-o67sjr', 208 | }, 209 | }); 210 | const $has = restore(was, '', { 211 | loc: { 212 | file: _effectorFileName, 213 | line: 10, 214 | column: 13, 215 | }, 216 | name: '$has', 217 | sid: '3zimrf', 218 | }); 219 | 220 | 221 | `; 222 | -------------------------------------------------------------------------------- /__snapshots__/macro.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`effector-logger/macro add sid and loc: add sid and loc 1`] = ` 4 | 5 | import { createStore, createEvent, createEffect, attach, restore } from './macro'; 6 | const fx = createEffect(() => 1); 7 | const $data = createStore(0); 8 | const was = createEvent(); 9 | const anotherFx = attach({ 10 | effect: fx, 11 | source: $data, 12 | mapParams: (a) => a, 13 | }) 14 | const $has = restore(was, ""); 15 | 16 | ↓ ↓ ↓ ↓ ↓ ↓ 17 | 18 | var _effectorFileName = '/macro.test.js'; 19 | import { restore as _restore } from 'effector-logger'; 20 | import { attach as _attach } from 'effector-logger'; 21 | import { createEffect as _createEffect } from 'effector-logger'; 22 | import { createEvent as _createEvent } from 'effector-logger'; 23 | import { createStore as _createStore } from 'effector-logger'; 24 | 25 | const fx = _createEffect(() => 1, { 26 | loc: { 27 | file: _effectorFileName, 28 | line: 2, 29 | column: 11, 30 | }, 31 | name: 'fx', 32 | sid: '-u6p3sc', 33 | }); 34 | 35 | const $data = _createStore(0, { 36 | loc: { 37 | file: _effectorFileName, 38 | line: 3, 39 | column: 14, 40 | }, 41 | name: '$data', 42 | sid: '8tv02c', 43 | }); 44 | 45 | const was = _createEvent({ 46 | loc: { 47 | file: _effectorFileName, 48 | line: 4, 49 | column: 12, 50 | }, 51 | name: 'was', 52 | sid: '-3z5gxy', 53 | }); 54 | 55 | const anotherFx = _attach({ 56 | ɔ: { 57 | effect: fx, 58 | source: $data, 59 | mapParams: (a) => a, 60 | }, 61 | config: { 62 | loc: { 63 | file: _effectorFileName, 64 | line: 5, 65 | column: 18, 66 | }, 67 | name: 'anotherFx', 68 | sid: '-4ce2k9', 69 | }, 70 | }); 71 | 72 | const $has = _restore(was, '', { 73 | loc: { 74 | file: _effectorFileName, 75 | line: 10, 76 | column: 13, 77 | }, 78 | name: '$has', 79 | sid: 'css95b', 80 | }); 81 | 82 | 83 | `; 84 | 85 | exports[`effector-logger/macro add sid, loc, and inspector: add sid, loc, and inspector 1`] = ` 86 | 87 | import { createStore, createEvent, createEffect, attach, restore } from './macro'; 88 | const fx = createEffect(() => 1); 89 | const $data = createStore(0); 90 | const was = createEvent(); 91 | const anotherFx = attach({ 92 | effect: fx, 93 | source: $data, 94 | mapParams: (a) => a, 95 | }) 96 | const $has = restore(was, ""); 97 | 98 | ↓ ↓ ↓ ↓ ↓ ↓ 99 | 100 | var _effectorFileName = '/macro.test.js'; 101 | import 'effector-logger/inspector'; 102 | import { restore as _restore } from 'effector-logger'; 103 | import { attach as _attach } from 'effector-logger'; 104 | import { createEffect as _createEffect } from 'effector-logger'; 105 | import { createEvent as _createEvent } from 'effector-logger'; 106 | import { createStore as _createStore } from 'effector-logger'; 107 | 108 | const fx = _createEffect(() => 1, { 109 | loc: { 110 | file: _effectorFileName, 111 | line: 2, 112 | column: 11, 113 | }, 114 | name: 'fx', 115 | sid: '-u6p3sc', 116 | }); 117 | 118 | const $data = _createStore(0, { 119 | loc: { 120 | file: _effectorFileName, 121 | line: 3, 122 | column: 14, 123 | }, 124 | name: '$data', 125 | sid: '8tv02c', 126 | }); 127 | 128 | const was = _createEvent({ 129 | loc: { 130 | file: _effectorFileName, 131 | line: 4, 132 | column: 12, 133 | }, 134 | name: 'was', 135 | sid: '-3z5gxy', 136 | }); 137 | 138 | const anotherFx = _attach({ 139 | ɔ: { 140 | effect: fx, 141 | source: $data, 142 | mapParams: (a) => a, 143 | }, 144 | config: { 145 | loc: { 146 | file: _effectorFileName, 147 | line: 5, 148 | column: 18, 149 | }, 150 | name: 'anotherFx', 151 | sid: '-4ce2k9', 152 | }, 153 | }); 154 | 155 | const $has = _restore(was, '', { 156 | loc: { 157 | file: _effectorFileName, 158 | line: 10, 159 | column: 13, 160 | }, 161 | name: '$has', 162 | sid: 'css95b', 163 | }); 164 | 165 | 166 | `; 167 | 168 | exports[`effector-logger/macro add sid: add sid 1`] = ` 169 | 170 | import { createStore, createEvent, createEffect, attach, restore } from './macro'; 171 | const fx = createEffect(() => 1); 172 | const $data = createStore(0); 173 | const was = createEvent(); 174 | const anotherFx = attach({ 175 | effect: fx, 176 | source: $data, 177 | mapParams: (a) => a, 178 | }) 179 | const $has = restore(was, ""); 180 | 181 | ↓ ↓ ↓ ↓ ↓ ↓ 182 | 183 | import { restore as _restore } from 'effector-logger'; 184 | import { attach as _attach } from 'effector-logger'; 185 | import { createEffect as _createEffect } from 'effector-logger'; 186 | import { createEvent as _createEvent } from 'effector-logger'; 187 | import { createStore as _createStore } from 'effector-logger'; 188 | 189 | const fx = _createEffect(() => 1, { 190 | name: 'fx', 191 | sid: '-u6p3sc', 192 | }); 193 | 194 | const $data = _createStore(0, { 195 | name: '$data', 196 | sid: '8tv02c', 197 | }); 198 | 199 | const was = _createEvent({ 200 | name: 'was', 201 | sid: '-3z5gxy', 202 | }); 203 | 204 | const anotherFx = _attach({ 205 | ɔ: { 206 | effect: fx, 207 | source: $data, 208 | mapParams: (a) => a, 209 | }, 210 | config: { 211 | name: 'anotherFx', 212 | sid: '-4ce2k9', 213 | }, 214 | }); 215 | 216 | const $has = _restore(was, '', { 217 | name: '$has', 218 | sid: 'css95b', 219 | }); 220 | 221 | 222 | `; 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Effector Logger 2 | 3 | Pretty logger for stores, events, effects and domains. 4 | 5 | 6 | 7 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) 8 | 9 | 10 | 11 | | ![Chrome-DevTools-Console](https://i.imgur.com/Pp4zPKy.png) | 12 | | ---------------------------------------------------------------- | 13 | | ![Chrome-DevTools-Console-Dark](https://i.imgur.com/Vg54DsD.png) | 14 | 15 | ## Installation 16 | 17 | ```bash 18 | npm add effector 19 | npm add --dev effector-logger 20 | ``` 21 | 22 | or yarn 23 | 24 | ```bash 25 | yarn add effector 26 | yarn add -D effector-logger 27 | ``` 28 | 29 | > Note: **effector-logger** requires `effector` to be installed 30 | 31 | ## Usage 32 | 33 | ### Prepare metadata 34 | 35 | To make logs more useful we need additional metadata (like names, locations in the code, etc), which is provided by one of the `effector` plugins. 36 | 37 | #### Babel-plugin 38 | 39 | Babel-plugin is built-in in the `effector` package. 40 | 41 | Just add it to your babel configuration. 42 | 43 | ```json 44 | { 45 | "plugins": ["effector/babel-plugin"] 46 | } 47 | ``` 48 | 49 | It is also useful to enable `loc` generation for dev environment, to see for exact locations of units in the code. 50 | 51 | ```json 52 | { 53 | "plugins": [["effector/babel-plugin", { "addLoc": true }]] 54 | } 55 | ``` 56 | 57 | [Read the docs](https://effector.dev/docs/api/effector/babel-plugin/#usage) 58 | 59 | #### SWC Plugin 60 | 61 | [Read effector SWC plugin documentation](https://github.com/effector/swc-plugin) 62 | 63 | ### Start logging 64 | 65 | Just call `attachLogger` in your entrypoint module. 66 | 67 | NOTE: To "see" the `createStore`, `createEvent`, etc calls `effector-logger` needs to be imported at the very top of your entrypoint module. This way initial states of stores will be properly logged at the moment of `attachLogger` call. 68 | 69 | Update logs are not affected by import order. 70 | 71 | ```ts 72 | // src/index.tsx 73 | import { attachLogger } from 'effector-logger'; 74 | 75 | import React from 'react'; 76 | import { createRoot } from 'react-dom/client'; 77 | import { App } from './app'; 78 | import { appStarted } from './shared/app-events'; 79 | 80 | attachLogger(); 81 | 82 | appStarted(); 83 | 84 | createRoot(document.querySelector('#app')).render(); 85 | ``` 86 | 87 | After that you will see the logs in your console. 88 | 89 | ### With `Scope` 90 | 91 | If your app uses scope (e.g. you have Server-Side-Rendering) - you will need to pass it to the logger to work. 92 | 93 | ```ts 94 | attachLogger({ scope }); 95 | ``` 96 | 97 | Updates related to provided scope will be prefixed with scope id. 98 | 99 | ### Name 100 | 101 | There optional `name` prefix to the logs. 102 | It can be useful if there are few instances of your app, which are using different scopes or if you just want prefix that is better than boring scope id. 103 | 104 | ```ts 105 | attachLogger({ 106 | scope, 107 | name: `my-cool-app-${appId}`, // all logs will be prefixed with this string 108 | }); 109 | ``` 110 | 111 | ### Stop logs 112 | 113 | To stop logs just call unsubscribe function. 114 | 115 | ```ts 116 | const unlogger = attachLogger(); 117 | 118 | unlogger(); 119 | ``` 120 | 121 | ### Hide any unit from log 122 | 123 | Sometimes it is required to hide some events or stores from log. 124 | It is simple to implement: just call `configure` on your unit. 125 | 126 | ```ts 127 | import { createEvent } from 'effector'; 128 | import { configure } from 'effector-logger'; 129 | import { $data, loadDataFx } from './model'; 130 | 131 | const pageMounted = createEvent(); 132 | 133 | configure(pageMounted, { log: 'disabled' }); 134 | 135 | // You can pass multiple units as array 136 | configure([$data, loadDataFx], { log: 'disabled' }); 137 | ``` 138 | 139 | ### Force any unit to be logged 140 | 141 | By default only non-derived units are logged. If you want to force some unit to be logged, use configure `enabled` 142 | 143 | ```ts 144 | import { createEvent } from 'effector'; 145 | import { configure } from 'effector-logger'; 146 | import { $data, loadDataFx } from './model'; 147 | 148 | const pageMounted = createEvent(); 149 | 150 | const mappedMounted = pageMounter.map((x) => x); 151 | 152 | configure(mappedMounted, { log: 'enabled' }); 153 | 154 | // You can pass multiple units as array 155 | configure([$data, loadDataFx], { log: 'enabled' }); 156 | ``` 157 | 158 | ## Redux DevTools support 159 | 160 | Redux DevTools is moved to a different package. 161 | 162 | See the [`@effector/redux-devtools-adapter`](https://github.com/effector/redux-devtools-adapter) 163 | 164 | ## Contributors ✨ 165 | 166 | Thanks go to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 |

Andrey Antropov

💻

Sergey Sova

💻

Sozonov

💻
178 | 179 | 180 | 181 | 182 | 183 | 184 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 185 | 186 | ## Release process 187 | 188 | 1. Check out the [draft release](https://github.com/effector/logger/releases). 189 | 1. All PRs should have correct labels and useful titles. You can [review available labels here](https://github.com/effector/logger/blob/master/.github/release-drafter.yml). 190 | 1. Update labels for PRs and titles, next [manually run the release drafter action](https://github.com/effector/logger/actions/workflows/release-drafter.yml) to regenerate the draft release. 191 | 1. Review the new version and press "Publish" 192 | 1. If required check "Create discussion for this release" 193 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import debounce from 'just-debounce-it'; 3 | import isNode from 'detect-node'; 4 | import { inspectGraph, Declaration } from 'effector/inspect'; 5 | import { Scope } from 'effector'; 6 | 7 | import { getName, locToString, getStateFromDeclaration } from './lib'; 8 | 9 | const SEPARATOR = isNode ? '  ' : ''; 10 | 11 | const storeListToInit: Array = []; 12 | const eventListToInit: Array = []; 13 | const effectListToInit: Array = []; 14 | 15 | inspectGraph({ 16 | fn: (d) => { 17 | if (!d.derived) { 18 | if (d.kind === 'store' && !d.meta.named) { 19 | storeListToInit.push(d); 20 | } 21 | if (d.kind === 'event' && !d.meta.named) { 22 | eventListToInit.push(d); 23 | } 24 | if (d.kind === 'effect' && !d.meta.named) { 25 | effectListToInit.push(d); 26 | } 27 | } 28 | }, 29 | }); 30 | 31 | const styles = { 32 | block: 'padding-left: 4px; padding-right: 4px; font-weight: normal;', 33 | chunk: 'padding-left: 4px; padding-right: 4px; font-weight: normal;', 34 | effector: 35 | 'line-height:1.5; color: #000; font-family: "Apple Emoji Font"; font-weight: normal !important;', 36 | new: 'background-color: #29b6f6; color: #000', 37 | store: 'background-color: #7e57c2; color: #fff', 38 | event: 'background-color: #9ccc65; color: #000', 39 | effect: 'background-color: #26a69a; color: #000', 40 | emoji: '', 41 | file: 'color: #9e9e9e; padding-left: 20px;', 42 | reset: 'color: currentColor; background-color: transparent;', 43 | }; 44 | 45 | const effectorLabel: Block[] = [ 46 | ['☄️', '%s', styles.effector], 47 | ['effector', '%s', 'font-family: Menlo, monospace;'], 48 | ]; 49 | 50 | const reset = (index: number, count: number, style: string): string => 51 | index === count - 1 ? styles.reset : style; 52 | 53 | type Block = [string, string, string]; 54 | type Chunk = [any, string, string]; 55 | 56 | function log( 57 | blocks: Block[], 58 | chunks: Chunk[], 59 | group: 'collapsed' | 'open' | void = undefined, 60 | ): void { 61 | const str: string[] = []; 62 | const params: any[] = []; 63 | 64 | blocks.unshift(...effectorLabel); 65 | 66 | blocks.forEach(([value, view, style], index) => { 67 | str.push(`%c${view}%c`); 68 | params.push( 69 | `${styles.block} ${style}`, 70 | value, 71 | reset(index, blocks.length, `${styles.block} ${style}`), 72 | ); 73 | }); 74 | 75 | chunks.forEach(([value, view, style]) => { 76 | str.push(`%c${view}`); 77 | params.push(`${styles.chunk} ${style}`, value); 78 | }); 79 | 80 | const args = [str.join(SEPARATOR), ...params]; 81 | 82 | if (group === 'open') { 83 | console.group(...args); 84 | } else if (group === 'collapsed') { 85 | console.groupCollapsed(...args); 86 | } else { 87 | console.log(...args); 88 | } 89 | } 90 | 91 | const blockNew: Block = ['new', '%s', styles.new]; 92 | 93 | const stripDomain = (name: string) => name.split('/').pop() || name; 94 | 95 | const createBlockStore: (name: string) => Block = (name) => [stripDomain(name), '%s', styles.store]; 96 | const createBlockEvent: (name: string) => Block = (name) => [stripDomain(name), '%s', styles.event]; 97 | const createBlockEffect: (name: string) => Block = (name) => [ 98 | stripDomain(name), 99 | '%s', 100 | styles.effect, 101 | ]; 102 | 103 | export const createDeclarationsLogger = (config: { name: string; scope?: Scope }) => 104 | debounce(() => { 105 | const stores = storeListToInit.splice(0, storeListToInit.length); 106 | const events = eventListToInit.splice(0, eventListToInit.length); 107 | const effects = effectListToInit.splice(0, effectListToInit.length); 108 | 109 | if (stores.length + events.length + effects.length > 0) { 110 | log( 111 | [blockNew], 112 | [ 113 | ['Initialized', '%s', ''], 114 | [`events(${events.length})`, '%s', ''], 115 | [`effects(${effects.length})`, '%s', ''], 116 | [`stores(${stores.length})`, '%s', ''], 117 | ], 118 | 'collapsed', 119 | ); 120 | 121 | if (stores.length) { 122 | stores.forEach((store) => { 123 | const name = getName(store, config.name); 124 | const fileName = locToString(store.loc); 125 | 126 | log( 127 | [blockNew, createBlockStore(name)], 128 | [ 129 | ['-> ', '%s', ''], 130 | [getStateFromDeclaration(store, config.scope), '%o', ''], 131 | [fileName, '%s', styles.file], 132 | [name, '%s', ''], 133 | ], 134 | ); 135 | }); 136 | } 137 | if (events.length > 0) { 138 | events.forEach((event) => { 139 | const name = getName(event, config.name); 140 | const fileName = locToString(event.loc); 141 | 142 | log( 143 | [blockNew, createBlockEvent(name)], 144 | [ 145 | [fileName, '%s', styles.file], 146 | [name, '%s', ''], 147 | ], 148 | ); 149 | }); 150 | } 151 | if (effects.length > 0) { 152 | effects.forEach((effect) => { 153 | const name = getName(effect, config.name); 154 | const fileName = locToString(effect.loc); 155 | 156 | log( 157 | [blockNew, createBlockEffect(name)], 158 | [ 159 | [fileName, '%s', styles.file], 160 | [name, '%s', ''], 161 | ], 162 | ); 163 | }); 164 | } 165 | console.groupEnd(); 166 | } 167 | }, 5); 168 | 169 | export function storeUpdated(name: string, fileName: string, value: any): void { 170 | log( 171 | [createBlockStore(name)], 172 | [ 173 | ['-> ', '%s', ''], 174 | [value, '%o', ''], 175 | [fileName, '%s', styles.file], 176 | [name, '%s', styles.file], 177 | ], 178 | ); 179 | } 180 | 181 | export function eventCalled(name: string, fileName: string, payload: any): void { 182 | log( 183 | [createBlockEvent(name)], 184 | [ 185 | [payload, '%o', 'padding-left: 4px;'], 186 | [fileName, '%s', styles.file], 187 | [name, '%s', styles.file], 188 | ], 189 | ); 190 | } 191 | 192 | export function effectCalled(name: string, fileName: string, parameters: any): void { 193 | log( 194 | [createBlockEffect(name)], 195 | [ 196 | [parameters, '%o', 'padding-left: 4px;'], 197 | [fileName, '%s', styles.file], 198 | [name, '%s', styles.file], 199 | ], 200 | ); 201 | } 202 | 203 | export function effectDone(name: string, fileName: string, parameters: any, result: any): void { 204 | log( 205 | [createBlockEffect(name)], 206 | [ 207 | ['done ✅', '%s', styles.emoji], 208 | [parameters, '(%o)', 'padding-left: 4px;'], 209 | ['-> ', '%s', ''], 210 | [result, '%o', 'padding: 0;'], 211 | [fileName, '%s', styles.file], 212 | [name, '%s', styles.file], 213 | ], 214 | ); 215 | } 216 | 217 | export function effectFail(name: string, fileName: string, parameters: any, error: any): void { 218 | const instanceofError = error instanceof Error; 219 | 220 | log( 221 | [createBlockEffect(name)], 222 | [ 223 | ['fail ❌', '%s', styles.emoji], 224 | [parameters, '(%o)', 'padding-left: 4px;'], 225 | ['-> ', '%s', ''], 226 | instanceofError ? [String(error), '%s', ''] : [error, '%o', 'padding: 0;'], 227 | [fileName, '%s', styles.file], 228 | [name, '%s', styles.file], 229 | ], 230 | instanceofError ? 'collapsed' : undefined, 231 | ); 232 | 233 | if (instanceofError) { 234 | log( 235 | [], 236 | [ 237 | [' ', '%s', ''], 238 | [error, '%o', 'padding-left: 20px;'], 239 | ], 240 | ); 241 | } 242 | 243 | console.groupEnd(); 244 | } 245 | -------------------------------------------------------------------------------- /src/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe, vi } from 'vitest'; 2 | 3 | import { createStore, createEvent, createEffect, sample, fork, allSettled } from 'effector'; 4 | 5 | import { attachLogger, configure } from '../dist'; 6 | 7 | describe('logger tests', () => { 8 | test('logger works', () => { 9 | const $abc = createStore(0); 10 | const inc = createEvent(); 11 | $abc.on(inc, (state) => state + 1); 12 | const myFx = createEffect(() => 42); 13 | sample({ 14 | clock: inc, 15 | target: myFx, 16 | }); 17 | 18 | const logged = vi.fn(); 19 | 20 | vi.stubGlobal('console', { 21 | log: logged, 22 | warn: logged, 23 | error: logged, 24 | info: logged, 25 | group: logged, 26 | groupEnd: logged, 27 | groupCollapsed: logged, 28 | }); 29 | 30 | const unlogger = attachLogger(); 31 | 32 | inc(); 33 | 34 | const logs = logged.mock.calls; 35 | expect(logs).toMatchInlineSnapshot(` 36 | [ 37 | [ 38 | "%c%s%c  %c%s%c  %c%s%c  %c%o  %c%s  %c%s", 39 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 40 | "☄️", 41 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 42 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 43 | "effector", 44 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 45 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #9ccc65; color: #000", 46 | "4", 47 | "color: currentColor; background-color: transparent;", 48 | "padding-left: 4px; padding-right: 4px; font-weight: normal; padding-left: 4px;", 49 | undefined, 50 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 51 | "", 52 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 53 | "4", 54 | ], 55 | [ 56 | "%c%s%c  %c%s%c  %c%s%c  %c%s  %c%o  %c%s  %c%s", 57 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 58 | "☄️", 59 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 60 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 61 | "effector", 62 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 63 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #7e57c2; color: #fff", 64 | "2", 65 | "color: currentColor; background-color: transparent;", 66 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 67 | "-> ", 68 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 69 | 1, 70 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 71 | "", 72 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 73 | "2", 74 | ], 75 | [ 76 | "%c%s%c  %c%s%c  %c%s%c  %c%o  %c%s  %c%s", 77 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 78 | "☄️", 79 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 80 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 81 | "effector", 82 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 83 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #26a69a; color: #000", 84 | "5", 85 | "color: currentColor; background-color: transparent;", 86 | "padding-left: 4px; padding-right: 4px; font-weight: normal; padding-left: 4px;", 87 | undefined, 88 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 89 | "", 90 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 91 | "5", 92 | ], 93 | [ 94 | "%c%s%c  %c%s%c  %c%s%c  %c%s  %c%o  %c%s  %c%s", 95 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 96 | "☄️", 97 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 98 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 99 | "effector", 100 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 101 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #7e57c2; color: #fff", 102 | "5.inFlight", 103 | "color: currentColor; background-color: transparent;", 104 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 105 | "-> ", 106 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 107 | 1, 108 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 109 | "", 110 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 111 | "5.inFlight", 112 | ], 113 | [ 114 | "%c%s%c  %c%s%c  %c%s%c  %c%s  %c(%o)  %c%s  %c%o  %c%s  %c%s", 115 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 116 | "☄️", 117 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 118 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 119 | "effector", 120 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 121 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #26a69a; color: #000", 122 | "5", 123 | "color: currentColor; background-color: transparent;", 124 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 125 | "done ✅", 126 | "padding-left: 4px; padding-right: 4px; font-weight: normal; padding-left: 4px;", 127 | undefined, 128 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 129 | "-> ", 130 | "padding-left: 4px; padding-right: 4px; font-weight: normal; padding: 0;", 131 | 42, 132 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 133 | "", 134 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 135 | "5", 136 | ], 137 | [ 138 | "%c%s%c  %c%s%c  %c%s%c  %c%s  %c%o  %c%s  %c%s", 139 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 140 | "☄️", 141 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 142 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 143 | "effector", 144 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 145 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #7e57c2; color: #fff", 146 | "5.inFlight", 147 | "color: currentColor; background-color: transparent;", 148 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 149 | "-> ", 150 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 151 | 0, 152 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 153 | "", 154 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 155 | "5.inFlight", 156 | ], 157 | ] 158 | `); 159 | unlogger(); 160 | 161 | inc(); 162 | 163 | const nextLogs = logged.mock.calls; 164 | 165 | expect(logs).toStrictEqual(nextLogs); 166 | }); 167 | 168 | test('logger works (scope)', () => { 169 | const $abc = createStore(0); 170 | const inc = createEvent(); 171 | $abc.on(inc, (state) => state + 1); 172 | const myFx = createEffect(() => 42); 173 | sample({ 174 | clock: inc, 175 | target: myFx, 176 | }); 177 | 178 | const logged = vi.fn(); 179 | 180 | vi.stubGlobal('console', { 181 | log: logged, 182 | warn: logged, 183 | error: logged, 184 | info: logged, 185 | group: logged, 186 | groupEnd: logged, 187 | groupCollapsed: logged, 188 | }); 189 | 190 | const scope = fork(); 191 | 192 | const unlogger = attachLogger({ 193 | scope, 194 | }); 195 | 196 | allSettled(inc, { scope }); 197 | 198 | const logs = logged.mock.calls; 199 | expect(logs).toMatchInlineSnapshot(` 200 | [ 201 | [ 202 | "%c%s%c  %c%s%c  %c%s%c  %c%o  %c%s  %c%s", 203 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 204 | "☄️", 205 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 206 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 207 | "effector", 208 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 209 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #9ccc65; color: #000", 210 | "(scope: 65) 21", 211 | "color: currentColor; background-color: transparent;", 212 | "padding-left: 4px; padding-right: 4px; font-weight: normal; padding-left: 4px;", 213 | undefined, 214 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 215 | "", 216 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 217 | "(scope: 65) 21", 218 | ], 219 | [ 220 | "%c%s%c  %c%s%c  %c%s%c  %c%s  %c%o  %c%s  %c%s", 221 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 222 | "☄️", 223 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 224 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 225 | "effector", 226 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 227 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #7e57c2; color: #fff", 228 | "(scope: 65) 19", 229 | "color: currentColor; background-color: transparent;", 230 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 231 | "-> ", 232 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 233 | 1, 234 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 235 | "", 236 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 237 | "(scope: 65) 19", 238 | ], 239 | [ 240 | "%c%s%c  %c%s%c  %c%s%c  %c%o  %c%s  %c%s", 241 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 242 | "☄️", 243 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 244 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 245 | "effector", 246 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 247 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #26a69a; color: #000", 248 | "(scope: 65) 22", 249 | "color: currentColor; background-color: transparent;", 250 | "padding-left: 4px; padding-right: 4px; font-weight: normal; padding-left: 4px;", 251 | undefined, 252 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 253 | "", 254 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 255 | "(scope: 65) 22", 256 | ], 257 | [ 258 | "%c%s%c  %c%s%c  %c%s%c  %c%s  %c%o  %c%s  %c%s", 259 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 260 | "☄️", 261 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 262 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 263 | "effector", 264 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 265 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #7e57c2; color: #fff", 266 | "(scope: 65) 22.inFlight", 267 | "color: currentColor; background-color: transparent;", 268 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 269 | "-> ", 270 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 271 | 1, 272 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 273 | "", 274 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 275 | "(scope: 65) 22.inFlight", 276 | ], 277 | [ 278 | "%c%s%c  %c%s%c  %c%s%c  %c%s  %c(%o)  %c%s  %c%o  %c%s  %c%s", 279 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 280 | "☄️", 281 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 282 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 283 | "effector", 284 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 285 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #26a69a; color: #000", 286 | "(scope: 65) 22", 287 | "color: currentColor; background-color: transparent;", 288 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 289 | "done ✅", 290 | "padding-left: 4px; padding-right: 4px; font-weight: normal; padding-left: 4px;", 291 | undefined, 292 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 293 | "-> ", 294 | "padding-left: 4px; padding-right: 4px; font-weight: normal; padding: 0;", 295 | 42, 296 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 297 | "", 298 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 299 | "(scope: 65) 22", 300 | ], 301 | [ 302 | "%c%s%c  %c%s%c  %c%s%c  %c%s  %c%o  %c%s  %c%s", 303 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 304 | "☄️", 305 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 306 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 307 | "effector", 308 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 309 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #7e57c2; color: #fff", 310 | "(scope: 65) 22.inFlight", 311 | "color: currentColor; background-color: transparent;", 312 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 313 | "-> ", 314 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 315 | 0, 316 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 317 | "", 318 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 319 | "(scope: 65) 22.inFlight", 320 | ], 321 | ] 322 | `); 323 | unlogger(); 324 | 325 | allSettled(inc, { scope }); 326 | 327 | const nextLogs = logged.mock.calls; 328 | 329 | expect(logs).toStrictEqual(nextLogs); 330 | }); 331 | 332 | describe('configure', () => { 333 | test('configure can supress logs', () => { 334 | const $abc = createStore(0); 335 | const inc = createEvent(); 336 | $abc.on(inc, (state) => state + 1); 337 | const myFx = createEffect(() => 42); 338 | sample({ 339 | clock: inc, 340 | target: myFx, 341 | }); 342 | 343 | const logged = vi.fn(); 344 | 345 | vi.stubGlobal('console', { 346 | log: logged, 347 | warn: logged, 348 | error: logged, 349 | info: logged, 350 | group: logged, 351 | groupEnd: logged, 352 | groupCollapsed: logged, 353 | }); 354 | 355 | configure($abc, { log: 'disabled' }); 356 | configure([inc, myFx], { log: 'disabled' }); 357 | 358 | const unlogger = attachLogger(); 359 | 360 | inc(); 361 | 362 | expect(logged.mock.calls.length).toBe(2); // initial logs of logger installation 363 | expect(logged.mock.calls).toMatchInlineSnapshot(` 364 | [ 365 | [ 366 | "%c%s%c  %c%s%c  %c%s%c  %c%s  %c%o  %c%s  %c%s", 367 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 368 | "☄️", 369 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 370 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 371 | "effector", 372 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 373 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #7e57c2; color: #fff", 374 | "39.inFlight", 375 | "color: currentColor; background-color: transparent;", 376 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 377 | "-> ", 378 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 379 | 1, 380 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 381 | "", 382 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 383 | "39.inFlight", 384 | ], 385 | [ 386 | "%c%s%c  %c%s%c  %c%s%c  %c%s  %c%o  %c%s  %c%s", 387 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 388 | "☄️", 389 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 390 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 391 | "effector", 392 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 393 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #7e57c2; color: #fff", 394 | "39.inFlight", 395 | "color: currentColor; background-color: transparent;", 396 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 397 | "-> ", 398 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 399 | 0, 400 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 401 | "", 402 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 403 | "39.inFlight", 404 | ], 405 | ] 406 | `); 407 | 408 | unlogger(); 409 | }); 410 | 411 | test('configure can force logs', () => { 412 | const $abc = createStore(0); 413 | const inc = createEvent(); 414 | $abc.on(inc, (state) => state + 1); 415 | const myFx = createEffect(() => 42); 416 | sample({ 417 | clock: inc, 418 | target: myFx, 419 | }); 420 | 421 | const derivedInc = inc.map((x) => 'LOGGED!!!!'); 422 | 423 | const logged = vi.fn(); 424 | 425 | vi.stubGlobal('console', { 426 | log: logged, 427 | warn: logged, 428 | error: logged, 429 | info: logged, 430 | group: logged, 431 | groupEnd: logged, 432 | groupCollapsed: logged, 433 | }); 434 | 435 | configure($abc, { log: 'disabled' }); 436 | configure([inc, myFx], { log: 'disabled' }); 437 | 438 | // force logs of derived unit 439 | configure(derivedInc, { log: 'enabled' }); 440 | 441 | const unlogger = attachLogger(); 442 | 443 | inc(); 444 | 445 | expect(logged.mock.calls.length).toBe(3); // initial logs of logger installation + single update of derivedInc 446 | expect(logged.mock.calls).toMatchInlineSnapshot(` 447 | [ 448 | [ 449 | "%c%s%c  %c%s%c  %c%s%c  %c%o  %c%s  %c%s", 450 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 451 | "☄️", 452 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 453 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 454 | "effector", 455 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 456 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #9ccc65; color: #000", 457 | "55 → *", 458 | "color: currentColor; background-color: transparent;", 459 | "padding-left: 4px; padding-right: 4px; font-weight: normal; padding-left: 4px;", 460 | "LOGGED!!!!", 461 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 462 | "", 463 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 464 | "55 → *", 465 | ], 466 | [ 467 | "%c%s%c  %c%s%c  %c%s%c  %c%s  %c%o  %c%s  %c%s", 468 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 469 | "☄️", 470 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 471 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 472 | "effector", 473 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 474 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #7e57c2; color: #fff", 475 | "56.inFlight", 476 | "color: currentColor; background-color: transparent;", 477 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 478 | "-> ", 479 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 480 | 1, 481 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 482 | "", 483 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 484 | "56.inFlight", 485 | ], 486 | [ 487 | "%c%s%c  %c%s%c  %c%s%c  %c%s  %c%o  %c%s  %c%s", 488 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 489 | "☄️", 490 | "padding-left: 4px; padding-right: 4px; font-weight: normal; line-height:1.5; color: #000; font-family: \\"Apple Emoji Font\\"; font-weight: normal !important;", 491 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 492 | "effector", 493 | "padding-left: 4px; padding-right: 4px; font-weight: normal; font-family: Menlo, monospace;", 494 | "padding-left: 4px; padding-right: 4px; font-weight: normal; background-color: #7e57c2; color: #fff", 495 | "56.inFlight", 496 | "color: currentColor; background-color: transparent;", 497 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 498 | "-> ", 499 | "padding-left: 4px; padding-right: 4px; font-weight: normal; ", 500 | 0, 501 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 502 | "", 503 | "padding-left: 4px; padding-right: 4px; font-weight: normal; color: #9e9e9e; padding-left: 20px;", 504 | "56.inFlight", 505 | ], 506 | ] 507 | `); 508 | 509 | unlogger(); 510 | }); 511 | }); 512 | }); 513 | --------------------------------------------------------------------------------