├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .prettierrc.json ├── .yarnrc.yml ├── LICENSE ├── README.md ├── package.json ├── packages ├── devtools-fe │ ├── craco.config.ts │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── app.module.scss │ │ ├── app.tsx │ │ ├── components │ │ │ ├── root.module.scss │ │ │ ├── root.tsx │ │ │ ├── timeline │ │ │ │ ├── inspector │ │ │ │ │ ├── code.tsx │ │ │ │ │ ├── index.module.scss │ │ │ │ │ └── index.tsx │ │ │ │ └── table │ │ │ │ │ ├── index.module.scss │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── tween.ts │ │ │ ├── timer.module.scss │ │ │ ├── timer.tsx │ │ │ ├── titlebar.module.scss │ │ │ └── titlebar.tsx │ │ ├── index.scss │ │ ├── index.tsx │ │ ├── pages │ │ │ ├── config.module.scss │ │ │ ├── config.tsx │ │ │ ├── timeline.module.scss │ │ │ └── timeline.tsx │ │ ├── react-app-env.d.ts │ │ ├── services │ │ │ └── remote.tsx │ │ └── states.tsx │ └── tsconfig.json ├── devtools │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json └── ipcman │ ├── README.md │ ├── graph.svg │ ├── package.json │ ├── src │ ├── electron.d.ts │ └── index.ts │ ├── tsconfig.cjs.json │ └── tsconfig.json ├── scripts ├── build.cts ├── bundle.cts └── clean.cts ├── tsconfig.base.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "es2020": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:import/recommended", 10 | "plugin:import/typescript", 11 | "plugin:@typescript-eslint/strict", 12 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 13 | ], 14 | "plugins": [ 15 | "@typescript-eslint", 16 | "import", 17 | "prettier" 18 | ], 19 | "parser": "@typescript-eslint/parser", 20 | "parserOptions": { 21 | "project": true 22 | }, 23 | "settings": { 24 | "import/resolver": { 25 | "typescript": { 26 | "alwaysTryTypes": true 27 | }, 28 | "node": true 29 | } 30 | }, 31 | "rules": { 32 | "require-await": "off", 33 | "no-constant-condition": [ 34 | "warn", 35 | { 36 | "checkLoops": false 37 | } 38 | ], 39 | "no-unused-vars": "off", 40 | "prefer-const": [ 41 | "warn", 42 | { 43 | "destructuring": "all" 44 | } 45 | ], 46 | "import/no-default-export": "warn", 47 | "import/consistent-type-specifier-style": "warn", 48 | "@typescript-eslint/no-non-null-assertion": "off", 49 | "@typescript-eslint/require-await": "off", 50 | "@typescript-eslint/no-unused-vars": [ 51 | "warn", 52 | { 53 | "argsIgnorePattern": "^_", 54 | "destructuredArrayIgnorePattern": "^_" 55 | } 56 | ], 57 | "@typescript-eslint/no-namespace": "off", 58 | "@typescript-eslint/consistent-type-exports": "warn", 59 | "@typescript-eslint/consistent-type-imports": "warn" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | *.png -text 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | fetch-depth: 0 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: '20.3.1' 25 | 26 | - name: Build Package 27 | run: | 28 | corepack enable 29 | yarn install --immutable 30 | yarn build 31 | yarn workspace ipcman pack --filename ipcman.tgz 32 | mkdir -p build/dist 33 | mv packages/devtools/lib/index.js build/dist/ipcman.js 34 | mv packages/devtools-fe/build build/dist/build 35 | shell: bash 36 | 37 | - uses: actions/upload-artifact@v3 38 | with: 39 | name: ipcman-${{ github.sha }} 40 | path: packages/ipcman/ipcman.tgz 41 | 42 | - uses: actions/upload-artifact@v3 43 | with: 44 | name: ipcman-devtools-${{ github.sha }} 45 | path: build/dist 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | .cache-loader 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # TernJS port file 121 | .tern-port 122 | 123 | # Stores VSCode versions used for testing VSCode extensions 124 | .vscode-test 125 | 126 | # yarn v2 127 | .yarn/cache 128 | .yarn/unplugged 129 | .yarn/build-state.yml 130 | .yarn/install-state.gz 131 | .pnp.* 132 | 133 | # Artifacts 134 | build/ 135 | lib/ 136 | dist/ 137 | esm/ 138 | cjs/ 139 | 140 | # Temp files 141 | tmp/ 142 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "arrowParens": "always", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Il Harper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/ipcman/README.md -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ipcman/workspace", 3 | "version": "0.0.0", 4 | "private": true, 5 | "packageManager": "yarn@4.1.0", 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "clean": "ts-node -T scripts/clean.cts", 11 | "build": "yarn workspace ipcman build && concurrently -n devtools,fe \"yarn workspace @ipcman/devtools build\" \"yarn workspace @ipcman/devtools-fe build\"", 12 | "build-fast": "node -r esbuild-register ./scripts/build.cts" 13 | }, 14 | "devDependencies": { 15 | "@tsconfig/strictest": "^2.0.3", 16 | "@types/jest": "^29.5.12", 17 | "@types/node": "^20.11.19", 18 | "@types/react": "^18.0.28", 19 | "@types/react-dom": "^18.0.11", 20 | "@typescript-eslint/eslint-plugin": "^7.0.1", 21 | "@typescript-eslint/parser": "^7.0.1", 22 | "concurrently": "^8.2.2", 23 | "esbuild": "^0.20.2", 24 | "esbuild-register": "^3.5.0", 25 | "eslint": "^8.56.0", 26 | "eslint-config-prettier": "^9.1.0", 27 | "eslint-config-react-app": "^7.0.1", 28 | "eslint-import-resolver-typescript": "^3.6.1", 29 | "eslint-plugin-import": "^2.29.1", 30 | "eslint-plugin-prettier": "^5.1.3", 31 | "jest": "^29.7.0", 32 | "prettier": "^3.2.5", 33 | "ts-jest": "^29.1.2", 34 | "ts-node": "^10.9.2", 35 | "typescript": "^5.3.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/devtools-fe/craco.config.ts: -------------------------------------------------------------------------------- 1 | import type { CracoConfig } from '@craco/types' 2 | 3 | const config: CracoConfig = { 4 | webpack: { 5 | configure: (webpackConfig, ctx) => { 6 | if (ctx.env === 'production') webpackConfig.devtool = false 7 | 8 | return webpackConfig 9 | }, 10 | }, 11 | } 12 | 13 | // eslint-disable-next-line import/no-default-export 14 | export default config 15 | -------------------------------------------------------------------------------- /packages/devtools-fe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ipcman/devtools-fe", 3 | "version": "0.0.0", 4 | "private": true, 5 | "author": { 6 | "name": "Il Harper", 7 | "email": "hi@ilharper.com", 8 | "url": "https://ilharper.com" 9 | }, 10 | "license": "MIT", 11 | "files": [ 12 | "build" 13 | ], 14 | "scripts": { 15 | "dev": "craco start", 16 | "build": "craco build" 17 | }, 18 | "devDependencies": { 19 | "@craco/craco": "^7.1.0", 20 | "@craco/types": "^7.1.0", 21 | "@fluentui/fluent2-theme": "^8.105.1", 22 | "@fluentui/react": "^8.106.4", 23 | "@monaco-editor/react": "^4.6.0", 24 | "@tanstack/react-table": "^8.12.0", 25 | "@tanstack/react-virtual": "^3.1.2", 26 | "ahooks": "^3.7.5", 27 | "clsx": "^2.1.0", 28 | "immer": "^9.0.19", 29 | "monaco-editor": "^0.46.0", 30 | "react": "18.2.0", 31 | "react-dom": "18.2.0", 32 | "react-scripts": "5.0.1", 33 | "sass": "^1.71.0", 34 | "use-immer": "^0.8.1" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "dependencies": { 49 | "react-use": "^17.5.0", 50 | "zustand": "^4.5.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/devtools-fe/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IpcMan Devtools 7 | 8 | 9 |
10 | 50 |
51 | 52 |
53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/app.module.scss: -------------------------------------------------------------------------------- 1 | .themeProvider { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { Fluent2WebDarkTheme } from '@fluentui/fluent2-theme' 2 | import type { IRawStyle, PartialTheme } from '@fluentui/react' 3 | import { ThemeProvider } from '@fluentui/react' 4 | import type { FC } from 'react' 5 | import styles from './app.module.scss' 6 | import { Root } from './components/root' 7 | import { RemoteProvider } from './services/remote' 8 | 9 | const defaultFontStyle: IRawStyle = { 10 | fontFamily: 11 | "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Noto Sans', 'Helvetica Neue', Helvetica, sans-serif", 12 | fontWeight: 400, 13 | fontStyle: 'normal', 14 | fontStretch: 'normal', 15 | textRendering: 'optimizeLegibility', 16 | textIndent: '0', 17 | textShadow: 'none', 18 | textDecoration: 'none', 19 | writingMode: 'horizontal-tb', 20 | letterSpacing: 'normal', 21 | wordSpacing: 'normal', 22 | } 23 | 24 | const appFontTheme: PartialTheme = { 25 | defaultFontStyle, 26 | fonts: { 27 | tiny: defaultFontStyle, 28 | xSmall: defaultFontStyle, 29 | small: defaultFontStyle, 30 | smallPlus: defaultFontStyle, 31 | medium: defaultFontStyle, 32 | mediumPlus: defaultFontStyle, 33 | large: defaultFontStyle, 34 | xLarge: defaultFontStyle, 35 | }, 36 | } 37 | 38 | const appDarkTheme: PartialTheme = { 39 | ...Fluent2WebDarkTheme, 40 | ...appFontTheme, 41 | } 42 | 43 | export const App: FC = () => ( 44 | 45 | 46 | 47 | 48 | 49 | ) 50 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/components/root.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | height: 100%; 3 | // width: 100%; 4 | // max-width: 100%; 5 | // overflow: hidden; 6 | } 7 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/components/root.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from '@fluentui/react' 2 | import type { FC } from 'react' 3 | import { TimelinePage } from '../pages/timeline' 4 | import { ConfigPage } from '../pages/config' 5 | import styles from './root.module.scss' 6 | import { TitleBar } from './titlebar' 7 | import { useCurrentPage } from '../states' 8 | 9 | export const Root: FC = () => { 10 | const {currentPage, setCurrentPage} = useCurrentPage() 11 | console.log(currentPage) 12 | 13 | return 14 | 15 | {currentPage === 'timeline' && } 16 | {currentPage === 'settings' && } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/components/timeline/inspector/code.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner, SpinnerSize, Stack } from '@fluentui/react' 2 | import type { OnMount } from '@monaco-editor/react' 3 | import { Editor } from '@monaco-editor/react' 4 | import type { editor } from 'monaco-editor' 5 | import type { FC } from 'react' 6 | import { useEffect, useRef } from 'react' 7 | 8 | const options: editor.IStandaloneEditorConstructionOptions = { 9 | domReadOnly: true, 10 | readOnly: true, 11 | } 12 | 13 | const loading = ( 14 | 20 | ) 21 | 22 | export const CodeInspector: FC<{ 23 | value: string 24 | folding: boolean 25 | }> = ({ value, folding }) => { 26 | const editorRef = useRef(null) 27 | 28 | const handleEditorDidMount: OnMount = (editor, _monaco) => { 29 | editorRef.current = editor 30 | } 31 | 32 | useEffect(() => { 33 | editorRef.current?.setPosition({ 34 | lineNumber: 1, 35 | column: 1, 36 | }) 37 | if (folding) 38 | void editorRef.current?.getAction('editor.foldRecursively')?.run() 39 | else void editorRef.current?.getAction('editor.unfoldAll')?.run() 40 | }, [value, folding]) 41 | 42 | return ( 43 | 44 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/components/timeline/inspector/index.module.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | height: 48px; 3 | } 4 | 5 | .toggle { 6 | margin: 0 12px; 7 | 8 | min-width: 150px; 9 | } 10 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/components/timeline/inspector/index.tsx: -------------------------------------------------------------------------------- 1 | import { Pivot, PivotItem, Stack, Toggle } from '@fluentui/react' 2 | import type { FC, MouseEvent } from 'react' 3 | import { useCallback, useEffect, useState } from 'react' 4 | import { useRemote, type IpcManItem } from '../../../services/remote' 5 | import { CodeInspector } from './code' 6 | import styles from './index.module.scss' 7 | import { useSelectedRow } from '../../../states' 8 | 9 | type Tab = 'request' | 'response' 10 | 11 | export const TimelineInspector: FC<{ 12 | item: IpcManItem | undefined 13 | }> = ({ item }) => { 14 | const [tab, setTab] = useState('request') 15 | const [value, setValue] = useState('') 16 | const [folding, setFolding] = useState(false) 17 | const remote = useRemote() 18 | 19 | // Use useEffect to compute value and set tab in a single render. 20 | useEffect(() => { 21 | if (!item) { 22 | setTab('request') 23 | setValue('') 24 | return 25 | } 26 | 27 | let newTab = tab 28 | 29 | if (item.data.requestArgs && !item.data.responseArgs) newTab = 'request' 30 | if (!item.data.requestArgs && item.data.responseArgs) newTab = 'response' 31 | 32 | if (newTab !== tab) setTab(newTab) 33 | setValue( 34 | JSON.stringify( 35 | newTab === 'request' ? item?.data.requestArgs : item?.data.responseArgs, 36 | null, 37 | 2, 38 | ), 39 | ) 40 | }, [tab, item]) 41 | 42 | const { setSelectedRow } = useSelectedRow() 43 | 44 | const handleTabClick = useCallback((itemTab?: PivotItem) => { 45 | if (itemTab) { 46 | const newTab = itemTab.props.itemKey as Tab 47 | // find the corresponding data 48 | const data = item?.data && remote.data.findLast(d => d?.data?.bindId === item?.data?.bindId && d?.data?.type !== item?.data?.type) 49 | // console.log(data, item, data?.data?.bindId, item?.data?.bindId) 50 | if (data) { 51 | setSelectedRow(data.index - 1) 52 | } else { 53 | setTab(v => v) 54 | } 55 | } 56 | }, [remote]) 57 | 58 | const handleFoldingToggle = useCallback( 59 | (_: MouseEvent, v: boolean | undefined) => setFolding(v), 60 | [], 61 | ) 62 | 63 | return ( 64 | 65 | 66 | 67 | 73 | 78 | 83 | 84 | 85 | 86 | 95 | 96 | 97 | 98 | 99 | ) 100 | } 101 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/components/timeline/table/index.module.scss: -------------------------------------------------------------------------------- 1 | .options { 2 | display: flex; 3 | justify-content: space-between; 4 | margin: 7px 10px; 5 | 6 | .navigation { 7 | display: flex; 8 | align-items: center; 9 | flex-direction: row; 10 | gap: 10px; 11 | 12 | & > * { 13 | margin: 0; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/components/timeline/table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef, useState } from 'react' 2 | import { IpcManItem, useRemote } from '../../../services/remote' 3 | import { createVelocity } from './tween' 4 | import { BaseButton, CommandButton, DefaultButton, Toggle } from '@fluentui/react' 5 | import s from './index.module.scss' 6 | import { useDataColorFlag, useDataFilter, useReqDataExtractorCode, useRespDataExtractorCode } from '../../../states' 7 | 8 | const lineHeight = 40 9 | const padding = 10 10 | 11 | interface RowInfo { 12 | title: string 13 | width: number 14 | data?(data: IpcManItem): string 15 | draw?(data: IpcManItem, ctx: CanvasRenderingContext2D, x: number, y: number): void 16 | } 17 | 18 | 19 | let cursorX = 0, cursorY = 0 20 | const mmHandler: (e: MouseEvent) => void = e => { 21 | cursorX = e.offsetX 22 | cursorY = e.offsetY 23 | } 24 | let currentHoveringItem = -1 25 | 26 | /* DONT REUSE THIS COMPONENT */ 27 | export const TimelineTable = ({ 28 | rowSelection, 29 | handleRowSelection, 30 | }: { 31 | rowSelection: number 32 | handleRowSelection: (rowSelection: number) => void 33 | }) => { 34 | const [autoscroll, setAutoscroll] = useState(true) 35 | 36 | const canvasRef = useRef(null) 37 | 38 | const remote = useRemote() 39 | 40 | const scrollHeightTween = useMemo(() => createVelocity({ 41 | minVal: -lineHeight, 42 | value: -lineHeight 43 | }), []) 44 | 45 | useEffect(() => { 46 | if (autoscroll) { 47 | let lastDataIndex = remote.data.length 48 | const h = setInterval(() => { 49 | if (lastDataIndex === remote.data.length) return 50 | scrollHeightTween.value += lineHeight * (remote.data.length - lastDataIndex) 51 | lastDataIndex = remote.data.length 52 | }, 40) 53 | return () => { 54 | clearInterval(h) 55 | } 56 | } 57 | }, [autoscroll]) 58 | 59 | // eslint-disable-next-line @typescript-eslint/ban-types 60 | const wrapTryCatch = (fn: Function) => { 61 | return (...args: unknown[]) => { 62 | try { 63 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 64 | return fn(...args) ?? '' 65 | } catch (e) { 66 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call 67 | return e 68 | } 69 | } 70 | } 71 | 72 | const useCodeFunc = (hook: typeof useReqDataExtractorCode, defaultFunc: T) => { 73 | const [code] = hook() 74 | return useMemo(() => { 75 | try { 76 | // eslint-disable-next-line @typescript-eslint/no-implied-eval 77 | const fn = new Function('data', 'type', 'index', code) 78 | return wrapTryCatch(fn) 79 | } catch (e) { 80 | return defaultFunc 81 | } 82 | }, [code]) 83 | } 84 | const reqDataExtractor = useCodeFunc(useReqDataExtractorCode, (data: unknown[], type: string, index: number) => JSON.stringify(data)) 85 | const respDataExtractor = useCodeFunc(useRespDataExtractorCode, (data: unknown[], type: string, index: number) => JSON.stringify(data)) 86 | const dataColorFlagExtractor = useCodeFunc(useDataColorFlag, (data: unknown[], type: string, index: number) => 'black') 87 | const dataFilter = useCodeFunc(useDataFilter, (data: unknown[], type: string, index: number) => true) 88 | 89 | useEffect(() => { 90 | if (!canvasRef.current) return 91 | 92 | const ctx = canvasRef.current.getContext('2d') 93 | 94 | canvasRef.current.addEventListener('mousemove', mmHandler) 95 | 96 | canvasRef.current.onclick = () => { 97 | if (currentHoveringItem === -1) return 98 | handleRowSelection(currentHoveringItem - 1) 99 | } 100 | 101 | const canvasWidth = canvasRef.current.width 102 | const argWidth = (canvasWidth - 40 - 70 - 50 - padding * 5 * 2) / 2 103 | 104 | const rowInfo: RowInfo[] = [ 105 | { 106 | title: '', 107 | width: 6, 108 | draw(data, ctx, x, y) { 109 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 110 | ctx.fillStyle = dataColorFlagExtractor(data.data.requestArgs || [], data.data.type, data.index) ?? 'black' 111 | ctx.fillRect(x - padding, y, 15 + padding, lineHeight - padding * 2) 112 | }, 113 | }, 114 | { 115 | title: 'Index', 116 | width: 40, 117 | data(data) { 118 | return data.index.toString() 119 | } 120 | }, 121 | { 122 | title: 'Timestamp', 123 | width: 70, 124 | data(data) { 125 | return new Date(data.timestamp).toLocaleTimeString() 126 | } 127 | }, 128 | { 129 | title: 'Type', 130 | width: 50, 131 | data(data) { 132 | return data.data.type 133 | } 134 | }, 135 | { 136 | title: 'Request Args', 137 | width: argWidth, 138 | data(data) { 139 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 140 | return reqDataExtractor(data.data.requestArgs || [], data.data.type, data.index) 141 | } 142 | }, 143 | { 144 | title: 'Response Args', 145 | width: argWidth, 146 | data(data) { 147 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 148 | return respDataExtractor(data.data.responseArgs || [], data.data.type, data.index) 149 | } 150 | } 151 | ] 152 | 153 | const draw = (deltaT) => { 154 | if (!ctx) return 155 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) 156 | 157 | const top = -scrollHeightTween.value; 158 | 159 | 160 | const firstDataIndex = Math.floor(-top / lineHeight) 161 | const lastDataIndex = Math.ceil((-top + ctx.canvas.height) / lineHeight) 162 | 163 | // Draw data 164 | for (let i = firstDataIndex; i < lastDataIndex; i++) { 165 | const data = remote.data[i] 166 | if (!data) continue 167 | 168 | const y = i * lineHeight + top + padding 169 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 170 | ctx.fillStyle = rowSelection === data.index ? 'rgba(0, 0, 0, 0.1)' : 'transparent' 171 | ctx.fillRect(0, y, ctx.canvas.width, lineHeight - padding) 172 | ctx.font = '14px Noto Sans CJK SC' 173 | let x = 0 174 | for (const row of rowInfo) { 175 | // inner content(clipped) 176 | ctx.save() 177 | ctx.beginPath() 178 | x += padding 179 | ctx.rect(x, y, row.width, lineHeight - padding) 180 | ctx.clip() 181 | 182 | if (row.data) { 183 | ctx.fillStyle = 'white' 184 | ctx.textAlign = 'center' 185 | ctx.fillText(row.data(data), x + 186 | row.width / 2 187 | , y + padding + 7) 188 | } else if (row.draw) { 189 | row.draw(data, ctx, x, y) 190 | } else { 191 | ctx.fillText('Unknown', x, y + padding) 192 | } 193 | 194 | x += row.width + padding 195 | 196 | ctx.restore() 197 | 198 | // border 199 | ctx.fillStyle = '#3E4452' 200 | ctx.fillRect(x, y, 1, lineHeight + padding) 201 | } 202 | 203 | // border 204 | ctx.fillStyle = '#3E4452' 205 | ctx.fillRect(0, y - padding, ctx.canvas.width, 1) 206 | 207 | // hover effect 208 | if (cursorY > y && cursorY < y + lineHeight) { 209 | ctx.fillStyle = '#ffffff11' 210 | ctx.fillRect(0, y - padding, ctx.canvas.width, lineHeight) 211 | currentHoveringItem = data.index 212 | } 213 | 214 | // selected effect 215 | if (rowSelection === data.index - 1) { 216 | ctx.fillStyle = '#aaaaff22' 217 | ctx.fillRect(0, y - padding, ctx.canvas.width, lineHeight) 218 | } 219 | } 220 | 221 | // Draw header 222 | let x = 0 223 | for (const row of rowInfo) { 224 | const width = row.width + padding * 2 225 | ctx.fillStyle = '#252525' 226 | ctx.font = '14px Noto Sans CJK SC' 227 | ctx.fillRect(x, 0, width, lineHeight) 228 | ctx.fillStyle = 'white' 229 | ctx.textAlign = 'center' 230 | ctx.textBaseline = 'middle' 231 | ctx.fillText(row.title, x + width / 2, lineHeight / 2) 232 | 233 | // border 234 | ctx.fillStyle = '#3E4452' 235 | ctx.fillRect(x, 0, 1, lineHeight) 236 | x += width 237 | ctx.fillRect(x, 0, 1, lineHeight) 238 | } 239 | } 240 | 241 | let rAFHandle: number 242 | let lastTime: number = -1 243 | const loop = (time: number) => { 244 | if (lastTime === -1) lastTime = time 245 | const deltaT = time - lastTime 246 | lastTime = time 247 | scrollHeightTween.update(deltaT) 248 | draw(deltaT) 249 | 250 | rAFHandle = requestAnimationFrame(loop) 251 | } 252 | 253 | rAFHandle = requestAnimationFrame(loop) 254 | 255 | return () => { 256 | cancelAnimationFrame(rAFHandle) 257 | canvasRef.current?.removeEventListener('mousemove', mmHandler) 258 | } 259 | }, [rowSelection, remote]) 260 | 261 | const [width, setWidth] = useState(0) 262 | const [height, setHeight] = useState(0) 263 | useEffect(() => { 264 | const handleResize = () => { 265 | if (!canvasRef.current) return 266 | const { width, height } = canvasRef.current.parentElement.getBoundingClientRect() 267 | setWidth(width) 268 | setHeight(height) 269 | } 270 | 271 | handleResize() 272 | window.addEventListener('resize', handleResize) 273 | 274 | return () => { 275 | window.removeEventListener('resize', handleResize) 276 | } 277 | }, []) 278 | 279 | return ( 280 |
283 |
284 |
285 | setAutoscroll(v)} label="Autoscroll" 286 | inlineLabel /> 287 | { 288 | scrollHeightTween.value = 0 289 | }}> 290 | Scroll to top 291 | 292 | 293 | { 294 | scrollHeightTween.value = remote.data.length * lineHeight - height + 100 295 | }}> 296 | Scroll to bottom 297 | 298 |
299 |
300 |
303 | { 304 | if (e.shiftKey) scrollHeightTween.speed = e.deltaY / 3 305 | else scrollHeightTween.speed = e.deltaY / 30 306 | }} width={width} height={height} /> 307 |
308 |
309 | ) 310 | } 311 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/components/timeline/table/tween.ts: -------------------------------------------------------------------------------- 1 | export const createVelocity = ({ 2 | value = 0, ds = 0.9, 3 | minVal = -Infinity, maxVal = Infinity 4 | } = {}) => { 5 | let speed = 0 6 | 7 | return { 8 | get speed() { 9 | return speed 10 | }, 11 | set speed(v) { 12 | speed = v 13 | }, 14 | get value() { 15 | return value 16 | }, 17 | set value(v) { 18 | value = v 19 | speed = 0 20 | }, 21 | update(deltaTime: number) { 22 | value += speed * deltaTime 23 | speed *= ds 24 | if (value < minVal) { 25 | value = minVal 26 | speed = 0 27 | } 28 | if (value > maxVal) { 29 | value = maxVal 30 | speed = 0 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/components/timer.module.scss: -------------------------------------------------------------------------------- 1 | .timerText { 2 | font-size: 20px; 3 | margin: 0 8px; 4 | 5 | &Container { 6 | background-color: #2886de; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/components/timer.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, Text } from '@fluentui/react' 2 | import { useInterval } from 'ahooks' 3 | import type { FC } from 'react' 4 | import { useState } from 'react' 5 | import { useRemote } from '../services/remote' 6 | import styles from './timer.module.scss' 7 | 8 | export const Timer: FC = () => { 9 | const { info } = useRemote() 10 | 11 | const [now, setNow] = useState(() => new Date().getTime()) 12 | 13 | useInterval(() => setNow(new Date().getTime()), 400) 14 | 15 | return ( 16 | 17 | 18 | +{Math.floor((now - info.startTime) / 1000)} 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/components/titlebar.module.scss: -------------------------------------------------------------------------------- 1 | .titleBar { 2 | height: 48px; 3 | 4 | background-color: #252525; 5 | } 6 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/components/titlebar.tsx: -------------------------------------------------------------------------------- 1 | import { Pivot, PivotItem, Stack } from '@fluentui/react' 2 | import type { FC } from 'react' 3 | import { Timer } from './timer' 4 | import styles from './titlebar.module.scss' 5 | import { useCurrentPage } from '../states' 6 | 7 | export const TitleBar: FC = () => { 8 | const {setCurrentPage} = useCurrentPage() 9 | 10 | return ( 11 | 16 | 17 | 18 | { 19 | setCurrentPage(e?.props.itemKey) 20 | }}> 21 | 26 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/index.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #ipcman { 4 | margin: 0; 5 | padding: 0; 6 | width: 100%; 7 | height: 100%; 8 | overflow: hidden; 9 | } 10 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { initializeIcons } from '@fluentui/react' 2 | import { setAutoFreeze } from 'immer' 3 | import { StrictMode } from 'react' 4 | import { createRoot } from 'react-dom/client' 5 | import { App } from './app' 6 | import './index.scss' 7 | 8 | setAutoFreeze(false) 9 | 10 | createRoot(document.getElementById('ipcman')).render( 11 | 12 | 13 | , 14 | ) 15 | 16 | initializeIcons() 17 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/pages/config.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 10px 30px; 3 | } 4 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/pages/config.tsx: -------------------------------------------------------------------------------- 1 | import s from "./config.module.scss" 2 | import { useDataColorFlag, useDataFilter, useReqDataExtractorCode, useRespDataExtractorCode } from "../states" 3 | import { TextField } from "@fluentui/react" 4 | 5 | export const ConfigPage = () => { 6 | const [reqDataExtractorCode, setReqDataExtractorCode] = useReqDataExtractorCode() 7 | const [respDataExtractorCode, setRespDataExtractorCode] = useRespDataExtractorCode() 8 | const [dataColorFlag, setDataColorFlag] = useDataColorFlag() 9 | const [dataFilter, setDataFilter] = useDataFilter() 10 | 11 | return ( 12 |
13 |

Config Page

14 | 15 |
16 |

Data Extractor

17 | setReqDataExtractorCode(v || '')} label="Request preview generator" multiline/> 18 | setRespDataExtractorCode(v || '')} label="Response preview generator" multiline/> 19 | 20 |

Data Filter

21 | setDataFilter(v || '')} label="Data filter" multiline/> 22 | 23 |

Data Color

24 | setDataColorFlag(v || '')} label="Data color flag" multiline/> 25 | 26 |
27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/pages/timeline.module.scss: -------------------------------------------------------------------------------- 1 | .timelinePageContainer { 2 | } 3 | 4 | .tableContainer { 5 | width: 67%; 6 | } 7 | 8 | .inspectorContainer { 9 | width: 33%; 10 | 11 | border-left: 1px solid #252525; 12 | } 13 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/pages/timeline.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from '@fluentui/react' 2 | import type { RowSelectionState } from '@tanstack/react-table' 3 | import type { FC } from 'react' 4 | import { useMemo, useState } from 'react' 5 | import { TimelineInspector } from '../components/timeline/inspector' 6 | import { TimelineTable } from '../components/timeline/table' 7 | import { useRemote } from '../services/remote' 8 | import styles from './timeline.module.scss' 9 | import { useSelectedRow } from '../states' 10 | 11 | export const TimelinePage: FC = () => { 12 | const { data } = useRemote() 13 | 14 | const {selectedRow, setSelectedRow} = useSelectedRow() 15 | 16 | return ( 17 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/services/remote.tsx: -------------------------------------------------------------------------------- 1 | import { original } from 'immer' 2 | import type { IpcMan } from 'ipcman' 3 | import type { FC, ReactNode } from 'react' 4 | import { createContext, useContext, useEffect, useState } from 'react' 5 | import { useImmer } from 'use-immer' 6 | 7 | export interface IpcManInfo { 8 | startTime: number 9 | } 10 | 11 | export interface IpcManItem { 12 | index: number 13 | timestamp: number 14 | data: IpcMan.Data & { 15 | requestArgs?: unknown[] 16 | responseArgs?: unknown[] 17 | } 18 | } 19 | 20 | export interface RemoteIntl { 21 | data: IpcManItem[] 22 | info: IpcManInfo 23 | } 24 | 25 | const defultRemote = { 26 | data: [], 27 | info: { 28 | startTime: new Date().getTime(), 29 | }, 30 | } 31 | 32 | export interface Remote extends RemoteIntl {} 33 | 34 | export const RemoteContext = createContext(defultRemote) 35 | 36 | export const useRemote = (): Remote => { 37 | const { data, info } = useContext(RemoteContext) 38 | 39 | return { 40 | data, 41 | info, 42 | } 43 | } 44 | 45 | export const RemoteProvider: FC<{ 46 | children?: ReactNode 47 | }> = ({ children }) => { 48 | // const [data, editData] = useImmer(defultRemote) 49 | const [data, setData] = useState(defultRemote) 50 | 51 | useEffect(() => { 52 | const searchParams = new URLSearchParams(location.search) 53 | let remoteUrl: string 54 | if (searchParams.has('ws')) remoteUrl = searchParams.get('ws')! 55 | else remoteUrl = location.origin.replace(/^http/, 'ws') + '/v0/events' 56 | 57 | const ws = new WebSocket(remoteUrl) 58 | 59 | ws.addEventListener('message', (e) => { 60 | const newData = JSON.parse(e.data as string) as IpcManItem[] 61 | 62 | setData((prev) => { 63 | // console.log('prev', prev) 64 | prev.data.push(...newData.map(v=>({ 65 | ...v, 66 | data: { 67 | ...v.data, 68 | requestArgs: v.data.type === 'receive' && v.data.args, 69 | responseArgs: v.data.type === 'send' && v.data.args, 70 | } 71 | }))) 72 | return prev 73 | }) 74 | 75 | // editData((draft) => { 76 | // const originalList = original(draft).data.map((x) => x.index) 77 | 78 | // for (const d of newData) { 79 | // if (originalList.findIndex((x) => d.index === x) !== -1) continue 80 | 81 | // switch (d.data.type) { 82 | // case 'send': 83 | // d.data.requestArgs = d.data.args 84 | // break 85 | 86 | // case 'receive': 87 | // d.data.responseArgs = d.data.args 88 | // break 89 | // } 90 | 91 | // // FIXME: If you freeze `d`, its stale proxy will magically leaked 92 | // // and used by tanstack table. `JSON.stringify` will then throw errors like 93 | // // `Cannot perform 'get' on a proxy that has been revoked.` 94 | // // freeze(d) 95 | 96 | // draft.data.push(d) 97 | // } 98 | 99 | // draft.data.forEach((x) => { 100 | // if (x.data.responseArgs) return 101 | // if (!x.data.binded) return 102 | // // if (!x.data.id) return 103 | 104 | // // const rType = x.data.type.replace('request', 'response') as 105 | // // | 'handle-response' 106 | // // | 'wrapped-response' 107 | 108 | // // const r = newData.find((y) => y.data.type === rType) 109 | 110 | // const r = draft.data.find( 111 | // (y) => y !== x && x.data.bindId === (y.data as IpcMan.Data).bindId, 112 | // ) 113 | 114 | // if (r) { 115 | // // x.data.requestArgs = x.data.args 116 | // x.data.responseArgs = r.data.args 117 | // r.data.requestArgs = x.data.args 118 | // // r.data.responseArgs = r.data.args 119 | // } 120 | // }) 121 | // }) 122 | }) 123 | 124 | return () => ws.close() 125 | }, []) 126 | 127 | useEffect( 128 | () => 129 | void (async () => { 130 | const info = (await (await fetch('/v0/info')).json()) as IpcManInfo 131 | // editData((draft) => void (draft.info = info)) 132 | setData((prev) => { 133 | prev.info = info 134 | return prev 135 | }) 136 | })(), 137 | [], 138 | ) 139 | 140 | return 141 | } 142 | -------------------------------------------------------------------------------- /packages/devtools-fe/src/states.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from "react-use" 2 | import { create } from "zustand" 3 | 4 | export const useCurrentPage = create<{ 5 | currentPage: string 6 | setCurrentPage: (page: string) => void 7 | }>(set => ({ 8 | currentPage: 'timeline', 9 | setCurrentPage: (page) => set({ currentPage: page }) 10 | })) 11 | 12 | export const useReqDataExtractorCode = () => useLocalStorage('req-data-extractor-code', 'return data.join(",")') 13 | export const useRespDataExtractorCode = () => useLocalStorage('resp-data-extractor-code', 'return data.join(",")') 14 | export const useDataColorFlag = () => useLocalStorage('data-color-flag', 'return "#141414"') 15 | export const useDataFilter = () => useLocalStorage('data-filter', 'return true') 16 | 17 | export const useSelectedRow = create<{ 18 | selectedRow: number 19 | setSelectedRow: (row: number) => void 20 | }>(set => ({ 21 | selectedRow: -1, 22 | setSelectedRow: (row) => set({ selectedRow: row }) 23 | })) 24 | -------------------------------------------------------------------------------- /packages/devtools-fe/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "noEmit": true, 6 | "declaration": false, 7 | }, 8 | "include": [ 9 | "src" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ipcman/devtools", 3 | "version": "0.0.0", 4 | "description": "Electron IPC Devtools", 5 | "repository": "https://github.com/ilharp/ipcman.git", 6 | "author": { 7 | "name": "Il Harper", 8 | "email": "hi@ilharper.com", 9 | "url": "https://ilharper.com" 10 | }, 11 | "license": "MIT", 12 | "files": [ 13 | "lib" 14 | ], 15 | "scripts": { 16 | "build": "ts-node -T ../../scripts/bundle.cts" 17 | }, 18 | "devDependencies": { 19 | "@koa/bodyparser": "^5.0.0", 20 | "@koa/cors": "^5.0.0", 21 | "@koa/router": "^12.0.1", 22 | "@types/koa": "^2.14.0", 23 | "@types/koa-static": "^4.0.4", 24 | "@types/koa__cors": "^5.0.0", 25 | "@types/koa__router": "^12.0.4", 26 | "@types/ws": "^8.5.10", 27 | "koa": "^2.15.0", 28 | "koa-static": "^5.0.0", 29 | "ws": "^8.16.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/devtools/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 2 | import { bodyParser } from '@koa/bodyparser' 3 | import cors from '@koa/cors' 4 | import Router from '@koa/router' 5 | import type { IpcMan } from 'ipcman' 6 | import { ipcMan } from 'ipcman' 7 | import Koa from 'koa' 8 | import serve from 'koa-static' 9 | import { createServer } from 'node:http' 10 | import { existsSync } from 'node:fs' 11 | import { join } from 'node:path' 12 | import { WebSocketServer } from 'ws' 13 | 14 | export * from 'ipcman' 15 | 16 | export interface IpcManDevtoolsConfig 17 | extends Omit { 18 | port?: number 19 | host?: string 20 | } 21 | 22 | interface Item { 23 | index: number 24 | timestamp: number 25 | data: IpcMan.Data 26 | } 27 | 28 | interface DevtoolsPlugin { 29 | name: string 30 | apply: (ctx: IpcManDevtoolsContext) => void 31 | } 32 | 33 | interface IpcManDevtoolsContext { 34 | register: (plugin: DevtoolsPlugin) => void 35 | emit: (data: IpcMan.Data) => void 36 | config: IpcManDevtoolsConfig 37 | } 38 | 39 | export const ipcManDevtoolsRaw: (config: IpcManDevtoolsConfig) => IpcManDevtoolsContext 40 | = (config: IpcManDevtoolsConfig) => { 41 | const parsedConfig = Object.assign( 42 | { 43 | port: 9009, 44 | }, 45 | config, 46 | ) 47 | 48 | const startTime = new Date().getTime() 49 | 50 | let i = 1 51 | const items: Item[] = [] 52 | 53 | const pushHandlers: ((raw: string) => void)[] = [] 54 | 55 | const emit = (data: IpcMan.Data) => { 56 | if (data.type.endsWith('after')) return 57 | const index = i++ 58 | const item = { 59 | index, 60 | timestamp: new Date().getTime(), 61 | data, 62 | } 63 | items.push(item) 64 | pushHandlers.forEach((x) => x(JSON.stringify([item]))) 65 | } 66 | 67 | const app = new Koa() 68 | const router = new Router({ 69 | prefix: '/v0', 70 | }) 71 | 72 | router.get('/info', (ctx) => { 73 | ctx.body = { 74 | startTime, 75 | } 76 | }) 77 | 78 | router.post('/get', (ctx) => { 79 | const { start, end } = ctx.request.body as { 80 | start?: number 81 | end?: number 82 | } 83 | 84 | ctx.body = items.slice(start, end) 85 | }) 86 | 87 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 88 | const server = createServer(app.callback()) 89 | 90 | const wsServer = new WebSocketServer({ 91 | server, 92 | path: '/v0/events', 93 | }) 94 | 95 | wsServer.on('connection', (ws) => { 96 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 97 | const fn = ws.send.bind(ws) 98 | ws.on('close', () => { 99 | pushHandlers.splice(pushHandlers.indexOf(fn), 1) 100 | }) 101 | pushHandlers.push(fn) 102 | 103 | ws.send(JSON.stringify(items)) 104 | }) 105 | 106 | const DEVTOOLS_BUILD_PATH = 107 | process.env.DEVTOOLS_BUILD_PATH || join(__dirname, 'build') 108 | 109 | let frontendErrMsg: string | null = null 110 | // Check if build folder exists 111 | if ( 112 | !existsSync(DEVTOOLS_BUILD_PATH) || 113 | !existsSync(join(DEVTOOLS_BUILD_PATH, 'index.html')) 114 | ) { 115 | frontendErrMsg = `ipcman: Frontend build does not exist, should be placed in ${DEVTOOLS_BUILD_PATH}` 116 | console.warn(frontendErrMsg) 117 | } 118 | 119 | app.use(cors()) 120 | 121 | if (!frontendErrMsg) { 122 | app.use(serve(DEVTOOLS_BUILD_PATH)) 123 | } else { 124 | app.use(async (ctx) => { 125 | ctx.body = frontendErrMsg 126 | }) 127 | } 128 | 129 | app 130 | .use(bodyParser()) 131 | .use(async (ctx, next) => { 132 | ctx.request.body ||= {} 133 | await next() 134 | }) 135 | 136 | .use(router.routes()) 137 | .use(router.allowedMethods()) 138 | 139 | server.listen(parsedConfig.port, parsedConfig.host) 140 | 141 | console.log( 142 | `ipcman-devtools: Listening on ${parsedConfig.host}:${parsedConfig.port}`, 143 | ) 144 | 145 | const ctx = { 146 | register: (plugin: DevtoolsPlugin) => plugin.apply(ctx), 147 | emit, 148 | config: parsedConfig, 149 | } 150 | 151 | return ctx 152 | } 153 | 154 | export const ipcManDevtoolsIPC: (config: IpcManDevtoolsConfig) => IpcManDevtoolsContext = (config: IpcManDevtoolsConfig)=>{ 155 | const ctx = ipcManDevtoolsRaw(config) 156 | ipcMan({ 157 | ...config, 158 | rawHandler: ctx.emit, 159 | }) 160 | return ctx; 161 | } 162 | 163 | export const ipcManDevtools = ipcManDevtoolsIPC 164 | -------------------------------------------------------------------------------- /packages/devtools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | }, 6 | "include": [ 7 | "src", 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /packages/ipcman/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

IpcMan

4 |

Electron IPC Hook/Devtools

5 | 6 | [![NPM Version](https://img.shields.io/npm/v/ipcman?style=flat-square)](https://www.npmjs.com/package/ipcman) 7 | [![npm package minimized gzipped size](https://img.shields.io/bundlejs/size/ipcman?style=flat-square)](https://www.npmjs.com/package/ipcman) 8 | [![License](https://img.shields.io/github/license/ilharp/ipcman?style=flat-square)](https://github.com/ilharp/ipcman/blob/master/LICENSE) 9 |
10 | 11 | 12 | 13 | ## Contents 14 | 15 | Package|Contents 16 | -|- 17 | ipcman|~3KB core module, providing Electron IPC hook with request/response tagging 18 | @ipcman/devtools|Devtools backend, providing an HTTP/WebSocket API 19 | @ipcman/devtools-fe|Devtools frontend, providing real-time IPC monitoring, history playback, request/response binding and inspector 20 | 21 | ## IpcMan Devtools Guide 22 | 23 | ### Step 1: Download IpcMan Devtools 24 | 25 | First, download IpcMan Devtools from [Releases](https://github.com/ilharp/ipcman/releases) and extract. 26 | 27 | ### Step 2: Inject 28 | 29 | Next, inject IpcMan Devtools into the target electron app. There are several ways to do this, 30 | the simplest of which is to directly modify the entry JavaScript file. We'll take [Waves Central](https://www.waves.com/downloads/central) as an example. 31 | 32 | Open `resources` folder under app directory. If there's no `app` folder in it but only `app.asar` file, run following command to extract `app` folder: 33 | 34 | ```sh 35 | npx asar extract app.asar app 36 | mv app.asar app.asar.bak 37 | ``` 38 | 39 | Next, open `package.json` inside `app`, you'll see the entrypoint of the app: 40 | 41 | ```js 42 | { 43 | // ... 44 | "main": "src/main.js", 45 | // ... 46 | } 47 | ``` 48 | 49 | Open `src/main.js` and insert the line below at the top of the file: 50 | 51 | ```js 52 | require('/path/to/downloaded/ipcman.js').ipcManDevtools({}) 53 | ``` 54 | 55 | ### Step 3: Run App 56 | 57 | Finally, directly run the app. Once the app starts, head to and you'll see the Devtools frontend. Happy hacking! 58 | 59 | ## `ipcman` Reference 60 | 61 | ### Events 62 | 63 | Event|Description 64 | --|-- 65 | send|Emits before sending a message to frontend. 66 | receive|Emits before receiving a message from frontend. 67 | 68 | ![](./graph.svg) 69 | 70 | ### Function `ipcMan: (config: IpcManConfig) => IpcManContext` 71 | 72 | The `ipcMan()` function. 73 | 74 | ### Interface `IpcManConfig` 75 | 76 | Options for `ipcMan()`. 77 | 78 | #### Function `handler: (data: IpcManData) => unknown` 79 | 80 | The event handler. Required. 81 | 82 | #### Function `getId?: (p: IpcArgs) => string | undefined` 83 | 84 | ID detect logic for resoving wrapped request/response of target app. Optional. 85 | 86 | ## `@ipcman/devtools` Reference 87 | 88 | ### Function `ipcManDevtools: (config: IpcManDevtoolsConfig) => Promise` 89 | 90 | Start IpcMan Devtools. 91 | 92 | ### Interface `IpcManDevtoolsConfig extends Omit` 93 | 94 | Options for `ipcManDevtools()`. Same as `IpcManConfig` but without `handler`. 95 | 96 | ## LICENSE 97 | 98 | MIT 99 | -------------------------------------------------------------------------------- /packages/ipcman/graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
Renderer Process
Renderer Process
send-after
send-after
receive
receive
receive-after
receive-after
Plain send/receive
Plain se...
Invoke
Invoke
send
send
receive
receive
IpcMain
IpcMain
receive-after
receive-after
send
send
send-after
send-after
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /packages/ipcman/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ipcman", 3 | "version": "0.1.2", 4 | "description": "Electron IPC Hook/Devtools", 5 | "repository": "https://github.com/ilharp/ipcman.git", 6 | "author": { 7 | "name": "Il Harper", 8 | "email": "hi@ilharper.com", 9 | "url": "https://ilharper.com" 10 | }, 11 | "license": "MIT", 12 | "type": "module", 13 | "main": "cjs/index.js", 14 | "module": "esm/index.js", 15 | "typings": "esm/index.d.ts", 16 | "exports": { 17 | ".": { 18 | "require": "./cjs/index.js", 19 | "import": "./esm/index.js", 20 | "types": "./esm/index.d.ts" 21 | }, 22 | "./package.json": "./package.json" 23 | }, 24 | "files": [ 25 | "cjs", 26 | "esm" 27 | ], 28 | "sideEffects": false, 29 | "scripts": { 30 | "build": "concurrently -n esm,cjs yarn:build:esm yarn:build:cjs", 31 | "build:esm": "tsc -b tsconfig.json", 32 | "build:cjs": "tsc -b tsconfig.cjs.json", 33 | "watch": "concurrently -n esm,cjs yarn:watch:esm yarn:watch:cjs", 34 | "watch:esm": "tsc -b -w tsconfig.json", 35 | "watch:cjs": "tsc -b -w tsconfig.cjs.json" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/ipcman/src/index.ts: -------------------------------------------------------------------------------- 1 | import type EventEmitter from 'node:events' 2 | 3 | import type { 4 | IpcMain, 5 | IpcMainEvent, 6 | IpcMainInvokeEvent, 7 | WebContents, 8 | } from 'electron' 9 | 10 | // eslint-disable-next-line import/no-unresolved 11 | import { ipcMain } from 'electron' 12 | 13 | export namespace IpcMan { 14 | export type IpcEvent = Omit & { 15 | sender: WebContents & Record 16 | } 17 | 18 | export type IpcSender = ( 19 | channel: string, 20 | ...detail: IpcArgs 21 | ) => void 22 | 23 | export interface BaseData { 24 | channel: string 25 | args: unknown[] 26 | } 27 | 28 | export type SendData = ( 29 | | { 30 | type: 'send' 31 | cancellable: true 32 | } 33 | | { 34 | type: 'send-after' 35 | cancellable: false 36 | } 37 | ) & 38 | BaseData & 39 | ( 40 | | { 41 | binded: true 42 | bindId: string 43 | bindData: ReceiveData | null 44 | } 45 | | { 46 | binded: false 47 | bindId: null 48 | bindData: null 49 | } 50 | ) 51 | 52 | export type ReceiveData = ( 53 | | { 54 | type: 'receive' 55 | cancellable: true 56 | } 57 | | { 58 | type: 'receive-after' 59 | cancellable: false 60 | } 61 | ) & 62 | BaseData & 63 | ( 64 | | { 65 | binded: true 66 | bindId: string 67 | bindData: SendData | null 68 | } 69 | | { 70 | binded: false 71 | bindId: null 72 | bindData: null 73 | } 74 | ) 75 | 76 | export type Data = SendData | ReceiveData 77 | 78 | export type EventContext = { 79 | stopPropagation: () => void 80 | } & (Extract extends infer U 81 | ? U extends Data 82 | ? U 83 | : never 84 | : never) & 85 | (Extract extends { 86 | cancellable: true 87 | } 88 | ? { cancel: () => void } 89 | : object) 90 | 91 | type Awaitable = T | Promise 92 | 93 | export interface IpcManConfig { 94 | rawHandler: (data: EventContext) => Awaitable 95 | getId?: (p: IpcArgs) => string | undefined 96 | enableRequestCache?: boolean 97 | requestCacheMaxAge?: number 98 | } 99 | 100 | export type IpcManEventHandler = 101 | Extract extends { cancellable: true } 102 | ? ( 103 | data: EventContext, 104 | ) => Awaitable< 105 | | (( 106 | data: EventContext< 107 | T extends 'send' ? 'send-after' : 'receive-after' 108 | >, 109 | ) => Awaitable) 110 | | undefined 111 | > 112 | : (data: EventContext) => Awaitable 113 | 114 | export interface IpcManContext { 115 | emit: EventEmitter['emit'] 116 | senderExcludeSymbol: symbol 117 | on: ( 118 | type: T, 119 | handler: IpcManEventHandler, 120 | ) => IpcManContext 121 | remove: (handler: unknown) => void 122 | once: ( 123 | type: T, 124 | handler: IpcManEventHandler, 125 | ) => IpcManContext 126 | } 127 | 128 | export const ipcMan = ( 129 | _config: IpcManConfig, 130 | ): IpcManContext => { 131 | const senderExcludeSymbol: unique symbol = Symbol() 132 | const config = { 133 | enableRequestCache: true, 134 | requestCacheMaxAge: 1000 * 10, 135 | ..._config, 136 | } 137 | 138 | const eventHandlers = new Map>() 139 | 140 | const receiveEventCache = new Map< 141 | string, 142 | { data: ReceiveData; timestamp: number } 143 | >() 144 | 145 | const cleanCache = () => { 146 | if (config.requestCacheMaxAge === 0) return 147 | const now = Date.now() 148 | for (const [id, { timestamp }] of receiveEventCache) { 149 | if (now - timestamp > config.requestCacheMaxAge) { 150 | receiveEventCache.delete(id) 151 | } 152 | } 153 | } 154 | 155 | if (config.enableRequestCache) { 156 | setInterval(cleanCache, config.requestCacheMaxAge / 5) 157 | } 158 | 159 | let ctxRef: IpcManContext | null = null 160 | 161 | const emitManEvent = async < 162 | T extends EventContext | { stopPropagation?: undefined }, 163 | R extends Data['type'], 164 | >( 165 | type: R, 166 | _context: T, 167 | ) => { 168 | let stopPropagation = false 169 | const context = { 170 | stopPropagation() { 171 | stopPropagation = true 172 | }, 173 | ..._context, 174 | } as EventContext 175 | 176 | config.rawHandler?.(context) 177 | if (stopPropagation) return 178 | 179 | const set = eventHandlers.get(type) 180 | for (const handler of set ?? []) { 181 | const res = await (handler as IpcManEventHandler)( 182 | context as unknown as EventContext, 183 | ) 184 | 185 | if (res && !type.endsWith('after')) { 186 | ctxRef!.once( 187 | (type + '-after') as unknown as R, 188 | res as IpcManEventHandler, 189 | ) 190 | } 191 | 192 | if (stopPropagation) return 193 | } 194 | } 195 | 196 | const emit = ipcMain.emit.bind(ipcMain) 197 | ipcMain.emit = function ( 198 | this: IpcMain, 199 | eventName: string | symbol, 200 | event: IpcEvent, 201 | ...p: IpcArgs 202 | ) { 203 | void (async () => { 204 | const sender = event.sender 205 | if (!sender[senderExcludeSymbol]) { 206 | sender[senderExcludeSymbol] = true 207 | 208 | const send = sender.send.bind(sender) 209 | sender.send = function (channel, ...e) { 210 | void (async () => { 211 | const id = config.getId?.(e as IpcArgs) 212 | const ref = null //receiveEventCache.get(id ?? '') 213 | let cancelled = false 214 | 215 | const data = { 216 | type: 'send', 217 | channel, 218 | args: e, 219 | binded: !!id, 220 | bindId: id ?? null, 221 | bindData: null, 222 | cancellable: true, 223 | } as SendData 224 | 225 | // if (ref) { 226 | // ref.data.binded = true 227 | // ref.data.bindId = id! 228 | // ref.data.bindData = data 229 | // } 230 | 231 | await emitManEvent('send', { 232 | ...data, 233 | cancellable: true, 234 | cancel() { 235 | cancelled = true 236 | }, 237 | } as unknown as EventContext<'send'>) 238 | 239 | if (cancelled) return 240 | send.call(this, channel, ...(e as unknown[])) 241 | 242 | await emitManEvent('send-after', { 243 | ...data, 244 | cancellable: false, 245 | } as unknown as EventContext<'send-after'>) 246 | 247 | if (ref) { 248 | receiveEventCache.delete(id!) 249 | } 250 | })() 251 | } 252 | } 253 | 254 | const id = config.getId?.(p) 255 | 256 | const data: ReceiveData = { 257 | type: 'receive', 258 | channel: eventName as string, 259 | args: p, 260 | binded: !!id, 261 | bindId: id, 262 | bindData: null, 263 | cancellable: true, 264 | } as ReceiveData 265 | 266 | // receiveEventCache.set(id!, { data, timestamp: Date.now() }) 267 | 268 | let cancelled = false 269 | await emitManEvent('receive', { 270 | ...data, 271 | cancellable: true, 272 | cancel() { 273 | cancelled = true 274 | }, 275 | } as unknown as EventContext<'receive'>) 276 | 277 | if (cancelled) return 278 | emit.call(this, eventName, event, ...p) 279 | 280 | await emitManEvent('receive-after', { 281 | ...data, 282 | cancellable: false, 283 | } as unknown as EventContext<'receive-after'>) 284 | })() 285 | return false 286 | } 287 | 288 | const handle = ipcMain.handle.bind(ipcMain) 289 | ipcMain.handle = function (method, fn) { 290 | if (typeof fn !== 'function') { 291 | throw new TypeError( 292 | `ipcman: Expected handler to be a function, but found type '${typeof fn}'`, 293 | ) 294 | } 295 | 296 | const wrappedFn = async ( 297 | event: IpcMainInvokeEvent, 298 | ...args: unknown[] 299 | ) => { 300 | const dataReceive = { 301 | type: 'receive', 302 | channel: method, 303 | args, 304 | binded: false, 305 | bindId: null, 306 | bindData: null, 307 | cancellable: true, 308 | } as ReceiveData 309 | 310 | let cancelled = false 311 | await emitManEvent('receive', { 312 | ...dataReceive, 313 | cancellable: true, 314 | cancel() { 315 | cancelled = true 316 | }, 317 | } as unknown as EventContext<'receive'>) 318 | 319 | const result = cancelled 320 | ? undefined 321 | : ((await Promise.resolve(fn(event, ...args))) as unknown) 322 | 323 | const resArr = [result] 324 | 325 | await emitManEvent('receive-after', { 326 | ...dataReceive, 327 | cancellable: false, 328 | } as unknown as EventContext<'receive-after'>) 329 | 330 | await emitManEvent('send', { 331 | type: 'send', 332 | channel: method, 333 | args: resArr, 334 | binded: false, 335 | bindId: null, 336 | bindData: null, 337 | cancellable: true, 338 | } as unknown as EventContext<'send'>) 339 | 340 | return resArr[0] 341 | } 342 | 343 | handle.call(this, method, wrappedFn) 344 | } 345 | 346 | const addEventListener: IpcManContext['on'] = (type, handler) => { 347 | if (!eventHandlers.has(type)) eventHandlers.set(type, new Set()) 348 | const set = eventHandlers.get(type) 349 | 350 | if (!set?.has(handler)) set?.add(handler) 351 | return ctxRef! 352 | } 353 | 354 | const ctx = { 355 | emit, 356 | senderExcludeSymbol, 357 | on: addEventListener, 358 | remove(handler: unknown) { 359 | for (const [_, set] of eventHandlers) if (set.delete(handler)) return 360 | }, 361 | once(type, handler) { 362 | const wrappedHandler = (async (context) => { 363 | const res = await handler(context) 364 | ctxRef!.remove(wrappedHandler) 365 | return res 366 | }) as IpcManEventHandler 367 | return ctxRef!.on(type, wrappedHandler) 368 | }, 369 | } as IpcManContext 370 | 371 | ctxRef = ctx 372 | 373 | return ctx 374 | } 375 | } 376 | 377 | export const ipcMan = IpcMan.ipcMan 378 | -------------------------------------------------------------------------------- /packages/ipcman/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "moduleResolution": "Node10", 6 | "rootDir": "src", 7 | "outDir": "cjs", 8 | "declaration": false, 9 | }, 10 | "include": [ 11 | "src", 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /packages/ipcman/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "esm", 6 | }, 7 | "include": [ 8 | "src", 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /scripts/build.cts: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild' 2 | import { copyFile, readFile, writeFile } from 'fs/promises' 3 | 4 | void (async () => { 5 | const startTs = Date.now() 6 | await Promise.all([build({ 7 | entryPoints: ['packages/ipcman/src/index.ts'], 8 | bundle: true, 9 | platform: 'node', 10 | target: 'node18', 11 | external: ['fs/promises', 'node:path', 'electron'], 12 | sourcemap: true, 13 | outfile: 'build/ipcman.js', 14 | }), build({ 15 | entryPoints: ['packages/ipcman/src/index.ts'], 16 | bundle: true, 17 | platform: 'node', 18 | target: 'node18', 19 | external: ['fs/promises', 'node:path', 'electron'], 20 | sourcemap: true, 21 | outfile: 'packages/ipcman/cjs/index.js', 22 | format: 'cjs' 23 | }), build({ 24 | entryPoints: ['packages/ipcman/src/index.ts'], 25 | bundle: true, 26 | platform: 'node', 27 | target: 'node18', 28 | external: ['fs/promises', 'node:path', 'electron'], 29 | sourcemap: true, 30 | outfile: 'packages/ipcman/esm/index.js', 31 | format: 'esm' 32 | })]) 33 | 34 | await build({ 35 | entryPoints: ['packages/devtools/src/index.ts'], 36 | bundle: true, 37 | platform: 'node', 38 | target: 'node18', 39 | external: ['fs/promises', 'node:path', 'electron'], 40 | sourcemap: true, 41 | outfile: 'build/devtools.js', 42 | }) 43 | 44 | // replace require("electron") with eval('require("electron")') 45 | const devtools = await readFile('build/devtools.js', 'utf-8') 46 | await writeFile('build/devtools.js', devtools.replaceAll('require("electron")', 'eval(\'require("electron")\')')) 47 | 48 | await copyFile('build/devtools.js','packages/devtools/lib/index.js') 49 | 50 | console.log(`Build complete in ${Date.now() - startTs}ms`) 51 | })() 52 | -------------------------------------------------------------------------------- /scripts/bundle.cts: -------------------------------------------------------------------------------- 1 | import { analyzeMetafile, context } from 'esbuild' 2 | import { join } from 'node:path' 3 | import { argv, cwd } from 'node:process' 4 | 5 | const [_node, _tsNode, mode] = argv 6 | const wd = cwd() 7 | 8 | void (async () => { 9 | const ctx = await context({ 10 | entryPoints: [join(wd, 'src/index.ts')], 11 | write: true, 12 | outdir: 'lib', 13 | 14 | platform: 'node', 15 | format: 'cjs', 16 | tsconfig: join(wd, 'tsconfig.json'), 17 | 18 | define: {}, 19 | external: ['electron'], 20 | 21 | bundle: true, 22 | minify: true, 23 | sourcemap: false, 24 | 25 | metafile: true, 26 | color: true, 27 | }) 28 | 29 | if (mode === 'watch') await ctx.watch() 30 | else { 31 | console.log(await analyzeMetafile((await ctx.rebuild()).metafile)) 32 | await ctx.dispose() 33 | } 34 | })() 35 | -------------------------------------------------------------------------------- /scripts/clean.cts: -------------------------------------------------------------------------------- 1 | import { rm } from 'fs/promises' 2 | import { resolve } from 'node:path' 3 | 4 | void Promise.all( 5 | [ 6 | '../build', 7 | '../packages/ipcman/cjs', 8 | '../packages/ipcman/esm', 9 | '../packages/ipcman/tsconfig.cjs.tsbuildinfo', 10 | '../packages/ipcman/tsconfig.tsbuildinfo', 11 | '../packages/devtools/lib', 12 | '../packages/devtools-fe/build', 13 | ].map((x) => 14 | rm(resolve(__dirname, x), { 15 | force: true, 16 | recursive: true, 17 | }), 18 | ), 19 | ) 20 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "declaration": true, 7 | "composite": false, 8 | "incremental": true, 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "jsx": "react-jsx", 12 | "noPropertyAccessFromIndexSignature": false, 13 | "allowJs": true, 14 | "strict": false 15 | }, 16 | "ts-node": { 17 | "compilerOptions": { 18 | "moduleResolution": "Node", 19 | } 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base", 3 | "include": [ 4 | "jest.config.ts", 5 | "packages", 6 | "scripts" 7 | ], 8 | } 9 | --------------------------------------------------------------------------------