This commit is contained in:
Martin Aeschlimann 2021-07-20 20:21:39 +02:00
Коммит 0a0f80700d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 2609A01E695523E3
34 изменённых файлов: 5291 добавлений и 0 удалений

15
.editorconfig Normal file
Просмотреть файл

@ -0,0 +1,15 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Tab indentation
[*]
indent_style = tab
trim_trailing_whitespace = true
# The indent size used in the `package.json` file cannot be changed
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
[{*.yml,*.yaml,package.json}]
indent_style = space
indent_size = 2

26
.eslintrc.js Normal file
Просмотреть файл

@ -0,0 +1,26 @@
module.exports = {
ignorePatterns: ['**/*.d.ts', '**/*.test.ts', '**/*.js', 'sample/**/*.*'],
parser: '@typescript-eslint/parser',
extends: ['plugin:@typescript-eslint/recommended'],
plugins: ['header'],
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
},
rules: {
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'header/header': [
'error',
'block',
`---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------`,
],
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
},
};

28
.github/workflows/sample.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,28 @@
on: [push]
name: Tests
jobs:
build:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install root project dependencies
run: yarn
- name: Buold root project
run: yarn compile
- name: Install sample project dependencies
run: yarn
working-directory: ./sample
- name: Run tests
uses: GabrielBB/xvfb-action@v1.0
with:
run: yarn --cwd ./sample test

7
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
.vscode-test
.vscode-test-web/
node_modules
out
sample/dist

16
.npmignore Normal file
Просмотреть файл

@ -0,0 +1,16 @@
.vscode
.npmignore
.editorconfig
.eslintrc.js
.prettierrc
.github/
src/
sample/
.vscode-test-web/
tsconfig.json
tslint.json
**/*.js.map
**/*.d.ts
!out/index.d.ts
*.tgz

5
.prettierrc Normal file
Просмотреть файл

@ -0,0 +1,5 @@
{
"semi": true,
"printWidth": 120,
"singleQuote": true
}

80
.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,80 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch sample test",
"outputCapture": "std",
"program": "${workspaceFolder}/sample/dist/web/test/runTest.js",
"args": ["--waitForDebugger=9229"],
"cascadeTerminateToConfigurations": ["Launch sample test"],
"presentation": {
"hidden": true,
}
},
{
"type": "pwa-chrome",
"request": "attach",
"name": "Attach sample test",
"skipFiles": [
"<node_internals>/**"
],
"port": 9229,
"timeout": 30000, // give it time to download vscode if needed
"resolveSourceMapLocations": [
"!**/vs/**", // exclude core vscode sources
"!**/static/build/extensions/**", // exclude built-in extensions
],
"webRoot": "${workspaceFolder}/sample", // only needed since sample is in a subdir
"presentation": {
"hidden": true,
}
},
{
"type": "pwa-node",
"request": "launch",
"name": "Run in Chromium",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/out/index.js",
"args": [
"--browserType=chromium",
"--extensionDevelopmentPath=${workspaceFolder}/sample"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
]
},
{
"type": "pwa-node",
"request": "launch",
"name": "Run Test in Chromium",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/out/index.js",
"args": [
"--browserType=chromium",
"--extensionDevelopmentPath=${workspaceFolder}/sample",
"--extensionTestsPath=${workspaceFolder}/sample/dist/web/test/suite/index.js"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
]
}
],
"compounds": [
{
"name": "Debug Sample Test",
"configurations": [
"Launch sample test",
"Attach sample test"
]
}
]
}

