├── .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 |
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 |
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 | [](https://www.npmjs.com/package/ipcman)
7 | [](https://www.npmjs.com/package/ipcman)
8 | [](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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------