├── .editorconfig ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .prettierrc ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── eslint.config.mjs ├── killTree.sh ├── lib ├── download.test.mts ├── download.ts ├── index.ts ├── progress.ts ├── request.ts ├── runTest.ts └── util.ts ├── package-lock.json ├── package.json ├── pipeline.yml ├── sample ├── .github │ └── workflows │ │ └── ci.yml ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscodeignore ├── README.md ├── azure-pipelines.yml ├── package-lock.json ├── package.json ├── src │ ├── extension.ts │ ├── test-fixtures │ │ ├── fixture1 │ │ │ └── foo.js │ │ └── fixture2 │ │ │ └── bar.js │ └── test │ │ ├── runTest.ts │ │ ├── suite │ │ ├── extension.test.ts │ │ └── index.ts │ │ └── suite2 │ │ ├── extension.test.ts │ │ └── index.ts └── tsconfig.json ├── tsconfig.json └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Tab indentation 7 | [*] 8 | indent_style = tab 9 | trim_trailing_whitespace = true 10 | 11 | # The indent size used in the `package.json` file cannot be changed 12 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 13 | [{*.yml,*.yaml,package.json}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode-test 2 | node_modules 3 | 4 | out 5 | *.tgz 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .npmignore 3 | lib/ 4 | sample/ 5 | tsconfig.json 6 | tslint.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "printWidth": 120, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true, 5 | "editor.insertSpaces": true, 6 | "files.eol": "\n", 7 | "files.exclude": { 8 | "**/*.js": { 9 | "when": "$(basename).ts" 10 | }, 11 | "**/.DS_Store": true, 12 | "**/.git": true 13 | }, 14 | "files.trimTrailingWhitespace": true, 15 | "git.branchProtection": [ 16 | "main" 17 | ], 18 | "githubIssues.queries": [ 19 | { 20 | "label": "Recent Issues", 21 | "query": "state:open repo:${owner}/${repository} sort:updated-desc" 22 | } 23 | ], 24 | "prettier.semi": true 25 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "command": "npm", 4 | "isBackground": true, 5 | "args": ["run", "watch"], 6 | "problemMatcher": "$tsc-watch", 7 | "tasks": [ 8 | { 9 | "label": "npm", 10 | "type": "shell", 11 | "command": "npm", 12 | "args": [ 13 | "run", 14 | "watch" 15 | ], 16 | "isBackground": true, 17 | "problemMatcher": "$tsc-watch", 18 | "group": { 19 | "isDefault": true, 20 | "kind": "build" 21 | } 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 2.5.2 | 2024-04-09 4 | 5 | - Fix install issue on various platforms 6 | 7 | ### 2.5.1 | 2024-04-07 8 | 9 | - Allow downloading server versions 10 | 11 | ### 2.5.0 | 2024-02-04 12 | 13 | - Fix deleting old Insiders in electron not working 14 | 15 | ### 2.4.1 | 2024-07-05 16 | 17 | - Throw a typed `TestRunFailedError` on failure instead of a string. 18 | 19 | ### 2.4.0 | 2024-05-24 20 | 21 | - Allow installing unreleased builds using an `-unreleased` suffix, such as `insiders-unreleased`. 22 | - Allow passing different data directories in `runVSCodeCommand`, using it for extension development. 23 | - Improve the appearance progress reporting. 24 | 25 | ### 2.3.10 | 2024-05-13 26 | 27 | - Add `runVSCodeCommand` method and workaround for Node CVE-2024-27980 28 | 29 | ### 2.3.9 | 2024-01-19 30 | 31 | - Fix archive extraction on Windows failing when run under Electron 32 | 33 | ### 2.3.8 | 2023-11-24 34 | 35 | - Fix archive extraction on macOS and Linux sometimes failing 36 | 37 | ### 2.3.7 | 2023-11-23 38 | 39 | - Remove detection for unsupported win32 builds 40 | - Add length and hash validation for downloaded builds 41 | 42 | ### 2.3.6 | 2023-10-24 43 | 44 | - Fix windows sometimes failing on EPERM in download (again) 45 | 46 | ### 2.3.5 | 2023-10-04 47 | 48 | - Fix windows sometimes failing on EPERM in download 49 | 50 | ### 2.3.4 | 2023-07-31 51 | 52 | - Fix "insiders" string not matching correctly 53 | 54 | ### 2.3.3 | 2023-06-10 55 | 56 | - Disable GPU sandbox by default, fixing failures in some CI's. 57 | 58 | ### 2.3.2 | 2023-05-11 59 | 60 | - Fix download method not working for the vscode cli. 61 | 62 | ### 2.3.1 | 2023-04-04 63 | 64 | - Gracefully kill VS Code if SIGINT is received 65 | 66 | ### 2.3.0 | 2023-02-27 67 | 68 | - Automatically use the most recent version matching `engines.vscode` in extensions' package.json 69 | - Allow insiders `version`s to be specified, such as `version: "1.76.0-insider"` 70 | - Reduce the likelihood of 'broken' installations on interrupted downloads 71 | - Remove dependency on outdated `unzipper` module 72 | 73 | ### 2.2.4 | 2023-02-19 74 | 75 | - Use existing downloads if internet is inaccessible 76 | 77 | ### 2.2.3 | 2023-01-30 78 | 79 | - Fix tests sometimes hanging on windows 80 | 81 | ### 2.2.2 | 2023-01-09 82 | 83 | - Add default for platform in `resolveCliPathFromVSCodeExecutablePath` to match docs 84 | 85 | ### 2.2.1 | 2022-12-06 86 | 87 | - Add an idle `timeout` for downloads 88 | 89 | ### 2.1.5 | 2022-06-27 90 | 91 | - Automatically retry if VS Code download fails 92 | 93 | ### 2.1.4 | 2022-06-10 94 | 95 | - Fix uncaught error when failing to connect to the extension service 96 | 97 | ### 2.1.3 | 2022-03-04 98 | 99 | - Support arm64 builds on Linux 100 | 101 | ### 2.1.2 | 2022-02-04 102 | 103 | - Fix executable path being returned incorrectly on cross-platform downloads 104 | - Fix tests sometimes failing with EACCESS errors on OSX 105 | 106 | ### 2.1.1 | 2022-01-20 107 | 108 | - Fix excessive logging when running in CI 109 | 110 | ### 2.1.0 | 2022-01-14 111 | 112 | - Add a progress `reporter` option on the `TestOptions`, which can be used to see more detail or silence download progress. 113 | 114 | ### 2.0.3 | 2022-01-11 115 | 116 | - Fix `@vscode/test-electron` auto updating 117 | - Use arm64 version of VS Code on relevant platforms 118 | 119 | ### 2.0.2 | 2022-01-07 120 | 121 | - Add `resolveCliArgsFromVSCodeExecutablePath` 122 | 123 | ### 2.0.1 | 2021-12-29 124 | 125 | - Fix extra new lines added to test output 126 | 127 | ### 2.0.0 | 2021-12-14 128 | 129 | - Run tests using a separate instance of VS Code by default. This can be disabled by setting `reuseMachineInstall: true`. 130 | 131 | ### 1.6.2 | 2021-07-15 132 | 133 | - Add `--disable-workspace-trust` flag when running tests by default 134 | 135 | ### 1.6.1 | 2021-07-15 136 | 137 | - Rename to `@vscode/test-electron` 138 | 139 | ### 1.6.0 | 2021-07-14 140 | 141 | - Expose generic `download` API with support for `cachePath` 142 | 143 | ### 1.5.2 | 2021-03-29 144 | 145 | - Don't write progress report when output is not connected to tty [#91](https://github.com/microsoft/vscode-test/pull/91) 146 | 147 | ### 1.5.1 | 2021-01-25 148 | 149 | - Fix wrong http proxy agent used [#82](https://github.com/microsoft/vscode-test/issues/82) 150 | 151 | ### 1.5.0 | 2021-01-25 152 | 153 | - Fix download failing on windows with long file paths 154 | - Make installation platform aware [#78](https://github.com/microsoft/vscode-test/issues/78) 155 | - Download and unzip directly for faster setup 156 | - Add download progress indicator 157 | - Show signal that caused vscode to quit if no exit code is present [#64](https://github.com/microsoft/vscode-test/issues/64) 158 | 159 | ### 1.4.1 | 2020-10-27 160 | 161 | - Use "exit" event in runTest.ts. [#74](https://github.com/microsoft/vscode-test/issues/74). 162 | 163 | ### 1.4.0 | 2020-04-11 164 | 165 | - Propagate signal when subprocess terminates. [#56](https://github.com/microsoft/vscode-test/pull/56). 166 | 167 | ### 1.3.0 | 2019-12-11 168 | 169 | - Add `platform` option. By default, Windows/macOS/Linux defaults to use `win32-archive`, `darwin` and `linux-x64`. 170 | On Windows, `win32-x64-archive` is also available for using 64 bit version of VS Code. #18. 171 | - Allow running offline when `version` is specified and a matching version is found locally. #51. 172 | - Show error when failing to unzip downloaded vscode archive. #50. 173 | 174 | ### 1.2.3 | 2019-10-31 175 | 176 | - Add `--no-sandbox` option to default `launchArgs` for https://github.com/microsoft/vscode/issues/84238. 177 | 178 | ### 1.2.2 | 2019-10-31 179 | 180 | - Reject `downloadAndUnzipVSCode` when `https.get` fails to parse the JSON sent back from VS Code update server. #44. 181 | - Reject `downloadAndUnzipVSCode` promise when download fails due to network error. #49. 182 | 183 | ### 1.2.1 | 2019-10-31 184 | 185 | - Update https-proxy-agent for https://www.npmjs.com/advisories/1184. 186 | 187 | ### 1.2.0 | 2019-08-06 188 | 189 | - Remove downloaded Insiders at `.vscode-test/vscode-insiders` if it's outdated. [#25](https://github.com/microsoft/vscode-test/issues/25). 190 | 191 | ### 1.1.0 | 2019-08-02 192 | 193 | - Add `resolveCliPathFromVSCodeExecutablePath` that would resolve `vscodeExecutablePath` to VS Code CLI path, which can be used 194 | for extension management features such as `--install-extension` and `--uninstall-extension`. [#31](https://github.com/microsoft/vscode-test/issues/31). 195 | 196 | ### 1.0.2 | 2019-07-17 197 | 198 | - Revert faulty fix for #29. 199 | 200 | ### 1.0.1 | 2019-07-16 201 | 202 | - Use correct CLI path for launching VS Code on macOS / Linux. [#29](https://github.com/Microsoft/vscode-test/issues/29). 203 | 204 | ### 1.0.0 | 2019-07-03 205 | 206 | - Stable release for changes introduced in the `next` tags. 207 | 208 | ### 1.0.0-next.1 | 2019-06-24 209 | 210 | - Improve console message for downloading VS Code. [microsoft/vscode#76090](https://github.com/microsoft/vscode/issues/76090). 211 | - Improve logging. No more prefix `Spawn Error` and direct `stdout` and `stderr` of launched process to `console.log` and `console.error`. 212 | - `stable` added as a download version option. 213 | 214 | ### 1.0.0-next.0 | 2019-06-24 215 | 216 | - Updated API: 217 | - One single set of options. 218 | - `extensionPath` => `extensionDevelopmentPath` to align with VS Code launch flags 219 | - `testRunnerPath` => `extensionTestsPath` to align with VS Code launch flags 220 | - `testRunnerEnv` => `extensionTestsEnv` to align with VS Code launch flags 221 | - `additionalLaunchArgs` => `launchArgs` 222 | - `testWorkspace` removed. Pass path to file/folder/workspace as first argument to `launchArgs` instead. 223 | - `locale` removed. Pass `--locale` to `launchArgs` instead. 224 | 225 | ### 0.4.3 | 2019-05-30 226 | 227 | - Improved API documentation. 228 | 229 | ### 0.4.2 | 2019-05-24 230 | 231 | - `testWorkspace` is now optional. 232 | 233 | ### 0.4.1 | 2019-05-02 234 | 235 | - Fix Linux crash because `testRunnerEnv` is not merged with `process.env` for spawning the 236 | testing process. [#14](https://github.com/Microsoft/vscode-test/issues/14c). 237 | 238 | ### 0.4.0 | 2019-04-18 239 | 240 | - Add `testRunnerEnv` option. [#13](https://github.com/Microsoft/vscode-test/issues/13). 241 | 242 | ### 0.3.5 | 2019-04-17 243 | 244 | - Fix macOS Insiders incorrect url resolve. 245 | 246 | ### 0.3.4 | 2019-04-17 247 | 248 | - One more fix for Insiders url resolver. 249 | 250 | ### 0.3.3 | 2019-04-17 251 | 252 | - Correct Insiders download link. 253 | 254 | ### 0.3.2 | 2019-04-17 255 | 256 | - Correctly resolve Insider exectuable. [#12](https://github.com/Microsoft/vscode-test/issues/12). 257 | 258 | ### 0.3.1 | 2019-04-16 259 | 260 | - Log errors from stderr of the command to launch VS Code. 261 | 262 | ### 0.3.0 | 2019-04-13 263 | 264 | - 🙌 Add TypeScript as dev dependency. [#9](https://github.com/Microsoft/vscode-test/pull/9). 265 | - 🙌 Adding a simpler way of running tests with only `vscodeExecutablePath` and `launchArgs`. [#8](https://github.com/Microsoft/vscode-test/pull/8). 266 | 267 | ### 0.2.0 | 2019-04-12 268 | 269 | - 🙌 Set `ExecutionPolicy` for Windows unzip command. [#6](https://github.com/Microsoft/vscode-test/pull/6). 270 | - 🙌 Fix NPM http/https proxy handling. [#5](https://github.com/Microsoft/vscode-test/pull/5). 271 | - Fix the option `vscodeLaunchArgs` so it's being used for launching VS Code. [#7](https://github.com/Microsoft/vscode-test/issues/7). 272 | 273 | ### 0.1.5 | 2019-03-21 274 | 275 | - Log folder to download VS Code into. 276 | 277 | ### 0.1.4 | 2019-03-21 278 | 279 | - Add `-NoProfile`, `-NonInteractive` and `-NoLogo` for using PowerShell to extract VS Code. [#2](https://github.com/Microsoft/vscode-test/issues/2). 280 | - Use `Microsoft.PowerShell.Archive\Expand-Archive` to ensure using built-in `Expand-Archive`. [#2](https://github.com/Microsoft/vscode-test/issues/2). 281 | 282 | ### 0.1.3 | 2019-03-21 283 | 284 | - Support specifying testing locale. [#1](https://github.com/Microsoft/vscode-test/pull/1). 285 | - Fix zip extraction failure where `.vscode-test/vscode-` dir doesn't exist on Linux. [#3](https://github.com/Microsoft/vscode-test/issues/3). 286 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-test 2 | 3 | ![Test Status Badge](https://github.com/microsoft/vscode-test/workflows/Tests/badge.svg) 4 | 5 | This module helps you test VS Code extensions. Note that new extensions may want to use the [VS Code Test CLI](https://github.com/microsoft/vscode-test-cli/blob/main/README.md), which leverages this module, for a richer editing and execution experience. 6 | 7 | Supported: 8 | 9 | - Node >= 16.x 10 | - Windows >= Windows Server 2012+ / Win10+ (anything with Powershell >= 5.0) 11 | - macOS 12 | - Linux 13 | 14 | ## Usage 15 | 16 | See [./sample](./sample) for a runnable sample, with [Azure DevOps Pipelines](https://github.com/microsoft/vscode-test/blob/main/sample/azure-pipelines.yml) and [Github Actions](https://github.com/microsoft/vscode-test/blob/main/sample/.github/workflows/ci.yml) configuration. 17 | 18 | ```ts 19 | import { runTests, runVSCodeCommand, downloadAndUnzipVSCode } from '@vscode/test-electron'; 20 | 21 | async function go() { 22 | try { 23 | const extensionDevelopmentPath = path.resolve(__dirname, '../../../'); 24 | const extensionTestsPath = path.resolve(__dirname, './suite'); 25 | 26 | /** 27 | * Basic usage 28 | */ 29 | await runTests({ 30 | extensionDevelopmentPath, 31 | extensionTestsPath, 32 | }); 33 | 34 | const extensionTestsPath2 = path.resolve(__dirname, './suite2'); 35 | const testWorkspace = path.resolve(__dirname, '../../../test-fixtures/fixture1'); 36 | 37 | /** 38 | * Running another test suite on a specific workspace 39 | */ 40 | await runTests({ 41 | extensionDevelopmentPath, 42 | extensionTestsPath: extensionTestsPath2, 43 | launchArgs: [testWorkspace], 44 | }); 45 | 46 | /** 47 | * Use 1.36.1 release for testing 48 | */ 49 | await runTests({ 50 | version: '1.36.1', 51 | extensionDevelopmentPath, 52 | extensionTestsPath, 53 | launchArgs: [testWorkspace], 54 | }); 55 | 56 | /** 57 | * Use Insiders release for testing 58 | */ 59 | await runTests({ 60 | version: 'insiders', 61 | extensionDevelopmentPath, 62 | extensionTestsPath, 63 | launchArgs: [testWorkspace], 64 | }); 65 | 66 | /** 67 | * Noop, since 1.36.1 already downloaded to .vscode-test/vscode-1.36.1 68 | */ 69 | await downloadAndUnzipVSCode('1.36.1'); 70 | 71 | /** 72 | * Manually download VS Code 1.35.0 release for testing. 73 | */ 74 | const vscodeExecutablePath = await downloadAndUnzipVSCode('1.35.0'); 75 | await runTests({ 76 | vscodeExecutablePath, 77 | extensionDevelopmentPath, 78 | extensionTestsPath, 79 | launchArgs: [testWorkspace], 80 | }); 81 | 82 | /** 83 | * Install Python extension 84 | */ 85 | await runVSCodeCommand(['--install-extension', 'ms-python.python'], { version: '1.35.0' }); 86 | 87 | /** 88 | * - Add additional launch flags for VS Code 89 | * - Pass custom environment variables to test runner 90 | */ 91 | await runTests({ 92 | vscodeExecutablePath, 93 | extensionDevelopmentPath, 94 | extensionTestsPath, 95 | launchArgs: [ 96 | testWorkspace, 97 | // This disables all extensions except the one being tested 98 | '--disable-extensions', 99 | ], 100 | // Custom environment variables for extension test script 101 | extensionTestsEnv: { foo: 'bar' }, 102 | }); 103 | 104 | /** 105 | * Use win64 instead of win32 for testing Windows 106 | */ 107 | if (process.platform === 'win32') { 108 | await runTests({ 109 | extensionDevelopmentPath, 110 | extensionTestsPath, 111 | version: '1.40.0', 112 | platform: 'win32-x64-archive', 113 | }); 114 | } 115 | } catch (err) { 116 | console.error('Failed to run tests'); 117 | process.exit(1); 118 | } 119 | } 120 | 121 | go(); 122 | ``` 123 | 124 | ## Development 125 | 126 | - `npm install` 127 | - Make necessary changes in [`lib`](./lib) 128 | - `npm run compile` (or `npm run watch`) 129 | - In [`sample`](./sample), run `npm install`, `npm run compile` and `npm run test` to make sure integration test can run successfully 130 | 131 | ## License 132 | 133 | [MIT](LICENSE) 134 | 135 | ## Contributing 136 | 137 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 138 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 139 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 140 | 141 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 142 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 143 | provided by the bot. You will only need to do this once across all repos using our CLA. 144 | 145 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 146 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 147 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 148 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | - Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | - Full paths of source file(s) related to the manifestation of the issue 23 | - The location of the affected source code (tag/branch/commit or direct URL) 24 | - Any special configuration required to reproduce the issue 25 | - Step-by-step instructions to reproduce the issue 26 | - Proof-of-concept or exploit code (if possible) 27 | - Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import tseslint from 'typescript-eslint'; 3 | 4 | export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended); 5 | -------------------------------------------------------------------------------- /killTree.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ROOT_PID=$1 4 | SIGNAL=$2 5 | 6 | terminateTree() { 7 | for cpid in $(/usr/bin/pgrep -P $1); do 8 | terminateTree $cpid 9 | done 10 | kill -$SIGNAL $1 > /dev/null 2>&1 11 | } 12 | 13 | terminateTree $ROOT_PID 14 | -------------------------------------------------------------------------------- /lib/download.test.mts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { spawnSync } from 'child_process'; 7 | import { existsSync, promises as fs } from 'fs'; 8 | import { tmpdir } from 'os'; 9 | import { dirname, join } from 'path'; 10 | import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; 11 | import { 12 | downloadAndUnzipVSCode, 13 | fetchInsiderVersions, 14 | fetchStableVersions, 15 | fetchTargetInferredVersion, 16 | } from './download.js'; 17 | import { SilentReporter } from './progress.js'; 18 | import { 19 | isPlatformDarwin, 20 | isPlatformLinux, 21 | isPlatformWindows, 22 | resolveCliPathFromVSCodeExecutablePath 23 | } from './util.js'; 24 | 25 | const platforms = [ 26 | 'darwin', 27 | 'darwin-arm64', 28 | 'win32-x64-archive', 29 | 'win32-arm64-archive', 30 | 'linux-x64', 31 | 'linux-arm64', 32 | 'linux-armhf', 33 | 34 | 'cli-linux-x64', 35 | 'cli-win32-x64', 36 | 'cli-darwin-x64', 37 | 38 | 'server-win32-x64', 39 | 'server-darwin', 40 | 'server-linux-x64', 41 | ]; 42 | 43 | describe('sane downloads', () => { 44 | const testTempDir = join(tmpdir(), 'vscode-test-download'); 45 | 46 | beforeAll(async () => { 47 | await fs.mkdir(testTempDir, { recursive: true }); 48 | }); 49 | 50 | const isRunnableOnThisPlatform = 51 | process.platform === 'win32' 52 | ? isPlatformWindows 53 | : process.platform === 'darwin' 54 | ? isPlatformDarwin 55 | : isPlatformLinux; 56 | 57 | for (const quality of ['insiders', 'stable']) { 58 | for (const platform of platforms) { 59 | test.concurrent(`${quality}/${platform}`, async () => { 60 | const location = await downloadAndUnzipVSCode({ 61 | platform, 62 | version: quality, 63 | cachePath: testTempDir, 64 | reporter: new SilentReporter(), 65 | }); 66 | 67 | if (!existsSync(location)) { 68 | throw new Error(`expected ${location} to exist for ${platform}`); 69 | } 70 | 71 | const exePath = resolveCliPathFromVSCodeExecutablePath(location, platform); 72 | if (!existsSync(exePath)) { 73 | throw new Error(`expected ${exePath} to from ${location}`); 74 | } 75 | 76 | if (platform.includes(process.arch) && isRunnableOnThisPlatform(platform)) { 77 | const shell = process.platform === 'win32'; 78 | const version = spawnSync(shell ? `"${exePath}"` : exePath, ['--version'], { shell }); 79 | expect(version.status).to.equal(0); 80 | expect(version.stdout.toString().trim()).to.not.be.empty; 81 | } 82 | }); 83 | } 84 | } 85 | 86 | afterAll(async () => { 87 | try { 88 | await fs.rmdir(testTempDir, { recursive: true }); 89 | } catch { 90 | // ignored 91 | } 92 | }); 93 | }); 94 | 95 | describe('fetchTargetInferredVersion', () => { 96 | let stable: string[]; 97 | let insiders: string[]; 98 | const extensionsDevelopmentPath = join(tmpdir(), 'vscode-test-tmp-workspace'); 99 | 100 | beforeAll(async () => { 101 | [stable, insiders] = await Promise.all([fetchStableVersions(true, 5000), fetchInsiderVersions(true, 5000)]); 102 | }); 103 | 104 | afterEach(async () => { 105 | await fs.rm(extensionsDevelopmentPath, { recursive: true, force: true }); 106 | }); 107 | 108 | const writeJSON = async (path: string, contents: unknown) => { 109 | const target = join(extensionsDevelopmentPath, path); 110 | await fs.mkdir(dirname(target), { recursive: true }); 111 | await fs.writeFile(target, JSON.stringify(contents)); 112 | }; 113 | 114 | const doFetch = (paths = ['./']) => 115 | fetchTargetInferredVersion({ 116 | cachePath: join(extensionsDevelopmentPath, '.cache'), 117 | platform: 'win32-x64-archive', 118 | timeout: 5000, 119 | extensionsDevelopmentPath: paths.map((p) => join(extensionsDevelopmentPath, p)), 120 | }); 121 | 122 | test('matches stable if no workspace', async () => { 123 | const version = await doFetch(); 124 | expect(version.id).to.equal(stable[0]); 125 | }); 126 | 127 | test('matches stable by default', async () => { 128 | await writeJSON('package.json', {}); 129 | const version = await doFetch(); 130 | expect(version.id).to.equal(stable[0]); 131 | }); 132 | 133 | test('matches if stable is defined', async () => { 134 | await writeJSON('package.json', { engines: { vscode: '^1.50.0' } }); 135 | const version = await doFetch(); 136 | expect(version.id).to.equal(stable[0]); 137 | }); 138 | 139 | test('matches best', async () => { 140 | await writeJSON('package.json', { engines: { vscode: '<=1.60.5' } }); 141 | const version = await doFetch(); 142 | expect(version.id).to.equal('1.60.2'); 143 | }); 144 | 145 | test('matches multiple workspaces', async () => { 146 | await writeJSON('a/package.json', { engines: { vscode: '<=1.60.5' } }); 147 | await writeJSON('b/package.json', { engines: { vscode: '<=1.55.5' } }); 148 | const version = await doFetch(['a', 'b']); 149 | expect(version.id).to.equal('1.55.2'); 150 | }); 151 | 152 | test('matches insiders to better stable if there is one', async () => { 153 | await writeJSON('package.json', { engines: { vscode: '^1.60.0-insider' } }); 154 | const version = await doFetch(); 155 | expect(version.id).to.equal(stable[0]); 156 | }); 157 | 158 | test('matches current insiders', async () => { 159 | await writeJSON('package.json', { engines: { vscode: `^${insiders[0]}` } }); 160 | const version = await doFetch(); 161 | expect(version.id).to.equal(insiders[0]); 162 | }); 163 | 164 | test('matches insiders to exact', async () => { 165 | await writeJSON('package.json', { engines: { vscode: '1.60.0-insider' } }); 166 | const version = await doFetch(); 167 | expect(version.id).to.equal('1.60.0-insider'); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /lib/download.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as cp from 'child_process'; 7 | import * as fs from 'fs'; 8 | import { tmpdir } from 'os'; 9 | import * as path from 'path'; 10 | import * as semver from 'semver'; 11 | import { pipeline } from 'stream'; 12 | import { promisify } from 'util'; 13 | import { makeConsoleReporter, ProgressReporter, ProgressReportStage } from './progress.js'; 14 | import * as request from './request'; 15 | import { 16 | downloadDirToExecutablePath, 17 | getInsidersVersionMetadata, 18 | getLatestInsidersMetadata, 19 | getVSCodeDownloadUrl, 20 | insidersDownloadDirMetadata, 21 | insidersDownloadDirToExecutablePath, 22 | isDefined, 23 | isPlatformCLI, 24 | isPlatformServer, 25 | isSubdirectory, 26 | onceWithoutRejections, 27 | streamToBuffer, 28 | systemDefaultPlatform, 29 | validateStream, 30 | Version, 31 | } from './util'; 32 | 33 | const extensionRoot = process.cwd(); 34 | const pipelineAsync = promisify(pipeline); 35 | 36 | const vscodeStableReleasesAPI = `https://update.code.visualstudio.com/api/releases/stable`; 37 | const vscodeInsiderReleasesAPI = `https://update.code.visualstudio.com/api/releases/insider`; 38 | 39 | const downloadDirNameFormat = /^vscode-(?[a-z0-9-]+)-(?[0-9.]+)$/; 40 | const makeDownloadDirName = (platform: string, version: Version) => `vscode-${platform}-${version.id}`; 41 | 42 | const DOWNLOAD_ATTEMPTS = 3; 43 | 44 | interface IFetchStableOptions { 45 | timeout: number; 46 | cachePath: string; 47 | platform: string; 48 | } 49 | 50 | interface IFetchInferredOptions extends IFetchStableOptions { 51 | extensionsDevelopmentPath?: string | string[]; 52 | } 53 | 54 | // Turn off Electron's special handling of .asar files, otherwise 55 | // extraction will fail when we try to extract node_modules.asar 56 | // under Electron's Node (i.e. in the test CLI invoked by an extension) 57 | // https://github.com/electron/packager/issues/875 58 | // 59 | // Also, trying to delete a directory with an asar in it will fail. 60 | // 61 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 62 | (process as any).noAsar = true; 63 | 64 | export const fetchStableVersions = onceWithoutRejections((released: boolean, timeout: number) => 65 | request.getJSON(`${vscodeStableReleasesAPI}?released=${released}`, timeout), 66 | ); 67 | export const fetchInsiderVersions = onceWithoutRejections((released: boolean, timeout: number) => 68 | request.getJSON(`${vscodeInsiderReleasesAPI}?released=${released}`, timeout), 69 | ); 70 | 71 | /** 72 | * Returns the stable version to run tests against. Attempts to get the latest 73 | * version from the update sverice, but falls back to local installs if 74 | * not available (e.g. if the machine is offline). 75 | */ 76 | async function fetchTargetStableVersion({ timeout, cachePath, platform }: IFetchStableOptions): Promise { 77 | try { 78 | const versions = await fetchStableVersions(true, timeout); 79 | return new Version(versions[0]); 80 | } catch (e) { 81 | return fallbackToLocalEntries(cachePath, platform, e as Error); 82 | } 83 | } 84 | 85 | export async function fetchTargetInferredVersion(options: IFetchInferredOptions): Promise { 86 | if (!options.extensionsDevelopmentPath) { 87 | return fetchTargetStableVersion(options); 88 | } 89 | 90 | // load all engines versions from all development paths. Then, get the latest 91 | // stable version (or, latest Insiders version) that satisfies all 92 | // `engines.vscode` constraints. 93 | const extPaths = Array.isArray(options.extensionsDevelopmentPath) 94 | ? options.extensionsDevelopmentPath 95 | : [options.extensionsDevelopmentPath]; 96 | const maybeExtVersions = await Promise.all(extPaths.map(getEngineVersionFromExtension)); 97 | const extVersions = maybeExtVersions.filter(isDefined); 98 | const matches = (v: string) => !extVersions.some((range) => !semver.satisfies(v, range, { includePrerelease: true })); 99 | 100 | try { 101 | const stable = await fetchStableVersions(true, options.timeout); 102 | const found1 = stable.find(matches); 103 | if (found1) { 104 | return new Version(found1); 105 | } 106 | 107 | const insiders = await fetchInsiderVersions(true, options.timeout); 108 | const found2 = insiders.find(matches); 109 | if (found2) { 110 | return new Version(found2); 111 | } 112 | 113 | const v = extVersions.join(', '); 114 | console.warn(`No version of VS Code satisfies all extension engine constraints (${v}). Falling back to stable.`); 115 | 116 | return new Version(stable[0]); // 🤷 117 | } catch (e) { 118 | return fallbackToLocalEntries(options.cachePath, options.platform, e as Error); 119 | } 120 | } 121 | 122 | async function getEngineVersionFromExtension(extensionPath: string): Promise { 123 | try { 124 | const packageContents = await fs.promises.readFile(path.join(extensionPath, 'package.json'), 'utf8'); 125 | const packageJson = JSON.parse(packageContents); 126 | return packageJson?.engines?.vscode; 127 | } catch { 128 | return undefined; 129 | } 130 | } 131 | 132 | async function fallbackToLocalEntries(cachePath: string, platform: string, fromError: Error) { 133 | const entries = await fs.promises.readdir(cachePath).catch(() => [] as string[]); 134 | const [fallbackTo] = entries 135 | .map((e) => downloadDirNameFormat.exec(e)) 136 | .filter(isDefined) 137 | .filter((e) => e.groups!.platform === platform) 138 | .map((e) => e.groups!.version) 139 | .sort((a, b) => semver.compare(b, a)); 140 | 141 | if (fallbackTo) { 142 | console.warn(`Error retrieving VS Code versions, using already-installed version ${fallbackTo}`, fromError); 143 | return new Version(fallbackTo); 144 | } 145 | 146 | throw fromError; 147 | } 148 | 149 | async function isValidVersion(version: Version, timeout: number) { 150 | if (version.id === 'insiders' || version.id === 'stable' || version.isCommit) { 151 | return true; 152 | } 153 | 154 | if (version.isStable) { 155 | const stableVersionNumbers = await fetchStableVersions(version.isReleased, timeout); 156 | if (stableVersionNumbers.includes(version.id)) { 157 | return true; 158 | } 159 | } 160 | 161 | if (version.isInsiders) { 162 | const insiderVersionNumbers = await fetchInsiderVersions(version.isReleased, timeout); 163 | if (insiderVersionNumbers.includes(version.id)) { 164 | return true; 165 | } 166 | } 167 | 168 | return false; 169 | } 170 | 171 | /** 172 | * Adapted from https://github.com/microsoft/TypeScript/issues/29729 173 | * Since `string | 'foo'` doesn't offer auto completion 174 | */ 175 | // eslint-disable-next-line @typescript-eslint/ban-types 176 | type StringLiteralUnion = T | (string & {}); 177 | export type DownloadVersion = StringLiteralUnion<'insiders' | 'stable'>; 178 | export type DownloadPlatform = StringLiteralUnion< 179 | 'darwin' | 'darwin-arm64' | 'win32-x64-archive' | 'win32-arm64-archive' | 'linux-x64' | 'linux-arm64' | 'linux-armhf' 180 | >; 181 | 182 | export interface DownloadOptions { 183 | /** 184 | * The VS Code version to download. Valid versions are: 185 | * - `'stable'` 186 | * - `'insiders'` 187 | * - `'1.32.0'`, `'1.31.1'`, etc 188 | * 189 | * Defaults to `stable`, which is latest stable version. 190 | * 191 | * *If a local copy exists at `.vscode-test/vscode-`, skip download.* 192 | */ 193 | version: DownloadVersion; 194 | 195 | /** 196 | * The VS Code platform to download. If not specified, it defaults to the 197 | * current platform. 198 | * 199 | * Possible values are: 200 | * - `win32-x64-archive` 201 | * - `win32-arm64-archive ` 202 | * - `darwin` 203 | * - `darwin-arm64` 204 | * - `linux-x64` 205 | * - `linux-arm64` 206 | * - `linux-armhf` 207 | */ 208 | platform: DownloadPlatform; 209 | 210 | /** 211 | * Path where the downloaded VS Code instance is stored. 212 | * Defaults to `.vscode-test` within your working directory folder. 213 | */ 214 | cachePath: string; 215 | 216 | /** 217 | * Absolute path to the extension root. Passed to `--extensionDevelopmentPath`. 218 | * Must include a `package.json` Extension Manifest. 219 | */ 220 | extensionDevelopmentPath?: string | string[]; 221 | 222 | /** 223 | * Progress reporter to use while VS Code is downloaded. Defaults to a 224 | * console reporter. A {@link SilentReporter} is also available, and you 225 | * may implement your own. 226 | */ 227 | reporter?: ProgressReporter; 228 | 229 | /** 230 | * Whether the downloaded zip should be synchronously extracted. Should be 231 | * omitted unless you're experiencing issues installing VS Code versions. 232 | */ 233 | extractSync?: boolean; 234 | 235 | /** 236 | * Number of milliseconds after which to time out if no data is received from 237 | * the remote when downloading VS Code. Note that this is an 'idle' timeout 238 | * and does not enforce the total time VS Code may take to download. 239 | */ 240 | timeout?: number; 241 | } 242 | 243 | interface IDownload { 244 | stream: NodeJS.ReadableStream; 245 | format: 'zip' | 'tgz'; 246 | sha256?: string; 247 | length: number; 248 | } 249 | 250 | function getFilename(contentDisposition: string) { 251 | const parts = contentDisposition.split(';').map((s) => s.trim()); 252 | 253 | for (const part of parts) { 254 | const match = /^filename="?([^"]*)"?$/i.exec(part); 255 | 256 | if (match) { 257 | return match[1]; 258 | } 259 | } 260 | 261 | return undefined; 262 | } 263 | 264 | /** 265 | * Download a copy of VS Code archive to `.vscode-test`. 266 | * 267 | * @param version The version of VS Code to download such as '1.32.0'. You can also use 268 | * `'stable'` for downloading latest stable release. 269 | * `'insiders'` for downloading latest Insiders. 270 | */ 271 | async function downloadVSCodeArchive(options: DownloadOptions): Promise { 272 | if (!fs.existsSync(options.cachePath)) { 273 | fs.mkdirSync(options.cachePath); 274 | } 275 | 276 | const timeout = options.timeout!; 277 | const version = Version.parse(options.version); 278 | const downloadUrl = getVSCodeDownloadUrl(version, options.platform); 279 | 280 | options.reporter?.report({ stage: ProgressReportStage.ResolvingCDNLocation, url: downloadUrl }); 281 | const res = await request.getStream(downloadUrl, timeout); 282 | if (res.statusCode !== 302) { 283 | throw 'Failed to get VS Code archive location'; 284 | } 285 | const url = res.headers.location; 286 | if (!url) { 287 | throw 'Failed to get VS Code archive location'; 288 | } 289 | 290 | const contentSHA256 = res.headers['x-sha256'] as string | undefined; 291 | res.destroy(); 292 | 293 | const download = await request.getStream(url, timeout); 294 | const totalBytes = Number(download.headers['content-length']); 295 | const contentDisposition = download.headers['content-disposition']; 296 | const fileName = contentDisposition ? getFilename(contentDisposition) : undefined; 297 | const isZip = fileName?.endsWith('zip') ?? url.endsWith('.zip'); 298 | 299 | const timeoutCtrl = new request.TimeoutController(timeout); 300 | options.reporter?.report({ 301 | stage: ProgressReportStage.Downloading, 302 | url, 303 | bytesSoFar: 0, 304 | totalBytes, 305 | }); 306 | 307 | let bytesSoFar = 0; 308 | download.on('data', (chunk) => { 309 | bytesSoFar += chunk.length; 310 | timeoutCtrl.touch(); 311 | options.reporter?.report({ 312 | stage: ProgressReportStage.Downloading, 313 | url, 314 | bytesSoFar, 315 | totalBytes, 316 | }); 317 | }); 318 | 319 | download.on('end', () => { 320 | timeoutCtrl.dispose(); 321 | options.reporter?.report({ 322 | stage: ProgressReportStage.Downloading, 323 | url, 324 | bytesSoFar: totalBytes, 325 | totalBytes, 326 | }); 327 | }); 328 | 329 | timeoutCtrl.signal.addEventListener('abort', () => { 330 | download.emit('error', new request.TimeoutError(timeout)); 331 | download.destroy(); 332 | }); 333 | 334 | return { 335 | stream: download, 336 | format: isZip ? 'zip' : 'tgz', 337 | sha256: contentSHA256, 338 | length: totalBytes, 339 | }; 340 | } 341 | 342 | /** 343 | * Unzip a .zip or .tar.gz VS Code archive stream. 344 | */ 345 | async function unzipVSCode( 346 | reporter: ProgressReporter, 347 | extractDir: string, 348 | platform: DownloadPlatform, 349 | { format, stream, length, sha256 }: IDownload, 350 | ) { 351 | const stagingFile = path.join(tmpdir(), `vscode-test-${Date.now()}.zip`); 352 | const checksum = validateStream(stream, length, sha256); 353 | 354 | if (format === 'zip') { 355 | const stripComponents = isPlatformServer(platform) ? 1 : 0; 356 | try { 357 | reporter.report({ stage: ProgressReportStage.ExtractingSynchonrously }); 358 | 359 | // note: this used to use Expand-Archive, but this caused a failure 360 | // on longer file paths on windows. And we used to use the streaming 361 | // "unzipper", but the module was very outdated and a bit buggy. 362 | // Instead, use jszip. It's well-used and actually 8x faster than 363 | // Expand-Archive on my machine. 364 | if (process.platform === 'win32') { 365 | const [buffer, JSZip] = await Promise.all([streamToBuffer(stream), import('jszip')]); 366 | await checksum; 367 | 368 | const content = await JSZip.default.loadAsync(buffer); 369 | // extract file with jszip 370 | for (const filename of Object.keys(content.files)) { 371 | const file = content.files[filename]; 372 | if (file.dir) { 373 | continue; 374 | } 375 | 376 | const filepath = stripComponents 377 | ? path.join(extractDir, filename.split(/[/\\]/g).slice(stripComponents).join(path.sep)) 378 | : path.join(extractDir, filename); 379 | 380 | // vscode update zips are trusted, but check for zip slip anyway. 381 | if (!isSubdirectory(extractDir, filepath)) { 382 | throw new Error(`Invalid zip file: ${filename}`); 383 | } 384 | 385 | await fs.promises.mkdir(path.dirname(filepath), { recursive: true }); 386 | await pipelineAsync(file.nodeStream(), fs.createWriteStream(filepath)); 387 | } 388 | } else { 389 | // darwin or *nix sync 390 | await pipelineAsync(stream, fs.createWriteStream(stagingFile)); 391 | await checksum; 392 | 393 | // unzip does not create intermediate directories when using -d 394 | await fs.promises.mkdir(extractDir, { recursive: true }); 395 | 396 | await spawnDecompressorChild('unzip', ['-q', stagingFile, '-d', extractDir]); 397 | 398 | // unzip has no --strip-components equivalent 399 | if (stripComponents) { 400 | const files = await fs.promises.readdir(extractDir); 401 | for (const file of files) { 402 | const dirPath = path.join(extractDir, file); 403 | const children = await fs.promises.readdir(dirPath); 404 | await Promise.all(children.map((c) => fs.promises.rename(path.join(dirPath, c), path.join(extractDir, c)))); 405 | await fs.promises.rmdir(dirPath); 406 | } 407 | } 408 | } 409 | } finally { 410 | fs.unlink(stagingFile, () => undefined); 411 | } 412 | } else { 413 | const stripComponents = isPlatformCLI(platform) ? 0 : 1; 414 | 415 | // tar does not create extractDir by default 416 | if (!fs.existsSync(extractDir)) { 417 | fs.mkdirSync(extractDir); 418 | } 419 | 420 | // The CLI is a singular binary that doesn't have a wrapper component to remove 421 | await spawnDecompressorChild( 422 | 'tar', 423 | ['-xzf', '-', `--strip-components=${stripComponents}`, '-C', extractDir], 424 | stream, 425 | ); 426 | await checksum; 427 | } 428 | } 429 | 430 | function spawnDecompressorChild(command: string, args: ReadonlyArray, input?: NodeJS.ReadableStream) { 431 | return new Promise((resolve, reject) => { 432 | const child = cp.spawn(command, args, { stdio: 'pipe' }); 433 | if (input) { 434 | input.on('error', reject); 435 | input.pipe(child.stdin); 436 | } 437 | 438 | child.stderr.pipe(process.stderr); 439 | child.stdout.pipe(process.stdout); 440 | 441 | child.on('error', reject); 442 | child.on('exit', (code) => 443 | code === 0 ? resolve() : reject(new Error(`Failed to unzip archive, exited with ${code}`)), 444 | ); 445 | }); 446 | } 447 | 448 | export const defaultCachePath = path.resolve(extensionRoot, '.vscode-test'); 449 | 450 | const COMPLETE_FILE_NAME = 'is-complete'; 451 | 452 | /** 453 | * Download and unzip a copy of VS Code. 454 | * @returns Promise of `vscodeExecutablePath`. 455 | */ 456 | export async function download(options: Partial = {}): Promise { 457 | const inputVersion = options?.version ? Version.parse(options.version) : undefined; 458 | const { 459 | platform = systemDefaultPlatform, 460 | cachePath = defaultCachePath, 461 | reporter = await makeConsoleReporter(), 462 | timeout = 15_000, 463 | } = options; 464 | 465 | let version: Version; 466 | if (inputVersion?.id === 'stable') { 467 | version = await fetchTargetStableVersion({ timeout, cachePath, platform }); 468 | } else if (inputVersion) { 469 | /** 470 | * Only validate version against server when no local download that matches version exists 471 | */ 472 | if (!fs.existsSync(path.resolve(cachePath, makeDownloadDirName(platform, inputVersion)))) { 473 | if (!(await isValidVersion(inputVersion, timeout))) { 474 | throw Error(`Invalid version ${inputVersion.id}`); 475 | } 476 | } 477 | version = inputVersion; 478 | } else { 479 | version = await fetchTargetInferredVersion({ 480 | timeout, 481 | cachePath, 482 | platform, 483 | extensionsDevelopmentPath: options.extensionDevelopmentPath, 484 | }); 485 | } 486 | 487 | if (platform === 'win32-archive' && semver.satisfies(version.id, '>= 1.85.0', { includePrerelease: true })) { 488 | throw new Error('Windows 32-bit is no longer supported from v1.85 onwards'); 489 | } 490 | 491 | reporter.report({ stage: ProgressReportStage.ResolvedVersion, version: version.toString() }); 492 | 493 | const downloadedPath = path.resolve(cachePath, makeDownloadDirName(platform, version)); 494 | if (fs.existsSync(path.join(downloadedPath, COMPLETE_FILE_NAME))) { 495 | if (version.isInsiders) { 496 | reporter.report({ stage: ProgressReportStage.FetchingInsidersMetadata }); 497 | const { version: currentHash, date: currentDate } = insidersDownloadDirMetadata( 498 | downloadedPath, 499 | platform, 500 | reporter, 501 | ); 502 | 503 | const { version: latestHash, timestamp: latestTimestamp } = 504 | version.id === 'insiders' // not qualified with a date 505 | ? await getLatestInsidersMetadata(systemDefaultPlatform, version.isReleased) 506 | : await getInsidersVersionMetadata(systemDefaultPlatform, version.id, version.isReleased); 507 | 508 | if (currentHash === latestHash) { 509 | reporter.report({ stage: ProgressReportStage.FoundMatchingInstall, downloadedPath }); 510 | return Promise.resolve(insidersDownloadDirToExecutablePath(downloadedPath, platform)); 511 | } else { 512 | try { 513 | reporter.report({ 514 | stage: ProgressReportStage.ReplacingOldInsiders, 515 | downloadedPath, 516 | oldDate: currentDate, 517 | oldHash: currentHash, 518 | newDate: new Date(latestTimestamp), 519 | newHash: latestHash, 520 | }); 521 | await fs.promises.rm(downloadedPath, { force: true, recursive: true }); 522 | } catch (err) { 523 | reporter.error(err); 524 | throw Error(`Failed to remove outdated Insiders at ${downloadedPath}.`); 525 | } 526 | } 527 | } else if (version.isStable) { 528 | reporter.report({ stage: ProgressReportStage.FoundMatchingInstall, downloadedPath }); 529 | return Promise.resolve(downloadDirToExecutablePath(downloadedPath, platform)); 530 | } else { 531 | reporter.report({ stage: ProgressReportStage.FoundMatchingInstall, downloadedPath }); 532 | return Promise.resolve(insidersDownloadDirToExecutablePath(downloadedPath, platform)); 533 | } 534 | } 535 | 536 | for (let i = 0; ; i++) { 537 | try { 538 | await fs.promises.rm(downloadedPath, { recursive: true, force: true }); 539 | 540 | const download = await downloadVSCodeArchive({ 541 | version: version.toString(), 542 | platform, 543 | cachePath, 544 | reporter, 545 | timeout, 546 | }); 547 | // important! do not put anything async here, since unzipVSCode will need 548 | // to start consuming the stream immediately. 549 | await unzipVSCode(reporter, downloadedPath, platform, download); 550 | 551 | await fs.promises.writeFile(path.join(downloadedPath, COMPLETE_FILE_NAME), ''); 552 | reporter.report({ stage: ProgressReportStage.NewInstallComplete, downloadedPath }); 553 | break; 554 | } catch (error) { 555 | if (i++ < DOWNLOAD_ATTEMPTS) { 556 | reporter.report({ 557 | stage: ProgressReportStage.Retrying, 558 | attempt: i, 559 | error: error as Error, 560 | totalAttempts: DOWNLOAD_ATTEMPTS, 561 | }); 562 | } else { 563 | reporter.error(error); 564 | throw Error(`Failed to download and unzip VS Code ${version}`); 565 | } 566 | } 567 | } 568 | reporter.report({ stage: ProgressReportStage.NewInstallComplete, downloadedPath }); 569 | 570 | if (version.isStable) { 571 | return downloadDirToExecutablePath(downloadedPath, platform); 572 | } else { 573 | return insidersDownloadDirToExecutablePath(downloadedPath, platform); 574 | } 575 | } 576 | 577 | /** 578 | * Download and unzip a copy of VS Code in `.vscode-test`. The paths are: 579 | * - `.vscode-test/vscode--`. For example, `./vscode-test/vscode-win32-1.32.0` 580 | * - `.vscode-test/vscode-win32-insiders`. 581 | * 582 | * *If a local copy exists at `.vscode-test/vscode--`, skip download.* 583 | * 584 | * @param version The version of VS Code to download such as `1.32.0`. You can also use 585 | * `'stable'` for downloading latest stable release. 586 | * `'insiders'` for downloading latest Insiders. 587 | * When unspecified, download latest stable version. 588 | * 589 | * @returns Promise of `vscodeExecutablePath`. 590 | */ 591 | export async function downloadAndUnzipVSCode(options: Partial): Promise; 592 | export async function downloadAndUnzipVSCode( 593 | version?: DownloadVersion, 594 | platform?: DownloadPlatform, 595 | reporter?: ProgressReporter, 596 | extractSync?: boolean, 597 | ): Promise; 598 | export async function downloadAndUnzipVSCode( 599 | versionOrOptions?: DownloadVersion | Partial, 600 | platform?: DownloadPlatform, 601 | reporter?: ProgressReporter, 602 | extractSync?: boolean, 603 | ): Promise { 604 | return await download( 605 | typeof versionOrOptions === 'object' 606 | ? (versionOrOptions as Partial) 607 | : { version: versionOrOptions, platform, reporter, extractSync }, 608 | ); 609 | } 610 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export { download, downloadAndUnzipVSCode, DownloadOptions } from './download'; 7 | export { runTests, TestOptions, TestRunFailedError } from './runTest'; 8 | export { 9 | resolveCliPathFromVSCodeExecutablePath, 10 | resolveCliArgsFromVSCodeExecutablePath, 11 | runVSCodeCommand, 12 | VSCodeCommandError, 13 | RunVSCodeCommandOptions, 14 | } from './util'; 15 | export * from './progress.js'; 16 | -------------------------------------------------------------------------------- /lib/progress.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | /** Stages of progress while downloading VS Code */ 7 | export enum ProgressReportStage { 8 | /** Initial fetch of the latest version if not explicitly given */ 9 | FetchingVersion = 'fetchingVersion', 10 | /** Always fired when the version is determined. */ 11 | ResolvedVersion = 'resolvedVersion', 12 | /** Fired before fetching info about the latest Insiders version, when requesting insiders builds */ 13 | FetchingInsidersMetadata = 'fetchingInsidersMetadata', 14 | /** Fired if the current Insiders is out of date */ 15 | ReplacingOldInsiders = 'replacingOldInsiders', 16 | /** Fired when an existing install is found which does not require a download */ 17 | FoundMatchingInstall = 'foundMatchingInstall', 18 | /** Fired before the URL to the download zip or tarball is looked up */ 19 | ResolvingCDNLocation = 'resolvingCDNLocation', 20 | /** Fired continuously while a download happens */ 21 | Downloading = 'downloading', 22 | /** Fired when the command is issued to do a synchronous extraction. May not fire depending on the platform and options. */ 23 | ExtractingSynchonrously = 'extractingSynchonrously', 24 | /** Fired when the download fails and a retry will be attempted */ 25 | Retrying = 'retrying', 26 | /** Fired after folder is downloaded and unzipped */ 27 | NewInstallComplete = 'newInstallComplete', 28 | } 29 | 30 | export type ProgressReport = 31 | | { stage: ProgressReportStage.FetchingVersion } 32 | | { stage: ProgressReportStage.ResolvedVersion; version: string } 33 | | { stage: ProgressReportStage.FetchingInsidersMetadata } 34 | | { 35 | stage: ProgressReportStage.ReplacingOldInsiders; 36 | downloadedPath: string; 37 | oldHash: string; 38 | oldDate: Date; 39 | newHash: string; 40 | newDate: Date; 41 | } 42 | | { stage: ProgressReportStage.FoundMatchingInstall; downloadedPath: string } 43 | | { stage: ProgressReportStage.ResolvingCDNLocation; url: string } 44 | | { stage: ProgressReportStage.Downloading; url: string; totalBytes: number; bytesSoFar: number } 45 | | { stage: ProgressReportStage.Retrying; error: Error; attempt: number; totalAttempts: number } 46 | | { stage: ProgressReportStage.ExtractingSynchonrously } 47 | | { stage: ProgressReportStage.NewInstallComplete; downloadedPath: string }; 48 | 49 | export interface ProgressReporter { 50 | report(report: ProgressReport): void; 51 | error(err: unknown): void; 52 | } 53 | 54 | /** Silent progress reporter */ 55 | export class SilentReporter implements ProgressReporter { 56 | report(): void { 57 | // no-op 58 | } 59 | 60 | error(): void { 61 | // no-op 62 | } 63 | } 64 | 65 | /** Default progress reporter that logs VS Code download progress to console */ 66 | export const makeConsoleReporter = async (): Promise => { 67 | // needs to be async targeting Node 16 because ora is an es module that cannot be required 68 | const { default: ora } = await import('ora'); 69 | let version: undefined | string; 70 | 71 | let spinner: undefined | ReturnType = ora('Resolving version...').start(); 72 | function toMB(bytes: number) { 73 | return (bytes / 1024 / 1024).toFixed(2); 74 | } 75 | 76 | return { 77 | error(err: unknown): void { 78 | if (spinner) { 79 | spinner?.fail(`Error: ${err}`); 80 | spinner = undefined; 81 | } else { 82 | console.error(err); 83 | } 84 | }, 85 | 86 | report(report: ProgressReport): void { 87 | switch (report.stage) { 88 | case ProgressReportStage.ResolvedVersion: 89 | version = report.version; 90 | spinner?.succeed(`Validated version: ${version}`); 91 | spinner = undefined; 92 | break; 93 | case ProgressReportStage.ReplacingOldInsiders: 94 | spinner?.succeed(); 95 | spinner = ora( 96 | `Updating Insiders ${report.oldHash} (${report.oldDate.toISOString()}) -> ${report.newHash}` 97 | ).start(); 98 | break; 99 | case ProgressReportStage.FoundMatchingInstall: 100 | spinner?.succeed(); 101 | spinner = undefined; 102 | ora(`Found existing install in ${report.downloadedPath}`).succeed(); 103 | break; 104 | case ProgressReportStage.ResolvingCDNLocation: 105 | spinner?.succeed(); 106 | spinner = ora(`Found at ${report.url}`).start(); 107 | break; 108 | case ProgressReportStage.Downloading: 109 | if (report.bytesSoFar === 0) { 110 | spinner?.succeed(); 111 | spinner = ora(`Downloading (${toMB(report.totalBytes)} MB)`).start(); 112 | } else if (spinner) { 113 | if (report.bytesSoFar === report.totalBytes) { 114 | spinner.text = 'Extracting...'; 115 | } else { 116 | const percent = Math.max(0, Math.min(1, report.bytesSoFar / report.totalBytes)); 117 | const size = `${toMB(report.bytesSoFar)}/${toMB(report.totalBytes)}MB`; 118 | spinner.text = `Downloading VS Code: ${size} (${(percent * 100).toFixed()}%)`; 119 | } 120 | } 121 | break; 122 | case ProgressReportStage.Retrying: 123 | spinner?.fail( 124 | `Error downloading, retrying (attempt ${report.attempt} of ${report.totalAttempts}): ${report.error.message}` 125 | ); 126 | spinner = undefined; 127 | break; 128 | case ProgressReportStage.NewInstallComplete: 129 | spinner?.succeed(`Downloaded VS Code into ${report.downloadedPath}`); 130 | spinner = undefined; 131 | break; 132 | } 133 | }, 134 | }; 135 | }; 136 | -------------------------------------------------------------------------------- /lib/request.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IncomingMessage } from 'http'; 7 | import * as https from 'https'; 8 | import { urlToOptions } from './util'; 9 | 10 | export async function getStream(api: string, timeout: number): Promise { 11 | const ctrl = new TimeoutController(timeout); 12 | return new Promise((resolve, reject) => { 13 | ctrl.signal.addEventListener('abort', () => { 14 | reject(new TimeoutError(timeout)); 15 | req.destroy(); 16 | }); 17 | const req = https.get(api, urlToOptions(api), (res) => resolve(res)).on('error', reject); 18 | }).finally(() => ctrl.dispose()); 19 | } 20 | 21 | export async function getJSON(api: string, timeout: number): Promise { 22 | const ctrl = new TimeoutController(timeout); 23 | 24 | return new Promise((resolve, reject) => { 25 | ctrl.signal.addEventListener('abort', () => { 26 | reject(new TimeoutError(timeout)); 27 | req.destroy(); 28 | }); 29 | 30 | const req = https 31 | .get(api, urlToOptions(api), (res) => { 32 | if (res.statusCode !== 200) { 33 | reject('Failed to get JSON'); 34 | } 35 | 36 | let data = ''; 37 | 38 | res.on('data', (chunk) => { 39 | ctrl.touch(); 40 | data += chunk; 41 | }); 42 | 43 | res.on('end', () => { 44 | ctrl.dispose(); 45 | 46 | try { 47 | const jsonData = JSON.parse(data); 48 | resolve(jsonData); 49 | } catch (err) { 50 | console.error(`Failed to parse response from ${api} as JSON`); 51 | reject(err); 52 | } 53 | }); 54 | 55 | res.on('error', reject); 56 | }) 57 | .on('error', reject); 58 | }).finally(() => ctrl.dispose()); 59 | } 60 | 61 | export class TimeoutController { 62 | private handle: NodeJS.Timeout; 63 | private readonly ctrl = new AbortController(); 64 | 65 | public get signal() { 66 | return this.ctrl.signal; 67 | } 68 | 69 | constructor(private readonly timeout: number) { 70 | this.handle = setTimeout(this.reject, timeout); 71 | } 72 | 73 | public touch() { 74 | clearTimeout(this.handle); 75 | this.handle = setTimeout(this.reject, this.timeout); 76 | } 77 | 78 | public dispose() { 79 | clearTimeout(this.handle); 80 | } 81 | 82 | private readonly reject = () => { 83 | this.ctrl.abort(); 84 | }; 85 | } 86 | 87 | export class TimeoutError extends Error { 88 | constructor(duration: number) { 89 | super(`@vscode/test-electron request timeout out after ${duration}ms`); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/runTest.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as cp from 'child_process'; 7 | import { DownloadOptions, downloadAndUnzipVSCode } from './download'; 8 | import { getProfileArguments, killTree } from './util'; 9 | 10 | export interface TestOptions extends Partial { 11 | /** 12 | * The VS Code executable path used for testing. 13 | * 14 | * If not passed, will use `options.version` to download a copy of VS Code for testing. 15 | * If `version` is not specified either, will download and use latest stable release. 16 | */ 17 | vscodeExecutablePath?: string; 18 | 19 | /** 20 | * Whether VS Code should be launched using default settings and extensions 21 | * installed on this machine. If `false`, then separate directories will be 22 | * used inside the `.vscode-test` folder within the project. 23 | * 24 | * Defaults to `false`. 25 | */ 26 | reuseMachineInstall?: boolean; 27 | 28 | /** 29 | * Absolute path to the extension root. Passed to `--extensionDevelopmentPath`. 30 | * Must include a `package.json` Extension Manifest. 31 | */ 32 | extensionDevelopmentPath: string | string[]; 33 | 34 | /** 35 | * Absolute path to the extension tests runner. Passed to `--extensionTestsPath`. 36 | * Can be either a file path or a directory path that contains an `index.js`. 37 | * Must export a `run` function of the following signature: 38 | * 39 | * ```ts 40 | * function run(): Promise; 41 | * ``` 42 | * 43 | * When running the extension test, the Extension Development Host will call this function 44 | * that runs the test suite. This function should throws an error if any test fails. 45 | * 46 | * The first argument is the path to the file specified in `extensionTestsPath`. 47 | * 48 | */ 49 | extensionTestsPath: string; 50 | 51 | /** 52 | * Environment variables being passed to the extension test script. 53 | */ 54 | extensionTestsEnv?: { 55 | [key: string]: string | undefined; 56 | }; 57 | 58 | /** 59 | * A list of launch arguments passed to VS Code executable, in addition to `--extensionDevelopmentPath` 60 | * and `--extensionTestsPath` which are provided by `extensionDevelopmentPath` and `extensionTestsPath` 61 | * options. 62 | * 63 | * If the first argument is a path to a file/folder/workspace, the launched VS Code instance 64 | * will open it. 65 | * 66 | * See `code --help` for possible arguments. 67 | */ 68 | launchArgs?: string[]; 69 | } 70 | 71 | /** 72 | * Run VS Code extension test 73 | * 74 | * @returns The exit code of the command to launch VS Code extension test 75 | */ 76 | export async function runTests(options: TestOptions): Promise { 77 | if (!options.vscodeExecutablePath) { 78 | options.vscodeExecutablePath = await downloadAndUnzipVSCode(options); 79 | } 80 | 81 | let args = [ 82 | // https://github.com/microsoft/vscode/issues/84238 83 | '--no-sandbox', 84 | // https://github.com/microsoft/vscode-test/issues/221 85 | '--disable-gpu-sandbox', 86 | // https://github.com/microsoft/vscode-test/issues/120 87 | '--disable-updates', 88 | '--skip-welcome', 89 | '--skip-release-notes', 90 | '--disable-workspace-trust', 91 | '--extensionTestsPath=' + options.extensionTestsPath, 92 | ]; 93 | 94 | if (Array.isArray(options.extensionDevelopmentPath)) { 95 | args.push(...options.extensionDevelopmentPath.map((devPath) => `--extensionDevelopmentPath=${devPath}`)); 96 | } else { 97 | args.push(`--extensionDevelopmentPath=${options.extensionDevelopmentPath}`); 98 | } 99 | 100 | if (options.launchArgs) { 101 | args = options.launchArgs.concat(args); 102 | } 103 | 104 | if (!options.reuseMachineInstall) { 105 | args.push(...getProfileArguments(args)); 106 | } 107 | 108 | return innerRunTests(options.vscodeExecutablePath, args, options.extensionTestsEnv); 109 | } 110 | const SIGINT = 'SIGINT'; 111 | 112 | async function innerRunTests( 113 | executable: string, 114 | args: string[], 115 | testRunnerEnv?: { 116 | [key: string]: string | undefined; 117 | } 118 | ): Promise { 119 | const fullEnv = Object.assign({}, process.env, testRunnerEnv); 120 | const shell = process.platform === 'win32'; 121 | const cmd = cp.spawn(shell ? `"${executable}"` : executable, args, { env: fullEnv, shell }); 122 | 123 | let exitRequested = false; 124 | const ctrlc1 = () => { 125 | process.removeListener(SIGINT, ctrlc1); 126 | process.on(SIGINT, ctrlc2); 127 | console.log('Closing VS Code gracefully. Press Ctrl+C to force close.'); 128 | exitRequested = true; 129 | cmd.kill(SIGINT); // this should cause the returned promise to resolve 130 | }; 131 | 132 | const ctrlc2 = () => { 133 | console.log('Closing VS Code forcefully.'); 134 | process.removeListener(SIGINT, ctrlc2); 135 | exitRequested = true; 136 | killTree(cmd.pid!, true); 137 | }; 138 | 139 | const prom = new Promise((resolve, reject) => { 140 | if (cmd.pid) { 141 | process.on(SIGINT, ctrlc1); 142 | } 143 | 144 | cmd.stdout.on('data', (d) => process.stdout.write(d)); 145 | cmd.stderr.on('data', (d) => process.stderr.write(d)); 146 | 147 | cmd.on('error', function (data) { 148 | console.log('Test error: ' + data.toString()); 149 | }); 150 | 151 | let finished = false; 152 | function onProcessClosed(code: number | null, signal: NodeJS.Signals | null): void { 153 | if (finished) { 154 | return; 155 | } 156 | finished = true; 157 | console.log(`Exit code: ${code ?? signal}`); 158 | 159 | // fix: on windows, it seems like these descriptors can linger for an 160 | // indeterminate amount of time, causing the process to hang. 161 | cmd.stdout.destroy(); 162 | cmd.stderr.destroy(); 163 | 164 | if (code !== 0) { 165 | reject(new TestRunFailedError(code ?? undefined, signal ?? undefined)); 166 | } else { 167 | resolve(0); 168 | } 169 | } 170 | 171 | cmd.on('close', onProcessClosed); 172 | cmd.on('exit', onProcessClosed); 173 | }); 174 | 175 | let code: number; 176 | try { 177 | code = await prom; 178 | } finally { 179 | process.removeListener(SIGINT, ctrlc1); 180 | process.removeListener(SIGINT, ctrlc2); 181 | } 182 | 183 | // exit immediately if we handled a SIGINT and no one else did 184 | if (exitRequested && process.listenerCount(SIGINT) === 0) { 185 | process.exit(1); 186 | } 187 | 188 | return code; 189 | } 190 | 191 | export class TestRunFailedError extends Error { 192 | constructor(public readonly code: number | undefined, public readonly signal: string | undefined) { 193 | super(signal ? `Test run terminated with signal ${signal}` : `Test run failed with code ${code}`); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/util.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ChildProcess, SpawnOptions, spawn } from 'child_process'; 7 | import { createHash } from 'crypto'; 8 | import { readFileSync } from 'fs'; 9 | import { HttpProxyAgent } from 'http-proxy-agent'; 10 | import * as https from 'https'; 11 | import { HttpsProxyAgent } from 'https-proxy-agent'; 12 | import * as path from 'path'; 13 | import { URL } from 'url'; 14 | import { DownloadOptions, DownloadPlatform, defaultCachePath, downloadAndUnzipVSCode } from './download'; 15 | import * as request from './request'; 16 | import { TestOptions } from './runTest'; 17 | import { ProgressReporter } from './progress'; 18 | 19 | export let systemDefaultPlatform: DownloadPlatform; 20 | 21 | export const isPlatformWindows = (platform: string) => platform.includes('win32'); 22 | export const isPlatformDarwin = (platform: string) => platform.includes('darwin'); 23 | export const isPlatformLinux = (platform: string) => platform.includes('linux'); 24 | export const isPlatformServer = (platform: string) => platform.includes('server'); 25 | export const isPlatformCLI = (platform: string) => platform.includes('cli-'); 26 | 27 | switch (process.platform) { 28 | case 'darwin': 29 | systemDefaultPlatform = process.arch === 'arm64' ? 'darwin-arm64' : 'darwin'; 30 | break; 31 | case 'win32': 32 | systemDefaultPlatform = process.arch === 'arm64' ? 'win32-arm64-archive' : 'win32-x64-archive'; 33 | break; 34 | default: 35 | systemDefaultPlatform = 36 | process.arch === 'arm64' ? 'linux-arm64' : process.arch === 'arm' ? 'linux-armhf' : 'linux-x64'; 37 | } 38 | 39 | const UNRELEASED_SUFFIX = '-unreleased'; 40 | 41 | export class Version { 42 | public static parse(version: string): Version { 43 | const unreleased = version.endsWith(UNRELEASED_SUFFIX); 44 | if (unreleased) { 45 | version = version.slice(0, -UNRELEASED_SUFFIX.length); 46 | } 47 | 48 | return new Version(version, !unreleased); 49 | } 50 | 51 | constructor( 52 | public readonly id: string, 53 | public readonly isReleased = true, 54 | ) {} 55 | 56 | public get isCommit() { 57 | return /^[0-9a-f]{40}$/.test(this.id); 58 | } 59 | 60 | public get isInsiders() { 61 | return this.id === 'insiders' || this.id.endsWith('-insider'); 62 | } 63 | 64 | public get isStable() { 65 | return this.id === 'stable' || /^[0-9]+\.[0-9]+\.[0-9]$/.test(this.id); 66 | } 67 | 68 | public toString() { 69 | return this.id + (this.isReleased ? '' : UNRELEASED_SUFFIX); 70 | } 71 | } 72 | 73 | export function getVSCodeDownloadUrl(version: Version, platform: string) { 74 | if (version.id === 'insiders') { 75 | return `https://update.code.visualstudio.com/latest/${platform}/insider?released=${version.isReleased}`; 76 | } else if (version.isInsiders) { 77 | return `https://update.code.visualstudio.com/${version.id}/${platform}/insider?released=${version.isReleased}`; 78 | } else if (version.isStable) { 79 | return `https://update.code.visualstudio.com/${version.id}/${platform}/stable?released=${version.isReleased}`; 80 | } else { 81 | // insiders commit hash 82 | return `https://update.code.visualstudio.com/commit:${version.id}/${platform}/insider`; 83 | } 84 | } 85 | 86 | let PROXY_AGENT: HttpProxyAgent | undefined = undefined; 87 | let HTTPS_PROXY_AGENT: HttpsProxyAgent | undefined = undefined; 88 | 89 | if (process.env.npm_config_proxy) { 90 | PROXY_AGENT = new HttpProxyAgent(process.env.npm_config_proxy); 91 | HTTPS_PROXY_AGENT = new HttpsProxyAgent(process.env.npm_config_proxy); 92 | } 93 | if (process.env.npm_config_https_proxy) { 94 | HTTPS_PROXY_AGENT = new HttpsProxyAgent(process.env.npm_config_https_proxy); 95 | } 96 | 97 | export function urlToOptions(url: string): https.RequestOptions { 98 | const parsed = new URL(url); 99 | const options: https.RequestOptions = {}; 100 | if (PROXY_AGENT && parsed.protocol.startsWith('http:')) { 101 | options.agent = PROXY_AGENT; 102 | } 103 | 104 | if (HTTPS_PROXY_AGENT && parsed.protocol.startsWith('https:')) { 105 | options.agent = HTTPS_PROXY_AGENT; 106 | } 107 | 108 | return options; 109 | } 110 | 111 | export function downloadDirToExecutablePath(dir: string, platform: DownloadPlatform) { 112 | if (isPlatformServer(platform)) { 113 | return isPlatformWindows(platform) 114 | ? path.resolve(dir, 'bin', 'code-server.cmd') 115 | : path.resolve(dir, 'bin', 'code-server'); 116 | } else if (isPlatformCLI(platform)) { 117 | return isPlatformWindows(platform) ? path.resolve(dir, 'code.exe') : path.resolve(dir, 'code'); 118 | } else { 119 | if (isPlatformWindows(platform)) { 120 | return path.resolve(dir, 'Code.exe'); 121 | } else if (isPlatformDarwin(platform)) { 122 | return path.resolve(dir, 'Visual Studio Code.app/Contents/MacOS/Electron'); 123 | } else { 124 | return path.resolve(dir, 'code'); 125 | } 126 | } 127 | } 128 | 129 | export function insidersDownloadDirToExecutablePath(dir: string, platform: DownloadPlatform) { 130 | if (isPlatformServer(platform)) { 131 | return isPlatformWindows(platform) 132 | ? path.resolve(dir, 'bin', 'code-server-insiders.cmd') 133 | : path.resolve(dir, 'bin', 'code-server-insiders'); 134 | } else if (isPlatformCLI(platform)) { 135 | return isPlatformWindows(platform) ? path.resolve(dir, 'code-insiders.exe') : path.resolve(dir, 'code-insiders'); 136 | } else { 137 | if (isPlatformWindows(platform)) { 138 | return path.resolve(dir, 'Code - Insiders.exe'); 139 | } else if (isPlatformDarwin(platform)) { 140 | return path.resolve(dir, 'Visual Studio Code - Insiders.app/Contents/MacOS/Electron'); 141 | } else { 142 | return path.resolve(dir, 'code-insiders'); 143 | } 144 | } 145 | } 146 | 147 | export function insidersDownloadDirMetadata(dir: string, platform: DownloadPlatform, reporter: ProgressReporter) { 148 | let productJsonPath; 149 | if (isPlatformServer(platform)) { 150 | productJsonPath = path.resolve(dir, 'product.json'); 151 | } else if (isPlatformWindows(platform)) { 152 | productJsonPath = path.resolve(dir, 'resources/app/product.json'); 153 | } else if (isPlatformDarwin(platform)) { 154 | productJsonPath = path.resolve(dir, 'Visual Studio Code - Insiders.app/Contents/Resources/app/product.json'); 155 | } else { 156 | productJsonPath = path.resolve(dir, 'resources/app/product.json'); 157 | } 158 | 159 | try { 160 | const productJson = JSON.parse(readFileSync(productJsonPath, 'utf-8')); 161 | 162 | return { 163 | version: productJson.commit, 164 | date: new Date(productJson.date), 165 | }; 166 | } catch (e) { 167 | reporter.error(`Error reading product.json (${e}) will download again`); 168 | return { 169 | version: 'unknown', 170 | date: new Date(0), 171 | }; 172 | } 173 | } 174 | 175 | export interface IUpdateMetadata { 176 | url: string; 177 | name: string; 178 | version: string; 179 | productVersion: string; 180 | hash: string; 181 | timestamp: number; 182 | sha256hash: string; 183 | supportsFastUpdate: boolean; 184 | } 185 | 186 | export async function getInsidersVersionMetadata(platform: string, version: string, released: boolean) { 187 | const remoteUrl = `https://update.code.visualstudio.com/api/versions/${version}/${platform}/insider?released=${released}`; 188 | return await request.getJSON(remoteUrl, 30_000); 189 | } 190 | 191 | export async function getLatestInsidersMetadata(platform: string, released: boolean) { 192 | const remoteUrl = `https://update.code.visualstudio.com/api/update/${platform}/insider/latest?released=${released}`; 193 | return await request.getJSON(remoteUrl, 30_000); 194 | } 195 | 196 | /** 197 | * Resolve the VS Code cli path from executable path returned from `downloadAndUnzipVSCode`. 198 | * Usually you will want {@link resolveCliArgsFromVSCodeExecutablePath} instead. 199 | */ 200 | export function resolveCliPathFromVSCodeExecutablePath( 201 | vscodeExecutablePath: string, 202 | platform: DownloadPlatform = systemDefaultPlatform, 203 | ) { 204 | if (platform === 'win32-archive') { 205 | throw new Error('Windows 32-bit is no longer supported'); 206 | } 207 | if (isPlatformServer(platform) || isPlatformCLI(platform)) { 208 | // no separate CLI 209 | return vscodeExecutablePath; 210 | } 211 | if (isPlatformWindows(platform)) { 212 | if (vscodeExecutablePath.endsWith('Code - Insiders.exe')) { 213 | return path.resolve(vscodeExecutablePath, '../bin/code-insiders.cmd'); 214 | } else { 215 | return path.resolve(vscodeExecutablePath, '../bin/code.cmd'); 216 | } 217 | } else if (isPlatformDarwin(platform)) { 218 | return path.resolve(vscodeExecutablePath, '../../../Contents/Resources/app/bin/code'); 219 | } else { 220 | if (vscodeExecutablePath.endsWith('code-insiders')) { 221 | return path.resolve(vscodeExecutablePath, '../bin/code-insiders'); 222 | } else { 223 | return path.resolve(vscodeExecutablePath, '../bin/code'); 224 | } 225 | } 226 | } 227 | /** 228 | * Resolve the VS Code cli arguments from executable path returned from `downloadAndUnzipVSCode`. 229 | * You can use this path to spawn processes for extension management. For example: 230 | * 231 | * ```ts 232 | * const cp = require('child_process'); 233 | * const { downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath } = require('@vscode/test-electron') 234 | * const vscodeExecutablePath = await downloadAndUnzipVSCode('1.36.0'); 235 | * const [cli, ...args] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath); 236 | * 237 | * cp.spawnSync(cli, [...args, '--install-extension', ''], { 238 | * encoding: 'utf-8', 239 | * stdio: 'inherit' 240 | * shell: process.platform === 'win32', 241 | * }); 242 | * ``` 243 | * 244 | * @param vscodeExecutablePath The `vscodeExecutablePath` from `downloadAndUnzipVSCode`. 245 | */ 246 | export function resolveCliArgsFromVSCodeExecutablePath( 247 | vscodeExecutablePath: string, 248 | options?: Pick, 249 | ) { 250 | const args = [ 251 | resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath, options?.platform ?? systemDefaultPlatform), 252 | ]; 253 | if (!options?.reuseMachineInstall) { 254 | args.push(...getProfileArguments(args)); 255 | } 256 | 257 | return args; 258 | } 259 | 260 | export interface RunVSCodeCommandOptions extends Partial { 261 | /** 262 | * Additional options to pass to `child_process.spawn` 263 | */ 264 | spawn?: SpawnOptions; 265 | 266 | /** 267 | * Whether VS Code should be launched using default settings and extensions 268 | * installed on this machine. If `false`, then separate directories will be 269 | * used inside the `.vscode-test` folder within the project. 270 | * 271 | * Defaults to `false`. 272 | */ 273 | reuseMachineInstall?: boolean; 274 | } 275 | 276 | /** Adds the extensions and user data dir to the arguments for the VS Code CLI */ 277 | export function getProfileArguments(args: readonly string[]) { 278 | const out: string[] = []; 279 | if (!hasArg('extensions-dir', args)) { 280 | out.push(`--extensions-dir=${path.join(defaultCachePath, 'extensions')}`); 281 | } 282 | 283 | if (!hasArg('user-data-dir', args)) { 284 | out.push(`--user-data-dir=${path.join(defaultCachePath, 'user-data')}`); 285 | } 286 | 287 | return out; 288 | } 289 | 290 | export function hasArg(argName: string, argList: readonly string[]) { 291 | return argList.some((a) => a === `--${argName}` || a.startsWith(`--${argName}=`)); 292 | } 293 | 294 | export class VSCodeCommandError extends Error { 295 | constructor( 296 | args: string[], 297 | public readonly exitCode: number | null, 298 | public readonly stderr: string, 299 | public stdout: string, 300 | ) { 301 | super(`'code ${args.join(' ')}' failed with exit code ${exitCode}:\n\n${stderr}\n\n${stdout}`); 302 | } 303 | } 304 | 305 | /** 306 | * Runs a VS Code command, and returns its output. 307 | * 308 | * @throws a {@link VSCodeCommandError} if the command fails 309 | */ 310 | export async function runVSCodeCommand(_args: readonly string[], options: RunVSCodeCommandOptions = {}) { 311 | const args = _args.slice(); 312 | 313 | let executable = await downloadAndUnzipVSCode(options); 314 | let shell = false; 315 | if (!options.reuseMachineInstall) { 316 | args.push(...getProfileArguments(args)); 317 | } 318 | 319 | // Unless the user is manually running tests or extension development, then resolve to the CLI script 320 | if (!hasArg('extensionTestsPath', args) && !hasArg('extensionDevelopmentPath', args)) { 321 | executable = resolveCliPathFromVSCodeExecutablePath(executable, options?.platform ?? systemDefaultPlatform); 322 | shell = process.platform === 'win32'; // CVE-2024-27980 323 | } 324 | 325 | return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { 326 | let stdout = ''; 327 | let stderr = ''; 328 | 329 | const child = spawn(shell ? `"${executable}"` : executable, args, { 330 | stdio: 'pipe', 331 | shell, 332 | windowsHide: true, 333 | ...options.spawn, 334 | }); 335 | 336 | child.stdout?.setEncoding('utf-8').on('data', (data) => (stdout += data)); 337 | child.stderr?.setEncoding('utf-8').on('data', (data) => (stderr += data)); 338 | 339 | child.on('error', reject); 340 | child.on('exit', (code) => { 341 | if (code !== 0) { 342 | reject(new VSCodeCommandError(args, code, stderr, stdout)); 343 | } else { 344 | resolve({ stdout, stderr }); 345 | } 346 | }); 347 | }); 348 | } 349 | 350 | /** Predicates whether arg is undefined or null */ 351 | export function isDefined(arg: T | undefined | null): arg is T { 352 | return arg != null; 353 | } 354 | 355 | /** 356 | * Validates the stream data matches the given length and checksum, if any. 357 | * 358 | * Note: md5 is not ideal, but it's what we get from the CDN, and for the 359 | * purposes of self-reported content verification is sufficient. 360 | */ 361 | export function validateStream(readable: NodeJS.ReadableStream, length: number, sha256?: string) { 362 | let actualLen = 0; 363 | const checksum = sha256 ? createHash('sha256') : undefined; 364 | return new Promise((resolve, reject) => { 365 | readable.on('data', (chunk) => { 366 | checksum?.update(chunk); 367 | actualLen += chunk.length; 368 | }); 369 | readable.on('error', reject); 370 | readable.on('end', () => { 371 | if (actualLen !== length) { 372 | return reject(new Error(`Downloaded stream length ${actualLen} does not match expected length ${length}`)); 373 | } 374 | 375 | const digest = checksum?.digest('hex'); 376 | if (digest && digest !== sha256) { 377 | return reject(new Error(`Downloaded file checksum ${digest} does not match expected checksum ${sha256}`)); 378 | } 379 | 380 | resolve(); 381 | }); 382 | }); 383 | } 384 | 385 | /** Gets a Buffer from a Node.js stream */ 386 | export function streamToBuffer(readable: NodeJS.ReadableStream) { 387 | return new Promise((resolve, reject) => { 388 | const chunks: Buffer[] = []; 389 | readable.on('data', (chunk) => chunks.push(chunk)); 390 | readable.on('error', reject); 391 | readable.on('end', () => resolve(Buffer.concat(chunks))); 392 | }); 393 | } 394 | /** Gets whether child is a subdirectory of the parent */ 395 | export function isSubdirectory(parent: string, child: string) { 396 | const relative = path.relative(parent, child); 397 | return !relative.startsWith('..') && !path.isAbsolute(relative); 398 | } 399 | 400 | /** 401 | * Wraps a function so that it's called once, and never again, memoizing 402 | * the result unless it rejects. 403 | */ 404 | export function onceWithoutRejections(fn: (...args: Args) => Promise) { 405 | let value: Promise | undefined; 406 | return (...args: Args) => { 407 | if (!value) { 408 | value = fn(...args).catch((err) => { 409 | value = undefined; 410 | throw err; 411 | }); 412 | } 413 | 414 | return value; 415 | }; 416 | } 417 | 418 | export function killTree(processId: number, force: boolean) { 419 | let cp: ChildProcess; 420 | 421 | if (process.platform === 'win32') { 422 | const windir = process.env['WINDIR'] || 'C:\\Windows'; 423 | 424 | // when killing a process in Windows its child processes are *not* killed but become root processes. 425 | // Therefore we use TASKKILL.EXE 426 | cp = spawn( 427 | path.join(windir, 'System32', 'taskkill.exe'), 428 | [...(force ? ['/F'] : []), '/T', '/PID', processId.toString()], 429 | { stdio: 'inherit' }, 430 | ); 431 | } else { 432 | // on linux and OS X we kill all direct and indirect child processes as well 433 | cp = spawn('sh', [path.resolve(__dirname, '../killTree.sh'), processId.toString(), force ? '9' : '15'], { 434 | stdio: 'inherit', 435 | }); 436 | } 437 | 438 | return new Promise((resolve, reject) => { 439 | cp.on('error', reject).on('exit', resolve); 440 | }); 441 | } 442 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vscode/test-electron", 3 | "version": "2.5.2", 4 | "scripts": { 5 | "compile": "tsc -p ./", 6 | "watch": "tsc -w -p ./", 7 | "prepack": "tsc -p ./", 8 | "fmt": "prettier --write \"lib/**/*.{ts,mts}\" \"*.md\"", 9 | "test": "eslint \"lib/**/*.{ts,mts}\" && vitest && tsc --noEmit", 10 | "prepare": "husky" 11 | }, 12 | "lint-staged": { 13 | "*.ts": [ 14 | "eslint --fix", 15 | "prettier --write" 16 | ], 17 | "*.md": [ 18 | "prettier --write" 19 | ] 20 | }, 21 | "main": "./out/index.js", 22 | "engines": { 23 | "node": ">=16" 24 | }, 25 | "dependencies": { 26 | "http-proxy-agent": "^7.0.2", 27 | "https-proxy-agent": "^7.0.5", 28 | "jszip": "^3.10.1", 29 | "ora": "^8.1.0", 30 | "semver": "^7.6.2" 31 | }, 32 | "devDependencies": { 33 | "@eslint/js": "^9.6.0", 34 | "@types/node": "^20", 35 | "@types/rimraf": "^3.0.2", 36 | "@types/semver": "^7.5.8", 37 | "eslint": "^8.56.0", 38 | "husky": "^9.0.11", 39 | "lint-staged": "^15.2.7", 40 | "prettier": "^3.3.2", 41 | "typescript": "^5.5.3", 42 | "typescript-eslint": "^7.15.0", 43 | "vitest": "^3.1.1" 44 | }, 45 | "license": "MIT", 46 | "author": "Visual Studio Code Team", 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/Microsoft/vscode-test.git" 50 | }, 51 | "bugs": { 52 | "url": "https://github.com/Microsoft/vscode-test/issues" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pipeline.yml: -------------------------------------------------------------------------------- 1 | name: $(Date:yyyyMMdd)$(Rev:.r) 2 | 3 | trigger: 4 | branches: 5 | include: 6 | - main 7 | 8 | resources: 9 | repositories: 10 | - repository: templates 11 | type: github 12 | name: microsoft/vscode-engineering 13 | ref: main 14 | endpoint: Monaco 15 | 16 | parameters: 17 | - name: publishPackage 18 | displayName: 🚀 Publish test-electron 19 | type: boolean 20 | default: false 21 | 22 | extends: 23 | template: azure-pipelines/npm-package/pipeline.yml@templates 24 | parameters: 25 | npmPackages: 26 | - name: test-electron 27 | ghCreateTag: false 28 | buildSteps: 29 | - script: npm ci 30 | displayName: Install dependencies 31 | 32 | testPlatforms: 33 | - name: Linux 34 | nodeVersions: 35 | - 20.x 36 | - name: MacOS 37 | nodeVersions: 38 | - 20.x 39 | - name: Windows 40 | nodeVersions: 41 | - 20.x 42 | 43 | testSteps: 44 | - script: npm ci 45 | displayName: Install dependencies 46 | 47 | - script: npm test 48 | displayName: Run own tests 49 | 50 | - script: cd sample && npm ci 51 | displayName: Install dependencies (fs-provider) 52 | 53 | - bash: | 54 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 55 | echo ">>> Started xvfb" 56 | displayName: Start xvfb 57 | condition: eq(variables['Agent.OS'], 'Linux') 58 | 59 | - task: NodeTool@0 60 | displayName: Switch to Node 16 61 | inputs: 62 | versionSpec: 16.x 63 | 64 | - script: cd sample && npm run test 65 | displayName: Test package 66 | env: 67 | DISPLAY: ':99.0' 68 | 69 | publishPackage: ${{ parameters.publishPackage }} 70 | -------------------------------------------------------------------------------- /sample/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run VSCode Extension Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup Node.js environment 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Install dependencies 24 | run: npm install 25 | 26 | - name: Compile 27 | run: npm run compile 28 | 29 | - name: Run tests 30 | run: xvfb-run -a npm test 31 | if: runner.os == 'Linux' 32 | 33 | - name: Run tests 34 | run: npm test 35 | if: runner.os != 'Linux' 36 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | -------------------------------------------------------------------------------- /sample/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /sample/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension Tests - Suite 1", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}", 15 | "--extensionTestsPath=${workspaceFolder}/out/sample/test/suite" 16 | ], 17 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 18 | "preLaunchTask": "npm: watch" 19 | }, 20 | { 21 | "name": "Extension Tests - Suite 2", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "runtimeExecutable": "${execPath}", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceFolder}", 27 | "--extensionTestsPath=${workspaceFolder}/out/sample/test/suite2" 28 | ], 29 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 30 | "preLaunchTask": "npm: watch" 31 | }, 32 | { 33 | "name": "Run Extension", 34 | "type": "extensionHost", 35 | "request": "launch", 36 | "runtimeExecutable": "${execPath}", 37 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 38 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 39 | "preLaunchTask": "npm: watch" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /sample/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /sample/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /sample/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.* 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 |