14
.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,14 @@
// Place your settings in this file to overwrite default and user settings.
{
"editor.insertSpaces": true,
"files.eol": "\n",
"files.trimTrailingWhitespace": true,
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/*.js": {
"when": "$(basename).ts"
}
},
"prettier.semi": true
}

17
.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,17 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "npm",
"type": "shell",
"command": "npm",
"args": [
"run",
"watch"
],
"isBackground": true,
"problemMatcher": "$tsc-watch",
"group": "build"
}
]
}

7
CHANGELOG.md Normal file
Просмотреть файл

@ -0,0 +1,7 @@
# Changelog
### 0.0.1 |
- Initial version

21
LICENSE Normal file
Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

84
README.md Normal file
Просмотреть файл

@ -0,0 +1,84 @@
# vscode-test-web
<!-- ![Test Status Badge](https://github.com/microsoft/vscode-test-web/workflows/Tests/badge.svg) -->
This module helps testing VS Code web extensions.
## Usage
Via command line:
Test web extension in browser:
```sh
vscode-test-web --browserType=webkit --extensionDevelopmentPath=$extensionLocation
```
Run web extension tests:
```sh
vscode-test-web --browserType=webkit --extensionDevelopmentPath=$extensionLocation --extensionTestsPath=$extensionLocation/dist/web/test/suite/index.js
```
Via API:
```ts
async function go() {
try {
// The folder containing the Extension Manifest package.json
const extensionDevelopmentPath = path.resolve(__dirname, '../../../');
// The path to module with the test runner and tests
const extensionTestsPath = path.resolve(__dirname, './suite/index');
// Start a web server that serves VSCode in a browser, run the tests
await runTests({ browserType: 'chromium', extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error('Failed to run tests');
process.exit(1);
}
}
go()
```
CLI options:
```
--browserType 'chromium' | 'firefox' | 'webkit': The browser to launch
--extensionDevelopmentPath path. [Optional]: A path pointing to a extension to include.
--extensionTestsPath path. [Optional]: A path to a test module to run
--folder-uri. [Optional]: The folder to open VS Code on
--version. 'insiders' (Default) | 'stable' | 'sources' [Optional]
--open-devtools. Opens the dev tools [Optional]
--headless. Whether to show the browser. Defaults to true when an extensionTestsPath is provided, otherwise false. [Optional]
```
Corrsponding options are available in the API.
## Development
- `yarn install`
- Make necessary changes in [`src`](./src)
- `yarn compile` (or `yarn watch`)
## License
[MIT](LICENSE)
## Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

60
package.json Normal file
Просмотреть файл

@ -0,0 +1,60 @@
{
"name": "@vscode/test-web",
"version": "0.0.6",
"scripts": {
"compile": "tsc -p ./",
"watch": "tsc -w -p ./",
"prepublishOnly": "tsc -p ./",
"test": "eslint src --ext ts && tsc --noEmit",
"preversion": "npm test",
"postversion": "git push && git push --tags",
"compile-sample": "yarn --cwd=sample compile-web",
"sample": "npm run compile && npm run compile-sample && node . --extensionDevelopmentPath=sample --browserType=chromium",
"sample-tests": "npm run compile && npm run compile-sample && node . --extensionDevelopmentPath=sample --extensionTestsPath=sample/dist/web/test/suite/index.js --browserType=chromium"
},
"main": "./out/index.js",
"bin": {
"vscode-test-web": "./out/index.js"
},
"engines": {
"node": ">=8.9.3"
},
"dependencies": {
"@koa/router": "^10.0.0",
"koa": "^2.13.1",
"koa-morgan": "^1.0.1",
"koa-mount": "^4.0.0",
"koa-static": "^5.0.0",
"minimist": "^1.2.5",
"playwright": "^1.12.2",
"vscode-uri": "^3.0.2",
"http-proxy-agent": "^4.0.1",
"https-proxy-agent": "^5.0.0",
"decompress": "^4.2.1",
"decompress-targz": "^4.1.1"
},
"devDependencies": {
"@types/koa": "^2.13.1",
"@types/koa-morgan": "^1.0.4",
"@types/koa-mount": "^4.0.0",
"@types/koa-static": "^4.0.1",
"@types/koa__router": "^8.0.4",
"@types/minimist": "^1.2.1",
"@types/node": "^12.19.9",
"@typescript-eslint/eslint-plugin": "^4.13.0",
"@typescript-eslint/parser": "^4.13.0",
"@types/decompress": "^4.2.3",
"eslint": "^7.17.0",
"eslint-plugin-header": "^3.1.0",
"typescript": "^4.1.3"
},
"license": "MIT",
"author": "Visual Studio Code Team",
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/vscode-test-web.git"
},
"bugs": {
"url": "https://github.com/Microsoft/vscode-test-web/issues"
}
}

8
sample/.vscode/extensions.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,8 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint",
"eamodio.tsl-problem-matcher"
]
}

11
sample/.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,11 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
}

29
sample/.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,29 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "compile-web",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$ts-webpack",
"$tslint-webpack"
]
},
{
"type": "npm",
"script": "watch-web",
"group": "build",
"isBackground": true,
"problemMatcher": [
"$ts-webpack-watch",
"$tslint-webpack-watch"
]
}
]
}

18
sample/README.md Normal file
Просмотреть файл

@ -0,0 +1,18 @@
<p>
<h1 align="center">vscode-test-web-sample</h1>
</p>
Sample for using https://github.com/microsoft/vscode-test-web.
Continuously tested with latest changes:
- [Azure DevOps](https://dev.azure.com/vscode/vscode-test-web/_build?definitionId=15)
- [Travis](https://travis-ci.org/github/microsoft/vscode-test-web)
When making changes to `vscode-test-web` library, you should compile and run the tests in this sample project locally to make sure the tests can still run successfully.
```bash
yarn install
yarn compile
yarn test
```

Просмотреть файл

@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
//@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/
const path = require('path');
const webpack = require('webpack');
module.exports = /** @type WebpackConfig */ {
context: path.dirname(__dirname),
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
target: 'webworker', // extensions run in a webworker context
entry: {
'extension': './src/web/extension.ts',
'test/suite/index': './src/web/test/suite/index.ts'
},
resolve: {
mainFields: ['module', 'main'],
extensions: ['.ts', '.js'], // support ts-files and js-files
alias: {
},
fallback: {
'assert': require.resolve('assert')
}
},
module: {
rules: [{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader'
}
]
}]
},
plugins: [
new webpack.ProvidePlugin({
process: 'process/browser',
}),
],
externals: {
'vscode': 'commonjs vscode', // ignored because it doesn't exist
},
performance: {
hints: false
},
output: {
filename: '[name].js',
path: path.join(__dirname, '../dist/web'),
libraryTarget: 'commonjs'
},
devtool: 'nosources-source-map'
};

