├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.js ├── debug.js ├── docs └── issue_template.md ├── front_end ├── favicon.png ├── ndb.html ├── ndb.js ├── ndb.json ├── ndb │ ├── Connection.js │ ├── FileSystem.js │ ├── InspectorFrontendHostOverrides.js │ ├── NdbMain.js │ └── module.json ├── ndb_sdk │ ├── NodeRuntime.js │ ├── NodeWorker.js │ └── module.json ├── ndb_ui │ ├── NodeProcesses.js │ ├── RunConfiguration.js │ ├── Terminal.js │ ├── module.json │ ├── nodeProcesses.css │ └── runConfiguration.css └── xterm │ └── module.json ├── lib ├── backend.js ├── filepath_to_url.js ├── launcher.js ├── preload │ └── ndb │ │ └── preload.js └── process_utility.js ├── ndb.js ├── package-lock.json ├── package.json ├── scripts └── builder.js ├── services ├── file_system.js ├── file_system_io.js ├── ndd_service.js ├── search.js └── terminal.js ├── test ├── assets │ └── test-project │ │ ├── index.js │ │ ├── index.mjs │ │ └── package.json ├── basic.spec.js ├── integration.js └── platform.spec.js ├── utils └── testrunner │ ├── Matchers.js │ ├── Multimap.js │ ├── README.md │ ├── Reporter.js │ ├── TestRunner.js │ ├── examples │ ├── fail.js │ ├── hookfail.js │ ├── hooktimeout.js │ ├── timeout.js │ └── unhandledpromiserejection.js │ └── index.js └── version.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/test.js 2 | utils/testrunner/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | 4 | "env": { 5 | "node": true, 6 | "es6": true 7 | }, 8 | 9 | "parserOptions": { 10 | "ecmaVersion": 9 11 | }, 12 | 13 | /** 14 | * ESLint rules 15 | * 16 | * All available rules: http://eslint.org/docs/rules/ 17 | * 18 | * Rules take the following form: 19 | * "rule-name", [severity, { opts }] 20 | * Severity: 2 == error, 1 == warning, 0 == off. 21 | */ 22 | "rules": { 23 | /** 24 | * Enforced rules 25 | */ 26 | 27 | 28 | // syntax preferences 29 | "quotes": [2, "single", { 30 | "avoidEscape": true, 31 | "allowTemplateLiterals": true 32 | }], 33 | "semi": 2, 34 | "no-extra-semi": 2, 35 | "comma-style": [2, "last"], 36 | "wrap-iife": [2, "inside"], 37 | "spaced-comment": [2, "always", { 38 | "markers": ["*"] 39 | }], 40 | "eqeqeq": [2], 41 | "arrow-body-style": [2, "as-needed"], 42 | "accessor-pairs": [2, { 43 | "getWithoutSet": false, 44 | "setWithoutGet": false 45 | }], 46 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 47 | "curly": [2, "multi-or-nest", "consistent"], 48 | "new-parens": 2, 49 | "func-call-spacing": 2, 50 | "arrow-parens": [2, "as-needed"], 51 | "prefer-const": 2, 52 | "quote-props": [2, "consistent"], 53 | 54 | // anti-patterns 55 | "no-var": 2, 56 | "no-with": 2, 57 | "no-multi-str": 2, 58 | "no-caller": 2, 59 | "no-implied-eval": 2, 60 | "no-labels": 2, 61 | "no-new-object": 2, 62 | "no-octal-escape": 2, 63 | "no-self-compare": 2, 64 | "no-shadow-restricted-names": 2, 65 | "no-cond-assign": 2, 66 | "no-debugger": 2, 67 | "no-dupe-keys": 2, 68 | "no-duplicate-case": 2, 69 | "no-empty-character-class": 2, 70 | "no-unreachable": 2, 71 | "no-unsafe-negation": 2, 72 | "radix": 2, 73 | "valid-typeof": 2, 74 | "no-unused-vars": [2, { "args": "none", "vars": "local", "varsIgnorePattern": "(_|load)" }], 75 | "no-implicit-globals": [2], 76 | 77 | // es2015 features 78 | "require-yield": 2, 79 | "template-curly-spacing": [2, "never"], 80 | 81 | // spacing details 82 | "space-infix-ops": 2, 83 | "space-in-parens": [2, "never"], 84 | "space-before-function-paren": [2, "never"], 85 | "no-whitespace-before-property": 2, 86 | "keyword-spacing": [2, { 87 | "overrides": { 88 | "if": {"after": true}, 89 | "else": {"after": true}, 90 | "for": {"after": true}, 91 | "while": {"after": true}, 92 | "do": {"after": true}, 93 | "switch": {"after": true}, 94 | "return": {"after": true} 95 | } 96 | }], 97 | "arrow-spacing": [2, { 98 | "after": true, 99 | "before": true 100 | }], 101 | 102 | // file whitespace 103 | "no-multiple-empty-lines": [2, {"max": 2}], 104 | "no-mixed-spaces-and-tabs": 2, 105 | "no-trailing-spaces": 2, 106 | "linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ], 107 | "indent": [2, 2, { "SwitchCase": 1, "CallExpression": {"arguments": 2}, "MemberExpression": 2 }], 108 | "key-spacing": [2, { 109 | "beforeColon": false 110 | }] 111 | } 112 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.local-frontend/ 2 | /node_modules/ 3 | .DS_Store 4 | *.swp 5 | *.pyc 6 | .vscode 7 | package-lock.json 8 | yarn.lock 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # exclude all tests 2 | test/ 3 | test/fs/ 4 | utils/testrunner 5 | 6 | # repeats from .gitignore 7 | /node_modules/ 8 | .DS_Store 9 | *.swp 10 | *.pyc 11 | .vscode 12 | package-lock.json 13 | yarn.lock 14 | 15 | # other 16 | .editorconfig 17 | .eslintignore 18 | .eslintrc.js 19 | README.md 20 | ndb-*.tgz 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | - "10" 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | First of all, thank you for your interest in ndb! 4 | We'd love to accept your patches and contributions! 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Getting setup 19 | 20 | 1. Clone this repository 21 | 22 | ```bash 23 | git clone https://github.com/GoogleChromeLabs/ndb 24 | cd ndb 25 | ``` 26 | 27 | 2. Install dependencies 28 | 29 | ```bash 30 | npm install 31 | ``` 32 | 33 | ## Code reviews 34 | 35 | All submissions, including submissions by project members, require review. We 36 | use GitHub pull requests for this purpose. Consult 37 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 38 | information on using pull requests. 39 | 40 | ## Code Style 41 | 42 | - Coding style is fully defined in [.eslintrc](https://github.com/GoogleChrome/puppeteer/blob/master/.eslintrc.js) 43 | - Code should be annotated with [closure annotations](https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler). 44 | - Comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory. 45 | 46 | To run code linter, use: 47 | 48 | ```bash 49 | npm run lint 50 | ``` 51 | 52 | ## Commit Messages 53 | 54 | Commit messages should follow the Semantic Commit Messages format: 55 | 56 | ``` 57 | label(namespace): title 58 | 59 | description 60 | 61 | footer 62 | ``` 63 | 64 | 1. *label* is one of the following: 65 | - `fix` - ndb bug fixes. 66 | - `feat` - ndb features. 67 | - `docs` - changes to docs, e.g. `docs(api.md): ..` to change documentation. 68 | - `test` - changes to ndb tests infrastructure. 69 | - `style` - ndb code style: spaces/alignment/wrapping etc. 70 | - `chore` - build-related work. 71 | 2. *namespace* is put in parenthesis after label and is **optional**. 72 | 3. *title* is a brief summary of changes. 73 | 4. *description* is **optional**, new-line separated from title and is in present tense. 74 | 5. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues. 75 | 76 | Example: 77 | 78 | ``` 79 | fix(NddService): fix NddService.attach method 80 | 81 | This patch fixes NddService.attach so that it works with Node 12. 82 | 83 | Fixes #123, Fixes #234 84 | ``` 85 | 86 | ## Adding New Dependencies 87 | 88 | For all dependencies (both installation and development): 89 | - **Do not add** a dependency if the desired functionality is easily implementable. 90 | - If adding a dependency, it should be well-maintained and trustworthy. 91 | 92 | A barrier for introducing new installation dependencies is especially high: 93 | - **Do not add** installation dependency unless it's critical to project success. 94 | 95 | ## Writing Tests 96 | 97 | - Every ndb service feature should be accompanied by a test. 98 | - Tests should be *hermetic*. Tests should not depend on external services. 99 | - Tests should work on all three platforms: Mac, Linux and Win. 100 | 101 | ndb tests are located in [test/test.js](https://github.com/GoogleChromeLabs/ndb/blob/master/test/test.js) 102 | and are written with a [mocha](https://mochajs.org/) framework. 103 | 104 | - To run all tests: 105 | 106 | ```bash 107 | npm run unit 108 | ``` 109 | 110 | - To run a specific test, substitute the `it` with `fit` (mnemonic rule: '*focus it*'): 111 | 112 | ```js 113 | ... 114 | // Using "fit" to run specific test 115 | fit('should work', async function({service}) { 116 | const response = await service.method(); 117 | expect(response).toBe(true); 118 | }) 119 | ``` 120 | 121 | - To disable a specific test, substitute the `it` with `xit` (mnemonic rule: '*cross it*'): 122 | 123 | ```js 124 | ... 125 | // Using "xit" to skip specific test 126 | xit('should work', async function({service}) { 127 | const response = await service.method(); 128 | expect(response).toBe(true); 129 | }) 130 | ``` 131 | 132 | ## Developing ndb hints 133 | 134 | - Environment variable NDB_DEBUG_FRONTEND=1 forces ndb to fetch 135 | frontend from front_end folder and chrome-devtools-frontend 136 | package. 137 | 138 | ```bash 139 | NDB_DEBUG_FRONTEND=1 ndb . 140 | ``` 141 | 142 | - To debug ndb by itself or any ndb service you can use ndb. 143 | ```bash 144 | NDB_DEBUG_FRONTEND=1 ndb ndb index.js 145 | ``` 146 | 147 | - To debug running Chrome DevTools frontend you can open DevTools, 148 | use Ctrl+Shift+I on Linux or View > Developer > Developer Tools menu 149 | item on Mac OS. 150 | 151 | ## [For Project Maintainers] Releasing to NPM 152 | 153 | Releasing to NPM consists of 3 phases: 154 | 1. Source Code: mark a release. 155 | 1. Bump `package.json` version following the SEMVER rules and send a PR titled `'chore: mark version vXXX.YYY.ZZZ'`. 156 | 2. Make sure the PR passes **all checks**. 157 | 3. Merge the PR. 158 | 4. Once merged, publish release notes using the "create new tag" option. 159 | - **NOTE**: tag names are prefixed with `'v'`, e.g. for version `1.4.0` tag is `v1.4.0`. 160 | 2. Publish to NPM. 161 | 1. On your local machine, pull from [upstream](https://github.com/GoogleChromeLabs/ndb) and make sure the last commit is the one just merged. 162 | 2. Run `git status` and make sure there are no untracked files. 163 | - **WHY**: this is to avoid bundling unnecessary files to NPM package 164 | 3. Run [`pkgfiles`](https://www.npmjs.com/package/pkgfiles) to make sure you don't publish anything unnecessary. 165 | 4. Run `npm publish`. 166 | 3. Source Code: mark post-release. 167 | 1. Bump `package.json` version to `-post` version and send a PR titled `'chore: bump version to vXXX.YYY.ZZZ-post'` 168 | - **NOTE**: no other commits should be landed in-between release commit and bump commit. 169 | 170 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2018 Google Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ndb 2 | 3 | 4 | [![Build Status](https://img.shields.io/travis/com/GoogleChromeLabs/ndb/master.svg)](https://travis-ci.com/GoogleChromeLabs/ndb) 5 | [![NPM ndb package](https://img.shields.io/npm/v/ndb.svg)](https://npmjs.org/package/ndb) 6 | 7 | 8 | 9 | 10 | > ndb is an improved debugging experience for Node.js, enabled by Chrome DevTools 11 | 12 | ## Installation 13 | 14 | Compatibility: ndb requires Node >=8.0.0. It works best with Node >=10. 15 | 16 | Installation: ndb depends on [Puppeteer](https://github.com/GoogleChrome/puppeteer) which downloads a recent version of Chromium (~170MB Mac, ~280MB Linux, ~280MB Win). 17 | 18 | ```bash 19 | # global install with npm: 20 | npm install -g ndb 21 | 22 | 23 | # alternatively, with yarn: 24 | yarn global add ndb 25 | ``` 26 | 27 | Global installation may fail with different permission errors, you can find help in this [thread](https://github.com/GoogleChromeLabs/ndb/issues/20). 28 | 29 | Windows users: Installation may fail on Windows during compilation the native dependencies. The following command may help: `npm install -g windows-build-tools` 30 | 31 | ### Local install 32 | 33 | If you want ndb available from an npm script (eg. `npm run debug` runs `ndb index.js`), you can install it as a development dependency: 34 | 35 | ```bash 36 | # local install with npm: 37 | npm install --save-dev ndb 38 | 39 | 40 | # alternatively, with yarn: 41 | yarn add ndb --dev 42 | ``` 43 | 44 | You can then [set up an npm script](https://docs.npmjs.com/misc/scripts#examples). In this case, ndb will not be available in your system path. 45 | 46 | 47 | ## Getting Started 48 | 49 | You can start debugging your Node.js application using one of the following ways: 50 | 51 | - Use `ndb` instead of the `node` command 52 | 53 | ```bash 54 | ndb server.js 55 | 56 | # Alternatively, you can prepend `ndb` 57 | ndb node server.js 58 | ``` 59 | 60 | - Prepend `ndb` in front of any other binary 61 | 62 | ```bash 63 | # If you use some other binary, just prepend `ndb` 64 | ## npm run unit 65 | ndb npm run unit 66 | 67 | # Debug any globally installed package 68 | ## mocha 69 | ndb mocha 70 | 71 | # To use a local binary, use `npx` and prepend before it 72 | ndb npx mocha 73 | ``` 74 | 75 | - Launch `ndb` as a standalone application 76 | - Then, debug any npm script from your `package.json`, e.g. unit tests 77 | 78 | ```bash 79 | # cd to your project folder (with a package.json) 80 | ndb . 81 | # In Sources panel > "NPM Scripts" sidebar, click the selected "Run" button 82 | ``` 83 | 84 | - Use `Ctrl`/`Cmd` + `R` to restart last run 85 | - Run any node command from within ndb's integrated terminal and ndb will connect automatically 86 | - Run any open script source by using 'Run this script' context menu item, ndb will connect automatically as well 87 | 88 | - Use `--prof` flag to profile your app, `Ctrl`/`Cmd` + `R` restarts profiling 89 | ```bash 90 | ndb --prof npm run unit 91 | ``` 92 | 93 | ## What can I do? 94 | 95 | `ndb` has some powerful features exclusively for Node.js: 96 | 1. Child processes are detected and attached to. 97 | 1. You can place breakpoints before the modules are required. 98 | 1. You can edit your files within the UI. On Ctrl-S/Cmd-S, DevTools will [save the changes to disk](https://developers.google.com/web/tools/chrome-devtools/workspaces/). 99 | 1. By default, ndb [blackboxes](https://developers.google.com/web/tools/chrome-devtools/javascript/reference#blackbox) all scripts outside current working directory to improve focus. This includes node internal libraries (like `_stream_wrap.js`, `async_hooks.js`, `fs.js`) This behaviour may be changed by "Blackbox anything outside working dir" setting. 100 | 101 | In addition, you can use all the DevTools functionality that you've used in [typical Node debugging](https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27): 102 | - breakpoint debugging, async stacks (AKA long stack traces), [async stepping](https://developers.google.com/web/updates/2018/01/devtools#async), etc... 103 | - console (top-level await, object inspection, advanced filtering) 104 | - [eager evaluation](https://developers.google.com/web/updates/2018/05/devtools#eagerevaluation) in console (requires Node >= 10) 105 | - JS sampling profiler 106 | - memory profiler 107 | 108 | ### Screenshot 109 | ![image](https://user-images.githubusercontent.com/39191/43023843-14a085a6-8c21-11e8-85b7-b9fd3405938a.png) 110 | 111 | 112 | ## Contributing 113 | 114 | Check out [contributing guide](https://github.com/GoogleChromeLabs/ndb/blob/master/CONTRIBUTING.md) to get an overview of ndb development. 115 | 116 | #### Thanks to the 'OG' `ndb` 117 | 118 | In early 2011, [@smtlaissezfaire](https://github.com/smtlaissezfaire) released the first serious debugger for Node.js, under the `ndb` package name. It's still preserved at [github.com/smtlaissezfaire/ndb](https://github.com/smtlaissezfaire/ndb#readme). We thank Scott for generously donating the package name. 119 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const Terser = require('terser'); 4 | const rimraf = require('rimraf'); 5 | 6 | const { buildApp } = require('./scripts/builder.js'); 7 | 8 | const DEVTOOLS_DIR = path.dirname( 9 | require.resolve('chrome-devtools-frontend/front_end/shell.json')); 10 | 11 | (async function main() { 12 | const outFolder = path.join(__dirname, '.local-frontend'); 13 | await new Promise(resolve => rimraf(outFolder, resolve)); 14 | 15 | return buildApp( 16 | ['ndb', 'heap_snapshot_worker', 'formatter_worker'], [ 17 | path.join(__dirname, 'front_end'), 18 | DEVTOOLS_DIR, 19 | path.join(__dirname, 'node_modules'), 20 | ], outFolder, 21 | minifyJS); 22 | })(); 23 | 24 | function minifyJS(code) { 25 | return Terser.minify(code, { 26 | mangle: true, 27 | ecma: 8, 28 | compress: false 29 | }).code; 30 | } 31 | -------------------------------------------------------------------------------- /debug.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | process.env.NDB_DEBUG_FRONTEND = 1; 3 | require('./ndb.js'); 4 | -------------------------------------------------------------------------------- /docs/issue_template.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ### Steps to reproduce 6 | 7 | **Tell us about your environment:** 8 | 9 | * ndb version: 10 | * Platform / OS version: 11 | * Node.js version: 12 | 13 | **What steps will reproduce the problem?** 14 | 15 | _Please include code that reproduces the issue._ 16 | 17 | 1. 18 | 2. 19 | 3. 20 | 21 | **What is the expected result?** 22 | 23 | 24 | **What happens instead?** 25 | -------------------------------------------------------------------------------- /front_end/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ndb/ade2052ad27f3c90cfd97c6aa87465c179572f8a/front_end/favicon.png -------------------------------------------------------------------------------- /front_end/ndb.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /front_end/ndb.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | Runtime.startApplication('ndb'); 6 | -------------------------------------------------------------------------------- /front_end/ndb.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules" : [ 3 | { "name": "ndb_sdk", "type": "autostart" }, 4 | { "name": "ndb", "type": "autostart" }, 5 | { "name": "layer_viewer" }, 6 | { "name": "timeline_model" }, 7 | { "name": "timeline" }, 8 | { "name": "product_registry" }, 9 | { "name": "mobile_throttling" }, 10 | { "name": "ndb_ui" }, 11 | { "name": "xterm" } 12 | ], 13 | "extends": "shell", 14 | "has_html": true 15 | } 16 | -------------------------------------------------------------------------------- /front_end/ndb/Connection.js: -------------------------------------------------------------------------------- 1 | Ndb.Connection = class { 2 | constructor(channel) { 3 | this._onMessage = null; 4 | this._onDisconnect = null; 5 | this._channel = channel; 6 | } 7 | 8 | static async create(channel) { 9 | const connection = new Ndb.Connection(channel); 10 | await channel.listen(rpc.handle(connection)); 11 | return connection; 12 | } 13 | 14 | /** 15 | * @param {function((!Object|string))} onMessage 16 | */ 17 | setOnMessage(onMessage) { 18 | this._onMessage = onMessage; 19 | } 20 | 21 | /** 22 | * @param {function(string)} onDisconnect 23 | */ 24 | setOnDisconnect(onDisconnect) { 25 | this._onDisconnect = onDisconnect; 26 | } 27 | 28 | /** 29 | * @param {string} message 30 | */ 31 | sendRawMessage(message) { 32 | this._channel.send(message); 33 | } 34 | 35 | /** 36 | * @return {!Promise} 37 | */ 38 | disconnect() { 39 | this._channel.close(); 40 | } 41 | 42 | /** 43 | * @param {message} 44 | */ 45 | dispatchMessage(message) { 46 | if (this._onMessage) 47 | this._onMessage(message); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /front_end/ndb/FileSystem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | Ndb.FileSystem = class extends Persistence.PlatformFileSystem { 8 | constructor(fsService, fsIOService, searchService, manager, rootURL) { 9 | super(rootURL, ''); 10 | this._fsService = fsService; 11 | this._fsIOService = fsIOService; 12 | this._searchService = searchService; 13 | this._rootURL = rootURL; 14 | this._manager = manager; 15 | 16 | /** @type {!Array} */ 17 | this._initialFilePaths = []; 18 | } 19 | 20 | static async create(manager, rootURL) { 21 | const searchClient = new Ndb.FileSystem.SearchClient(); 22 | const [fsService, fsIOService, searchService] = await Promise.all([ 23 | Ndb.backend.createService('file_system.js'), 24 | Ndb.backend.createService('file_system_io.js'), 25 | Ndb.backend.createService('search.js', rpc.handle(searchClient))]); 26 | 27 | // TODO: fix PlatformFileSystem upstream, entire search / indexing pipeline should go 28 | // through the platform filesystem. This should make searchClient also go away. 29 | InspectorFrontendHost.stopIndexing = searchService.stopIndexing.bind(searchService); 30 | 31 | const fs = new Ndb.FileSystem(fsService, fsIOService, searchService, manager, rootURL); 32 | await fs._initFilePaths(); 33 | return fs; 34 | } 35 | 36 | /** 37 | * @override 38 | * @return {string} 39 | */ 40 | embedderPath() { 41 | throw new Error('Not implemented'); 42 | } 43 | 44 | /** 45 | * @override 46 | * @return {!Promise} 47 | */ 48 | async _initFilePaths() { 49 | await this._fsService.startWatcher(this._rootURL, this._excludePattern(), rpc.handle(this)); 50 | } 51 | 52 | forceFileLoad(scriptName) { 53 | return this._fsService.forceFileLoad(scriptName); 54 | } 55 | 56 | /** 57 | * @override 58 | * @return {!Array} 59 | */ 60 | initialFilePaths() { 61 | return this._initialFilePaths; 62 | } 63 | 64 | /** 65 | * @override 66 | * @return {!Array} 67 | */ 68 | initialGitFolders() { 69 | return []; 70 | } 71 | 72 | /** 73 | * @override 74 | * @param {string} path 75 | * @return {!Promise} 76 | */ 77 | getMetadata(path) { 78 | // This method should never be called as long as we are matching using file urls. 79 | throw new Error('not implemented'); 80 | } 81 | 82 | /** 83 | * @override 84 | * @param {string} path 85 | * @return {!Promise} 86 | */ 87 | requestFileBlob(path) { 88 | throw new Error('not implemented'); 89 | } 90 | 91 | /** 92 | * @override 93 | * @param {string} path 94 | * @param {function(?string,boolean)} callback 95 | */ 96 | async requestFileContent(path, callback) { 97 | const result = await this._fsIOService.readFile(this._rootURL + path, 'base64'); 98 | if (this.contentType(path) === Common.resourceTypes.Image) { 99 | callback(result, true); 100 | } else { 101 | const content = await(await fetch(`data:application/octet-stream;base64,${result}`)).text(); 102 | callback(content, false); 103 | } 104 | } 105 | 106 | /** 107 | * @override 108 | * @param {string} path 109 | * @param {string} content 110 | * @param {boolean} isBase64 111 | */ 112 | async setFileContent(path, content, isBase64) { 113 | await this._fsIOService.writeFile(this._rootURL + path, isBase64 ? content : content.toBase64(), 'base64'); 114 | } 115 | 116 | /** 117 | * @override 118 | * @param {string} path 119 | * @param {?string} name 120 | * @return {!Promise} 121 | */ 122 | async createFile(path, name) { 123 | const result = await this._fsIOService.createFile(this._rootURL + (path.length === 0 || path.startsWith('/') ? '' : '/') + path); 124 | return result.substr(this._rootURL.length + 1); 125 | } 126 | 127 | /** 128 | * @override 129 | * @param {string} path 130 | * @return {!Promise} 131 | */ 132 | async deleteFile(path) { 133 | return await this._fsIOService.deleteFile(this._rootURL + path); 134 | } 135 | 136 | /** 137 | * @override 138 | * @param {string} path 139 | * @param {string} newName 140 | * @param {function(boolean, string=)} callback 141 | */ 142 | async renameFile(path, newName, callback) { 143 | const result = await this._fsIOService.renameFile(this._rootURL + path, newName); 144 | callback(result, result ? newName : null); 145 | } 146 | 147 | /** 148 | * @override 149 | * @param {string} path 150 | * @return {!Common.ResourceType} 151 | */ 152 | contentType(path) { 153 | const extension = Common.ParsedURL.extractExtension(path); 154 | if (Persistence.IsolatedFileSystem._styleSheetExtensions.has(extension)) 155 | return Common.resourceTypes.Stylesheet; 156 | if (Persistence.IsolatedFileSystem._documentExtensions.has(extension)) 157 | return Common.resourceTypes.Document; 158 | if (Persistence.IsolatedFileSystem.ImageExtensions.has(extension)) 159 | return Common.resourceTypes.Image; 160 | if (Persistence.IsolatedFileSystem._scriptExtensions.has(extension)) 161 | return Common.resourceTypes.Script; 162 | return Persistence.IsolatedFileSystem.BinaryExtensions.has(extension) ? Common.resourceTypes.Other : 163 | Common.resourceTypes.Document; 164 | } 165 | 166 | /** 167 | * @override 168 | * @param {string} path 169 | * @return {string} 170 | */ 171 | mimeFromPath(path) { 172 | return Common.ResourceType.mimeFromURL(path) || 'text/plain'; 173 | } 174 | 175 | /** 176 | * @override 177 | * @param {string} path 178 | * @return {boolean} 179 | */ 180 | canExcludeFolder(path) { 181 | return !!path ; 182 | } 183 | 184 | /** 185 | * @override 186 | * @param {string} url 187 | * @return {string} 188 | */ 189 | tooltipForURL(url) { 190 | const path = Common.ParsedURL.urlToPlatformPath(url, Host.isWin()).trimMiddle(150); 191 | return ls`Linked to ${path}`; 192 | } 193 | 194 | /** 195 | * @override 196 | * @param {string} query 197 | * @param {!Common.Progress} progress 198 | * @return {!Promise>} 199 | */ 200 | searchInPath(query, progress) { 201 | return new Promise(resolve => { 202 | const requestId = this._manager.registerCallback(innerCallback); 203 | this._searchService.searchInPath(requestId, this._rootURL, query); 204 | 205 | /** 206 | * @param {!Array} files 207 | */ 208 | function innerCallback(files) { 209 | resolve(files); 210 | progress.worked(1); 211 | } 212 | }); 213 | } 214 | 215 | /** 216 | * @param {string} name 217 | */ 218 | filesChanged(events) { 219 | for (const event of events) { 220 | const paths = new Multimap(); 221 | paths.set(this._rootURL, event.name); 222 | const emptyMap = new Multimap(); 223 | Persistence.isolatedFileSystemManager.dispatchEventToListeners(Persistence.IsolatedFileSystemManager.Events.FileSystemFilesChanged, { 224 | changed: event.type === 'change' ? paths : emptyMap, 225 | added: event.type === 'add' ? paths : emptyMap, 226 | removed: event.type === 'unlink' ? paths : emptyMap 227 | }); 228 | } 229 | } 230 | 231 | /** 232 | * @override 233 | * @param {!Common.Progress} progress 234 | */ 235 | indexContent(progress) { 236 | progress.setTotalWork(1); 237 | const requestId = this._manager.registerProgress(progress); 238 | this._searchService.indexPath(requestId, this._rootURL, this._excludePattern()); 239 | } 240 | 241 | /** 242 | * @override 243 | * @return {boolean} 244 | */ 245 | supportsAutomapping() { 246 | return true; 247 | } 248 | 249 | /** 250 | * @return {string} 251 | */ 252 | _excludePattern() { 253 | return this._manager.workspaceFolderExcludePatternSetting().get(); 254 | } 255 | }; 256 | 257 | Ndb.FileSystem.SearchClient = class { 258 | /** 259 | * @param {number} requestId 260 | * @param {string} fileSystemPath 261 | * @param {number} totalWork 262 | */ 263 | indexingTotalWorkCalculated(requestId, fileSystemPath, totalWork) { 264 | this._callFrontend(() => InspectorFrontendAPI.indexingTotalWorkCalculated(requestId, fileSystemPath, totalWork)); 265 | } 266 | 267 | /** 268 | * @param {number} requestId 269 | * @param {string} fileSystemPath 270 | * @param {number} worked 271 | */ 272 | indexingWorked(requestId, fileSystemPath, worked) { 273 | this._callFrontend(() => InspectorFrontendAPI.indexingWorked(requestId, fileSystemPath, worked)); 274 | } 275 | 276 | /** 277 | * @param {number} requestId 278 | * @param {string} fileSystemPath 279 | */ 280 | indexingDone(requestId, fileSystemPath) { 281 | this._callFrontend(_ => InspectorFrontendAPI.indexingDone(requestId, fileSystemPath)); 282 | } 283 | 284 | /** 285 | * @param {number} requestId 286 | * @param {string} fileSystemPath 287 | * @param {!Array.} files 288 | */ 289 | searchCompleted(requestId, fileSystemPath, files) { 290 | this._callFrontend(_ => InspectorFrontendAPI.searchCompleted(requestId, fileSystemPath, files)); 291 | } 292 | 293 | _callFrontend(f) { 294 | if (Runtime.queryParam('debugFrontend')) 295 | setTimeout(f, 0); 296 | else 297 | f(); 298 | } 299 | }; 300 | -------------------------------------------------------------------------------- /front_end/ndb/InspectorFrontendHostOverrides.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | (function(){ 8 | InspectorFrontendHost.getPreferences = async function(callback) { 9 | [Ndb.backend] = await carlo.loadParams(); 10 | const prefs = { 11 | '__bundled__uiTheme': '"dark"' 12 | }; 13 | for (let i = 0; i < window.localStorage.length; i++) { 14 | const key = window.localStorage.key(i); 15 | prefs[key] = window.localStorage.getItem(key); 16 | } 17 | callback(prefs); 18 | }; 19 | 20 | InspectorFrontendHost.isHostedMode = () => false; 21 | InspectorFrontendHost.copyText = text => navigator.clipboard.writeText(text); 22 | InspectorFrontendHost.openInNewTab = url => Ndb.backend.openInNewTab(url); 23 | InspectorFrontendHost.bringToFront = () => Ndb.backend.bringToFront(); 24 | InspectorFrontendHost.loadNetworkResource = async(url, headers, streamId, callback) => { 25 | const text = await Ndb.backend.loadNetworkResource(url, headers); 26 | if (text) { 27 | Host.ResourceLoader.streamWrite(streamId, text); 28 | callback({statusCode: 200}); 29 | } else { 30 | callback({statusCode: 404}); 31 | } 32 | }; 33 | })(); 34 | -------------------------------------------------------------------------------- /front_end/ndb/NdbMain.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | Ndb.npmExecPath = function() { 8 | if (!Ndb._npmExecPathPromise) 9 | Ndb._npmExecPathPromise = Ndb.backend.which('npm').then(result => result.resolvedPath); 10 | return Ndb._npmExecPathPromise; 11 | }; 12 | 13 | /** 14 | * @implements {Common.Runnable} 15 | */ 16 | Ndb.NdbMain = class extends Common.Object { 17 | /** 18 | * @override 19 | */ 20 | async run() { 21 | InspectorFrontendAPI.setUseSoftMenu(true); 22 | document.title = 'ndb'; 23 | Common.moduleSetting('blackboxInternalScripts').addChangeListener(Ndb.NdbMain._calculateBlackboxState); 24 | Ndb.NdbMain._calculateBlackboxState(); 25 | 26 | const setting = Persistence.isolatedFileSystemManager.workspaceFolderExcludePatternSetting(); 27 | setting.set(Ndb.NdbMain._defaultExcludePattern().join('|')); 28 | Ndb.nodeProcessManager = await Ndb.NodeProcessManager.create(SDK.targetManager); 29 | 30 | Ndb.processInfo = await Ndb.backend.processInfo(); 31 | await Ndb.nodeProcessManager.addFileSystem(Ndb.processInfo.cwd); 32 | 33 | // TODO(ak239): we do not want to create this model for workers, so we need a way to add custom capabilities. 34 | SDK.SDKModel.register(NdbSdk.NodeWorkerModel, SDK.Target.Capability.JS, true); 35 | SDK.SDKModel.register(NdbSdk.NodeRuntimeModel, SDK.Target.Capability.JS, true); 36 | 37 | await new Promise(resolve => SDK.initMainConnection(resolve)); 38 | SDK.targetManager.createTarget('', ls`Root`, SDK.Target.Type.Browser, null); 39 | if (Common.moduleSetting('autoStartMain').get()) { 40 | const main = await Ndb.mainConfiguration(); 41 | if (main) { 42 | if (main.prof) 43 | await Ndb.nodeProcessManager.profile(main.execPath, main.args); 44 | else 45 | Ndb.nodeProcessManager.debug(main.execPath, main.args); 46 | } 47 | } 48 | Ndb.nodeProcessManager.startRepl(); 49 | } 50 | 51 | static _defaultExcludePattern() { 52 | const defaultCommonExcludedFolders = [ 53 | '/bower_components/', '/\\.devtools', '/\\.git/', '/\\.sass-cache/', '/\\.hg/', '/\\.idea/', 54 | '/\\.svn/', '/\\.cache/', '/\\.project/' 55 | ]; 56 | const defaultWinExcludedFolders = ['/Thumbs.db$', '/ehthumbs.db$', '/Desktop.ini$', '/\\$RECYCLE.BIN/']; 57 | const defaultMacExcludedFolders = [ 58 | '/\\.DS_Store$', '/\\.Trashes$', '/\\.Spotlight-V100$', '/\\.AppleDouble$', '/\\.LSOverride$', '/Icon$', 59 | '/\\._.*$' 60 | ]; 61 | const defaultLinuxExcludedFolders = ['/.*~$']; 62 | let defaultExcludedFolders = defaultCommonExcludedFolders; 63 | if (Host.isWin()) 64 | defaultExcludedFolders = defaultExcludedFolders.concat(defaultWinExcludedFolders); 65 | else if (Host.isMac()) 66 | defaultExcludedFolders = defaultExcludedFolders.concat(defaultMacExcludedFolders); 67 | else 68 | defaultExcludedFolders = defaultExcludedFolders.concat(defaultLinuxExcludedFolders); 69 | return defaultExcludedFolders; 70 | } 71 | 72 | static _calculateBlackboxState() { 73 | const blackboxInternalScripts = Common.moduleSetting('blackboxInternalScripts').get(); 74 | const PATTERN = '^internal[\\/].*|bin/npm-cli\.js$|bin/yarn\.js$'; 75 | const regexPatterns = Common.moduleSetting('skipStackFramesPattern').getAsArray() 76 | .filter(({pattern}) => pattern !== PATTERN && pattern !== '^internal/.*'); 77 | if (blackboxInternalScripts) 78 | regexPatterns.push({pattern: PATTERN }); 79 | Common.moduleSetting('skipStackFramesPattern').setAsArray(regexPatterns); 80 | } 81 | }; 82 | 83 | Ndb.mainConfiguration = async() => { 84 | const info = Ndb.processInfo; 85 | const cmd = info.argv.slice(2); 86 | if (cmd.length === 0 || cmd[0] === '.') 87 | return null; 88 | let execPath; 89 | let args; 90 | let prof = false; 91 | if (cmd[0] === '--prof') { 92 | prof = true; 93 | cmd.shift(); 94 | } 95 | if (cmd[0].endsWith('.js') 96 | || cmd[0].endsWith('.mjs') 97 | || cmd[0].startsWith('-')) { 98 | execPath = await Ndb.processInfo.nodeExecPath; 99 | args = cmd; 100 | } else { 101 | execPath = cmd[0]; 102 | args = cmd.slice(1); 103 | } 104 | if (execPath === 'npm') 105 | execPath = await Ndb.npmExecPath(); 106 | else if (execPath === 'node') 107 | execPath = await Ndb.processInfo.nodeExecPath; 108 | return { 109 | name: 'main', 110 | command: cmd.join(' '), 111 | execPath, 112 | args, 113 | prof 114 | }; 115 | }; 116 | 117 | /** 118 | * @implements {UI.ContextMenu.Provider} 119 | * @unrestricted 120 | */ 121 | Ndb.ContextMenuProvider = class { 122 | /** 123 | * @override 124 | * @param {!Event} event 125 | * @param {!UI.ContextMenu} contextMenu 126 | * @param {!Object} object 127 | */ 128 | appendApplicableItems(event, contextMenu, object) { 129 | if (!(object instanceof Workspace.UISourceCode)) 130 | return; 131 | const url = object.url(); 132 | if (!url.startsWith('file://') || (!url.endsWith('.js') && !url.endsWith('.mjs'))) 133 | return; 134 | contextMenu.debugSection().appendItem(ls`Run this script`, async() => { 135 | const platformPath = await Ndb.backend.fileURLToPath(url); 136 | const args = url.endsWith('.mjs') ? ['--experimental-modules', platformPath] : [platformPath]; 137 | Ndb.nodeProcessManager.debug(Ndb.processInfo.nodeExecPath, args); 138 | }); 139 | } 140 | }; 141 | 142 | Ndb._connectionSymbol = Symbol('connection'); 143 | 144 | Ndb.NodeProcessManager = class extends Common.Object { 145 | constructor(targetManager) { 146 | super(); 147 | this._service = null; 148 | this._lastDebugId = 0; 149 | this._lastStarted = null; 150 | this._targetManager = targetManager; 151 | this._cwds = new Map(); 152 | this._finishProfiling = null; 153 | this._cpuProfiles = []; 154 | this._targetManager.addModelListener( 155 | SDK.RuntimeModel, SDK.RuntimeModel.Events.ExecutionContextDestroyed, this._onExecutionContextDestroyed, this); 156 | this._targetManager.addModelListener( 157 | NdbSdk.NodeRuntimeModel, NdbSdk.NodeRuntimeModel.Events.WaitingForDisconnect, this._onWaitingForDisconnect, this); 158 | } 159 | 160 | static async create(targetManager) { 161 | const manager = new Ndb.NodeProcessManager(targetManager); 162 | manager._service = await Ndb.backend.createService('ndd_service.js', rpc.handle(manager)); 163 | return manager; 164 | } 165 | 166 | env() { 167 | return this._service.env(); 168 | } 169 | 170 | /** 171 | * @param {string} cwd 172 | * @param {string=} mainFileName 173 | * @return {!Promise} 174 | */ 175 | async addFileSystem(cwd, mainFileName) { 176 | let promise = this._cwds.get(cwd); 177 | if (!promise) { 178 | async function innerAdd() { 179 | const fileSystemManager = Persistence.isolatedFileSystemManager; 180 | const fs = await Ndb.FileSystem.create(fileSystemManager, cwd); 181 | fileSystemManager.addPlatformFileSystem(cwd, fs); 182 | return fs; 183 | } 184 | promise = innerAdd(); 185 | this._cwds.set(cwd, promise); 186 | } 187 | if (mainFileName) 188 | await (await promise).forceFileLoad(mainFileName); 189 | await promise; 190 | } 191 | 192 | async detected(info, channel) { 193 | const connection = await Ndb.Connection.create(channel); 194 | const target = this._targetManager.createTarget( 195 | info.id, userFriendlyName(info), SDK.Target.Type.Node, 196 | this._targetManager.targetById(info.ppid) || this._targetManager.mainTarget(), undefined, false, connection); 197 | target[NdbSdk.connectionSymbol] = connection; 198 | await this.addFileSystem(info.cwd, info.scriptName); 199 | if (info.scriptName) { 200 | const scriptURL = info.scriptName; 201 | const uiSourceCode = Workspace.workspace.uiSourceCodeForURL(scriptURL); 202 | const isBlackboxed = Bindings.blackboxManager.isBlackboxedURL(scriptURL, false); 203 | if (isBlackboxed) 204 | return connection.disconnect(); 205 | if (uiSourceCode) { 206 | if (Common.moduleSetting('pauseAtStart').get() && !isBlackboxed) 207 | Bindings.breakpointManager.setBreakpoint(uiSourceCode, 0, 0, '', true); 208 | else 209 | Common.Revealer.reveal(uiSourceCode); 210 | } 211 | } 212 | if (info.data === this._profilingNddData) 213 | this._profiling.add(target.id()); 214 | 215 | function userFriendlyName(info) { 216 | if (info.data === 'ndb/repl') 217 | return 'repl'; 218 | return info.argv.map(arg => { 219 | const index1 = arg.lastIndexOf('/'); 220 | const index2 = arg.lastIndexOf('\\'); 221 | if (index1 === -1 && index2 === -1) 222 | return arg; 223 | return arg.slice(Math.max(index1, index2) + 1); 224 | }).join(' '); 225 | } 226 | } 227 | 228 | disconnected(sessionId) { 229 | const target = this._targetManager.targetById(sessionId); 230 | if (target) { 231 | this._targetManager.removeTarget(target); 232 | target.dispose(); 233 | } 234 | } 235 | 236 | async terminalData(stream, data) { 237 | const content = await(await fetch(`data:application/octet-stream;base64,${data}`)).text(); 238 | if (content.startsWith('Debugger listening on') || content.startsWith('Debugger attached.') || content.startsWith('Waiting for the debugger to disconnect...')) 239 | return; 240 | await Ndb.backend.writeTerminalData(stream, data); 241 | this.dispatchEventToListeners(Ndb.NodeProcessManager.Events.TerminalData, content); 242 | } 243 | 244 | async _onExecutionContextDestroyed(event) { 245 | const executionContext = event.data; 246 | if (!executionContext.isDefault) 247 | return; 248 | return this._onWaitingForDisconnect({data: executionContext.target()}); 249 | } 250 | 251 | async _onWaitingForDisconnect(event) { 252 | const target = event.data; 253 | if (target.name() === 'repl') 254 | this.startRepl(); 255 | if (this._profiling && (this._profiling.has(target.id()) || this._profiling.has(target.parentTarget().id()))) { 256 | this._cpuProfiles.push({ 257 | profile: await target.model(SDK.CPUProfilerModel).stopRecording(), 258 | name: target.name(), 259 | id: target.id() 260 | }); 261 | this._profiling.delete(target.id()); 262 | if (this._profiling.size === 0) 263 | this._finishProfiling(); 264 | } 265 | const connection = target[NdbSdk.connectionSymbol]; 266 | if (connection) 267 | await connection.disconnect(); 268 | } 269 | 270 | async startRepl() { 271 | const code = btoa(`console.log('Welcome to the ndb %cR%cE%cP%cL%c!', 272 | 'color:#8bc34a', 'color:#ffc107', 'color:#ff5722', 'color:#2196f3', 'color:inherit'); 273 | process.title = 'ndb/repl'; 274 | process.on('uncaughtException', console.error); 275 | setInterval(_ => 0, 2147483647)//# sourceURL=repl.js`); 276 | const args = ['-e', `eval(Buffer.from('${code}', 'base64').toString())`]; 277 | const options = { ignoreOutput: true, data: 'ndb/repl' }; 278 | const node = Ndb.processInfo.nodeExecPath; 279 | return this.debug(node, args, options); 280 | } 281 | 282 | async debug(execPath, args, options) { 283 | options = options || {}; 284 | const debugId = options.data || String(++this._lastDebugId); 285 | if (!options.data) 286 | this._lastStarted = {execPath, args, debugId, isProfiling: !!this._finishProfiling}; 287 | 288 | return this._service.debug( 289 | execPath, args, { 290 | ...options, 291 | data: debugId, 292 | cwd: Ndb.processInfo.cwd 293 | }); 294 | } 295 | 296 | async profile(execPath, args, options) { 297 | // TODO(ak239): move it out here. 298 | await UI.viewManager.showView('timeline'); 299 | const action = UI.actionRegistry.action('timeline.toggle-recording'); 300 | await action.execute(); 301 | this._profilingNddData = String(++this._lastDebugId); 302 | this._profiling = new Set(); 303 | this.debug(execPath, args, { data: this._profilingNddData }); 304 | await new Promise(resolve => this._finishProfiling = resolve); 305 | this._profilingNddData = ''; 306 | await Promise.all(SDK.targetManager.models(SDK.CPUProfilerModel).map(profiler => profiler.stopRecording())); 307 | const controller = Timeline.TimelinePanel.instance()._controller; 308 | const mainProfile = this._cpuProfiles.find(data => !data.id.includes('#')); 309 | controller.traceEventsCollected([{ 310 | cat: SDK.TracingModel.DevToolsMetadataEventCategory, 311 | name: TimelineModel.TimelineModel.DevToolsMetadataEvent.TracingStartedInPage, 312 | ph: 'M', pid: 1, tid: mainProfile.id, ts: 0, 313 | args: {data: {sessionId: 1}} 314 | }]); 315 | for (const {profile, name, id} of this._cpuProfiles) { 316 | controller.traceEventsCollected([{ 317 | cat: SDK.TracingModel.DevToolsMetadataEventCategory, 318 | name: TimelineModel.TimelineModel.DevToolsMetadataEvent.TracingSessionIdForWorker, 319 | ph: 'M', pid: 1, tid: id, ts: 0, 320 | args: {data: {sessionId: 1, workerThreadId: id, workerId: id, url: name}} 321 | }]); 322 | controller.traceEventsCollected(TimelineModel.TimelineJSProfileProcessor.buildTraceProfileFromCpuProfile( 323 | profile, id, false, TimelineModel.TimelineModel.WorkerThreadName)); 324 | } 325 | this._cpuProfiles = []; 326 | await action.execute(); 327 | } 328 | 329 | async kill(target) { 330 | return target.runtimeAgent().invoke_evaluate({ 331 | expression: 'process.exit(-1)' 332 | }); 333 | } 334 | 335 | async restartLast() { 336 | if (!this._lastStarted) 337 | return; 338 | await Promise.all(SDK.targetManager.targets() 339 | .filter(target => target.id() !== '') 340 | .map(target => target.runtimeAgent().invoke_evaluate({ 341 | expression: `'${this._lastStarted.debugId}' === process.env.NDD_DATA && process.exit(-1)` 342 | }))); 343 | const {execPath, args, isProfiling} = this._lastStarted; 344 | if (!isProfiling) 345 | await this.debug(execPath, args); 346 | else 347 | await this.profile(execPath, args); 348 | } 349 | }; 350 | 351 | Ndb.NodeProcessManager.Events = { 352 | TerminalData: Symbol('terminalData') 353 | }; 354 | 355 | /** 356 | * @implements {UI.ActionDelegate} 357 | * @unrestricted 358 | */ 359 | Ndb.RestartActionDelegate = class { 360 | /** 361 | * @override 362 | * @param {!UI.Context} context 363 | * @param {string} actionId 364 | * @return {boolean} 365 | */ 366 | handleAction(context, actionId) { 367 | switch (actionId) { 368 | case 'ndb.restart': 369 | Ndb.nodeProcessManager.restartLast(); 370 | return true; 371 | } 372 | return false; 373 | } 374 | }; 375 | 376 | // Temporary hack until frontend with fix is rolled. 377 | // fix: TBA. 378 | SDK.Target.prototype.decorateLabel = function(label) { 379 | return this.name(); 380 | }; 381 | 382 | // Front-end does not respect modern toggle semantics, patch it. 383 | const originalToggle = DOMTokenList.prototype.toggle; 384 | DOMTokenList.prototype.toggle = function(token, force) { 385 | if (arguments.length === 1) 386 | force = !this.contains(token); 387 | return originalToggle.call(this, token, !!force); 388 | }; 389 | 390 | /** 391 | * @param {string} sourceMapURL 392 | * @param {string} compiledURL 393 | * @return {!Promise} 394 | * @this {SDK.TextSourceMap} 395 | */ 396 | SDK.TextSourceMap.load = async function(sourceMapURL, compiledURL) { 397 | const {payload, error} = await Ndb.backend.loadSourceMap(sourceMapURL, compiledURL); 398 | if (error || !payload) 399 | return null; 400 | 401 | let textSourceMap; 402 | try { 403 | textSourceMap = new SDK.TextSourceMap(compiledURL, sourceMapURL, payload); 404 | } catch (e) { 405 | Common.console.warn('DevTools failed to parse SourceMap: ' + sourceMapURL); 406 | return null; 407 | } 408 | 409 | const modulePrefix = await Ndb.backend.getNodeScriptPrefix(); 410 | for (const uiSourceCode of Workspace.workspace.uiSourceCodes()) { 411 | if (uiSourceCode.url() === compiledURL && uiSourceCode.project().type() === Workspace.projectTypes.Network) { 412 | const content = await uiSourceCode.requestContent(); 413 | if (content.startsWith(modulePrefix)) { 414 | for (const mapping of textSourceMap.mappings()) { 415 | if (!mapping.lineNumber) 416 | mapping.columnNumber += modulePrefix.length; 417 | } 418 | break; 419 | } 420 | } 421 | } 422 | 423 | return textSourceMap; 424 | }; 425 | -------------------------------------------------------------------------------- /front_end/ndb/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": [ 3 | { 4 | "type": "early-initialization", 5 | "className": "Ndb.NdbMain" 6 | }, 7 | { 8 | "type": "setting", 9 | "category": "Debugger", 10 | "title": "Set breakpoint at first line", 11 | "settingName": "pauseAtStart", 12 | "settingType": "boolean", 13 | "defaultValue": false 14 | }, 15 | { 16 | "type": "setting", 17 | "category": "Debugger", 18 | "title": "Autostart main", 19 | "settingName": "autoStartMain", 20 | "settingType": "boolean", 21 | "defaultValue": true 22 | }, 23 | { 24 | "type": "setting", 25 | "category": "Debugger", 26 | "title": "Blackbox internal/* Node.js scripts", 27 | "settingName": "blackboxInternalScripts", 28 | "settingType": "boolean", 29 | "defaultValue": true 30 | }, 31 | { 32 | "type": "action", 33 | "actionId": "ndb.restart", 34 | "className": "Ndb.RestartActionDelegate", 35 | "title": "Restart last run configuration", 36 | "bindings": [ 37 | { 38 | "platform": "windows,linux", 39 | "shortcut": "F5 Ctrl+R" 40 | }, 41 | { 42 | "platform": "mac", 43 | "shortcut": "Meta+R" 44 | } 45 | ] 46 | }, 47 | { 48 | "type": "@UI.ContextMenu.Provider", 49 | "contextTypes": [ 50 | "Workspace.UISourceCode" 51 | ], 52 | "className": "Ndb.ContextMenuProvider" 53 | } 54 | ], 55 | "dependencies": ["common", "sdk", "ndb_sdk", "bindings", "persistence", "components"], 56 | "scripts": [ 57 | "InspectorFrontendHostOverrides.js", 58 | "Connection.js", 59 | "FileSystem.js", 60 | "NdbMain.js" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /front_end/ndb_sdk/NodeRuntime.js: -------------------------------------------------------------------------------- 1 | Protocol.inspectorBackend.registerCommand('NodeRuntime.notifyWhenWaitingForDisconnect', [{'name': 'enabled', 'type': 'boolean', 'optional': false}], [], false); 2 | Protocol.inspectorBackend.registerEvent('NodeRuntime.waitingForDisconnect', []); 3 | 4 | NdbSdk.NodeRuntimeModel = class extends SDK.SDKModel { 5 | /** 6 | * @param {!SDK.Target} target 7 | */ 8 | constructor(target) { 9 | super(target); 10 | 11 | this._agent = target.nodeRuntimeAgent(); 12 | this.target().registerNodeRuntimeDispatcher(new NdbSdk.NodeRuntimeDispatcher(this)); 13 | this._agent.notifyWhenWaitingForDisconnect(true); 14 | } 15 | 16 | /** 17 | * @param {string} sessionId 18 | * @param {!Object} workerInfo 19 | * @param {boolean} waitingForDebugger 20 | */ 21 | _waitingForDisconnect() { 22 | this.dispatchEventToListeners(NdbSdk.NodeRuntimeModel.Events.WaitingForDisconnect, this.target()); 23 | } 24 | }; 25 | 26 | /** @enum {symbol} */ 27 | NdbSdk.NodeRuntimeModel.Events = { 28 | WaitingForDisconnect: Symbol('WaitingForDisconnect') 29 | }; 30 | 31 | NdbSdk.NodeRuntimeDispatcher = class { 32 | constructor(nodeRuntimeModel) { 33 | this._nodeRuntimeModel = nodeRuntimeModel; 34 | } 35 | 36 | waitingForDisconnect() { 37 | this._nodeRuntimeModel._waitingForDisconnect(); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /front_end/ndb_sdk/NodeWorker.js: -------------------------------------------------------------------------------- 1 | Protocol.inspectorBackend.registerCommand('NodeWorker.enable', [{'name': 'waitForDebuggerOnStart', 'type': 'boolean', 'optional': false}], [], false); 2 | Protocol.inspectorBackend.registerCommand('NodeWorker.disable', [], [], false); 3 | Protocol.inspectorBackend.registerCommand('NodeWorker.sendMessageToWorker', [{'name': 'message', 'type': 'string', 'optional': false}, {'name': 'sessionId', 'type': 'string', 'optional': false}], [], false); 4 | Protocol.inspectorBackend.registerCommand('NodeWorker.detach', [{'name': 'sessionId', 'type': 'string', 'optional': false}], [], false); 5 | Protocol.inspectorBackend.registerEvent('NodeWorker.attachedToWorker', ['sessionId', 'workerInfo', 'waitingForDebugger']); 6 | Protocol.inspectorBackend.registerEvent('NodeWorker.detachedFromWorker', ['sessionId']); 7 | Protocol.inspectorBackend.registerEvent('NodeWorker.receivedMessageFromWorker', ['sessionId', 'message']); 8 | 9 | NdbSdk.connectionSymbol = Symbol('connection'); 10 | 11 | NdbSdk.NodeWorkerModel = class extends SDK.SDKModel { 12 | /** 13 | * @param {!SDK.Target} target 14 | */ 15 | constructor(target) { 16 | super(target); 17 | 18 | this._sessions = new Map(); 19 | this._targets = new Map(); 20 | this._agent = target.nodeWorkerAgent(); 21 | this.target().registerNodeWorkerDispatcher(new NdbSdk.NodeWorkerDispatcher(this)); 22 | this._agent.invoke_enable({waitForDebuggerOnStart: true}); 23 | } 24 | 25 | /** 26 | * @param {string} message 27 | * @param {string} sessionId 28 | * @return {!Promise} 29 | */ 30 | sendMessageToWorker(message, sessionId) { 31 | return this._agent.sendMessageToWorker(message, sessionId); 32 | } 33 | 34 | /** 35 | * @param {string} sessionId 36 | * @return {!Promise} 37 | */ 38 | detach(sessionId) { 39 | return this._agent.detach(sessionId); 40 | } 41 | 42 | /** 43 | * @override 44 | */ 45 | dispose() { 46 | this._sessions.clear(); 47 | for (const target of this._targets.values()) { 48 | SDK.targetManager.removeTarget(target); 49 | target.dispose(); 50 | } 51 | this._targets.clear(); 52 | } 53 | 54 | /** 55 | * @param {string} sessionId 56 | * @param {!Object} workerInfo 57 | * @param {boolean} waitingForDebugger 58 | */ 59 | _attachedToWorker(sessionId, workerInfo, waitingForDebugger) { 60 | const id = this.target().id() + '#' + workerInfo.workerId; 61 | const connection = new NdbSdk.NodeWorkerConnection(sessionId, this); 62 | this._sessions.set(sessionId, connection); 63 | const target = SDK.targetManager.createTarget( 64 | id, workerInfo.title, SDK.Target.Type.Node, this.target(), 65 | undefined, false, connection); 66 | target[NdbSdk.connectionSymbol] = connection; 67 | this._targets.set(sessionId, target); 68 | target.runtimeAgent().runIfWaitingForDebugger(); 69 | } 70 | 71 | /** 72 | * @param {string} sessionId 73 | */ 74 | _detachedFromWorker(sessionId) { 75 | const session = this._sessions.get(sessionId); 76 | if (session) { 77 | this._sessions.delete(sessionId); 78 | const target = this._targets.get(sessionId); 79 | if (target) { 80 | SDK.targetManager.removeTarget(target); 81 | target.dispose(); 82 | this._targets.delete(sessionId); 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * @param {string} sessionId 89 | * @param {string} message 90 | */ 91 | _receivedMessageFromWorker(sessionId, message) { 92 | const session = this._sessions.get(sessionId); 93 | if (session) 94 | session.receivedMessageFromWorker(message); 95 | } 96 | }; 97 | 98 | NdbSdk.NodeWorkerConnection = class { 99 | constructor(sessionId, nodeWorkerModel) { 100 | this._onMessage = null; 101 | this._onDisconnect = null; 102 | this._sessionId = sessionId; 103 | this._nodeWorkerModel = nodeWorkerModel; 104 | } 105 | 106 | /** 107 | * @param {function((!Object|string))} onMessage 108 | */ 109 | setOnMessage(onMessage) { 110 | this._onMessage = onMessage; 111 | } 112 | 113 | /** 114 | * @param {function(string)} onDisconnect 115 | */ 116 | setOnDisconnect(onDisconnect) { 117 | this._onDisconnect = onDisconnect; 118 | } 119 | 120 | /** 121 | * @param {string} message 122 | */ 123 | sendRawMessage(message) { 124 | this._nodeWorkerModel.sendMessageToWorker(message, this._sessionId); 125 | } 126 | 127 | /** 128 | * @return {!Promise} 129 | */ 130 | disconnect() { 131 | return this._nodeWorkerModel.detach(this._sessionId); 132 | } 133 | 134 | /** 135 | * @param {string} message 136 | */ 137 | receivedMessageFromWorker(message) { 138 | if (this._onMessage) 139 | this._onMessage(message); 140 | } 141 | 142 | detachedFromWorker() { 143 | if (this._onDisconnect) 144 | this._onDisconnect(); 145 | } 146 | }; 147 | 148 | NdbSdk.NodeWorkerDispatcher = class { 149 | /** 150 | * @param {!NdbSdk.NodeWorkerModel} 151 | */ 152 | constructor(nodeWorkerModel) { 153 | this._nodeWorkerModel = nodeWorkerModel; 154 | } 155 | 156 | /** 157 | * @param {string} sessionId 158 | * @param {!Object} workerInfo 159 | * @param {boolean} waitingForDebugger 160 | */ 161 | attachedToWorker(sessionId, workerInfo, waitingForDebugger) { 162 | this._nodeWorkerModel._attachedToWorker(sessionId, workerInfo, waitingForDebugger); 163 | } 164 | 165 | /** 166 | * @param {string} sessionId 167 | */ 168 | detachedFromWorker(sessionId) { 169 | this._nodeWorkerModel._detachedFromWorker(sessionId); 170 | } 171 | 172 | /** 173 | * @param {string} sessionId 174 | * @param {string} message 175 | */ 176 | receivedMessageFromWorker(sessionId, message) { 177 | this._nodeWorkerModel._receivedMessageFromWorker(sessionId, message); 178 | } 179 | }; 180 | -------------------------------------------------------------------------------- /front_end/ndb_sdk/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": ["sdk"], 3 | "scripts": [ 4 | "NodeRuntime.js", 5 | "NodeWorker.js" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /front_end/ndb_ui/NodeProcesses.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | Ndb.NodeProcesses = class extends UI.VBox { 8 | constructor() { 9 | super(true); 10 | this.registerRequiredCSS('ndb_ui/nodeProcesses.css'); 11 | 12 | const toolbar = new UI.Toolbar('process-toolbar', this.contentElement); 13 | this._pauseAtStartCheckbox = new UI.ToolbarSettingCheckbox( 14 | Common.moduleSetting('pauseAtStart')); 15 | this._pauseAtStartCheckbox.element.id = 'pause-at-start-checkbox'; 16 | toolbar.appendToolbarItem(this._pauseAtStartCheckbox); 17 | 18 | this._emptyElement = this.contentElement.createChild('div', 'gray-info-message'); 19 | this._emptyElement.id = 'no-running-nodes-msg'; 20 | this._emptyElement.textContent = Common.UIString('No running nodes'); 21 | 22 | this._treeOutline = new UI.TreeOutlineInShadow(); 23 | this._treeOutline.registerRequiredCSS('ndb_ui/nodeProcesses.css'); 24 | this.contentElement.appendChild(this._treeOutline.element); 25 | this._treeOutline.element.classList.add('hidden'); 26 | 27 | this._targetToUI = new Map(); 28 | SDK.targetManager.observeTargets(this); 29 | } 30 | 31 | /** 32 | * @override 33 | * @param {!SDK.Target} target 34 | */ 35 | targetAdded(target) { 36 | if (target.id() === '') 37 | return; 38 | if (target.name() === 'repl') 39 | return; 40 | const f = UI.Fragment.build` 41 |
42 |
${target.name()}
43 |
44 |
45 |
46 |
47 |
48 |
49 | `; 50 | const debuggerModel = target.model(SDK.DebuggerModel); 51 | debuggerModel.addEventListener(SDK.DebuggerModel.Events.DebuggerPaused, () => { 52 | f.$('state').textContent = 'paused'; 53 | }); 54 | debuggerModel.addEventListener(SDK.DebuggerModel.Events.DebuggerResumed, () => { 55 | f.$('state').textContent = 'attached'; 56 | }); 57 | f.$('state').textContent = debuggerModel.isPaused() ? 'paused' : 'attached'; 58 | 59 | const buttons = f.$('controls-buttons'); 60 | const toolbar = new UI.Toolbar('', buttons); 61 | const button = new UI.ToolbarButton(Common.UIString('Kill'), 'largeicon-terminate-execution'); 62 | button.addEventListener(UI.ToolbarButton.Events.Click, _ => Ndb.nodeProcessManager.kill(target)); 63 | toolbar.appendToolbarItem(button); 64 | 65 | const treeElement = new UI.TreeElement(f.element()); 66 | treeElement.onselect = _ => { 67 | if (UI.context.flavor(SDK.Target) !== target) 68 | UI.context.setFlavor(SDK.Target, target); 69 | }; 70 | 71 | const parentTarget = target.parentTarget(); 72 | let parentTreeElement = this._treeOutline.rootElement(); 73 | if (parentTarget) { 74 | const parentUI = this._targetToUI.get(parentTarget); 75 | if (parentUI) 76 | parentTreeElement = parentUI.treeElement; 77 | } 78 | parentTreeElement.appendChild(treeElement); 79 | parentTreeElement.expand(); 80 | 81 | if (!this._targetToUI.size) { 82 | this._emptyElement.classList.add('hidden'); 83 | this._treeOutline.element.classList.remove('hidden'); 84 | } 85 | this._targetToUI.set(target, {treeElement, f}); 86 | } 87 | 88 | /** 89 | * @override 90 | * @param {!SDK.Target} target 91 | */ 92 | targetRemoved(target) { 93 | const ui = this._targetToUI.get(target); 94 | if (ui) { 95 | const parentTreeElement = ui.treeElement.parent; 96 | for (const child of ui.treeElement.children().slice()) { 97 | ui.treeElement.removeChild(child); 98 | parentTreeElement.appendChild(child); 99 | } 100 | parentTreeElement.removeChild(ui.treeElement); 101 | this._targetToUI.delete(target); 102 | } 103 | if (!this._targetToUI.size) { 104 | this._emptyElement.classList.remove('hidden'); 105 | this._treeOutline.element.classList.add('hidden'); 106 | } 107 | } 108 | 109 | _targetFlavorChanged({data: target}) { 110 | const treeElement = this._targetToUI.get(target); 111 | if (treeElement) 112 | treeElement.select(); 113 | } 114 | }; 115 | -------------------------------------------------------------------------------- /front_end/ndb_ui/RunConfiguration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | Ndb.RunConfiguration = class extends UI.VBox { 8 | constructor() { 9 | super(true); 10 | this.registerRequiredCSS('ndb_ui/runConfiguration.css'); 11 | this._items = new UI.ListModel(); 12 | this._list = new UI.ListControl(this._items, this, UI.ListMode.NonViewport); 13 | this.contentElement.appendChild(this._list.element); 14 | this.update(); 15 | } 16 | 17 | async update() { 18 | const configurations = []; 19 | const main = await Ndb.mainConfiguration(); 20 | if (main) 21 | configurations.push(main); 22 | const pkg = await Ndb.backend.pkg(); 23 | if (pkg) { 24 | const scripts = pkg.scripts || {}; 25 | this._items.replaceAll(configurations.concat(Object.keys(scripts).map(name => ({ 26 | name, 27 | command: scripts[name], 28 | args: ['run', name] 29 | })))); 30 | } 31 | } 32 | 33 | /** 34 | * @override 35 | * @param {!SDK.DebuggerModel} debuggerModel 36 | * @return {!Element} 37 | */ 38 | createElementForItem(item) { 39 | const f = UI.Fragment.build` 40 |
41 |
42 |
${item.name}
43 |
${item.command}
44 |
45 |
46 |
47 |
48 |
49 |
`; 50 | const buttons = f.$('controls-buttons'); 51 | const toolbar = new UI.Toolbar('', buttons); 52 | const runButton = new UI.ToolbarButton(Common.UIString('Run'), 'largeicon-play'); 53 | runButton.addEventListener(UI.ToolbarButton.Events.Click, this._runConfig.bind(this, item.execPath, item.args)); 54 | toolbar.appendToolbarItem(runButton); 55 | const profileButton = new UI.ToolbarButton(Common.UIString('Start recording..'), 'largeicon-start-recording'); 56 | profileButton.addEventListener(UI.ToolbarButton.Events.Click, this._profileConfig.bind(this, item.execPath, item.args)); 57 | toolbar.appendToolbarItem(profileButton); 58 | return f.element(); 59 | } 60 | 61 | async _runConfig(execPath, args) { 62 | await Ndb.nodeProcessManager.debug(execPath || await Ndb.npmExecPath(), args); 63 | } 64 | 65 | async _profileConfig(execPath, args) { 66 | await Ndb.nodeProcessManager.profile(execPath || await Ndb.npmExecPath(), args); 67 | } 68 | 69 | /** 70 | * @override 71 | * @param {!SDK.DebuggerModel} debuggerModel 72 | * @return {number} 73 | */ 74 | heightForItem(debuggerModel) { 75 | return 12; 76 | } 77 | 78 | /** 79 | * @override 80 | * @param {!SDK.DebuggerModel} debuggerModel 81 | * @return {boolean} 82 | */ 83 | isItemSelectable(debuggerModel) { 84 | return false; 85 | } 86 | 87 | /** 88 | * @override 89 | * @param {?Profiler.IsolateSelector.ListItem} from 90 | * @param {?Profiler.IsolateSelector.ListItem} to 91 | * @param {?Element} fromElement 92 | * @param {?Element} toElement 93 | */ 94 | selectedItemChanged(from, to, fromElement, toElement) {} 95 | }; 96 | -------------------------------------------------------------------------------- /front_end/ndb_ui/Terminal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | Terminal.applyAddon(fit); 8 | 9 | Ndb.Terminal = class extends UI.VBox { 10 | constructor() { 11 | super(true); 12 | this._init = false; 13 | this.registerRequiredCSS('xterm/dist/xterm.css'); 14 | this.element.addEventListener('contextmenu', this._onContextMenu.bind(this)); 15 | this._terminal = Ndb.Terminal._createTerminal(); 16 | this._terminal.on('resize', this._sendResize.bind(this)); 17 | this._terminal.on('data', this._sendData.bind(this)); 18 | Ndb.nodeProcessManager.addEventListener(Ndb.NodeProcessManager.Events.TerminalData, this._terminalData, this); 19 | } 20 | 21 | static _createTerminal() { 22 | const terminal = new Terminal(); 23 | let fontFamily; 24 | let fontSize = 11; 25 | if (Host.isMac()) { 26 | fontFamily = 'Menlo, monospace'; 27 | } else if (Host.isWin()) { 28 | fontFamily = 'Consolas, Lucida Console, Courier New, monospace'; 29 | fontSize = 12; 30 | } else { 31 | fontFamily = 'dejavu sans mono, monospace'; 32 | } 33 | terminal.setOption('fontFamily', fontFamily); 34 | terminal.setOption('fontSize', fontSize); 35 | terminal.setOption('cursorStyle', 'bar'); 36 | terminal.setOption('convertEol', true); 37 | return terminal; 38 | } 39 | 40 | async _restartService() { 41 | if (this._backend) 42 | this._backend.dispose(); 43 | const env = await Ndb.nodeProcessManager.env(); 44 | this._anotherTerminalHint(env); 45 | this._backend = await Ndb.backend.createService( 46 | 'terminal.js', 47 | rpc.handle(this), 48 | env, 49 | this._terminal.cols, 50 | this._terminal.rows); 51 | } 52 | 53 | _anotherTerminalHint(env) { 54 | this._terminal.writeln('# Want to use your own terminal? Copy paste following lines..'); 55 | this._terminal.writeln(''); 56 | this._terminal.writeln(Object.keys(env).map(k => `export ${k}='${env[k]}'`).join('\n')); 57 | this._terminal.writeln(''); 58 | this._terminal.writeln('# ..and after you can run any node program (e.g., npm run unit), ndb will detect it.'); 59 | this._terminal.writeln(''); 60 | } 61 | 62 | /** 63 | * @param {!Event} event 64 | */ 65 | _onContextMenu(event) { 66 | const selection = this._terminal ? this._terminal.getSelection() : null; 67 | const contextMenu = new UI.ContextMenu(event); 68 | const copyItem = contextMenu.defaultSection().appendItem(ls`Copy`, () => navigator.clipboard.writeText(selection)); 69 | copyItem.setEnabled(!!selection); 70 | contextMenu.defaultSection().appendItem(ls`Paste`, async() => { 71 | if (this._backend) 72 | this._backend.write(await navigator.clipboard.readText()); 73 | }); 74 | contextMenu.show(); 75 | } 76 | 77 | /** 78 | * @param {string} error 79 | */ 80 | async initFailed(error) { 81 | this._terminal.write('# Builtin terminal is unvailable: ' + error.replace(/\n/g, '\n#')); 82 | this._terminal.writeln(''); 83 | } 84 | 85 | /** 86 | * @param {string} data 87 | */ 88 | dataAdded(data) { 89 | if (data.startsWith('Debugger listening on') || data.startsWith('Debugger attached.') || data.startsWith('Waiting for the debugger to disconnect...')) 90 | return; 91 | this._terminal.write(data); 92 | } 93 | 94 | closed() { 95 | this._restartService(); 96 | } 97 | 98 | /** 99 | * @param {!{cols: number, rows: number}} size 100 | */ 101 | _sendResize(size) { 102 | if (this._backend) 103 | this._backend.resize(size.cols, size.rows); 104 | } 105 | 106 | /** 107 | * @param {string} data 108 | */ 109 | _sendData(data) { 110 | if (this._backend) 111 | this._backend.write(data); 112 | } 113 | 114 | onResize() { 115 | this._terminal.fit(); 116 | } 117 | 118 | _terminalData(event) { 119 | this._terminal.write(event.data); 120 | } 121 | 122 | wasShown() { 123 | if (this._init) 124 | return; 125 | this._init = true; 126 | this._terminal.open(this.contentElement); 127 | this._restartService(); 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /front_end/ndb_ui/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": [ 3 | { 4 | "type": "view", 5 | "category": "NDB", 6 | "id": "ndb.runView", 7 | "title": "NPM Scripts", 8 | "persistence": "permanent", 9 | "location": "run-view-sidebar", 10 | "className": "Ndb.RunConfiguration" 11 | }, 12 | { 13 | "type": "view", 14 | "location": "drawer-view", 15 | "id": "ndb.terminal", 16 | "title": "Terminal", 17 | "persistence": "permanent", 18 | "order": 1, 19 | "className": "Ndb.Terminal" 20 | }, 21 | { 22 | "type": "view", 23 | "id": "ndb.processes-view", 24 | "title": "Node processes", 25 | "persistence": "permanent", 26 | "className": "Ndb.NodeProcesses", 27 | "location": "sources.sidebar-top" 28 | } 29 | ], 30 | "dependencies": ["ui", "sources", "timeline", "ndb", "xterm"], 31 | "scripts": [ 32 | "RunConfiguration.js", 33 | "NodeProcesses.js", 34 | "Terminal.js" 35 | ], 36 | "resources": [ 37 | "runConfiguration.css", 38 | "nodeProcesses.css" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /front_end/ndb_ui/nodeProcesses.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | .process-toolbar { 8 | background-color: var(--toolbar-bg-color); 9 | border-bottom: var(--divider-border); 10 | } 11 | 12 | .tree-outline li { 13 | min-height: 20px; 14 | } 15 | 16 | .tree-outline li::before { 17 | display: none; 18 | } 19 | 20 | .process-item { 21 | width: 100%; 22 | display: flex; 23 | flex-wrap: wrap; 24 | } 25 | 26 | .process-item-state { 27 | color: #888; 28 | margin-left: auto; 29 | padding: 0 10px 0 10px; 30 | } 31 | 32 | .controls-container { 33 | display: flex; 34 | flex-direction: row; 35 | justify-content: flex-end; 36 | align-items: stretch; 37 | pointer-events: none; 38 | } 39 | 40 | .controls-gradient { 41 | flex: 0 1 50px; 42 | } 43 | 44 | li:hover .controls-gradient { 45 | background-image: linear-gradient(90deg, transparent, hsl(0, 0%, 96%)); 46 | } 47 | 48 | .controls-buttons { 49 | flex: none; 50 | display: flex; 51 | flex-direction: row; 52 | align-items: center; 53 | pointer-events: auto; 54 | visibility: hidden; 55 | } 56 | 57 | li:hover .controls-buttons { 58 | background-color: hsl(0, 0%, 96%); 59 | visibility: visible; 60 | } 61 | -------------------------------------------------------------------------------- /front_end/ndb_ui/runConfiguration.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | .list-item { 8 | flex: none; 9 | min-height: 30px; 10 | display: flex; 11 | align-items: center; 12 | position: relative; 13 | overflow: hidden; 14 | padding-bottom: 3px; 15 | } 16 | 17 | .list-item:hover { 18 | background: hsl(0, 0%, 96%); 19 | } 20 | 21 | .list-item { 22 | border-top: 1px solid #efefef; 23 | } 24 | 25 | .configuration-item { 26 | padding: 3px 5px 3px 5px; 27 | height: 30px; 28 | align-items: center; 29 | position: relative; 30 | flex: auto 1 0; 31 | width: 100%; 32 | } 33 | 34 | .configuration-command { 35 | color: #888; 36 | margin-left: auto; 37 | padding: 0 10px 0 10px; 38 | overflow: hidden; 39 | text-overflow: ellipsis; 40 | white-space: nowrap; 41 | } 42 | 43 | .controls-container { 44 | display: flex; 45 | flex-direction: row; 46 | justify-content: flex-end; 47 | align-items: stretch; 48 | pointer-events: none; 49 | } 50 | 51 | .controls-gradient { 52 | flex: 0 1 50px; 53 | } 54 | 55 | .list-item:hover .controls-gradient { 56 | background-image: linear-gradient(90deg, transparent, hsl(0, 0%, 96%)); 57 | } 58 | 59 | .controls-buttons { 60 | flex: none; 61 | display: flex; 62 | flex-direction: row; 63 | align-items: center; 64 | pointer-events: auto; 65 | visibility: hidden; 66 | } 67 | 68 | .list-item:hover .controls-buttons { 69 | background-color: hsl(0, 0%, 96%); 70 | visibility: visible; 71 | } -------------------------------------------------------------------------------- /front_end/xterm/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": [ 3 | "dist/xterm.js", 4 | "dist/addons/fit/fit.js" 5 | ], 6 | "resources": [ 7 | "dist/xterm.css" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /lib/backend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | const { rpc_process } = require('carlo/rpc'); 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | const readline = require('readline'); 11 | const util = require('util'); 12 | const { URL } = require('url'); 13 | const { Readable } = require('stream'); 14 | const opn = require('opn'); 15 | const querystring = require('querystring'); 16 | const which = require('which'); 17 | 18 | const fsReadFile = util.promisify(fs.readFile); 19 | const { fileURLToPath } = require('./filepath_to_url.js'); 20 | 21 | const MODULE_WRAP_PREFIX = (() => { 22 | const wrapped = require('module').wrap('☃'); 23 | return wrapped.substring(0, wrapped.indexOf('☃')); 24 | })(); 25 | 26 | class Backend { 27 | constructor(window) { 28 | this._window = window; 29 | this._info = JSON.parse(Buffer.from(window.paramsForReuse().data, 'base64').toString('utf8')); 30 | this._handles = []; 31 | this._window.on('close', () => this._handles.splice(0).forEach(handle => handle.dispose())); 32 | } 33 | 34 | async createService(name, ...args) { 35 | const fileName = path.join(__dirname, '..', 'services', name); 36 | const handle = await rpc_process.spawn(fileName, ...args); 37 | this._handles.push(handle); 38 | return handle; 39 | } 40 | 41 | bringToFront() { 42 | return this._window.bringToFront(); 43 | } 44 | 45 | /** 46 | * @param {text} url 47 | */ 48 | openInNewTab(url) { 49 | opn(url); 50 | } 51 | 52 | pkg() { 53 | // TODO(ak239spb): implement it as decorations over package.json file. 54 | try { 55 | return require(path.join(fileURLToPath(this._info.cwd), 'package.json')); 56 | } catch (e) { 57 | return null; 58 | } 59 | } 60 | 61 | async loadSourceMap(sourceMapURL, compiledURL) { 62 | try { 63 | let payload; 64 | if (sourceMapURL.startsWith('data:')) { 65 | const [metadata, ...other] = sourceMapURL.split(','); 66 | const urlPayload = other.join(','); 67 | const isBase64 = metadata.endsWith(';base64'); 68 | payload = JSON.parse(Buffer.from(isBase64 ? urlPayload : querystring.unescape(urlPayload), isBase64 ? 'base64' : 'utf8').toString('utf8')); 69 | } else { 70 | const fileURL = new URL(sourceMapURL); 71 | const content = await fsReadFile(fileURL, 'utf8'); 72 | payload = JSON.parse(content); 73 | } 74 | await removeSourceContentIfMatch(sourceMapURL, compiledURL, payload); 75 | return {payload}; 76 | } catch (e) { 77 | return {error: e.stack}; 78 | } 79 | } 80 | 81 | getNodeScriptPrefix() { 82 | return MODULE_WRAP_PREFIX; 83 | } 84 | 85 | which(command) { 86 | return new Promise(resolve => which(command, (error, resolvedPath) => resolve({ 87 | resolvedPath: resolvedPath, 88 | error: error ? error.message : null 89 | }))); 90 | } 91 | 92 | processInfo() { 93 | return this._info; 94 | } 95 | 96 | writeTerminalData(stream, data) { 97 | const buffer = Buffer.from(data, 'base64'); 98 | if (stream === 'stderr') 99 | process.stderr.write(buffer); 100 | else if (stream === 'stdout') 101 | process.stdout.write(buffer); 102 | } 103 | 104 | fileURLToPath(url) { 105 | return fileURLToPath(url); 106 | } 107 | 108 | async loadNetworkResource(url, headers) { 109 | try { 110 | if (url.startsWith('file://')) { 111 | const fileURL = new URL(url); 112 | return await fsReadFile(fileURL, 'utf8'); 113 | } else { 114 | return null; 115 | } 116 | } catch (e) { 117 | return null; 118 | } 119 | } 120 | } 121 | 122 | class StringStream extends Readable { 123 | constructor(str) { 124 | super(); 125 | this._str = str; 126 | this._ended = false; 127 | } 128 | 129 | _read() { 130 | if (this._ended) 131 | return; 132 | this._ended = true; 133 | process.nextTick(_ => { 134 | this.push(Buffer.from(this._str, 'utf8')); 135 | this.push(null); 136 | }); 137 | } 138 | } 139 | 140 | async function removeSourceContentIfMatch(sourceMapURL, compiledURL, payload) { 141 | const {sourcesContent, sources} = payload; 142 | if (!sourcesContent || !sources) 143 | return; 144 | for (let i = 0; i < sources.length; ++i) { 145 | if (!sources[i] || !sourcesContent[i]) continue; 146 | let url = sources[i]; 147 | if (!path.isAbsolute(url)) 148 | url = path.join(path.dirname(compiledURL), url); 149 | if (!fs.existsSync(url)) 150 | continue; 151 | const sourceContentStream = new StringStream(sourcesContent[i]); 152 | const sourceContentLines = await readLines(sourceContentStream); 153 | const fileStream = fs.createReadStream(url); 154 | const fileStreamLines = await readLines(fileStream); 155 | if (sourceContentLines.length === fileStreamLines.length) { 156 | let equal = true; 157 | for (let i = 0; i < sourceContentLines.length; ++i) { 158 | if (sourceContentLines[i] !== fileStreamLines[i]) { 159 | equal = false; 160 | break; 161 | } 162 | } 163 | if (equal) 164 | sourcesContent[i] = undefined; 165 | } 166 | } 167 | } 168 | 169 | async function readLines(stream) { 170 | const rl = readline.createInterface({ 171 | input: stream, 172 | crlfDelay: Infinity 173 | }); 174 | return new Promise(resolve => { 175 | stream.once('error', _ => resolve(null)); 176 | const lines = []; 177 | rl.on('line', line => lines.push(line)); 178 | rl.on('close', _ => resolve(lines)); 179 | }); 180 | } 181 | 182 | module.exports = { Backend }; 183 | -------------------------------------------------------------------------------- /lib/filepath_to_url.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | 3 | if (url.pathToFileURL) { 4 | module.exports = { 5 | pathToFileURL: url.pathToFileURL, 6 | fileURLToPath: url.fileURLToPath 7 | }; 8 | } else { 9 | // Node 8 does not have nice url methods. 10 | // Polyfill should match DevTools frontend behavior, 11 | // otherwise breakpoints will not work. 12 | function pathToFileURL(fileSystemPath) { 13 | fileSystemPath = fileSystemPath.replace(/\\/g, '/'); 14 | if (!fileSystemPath.startsWith('file://')) { 15 | if (fileSystemPath.startsWith('/')) 16 | fileSystemPath = 'file://' + fileSystemPath; 17 | else 18 | fileSystemPath = 'file:///' + fileSystemPath; 19 | } 20 | return fileSystemPath; 21 | } 22 | /** 23 | * @param {string} fileURL 24 | * @return {string} 25 | */ 26 | function fileURLToPath(fileURL) { 27 | if (process.platform === 'win32') 28 | return fileURL.substr('file:///'.length).replace(/\//g, '\\'); 29 | return fileURL.substr('file://'.length); 30 | } 31 | 32 | module.exports = { 33 | fileURLToPath, 34 | pathToFileURL, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /lib/launcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | const path = require('path'); 8 | const carlo = require('carlo'); 9 | const { rpc, rpc_process } = require('carlo/rpc'); 10 | const { pathToFileURL } = require('./filepath_to_url.js'); 11 | const { Backend } = require('./backend.js'); 12 | 13 | process.on('unhandledRejection', error => { 14 | if (error.message.includes('Protocol error') && error.message.includes('Target closed')) 15 | process.exit(1); 16 | console.log('unhandledRejection', error.stack || error.message); 17 | }); 18 | 19 | async function launch() { 20 | let app; 21 | const carloArgs = process.env.NDB_CARLO_ARGS ? JSON.parse(process.env.NDB_CARLO_ARGS) : {}; 22 | try { 23 | app = await carlo.launch({ 24 | bgcolor: '#242424', 25 | channel: ['chromium'], 26 | paramsForReuse: { 27 | data: Buffer.from(JSON.stringify({ 28 | cwd: pathToFileURL(process.cwd()).toString(), 29 | argv: process.argv, 30 | nodeExecPath: process.execPath 31 | })).toString('base64') 32 | }, 33 | ...carloArgs 34 | }); 35 | } catch (e) { 36 | if (e.message !== 'Could not start the browser or the browser was already running with the given profile.') 37 | throw e; 38 | process.exit(0); 39 | } 40 | 41 | process.title = 'ndb/main'; 42 | const appName = 'ndb'; 43 | const debugFrontend = !!process.env.NDB_DEBUG_FRONTEND; 44 | 45 | app.setIcon(path.join(__dirname, '..', 'front_end', 'favicon.png')); 46 | const overridesFolder = debugFrontend 47 | ? path.dirname(require.resolve(`../front_end/${appName}.json`)) 48 | : path.join(__dirname, '..', '.local-frontend'); 49 | app.serveFolder(overridesFolder); 50 | if (debugFrontend) { 51 | try { 52 | app.serveFolder(path.join(__dirname, '..', 'node_modules')); 53 | app.serveFolder(path.dirname(require.resolve(`chrome-devtools-frontend/front_end/ndb_app.json`))); 54 | } catch (e) { 55 | console.log('To use NDB_DEBUG_FRONTEND=1 you should run npm install from ndb folder first'); 56 | process.exit(1); 57 | } 58 | } 59 | app.on('exit', () => setTimeout(() => process.exit(0), 0)); 60 | app.on('window', load); 61 | load(app.mainWindow()); 62 | 63 | async function load(window) { 64 | const params = [ 65 | ['experiments', true], 66 | ['debugFrontend', debugFrontend], 67 | ['sources.hide_add_folder', true], 68 | ['sources.hide_thread_sidebar', true], 69 | ['timelineTracingJSProfileDisabled', true], 70 | ['panel', 'sources']]; 71 | const paramString = params.reduce((acc, p) => acc += `${p[0]}=${p[1]}&`, ''); 72 | window.load(`${appName}.html?${paramString}`, rpc.handle(new Backend(window))); 73 | } 74 | } 75 | 76 | module.exports = {launch}; 77 | -------------------------------------------------------------------------------- /lib/preload/ndb/preload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | (function() { 8 | if (!process.env.NDD_IPC) 9 | return; 10 | if (!process.env.NDD_PUBLISH_DATA) { 11 | try { 12 | if (!require('worker_threads').isMainThread) 13 | return; 14 | } catch (e) { 15 | // node 8 does not support workers 16 | } 17 | const { pathToFileURL } = require('../../filepath_to_url.js'); 18 | let scriptName = ''; 19 | try { 20 | scriptName = pathToFileURL(require.resolve(process.argv[1])).toString(); 21 | } catch (e) { 22 | // preload can get scriptName iff node starts with script as first argument, 23 | // we should be ready for exception in other cases, e.g., node -e '...' 24 | } 25 | const ppid = process.env.NDD_PPID; 26 | process.env.NDD_PPID = process.pid; 27 | if (!process.env.NDD_DATA) 28 | process.env.NDD_DATA = process.pid + '_ndbId'; 29 | process.versions['ndb'] = '1.1.5'; 30 | const inspector = require('inspector'); 31 | inspector.open(0, undefined, false); 32 | const info = { 33 | cwd: pathToFileURL(process.cwd()), 34 | argv: process.argv.concat(process.execArgv), 35 | data: process.env.NDD_DATA, 36 | ppid: ppid, 37 | id: String(process.pid), 38 | inspectorUrl: inspector.url(), 39 | scriptName: scriptName 40 | }; 41 | const {execFileSync} = require('child_process'); 42 | execFileSync(process.execPath, [__filename], { 43 | env: { 44 | NDD_IPC: process.env.NDD_IPC, 45 | NDD_PUBLISH_DATA: JSON.stringify(info) 46 | } 47 | }); 48 | } else { 49 | const net = require('net'); 50 | const TIMEOUT = 30000; 51 | const socket = net.createConnection(process.env.NDD_IPC, () => { 52 | socket.write(process.env.NDD_PUBLISH_DATA); 53 | const timeoutId = setTimeout(() => socket.destroy(), TIMEOUT); 54 | socket.on('data', () => { 55 | clearTimeout(timeoutId); 56 | socket.destroy(); 57 | }); 58 | }); 59 | socket.on('error', err => { 60 | process.stderr.write('\u001b[31mndb is not found:\u001b[0m\n'); 61 | process.stderr.write('please restart it and update env variables or unset NDD_IPC and NODE_OPTIONS.\n'); 62 | process.exit(0); 63 | }); 64 | } 65 | })(); 66 | // eslint-disable-next-line spaced-comment 67 | //# sourceURL=internal/preload.js 68 | -------------------------------------------------------------------------------- /lib/process_utility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | module.exports = prepareProcess; 8 | 9 | function prepareProcess(name, disposeCallback) { 10 | process.title = 'ndb/' + name; 11 | function silentRpcErrors(error) { 12 | if (!process.connected && error.code === 'ERR_IPC_CHANNEL_CLOSED') 13 | return; 14 | throw error; 15 | } 16 | process.on('uncaughtException', silentRpcErrors); 17 | process.on('unhandledRejection', silentRpcErrors); 18 | // dispose when child process is disconnected 19 | process.on('disconnect', () => disposeCallback()); 20 | } 21 | -------------------------------------------------------------------------------- /ndb.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * @license Copyright 2018 Google Inc. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 6 | */ 7 | 8 | require('update-notifier')({pkg: require('./package.json')}).notify({isGlobal: true}); 9 | 10 | if (process.argv.length > 2 && (process.argv[2] === '-v' || process.argv[2] === '--version')) { 11 | console.log(`v${require('./package.json').version}`); 12 | process.exit(0); 13 | } 14 | 15 | if (process.argv.length > 2 && process.argv[2] === '--help') { 16 | console.log('Usage:'); 17 | console.log(''); 18 | console.log('Use ndb instead of node command:'); 19 | console.log('\tndb server.js'); 20 | console.log('\tndb node server.js'); 21 | console.log(''); 22 | console.log('Prepend ndb in front of any other binary:'); 23 | console.log('\tndb npm run unit'); 24 | console.log('\tndb mocha'); 25 | console.log('\tndb npx mocha'); 26 | console.log(''); 27 | console.log('Launch ndb as a standalone application:'); 28 | console.log('\tndb .'); 29 | console.log(''); 30 | console.log('More information is available at https://github.com/GoogleChromeLabs/ndb#readme'); 31 | process.exit(0); 32 | } 33 | 34 | const {launch} = require('./lib/launcher.js'); 35 | launch(); 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ndb", 3 | "version": "1.1.5", 4 | "description": "Chrome DevTools for Node.js", 5 | "main": "ndb.js", 6 | "repository": "github:GoogleChromeLabs/ndb", 7 | "homepage": "https://github.com/GoogleChromeLabs/ndb#readme", 8 | "engines": { 9 | "node": ">=8.0.0" 10 | }, 11 | "bin": { 12 | "ndb": "./ndb.js" 13 | }, 14 | "scripts": { 15 | "lint": "./node_modules/.bin/eslint .", 16 | "prepare": "node build.js", 17 | "test": "npm run lint", 18 | "version": "node version.js && git add ./lib/preload/ndb/preload.js" 19 | }, 20 | "author": "The Chromium Authors", 21 | "license": "Apache-2.0", 22 | "dependencies": { 23 | "carlo": "^0.9.46", 24 | "chokidar": "^3.0.2", 25 | "debug": "^4.1.1", 26 | "isbinaryfile": "^3.0.3", 27 | "mime": "^2.4.4", 28 | "opn": "^5.5.0", 29 | "update-notifier": "^2.5.0", 30 | "which": "^1.3.1", 31 | "ws": "^6.2.1", 32 | "xterm": "^3.14.5" 33 | }, 34 | "optionalDependencies": { 35 | "node-pty": "^0.9.0-beta18" 36 | }, 37 | "devDependencies": { 38 | "chrome-devtools-frontend": "1.0.672485", 39 | "eslint": "^5.16.0", 40 | "mocha": "^5.2.0", 41 | "rimraf": "^2.6.3", 42 | "terser": "^3.17.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts/builder.js: -------------------------------------------------------------------------------- 1 | const fs = { 2 | ...require('fs'), 3 | ...require('fs').promises 4 | }; 5 | const os = require('os'); 6 | const path = require('path'); 7 | 8 | /** 9 | * @typedef {Object} AppDescriptor 10 | * @property {string} name 11 | * @property {!Array} modules 12 | * @property {boolean} hasHtml 13 | * @property {?string} extends 14 | */ 15 | 16 | /** 17 | * @typedef {Object} RawModuleDescriptor 18 | * @property {!Array} dependencies 19 | * @property {!Array} scripts 20 | * @property {!Array} resources 21 | * @property {!Array} extensions 22 | * @property {string} experiment 23 | */ 24 | 25 | /** 26 | * @typedef {Object} ModuleDescriptor 27 | * @property {string} content 28 | * @property {!Array=} extensions 29 | * @property {!Array=} dependencies 30 | * @property {string=} experiment 31 | */ 32 | 33 | /** 34 | * @param {!Array} appNames 35 | * @param {!Array} pathFolders 36 | * @return {!Promise>} 37 | */ 38 | async function loadAppDescriptors(appNames, pathFolders) { 39 | const descriptors = new Map(); 40 | const descriptorQueue = appNames.slice(0); 41 | while (descriptorQueue.length) { 42 | const name = descriptorQueue.shift(); 43 | if (descriptors.has(name)) 44 | continue; 45 | const source = await loadSource(pathFolders, name + '.json'); 46 | const content = JSON.parse(source); 47 | const descriptor = { 48 | name: name, 49 | modules: content.modules || [], 50 | hasHtml: content.has_html || false, 51 | }; 52 | if (content.extends) { 53 | descriptor.extends = content.extends; 54 | descriptorQueue.push(descriptor.extends); 55 | } 56 | descriptors.set(name, descriptor); 57 | } 58 | return descriptors; 59 | } 60 | 61 | /** 62 | * @param {string} moduleName 63 | * @return {string} 64 | */ 65 | function moduleNamespace(moduleName) { 66 | const specialCaseNameSpaces = { 67 | 'sdk': 'SDK', 68 | 'js_sdk': 'JSSDK', 69 | 'browser_sdk': 'BrowserSDK', 70 | 'ui': 'UI', 71 | 'object_ui': 'ObjectUI', 72 | 'perf_ui': 'PerfUI', 73 | 'har_importer': 'HARImporter', 74 | 'sdk_test_runner': 'SDKTestRunner', 75 | 'cpu_profiler_test_runner': 'CPUProfilerTestRunner' 76 | }; 77 | return moduleName in specialCaseNameSpaces 78 | ? specialCaseNameSpaces[moduleName] 79 | : moduleName.split('_').map(name => name.charAt(0).toUpperCase() + name.substr(1)).join(''); 80 | } 81 | 82 | /** 83 | * @param {!Map} appDescriptors 84 | * @param {!Array} pathFolders 85 | * @return {!Promise>} 86 | */ 87 | async function loadModules(appDescriptors, pathFolders, customLoadModuleSource) { 88 | const modules = new Map(); 89 | appDescriptors.forEach(descriptor => descriptor.modules.forEach(module => modules.set(module.name, null))); 90 | await Promise.all(Array.from(modules).map(async([moduleName, module]) => { 91 | modules.set(moduleName, await loadModule(pathFolders, moduleName, customLoadModuleSource)); 92 | })); 93 | return modules; 94 | } 95 | 96 | /** 97 | * @param {!Array} pathFolders 98 | * @param {string} moduleName 99 | * @param {!function(!Object):!Promise} customLoadModuleSource 100 | */ 101 | async function loadModule(pathFolders, moduleName, customLoadModuleSource) { 102 | const { descriptor: rawDescriptor, paths } = await loadRawModule(pathFolders, moduleName, 'module.json'); 103 | 104 | let scriptContent = await customLoadModuleSource(rawDescriptor, paths); 105 | const promises = []; 106 | if (scriptContent === null) { 107 | scriptContent = ''; 108 | promises.push(...(rawDescriptor.scripts || []).map(name => loadSource(pathFolders, moduleName, name))); 109 | } 110 | promises.push(...(rawDescriptor.resources || []).map(name => loadResource(pathFolders, moduleName, name))); 111 | scriptContent += (await Promise.all(promises)).join('\n'); 112 | 113 | const namespace = moduleNamespace(moduleName); 114 | const content = `self['${namespace}'] = self['${namespace}'] || {};\n${scriptContent}\n`; 115 | 116 | const descriptor = { content }; 117 | if (rawDescriptor.extensions) 118 | descriptor.extensions = rawDescriptor.extensions; 119 | if (rawDescriptor.dependencies) 120 | descriptor.dependencies = rawDescriptor.dependencies; 121 | if (rawDescriptor.experiment) 122 | descriptor.experiment = rawDescriptor.experiment; 123 | return descriptor; 124 | 125 | /** 126 | * @param {!Array} pathFolders 127 | * @param {string} moduleName 128 | * @param {string} name 129 | */ 130 | async function loadResource(pathFolders, moduleName, name) { 131 | const resource = await loadSource(pathFolders, moduleName, name); 132 | const content = resource.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/'/g, '\\\''); 133 | return `Runtime.cachedResources['${moduleName}/${name}'] = '${content}';`; 134 | } 135 | } 136 | 137 | /** 138 | * @param {!Map} moduleDescriptors 139 | * @return {!Promise>} 140 | */ 141 | async function loadImages(moduleDescriptors, pathFolders) { 142 | const images = []; 143 | const re = /Images\/[\w.-]+/g; 144 | for (const [, module] of moduleDescriptors) { 145 | const m = module.content.match(re); 146 | if (m) 147 | images.push(...m); 148 | } 149 | return Promise.all(Array.from(new Set(images)).map(async image => ({ 150 | content: await fs.readFile(lookupFile(pathFolders, image)[0]), 151 | name: image 152 | }))); 153 | } 154 | 155 | /** 156 | * @params {!Array} pathFolders 157 | * @param {!Array} fileNameParts 158 | * @return {!Array} 159 | */ 160 | function lookupFile(pathFolders, ...fileNameParts) { 161 | const paths = []; 162 | for (const pathFolder of pathFolders) { 163 | const absoluteFileName = path.join(pathFolder, ...fileNameParts); 164 | if (fs.existsSync(absoluteFileName)) 165 | paths.push(absoluteFileName); 166 | } 167 | if (paths.length === 0) 168 | console.error(`File ${fileNameParts.join(path.sep)} not found in ${pathFolders}`); 169 | return paths; 170 | } 171 | 172 | /** 173 | * @params {!Array} pathFolders 174 | * @param {!Array} fileNameParts 175 | * @return {!Promise} 176 | */ 177 | async function loadSource(pathFolders, ...fileNameParts) { 178 | const paths = lookupFile(pathFolders, ...fileNameParts).reverse(); 179 | return (await Promise.all(paths.map(name => fs.readFile(name, 'utf8')))).join('\n'); 180 | } 181 | 182 | /** 183 | * @params {!Array} pathFolders 184 | * @param {!Array} fileNameParts 185 | * @return {!Promise}>} 186 | */ 187 | async function loadRawModule(pathFolders, ...fileNameParts) { 188 | const paths = lookupFile(pathFolders, ...fileNameParts); 189 | const sources = await Promise.all(paths.map(name => fs.readFile(name, 'utf8'))); 190 | if (paths.length > 1) 191 | console.error('Module ' + fileNameParts[0] + ' overriden'); 192 | const descriptors = sources.map(data => JSON.parse(data)).reverse(); 193 | const descriptor = { 194 | dependencies: [], 195 | scripts: [], 196 | resources: [], 197 | extensions: [], 198 | experiment: '', 199 | ...descriptors[0] 200 | }; 201 | return { descriptor, paths: [path[0]] }; 202 | } 203 | 204 | /** 205 | * @param {!Array} appNames 206 | * @param {!Array} pathFolders 207 | * @param {string} outFolder 208 | * @param {function(string):string=} minifyJS 209 | * @return {!Promise} 210 | */ 211 | async function buildApp(appNames, pathFolders, outFolder, minifyJS = code => code, customLoadModuleSource = descriptor => Promise.resolve(null)) { 212 | const descriptors = await loadAppDescriptors(appNames, pathFolders); 213 | const modules = await loadModules(descriptors, pathFolders, customLoadModuleSource); 214 | const fetchedImages = await loadImages(modules, pathFolders); 215 | const runtime = await loadSource(pathFolders, 'Runtime.js'); 216 | 217 | const builtApps = []; 218 | const notAutoStartModules = new Set(); 219 | for (const appName of appNames) { 220 | const appDescriptor = { modules: [], hasHtml: false }; 221 | let current = descriptors.get(appName); 222 | while (current) { 223 | appDescriptor.modules.push(...current.modules); 224 | appDescriptor.hasHtml = appDescriptor.hasHtml || current.hasHtml; 225 | current = current.extends ? descriptors.get(current.extends) : null; 226 | } 227 | 228 | const moduleDescriptors = appDescriptor.modules.map(module => { 229 | const moduleName = module.name; 230 | const moduleDescriptor = modules.get(module.name); 231 | const descriptor = { name: moduleName, remote: false }; 232 | if (module.type !== 'autostart') 233 | descriptor.scripts = [`${moduleName}_module.js`]; 234 | if (moduleDescriptor.extensions) 235 | descriptor.extensions = moduleDescriptor.extensions; 236 | if (moduleDescriptor.dependencies) 237 | descriptor.dependencies = moduleDescriptor.dependencies; 238 | if (moduleDescriptor.experiment) 239 | descriptor.experiment = moduleDescriptor.experiment; 240 | return descriptor; 241 | }); 242 | 243 | const autoStartModulesByName = new Map(); 244 | appDescriptor.modules.map(module => { 245 | if (module.type === 'autostart') 246 | autoStartModulesByName.set(module.name, module); 247 | else 248 | notAutoStartModules.add(module.name); 249 | }); 250 | 251 | const appScript = await loadSource(pathFolders, appName + '.js'); 252 | 253 | let scriptContent = ''; 254 | scriptContent += '/* Runtime.js */\n' + runtime + '\n'; 255 | scriptContent += `allDescriptors.push(...${JSON.stringify(moduleDescriptors)});\n`; 256 | scriptContent += `applicationDescriptor = ${JSON.stringify(appDescriptor)};\n`; 257 | scriptContent += appScript; 258 | const visitedModule = new Set(); 259 | for (const [, module] of autoStartModulesByName) 260 | scriptContent += writeModule(modules, module, autoStartModulesByName, visitedModule); 261 | 262 | let htmlContent = ''; 263 | if (appDescriptor.hasHtml) { 264 | const content = await loadSource(pathFolders, appName + '.html'); 265 | htmlContent = content.replace(/<\/script>/, ''); 266 | } 267 | 268 | builtApps.push({ 269 | scriptContent, 270 | htmlContent, 271 | name: appName 272 | }); 273 | 274 | function writeModule(modules, module, autoStartModulesByName, visitedModule) { 275 | if (visitedModule.has(module.name)) 276 | return ''; 277 | visitedModule.add(module.name); 278 | const builtModule = modules.get(module.name); 279 | let content = ''; 280 | for (const dep of builtModule.dependencies) { 281 | const depModule = autoStartModulesByName.get(dep); 282 | if (!depModule) 283 | console.error(`Autostart module ${module.name} depends on not autostart module ${dep}`); 284 | content += writeModule(modules, depModule, autoStartModulesByName, visitedModule); 285 | } 286 | return content + builtModule.content; 287 | } 288 | } 289 | 290 | const favicon = await fs.readFile(lookupFile(pathFolders, 'favicon.png')[0]); 291 | 292 | const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devtools-frontend-')); 293 | const promises = []; 294 | for (const app of builtApps) { 295 | promises.push(fs.writeFile(path.join(tmpDir, app.name + '.js'), await minifyJS(app.scriptContent))); 296 | if (app.htmlContent) 297 | promises.push(fs.writeFile(path.join(tmpDir, app.name + '.html'), app.htmlContent)); 298 | } 299 | 300 | promises.push(...Array.from(notAutoStartModules).map(async moduleName => { 301 | await fs.mkdir(path.join(tmpDir, moduleName)); 302 | return fs.writeFile(path.join(tmpDir, moduleName, moduleName + '_module.js'), await minifyJS(modules.get(moduleName).content)); 303 | })); 304 | 305 | const createImageFolder = fs.mkdir(path.join(tmpDir, 'Images')); 306 | promises.push(...fetchedImages.map(async image => { 307 | await createImageFolder; 308 | if (image.content) 309 | return fs.writeFile(path.join(tmpDir, 'Images', image.name.substr('Images/'.length)), image.content); 310 | })); 311 | if (favicon) 312 | promises.push(fs.writeFile(path.join(tmpDir, 'favicon.png'), favicon)); 313 | await Promise.all(promises); 314 | 315 | await fs.rename(tmpDir, outFolder); 316 | } 317 | 318 | module.exports = { buildApp }; 319 | -------------------------------------------------------------------------------- /services/file_system.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const { rpc, rpc_process } = require('carlo/rpc'); 4 | const chokidar = require('chokidar'); 5 | const {pathToFileURL, fileURLToPath} = require('../lib/filepath_to_url.js'); 6 | 7 | class FileSystemHandler { 8 | constructor() { 9 | require('../lib/process_utility.js')('file_system', () => this.dispose()); 10 | this._watcher = null; 11 | this._embedderPath = ''; 12 | this._client = null; 13 | } 14 | 15 | startWatcher(embedderPath, exludePattern, client, mainFileName) { 16 | this._embedderPath = fileURLToPath(embedderPath); 17 | this._client = client; 18 | this._watcher = chokidar.watch([this._embedderPath], { 19 | ignored: new RegExp(exludePattern), 20 | awaitWriteFinish: true, 21 | ignorePermissionErrors: true 22 | }); 23 | const events = []; 24 | this._watcher.on('all', (event, name) => { 25 | if (event === 'add' || event === 'change' || event === 'unlink') { 26 | if (!events.length) 27 | setTimeout(() => client.filesChanged(events.splice(0)), 100); 28 | events.push({ 29 | type: event, 30 | name: pathToFileURL(name).toString() 31 | }); 32 | } 33 | }); 34 | this._watcher.on('error', console.error); 35 | } 36 | 37 | forceFileLoad(fileNameURL) { 38 | const fileName = fileURLToPath(fileNameURL); 39 | if (fileName.startsWith(this._embedderPath) && fs.existsSync(fileName)) 40 | this._client.filesChanged([{type: 'add', name: fileNameURL}]); 41 | } 42 | 43 | dispose() { 44 | this._watcher.close(); 45 | Promise.resolve().then(() => process.exit(0)); 46 | } 47 | } 48 | 49 | rpc_process.init(args => rpc.handle(new FileSystemHandler())); 50 | -------------------------------------------------------------------------------- /services/file_system_io.js: -------------------------------------------------------------------------------- 1 | const { rpc, rpc_process } = require('carlo/rpc'); 2 | const fs = require('fs'); 3 | const { URL } = require('url'); 4 | 5 | class FileSystemIO { 6 | constructor() { 7 | require('../lib/process_utility.js')('file_system_io', () => this.dispose()); 8 | } 9 | 10 | /** 11 | * @param {string} fileURL 12 | * @param {string} encoding 13 | */ 14 | readFile(fileURL, encoding) { 15 | return fs.readFileSync(new URL(fileURL), encoding); 16 | } 17 | 18 | /** 19 | * @param {string} fileURL 20 | * @param {string} content 21 | * @param {string} encoding 22 | */ 23 | writeFile(fileURL, content, encoding) { 24 | if (encoding === 'base64') 25 | content = Buffer.from(content, 'base64'); 26 | fs.writeFileSync(new URL(fileURL), content, {encoding: encoding}); 27 | } 28 | 29 | /** 30 | * @param {string} folderURL 31 | */ 32 | createFile(folderURL) { 33 | let name = 'NewFile'; 34 | let counter = 1; 35 | while (fs.existsSync(new URL(folderURL + '/' + name))) { 36 | name = 'NewFile' + counter; 37 | ++counter; 38 | } 39 | fs.writeFileSync(new URL(folderURL + '/' + name), ''); 40 | return folderURL + '/' + name; 41 | } 42 | 43 | /** 44 | * @param {string} fileURL 45 | */ 46 | deleteFile(fileURL) { 47 | try { 48 | fs.unlinkSync(new URL(fileURL)); 49 | return true; 50 | } catch (e) { 51 | return false; 52 | } 53 | } 54 | 55 | /** 56 | * @param {string} fileURL 57 | * @param {string} newName 58 | */ 59 | renameFile(fileURL, newName) { 60 | const newURL = new URL(fileURL.substr(0, fileURL.lastIndexOf('/') + 1) + newName); 61 | try { 62 | if (fs.existsSync(newURL)) return false; 63 | fs.renameSync(new URL(fileURL), newURL); 64 | return true; 65 | } catch (e) { 66 | return false; 67 | } 68 | } 69 | 70 | dispose() { 71 | Promise.resolve().then(() => process.exit(0)); 72 | } 73 | } 74 | 75 | rpc_process.init(args => rpc.handle(new FileSystemIO())); 76 | -------------------------------------------------------------------------------- /services/ndd_service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | const { spawn } = require('child_process'); 8 | const os = require('os'); 9 | const path = require('path'); 10 | const net = require('net'); 11 | const { fileURLToPath } = require('../lib/filepath_to_url.js'); 12 | 13 | const protocolDebug = require('debug')('ndd_service:protocol'); 14 | const caughtErrorDebug = require('debug', 'ndd_service:caught'); 15 | const { rpc, rpc_process } = require('carlo/rpc'); 16 | const WebSocket = require('ws'); 17 | 18 | function silentRpcErrors(error) { 19 | if (!process.connected && error.code === 'ERR_IPC_CHANNEL_CLOSED') 20 | return; 21 | throw error; 22 | } 23 | 24 | process.on('uncaughtException', silentRpcErrors); 25 | process.on('unhandledRejection', silentRpcErrors); 26 | 27 | const DebugState = { 28 | WS_OPEN: 1, 29 | WS_ERROR: 2, 30 | WS_CLOSE: 3, 31 | PROCESS_DISCONNECT: 4 32 | }; 33 | 34 | const CALL_EXIT_MESSAGE = JSON.stringify({ 35 | id: -1, 36 | method: 'Runtime.evaluate', 37 | params: { expression: 'process.exit(-1)' } 38 | }); 39 | 40 | class Channel { 41 | /** 42 | * @param {!WebSocket} ws 43 | */ 44 | constructor(ws) { 45 | this._ws = ws; 46 | this._handler = null; 47 | this._messageListener = this._messageReceived.bind(this); 48 | this._ws.on('message', this._messageListener); 49 | } 50 | 51 | /** 52 | * @param {string} message 53 | */ 54 | send(message) { 55 | if (this._ws.readyState === WebSocket.OPEN) { 56 | protocolDebug('>', message); 57 | this._ws.send(message); 58 | } 59 | } 60 | 61 | close() { 62 | this._ws.close(); 63 | } 64 | 65 | /** 66 | * @param {!Object} 67 | */ 68 | listen(handler) { 69 | this._handler = handler; 70 | } 71 | 72 | dispose() { 73 | this._ws.removeListener('message', this._messageListener); 74 | } 75 | 76 | /** 77 | * @param {string} message 78 | */ 79 | _messageReceived(message) { 80 | if (this._handler) { 81 | protocolDebug('<', message); 82 | this._handler.dispatchMessage(message); 83 | } 84 | } 85 | } 86 | 87 | class NddService { 88 | constructor(frontend) { 89 | process.title = 'ndb/ndd_service'; 90 | this._disconnectPromise = new Promise(resolve => process.once('disconnect', () => resolve(DebugState.PROCESS_DISCONNECT))); 91 | this._connected = new Set(); 92 | this._frontend = frontend; 93 | 94 | const pipePrefix = process.platform === 'win32' ? '\\\\.\\pipe\\' : os.tmpdir(); 95 | const pipeName = `node-ndb.${process.pid}.sock`; 96 | this._pipe = path.join(pipePrefix, pipeName); 97 | const server = net.createServer(socket => { 98 | socket.on('data', async d => { 99 | const runSession = await this._startSession(JSON.parse(d), frontend); 100 | socket.write('run'); 101 | runSession(); 102 | }); 103 | socket.on('error', e => caughtErrorDebug(e)); 104 | }).listen(this._pipe); 105 | server.unref(); 106 | } 107 | 108 | dispose() { 109 | process.disconnect(); 110 | } 111 | 112 | async _startSession(info, frontend) { 113 | const ws = new WebSocket(info.inspectorUrl); 114 | const openPromise = new Promise(resolve => ws.once('open', () => resolve(DebugState.WS_OPEN))); 115 | const errorPromise = new Promise(resolve => ws.once('error', () => resolve(DebugState.WS_ERROR))); 116 | const closePromise = new Promise(resolve => ws.once('close', () => resolve(DebugState.WS_CLOSE))); 117 | let state = await Promise.race([openPromise, errorPromise, closePromise, this._disconnectPromise]); 118 | if (state === DebugState.WS_OPEN) { 119 | this._connected.add(info.id); 120 | const channel = new Channel(ws); 121 | state = await Promise.race([frontend.detected(info, rpc.handle(channel)), this._disconnectPromise]); 122 | return async() => { 123 | if (state !== DebugState.PROCESS_DISCONNECT) 124 | state = await Promise.race([closePromise, errorPromise, this._disconnectPromise]); 125 | channel.dispose(); 126 | this._connected.delete(info.id); 127 | if (state !== DebugState.PROCESS_DISCONNECT) 128 | frontend.disconnected(info.id); 129 | else 130 | ws.send(CALL_EXIT_MESSAGE, () => ws.close()); 131 | }; 132 | } else { 133 | return async function() {}; 134 | } 135 | } 136 | 137 | env() { 138 | return { 139 | NODE_OPTIONS: `--require ndb/preload.js`, 140 | NODE_PATH: `${process.env.NODE_PATH || ''}${path.delimiter}${path.join(__dirname, '..', 'lib', 'preload')}`, 141 | NDD_IPC: this._pipe 142 | }; 143 | } 144 | 145 | async debug(execPath, args, options) { 146 | const env = this.env(); 147 | if (options.data) 148 | env.NDD_DATA = options.data; 149 | const p = spawn(execPath, args, { 150 | cwd: options.cwd ? fileURLToPath(options.cwd) : undefined, 151 | env: { ...process.env, ...env }, 152 | stdio: options.ignoreOutput ? 'ignore' : ['inherit', 'pipe', 'pipe'], 153 | windowsHide: true 154 | }); 155 | if (!options.ignoreOutput) { 156 | p.stderr.on('data', data => { 157 | if (process.connected) 158 | this._frontend.terminalData('stderr', data.toString('base64')); 159 | }); 160 | p.stdout.on('data', data => { 161 | if (process.connected) 162 | this._frontend.terminalData('stdout', data.toString('base64')); 163 | }); 164 | } 165 | const finishPromise = new Promise(resolve => { 166 | p.once('exit', resolve); 167 | p.once('error', resolve); 168 | }); 169 | const result = await Promise.race([finishPromise, this._disconnectPromise]); 170 | if (result === DebugState.PROCESS_DISCONNECT && !this._connected.has(p.pid)) { 171 | // The frontend can start the process but disconnects before it is 172 | // finished if it is blackboxed (e.g., npm process); in this case, we need 173 | // to kill this process here. 174 | p.kill(); 175 | } 176 | } 177 | } 178 | 179 | rpc_process.init(frontend => rpc.handle(new NddService(frontend))); 180 | -------------------------------------------------------------------------------- /services/search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | const { rpc, rpc_process } = require('carlo/rpc'); 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | 11 | const { fileURLToPath, pathToFileURL } = require('../lib/filepath_to_url.js'); 12 | 13 | const isbinaryfile = require('isbinaryfile'); 14 | 15 | // TODO(ak239): track changed files. 16 | 17 | class SearchBackend { 18 | constructor(frontend) { 19 | require('../lib/process_utility.js')('search', () => this.dispose()); 20 | this._frontend = frontend; 21 | this._activeIndexing = new Set(); 22 | this._index = new Map(); 23 | this._filesQueue = new Set(); 24 | 25 | this._lastFileNameIndex = 0; 26 | this._indexToFileName = new Map(); 27 | this._fileNameToIndex = new Map(); 28 | } 29 | 30 | /** 31 | * @param {number} requestId 32 | * @param {string} fileSystemPath 33 | */ 34 | async indexPath(requestId, fileSystemPath, excludedPattern) { 35 | fileSystemPath = fileURLToPath(fileSystemPath); 36 | const excludeRegex = new RegExp(excludedPattern); 37 | if (this._index.has(fileSystemPath)) { 38 | this._indexChangedFiles(requestId, fileSystemPath); 39 | return; 40 | } 41 | this._activeIndexing.add(requestId); 42 | const index = new Map(); 43 | const directories = [fileSystemPath]; 44 | const allFiles = []; 45 | while (directories.length) { 46 | if (!this._activeIndexing.has(requestId)) 47 | return; 48 | const directory = directories.shift(); 49 | await new Promise(done => fs.readdir(directory, 'utf8', async(err, files) => { 50 | if (err) { 51 | done(); 52 | return; 53 | } 54 | files = files.filter(file => !file.startsWith('.')); 55 | files = files.map(file => path.join(directory, file)); 56 | await Promise.all(files.map(file => new Promise(done => fs.stat(file, (err, stats) => { 57 | if (err) { 58 | done(); 59 | return; 60 | } 61 | const relativeName = path.relative(fileSystemPath, file); 62 | const testName = `/${relativeName}${stats.isDirectory() ? '/' : ''}`.replace(/\\/g, '/'); 63 | if (excludeRegex && excludeRegex.test(testName)) { 64 | done(); 65 | return; 66 | } 67 | if (stats.isDirectory()) 68 | directories.push(file); 69 | if (stats.isFile()) 70 | allFiles.push(file); 71 | done(); 72 | })))); 73 | done(); 74 | })); 75 | } 76 | 77 | const textFiles = []; 78 | for (const file of allFiles) { 79 | if (file.endsWith('.js') || file.endsWith('.json')) 80 | textFiles.push(file); 81 | else if (!await new Promise(resolve => isbinaryfile(file, (err, isBinary) => resolve(err || isBinary)))) 82 | textFiles.push(file); 83 | } 84 | this._frontend.indexingTotalWorkCalculated(requestId, fileSystemPath, textFiles.length); 85 | for (const fileName of textFiles) { 86 | if (!this._activeIndexing.has(requestId)) 87 | return; 88 | await this._indexFile(fileName, index); 89 | this._frontend.indexingWorked(requestId, fileSystemPath, 1); 90 | } 91 | this._index.set(fileSystemPath, index); 92 | this._frontend.indexingDone(requestId, fileSystemPath); 93 | this._activeIndexing.delete(requestId); 94 | } 95 | 96 | /** 97 | * @param {string} fileName 98 | * @param {!Map>} index 99 | * @return {!Promise} 100 | */ 101 | _indexFile(fileName, index) { 102 | const stream = fs.createReadStream(fileName, {encoding: 'utf8'}); 103 | return new Promise(done => { 104 | let prev = ''; 105 | const trigrams = new Set(); 106 | stream.on('error', finished.bind(this)); 107 | stream.on('data', chunk => { 108 | chunk = prev + chunk; 109 | chunk = chunk.toLowerCase(); 110 | while (chunk.length > 3) { 111 | trigrams.add(chunk.substring(0, 3)); 112 | chunk = chunk.substring(1); 113 | } 114 | prev = chunk; 115 | }); 116 | stream.on('end', finished.bind(this)); 117 | 118 | function finished() { 119 | let fileNameIndex; 120 | if (this._fileNameToIndex.has(fileName)) { 121 | fileNameIndex = this._fileNameToIndex.get(fileName); 122 | } else { 123 | fileNameIndex = ++this._lastFileNameIndex; 124 | this._indexToFileName.set(fileNameIndex, fileName); 125 | this._fileNameToIndex.set(fileName, fileNameIndex); 126 | } 127 | for (const trigram of trigrams) { 128 | let values = index.get(trigram); 129 | if (!values) { 130 | values = new Set(); 131 | index.set(trigram, values); 132 | } 133 | values.add(fileNameIndex); 134 | } 135 | done(); 136 | } 137 | }).then(() => stream.close()); 138 | } 139 | 140 | /** 141 | * @param {number} requestId 142 | * @param {string} fileSystemPath 143 | */ 144 | async _indexChangedFiles(requestId, fileSystemPath) { 145 | if (!this._filesQueue.size) { 146 | this._frontend.indexingDone(requestId); 147 | return; 148 | } 149 | this._activeIndexing.add(requestId); 150 | 151 | const allFiles = Array.from(this._filesQueue); 152 | this._filesQueue.clear(); 153 | 154 | const textFiles = []; 155 | for (const file of allFiles) { 156 | if (file.endsWith('.js') || file.endsWith('.json')) 157 | textFiles.push(file); 158 | else if (!await new Promise(resolve => isbinaryfile(file, (err, isBinary) => resolve(err || isBinary)))) 159 | textFiles.push(file); 160 | } 161 | 162 | this._frontend.indexingTotalWorkCalculated(requestId, textFiles.length); 163 | const index = this._index.get(fileSystemPath); 164 | while (textFiles.length) { 165 | if (!this._activeIndexing.has(requestId)) { 166 | for (const fileName of textFiles) 167 | this._filesQueue.add(fileName); 168 | return; 169 | } 170 | const fileName = textFiles.shift(); 171 | await this._indexFile(fileName, index); 172 | this._frontend.indexingWorked(requestId, 1); 173 | } 174 | this._activeIndexing.delete(requestId); 175 | this._frontend.indexingDone(requestId); 176 | } 177 | 178 | /** 179 | * @param {number} requestId 180 | */ 181 | stopIndexing(requestId) { 182 | this._activeIndexing.delete(requestId); 183 | } 184 | 185 | /** 186 | * @param {number} requestId 187 | * @param {string} fileSystemPath 188 | * @param {string} query 189 | */ 190 | searchInPath(requestId, fileSystemPath, query) { 191 | fileSystemPath = fileURLToPath(fileSystemPath); 192 | const index = this._index.get(fileSystemPath); 193 | let result = []; 194 | query = query.toLowerCase(); 195 | if (index && query.length === 0) { 196 | result = Array.from(new Set(index.values())); 197 | } else if (index && query.length > 2) { 198 | let resultSet = index.get(query.substring(0, 3)) || new Set(); 199 | for (let i = 1; i < query.length - 2; ++i) { 200 | const trigram = query.substring(i, i + 3); 201 | const current = index.get(trigram) || new Set(); 202 | const nextCurrent = new Set(); 203 | for (const file of current) { 204 | if (resultSet.has(file)) 205 | nextCurrent.add(file); 206 | } 207 | resultSet = nextCurrent; 208 | } 209 | result = Array.from(resultSet); 210 | } 211 | result = result.map(index => this._indexToFileName.get(index)); 212 | result = result.map(result => pathToFileURL(result).toString()); 213 | this._frontend.searchCompleted(requestId, fileSystemPath, result); 214 | } 215 | 216 | dispose() { 217 | Promise.resolve().then(() => process.exit(0)); 218 | } 219 | } 220 | 221 | rpc_process.init(frontend => rpc.handle(new SearchBackend(frontend))); 222 | -------------------------------------------------------------------------------- /services/terminal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | const fs = require('fs'); 8 | const { rpc, rpc_process } = require('carlo/rpc'); 9 | 10 | class Terminal { 11 | constructor(frontend, pty, env, cols, rows) { 12 | require('../lib/process_utility.js')('terminal', () => this.dispose()); 13 | let shell = process.env.SHELL; 14 | if (!shell || !fs.existsSync(shell)) 15 | shell = process.platform === 'win32' ? 'cmd.exe' : 'bash'; 16 | this._term = pty.spawn(shell, [], { 17 | name: 'xterm-color', 18 | cols: cols, 19 | rows: rows, 20 | cwd: process.cwd(), 21 | env: { 22 | ...process.env, 23 | ...env 24 | } 25 | }); 26 | this._term.on('data', data => frontend.dataAdded(data)); 27 | this._term.on('close', () => frontend.closed()); 28 | } 29 | 30 | dispose() { 31 | Promise.resolve().then(() => process.exit(0)); 32 | } 33 | 34 | resize(cols, rows) { 35 | this._term.resize(cols, rows); 36 | } 37 | 38 | write(data) { 39 | this._term.write(data); 40 | } 41 | } 42 | 43 | async function init(frontend, env, cols, rows) { 44 | try { 45 | const pty = require('node-pty'); 46 | return rpc.handle(new Terminal(frontend, pty, env, cols, rows)); 47 | } catch (e) { 48 | await frontend.initFailed(e.stack); 49 | setImmediate(() => process.exit(0)); 50 | return null; 51 | } 52 | } 53 | 54 | rpc_process.init(init); 55 | -------------------------------------------------------------------------------- /test/assets/test-project/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | console.log(42); 8 | -------------------------------------------------------------------------------- /test/assets/test-project/index.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | console.log(239); 8 | -------------------------------------------------------------------------------- /test/assets/test-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-project", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "run": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "run-module": "node --experimental-modules index.mjs", 10 | "run-module-without-flag": "node index.mjs", 11 | "atexit": "node -e \"process.once('exit', _ => console.log(42))\"" 12 | }, 13 | "keywords": [] 14 | } 15 | -------------------------------------------------------------------------------- /test/basic.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | const assert = require('assert'); 8 | const fs = require('fs'); 9 | const os = require('os'); 10 | const path = require('path'); 11 | const removeFolder = require('rimraf'); 12 | const util = require('util'); 13 | 14 | const {launch} = require('../lib/launcher.js'); 15 | const {ReleaseBuilder} = require('../scripts/build_release_application.js'); 16 | 17 | const fsMkdtemp = util.promisify(fs.mkdtemp); 18 | 19 | module.exports.addTests = function({testRunner}) { 20 | // eslint-disable-next-line 21 | const {beforeAll, afterAll} = testRunner; 22 | // eslint-disable-next-line 23 | const {it, fit, xit} = testRunner; 24 | xit('run configuration', async function() { 25 | const configDir = await fsMkdtemp(path.join(os.tmpdir(), 'ndb-test-')); 26 | const frontend = await launch({ 27 | configDir: configDir, 28 | argv: ['.'], 29 | cwd: path.join(__dirname, 'assets', 'test-project'), 30 | debugFrontend: false, 31 | doNotCopyPreferences: true 32 | }); 33 | 34 | const configItem = await frontend.waitForSelector('body /deep/ .list-item'); 35 | configItem.hover(); 36 | const runButton = await frontend.waitForSelector('body /deep/ .list-item /deep/ [aria-label=Run]', { 37 | visible: true 38 | }); 39 | runButton.click(); 40 | const consoleMessage = await frontend.waitForSelector('body /deep/ .console-message-wrapper:nth-child(3) .console-message-text'); 41 | assert.equal('42', await frontend.evaluate(x => x.innerText, consoleMessage)); 42 | 43 | await frontend.close(); 44 | await util.promisify(removeFolder)(configDir); 45 | }); 46 | 47 | xit('run, pause at start, kill', async function() { 48 | const configDir = await fsMkdtemp(path.join(os.tmpdir(), 'ndb-test-')); 49 | const frontend = await launch({ 50 | configDir: configDir, 51 | argv: ['.'], 52 | cwd: path.join(__dirname, 'assets', 'test-project'), 53 | debugFrontend: false, 54 | doNotCopyPreferences: true 55 | }); 56 | 57 | const [pauseAtStartCheckbox, configItem] = await Promise.all([ 58 | frontend.waitForSelector('body /deep/ #pause-at-start-checkbox'), 59 | frontend.waitForSelector('body /deep/ .list-item') 60 | ]); 61 | await pauseAtStartCheckbox.click(); 62 | configItem.hover(); 63 | const runButton = await frontend.waitForSelector('body /deep/ .list-item /deep/ [aria-label=Run]', { 64 | visible: true 65 | }); 66 | runButton.click(); 67 | const executionLine = await frontend.waitForSelector('.cm-execution-line .CodeMirror-line'); 68 | const executionLineText = await frontend.evaluate(x => x.innerText, executionLine); 69 | assert.equal(executionLineText, 'console.log(42);'); 70 | 71 | const processItem = await frontend.waitForSelector('body /deep/ li.selected'); 72 | processItem.hover(); 73 | 74 | const killButton = await frontend.waitForSelector('body /deep/ li.selected /deep/ [aria-label=Kill]'); 75 | killButton.click(); 76 | await frontend.waitForSelector('body /deep/ #no-running-nodes-msg', { 77 | visible: true 78 | }); 79 | 80 | await frontend.close(); 81 | await util.promisify(removeFolder)(configDir); 82 | }); 83 | 84 | xit('terminal', async function() { 85 | const configDir = await fsMkdtemp(path.join(os.tmpdir(), 'ndb-test-')); 86 | const frontend = await launch({ 87 | configDir: configDir, 88 | argv: ['.'], 89 | cwd: path.join(__dirname, 'assets', 'test-project'), 90 | debugFrontend: false, 91 | doNotCopyPreferences: true 92 | }); 93 | 94 | const [pauseAtStartCheckbox, terminalTab, resumeButton, consoleTab] = await Promise.all([ 95 | frontend.waitForSelector('body /deep/ #pause-at-start-checkbox'), 96 | frontend.waitForSelector('body /deep/ #tab-ndb\\.terminal'), 97 | frontend.waitForSelector('body /deep/ [aria-label="Pause script execution"]'), 98 | frontend.waitForSelector('body /deep/ #tab-console-view') 99 | ]); 100 | await pauseAtStartCheckbox.click(); 101 | terminalTab.click(); 102 | const terminal = await frontend.waitForSelector('body /deep/ .xterm-cursor-layer', { 103 | visible: true 104 | }); 105 | await frontend.click('body /deep/ .xterm-cursor-layer'); 106 | await frontend.type('body /deep/ .xterm-cursor-layer', 'node -e "console.log(42)"'); 107 | await terminal.press('Enter'); 108 | 109 | const executionLine = await frontend.waitForSelector('.cm-execution-line .CodeMirror-line'); 110 | const executionLineText = await frontend.evaluate(x => x.innerText, executionLine); 111 | assert.equal(executionLineText, 'console.log(42);'); 112 | 113 | resumeButton.click(); 114 | 115 | await frontend.waitForSelector('body /deep/ #no-running-nodes-msg', { 116 | visible: true 117 | }); 118 | 119 | consoleTab.click(); 120 | const consoleMessage = await frontend.waitForSelector('body /deep/ .console-message-wrapper:nth-child(2) .console-message-text'); 121 | assert.equal('42', await frontend.evaluate(x => x.innerText, consoleMessage)); 122 | 123 | await frontend.close(); 124 | await util.promisify(removeFolder)(configDir); 125 | }); 126 | 127 | xit('terminal exit', async function() { 128 | const configDir = await fsMkdtemp(path.join(os.tmpdir(), 'ndb-test-')); 129 | const frontend = await launch({ 130 | configDir: configDir, 131 | argv: ['.'], 132 | cwd: path.join(__dirname, 'assets', 'test-project'), 133 | debugFrontend: false, 134 | doNotCopyPreferences: true 135 | }); 136 | 137 | const [terminalTab, consoleTab] = await Promise.all([ 138 | frontend.waitForSelector('body /deep/ #tab-ndb\\.terminal'), 139 | frontend.waitForSelector('body /deep/ #tab-console-view'), 140 | ]); 141 | terminalTab.click(); 142 | const terminal = await frontend.waitForSelector('body /deep/ .xterm-cursor-layer', { 143 | visible: true 144 | }); 145 | await frontend.click('body /deep/ .xterm-cursor-layer'); 146 | await frontend.type('body /deep/ .xterm-cursor-layer', 'exit'); 147 | await terminal.press('Enter'); 148 | // we need better way to wait until terminal reconnected. 149 | await new Promise(resolve => setTimeout(resolve, 300)); 150 | await frontend.type('body /deep/ .xterm-cursor-layer', 'node -e "console.log(42)"'); 151 | await terminal.press('Enter'); 152 | 153 | consoleTab.click(); 154 | const consoleMessage = await frontend.waitForSelector('body /deep/ .console-message-wrapper:nth-child(2) .console-message-text'); 155 | assert.equal('42', await frontend.evaluate(x => x.innerText, consoleMessage)); 156 | 157 | await frontend.close(); 158 | await util.promisify(removeFolder)(configDir); 159 | }); 160 | 161 | xit('repl and uncaught error', async function() { 162 | const configDir = await fsMkdtemp(path.join(os.tmpdir(), 'ndb-test-')); 163 | const frontend = await launch({ 164 | configDir: configDir, 165 | argv: ['.'], 166 | cwd: path.join(__dirname, 'assets', 'test-project'), 167 | debugFrontend: false, 168 | doNotCopyPreferences: true 169 | }); 170 | const consolePrompt = await frontend.waitForSelector('body /deep/ #console-prompt'); 171 | await frontend.type('body /deep/ #console-prompt', 'require("child_process").spawn("!@#$%")'); 172 | await consolePrompt.press('Enter'); 173 | await frontend.type('body /deep/ #console-prompt', 'console.log(42)'); 174 | consolePrompt.press('Enter'); 175 | const consoleMessage = await frontend.waitForSelector('body /deep/ .console-message-wrapper:nth-child(6) .console-message-text'); 176 | assert.equal('42', await frontend.evaluate(x => x.innerText, consoleMessage)); 177 | await frontend.close(); 178 | await util.promisify(removeFolder)(configDir); 179 | }); 180 | 181 | beforeAll(async function(state) { 182 | const DEVTOOLS_DIR = path.dirname( 183 | require.resolve('chrome-devtools-frontend/front_end/shell.json')); 184 | const frontendFolder = await fsMkdtemp(path.join(os.tmpdir(), 'ndb-test-frontend-')); 185 | await new ReleaseBuilder([ 186 | path.join(__dirname, '..', 'front_end'), 187 | DEVTOOLS_DIR, 188 | path.join(__dirname, '..'), 189 | path.join(__dirname, '..', '..', '..') 190 | ], frontendFolder).buildApp('integration_test_runner'); 191 | state.frontendFolder = frontendFolder; 192 | }); 193 | 194 | afterAll(async function(state) { 195 | return util.promisify(removeFolder)(state.frontendFolder); 196 | }); 197 | 198 | it('breakpoint inside .mjs file', async function(state) { 199 | const configDir = await fsMkdtemp(path.join(os.tmpdir(), 'ndb-test-')); 200 | const frontend = await launch({ 201 | configDir: configDir, 202 | argv: ['.'], 203 | cwd: path.join(__dirname, 'assets', 'test-project'), 204 | debugFrontend: false, 205 | doNotCopyPreferences: true, 206 | appName: 'integration_test_runner', 207 | releaseFrontendFolder: state.frontendFolder, 208 | doNotProcessExit: true 209 | }); 210 | await setupHelpers(frontend); 211 | await frontend.showScriptSource('index.mjs'); 212 | await frontend.setBreakpoint(6, ''); 213 | await frontend.waitForConfigurations(); 214 | 215 | { 216 | frontend.runConfiguration('run-module'); 217 | const {frames: [{location}]} = await frontend.waitUntilPaused(); 218 | assert.equal(6, location.lineNumber); 219 | assert.equal(2, location.columnNumber); 220 | await frontend.resumeExecution(); 221 | } 222 | 223 | { 224 | frontend.runConfiguration('run-module-without-flag'); 225 | const {frames: [{location}]} = await frontend.waitUntilPaused(); 226 | assert.equal(6, location.lineNumber); 227 | assert.equal(2, location.columnNumber); 228 | await frontend.resumeExecution(); 229 | } 230 | 231 | await frontend.close(); 232 | await util.promisify(removeFolder)(configDir); 233 | }); 234 | 235 | it('Stay attached', async function(state) { 236 | const configDir = await fsMkdtemp(path.join(os.tmpdir(), 'ndb-test-')); 237 | const frontend = await launch({ 238 | configDir: configDir, 239 | argv: ['.'], 240 | cwd: path.join(__dirname, 'assets', 'test-project'), 241 | debugFrontend: false, 242 | doNotCopyPreferences: true, 243 | appName: 'integration_test_runner', 244 | releaseFrontendFolder: state.frontendFolder, 245 | doNotProcessExit: true 246 | }); 247 | await setupHelpers(frontend); 248 | await frontend.setSetting('waitAtEnd', true); 249 | frontend.runConfiguration('atexit'); 250 | await frontend.waitForConsoleMessage('42'); 251 | const processes = await frontend.nodeProcess(); 252 | processes.sort(); 253 | assert.equal(`node -e process.once('exit', _ => console.log(42))`, processes[0]); 254 | assert.equal(`node npm run atexit`, processes[1]); 255 | const targetDestroyed = frontend.waitTargetDestroyed(2); 256 | await frontend.killProcess(`node -e process.once('exit', _ => console.log(42))`); 257 | await targetDestroyed; 258 | assert.deepStrictEqual([], await frontend.nodeProcess()); 259 | await frontend.close(); 260 | await util.promisify(removeFolder)(configDir); 261 | }); 262 | }; 263 | 264 | // eslint-disable-next-line 265 | function sleep() { 266 | return new Promise(resolve => setTimeout(resolve, 2147483647)); 267 | } 268 | 269 | async function setupHelpers(frontend) { 270 | await frontend.evaluate(() => self.runtime.loadModulePromise('sources_test_runner')); 271 | await frontend.evaluate(_ => SourcesTestRunner.startDebuggerTest()); 272 | frontend.waitForConfigurations = function() { 273 | return this.waitForSelector('body /deep/ div.configuration-item'); 274 | }; 275 | 276 | frontend.showScriptSource = function(name) { 277 | return this.evaluate(name => SourcesTestRunner.showScriptSourcePromise(name), name); 278 | }; 279 | 280 | frontend.setBreakpoint = function(line, condition) { 281 | return this.evaluate((line, condition) => { 282 | const sourcesView = Sources.SourcesPanel.instance().sourcesView(); 283 | const frame = sourcesView.currentSourceFrame(); 284 | SourcesTestRunner.setBreakpoint(frame, line, condition, true); 285 | }, line, condition); 286 | }; 287 | 288 | frontend.runConfiguration = async function(name) { 289 | const handle = await this.evaluateHandle(name => { 290 | const items = runtime.sharedInstance(Ndb.RunConfiguration).contentElement.querySelectorAll('div.list-item'); 291 | return Array.from(items).find(e => e.innerText.split('\n')[0] === name); 292 | }, name); 293 | const element = handle.asElement(); 294 | await element.hover(); 295 | const runButton = await element.$('div.controls-buttons'); 296 | await runButton.click(); 297 | }; 298 | 299 | frontend.waitUntilPaused = function() { 300 | return this.evaluate(_ => new Promise(resolve => { 301 | SourcesTestRunner.waitUntilPaused(frames => resolve({frames: frames.map(frame => frame._payload)})); 302 | })); 303 | }; 304 | 305 | frontend.resumeExecution = function() { 306 | return this.evaluate(_ => new Promise(resolve => SourcesTestRunner.resumeExecution(resolve))); 307 | }; 308 | 309 | frontend.setSetting = function(name, value) { 310 | return this.evaluate((name, value) => Common.moduleSetting(name).set(value), name, value); 311 | }; 312 | 313 | frontend.waitForConsoleMessage = function(text) { 314 | return this.evaluate(text => 315 | new Promise(resolve => TestRunner.addSniffer(SDK.ConsoleModel.prototype, 'addMessage', msg => { 316 | console.log(msg.messageText); 317 | if (msg.messageText === text) 318 | resolve(); 319 | }, true)) 320 | , text); 321 | }; 322 | 323 | frontend.nodeProcess = function() { 324 | return this.evaluate(_ => { 325 | const titles = self.runtime.sharedInstance(Ndb.NodeProcesses).contentElement.querySelectorAll('div /deep/ .process-title'); 326 | return Array.from(titles).map(el => el.innerText); 327 | }); 328 | }; 329 | 330 | frontend.killProcess = async function(name) { 331 | const handle = await this.evaluateHandle(name => { 332 | const titles = self.runtime.sharedInstance(Ndb.NodeProcesses).contentElement.querySelectorAll('div /deep/ li'); 333 | return Array.from(titles).find(e => e.innerText.split('\n')[0] === name); 334 | }, name); 335 | const element = handle.asElement(); 336 | await element.hover(); 337 | const btn = await element.$('div.controls-buttons'); 338 | await btn.click(); 339 | }; 340 | 341 | frontend.waitTargetDestroyed = async function(num) { 342 | return this.evaluate(num => 343 | new Promise(resolve => Ndb.NodeProcessManager.instance().then(manager => { 344 | manager.addEventListener(Ndb.NodeProcessManager.Events.Finished, _ => !--num && resolve()); 345 | })) 346 | , num); 347 | }; 348 | } 349 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | const {TestRunner, Reporter} = require('../utils/testrunner/'); 8 | let parallel = 1; 9 | if (process.env.NDB_PARALLEL_TESTS) 10 | parallel = parseInt(process.env.NDB_PARALLEL_TESTS.trim(), 10); 11 | const timeout = 10000; 12 | const testRunner = new TestRunner({timeout, parallel}); 13 | 14 | require('./basic.spec.js').addTests({testRunner}); 15 | 16 | new Reporter(testRunner); 17 | testRunner.run(); 18 | -------------------------------------------------------------------------------- /test/platform.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | const {TestRunner, Reporter, Matchers} = require('../utils/testrunner/'); 8 | let parallel = 1; 9 | if (process.env.NDB_PARALLEL_TESTS) 10 | parallel = parseInt(process.env.NDB_PARALLEL_TESTS.trim(), 10); 11 | const timeout = 10000; 12 | const testRunner = new TestRunner({timeout, parallel}); 13 | const {expect} = new Matchers(); 14 | addTests(testRunner); 15 | new Reporter(testRunner); 16 | testRunner.run(); 17 | 18 | const { execFile } = require('child_process'); 19 | 20 | // Tests for specific Node platform features. 21 | function addTests(testRunner) { 22 | // eslint-disable-next-line 23 | const {beforeAll, afterAll} = testRunner; 24 | // eslint-disable-next-line 25 | const {it, fit, xit} = testRunner; 26 | 27 | xit('--title flag (fails on Node v8.x)', async function() { 28 | const result = await new Promise(resolve => execFile( 29 | process.execPath, ['--title=abc', '-p', 'process.title'], (error, stdout, stderr) => { 30 | resolve(stdout + stderr); 31 | })); 32 | expect(result).toBe('abc\n'); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /utils/testrunner/Matchers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = class Matchers { 18 | constructor(customMatchers = {}) { 19 | this._matchers = {}; 20 | Object.assign(this._matchers, DefaultMatchers); 21 | Object.assign(this._matchers, customMatchers); 22 | this.expect = this.expect.bind(this); 23 | } 24 | 25 | addMatcher(name, matcher) { 26 | this._matchers[name] = matcher; 27 | } 28 | 29 | expect(value) { 30 | return new Expect(value, this._matchers); 31 | } 32 | }; 33 | 34 | class Expect { 35 | constructor(value, matchers) { 36 | this.not = {}; 37 | this.not.not = this; 38 | for (const matcherName of Object.keys(matchers)) { 39 | const matcher = matchers[matcherName]; 40 | this[matcherName] = applyMatcher.bind(null, matcherName, matcher, false /* inverse */, value); 41 | this.not[matcherName] = applyMatcher.bind(null, matcherName, matcher, true /* inverse */, value); 42 | } 43 | 44 | function applyMatcher(matcherName, matcher, inverse, value, ...args) { 45 | const result = matcher.call(null, value, ...args); 46 | const message = `expect.${inverse ? 'not.' : ''}${matcherName} failed` + (result.message ? `: ${result.message}` : ''); 47 | if (result.pass === inverse) 48 | throw new Error(message); 49 | } 50 | } 51 | } 52 | 53 | const DefaultMatchers = { 54 | toBe: function(value, other, message) { 55 | message = message || `${value} == ${other}`; 56 | return { pass: value === other, message }; 57 | }, 58 | 59 | toBeFalsy: function(value, message) { 60 | message = message || `${value}`; 61 | return { pass: !value, message }; 62 | }, 63 | 64 | toBeTruthy: function(value, message) { 65 | message = message || `${value}`; 66 | return { pass: !!value, message }; 67 | }, 68 | 69 | toBeGreaterThan: function(value, other, message) { 70 | message = message || `${value} > ${other}`; 71 | return { pass: value > other, message }; 72 | }, 73 | 74 | toBeGreaterThanOrEqual: function(value, other, message) { 75 | message = message || `${value} >= ${other}`; 76 | return { pass: value >= other, message }; 77 | }, 78 | 79 | toBeLessThan: function(value, other, message) { 80 | message = message || `${value} < ${other}`; 81 | return { pass: value < other, message }; 82 | }, 83 | 84 | toBeLessThanOrEqual: function(value, other, message) { 85 | message = message || `${value} <= ${other}`; 86 | return { pass: value <= other, message }; 87 | }, 88 | 89 | toBeNull: function(value, message) { 90 | message = message || `${value} == null`; 91 | return { pass: value === null, message }; 92 | }, 93 | 94 | toContain: function(value, other, message) { 95 | message = message || `${value} ⊇ ${other}`; 96 | return { pass: value.includes(other), message }; 97 | }, 98 | 99 | toEqual: function(value, other, message) { 100 | const valueJson = stringify(value); 101 | const otherJson = stringify(other); 102 | message = message || `${valueJson} ≈ ${otherJson}`; 103 | return { pass: valueJson === otherJson, message }; 104 | }, 105 | 106 | toBeCloseTo: function(value, other, precision, message) { 107 | return { 108 | pass: Math.abs(value - other) < Math.pow(10, -precision), 109 | message 110 | }; 111 | } 112 | }; 113 | 114 | function stringify(value) { 115 | function stabilize(key, object) { 116 | if (typeof object !== 'object' || object === undefined || object === null) 117 | return object; 118 | const result = {}; 119 | for (const key of Object.keys(object).sort()) 120 | result[key] = object[key]; 121 | return result; 122 | } 123 | 124 | return JSON.stringify(stabilize(null, value), stabilize); 125 | } 126 | -------------------------------------------------------------------------------- /utils/testrunner/Multimap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | class Multimap { 18 | constructor() { 19 | this._map = new Map(); 20 | } 21 | 22 | set(key, value) { 23 | let set = this._map.get(key); 24 | if (!set) { 25 | set = new Set(); 26 | this._map.set(key, set); 27 | } 28 | set.add(value); 29 | } 30 | 31 | get(key) { 32 | let result = this._map.get(key); 33 | if (!result) 34 | result = new Set(); 35 | return result; 36 | } 37 | 38 | has(key) { 39 | return this._map.has(key); 40 | } 41 | 42 | hasValue(key, value) { 43 | const set = this._map.get(key); 44 | if (!set) 45 | return false; 46 | return set.has(value); 47 | } 48 | 49 | /** 50 | * @return {number} 51 | */ 52 | get size() { 53 | return this._map.size; 54 | } 55 | 56 | delete(key, value) { 57 | const values = this.get(key); 58 | const result = values.delete(value); 59 | if (!values.size) 60 | this._map.delete(key); 61 | return result; 62 | } 63 | 64 | deleteAll(key) { 65 | this._map.delete(key); 66 | } 67 | 68 | firstValue(key) { 69 | const set = this._map.get(key); 70 | if (!set) 71 | return null; 72 | return set.values().next().value; 73 | } 74 | 75 | firstKey() { 76 | return this._map.keys().next().value; 77 | } 78 | 79 | valuesArray() { 80 | const result = []; 81 | for (const key of this._map.keys()) 82 | result.push(...Array.from(this._map.get(key).values())); 83 | return result; 84 | } 85 | 86 | keysArray() { 87 | return Array.from(this._map.keys()); 88 | } 89 | 90 | clear() { 91 | this._map.clear(); 92 | } 93 | } 94 | 95 | module.exports = Multimap; 96 | -------------------------------------------------------------------------------- /utils/testrunner/README.md: -------------------------------------------------------------------------------- 1 | # TestRunner 2 | 3 | - testrunner is a library: no additional binary required; tests are `node.js` scripts 4 | - parallel wrt IO operations 5 | - supports async/await 6 | - modular 7 | - well-isolated state per execution thread 8 | 9 | Example 10 | 11 | ```js 12 | const {TestRunner, Reporter, Matchers} = require('../utils/testrunner'); 13 | 14 | // Runner holds and runs all the tests 15 | const runner = new TestRunner({ 16 | parallel: 2, // run 2 parallel threads 17 | timeout: 1000, // setup timeout of 1 second per test 18 | }); 19 | // Simple expect-like matchers 20 | const {expect} = new Matchers(); 21 | 22 | // Extract jasmine-like DSL into the global namespace 23 | const {describe, xdescribe, fdescribe} = runner; 24 | const {it, fit, xit} = runner; 25 | const {beforeAll, beforeEach, afterAll, afterEach} = runner; 26 | 27 | beforeAll(state => { 28 | state.parallelIndex; // either 0 or 1 in this example, depending on the executing thread 29 | state.foo = 'bar'; // set state for every test 30 | }); 31 | 32 | describe('math', () => { 33 | it('to be sane', async (state, test) => { 34 | state.parallelIndex; // Very first test will always be ran by the 0's thread 35 | state.foo; // this will be 'bar' 36 | expect(2 + 2).toBe(4); 37 | }); 38 | }); 39 | 40 | // Reporter subscribes to TestRunner events and displays information in terminal 41 | const reporter = new Reporter(runner); 42 | 43 | // Run all tests. 44 | runner.run(); 45 | ``` 46 | -------------------------------------------------------------------------------- /utils/testrunner/Reporter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const RED_COLOR = '\x1b[31m'; 18 | const GREEN_COLOR = '\x1b[32m'; 19 | const YELLOW_COLOR = '\x1b[33m'; 20 | const RESET_COLOR = '\x1b[0m'; 21 | 22 | class Reporter { 23 | constructor(runner) { 24 | this._runner = runner; 25 | runner.on('started', this._onStarted.bind(this)); 26 | runner.on('terminated', this._onTerminated.bind(this)); 27 | runner.on('finished', this._onFinished.bind(this)); 28 | runner.on('teststarted', this._onTestStarted.bind(this)); 29 | runner.on('testfinished', this._onTestFinished.bind(this)); 30 | } 31 | 32 | _onStarted() { 33 | this._timestamp = Date.now(); 34 | console.log(`Running ${YELLOW_COLOR}${this._runner.parallel()}${RESET_COLOR} worker(s):\n`); 35 | } 36 | 37 | _onTerminated(message, error) { 38 | this._printTestResults(); 39 | console.log(`${RED_COLOR}## TERMINATED ##${RESET_COLOR}`); 40 | console.log('Message:'); 41 | console.log(` ${RED_COLOR}${message}${RESET_COLOR}`); 42 | if (error && error.stack) { 43 | console.log('Stack:'); 44 | console.log(error.stack.split('\n').map(line => ' ' + line).join('\n')); 45 | } 46 | process.exit(2); 47 | } 48 | 49 | _onFinished() { 50 | this._printTestResults(); 51 | const failedTests = this._runner.failedTests(); 52 | process.exit(failedTests.length > 0 ? 1 : 0); 53 | } 54 | 55 | _printTestResults() { 56 | // 2 newlines after completing all tests. 57 | console.log('\n'); 58 | 59 | const failedTests = this._runner.failedTests(); 60 | if (failedTests.length > 0) { 61 | console.log('\nFailures:'); 62 | for (let i = 0; i < failedTests.length; ++i) { 63 | const test = failedTests[i]; 64 | console.log(`${i + 1}) ${test.fullName} (${formatLocation(test)})`); 65 | if (test.result === 'timedout') { 66 | console.log(' Message:'); 67 | console.log(` ${YELLOW_COLOR}Timeout Exceeded ${this._runner.timeout()}ms${RESET_COLOR}`); 68 | } else { 69 | console.log(' Message:'); 70 | console.log(` ${RED_COLOR}${test.error.message || test.error}${RESET_COLOR}`); 71 | console.log(' Stack:'); 72 | if (test.error.stack) { 73 | const stack = test.error.stack.split('\n').map(line => ' ' + line); 74 | let i = 0; 75 | while (i < stack.length && !stack[i].includes(__dirname)) 76 | ++i; 77 | while (i < stack.length && stack[i].includes(__dirname)) 78 | ++i; 79 | if (i < stack.length) { 80 | const indent = stack[i].match(/^\s*/)[0]; 81 | stack[i] = stack[i].substring(0, indent.length - 3) + YELLOW_COLOR + '⇨ ' + RESET_COLOR + stack[i].substring(indent.length - 1); 82 | } 83 | console.log(stack.join('\n')); 84 | } 85 | } 86 | if (test.output) { 87 | console.log(' Output:'); 88 | console.log(test.output.split('\n').map(line => ' ' + line).join('\n')); 89 | } 90 | console.log(''); 91 | } 92 | } 93 | 94 | const tests = this._runner.tests(); 95 | const skippedTests = tests.filter(test => test.result === 'skipped'); 96 | if (skippedTests.length > 0) { 97 | console.log('\nSkipped:'); 98 | for (let i = 0; i < skippedTests.length; ++i) { 99 | const test = skippedTests[i]; 100 | console.log(`${i + 1}) ${test.fullName}`); 101 | console.log(` ${YELLOW_COLOR}Temporary disabled with xit${RESET_COLOR} ${formatLocation(test)}\n`); 102 | } 103 | } 104 | 105 | const executedTests = tests.filter(test => test.result); 106 | console.log(`\nRan ${executedTests.length} of ${tests.length} test(s)`); 107 | const milliseconds = Date.now() - this._timestamp; 108 | const seconds = milliseconds / 1000; 109 | console.log(`Finished in ${YELLOW_COLOR}${seconds}${RESET_COLOR} seconds`); 110 | 111 | function formatLocation(test) { 112 | const location = test.location; 113 | if (!location) 114 | return ''; 115 | return `${location.fileName}:${location.lineNumber}:${location.columnNumber}`; 116 | } 117 | } 118 | 119 | _onTestStarted() { 120 | } 121 | 122 | _onTestFinished(test) { 123 | if (test.result === 'ok') 124 | process.stdout.write(`${GREEN_COLOR}.${RESET_COLOR}`); 125 | else if (test.result === 'skipped') 126 | process.stdout.write(`${YELLOW_COLOR}*${RESET_COLOR}`); 127 | else if (test.result === 'failed') 128 | process.stdout.write(`${RED_COLOR}F${RESET_COLOR}`); 129 | else if (test.result === 'timedout') 130 | process.stdout.write(`${RED_COLOR}T${RESET_COLOR}`); 131 | } 132 | } 133 | 134 | module.exports = Reporter; 135 | -------------------------------------------------------------------------------- /utils/testrunner/TestRunner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const path = require('path'); 18 | const EventEmitter = require('events'); 19 | const Multimap = require('./Multimap'); 20 | 21 | const TimeoutError = new Error('Timeout'); 22 | const TerminatedError = new Error('Terminated'); 23 | 24 | const MAJOR_NODEJS_VERSION = parseInt(process.version.substring(1).split('.')[0], 10); 25 | 26 | class UserCallback { 27 | constructor(callback, timeout) { 28 | this._callback = callback; 29 | this._terminatePromise = new Promise(resolve => { 30 | this._terminateCallback = resolve; 31 | }); 32 | 33 | this.timeout = timeout; 34 | this.location = this._getLocation(); 35 | } 36 | 37 | async run(...args) { 38 | const timeoutPromise = new Promise(resolve => { 39 | setTimeout(resolve.bind(null, TimeoutError), this.timeout); 40 | }); 41 | try { 42 | return await Promise.race([ 43 | Promise.resolve().then(this._callback.bind(null, ...args)).then(() => null).catch(e => e), 44 | timeoutPromise, 45 | this._terminatePromise 46 | ]); 47 | } catch (e) { 48 | return e; 49 | } 50 | } 51 | 52 | _getLocation() { 53 | const error = new Error(); 54 | const stackFrames = error.stack.split('\n').slice(1); 55 | // Find first stackframe that doesn't point to this file. 56 | for (let frame of stackFrames) { 57 | frame = frame.trim(); 58 | if (!frame.startsWith('at ')) 59 | return null; 60 | if (frame.endsWith(')')) { 61 | const from = frame.indexOf('('); 62 | frame = frame.substring(from + 1, frame.length - 1); 63 | } else { 64 | frame = frame.substring('at '.length + 1); 65 | } 66 | 67 | const match = frame.match(/^(.*):(\d+):(\d+)$/); 68 | if (!match) 69 | return null; 70 | const filePath = match[1]; 71 | const lineNumber = match[2]; 72 | const columnNumber = match[3]; 73 | if (filePath === __filename) 74 | continue; 75 | const fileName = filePath.split(path.sep).pop(); 76 | return { fileName, filePath, lineNumber, columnNumber }; 77 | } 78 | return null; 79 | } 80 | 81 | terminate() { 82 | this._terminateCallback(TerminatedError); 83 | } 84 | } 85 | 86 | const TestMode = { 87 | Run: 'run', 88 | Skip: 'skip', 89 | Focus: 'focus' 90 | }; 91 | 92 | const TestResult = { 93 | Ok: 'ok', 94 | Skipped: 'skipped', // User skipped the test 95 | Failed: 'failed', // Exception happened during running 96 | TimedOut: 'timedout', // Timeout Exceeded while running 97 | }; 98 | 99 | class Test { 100 | constructor(suite, name, callback, declaredMode, timeout) { 101 | this.suite = suite; 102 | this.name = name; 103 | this.fullName = (suite.fullName + ' ' + name).trim(); 104 | this.declaredMode = declaredMode; 105 | this._userCallback = new UserCallback(callback, timeout); 106 | this.location = this._userCallback.location; 107 | 108 | // Test results 109 | this.result = null; 110 | this.error = null; 111 | } 112 | } 113 | 114 | class Suite { 115 | constructor(parentSuite, name, declaredMode) { 116 | this.parentSuite = parentSuite; 117 | this.name = name; 118 | this.fullName = (parentSuite ? parentSuite.fullName + ' ' + name : name).trim(); 119 | this.declaredMode = declaredMode; 120 | /** @type {!Array<(!Test|!Suite)>} */ 121 | this.children = []; 122 | 123 | this.beforeAll = null; 124 | this.beforeEach = null; 125 | this.afterAll = null; 126 | this.afterEach = null; 127 | } 128 | } 129 | 130 | class TestPass { 131 | constructor(runner, rootSuite, tests, parallel) { 132 | this._runner = runner; 133 | this._parallel = parallel; 134 | this._runningUserCallbacks = new Multimap(); 135 | 136 | this._rootSuite = rootSuite; 137 | this._workerDistribution = new Multimap(); 138 | 139 | let workerId = 0; 140 | for (const test of tests) { 141 | // Reset results for tests that will be run. 142 | test.result = null; 143 | test.error = null; 144 | this._workerDistribution.set(test, workerId); 145 | for (let suite = test.suite; suite; suite = suite.parentSuite) 146 | this._workerDistribution.set(suite, workerId); 147 | // Do not shard skipped tests across workers. 148 | if (test.declaredMode !== TestMode.Skip) 149 | workerId = (workerId + 1) % parallel; 150 | } 151 | 152 | this._termination = null; 153 | } 154 | 155 | async run() { 156 | const terminations = [ 157 | createTermination.call(this, 'SIGINT', 'SIGINT received'), 158 | createTermination.call(this, 'SIGHUP', 'SIGHUP received'), 159 | createTermination.call(this, 'SIGTERM', 'SIGTERM received'), 160 | createTermination.call(this, 'unhandledRejection', 'UNHANDLED PROMISE REJECTION'), 161 | ]; 162 | for (const termination of terminations) 163 | process.on(termination.event, termination.handler); 164 | 165 | const workerPromises = []; 166 | for (let i = 0; i < this._parallel; ++i) 167 | workerPromises.push(this._runSuite(i, [this._rootSuite], {parallelIndex: i})); 168 | await Promise.all(workerPromises); 169 | 170 | for (const termination of terminations) 171 | process.removeListener(termination.event, termination.handler); 172 | return this._termination; 173 | 174 | function createTermination(event, message) { 175 | return { 176 | event, 177 | message, 178 | handler: error => this._terminate(message, error) 179 | }; 180 | } 181 | } 182 | 183 | async _runSuite(workerId, suitesStack, state) { 184 | if (this._termination) 185 | return; 186 | const currentSuite = suitesStack[suitesStack.length - 1]; 187 | if (!this._workerDistribution.hasValue(currentSuite, workerId)) 188 | return; 189 | await this._runHook(workerId, currentSuite, 'beforeAll', state); 190 | for (const child of currentSuite.children) { 191 | if (!this._workerDistribution.hasValue(child, workerId)) 192 | continue; 193 | if (child instanceof Test) { 194 | for (let i = 0; i < suitesStack.length; i++) 195 | await this._runHook(workerId, suitesStack[i], 'beforeEach', state, child); 196 | await this._runTest(workerId, child, state); 197 | for (let i = suitesStack.length - 1; i >= 0; i--) 198 | await this._runHook(workerId, suitesStack[i], 'afterEach', state, child); 199 | } else { 200 | suitesStack.push(child); 201 | await this._runSuite(workerId, suitesStack, state); 202 | suitesStack.pop(); 203 | } 204 | } 205 | await this._runHook(workerId, currentSuite, 'afterAll', state); 206 | } 207 | 208 | async _runTest(workerId, test, state) { 209 | if (this._termination) 210 | return; 211 | this._runner._willStartTest(test); 212 | if (test.declaredMode === TestMode.Skip) { 213 | test.result = TestResult.Skipped; 214 | this._runner._didFinishTest(test); 215 | return; 216 | } 217 | this._runningUserCallbacks.set(workerId, test._userCallback); 218 | const error = await test._userCallback.run(state, test); 219 | this._runningUserCallbacks.delete(workerId, test._userCallback); 220 | if (this._termination) 221 | return; 222 | test.error = error; 223 | if (!error) 224 | test.result = TestResult.Ok; 225 | else if (test.error === TimeoutError) 226 | test.result = TestResult.TimedOut; 227 | else 228 | test.result = TestResult.Failed; 229 | this._runner._didFinishTest(test); 230 | } 231 | 232 | async _runHook(workerId, suite, hookName, ...args) { 233 | if (this._termination) 234 | return; 235 | const hook = suite[hookName]; 236 | if (!hook) 237 | return; 238 | this._runningUserCallbacks.set(workerId, hook); 239 | const error = await hook.run(...args); 240 | this._runningUserCallbacks.delete(workerId, hook); 241 | if (error === TimeoutError) { 242 | const location = `${hook.location.fileName}:${hook.location.lineNumber}:${hook.location.columnNumber}`; 243 | const message = `${location} - Timeout Exceeded ${hook.timeout}ms while running "${hookName}" in suite "${suite.fullName}"`; 244 | this._terminate(message, null); 245 | } else if (error) { 246 | const location = `${hook.location.fileName}:${hook.location.lineNumber}:${hook.location.columnNumber}`; 247 | const message = `${location} - FAILED while running "${hookName}" in suite "${suite.fullName}"`; 248 | this._terminate(message, error); 249 | } 250 | } 251 | 252 | _terminate(message, error) { 253 | if (this._termination) 254 | return; 255 | this._termination = {message, error}; 256 | for (const userCallback of this._runningUserCallbacks.valuesArray()) 257 | userCallback.terminate(); 258 | } 259 | } 260 | 261 | class TestRunner extends EventEmitter { 262 | constructor(options = {}) { 263 | super(); 264 | this._rootSuite = new Suite(null, '', TestMode.Run); 265 | this._currentSuite = this._rootSuite; 266 | this._tests = []; 267 | // Default timeout is 10 seconds. 268 | this._timeout = options.timeout === 0 ? 2147483647 : options.timeout || 10 * 1000; 269 | this._parallel = options.parallel || 1; 270 | this._retryFailures = !!options.retryFailures; 271 | 272 | this._hasFocusedTestsOrSuites = false; 273 | 274 | if (MAJOR_NODEJS_VERSION >= 8) { 275 | const inspector = require('inspector'); 276 | if (inspector.url()) { 277 | console.log('TestRunner detected inspector; overriding certain properties to be debugger-friendly'); 278 | console.log(' - timeout = 0 (Infinite)'); 279 | this._timeout = 2147483647; 280 | this._parallel = 1; 281 | } 282 | } 283 | 284 | // bind methods so that they can be used as a DSL. 285 | this.describe = this._addSuite.bind(this, TestMode.Run); 286 | this.fdescribe = this._addSuite.bind(this, TestMode.Focus); 287 | this.xdescribe = this._addSuite.bind(this, TestMode.Skip); 288 | this.it = this._addTest.bind(this, TestMode.Run); 289 | this.fit = this._addTest.bind(this, TestMode.Focus); 290 | this.xit = this._addTest.bind(this, TestMode.Skip); 291 | this.beforeAll = this._addHook.bind(this, 'beforeAll'); 292 | this.beforeEach = this._addHook.bind(this, 'beforeEach'); 293 | this.afterAll = this._addHook.bind(this, 'afterAll'); 294 | this.afterEach = this._addHook.bind(this, 'afterEach'); 295 | } 296 | 297 | _addTest(mode, name, callback) { 298 | let suite = this._currentSuite; 299 | let isSkipped = suite.declaredMode === TestMode.Skip; 300 | while ((suite = suite.parentSuite)) 301 | isSkipped |= suite.declaredMode === TestMode.Skip; 302 | const test = new Test(this._currentSuite, name, callback, isSkipped ? TestMode.Skip : mode, this._timeout); 303 | this._currentSuite.children.push(test); 304 | this._tests.push(test); 305 | this._hasFocusedTestsOrSuites = this._hasFocusedTestsOrSuites || mode === TestMode.Focus; 306 | } 307 | 308 | _addSuite(mode, name, callback) { 309 | const oldSuite = this._currentSuite; 310 | const suite = new Suite(this._currentSuite, name, mode); 311 | this._currentSuite.children.push(suite); 312 | this._currentSuite = suite; 313 | callback(); 314 | this._currentSuite = oldSuite; 315 | this._hasFocusedTestsOrSuites = this._hasFocusedTestsOrSuites || mode === TestMode.Focus; 316 | } 317 | 318 | _addHook(hookName, callback) { 319 | assert(this._currentSuite[hookName] === null, `Only one ${hookName} hook available per suite`); 320 | const hook = new UserCallback(callback, this._timeout); 321 | this._currentSuite[hookName] = hook; 322 | } 323 | 324 | async run() { 325 | this.emit(TestRunner.Events.Started); 326 | const pass = new TestPass(this, this._rootSuite, this._runnableTests(), this._parallel); 327 | const termination = await pass.run(); 328 | if (termination) 329 | this.emit(TestRunner.Events.Terminated, termination.message, termination.error); 330 | else 331 | this.emit(TestRunner.Events.Finished); 332 | } 333 | 334 | timeout() { 335 | return this._timeout; 336 | } 337 | 338 | _runnableTests() { 339 | if (!this._hasFocusedTestsOrSuites) 340 | return this._tests; 341 | 342 | const tests = []; 343 | const blacklistSuites = new Set(); 344 | // First pass: pick "fit" and blacklist parent suites 345 | for (const test of this._tests) { 346 | if (test.declaredMode !== TestMode.Focus) 347 | continue; 348 | tests.push(test); 349 | for (let suite = test.suite; suite; suite = suite.parentSuite) 350 | blacklistSuites.add(suite); 351 | } 352 | // Second pass: pick all tests that belong to non-blacklisted "fdescribe" 353 | for (const test of this._tests) { 354 | let insideFocusedSuite = false; 355 | for (let suite = test.suite; suite; suite = suite.parentSuite) { 356 | if (!blacklistSuites.has(suite) && suite.declaredMode === TestMode.Focus) { 357 | insideFocusedSuite = true; 358 | break; 359 | } 360 | } 361 | if (insideFocusedSuite) 362 | tests.push(test); 363 | } 364 | return tests; 365 | } 366 | 367 | hasFocusedTestsOrSuites() { 368 | return this._hasFocusedTestsOrSuites; 369 | } 370 | 371 | tests() { 372 | return this._tests.slice(); 373 | } 374 | 375 | failedTests() { 376 | return this._tests.filter(test => test.result === 'failed' || test.result === 'timedout'); 377 | } 378 | 379 | parallel() { 380 | return this._parallel; 381 | } 382 | 383 | _willStartTest(test) { 384 | this.emit('teststarted', test); 385 | } 386 | 387 | _didFinishTest(test) { 388 | this.emit('testfinished', test); 389 | } 390 | } 391 | 392 | /** 393 | * @param {*} value 394 | * @param {string=} message 395 | */ 396 | function assert(value, message) { 397 | if (!value) 398 | throw new Error(message); 399 | } 400 | 401 | TestRunner.Events = { 402 | Started: 'started', 403 | TestStarted: 'teststarted', 404 | TestFinished: 'testfinished', 405 | Terminated: 'terminated', 406 | Finished: 'finished', 407 | }; 408 | 409 | module.exports = TestRunner; 410 | -------------------------------------------------------------------------------- /utils/testrunner/examples/fail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const {TestRunner, Reporter, Matchers} = require('..'); 18 | 19 | const runner = new TestRunner(); 20 | const reporter = new Reporter(runner); 21 | const {expect} = new Matchers(); 22 | 23 | const {describe, xdescribe, fdescribe} = runner; 24 | const {it, fit, xit} = runner; 25 | const {beforeAll, beforeEach, afterAll, afterEach} = runner; 26 | 27 | describe('testsuite', () => { 28 | it('toBe', async (state) => { 29 | expect(2 + 2).toBe(5); 30 | }); 31 | it('toBeFalsy', async (state) => { 32 | expect(true).toBeFalsy(); 33 | }); 34 | it('toBeTruthy', async (state) => { 35 | expect(false).toBeTruthy(); 36 | }); 37 | it('toBeGreaterThan', async (state) => { 38 | expect(2).toBeGreaterThan(3); 39 | }); 40 | it('toBeNull', async (state) => { 41 | expect(2).toBeNull(); 42 | }); 43 | it('toContain', async (state) => { 44 | expect('asdf').toContain('e'); 45 | }); 46 | it('not.toContain', async (state) => { 47 | expect('asdf').not.toContain('a'); 48 | }); 49 | it('toEqual', async (state) => { 50 | expect([1,2,3]).toEqual([1,2,3,4]); 51 | }); 52 | }); 53 | 54 | runner.run(); 55 | -------------------------------------------------------------------------------- /utils/testrunner/examples/hookfail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const {TestRunner, Reporter, Matchers} = require('..'); 18 | 19 | const runner = new TestRunner(); 20 | const reporter = new Reporter(runner); 21 | const {expect} = new Matchers(); 22 | 23 | const {describe, xdescribe, fdescribe} = runner; 24 | const {it, fit, xit} = runner; 25 | const {beforeAll, beforeEach, afterAll, afterEach} = runner; 26 | 27 | describe('testsuite', () => { 28 | beforeAll(() => { 29 | expect(false).toBeTruthy(); 30 | }); 31 | it('test', async () => { 32 | }); 33 | }); 34 | 35 | runner.run(); 36 | -------------------------------------------------------------------------------- /utils/testrunner/examples/hooktimeout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const {TestRunner, Reporter, Matchers} = require('..'); 18 | 19 | const runner = new TestRunner({ timeout: 100 }); 20 | const reporter = new Reporter(runner); 21 | const {expect} = new Matchers(); 22 | 23 | const {describe, xdescribe, fdescribe} = runner; 24 | const {it, fit, xit} = runner; 25 | const {beforeAll, beforeEach, afterAll, afterEach} = runner; 26 | 27 | describe('testsuite', () => { 28 | beforeAll(async () => { 29 | await new Promise(() => {}); 30 | }); 31 | it('something', async (state) => { 32 | }); 33 | }); 34 | 35 | runner.run(); 36 | -------------------------------------------------------------------------------- /utils/testrunner/examples/timeout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const {TestRunner, Reporter} = require('..'); 18 | 19 | const runner = new TestRunner({ timeout: 100 }); 20 | const reporter = new Reporter(runner); 21 | 22 | const {describe, xdescribe, fdescribe} = runner; 23 | const {it, fit, xit} = runner; 24 | const {beforeAll, beforeEach, afterAll, afterEach} = runner; 25 | 26 | describe('testsuite', () => { 27 | it('timeout', async (state) => { 28 | await new Promise(() => {}); 29 | }); 30 | }); 31 | 32 | runner.run(); 33 | -------------------------------------------------------------------------------- /utils/testrunner/examples/unhandledpromiserejection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const {TestRunner, Reporter} = require('..'); 18 | 19 | const runner = new TestRunner(); 20 | const reporter = new Reporter(runner); 21 | 22 | const {describe, xdescribe, fdescribe} = runner; 23 | const {it, fit, xit} = runner; 24 | const {beforeAll, beforeEach, afterAll, afterEach} = runner; 25 | 26 | describe('testsuite', () => { 27 | it('failure', async (state) => { 28 | Promise.reject(new Error('fail!')); 29 | }); 30 | it('slow', async () => { 31 | await new Promise(x => setTimeout(x, 1000)); 32 | }); 33 | }); 34 | 35 | runner.run(); 36 | -------------------------------------------------------------------------------- /utils/testrunner/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const TestRunner = require('./TestRunner'); 18 | const Reporter = require('./Reporter'); 19 | const Matchers = require('./Matchers'); 20 | 21 | module.exports = { TestRunner, Reporter, Matchers }; 22 | -------------------------------------------------------------------------------- /version.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | (function main() { 4 | const version = require('./package.json').version; 5 | let preload = fs.readFileSync('./lib/preload/ndb/preload.js', 'utf8'); 6 | preload = preload.replace(/process\.versions\['ndb'\] = '[\d+\.]+';/, `process.versions['ndb'] = '${version}';`); 7 | fs.writeFileSync('./lib/preload/ndb/preload.js', preload, 'utf8'); 8 | })(); 9 | --------------------------------------------------------------------------------