├── .babelrc ├── .github └── workflows │ └── coverage.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── README_CN.md ├── docs ├── API.md ├── API_CN.md ├── OPTIMIZATION.md └── REWRITE.md ├── index.html ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── consts.js ├── index.d.ts ├── index.js ├── sandbox.js ├── scope.js ├── transformer.js └── utils.js └── tests ├── api.test.js ├── async.test.js ├── benchmark.js ├── breakpoint.test.js ├── class.test.js ├── descriptor.test.js ├── module.test.js ├── react.test.js ├── sandbox.test.js ├── transform.test.js ├── trick.test.js ├── utils.js └── vue.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "chrome": 55 6 | } 7 | }] 8 | ], 9 | "plugins": [ 10 | "@babel/plugin-transform-runtime" 11 | ] 12 | } -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | coverage: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out repository code 11 | uses: actions/checkout@v3 12 | - name: Setup node environment 13 | uses: actions/setup-node@v3.4.1 14 | - name: Install node modules 15 | run: npm install 16 | - name: Test code 17 | run: npm test 18 | - name: Send coverage info to Coveralls 19 | uses: coverallsapp/github-action@1.1.3 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | /coverage 5 | /dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | /src 5 | /docs 6 | /tests 7 | /coverage 8 | /.github 9 | /.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Wechat.js 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 | # vDebugger · [![npm](https://img.shields.io/npm/v/vdebugger.svg?style=flat-square)](https://www.npmjs.com/package/vdebugger) [![github-actions](https://img.shields.io/github/actions/workflow/status/wechatjs/vdebugger/coverage.yml?style=flat-square)](https://github.com/wechatjs/vdebugger/actions/workflows/coverage.yml) [![coveralls](https://img.shields.io/coveralls/github/wechatjs/vdebugger.svg?style=flat-square)](https://coveralls.io/github/wechatjs/vdebugger) 2 | 3 | **English | [简体中文](./README_CN.md)** 4 | 5 | A Front-End JavaScript Breakpoint Debugger. 6 | 7 | Make it possible to debug your JavaScript in browser, Node.js, JavaScriptCore or other JavaScript runtimes without any extra supports from host environments. [Try `vDebugger` on playground.](https://jsbin.com/jibezuvohe/edit?js,console) 8 | 9 | ## Installation 10 | 11 | `vDebugger` requires ES2015 for [`Generator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) support. 12 | 13 | Install by NPM: 14 | 15 | ```bash 16 | $ npm install vdebugger 17 | ``` 18 | 19 | ```js 20 | import vDebugger from 'vdebugger'; 21 | ``` 22 | 23 | Or, import from CDN: 24 | 25 | ```html 26 | 27 | 28 | ``` 29 | 30 | ## Getting Started 31 | 32 | ```js 33 | import vDebugger from 'vdebugger'; 34 | 35 | const run = vDebugger.debug(`// here's line 1 36 | let a = 1; 37 | a = 2; // break at line 3 later 38 | a = 3; 39 | a = 4; 40 | console.log(a); // output "4" 41 | `, './test.js'); 42 | // the second argument is debuggerId for identifing the script, 43 | // which normally is the script url 44 | 45 | vDebugger.setBreakpoint('./test.js', 3); // break at line 3 46 | 47 | run(); 48 | 49 | vDebugger.evaluate('console.log(a)'); // output "1" 50 | 51 | vDebugger.resume('stepOver'); 52 | 53 | vDebugger.evaluate('console.log(a)'); // output "2" 54 | 55 | vDebugger.resume(); // output "4" 56 | ``` 57 | 58 | ## Pre-Transform 59 | 60 | `vDebugger` needs code transform for break, while transforming at runtime by default causes performance loss, and therefore, `vDebugger` provides a method called `transform` for code pre-transform at compilation. 61 | 62 | ```js 63 | /* ----- Compilation ----- */ 64 | 65 | // pre-transform at compilation, and pass the result to vDebugger.debug at runtime 66 | import vDebugger from 'vdebugger'; 67 | 68 | const result = vDebugger.transform(`// here's line 1 69 | let a = 1; 70 | a = 2; // break at line 3 later 71 | a = 3; 72 | a = 4; 73 | console.log(a); // output "4" 74 | `, './test.js'); 75 | // the second argument is debuggerId for identifing the script, 76 | // which normally is the script url 77 | ``` 78 | 79 | Pass the transformed `result` to `vDebugger.debug` at runtime. 80 | 81 | ```js 82 | /* ----- Runtime ----- */ 83 | 84 | // except for passing the transformed result to vDebugger.debug, 85 | // runtime debugging has no difference from which without pre-transform 86 | import vDebugger from 'vdebugger'; 87 | 88 | const run = vDebugger.debug(result); 89 | // the result contains debuggerId, so the second argument is optional 90 | 91 | vDebugger.setBreakpoint('./test.js', 3); // break at line 3 92 | 93 | run(); 94 | 95 | vDebugger.evaluate('console.log(a)'); // output "1" 96 | 97 | vDebugger.resume('stepOver'); 98 | 99 | vDebugger.evaluate('console.log(a)'); // output "2" 100 | 101 | vDebugger.resume(); // output "4" 102 | ``` 103 | 104 | ## Development 105 | 106 | ```bash 107 | $ npm start 108 | ``` 109 | 110 | ## Testing 111 | 112 | ```bash 113 | $ npm test 114 | ``` 115 | 116 | ## Documentation 117 | 118 | - [API Documentation](./docs/API.md) 119 | 120 | ## License 121 | 122 | [MIT](./LICENSE) 123 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # vDebugger · [![npm](https://img.shields.io/npm/v/vdebugger.svg?style=flat-square)](https://www.npmjs.com/package/vdebugger) [![github-actions](https://img.shields.io/github/actions/workflow/status/wechatjs/vdebugger/coverage.yml?style=flat-square)](https://github.com/wechatjs/vdebugger/actions/workflows/coverage.yml) [![coveralls](https://img.shields.io/coveralls/github/wechatjs/vdebugger.svg?style=flat-square)](https://coveralls.io/github/wechatjs/vdebugger) 2 | 3 | **[English](./README.md) | 简体中文** 4 | 5 | 前端JavaScript断点调试工具。 6 | 7 | 让你在浏览器、Node.js、JavaScriptCore或其他JavaScript运行时中调试JavaScript,而不需要宿主环境提供任何额外的支持。[点击查看Demo。](https://jsbin.com/jibezuvohe/edit?js,console) 8 | 9 | ## 安装 10 | 11 | `vDebugger` 需要ES2015的支持,因为使用到了 [`Generator`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Generator)。 12 | 13 | 通过NPM安装: 14 | 15 | ```bash 16 | $ npm install vdebugger 17 | ``` 18 | 19 | ```js 20 | import vDebugger from 'vdebugger'; 21 | ``` 22 | 23 | 或者通过CDN引入: 24 | 25 | ```html 26 | 27 | 28 | ``` 29 | 30 | ## 快速开始 31 | 32 | ```js 33 | import vDebugger from 'vdebugger'; 34 | 35 | const run = vDebugger.debug(`// 这里是第1行 36 | let a = 1; 37 | a = 2; // 设置断点在第3行 38 | a = 3; 39 | a = 4; 40 | console.log(a); // 输出 4 41 | `, './test.js'); // 第二个参数为debuggerId,用于标识脚本,通常为脚本url 42 | 43 | vDebugger.setBreakpoint('./test.js', 3); // 设置断点在第3行 44 | 45 | run(); 46 | 47 | vDebugger.evaluate('console.log(a)'); // 输出 1 48 | 49 | vDebugger.resume('stepOver'); 50 | 51 | vDebugger.evaluate('console.log(a)'); // 输出 2 52 | 53 | vDebugger.resume(); // 输出 4 54 | ``` 55 | 56 | ## 预转换 57 | 58 | 由于 `vDebugger` 需要对源码进行转换才能进行断点,而默认在运行时转换的话,初始化性能会有一定损失,因此提供了 `transform` 接口进行编译期转换。 59 | 60 | ```js 61 | /* ----- 编译期 ----- */ 62 | 63 | // 编译期转换,将转换结果result原封不动地交给运行时的vDebugger.debug接口即可 64 | import vDebugger from 'vdebugger'; 65 | 66 | const result = vDebugger.transform(`// 这里是第1行 67 | let a = 1; 68 | a = 2; // 设置断点在第3行 69 | a = 3; 70 | a = 4; 71 | console.log(a); // 输出 4 72 | `, './test.js'); // 第二个参数为debuggerId,用于标识脚本,通常为脚本url 73 | ``` 74 | 75 | 拿到转换结果 `result` 后,在运行时传入 `vDebugger.debug` 接口。 76 | 77 | 78 | ```js 79 | /* ----- 运行时 ----- */ 80 | 81 | // 运行时调试,除了将编译结果result传入vDebugger.debug接口,其他用法和没有预编译时保持一致 82 | import vDebugger from 'vdebugger'; 83 | 84 | const run = vDebugger.debug(result); // result中会带有debuggerId信息,因此第2个参数可选 85 | 86 | vDebugger.setBreakpoint('./test.js', 3); // 设置断点在第3行 87 | 88 | run(); 89 | 90 | vDebugger.evaluate('console.log(a)'); // 输出 1 91 | 92 | vDebugger.resume('stepOver'); 93 | 94 | vDebugger.evaluate('console.log(a)'); // 输出 2 95 | 96 | vDebugger.resume(); // 输出 4 97 | ``` 98 | 99 | ## 开发 100 | 101 | ```bash 102 | $ npm start 103 | ``` 104 | 105 | ## 测试 106 | 107 | ```bash 108 | $ npm test 109 | ``` 110 | 111 | ## 文档 112 | 113 | - [接口文档](./docs/API_CN.md) 114 | 115 | ## 协议 116 | 117 | [MIT](./LICENSE) 118 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API Documentation 2 | 3 | **English | [简体中文](./API_CN.md)** 4 | 5 | - [`debug`](#debug) 6 | - [`transform`](#transform) 7 | - [`resume`](#resume) 8 | - [`evaluate`](#evaluate) 9 | - [`getPossibleBreakpoints`](#getPossibleBreakpoints) 10 | - [`setBreakpoint`](#setbreakpoint) 11 | - [`removeBreakpoint`](#removebreakpoint) 12 | - [`setBreakpointsActive`](#setbreakpointsactive) 13 | - [`setExecutionPause`](#setexecutionpause) 14 | - [`setExceptionPause`](#setexceptionpause) 15 | - [`getPausedInfo`](#getpausedinfo) 16 | - [`getScopeChain`](#getscopechain) 17 | - [`getScriptContent`](#getscriptcontent) 18 | - [`runInNativeEnv`](#runinnativeenv) 19 | - [`runInSkipOver`](#runinskipover) 20 | - [`setModuleRequest`](#setmodulerequest) 21 | - [`addEventListener` / `removeEventListener`](#addeventlistener--removeeventlistener) 22 | 23 | ## `debug` 24 | 25 | Debug scripts. Two arguments accepted, which are script code string `script` and script debug id `debuggerId`, respectively. 26 | 27 | The `script` can be not only the source code, but also the transformed result generated by the `transform` method. And the `debuggerId` is the script url normally. If missing `debuggerId`, the debugging script will be treated as a temporary script, and a random `debuggerId` will be generated. 28 | 29 | This method returns an execution function called `run`, for code real execution. Therefore, before `run` calls, you can use other APIs like `setBreakpoint` to modify breakpoints. If inputed arguments is invalid, or current environment doesn't support debug, this method returns `false`. 30 | 31 | ```ts 32 | function debug(script: string, debuggerId?: string): (() => void) | false 33 | ``` 34 | 35 | ## `transform` 36 | 37 | Transform scripts. Two arguments accepted, which are script code string `script` and script debug id `debuggerId`, respectively. 38 | 39 | The `debuggerId` is the script url normally. If missing `debuggerId`, the debugging script will be treated as a temporary script, and a random `debuggerId` will be generated. 40 | 41 | This method returns a code transformed result, which can be passed to the `debug` method for debugging scripts at runtime. If inputed arguments is invalid, this method returns `false`. 42 | 43 | ```ts 44 | function transform(script: string, debuggerId?: string): string | false 45 | ``` 46 | 47 | ## `resume` 48 | 49 | Resume execution when paused by hitting breakpoints. One optional argument accepted, which can be `stepInto`, `stepOver` or `stepOut`: 50 | 51 | 1. Continue executing by default if missing. 52 | 2. Step into child process if passing `stepInto`. 53 | 3. Step over child process if passing `stepOver`. 54 | 4. Step out to parent process if passing `stepOut`. 55 | 56 | This method returns a boolean value to inform whether resume succeeds. 57 | 58 | ```ts 59 | function resume(type?: ResumeType): boolean 60 | 61 | type ResumeType = 'stepInto' | 'stepOver' | 'stepOut' 62 | ``` 63 | 64 | ## `evaluate` 65 | 66 | Evaluate expressions in specific scopes. Two arguments accepted, which are expression code string `expression` and call frame id `callFrameId`, respectively. 67 | 68 | The `callFrameId` is optional, which can be achieved from the `scopeChain` of paused info, by `getPausedInfo` method or `paused` event. If `callFrameId` inputed, the `expression` will be evaluated in the corresponding scope. Or, if `callFrameId` missed, the `expression` will be evaluated in the global scope. 69 | 70 | ```ts 71 | function evaluate(expression: string, callFrameId?: number): Result | false 72 | ``` 73 | 74 | ## `getPossibleBreakpoints` 75 | 76 | Get all possible breakpoints of a script. One script debug id argument accepted. 77 | 78 | If get succeeds, this method returns an array of all possible breakpoint info, including breakpoint id `id`, exact break line number `lineNumber` (1-based) and exact break column number `columnNumber` (0-based) of all breakpoints. Or, if get fails, this method returns `false`. 79 | 80 | ```ts 81 | function getPossibleBreakpoints(debuggerId: string): Breakpoint[] | false 82 | 83 | interface Breakpoint { id: number, lineNumber: number, columnNumber: number } 84 | ``` 85 | 86 | ## `setBreakpoint` 87 | 88 | Set breakpoints by script debug id, including 2 overloads: 89 | 90 | 1. Three arguments accepted, which are script debug id `debuggerId`, line number `lineNumber` (1-based) and optional break condition `condition`, respectively. 91 | 2. Four arguments accepted, which are script debug id `debuggerId`, line number `lineNumber` (1-based), column number `columnNumber` (0-based) and optional break condition `condition`, respectively. 92 | 93 | Besides, the `condition` is an expression string, and execution will be paused if the evaluation returns `true`. If `condition` missed, execution will be paused when hitting the corresponding position by default. 94 | 95 | If set succeeds, this method returns the breakpoint info, including breakpoint id `id`, exact break line number `lineNumber` (1-based) and exact break column number `columnNumber` (0-based). Or, if set fails, this method returns `false`. 96 | 97 | ```ts 98 | function setBreakpoint(debuggerId: string, lineNumber: number, condition?: string): Breakpoint | false 99 | function setBreakpoint(debuggerId: string, lineNumber: number, columnNumber: number, condition?: string): Breakpoint | false 100 | 101 | interface Breakpoint { id: number, lineNumber: number, columnNumber: number } 102 | ``` 103 | 104 | ## `removeBreakpoint` 105 | 106 | Remove breakpoint by breakpoint id. One breakpoint id argument accepted. 107 | 108 | This method returns a boolean value to inform whether remove succeeds. 109 | 110 | ```ts 111 | function removeBreakpoint(id: number): boolean 112 | ``` 113 | 114 | ## `setBreakpointsActive` 115 | 116 | Set whether all breakpoints are active. One boolean argument accepted. 117 | 118 | This method returns a boolean value to inform whether breakpoints are active. 119 | 120 | ```ts 121 | function setBreakpointsActive(value: boolean): boolean 122 | ``` 123 | 124 | ## `setExecutionPause` 125 | 126 | Set whether execution pauses. One boolean argument accepted. 127 | 128 | This method returns a boolean value to inform whether execution will pause. If the return is `true`, execution will pause at the next coming step. 129 | 130 | ```ts 131 | function setExecutionPause(value: boolean): boolean 132 | ``` 133 | 134 | ## `setExceptionPause` 135 | 136 | Set whether execution pauses when exception. One boolean argument accepted. 137 | 138 | This method returns a boolean value to inform whether execution will pause when exception. If the return is `true`, execution will pause right before the exception thro 139 | 140 | ```ts 141 | function setExceptionPause(value: boolean): boolean 142 | ``` 143 | 144 | ## `getPausedInfo` 145 | 146 | Get paused info when execution paused. 147 | 148 | When execution paused, This method returns debuger id `debuggerId`, breakpoint id `breakpointId`, line number `lineNumber` (1-based), column number `columnNumber` (0-based), scope chain `scopeChain` and script source content `scriptContent`. If execution doesn't pause, this method returns `false`. 149 | 150 | The `scopeChain` includes the global scope, function scopes and block scopes, and you can check if the `callFrame` field exists to determine whether the scope is the global scope or function scope. The call stack can be generated by filtering all scopes with the `callFrame` field. 151 | 152 | Also, the `callFrame` contains a `callFrameId` which can be passed to the `evaluate` method for evaluating expressions in specific scopes. 153 | 154 | ```ts 155 | function getPausedInfo(): PausedInfo | false 156 | 157 | interface PausedInfo { breakpointId?: number, reason?: string, data?: any, debuggerId: string, lineNumber: number, columnNumber: number, scopeChain: Scope[], scriptContent: string } 158 | interface Scope { eval: (expression: string) => any, name: string, callFrameId: number, callFrame?: CallFrame } 159 | interface CallFrame { debuggerId: string, lineNumber: number, columnNumber: number } 160 | ``` 161 | 162 | ## `getScopeChain` 163 | 164 | Get current scope chain. 165 | 166 | ```ts 167 | function getScopeChain(): Scope[] 168 | 169 | interface Scope { eval: (expression: string) => any, name: string, callFrameId: number, callFrame?: CallFrame } 170 | interface CallFrame { debuggerId: string, lineNumber: number, columnNumber: number } 171 | ``` 172 | 173 | ## `getScriptContent` 174 | 175 | Get script source content by debugger id `debuggerId`. 176 | 177 | ```ts 178 | function getScriptContent(debuggerId: string): string 179 | ``` 180 | 181 | ## `runInNativeEnv` 182 | 183 | Run callbacks in the native environment, like browser or node.js. One argument accepted, which is the `callback` function that need to run. 184 | 185 | The `debug` method executes scripts in a internal sandbox, so this method is used to run callbacks in the native host environment. 186 | 187 | If run succeeds, this method returns the callback return. Or, if run fails, this method returns `false`. 188 | 189 | ```ts 190 | function runInNativeEnv(callback: () => Return): Return | false 191 | ``` 192 | 193 | ## `runInSkipOver` 194 | 195 | Run callbacks with skipping over all breakpoints. One argument accepted, which is the `callback` function that need to run. 196 | 197 | When paused by hitting breakpoints, if you call some functions of debugging scripts in the internal sandbox, they will be blocked until resume. So, this method is generally used to call the functions in the sandbox continuously by `evaluate` method, in order to get variables or returns in the corresponding scope. 198 | 199 | If run succeeds, this method returns the callback return. Or, if run fails, this method returns `false`. 200 | 201 | ```ts 202 | function runInSkipOver(callback: () => Return): Return | false 203 | ``` 204 | 205 | ## `setModuleRequest` 206 | 207 | Set the request function of modules. One argument accepted, which is the custom `request` function that need to override the internal default request function. 208 | 209 | When a module requests other modules, `fetch` is used for request by default. If `fetch` isn't supported, or you want to customize the behavior of module request like caching module response, you can use this method to set the request function of modules. 210 | 211 | The `request` function requires a module url `importUrl` as input, and is requested to return a module script wrapped by `Promise`. Also, the module script can be not only the source code, but also the transformed result generated by the `transform` method. 212 | 213 | This method returns a boolean value to inform whether set succeeds. 214 | 215 | ```ts 216 | function setModuleRequest(request: (importUrl: string) => Promise): boolean 217 | ``` 218 | 219 | ## `addEventListener` / `removeEventListener` 220 | 221 | Add or remove event listeners. Two arguments accepted, which are `event` and callback function `listener`, respectively. 222 | 223 | Currently we have four events, which are `paused`, `resumed`, `error` and `sandboxchange`: 224 | 225 | 1. Paused event `paused` emits when break, and returns paused info which is the same as the return of `getPausedInfo` method. 226 | 2. Resumed event `resumed` emits when resume, and has no returns. 227 | 3. Error event `error` emits when an error occurred, and returns error info, including reasons and the scope chain. 228 | 4. Sandbox status change event `sandboxchange` emits when sandbox environment switched, and returns sandbox info, including whether sandbox enabled. 229 | 230 | This method returns a boolean value to inform whether adding or removing listeners succeeds. 231 | 232 | ```ts 233 | function addEventListener(event: Event, listener: EventListener[Event]): boolean 234 | function removeEventListener(event: Event, listener: EventListener[Event]): boolean 235 | 236 | interface EventListener { 237 | resumed: () => void, 238 | paused: (pausedInfo: PausedInfo) => void, 239 | error: (errorInfo: ErrorInfo) => void, 240 | sandboxchange: (sandboxInfo: SandboxInfo) => void, 241 | } 242 | interface PausedInfo { breakpointId?: number, reason?: string, data?: any, debuggerId: string, lineNumber: number, columnNumber: number, scopeChain: Scope[], scriptContent: string } 243 | interface ErrorInfo { error: Error, scopeChain: Scope[] } 244 | interface SandboxInfo { enable: boolean } 245 | interface Scope { eval: (expression: string) => any, name: string, callFrameId: number, callFrame?: CallFrame } 246 | interface CallFrame { debuggerId: string, lineNumber: number, columnNumber: number } 247 | ``` 248 | 249 | -------------------------------------------------------------------------------- /docs/API_CN.md: -------------------------------------------------------------------------------- 1 | # 接口文档 2 | 3 | **[English](./API.md) | 简体中文** 4 | 5 | - [`debug`](#debug) 6 | - [`transform`](#transform) 7 | - [`resume`](#resume) 8 | - [`evaluate`](#evaluate) 9 | - [`getPossibleBreakpoints`](#getPossibleBreakpoints) 10 | - [`setBreakpoint`](#setbreakpoint) 11 | - [`removeBreakpoint`](#removebreakpoint) 12 | - [`setBreakpointsActive`](#setbreakpointsactive) 13 | - [`setExecutionPause`](#setexecutionpause) 14 | - [`setExceptionPause`](#setexceptionpause) 15 | - [`getPausedInfo`](#getpausedinfo) 16 | - [`getScopeChain`](#getscopechain) 17 | - [`getScriptContent`](#getscriptcontent) 18 | - [`runInNativeEnv`](#runinnativeenv) 19 | - [`runInSkipOver`](#runinskipover) 20 | - [`setModuleRequest`](#setmodulerequest) 21 | - [`addEventListener` / `removeEventListener`](#addeventlistener--removeeventlistener) 22 | 23 | ## `debug` 24 | 25 | 调试脚本。接受两个参数,分别是脚本字符串 `script` 和调试ID `debuggerId`。 26 | 27 | 其中,脚本字符串可以是源码,也可以是 `transform` 接口返回的转换结果。而调试ID通常为脚本的URL。调试ID可选,不传时会当作临时脚本,分配随机的调试ID。 28 | 29 | 该接口会返回执行函数 `run`,用于真正地执行脚本,因此可以在 `run` 调用之前使用 `setBreakpoint` 等接口编辑断点设置。当传入参数不合法或者当前环境不支持断点调试的时候,该接口会返回 `false`。 30 | 31 | ```ts 32 | function debug(script: string, debuggerId?: string): (() => void) | false 33 | ``` 34 | 35 | ## `transform` 36 | 37 | 转换需要调试的脚本。接受两个参数,分别是脚本字符串 `script` 和调试ID `debuggerId`。 38 | 39 | 其中,调试ID通常为脚本的URL。调试ID可选,不传时会当作临时脚本,分配随机的调试ID。 40 | 41 | 该接口会返回转换结果,用于运行时传入 `debug` 接口进行调试。当传入参数不合法的时候,该接口会返回 `false`。 42 | 43 | ```ts 44 | function transform(script: string, debuggerId?: string): string | false 45 | ``` 46 | 47 | ## `resume` 48 | 49 | 当命中断点暂停时,使用该接口恢复执行。接受一个可选的单步调试参数,可选值为 `stepInto`、`stepOver` 或 `stepOut`: 50 | 51 | 1. 不传时默认直接继续执行; 52 | 2. 传 `stepInto` 时遇到子函数就进入并且继续单步执行; 53 | 3. 传 `stepOver` 时遇到子函数时不会进入子函数内单步执行; 54 | 4. 传 `stepOut` 时执行完当前函数剩余部分,并返回到上一层函数。 55 | 56 | 该接口返回的布尔值用于标记是否恢复成功。 57 | 58 | ```ts 59 | function resume(type?: ResumeType): boolean 60 | 61 | type ResumeType = 'stepInto' | 'stepOver' | 'stepOut' 62 | ``` 63 | 64 | ## `evaluate` 65 | 66 | 在特定作用域环境中执行表达式。接受两个参数,分别是表达式字符串 `expression` 和调用栈ID `callFrameId`。 67 | 68 | 其中,调用栈ID可选,可通过 `getPausedInfo` 接口或 `paused` 事件的断点相关信息中作用域链 `scopeChain` 获取得到。如果传了调用栈ID,将会在对应调用栈的作用域中执行表达式;如果没传,默认在顶层全局作用域中执行表达式。 69 | 70 | ```ts 71 | function evaluate(expression: string, callFrameId?: number): Result | false 72 | ``` 73 | 74 | ## `getPossibleBreakpoints` 75 | 76 | 获取脚本所有可能的断点。接受一个调试ID的参数。 77 | 78 | 如果获取成功,该接口会返回所有可能的断点信息的数组,包括所有断点的断点ID `id` 、实际行号 `lineNumber` (从1开始) 和实际列号 `columnNumber` (从0开始);如果设置不成功,则返回 `false`。 79 | 80 | ```ts 81 | function getPossibleBreakpoints(debuggerId: string): Breakpoint[] | false 82 | 83 | interface Breakpoint { id: number, lineNumber: number, columnNumber: number } 84 | ``` 85 | 86 | ## `setBreakpoint` 87 | 88 | 根据调试ID设置断点,包含两个重载: 89 | 90 | 1. 接受三个参数,分别是调试ID `debuggerId`、行号 `lineNumber` (从1开始) 和可选条件 `condition`; 91 | 2. 接受四个参数,分别是调试ID `debuggerId`、行号 `lineNumber` (从1开始)、列号 `columnNumber` (从0开始) 和可选条件 `condition`。 92 | 93 | 另外,可选条件为一段脚本,当该脚本返回为 `true` 时会中断执行,如果没有条件,则默认到该脚本对应位置时都中断执行。 94 | 95 | 如果设置成功,该接口会返回断点信息,包括断点ID `id` 、实际行号 `lineNumber` (从1开始) 和实际列号 `columnNumber` (从0开始);如果设置不成功,则返回 `false`。 96 | 97 | ```ts 98 | function setBreakpoint(debuggerId: string, lineNumber: number, condition?: string): Breakpoint | false 99 | function setBreakpoint(debuggerId: string, lineNumber: number, columnNumber: number, condition?: string): Breakpoint | false 100 | 101 | interface Breakpoint { id: number, lineNumber: number, columnNumber: number } 102 | ``` 103 | 104 | ## `removeBreakpoint` 105 | 106 | 根据断点ID移除断点。接受一个断点ID的参数,返回的布尔值用于标记是否移除成功。 107 | 108 | ```ts 109 | function removeBreakpoint(id: number): boolean 110 | ``` 111 | 112 | ## `setBreakpointsActive` 113 | 114 | 设置是否启用断点。接受一个布尔值参数,返回的布尔值用于标记设置情况。 115 | 116 | ```ts 117 | function setBreakpointsActive(value: boolean): boolean 118 | ``` 119 | 120 | ## `setExecutionPause` 121 | 122 | 设置是否暂停执行。接受一个布尔值参数,返回的布尔值用于标记设置情况。设置为 `true` 以后,将在接下来要执行的语句前暂停。 123 | 124 | ```ts 125 | function setExecutionPause(value: boolean): boolean 126 | ``` 127 | 128 | ## `setExceptionPause` 129 | 130 | 设置在遇到异常时是否暂停执行。接受一个布尔值参数,返回的布尔值用于标记设置情况。设置为 `true` 以后,将在遇到异常时暂停。 131 | 132 | ```ts 133 | function setExceptionPause(value: boolean): boolean 134 | ``` 135 | 136 | ## `getPausedInfo` 137 | 138 | 获取断点信息。当目前处于暂停状态时,返回的当前断点相关信息,包括调试ID `debuggerId`、断点ID `breakpointId`、行号 `lineNumber` (从1开始)、列号 `columnNumber` (从0开始)、作用域链 `scopeChain` 以及调试源码 `scriptContent`。当目前没有处在暂停状态时,返回 `false`。 139 | 140 | 其中,作用域链 `scopeChain` 包括全局、函数和块级作用域,可通过是否有调用帧 `callFrame` 字段来判断是否是全局或函数作用域。过滤出作用域链中含有调用帧的作用域,即可获得调用栈。 141 | 142 | 另外,调用帧 `callFrame` 中的 `callFrameId` 可传给 `evaluate` 接口用于在特定作用域下执行表达式。 143 | 144 | ```ts 145 | function getPausedInfo(): PausedInfo | false 146 | 147 | interface PausedInfo { breakpointId?: number, reason?: string, data?: any, debuggerId: string, lineNumber: number, columnNumber: number, scopeChain: Scope[], scriptContent: string } 148 | interface Scope { eval: (expression: string) => any, name: string, callFrameId: number, callFrame?: CallFrame } 149 | interface CallFrame { debuggerId: string, lineNumber: number, columnNumber: number } 150 | ``` 151 | 152 | ## `getScopeChain` 153 | 154 | 获取当前作用域链。 155 | 156 | ```ts 157 | function getScopeChain(): Scope[] 158 | 159 | interface Scope { eval: (expression: string) => any, name: string, callFrameId: number, callFrame?: CallFrame } 160 | interface CallFrame { debuggerId: string, lineNumber: number, columnNumber: number } 161 | ``` 162 | 163 | ## `getScriptContent` 164 | 165 | 根据调试ID `debuggerId` 获取调试源码。 166 | 167 | ```ts 168 | function getScriptContent(debuggerId: string): string 169 | ``` 170 | 171 | ## `runInNativeEnv` 172 | 173 | 在原生环境中执行。接受一个回调函数作为参数。 174 | 175 | 调试器默认将脚本在内部的沙盒中运行,该接口用于在调试过程中,执行需要在原生环境下运行的代码。 176 | 177 | 如果执行成功,该接口返回值为回调函数的返回值;如果执行失败,返回 `false`。 178 | 179 | ```ts 180 | function runInNativeEnv(callback: () => Return): Return | false 181 | ``` 182 | 183 | ## `runInSkipOver` 184 | 185 | 跳过断点执行。接受一个回调函数作为参数。 186 | 187 | 当命中断点暂停时,如果执行调试脚本,会被阻塞直至断点恢复,该接口用于在暂停情况下,不阻塞直接执行调试脚本,常用于通过 `evaluate` 接口调用调试脚本时,不阻塞执行以获取相应作用域中的值。 188 | 189 | 如果执行成功,该接口返回值为回调函数的返回值;如果执行失败,返回 `false`。 190 | 191 | ```ts 192 | function runInSkipOver(callback: () => Return): Return | false 193 | ``` 194 | 195 | ## `setModuleRequest` 196 | 197 | 设置模块请求函数。接受一个请求函数作为参数,用于覆盖内部的默认请求函数。 198 | 199 | 当需要import模块时,默认会使用fetch进行请求。如果在不支持fetch的环境使用,或者想自定义模块请求的行为(比如对请求结果进行缓存),可通过该接口进行设置。 200 | 201 | 其中,请求函数 `request` 可接受一个模块地址 `importUrl` 作为入参,并要求返回一个 `Promise` 包裹的模块脚本文本内容。而模块脚本文本内容可以是转换前的脚本源码字符串,也可以是通过 `transform` 接口转换后的脚本字符串。 202 | 203 | 该接口返回的布尔值告知是否设置成功。 204 | 205 | ```ts 206 | function setModuleRequest(request: (importUrl: string) => Promise): boolean 207 | ``` 208 | 209 | ## `addEventListener` / `removeEventListener` 210 | 211 | 添加或移除事件监听器,接受两个参数,分别是事件 `event` 和监听函数 `listener`。 212 | 213 | 目前有四个事件,为暂停 `paused`、恢复 `resumed`、错误 `error` 和沙盒状态变化 `sandboxchange`: 214 | 215 | 1. 暂停事件 `paused` 会返回断点相关信息,该信息与 `getPausedInfo` 的返回值一致; 216 | 2. 恢复事件 `resumed` 没有返回值,用于通知执行恢复; 217 | 3. 错误事件 `error` 会返回错误相关信息,包括错误原因和作用域链; 218 | 4. 沙盒状态变化事件 `sandboxchange` 会返回沙盒相关信息,包括是否启用了沙盒。 219 | 220 | 该接口返回的布尔值用于标记是否添加或移除事件监听器成功。 221 | 222 | ```ts 223 | function addEventListener(event: Event, listener: EventListener[Event]): boolean 224 | function removeEventListener(event: Event, listener: EventListener[Event]): boolean 225 | 226 | interface EventListener { 227 | resumed: () => void, 228 | paused: (pausedInfo: PausedInfo) => void, 229 | error: (errorInfo: ErrorInfo) => void, 230 | sandboxchange: (sandboxInfo: SandboxInfo) => void, 231 | } 232 | interface PausedInfo { breakpointId?: number, reason?: string, data?: any, debuggerId: string, lineNumber: number, columnNumber: number, scopeChain: Scope[], scriptContent: string } 233 | interface ErrorInfo { error: Error, scopeChain: Scope[] } 234 | interface SandboxInfo { enable: boolean } 235 | interface Scope { eval: (expression: string) => any, name: string, callFrameId: number, callFrame?: CallFrame } 236 | interface CallFrame { debuggerId: string, lineNumber: number, columnNumber: number } 237 | ``` 238 | -------------------------------------------------------------------------------- /docs/OPTIMIZATION.md: -------------------------------------------------------------------------------- 1 | # 优化记录 2 | 3 | 测试项目及环境: 4 | - For循环10000次 (8C16G / Node 16) 5 | - React初始化 (8C16G / Node 16) 6 | - React Demo页渲染 (Macbook Pro 2021 / Chrome 121) 7 | - React Demo页点击事件处理 (Macbook Pro 2021 / Chrome 121) 8 | 9 | 原生: 10 | - 0.2ms 11 | - 1.8ms 12 | - 1.3ms 13 | - 1ms 14 | 15 | 原始效果: 16 | - 23.4ms 17 | - 11.1ms 18 | - 10.6ms 19 | - 6.2ms 20 | 21 | ## 第一次优化 22 | 23 | 优化项: 24 | - 原来每一行都yield出来再判断是否断点,改成先判断断点再yield出来,减少generator保存状态的性能消耗 25 | 26 | 优化效果: 27 | - 17.2ms (-26.5%) 28 | - 10.2ms (-8.1%) 29 | - 6ms (-43.4%) 30 | - 5ms (-19.4%) 31 | 32 | ## 第二次优化 33 | 34 | 优化项: 35 | - 每次checkIfBreak都需要调用Scope.updateCallFrame更新调用帧信息,而每次updateCallFrame被调用,都需要寻找最近的函数作用域,改成缓存最近的函数作用域,用空间换时间 36 | 37 | 优化效果: 38 | - 14.6ms (-37.6%) 39 | - 10.1ms (-9%) 40 | - 6ms (-43.4%) 41 | - 2.8ms (-54.8%) 42 | 43 | ## 第三次优化 44 | 45 | 优化项: 46 | - 原来一些临时变量声明在每个块级作用域中,改成全局声明,优化有限 47 | - 获取断点进行判断的时候,从直接获取改成先判断是否有断点再获取 48 | 49 | 优化效果: 50 | - 14.5ms (-38%) 51 | - 10ms (-9.9%) 52 | - 5.9ms (-44.3%) 53 | - 2.8ms (-54.8%) 54 | -------------------------------------------------------------------------------- /docs/REWRITE.md: -------------------------------------------------------------------------------- 1 | # 原生方法重写原理 2 | 3 | 折叠是haskell里很原子的函数,可以派生出很多数组操作的函数,其中右折叠(相当于js的reduce)的定义是: 4 | 5 | ```hs 6 | foldr :: (a -> b -> b) -> b -> [a] -> b 7 | foldr f z [] = z 8 | foldr f z (x:xs) = f x (foldr f z xs) 9 | ``` 10 | 11 | 所有数组操作,都可以基于foldr派生,比如map: 12 | 13 | ```hs 14 | map :: a -> b -> [a] -> [b] 15 | map f = foldr ((:) . f) [] 16 | ``` 17 | 18 | 由于map的操作函数f可能抛出中断信号,普通map无法阻塞后续f运行。定义类型类P代表断点信号,此时map: 19 | 20 | ```hs 21 | map :: a -> (P b) -> [a] -> [P b] 22 | ``` 23 | 24 | 可以看到,他仅仅会阻塞某个元素操作的执行。但这不符合我们预期,假设gmap是重写后的map函数,我们希望的行为应该是: 25 | 26 | ```hs 27 | gmap :: a -> (P b) -> [a] -> (P [b]) 28 | ``` 29 | 30 | 因此,需要定义两个函数来进行复合,分别用于等待恢复和触发中断: 31 | 32 | ```hs 33 | y :: P a -> a 34 | y (P a) = a 35 | g :: a -> P a 36 | g a = P a 37 | ``` 38 | 39 | 所以,实际上gmap的定义是: 40 | 41 | ```hs 42 | gmap :: a -> (P b) -> [a] -> (P [b]) 43 | gmap f = foldr (g . (:) . f . y) (P []) 44 | ``` 45 | 46 | 简单对比一下map和gmap的定义不难发现,如果我们能以foldr来声明对应的函数,那么只需要在foldr的操作函数f执行前,先用y展开输入,在返回前重新用g包裹起来。通过这样的复合,就得到了能理解中断信号并阻塞执行的协程函数: 47 | 48 | ```hs 49 | map f = foldr ((:) . f) [] 50 | gmap f = foldr (g . (:) . f . y) (P []) 51 | ``` 52 | 53 | 对于js中的forEach、filter、reduce、replace等方法,也是类似: 54 | 55 | ```hs 56 | each f = foldr f [] 57 | geach f = foldr (g . f . y) (P []) 58 | 59 | filter f = foldr (\x xs -> if (f x) then x:xs else xs) [] 60 | gfilter f = foldr (\x xs -> if ((f . y) x) then (g x:xs) else (g xs)) (P []) 61 | 62 | reduce = foldr 63 | greduce f x = folder (g . f . y) (P x) 64 | 65 | replace f r = foldr (\x xs -> if (f x) then (r x):xs else x:xs) [] 66 | greplace f r = foldr (\x xs -> if ((f . y) x) then (g (r x):xs) else (g x:xs)) (P []) 67 | ``` 68 | 69 | 那么,根据上述推导,以js的map为例,可以进行以下重写: 70 | 71 | ```js 72 | // 基于foldr也就是reduce实现的原生等效map 73 | Array.prototype.map = function map(mapper) { 74 | return Array.prototype.reduce.call(this, (prev, ...args) => { 75 | const res = mapper(...args); // 执行 f 函数 76 | prev.push(res); // 执行 (:) 拼接 77 | return prev; 78 | }, []); // 初始值采用 [] 79 | }; 80 | // 重写成协程后的gmap 81 | Array.prototype.map = function map(mapper) { 82 | return executor(Array.prototype.reduce.call(this, (prevGenerator, ...args) => { 83 | return (function* () { 84 | const prev = yield* prevGenerator; // 先用 y 展开前者中断信号,等待恢复 85 | const res = yield mapper(...args); // 执行 f 函数,注意加上 yield 保证中断信号拋出 86 | prev.push(res); // 执行 (:) 拼接 87 | return prev; 88 | })(); // 最后用 g 重新包裹,允许触发中断 89 | }, (function* () { return [] })())); // 初始值采用 P[] 90 | }; 91 | ``` 92 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Dev Page 8 | 9 | 10 |
11 | 12 | 43 | 121 | 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vdebugger", 3 | "version": "0.1.19", 4 | "description": "A Front-End JavaScript Debugger", 5 | "main": "dist/vdebugger.js", 6 | "types": "dist/vdebugger.d.ts", 7 | "scripts": { 8 | "start": "rollup -wc --environment NODE_ENV:development & serve -l 8096", 9 | "build": "rollup -c --environment NODE_ENV:production && cp src/index.d.ts dist/vdebugger.d.ts", 10 | "pub": "npm run test && npm run build && npm publish --registry https://registry.npmjs.org", 11 | "cov": "cat ./coverage/lcov.info | coveralls", 12 | "benchmark": "node tests/benchmark.js", 13 | "test": "jest --colors ---coverage" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/wechatjs/vdebugger" 18 | }, 19 | "keywords": [ 20 | "vdebugger", 21 | "debugger", 22 | "debug", 23 | "js", 24 | "javascript", 25 | "break", 26 | "breakpoint" 27 | ], 28 | "author": "siubaak", 29 | "license": "MIT", 30 | "dependencies": { 31 | "acorn-walk": "^8.3.2", 32 | "astring": "^1.8.6", 33 | "meriyah": "^4.3.9" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.18.5", 37 | "@babel/plugin-transform-runtime": "^7.18.5", 38 | "@babel/preset-env": "^7.18.2", 39 | "@rollup/plugin-babel": "^5.3.1", 40 | "@rollup/plugin-commonjs": "^22.0.0", 41 | "@rollup/plugin-json": "^4.1.0", 42 | "@rollup/plugin-node-resolve": "^13.3.0", 43 | "@rollup/plugin-replace": "^4.0.0", 44 | "axios": "^0.27.2", 45 | "jest": "^28.1.1", 46 | "jest-environment-jsdom": "^28.1.1", 47 | "rollup": "^2.75.7", 48 | "serve": "^14.0.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import { babel } from '@rollup/plugin-babel'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import replace from '@rollup/plugin-replace'; 5 | import json from '@rollup/plugin-json'; 6 | import path from 'path'; 7 | 8 | export default { 9 | input: 'src/index.js', 10 | output: { 11 | file: 'dist/vdebugger.js', 12 | name: 'vDebugger', 13 | exports: 'named', 14 | format: 'umd', 15 | }, 16 | plugins: [ 17 | nodeResolve(), 18 | commonjs(), 19 | replace({ 20 | preventAssignment: true, 21 | values: { 22 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 23 | }, 24 | }), 25 | babel({ 26 | babelHelpers: 'runtime', 27 | configFile: path.resolve(__dirname, './.babelrc'), 28 | }), 29 | json(), 30 | ], 31 | } -------------------------------------------------------------------------------- /src/consts.js: -------------------------------------------------------------------------------- 1 | export const EXECUTOR_BREAK_NAME = '$$$_br_'; 2 | export const EXECUTOR_FUNC_NAME = '$$$_ec_'; 3 | export const IMPORT_REQ_NAME = '$$$_iq_'; 4 | export const IMPORT_FUNC_NAME = '$$$_ip_'; 5 | export const IMPORT_META_NAME = '$$$_im_'; 6 | export const NEW_TARGET_NAME = '$$$_nt_'; 7 | export const EXPORT_OBJECT_NAME = '$$$_ep_'; 8 | export const TMP_VARIABLE_NAME = '$$$_tv_'; 9 | export const DEBUGGER_ID_NAME = '$$$_di_'; 10 | export const SCOPE_TRACER_NAME = '$$$_st_'; 11 | export const CLASS_CONSTRUCTOR_NAME = '$$$_cr_'; 12 | export const CLASS_CREATE_NAME = '$$$_nw_'; 13 | export const VALUE_SETTER_NAME = '$$$_se_'; 14 | export const SANDBOX_PREFIX = '$$$_sb_'; 15 | export const PROXY_MARK = '$$$_px_'; 16 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | type ResumeType = 'stepInto' | 'stepOver' | 'stepOut' 2 | interface Breakpoint { id: number, lineNumber: number, columnNumber: number } 3 | interface CallFrame { debuggerId: string, lineNumber: number, columnNumber: number } 4 | interface Scope { eval: (expression: string) => any, name: string, callFrameId: number, callFrame?: CallFrame } 5 | interface ErrorInfo { error: Error, scopeChain: Scope[] } 6 | interface SandboxInfo { enable: boolean } 7 | interface PausedInfo { 8 | breakpointId?: number, 9 | reason?: string, 10 | data?: any, 11 | debuggerId: string, 12 | lineNumber: number, 13 | columnNumber: number, 14 | scopeChain: Scope[], 15 | scriptContent: string, 16 | } 17 | interface EventListener { 18 | resumed: () => void, 19 | paused: (pausedInfo: PausedInfo) => void, 20 | error: (errorInfo: ErrorInfo) => void, 21 | sandboxchange: (sandboxInfo: SandboxInfo) => void, 22 | } 23 | 24 | export declare const version: string 25 | export declare function debug(script: string, debuggerId?: string): (() => void) | false 26 | export declare function transform(script: string, debuggerId?: string): string | false 27 | export declare function resume(type?: ResumeType): boolean 28 | export declare function evaluate(expression: string, callFrameId?: number): Result | false 29 | export declare function getPossibleBreakpoints(debuggerId: string): Breakpoint[] | false 30 | export declare function setBreakpoint(debuggerId: string, lineNumber: number, condition?: string): Breakpoint | false 31 | export declare function setBreakpoint(debuggerId: string, lineNumber: number, columnNumber: number, condition?: string): Breakpoint | false 32 | export declare function removeBreakpoint(id: number): boolean 33 | export declare function setBreakpointsActive(value: boolean): boolean 34 | export declare function setExecutionPause(value: boolean): boolean 35 | export declare function setExceptionPause(value: boolean): boolean 36 | export declare function getPausedInfo(): PausedInfo | false 37 | export declare function getScopeChain(): Scope[] 38 | export declare function getScriptContent(debuggerId: string): string 39 | export declare function runInNativeEnv(callback: () => Return): Return | false 40 | export declare function runInSkipOver(callback: () => Return): Return | false 41 | export declare function setModuleRequest(request: (importUrl: string) => Promise): boolean 42 | export declare function addEventListener(event: Event, listener: EventListener[Event]): boolean 43 | export declare function removeEventListener(event: Event, listener: EventListener[Event]): boolean 44 | 45 | declare const vDebugger: { 46 | version: typeof version, 47 | debug: typeof debug, 48 | transform: typeof transform, 49 | resume: typeof resume, 50 | evaluate: typeof evaluate, 51 | getPossibleBreakpoints: typeof getPossibleBreakpoints, 52 | setBreakpoint: typeof setBreakpoint, 53 | removeBreakpoint: typeof removeBreakpoint, 54 | setBreakpointsActive: typeof setBreakpointsActive, 55 | setExecutionPause: typeof setExecutionPause, 56 | setExceptionPause: typeof setExceptionPause, 57 | getPausedInfo: typeof getPausedInfo, 58 | getScopeChain: typeof getScopeChain, 59 | getScriptContent: typeof getScriptContent, 60 | runInNativeEnv: typeof runInNativeEnv, 61 | runInSkipOver: typeof runInSkipOver, 62 | setModuleRequest: typeof setModuleRequest, 63 | addEventListener: typeof addEventListener, 64 | removeEventListener: typeof removeEventListener, 65 | } 66 | 67 | export default vDebugger 68 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { addEventListener, removeEventListener, emitEventListener, getPropertyDescriptor, emptyYield, getImportUrl } from './utils'; 2 | import { EXECUTOR_BREAK_NAME, CLASS_CONSTRUCTOR_NAME, PROXY_MARK } from './consts'; 3 | import { wrapProtoMethod, switchObjectMethod, switchGlobalObject } from './sandbox'; 4 | import { version } from '../package.json'; 5 | import Transformer from './transformer'; 6 | import Scope from './scope'; 7 | 8 | const TRANSFORMED_MARK = '/** VDEBUGGER_TRANSFORMED */'; 9 | const nativeEval = typeof eval !== 'undefined' ? eval : false; 10 | const nativeFetch = typeof fetch !== 'undefined' ? fetch : false; 11 | const nativePromise = Promise; 12 | 13 | const transformerMap = new Map(); 14 | const moduleMap = new Map(); 15 | const macroTaskList = []; 16 | let pause = 0; // 0不暂停; 1下次执行暂停; 2异常时暂停 17 | let skip = false; 18 | let active = true; 19 | let sandbox = false; 20 | let supported = true; 21 | let pausedInfo = null; 22 | let resumeOptions = null; 23 | let resumeExecutor = null; 24 | let currentBreaker = null; 25 | let moduleRequest = (importUrl) => nativeFetch(importUrl).then((res) => res.text()); 26 | 27 | if (!nativeEval) { 28 | supported = false; 29 | } 30 | try { nativeEval('function*t(){}') } catch (err) { 31 | supported = false; 32 | } 33 | 34 | /** 35 | * 中断标识,用于执行器判断是否需要中断 36 | * @param {Array} args 检查参数 37 | */ 38 | function breaker(...args) { 39 | return { [EXECUTOR_BREAK_NAME]: args }; 40 | } 41 | 42 | /** 43 | * 判断当前值是否是中断标识 44 | * @param {Any} value 当前值 45 | */ 46 | function isBreaker(value) { 47 | if (value && typeof value === 'object') { 48 | try { 49 | // 之所以要包一层try catch,是因为value有可能是iframe.contentWindow,直接获取的时候会导致跨域报错 50 | return value[EXECUTOR_BREAK_NAME]?.length; 51 | } catch (err) { 52 | return false; 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * 用于内部检查是否需要中断执行 59 | * @param {String} debuggerId 调试id,通常为脚本的url 60 | * @param {Number} breakpointId 断点id 61 | * @param {Number} lineNumber 断点行号 62 | * @param {Number} columnNumber 断点列号 63 | * @param {Boolean} scopeBlocker 是否为作用域阻塞执行的断点 64 | */ 65 | function checkIfBreak(debuggerId, breakpointId, lineNumber, columnNumber, scopeBlocker) { 66 | if (!skip) { 67 | if (scopeBlocker) { 68 | // 如果是作用域阻塞执行的断点,且目前命中了断点,暂停执行 69 | if (resumeExecutor) { 70 | return breaker(debuggerId, breakpointId, lineNumber, columnNumber, scopeBlocker); 71 | } 72 | // 否则就继续执行 73 | return; 74 | } else { 75 | // 否则记录一下调用帧信息 76 | Scope.updateCallFrame({ debuggerId, lineNumber, columnNumber }); 77 | } 78 | if (pause === 1) { 79 | // 如果被手动暂停了,那就直接当成命中断点处理 80 | pause = 0; 81 | } else { 82 | if (!active) { 83 | // 如果当前禁用了断点,那么继续执行 84 | return; 85 | } 86 | const condition = Transformer.breakpointMap.size && Transformer.breakpointMap.get(breakpointId); 87 | if (!condition && ['stepInto', 'stepOver', 'stepOut'].indexOf(resumeOptions?.type) === -1) { 88 | // 如果没有命中断点,并且不是单步调试,那么继续执行 89 | return; 90 | } 91 | const callFrameId = Scope.getCurrentCallFrameId(); 92 | if (resumeOptions?.type === 'stepOver' && resumeOptions?.callFrameId < callFrameId) { 93 | // 如果是跳过当前函数,但进入到子函数了,那么继续执行 94 | return; 95 | } 96 | if (resumeOptions?.type === 'stepOut' && resumeOptions?.callFrameId <= callFrameId) { 97 | // 如果是跳出当前函数,但仍在函数内,那么继续执行 98 | return; 99 | } 100 | if (typeof condition === 'string') { 101 | // 对于条件断点和日志断点,执行一下表达式,再判断是否需要中断 102 | const conditionRes = evaluate(condition, callFrameId); 103 | if (!conditionRes || condition.startsWith('/** DEVTOOLS_LOGPOINT */')) { 104 | return; 105 | } 106 | } 107 | } 108 | // 否则,就是命中断点,中断执行 109 | return breaker(debuggerId, breakpointId, lineNumber, columnNumber); 110 | } 111 | } 112 | 113 | /** 114 | * 启用沙盒环境 115 | */ 116 | function enableSandbox(enable) { 117 | if (enable) { 118 | wrapProtoMethod(executor); 119 | } 120 | if (sandbox !== !!enable) { 121 | sandbox = !!enable; 122 | switchObjectMethod(Array, ['from', 'fromAsync', 'of', 'isArray']); 123 | switchObjectMethod(String, ['fromCharCode', 'fromCharPoint', 'raw']); 124 | switchObjectMethod(Object, [ 125 | 'assign', 'create', 'defineProperties', 'defineProperty', 'entries', 'freeze', 'fromEntries', 126 | 'getOwnPropertyDescriptor', 'getOwnPropertyDescriptors', 'getOwnPropertyNames', 'getOwnPropertySymbols', 127 | 'getPrototypeOf', 'groupBy', 'hasOwn', 'is', 'isExtensible', 'isFrozen', 'isSealed', 'keys', 'preventExtensions', 128 | 'seal', 'setPrototypeOf', 'values' 129 | ]); 130 | switchObjectMethod(RegExp.prototype, [Symbol.replace]); 131 | switchObjectMethod(Object.prototype, ['hasOwnProperty', 'propertyIsEnumerable', 'valueOf']); 132 | switchObjectMethod(Array.prototype, [ 133 | 'at', 'concat', 'copyWithin', 'entries', 'every', 'fill', 'filter', 'find', 'findIndex', 134 | 'findLast', 'findLastIndex', 'flat', 'flatMap', 'forEach', 'includes', 'indexOf', 'join', 135 | 'keys', 'lastIndexOf', 'map', 'pop', 'push', 'reduce', 'reduceRight', 'reverse', 'shift', 136 | 'slice', 'some', 'sort', 'splice', 'toLocaleString', 'toString', 'unshift', 'values' 137 | ]); 138 | switchObjectMethod(String.prototype, [ 139 | 'anchor', 'at', 'big', 'blink', 'bold', 'charAt', 'charCodeAt', 'codePointAt', 'concat', 'endsWith', 140 | 'fixed', 'fontcolor', 'fontsize', 'includes', 'indexOf', 'italics', 'lastIndexOf', 'link', 'localeCompare', 141 | 'match', 'matchAll', 'normalize', 'padEnd', 'padStart', 'repeat', 'replace', 'replaceAll', 'search', 142 | 'slice', 'small', 'split', 'startsWith', 'strike', 'sub', 'substr', 'substring', 'sup', 'toLocaleLowerCase', 143 | 'toLocaleUpperCase', 'toLowerCase', 'toString', 'toUpperCase', 'trim', 'trimEnd', 'trimLeft', 'trimRight', 144 | 'trimStart', 'valueOf' 145 | ]); 146 | switchGlobalObject(); 147 | emitEventListener('sandboxchange', { enable: sandbox }); 148 | } 149 | } 150 | 151 | /** 152 | * 执行宏任务队列中下一个任务 153 | */ 154 | function executeNextMacroTask() { 155 | enableSandbox(false); 156 | if (macroTaskList.length) { 157 | // 当前脚本执行完毕,如果宏任务队列中有需要恢复执行的任务,取出并执行 158 | const nextMacroTask = macroTaskList.shift(); 159 | nextMacroTask(); 160 | } 161 | } 162 | 163 | /** 164 | * 执行器,控制协程执行 165 | * @param {Generator} generator 需要执行generator 166 | * @param {Object} result yield返回值,用于返回恢复执行的值 167 | */ 168 | function executor(generator, result) { 169 | enableSandbox(true); 170 | const breakerResolved = (value) => executor(generator, { value, done: false }); 171 | const breakerCatched = (err) => { scope(false); throw err }; 172 | let ret = result || emptyYield(); 173 | while (!ret.done) { 174 | if (!skip) { 175 | if (isBreaker(ret.value) && typeof ret.value[EXECUTOR_BREAK_NAME][0].then === 'function') { 176 | // 如果是promise,中断执行,等resolve后继续执行 177 | const nextBreaker = breaker(ret.value[EXECUTOR_BREAK_NAME][0].then(breakerResolved).catch(breakerCatched)); 178 | // 如果这个breaker是断点,记录全局当前断点记录 179 | if (ret.value[EXECUTOR_BREAK_NAME][1]) { 180 | nextBreaker[EXECUTOR_BREAK_NAME].push(1); 181 | currentBreaker = nextBreaker; 182 | } 183 | return nextBreaker; 184 | } 185 | if (isBreaker(currentBreaker) && typeof currentBreaker[EXECUTOR_BREAK_NAME][0].then === 'function') { 186 | // 如果是有全局当前断点记录,中断执行,等resolve后继续执行 187 | return currentBreaker = breaker(currentBreaker[EXECUTOR_BREAK_NAME][0].then(breakerResolved).catch(breakerCatched), 1); 188 | } 189 | if (resumeExecutor) { 190 | // 如果目前命中了断点,暂停执行,并将恢复执行任务放入宏任务队列,等待断点恢复后再执行 191 | return breaker(new nativePromise((resolve) => { 192 | macroTaskList.push(() => { 193 | resolve(executor(generator, ret)); 194 | }); 195 | })); 196 | } 197 | } 198 | try { 199 | ret = generator.next(ret.value); 200 | } catch (err) { 201 | enableSandbox(false); 202 | if (err instanceof Error && !err.exceptionOriginMark) { 203 | const scopeChain = getScopeChain(); 204 | if (Scope.lastPop) { 205 | // 因为此时错误所在作用域已经被pop了,所以取lastPop补充下信息 206 | scopeChain.push(Scope.lastPop); 207 | } 208 | const callStack = scopeChain.filter((scope) => !!scope.callFrame).reverse(); 209 | if (err.name && err.message) { 210 | err.stack = [ 211 | // 具体错误 212 | `${err.name}: ${err.message}`, 213 | // 调用栈信息 214 | ...callStack.map((scope) => { 215 | const callFrame = scope.callFrame; 216 | return ` at ${scope.name} (${callFrame.debuggerId}:${callFrame.lineNumber}:${callFrame.columnNumber})`; 217 | }) 218 | ].join('\n'); 219 | } 220 | emitEventListener('error', { error: err, scopeChain }); 221 | err.exceptionOriginMark = true; 222 | // 如果设置了异常时暂停,中断执行,不抛出错误,等恢复时才抛出 223 | if (pause === 2) { 224 | const { debuggerId, lineNumber, columnNumber } = callStack[0].callFrame; 225 | emitEventListener('paused', pausedInfo = { 226 | reason: 'exception', 227 | data: err, 228 | debuggerId, 229 | lineNumber, 230 | columnNumber, 231 | scopeChain, 232 | scriptContent: getScriptContent(debuggerId) 233 | }); 234 | // 记录全局当前断点 235 | return currentBreaker = breaker(new nativePromise((_, reject) => { 236 | resumeExecutor = () => reject(err); 237 | }), 1); 238 | } 239 | } 240 | throw err; 241 | } 242 | const pauseIfBreak = (breakRet) => { 243 | if (!skip && isBreaker(breakRet.value) && typeof breakRet.value[EXECUTOR_BREAK_NAME][0].then !== 'function') { 244 | const [ 245 | debuggerId, // 调试id,通常为脚本的url 246 | breakpointId, // 断点id 247 | lineNumber, // 断点行号 248 | columnNumber, // 断点列号 249 | ] = breakRet.value[EXECUTOR_BREAK_NAME]; 250 | // 否则,就是命中断点,中断执行 251 | enableSandbox(false); 252 | emitEventListener('paused', pausedInfo = { 253 | breakpointId, 254 | debuggerId, 255 | lineNumber, 256 | columnNumber, 257 | scopeChain: getScopeChain(), 258 | scriptContent: getScriptContent(debuggerId) 259 | }); 260 | // 记录全局当前断点 261 | return currentBreaker = breaker(new nativePromise((resolve, reject) => { 262 | resumeExecutor = () => { 263 | try { 264 | resolve(executor(generator, breakRet)); 265 | } catch (err) { 266 | reject(err); 267 | } 268 | }; 269 | }), 1); 270 | } 271 | }; 272 | if (typeof ret.then === 'function') { 273 | // 如果是async function,等resolve后再继续 274 | return ret.then((resolvedRet) => { 275 | const res = pauseIfBreak(resolvedRet); 276 | return res?.[EXECUTOR_BREAK_NAME][0] || executor(generator, resolvedRet); 277 | }); 278 | } 279 | const res = pauseIfBreak(ret); 280 | if (res) { 281 | return res; 282 | } 283 | } 284 | return ret.value; 285 | } 286 | 287 | /** 288 | * 跟踪作用域链 289 | * @param {Boolean} push 是否入栈 290 | * @param {Function} scopeEval 当前作用域eval函数 291 | * @param {Number} scopeName 当前函数作用域名称 292 | */ 293 | function scope(push, scopeEval, scopeName) { 294 | if (push) { 295 | const scope = new Scope(scopeEval, scopeName); 296 | Scope.chain.push(scope); 297 | if (scopeName) { 298 | Scope.curNamedScope = scope; 299 | } 300 | } else { 301 | Scope.lastPop = Scope.chain.pop(); 302 | if (Scope.lastPop.name) { 303 | Scope.curNamedScope = Scope.getSpecifiedScope((scope) => !!scope.name); 304 | } 305 | if (Scope.chain.length < 2) { // 只剩下全局作用域(length:1)或没有在执行(length:0) 306 | // 如果有定义断点恢复设置,callFrameId设为极大,当前过程跑完后,保证能在下个循环中断住 307 | resumeOptions && (resumeOptions.callFrameId = Number.MAX_SAFE_INTEGER); 308 | // 执行下一个宏任务 309 | executeNextMacroTask(); 310 | } 311 | } 312 | } 313 | 314 | /** 315 | * 创建对象 316 | * @param {Function} constructor 构造函数 317 | * @param {Array} args 初始化参数 318 | */ 319 | function* newObject(constructor, args, target) { 320 | if (constructor === Proxy) { 321 | const [ctx, handlers] = args; 322 | return new Proxy(ctx, Object.assign(Object.create(handlers.__proto__), handlers, { 323 | get(...getArgs) { 324 | if (PROXY_MARK === getArgs[1]) { 325 | return true; 326 | } 327 | if (typeof handlers.get === 'function' && EXECUTOR_BREAK_NAME !== getArgs[1]) { 328 | return handlers.get.apply(this, getArgs); 329 | } 330 | return Reflect.get.apply(this, getArgs); 331 | } 332 | })); 333 | } 334 | const obj = Reflect.construct(constructor, args, arguments.length > 2 ? target : constructor); 335 | if (obj[CLASS_CONSTRUCTOR_NAME]) { 336 | try { 337 | const ret = yield* obj[CLASS_CONSTRUCTOR_NAME](...args); 338 | delete obj[CLASS_CONSTRUCTOR_NAME]; 339 | if (ret && (typeof ret === 'object' || typeof ret === 'function')) { 340 | return ret; 341 | } 342 | } finally { 343 | scope(false); 344 | } 345 | } 346 | return obj; 347 | } 348 | 349 | /** 350 | * 赋值,如果有setter,获取到原set来调用,防止协程插队 351 | * @param {Object} obj 赋值对象 352 | * @param {String} key 赋值属性 353 | * @param {Any} value 值 354 | */ 355 | function setter(obj, key, value) { 356 | const dptor = getPropertyDescriptor(obj, key); 357 | if (!obj[PROXY_MARK] && dptor?.set) { 358 | try { 359 | dptor.set.call(obj, value); 360 | return value; 361 | } catch (err) { /* empty */ } 362 | } 363 | return obj[key] = value; 364 | } 365 | 366 | /** 367 | * 处理模块exports 368 | * @param {String} debuggerId 调试id,通常为脚本的url 369 | * @param {Object} exports 模块exports 370 | */ 371 | function resolveModuleExports(debuggerId, exports) { 372 | const importResolve = moduleMap.get(debuggerId); 373 | moduleMap.set(debuggerId, exports); 374 | if (typeof importResolve === 'function') { 375 | importResolve(); 376 | } 377 | } 378 | 379 | /** 380 | * 请求模块 381 | * @param {Array} paths 需要请求的模块路径列表 382 | * @param {String} debuggerId 调试id,通常为脚本的url 383 | */ 384 | function requestModules(paths, debuggerId) { 385 | const resolveList = []; 386 | for (let i = 0; i < paths.length; i++) { 387 | const importUrl = getImportUrl(paths[i], debuggerId); 388 | const cacheScript = moduleMap.get(importUrl); 389 | if (!cacheScript) { 390 | resolveList.push(moduleRequest(importUrl).then((script) => moduleMap.set(importUrl, script))); 391 | } 392 | } 393 | if (resolveList.length) { 394 | enableSandbox(false); 395 | return breaker(nativePromise.all(resolveList)); 396 | } 397 | } 398 | 399 | /** 400 | * 引入模块 401 | * @param {String} path 需要引入的模块路径 402 | * @param {String} debuggerId 调试id,通常为脚本的url 403 | * @param {Boolean} dynamic 是否为动态import 404 | */ 405 | function importModule(path, debuggerId, dynamic) { 406 | const importUrl = getImportUrl(path, debuggerId); 407 | const cacheModule = moduleMap.get(importUrl); 408 | if (typeof cacheModule === 'object') { 409 | return cacheModule; 410 | } else if (typeof cacheModule === 'string') { 411 | nativePromise.resolve() 412 | .then(() => debug(cacheModule, importUrl)()); 413 | } else { 414 | requestModules([path], debuggerId)[EXECUTOR_BREAK_NAME][0] 415 | .then(() => debug(moduleMap.get(importUrl), importUrl)()); 416 | } 417 | // TODO: 动态import 418 | return breaker(new nativePromise((resolve) => { 419 | moduleMap.set(importUrl, () => resolve(moduleMap.get(importUrl))); 420 | })); 421 | } 422 | 423 | /** 424 | * 获取调试脚本 425 | * @param {String} script 源码 426 | * @param {Transformer} transformer 转换器 427 | */ 428 | function getDebugProgram(script, transformer) { 429 | if (script.startsWith(TRANSFORMED_MARK)) { 430 | // 如果是预转换的脚本,则覆盖transformer设置并返回预转换结果 431 | const [ 432 | debuggerId, debugProgram, lBpIdMInit, staticBpMInit, 433 | ] = JSON.parse(script.slice(TRANSFORMED_MARK.length)); 434 | 435 | transformer.debuggerId = debuggerId || transformer.debuggerId; 436 | transformer.lineBreakpointIdsMap = new Map(lBpIdMInit); 437 | 438 | staticBpMInit.forEach(([key, value]) => { 439 | if (!Transformer.breakpointMap.has(key)) { 440 | Transformer.breakpointMap.set(key, value); 441 | } 442 | }); 443 | 444 | return debugProgram; 445 | } 446 | 447 | // 否则,运行时转换代码 448 | return transformer.run(script); 449 | } 450 | 451 | /** 452 | * 调试代码 453 | * @param {String} script 源码 454 | * @param {String} debuggerId 调试id,通常为脚本的url 455 | */ 456 | export function debug(script, debuggerId) { 457 | if (!supported || typeof script !== 'string' || debuggerId && typeof debuggerId !== 'string') { 458 | if (!supported) { 459 | console.warn('[vDebugger] Current environment is unsupported.'); 460 | } 461 | return false; 462 | } 463 | 464 | // 转换代码 465 | const transformer = new Transformer(debuggerId); 466 | const debugProgram = getDebugProgram(script, transformer); 467 | // console.warn(debugProgram); 468 | 469 | // 保存当前实例 470 | transformerMap.set(transformer.debuggerId, transformer); 471 | 472 | // 执行代码 473 | const complete = (exports) => { 474 | resolveModuleExports(transformer.debuggerId, exports); 475 | executeNextMacroTask(); 476 | }; 477 | const execute = () => { 478 | const fn = nativeEval(debugProgram)[0]; 479 | const ret = executor(fn( 480 | checkIfBreak, executor, scope, setter, 481 | newObject, requestModules, importModule 482 | )); 483 | ret?.then && ret.then(complete) || complete(ret); 484 | }; 485 | const run = () => { 486 | // 检查是否需要阻塞,如果需要阻塞,则暂停执行,并将恢复执行任务放入宏任务队列,等待断点恢复后再执行 487 | resumeExecutor ? macroTaskList.push(execute) : execute(); 488 | }; 489 | 490 | // 返回执行函数 491 | return run; 492 | } 493 | 494 | /** 495 | * 转换代码,用于编译时转换,优化运行时性能 496 | * @param {String} script 源码 497 | * @param {String} debuggerId 调试id,通常为脚本的url 498 | */ 499 | export function transform(script, debuggerId) { 500 | if (typeof script !== 'string' || debuggerId && typeof debuggerId !== 'string') { 501 | return false; 502 | } 503 | 504 | // 如果是已经转换过的代码,直接返回结果 505 | if (script.startsWith(TRANSFORMED_MARK)) { 506 | return script; 507 | } 508 | 509 | // 转换代码 510 | const transformer = new Transformer(debuggerId); 511 | const debugProgram = transformer.run(script); 512 | // console.warn(debugProgram); 513 | 514 | // 格式化数据结构 515 | const lBpIdMEntries = transformer.lineBreakpointIdsMap.entries(); 516 | const staticBpMEntries = Transformer.breakpointMap.entries(); 517 | const result = [transformer.debuggerId, debugProgram, [...lBpIdMEntries], [...staticBpMEntries]]; 518 | 519 | return TRANSFORMED_MARK + JSON.stringify(result); 520 | } 521 | 522 | /** 523 | * 恢复执行 524 | * @param {String} type 恢复类型 525 | */ 526 | export function resume(type) { 527 | if (type && ['stepInto', 'stepOver', 'stepOut'].indexOf(type) === -1) { 528 | return false; 529 | } 530 | const currentExecutor = resumeExecutor; 531 | if (currentExecutor) { 532 | resumeOptions = { type, callFrameId: Scope.getCurrentCallFrameId() }; // 定义断点恢复设置 533 | resumeExecutor = null; 534 | currentBreaker = null; 535 | pausedInfo = null; 536 | emitEventListener('resumed'); 537 | currentExecutor(); 538 | return true; 539 | } 540 | return false; 541 | } 542 | 543 | /** 544 | * 在特定作用域下执行表达式 545 | * @param {String} expression 表达式 546 | * @param {Number} callFrameId 调用帧id 547 | */ 548 | export function evaluate(expression, callFrameId) { 549 | if (typeof expression !== 'string') { 550 | return false; 551 | } 552 | const scope = Scope.getScopeByCallFrameId(callFrameId); 553 | return scope ? scope.eval(expression) : nativeEval(expression); 554 | } 555 | 556 | /** 557 | * 获取脚本所有可能的断点 558 | * @param {String} debuggerId 调试id,通常为脚本的url 559 | */ 560 | export function getPossibleBreakpoints(debuggerId) { 561 | if (typeof debuggerId !== 'string') { 562 | return false; 563 | } 564 | const transformer = transformerMap.get(debuggerId); 565 | if (transformer) { 566 | let breakpoints = []; 567 | for (const [lineNumber, lineBreakpointIds] of transformer.lineBreakpointIdsMap.entries()) { 568 | breakpoints = breakpoints.concat( 569 | Object.keys(lineBreakpointIds) 570 | .map((c) => ({ id: lineBreakpointIds[c], lineNumber, columnNumber: c * 1 })) 571 | ); 572 | } 573 | return breakpoints; 574 | } 575 | return false; 576 | } 577 | 578 | /** 579 | * 设置断点 580 | * @param {String} debuggerId 调试id,通常为脚本的url 581 | * @param {Number} lineNumber 尝试断点的行号 582 | * @param {Number} columnNumber 尝试断点的列号(或断点条件,重载) 583 | * @param {String} condition 断点条件 584 | */ 585 | export function setBreakpoint(debuggerId, lineNumber, columnNumber, condition) { 586 | if (typeof debuggerId !== 'string' || typeof lineNumber !== 'number') { 587 | return false; 588 | } 589 | if (typeof columnNumber === 'string') { 590 | condition = columnNumber; 591 | columnNumber = null; 592 | } 593 | const transformer = transformerMap.get(debuggerId); 594 | if (transformer) { 595 | for (let l = lineNumber; l < lineNumber + 50; l++) { // 向下找最多50行 596 | const lineBreakpointIds = transformer.lineBreakpointIdsMap.get(l); 597 | if (lineBreakpointIds) { 598 | let id, c; 599 | if (typeof columnNumber === 'number') { 600 | for (c = columnNumber; c < columnNumber + 200; c++) { // 向右找最多200列 601 | if (id = lineBreakpointIds[c]) break; 602 | } 603 | } else { 604 | c = Object.keys(lineBreakpointIds)[0]; 605 | id = lineBreakpointIds[c]; 606 | } 607 | if (id) { 608 | Transformer.breakpointMap.set(id, typeof condition === 'string' && condition || true); 609 | return { id, lineNumber: l, columnNumber: c }; 610 | } 611 | } 612 | } 613 | } 614 | return false; 615 | } 616 | 617 | /** 618 | * 移除断点 619 | * @param {Number} id 断点id 620 | */ 621 | export function removeBreakpoint(id) { 622 | if (typeof id !== 'number') { 623 | return false; 624 | } 625 | return Transformer.breakpointMap.delete(id); 626 | } 627 | 628 | /** 629 | * 断点启用设置 630 | * @param {Boolean} value 是否启用断点 631 | */ 632 | export function setBreakpointsActive(value) { 633 | return active = !!value; 634 | } 635 | 636 | /** 637 | * 执行暂停配置 638 | * @param {Boolean} value 是否暂停执行 639 | */ 640 | export function setExecutionPause(value) { 641 | return !!(pause = value ? 1 : 0); 642 | } 643 | 644 | /** 645 | * 执行异常暂停配置 646 | * @param {Boolean} value 是否异常时暂停执行 647 | */ 648 | export function setExceptionPause(value) { 649 | return !!(pause = value ? 2 : 0); 650 | } 651 | 652 | /** 653 | * 获取当前暂停信息 654 | */ 655 | export function getPausedInfo() { 656 | return pausedInfo || false; 657 | } 658 | 659 | /** 660 | * 获取当前作用域链 661 | */ 662 | export function getScopeChain() { 663 | return [].concat(Scope.chain); 664 | } 665 | 666 | /** 667 | * 获取调试源码 668 | */ 669 | export function getScriptContent(debuggerId) { 670 | const transformer = transformerMap.get(debuggerId); 671 | return transformer?.scriptContent || ''; 672 | } 673 | 674 | /** 675 | * 在原生环境下执行代码 676 | * @param {Function} callback 执行回调 677 | */ 678 | export function runInNativeEnv(callback) { 679 | if (typeof callback !== 'function') { 680 | return false; 681 | } 682 | const oriSandbox = sandbox; 683 | enableSandbox(false); 684 | try { 685 | return callback(); 686 | } catch (err) { 687 | return false; 688 | } finally { 689 | enableSandbox(oriSandbox); 690 | } 691 | } 692 | 693 | /** 694 | * 跳过调试执行代码 695 | * @param {Function} callback 执行回调 696 | */ 697 | export function runInSkipOver(callback) { 698 | if (typeof callback !== 'function') { 699 | return false; 700 | } 701 | const oriSkip = skip; 702 | skip = true; 703 | try { 704 | return callback(); 705 | } catch (err) { 706 | return false; 707 | } finally { 708 | skip = oriSkip; 709 | } 710 | } 711 | 712 | /** 713 | * 设置模块请求函数 714 | * @param {Function} request 请求函数 715 | */ 716 | export function setModuleRequest(request) { 717 | if (typeof request === 'function') { 718 | return !!(moduleRequest = request); 719 | } 720 | return false; 721 | } 722 | 723 | export { version, addEventListener, removeEventListener } 724 | export default { 725 | version, debug, transform, resume, evaluate, getPossibleBreakpoints, setBreakpoint, removeBreakpoint, 726 | setBreakpointsActive, setExecutionPause, setExceptionPause, getPausedInfo, getScopeChain, getScriptContent, 727 | runInNativeEnv, runInSkipOver, setModuleRequest, addEventListener, removeEventListener, 728 | } 729 | -------------------------------------------------------------------------------- /src/sandbox.js: -------------------------------------------------------------------------------- 1 | import { CLASS_CONSTRUCTOR_NAME, SANDBOX_PREFIX } from './consts'; 2 | 3 | const FUNC_MARK = `const ${CLASS_CONSTRUCTOR_NAME}`; 4 | const funcToString = Function.prototype.toString; 5 | const oriArrayFrom = Array.from; 6 | const oriArrayMap = Array.prototype.map; 7 | const oriArrayForEach = Array.prototype.forEach; 8 | const oriArrayFilter = Array.prototype.filter; 9 | const oriArrayReduce = Array.prototype.reduce; 10 | const oriArrayReduceRight = Array.prototype.reduceRight; 11 | const oriArrayEvery = Array.prototype.every; 12 | const oriArraySome = Array.prototype.some; 13 | const oriArraySort = Array.prototype.sort; 14 | const oriArrayFind = Array.prototype.find; 15 | const oriArrayFindIndex = Array.prototype.findIndex; 16 | const oriArrayConcat = Array.prototype.concat; 17 | const oriArrayFlatMap = Array.prototype.flatMap; 18 | const oriStringReplace = String.prototype.replace; 19 | const oriStringReplaceAll = String.prototype.replaceAll; 20 | const oriMapForEach = Map.prototype.forEach; 21 | const oriSetForEach = Set.prototype.forEach; 22 | const oriCustomElementDefine = typeof CustomElementRegistry === 'function' && CustomElementRegistry.prototype.define; 23 | 24 | let hasWrappedProtoMethod = false; 25 | export function wrapProtoMethod(executor) { 26 | if (hasWrappedProtoMethod) return; 27 | 28 | hasWrappedProtoMethod = true; 29 | 30 | Array.from = function from(arrayLike, mapper, thisArg) { 31 | if (typeof mapper === 'function' && funcToString.call(mapper).indexOf(FUNC_MARK) !== -1) { 32 | return executor((function* (array) { 33 | const result = []; 34 | for (let i = 0; i < array.length; i++) result.push(yield mapper.call(thisArg, array[i], i, array)); 35 | return result; 36 | })(oriArrayFrom(arrayLike))); 37 | } 38 | return oriArrayFrom(arrayLike, mapper, thisArg); 39 | }; 40 | 41 | Array.prototype.forEach = function forEach(iterator, thisArg) { 42 | if (typeof iterator === 'function' && funcToString.call(iterator).indexOf(FUNC_MARK) !== -1) { 43 | return executor((function* (array) { 44 | for (let i = 0; i < array.length; i++) yield iterator.call(thisArg, array[i], i, array); 45 | })(this)); 46 | } 47 | return oriArrayForEach.call(this, iterator, thisArg); 48 | }; 49 | 50 | Array.prototype.map = function map(mapper, thisArg) { 51 | if (typeof mapper === 'function' && funcToString.call(mapper).indexOf(FUNC_MARK) !== -1) { 52 | return executor((function* (array) { 53 | const result = []; 54 | for (let i = 0; i < array.length; i++) result.push(yield mapper.call(thisArg, array[i], i, array)); 55 | return result; 56 | })(this)); 57 | } 58 | return oriArrayMap.call(this, mapper, thisArg); 59 | }; 60 | 61 | Array.prototype.filter = function filter(filter, thisArg) { 62 | if (typeof filter === 'function' && funcToString.call(filter).indexOf(FUNC_MARK) !== -1) { 63 | return executor((function* (array) { 64 | const result = []; 65 | for (let i = 0; i < array.length; i++) (yield filter.call(thisArg, array[i], i, array)) && result.push(array[i]); 66 | return result; 67 | })(this)); 68 | } 69 | return oriArrayFilter.call(this, filter, thisArg); 70 | }; 71 | 72 | Array.prototype.reduce = function reduce(reducer, init) { 73 | const hasInit = arguments.length > 1; 74 | if (typeof reducer === 'function' && funcToString.call(reducer).indexOf(FUNC_MARK) !== -1) { 75 | return executor(function* (array) { 76 | let result = hasInit ? init : array[0]; 77 | for (let i = hasInit ? 0 : 1; i < array.length; i++) result = yield reducer(result, array[i], i, array); 78 | return result; 79 | }(this)); 80 | } 81 | if (hasInit) return oriArrayReduce.call(this, reducer, init); 82 | return oriArrayReduce.call(this, reducer); 83 | }; 84 | 85 | Array.prototype.reduceRight = function reduceRight(reducer, init) { 86 | const hasInit = arguments.length > 1; 87 | if (typeof reducer === 'function' && funcToString.call(reducer).indexOf(FUNC_MARK) !== -1) { 88 | return executor(function* (array) { 89 | let result = hasInit? init : array[array.length - 1];; 90 | 91 | for (let i = hasInit ? array.length - 1: array.length - 2; i !== -1; i--) result = yield reducer(result, array[i], i, array); 92 | 93 | return result; 94 | }(this)); 95 | } 96 | 97 | if(hasInit) return oriArrayReduceRight.call(this, reducer, init); 98 | return oriArrayReduceRight.call(this, reducer); 99 | }; 100 | 101 | Array.prototype.every = function every(predicate, thisArg) { 102 | if (typeof predicate === 'function' && funcToString.call(predicate).indexOf(FUNC_MARK) !== -1) { 103 | return executor((function* (array) { 104 | for (let i = 0; i < array.length; i++) { 105 | if (!(yield predicate.call(thisArg, array[i], i, array))) return false; 106 | } 107 | return true; 108 | })(this)); 109 | } 110 | return oriArrayEvery.call(this, predicate, thisArg); 111 | }; 112 | 113 | Array.prototype.some = function some(predicate, thisArg) { 114 | if (typeof predicate === 'function' && funcToString.call(predicate).indexOf(FUNC_MARK) !== -1) { 115 | return executor((function* (array) { 116 | for (let i = 0; i < array.length; i++) { 117 | if (yield predicate.call(thisArg, array[i], i, array)) return true; 118 | } 119 | return false; 120 | })(this)); 121 | } 122 | return oriArraySome.call(this, predicate, thisArg); 123 | }; 124 | 125 | Array.prototype.sort = function sort(compare) { 126 | if (typeof compare === 'function' && funcToString.call(compare).indexOf(FUNC_MARK) !== -1) { 127 | return executor((function* (array) { 128 | // EMCA规定sort必须稳定,V8使用了timsort,这边简单处理,统一用归并排序 129 | function* sort(arr, l, r) { 130 | if (l >= r) return; 131 | const mid = l + Math.floor((r - l) / 2); 132 | yield* sort(arr, l, mid); 133 | yield* sort(arr, mid + 1, r); 134 | const res = yield compare(arr[mid], arr[mid + 1]); 135 | if (typeof res === 'number' && res > 0) { 136 | for (let k = l, i = l, j = mid + 1, aux = arr.slice(l, r + 1); k <= r; k++) { 137 | if (i > mid) arr[k] = aux[j++ - l]; 138 | else if (j > r) arr[k] = aux[i++ - l]; 139 | else { 140 | const res = yield compare(aux[i - l], aux[j - l]); 141 | if (typeof res === 'number' && res > 0) arr[k] = aux[j++ - l]; 142 | else arr[k] = aux[i++ - l]; 143 | } 144 | } 145 | } 146 | } 147 | yield* sort(array, 0, array.length - 1); 148 | return array; 149 | })(this)); 150 | } 151 | return oriArraySort.call(this, compare); 152 | }; 153 | 154 | Array.prototype.find = function find(predicate, thisArg) { 155 | if (typeof predicate === 'function' && funcToString.call(predicate).indexOf(FUNC_MARK) !== -1) { 156 | return executor((function* (array) { 157 | for (let i = 0; i < array.length; i++) { 158 | if (yield predicate.call(thisArg, array[i], i, array)) return array[i]; 159 | } 160 | })(this)); 161 | } 162 | return oriArrayFind.call(this, predicate, thisArg); 163 | }; 164 | 165 | Array.prototype.findIndex = function findIndex(predicate, thisArg) { 166 | if (typeof predicate === 'function' && funcToString.call(predicate).indexOf(FUNC_MARK) !== -1) { 167 | return executor((function* (array) { 168 | for (let i = 0; i < array.length; i++) { 169 | if (yield predicate.call(thisArg, array[i], i, array)) return i; 170 | } 171 | })(this)); 172 | } 173 | return oriArrayFindIndex.call(this, predicate, thisArg); 174 | }; 175 | 176 | oriArrayFlatMap && (Array.prototype.flatMap = function map(mapper, thisArg) { 177 | if (typeof mapper === 'function' && funcToString.call(mapper).indexOf(FUNC_MARK) !== -1) { 178 | return executor((function* (array) { 179 | const result = []; 180 | for (let i = 0; i < array.length; i++) result.push(yield mapper.call(thisArg, array[i], i, array)); 181 | return oriArrayConcat.apply([], result); 182 | })(this)); 183 | } 184 | return oriArrayFlatMap.call(this, mapper, thisArg); 185 | }); 186 | 187 | // Array.prototype.findLast 188 | // Array.prototype.findLastIndex 189 | // Array.prototype.group 190 | // Array.prototype.groupToMap 191 | 192 | const replaceString = (string, search, replacer, global) => { 193 | const reg = typeof search === 'string' ? search : new RegExp(search.source, oriStringReplace.call(search.flags, 'g', '')); 194 | return executor((function* () { 195 | let index = 0; 196 | let result = ''; 197 | do { 198 | const rest = string.substring(index); 199 | const match = rest.match(reg); 200 | if (match) { 201 | const restIndex = match.index; 202 | match.index += index; 203 | match.input = string; 204 | result += rest.substring(0, restIndex) + (yield replacer(...match, match.index, string)); 205 | index = match.index + match[0].length; 206 | } else break; 207 | } while (search.global || global) 208 | result += string.substring(index); 209 | return result; 210 | })()); 211 | }; 212 | 213 | String.prototype.replace = function replace(search, replacer) { 214 | if (typeof replacer === 'function' && funcToString.call(replacer).indexOf(FUNC_MARK) !== -1) { 215 | if (typeof search === 'string' || search instanceof RegExp) { 216 | return replaceString(this, search, replacer); 217 | } 218 | } 219 | return oriStringReplace.call(this, search, replacer); 220 | }; 221 | 222 | String.prototype.replaceAll && (String.prototype.replaceAll = function replaceAll(search, replacer) { 223 | if (typeof replacer === 'function' && funcToString.call(replacer).indexOf(FUNC_MARK) !== -1) { 224 | if (typeof search === 'string' || search instanceof RegExp) { 225 | if (search instanceof RegExp && search.flags.indexOf('g') === -1) { 226 | throw new TypeError('String.prototype.replaceAll called with a non-global RegExp argument'); 227 | } 228 | return replaceString(this, search, replacer, true); 229 | } 230 | } 231 | return oriStringReplaceAll.call(this, search, replacer); 232 | }); 233 | 234 | Map.prototype.forEach = function forEach(iterator, thisArg) { 235 | if (typeof iterator === 'function' && funcToString.call(iterator).indexOf(FUNC_MARK) !== -1) { 236 | return executor((function* (map) { 237 | const entries = oriArrayFrom(map); 238 | for (let i = 0; i < entries.length; i++) yield iterator.call(thisArg, entries[i][1], entries[i][0], map); 239 | })(this)); 240 | } 241 | return oriMapForEach.call(this, iterator, thisArg); 242 | }; 243 | 244 | Set.prototype.forEach = function forEach(iterator, thisArg) { 245 | if (typeof iterator === 'function' && funcToString.call(iterator).indexOf(FUNC_MARK) !== -1) { 246 | return executor((function* (set) { 247 | const values = oriArrayFrom(set); 248 | for (let i = 0; i < values.length; i++) yield iterator.call(thisArg, values[i], values[i], set); 249 | })(this)); 250 | } 251 | return oriSetForEach.call(this, iterator, thisArg); 252 | }; 253 | 254 | oriCustomElementDefine && (CustomElementRegistry.prototype.define = function define(_, ctor) { 255 | typeof ctor === 'function' && (ctor[CLASS_CONSTRUCTOR_NAME] = 1); 256 | return oriCustomElementDefine.apply(this, arguments); 257 | }); 258 | } 259 | 260 | const globalObjectCache = {}; 261 | export function switchGlobalObject() { 262 | [Promise, globalObjectCache.Promise] = [globalObjectCache.Promise || Promise, Promise]; 263 | } 264 | 265 | export function switchObjectMethod(object, methodNameList) { 266 | oriArrayForEach.call(methodNameList, (methodName) => { 267 | if (methodName in object) { 268 | const switchName = `${SANDBOX_PREFIX}${methodName.toString()}`; 269 | const method = object[methodName]; 270 | const switchMethod = object[switchName] || method; 271 | object[methodName] = switchMethod; 272 | Object.defineProperty(object, switchName, { 273 | value: method, 274 | writable: false, 275 | enumerable: false, 276 | configurable: true, 277 | }); 278 | } 279 | }); 280 | } 281 | -------------------------------------------------------------------------------- /src/scope.js: -------------------------------------------------------------------------------- 1 | export default class Scope { 2 | static chain = []; 3 | static callFrameId = 0; 4 | static lastPop = null; // 记录一下最后被pop的scope 5 | static curNamedScope = null; // 记录一下当前函数scope 6 | 7 | constructor(scopeEval, scopeName) { 8 | this.eval = scopeEval; 9 | this.name = scopeName; 10 | this.callFrameId = scopeName ? ++Scope.callFrameId : Scope.callFrameId; 11 | } 12 | 13 | static getSpecifiedScope(check) { 14 | const chain = Scope.chain; 15 | const len = chain.length; 16 | for (let i = len - 1; i !== -1; i--) { 17 | if (check(chain[i])) { 18 | return chain[i]; 19 | } 20 | } 21 | return chain[len - 1]; 22 | } 23 | 24 | static getScopeByCallFrameId(callFrameId) { 25 | return Scope.getSpecifiedScope((scope) => scope.callFrameId === callFrameId); 26 | } 27 | 28 | static getCurrentCallFrameId() { 29 | return Scope.chain[Scope.chain.length - 1]?.callFrameId; 30 | } 31 | 32 | static updateCallFrame(callFrame) { 33 | Scope.curNamedScope && (Scope.curNamedScope.callFrame = callFrame); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/transformer.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'meriyah'; 2 | import { generate } from 'astring'; 3 | import { ancestor } from 'acorn-walk'; 4 | import { 5 | EXECUTOR_FUNC_NAME, 6 | EXECUTOR_BREAK_NAME, 7 | IMPORT_REQ_NAME, 8 | IMPORT_FUNC_NAME, 9 | EXPORT_OBJECT_NAME, 10 | TMP_VARIABLE_NAME, 11 | DEBUGGER_ID_NAME, 12 | SCOPE_TRACER_NAME, 13 | CLASS_CONSTRUCTOR_NAME, 14 | CLASS_CREATE_NAME, 15 | VALUE_SETTER_NAME, 16 | IMPORT_META_NAME, 17 | NEW_TARGET_NAME, 18 | } from './consts'; 19 | 20 | export default class Transformer { 21 | static breakpointId = 1; 22 | static breakpointMap = new Map(); 23 | 24 | debuggerId = ''; 25 | scriptContent = ''; 26 | importNodeList = []; 27 | exportDeclarationNodeList = []; 28 | lineBreakpointIdsMap = new Map(); 29 | 30 | constructor(debuggerId) { 31 | this.debuggerId = debuggerId || Transformer.genTmpDebuggerId(); 32 | } 33 | 34 | // 生成临时debuggerId 35 | static genTmpDebuggerId() { 36 | return 'VM' + parseInt(Math.random() * 100000); 37 | } 38 | 39 | /** 40 | * 转换代码,进行插桩、处理ESModule语句等操作 41 | * @param {String} scriptContent 源码 42 | */ 43 | run(scriptContent) { 44 | // 保存源码信息 45 | this.scriptContent = scriptContent; 46 | 47 | // 生成ast 48 | const ast = parse(scriptContent, { module: true, loc: true }); 49 | 50 | // 插入断点检查函数 51 | ancestor(ast, { 52 | DebuggerStatement: (node) => { 53 | // 对debugger语句插入断点 54 | Object.assign(node, this.createExpressionStatement( 55 | this.createBreakpointExpression(node) 56 | )); 57 | const breakpointId = node?.expression?.expressions?.[0]?.left?.right.arguments?.[1]?.value; 58 | if (breakpointId) { 59 | Transformer.breakpointMap.set(breakpointId, true); 60 | } 61 | }, 62 | Program: (node) => { 63 | // 转换程序入口,插入断点 64 | this.transformProgram(node); 65 | }, 66 | BlockStatement: (node, ancestors) => { 67 | // 转换块级语句,插入断点并记录作用域链 68 | this.transformBlockStatement(node, ancestors); 69 | }, 70 | AssignmentExpression: (node) => { 71 | // 转换赋值表达式,保证setter在协程中顺序执行 72 | this.transformAssignmentExpression(node); 73 | }, 74 | FunctionDeclaration: (node) => { 75 | // 转换函数声明,同时支持断点以及new创建实例 76 | this.transformFunction(node); 77 | }, 78 | FunctionExpression: (node) => { 79 | // 转换函数表达式,同时支持断点以及new创建 80 | this.transformFunction(node); 81 | }, 82 | ArrowFunctionExpression: (node) => { 83 | // 把所有箭头函数表达式改成generator 84 | this.transformFunction(node); 85 | }, 86 | CallExpression: (node, ancestors) => { 87 | // 对于被debugger转换成generator函数的调用,套一层yield语句 88 | this.transformCallExpression(node, ancestors); 89 | }, 90 | MemberExpression: (node, ancestors) => { 91 | // 对于被debugger转换成generator函数的调用,套一层yield语句 92 | this.transformMemberExpression(node, ancestors); 93 | }, 94 | ClassBody: (node, ancestors) => { 95 | // 因为class constructor要求同步执行,这里对constructor进行转换一下,变成generator,搭配NewExpression转换使用 96 | this.transformClass(node, ancestors); 97 | }, 98 | ThisExpression: (node) => { 99 | Object.assign(node, this.createIdentifier(TMP_VARIABLE_NAME + 't')); 100 | }, 101 | Identifier: (node) => { 102 | if (node.name === 'arguments') { 103 | node.name = TMP_VARIABLE_NAME + 'a'; 104 | } 105 | }, 106 | NewExpression: (node) => { 107 | // 因为class constructor要求同步执行,上面对constructor转换成了generator,因此这里需要转换一下,主动调用这个迭代器 108 | this.transformNewExpression(node); 109 | }, 110 | ImportDeclaration: (node) => { 111 | // 转换import声明为自己实现的import,代理import行为 112 | this.transformImportDeclaration(node); 113 | this.importNodeList.push(node); 114 | }, 115 | ImportExpression: (node) => { 116 | // 转换动态import表达式为自己实现的import,代理import行为 117 | this.transformImportExpression(node); 118 | }, 119 | ExportDefaultDeclaration: (node) => { 120 | // 转换默认export声明来代理export行为 121 | this.transformExportDefaultDeclaration(node); 122 | }, 123 | ExportNamedDeclaration: (node) => { 124 | // 转换具名export声明来代理export行为 125 | this.transformExportNamedDeclaration(node); 126 | }, 127 | ExportAllDeclaration: (node) => { 128 | // 转换全导出export声明来代理export行为 129 | this.transformExportAllDeclaration(node); 130 | }, 131 | MetaProperty: (node) => { 132 | // 转换import.meta 133 | this.transformMetaProperty(node); 134 | }, 135 | }); 136 | 137 | // 处理全局信息 138 | this.transformGlobalInfo(ast); 139 | 140 | // 重新生成代码 141 | return '[function*(' + 142 | `${EXECUTOR_BREAK_NAME},${EXECUTOR_FUNC_NAME},${SCOPE_TRACER_NAME},${VALUE_SETTER_NAME},` + 143 | `${CLASS_CREATE_NAME},${IMPORT_REQ_NAME},${IMPORT_FUNC_NAME}` + 144 | '){' + 145 | 'try{' + 146 | 'return yield* (function* () {' + 147 | `${SCOPE_TRACER_NAME}(true,x=>eval(x),'(global)');` + 148 | `${generate(ast, process.env.NODE_ENV === 'production' ? { indent: '', lineEnd: '' } : {})}` + 149 | '})()' + 150 | '}finally{' + 151 | `${SCOPE_TRACER_NAME}(false)` + 152 | '}' + 153 | '}]'; 154 | } 155 | 156 | // 创建export赋值表达式AST节点 157 | createExportAssginmentExpression(exportIdentifier, exportNameIdentifier, declaration) { 158 | return this.createAssignmentExpression( 159 | this.createMemberExpression( 160 | exportIdentifier, 161 | exportNameIdentifier 162 | ), 163 | declaration 164 | ); 165 | } 166 | 167 | // 创建module请求语句AST节点 168 | createModuleRequestStatement() { 169 | return this.createExpressionStatement( 170 | this.createYieldExpression( 171 | this.createCallExpression( 172 | this.createIdentifier(IMPORT_REQ_NAME), [ 173 | this.createArrayExpression( 174 | this.importNodeList.map((node) => node.source) 175 | ), 176 | this.createIdentifier(DEBUGGER_ID_NAME) 177 | ] 178 | ), false 179 | ) 180 | ) 181 | } 182 | 183 | // 创建import表达式AST节点 184 | createImportCallExpression(source, dynamic) { 185 | const args = [source, this.createIdentifier(DEBUGGER_ID_NAME)]; 186 | if (dynamic) { 187 | args.push(this.createLiteral(true)); 188 | } 189 | return this.createCallExpression( 190 | this.createIdentifier(IMPORT_FUNC_NAME), args 191 | ); 192 | } 193 | 194 | // 创建断点的AST节点 195 | createBreakpointExpression(node, blocker) { 196 | const breakpointId = Transformer.breakpointId++; 197 | const params = [ 198 | this.createIdentifier(DEBUGGER_ID_NAME), 199 | this.createLiteral(breakpointId), 200 | this.createLiteral(node.loc.start.line), 201 | this.createLiteral(node.loc.start.column) 202 | ]; 203 | if (blocker) { 204 | params.push(this.createLiteral(1)); 205 | } else { 206 | const lineBreakpointIds = this.lineBreakpointIdsMap.get(node.loc.start.line); 207 | if (lineBreakpointIds) { 208 | lineBreakpointIds[node.loc.start.column] = breakpointId; 209 | } else { 210 | this.lineBreakpointIdsMap.set(node.loc.start.line, { 211 | [node.loc.start.column]: breakpointId 212 | }); 213 | } 214 | } 215 | const tmpYieldIdentifier = this.createIdentifier(TMP_VARIABLE_NAME + 'y'); 216 | return this.createSequenceExpression([ 217 | this.createLogicalExpression( 218 | this.createAssignmentExpression( 219 | tmpYieldIdentifier, 220 | this.createCallExpression( 221 | this.createIdentifier(EXECUTOR_BREAK_NAME), 222 | params 223 | ) 224 | ), 225 | '&&', this.createYieldExpression(tmpYieldIdentifier, false) 226 | ) 227 | ]); 228 | } 229 | 230 | // 创建作用域所需变量声明 231 | createScopeVariableDeclarators() { 232 | return [ 233 | this.createVariableDeclarator(this.createIdentifier(TMP_VARIABLE_NAME + 'o'), null), 234 | this.createVariableDeclarator(this.createIdentifier(TMP_VARIABLE_NAME + 'f'), null), 235 | this.createVariableDeclarator(this.createIdentifier(TMP_VARIABLE_NAME + 'y'), null), 236 | this.createVariableDeclarator(this.createIdentifier(TMP_VARIABLE_NAME + 't'), this.createThisExpression()), 237 | this.createVariableDeclarator(this.createIdentifier(TMP_VARIABLE_NAME + 'a'), this.createIdentifier('arguments')), 238 | this.createVariableDeclarator(this.createIdentifier(NEW_TARGET_NAME), 239 | this.createMetaProperty(this.createIdentifier('new'), this.createIdentifier('target')) 240 | ) 241 | ]; 242 | } 243 | 244 | // 转换入口,插入断点 245 | transformProgram(node) { 246 | node.body = [].concat(...node.body.map((bodyNode) => [ 247 | // 给每个语句前都插入断点 248 | this.createExpressionStatement( 249 | this.createBreakpointExpression(bodyNode) 250 | ), 251 | bodyNode 252 | ])); 253 | } 254 | 255 | // 转换块级语句,插入断点,记录scope eval等信息 256 | transformBlockStatement(node, ancestors) { 257 | const parentNode = ancestors[ancestors.length - 2]; 258 | const grandParentNode = ancestors[ancestors.length - 3]; 259 | const isFunction = parentNode?.type.indexOf('Function') !== -1; 260 | const undefinedIdentifier = this.createIdentifier('undefined'); 261 | const assignmentPatternParamsDeclarators = []; 262 | let scopeNameIdentifier = undefinedIdentifier; 263 | if (isFunction) { 264 | scopeNameIdentifier = this.createLiteral('(anonymous)'); 265 | if (parentNode.id?.name) { 266 | scopeNameIdentifier = this.createLiteral(parentNode.id?.name); 267 | } else if (grandParentNode?.key?.name) { 268 | scopeNameIdentifier = this.createLiteral(grandParentNode?.key?.name); 269 | } 270 | parentNode.params.forEach((p, i) => { 271 | if (p.type === 'AssignmentPattern') { 272 | const paramIdentifier = this.createIdentifier(TMP_VARIABLE_NAME + 'a' + i); 273 | assignmentPatternParamsDeclarators.push( 274 | this.createVariableDeclarator( 275 | p.left, 276 | this.createConditionalExpression( 277 | this.createBinaryExpression(paramIdentifier, '===', undefinedIdentifier), 278 | p.right, paramIdentifier 279 | ) 280 | ) 281 | ); 282 | Object.assign(p, paramIdentifier); 283 | } 284 | }); 285 | } 286 | const scopeInitCallExpr = this.createCallExpression( 287 | this.createIdentifier(SCOPE_TRACER_NAME), 288 | [ 289 | this.createLiteral(true), 290 | this.createFunctionExpression( 291 | this.createCallExpression( 292 | this.createIdentifier('eval'), 293 | [this.createIdentifier('x')] 294 | ), 295 | [this.createIdentifier('x')], 296 | 'ArrowFunctionExpression', false 297 | ), 298 | scopeNameIdentifier 299 | ] 300 | ); 301 | const cookedBody = [ 302 | ...( 303 | assignmentPatternParamsDeclarators.length 304 | ? [this.createVariableDeclaration('let', assignmentPatternParamsDeclarators)] 305 | : [] 306 | ), 307 | this.createExpressionStatement( 308 | isFunction ? this.createSequenceExpression([ 309 | this.createBreakpointExpression(node, true), 310 | scopeInitCallExpr 311 | ]) : scopeInitCallExpr 312 | ) 313 | ].concat(...node.body.map((bodyNode) => [ 314 | // 给每个语句前都插入断点 315 | this.createExpressionStatement( 316 | this.createBreakpointExpression(bodyNode) 317 | ), 318 | bodyNode 319 | ])); 320 | node.body = isFunction ? cookedBody : [ 321 | this.createTryStatement( 322 | this.createBlockStatement(cookedBody), 323 | null, 324 | this.createBlockStatement([ 325 | this.createExpressionStatement( 326 | this.createCallExpression( 327 | this.createIdentifier(SCOPE_TRACER_NAME), 328 | [this.createLiteral(false)] 329 | ) 330 | ) 331 | ]) 332 | ) 333 | ]; 334 | } 335 | 336 | // 转换赋值表达式,保证setter在协程中顺序执行,将赋值表达式后面的语句阻塞 337 | transformAssignmentExpression(node) { 338 | const operator = node.operator.substring(0, node.operator.length - 1); 339 | if (operator) { 340 | node.right = this.createBinaryExpression( 341 | this.createYieldExpression(Object.assign({}, node.left), false), 342 | operator, 343 | Object.assign({}, node.right) 344 | ); 345 | } 346 | node.operator = '='; 347 | if (node.left.type === 'MemberExpression') { 348 | const key = !node.left.computed && node.left.property.type === 'Identifier' ? this.createLiteral(node.left.property.name): node.left.property; 349 | Object.assign(node, this.createYieldExpression( 350 | this.createCallExpression( 351 | this.createIdentifier(VALUE_SETTER_NAME), 352 | [node.left.object, key, node.right] 353 | ), false 354 | )); 355 | } 356 | } 357 | 358 | // 转换属性访问,探寻是否是debugger转换的generator函数,如果是加上yield 359 | transformMemberExpression(node, ancestors) { 360 | const parentNode = ancestors[ancestors.length - 2]; 361 | if (parentNode.type === 'AssignmentExpression' && parentNode.left === node) { 362 | // 左值,不处理 363 | return; 364 | } 365 | if (parentNode.type === 'UnaryExpression' && parentNode.operator === 'delete') { 366 | // delete操作,不处理 367 | return; 368 | } 369 | if (ancestors.find((a) => a.type === 'AssignmentPattern' || a.type === 'ArrayPattern')) { 370 | // 解构,暂时不处理 371 | return; 372 | } 373 | if (parentNode.type === 'CallExpression' && parentNode.callee === node) { 374 | if (node.object?.name === 'Reflect' && node.property?.name === 'construct') { 375 | Object.assign(parentNode, this.createYieldExpression( 376 | this.createCallExpression( 377 | this.createIdentifier(CLASS_CREATE_NAME), 378 | parentNode.arguments 379 | )) 380 | ); 381 | return; 382 | } 383 | const grandParentNode = ancestors[ancestors.length - 3]; 384 | const isOptionalChain = grandParentNode.type === 'ChainExpression'; 385 | const tmpObjIdentifier = this.createIdentifier(TMP_VARIABLE_NAME + 'o'); 386 | const tmpFuncIdentifier = this.createIdentifier(TMP_VARIABLE_NAME + 'f'); 387 | Object.assign(parentNode, this.createSequenceExpression([ 388 | this.createAssignmentExpression( 389 | tmpObjIdentifier, 390 | node.object 391 | ), 392 | this.createAssignmentExpression( 393 | tmpFuncIdentifier, 394 | this.createYieldExpression( 395 | isOptionalChain 396 | ? this.createChainExpression(Object.assign({}, node, { object: tmpObjIdentifier, optional: true })) 397 | : Object.assign({}, node, { object: tmpObjIdentifier }) 398 | , false) 399 | ), 400 | this.createCallExpression( 401 | this.createCallExpression( 402 | isOptionalChain 403 | ? this.createChainExpression(this.createMemberExpression(tmpFuncIdentifier, this.createIdentifier('bind'), true)) 404 | : this.createMemberExpression(tmpFuncIdentifier, this.createIdentifier('bind')), 405 | [tmpObjIdentifier] 406 | ), 407 | parentNode.arguments 408 | ) 409 | ])); 410 | return; 411 | } 412 | if (parentNode.type === 'UpdateExpression') { 413 | if (parentNode.prefix) { 414 | Object.assign(parentNode, this.createAssignmentExpression( 415 | node, 416 | this.createBinaryExpression( 417 | this.createYieldExpression(Object.assign({}, node), false), 418 | parentNode.operator.slice(0, 1), 419 | this.createLiteral(1) 420 | ) 421 | )); 422 | } else { 423 | const tmpObjIdentifier = this.createIdentifier(TMP_VARIABLE_NAME + 'o'); 424 | Object.assign(parentNode, this.createSequenceExpression([ 425 | this.createAssignmentExpression( 426 | tmpObjIdentifier, 427 | this.createYieldExpression(Object.assign({}, node), false) 428 | ), 429 | this.createAssignmentExpression( 430 | node, 431 | this.createBinaryExpression( 432 | tmpObjIdentifier, 433 | parentNode.operator.slice(0, 1), 434 | this.createLiteral(1) 435 | ) 436 | ), 437 | tmpObjIdentifier 438 | ])); 439 | } 440 | return; 441 | } 442 | Object.assign(node, this.createSequenceExpression([ 443 | this.createYieldExpression(Object.assign({}, node), false) 444 | ])); 445 | } 446 | 447 | // 转换函数调用 448 | transformCallExpression(node, ancestors) { 449 | if (node.type === 'YieldExpression') { 450 | // 如果已经被转换成yield了,就不用再处理了 451 | return; 452 | } 453 | if (node.callee.type === 'Super') { 454 | // 由于class constructor转换了,所以无法直接调用super构造函数,这里也转换一下,主动调用父类相应的generator 455 | node.callee = this.createMemberExpression(node.callee, this.createIdentifier(CLASS_CONSTRUCTOR_NAME)); 456 | Object.assign(node, this.createYieldExpression(Object.assign({}, node))); 457 | return; 458 | } 459 | const parentNode = ancestors[ancestors.length - 2]; 460 | if (parentNode.type === 'ChainExpression') { 461 | Object.assign(parentNode, this.createYieldExpression(Object.assign({}, parentNode), false)); 462 | return; 463 | } 464 | Object.assign(node, this.createYieldExpression(Object.assign({}, node), false)); 465 | } 466 | 467 | // 转换function 468 | transformFunction(node) { 469 | const thisIdentifier = this.createIdentifier(TMP_VARIABLE_NAME + 't'); 470 | const ctorIdentifier = this.createIdentifier(CLASS_CONSTRUCTOR_NAME); 471 | const nwTgIdentifier = this.createIdentifier(NEW_TARGET_NAME); 472 | const functionBody = node.body.type === 'BlockStatement' 473 | ? node.body 474 | : this.createBlockStatement([ 475 | this.createReturnStatement(node.body) 476 | ]); 477 | const returnCall = this.createReturnStatement( 478 | this.createYieldExpression( 479 | this.createCallExpression( 480 | this.createMemberExpression( 481 | ctorIdentifier, 482 | this.createIdentifier('call') 483 | ), [thisIdentifier] 484 | ) 485 | ) 486 | ); 487 | node.expression = false; 488 | node.body = this.createBlockStatement([ 489 | this.createVariableDeclaration('let', this.createScopeVariableDeclarators()), 490 | this.createVariableDeclaration('const', [ 491 | this.createVariableDeclarator( 492 | ctorIdentifier, 493 | this.createFunctionExpression(functionBody, [], 'FunctionExpression', true, node.async) 494 | ) 495 | ]), 496 | this.createReturnStatement( 497 | this.createConditionalExpression( 498 | this.createLogicalExpression( 499 | nwTgIdentifier, '&&', 500 | this.createUnaryExpression( 501 | this.createMemberExpression( 502 | nwTgIdentifier, 503 | ctorIdentifier 504 | ), '!' 505 | ) 506 | ), 507 | this.createSequenceExpression([ 508 | this.createAssignmentExpression( 509 | this.createMemberExpression( 510 | thisIdentifier, 511 | ctorIdentifier 512 | ), 513 | ctorIdentifier 514 | ), 515 | thisIdentifier 516 | ]), 517 | this.createCallExpression( 518 | this.createIdentifier(EXECUTOR_FUNC_NAME), [ 519 | this.createCallExpression( 520 | this.createFunctionExpression( 521 | this.createBlockStatement([ 522 | node.body.type === 'BlockStatement' 523 | ? this.createTryStatement( 524 | this.createBlockStatement([ 525 | returnCall 526 | ]), 527 | null, 528 | this.createBlockStatement([ 529 | this.createExpressionStatement( 530 | this.createCallExpression( 531 | this.createIdentifier(SCOPE_TRACER_NAME), 532 | [this.createLiteral(false)] 533 | ) 534 | ) 535 | ]) 536 | ) 537 | : returnCall 538 | ]), [], 'FunctionExpression', true, node.async 539 | ) 540 | ) 541 | ] 542 | ) 543 | ) 544 | ) 545 | ]); 546 | } 547 | 548 | // 转换class 549 | transformClass(node, ancestors) { 550 | const ctorNode = node.body.find((m) => m.kind === 'constructor'); 551 | if (ctorNode) { 552 | const ctorFuncExpr = ctorNode.value; 553 | const innerFuncExpr = ctorFuncExpr.body.body[1].declarations[0].init; 554 | innerFuncExpr.body.body.unshift(ctorFuncExpr.body.body[0]); 555 | innerFuncExpr.params = ctorFuncExpr.params; 556 | ctorNode.key.name = CLASS_CONSTRUCTOR_NAME; 557 | ctorNode.value = innerFuncExpr; 558 | ctorNode.kind = 'method'; 559 | } 560 | const parentNode = ancestors[ancestors.length - 2]; 561 | const ctorIdentifier = this.createIdentifier(CLASS_CONSTRUCTOR_NAME); 562 | const ctorArgs = []; 563 | const ctorBody = []; 564 | if (parentNode.superClass) { 565 | ctorArgs.push(this.createRestElement(this.createIdentifier('args'))); 566 | ctorBody.push(this.createExpressionStatement( 567 | this.createCallExpression( 568 | this.createSuper(), 569 | [this.createSpreadElement(this.createIdentifier('args'))] 570 | ) 571 | )); 572 | } 573 | ctorBody.push(this.createReturnStatement( 574 | this.createConditionalExpression( 575 | this.createUnaryExpression( 576 | this.createMemberExpression( 577 | this.createMetaProperty(this.createIdentifier('new'), this.createIdentifier('target')), 578 | ctorIdentifier 579 | ), '!' 580 | ), 581 | this.createThisExpression(), 582 | this.createCallExpression( 583 | this.createIdentifier(EXECUTOR_FUNC_NAME), 584 | [this.createCallExpression( 585 | this.createMemberExpression( 586 | this.createMemberExpression(this.createThisExpression(), ctorIdentifier), 587 | this.createIdentifier('call') 588 | ), [this.createThisExpression()] 589 | )] 590 | ) 591 | ) 592 | )); 593 | const ctorDefinition = this.createMethodDefinition( 594 | 'constructor', 595 | this.createIdentifier('constructor'), 596 | this.createFunctionExpression( 597 | this.createBlockStatement(ctorBody), 598 | ctorArgs, 'FunctionExpression', false 599 | ) 600 | ); 601 | node.body.push(ctorDefinition); 602 | } 603 | 604 | // 转换new表达式 605 | transformNewExpression(node) { 606 | Object.assign(node, this.createYieldExpression( 607 | this.createCallExpression( 608 | this.createIdentifier(CLASS_CREATE_NAME), 609 | [node.callee, this.createArrayExpression(node.arguments)] 610 | )) 611 | ); 612 | } 613 | 614 | // 转换import语句 615 | transformImportDeclaration(node) { 616 | const importExpression = this.createImportCallExpression(node.source); 617 | const yieldExpression = this.createYieldExpression(importExpression, false); 618 | if (node.specifiers?.length) { 619 | const importNsSpecifier = node.specifiers.find((specifier) => specifier.type === 'ImportNamespaceSpecifier'); 620 | const varNsDeclarator = importNsSpecifier && this.createVariableDeclarator(importNsSpecifier.local, yieldExpression); 621 | const objPatternProperties = node.specifiers 622 | .filter((specifier) => specifier.type !== 'ImportNamespaceSpecifier') 623 | .map((specifier) => { 624 | return this.createProperty(specifier.imported || this.createIdentifier('default'), specifier.local); 625 | }); 626 | const objPattern = this.createObjectPattern(objPatternProperties); 627 | const varDeclarator = this.createVariableDeclarator(objPattern, varNsDeclarator || yieldExpression); 628 | const varDeclaration = this.createVariableDeclaration('const', varNsDeclarator ? [varNsDeclarator, varDeclarator] : [varDeclarator]); 629 | Object.assign(node, varDeclaration); 630 | } else { 631 | Object.assign(node, this.createExpressionStatement(yieldExpression)); 632 | } 633 | } 634 | 635 | // 转换动态import表达式 636 | transformImportExpression(node) { 637 | Object.assign(node, this.createImportCallExpression(node.source, true)); 638 | } 639 | 640 | // 转换默认export声明 641 | transformExportDefaultDeclaration(node) { 642 | const exportIdentifier = this.createIdentifier(EXPORT_OBJECT_NAME); 643 | const declaration = node.declaration; 644 | switch (declaration.type) { 645 | case 'FunctionDeclaration': 646 | case 'ClassDeclaration': 647 | case 'VariableDeclaration': { 648 | if (declaration.id) { 649 | const exportAssignmentExpression = this.createExportAssginmentExpression(exportIdentifier, this.createIdentifier('default'), declaration.id); 650 | const exportExprStatment = this.createExpressionStatement(exportAssignmentExpression); 651 | this.exportDeclarationNodeList.push(exportExprStatment); 652 | Object.assign(node, declaration); 653 | break; 654 | } 655 | } 656 | default: { 657 | const exportAssignmentExpression = this.createExportAssginmentExpression(exportIdentifier, this.createIdentifier('default'), declaration); 658 | const exportExprStatment = this.createExpressionStatement(exportAssignmentExpression); 659 | Object.assign(node, exportExprStatment); 660 | } 661 | } 662 | } 663 | 664 | // 转换具名export声明 665 | transformExportNamedDeclaration(node) { 666 | const exportIdentifier = this.createIdentifier(EXPORT_OBJECT_NAME); 667 | if (node.specifiers?.length) { 668 | if (node.source) { 669 | const tmpImportExpression = this.createImportCallExpression(node.source); 670 | const tmpYieldExprression = this.createYieldExpression(tmpImportExpression, false); 671 | const tmpExportsIdentifier = this.createIdentifier(TMP_VARIABLE_NAME); 672 | const tmpExportsMemberExpr = this.createMemberExpression(exportIdentifier, tmpExportsIdentifier); 673 | const tmpExportsAssignmentExpr = [this.createExportAssginmentExpression(exportIdentifier, tmpExportsIdentifier, tmpYieldExprression)]; 674 | const exportAssignmentExprs = node.specifiers.map((specifier) => { 675 | return this.createExportAssginmentExpression(exportIdentifier, specifier.exported, this.createMemberExpression(tmpExportsMemberExpr, specifier.local)); 676 | }); 677 | const exportSeqExpression = this.createSequenceExpression(tmpExportsAssignmentExpr.concat(exportAssignmentExprs)); 678 | const exportExprStatment = this.createExpressionStatement(exportSeqExpression); 679 | Object.assign(node, exportExprStatment); 680 | } else { 681 | const exportAssignmentExprs = node.specifiers.map((specifier) => { 682 | return this.createExportAssginmentExpression(exportIdentifier, specifier.exported, specifier.local); 683 | }); 684 | const exportSeqExpression = this.createSequenceExpression(exportAssignmentExprs); 685 | const exportExprStatment = this.createExpressionStatement(exportSeqExpression); 686 | Object.assign(node, exportExprStatment); 687 | } 688 | } else if (node.declaration) { 689 | const declaration = node.declaration; 690 | switch (declaration.type) { 691 | case 'FunctionDeclaration': 692 | case 'ClassDeclaration': { 693 | const exportNameIdentifier = declaration.id; 694 | const exportAssignmentExpression = this.createExportAssginmentExpression(exportIdentifier, exportNameIdentifier, exportNameIdentifier); 695 | const exportExprStatment = this.createExpressionStatement(exportAssignmentExpression); 696 | this.exportDeclarationNodeList.push(exportExprStatment); 697 | Object.assign(node, declaration); 698 | break; 699 | } 700 | case 'VariableDeclaration': { 701 | const exportAssignmentExprs = declaration.declarations.map((varDeclarator) => { 702 | switch (varDeclarator.id.type) { 703 | case 'Identifier': return this.createExportAssginmentExpression(exportIdentifier, varDeclarator.id, varDeclarator.id); 704 | case 'ObjectPattern': { 705 | const properties = varDeclarator.id.properties; 706 | const tmpExportsIdentifier = this.createIdentifier(TMP_VARIABLE_NAME); 707 | const tmpExportsMemberExpr = this.createMemberExpression(exportIdentifier, tmpExportsIdentifier); 708 | const tmpExportsAssignmentExpr = [this.createExportAssginmentExpression(exportIdentifier, tmpExportsIdentifier, varDeclarator.id)]; 709 | const exportsAssignmentExprs = properties.map((property) => { 710 | return this.createExportAssginmentExpression(exportIdentifier, property.value, this.createMemberExpression(tmpExportsMemberExpr, property.key)); 711 | }); 712 | return tmpExportsAssignmentExpr.concat(exportsAssignmentExprs); 713 | } 714 | default: throw new SyntaxError('Unexpected token of named export'); 715 | } 716 | }); 717 | const exportSeqExpression = this.createSequenceExpression([].concat(...exportAssignmentExprs)); 718 | const exportExprStatment = this.createExpressionStatement(exportSeqExpression); 719 | this.exportDeclarationNodeList.push(exportExprStatment); 720 | Object.assign(node, declaration); 721 | break; 722 | } 723 | default: throw new SyntaxError('Unexpected token of named export'); 724 | } 725 | } 726 | } 727 | 728 | // 转换全导出export声明 729 | transformExportAllDeclaration(node) { 730 | // TODO: export * from 'a.js' / export * as k from 'a.js' 731 | throw new SyntaxError('Unexpected export all declaration'); 732 | } 733 | 734 | // 转换import.meta 735 | transformMetaProperty(node) { 736 | if (node.meta?.name === 'import' && node.property?.name === 'meta') { 737 | const importMetaIdentifier = this.createIdentifier(IMPORT_META_NAME); 738 | Object.assign(node, importMetaIdentifier); 739 | } else if (node.meta?.name === 'new' && node.property?.name === 'target') { 740 | const newTargetIdentifier = this.createIdentifier(NEW_TARGET_NAME); 741 | Object.assign(node, newTargetIdentifier); 742 | } 743 | } 744 | 745 | // 处理全局信息,包括提升import语句到开头、在开头创建exports对象、在最好返回exports对象 746 | transformGlobalInfo(node) { 747 | if (this.importNodeList.length) { 748 | for (let i = 0; i < this.importNodeList.length; i++) { 749 | const importNode = this.importNodeList[i]; 750 | const importIndex = node.body.indexOf(importNode); 751 | node.body.splice(importIndex, 1); 752 | } 753 | node.body = this.importNodeList.concat(node.body); 754 | node.body = [this.createModuleRequestStatement()].concat(node.body); 755 | } 756 | if (this.exportDeclarationNodeList.length) { 757 | node.body = node.body.concat(this.exportDeclarationNodeList); 758 | } 759 | const debuggerIdIdentifier = this.createIdentifier(DEBUGGER_ID_NAME); 760 | const importMetaIdentifier = this.createIdentifier(IMPORT_META_NAME); 761 | const exportObjIdentifier = this.createIdentifier(EXPORT_OBJECT_NAME); 762 | node.body.unshift(this.createVariableDeclaration('let', [ 763 | this.createVariableDeclarator(debuggerIdIdentifier, this.createLiteral(this.debuggerId)), 764 | this.createVariableDeclarator( 765 | importMetaIdentifier, 766 | this.createObjectExpression([ 767 | this.createProperty( 768 | this.createIdentifier('url'), 769 | debuggerIdIdentifier 770 | ) 771 | ]) 772 | ), 773 | this.createVariableDeclarator(exportObjIdentifier, this.createObjectExpression()), 774 | ...this.createScopeVariableDeclarators() 775 | ])); 776 | node.body.push(this.createReturnStatement(exportObjIdentifier)); 777 | } 778 | 779 | // 创建字面量AST节点 780 | createLiteral(value) { 781 | return { type: 'Literal', value, raw: JSON.stringify(value) }; 782 | } 783 | 784 | // 创建标识符AST节点 785 | createIdentifier(name) { 786 | return { type: 'Identifier', name }; 787 | } 788 | 789 | // 创建块级语句AST节点 790 | createBlockStatement(body = []) { 791 | return { type: 'BlockStatement', body }; 792 | } 793 | 794 | // 创建generator函数表达式AST节点 795 | createFunctionExpression(body, params = [], type = 'FunctionExpression', generator = true, async = false) { 796 | return { type, id: null, expression: body.type !== 'BlockStatement', generator, async, params, body }; 797 | } 798 | 799 | // 创建this表达式AST节点 800 | createThisExpression() { 801 | return { type: 'ThisExpression' }; 802 | } 803 | 804 | // 创建函数调用表达式AST节点 805 | createCallExpression(callee, args = []) { 806 | return { type: 'CallExpression', callee, arguments: args }; 807 | } 808 | 809 | // 创建yield表达式AST节点 810 | createYieldExpression(argument, delegate = true) { 811 | return { type: 'YieldExpression', argument, delegate }; 812 | } 813 | 814 | // 创建变量声明AST节点 815 | createVariableDeclaration(kind, declarations) { 816 | return { type: 'VariableDeclaration', kind, declarations }; 817 | } 818 | 819 | // 创建变量声明项AST节点 820 | createVariableDeclarator(id, init) { 821 | return { type: 'VariableDeclarator', id, init }; 822 | } 823 | 824 | // 创建对象解构AST节点 825 | createObjectPattern(properties) { 826 | return { type: 'ObjectPattern', properties }; 827 | } 828 | 829 | // 创建对象属性AST节点 830 | createProperty(key, value) { 831 | return { type: 'Property', shorthand: false, computed: false, method: false, kind: 'init', key, value }; 832 | } 833 | 834 | // 创建赋值表达式AST节点 835 | createAssignmentExpression(left, right) { 836 | return { type: 'AssignmentExpression', operator: '=', left, right }; 837 | } 838 | 839 | // 创建对象成员AST节点 840 | createMemberExpression(object, property, optional = false) { 841 | return { type: 'MemberExpression', computed: false, optional, object, property }; 842 | } 843 | 844 | // 创建可选链表达式AST节点 845 | createChainExpression(expression) { 846 | return { type: 'ChainExpression', expression }; 847 | } 848 | 849 | // 创建序列表达式AST节点 850 | createSequenceExpression(expressions) { 851 | return { type: 'SequenceExpression', expressions }; 852 | } 853 | 854 | // 创建表达式语句AST节点 855 | createExpressionStatement(expression) { 856 | return { type: 'ExpressionStatement', expression }; 857 | } 858 | 859 | // 创建对象表达式AST节点 860 | createObjectExpression(properties = []) { 861 | return { type: 'ObjectExpression', properties }; 862 | } 863 | 864 | // 创建数组表达式AST节点 865 | createArrayExpression(elements = []) { 866 | return { type: 'ArrayExpression', elements }; 867 | } 868 | 869 | // 创建返回语句AST节点 870 | createReturnStatement(argument) { 871 | return { type: 'ReturnStatement', argument }; 872 | } 873 | 874 | // 创建三元表达式AST节点 875 | createConditionalExpression(test, consequent, alternate) { 876 | return { type: 'ConditionalExpression', test, consequent, alternate }; 877 | } 878 | 879 | // 创建二元表达式AST节点 880 | createBinaryExpression(left, operator, right) { 881 | return { type: 'BinaryExpression', left, operator, right }; 882 | } 883 | 884 | // 创建一元表达式AST节点 885 | createUnaryExpression(argument, operator, prefix = true) { 886 | return { type: 'UnaryExpression', argument, operator, prefix }; 887 | } 888 | 889 | // 创建逻辑表达式AST节点 890 | createLogicalExpression(left, operator, right) { 891 | return { type: 'LogicalExpression', left, operator, right }; 892 | } 893 | 894 | // 创建meta属性AST节点,如new.target 895 | createMetaProperty(meta, property) { 896 | return { type: 'MetaProperty', meta, property }; 897 | } 898 | 899 | // 创建try catch语句AST节点 900 | createTryStatement(block, handler, finalizer) { 901 | return { type: 'TryStatement', block, handler, finalizer }; 902 | } 903 | 904 | // 创建类方法定义AST节点 905 | createMethodDefinition(kind, key, value) { 906 | return { type: 'MethodDefinition', kind, key, value, static: false, computed: false }; 907 | } 908 | 909 | // 创建super节点 910 | createSuper() { 911 | return { type: 'Super' }; 912 | } 913 | 914 | // 创建剩余参数AST节点 915 | createRestElement(argument) { 916 | return { type: 'RestElement', argument }; 917 | } 918 | 919 | // 创建展开参数AST节点 920 | createSpreadElement(argument) { 921 | return { type: 'SpreadElement', argument }; 922 | } 923 | } 924 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const eventBus = new Map(); 2 | 3 | export function addEventListener(event, listener) { 4 | if (typeof event !== 'string' || typeof listener !== 'function') { 5 | return false; 6 | } 7 | if (!eventBus.get(event)) { 8 | eventBus.set(event, []); 9 | } 10 | const bus = eventBus.get(event); 11 | bus.push(listener); 12 | return true; 13 | } 14 | 15 | export function removeEventListener(event, listener) { 16 | if (typeof event !== 'string' || typeof listener !== 'function' || !eventBus.get(event)) { 17 | return false; 18 | } 19 | const bus = eventBus.get(event); 20 | const idx = bus.indexOf(listener); 21 | if (idx === -1) { 22 | return false; 23 | } 24 | bus.splice(idx, 1); 25 | return true; 26 | } 27 | 28 | export function emitEventListener(event, ...args) { 29 | if (typeof event !== 'string' || !eventBus.get(event)) { 30 | return false; 31 | } 32 | const bus = eventBus.get(event); 33 | bus.forEach((listener) => listener(...args)); 34 | return true; 35 | } 36 | 37 | export function emptyYield() { 38 | return { value: undefined, done: false }; 39 | } 40 | 41 | export function getPropertyDescriptor(obj, key) { 42 | let dptor; 43 | let proto = obj; 44 | while (proto && !(dptor = Object.getOwnPropertyDescriptor(proto, key))) { 45 | proto = proto.__proto__; 46 | } 47 | return dptor; 48 | } 49 | 50 | export function getImportUrl(url, base) { 51 | try { 52 | const absURL = new URL(url, base); 53 | return absURL.href; 54 | } catch (err) { 55 | throw new Error(`Failed to parse the import url from '${url}' based on '${base}'`); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/api.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import vDebugger from '../src'; 6 | 7 | describe('api tests', () => { 8 | it('debug params check normally', () => { 9 | expect(vDebugger.debug(1)).toBeFalsy(); // 脚本非字符串 10 | expect(vDebugger.debug('1', 1)).toBeFalsy(); // debuggerId非字符串 11 | }); 12 | 13 | it('transform params check normally', () => { 14 | expect(vDebugger.transform(1)).toBeFalsy(); // 脚本非字符串 15 | expect(vDebugger.transform('1', 1)).toBeFalsy(); // debuggerId非字符串 16 | 17 | const transformed = vDebugger.transform('1', '1'); 18 | expect(vDebugger.transform(transformed)).toBe(transformed); // 如果传入转换后的代码,则直接返回相同的 19 | }); 20 | 21 | it('resume params check normally', () => { 22 | expect(vDebugger.resume(1)).toBeFalsy(); // type非字符串 23 | expect(vDebugger.resume('error')).toBeFalsy(); // type非指定值 24 | }); 25 | 26 | it('evaluate params check normally', () => { 27 | expect(vDebugger.evaluate(1)).toBeFalsy(); // expression非字符串 28 | }); 29 | 30 | it('set breakpoints params check normally', () => { 31 | expect(vDebugger.setBreakpoint(1)).toBeFalsy(); // debuggerId非字符串 32 | expect(vDebugger.setBreakpoint('1', '1')).toBeFalsy(); // lineNumber非数字 33 | expect(vDebugger.setBreakpoint('1', 1)).toBeFalsy(); // 未能根据debuggerId找到脚本 34 | }); 35 | 36 | it('remove breakpoint params check normally', () => { 37 | expect(vDebugger.removeBreakpoint('1')).toBeFalsy(); // breakpointId非数字 38 | }); 39 | 40 | it('run in native env params check normally', () => { 41 | expect(vDebugger.runInNativeEnv(1)).toBeFalsy(); // callback非函数 42 | expect(vDebugger.runInNativeEnv(() => exception)).toBeFalsy(); // callback异常 43 | }); 44 | 45 | it('run in skip over params check normally', () => { 46 | expect(vDebugger.runInSkipOver(1)).toBeFalsy(); // callback非函数 47 | expect(vDebugger.runInSkipOver(() => exception)).toBeFalsy(); // callback异常 48 | }); 49 | 50 | it('set module request params check normally', () => { 51 | expect(vDebugger.setModuleRequest(1)).toBeFalsy(); // request非函数 52 | }); 53 | 54 | it('get possible breakpoints normally', () => { 55 | const debuggerId = 'possible-breakpoints'; 56 | vDebugger.debug('1;2;3;4\n5;6', debuggerId); 57 | const breakpoints = vDebugger.getPossibleBreakpoints(debuggerId); 58 | expect(breakpoints.length).toBe(6); 59 | expect(breakpoints[0].lineNumber).toBe(1); 60 | expect(breakpoints[5].lineNumber).toBe(2); 61 | expect(breakpoints[3].columnNumber).toBe(6); 62 | expect(vDebugger.getPossibleBreakpoints(1)).toBeFalsy(); // debuggerId非字符串 63 | expect(vDebugger.getPossibleBreakpoints('1')).toBeFalsy(); // 未能根据debuggerId找到脚本 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/async.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import vDebugger from '../src'; 6 | import { nextTick } from './utils'; 7 | 8 | describe('async function tests', () => { 9 | it('run async function normally', async () => { 10 | const run = vDebugger.debug( 11 | 'async function a() { return 1; }\n' + 12 | 'async function b() { return (await a()) + 1; }\n' + 13 | 'b().then((r) => window.__trans_res__ = r);' 14 | , 'async.js'); 15 | expect(run).toBeTruthy(); 16 | 17 | run(); 18 | 19 | await nextTick(); 20 | expect(window.__trans_res__).toEqual(2); 21 | }); 22 | 23 | it('break async function normally', async () => { 24 | const run = vDebugger.debug( 25 | 'async function c() {\n' + 26 | ' window.__trans_res__ = await 3;\n' + 27 | ' window.__trans_res__ = await 4;\n' + 28 | ' window.__trans_res__ = await 5;\n' + 29 | '}\n' + 30 | 'c();' 31 | , 'async-break.js'); 32 | expect(run).toBeTruthy(); 33 | 34 | const breakLine = 3; 35 | vDebugger.setBreakpoint('async-break.js', breakLine); 36 | const breakLine2 = 4; 37 | vDebugger.setBreakpoint('async-break.js', breakLine2); 38 | 39 | run(); 40 | 41 | await nextTick(); 42 | expect(window.__trans_res__).toEqual(3); 43 | 44 | vDebugger.resume(); 45 | await nextTick(); 46 | expect(window.__trans_res__).toEqual(4); 47 | 48 | vDebugger.resume(); 49 | await nextTick(); 50 | expect(window.__trans_res__).toEqual(5); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/benchmark.js: -------------------------------------------------------------------------------- 1 | const vDebugger = require('../dist/vdebugger'); 2 | const axios = require('axios'); 3 | 4 | const code = ` 5 | for (let i = 0; i < 10000; i++) { 6 | i = i; 7 | } 8 | `; 9 | 10 | console.time('evalTime-for'); 11 | eval(code); 12 | console.timeEnd('evalTime-for'); 13 | 14 | const run = vDebugger.debug(code, 'for'); 15 | console.time('debugTime-for'); 16 | run(); 17 | console.timeEnd('debugTime-for'); 18 | 19 | (async () => { 20 | const res = await axios('https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.development.js'); 21 | 22 | console.time('evalTime-react'); 23 | eval(res.data); 24 | console.timeEnd('evalTime-react'); 25 | 26 | const run = vDebugger.debug(res.data, 'react'); 27 | console.time('debugTime-react'); 28 | run(); 29 | console.timeEnd('debugTime-react'); 30 | })(); -------------------------------------------------------------------------------- /tests/breakpoint.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { nextTick } from './utils'; 6 | import vDebugger from '../src'; 7 | 8 | describe('breakpoint tests', () => { 9 | let errorEventRes = null; 10 | let pausedEventRes = null; 11 | let resumedEventRes = true; 12 | const errorListener = (error) => errorEventRes = error; 13 | const resumedListener = () => resumedEventRes = true; 14 | const pausedListener = (pausedInfo) => { 15 | pausedEventRes = pausedInfo; 16 | resumedEventRes = false; 17 | }; 18 | 19 | beforeAll(() => { 20 | expect(vDebugger.addEventListener('error', errorListener)).toBeTruthy(); 21 | expect(vDebugger.addEventListener('resumed', resumedListener)).toBeTruthy(); 22 | expect(vDebugger.addEventListener('paused', pausedListener)).toBeTruthy(); 23 | expect(vDebugger.addEventListener()).toBeFalsy(); // 试一下传参不合法时是否有进行保护 24 | }); 25 | 26 | afterAll(() => { 27 | expect(vDebugger.removeEventListener('error', errorListener)).toBeTruthy(); 28 | expect(vDebugger.removeEventListener('resumed', resumedListener)).toBeTruthy(); 29 | expect(vDebugger.removeEventListener('paused', pausedListener)).toBeTruthy(); 30 | expect(vDebugger.removeEventListener('paused', pausedListener)).toBeFalsy(); // 试一下重复移除是否有进行保护 31 | expect(vDebugger.removeEventListener()).toBeFalsy(); // 试一下传参不合法时是否有进行保护 32 | }); 33 | 34 | it('transform normally', () => { 35 | const run = vDebugger.debug('window.__trans_res__ = 1;'); 36 | expect(run).toBeTruthy(); 37 | 38 | run(); 39 | expect(window.__trans_res__).toEqual(1); 40 | }); 41 | 42 | it('break normally', () => { 43 | const run = vDebugger.debug( 44 | 'window.__trans_res__ = 2;\n' + 45 | 'window.__trans_res__ = 3;\n' + 46 | 'window.__trans_res__ = 4;\n' + 47 | 'window.__trans_res__ = 5;' 48 | , 'breakpoint.js'); 49 | expect(run).toBeTruthy(); 50 | expect(resumedEventRes).toBeTruthy(); 51 | 52 | const breakLine = 2; 53 | vDebugger.setBreakpoint('breakpoint.js', breakLine); 54 | 55 | run(); 56 | const pausedInfo = vDebugger.getPausedInfo(); 57 | expect(pausedInfo).toBeTruthy(); 58 | expect(pausedInfo).toBe(pausedEventRes); 59 | expect(pausedInfo.lineNumber).toEqual(breakLine); 60 | expect(resumedEventRes).toBeFalsy(); 61 | expect(window.__trans_res__).toEqual(2); 62 | 63 | const resumeRes = vDebugger.resume('stepOver'); 64 | expect(resumeRes).toBeTruthy(); 65 | const pausedInfo2 = vDebugger.getPausedInfo(); 66 | expect(pausedInfo2).toBeTruthy(); 67 | expect(pausedInfo2.lineNumber).toEqual(breakLine + 1); 68 | expect(window.__trans_res__).toEqual(3); 69 | 70 | const resumeRes2 = vDebugger.resume(); 71 | expect(resumeRes2).toBeTruthy(); 72 | expect(resumedEventRes).toBeTruthy(); 73 | const pausedInfo3 = vDebugger.getPausedInfo(); 74 | expect(pausedInfo3).toBeFalsy(); 75 | expect(window.__trans_res__).toEqual(5); 76 | }); 77 | 78 | it('break normally if non-sandbox calls', async () => { 79 | window.call = function (c) { c() }; 80 | const run = vDebugger.debug( 81 | 'window.__trans_res__ = 6;\n' + 82 | 'window.call(() => {\n' + 83 | ' window.__trans_res__ = 7;\n' + 84 | '});\n' + 85 | 'window.__trans_res__ = 8;' 86 | , 'non-sandbox.js'); 87 | expect(run).toBeTruthy(); 88 | 89 | const breakLine = 3; 90 | vDebugger.setBreakpoint('non-sandbox.js', breakLine); 91 | 92 | run(); 93 | const pausedInfo = vDebugger.getPausedInfo(); 94 | expect(pausedInfo).toBeTruthy(); 95 | expect(pausedInfo.lineNumber).toEqual(breakLine); 96 | expect(pausedInfo.scopeChain.length).toEqual(2); 97 | expect(pausedInfo.scopeChain[1].callFrame).toBeTruthy(); 98 | expect(pausedInfo.scopeChain[1].callFrame.lineNumber).toEqual(breakLine); 99 | expect(window.__trans_res__).toEqual(6); 100 | 101 | const resumeRes = vDebugger.resume(); 102 | expect(resumeRes).toBeTruthy(); 103 | await nextTick(); 104 | expect(window.__trans_res__).toEqual(8); 105 | }); 106 | 107 | it('remove breakpoint normally', () => { 108 | const run = vDebugger.debug( 109 | 'window.__trans_res__ = 9;\n' + 110 | 'window.__trans_res__ = 10;\n' + 111 | 'window.__trans_res__ = 11;' 112 | , 'remove.js'); 113 | expect(run).toBeTruthy(); 114 | 115 | const breakLine = 2; 116 | vDebugger.setBreakpoint('remove.js', breakLine); 117 | const removeBreakpoint = vDebugger.setBreakpoint('remove.js', 3); 118 | 119 | run(); 120 | const pausedInfo = vDebugger.getPausedInfo(); 121 | expect(pausedInfo).toBeTruthy(); 122 | expect(pausedInfo.lineNumber).toEqual(breakLine); 123 | expect(window.__trans_res__).toEqual(9); 124 | 125 | vDebugger.removeBreakpoint(removeBreakpoint.id); 126 | 127 | const resumeRes = vDebugger.resume(); 128 | expect(resumeRes).toBeTruthy(); 129 | expect(window.__trans_res__).toEqual(11); 130 | }); 131 | 132 | it('evaluate normally', () => { 133 | const run = vDebugger.debug( 134 | 'const a = 999;\n' + 135 | 'window.__trans_res__ = 12;' 136 | , 'evaluate.js'); 137 | expect(run).toBeTruthy(); 138 | 139 | const breakLine = 2; 140 | vDebugger.setBreakpoint('evaluate.js', breakLine); 141 | 142 | run(); 143 | const pausedInfo = vDebugger.getPausedInfo(); 144 | expect(pausedInfo).toBeTruthy(); 145 | expect(pausedInfo.lineNumber).toEqual(breakLine); 146 | 147 | const value = vDebugger.evaluate('a'); 148 | expect(value).toEqual(999); 149 | 150 | const resumeRes = vDebugger.resume(); 151 | expect(resumeRes).toBeTruthy(); 152 | expect(window.__trans_res__).toEqual(12); 153 | }); 154 | 155 | it('pause execution normally', () => { 156 | const run = vDebugger.debug('window.__trans_res__ = 13;', 'pause.js'); 157 | expect(run).toBeTruthy(); 158 | 159 | vDebugger.setExecutionPause(true); 160 | 161 | run(); 162 | const pausedInfo = vDebugger.getPausedInfo(); 163 | expect(pausedInfo).toBeTruthy(); 164 | expect(pausedInfo.lineNumber).toEqual(1); 165 | expect(window.__trans_res__).toEqual(12); 166 | 167 | const resumeRes = vDebugger.resume(); 168 | expect(resumeRes).toBeTruthy(); 169 | expect(window.__trans_res__).toEqual(13); 170 | }); 171 | 172 | it('inactive breakpoint normally', () => { 173 | const run = vDebugger.debug('window.__trans_res__ = 14;', 'inactive.js'); 174 | expect(run).toBeTruthy(); 175 | 176 | const breakLine = 1; 177 | vDebugger.setBreakpoint('inactive.js', breakLine); 178 | vDebugger.setBreakpointsActive(false); 179 | 180 | run(); 181 | const pausedInfo = vDebugger.getPausedInfo(); 182 | expect(pausedInfo).toBeFalsy(); 183 | expect(window.__trans_res__).toEqual(14); 184 | vDebugger.setBreakpointsActive(true); 185 | }); 186 | 187 | it('run in native env normally', () => { 188 | window.native = () => { 189 | vDebugger.runInNativeEnv(() => { 190 | window.__trans_res__ = Array.from([15])[0]; 191 | }); 192 | }; 193 | const run = vDebugger.debug( 194 | 'const ori = Array.from;\n' + 195 | 'Array.from = () => [5];\n' + 196 | 'window.native();\n' + 197 | 'Array.from = ori;\n' 198 | , 'run-native.js'); 199 | expect(run).toBeTruthy(); 200 | 201 | run(); 202 | expect(window.__trans_res__).toEqual(15); 203 | }); 204 | 205 | it('run in skip over normally', async () => { 206 | const run = vDebugger.debug( 207 | 'function a() {\n' + 208 | 'return 16;\n' + 209 | '}\n' + 210 | 'window.__trans_res__ = a();' 211 | , 'run-skip.js'); 212 | expect(run).toBeTruthy(); 213 | 214 | const breakLine = 4; 215 | const fnBreakLine = 2; 216 | vDebugger.setBreakpoint('run-skip.js', breakLine); 217 | vDebugger.setBreakpoint('run-skip.js', fnBreakLine); 218 | 219 | run(); 220 | const pausedInfo = vDebugger.getPausedInfo(); 221 | expect(pausedInfo).toBeTruthy(); 222 | expect(pausedInfo.lineNumber).toEqual(breakLine); 223 | 224 | const scopeChain = vDebugger.getScopeChain(); 225 | const curScope = scopeChain[scopeChain.length - 1]; 226 | 227 | const value = vDebugger.runInSkipOver(() => { 228 | return vDebugger.evaluate('a()', curScope.callFrameId); 229 | }); 230 | expect(value).toEqual(16); 231 | 232 | const resumeRes = vDebugger.resume(); 233 | expect(resumeRes).toBeTruthy(); 234 | expect(window.__trans_res__).toEqual(15); 235 | 236 | const resumeRes2 = vDebugger.resume(); 237 | expect(resumeRes2).toBeTruthy(); 238 | await nextTick(); 239 | expect(window.__trans_res__).toEqual(16); 240 | }); 241 | 242 | it('emit error normally', () => { 243 | const run = vDebugger.debug( 244 | 'window.__trans_res__ = 17;\n' + 245 | ' windows.__trans_res__ = -1;\n' + // 前面故意空两格,看能不能正确定位到列 246 | 'window.__trans_res__ = 18;' 247 | , 'error.js'); 248 | expect(run).toBeTruthy(); 249 | 250 | let tryCatchErr = null; 251 | try { 252 | run(); 253 | } catch (err) { 254 | tryCatchErr = err; 255 | } 256 | 257 | expect(tryCatchErr).toBeTruthy(); 258 | expect(errorEventRes.error).toBeTruthy(); 259 | expect(tryCatchErr.stack).toEqual(errorEventRes.error.stack); 260 | expect(tryCatchErr.stack.indexOf('error.js:2:2')).not.toEqual(-1); // 看看定位到的位置对不对 261 | }); 262 | 263 | it('condition break normally', () => { 264 | const run = vDebugger.debug( 265 | 'window.__trans_res__ = 19;\n' + 266 | 'window.__trans_res__ = 20;\n' + 267 | 'window.__trans_res__ = 21;' 268 | , 'condition.js'); 269 | expect(run).toBeTruthy(); 270 | 271 | const condBreakLine = 2; 272 | const skipBreakLine = 3; 273 | vDebugger.setBreakpoint('condition.js', condBreakLine, 'window.__trans_res__ === 19'); 274 | vDebugger.setBreakpoint('condition.js', skipBreakLine, 'window.__trans_res__ === 19'); 275 | 276 | run(); 277 | const pausedInfo = vDebugger.getPausedInfo(); 278 | expect(pausedInfo).toBeTruthy(); 279 | expect(pausedInfo.lineNumber).toEqual(condBreakLine); 280 | expect(window.__trans_res__).toEqual(19); 281 | 282 | const resumeRes = vDebugger.resume(); 283 | expect(resumeRes).toBeTruthy(); 284 | const pausedInfo2 = vDebugger.getPausedInfo(); 285 | expect(pausedInfo2).toBeFalsy(); 286 | expect(window.__trans_res__).toEqual(21); 287 | }); 288 | 289 | it('step resume normally', async () => { 290 | const run = vDebugger.debug( 291 | 'window.__trans_res__ = 22;\n' + 292 | 'function p() {\n' + 293 | ' window.__trans_res__ += 2;\n' + 294 | ' window.__trans_res__--;\n' + 295 | '}\n' + 296 | 'p();\n' + // 第6行,直接断在这里,再逐步调试 297 | 'p();\n' + 298 | 'p();' 299 | , 'step.js'); 300 | expect(run).toBeTruthy(); 301 | 302 | const breakLine = 6; 303 | vDebugger.setBreakpoint('step.js', breakLine); 304 | 305 | run(); 306 | const pausedInfo = vDebugger.getPausedInfo(); 307 | expect(pausedInfo).toBeTruthy(); 308 | expect(pausedInfo.lineNumber).toEqual(breakLine); 309 | expect(window.__trans_res__).toEqual(22); 310 | 311 | const resumeRes = vDebugger.resume('stepInto'); 312 | expect(resumeRes).toBeTruthy(); 313 | const pausedInfo2 = vDebugger.getPausedInfo(); 314 | expect(pausedInfo2).toBeTruthy(); 315 | expect(pausedInfo2.lineNumber).toEqual(3); 316 | 317 | const resumeRes2 = vDebugger.resume('stepOut'); 318 | expect(resumeRes2).toBeTruthy(); 319 | await nextTick(); 320 | const pausedInfo3 = vDebugger.getPausedInfo(); 321 | expect(pausedInfo3).toBeTruthy(); 322 | expect(pausedInfo3.lineNumber).toEqual(7); 323 | expect(window.__trans_res__).toEqual(23); 324 | 325 | const resumeRes3 = vDebugger.resume('stepOver'); 326 | expect(resumeRes3).toBeTruthy(); 327 | const pausedInfo4 = vDebugger.getPausedInfo(); 328 | expect(pausedInfo4).toBeTruthy(); 329 | expect(pausedInfo4.lineNumber).toEqual(8); 330 | expect(window.__trans_res__).toEqual(24); 331 | 332 | const resumeRes4 = vDebugger.resume(); 333 | expect(resumeRes4).toBeTruthy(); 334 | const pausedInfo5 = vDebugger.getPausedInfo(); 335 | expect(pausedInfo5).toBeFalsy(); 336 | expect(window.__trans_res__).toEqual(25); 337 | }); 338 | 339 | it('block execution if paused normally', async () => { 340 | const run = vDebugger.debug( 341 | 'window.__trans_res__ = 26;\n' + 342 | 'window.__trans_res__ = 27;\n' + 343 | 'setTimeout(() => window.__trans_res__ = 29);\n' 344 | , 'block.js'); 345 | expect(run).toBeTruthy(); 346 | 347 | vDebugger.setBreakpoint('block.js', 2); 348 | 349 | run(); 350 | const pausedInfo = vDebugger.getPausedInfo(); 351 | expect(pausedInfo).toBeTruthy(); 352 | expect(pausedInfo.lineNumber).toEqual(2); 353 | expect(window.__trans_res__).toEqual(26); 354 | 355 | const run2 = vDebugger.debug('window.__trans_res__ = 28;', 'other.js'); 356 | expect(run2).toBeTruthy(); 357 | 358 | run2(); 359 | const pausedInfo2 = vDebugger.getPausedInfo(); 360 | expect(pausedInfo2).toBeTruthy(); 361 | expect(pausedInfo2.lineNumber).toEqual(2); 362 | expect(window.__trans_res__).toEqual(26); 363 | 364 | vDebugger.resume(); 365 | await nextTick(); 366 | 367 | expect(window.__trans_res__).toEqual(29); // 会先赋值28,因为是同步的,再回去setTimeout赋值29,因为是异步的 368 | }); 369 | 370 | it('break column normally', () => { 371 | const run = vDebugger.debug( 372 | 'window.__trans_res__ = 30; window.__trans_res__ = 31; window.__trans_res__ = 32;' 373 | , 'breakpoint-column.js'); 374 | expect(run).toBeTruthy(); 375 | expect(resumedEventRes).toBeTruthy(); 376 | 377 | const breakColumn = 27; 378 | vDebugger.setBreakpoint('breakpoint-column.js', 1, breakColumn); 379 | 380 | run(); 381 | const pausedInfo = vDebugger.getPausedInfo(); 382 | expect(pausedInfo.columnNumber).toEqual(breakColumn); 383 | expect(window.__trans_res__).toEqual(30); 384 | 385 | vDebugger.resume(); 386 | expect(window.__trans_res__).toEqual(32); 387 | }); 388 | 389 | it('pause exception normally', async () => { 390 | const run = vDebugger.debug( 391 | 'window.__trans_res__ = 99999;\n' + 392 | 'window.__trans_res__ = exception;\n' + 393 | 'window.__trans_res__ = 100000;\n' 394 | , 'exception.js'); 395 | expect(run).toBeTruthy(); 396 | 397 | vDebugger.setExceptionPause(true); 398 | 399 | run(); 400 | const pausedInfo = vDebugger.getPausedInfo(); 401 | expect(pausedInfo).toBeTruthy(); 402 | expect(pausedInfo.lineNumber).toEqual(2); 403 | expect(pausedInfo.reason).toEqual('exception'); 404 | expect(pausedInfo.data).toBeInstanceOf(ReferenceError); 405 | expect(window.__trans_res__).toEqual(99999); 406 | }); 407 | 408 | // 当前测试到此为止,因为最后一个测试用例是用于测试异常中断的,不会再恢复了 409 | }); 410 | -------------------------------------------------------------------------------- /tests/class.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import vDebugger from '../src'; 6 | 7 | describe('class tests', () => { 8 | beforeAll(() => { 9 | const run = vDebugger.debug( 10 | 'export function A() { this.v = 1 }\n' + 11 | 'export class B { constructor() { this.v = 1 } }\n' + 12 | 'export function C() { window.t = new.target; this.v = 2; return { v: 3 } }\n' + 13 | 'export function D() { this.v = 2; return 3 }' 14 | , 'https://class.test/class.js'); 15 | run(); 16 | }); 17 | 18 | it('new function normally', () => { 19 | const run = vDebugger.debug( 20 | 'import { A } from "class.js";\n' + 21 | 'const a = new A();\n' + 22 | 'window.a = a.v;' 23 | , 'https://class.test/new-function.js'); 24 | expect(run).toBeTruthy(); 25 | run(); 26 | expect(window.a).toBe(1); 27 | }); 28 | 29 | it('new class normally', () => { 30 | const run = vDebugger.debug( 31 | 'import { B } from "class.js";\n' + 32 | 'const b = new B();\n' + 33 | 'window.b = b.v;' 34 | , 'https://class.test/new-class.js'); 35 | expect(run).toBeTruthy(); 36 | run(); 37 | expect(window.b).toBe(1); 38 | }); 39 | 40 | it('reflect construct function normally', () => { 41 | const run = vDebugger.debug( 42 | 'import { A } from "class.js";\n' + 43 | 'const fa = Reflect.construct(A, []);\n' + 44 | 'window.fa = fa.v;' 45 | , 'https://class.test/reflect-function.js'); 46 | expect(run).toBeTruthy(); 47 | run(); 48 | expect(window.fa).toBe(1); 49 | }); 50 | 51 | it('reflect construct class normally', () => { 52 | const run = vDebugger.debug( 53 | 'import { B } from "class.js";\n' + 54 | 'const fb = Reflect.construct(B, []);\n' + 55 | 'window.fb = fb.v;' 56 | , 'https://class.test/reflect-class.js'); 57 | expect(run).toBeTruthy(); 58 | run(); 59 | expect(window.fb).toBe(1); 60 | }); 61 | 62 | it('get target from new normally', () => { 63 | const run = vDebugger.debug( 64 | 'import { C } from "class.js";\n' + 65 | 'window.C = C;\n' + 66 | 'new C();' 67 | , 'https://class.test/get-new-ori-target.js'); 68 | expect(run).toBeTruthy(); 69 | run(); 70 | expect(window.t).toBe(window.C); 71 | }); 72 | 73 | it('get target from reflect construct normally', () => { 74 | const run = vDebugger.debug( 75 | 'import { C } from "class.js";\n' + 76 | 'window.C = C;\n' + 77 | 'Reflect.construct(C, []);' 78 | , 'https://class.test/get-reflect-ori-target.js'); 79 | expect(run).toBeTruthy(); 80 | run(); 81 | expect(window.t).toBe(window.C); 82 | 83 | const run2 = vDebugger.debug( 84 | 'import { B, C } from "class.js";\n' + 85 | 'window.B = B;\n' + 86 | 'Reflect.construct(C, [], B);' 87 | , 'https://class.test/get-reflect-new-target.js'); 88 | expect(run2).toBeTruthy(); 89 | run2(); 90 | expect(window.t).toBe(window.B); 91 | }); 92 | 93 | it('get return normally', () => { 94 | const run = vDebugger.debug( 95 | 'import { C } from "class.js";\n' + 96 | 'const rc = new C();\n' + 97 | 'window.rc = rc.v;' 98 | , 'https://class.test/get-return.js'); 99 | expect(run).toBeTruthy(); 100 | run(); 101 | expect(window.rc).toBe(3); 102 | 103 | const run2 = vDebugger.debug( 104 | 'import { D } from "class.js";\n' + 105 | 'const rd = new D();\n' + 106 | 'window.rd = rd.v;' 107 | , 'https://class.test/get-return.js'); 108 | expect(run2).toBeTruthy(); 109 | run2(); 110 | expect(window.rd).toBe(2); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /tests/descriptor.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import vDebugger from '../src'; 6 | 7 | describe('breakpoint tests', () => { 8 | it('transform normally', () => { 9 | const run = vDebugger.debug( 10 | 'Object.defineProperty(window, "__setter__", { set() { window.__trans_res__ = 1 } });\n' + 11 | 'window.__trans_res__ = window.__setter__ = 2;' 12 | ); 13 | expect(run).toBeTruthy(); 14 | 15 | run(); 16 | expect(window.__trans_res__).toEqual(2); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/module.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import vDebugger from '../src'; 6 | import axios from 'axios'; 7 | 8 | describe('module tests', () => { 9 | beforeAll(() => { 10 | vDebugger.setModuleRequest((url) => axios(url).then((res) => res.data)); 11 | }); 12 | 13 | it('import normally', (done) => { 14 | window.importTestExpect = expect; 15 | window.importTestDone = done; 16 | const run = vDebugger.debug( 17 | 'import { nextTick } from "./npm/vue@2.7.8/dist/vue.esm.browser.min.js";\n' + 18 | 'window.importTestExpect(nextTick).toBeTruthy();\n' + 19 | 'window.importTestDone();' 20 | , 'https://cdn.jsdelivr.net/index.js'); 21 | expect(run).toBeTruthy(); 22 | run(); 23 | }); 24 | 25 | it('export normally', () => { 26 | const run = vDebugger.debug( 27 | 'const a = 1;\n' + 28 | 'const d = 3;\n' + 29 | 'export const b = 2;\n' + 30 | 'export { a, d as c }\n' + 31 | 'export function e(f) { return f };\n' + 32 | 'export class g { constructor(h) { this.h = h } };\n' + 33 | 'export const { i } = { i: 6 };\n' + 34 | 'export default function j(k) { return k };' 35 | , 'https://export.test/export.js'); 36 | expect(run).toBeTruthy(); 37 | run(); 38 | 39 | const run2 = vDebugger.debug('export { i } from "./export.js"', 'https://export.test/export2.js'); 40 | expect(run2).toBeTruthy(); 41 | run2(); 42 | 43 | const run3 = vDebugger.debug('window.l = 8', 'https://export.test/export3.js'); 44 | expect(run3).toBeTruthy(); 45 | run3(); 46 | 47 | const entry = 'https://export.test/index.js'; 48 | const run4 = vDebugger.debug( 49 | 'import j, { a, b, c, e, g } from "./export.js";\n' + 50 | 'import { i } from "./export2.js";\n' + 51 | 'import "./export3.js";\n' + 52 | 'window.a = a;\n' + 53 | 'window.b = b;\n' + 54 | 'window.c = c;\n' + 55 | 'window.e = e(4);\n' + 56 | 'window.g = new g(5).h;\n' + 57 | 'window.i = i;\n' + 58 | 'window.j = j(7);\n' + 59 | 'window.meta = import.meta;' 60 | , entry); 61 | expect(run4).toBeTruthy(); 62 | run4(); 63 | 64 | expect(window.a).toBe(1); 65 | expect(window.b).toBe(2); 66 | expect(window.c).toBe(3); 67 | expect(window.e).toBe(4); 68 | expect(window.g).toBe(5); 69 | expect(window.i).toBe(6); 70 | expect(window.j).toBe(7); 71 | expect(window.l).toBe(8); 72 | expect(window.meta.url).toBe(entry); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/react.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { nextTick } from './utils'; 6 | import vDebugger from '../src'; 7 | import axios from 'axios'; 8 | 9 | describe('react tests', () => { 10 | let reactScript = ''; 11 | let reactDomScript = ''; 12 | 13 | beforeAll(async () => { 14 | const res = await axios('https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.development.js'); 15 | const res2 = await axios('https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.development.js'); 16 | reactScript = res.data; 17 | reactDomScript = res2.data; 18 | document.body.innerHTML = '
'; 19 | }); 20 | 21 | it('transform dependency normally', () => { 22 | const run = vDebugger.debug(reactScript, 'https://react.test/react.js'); 23 | expect(run).toBeTruthy(); 24 | run(); 25 | const run2 = vDebugger.debug(reactDomScript, 'https://react.test/react-dom.js'); 26 | expect(run2).toBeTruthy(); 27 | run2(); 28 | }); 29 | 30 | it('render react app normally', async () => { 31 | const run = vDebugger.debug( 32 | "function App() {\n" + 33 | " const [color, setColor] = React.useState(false);\n" + 34 | " const boxStyle = { height: '100px', border: '1px solid red', background: color ? 'blue' : 'white' };\n" + 35 | " const onClick = () => {\n" + 36 | " setColor(c => !c);\n" + // 第5行,等下断在这里 37 | " };\n" + 38 | " return React.createElement('div', null, React.createElement('div', {\n" + 39 | " id: 'box', className: 'box', style: boxStyle, onClick\n" + 40 | " }));\n" + 41 | "}\n" + 42 | "ReactDOM.createRoot(document.getElementById('app')).render(React.createElement(App));", 'https://react.test/index.js' 43 | ); 44 | expect(run).toBeTruthy(); 45 | run(); 46 | 47 | await nextTick(); // react 18 createRoot 默认开启异步渲染,这里等一下渲染 48 | const box = document.getElementById('box'); 49 | expect(box.style.backgroundColor).toBe('white'); 50 | }); 51 | 52 | it('emit react event normally', async () => { 53 | const box = document.getElementById('box'); 54 | box.click(); 55 | 56 | await nextTick(); 57 | expect(box.style.backgroundColor).toBe('blue'); 58 | }); 59 | 60 | it('break react event handler normally', async () => { 61 | const breakLine = 5; 62 | vDebugger.setBreakpoint('https://react.test/index.js', breakLine); 63 | 64 | const box = document.getElementById('box'); 65 | box.click(); 66 | 67 | await nextTick(); 68 | const pausedInfo = vDebugger.getPausedInfo(); 69 | expect(pausedInfo).toBeTruthy(); 70 | expect(pausedInfo.lineNumber).toEqual(breakLine); 71 | expect(box.style.backgroundColor).toBe('blue'); 72 | 73 | const resumeRes = vDebugger.resume(); 74 | expect(resumeRes).toBeTruthy(); 75 | 76 | await nextTick(); 77 | const pausedInfo2 = vDebugger.getPausedInfo(); 78 | expect(pausedInfo2).toBeFalsy(); 79 | expect(box.style.backgroundColor).toBe('white'); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/sandbox.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { nextTick } from './utils'; 6 | import vDebugger from '../src'; 7 | 8 | describe('class tests', () => { 9 | it('array from normally', async () => { 10 | const run = vDebugger.debug( 11 | 'window.res = [];\n' + 12 | 'window.arr = [1, 2, 3, 4];\n' + 13 | 'window.ret = Array.from(window.arr, (e, i, a) => {\n' + 14 | ' window.res.push([e, i, a]);\n' + 15 | ' return e + 1;\n' + 16 | '});' 17 | , 'https://sandbox.test/array-from.js'); 18 | expect(run).toBeTruthy(); 19 | vDebugger.setBreakpoint('https://sandbox.test/array-from.js', 4); 20 | run(); 21 | for (let i = 0; i < window.arr.length; i++) { 22 | expect(window.res.length).toBe(i); 23 | vDebugger.resume(); 24 | await nextTick(); 25 | } 26 | expect(window.res.length).toBe(window.arr.length); 27 | expect(window.ret).toEqual(Array.from(window.arr, (e) => e + 1)); 28 | for (let i = 0; i < window.arr.length; i++) { 29 | expect(window.res[i][0]).toBe(window.arr[i]); 30 | expect(window.res[i][1]).toBe(i); 31 | expect(window.res[i][2]).toEqual(window.arr); 32 | } 33 | }); 34 | 35 | it('array foreach normally', async () => { 36 | const run = vDebugger.debug( 37 | 'window.res = [];\n' + 38 | 'window.arr = [1, 2, 3, 4];\n' + 39 | 'window.ret = window.arr.forEach((e, i, a) => {\n' + 40 | ' window.res.push([e, i, a]);\n' + 41 | '});' 42 | , 'https://sandbox.test/array-foreach.js'); 43 | expect(run).toBeTruthy(); 44 | vDebugger.setBreakpoint('https://sandbox.test/array-foreach.js', 4); 45 | run(); 46 | for (let i = 0; i < window.arr.length; i++) { 47 | expect(window.res.length).toBe(i); 48 | vDebugger.resume(); 49 | await nextTick(); 50 | } 51 | expect(window.res.length).toBe(window.arr.length); 52 | expect(window.ret).toBeUndefined(); 53 | for (let i = 0; i < window.arr.length; i++) { 54 | expect(window.res[i][0]).toBe(window.arr[i]); 55 | expect(window.res[i][1]).toBe(i); 56 | expect(window.res[i][2]).toEqual(window.arr); 57 | } 58 | }); 59 | 60 | it('array map normally', async () => { 61 | const run = vDebugger.debug( 62 | 'window.res = [];\n' + 63 | 'window.arr = [1, 2, 3, 4];\n' + 64 | 'window.ret = window.arr.map((e, i, a) => {\n' + 65 | ' window.res.push([e, i, a]);\n' + 66 | ' return e + 1;\n' + 67 | '});' 68 | , 'https://sandbox.test/array-map.js'); 69 | expect(run).toBeTruthy(); 70 | vDebugger.setBreakpoint('https://sandbox.test/array-map.js', 4); 71 | run(); 72 | for (let i = 0; i < window.arr.length; i++) { 73 | expect(window.res.length).toBe(i); 74 | vDebugger.resume(); 75 | await nextTick(); 76 | } 77 | expect(window.res.length).toBe(window.arr.length); 78 | expect(window.ret).toEqual(window.arr.map((e) => e + 1)); 79 | for (let i = 0; i < window.arr.length; i++) { 80 | expect(window.res[i][0]).toBe(window.arr[i]); 81 | expect(window.res[i][1]).toBe(i); 82 | expect(window.res[i][2]).toEqual(window.arr); 83 | } 84 | }); 85 | 86 | it('array filter normally', async () => { 87 | const run = vDebugger.debug( 88 | 'window.res = [];\n' + 89 | 'window.arr = [1, 2, 3, 4];\n' + 90 | 'window.ret = window.arr.filter((e, i, a) => {\n' + 91 | ' window.res.push([e, i, a]);\n' + 92 | ' return e < 3;\n' + 93 | '});' 94 | , 'https://sandbox.test/array-filter.js'); 95 | expect(run).toBeTruthy(); 96 | vDebugger.setBreakpoint('https://sandbox.test/array-filter.js', 4); 97 | run(); 98 | for (let i = 0; i < window.arr.length; i++) { 99 | expect(window.res.length).toBe(i); 100 | vDebugger.resume(); 101 | await nextTick(); 102 | } 103 | expect(window.res.length).toBe(window.arr.length); 104 | expect(window.ret).toEqual(window.arr.filter((e) => e < 3)); 105 | for (let i = 0; i < window.arr.length; i++) { 106 | expect(window.res[i][0]).toBe(window.arr[i]); 107 | expect(window.res[i][1]).toBe(i); 108 | expect(window.res[i][2]).toEqual(window.arr); 109 | } 110 | }); 111 | 112 | it('array reduce normally', async () => { 113 | const run = vDebugger.debug( 114 | 'window.res = [];\n' + 115 | 'window.arr = [1, 2, 3, 4];\n' + 116 | 'window.ret = window.arr.reduce((p, e, i, a) => {\n' + 117 | ' window.res.push([e, i, a]);\n' + 118 | ' return p + e;\n' + 119 | '}, 0);' 120 | , 'https://sandbox.test/array-reduce.js'); 121 | expect(run).toBeTruthy(); 122 | vDebugger.setBreakpoint('https://sandbox.test/array-reduce.js', 4); 123 | run(); 124 | for (let i = 0; i < window.arr.length; i++) { 125 | expect(window.res.length).toBe(i); 126 | vDebugger.resume(); 127 | await nextTick(); 128 | } 129 | expect(window.res.length).toBe(window.arr.length); 130 | expect(window.ret).toEqual(window.arr.reduce((p, e) => p + e, 0)); 131 | for (let i = 0; i < window.arr.length; i++) { 132 | expect(window.res[i][0]).toBe(window.arr[i]); 133 | expect(window.res[i][1]).toBe(i); 134 | expect(window.res[i][2]).toEqual(window.arr); 135 | } 136 | }); 137 | 138 | it('array reduceright normally', async () => { 139 | const run = vDebugger.debug( 140 | 'window.res = [];\n' + 141 | 'window.arr = [1, 2, 3, 4];\n' + 142 | 'window.ret = window.arr.reduceRight((p, e, i, a) => {\n' + 143 | ' window.res.push([e, i, a]);\n' + 144 | ' return p + e;\n' + 145 | '}, 0);' 146 | , 'https://sandbox.test/array-reduceright.js'); 147 | expect(run).toBeTruthy(); 148 | vDebugger.setBreakpoint('https://sandbox.test/array-reduceright.js', 4); 149 | run(); 150 | for (let i = 0; i < window.arr.length; i++) { 151 | expect(window.res.length).toBe(i); 152 | vDebugger.resume(); 153 | await nextTick(); 154 | } 155 | expect(window.res.length).toBe(window.arr.length); 156 | expect(window.ret).toEqual(window.arr.reduceRight((p, e) => p + e, 0)); 157 | for (let i = 0; i < window.arr.length; i++) { 158 | expect(window.res[i][0]).toBe(window.arr[window.arr.length - i - 1]); 159 | expect(window.res[i][1]).toBe(window.arr.length - i - 1); 160 | expect(window.res[i][2]).toEqual(window.arr); 161 | } 162 | }); 163 | 164 | it('array every normally', async () => { 165 | const run = vDebugger.debug( 166 | 'window.res = [];\n' + 167 | 'window.arr = [1, 2, 3, 4];\n' + 168 | 'window.ret = window.arr.every((e, i, a) => {\n' + 169 | ' window.res.push([e, i, a]);\n' + 170 | ' return e < 3;\n' + 171 | '}, 0);' 172 | , 'https://sandbox.test/array-every.js'); 173 | expect(run).toBeTruthy(); 174 | vDebugger.setBreakpoint('https://sandbox.test/array-every.js', 4); 175 | run(); 176 | for (let i = 0; i < 3; i++) { 177 | expect(window.res.length).toBe(i); 178 | vDebugger.resume(); 179 | await nextTick(); 180 | } 181 | expect(window.res.length).toBe(3); 182 | expect(window.ret).toEqual(window.arr.every((e) => e < 3)); 183 | for (let i = 0; i < 3; i++) { 184 | expect(window.res[i][0]).toBe(window.arr[i]); 185 | expect(window.res[i][1]).toBe(i); 186 | expect(window.res[i][2]).toEqual(window.arr); 187 | } 188 | }); 189 | 190 | it('array some normally', async () => { 191 | const run = vDebugger.debug( 192 | 'window.res = [];\n' + 193 | 'window.arr = [1, 2, 3, 4];\n' + 194 | 'window.ret = window.arr.some((e, i, a) => {\n' + 195 | ' window.res.push([e, i, a]);\n' + 196 | ' return e > 2;\n' + 197 | '}, 0);' 198 | , 'https://sandbox.test/array-some.js'); 199 | expect(run).toBeTruthy(); 200 | vDebugger.setBreakpoint('https://sandbox.test/array-some.js', 4); 201 | run(); 202 | for (let i = 0; i < 3; i++) { 203 | expect(window.res.length).toBe(i); 204 | vDebugger.resume(); 205 | await nextTick(); 206 | } 207 | expect(window.res.length).toBe(3); 208 | expect(window.ret).toEqual(window.arr.some((e) => e > 2)); 209 | for (let i = 0; i < 3; i++) { 210 | expect(window.res[i][0]).toBe(window.arr[i]); 211 | expect(window.res[i][1]).toBe(i); 212 | expect(window.res[i][2]).toEqual(window.arr); 213 | } 214 | }); 215 | 216 | it('array sort normally', async () => { 217 | const run = vDebugger.debug( 218 | 'window.arr = [2, 1, 4, 3];\n' + 219 | 'window.ret = window.arr.sort((a, b) => {\n' + 220 | ' return a - b;\n' + 221 | '});' 222 | , 'https://sandbox.test/array-sort.js'); 223 | expect(run).toBeTruthy(); 224 | vDebugger.setBreakpoint('https://sandbox.test/array-sort.js', 3); 225 | run(); 226 | let r = true; 227 | while (r) { 228 | r = vDebugger.resume(); 229 | await nextTick(); 230 | } 231 | expect(window.ret).toEqual(window.arr); 232 | expect(window.ret).toEqual([2, 1, 4, 3].sort((a, b) => a - b)); 233 | 234 | const run2 = vDebugger.debug( 235 | 'window.arr = [3, 1, 4, 2];\n' + 236 | 'window.ret = window.arr.sort((a, b) => {\n' + 237 | ' return b - a;\n' + 238 | '});' 239 | , 'https://sandbox.test/array-sortrev.js'); 240 | run2(); 241 | expect(window.ret).toEqual(window.arr); 242 | expect(window.ret).toEqual([3, 1, 4, 2].sort((a, b) => b - a)); 243 | }); 244 | 245 | it('array find normally', async () => { 246 | const run = vDebugger.debug( 247 | 'window.res = [];\n' + 248 | 'window.arr = [1, 2, 3, 4];\n' + 249 | 'window.ret = window.arr.find((e, i, a) => {\n' + 250 | ' window.res.push([e, i, a]);\n' + 251 | ' return e === 3;\n' + 252 | '}, 0);' 253 | , 'https://sandbox.test/array-find.js'); 254 | expect(run).toBeTruthy(); 255 | vDebugger.setBreakpoint('https://sandbox.test/array-find.js', 4); 256 | run(); 257 | for (let i = 0; i < 3; i++) { 258 | expect(window.res.length).toBe(i); 259 | vDebugger.resume(); 260 | await nextTick(); 261 | } 262 | expect(window.res.length).toBe(3); 263 | expect(window.ret).toEqual(window.arr.find((e) => e === 3)); 264 | for (let i = 0; i < 3; i++) { 265 | expect(window.res[i][0]).toBe(window.arr[i]); 266 | expect(window.res[i][1]).toBe(i); 267 | expect(window.res[i][2]).toEqual(window.arr); 268 | } 269 | }); 270 | 271 | it('array findindex normally', async () => { 272 | const run = vDebugger.debug( 273 | 'window.res = [];\n' + 274 | 'window.arr = [1, 2, 3, 4];\n' + 275 | 'window.ret = window.arr.findIndex((e, i, a) => {\n' + 276 | ' window.res.push([e, i, a]);\n' + 277 | ' return e === 3;\n' + 278 | '}, 0);' 279 | , 'https://sandbox.test/array-findindex.js'); 280 | expect(run).toBeTruthy(); 281 | vDebugger.setBreakpoint('https://sandbox.test/array-findindex.js', 4); 282 | run(); 283 | for (let i = 0; i < 3; i++) { 284 | expect(window.res.length).toBe(i); 285 | vDebugger.resume(); 286 | await nextTick(); 287 | } 288 | expect(window.res.length).toBe(3); 289 | expect(window.ret).toBe(window.arr.findIndex((e) => e === 3)); 290 | for (let i = 0; i < 3; i++) { 291 | expect(window.res[i][0]).toBe(window.arr[i]); 292 | expect(window.res[i][1]).toBe(i); 293 | expect(window.res[i][2]).toEqual(window.arr); 294 | } 295 | }); 296 | 297 | it('array flatmap normally', async () => { 298 | const run = vDebugger.debug( 299 | 'window.res = [];\n' + 300 | 'window.arr = [1, 2, 3, 4];\n' + 301 | 'window.ret = window.arr.flatMap((e, i, a) => {\n' + 302 | ' window.res.push([e, i, a]);\n' + 303 | ' return [e + 1];\n' + 304 | '});' 305 | , 'https://sandbox.test/array-flatmap.js'); 306 | expect(run).toBeTruthy(); 307 | vDebugger.setBreakpoint('https://sandbox.test/array-flatmap.js', 4); 308 | run(); 309 | for (let i = 0; i < window.arr.length; i++) { 310 | expect(window.res.length).toBe(i); 311 | vDebugger.resume(); 312 | await nextTick(); 313 | } 314 | expect(window.res.length).toBe(window.arr.length); 315 | expect(window.ret).toEqual(window.arr.flatMap((e) => [e + 1])); 316 | for (let i = 0; i < window.arr.length; i++) { 317 | expect(window.res[i][0]).toBe(window.arr[i]); 318 | expect(window.res[i][1]).toBe(i); 319 | expect(window.res[i][2]).toEqual(window.arr); 320 | } 321 | }); 322 | 323 | it('string replace normally', async () => { 324 | const run = vDebugger.debug( 325 | 'window.res = [];\n' + 326 | 'window.ret = "abcbd".replace(/(b)/g, (m0, m1, i, s) => {\n' + 327 | ' window.res.push([m0, m1, i, s]);\n' + 328 | ' return "o";\n' + 329 | '});' 330 | , 'https://sandbox.test/string-replace.js'); 331 | expect(run).toBeTruthy(); 332 | vDebugger.setBreakpoint('https://sandbox.test/string-replace.js', 3); 333 | run(); 334 | for (let i = 0; i < 2; i++) { 335 | expect(window.res.length).toBe(i); 336 | vDebugger.resume(); 337 | await nextTick(); 338 | } 339 | expect(window.res.length).toBe(2); 340 | expect(window.ret).toBe('abcbd'.replace(/(b)/g, 'o')); 341 | for (let i = 0; i < 2; i++) { 342 | expect(window.res[i][0]).toBe('b'); 343 | expect(window.res[i][1]).toBe('b'); 344 | expect(window.res[i][2]).toBe(i * 2 + 1); 345 | expect(window.res[i][3]).toBe('abcbd'); 346 | } 347 | }); 348 | 349 | it('string replace all normally', async () => { 350 | const run = vDebugger.debug( 351 | 'window.res = [];\n' + 352 | 'window.ret = "abcbd".replaceAll("b", (m0, i, s) => {\n' + 353 | ' window.res.push([m0, i, s]);\n' + 354 | ' return "o";\n' + 355 | '});' 356 | , 'https://sandbox.test/string-replaceall.js'); 357 | expect(run).toBeTruthy(); 358 | vDebugger.setBreakpoint('https://sandbox.test/string-replaceall.js', 3); 359 | run(); 360 | for (let i = 0; i < 2; i++) { 361 | expect(window.res.length).toBe(i); 362 | vDebugger.resume(); 363 | await nextTick(); 364 | } 365 | expect(window.res.length).toBe(2); 366 | expect(window.ret).toBe('abcbd'.replaceAll('b', 'o')); 367 | for (let i = 0; i < 2; i++) { 368 | expect(window.res[i][0]).toBe('b'); 369 | expect(window.res[i][1]).toBe(i * 2 + 1); 370 | expect(window.res[i][2]).toBe('abcbd'); 371 | } 372 | 373 | const run2 = vDebugger.debug( 374 | 'try {\n' + 375 | ' "abcbd".replaceAll(/b/, () => "o");\n' + 376 | '} catch (err) {\n' + 377 | ' window.ret = err;\n' + 378 | '}' 379 | , 'https://sandbox.test/string-replaceallerr.js'); 380 | run2(); 381 | expect(window.ret).toBeInstanceOf(TypeError); 382 | }); 383 | 384 | it('map foreach normally', async () => { 385 | const run = vDebugger.debug( 386 | 'window.res = [];\n' + 387 | 'window.map = new Map([[1, 2], [3, 4]]);\n' + 388 | 'window.ret = window.map.forEach((v, i, m) => {\n' + 389 | ' window.res.push([v, i, m]);\n' + 390 | '});' 391 | , 'https://sandbox.test/map-foreach.js'); 392 | expect(run).toBeTruthy(); 393 | vDebugger.setBreakpoint('https://sandbox.test/map-foreach.js', 4); 394 | run(); 395 | for (let i = 0; i < window.map.size; i++) { 396 | expect(window.res.length).toBe(i); 397 | vDebugger.resume(); 398 | await nextTick(); 399 | } 400 | expect(window.res.length).toBe(window.map.size); 401 | expect(window.ret).toBeUndefined(); 402 | for (let i = 0; i < window.map.size; i++) { 403 | expect(window.map.get(window.res[i][1])).toBe(window.res[i][0]); 404 | expect(window.res[i][2]).toBe(window.map); 405 | } 406 | const res = []; 407 | window.map.forEach((v, i, m) => res.push([v, i, m])); 408 | expect(window.res).toEqual(res); 409 | }); 410 | 411 | it('set foreach normally', async () => { 412 | const run = vDebugger.debug( 413 | 'window.res = [];\n' + 414 | 'window.set = new Set([1, 2, 3, 4]);\n' + 415 | 'window.ret = window.set.forEach((v, i, s) => {\n' + 416 | ' window.res.push([v, i, s]);\n' + 417 | '});' 418 | , 'https://sandbox.test/set-foreach.js'); 419 | expect(run).toBeTruthy(); 420 | vDebugger.setBreakpoint('https://sandbox.test/set-foreach.js', 4); 421 | run(); 422 | for (let i = 0; i < window.set.size; i++) { 423 | expect(window.res.length).toBe(i); 424 | vDebugger.resume(); 425 | await nextTick(); 426 | } 427 | expect(window.res.length).toBe(window.set.size); 428 | expect(window.ret).toBeUndefined(); 429 | for (let i = 0; i < window.set.size; i++) { 430 | expect(window.set.has(window.res[i][0])).toBeTruthy(); 431 | expect(window.set.has(window.res[i][1])).toBeTruthy(); 432 | expect(window.res[i][2]).toBe(window.set); 433 | } 434 | const res = []; 435 | window.set.forEach((v, i, s) => res.push([v, i, s])); 436 | expect(window.res).toEqual(res); 437 | }); 438 | 439 | it('define custom element', () => { 440 | const run = vDebugger.debug( 441 | 'customElements.define("sb-test", class a {})' 442 | , 'https://sandbox.test/define-custom-element.js'); 443 | expect(run).toBeTruthy(); 444 | vDebugger.setBreakpoint('https://sandbox.test/define-custom-element.js', 4); 445 | run(); 446 | }); 447 | }); 448 | -------------------------------------------------------------------------------- /tests/transform.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import vDebugger from '../src'; 6 | 7 | describe('compiler tests', () => { 8 | it('compile normally', () => { 9 | const res = vDebugger.transform( 10 | 'window.__trans_res__ = 2;\n' + 11 | 'debugger;\n' + // 第2行 12 | 'window.__trans_res__ = 3;\n' + 13 | 'window.__trans_res__ = 4;\n' + 14 | 'window.__trans_res__ = 5;' 15 | , 'transform.js'); 16 | expect(res).toBeTruthy(); 17 | 18 | const run = vDebugger.debug(res); 19 | expect(run).toBeTruthy(); 20 | 21 | const breakLine = 2; // 对应第2行的debugger断点 22 | 23 | run(); 24 | const pausedInfo = vDebugger.getPausedInfo(); 25 | expect(pausedInfo).toBeTruthy(); 26 | expect(pausedInfo.lineNumber).toEqual(breakLine); 27 | expect(window.__trans_res__).toEqual(2); 28 | 29 | const resumeRes = vDebugger.resume('stepOver'); 30 | expect(resumeRes).toBeTruthy(); 31 | const resumeRes2 = vDebugger.resume('stepOver'); 32 | expect(resumeRes2).toBeTruthy(); 33 | const pausedInfo2 = vDebugger.getPausedInfo(); 34 | expect(pausedInfo2).toBeTruthy(); 35 | expect(pausedInfo2.lineNumber).toEqual(breakLine + 2); 36 | expect(window.__trans_res__).toEqual(3); 37 | 38 | const resumeRes3 = vDebugger.resume(); 39 | expect(resumeRes3).toBeTruthy(); 40 | const pausedInfo3 = vDebugger.getPausedInfo(); 41 | expect(pausedInfo3).toBeFalsy(); 42 | expect(window.__trans_res__).toEqual(5); 43 | }); 44 | 45 | it('compile chain expression normally', () => { 46 | const res = vDebugger.transform( 47 | 'const a = { b: { c: () => 7 } }\n' + 48 | 'window.__trans_res__ = 6 && a?.b.c();\n' 49 | , 'chain-expr-transform.js'); 50 | expect(res).toBeTruthy(); 51 | 52 | const run = vDebugger.debug(res); 53 | expect(run).toBeTruthy(); 54 | 55 | run(); 56 | 57 | expect(window.__trans_res__).toEqual(7); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/trick.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import vDebugger from '../src'; 6 | 7 | describe('trick tests', () => { 8 | it('redeclare in global scope normally', () => { 9 | const run = vDebugger.debug( 10 | 'function a() {}\n' + 11 | 'var a;' 12 | ); 13 | expect(run).toBeTruthy(); 14 | 15 | run(); 16 | }); 17 | 18 | it('redeclare in function scope normally', () => { 19 | const run = vDebugger.debug( 20 | 'function b() {\n' + 21 | ' function a() {}\n' + 22 | ' var a;\n' + 23 | '}' 24 | ); 25 | expect(run).toBeTruthy(); 26 | 27 | run(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/utils.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | // 测试环境没有replaceAll,补一个超简单的实现,保证可以走到相应的逻辑 4 | String.prototype.replaceAll = function replace(search, replacer) { 5 | return this.replace(new RegExp(search, 'g'), replacer); 6 | }; 7 | 8 | export const nextTick = () => new Promise((resolve) => setTimeout(resolve)); 9 | -------------------------------------------------------------------------------- /tests/vue.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { nextTick } from './utils'; 6 | import vDebugger from '../src'; 7 | import axios from 'axios'; 8 | 9 | describe('vue2 tests', () => { 10 | let vueScript = ''; 11 | 12 | beforeAll(async () => { 13 | const res = await axios('https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.min.js'); 14 | vueScript = res.data; 15 | document.body.innerHTML = '
'; 16 | }); 17 | 18 | it('transform dependency normally', () => { 19 | const run = vDebugger.debug(vueScript, 'https://vue2.test/vue.js'); 20 | expect(run).toBeTruthy(); 21 | run(); 22 | }); 23 | 24 | it('render vue app normally', () => { 25 | const run = vDebugger.debug( 26 | "import Vue from './vue.js';\n" + 27 | "new Vue({\n" + 28 | " template: '
'\n" + 29 | " + '
'\n" + 30 | " + '
',\n" + 31 | " data() {\n" + 32 | " return {\n" + 33 | " color: false\n" + 34 | " };\n" + 35 | " },\n" + 36 | " computed: {\n" + 37 | " boxStyle() {\n" + 38 | " return {\n" + 39 | " height: '100px',\n" + 40 | " border: '1px solid red',\n" + 41 | " background: this.color ? 'blue' : 'white'\n" + 42 | " };\n" + 43 | " }\n" + 44 | " },\n" + 45 | " methods: {\n" + 46 | " click() {\n" + 47 | " this.color = !this.color;\n" + // 第22行,等下断在这里 48 | " }\n" + 49 | " }\n" + 50 | "}).$mount('#app');", 'https://vue2.test/index.js' 51 | ); 52 | expect(run).toBeTruthy(); 53 | run(); 54 | 55 | const box = document.getElementById('box'); 56 | expect(box.style.backgroundColor).toBe('white'); 57 | }); 58 | 59 | it('emit vue event normally', async () => { 60 | const box = document.getElementById('box'); 61 | box.click(); 62 | 63 | await nextTick(); 64 | expect(box.style.backgroundColor).toBe('blue'); 65 | }); 66 | 67 | it('break vue event handler normally', async () => { 68 | const breakLine = 22; 69 | vDebugger.setBreakpoint('https://vue2.test/index.js', breakLine); 70 | 71 | const box = document.getElementById('box'); 72 | box.click(); 73 | 74 | await nextTick(); 75 | const pausedInfo = vDebugger.getPausedInfo(); 76 | expect(pausedInfo).toBeTruthy(); 77 | expect(pausedInfo.lineNumber).toEqual(breakLine); 78 | expect(box.style.backgroundColor).toBe('blue'); 79 | 80 | const resumeRes = vDebugger.resume(); 81 | expect(resumeRes).toBeTruthy(); 82 | 83 | await nextTick(); 84 | const pausedInfo2 = vDebugger.getPausedInfo(); 85 | expect(pausedInfo2).toBeFalsy(); 86 | expect(box.style.backgroundColor).toBe('white'); 87 | }); 88 | }); 89 | 90 | describe('vue3 tests', () => { 91 | let vueScript = ''; 92 | 93 | beforeAll(async () => { 94 | const res = await axios('https://cdn.jsdelivr.net/npm/vue@3.2.37/dist/vue.esm-browser.prod.js'); 95 | vueScript = res.data; 96 | document.body.innerHTML = '
'; 97 | }); 98 | 99 | it('transform dependency normally', () => { 100 | const run = vDebugger.debug(vueScript, 'https://vue3.test/vue.js'); 101 | expect(run).toBeTruthy(); 102 | run(); 103 | }); 104 | 105 | it('render vue app normally', () => { 106 | const run = vDebugger.debug( 107 | "import { createApp, nextTick } from './vue.js';\n" + 108 | "createApp({\n" + 109 | " template: '
'\n" + 110 | " + '
'\n" + 111 | " + '
',\n" + 112 | " data() {\n" + 113 | " return {\n" + 114 | " color: false\n" + 115 | " };\n" + 116 | " },\n" + 117 | " computed: {\n" + 118 | " boxStyle() {\n" + 119 | " return {\n" + 120 | " height: '100px',\n" + 121 | " border: '1px solid red',\n" + 122 | " background: this.color ? 'blue' : 'white'\n" + 123 | " };\n" + 124 | " }\n" + 125 | " },\n" + 126 | " methods: {\n" + 127 | " click() {\n" + 128 | " this.color = !this.color;\n" + // 第22行,等下断在这里 129 | " }\n" + 130 | " }\n" + 131 | "}).mount('#app');", 'https://vue3.test/index.js' 132 | ); 133 | expect(run).toBeTruthy(); 134 | run(); 135 | 136 | const box = document.getElementById('box'); 137 | expect(box.style.backgroundColor).toBe('white'); 138 | }); 139 | 140 | it('emit vue event normally', async () => { 141 | const box = document.getElementById('box'); 142 | box.click(); 143 | 144 | await nextTick(); 145 | expect(box.style.backgroundColor).toBe('blue'); 146 | }); 147 | 148 | it('break vue event handler normally', async () => { 149 | const breakLine = 22; 150 | vDebugger.setBreakpoint('https://vue3.test/index.js', breakLine); 151 | 152 | const box = document.getElementById('box'); 153 | box.click(); 154 | 155 | await nextTick(); 156 | const pausedInfo = vDebugger.getPausedInfo(); 157 | expect(pausedInfo).toBeTruthy(); 158 | expect(pausedInfo.lineNumber).toEqual(breakLine); 159 | expect(box.style.backgroundColor).toBe('blue'); 160 | 161 | const resumeRes = vDebugger.resume(); 162 | expect(resumeRes).toBeTruthy(); 163 | 164 | await nextTick(); 165 | const pausedInfo2 = vDebugger.getPausedInfo(); 166 | expect(pausedInfo2).toBeFalsy(); 167 | expect(box.style.backgroundColor).toBe('white'); 168 | }); 169 | }); 170 | --------------------------------------------------------------------------------