45
sample/package.json Normal file
Просмотреть файл

@ -0,0 +1,45 @@
{
"name": "vscode-test-web-sample",
"displayName": "vscode-test-web-sample",
"description": "",
"version": "0.0.1",
"engines": {
"vscode": "^1.55.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onCommand:vscode-test-web-sample.helloWorld"
],
"browser": "./dist/web/extension.js",
"contributes": {
"commands": [
{
"command": "vscode-test-web-sample.helloWorld",
"title": "Hello World"
}
]
},
"scripts": {
"test": "node ./dist/web/test/runTest.js",
"pretest": "npm run compile-web && tsc ./src/web/test/runTest.ts --outDir ./dist --rootDir ./src --target es6 --module commonjs",
"vscode:prepublish": "npm run package-web",
"compile-web": "webpack --config ./build/web-extension.webpack.config.js",
"watch-web": "webpack --watch --config ./build/web-extension.webpack.config.js",
"package-web": "webpack --mode production --devtool hidden-source-map --config ./build/web-extension.webpack.config.js"
},
"devDependencies": {
"@types/vscode": "^1.55.0",
"@types/mocha": "^8.2.2",
"@types/node": "^12.11.7",
"mocha": "^9.0.1",
"typescript": "^4.3.4",
"ts-loader": "^9.2.3",
"webpack": "^5.40.0",
"webpack-cli": "^4.7.2",
"@types/webpack-env": "^1.16.0",
"assert": "^2.0.0",
"process": "^0.11.10"
}
}

Просмотреть файл

@ -0,0 +1,27 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
console.log('Congratulations, your extension "vscode-test-web-sample" is now active in the web extension host!');
// The command has been defined in the package.json file
// Now provide the implementation of the command with registerCommand
// The commandId parameter must match the command field in package.json
let disposable = vscode.commands.registerCommand('vscode-test-web-sample.helloWorld', () => {
// The code you place here will be executed every time your command is executed
// Display a message box to the user
vscode.window.showInformationMessage('Hello World from vscode-test-web-sample in a web extension host!');
});
context.subscriptions.push(disposable);
}
// this method is called when your extension is deactivated
export function deactivate() {}

Просмотреть файл

@ -0,0 +1,29 @@
import * as path from 'path';
import { runTests } from '../../../../out';
async function main() {
try {
// The folder containing the Extension Manifest package.json
const extensionDevelopmentPath = path.resolve(__dirname, '../../../');
// The path to module with the test runner and tests
const extensionTestsPath = path.resolve(__dirname, './suite/index');
const attachArgName = '--waitForDebugger=';
const waitForDebugger = process.argv.find(arg => arg.startsWith(attachArgName));
// Start a web server that serves VSCode in a browser, run the tests
await runTests({
browserType: 'chromium',
extensionDevelopmentPath,
extensionTestsPath,
waitForDebugger: waitForDebugger ? Number(waitForDebugger.slice(attachArgName.length)) : undefined,
});
} catch (err) {
console.error('Failed to run tests');
process.exit(1);
}
}
main();

Просмотреть файл

@ -0,0 +1,15 @@
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../../extension';
suite('Web Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});

Просмотреть файл

@ -0,0 +1,30 @@
// imports mocha for the browser, defining the `mocha` global.
require('mocha/mocha');
export function run(): Promise<void> {
return new Promise((c, e) => {
mocha.setup({
ui: 'tdd',
reporter: undefined
});
// bundles all files in the current directory matching `*.test`
const importAll = (r: __WebpackModuleApi.RequireContext) => r.keys().forEach(r);
importAll(require.context('.', true, /\.test$/));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
console.error(err);
e(err);
}
});
}

21
sample/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "dist",
"lib": [
"es6"
],
"sourceMap": true,
"rootDir": "src",
"strict": true /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
},
"exclude": [
"node_modules",
".vscode-test"
]
}

1671
sample/yarn.lock Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

263
src/index.ts Normal file
Просмотреть файл

