├── .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 | [](https://travis-ci.com/GoogleChromeLabs/ndb)
5 | [](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 | 
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{modificationTime: !Date, size: number}>}
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 |
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 |
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 |
--------------------------------------------------------------------------------