2 |

vscode-test-sample

3 |

4 | 5 | Sample for using https://github.com/microsoft/vscode-test. 6 | 7 | Continuously tested with latest changes: 8 | 9 | - [Azure DevOps](https://dev.azure.com/monacotools/Monaco/_build?definitionId=418) 10 | 11 | When making changes to `vscode-test` library, you should compile and run the tests in this sample project locally to make sure the tests can still run successfully. 12 | 13 | ```bash 14 | npm install 15 | npm run compile 16 | npm test 17 | ``` 18 | -------------------------------------------------------------------------------- /sample/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - '*' 3 | pr: 4 | - '*' 5 | 6 | strategy: 7 | matrix: 8 | linux: 9 | imageName: 'ubuntu-16.04' 10 | mac: 11 | imageName: 'macos-10.14' 12 | windows: 13 | imageName: 'vs2017-win2016' 14 | 15 | pool: 16 | vmImage: $(imageName) 17 | 18 | steps: 19 | - task: NodeTool@0 20 | inputs: 21 | versionSpec: '8.x' 22 | displayName: 'Install Node.js' 23 | 24 | - bash: | 25 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 26 | echo ">>> Started xvfb" 27 | displayName: Start xvfb 28 | condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) 29 | 30 | - bash: | 31 | echo ">>> Compile vscode-test" 32 | npm ci && npm run compile 33 | echo ">>> Compiled vscode-test" 34 | cd sample 35 | echo ">>> Run sample integration test" 36 | npm ci && npm run compile && npm test 37 | displayName: Run Tests 38 | env: 39 | DISPLAY: ':99.0' 40 | -------------------------------------------------------------------------------- /sample/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-test-sample", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "vscode-test-sample", 9 | "version": "0.0.1", 10 | "devDependencies": { 11 | "@types/mocha": "^10.0.0", 12 | "@types/node": "^20", 13 | "@types/vscode": "^1.52.0", 14 | "glob": "^10.3.10", 15 | "mocha": "^10.0.0", 16 | "typescript": "^5.4.5" 17 | }, 18 | "engines": { 19 | "vscode": "^1.32.0" 20 | } 21 | }, 22 | "node_modules/@isaacs/cliui": { 23 | "version": "8.0.2", 24 | "dev": true, 25 | "license": "ISC", 26 | "dependencies": { 27 | "string-width": "^5.1.2", 28 | "string-width-cjs": "npm:string-width@^4.2.0", 29 | "strip-ansi": "^7.0.1", 30 | "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 31 | "wrap-ansi": "^8.1.0", 32 | "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 33 | }, 34 | "engines": { 35 | "node": ">=12" 36 | } 37 | }, 38 | "node_modules/@isaacs/cliui/node_modules/ansi-regex": { 39 | "version": "6.0.1", 40 | "dev": true, 41 | "license": "MIT", 42 | "engines": { 43 | "node": ">=12" 44 | }, 45 | "funding": { 46 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 47 | } 48 | }, 49 | "node_modules/@isaacs/cliui/node_modules/ansi-styles": { 50 | "version": "6.2.1", 51 | "dev": true, 52 | "license": "MIT", 53 | "engines": { 54 | "node": ">=12" 55 | }, 56 | "funding": { 57 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 58 | } 59 | }, 60 | "node_modules/@isaacs/cliui/node_modules/emoji-regex": { 61 | "version": "9.2.2", 62 | "dev": true, 63 | "license": "MIT" 64 | }, 65 | "node_modules/@isaacs/cliui/node_modules/string-width": { 66 | "version": "5.1.2", 67 | "dev": true, 68 | "license": "MIT", 69 | "dependencies": { 70 | "eastasianwidth": "^0.2.0", 71 | "emoji-regex": "^9.2.2", 72 | "strip-ansi": "^7.0.1" 73 | }, 74 | "engines": { 75 | "node": ">=12" 76 | }, 77 | "funding": { 78 | "url": "https://github.com/sponsors/sindresorhus" 79 | } 80 | }, 81 | "node_modules/@isaacs/cliui/node_modules/strip-ansi": { 82 | "version": "7.1.0", 83 | "dev": true, 84 | "license": "MIT", 85 | "dependencies": { 86 | "ansi-regex": "^6.0.1" 87 | }, 88 | "engines": { 89 | "node": ">=12" 90 | }, 91 | "funding": { 92 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 93 | } 94 | }, 95 | "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { 96 | "version": "8.1.0", 97 | "dev": true, 98 | "license": "MIT", 99 | "dependencies": { 100 | "ansi-styles": "^6.1.0", 101 | "string-width": "^5.0.1", 102 | "strip-ansi": "^7.0.1" 103 | }, 104 | "engines": { 105 | "node": ">=12" 106 | }, 107 | "funding": { 108 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 109 | } 110 | }, 111 | "node_modules/@pkgjs/parseargs": { 112 | "version": "0.11.0", 113 | "dev": true, 114 | "license": "MIT", 115 | "optional": true, 116 | "engines": { 117 | "node": ">=14" 118 | } 119 | }, 120 | "node_modules/@types/mocha": { 121 | "version": "10.0.10", 122 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", 123 | "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", 124 | "dev": true 125 | }, 126 | "node_modules/@types/node": { 127 | "version": "20.17.30", 128 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", 129 | "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", 130 | "dev": true, 131 | "dependencies": { 132 | "undici-types": "~6.19.2" 133 | } 134 | }, 135 | "node_modules/@types/vscode": { 136 | "version": "1.52.0", 137 | "dev": true, 138 | "license": "MIT" 139 | }, 140 | "node_modules/ansi-colors": { 141 | "version": "4.1.3", 142 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", 143 | "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", 144 | "dev": true, 145 | "engines": { 146 | "node": ">=6" 147 | } 148 | }, 149 | "node_modules/ansi-regex": { 150 | "version": "5.0.1", 151 | "dev": true, 152 | "license": "MIT", 153 | "engines": { 154 | "node": ">=8" 155 | } 156 | }, 157 | "node_modules/ansi-styles": { 158 | "version": "4.3.0", 159 | "dev": true, 160 | "license": "MIT", 161 | "dependencies": { 162 | "color-convert": "^2.0.1" 163 | }, 164 | "engines": { 165 | "node": ">=8" 166 | }, 167 | "funding": { 168 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 169 | } 170 | }, 171 | "node_modules/anymatch": { 172 | "version": "3.1.3", 173 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 174 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 175 | "dev": true, 176 | "dependencies": { 177 | "normalize-path": "^3.0.0", 178 | "picomatch": "^2.0.4" 179 | }, 180 | "engines": { 181 | "node": ">= 8" 182 | } 183 | }, 184 | "node_modules/argparse": { 185 | "version": "2.0.1", 186 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 187 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 188 | "dev": true 189 | }, 190 | "node_modules/balanced-match": { 191 | "version": "1.0.0", 192 | "dev": true, 193 | "license": "MIT" 194 | }, 195 | "node_modules/binary-extensions": { 196 | "version": "2.3.0", 197 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 198 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 199 | "dev": true, 200 | "engines": { 201 | "node": ">=8" 202 | }, 203 | "funding": { 204 | "url": "https://github.com/sponsors/sindresorhus" 205 | } 206 | }, 207 | "node_modules/brace-expansion": { 208 | "version": "2.0.1", 209 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 210 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 211 | "dev": true, 212 | "dependencies": { 213 | "balanced-match": "^1.0.0" 214 | } 215 | }, 216 | "node_modules/braces": { 217 | "version": "3.0.3", 218 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 219 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 220 | "dev": true, 221 | "dependencies": { 222 | "fill-range": "^7.1.1" 223 | }, 224 | "engines": { 225 | "node": ">=8" 226 | } 227 | }, 228 | "node_modules/browser-stdout": { 229 | "version": "1.3.1", 230 | "dev": true, 231 | "license": "ISC" 232 | }, 233 | "node_modules/chalk": { 234 | "version": "4.1.2", 235 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 236 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 237 | "dev": true, 238 | "dependencies": { 239 | "ansi-styles": "^4.1.0", 240 | "supports-color": "^7.1.0" 241 | }, 242 | "engines": { 243 | "node": ">=10" 244 | }, 245 | "funding": { 246 | "url": "https://github.com/chalk/chalk?sponsor=1" 247 | } 248 | }, 249 | "node_modules/chalk/node_modules/supports-color": { 250 | "version": "7.2.0", 251 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 252 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 253 | "dev": true, 254 | "dependencies": { 255 | "has-flag": "^4.0.0" 256 | }, 257 | "engines": { 258 | "node": ">=8" 259 | } 260 | }, 261 | "node_modules/chokidar": { 262 | "version": "3.6.0", 263 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 264 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 265 | "dev": true, 266 | "dependencies": { 267 | "anymatch": "~3.1.2", 268 | "braces": "~3.0.2", 269 | "glob-parent": "~5.1.2", 270 | "is-binary-path": "~2.1.0", 271 | "is-glob": "~4.0.1", 272 | "normalize-path": "~3.0.0", 273 | "readdirp": "~3.6.0" 274 | }, 275 | "engines": { 276 | "node": ">= 8.10.0" 277 | }, 278 | "funding": { 279 | "url": "https://paulmillr.com/funding/" 280 | }, 281 | "optionalDependencies": { 282 | "fsevents": "~2.3.2" 283 | } 284 | }, 285 | "node_modules/cliui": { 286 | "version": "7.0.4", 287 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 288 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 289 | "dev": true, 290 | "dependencies": { 291 | "string-width": "^4.2.0", 292 | "strip-ansi": "^6.0.0", 293 | "wrap-ansi": "^7.0.0" 294 | } 295 | }, 296 | "node_modules/color-convert": { 297 | "version": "2.0.1", 298 | "dev": true, 299 | "license": "MIT", 300 | "dependencies": { 301 | "color-name": "~1.1.4" 302 | }, 303 | "engines": { 304 | "node": ">=7.0.0" 305 | } 306 | }, 307 | "node_modules/color-name": { 308 | "version": "1.1.4", 309 | "dev": true, 310 | "license": "MIT" 311 | }, 312 | "node_modules/cross-spawn": { 313 | "version": "7.0.3", 314 | "dev": true, 315 | "license": "MIT", 316 | "dependencies": { 317 | "path-key": "^3.1.0", 318 | "shebang-command": "^2.0.0", 319 | "which": "^2.0.1" 320 | }, 321 | "engines": { 322 | "node": ">= 8" 323 | } 324 | }, 325 | "node_modules/debug": { 326 | "version": "4.4.0", 327 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 328 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 329 | "dev": true, 330 | "dependencies": { 331 | "ms": "^2.1.3" 332 | }, 333 | "engines": { 334 | "node": ">=6.0" 335 | }, 336 | "peerDependenciesMeta": { 337 | "supports-color": { 338 | "optional": true 339 | } 340 | } 341 | }, 342 | "node_modules/diff": { 343 | "version": "5.2.0", 344 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", 345 | "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", 346 | "dev": true, 347 | "engines": { 348 | "node": ">=0.3.1" 349 | } 350 | }, 351 | "node_modules/eastasianwidth": { 352 | "version": "0.2.0", 353 | "dev": true, 354 | "license": "MIT" 355 | }, 356 | "node_modules/emoji-regex": { 357 | "version": "8.0.0", 358 | "dev": true, 359 | "license": "MIT" 360 | }, 361 | "node_modules/escalade": { 362 | "version": "3.2.0", 363 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 364 | "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 365 | "dev": true, 366 | "engines": { 367 | "node": ">=6" 368 | } 369 | }, 370 | "node_modules/escape-string-regexp": { 371 | "version": "4.0.0", 372 | "dev": true, 373 | "license": "MIT", 374 | "engines": { 375 | "node": ">=10" 376 | }, 377 | "funding": { 378 | "url": "https://github.com/sponsors/sindresorhus" 379 | } 380 | }, 381 | "node_modules/fill-range": { 382 | "version": "7.1.1", 383 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 384 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 385 | "dev": true, 386 | "dependencies": { 387 | "to-regex-range": "^5.0.1" 388 | }, 389 | "engines": { 390 | "node": ">=8" 391 | } 392 | }, 393 | "node_modules/find-up": { 394 | "version": "5.0.0", 395 | "dev": true, 396 | "license": "MIT", 397 | "dependencies": { 398 | "locate-path": "^6.0.0", 399 | "path-exists": "^4.0.0" 400 | }, 401 | "engines": { 402 | "node": ">=10" 403 | }, 404 | "funding": { 405 | "url": "https://github.com/sponsors/sindresorhus" 406 | } 407 | }, 408 | "node_modules/flat": { 409 | "version": "5.0.2", 410 | "dev": true, 411 | "license": "BSD-3-Clause", 412 | "bin": { 413 | "flat": "cli.js" 414 | } 415 | }, 416 | "node_modules/foreground-child": { 417 | "version": "3.1.1", 418 | "dev": true, 419 | "license": "ISC", 420 | "dependencies": { 421 | "cross-spawn": "^7.0.0", 422 | "signal-exit": "^4.0.1" 423 | }, 424 | "engines": { 425 | "node": ">=14" 426 | }, 427 | "funding": { 428 | "url": "https://github.com/sponsors/isaacs" 429 | } 430 | }, 431 | "node_modules/fs.realpath": { 432 | "version": "1.0.0", 433 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 434 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 435 | "dev": true 436 | }, 437 | "node_modules/fsevents": { 438 | "version": "2.3.3", 439 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 440 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 441 | "dev": true, 442 | "hasInstallScript": true, 443 | "optional": true, 444 | "os": [ 445 | "darwin" 446 | ], 447 | "engines": { 448 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 449 | } 450 | }, 451 | "node_modules/get-caller-file": { 452 | "version": "2.0.5", 453 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 454 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 455 | "dev": true, 456 | "engines": { 457 | "node": "6.* || 8.* || >= 10.*" 458 | } 459 | }, 460 | "node_modules/glob": { 461 | "version": "10.3.10", 462 | "dev": true, 463 | "license": "ISC", 464 | "dependencies": { 465 | "foreground-child": "^3.1.0", 466 | "jackspeak": "^2.3.5", 467 | "minimatch": "^9.0.1", 468 | "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", 469 | "path-scurry": "^1.10.1" 470 | }, 471 | "bin": { 472 | "glob": "dist/esm/bin.mjs" 473 | }, 474 | "engines": { 475 | "node": ">=16 || 14 >=14.17" 476 | }, 477 | "funding": { 478 | "url": "https://github.com/sponsors/isaacs" 479 | } 480 | }, 481 | "node_modules/glob-parent": { 482 | "version": "5.1.2", 483 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 484 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 485 | "dev": true, 486 | "dependencies": { 487 | "is-glob": "^4.0.1" 488 | }, 489 | "engines": { 490 | "node": ">= 6" 491 | } 492 | }, 493 | "node_modules/glob/node_modules/minimatch": { 494 | "version": "9.0.3", 495 | "dev": true, 496 | "license": "ISC", 497 | "dependencies": { 498 | "brace-expansion": "^2.0.1" 499 | }, 500 | "engines": { 501 | "node": ">=16 || 14 >=14.17" 502 | }, 503 | "funding": { 504 | "url": "https://github.com/sponsors/isaacs" 505 | } 506 | }, 507 | "node_modules/has-flag": { 508 | "version": "4.0.0", 509 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 510 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 511 | "dev": true, 512 | "engines": { 513 | "node": ">=8" 514 | } 515 | }, 516 | "node_modules/he": { 517 | "version": "1.2.0", 518 | "dev": true, 519 | "license": "MIT", 520 | "bin": { 521 | "he": "bin/he" 522 | } 523 | }, 524 | "node_modules/inflight": { 525 | "version": "1.0.6", 526 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 527 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 528 | "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", 529 | "dev": true, 530 | "dependencies": { 531 | "once": "^1.3.0", 532 | "wrappy": "1" 533 | } 534 | }, 535 | "node_modules/inherits": { 536 | "version": "2.0.4", 537 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 538 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 539 | "dev": true 540 | }, 541 | "node_modules/is-binary-path": { 542 | "version": "2.1.0", 543 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 544 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 545 | "dev": true, 546 | "dependencies": { 547 | "binary-extensions": "^2.0.0" 548 | }, 549 | "engines": { 550 | "node": ">=8" 551 | } 552 | }, 553 | "node_modules/is-extglob": { 554 | "version": "2.1.1", 555 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 556 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 557 | "dev": true, 558 | "engines": { 559 | "node": ">=0.10.0" 560 | } 561 | }, 562 | "node_modules/is-fullwidth-code-point": { 563 | "version": "3.0.0", 564 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 565 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 566 | "dev": true, 567 | "engines": { 568 | "node": ">=8" 569 | } 570 | }, 571 | "node_modules/is-glob": { 572 | "version": "4.0.3", 573 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 574 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 575 | "dev": true, 576 | "dependencies": { 577 | "is-extglob": "^2.1.1" 578 | }, 579 | "engines": { 580 | "node": ">=0.10.0" 581 | } 582 | }, 583 | "node_modules/is-number": { 584 | "version": "7.0.0", 585 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 586 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 587 | "dev": true, 588 | "engines": { 589 | "node": ">=0.12.0" 590 | } 591 | }, 592 | "node_modules/is-plain-obj": { 593 | "version": "2.1.0", 594 | "dev": true, 595 | "license": "MIT", 596 | "engines": { 597 | "node": ">=8" 598 | } 599 | }, 600 | "node_modules/is-unicode-supported": { 601 | "version": "0.1.0", 602 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 603 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 604 | "dev": true, 605 | "engines": { 606 | "node": ">=10" 607 | }, 608 | "funding": { 609 | "url": "https://github.com/sponsors/sindresorhus" 610 | } 611 | }, 612 | "node_modules/isexe": { 613 | "version": "2.0.0", 614 | "dev": true, 615 | "license": "ISC" 616 | }, 617 | "node_modules/jackspeak": { 618 | "version": "2.3.6", 619 | "dev": true, 620 | "license": "BlueOak-1.0.0", 621 | "dependencies": { 622 | "@isaacs/cliui": "^8.0.2" 623 | }, 624 | "engines": { 625 | "node": ">=14" 626 | }, 627 | "funding": { 628 | "url": "https://github.com/sponsors/isaacs" 629 | }, 630 | "optionalDependencies": { 631 | "@pkgjs/parseargs": "^0.11.0" 632 | } 633 | }, 634 | "node_modules/js-yaml": { 635 | "version": "4.1.0", 636 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 637 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 638 | "dev": true, 639 | "dependencies": { 640 | "argparse": "^2.0.1" 641 | }, 642 | "bin": { 643 | "js-yaml": "bin/js-yaml.js" 644 | } 645 | }, 646 | "node_modules/locate-path": { 647 | "version": "6.0.0", 648 | "dev": true, 649 | "license": "MIT", 650 | "dependencies": { 651 | "p-locate": "^5.0.0" 652 | }, 653 | "engines": { 654 | "node": ">=10" 655 | }, 656 | "funding": { 657 | "url": "https://github.com/sponsors/sindresorhus" 658 | } 659 | }, 660 | "node_modules/log-symbols": { 661 | "version": "4.1.0", 662 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 663 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 664 | "dev": true, 665 | "dependencies": { 666 | "chalk": "^4.1.0", 667 | "is-unicode-supported": "^0.1.0" 668 | }, 669 | "engines": { 670 | "node": ">=10" 671 | }, 672 | "funding": { 673 | "url": "https://github.com/sponsors/sindresorhus" 674 | } 675 | }, 676 | "node_modules/lru-cache": { 677 | "version": "10.2.0", 678 | "dev": true, 679 | "license": "ISC", 680 | "engines": { 681 | "node": "14 || >=16.14" 682 | } 683 | }, 684 | "node_modules/minimatch": { 685 | "version": "5.1.6", 686 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", 687 | "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", 688 | "dev": true, 689 | "dependencies": { 690 | "brace-expansion": "^2.0.1" 691 | }, 692 | "engines": { 693 | "node": ">=10" 694 | } 695 | }, 696 | "node_modules/minipass": { 697 | "version": "7.0.4", 698 | "dev": true, 699 | "license": "ISC", 700 | "engines": { 701 | "node": ">=16 || 14 >=14.17" 702 | } 703 | }, 704 | "node_modules/mocha": { 705 | "version": "10.8.2", 706 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", 707 | "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", 708 | "dev": true, 709 | "dependencies": { 710 | "ansi-colors": "^4.1.3", 711 | "browser-stdout": "^1.3.1", 712 | "chokidar": "^3.5.3", 713 | "debug": "^4.3.5", 714 | "diff": "^5.2.0", 715 | "escape-string-regexp": "^4.0.0", 716 | "find-up": "^5.0.0", 717 | "glob": "^8.1.0", 718 | "he": "^1.2.0", 719 | "js-yaml": "^4.1.0", 720 | "log-symbols": "^4.1.0", 721 | "minimatch": "^5.1.6", 722 | "ms": "^2.1.3", 723 | "serialize-javascript": "^6.0.2", 724 | "strip-json-comments": "^3.1.1", 725 | "supports-color": "^8.1.1", 726 | "workerpool": "^6.5.1", 727 | "yargs": "^16.2.0", 728 | "yargs-parser": "^20.2.9", 729 | "yargs-unparser": "^2.0.0" 730 | }, 731 | "bin": { 732 | "_mocha": "bin/_mocha", 733 | "mocha": "bin/mocha.js" 734 | }, 735 | "engines": { 736 | "node": ">= 14.0.0" 737 | } 738 | }, 739 | "node_modules/mocha/node_modules/glob": { 740 | "version": "8.1.0", 741 | "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", 742 | "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", 743 | "deprecated": "Glob versions prior to v9 are no longer supported", 744 | "dev": true, 745 | "dependencies": { 746 | "fs.realpath": "^1.0.0", 747 | "inflight": "^1.0.4", 748 | "inherits": "2", 749 | "minimatch": "^5.0.1", 750 | "once": "^1.3.0" 751 | }, 752 | "engines": { 753 | "node": ">=12" 754 | }, 755 | "funding": { 756 | "url": "https://github.com/sponsors/isaacs" 757 | } 758 | }, 759 | "node_modules/ms": { 760 | "version": "2.1.3", 761 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 762 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 763 | "dev": true 764 | }, 765 | "node_modules/normalize-path": { 766 | "version": "3.0.0", 767 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 768 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 769 | "dev": true, 770 | "engines": { 771 | "node": ">=0.10.0" 772 | } 773 | }, 774 | "node_modules/once": { 775 | "version": "1.4.0", 776 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 777 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 778 | "dev": true, 779 | "dependencies": { 780 | "wrappy": "1" 781 | } 782 | }, 783 | "node_modules/p-limit": { 784 | "version": "3.1.0", 785 | "dev": true, 786 | "license": "MIT", 787 | "dependencies": { 788 | "yocto-queue": "^0.1.0" 789 | }, 790 | "engines": { 791 | "node": ">=10" 792 | }, 793 | "funding": { 794 | "url": "https://github.com/sponsors/sindresorhus" 795 | } 796 | }, 797 | "node_modules/p-locate": { 798 | "version": "5.0.0", 799 | "dev": true, 800 | "license": "MIT", 801 | "dependencies": { 802 | "p-limit": "^3.0.2" 803 | }, 804 | "engines": { 805 | "node": ">=10" 806 | }, 807 | "funding": { 808 | "url": "https://github.com/sponsors/sindresorhus" 809 | } 810 | }, 811 | "node_modules/path-exists": { 812 | "version": "4.0.0", 813 | "dev": true, 814 | "license": "MIT", 815 | "engines": { 816 | "node": ">=8" 817 | } 818 | }, 819 | "node_modules/path-key": { 820 | "version": "3.1.1", 821 | "dev": true, 822 | "license": "MIT", 823 | "engines": { 824 | "node": ">=8" 825 | } 826 | }, 827 | "node_modules/path-scurry": { 828 | "version": "1.10.1", 829 | "dev": true, 830 | "license": "BlueOak-1.0.0", 831 | "dependencies": { 832 | "lru-cache": "^9.1.1 || ^10.0.0", 833 | "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 834 | }, 835 | "engines": { 836 | "node": ">=16 || 14 >=14.17" 837 | }, 838 | "funding": { 839 | "url": "https://github.com/sponsors/isaacs" 840 | } 841 | }, 842 | "node_modules/picomatch": { 843 | "version": "2.3.1", 844 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 845 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 846 | "dev": true, 847 | "engines": { 848 | "node": ">=8.6" 849 | }, 850 | "funding": { 851 | "url": "https://github.com/sponsors/jonschlinkert" 852 | } 853 | }, 854 | "node_modules/randombytes": { 855 | "version": "2.1.0", 856 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 857 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 858 | "dev": true, 859 | "dependencies": { 860 | "safe-buffer": "^5.1.0" 861 | } 862 | }, 863 | "node_modules/readdirp": { 864 | "version": "3.6.0", 865 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 866 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 867 | "dev": true, 868 | "dependencies": { 869 | "picomatch": "^2.2.1" 870 | }, 871 | "engines": { 872 | "node": ">=8.10.0" 873 | } 874 | }, 875 | "node_modules/require-directory": { 876 | "version": "2.1.1", 877 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 878 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 879 | "dev": true, 880 | "engines": { 881 | "node": ">=0.10.0" 882 | } 883 | }, 884 | "node_modules/safe-buffer": { 885 | "version": "5.2.1", 886 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 887 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 888 | "dev": true, 889 | "funding": [ 890 | { 891 | "type": "github", 892 | "url": "https://github.com/sponsors/feross" 893 | }, 894 | { 895 | "type": "patreon", 896 | "url": "https://www.patreon.com/feross" 897 | }, 898 | { 899 | "type": "consulting", 900 | "url": "https://feross.org/support" 901 | } 902 | ] 903 | }, 904 | "node_modules/serialize-javascript": { 905 | "version": "6.0.2", 906 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", 907 | "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", 908 | "dev": true, 909 | "dependencies": { 910 | "randombytes": "^2.1.0" 911 | } 912 | }, 913 | "node_modules/shebang-command": { 914 | "version": "2.0.0", 915 | "dev": true, 916 | "license": "MIT", 917 | "dependencies": { 918 | "shebang-regex": "^3.0.0" 919 | }, 920 | "engines": { 921 | "node": ">=8" 922 | } 923 | }, 924 | "node_modules/shebang-regex": { 925 | "version": "3.0.0", 926 | "dev": true, 927 | "license": "MIT", 928 | "engines": { 929 | "node": ">=8" 930 | } 931 | }, 932 | "node_modules/signal-exit": { 933 | "version": "4.1.0", 934 | "dev": true, 935 | "license": "ISC", 936 | "engines": { 937 | "node": ">=14" 938 | }, 939 | "funding": { 940 | "url": "https://github.com/sponsors/isaacs" 941 | } 942 | }, 943 | "node_modules/string-width": { 944 | "version": "4.2.3", 945 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 946 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 947 | "dev": true, 948 | "dependencies": { 949 | "emoji-regex": "^8.0.0", 950 | "is-fullwidth-code-point": "^3.0.0", 951 | "strip-ansi": "^6.0.1" 952 | }, 953 | "engines": { 954 | "node": ">=8" 955 | } 956 | }, 957 | "node_modules/string-width-cjs": { 958 | "name": "string-width", 959 | "version": "4.2.3", 960 | "dev": true, 961 | "license": "MIT", 962 | "dependencies": { 963 | "emoji-regex": "^8.0.0", 964 | "is-fullwidth-code-point": "^3.0.0", 965 | "strip-ansi": "^6.0.1" 966 | }, 967 | "engines": { 968 | "node": ">=8" 969 | } 970 | }, 971 | "node_modules/strip-ansi": { 972 | "version": "6.0.1", 973 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 974 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 975 | "dev": true, 976 | "dependencies": { 977 | "ansi-regex": "^5.0.1" 978 | }, 979 | "engines": { 980 | "node": ">=8" 981 | } 982 | }, 983 | "node_modules/strip-ansi-cjs": { 984 | "name": "strip-ansi", 985 | "version": "6.0.1", 986 | "dev": true, 987 | "license": "MIT", 988 | "dependencies": { 989 | "ansi-regex": "^5.0.1" 990 | }, 991 | "engines": { 992 | "node": ">=8" 993 | } 994 | }, 995 | "node_modules/strip-json-comments": { 996 | "version": "3.1.1", 997 | "dev": true, 998 | "license": "MIT", 999 | "engines": { 1000 | "node": ">=8" 1001 | }, 1002 | "funding": { 1003 | "url": "https://github.com/sponsors/sindresorhus" 1004 | } 1005 | }, 1006 | "node_modules/supports-color": { 1007 | "version": "8.1.1", 1008 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 1009 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1010 | "dev": true, 1011 | "dependencies": { 1012 | "has-flag": "^4.0.0" 1013 | }, 1014 | "engines": { 1015 | "node": ">=10" 1016 | }, 1017 | "funding": { 1018 | "url": "https://github.com/chalk/supports-color?sponsor=1" 1019 | } 1020 | }, 1021 | "node_modules/to-regex-range": { 1022 | "version": "5.0.1", 1023 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1024 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1025 | "dev": true, 1026 | "dependencies": { 1027 | "is-number": "^7.0.0" 1028 | }, 1029 | "engines": { 1030 | "node": ">=8.0" 1031 | } 1032 | }, 1033 | "node_modules/typescript": { 1034 | "version": "5.4.5", 1035 | "dev": true, 1036 | "license": "Apache-2.0", 1037 | "bin": { 1038 | "tsc": "bin/tsc", 1039 | "tsserver": "bin/tsserver" 1040 | }, 1041 | "engines": { 1042 | "node": ">=14.17" 1043 | } 1044 | }, 1045 | "node_modules/undici-types": { 1046 | "version": "6.19.8", 1047 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 1048 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 1049 | "dev": true 1050 | }, 1051 | "node_modules/which": { 1052 | "version": "2.0.2", 1053 | "dev": true, 1054 | "license": "ISC", 1055 | "dependencies": { 1056 | "isexe": "^2.0.0" 1057 | }, 1058 | "bin": { 1059 | "node-which": "bin/node-which" 1060 | }, 1061 | "engines": { 1062 | "node": ">= 8" 1063 | } 1064 | }, 1065 | "node_modules/workerpool": { 1066 | "version": "6.5.1", 1067 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", 1068 | "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", 1069 | "dev": true 1070 | }, 1071 | "node_modules/wrap-ansi": { 1072 | "version": "7.0.0", 1073 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1074 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1075 | "dev": true, 1076 | "dependencies": { 1077 | "ansi-styles": "^4.0.0", 1078 | "string-width": "^4.1.0", 1079 | "strip-ansi": "^6.0.0" 1080 | }, 1081 | "engines": { 1082 | "node": ">=10" 1083 | }, 1084 | "funding": { 1085 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1086 | } 1087 | }, 1088 | "node_modules/wrap-ansi-cjs": { 1089 | "name": "wrap-ansi", 1090 | "version": "7.0.0", 1091 | "dev": true, 1092 | "license": "MIT", 1093 | "dependencies": { 1094 | "ansi-styles": "^4.0.0", 1095 | "string-width": "^4.1.0", 1096 | "strip-ansi": "^6.0.0" 1097 | }, 1098 | "engines": { 1099 | "node": ">=10" 1100 | }, 1101 | "funding": { 1102 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1103 | } 1104 | }, 1105 | "node_modules/wrappy": { 1106 | "version": "1.0.2", 1107 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1108 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1109 | "dev": true 1110 | }, 1111 | "node_modules/y18n": { 1112 | "version": "5.0.8", 1113 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1114 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1115 | "dev": true, 1116 | "engines": { 1117 | "node": ">=10" 1118 | } 1119 | }, 1120 | "node_modules/yargs": { 1121 | "version": "16.2.0", 1122 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 1123 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 1124 | "dev": true, 1125 | "dependencies": { 1126 | "cliui": "^7.0.2", 1127 | "escalade": "^3.1.1", 1128 | "get-caller-file": "^2.0.5", 1129 | "require-directory": "^2.1.1", 1130 | "string-width": "^4.2.0", 1131 | "y18n": "^5.0.5", 1132 | "yargs-parser": "^20.2.2" 1133 | }, 1134 | "engines": { 1135 | "node": ">=10" 1136 | } 1137 | }, 1138 | "node_modules/yargs-parser": { 1139 | "version": "20.2.9", 1140 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", 1141 | "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", 1142 | "dev": true, 1143 | "engines": { 1144 | "node": ">=10" 1145 | } 1146 | }, 1147 | "node_modules/yargs-unparser": { 1148 | "version": "2.0.0", 1149 | "dev": true, 1150 | "license": "MIT", 1151 | "dependencies": { 1152 | "camelcase": "^6.0.0", 1153 | "decamelize": "^4.0.0", 1154 | "flat": "^5.0.2", 1155 | "is-plain-obj": "^2.1.0" 1156 | }, 1157 | "engines": { 1158 | "node": ">=10" 1159 | } 1160 | }, 1161 | "node_modules/yargs-unparser/node_modules/camelcase": { 1162 | "version": "6.2.0", 1163 | "dev": true, 1164 | "license": "MIT", 1165 | "engines": { 1166 | "node": ">=10" 1167 | }, 1168 | "funding": { 1169 | "url": "https://github.com/sponsors/sindresorhus" 1170 | } 1171 | }, 1172 | "node_modules/yargs-unparser/node_modules/decamelize": { 1173 | "version": "4.0.0", 1174 | "dev": true, 1175 | "license": "MIT", 1176 | "engines": { 1177 | "node": ">=10" 1178 | }, 1179 | "funding": { 1180 | "url": "https://github.com/sponsors/sindresorhus" 1181 | } 1182 | }, 1183 | "node_modules/yocto-queue": { 1184 | "version": "0.1.0", 1185 | "dev": true, 1186 | "license": "MIT", 1187 | "engines": { 1188 | "node": ">=10" 1189 | }, 1190 | "funding": { 1191 | "url": "https://github.com/sponsors/sindresorhus" 1192 | } 1193 | } 1194 | } 1195 | } 1196 | -------------------------------------------------------------------------------- /sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-test-sample", 3 | "displayName": "vscode-test-sample", 4 | "description": "", 5 | "version": "0.0.1", 6 | "engines": { 7 | "vscode": "^1.32.0" 8 | }, 9 | "categories": [ 10 | "Other" 11 | ], 12 | "activationEvents": [ 13 | "onCommand:extension.helloWorld" 14 | ], 15 | "main": "./out/src/extension.js", 16 | "contributes": { 17 | "commands": [ 18 | { 19 | "command": "extension.helloWorld", 20 | "title": "Hello World" 21 | } 22 | ] 23 | }, 24 | "scripts": { 25 | "vscode:prepublish": "npm run compile", 26 | "compile": "tsc -p ./", 27 | "watch": "tsc -watch -p ./", 28 | "test": "cd .. && tsc && cd sample && tsc && node ./out/test/runTest.js" 29 | }, 30 | "devDependencies": { 31 | "@types/mocha": "^10.0.0", 32 | "@types/node": "^20", 33 | "@types/vscode": "^1.52.0", 34 | "glob": "^10.3.10", 35 | "mocha": "^10.0.0", 36 | "typescript": "^5.4.5" 37 | }, 38 | "dependencies": {} 39 | } 40 | -------------------------------------------------------------------------------- /sample/src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from 'vscode'; 4 | 5 | // this method is called when your extension is activated 6 | // your extension is activated the very first time the command is executed 7 | export function activate(context: vscode.ExtensionContext) { 8 | // Use the console to output diagnostic information (console.log) and errors (console.error) 9 | // This line of code will only be executed once when your extension is activated 10 | console.log('Congratulations, your extension "vscode-test-ext" is now active!'); 11 | 12 | // The command has been defined in the package.json file 13 | // Now provide the implementation of the command with registerCommand 14 | // The commandId parameter must match the command field in package.json 15 | let disposable = vscode.commands.registerCommand('extension.helloWorld', () => { 16 | // The code you place here will be executed every time your command is executed 17 | 18 | // Display a message box to the user 19 | vscode.window.showInformationMessage('Hello World!'); 20 | }); 21 | 22 | context.subscriptions.push(disposable); 23 | } 24 | 25 | // this method is called when your extension is deactivated 26 | export function deactivate() {} 27 | -------------------------------------------------------------------------------- /sample/src/test-fixtures/fixture1/foo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-test/1737279a486ed07c9652a782f548eacf4d242fe8/sample/src/test-fixtures/fixture1/foo.js -------------------------------------------------------------------------------- /sample/src/test-fixtures/fixture2/bar.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-test/1737279a486ed07c9652a782f548eacf4d242fe8/sample/src/test-fixtures/fixture2/bar.js -------------------------------------------------------------------------------- /sample/src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests, downloadAndUnzipVSCode, runVSCodeCommand } from '../../..'; 4 | 5 | async function go() { 6 | const extensionDevelopmentPath = path.resolve(__dirname, '../../../'); 7 | const extensionTestsPath = path.resolve(__dirname, './suite'); 8 | 9 | /** 10 | * Basic usage 11 | */ 12 | await runTests({ 13 | extensionDevelopmentPath, 14 | extensionTestsPath, 15 | }); 16 | 17 | const extensionTestsPath2 = path.resolve(__dirname, './suite2'); 18 | const testWorkspace = path.resolve(__dirname, '../../src/test-fixtures/fixture1'); 19 | 20 | /** 21 | * Running another test suite on a specific workspace 22 | */ 23 | await runTests({ 24 | extensionDevelopmentPath, 25 | extensionTestsPath: extensionTestsPath2, 26 | launchArgs: [testWorkspace], 27 | }); 28 | 29 | /** 30 | * Use 1.61.0 release for testing 31 | */ 32 | await runTests({ 33 | version: '1.61.0', 34 | extensionDevelopmentPath, 35 | extensionTestsPath, 36 | launchArgs: [testWorkspace], 37 | }); 38 | 39 | /** 40 | * Use Insiders release for testing 41 | */ 42 | await runTests({ 43 | version: 'insiders', 44 | extensionDevelopmentPath, 45 | extensionTestsPath, 46 | launchArgs: [testWorkspace], 47 | }); 48 | 49 | /** 50 | * Use unreleased Insiders (here be dragons!) 51 | */ 52 | await runTests({ 53 | version: 'insiders-unreleased', 54 | extensionDevelopmentPath, 55 | extensionTestsPath, 56 | launchArgs: [testWorkspace], 57 | }); 58 | 59 | /** 60 | * Use a specific Insiders (1.85.0) for testing 61 | */ 62 | await runTests({ 63 | version: 'af28b32d7e553898b2a91af498b1fb666fdebe0c', 64 | extensionDevelopmentPath, 65 | extensionTestsPath, 66 | launchArgs: [testWorkspace], 67 | }); 68 | 69 | /** 70 | * Noop, since 1.61.0 already downloaded to .vscode-test/vscode-1.61.0 71 | */ 72 | await downloadAndUnzipVSCode('1.61.0'); 73 | 74 | /** 75 | * Manually download VS Code 1.60.0 release for testing. 76 | */ 77 | const vscodeExecutablePath = await downloadAndUnzipVSCode('1.60.0'); 78 | await runTests({ 79 | vscodeExecutablePath, 80 | extensionDevelopmentPath, 81 | extensionTestsPath, 82 | launchArgs: [testWorkspace], 83 | }); 84 | 85 | /** 86 | * Install Python extension 87 | */ 88 | await runVSCodeCommand(['--install-extension', 'ms-python.python'], { version: '1.60.0' }); 89 | 90 | /** 91 | * - Add additional launch flags for VS Code 92 | * - Pass custom environment variables to test runner 93 | */ 94 | await runTests({ 95 | vscodeExecutablePath, 96 | extensionDevelopmentPath, 97 | extensionTestsPath, 98 | launchArgs: [ 99 | testWorkspace, 100 | // This disables all extensions except the one being testing 101 | '--disable-extensions', 102 | ], 103 | // Custom environment variables for extension test script 104 | extensionTestsEnv: { foo: 'bar' }, 105 | }); 106 | } 107 | 108 | go(); 109 | -------------------------------------------------------------------------------- /sample/src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { before } from 'mocha'; 3 | 4 | // You can import and use all API from the 'vscode' module 5 | // as well as import your extension to test it 6 | import * as vscode from 'vscode'; 7 | // import * as myExtension from '../extension'; 8 | 9 | suite('Extension Test Suite 1', () => { 10 | vscode.window.showInformationMessage('Start all tests.'); 11 | 12 | test('Sample test', () => { 13 | assert.equal(-1, [1, 2, 3].indexOf(5)); 14 | assert.equal(-1, [1, 2, 3].indexOf(0)); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /sample/src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import { glob } from 'glob'; 4 | 5 | export function run(testsRoot: string, cb: (error: any, failures?: number) => void): void { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | }); 10 | 11 | glob('**/**.test.js', { cwd: testsRoot }) 12 | .then((files) => { 13 | // Add files to the test suite 14 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 15 | 16 | try { 17 | // Run the mocha test 18 | mocha.run((failures) => { 19 | cb(null, failures); 20 | }); 21 | } catch (err) { 22 | console.error(err); 23 | cb(err); 24 | } 25 | }) 26 | .catch((err) => { 27 | return cb(err); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /sample/src/test/suite2/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { before } from 'mocha'; 3 | 4 | // You can import and use all API from the 'vscode' module 5 | // as well as import your extension to test it 6 | import * as vscode from 'vscode'; 7 | // import * as myExtension from '../extension'; 8 | 9 | suite('Extension Test Suite 2', () => { 10 | vscode.window.showInformationMessage('Start all tests.'); 11 | 12 | test('Sample test', () => { 13 | assert.equal(-1, [1, 2, 3].indexOf(5)); 14 | assert.equal(-1, [1, 2, 3].indexOf(0)); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /sample/src/test/suite2/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import { glob } from 'glob'; 4 | 5 | export function run(testsRoot: string, cb: (error: any, failures?: number) => void): void { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | }); 10 | 11 | glob('**/**.test.js', { cwd: testsRoot }) 12 | .then((files) => { 13 | // Add files to the test suite 14 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 15 | 16 | try { 17 | // Run the mocha test 18 | mocha.run((failures) => { 19 | cb(null, failures); 20 | }); 21 | } catch (err) { 22 | cb(err); 23 | } 24 | }) 25 | .catch((err) => { 26 | return cb(err); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /sample/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2019", 5 | "outDir": "out", 6 | "lib": ["ES2019"], 7 | "sourceMap": true, 8 | "skipLibCheck": true 9 | }, 10 | "include": [ 11 | "src" 12 | ], 13 | "exclude": ["node_modules", ".vscode-test"] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "Node16", 5 | "lib": ["ES2021"], 6 | "outDir": "out", 7 | "moduleResolution": "Node16", 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "noImplicitThis": true, 12 | "noUnusedLocals": true, 13 | "alwaysStrict": true, 14 | "skipLibCheck": true, 15 | "forceConsistentCasingInFileNames": true 16 | }, 17 | "include": ["lib"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { defineConfig } from 'vitest/config'; 7 | 8 | export default defineConfig({ 9 | test: { 10 | include: ['lib/**/*.test.{ts,mts}'], 11 | testTimeout: 120_000, 12 | hookTimeout: 30_000, 13 | retry: 3, 14 | }, 15 | }); 16 | --------------------------------------------------------------------------------