@ -0,0 +1,263 @@
#!/usr/bin/env node
/* eslint-disable header/header */
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfig, runServer, Static, Sources } from './server/main';
import { downloadAndUnzipVSCode } from './server/download';
import * as playwright from 'playwright';
import * as minimist from 'minimist';
import * as path from 'path';
export type BrowserType = 'chromium' | 'firefox' | 'webkit';
export type VSCodeVersion = 'insiders' | 'stable' | 'sources';
export interface Options {
/**
* Browser to run the test against: 'chromium' | 'firefox' | 'webkit'
*/
browserType: BrowserType;
/**
* Absolute path to folder that contains one or more extensions (in subfolders).
* Extension folders include a `package.json` extension manifest.
*/
extensionDevelopmentPath?: string;
/**
* Absolute path to the extension tests runner module.
* Can be either a file path or a directory path that contains an `index.js`.
* The module is expected to have a `run` function of the following signature:
*
* ```ts
* function run(): Promise<void>;
* ```
*
* When running the extension test, the Extension Development Host will call this function
* that runs the test suite. This function should throws an error if any test fails.
*/
extensionTestsPath?: string;
/**
* The VS Code version to use. Valid versions are:
* - `'stable'` : The latest stable build
* - `'insiders'` : The latest insiders build
* - `'sources'`: From sources, served at localhost:8080 by running `yarn web` in the vscode repo
*
* Currently defaults to `insiders`, which is latest stable insiders.
*/
version?: VSCodeVersion;
/**
* Open the dev tools.
*/
devTools?: boolean;
/**
* Do not show the browser. Defaults to `true` if a extensionTestsPath is provided, `false` otherwise.
*/
headless?: boolean;
/**
* Expose browser debugging on this port number, and wait for the debugger to attach before running tests.
*/
waitForDebugger?: number;
/**
* The folder URI to open VSCode on
*/
folderUri?: string;
}
/**
* Runs the tests in a browser.
*
* @param options The options defining browser type, extension and test location.
*/
export async function runTests(options: Options & { extensionTestsPath: string }): Promise<void> {
const config: IConfig = {
extensionDevelopmentPath: options.extensionDevelopmentPath,
extensionTestsPath: options.extensionTestsPath,
build: await getBuild(options.version),
folderUri: options.folderUri
};
const port = 3000;
const server = await runServer(port, config);
const endpoint = `http://localhost:${port}`;
const result = await openInBrowser({
browserType: options.browserType,
endpoint,
headless: options.headless ?? true,
devTools: options.devTools,
waitForDebugger: options.waitForDebugger,
});
server.close();
if (result) {
return;
}
throw new Error('Test failed')
}
async function getBuild(version: VSCodeVersion | undefined): Promise<Static | Sources> {
if (version === 'sources') {
return { type: 'sources' };
}
return await downloadAndUnzipVSCode(version === 'stable' ? 'stable' : 'insider');
}
export async function open(options: Options): Promise<void> {
const config: IConfig = {
extensionDevelopmentPath: options.extensionDevelopmentPath,
build: await getBuild(options.version),
folderUri: options.folderUri
};
const port = 3000;
await runServer(port, config);
const endpoint = `http://localhost:${port}`;
await openInBrowser({
browserType: options.browserType,
endpoint,
headless: options.headless ?? false,
devTools: options.devTools
});
}
const width = 1200;
const height = 800;
interface BrowserOptions {
browserType: BrowserType;
endpoint: string;
headless?: boolean;
devTools?: boolean;
waitForDebugger?: number;
}
function openInBrowser(options: BrowserOptions): Promise<boolean> {
return new Promise(async (s) => {
const args: string[] = []
if (process.platform === 'linux' && options.browserType === 'chromium') {
args.push('--no-sandbox');
}
if (options.waitForDebugger) {
args.push(`--remote-debugging-port=${options.waitForDebugger}`);
}
const browser = await playwright[options.browserType].launch({ headless: options.headless, args, devtools: options.devTools });
const context = await browser.newContext();
const page = context.pages()[0] ?? await context.newPage();
if (options.waitForDebugger) {
await page.waitForFunction(() => '__jsDebugReady' in globalThis);
}
await page.setViewportSize({ width, height });
await page.goto(options.endpoint);
await page.exposeFunction('codeAutomationLog', (type: 'warn' | 'error' | 'info', args: unknown[]) => {
console[type](...args);
});
await page.exposeFunction('codeAutomationExit', async (code: number) => {
try {
await browser.close();
} catch (error) {
console.error(`Error when closing browser: ${error}`);
}
s(code === 0);
});
});
}
function isStringOrUndefined(value: unknown): value is string {
return value === undefined || (typeof value === 'string');
}
function isBooleanOrUndefined(value: unknown): value is string {
return value === undefined || (typeof value === 'boolean');
}
function isBrowserType(browserType: unknown): browserType is BrowserType {
return (typeof browserType === 'string') && ['chromium', 'firefox', 'webkit'].includes(browserType);
}
function isValidVersion(version: unknown): version is VSCodeVersion {
return version === undefined || ((typeof version === 'string') && ['insiders', 'stable', 'sources'].includes(version));
}
function getPortNumber(port: unknown): number | undefined {
if (typeof port === 'string') {
const number = Number.parseInt(port);
if (!Number.isNaN(number) && number >= 0) {
return number;
}
}
return undefined;
}
interface CommandLineOptions {
browserType?: string;
extensionDevelopmentPath?: string;
extensionTestsPath: string;
type?: string;
'open-devtools'?: boolean;
headless?: boolean;
}
if (require.main === module) {
const options: minimist.Opts = { string: ['extensionDevelopmentPath', 'extensionTestsPath', 'browserType', 'version', 'waitForDebugger', 'folder-uri'], boolean: ['open-devtools', 'headless'] };
const args = minimist<CommandLineOptions>(process.argv.slice(2), options);
const { browserType, extensionDevelopmentPath, extensionTestsPath, version, waitForDebugger, headless } = args;
const port = getPortNumber(waitForDebugger);
if (!isBrowserType(browserType) || !isStringOrUndefined(extensionDevelopmentPath) || !isStringOrUndefined(extensionTestsPath) || !isValidVersion(version) || !isStringOrUndefined(args['folder-uri']) || !isBooleanOrUndefined(args['open-devtools']) || !isBooleanOrUndefined(headless)) {
console.log('Usage:');
console.log(` --browserType 'chromium' | 'firefox' | 'webkit': The browser to launch`)
console.log(` --extensionDevelopmentPath path. [Optional]: A path pointing to a extension to include.`);
console.log(` --extensionTestsPath path. [Optional]: A path to a test module to run`);
console.log(` --folder-uri. [Optional]: The folder to open VS Code on`)
console.log(` --version. 'insiders' (Default) | 'stable' | 'sources' [Optional]`);
console.log(` --open-devtools. Opens the dev tools [Optional]`);
console.log(` --headless. Whether to show the browser. Defaults to true when an extensionTestsPath is provided, otherwise false. [Optional]`);
process.exit(-1);
}
if (extensionTestsPath) {
runTests({
extensionTestsPath: extensionTestsPath && path.resolve(extensionTestsPath),
extensionDevelopmentPath: extensionDevelopmentPath && path.resolve(extensionDevelopmentPath),
browserType,
version,
devTools: args['open-devtools'],
waitForDebugger: port,
folderUri: args['folder-uri'],
headless
})
} else {
open({
extensionDevelopmentPath: extensionDevelopmentPath && path.resolve(extensionDevelopmentPath),
browserType,
version,
devTools: args['open-devtools'],
waitForDebugger: port,
folderUri: args['folder-uri'],
headless
})
}
}

44
src/server/app.ts Normal file
Просмотреть файл

@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as Koa from 'koa';
import * as morgan from 'koa-morgan';
import * as kstatic from 'koa-static';
import * as kmount from 'koa-mount';
import { IConfig } from './main';
import workbench from './workbench';
import * as path from 'path';
export default async function createApp(config: IConfig): Promise<Koa> {
const app = new Koa();
app.use(morgan('dev'));
// this is here such that the iframe worker can fetch the extension files
app.use((ctx, next) => {
ctx.set('Access-Control-Allow-Origin', '*');
return next();
});
app.use(kmount('/static', kstatic(path.join(__dirname, '../static'))));
if (config.extensionPath) {
console.log('Serving extensions from ' + config.extensionPath);
app.use(kmount('/static/extensions', kstatic(config.extensionPath)));
}
if (config.extensionDevelopmentPath) {
console.log('Serving dev extensions from ' + config.extensionDevelopmentPath);
app.use(kmount('/static/devextensions', kstatic(config.extensionDevelopmentPath)));
}
if (config.build.type === 'static') {
app.use(kmount('/static/build', kstatic(config.build.location)));
}
app.use(workbench(config));
return app;
}

184
src/server/download.ts Normal file
Просмотреть файл

@ -0,0 +1,184 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises as fs, existsSync, createWriteStream } from 'fs';
import * as path from 'path';
import * as https from 'https';
import * as http from 'http';
import * as createHttpsProxyAgent from 'https-proxy-agent';
import * as createHttpProxyAgent from 'http-proxy-agent';
import { URL } from 'url';
import * as decompress from 'decompress';
import * as decompressTargz from 'decompress-targz';
import { Static } from './main';
interface DownloadInfo {
url: string;
version: string;
}
const extensionRoot = process.cwd();
const vscodeTestDir = path.resolve(extensionRoot, '.vscode-test-web');
async function getLatestVersion(quality: 'stable' | 'insider'): Promise<DownloadInfo> {
const update: DownloadInfo = await fetchJSON(`https://update.code.visualstudio.com/api/update/web-standalone/${quality}/latest`);
return update;
}
const reset = '\x1b[G\x1b[0K';
async function download(downloadUrl: string, destination: string, message: string) {
process.stdout.write(message);
return new Promise((resolve, reject) => {
const httpLibrary = downloadUrl.startsWith('https') ? https : http;
httpLibrary.get(downloadUrl, getAgent(downloadUrl), res => {
const total = Number(res.headers['content-length']);
let received = 0;
let timeout: NodeJS.Timeout | undefined;
const outStream = createWriteStream(destination);
outStream.on('close', () => resolve(destination));
outStream.on('error', reject);
res.on('data', chunk => {
if (!timeout) {
timeout = setTimeout(() => {
process.stdout.write(`${reset}${message}: ${received}/${total} (${(received / total * 100).toFixed()}%)`);
timeout = undefined;
}, 100);
}
received += chunk.length;
});
res.on('end', () => {
if (timeout) {
clearTimeout(timeout);
}
process.stdout.write(`${reset}${message}: complete\n`);
});
res.on('error', reject);
res.pipe(outStream);
});
});
}
async function unzip(source: string, destination: string, message: string) {
process.stdout.write(message);
if (!existsSync(destination)) {
await fs.mkdir(destination, { recursive: true });
}
await decompress(source, destination, {
plugins: [
decompressTargz()
],
strip: 1
});
process.stdout.write(`${reset}${message}: complete\n`);
}
export async function downloadAndUnzipVSCode(quality: 'stable' | 'insider'): Promise<Static> {
const info = await getLatestVersion(quality);
const folderName = `vscode-web-${quality}-${info.version}`;
const downloadedPath = path.resolve(vscodeTestDir, folderName);
if (existsSync(downloadedPath) && existsSync(path.join(downloadedPath, 'version'))) {
return { type: 'static', location: downloadedPath };
}
if (existsSync(vscodeTestDir)) {
await fs.rmdir(vscodeTestDir, { recursive: true, maxRetries: 5 });
}
await fs.mkdir(vscodeTestDir, { recursive: true });
const productName = `VS Code ${quality === 'stable' ? 'Stable' : 'Insiders'}`;
const tmpArchiveName = `vscode-web-${quality}-${info.version}-tmp`;
try {
await download(info.url, tmpArchiveName, `Downloading ${productName}`);
await unzip(tmpArchiveName, downloadedPath, `Unpacking ${productName}`);
await fs.writeFile(path.join(downloadedPath, 'version'), folderName);
} catch (err) {
console.error(err);
throw Error(`Failed to download and unpack ${productName}`);
} finally {
try {
fs.unlink(tmpArchiveName);
} catch (e) {
// ignore
}
}
return { type: 'static', location: downloadedPath };
}
export async function fetch(api: string): Promise<string> {
return new Promise((resolve, reject) => {
const httpLibrary = api.startsWith('https') ? https : http;
httpLibrary.get(api, getAgent(api), res => {
if (res.statusCode !== 200) {
reject('Failed to get content from ');
}
let data = '';
res.on('data', chunk => {
data += chunk;
});
res.on('end', () => {
resolve(data);
});
res.on('error', err => {
reject(err);
});
});
});
}
export async function fetchJSON<T>(api: string): Promise<T> {
const data = await fetch(api);
try {
return JSON.parse(data);
} catch (err) {
throw new Error(`Failed to parse response from ${api}`);
}
}
let PROXY_AGENT: createHttpProxyAgent.HttpProxyAgent | undefined = undefined;
let HTTPS_PROXY_AGENT: createHttpsProxyAgent.HttpsProxyAgent | undefined = undefined;
if (process.env.npm_config_proxy) {
PROXY_AGENT = createHttpProxyAgent(process.env.npm_config_proxy);
HTTPS_PROXY_AGENT = createHttpsProxyAgent(process.env.npm_config_proxy);
}
if (process.env.npm_config_https_proxy) {
HTTPS_PROXY_AGENT = createHttpsProxyAgent(process.env.npm_config_https_proxy);
}
function getAgent(url: string): https.RequestOptions {
const parsed = new URL(url);
const options: https.RequestOptions = {};
if (PROXY_AGENT && parsed.protocol.startsWith('http:')) {
options.agent = PROXY_AGENT;
}
if (HTTPS_PROXY_AGENT && parsed.protocol.startsWith('https:')) {
options.agent = HTTPS_PROXY_AGENT;
}
return options;
}

51
src/server/extensions.ts Normal file
Просмотреть файл

@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises as fs } from 'fs';
import * as path from 'path';
export interface URIComponents {
scheme: string;
authority: string;
path: string;
}
export async function scanForExtensions(
rootPath: string,
serverURI: URIComponents
): Promise<URIComponents[]> {
const result: URIComponents[] = [];
async function getExtension(relativePosixFolderPath: string): Promise<URIComponents | undefined> {
try {
const packageJSONPath = path.join(rootPath, relativePosixFolderPath, 'package.json');
if ((await fs.stat(packageJSONPath)).isFile()) {
return {
scheme: serverURI.scheme,
authority: serverURI.authority,
path: path.posix.join(serverURI.path, relativePosixFolderPath),
}
}
} catch {
return undefined;
}
}
async function processFolder(relativePosixFolderPath: string) {
const extension = await getExtension(relativePosixFolderPath);
if (extension) {
result.push(extension);
} else {
const folderPath = path.join(rootPath, relativePosixFolderPath);
const entries = await fs.readdir(folderPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.charAt(0) !== '.') {
await processFolder(path.posix.join(relativePosixFolderPath, entry.name));
}
}
}
}
await processFolder('');
return result;
}

40
src/server/main.ts Normal file
Просмотреть файл

@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import createApp from './app';
export interface IConfig {
readonly extensionPath?: string;
readonly extensionDevelopmentPath?: string;
readonly extensionTestsPath?: string;
readonly build: Sources | Static | CDN;
readonly folderUri?: string;
}
export interface Sources {
type: 'sources';
}
export interface Static {
type: 'static';
location: string;
}
export interface CDN {
type: 'cdn';
uri: string;
}
export interface IServer {
close(): void;
}
export async function runServer(port: number | undefined, config: IConfig): Promise<IServer> {
const app = await createApp(config);
const server = app.listen(port);
console.log(`Listening on http://localhost:${port}`);
return server;
}

180
src/server/workbench.ts Normal file
Просмотреть файл

@ -0,0 +1,180 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { promises as fs } from 'fs';
import { URI } from 'vscode-uri';
import * as Router from '@koa/router';
import { IConfig } from './main';
import { scanForExtensions, URIComponents } from './extensions';
import { fetch, fetchJSON } from './download';
interface IDevelopmentOptions {
extensionTestsPath?: URIComponents;
extensions?: URIComponents[];
}
interface IWorkbenchOptions {
additionalBuiltinExtensions?: (string | URIComponents)[];
developmentOptions?: IDevelopmentOptions;
folderUri?: URIComponents;
}
function asJSON(value: unknown): string {
return JSON.stringify(value).replace(/"/g, '&quot;');
}
class Workbench {
constructor(readonly baseUrl: string, readonly dev: boolean, private readonly builtInExtensions: unknown[] = []) { }
async render(workbenchWebConfiguration: IWorkbenchOptions): Promise<string> {
const values: { [key: string]: string } = {
WORKBENCH_WEB_CONFIGURATION: asJSON(workbenchWebConfiguration),
WORKBENCH_AUTH_SESSION: '',
WORKBENCH_WEB_BASE_URL: this.baseUrl,
WORKBENCH_BUILTIN_EXTENSIONS: asJSON(this.builtInExtensions),
WORKBENCH_MAIN: this.getMain()
};
try {
const workbenchTemplate = (await fs.readFile(path.resolve(__dirname, '../../views/workbench.html'))).toString();
return workbenchTemplate.replace(/\{\{([^}]+)\}\}/g, (_, key) => values[key] ?? 'undefined');
} catch (e) {
return e;
}
}
getMain() {
return this.dev
? `<script> require(['vs/code/browser/workbench/workbench'], function() {}); </script>`
: `<script src="${this.baseUrl}/out/vs/workbench/workbench.web.api.nls.js"></script>`
+ `<script src="${this.baseUrl}/out/vs/workbench/workbench.web.api.js"></script>`
+ `<script src="${this.baseUrl}/out/vs/code/browser/workbench/workbench.js"></script>`;
}
async renderCallback(): Promise<string> {
return await fetch(`${this.baseUrl}/out/vs/code/browser/workbench/callback.html`);
}
}
function valueOrFirst<T>(value: T | T[] | undefined): T | undefined {
return Array.isArray(value) ? value[0] : value;
}
async function getWorkbenchOptions(
ctx: { protocol: string; host: string },
config: IConfig
): Promise<IWorkbenchOptions> {
const options: IWorkbenchOptions = {};
if (config.extensionPath) {
options.additionalBuiltinExtensions = await scanForExtensions(config.extensionPath, {
scheme: ctx.protocol,
authority: ctx.host,
path: '/static/extensions',
});
}
if (config.extensionDevelopmentPath) {
const developmentOptions: IDevelopmentOptions = options.developmentOptions = {}
developmentOptions.extensions = await scanForExtensions(
config.extensionDevelopmentPath,
{ scheme: ctx.protocol, authority: ctx.host, path: '/static/devextensions' },
);
if (config.extensionTestsPath) {
let relativePath = path.relative(config.extensionDevelopmentPath, config.extensionTestsPath);
if (process.platform === 'win32') {
relativePath = relativePath.replace(/\\/g, '/');
}
developmentOptions.extensionTestsPath = {
scheme: ctx.protocol,
authority: ctx.host,
path: path.posix.join('/static/devextensions', relativePath),
};
}
}
if (config.folderUri) {
options.folderUri = URI.parse(config.folderUri);
}
return options;
}
export default function (config: IConfig): Router.Middleware {
const router = new Router<{ workbench: Workbench }>();
router.use(async (ctx, next) => {
if (ctx.query['dev'] || config.build.type === 'sources') {
try {
const builtInExtensions = await fetchJSON<unknown[]>('http://localhost:8080/builtin');
ctx.state.workbench = new Workbench('http://localhost:8080/static', true, builtInExtensions);
} catch (err) {
console.log(err);
ctx.throw('Could not connect to localhost:8080, make sure you start `yarn web`', 400);
}
} else if (config.build.type === 'static') {
ctx.state.workbench = new Workbench(`${ctx.protocol}://${ctx.host}/static/build`, false);
} else if (config.build.type === 'cdn') {
ctx.state.workbench = new Workbench(config.build.uri, false);
}
await next();
});
const callbacks = new Map<string, URI>();
router.get('/callback', async ctx => {
const {
'vscode-requestId': vscodeRequestId,
'vscode-scheme': vscodeScheme,
'vscode-authority': vscodeAuthority,
'vscode-path': vscodePath,
'vscode-query': vscodeQuery,
'vscode-fragment': vscodeFragment,
} = ctx.query;
if (!vscodeRequestId || !vscodeScheme || !vscodeAuthority) {
return ctx.throw(400);
}
const requestId = valueOrFirst(vscodeRequestId)!;
const uri = URI.from({
scheme: valueOrFirst(vscodeScheme)!,
authority: valueOrFirst(vscodeAuthority),
path: valueOrFirst(vscodePath),
query: valueOrFirst(vscodeQuery),
fragment: valueOrFirst(vscodeFragment),
});
callbacks.set(requestId, uri);
ctx.body = await ctx.state.workbench.renderCallback();
});
router.get('/fetch-callback', async ctx => {
const { 'vscode-requestId': vscodeRequestId } = ctx.query;
if (!vscodeRequestId) {
return ctx.throw(400);
}
const requestId = valueOrFirst(vscodeRequestId)!;
const uri = callbacks.get(requestId);
if (!uri) {
return ctx.throw(400);
}
callbacks.delete(requestId);
ctx.body = uri.toJSON();
});
router.get('/', async ctx => {
const options = await getWorkbenchOptions(ctx, config);
ctx.body = await ctx.state.workbench.render(options);
});
return router.routes();
}

25
tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"lib": [
"ES2019"
],
"outDir": "out",
"declaration": true,
"strict": true,
"noImplicitAny": false,
"noImplicitThis": true,
"noUnusedLocals": true,
"alwaysStrict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": false
},
"include": [
"src"
],
"exclude": [
"node_modules"
]
}

63
views/workbench.html Normal file
Просмотреть файл

@ -0,0 +1,63 @@
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html>
<head>
<script>
performance.mark('code/didStartRenderer')
</script>
<meta charset="utf-8" />
<!-- Disable pinch zooming -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<!-- Workbench Configuration -->
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}">
<!-- Workbench Auth Session -->
<meta id="vscode-workbench-auth-session" data-settings="{{WORKBENCH_AUTH_SESSION}}">
<!-- Builtin Extensions (running out of sources) -->
<meta id="vscode-workbench-builtin-extensions" data-settings="{{WORKBENCH_BUILTIN_EXTENSIONS}}">
<!-- Workbench Icon/Manifest/CSS -->
<link rel="icon" href="{{WORKBENCH_WEB_BASE_URL}}/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{WORKBENCH_WEB_BASE_URL}}/manifest.json">
</head>
<body aria-label="">
</body>
<!-- Startup (do not modify order of script tags!) -->
<script>
let baseUrl = '{{WORKBENCH_WEB_BASE_URL}}';
self.require = {
baseUrl: `${baseUrl}/out`,
recordStats: true,
trustedTypesPolicy: window.trustedTypes?.createPolicy('amdLoader', {
createScriptURL(value) {
if (value.startsWith(baseUrl)) {
return value;
}
throw new Error(`Invalid script url: ${value}`)
}
}),
paths: {
'vscode-textmate': `${baseUrl}/node_modules/vscode-textmate/release/main`,
'vscode-oniguruma': `${baseUrl}/node_modules/vscode-oniguruma/release/main`,
'xterm': `${baseUrl}/node_modules/xterm/lib/xterm.js`,
'xterm-addon-search': `${baseUrl}/node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
'xterm-addon-unicode11': `${baseUrl}/node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
'xterm-addon-webgl': `${baseUrl}/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
'tas-client-umd': `${baseUrl}/node_modules/tas-client-umd/lib/tas-client-umd.js`,
'iconv-lite-umd': `${baseUrl}/node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
'jschardet': `${baseUrl}/node_modules/jschardet/dist/jschardet.min.js`,
}
};
</script>
<script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/loader.js"></script>
<script>
performance.mark('code/willLoadWorkbenchMain');
</script>
{{WORKBENCH_MAIN}}
</html>

2097
yarn.lock Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу