diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 306dd2c22..738b0ee20 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -19,6 +19,9 @@ extends: - prettier/@typescript-eslint # Remove conflicts again. rules: + # Sometimes you need to add args to implement a function signature even + # if they are unused. + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }] # For overloads. no-dupe-class-members: off "@typescript-eslint/no-use-before-define": off @@ -30,6 +33,9 @@ rules: eqeqeq: error import/order: [error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }] + no-async-promise-executor: off + # This isn't a real module, just types, which apparently doesn't resolve. + import/no-unresolved: [error, { ignore: ["express-serve-static-core"] }] settings: # Does not work with CommonJS unfortunately. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c68d7da5e..ee281990c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,3 @@ * @code-asher @nhooyr + +ci/helm-chart @Matthew-Beckett @alexgorbatchev diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9dc409dd7..2f567fce3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,4 +2,7 @@ blank_issues_enabled: false contact_links: - name: Question url: https://github.com/cdr/code-server/discussions/new?category_id=22503114 - about: Ask the community for help + about: Ask the community for help on our GitHub Discussions board + - name: Chat + about: Need immediate help or just want to talk? Hop in our Slack + url: https://cdr.co/join-community diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dcf917841..a265c98ef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,7 +8,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Run ./ci/steps/fmt.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/fmt.sh @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Run ./ci/steps/lint.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/lint.sh @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Run ./ci/steps/test.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/test.sh @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Run ./ci/steps/release.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/release.sh - name: Upload npm package artifact @@ -116,7 +116,7 @@ jobs: name: release-packages path: ./release-packages - name: Run ./ci/steps/build-docker-image.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/build-docker-image.sh - name: Upload release image diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index c2fe429b9..74540651f 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Run ./ci/steps/publish-npm.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/publish-npm.sh env: @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Run ./ci/steps/push-docker-manifest.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/push-docker-manifest.sh env: diff --git a/.gitignore b/.gitignore index 616f9b01b..4929c46fb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ release-images/ node_modules node-* /plugins +/lib/coder-cloud-agent +.home diff --git a/.gitmodules b/.gitmodules index 9854a1b1d..f2cdafc7a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "lib/vscode"] path = lib/vscode url = https://github.com/microsoft/vscode + ignore = dirty diff --git a/README.md b/README.md index f395968ce..dd5532d16 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# code-server +# code-server · [!["GitHub Discussions"](https://img.shields.io/badge/%20GitHub-%20Discussions-gray.svg?longCache=true&logo=github&colorB=purple)](https://github.com/cdr/code-server/discussions) [!["Join us on Slack"](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen)](https://cdr.co/join-community) [![Twitter Follow](https://img.shields.io/twitter/follow/CoderHQ?label=%40CoderHQ&style=social)](https://twitter.com/coderhq) Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and access it in the browser. @@ -6,62 +6,64 @@ Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and a ## Highlights -- **Code everywhere** - - Code on your Chromebook, tablet, and laptop with a consistent development environment. - - Develop on a Linux machine and pick up from any device with a web browser. -- **Server-powered** - - Take advantage of large cloud servers to speed up tests, compilations, downloads, and more. - - Preserve battery life when you're on the go as all intensive tasks run on your server. - - Make use of a spare computer you have lying around and turn it into a full development environment. +- Code on any device with a consistent development environment +- Use cloud servers to speed up tests, compilations, downloads, and more +- Preserve battery life when you're on the go; all intensive tasks run on your server ## Getting Started -For a full setup and walkthrough, please see [./doc/guide.md](./doc/guide.md). +There are two ways to get started: -### Quick Install +1. Using the [install script](./install.sh), which automates most of the process. The script uses the system package manager (if possible) +2. Manually installing code-server; see [Installation](./doc/install.md) for instructions applicable to most use cases -We have a [script](./install.sh) to install code-server for Linux, macOS and FreeBSD. - -It tries to use the system package manager if possible. - -First run to print out the install process: +If you choose to use the install script, you can preview what occurs during the install process: ```bash curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run ``` -Now to actually install: +To install, run: ```bash curl -fsSL https://code-server.dev/install.sh | sh ``` -The install script will print out how to run and start using code-server. +When done, the install script prints out instructions for running and starting code-server. -### Manual Install +We also have an in-depth [setup and configuration](./doc/guide.md) guide. -Docs on the install script, manual installation and docker image are at [./doc/install.md](./doc/install.md). +### Alpha Program 🐣 + +We're working on a cloud platform that makes deploying and managing code-server easier. +Consider updating to the latest version and running code-server with our experimental flag `--link` if you don't want to worry about + +- TLS +- Authentication +- Port Forwarding + +```bash +$ code-server --link +Proxying code-server to Coder Cloud, you can access your IDE at https://valmar-jon.cdr.co +``` ## FAQ See [./doc/FAQ.md](./doc/FAQ.md). -## Contributing +## Want to help? -See [./doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md). +See [CONTRIBUTING](./doc/CONTRIBUTING.md) for details. ## Hiring -We ([@cdr](https://github.com/cdr)) are looking for engineers to help maintain -code-server, innovate on open source and streamline dev workflows. +We ([@cdr](https://github.com/cdr)) are looking for engineers to help [maintain +code-server](https://jobs.lever.co/coder/e40becde-2cbd-4885-9029-e5c7b0a734b8), innovate on open source, and streamline dev workflows. Our main office is in Austin, Texas. Remote is ok as long as you're in North America or Europe. -Please get in [touch](mailto:jobs@coder.com) with your resume/github if interested. - -We're also hiring someone specifically to help maintain code-server. -See the listing [here](https://jobs.lever.co/coder/e40becde-2cbd-4885-9029-e5c7b0a734b8). +Please get in [touch](mailto:jobs@coder.com) with your resume/GitHub if interested. ## For Organizations diff --git a/ci/README.md b/ci/README.md index 19b7ee8e8..3448f9e35 100644 --- a/ci/README.md +++ b/ci/README.md @@ -17,6 +17,8 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub) 1. Update the version of code-server and make a PR. 1. Update in `package.json` 2. Update in [./doc/install.md](../doc/install.md) + 3. Update in [./ci/helm-chart/README.md](../ci/helm-chart/README.md) + - Remember to update the chart version as well on top of appVersion in `Chart.yaml`. 2. GitHub actions will generate the `npm-package`, `release-packages` and `release-images` artifacts. 1. You do not have to wait for these. 3. Run `yarn release:github-draft` to create a GitHub draft release from the template with diff --git a/ci/build/build-code-server.sh b/ci/build/build-code-server.sh index df0852804..0aff035af 100755 --- a/ci/build/build-code-server.sh +++ b/ci/build/build-code-server.sh @@ -18,6 +18,12 @@ main() { chmod +x out/node/entry.js fi + if ! [ -f ./lib/coder-cloud-agent ]; then + OS="$(uname | tr '[:upper:]' '[:lower:]')" + curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent + chmod +x ./lib/coder-cloud-agent + fi + parcel build \ --public-url "." \ --out-dir dist \ diff --git a/ci/build/build-packages.sh b/ci/build/build-packages.sh index 058a54781..a5ef794e5 100755 --- a/ci/build/build-packages.sh +++ b/ci/build/build-packages.sh @@ -11,15 +11,6 @@ main() { mkdir -p release-packages release_archive - # Will stop the auto update issues and allow people to upgrade their scripts - # for the new release structure. - if [[ $ARCH == "amd64" ]]; then - if [[ $OS == "linux" ]]; then - ARCH=x86_64 release_archive - elif [[ $OS == "macos" ]]; then - OS=darwin ARCH=x86_64 release_archive - fi - fi if [[ $OS == "linux" ]]; then release_nfpm @@ -30,12 +21,6 @@ release_archive() { local release_name="code-server-$VERSION-$OS-$ARCH" if [[ $OS == "linux" ]]; then tar -czf "release-packages/$release_name.tar.gz" --transform "s/^\.\/release-standalone/$release_name/" ./release-standalone - elif [[ $OS == "darwin" && $ARCH == "x86_64" ]]; then - # Just exists to make autoupdating from 3.2.0 work again. - mv ./release-standalone "./$release_name" - zip -r "release-packages/$release_name.zip" "./$release_name" - mv "./$release_name" ./release-standalone - return else tar -czf "release-packages/$release_name.tar.gz" -s "/^release-standalone/$release_name/" release-standalone fi diff --git a/ci/build/build-release.sh b/ci/build/build-release.sh index 74d991ac9..3b88ed7e9 100755 --- a/ci/build/build-release.sh +++ b/ci/build/build-release.sh @@ -6,6 +6,10 @@ set -euo pipefail # MINIFY controls whether minified vscode is bundled. MINIFY="${MINIFY-true}" +# KEEP_MODULES controls whether the script cleans all node_modules requiring a yarn install +# to run first. +KEEP_MODULES="${KEEP_MODULES-0}" + main() { cd "$(dirname "${0}")/../.." source ./ci/lib.sh @@ -52,15 +56,25 @@ EOF ) > "$RELEASE_PATH/package.json" rsync yarn.lock "$RELEASE_PATH" rsync ci/build/npm-postinstall.sh "$RELEASE_PATH/postinstall.sh" + + if [ "$KEEP_MODULES" = 1 ]; then + rsync node_modules/ "$RELEASE_PATH/node_modules" + mkdir -p "$RELEASE_PATH/lib" + rsync ./lib/coder-cloud-agent "$RELEASE_PATH/lib" + fi } bundle_vscode() { mkdir -p "$VSCODE_OUT_PATH" rsync "$VSCODE_SRC_PATH/yarn.lock" "$VSCODE_OUT_PATH" - rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY+-min}/" "$VSCODE_OUT_PATH/out" + rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY:+-min}/" "$VSCODE_OUT_PATH/out" rsync "$VSCODE_SRC_PATH/.build/extensions/" "$VSCODE_OUT_PATH/extensions" - rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules" + if [ "$KEEP_MODULES" = 0 ]; then + rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules" + else + rsync "$VSCODE_SRC_PATH/node_modules/" "$VSCODE_OUT_PATH/node_modules" + fi rsync "$VSCODE_SRC_PATH/extensions/package.json" "$VSCODE_OUT_PATH/extensions" rsync "$VSCODE_SRC_PATH/extensions/yarn.lock" "$VSCODE_OUT_PATH/extensions" rsync "$VSCODE_SRC_PATH/extensions/postinstall.js" "$VSCODE_OUT_PATH/extensions" diff --git a/ci/build/clean.sh b/ci/build/clean.sh index 0e0425a4b..b80632278 100755 --- a/ci/build/clean.sh +++ b/ci/build/clean.sh @@ -5,16 +5,7 @@ main() { cd "$(dirname "${0}")/../.." source ./ci/lib.sh - rm -rf \ - out \ - release \ - release-standalone \ - release-packages \ - release-gcp \ - release-images \ - dist \ - .cache \ - node-* + git clean -Xffd pushd lib/vscode git clean -xffd diff --git a/ci/build/npm-postinstall.sh b/ci/build/npm-postinstall.sh index 127d6408a..743b549ff 100755 --- a/ci/build/npm-postinstall.sh +++ b/ci/build/npm-postinstall.sh @@ -24,6 +24,13 @@ main() { ;; esac + OS="$(uname | tr '[:upper:]' '[:lower:]')" + if curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent; then + chmod +x ./lib/coder-cloud-agent + else + echo "Failed to download cloud agent; --link will not work" + fi + if ! vscode_yarn; then echo "You may not have the required dependencies to build the native modules." echo "Please see https://github.com/cdr/code-server/blob/master/doc/npm.md" @@ -36,6 +43,13 @@ vscode_yarn() { yarn --production --frozen-lockfile cd extensions yarn --production --frozen-lockfile + for ext in */; do + ext="${ext%/}" + echo "extensions/$ext: installing dependencies" + cd "$ext" + yarn --production --frozen-lockfile + cd "$OLDPWD" + done } main "$@" diff --git a/ci/build/release-github-assets.sh b/ci/build/release-github-assets.sh index f2d9ff8c3..7fba67703 100755 --- a/ci/build/release-github-assets.sh +++ b/ci/build/release-github-assets.sh @@ -11,7 +11,7 @@ main() { source ./ci/lib.sh download_artifact release-packages ./release-packages - local assets=(./release-packages/code-server*"$VERSION"*{.tar.gz,.zip,.deb,.rpm}) + local assets=(./release-packages/code-server*"$VERSION"*{.tar.gz,.deb,.rpm}) for i in "${!assets[@]}"; do assets[$i]="--attach=${assets[$i]}" done diff --git a/ci/build/release-github-draft.sh b/ci/build/release-github-draft.sh index e345d04f6..4e077a356 100755 --- a/ci/build/release-github-draft.sh +++ b/ci/build/release-github-draft.sh @@ -15,7 +15,17 @@ v$VERSION VS Code v$(vscode_version) -- Summarize changes here with references to issues +Upgrading is as easy as installing the new version over the old one. code-server +maintains all user data in \`~/.local/share/code-server\` so that it is preserved in between +installations. + +## New Features + - ⭐ Summarize new features here with references to issues + +## Bug Fixes + - ⭐ Summarize bug fixes here with references to issues + +Cheers! 🍻 EOF } diff --git a/ci/build/test-standalone-release.sh b/ci/build/test-standalone-release.sh index 92b58e8c0..5f5656486 100755 --- a/ci/build/test-standalone-release.sh +++ b/ci/build/test-standalone-release.sh @@ -15,8 +15,8 @@ main() { ./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --install-extension ms-python.python local installed_extensions installed_extensions="$(./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --list-extensions 2>&1)" - if [[ $installed_extensions != *"info Using config file ~/.config/code-server/config.yaml -ms-python.python" ]]; then + # We use grep as ms-python.python may have dependency extensions that change. + if ! echo "$installed_extensions" | grep -q "ms-python.python"; then echo "Unexpected output from listing extensions:" echo "$installed_extensions" exit 1 diff --git a/ci/dev/fmt.sh b/ci/dev/fmt.sh index d3bd41915..ce9d7518b 100755 --- a/ci/dev/fmt.sh +++ b/ci/dev/fmt.sh @@ -19,13 +19,16 @@ main() { "*.yaml" "*.yml" ) - prettier --write --loglevel=warn $(git ls-files "${prettierExts[@]}") + prettier --write --loglevel=warn $( + git ls-files "${prettierExts[@]}" | grep -v 'helm-chart' + ) doctoc --title '# FAQ' doc/FAQ.md > /dev/null doctoc --title '# Setup Guide' doc/guide.md > /dev/null doctoc --title '# Install' doc/install.md > /dev/null doctoc --title '# npm Install Requirements' doc/npm.md > /dev/null doctoc --title '# Contributing' doc/CONTRIBUTING.md > /dev/null + doctoc --title '# iPad' doc/ipad.md > /dev/null if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then echo "Files need generation or are formatted incorrectly:" diff --git a/ci/dev/image/run.sh b/ci/dev/image/run.sh index 70ab67e1d..3d5e15dd6 100755 --- a/ci/dev/image/run.sh +++ b/ci/dev/image/run.sh @@ -4,16 +4,22 @@ set -euo pipefail main() { cd "$(dirname "$0")/../../.." source ./ci/lib.sh + mkdir -p .home docker run \ -it \ --rm \ -v "$PWD:/src" \ + -e HOME="/src/.home" \ + -e USER="coder" \ + -e GITHUB_TOKEN \ + -e KEEP_MODULES \ + -e MINIFY \ -w /src \ -p 127.0.0.1:8080:8080 \ -u "$(id -u):$(id -g)" \ -e CI \ - "$(docker_build ./ci/images/debian8)" \ + "$(docker_build ./ci/images/"${IMAGE-debian10}")" \ "$@" } diff --git a/ci/dev/lint.sh b/ci/dev/lint.sh index 219c3793b..6acae73ad 100755 --- a/ci/dev/lint.sh +++ b/ci/dev/lint.sh @@ -7,9 +7,9 @@ main() { eslint --max-warnings=0 --fix $(git ls-files "*.ts" "*.tsx" "*.js") stylelint $(git ls-files "*.css") tsc --noEmit - # See comment in ./ci/image/debian8 - if [[ ! ${CI-} ]]; then - shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh") + shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh") + if command -v helm && helm kubeval --help > /dev/null; then + helm kubeval ci/helm-chart fi } diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 031bacf99..9922a9c84 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -4,7 +4,10 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." - mocha -r ts-node/register ./test/*.test.ts + cd test/test-plugin + make -s out/index.js + cd "$OLDPWD" + mocha -r ts-node/register ./test/*.test.ts "$@" } main "$@" diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 5fe34a20f..23dc41770 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1,5 +1,5 @@ diff --git a/.gitignore b/.gitignore -index 0fe46b6eadc4ccc819fbf342ee1071bb657792b3..e545e004cef31fa5f40ba8df6a2317ea5b69ddb5 100644 +index b7f5b58c8ede171be547c56b61ce76f79a3accc3..856fbd8c67460fe099d7fbee1475e906b500f053 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ out-vscode-reh-web-pkg/ @@ -12,42 +12,38 @@ index 0fe46b6eadc4ccc819fbf342ee1071bb657792b3..e545e004cef31fa5f40ba8df6a2317ea coverage/ diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 -index 135e10442a7e5184cf8c47615322bb7d622855d9..0000000000000000000000000000000000000000 +index d97527dab46aa4e7aa2df386bda3a8b4f93fcb80..0000000000000000000000000000000000000000 --- a/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ --disturl "https://atom.io/download/electron" --target "7.3.2" +-disturl "https://electronjs.org/headers" +-target "9.3.3" -runtime "electron" diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js -index f2ea1bd37010b1eb8a43ce9beaae4a88810f6e2d..3f660f9981921ec465d2b8809a1a5ea5663f4c1f 100644 +index 5f367d1f0777d2cb46ad47e376337900733981b5..ba74af1d61a00ce42020418126e62879397f57bf 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js -@@ -52,6 +52,7 @@ gulp.task('vscode-reh-web-linux-x64-min', noop); - gulp.task('vscode-reh-web-linux-alpine-min', noop); +@@ -44,6 +44,7 @@ BUILD_TARGETS.forEach(({ platform, arch }) => { + }); function getNodeVersion() { + return process.versions.node; const yarnrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.yarnrc'), 'utf8'); const target = /^target "(.*)"$/m.exec(yarnrc)[1]; return target; -diff --git a/build/lib/node.js b/build/lib/node.js -index 403ae3d9657f823019542e739fc39292db20e4fe..738ee8cee0e79aa239af10e1abefc9e836b8ce33 100644 ---- a/build/lib/node.js -+++ b/build/lib/node.js -@@ -5,11 +5,8 @@ - *--------------------------------------------------------------------------------------------*/ - Object.defineProperty(exports, "__esModule", { value: true }); - const path = require("path"); --const fs = require("fs"); - const root = path.dirname(path.dirname(__dirname)); --const yarnrcPath = path.join(root, 'remote', '.yarnrc'); --const yarnrc = fs.readFileSync(yarnrcPath, 'utf8'); --const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)[1]; -+const version = process.versions.node; - const node = process.platform === 'win32' ? 'node.exe' : 'node'; - const nodePath = path.join(root, '.build', 'node', `v${version}`, `${process.platform}-${process.arch}`, node); - console.log(nodePath); +diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts +index dac71c814798ecfac99750be856078e043d239bf..6edd7ea56baef7cd9f87a9020df32d3b8519b615 100644 +--- a/build/lib/extensions.ts ++++ b/build/lib/extensions.ts +@@ -70,7 +70,7 @@ function fromLocal(extensionPath: string, forWeb: boolean): Stream { + if (isWebPacked) { + input = updateExtensionPackageJSON(input, (data: any) => { + delete data.scripts; +- delete data.dependencies; ++ // https://github.com/cdr/code-server/pull/2041#issuecomment-685910322 + delete data.devDependencies; + if (data.main) { + data.main = data.main.replace('/out/', /dist/); diff --git a/build/lib/node.ts b/build/lib/node.ts index 64397034461b1661f82007c141cbf4c039a3b722..c53dccf4dc0a99122ed96cf10c2eb632bb25059e 100644 --- a/build/lib/node.ts @@ -69,23 +65,11 @@ index 64397034461b1661f82007c141cbf4c039a3b722..c53dccf4dc0a99122ed96cf10c2eb632 -console.log(nodePath); \ No newline at end of file +console.log(nodePath); -diff --git a/build/lib/util.js b/build/lib/util.js -index e552a036f89bd581644459fd5c27fe4ae1379f62..169e8614b9f6a2bd68446144ab7e1ce5c6d49b64 100644 ---- a/build/lib/util.js -+++ b/build/lib/util.js -@@ -257,6 +257,7 @@ function streamToPromise(stream) { - } - exports.streamToPromise = streamToPromise; - function getElectronVersion() { -+ return process.versions.node; - const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); - const target = /^target "(.*)"$/m.exec(yarnrc)[1]; - return target; diff --git a/build/lib/util.ts b/build/lib/util.ts -index 035c7e95ea3006bb3dabd68bbf54db80de4aaaf2..4ff8dcfe6b21a0ec8064ebc7bb05506b8f1faa91 100644 +index c0a0d9619d736c6558b0b91e6c7537c1a06cc947..48853bc6201a602cadbef47a8f46281be93421e9 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts -@@ -322,6 +322,7 @@ export function streamToPromise(stream: NodeJS.ReadWriteStream): Promise { +@@ -336,6 +336,7 @@ export function streamToPromise(stream: NodeJS.ReadWriteStream): Promise { } export function getElectronVersion(): string { @@ -144,10 +128,10 @@ index cb88d37adefd4882f61a2711fdd7f72b89e1a6e3..6b3253af0a3a0aa4d75456379ef1c00f const cp = require('child_process'); diff --git a/coder.js b/coder.js new file mode 100644 -index 0000000000000000000000000000000000000000..9cb693af63b86b4a6b35c442e6ea501a1076d18a +index 0000000000000000000000000000000000000000..df5b42cba463b6c0043aebbc835f852f1284aa36 --- /dev/null +++ b/coder.js -@@ -0,0 +1,63 @@ +@@ -0,0 +1,64 @@ +// This must be ran from VS Code's root. +const gulp = require("gulp"); +const path = require("path"); @@ -163,6 +147,7 @@ index 0000000000000000000000000000000000000000..9cb693af63b86b4a6b35c442e6ea501a + buildfile.base, + buildfile.workbenchWeb, + buildfile.workerExtensionHost, ++ buildfile.workerNotebook, + buildfile.keyboardMaps, + buildfile.entrypoint("vs/platform/files/node/watcher/unix/watcherApp", ["vs/css", "vs/nls"]), + buildfile.entrypoint("vs/platform/files/node/watcher/nsfw/watcherApp", ["vs/css", "vs/nls"]), @@ -225,11 +210,23 @@ index da4fa3e9d0443d679dfbab1000b434af2ae01afd..50f3e1144f8057883dea8b91ec2f7073 } function processLib() { +diff --git a/extensions/typescript-language-features/src/utils/platform.ts b/extensions/typescript-language-features/src/utils/platform.ts +index 2d754bf4054713f53beed030f9211b33532c1b4b..708b7e40a662e4ca93420992bf7a5af0c62ea5b2 100644 +--- a/extensions/typescript-language-features/src/utils/platform.ts ++++ b/extensions/typescript-language-features/src/utils/platform.ts +@@ -6,6 +6,6 @@ + import * as vscode from 'vscode'; + + export function isWeb(): boolean { +- // @ts-expect-error ++ // NOTE@coder: Remove unused ts-expect-error directive which causes tsc to error. + return typeof navigator !== 'undefined' && vscode.env.uiKind === vscode.UIKind.Web; + } diff --git a/package.json b/package.json -index 226f51a1ec55fc31ae90e175bc1ab62d74eb6294..5c4e5af5f69c0fd994f4b54cb7a9dfb20f868bfa 100644 +index 28f8a69a2a91f9cb9f4dbd73ed3e689b2b3afe84..b5f5b10004d3e36092a30f685938a606b333c465 100644 --- a/package.json +++ b/package.json -@@ -45,7 +45,11 @@ +@@ -46,7 +46,11 @@ "watch-web": "gulp watch-web --max_old_space_size=4095", "eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./extensions" }, @@ -239,17 +236,17 @@ index 226f51a1ec55fc31ae90e175bc1ab62d74eb6294..5c4e5af5f69c0fd994f4b54cb7a9dfb2 + "@coder/node-browser": "^1.0.8", + "@coder/requirefs": "^1.1.5", "applicationinsights": "1.0.8", - "chokidar": "3.2.3", + "chokidar": "3.4.3", "graceful-fs": "4.2.3", -@@ -59,6 +63,7 @@ - "native-keymap": "2.1.2", +@@ -60,6 +64,7 @@ + "native-keymap": "2.2.0", "native-watchdog": "1.3.0", - "node-pty": "0.10.0-beta8", + "node-pty": "0.10.0-beta17", + "rimraf": "^2.2.8", - "semver-umd": "^5.5.7", "spdlog": "^0.11.1", "sudo-prompt": "9.1.1", -@@ -159,7 +164,6 @@ + "tas-client-umd": "0.1.2", +@@ -161,7 +166,6 @@ "pump": "^1.0.1", "queue": "3.0.6", "rcedit": "^1.1.0", @@ -257,7 +254,7 @@ index 226f51a1ec55fc31ae90e175bc1ab62d74eb6294..5c4e5af5f69c0fd994f4b54cb7a9dfb2 "sinon": "^1.17.2", "source-map": "^0.4.4", "style-loader": "^1.0.0", -@@ -190,5 +194,8 @@ +@@ -193,5 +197,8 @@ "windows-foreground-love": "0.2.0", "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" @@ -267,32 +264,57 @@ index 226f51a1ec55fc31ae90e175bc1ab62d74eb6294..5c4e5af5f69c0fd994f4b54cb7a9dfb2 } } diff --git a/product.json b/product.json -index 2b884d18f301b86bf29e3a8f1343cfb651f8727c..518b935b837dd21251089bdd317d6c3c03756d7b 100644 +index 7cab6d1b9f3b84bfc703856e93773a293fd198cf..6924d94f65b390f52885b1036f7e96bce0e34680 100644 --- a/product.json +++ b/product.json +@@ -1,6 +1,6 @@ + { +- "nameShort": "Code - OSS", +- "nameLong": "Code - OSS", ++ "nameShort": "code-server", ++ "nameLong": "code-server", + "applicationName": "code-oss", + "dataFolderName": ".vscode-oss", + "win32MutexName": "vscodeoss", @@ -20,7 +20,7 @@ "darwinBundleIdentifier": "com.visualstudio.code.oss", "linuxIconName": "com.visualstudio.code.oss", "licenseFileName": "LICENSE.txt", -- "reportIssueUrl": "https://github.com/Microsoft/vscode/issues/new", +- "reportIssueUrl": "https://github.com/microsoft/vscode/issues/new", + "reportIssueUrl": "https://github.com/cdr/code-server/issues/new", "urlProtocol": "code-oss", "extensionAllowedProposedApi": [ "ms-vscode.vscode-js-profile-flame", +@@ -136,5 +136,14 @@ + "publisherDisplayName": "Microsoft" + } + } +- ] ++ ], ++ ++ "//": "https://github.com/VSCodium/vscodium/pull/155/files", ++ "documentationUrl": "https://go.microsoft.com/fwlink/?LinkID=533484#vscode", ++ "keyboardShortcutsUrlMac": "https://go.microsoft.com/fwlink/?linkid=832143", ++ "keyboardShortcutsUrlLinux": "https://go.microsoft.com/fwlink/?linkid=832144", ++ "keyboardShortcutsUrlWin": "https://go.microsoft.com/fwlink/?linkid=832145", ++ "introductoryVideosUrl": "https://go.microsoft.com/fwlink/?linkid=832146", ++ "tipsAndTricksUrl": "https://go.microsoft.com/fwlink/?linkid=852118", ++ "newsletterSignupUrl": "https://www.research.net/r/vsc-newsletter" + } diff --git a/remote/.yarnrc b/remote/.yarnrc deleted file mode 100644 -index 1e16cde724c7703d2836b3641de48c99f7f47e68..0000000000000000000000000000000000000000 +index c1a32ce532afa501fb19bdbcf6bcb0ec151ecd99..0000000000000000000000000000000000000000 --- a/remote/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -disturl "http://nodejs.org/dist" --target "12.4.0" +-target "12.14.1" -runtime "node" diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts -index 1286c5117a4cae9d6075ed36f32f6414897d705b..e60dd11d03992800853e76d4d68b8ff211da7627 100644 +index f475b10e5e81d5c2511d8d36ca5fa30a54bc415a..e9a30b2cd2a7848241d9a430c28faccb51efdb9b 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts -@@ -111,16 +111,17 @@ class RemoteAuthoritiesImpl { +@@ -113,16 +113,17 @@ class RemoteAuthoritiesImpl { if (host && host.indexOf(':') !== -1) { host = `[${host}]`; } @@ -314,13 +336,14 @@ index 1286c5117a4cae9d6075ed36f32f6414897d705b..e60dd11d03992800853e76d4d68b8ff2 }); } diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts -index 0bbc5d6ef911b1e98d26ad796873a9b6b7fb04ec..61f139b9c557b9c46e5a9640ab0e37a6fb7692ee 100644 +index 3361d83be5b7c3d08bdbfbe6947942a4695882c6..69ead8484e042bbad7075659f8e47f074bc217e4 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts -@@ -59,6 +59,17 @@ if (typeof navigator === 'object' && !isElectronRenderer) { +@@ -71,6 +71,18 @@ if (typeof navigator === 'object' && !isElectronRenderer) { _isWeb = true; _locale = navigator.language; _language = _locale; ++ + // NOTE@coder: Make languages work. + const el = typeof document !== 'undefined' && document.getElementById('vscode-remote-nls-configuration'); + const rawNlsConfig = el && el.getAttribute('data-settings'); @@ -332,19 +355,17 @@ index 0bbc5d6ef911b1e98d26ad796873a9b6b7fb04ec..61f139b9c557b9c46e5a9640ab0e37a6 + _language = nlsConfig.availableLanguages['*'] || LANGUAGE_DEFAULT; + } catch (error) { /* Oh well. */ } + } - } else if (typeof process === 'object') { - _isWindows = (process.platform === 'win32'); - _isMacintosh = (process.platform === 'darwin'); + } + + // Native environment diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts -index c52f7b3774f399d3fa161682316b20d807072806..08a87fa970f159f84691c5068cf5e38f0926015c 100644 +index 17895a8510bca40924524dc107c33305c4783c45..ba019b43084e3998ab399108968c3c765a79eb32 100644 --- a/src/vs/base/common/processes.ts +++ b/src/vs/base/common/processes.ts -@@ -110,7 +110,8 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve - /^ELECTRON_.+$/, - /^GOOGLE_API_KEY$/, +@@ -112,6 +112,7 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve /^VSCODE_.+$/, -- /^SNAP(|_.*)$/ -+ /^SNAP(|_.*)$/, + /^SNAP(|_.*)$/, + /^GDK_PIXBUF_.+$/, + /^CODE_SERVER_.+$/, ]; const envKeys = Object.keys(env); @@ -432,19 +453,18 @@ index 2c64061da7b01aef0bfe3cec851da232ca9461c8..c0ef8faedd406c38bf9c55bbbdbbb060 // Do nothing. If we can't read the file we have no // language pack config. diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts -index c629f7fffa1faad78e5d9907fd38aec289db0428..c266e1fb06ffc44f1ec4230ac59fd094a53b257b 100644 +index 0ef8b9dc81419b53b27cf111fb206d72ba56bada..62a79602a831bca0dc62ad57dc10a9375f8b9cdb 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts -@@ -13,6 +13,8 @@ import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/wi - import { isEqual } from 'vs/base/common/resources'; - import { isStandalone } from 'vs/base/browser/browser'; +@@ -17,6 +17,7 @@ import { isStandalone } from 'vs/base/browser/browser'; import { localize } from 'vs/nls'; -+import { Schemas } from 'vs/base/common/network'; + import { Schemas } from 'vs/base/common/network'; + import product from 'vs/platform/product/common/product'; +import { encodePath } from 'vs/server/node/util'; - interface ICredential { - service: string; -@@ -243,12 +245,18 @@ class WorkspaceProvider implements IWorkspaceProvider { + function doCreateUri(path: string, queryValues: Map): URI { + let query: string | undefined = undefined; +@@ -309,12 +310,18 @@ class WorkspaceProvider implements IWorkspaceProvider { // Folder else if (isFolderToOpen(workspace)) { @@ -465,13 +485,13 @@ index c629f7fffa1faad78e5d9907fd38aec289db0428..c266e1fb06ffc44f1ec4230ac59fd094 } // Append payload if any -@@ -285,7 +293,22 @@ class WorkspaceProvider implements IWorkspaceProvider { +@@ -404,7 +411,22 @@ class WindowIndicator implements IWindowIndicator { throw new Error('Missing web configuration element'); } - const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = JSON.parse(configElementAttribute); + const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = { -+ webviewEndpoint: `${window.location.origin}${window.location.pathname.replace(/\/+$/, '')}/webview/`, ++ webviewEndpoint: `${window.location.origin}${window.location.pathname.replace(/\/+$/, '')}/webview`, + ...JSON.parse(configElementAttribute), + }; + @@ -489,7 +509,7 @@ index c629f7fffa1faad78e5d9907fd38aec289db0428..c266e1fb06ffc44f1ec4230ac59fd094 // Revive static extension locations if (Array.isArray(config.staticExtensions)) { -@@ -297,40 +320,7 @@ class WorkspaceProvider implements IWorkspaceProvider { +@@ -416,40 +438,7 @@ class WindowIndicator implements IWindowIndicator { // Find workspace to open and payload let foundWorkspace = false; let workspace: IWorkspace; @@ -531,20 +551,37 @@ index c629f7fffa1faad78e5d9907fd38aec289db0428..c266e1fb06ffc44f1ec4230ac59fd094 // If no workspace is provided through the URL, check for config attribute from server if (!foundWorkspace) { -diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts -index 2379b626c81321afe18267c69c6903efbfa354f4..28f8971cf398b048c7d8f56df2e9dc4e32aadb6d 100644 ---- a/src/vs/platform/environment/node/argv.ts -+++ b/src/vs/platform/environment/node/argv.ts -@@ -8,6 +8,8 @@ import { localize } from 'vs/nls'; - import { isWindows } from 'vs/base/common/platform'; - - export interface ParsedArgs { +diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts +index 409bb7e1960c9c06485a6f6d7f39b2efce451d56..f27b651c49ea3fc92b03e31eb64c1cf27c7e4433 100644 +--- a/src/vs/platform/environment/common/argv.ts ++++ b/src/vs/platform/environment/common/argv.ts +@@ -7,6 +7,8 @@ + * A list of command line arguments we support natively. + */ + export interface NativeParsedArgs { + 'extra-extensions-dir'?: string[]; + 'extra-builtin-extensions-dir'?: string[]; _: string[]; 'folder-uri'?: string[]; // undefined or array of 1 or more 'file-uri'?: string[]; // undefined or array of 1 or more -@@ -141,6 +143,8 @@ export const OPTIONS: OptionDescriptions> = { +diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts +index 21b4d719cec1a724bbad407aeec38db9eb8d6f5a..edf46f097bf11bfb8883d38d38ee78b735f35b3f 100644 +--- a/src/vs/platform/environment/common/environment.ts ++++ b/src/vs/platform/environment/common/environment.ts +@@ -122,6 +122,8 @@ export interface INativeEnvironmentService extends IEnvironmentService { + extensionsPath?: string; + extensionsDownloadPath: string; + builtinExtensionsPath: string; ++ extraExtensionPaths: string[] ++ extraBuiltinExtensionPaths: string[] + + // --- Smoke test support + driverHandle?: string; +diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts +index 149e6ffb41a82f1a69cf37f105a31872ad4af8b4..ed99aab42b31bc2ab804391b6e3f4c7ff67d9259 100644 +--- a/src/vs/platform/environment/node/argv.ts ++++ b/src/vs/platform/environment/node/argv.ts +@@ -54,6 +54,8 @@ export const OPTIONS: OptionDescriptions> = { 'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") }, 'extensions-download-dir': { type: 'string' }, 'builtin-extensions-dir': { type: 'string' }, @@ -553,25 +590,16 @@ index 2379b626c81321afe18267c69c6903efbfa354f4..28f8971cf398b048c7d8f56df2e9dc4e 'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") }, 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") }, 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extension.") }, -@@ -403,4 +407,3 @@ export function buildHelpMessage(productName: string, executableName: string, ve +@@ -318,4 +320,3 @@ export function buildHelpMessage(productName: string, executableName: string, ve export function buildVersionMessage(version: string | undefined, commit: string | undefined): string { return `${version || localize('unknownVersion', "Unknown version")}\n${commit || localize('unknownCommit', "Unknown commit")}\n${process.arch}`; } - diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts -index 5c0dc4ad4ae79a172bed4bc3d6440cdf6dd22386..38b8c7573a872d587c5f3f6c5e0521d2bd918daa 100644 +index 80f68fb1decfd1c4fa1bcc30840900240df83f76..d4478b0000a511af11647876a536b8147163f9f8 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts -@@ -38,6 +38,8 @@ export interface INativeEnvironmentService extends IEnvironmentService { - extensionsPath?: string; - extensionsDownloadPath: string; - builtinExtensionsPath: string; -+ extraExtensionPaths: string[]; -+ extraBuiltinExtensionPaths: string[]; - - driverHandle?: string; - driverVerbose: boolean; -@@ -180,6 +182,13 @@ export class EnvironmentService implements INativeEnvironmentService { +@@ -138,6 +138,13 @@ export class NativeEnvironmentService implements INativeEnvironmentService { return resources.joinPath(this.userHome, product.dataFolderName, 'extensions').fsPath; } @@ -586,10 +614,10 @@ index 5c0dc4ad4ae79a172bed4bc3d6440cdf6dd22386..38b8c7573a872d587c5f3f6c5e0521d2 get extensionDevelopmentLocationURI(): URI[] | undefined { const s = this._args.extensionDevelopmentPath; diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts -index 575b2aafc3802cd6f5f943930e30de9f2c2690de..873181f967856759e3dc001e5bbe06e2f9eb676a 100644 +index aee65f8eddbfbce3e42362be9590c98d46f2ace5..dc891fba7c7af3ace02b0091ef858bea59e754c6 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts -@@ -85,7 +85,7 @@ export class ExtensionsScanner extends Disposable { +@@ -91,7 +91,7 @@ export class ExtensionsScanner extends Disposable { } async scanAllUserExtensions(): Promise { @@ -598,7 +626,7 @@ index 575b2aafc3802cd6f5f943930e30de9f2c2690de..873181f967856759e3dc001e5bbe06e2 } async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise { -@@ -211,7 +211,13 @@ export class ExtensionsScanner extends Disposable { +@@ -236,7 +236,13 @@ export class ExtensionsScanner extends Disposable { private async scanExtensionsInDir(dir: string, type: ExtensionType): Promise { const limiter = new Limiter(10); @@ -613,7 +641,7 @@ index 575b2aafc3802cd6f5f943930e30de9f2c2690de..873181f967856759e3dc001e5bbe06e2 const extensions = await Promise.all(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, dir, type)))); return extensions.filter(e => e && e.identifier); } -@@ -241,7 +247,7 @@ export class ExtensionsScanner extends Disposable { +@@ -266,7 +272,7 @@ export class ExtensionsScanner extends Disposable { } private async scanDefaultSystemExtensions(): Promise { @@ -622,7 +650,7 @@ index 575b2aafc3802cd6f5f943930e30de9f2c2690de..873181f967856759e3dc001e5bbe06e2 this.logService.trace('Scanned system extensions:', result.length); return result; } -@@ -345,4 +351,9 @@ export class ExtensionsScanner extends Disposable { +@@ -370,4 +376,9 @@ export class ExtensionsScanner extends Disposable { } }); } @@ -633,10 +661,10 @@ index 575b2aafc3802cd6f5f943930e30de9f2c2690de..873181f967856759e3dc001e5bbe06e2 + } } diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts -index 3370a608b4b54c238a6ea69a92cdfcd4817cab57..37b3592d39d8ee3aed5455d599612485ea621323 100644 +index 2bea85740cb3e00c955ec0f7aa46d5f9bb8d5dc8..c0953d7b73178fc4a7b030246a5281609c3dfce6 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts -@@ -30,6 +30,12 @@ if (isWeb) { +@@ -37,6 +37,12 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire ! ], }); } @@ -648,12 +676,12 @@ index 3370a608b4b54c238a6ea69a92cdfcd4817cab57..37b3592d39d8ee3aed5455d599612485 + } } - // Node: AMD loader + // Native (non-sandboxed) diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts -index 040c869d94ceb278350c1d752f55712feedda379..bf16defcf7bc4229dedbbe9eae8a965e996c69d9 100644 +index 333e5b24b05c96e8d44e9025b7a777e6989de9e7..b13572327a6e91592eedea9bcb1e580397f5c224 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts -@@ -30,6 +30,8 @@ export type ConfigurationSyncStore = { +@@ -32,6 +32,8 @@ export type ConfigurationSyncStore = { }; export interface IProductConfiguration { @@ -684,10 +712,10 @@ index 3715cbb8e6ee41c3d9b5090918d243b723ae2d00..c65de8ad37e727d66da97a8f8b170cbc - - diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts -index 2185bb5228c3f9603f307237e7f146fe386708d8..35463ca6520a7da2308d01a51ef2d3e544f10a10 100644 +index fdd5890c69f72025b94913380f0d226226e8c8fb..e084236526b38c1144d47b8b3000b367c3207fe8 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts -@@ -89,7 +89,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio +@@ -93,7 +93,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio options.socketFactory.connect( options.host, options.port, @@ -697,10 +725,10 @@ index 2185bb5228c3f9603f307237e7f146fe386708d8..35463ca6520a7da2308d01a51ef2d3e5 if (err || !socket) { options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts -index 59b1baf9120cb0ccf1ebc425ed708224b5513d41..cf9805554b91176ac2521963a7775711e287e4cc 100644 +index ab3fd347b69f8a3d9b96e706cd87c911b8ffed6b..9d351037b577f9f1edfd18ae9b3c48a211f4467f 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts -@@ -116,8 +116,8 @@ export class BrowserStorageService extends Disposable implements IStorageService +@@ -122,8 +122,8 @@ export class BrowserStorageService extends Disposable implements IStorageService return this.getStorage(scope).getNumber(key, fallbackValue); } @@ -712,10 +740,10 @@ index 59b1baf9120cb0ccf1ebc425ed708224b5513d41..cf9805554b91176ac2521963a7775711 remove(key: string, scope: StorageScope): void { diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts -index 1623957cb18eedbf968cca6231d226b587f51935..d366438d54d36a86bd416aeb9f802ad9015f601c 100644 +index 6611f1dae42055f69a55c1c154d9475f11cd4d0a..d598d4909d5ff6d1614e4a038b1865e1f9a4e963 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts -@@ -83,7 +83,7 @@ export interface IStorageService { +@@ -85,7 +85,7 @@ export interface IStorageService { * The scope argument allows to define the scope of the storage * operation to either the current workspace only or all workspaces. */ @@ -725,10 +753,10 @@ index 1623957cb18eedbf968cca6231d226b587f51935..d366438d54d36a86bd416aeb9f802ad9 /** * Delete an element stored under the provided key from storage. diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts -index 75514fe5a4fabdc885556311954ab016c593bac3..62d97c60488856dfde0bb64fea85032b2e49bb94 100644 +index 096b9e23493539c9937940a56e555d95bbae38d9..ef37e614004f550f7b64eacd362f6894fc523a42 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts -@@ -204,8 +204,8 @@ export class NativeStorageService extends Disposable implements IStorageService +@@ -201,8 +201,8 @@ export class NativeStorageService extends Disposable implements IStorageService return this.getStorage(scope).getNumber(key, fallbackValue); } @@ -741,10 +769,10 @@ index 75514fe5a4fabdc885556311954ab016c593bac3..62d97c60488856dfde0bb64fea85032b remove(key: string, scope: StorageScope): void { diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts new file mode 100644 -index 0000000000000000000000000000000000000000..3c0703b7174ad792a4b42841e96ee93765d71601 +index 0000000000000000000000000000000000000000..385b9da491d38a9f5d10fab6e4666c84a892f49d --- /dev/null +++ b/src/vs/server/browser/client.ts -@@ -0,0 +1,189 @@ +@@ -0,0 +1,240 @@ +import { Emitter } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; @@ -763,6 +791,8 @@ index 0000000000000000000000000000000000000000..3c0703b7174ad792a4b42841e96ee937 +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { Options } from 'vs/server/ipc.d'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; ++import { ILogService } from 'vs/platform/log/common/log'; ++import * as path from 'vs/base/common/path'; + +class TelemetryService extends TelemetryChannelClient { + public constructor( @@ -894,8 +924,57 @@ index 0000000000000000000000000000000000000000..3c0703b7174ad792a4b42841e96ee937 + }); + } + ++ const logService = (services.get(ILogService) as ILogService); ++ const storageService = (services.get(IStorageService) as IStorageService); ++ // We set this here first in case the path changes. ++ const updateCheckEndpoint = path.join(window.location.pathname, "/update/check") ++ const getUpdate = async (): Promise => { ++ logService.debug("Checking for update..."); ++ ++ const response = await fetch(updateCheckEndpoint, { ++ headers: { "Accept": "application/json" }, ++ }); ++ if (!response.ok) { ++ throw new Error(response.statusText); ++ } ++ const json = await response.json(); ++ if (json.error) { ++ throw new Error(json.error); ++ } ++ if (json.isLatest) { ++ return; ++ } ++ ++ const lastNoti = storageService.getNumber("csLastUpdateNotification", StorageScope.GLOBAL); ++ if (lastNoti) { ++ // Only remind them again after two days. ++ const timeout = 1000*60*24*2; ++ const threshold = lastNoti + timeout; ++ if (Date.now() < threshold) { ++ return; ++ } ++ } ++ ++ storageService.store("csLastUpdateNotification", Date.now(), StorageScope.GLOBAL); ++ (services.get(INotificationService) as INotificationService).notify({ ++ severity: Severity.Info, ++ message: `[code-server v${json.latest}](https://github.com/cdr/code-server/releases/tag/v${json.latest}) has been released!`, ++ }); ++ }; ++ ++ const updateLoop = (): void => { ++ getUpdate().catch((error) => { ++ logService.debug(`failed to check for update: ${error}`); ++ }).finally(() => { ++ // Check again every 6 hours. ++ setTimeout(updateLoop, 1000*60*6); ++ }); ++ }; ++ ++ updateLoop(); ++ + // This will be used to set the background color while VS Code loads. -+ const theme = (services.get(IStorageService) as IStorageService).get("colorThemeData", StorageScope.GLOBAL); ++ const theme = storageService.get("colorThemeData", StorageScope.GLOBAL); + if (theme) { + localStorage.setItem("colorThemeData", theme); + } @@ -936,11 +1015,12 @@ index 0000000000000000000000000000000000000000..3c0703b7174ad792a4b42841e96ee937 +}; diff --git a/src/vs/server/browser/extHostNodeProxy.ts b/src/vs/server/browser/extHostNodeProxy.ts new file mode 100644 -index 0000000000000000000000000000000000000000..ed7c078077b0c375758529959b280e091436113a +index 0000000000000000000000000000000000000000..5dd5406befcb593ad6366d9e98f46485ed14fbc0 --- /dev/null +++ b/src/vs/server/browser/extHostNodeProxy.ts -@@ -0,0 +1,46 @@ +@@ -0,0 +1,51 @@ +import { Emitter } from 'vs/base/common/event'; ++import { UriComponents } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ExtHostNodeProxyShape, MainContext, MainThreadNodeProxyShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; @@ -982,17 +1062,24 @@ index 0000000000000000000000000000000000000000..ed7c078077b0c375758529959b280e09 + public send(message: string): void { + this.proxy.$send(message); + } ++ ++ public async fetchExtension(extensionUri: UriComponents): Promise { ++ return this.proxy.$fetchExtension(extensionUri).then(b => b.buffer); ++ } +} + +export interface IExtHostNodeProxy extends ExtHostNodeProxy { } +export const IExtHostNodeProxy = createDecorator('IExtHostNodeProxy'); diff --git a/src/vs/server/browser/mainThreadNodeProxy.ts b/src/vs/server/browser/mainThreadNodeProxy.ts new file mode 100644 -index 0000000000000000000000000000000000000000..0d2e93edae2baf34d27b7b52be0bf4960f244531 +index 0000000000000000000000000000000000000000..21a139288e5b8f56016491879d69d01da929decb --- /dev/null +++ b/src/vs/server/browser/mainThreadNodeProxy.ts -@@ -0,0 +1,37 @@ +@@ -0,0 +1,55 @@ ++import { VSBuffer } from 'vs/base/common/buffer'; +import { IDisposable } from 'vs/base/common/lifecycle'; ++import { FileAccess } from 'vs/base/common/network'; ++import { URI, UriComponents } from 'vs/base/common/uri'; +import { INodeProxyService } from 'vs/server/common/nodeProxy'; +import { ExtHostContext, IExtHostContext, MainContext, MainThreadNodeProxyShape } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; @@ -1023,6 +1110,21 @@ index 0000000000000000000000000000000000000000..0d2e93edae2baf34d27b7b52be0bf496 + } + } + ++ async $fetchExtension(extensionUri: UriComponents): Promise { ++ const fetchUri = URI.from({ ++ scheme: window.location.protocol.replace(':', ''), ++ authority: window.location.host, ++ // Use FileAccess to get the static base path. ++ path: FileAccess.asBrowserUri("", require).path, ++ query: `tar=${encodeURIComponent(extensionUri.path)}`, ++ }); ++ const response = await fetch(fetchUri.toString(true)); ++ if (response.status !== 200) { ++ throw new Error(`Failed to download extension "${module}"`); ++ } ++ return VSBuffer.wrap(new Uint8Array(await response.arrayBuffer())); ++ } ++ + dispose(): void { + this.disposables.forEach((d) => d.dispose()); + this.disposables = []; @@ -1031,10 +1133,10 @@ index 0000000000000000000000000000000000000000..0d2e93edae2baf34d27b7b52be0bf496 +} diff --git a/src/vs/server/browser/worker.ts b/src/vs/server/browser/worker.ts new file mode 100644 -index 0000000000000000000000000000000000000000..5ae44cdc856bf81326a4c516b8be9afb2c746a67 +index 0000000000000000000000000000000000000000..1d47ede49b76b1774329269ab5c86fedb5712c19 --- /dev/null +++ b/src/vs/server/browser/worker.ts -@@ -0,0 +1,56 @@ +@@ -0,0 +1,48 @@ +import { Client } from '@coder/node-browser'; +import { fromTar } from '@coder/requirefs'; +import { URI } from 'vs/base/common/uri'; @@ -1049,19 +1151,11 @@ index 0000000000000000000000000000000000000000..5ae44cdc856bf81326a4c516b8be9afb + logService: ILogService, + vscode: any, +): Promise => { -+ const fetchUri = URI.from({ -+ scheme: self.location.protocol.replace(':', ''), -+ authority: self.location.host, -+ path: self.location.pathname.replace(/\/static\/([^\/]+)\/.*$/, '/static/$1\/'), -+ query: `tar=${encodeURIComponent(module.path)}`, -+ }); -+ const response = await fetch(fetchUri.toString(true)); -+ if (response.status !== 200) { -+ throw new Error(`Failed to download extension "${module}"`); -+ } + const client = new Client(nodeProxy, { logger: logService }); -+ const init = await client.handshake(); -+ const buffer = new Uint8Array(await response.arrayBuffer()); ++ const [buffer, init] = await Promise.all([ ++ nodeProxy.fetchExtension(module), ++ client.handshake(), ++ ]); + const rfs = fromTar(buffer); + (self).global = self; + rfs.provide('vscode', vscode); @@ -1217,10 +1311,10 @@ index 0000000000000000000000000000000000000000..4ea6d95d36aaac07dbd4d0e16ab3c1bb +} diff --git a/src/vs/server/entry.ts b/src/vs/server/entry.ts new file mode 100644 -index 0000000000000000000000000000000000000000..ab020fbb4e4ab3748cc807765ff9c362389faafa +index 0000000000000000000000000000000000000000..8482c48bae007ed6b39183001ae2cc6d140fcd50 --- /dev/null +++ b/src/vs/server/entry.ts -@@ -0,0 +1,78 @@ +@@ -0,0 +1,79 @@ +import { field } from '@coder/logger'; +import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { CodeServerMessage, VscodeMessage } from 'vs/server/ipc'; @@ -1265,7 +1359,8 @@ index 0000000000000000000000000000000000000000..ab020fbb4e4ab3748cc807765ff9c362 +// Wait for the init message then start up VS Code. Subsequent messages will +// return new workbench options without starting a new instance. +process.on('message', async (message: CodeServerMessage, socket) => { -+ logger.debug('got message from code-server', field('message', message)); ++ logger.debug('got message from code-server', field('type', message.type)); ++ logger.trace('code-server message content', field('message', message)); + switch (message.type) { + case 'init': + try { @@ -1310,7 +1405,7 @@ index 0000000000000000000000000000000000000000..56331ff1fc32bbd82e769aaecb551e42 +require('../../bootstrap-amd').load('vs/server/entry'); diff --git a/src/vs/server/ipc.d.ts b/src/vs/server/ipc.d.ts new file mode 100644 -index 0000000000000000000000000000000000000000..33b28cf2d53746ee9c50c056ac2e087dcee0a4e2 +index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78fc72a5c6 --- /dev/null +++ b/src/vs/server/ipc.d.ts @@ -0,0 +1,131 @@ @@ -1328,7 +1423,7 @@ index 0000000000000000000000000000000000000000..33b28cf2d53746ee9c50c056ac2e087d + options: VscodeOptions; +} + -+export type Query = { [key: string]: string | string[] | undefined }; ++export type Query = { [key: string]: string | string[] | undefined | Query | Query[] }; + +export interface SocketMessage { + type: 'socket'; @@ -1447,23 +1542,26 @@ index 0000000000000000000000000000000000000000..33b28cf2d53746ee9c50c056ac2e087d +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 0000000000000000000000000000000000000000..e10cc9c218b27d859a523be3db5b8a30ef90d953 +index 0000000000000000000000000000000000000000..693174ee0d21353c3a08a42fd30eaad1e95c3b9d --- /dev/null +++ b/src/vs/server/node/channel.ts -@@ -0,0 +1,360 @@ +@@ -0,0 +1,897 @@ ++import { field, logger } from '@coder/logger'; +import { Server } from '@coder/node-browser'; ++import * as os from 'os'; +import * as path from 'path'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; -+import { OS } from 'vs/base/common/platform'; ++import * as platform from 'vs/base/common/platform'; ++import * as resources from 'vs/base/common/resources'; +import { ReadableStreamEventPayload } from 'vs/base/common/stream'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { transformOutgoingURIs } from 'vs/base/common/uriIpc'; +import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; -+import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; ++import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileType, FileWriteOptions, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; @@ -1475,6 +1573,17 @@ index 0000000000000000000000000000000000000000..e10cc9c218b27d859a523be3db5b8a30 +import { getTranslations } from 'vs/server/node/nls'; +import { getUriTransformer } from 'vs/server/node/util'; +import { IFileChangeDto } from 'vs/workbench/api/common/extHost.protocol'; ++import { IEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; ++import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; ++import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; ++import * as terminal from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; ++import { IShellLaunchConfig, ITerminalEnvironment, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; ++import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; ++import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; ++import { getSystemShell } from 'vs/workbench/contrib/terminal/node/terminal'; ++import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment'; ++import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess'; ++import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; +import { ExtensionScanner, ExtensionScannerInput } from 'vs/workbench/services/extensions/node/extensionPoints'; + +/** @@ -1703,7 +1812,7 @@ index 0000000000000000000000000000000000000000..e10cc9c218b27d859a523be3db5b8a30 + globalStorageHome: this.environment.globalStorageHome, + workspaceStorageHome: this.environment.workspaceStorageHome, + userHome: this.environment.userHome, -+ os: OS, ++ os: platform.OS, + }; + } + @@ -1811,20 +1920,543 @@ index 0000000000000000000000000000000000000000..e10cc9c218b27d859a523be3db5b8a30 + this._$onMessage.fire(message); + } +} ++ ++class VariableResolverService extends AbstractVariableResolverService { ++ constructor( ++ remoteAuthority: string, ++ args: terminal.ICreateTerminalProcessArguments, ++ env: platform.IProcessEnvironment, ++ ) { ++ super({ ++ getFolderUri: (name: string): URI | undefined => { ++ const folder = args.workspaceFolders.find((f) => f.name === name); ++ return folder && URI.revive(folder.uri); ++ }, ++ getWorkspaceFolderCount: (): number => { ++ return args.workspaceFolders.length; ++ }, ++ // In ../../workbench/contrib/terminal/common/remoteTerminalChannel.ts it ++ // looks like there are `config:` entries which must be for this? Not sure ++ // how/if the URI comes into play though. ++ getConfigurationValue: (_: URI, section: string): string | undefined => { ++ return args.resolvedVariables[`config:${section}`]; ++ }, ++ getExecPath: (): string | undefined => { ++ // Assuming that resolverEnv is just for use in the resolver and not for ++ // the terminal itself. ++ return (args.resolverEnv && args.resolverEnv['VSCODE_EXEC_PATH']) || env['VSCODE_EXEC_PATH']; ++ }, ++ // This is just a guess; this is the only file-related thing we're sent ++ // and none of these resolver methods seem to get called so I don't know ++ // how to test. ++ getFilePath: (): string | undefined => { ++ const resource = transformIncoming(remoteAuthority, args.activeFileResource); ++ if (!resource) { ++ return undefined; ++ } ++ // See ../../editor/standalone/browser/simpleServices.ts; ++ // `BaseConfigurationResolverService` calls `getUriLabel` from there. ++ if (resource.scheme === 'file') { ++ return resource.fsPath; ++ } ++ return resource.path; ++ }, ++ // It looks like these are set here although they aren't on the types: ++ // ../../workbench/contrib/terminal/common/remoteTerminalChannel.ts ++ getSelectedText: (): string | undefined => { ++ return args.resolvedVariables.selectedText; ++ }, ++ getLineNumber: (): string | undefined => { ++ return args.resolvedVariables.selectedText; ++ }, ++ }, undefined, env); ++ } ++} ++ ++class Terminal { ++ private readonly process: TerminalProcess; ++ private _pid: number = -1; ++ private _title: string = ""; ++ public readonly workspaceId: string; ++ public readonly workspaceName: string; ++ private readonly persist: boolean; ++ ++ private readonly _onDispose = new Emitter(); ++ public get onDispose(): Event { return this._onDispose.event; } ++ ++ // These are replayed when a client reconnects. ++ private cols: number; ++ private rows: number; ++ private replayData: string[] = []; ++ // This is based on string length and is pretty arbitrary. ++ private readonly maxReplayData = 10000; ++ private totalReplayData = 0; ++ ++ // According to the release notes the terminals are supposed to dispose after ++ // a short timeout; in our case we'll use 48 hours so you can get them back ++ // the next day or over the weekend. ++ private disposeTimeout: NodeJS.Timeout | undefined; ++ private disposeDelay = 48 * 60 * 60 * 1000; ++ ++ private buffering = false; ++ private readonly _onEvent = new Emitter({ ++ // Don't bind to data until something is listening. ++ onFirstListenerAdd: () => { ++ logger.debug('Terminal bound', field('id', this.id)); ++ if (!this.buffering) { ++ this.buffering = true; ++ this.bufferer.startBuffering(this.id, this.process.onProcessData); ++ } ++ }, ++ ++ // Replay stored events. ++ onFirstListenerDidAdd: () => { ++ // We only need to replay if the terminal is being reconnected which is ++ // true if there is a dispose timeout. ++ if (typeof this.disposeTimeout !== "undefined") { ++ return; ++ } ++ ++ clearTimeout(this.disposeTimeout); ++ this.disposeTimeout = undefined; ++ ++ logger.debug('Terminal replaying', field('id', this.id)); ++ this._onEvent.fire({ ++ type: 'replay', ++ events: [{ ++ cols: this.cols, ++ rows: this.rows, ++ data: this.replayData.join(""), ++ }] ++ }); ++ }, ++ ++ onLastListenerRemove: () => { ++ logger.debug('Terminal unbound', field('id', this.id)); ++ if (!this.persist) { // Used by debug consoles. ++ this.dispose(); ++ } else { ++ this.disposeTimeout = setTimeout(() => { ++ this.dispose(); ++ }, this.disposeDelay); ++ } ++ } ++ }); ++ ++ public get onEvent(): Event { return this._onEvent.event; } ++ ++ // Buffer to reduce the number of messages going to the renderer. ++ private readonly bufferer = new TerminalDataBufferer((_, data) => { ++ this._onEvent.fire({ ++ type: 'data', ++ data, ++ }); ++ ++ // No need to store data if we aren't persisting. ++ if (!this.persist) { ++ return; ++ } ++ ++ this.replayData.push(data); ++ this.totalReplayData += data.length; ++ ++ let overflow = this.totalReplayData - this.maxReplayData; ++ if (overflow <= 0) { ++ return; ++ } ++ ++ // Drop events until doing so would put us under budget. ++ let deleteCount = 0; ++ for (; deleteCount < this.replayData.length ++ && this.replayData[deleteCount].length <= overflow; ++deleteCount) { ++ overflow -= this.replayData[deleteCount].length; ++ } ++ ++ if (deleteCount > 0) { ++ this.replayData.splice(0, deleteCount); ++ } ++ ++ // Dropping any more events would put us under budget; trim the first event ++ // instead if still over budget. ++ if (overflow > 0 && this.replayData.length > 0) { ++ this.replayData[0] = this.replayData[0].substring(overflow); ++ } ++ ++ this.totalReplayData = this.replayData.reduce((p, c) => p + c.length, 0); ++ }); ++ ++ public get pid(): number { ++ return this._pid; ++ } ++ ++ public get title(): string { ++ return this._title; ++ } ++ ++ public constructor( ++ public readonly id: number, ++ config: IShellLaunchConfig & { cwd: string }, ++ args: terminal.ICreateTerminalProcessArguments, ++ env: platform.IProcessEnvironment, ++ logService: ILogService, ++ ) { ++ this.workspaceId = args.workspaceId; ++ this.workspaceName = args.workspaceName; ++ ++ this.cols = args.cols; ++ this.rows = args.rows; ++ ++ // TODO: Don't persist terminals until we make it work with things like ++ // htop, vim, etc. ++ // this.persist = args.shouldPersistTerminal; ++ this.persist = false; ++ ++ this.process = new TerminalProcess( ++ config, ++ config.cwd, ++ this.cols, ++ this.rows, ++ env, ++ process.env as platform.IProcessEnvironment, // Environment used for `findExecutable`. ++ false, // windowsEnableConpty: boolean, ++ logService, ++ ); ++ ++ // The current pid and title aren't exposed so they have to be tracked. ++ this.process.onProcessReady((event) => { ++ this._pid = event.pid; ++ this._onEvent.fire({ ++ type: 'ready', ++ pid: event.pid, ++ cwd: event.cwd, ++ }); ++ }); ++ ++ this.process.onProcessTitleChanged((title) => { ++ this._title = title; ++ this._onEvent.fire({ ++ type: 'titleChanged', ++ title, ++ }); ++ }); ++ ++ this.process.onProcessExit((exitCode) => { ++ logger.debug('Terminal exited', field('id', this.id), field('code', exitCode)); ++ this._onEvent.fire({ ++ type: 'exit', ++ exitCode, ++ }); ++ this.dispose(); ++ }); ++ ++ // TODO: I think `execCommand` must have something to do with running ++ // commands on the terminal that will do things in VS Code but we already ++ // have that functionality via a socket so I'm not sure what this is for. ++ // type: 'execCommand'; ++ // reqId: number; ++ // commandId: string; ++ // commandArgs: any[]; ++ ++ // TODO: Maybe this is to ask if the terminal is currently attached to ++ // anything? But we already know that on account of whether anything is ++ // listening to our event emitter. ++ // type: 'orphan?'; ++ } ++ ++ public dispose() { ++ logger.debug('Terminal disposing', field('id', this.id)); ++ this._onEvent.dispose(); ++ this.bufferer.dispose(); ++ this.process.dispose(); ++ this.process.shutdown(true); ++ this._onDispose.fire(); ++ this._onDispose.dispose(); ++ } ++ ++ public shutdown(immediate: boolean): void { ++ return this.process.shutdown(immediate); ++ } ++ ++ public getCwd(): Promise { ++ return this.process.getCwd(); ++ } ++ ++ public getInitialCwd(): Promise { ++ return this.process.getInitialCwd(); ++ } ++ ++ public start(): Promise { ++ return this.process.start(); ++ } ++ ++ public input(data: string): void { ++ return this.process.input(data); ++ } ++ ++ public resize(cols: number, rows: number): void { ++ this.cols = cols; ++ this.rows = rows; ++ return this.process.resize(cols, rows); ++ } ++} ++ ++// References: - ../../workbench/api/node/extHostTerminalService.ts ++// - ../../workbench/contrib/terminal/browser/terminalProcessManager.ts ++export class TerminalProviderChannel implements IServerChannel, IDisposable { ++ private readonly terminals = new Map(); ++ private id = 0; ++ ++ public constructor (private readonly logService: ILogService) { ++ ++ } ++ ++ public listen(_: RemoteAgentConnectionContext, event: string, args?: any): Event { ++ switch (event) { ++ case '$onTerminalProcessEvent': return this.onTerminalProcessEvent(args); ++ } ++ ++ throw new Error(`Invalid listen '${event}'`); ++ } ++ ++ private onTerminalProcessEvent(args: terminal.IOnTerminalProcessEventArguments): Event { ++ return this.getTerminal(args.id).onEvent; ++ } ++ ++ public call(context: RemoteAgentConnectionContext, command: string, args?: any): Promise { ++ switch (command) { ++ case '$createTerminalProcess': return this.createTerminalProcess(context.remoteAuthority, args); ++ case '$startTerminalProcess': return this.startTerminalProcess(args); ++ case '$sendInputToTerminalProcess': return this.sendInputToTerminalProcess(args); ++ case '$shutdownTerminalProcess': return this.shutdownTerminalProcess(args); ++ case '$resizeTerminalProcess': return this.resizeTerminalProcess(args); ++ case '$getTerminalInitialCwd': return this.getTerminalInitialCwd(args); ++ case '$getTerminalCwd': return this.getTerminalCwd(args); ++ case '$sendCommandResultToTerminalProcess': return this.sendCommandResultToTerminalProcess(args); ++ case '$orphanQuestionReply': return this.orphanQuestionReply(args[0]); ++ case '$listTerminals': return this.listTerminals(args[0]); ++ } ++ ++ throw new Error(`Invalid call '${command}'`); ++ } ++ ++ public dispose(): void { ++ this.terminals.forEach((t) => t.dispose()); ++ } ++ ++ private async createTerminalProcess(remoteAuthority: string, args: terminal.ICreateTerminalProcessArguments): Promise { ++ const terminalId = this.id++; ++ logger.debug('Creating terminal', field('id', terminalId), field("terminals", this.terminals.size)); ++ ++ const shellLaunchConfig: IShellLaunchConfig = { ++ name: args.shellLaunchConfig.name, ++ executable: args.shellLaunchConfig.executable, ++ args: args.shellLaunchConfig.args, ++ // TODO: Should we transform if it's a string as well? The incoming ++ // transform only takes `UriComponents` so I suspect it's not necessary. ++ cwd: typeof args.shellLaunchConfig.cwd !== "string" ++ ? transformIncoming(remoteAuthority, args.shellLaunchConfig.cwd) ++ : args.shellLaunchConfig.cwd, ++ env: args.shellLaunchConfig.env, ++ }; ++ ++ const activeWorkspaceUri = transformIncoming(remoteAuthority, args.activeWorkspaceFolder?.uri); ++ const activeWorkspace = activeWorkspaceUri && args.activeWorkspaceFolder ? { ++ ...args.activeWorkspaceFolder, ++ uri: activeWorkspaceUri, ++ toResource: (relativePath: string) => resources.joinPath(activeWorkspaceUri, relativePath), ++ } : undefined; ++ ++ const resolverService = new VariableResolverService(remoteAuthority, args, process.env as platform.IProcessEnvironment); ++ const resolver = terminalEnvironment.createVariableResolver(activeWorkspace, resolverService); ++ ++ const getDefaultShellAndArgs = (): { executable: string; args: string[] | string } => { ++ if (shellLaunchConfig.executable) { ++ const executable = resolverService.resolve(activeWorkspace, shellLaunchConfig.executable); ++ let resolvedArgs: string[] | string = []; ++ if (shellLaunchConfig.args && Array.isArray(shellLaunchConfig.args)) { ++ for (const arg of shellLaunchConfig.args) { ++ resolvedArgs.push(resolverService.resolve(activeWorkspace, arg)); ++ } ++ } else if (shellLaunchConfig.args) { ++ resolvedArgs = resolverService.resolve(activeWorkspace, shellLaunchConfig.args); ++ } ++ return { executable, args: resolvedArgs }; ++ } ++ ++ const executable = terminalEnvironment.getDefaultShell( ++ (key) => args.configuration[key], ++ args.isWorkspaceShellAllowed, ++ getSystemShell(platform.platform), ++ process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), ++ process.env.windir, ++ resolver, ++ this.logService, ++ false, // useAutomationShell ++ ); ++ ++ const resolvedArgs = terminalEnvironment.getDefaultShellArgs( ++ (key) => args.configuration[key], ++ args.isWorkspaceShellAllowed, ++ false, // useAutomationShell ++ resolver, ++ this.logService, ++ ); ++ ++ return { executable, args: resolvedArgs }; ++ }; ++ ++ const getInitialCwd = (): string => { ++ return terminalEnvironment.getCwd( ++ shellLaunchConfig, ++ os.homedir(), ++ resolver, ++ activeWorkspaceUri, ++ args.configuration['terminal.integrated.cwd'], ++ this.logService, ++ ); ++ }; ++ ++ // Use a separate var so Typescript recognizes these properties are no ++ // longer undefined. ++ const resolvedShellLaunchConfig = { ++ ...shellLaunchConfig, ++ ...getDefaultShellAndArgs(), ++ cwd: getInitialCwd(), ++ }; ++ ++ logger.debug('Resolved shell launch configuration', field('id', terminalId)); ++ ++ // Use instead of `terminal.integrated.env.${platform}` to make types work. ++ const getEnvFromConfig = (): terminal.ISingleTerminalConfiguration => { ++ if (platform.isWindows) { ++ return args.configuration['terminal.integrated.env.windows']; ++ } else if (platform.isMacintosh) { ++ return args.configuration['terminal.integrated.env.osx']; ++ } ++ return args.configuration['terminal.integrated.env.linux']; ++ }; ++ ++ const getNonInheritedEnv = async (): Promise => { ++ const env = await getMainProcessParentEnv(); ++ env.VSCODE_IPC_HOOK_CLI = process.env['VSCODE_IPC_HOOK_CLI']!; ++ return env; ++ }; ++ ++ const env = terminalEnvironment.createTerminalEnvironment( ++ shellLaunchConfig, ++ getEnvFromConfig(), ++ resolver, ++ args.isWorkspaceShellAllowed, ++ product.version, ++ args.configuration['terminal.integrated.detectLocale'], ++ args.configuration['terminal.integrated.inheritEnv'] !== false ++ ? process.env as platform.IProcessEnvironment ++ : await getNonInheritedEnv() ++ ); ++ ++ // Apply extension environment variable collections to the environment. ++ if (!shellLaunchConfig.strictEnv) { ++ // They come in an array and in serialized format. ++ const envVariableCollections = new Map(); ++ for (const [k, v] of args.envVariableCollections) { ++ envVariableCollections.set(k, { map: deserializeEnvironmentVariableCollection(v) }); ++ } ++ const mergedCollection = new MergedEnvironmentVariableCollection(envVariableCollections); ++ mergedCollection.applyToProcessEnvironment(env); ++ } ++ ++ logger.debug('Resolved terminal environment', field('id', terminalId)); ++ ++ const terminal = new Terminal(terminalId, resolvedShellLaunchConfig, args, env, this.logService); ++ this.terminals.set(terminalId, terminal); ++ logger.debug('Created terminal', field('id', terminalId)); ++ terminal.onDispose(() => this.terminals.delete(terminalId)); ++ ++ return { ++ terminalId, ++ resolvedShellLaunchConfig, ++ }; ++ } ++ ++ private getTerminal(id: number): Terminal { ++ const terminal = this.terminals.get(id); ++ if (!terminal) { ++ throw new Error(`terminal with id ${id} does not exist`); ++ } ++ return terminal; ++ } ++ ++ private async startTerminalProcess(args: terminal.IStartTerminalProcessArguments): Promise { ++ return this.getTerminal(args.id).start(); ++ } ++ ++ private async sendInputToTerminalProcess(args: terminal.ISendInputToTerminalProcessArguments): Promise { ++ return this.getTerminal(args.id).input(args.data); ++ } ++ ++ private async shutdownTerminalProcess(args: terminal.IShutdownTerminalProcessArguments): Promise { ++ return this.getTerminal(args.id).shutdown(args.immediate); ++ } ++ ++ private async resizeTerminalProcess(args: terminal.IResizeTerminalProcessArguments): Promise { ++ return this.getTerminal(args.id).resize(args.cols, args.rows); ++ } ++ ++ private async getTerminalInitialCwd(args: terminal.IGetTerminalInitialCwdArguments): Promise { ++ return this.getTerminal(args.id).getInitialCwd(); ++ } ++ ++ private async getTerminalCwd(args: terminal.IGetTerminalCwdArguments): Promise { ++ return this.getTerminal(args.id).getCwd(); ++ } ++ ++ private async sendCommandResultToTerminalProcess(_: terminal.ISendCommandResultToTerminalProcessArguments): Promise { ++ // NOTE: Not required unless we implement the `execCommand` event, see above. ++ throw new Error('not implemented'); ++ } ++ ++ private async orphanQuestionReply(_: terminal.IOrphanQuestionReplyArgs): Promise { ++ // NOTE: Not required unless we implement the `orphan?` event, see above. ++ throw new Error('not implemented'); ++ } ++ ++ private async listTerminals(_: terminal.IListTerminalsArgs): Promise { ++ // TODO: args.isInitialization. Maybe this is to have slightly different ++ // behavior when first listing terminals but I don't know what you'd want to ++ // do differently. Maybe it's to reset the terminal dispose timeouts or ++ // something like that, but why not do it each time you list? ++ return Promise.all(Array.from(this.terminals).map(async ([id, terminal]) => { ++ const cwd = await terminal.getCwd(); ++ return { ++ id, ++ pid: terminal.pid, ++ title: terminal.title, ++ cwd, ++ workspaceId: terminal.workspaceId, ++ workspaceName: terminal.workspaceName, ++ }; ++ })); ++ } ++} ++ ++function transformIncoming(remoteAuthority: string, uri: UriComponents | undefined): URI | undefined { ++ const transformer = getUriTransformer(remoteAuthority); ++ return uri ? URI.revive(transformer.transformIncoming(uri)) : uri; ++} diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts new file mode 100644 -index 0000000000000000000000000000000000000000..36e80fb6966ae2cb53c98f3d31e2193d00c509c3 +index 0000000000000000000000000000000000000000..5c3caf4d12cbf9b7228699ec4fa40cb406aa6307 --- /dev/null +++ b/src/vs/server/node/connection.ts -@@ -0,0 +1,157 @@ +@@ -0,0 +1,189 @@ ++import { field, Logger, logger } from '@coder/logger'; +import * as cp from 'child_process'; -+import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { Emitter } from 'vs/base/common/event'; ++import { FileAccess } from 'vs/base/common/network'; +import { ISocket } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -+import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; -+import { ILogService } from 'vs/platform/log/common/log'; ++import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { getNlsConfiguration } from 'vs/server/node/nls'; +import { Protocol } from 'vs/server/node/protocol'; +import { IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; @@ -1889,15 +2521,29 @@ index 0000000000000000000000000000000000000000..36e80fb6966ae2cb53c98f3d31e2193d + } +} + ++interface DisconnectedMessage { ++ type: 'VSCODE_EXTHOST_DISCONNECTED'; ++} ++ ++interface ConsoleMessage { ++ type: '__$console'; ++ // See bootstrap-fork.js#L135. ++ severity: 'log' | 'warn' | 'error'; ++ arguments: any[]; ++} ++ ++type ExtHostMessage = DisconnectedMessage | ConsoleMessage | IExtHostReadyMessage; ++ +export class ExtensionHostConnection extends Connection { + private process?: cp.ChildProcess; ++ private readonly logger: Logger; + + public constructor( + locale:string, protocol: Protocol, buffer: VSBuffer, token: string, -+ private readonly log: ILogService, + private readonly environment: INativeEnvironmentService, + ) { + super(protocol, token); ++ this.logger = logger.named("exthost", field("token", token)); + this.protocol.dispose(); + this.spawn(locale, buffer).then((p) => this.process = p); + this.protocol.getUnderlyingSocket().pause(); @@ -1920,6 +2566,7 @@ index 0000000000000000000000000000000000000000..36e80fb6966ae2cb53c98f3d31e2193d + private sendInitMessage(buffer: VSBuffer): void { + const socket = this.protocol.getUnderlyingSocket(); + socket.pause(); ++ this.logger.trace('Sending socket'); + this.process!.send({ // Process must be set at this point. + type: 'VSCODE_EXTHOST_IPC_SOCKET', + initialDataChunk: (buffer.buffer as Buffer).toString('base64'), @@ -1928,9 +2575,11 @@ index 0000000000000000000000000000000000000000..36e80fb6966ae2cb53c98f3d31e2193d + } + + private async spawn(locale: string, buffer: VSBuffer): Promise { ++ this.logger.trace('Getting NLS configuration...'); + const config = await getNlsConfiguration(locale, this.environment.userDataPath); ++ this.logger.trace('Spawning extension host...'); + const proc = cp.fork( -+ getPathFromAmdModule(require, 'bootstrap-fork'), ++ FileAccess.asFileUri('bootstrap-fork', require).fsPath, + [ '--type=extensionHost' ], + { + env: { @@ -1948,30 +2597,45 @@ index 0000000000000000000000000000000000000000..36e80fb6966ae2cb53c98f3d31e2193d + }, + ); + -+ proc.on('error', () => this.dispose()); -+ proc.on('exit', () => this.dispose()); ++ proc.on('error', (error) => { ++ this.logger.error('Exited unexpectedly', field('error', error)); ++ this.dispose(); ++ }); ++ proc.on('exit', (code) => { ++ this.logger.trace('Exited', field('code', code)); ++ this.dispose(); ++ }); + if (proc.stdout && proc.stderr) { -+ proc.stdout.setEncoding('utf8').on('data', (d) => this.log.info('Extension host stdout', d)); -+ proc.stderr.setEncoding('utf8').on('data', (d) => this.log.error('Extension host stderr', d)); ++ proc.stdout.setEncoding('utf8').on('data', (d) => this.logger.info(d)); ++ proc.stderr.setEncoding('utf8').on('data', (d) => this.logger.error(d)); + } -+ proc.on('message', (event) => { -+ if (event && event.type === '__$console') { -+ const severity = (this.log)[event.severity] ? event.severity : 'info'; -+ (this.log)[severity]('Extension host', event.arguments); -+ } -+ if (event && event.type === 'VSCODE_EXTHOST_DISCONNECTED') { -+ this.setOffline(); ++ ++ proc.on('message', (event: ExtHostMessage) => { ++ switch (event.type) { ++ case '__$console': ++ const fn = this.logger[event.severity === 'log' ? 'info' : event.severity]; ++ if (fn) { ++ fn('console', field('arguments', event.arguments)); ++ } else { ++ this.logger.error('Unexpected severity', field('event', event)); ++ } ++ break; ++ case 'VSCODE_EXTHOST_DISCONNECTED': ++ this.logger.trace('Going offline'); ++ this.setOffline(); ++ break; ++ case 'VSCODE_EXTHOST_IPC_READY': ++ this.logger.trace('Got ready message'); ++ this.sendInitMessage(buffer); ++ break; ++ default: ++ this.logger.error('Unexpected message', field("event", event)); ++ break; + } + }); + -+ const listen = (message: IExtHostReadyMessage) => { -+ if (message.type === 'VSCODE_EXTHOST_IPC_READY') { -+ proc.removeListener('message', listen); -+ this.sendInitMessage(buffer); -+ } -+ }; -+ -+ return proc.on('message', listen); ++ this.logger.trace('Waiting for handshake...'); ++ return proc; + } +} diff --git a/src/vs/server/node/insights.ts b/src/vs/server/node/insights.ts @@ -2455,15 +3119,17 @@ index 0000000000000000000000000000000000000000..3d428a57d31f29c40f9c3ce45f715b44 +}; diff --git a/src/vs/server/node/protocol.ts b/src/vs/server/node/protocol.ts new file mode 100644 -index 0000000000000000000000000000000000000000..3c74512192aec6220216bc8563b3127b9cfd5fbf +index 0000000000000000000000000000000000000000..0d9310038c0ca378579652d89bc8ac84924213db --- /dev/null +++ b/src/vs/server/node/protocol.ts -@@ -0,0 +1,73 @@ +@@ -0,0 +1,91 @@ ++import { field } from '@coder/logger'; +import * as net from 'net'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { AuthRequest, ConnectionTypeRequest, HandshakeMessage } from 'vs/platform/remote/common/remoteAgentConnection'; ++import { logger } from 'vs/server/node/logger'; + +export interface SocketOptions { + readonly reconnectionToken: string; @@ -2491,29 +3157,45 @@ index 0000000000000000000000000000000000000000..3c74512192aec6220216bc8563b3127b + * Perform a handshake to get a connection request. + */ + public handshake(): Promise { ++ logger.trace('Protocol handshake', field('token', this.options.reconnectionToken)); + return new Promise((resolve, reject) => { ++ const timeout = setTimeout(() => { ++ logger.error('Handshake timed out', field('token', this.options.reconnectionToken)); ++ reject(new Error("timed out")); ++ }, 10000); // Matches the client timeout. ++ + const handler = this.onControlMessage((rawMessage) => { + try { -+ const message = JSON.parse(rawMessage.toString()); ++ const raw = rawMessage.toString(); ++ logger.trace('Protocol message', field('token', this.options.reconnectionToken), field('message', raw)); ++ const message = JSON.parse(raw); + switch (message.type) { -+ case 'auth': return this.authenticate(message); ++ case 'auth': ++ return this.authenticate(message); + case 'connectionType': + handler.dispose(); ++ clearTimeout(timeout); + return resolve(message); -+ default: throw new Error('Unrecognized message type'); ++ default: ++ throw new Error('Unrecognized message type'); + } + } catch (error) { + handler.dispose(); ++ clearTimeout(timeout); + reject(error); + } + }); ++ ++ // Kick off the handshake in case we missed the client's opening shot. ++ // TODO: Investigate why that message seems to get lost. ++ this.authenticate(); + }); + } + + /** + * TODO: This ignores the authentication process entirely for now. + */ -+ private authenticate(_message: AuthRequest): void { ++ private authenticate(_?: AuthRequest): void { + this.sendMessage({ type: 'sign', data: '' }); + } + @@ -2534,10 +3216,11 @@ index 0000000000000000000000000000000000000000..3c74512192aec6220216bc8563b3127b +} diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts new file mode 100644 -index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d04b7fb5f +index 0000000000000000000000000000000000000000..c10a5a3a6771a94b2cbcb699bb1261051c71e08b --- /dev/null +++ b/src/vs/server/node/server.ts -@@ -0,0 +1,285 @@ +@@ -0,0 +1,302 @@ ++import { field } from '@coder/logger'; +import * as fs from 'fs'; +import * as net from 'net'; +import * as path from 'path'; @@ -2551,9 +3234,9 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; -+import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -+import { ParsedArgs } from 'vs/platform/environment/node/argv'; -+import { EnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; ++import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; ++import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; ++import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; @@ -2566,8 +3249,9 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; -+import { getLogLevel, ILogService } from 'vs/platform/log/common/log'; ++import { getLogLevel, ILoggerService, ILogService } from 'vs/platform/log/common/log'; +import { LoggerChannel } from 'vs/platform/log/common/logIpc'; ++import { LoggerService } from 'vs/platform/log/node/loggerService'; +import { SpdLogService } from 'vs/platform/log/node/spdlogService'; +import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; @@ -2578,20 +3262,22 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d +import { RequestService } from 'vs/platform/request/node/requestService'; +import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; ++import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; +import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -+import { combinedAppender, LogAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; ++import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; +import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; +import { INodeProxyService, NodeProxyChannel } from 'vs/server/common/nodeProxy'; +import { TelemetryChannel } from 'vs/server/common/telemetry'; +import { Query, VscodeOptions, WorkbenchOptions } from 'vs/server/ipc'; -+import { ExtensionEnvironmentChannel, FileProviderChannel, NodeProxyService } from 'vs/server/node/channel'; ++import { ExtensionEnvironmentChannel, FileProviderChannel, NodeProxyService, TerminalProviderChannel } from 'vs/server/node/channel'; +import { Connection, ExtensionHostConnection, ManagementConnection } from 'vs/server/node/connection'; +import { TelemetryClient } from 'vs/server/node/insights'; +import { logger } from 'vs/server/node/logger'; +import { getLocaleFromConfig, getNlsConfiguration } from 'vs/server/node/nls'; +import { Protocol } from 'vs/server/node/protocol'; +import { getUriTransformer } from 'vs/server/node/util'; ++import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from "vs/workbench/services/remote/common/remoteAgentFileSystemChannel"; +import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; + @@ -2606,7 +3292,7 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d + private readonly services = new ServiceCollection(); + private servicesPromise?: Promise; + -+ public async cli(args: ParsedArgs): Promise { ++ public async cli(args: NativeParsedArgs): Promise { + return main(args); + } + @@ -2701,6 +3387,7 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d + ); + } + ++ logger.debug('New connection', field('token', token)); + protocol.sendMessage(await ok()); + + let connection: Connection; @@ -2719,12 +3406,14 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d + connection = new ExtensionHostConnection( + message.args ? message.args.language : 'en', + protocol, buffer, token, -+ this.services.get(ILogService) as ILogService, + this.services.get(IEnvironmentService) as INativeEnvironmentService, + ); + } + connections.set(token, connection); -+ connection.onClose(() => connections.delete(token)); ++ connection.onClose(() => { ++ logger.debug('Connection closed', field('token', token)); ++ connections.delete(token); ++ }); + this.disposeOldOfflineConnections(connections); + break; + case ConnectionType.Tunnel: return protocol.tunnel(); @@ -2736,12 +3425,13 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d + const offline = Array.from(connections.values()) + .filter((connection) => typeof connection.offline !== 'undefined'); + for (let i = 0, max = offline.length - this.maxExtraOfflineConnections; i < max; ++i) { ++ logger.debug('Disposing offline connection', field("token", offline[i].token)); + offline[i].dispose(); + } + } + -+ private async initializeServices(args: ParsedArgs): Promise { -+ const environmentService = new EnvironmentService(args, process.execPath); ++ private async initializeServices(args: NativeParsedArgs): Promise { ++ const environmentService = new NativeEnvironmentService(args); + // https://github.com/cdr/code-server/issues/1693 + fs.mkdirSync(environmentService.globalStorageHome.fsPath, { recursive: true }); + @@ -2763,6 +3453,8 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d + + this.services.set(ILogService, logService); + this.services.set(IEnvironmentService, environmentService); ++ this.services.set(INativeEnvironmentService, environmentService); ++ this.services.set(ILoggerService, new SyncDescriptor(LoggerService)); + + const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); + await configurationService.initialize(); @@ -2771,46 +3463,53 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d + this.services.set(IRequestService, new SyncDescriptor(RequestService)); + this.services.set(IFileService, fileService); + this.services.set(IProductService, { _serviceBrand: undefined, ...product }); -+ this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); -+ this.services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + -+ if (!environmentService.disableTelemetry) { -+ this.services.set(ITelemetryService, new TelemetryService({ -+ appender: combinedAppender( -+ new AppInsightsAppender('code-server', null, () => new TelemetryClient() as any, logService), -+ new LogAppender(logService), -+ ), -+ sendErrorTelemetry: true, -+ commonProperties: resolveCommonProperties( -+ product.commit, product.version, await getMachineId(), -+ [], environmentService.installSourcePath, 'code-server', -+ ), -+ piiPaths, -+ }, configurationService)); -+ } else { -+ this.services.set(ITelemetryService, NullTelemetryService); -+ } ++ const machineId = await getMachineId(); + + await new Promise((resolve) => { + const instantiationService = new InstantiationService(this.services); -+ this.services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService)); -+ this.services.set(INodeProxyService, instantiationService.createInstance(NodeProxyService)); + -+ instantiationService.invokeFunction(() => { ++ instantiationService.invokeFunction((accessor) => { + instantiationService.createInstance(LogsDataCleaner); -+ const telemetryService = this.services.get(ITelemetryService) as ITelemetryService; ++ ++ let telemetryService: ITelemetryService; ++ if (!environmentService.disableTelemetry) { ++ telemetryService = new TelemetryService({ ++ appender: combinedAppender( ++ new AppInsightsAppender('code-server', null, () => new TelemetryClient() as any), ++ new TelemetryLogAppender(accessor.get(ILoggerService), environmentService) ++ ), ++ sendErrorTelemetry: true, ++ commonProperties: resolveCommonProperties( ++ product.commit, product.version, machineId, ++ [], environmentService.installSourcePath, 'code-server', ++ ), ++ piiPaths, ++ }, configurationService); ++ } else { ++ telemetryService = NullTelemetryService; ++ } ++ ++ this.services.set(ITelemetryService, telemetryService); ++ ++ this.services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); ++ this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); ++ this.services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); ++ this.services.set(INodeProxyService, new SyncDescriptor(NodeProxyService)); ++ + this.ipc.registerChannel('extensions', new ExtensionManagementChannel( -+ this.services.get(IExtensionManagementService) as IExtensionManagementService, ++ accessor.get(IExtensionManagementService), + (context) => getUriTransformer(context.remoteAuthority), + )); + this.ipc.registerChannel('remoteextensionsenvironment', new ExtensionEnvironmentChannel( + environmentService, logService, telemetryService, '', + )); -+ this.ipc.registerChannel('request', new RequestChannel(this.services.get(IRequestService) as IRequestService)); ++ this.ipc.registerChannel('request', new RequestChannel(accessor.get(IRequestService))); + this.ipc.registerChannel('telemetry', new TelemetryChannel(telemetryService)); -+ this.ipc.registerChannel('nodeProxy', new NodeProxyChannel(this.services.get(INodeProxyService) as INodeProxyService)); -+ this.ipc.registerChannel('localizations', >createChannelReceiver(this.services.get(ILocalizationsService) as ILocalizationsService)); ++ this.ipc.registerChannel('nodeProxy', new NodeProxyChannel(accessor.get(INodeProxyService))); ++ this.ipc.registerChannel('localizations', >createChannelReceiver(accessor.get(ILocalizationsService))); + this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(environmentService, logService)); ++ this.ipc.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new TerminalProviderChannel(logService)); + resolve(new ErrorTelemetry(telemetryService)); + }); + }); @@ -2843,10 +3542,10 @@ index 0000000000000000000000000000000000000000..fa47e993b46802f1a26457649e9e8bc4 + return path.split("/").map((p) => encodeURIComponent(p)).join("/"); +}; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts -index 3d77009b908f61690a56dc589360627f6f5a3924..11deb1b99ac9d3baa4aa583d711a5e020b4379ec 100644 +index a4df8523631563a498c9ab6e51105074616a481a..f03da094e9080544102bbd3f037a71b348e5bd83 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts -@@ -60,6 +60,7 @@ import './mainThreadComments'; +@@ -61,6 +61,7 @@ import './mainThreadComments'; import './mainThreadNotebook'; import './mainThreadTask'; import './mainThreadLabelService'; @@ -2855,10 +3554,10 @@ index 3d77009b908f61690a56dc589360627f6f5a3924..11deb1b99ac9d3baa4aa583d711a5e02 import './mainThreadAuthentication'; import './mainThreadTimeline'; diff --git a/src/vs/workbench/api/browser/mainThreadStorage.ts b/src/vs/workbench/api/browser/mainThreadStorage.ts -index 7bc3904963bed2925f3640b6bd929347159dd3cf..c6db2368ae9eaca61889efcf3c49763c01ff7459 100644 +index 57abf0e86a5edeeb2bc497af5e140ec13d9b5810..704d0f9ae19d436a7207ff735aabc289c422dd1e 100644 --- a/src/vs/workbench/api/browser/mainThreadStorage.ts +++ b/src/vs/workbench/api/browser/mainThreadStorage.ts -@@ -58,11 +58,11 @@ export class MainThreadStorage implements MainThreadStorageShape { +@@ -62,11 +62,11 @@ export class MainThreadStorage implements MainThreadStorageShape { return JSON.parse(jsonValue); } @@ -2873,7 +3572,7 @@ index 7bc3904963bed2925f3640b6bd929347159dd3cf..c6db2368ae9eaca61889efcf3c49763c return Promise.reject(err); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts -index 97793666ad8abf7d052ba96a88565042b21ebcec..13cd137db1e9435ef66ade3220774b1ddb608d91 100644 +index 284c6aff854a747d1202c34581a1419c35e9654f..f0173d80103ca91b5eab144a10935bc0990119c9 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -68,6 +68,7 @@ import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransf @@ -2884,7 +3583,7 @@ index 97793666ad8abf7d052ba96a88565042b21ebcec..13cd137db1e9435ef66ade3220774b1d import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -@@ -97,6 +98,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I +@@ -103,6 +104,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostStorage = accessor.get(IExtHostStorage); const extensionStoragePaths = accessor.get(IExtensionStoragePaths); const extHostLogService = accessor.get(ILogService); @@ -2892,7 +3591,7 @@ index 97793666ad8abf7d052ba96a88565042b21ebcec..13cd137db1e9435ef66ade3220774b1d const extHostTunnelService = accessor.get(IExtHostTunnelService); const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService); const extHostWindow = accessor.get(IExtHostWindow); -@@ -107,6 +109,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I +@@ -114,6 +116,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration); rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService); rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage); @@ -2901,15 +3600,16 @@ index 97793666ad8abf7d052ba96a88565042b21ebcec..13cd137db1e9435ef66ade3220774b1d rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts -index eb5d8ea84551030a9d72918813fca7adb49a5cd8..da9eb521ca4c660de1bb23df52c18c0efe6f5979 100644 +index 77ef6577821399b150407e980c8fd35e9d005ca6..264e3361accec20e4e1eaae10ae8ca05e47b1fae 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts -@@ -769,6 +769,16 @@ export interface MainThreadLabelServiceShape extends IDisposable { +@@ -816,6 +816,17 @@ export interface MainThreadLabelServiceShape extends IDisposable { $unregisterResourceLabelFormatter(handle: number): void; } +export interface MainThreadNodeProxyShape extends IDisposable { + $send(message: string): void; ++ $fetchExtension(extensionUri: UriComponents): Promise; +} +export interface ExtHostNodeProxyShape { + $onMessage(message: string): void; @@ -2921,7 +3621,7 @@ index eb5d8ea84551030a9d72918813fca7adb49a5cd8..da9eb521ca4c660de1bb23df52c18c0e export interface MainThreadSearchShape extends IDisposable { $registerFileSearchProvider(handle: number, scheme: string): void; $registerTextSearchProvider(handle: number, scheme: string): void; -@@ -1707,6 +1717,7 @@ export const MainContext = { +@@ -1796,6 +1807,7 @@ export const MainContext = { MainThreadWindow: createMainId('MainThreadWindow'), MainThreadLabelService: createMainId('MainThreadLabelService'), MainThreadNotebook: createMainId('MainThreadNotebook'), @@ -2929,7 +3629,7 @@ index eb5d8ea84551030a9d72918813fca7adb49a5cd8..da9eb521ca4c660de1bb23df52c18c0e MainThreadTheming: createMainId('MainThreadTheming'), MainThreadTunnelService: createMainId('MainThreadTunnelService'), MainThreadTimeline: createMainId('MainThreadTimeline') -@@ -1745,6 +1756,7 @@ export const ExtHostContext = { +@@ -1838,6 +1850,7 @@ export const ExtHostContext = { ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), @@ -2938,10 +3638,10 @@ index eb5d8ea84551030a9d72918813fca7adb49a5cd8..da9eb521ca4c660de1bb23df52c18c0e ExtHostTunnelService: createMainId('ExtHostTunnelService'), ExtHostAuthentication: createMainId('ExtHostAuthentication'), diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts -index 34639e18b6fb567feaf19cf20bd312b6b578723f..9c22fe6f090f3cfceea5c3f41695a1ab1d797a19 100644 +index 328b9327207e4f2068bfab6cf374c622d8c5fc69..38963843095c9116011665027f46d3fb85c30ff8 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts -@@ -32,6 +32,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData +@@ -31,6 +31,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -2973,7 +3673,7 @@ index 34639e18b6fb567feaf19cf20bd312b6b578723f..9c22fe6f090f3cfceea5c3f41695a1ab this._extHostTunnelService = extHostTunnelService; this._extHostTerminalService = extHostTerminalService; this._disposables = new DisposableStore(); -@@ -355,7 +359,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme +@@ -362,7 +366,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup); return Promise.all([ @@ -2988,7 +3688,7 @@ index 34639e18b6fb567feaf19cf20bd312b6b578723f..9c22fe6f090f3cfceea5c3f41695a1ab protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; - protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; + protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder, isRemote?: boolean): Promise; - public abstract async $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; + public abstract $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; } diff --git a/src/vs/workbench/api/node/extHost.node.services.ts b/src/vs/workbench/api/node/extHost.node.services.ts @@ -3009,6 +3709,31 @@ index b3c89e51cfc25a53293a352a2a8ad50d5f26d595..e21abe4e13bc25a5b72f556bbfb61085 registerSingleton(IExtHostTerminalService, ExtHostTerminalService); registerSingleton(IExtHostTunnelService, ExtHostTunnelService); +registerSingleton(IExtHostNodeProxy, class extends NotImplementedProxy(String(IExtHostNodeProxy)) { whenReady = Promise.resolve(); }); +diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts +index b3857616f7006127c423dcef7020ae4653da5ff6..1c1b80a2767bf77f30ca5bfee715c337120d3625 100644 +--- a/src/vs/workbench/api/node/extHostCLIServer.ts ++++ b/src/vs/workbench/api/node/extHostCLIServer.ts +@@ -11,6 +11,8 @@ import { IWindowOpenable, IOpenWindowOptions } from 'vs/platform/windows/common/ + import { URI } from 'vs/base/common/uri'; + import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; + import { ILogService } from 'vs/platform/log/common/log'; ++import { join } from 'vs/base/common/path'; ++import { tmpdir } from 'os'; + + export interface OpenCommandPipeArgs { + type: 'open'; +@@ -58,6 +60,11 @@ export class CLIServerBase { + } + + private async setup(): Promise { ++ // NOTE@coder: Write this out so we can get the most recent path. ++ fs.promises.writeFile(join(tmpdir(), "vscode-ipc"), this._ipcHandlePath).catch((error) => { ++ this.logService.error(error); ++ }); ++ + try { + this._server.listen(this.ipcHandlePath); + this._server.on('error', err => this.logService.error(err)); diff --git a/src/vs/workbench/api/worker/extHost.worker.services.ts b/src/vs/workbench/api/worker/extHost.worker.services.ts index 3843fdec386edc09a1d361b63de892a04e0070ed..8aac4df527857e964798362a69f5591bef07c165 100644 --- a/src/vs/workbench/api/worker/extHost.worker.services.ts @@ -3027,18 +3752,18 @@ index 3843fdec386edc09a1d361b63de892a04e0070ed..8aac4df527857e964798362a69f5591b registerSingleton(ILogService, ExtHostLogService); +registerSingleton(IExtHostNodeProxy, ExtHostNodeProxy); diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts -index c71ab1c7da462da8f4a12146d45e6cde7f06ad81..572b07ff2516154f49ab9e02bfcab2b4d8b3009f 100644 +index 021af6e0f8983c492f9cdd048ba2dcae7640bc1d..814dd0ff2fa7737e07833d8092c8f48953c73c47 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts -@@ -9,6 +9,7 @@ import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHost - import { URI } from 'vs/base/common/uri'; - import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; +@@ -11,6 +11,7 @@ import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterc import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; + import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; + import { timeout } from 'vs/base/common/async'; +import { loadCommonJSModule } from 'vs/server/browser/worker'; class WorkerRequireInterceptor extends RequireInterceptor { -@@ -42,10 +43,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { +@@ -46,10 +47,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined { @@ -3056,6 +3781,20 @@ index c71ab1c7da462da8f4a12146d45e6cde7f06ad81..572b07ff2516154f49ab9e02bfcab2b4 module = module.with({ path: ensureSuffix(module.path, '.js') }); const response = await fetch(module.toString(true)); +diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts +index 7344a3a29b32f7b370b99bf0cfdc79a322195ff8..dc21396e83e2f53914447d3460c2ee1103ecb28e 100644 +--- a/src/vs/workbench/browser/actions/navigationActions.ts ++++ b/src/vs/workbench/browser/actions/navigationActions.ts +@@ -310,4 +310,8 @@ actionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(FocusNextPart, + actionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(FocusPreviousPart, { primary: KeyMod.Shift | KeyCode.F6 }), 'View: Focus Previous Part', CATEGORIES.View.value); + + const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +-workbenchRegistry.registerWorkbenchContribution(GoHomeContributor, LifecyclePhase.Ready); ++// See https://github.com/cdr/code-server/issues/2328 ++// workbenchRegistry.registerWorkbenchContribution(GoHomeContributor, LifecyclePhase.Ready); ++export const _1 = workbenchRegistry; ++export const _2 = GoHomeContributor; ++export const _3 = LifecyclePhase.Ready; diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index ced2d815834e40a1543e80516472799075980733..dfcae73e8a042307600c67f163aa00ba9e0762f4 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -3072,18 +3811,18 @@ index ced2d815834e40a1543e80516472799075980733..dfcae73e8a042307600c67f163aa00ba .monaco-workbench .activitybar > .content > .home-bar > .home-bar-icon-badge { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts -index 0462617196b39111cb22e5abbf4b096406496bf8..11434d27af9ce3262c57918e0f5a44d4eebcf8ac 100644 +index 80544aab34c12bb42a36519885e9872ef2b24158..17b56856a0b3fd936dbc094ff39797d5b8ccaadf 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts -@@ -45,6 +45,7 @@ import { FileLogService } from 'vs/platform/log/common/fileLogService'; +@@ -43,6 +43,7 @@ import { FileLogService } from 'vs/platform/log/common/fileLogService'; import { toLocalISOString } from 'vs/base/common/date'; import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; +import { initialize } from 'vs/server/browser/client'; import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; - import { WebResourceIdentityService, IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; -@@ -84,6 +85,8 @@ class BrowserMain extends Disposable { + import { ICommandService } from 'vs/platform/commands/common/commands'; +@@ -101,6 +102,8 @@ class BrowserMain extends Disposable { // Startup const instantiationService = workbench.startup(); @@ -3093,7 +3832,7 @@ index 0462617196b39111cb22e5abbf4b096406496bf8..11434d27af9ce3262c57918e0f5a44d4 return instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts -index 18ea0bfedb4492327429a38237b05915b29f6dd0..d59a17c17f4fffa23d786ce36b4ff624d5688a58 100644 +index 94e7e7a4bac154c45078a1b5034e50634a7a43af..8164200dcef1efbc65b50eef9c270af3ca655fbd 100644 --- a/src/vs/workbench/common/resources.ts +++ b/src/vs/workbench/common/resources.ts @@ -15,6 +15,7 @@ import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob'; @@ -3104,7 +3843,7 @@ index 18ea0bfedb4492327429a38237b05915b29f6dd0..d59a17c17f4fffa23d786ce36b4ff624 export class ResourceContextKey extends Disposable implements IContextKey { -@@ -68,7 +69,8 @@ export class ResourceContextKey extends Disposable implements IContextKey { +@@ -74,7 +75,8 @@ export class ResourceContextKey extends Disposable implements IContextKey { if (!ResourceContextKey._uriEquals(this._resourceKey.get(), value)) { this._contextKeyService.bufferChangeEvents(() => { this._resourceKey.set(value); @@ -3112,13 +3851,13 @@ index 18ea0bfedb4492327429a38237b05915b29f6dd0..d59a17c17f4fffa23d786ce36b4ff624 + // NOTE@coder: Fixes source control context menus (#1104). + this._schemeKey.set(value ? (value.scheme === Schemas.vscodeRemote ? Schemas.file : value.scheme) : null); this._filenameKey.set(value ? basename(value) : null); - this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null); - this._extensionKey.set(value ? extname(value) : null); + this._dirnameKey.set(value ? dirname(value).fsPath : null); + this._pathKey.set(value ? value.fsPath : null); diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css -index 9947f240bf20b42069bd3d50a96d7a783615f54b..bdba0a2fc64a2e6c2cd2644bcc6afc0d131501a7 100644 +index 74f6922e98b4bb6a7fb100f5aac015afe9fc171b..3243a97c2d378013d96ffbe87e9df6dd4a66776d 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css -@@ -138,9 +138,11 @@ +@@ -149,9 +149,11 @@ margin-right: 8px; } @@ -3133,11 +3872,91 @@ index 9947f240bf20b42069bd3d50a96d7a783615f54b..bdba0a2fc64a2e6c2cd2644bcc6afc0d .scm-view .monaco-list .monaco-list-row .resource-group > .actions, .scm-view .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { +diff --git a/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts b/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts +index 6af6a4b7f42654ec6cc60e0ba5efd376919f3e04..3a8176951628e0b2528aae8796ba684c3ab53d9a 100644 +--- a/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts ++++ b/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts +@@ -4,6 +4,7 @@ + *--------------------------------------------------------------------------------------------*/ + + import { escape } from 'vs/base/common/strings'; ++import product from 'vs/platform/product/common/product'; + import { localize } from 'vs/nls'; + + export default () => ` +@@ -11,7 +12,7 @@ export default () => ` +
+
+

${escape(localize('welcomePage.vscode', "Visual Studio Code"))}

+-

${escape(localize({ key: 'welcomePage.editingEvolved', comment: ['Shown as subtitle on the Welcome page.'] }, "Editing evolved"))}

++

VS Code v${product.version}

+
+
+
+@@ -32,6 +33,19 @@ export default () => ` + +

${escape(localize('welcomePage.noRecentFolders', "No recent folders"))}

+
++
++

code-server ${escape(localize('welcomePage.help', "Help"))}

++ ++
+
+

${escape(localize('welcomePage.help', "Help"))}

+
    +diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.css b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.css +index 738ce140c1af76ee0017c59cc883578e966f5348..80833b7023ed5795bb3de303b54ec08d9dab9b94 100644 +--- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.css ++++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.css +@@ -94,7 +94,7 @@ + } + + .monaco-workbench .part.editor > .content .welcomePage .splash .section { +- margin-bottom: 5em; ++ margin-bottom: 3em; + } + + .monaco-workbench .part.editor > .content .welcomePage .splash ul { +diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +index 4a61a79fe447e2aa238af568791bff1e0cec4d29..69cc2e4331a3b04d05d79632920f5c5bbfa924e8 100644 +--- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts ++++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +@@ -328,7 +328,7 @@ class WelcomePage extends Disposable { + + const prodName = container.querySelector('.welcomePage .title .caption') as HTMLElement; + if (prodName) { +- prodName.textContent = this.productService.nameLong; ++ prodName.textContent = `code-server v${this.productService.codeServerVersion}`; + } + + recentlyOpened.then(({ workspaces }) => { +diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +index ed4f26407391bd62219a9f8245a5cd63a7cb7488..92f26d1b082f80475cf76409a4569e948e9e0bd9 100644 +--- a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts ++++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +@@ -130,6 +130,8 @@ export class SimpleNativeWorkbenchEnvironmentService implements INativeWorkbench + extensionsPath?: string | undefined; + extensionsDownloadPath: string = undefined!; + builtinExtensionsPath: string = undefined!; ++ extraExtensionPaths: string[] = undefined!; ++ extraBuiltinExtensionPaths: string[] = undefined!; + + driverHandle?: string | undefined; + diff --git a/src/vs/workbench/services/dialogs/browser/dialogService.ts b/src/vs/workbench/services/dialogs/browser/dialogService.ts -index 6e3182a696dd3443e68ad9e92d029b6ec2d01677..7df85da165a3ba157629c6b9e92f08dd18c7511a 100644 +index 85d83f37da179a1e39266cf72a02e971f590308e..0659738b36df1747c9afcabf8d9abf26c890990b 100644 --- a/src/vs/workbench/services/dialogs/browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/dialogService.ts -@@ -124,11 +124,12 @@ export class DialogService implements IDialogService { +@@ -125,11 +125,12 @@ export class DialogService implements IDialogService { async about(): Promise { const detailString = (useAgo: boolean): string => { return nls.localize('aboutDetail', @@ -3153,18 +3972,25 @@ index 6e3182a696dd3443e68ad9e92d029b6ec2d01677..7df85da165a3ba157629c6b9e92f08dd }; diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts -index ba2701ec54d1a70eaf66afacce585fe906644319..4d4aaa6958b636480178470570e856e62ab922ee 100644 +index a8d43045ecc8cbe04b3f8440cff16d42aadbcad0..8e122c761ac7ddfee11f9dda2ac5e845b893cc28 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts -@@ -121,8 +121,18 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment +@@ -119,8 +119,25 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get logFile(): URI { return joinPath(this.options.logsPath, 'window.log'); } -+ // NOTE@coder: Use the regular path for extensions that write directly to disk -+ // instead of using the VS Code API. ++ // NOTE@coder: Use the same path in // ../../../../platform/environment/node/environmentService.ts ++ // and don't use the user data scheme. This solves two problems: ++ // 1. Extensions running in the browser (like Vim) might use these paths ++ // directly instead of using the file service and most likely can't write ++ // to `/User` on disk. ++ // 2. Settings will be stored in the file system instead of in browser ++ // storage. Using browser storage makes sharing or seeding settings ++ // between browsers difficult. We may want to revisit this once/if we get ++ // settings sync. @memoize - get userRoamingDataHome(): URI { return URI.file('/User').with({ scheme: Schemas.userData }); } -+ get userRoamingDataHome(): URI { return URI.file(this.userDataPath).with({ scheme: Schemas.userData }); } ++ get userRoamingDataHome(): URI { return joinPath(URI.file(this.userDataPath).with({ scheme: Schemas.vscodeRemote }), 'User'); } + @memoize + get userDataPath(): string { + const dataPath = this.payload?.get("userDataPath"); @@ -3176,7 +4002,7 @@ index ba2701ec54d1a70eaf66afacce585fe906644319..4d4aaa6958b636480178470570e856e6 @memoize get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); } -@@ -284,7 +294,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment +@@ -301,7 +318,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment extensionHostDebugEnvironment.params.port = parseInt(value); break; case 'enableProposedApi': @@ -3190,11 +4016,11 @@ index ba2701ec54d1a70eaf66afacce585fe906644319..4d4aaa6958b636480178470570e856e6 break; } } -diff --git a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts -index c28b14774005509f58dddd2dec25547bac85e09f..6090200d9c3671fc1239880dbd060a01a84db1fb 100644 ---- a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts -+++ b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts -@@ -163,7 +163,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench +diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +index 50d4d812b76f09435fcff8148aac4ceeaeb30873..faacf88fcef119f9f959739656d64a84c8f64cbf 100644 +--- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts ++++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +@@ -221,7 +221,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } } @@ -3204,39 +4030,37 @@ index c28b14774005509f58dddd2dec25547bac85e09f..6090200d9c3671fc1239880dbd060a01 return false; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts -index 33eb56db3c25a0dc028b0d54dfa102e5584441cf..de70af33529e40a56969d8f241c82906cda72e1e 100644 +index de7e301d3f0c67ce662827f61427a5a7b3616b9f..877ea8e11e6e6d34b9a8fe16287af309e569285e 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts -@@ -202,8 +202,11 @@ export class ExtensionManagementService extends Disposable implements IExtension - } +@@ -251,7 +251,9 @@ export class ExtensionManagementService extends Disposable implements IWorkbench // Install Language pack on all servers -+ // NOTE@coder: It does not appear language packs can be installed on the web -+ // extension management server at this time. Filter out the web to fix this. if (isLanguagePackExtension(manifest)) { -- return Promise.all(this.servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(([local]) => local); -+ const servers = this.servers.filter(s => s !== this.extensionManagementServerService.webExtensionManagementServer); -+ return Promise.all(servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(([local]) => local); - } - - // 1. Install on preferred location -@@ -236,6 +239,11 @@ export class ExtensionManagementService extends Disposable implements IExtension - return this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.installFromGallery(gallery); +- servers.push(...this.servers); ++ // NOTE@coder: It does not appear language packs can be installed on the web ++ // extension management server at this time. Filter out the web to fix this. ++ servers.push(...this.servers.filter(s => s !== this.extensionManagementServerService.webExtensionManagementServer)); + } else { + const server = this.getExtensionManagementServerToInstall(manifest); + if (server) { +@@ -320,6 +322,11 @@ export class ExtensionManagementService extends Disposable implements IWorkbench + return this.extensionManagementServerService.webExtensionManagementServer; } + // NOTE@coder: Fall back to installing on the remote server. + if (this.extensionManagementServerService.remoteExtensionManagementServer) { -+ return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery); ++ return this.extensionManagementServerService.remoteExtensionManagementServer; + } + - if (this.extensionManagementServerService.remoteExtensionManagementServer) { - const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", gallery.displayName || gallery.name)); - error.name = INSTALL_ERROR_NOT_SUPPORTED; + return undefined; + } + diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts -index d0710e77fa28aacf5b4dfe85efbf67a6a9ae78ab..ceb27174aee3c78ca5a086f05a6b1d3188888034 100644 +index 1dff19bf177eff24f722b748b79835a653241c4d..0f59ad290c82cc4c9d09c565c1018cc275ca0249 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts -@@ -116,8 +116,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten +@@ -177,8 +177,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAgentService.getEnvironment(), this._remoteAgentService.scanExtensions() ]); @@ -3244,10 +4068,10 @@ index d0710e77fa28aacf5b4dfe85efbf67a6a9ae78ab..ceb27174aee3c78ca5a086f05a6b1d31 remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions); + // NOTE@coder: Include remotely hosted extensions that should run locally. + localExtensions = this._checkEnabledAndProposedAPI(localExtensions) -+ .concat(remoteExtensions.filter(ext => ext.extensionKind && (ext.extensionKind === "web" || ext.extensionKind.includes("web")))); ++ .concat(remoteExtensions.filter(ext => !ext.browser && ext.extensionKind && (ext.extensionKind === "web" || ext.extensionKind.includes("web")))); const remoteAgentConnection = this._remoteAgentService.getConnection(); - this._runningLocation = _determineRunningLocation(this._productService, this._configService, localExtensions, remoteExtensions, Boolean(remoteEnv && remoteAgentConnection)); + this._runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, remoteExtensions); diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index 65e532ee58dfc06ed944846d01b885cb8f260ebc..0b6282fde7ad03c7ea9872a777cbf487253abed1 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -3263,11 +4087,11 @@ index 65e532ee58dfc06ed944846d01b885cb8f260ebc..0b6282fde7ad03c7ea9872a777cbf487 export function getExtensionKind(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): ExtensionKind[] { diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts -index 49542eda74c65e485272cd37d586911886aa3ad7..de0e2da0a4c2dca91dc7e0e48c28a8a75ca3e7d4 100644 +index e39d131fe7b1dd4bd1093fedb8faba8e1fe969e8..94f2f1d7c4a0b3cb46eaaffe1181b3abbf997d7f 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -16,7 +16,7 @@ import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; - import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; + import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtensionHostMain, IExitFn } from 'vs/workbench/services/extensions/common/extensionHostMain'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IURITransformer, URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc'; @@ -3320,15 +4144,15 @@ index 49542eda74c65e485272cd37d586911886aa3ad7..de0e2da0a4c2dca91dc7e0e48c28a8a7 console.error(e); } diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts -index 79455414c06b95612a0dce2cad01f2bb2f40ef49..a407593b4dc6053309ed560898918cf67470e836 100644 +index b39a5cbb9eadbc046144d2e76d26a9b0e950ddaa..3b4cc7274e149ee10dba0dbbb09cf25939091f4b 100644 --- a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts -@@ -14,7 +14,11 @@ - +@@ -15,7 +15,11 @@ require.config({ baseUrl: monacoBaseUrl, -- catchError: true -+ catchError: true, + catchError: true, +- createTrustedScriptURL: (value: string) => value ++ createTrustedScriptURL: (value: string) => value, + paths: { + '@coder/node-browser': `../node_modules/@coder/node-browser/out/client/client.js`, + '@coder/requirefs': `../node_modules/@coder/requirefs/out/requirefs.js`, @@ -3337,10 +4161,10 @@ index 79455414c06b95612a0dce2cad01f2bb2f40ef49..a407593b4dc6053309ed560898918cf6 require(['vs/workbench/services/extensions/worker/extensionHostWorker'], () => { }, err => console.error(err)); diff --git a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts -index 44999bd842eae12b752b2e7e8c4904272b111dc1..601b1c5408835c743fe07e34da4d4534873bf832 100644 +index d7aefde89c74bc6096d6e66c45368c8582594efa..9758f3bb96b48603251336e6a64e270ee89744f0 100644 --- a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts +++ b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts -@@ -5,17 +5,17 @@ +@@ -5,8 +5,8 @@ import { createChannelSender } from 'vs/base/parts/ipc/common/ipc'; import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; @@ -3348,8 +4172,9 @@ index 44999bd842eae12b752b2e7e8c4904272b111dc1..601b1c5408835c743fe07e34da4d4534 import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; - export class LocalizationsService { - + // @ts-ignore: interface is implemented via proxy + export class LocalizationsService implements ILocalizationsService { +@@ -14,9 +14,9 @@ export class LocalizationsService implements ILocalizationsService { declare readonly _serviceBrand: undefined; constructor( @@ -3362,7 +4187,7 @@ index 44999bd842eae12b752b2e7e8c4904272b111dc1..601b1c5408835c743fe07e34da4d4534 } diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts -index 0669178db4cf5efe28ffd2a8fe3301de47bcc545..28fafeb2de2efea5c6412853044ce84775f1e038 100644 +index 509f8ac8ce3a689386e439302a53c27e4fdfcef7..2bf9a737bd0dbfa1e604acfc890be45823f02ebe 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -35,7 +35,8 @@ import 'vs/workbench/services/textfile/browser/browserTextFileService'; @@ -3376,7 +4201,7 @@ index 0669178db4cf5efe28ffd2a8fe3301de47bcc545..28fafeb2de2efea5c6412853044ce847 import 'vs/workbench/services/credentials/browser/credentialsService'; import 'vs/workbench/services/url/browser/urlService'; diff --git a/yarn.lock b/yarn.lock -index b2fbf543af319fcc3973248b4ed4981db1ca213a..f10dddd6594bed959e2caa69911f70a6ae8e0554 100644 +index ff358cb6a10984868ed5a5aed5729ac6eb8ebeb7..69668d95ecad219da26ccc4d837913b9324a0e28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -140,6 +140,23 @@ @@ -3403,7 +4228,7 @@ index b2fbf543af319fcc3973248b4ed4981db1ca213a..f10dddd6594bed959e2caa69911f70a6 "@electron/get@^1.0.1": version "1.7.2" resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.7.2.tgz#286436a9fb56ff1a1fcdf0e80131fd65f4d1e0fd" -@@ -5421,6 +5438,13 @@ jsprim@^1.2.2: +@@ -5403,6 +5420,13 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" @@ -3417,7 +4242,7 @@ index b2fbf543af319fcc3973248b4ed4981db1ca213a..f10dddd6594bed959e2caa69911f70a6 just-debounce@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea" -@@ -6008,26 +6032,11 @@ minimatch@0.3: +@@ -5983,26 +6007,11 @@ minimatch@0.3: dependencies: brace-expansion "^1.1.7" @@ -3445,7 +4270,7 @@ index b2fbf543af319fcc3973248b4ed4981db1ca213a..f10dddd6594bed959e2caa69911f70a6 minipass@^2.2.1, minipass@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" -@@ -6797,6 +6806,11 @@ p-try@^2.0.0: +@@ -6744,6 +6753,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== diff --git a/ci/helm-chart/.helmignore b/ci/helm-chart/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/ci/helm-chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/ci/helm-chart/Chart.yaml b/ci/helm-chart/Chart.yaml new file mode 100644 index 000000000..e4665ec49 --- /dev/null +++ b/ci/helm-chart/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: code-server +description: A Helm chart for cdr/code-server + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 1.0.2 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +appVersion: 3.7.3 diff --git a/ci/helm-chart/README.md b/ci/helm-chart/README.md new file mode 100644 index 000000000..85642cf87 --- /dev/null +++ b/ci/helm-chart/README.md @@ -0,0 +1,117 @@ +# code-server + +![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.7.3](https://img.shields.io/badge/AppVersion-3.7.3-informational?style=flat-square) + +[code-server](https://github.com/cdr/code-server) code-server is VS Code running +on a remote server, accessible through the browser. + +This chart is community maintained by [@Matthew-Beckett](https://github.com/Matthew-Beckett) and [@alexgorbatchev](https://github.com/alexgorbatchev) + +## TL;DR; + +```console +$ git clone https://github.com/cdr/code-server +$ cd code-server +$ helm upgrade --install code-server ci/helm-chart +``` + +## Introduction + +This chart bootstraps a code-server deployment on a +[Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) +package manager. + +## Prerequisites + + - Kubernetes 1.6+ + +## Installing the Chart + +To install the chart with the release name `code-server`: + +```console +$ git clone https://github.com/cdr/code-server +$ cd code-server +$ helm upgrade --install code-server ci/helm-chart +``` + +The command deploys code-server on the Kubernetes cluster in the default +configuration. The [configuration](#configuration) section lists the parameters +that can be configured during installation. + +> **Tip**: List all releases using `helm list` + +## Uninstalling the Chart + +To uninstall/delete the `code-server` deployment: + +```console +$ helm delete code-server +``` + +The command removes all the Kubernetes components associated with the chart and +deletes the release. + +## Configuration + +The following table lists the configurable parameters of the code-server chart +and their default values. + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| extraArgs | list | `[]` | | +| extraConfigmapMounts | list | `[]` | | +| extraContainers | string | `""` | | +| extraSecretMounts | list | `[]` | | +| extraVars | list | `[]` | | +| extraVolumeMounts | list | `[]` | | +| fullnameOverride | string | `""` | | +| hostnameOverride | string | `""` | | +| image.pullPolicy | string | `"Always"` | | +| image.repository | string | `"codercom/code-server"` | | +| image.tag | string | `"3.7.3"` | | +| imagePullSecrets | list | `[]` | | +| ingress.enabled | bool | `false` | | +| nameOverride | string | `""` | | +| nodeSelector | object | `{}` | | +| persistence.accessMode | string | `"ReadWriteOnce"` | | +| persistence.annotations | object | `{}` | | +| persistence.enabled | bool | `true` | | +| persistence.size | string | `"1Gi"` | | +| podAnnotations | object | `{}` | | +| podSecurityContext | object | `{}` | | +| replicaCount | int | `1` | | +| resources | object | `{}` | | +| securityContext.enabled | bool | `true` | | +| securityContext.fsGroup | int | `1000` | | +| securityContext.runAsUser | int | `1000` | | +| service.port | int | `8443` | | +| service.type | string | `"ClusterIP"` | | +| serviceAccount.create | bool | `true` | | +| serviceAccount.name | string | `nil` | | +| tolerations | list | `[]` | | +| volumePermissions.enabled | bool | `true` | | +| volumePermissions.securityContext.runAsUser | int | `0` | | + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm +install`. For example, + +```console +$ helm upgrade --install code-server \ + ci/helm-chart \ + --set persistence.enabled=false +``` + +The above command sets the the persistence storage to false. + +Alternatively, a YAML file that specifies the values for the above parameters +can be provided while installing the chart. For example, + +```console +$ helm upgrade --install code-server ci/helm-chart -f values.yaml +``` + +> **Tip**: You can use the default [values.yaml](values.yaml) diff --git a/ci/helm-chart/templates/NOTES.txt b/ci/helm-chart/templates/NOTES.txt new file mode 100644 index 000000000..17c25f646 --- /dev/null +++ b/ci/helm-chart/templates/NOTES.txt @@ -0,0 +1,25 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "code-server.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "code-server.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "code-server.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "code-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} + +Administrator credentials: + + Password: echo $(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "code-server.fullname" . }} -o jsonpath="{.data.password}" | base64 --decode) diff --git a/ci/helm-chart/templates/_helpers.tpl b/ci/helm-chart/templates/_helpers.tpl new file mode 100644 index 000000000..bb36e8c21 --- /dev/null +++ b/ci/helm-chart/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "code-server.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "code-server.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "code-server.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "code-server.labels" -}} +helm.sh/chart: {{ include "code-server.chart" . }} +{{ include "code-server.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "code-server.selectorLabels" -}} +app.kubernetes.io/name: {{ include "code-server.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "code-server.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (include "code-server.fullname" .) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} diff --git a/ci/helm-chart/templates/deployment.yaml b/ci/helm-chart/templates/deployment.yaml new file mode 100644 index 000000000..9364a4706 --- /dev/null +++ b/ci/helm-chart/templates/deployment.yaml @@ -0,0 +1,152 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "code-server.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + {{- if .Values.hostnameOverride }} + hostname: {{ .Values.hostnameOverride }} + {{- end }} + {{- if .Values.securityContext.enabled }} + securityContext: + fsGroup: {{ .Values.securityContext.fsGroup }} + {{- end }} + {{- if and .Values.volumePermissions.enabled .Values.persistence.enabled }} + initContainers: + - name: init-chmod-data + image: busybox:latest + imagePullPolicy: IfNotPresent + command: + - sh + - -c + - | + chown -R {{ .Values.securityContext.runAsUser }}:{{ .Values.securityContext.fsGroup }} /home/coder + securityContext: + runAsUser: {{ .Values.volumePermissions.securityContext.runAsUser }} + volumeMounts: + - name: data + mountPath: /home/coder + {{- end }} + containers: +{{- if .Values.extraContainers }} +{{ toYaml .Values.extraContainers | indent 8}} +{{- end }} + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.securityContext.enabled }} + securityContext: + runAsUser: {{ .Values.securityContext.runAsUser }} + {{- end }} + env: + {{- if .Values.extraVars }} +{{ toYaml .Values.extraVars | indent 10 }} + {{- end }} + - name: PASSWORD + valueFrom: + secretKeyRef: + {{- if .Values.existingSecret }} + name: {{ .Values.existingSecret }} + {{- else }} + name: {{ template "code-server.fullname" . }} + {{- end }} + key: password + {{- if .Values.extraArgs }} + args: +{{ toYaml .Values.extraArgs | indent 10 }} + {{- end }} + volumeMounts: + - name: data + mountPath: /home/coder + {{- range .Values.extraConfigmapMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + subPath: {{ .subPath | default "" }} + readOnly: {{ .readOnly }} + {{- end }} + {{- range .Values.extraSecretMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + readOnly: {{ .readOnly }} + {{- end }} + {{- range .Values.extraVolumeMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + subPath: {{ .subPath | default "" }} + readOnly: {{ .readOnly }} + {{- end }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ template "code-server.serviceAccountName" . }} + volumes: + - name: data + {{- if .Values.persistence.enabled }} + {{- if not .Values.persistence.hostPath }} + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim | default (include "code-server.fullname" .) }} + {{- else }} + hostPath: + path: {{ .Values.persistence.hostPath }} + type: Directory + {{- end -}} + {{- else }} + emptyDir: {} + {{- end -}} + {{- range .Values.extraSecretMounts }} + - name: {{ .name }} + secret: + secretName: {{ .secretName }} + defaultMode: {{ .defaultMode }} + {{- end }} + {{- range .Values.extraVolumeMounts }} + - name: {{ .name }} + {{- if .existingClaim }} + persistentVolumeClaim: + claimName: {{ .existingClaim }} + {{- else }} + hostPath: + path: {{ .hostPath }} + type: Directory + {{- end }} + {{- end }} diff --git a/ci/helm-chart/templates/ingress.yaml b/ci/helm-chart/templates/ingress.yaml new file mode 100644 index 000000000..07a3abd0b --- /dev/null +++ b/ci/helm-chart/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "code-server.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "code-server.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + backend: + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} diff --git a/ci/helm-chart/templates/pvc.yaml b/ci/helm-chart/templates/pvc.yaml new file mode 100644 index 000000000..2f1c87405 --- /dev/null +++ b/ci/helm-chart/templates/pvc.yaml @@ -0,0 +1,29 @@ +{{- if and (and .Values.persistence.enabled (not .Values.persistence.existingClaim)) (not .Values.persistence.hostPath) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ include "code-server.fullname" . }} + namespace: {{ .Release.Namespace }} +{{- with .Values.persistence.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + accessModes: + - {{ .Values.persistence.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} +{{- if .Values.persistence.storageClass }} +{{- if (eq "-" .Values.persistence.storageClass) }} + storageClassName: "" +{{- else }} + storageClassName: "{{ .Values.persistence.storageClass }}" +{{- end }} +{{- end }} +{{- end }} diff --git a/ci/helm-chart/templates/secrets.yaml b/ci/helm-chart/templates/secrets.yaml new file mode 100644 index 000000000..6c600417a --- /dev/null +++ b/ci/helm-chart/templates/secrets.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "code-server.fullname" . }} + annotations: + "helm.sh/hook": "pre-install" + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +type: Opaque +data: + {{ if .Values.password }} + password: "{{ .Values.password | b64enc }}" + {{ else }} + password: "{{ randAlphaNum 24 | b64enc }}" + {{ end }} diff --git a/ci/helm-chart/templates/service.yaml b/ci/helm-chart/templates/service.yaml new file mode 100644 index 000000000..038b6cd0d --- /dev/null +++ b/ci/helm-chart/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "code-server.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: {{ include "code-server.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} diff --git a/ci/helm-chart/templates/serviceaccount.yaml b/ci/helm-chart/templates/serviceaccount.yaml new file mode 100644 index 000000000..df9e1e375 --- /dev/null +++ b/ci/helm-chart/templates/serviceaccount.yaml @@ -0,0 +1,11 @@ +{{- if or .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + name: {{ template "code-server.serviceAccountName" . }} +{{- end -}} diff --git a/ci/helm-chart/templates/tests/test-connection.yaml b/ci/helm-chart/templates/tests/test-connection.yaml new file mode 100644 index 000000000..2e67f56ec --- /dev/null +++ b/ci/helm-chart/templates/tests/test-connection.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "code-server.fullname" . }}-test-connection" + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + annotations: + "helm.sh/hook": test-success +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "code-server.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/ci/helm-chart/values.yaml b/ci/helm-chart/values.yaml new file mode 100644 index 000000000..6594ca5ed --- /dev/null +++ b/ci/helm-chart/values.yaml @@ -0,0 +1,163 @@ +# Default values for code-server. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: codercom/code-server + tag: '3.7.3' + pullPolicy: Always + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +hostnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + #annotations: + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + #hosts: + # - host: code-server.example.loc + # paths: + # - / + + #tls: + # - secretName: code-server + # hosts: + # - code-server.example.loc + +# Optional additional arguments +extraArgs: [] +# - --allow-http +# - --no-auth + +# Optional additional environment variables +extraVars: [] +# - name: DISABLE_TELEMETRY +# value: true + +## +## Init containers parameters: +## volumePermissions: Change the owner of the persist volume mountpoint to RunAsUser:fsGroup +## +volumePermissions: + enabled: true + securityContext: + runAsUser: 0 + +## Pod Security Context +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ +## +securityContext: + enabled: true + fsGroup: 1000 + runAsUser: 1000 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 1000Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +## Persist data to a persistent volume +persistence: + enabled: true + ## code-server data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + accessMode: ReadWriteOnce + size: 10Gi + annotations: {} + # existingClaim: "" + # hostPath: /data + +serviceAccount: + create: true + name: + +## Enable an Specify container in extraContainers. +## This is meant to allow adding code-server dependencies, like docker-dind. +extraContainers: | +#- name: docker-dind +# image: docker:19.03-dind +# imagePullPolicy: IfNotPresent +# resources: +# requests: +# cpu: 250m +# memory: 256M +# securityContext: +# privileged: true +# procMount: Default +# env: +# - name: DOCKER_TLS_CERTDIR +# value: "" +# - name: DOCKER_DRIVER +# value: "overlay2" + +## Additional code-server secret mounts +extraSecretMounts: [] + # - name: secret-files + # mountPath: /etc/secrets + # secretName: code-server-secret-files + # readOnly: true + +## Additional code-server volume mounts +extraVolumeMounts: [] + # - name: extra-volume + # mountPath: /mnt/volume + # readOnly: true + # existingClaim: volume-claim + # hostPath: "" + +extraConfigmapMounts: [] + # - name: certs-configmap + # mountPath: /etc/code-server/ssl/ + # subPath: certificates.crt # (optional) + # configMap: certs-configmap + # readOnly: true diff --git a/ci/images/centos7/Dockerfile b/ci/images/centos7/Dockerfile index 92c212024..a37e590bb 100644 --- a/ci/images/centos7/Dockerfile +++ b/ci/images/centos7/Dockerfile @@ -1,6 +1,6 @@ FROM centos:7 -ARG NODE_VERSION=v12.18.3 +ARG NODE_VERSION=v12.18.4 RUN ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')" && \ curl -fsSL "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-$ARCH.tar.xz" | tar -C /usr/local -xJ && \ mv "/usr/local/node-$NODE_VERSION-linux-$ARCH" "/usr/local/node-$NODE_VERSION" @@ -15,13 +15,18 @@ RUN npm config set python python2 RUN yum install -y epel-release && yum install -y jq RUN yum install -y rsync -# Copied from ../debian8/Dockerfile -# Install Go dependencies +# Copied from ../debian10/Dockerfile +# Install Go. RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \ curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz -ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH +ENV GOPATH=/gopath +# Ensures running this image as another user works. +RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH +ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH + +# Install Go dependencies ENV GO111MODULE=on RUN go get mvdan.cc/sh/v3/cmd/shfmt -RUN go get github.com/goreleaser/nfpm/cmd/nfpm +RUN go get github.com/goreleaser/nfpm/cmd/nfpm@v1.9.0 RUN curl -fsSL https://get.docker.com | sh diff --git a/ci/images/debian8/Dockerfile b/ci/images/debian10/Dockerfile similarity index 52% rename from ci/images/debian8/Dockerfile rename to ci/images/debian10/Dockerfile index 4c62a398b..5e4a5f859 100644 --- a/ci/images/debian8/Dockerfile +++ b/ci/images/debian10/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:8 +FROM debian:10 RUN apt-get update @@ -24,30 +24,31 @@ RUN apt-get install -y build-essential \ RUN apt-get install -y gettext-base # Misc build dependencies. -RUN apt-get install -y git rsync unzip - -# We need latest jq from debian buster for date support. -RUN ARCH="$(dpkg --print-architecture)" && \ - curl -fsSOL http://http.us.debian.org/debian/pool/main/libo/libonig/libonig5_6.9.1-1_$ARCH.deb && \ - dpkg -i libonig*.deb && \ - curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/libjq1_1.5+dfsg-2+b1_$ARCH.deb && \ - dpkg -i libjq*.deb && \ - curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/jq_1.5+dfsg-2+b1_$ARCH.deb && \ - dpkg -i jq*.deb && rm *.deb +RUN apt-get install -y git rsync unzip jq # Installs shellcheck. -# Unfortunately coredumps on debian:8 so disabled for now. -#RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \ -# tar -xJ && \ -# mv shellcheck*/shellcheck /usr/local/bin && \ -# rm -R shellcheck* +RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \ + tar -xJ && \ + mv shellcheck*/shellcheck /usr/local/bin && \ + rm -R shellcheck* -# Install Go dependencies +# Install Go. RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \ curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz -ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH +ENV GOPATH=/gopath +# Ensures running this image as another user works. +RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH +ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH + +# Install Go dependencies ENV GO111MODULE=on RUN go get mvdan.cc/sh/v3/cmd/shfmt -RUN go get github.com/goreleaser/nfpm/cmd/nfpm +RUN go get github.com/goreleaser/nfpm/cmd/nfpm@v1.9.0 + +RUN VERSION="$(curl -fsSL https://storage.googleapis.com/kubernetes-release/release/stable.txt)" && \ + curl -fsSL "https://storage.googleapis.com/kubernetes-release/release/$VERSION/bin/linux/amd64/kubectl" > /usr/local/bin/kubectl \ + && chmod +x /usr/local/bin/kubectl +RUN curl -fsSL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash +RUN helm plugin install https://github.com/instrumenta/helm-kubeval RUN curl -fsSL https://get.docker.com | sh diff --git a/ci/release-image/Dockerfile b/ci/release-image/Dockerfile index 5c31ecbe7..a0b6aed71 100644 --- a/ci/release-image/Dockerfile +++ b/ci/release-image/Dockerfile @@ -43,5 +43,6 @@ EXPOSE 8080 # the uid will remain the same. note: only relevant if -u isn't passed to # docker-run. USER 1000 +ENV USER=coder WORKDIR /home/coder ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."] diff --git a/ci/release-image/entrypoint.sh b/ci/release-image/entrypoint.sh index ee58d07a6..4f2f7cfe2 100755 --- a/ci/release-image/entrypoint.sh +++ b/ci/release-image/entrypoint.sh @@ -1,20 +1,20 @@ #!/bin/sh set -eu -# This isn't set by default. -export USER="$(whoami)" +# We do this first to ensure sudo works below when renaming the user. +# Otherwise the current container UID may not exist in the passwd database. +eval "$(fixuid -q)" -if [ "${DOCKER_USER-}" != "$USER" ]; then +if [ "${DOCKER_USER-}" ]; then echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null # Unfortunately we cannot change $HOME as we cannot move any bind mounts # nor can we bind mount $HOME into a new home as that requires a privileged container. sudo usermod --login "$DOCKER_USER" coder sudo groupmod -n "$DOCKER_USER" coder - export USER="$(whoami)" + USER="$DOCKER_USER" sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd - sudo sed -i "s/coder/$DOCKER_USER/g" /etc/fixuid/config.yml fi -dumb-init fixuid -q /usr/bin/code-server "$@" +dumb-init /usr/bin/code-server "$@" diff --git a/ci/steps/release-packages.sh b/ci/steps/release-packages.sh index cc6cd2a06..ba8d61d5c 100755 --- a/ci/steps/release-packages.sh +++ b/ci/steps/release-packages.sh @@ -4,7 +4,7 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." - NODE_VERSION=v12.18.3 + NODE_VERSION=v12.18.4 NODE_OS="$(uname | tr '[:upper:]' '[:lower:]')" NODE_ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')" curl -L "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-$NODE_OS-$NODE_ARCH.tar.gz" | tar -xz diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md index 80348848d..52b4153a5 100644 --- a/doc/CONTRIBUTING.md +++ b/doc/CONTRIBUTING.md @@ -8,6 +8,7 @@ - [Build](#build) - [Structure](#structure) - [VS Code Patch](#vs-code-patch) + - [Currently Known Issues](#currently-known-issues) @@ -15,24 +16,26 @@ ## Pull Requests -Please link to the issue each PR solves. -If there is no existing issue, please first create one unless the fix is minor. +Please create a [GitHub Issue](https://github.com/cdr/code-server/issues) for each issue +you'd like to address unless the proposed fix is minor. -Please make sure the base of your PR is the master branch. We keep the GitHub -default branch the latest release branch to avoid confusion as the -documentation is on GitHub and we don't want users to see docs on unreleased -features. +In your Pull Requests (PR), link to the issue that the PR solves. + +Please ensure that the base of your PR is the **master** branch. (Note: The default +GitHub branch is the latest release branch, though you should point all of your changes to be merged into +master). ## Requirements -Please refer to [VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites). +The prerequisites for contributing to code-server are almost the same as those for +[VS Code](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites). +There are several differences, however. You must: -Differences: +- Use Node.js version 12.x (or greater) +- Have [nfpm](https://github.com/goreleaser/nfpm) (which is used to build `.deb` and `.rpm` packages and [jq](https://stedolan.github.io/jq/) (used to build code-server releases) installed -- We require a minimum of node v12 but later versions should work. -- We use [nfpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages. -- We use [jq](https://stedolan.github.io/jq/) to build code-server releases. -- The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all our dependencies. +The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all +of the dependencies code-server uses. ## Development Workflow @@ -40,10 +43,10 @@ Differences: yarn yarn vscode yarn watch -# Visit http://localhost:8080 once the build completed. +# Visit http://localhost:8080 once the build is completed. ``` -To develop inside of an isolated docker container: +To develop inside an isolated Docker container: ```shell ./ci/dev/image/run.sh yarn @@ -53,12 +56,12 @@ To develop inside of an isolated docker container: `yarn watch` will live reload changes to the source. -If changes are made to the patch and you've built previously you must manually -reset VS Code then run `yarn vscode:patch`. +If you introduce changes to the patch and you've previously built, you +must (1) manually reset VS Code and (2) run `yarn vscode:patch`. ## Build -You can build with: +You can build using: ```shell ./ci/dev/image/run.sh ./ci/steps/release.sh @@ -66,22 +69,22 @@ You can build with: Run your build with: -``` +```shell cd release yarn --production # Runs the built JavaScript with Node. node . ``` -Build release packages (make sure you run `./ci/steps/release.sh` first): +Build the release packages (make sure that you run `./ci/steps/release.sh` first): -``` -./ci/dev/image/run.sh ./ci/steps/release-packages.sh +```shell +IMAGE=centos7 ./ci/dev/image/run.sh ./ci/steps/release-packages.sh # The standalone release is in ./release-standalone # .deb, .rpm and the standalone archive are in ./release-packages ``` -The `release.sh` script is the equivalent of: +The `release.sh` script is equal to running: ```shell yarn @@ -91,66 +94,69 @@ yarn build:vscode yarn release ``` -And `release-packages.sh` is: +And `release-packages.sh` is equal to: -``` +```shell yarn release:standalone yarn test:standalone-release yarn package ``` +For a faster release build, you can run instead: + +```shell +KEEP_MODULES=1 ./ci/steps/release.sh +node ./release +``` + ## Structure -The `code-server` script serves an HTTP API to login and start a remote VS Code process. +The `code-server` script serves an HTTP API for login and starting a remote VS Code process. The CLI code is in [./src/node](./src/node) and the HTTP routes are implemented in [./src/node/app](./src/node/app). -Most of the meaty parts are in our VS Code patch which is described next. +Most of the meaty parts are in the VS Code patch, which we described next. ### VS Code Patch -Back in v1 of code-server, we had an extensive patch of VS Code that split the codebase -into a frontend and server. The frontend consisted of all UI code and the server ran -the extensions and exposed an API to the frontend for file access and everything else -that the UI needed. +In v1 of code-server, we had a patch of VS Code that split the codebase into a front-end +and a server. The front-end consisted of all UI code, while the server ran the extensions +and exposed an API to the front-end for file access and all UI needs. -This worked but eventually Microsoft added support to VS Code to run it in the web. -They have open sourced the frontend but have kept the server closed source. - -So in interest of piggy backing off their work, v2 and beyond use the VS Code -web frontend and fill in the server. This is contained in our +Over time, Microsoft added support to VS Code to run it on the web. They have made +the front-end open source, but not the server. As such, code-server v2 (and later) uses +the VS Code front-end and implements the server. You can find this in [./ci/dev/vscode.patch](../ci/dev/vscode.patch) under the path `src/vs/server`. Other notable changes in our patch include: -- Add our own build file which includes our code and VS Code's web code. -- Allow multiple extension directories (both user and built-in). -- Modify the loader, websocket, webview, service worker, and asset requests to - use the URL of the page as a base (and TLS if necessary for the websocket). -- Send client-side telemetry through the server. -- Allow modification of the display language. -- Make it possible for us to load code on the client. -- Make extensions work in the browser. -- Make it possible to install extensions of any kind. -- Fix getting permanently disconnected when you sleep or hibernate for a while. -- Add connection type to web socket query parameters. +- Adding our build file, which includes our code and VS Code's web code +- Allowing multiple extension directories (both user and built-in) +- Modifying the loader, websocket, webview, service worker, and asset requests to + use the URL of the page as a base (and TLS, if necessary for the websocket) +- Sending client-side telemetry through the server +- Allowing modification of the display language +- Making it possible for us to load code on the client +- Making extensions work in the browser +- Making it possible to install extensions of any kind +- Fixing issue with getting disconnected when your machine sleeps or hibernates +- Adding connection type to web socket query parameters -Some known issues presently: - -- Creating custom VS Code extensions and debugging them doesn't work. -- Extension profiling and tips are currently disabled. - -As the web portion of VS Code matures, we'll be able to shrink and maybe even entirely -eliminate our patch. In the meantime, however, upgrading the VS Code version requires -ensuring that the patch still applies and has the intended effects. - -To generate a new patch run `yarn vscode:diff`. - -**note**: We have extension docs on the CI and build system at [./ci/README.md](../ci/README.md) - -If functionality doesn't depend on code from VS Code then it should be moved -into code-server otherwise it should be in the patch. - -In the future we'd like to run VS Code unit tests against our builds to ensure features +As the web portion of VS Code matures, we'll be able to shrink and possibly +eliminate our patch. In the meantime, upgrading the VS Code version requires +us to ensure that the patch is applied and works as intended. In the future, +we'd like to run VS Code unit tests against our builds to ensure that features work as expected. + +To generate a new patch, run `yarn vscode:diff` + +**Note**: We have [extension docs](../ci/README.md) on the CI and build system. + +If the functionality you're working on does NOT depend on code from VS Code, please +move it out and into code-server. + +### Currently Known Issues + +- Creating custom VS Code extensions and debugging them doesn't work +- Extension profiling and tips are currently disabled diff --git a/doc/FAQ.md b/doc/FAQ.md index 370dd6660..1a6a217d6 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -3,6 +3,7 @@ # FAQ - [Questions?](#questions) +- [iPad Status?](#ipad-status) - [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration) - [Differences compared to VS Code?](#differences-compared-to-vs-code) - [How can I request a missing extension?](#how-can-i-request-a-missing-extension) @@ -21,7 +22,6 @@ - [Heartbeat File](#heartbeat-file) - [Healthz endpoint](#healthz-endpoint) - [How does the config file work?](#how-does-the-config-file-work) -- [Blank screen on iPad?](#blank-screen-on-ipad) - [Isn't an install script piped into sh insecure?](#isnt-an-install-script-piped-into-sh-insecure) - [How do I make my keyboard shortcuts work?](#how-do-i-make-my-keyboard-shortcuts-work) - [Differences compared to Theia?](#differences-compared-to-theia) @@ -33,6 +33,10 @@ Please file all questions and support requests at https://github.com/cdr/code-server/discussions. +## iPad Status? + +Please see [./ipad.md](./ipad.md). + ## How can I reuse my VS Code configuration? The very popular [Settings Sync](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) extension works. @@ -144,6 +148,9 @@ For HTTPS, you can use a self signed certificate by passing in just `--cert` or pass in an existing certificate by providing the path to `--cert` and the path to the key with `--cert-key`. +The self signed certificate will be generated into +`~/.local/share/code-server/self-signed.crt`. + If `code-server` has been passed a certificate it will also respond to HTTPS requests and will redirect all HTTP requests to HTTPS. @@ -279,15 +286,6 @@ The `--config` flag or `$CODE_SERVER_CONFIG` can be used to change the config fi The default location also respects `$XDG_CONFIG_HOME`. -## Blank screen on iPad? - -Unfortunately at the moment self signed certificates cause a blank screen on iPadOS - -There does seem to be a way to get it to work if you create your own CA and create a -certificate using the CA and then import the CA onto your iPad. - -See [#1566](https://github.com/cdr/code-server/issues/1566#issuecomment-623159434). - ## Isn't an install script piped into sh insecure? Please give diff --git a/doc/guide.md b/doc/guide.md index 8782f57f8..ce17a3614 100644 --- a/doc/guide.md +++ b/doc/guide.md @@ -251,8 +251,7 @@ Visit `https://` to access `code-server`. Congratulations! ### Self Signed Certificate -**note:** Self signed certificates do not work with iPad and will cause a blank page. You'll -have to use [Let's Encrypt](#lets-encrypt) instead. See the [FAQ](./FAQ.md#blank-screen-on-ipad). +**note:** Self signed certificates do not work with iPad normally. See [./ipad.md](./ipad.md) for details. Recommended reading: https://security.stackexchange.com/a/8112. diff --git a/doc/install.md b/doc/install.md index 8d7e98ba8..d987cea26 100644 --- a/doc/install.md +++ b/doc/install.md @@ -2,6 +2,7 @@ # Install +- [Upgrading](#upgrading) - [install.sh](#installsh) - [Flags](#flags) - [Detection Reference](#detection-reference) @@ -12,12 +13,19 @@ - [macOS](#macos) - [Standalone Releases](#standalone-releases) - [Docker](#docker) +- [helm](#helm) This document demonstrates how to install `code-server` on various distros and operating systems. +## Upgrading + +When upgrading you can just install the new version over the old one. code-server +maintains all user data in `~/.local/share/code-server` so that it is preserved in between +installations. + ## install.sh We have a [script](../install.sh) to install code-server for Linux, macOS and FreeBSD. @@ -79,8 +87,8 @@ commands presented in the rest of this document. ## Debian, Ubuntu ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server_3.5.0_amd64.deb -sudo dpkg -i code-server_3.5.0_amd64.deb +curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.3/code-server_3.7.3_amd64.deb +sudo dpkg -i code-server_3.7.3_amd64.deb sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -88,8 +96,8 @@ sudo systemctl enable --now code-server@$USER ## Fedora, CentOS, RHEL, SUSE ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server-3.5.0-amd64.rpm -sudo rpm -i code-server-3.5.0-amd64.rpm +curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.3/code-server-3.7.3-amd64.rpm +sudo rpm -i code-server-3.7.3-amd64.rpm sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -158,10 +166,10 @@ Here is an example script for installing and using a standalone `code-server` re ```bash mkdir -p ~/.local/lib ~/.local/bin -curl -fL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server-3.5.0-linux-amd64.tar.gz \ +curl -fL https://github.com/cdr/code-server/releases/download/v3.7.3/code-server-3.7.3-linux-amd64.tar.gz \ | tar -C ~/.local/lib -xz -mv ~/.local/lib/code-server-3.5.0-linux-amd64 ~/.local/lib/code-server-3.5.0 -ln -s ~/.local/lib/code-server-3.5.0/bin/code-server ~/.local/bin/code-server +mv ~/.local/lib/code-server-3.7.3-linux-amd64 ~/.local/lib/code-server-3.7.3 +ln -s ~/.local/lib/code-server-3.7.3/bin/code-server ~/.local/bin/code-server PATH="~/.local/bin:$PATH" code-server # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml @@ -192,3 +200,7 @@ Our official image supports `amd64` and `arm64`. For `arm32` support there is a popular community maintained alternative: https://hub.docker.com/r/linuxserver/code-server + +## helm + +See [the chart](../ci/helm-chart). diff --git a/doc/ipad.md b/doc/ipad.md new file mode 100644 index 000000000..9c1b4ac84 --- /dev/null +++ b/doc/ipad.md @@ -0,0 +1,53 @@ + + +# iPad + +- [Known Issues](#known-issues) +- [How to access code-server with a self signed certificate on iPad?](#how-to-access-code-server-with-a-self-signed-certificate-on-ipad) + + + +## Known Issues + +- Getting self signed certificates certificates to work is involved, see below. +- Keyboard may disappear sometimes [#1313](https://github.com/cdr/code-server/issues/1313), [#979](https://github.com/cdr/code-server/issues/979) +- Trackpad scrolling does not work [#1455](https://github.com/cdr/code-server/issues/1455) +- See [issues tagged with the iPad label](https://github.com/cdr/code-server/issues?q=is%3Aopen+is%3Aissue+label%3AiPad) for more. + +## How to access code-server with a self signed certificate on iPad? + +Accessing a self signed certificate on iPad isn't as easy as accepting through all +the security warnings. Safari will prevent WebSocket connections unless the certificate +is installed as a profile on the device. + +The below assumes you are using the self signed certificate that code-server +generates for you. If not, that's fine but you'll have to make sure your certificate +abides by the following guidelines from Apple: https://support.apple.com/en-us/HT210176 + +**note**: Another undocumented requirement we noticed is that the certificate has to have `basicConstraints=CA:true`. + +The following instructions assume you have code-server installed and running +with a self signed certificate. If not, please first go through [./guide.md](./guide.md)! + +**warning**: Your iPad must access code-server via a domain name. It could be local +DNS like `mymacbookpro.local` but it must be a domain name. Otherwise Safari will +refuse to allow WebSockets to connect. + +1. Your certificate **must** have a subject alt name that matches the hostname + at which you will access code-server from your iPad. You can pass this to code-server + so that it generates the certificate correctly with `--cert-host`. +2. Share your self signed certificate with the iPad. + - code-server will print the location of the certificate it has generated in the logs. + +``` +[2020-10-30T08:55:45.139Z] info - Using generated certificate and key for HTTPS: ~/.local/share/code-server/mymbp_local.crt +``` + +- You can mail it to yourself or if you have a Mac, it's easiest to just Airdrop to the iPad. + +3. When opening the `*.crt` file, you'll be prompted to go into settings to install. +4. Go to `Settings -> General -> Profile`, select the profile and then hit `Install`. + - It should say the profile is verified. +5. Go to `Settings -> About -> Certificate Trust Settings` and enable full trust for + the certificate. +6. Now you can access code-server! 🍻 diff --git a/install.sh b/install.sh index 0b768def3..2c0240297 100755 --- a/install.sh +++ b/install.sh @@ -17,27 +17,37 @@ usage() { Installs code-server for Linux, macOS and FreeBSD. It tries to use the system package manager if possible. After successful installation it explains how to start using code-server. + +Pass in user@host to install code-server on user@host over ssh. +The remote host must have internet access. ${not_curl_usage-} Usage: - $arg0 [--dry-run] [--version X.X.X] [--method detect] [--prefix ~/.local] + $arg0 [--dry-run] [--version X.X.X] [--method detect] \ + [--prefix ~/.local] [--rsh ssh] [user@host] --dry-run Echo the commands for the install process without running them. + --version X.X.X Install a specific version instead of the latest. + --method [detect | standalone] Choose the installation method. Defaults to detect. - detect detects the system package manager and tries to use it. Full reference on the process is further below. - standalone installs a standalone release archive into ~/.local Add ~/.local/bin to your \$PATH to use it. + --prefix Sets the prefix used by standalone release archives. Defaults to ~/.local The release is unarchived into ~/.local/lib/code-server-X.X.X and the binary symlinked into ~/.local/bin/code-server To install system wide pass ---prefix=/usr/local + --rsh + Specifies the remote shell for remote installation. Defaults to ssh. + - For Debian, Ubuntu and Raspbian it will install the latest deb package. - For Fedora, CentOS, RHEL and openSUSE it will install the latest rpm package. - For Arch Linux it will install the AUR package. @@ -100,9 +110,19 @@ main() { METHOD \ STANDALONE_INSTALL_PREFIX \ VERSION \ - OPTIONAL + OPTIONAL \ + ALL_FLAGS \ + RSH_ARGS \ + RSH + ALL_FLAGS="" while [ "$#" -gt 0 ]; do + case "$1" in + -*) + ALL_FLAGS="${ALL_FLAGS} $1" + ;; + esac + case "$1" in --dry-run) DRY_RUN=1 @@ -128,20 +148,45 @@ main() { --version=*) VERSION="$(parse_arg "$@")" ;; + --rsh) + RSH="$(parse_arg "$@")" + shift + ;; + --rsh=*) + RSH="$(parse_arg "$@")" + ;; -h | --h | -help | --help) usage exit 0 ;; - *) + --) + shift + # We remove the -- added above. + ALL_FLAGS="${ALL_FLAGS% --}" + RSH_ARGS="$*" + break + ;; + -*) echoerr "Unknown flag $1" echoerr "Run with --help to see usage." exit 1 ;; + *) + RSH_ARGS="$*" + break + ;; esac shift done + if [ "${RSH_ARGS-}" ]; then + RSH="${RSH-ssh}" + echoh "Installing remotely with $RSH $RSH_ARGS" + curl -fsSL https://code-server.dev/install.sh | prefix "$RSH_ARGS" "$RSH" "$RSH_ARGS" sh -s -- "$ALL_FLAGS" + return + fi + VERSION="${VERSION-$(echo_latest_version)}" METHOD="${METHOD-detect}" if [ "$METHOD" != detect ] && [ "$METHOD" != standalone ]; then @@ -446,7 +491,7 @@ arch() { } command_exists() { - command -v "$@" > /dev/null 2>&1 + command -v "$@" > /dev/null } sh_c() { @@ -500,4 +545,15 @@ humanpath() { sed "s# $HOME# ~#g; s#\"$HOME#\"\$HOME#g" } +# We need to make sure we exit with a non zero exit if the command fails. +# /bin/sh does not support -o pipefail unfortunately. +prefix() { + PREFIX="$1" + shift + fifo="$(mktemp -d)/fifo" + mkfifo "$fifo" + sed -e "s#^#$PREFIX: #" "$fifo" & + "$@" > "$fifo" 2>&1 +} + main "$@" diff --git a/lib/vscode b/lib/vscode index a0479759d..e5a624b78 160000 --- a/lib/vscode +++ b/lib/vscode @@ -1 +1 @@ -Subproject commit a0479759d6e9ea56afa657e454193f72aef85bd0 +Subproject commit e5a624b788d92b8d34d1392e4c4d9789406efe8f diff --git a/package.json b/package.json index 4d75331e9..c96242477 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-server", "license": "MIT", - "version": "3.5.0", + "version": "3.7.3", "description": "Run VS Code on a remote server.", "homepage": "https://github.com/cdr/code-server", "bugs": { @@ -30,6 +30,9 @@ }, "main": "out/node/entry.js", "devDependencies": { + "@types/body-parser": "^1.19.0", + "@types/cookie-parser": "^1.4.2", + "@types/express": "^4.17.8", "@types/fs-extra": "^8.0.1", "@types/http-proxy": "^1.17.4", "@types/js-yaml": "^3.12.3", @@ -39,11 +42,13 @@ "@types/pem": "^1.9.5", "@types/safe-compare": "^1.1.0", "@types/semver": "^7.1.0", + "@types/split2": "^2.1.6", + "@types/supertest": "^2.0.10", "@types/tar-fs": "^2.0.0", "@types/tar-stream": "^2.1.0", "@types/ws": "^7.2.6", - "@typescript-eslint/eslint-plugin": "^3.10.1", - "@typescript-eslint/parser": "^3.10.1", + "@typescript-eslint/eslint-plugin": "^4.7.0", + "@typescript-eslint/parser": "^4.7.0", "doctoc": "^1.4.0", "eslint": "^7.7.0", "eslint-config-prettier": "^6.0.0", @@ -55,6 +60,7 @@ "prettier": "^2.0.5", "stylelint": "^13.0.0", "stylelint-config-recommended": "^3.0.0", + "supertest": "^6.0.1", "ts-node": "^9.0.0", "typescript": "4.0.2" }, @@ -65,17 +71,22 @@ }, "dependencies": { "@coder/logger": "1.1.16", + "body-parser": "^1.19.0", + "cookie-parser": "^1.4.5", "env-paths": "^2.2.0", + "express": "^5.0.0-alpha.8", "fs-extra": "^9.0.1", "http-proxy": "^1.18.0", "httpolyglot": "^0.1.2", "js-yaml": "^3.13.1", "limiter": "^1.1.5", "pem": "^1.14.2", + "qs": "6.7.0", "rotating-file-stream": "^2.1.1", "safe-buffer": "^5.1.1", "safe-compare": "^1.1.4", "semver": "^7.1.3", + "split2": "^3.2.2", "tar": "^6.0.1", "tar-fs": "^2.0.0", "ws": "^7.2.0", diff --git a/src/browser/pages/login.css b/src/browser/pages/login.css index 43d03e7ee..f0586ee81 100644 --- a/src/browser/pages/login.css +++ b/src/browser/pages/login.css @@ -37,3 +37,7 @@ body { .login-form > .field > .submit { margin-left: 20px; } + +input { + -webkit-appearance: none; +} diff --git a/src/browser/pages/vscode.html b/src/browser/pages/vscode.html index accb07e26..727a1ca87 100644 --- a/src/browser/pages/vscode.html +++ b/src/browser/pages/vscode.html @@ -9,11 +9,6 @@ - - - - - diff --git a/src/browser/pages/vscode.ts b/src/browser/pages/vscode.ts index 3ca1db83e..d95e91741 100644 --- a/src/browser/pages/vscode.ts +++ b/src/browser/pages/vscode.ts @@ -31,7 +31,8 @@ try { } ;(self.require as any) = { - baseUrl: `${options.csStaticBase}/lib/vscode/out`, + // Without the full URL VS Code will try to load file://. + baseUrl: `${window.location.origin}${options.csStaticBase}/lib/vscode/out`, recordStats: true, paths: { "vscode-textmate": `../node_modules/vscode-textmate/release/main`, @@ -40,7 +41,7 @@ try { "xterm-addon-search": `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`, "xterm-addon-unicode11": `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`, "xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, - "semver-umd": `../node_modules/semver-umd/lib/semver-umd.js`, + "tas-client-umd": `../node_modules/tas-client-umd/lib/tas-client-umd.js`, "iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`, jschardet: `../node_modules/jschardet/dist/jschardet.min.js`, }, diff --git a/src/common/emitter.ts b/src/common/emitter.ts index 7a1ebf668..353ce851e 100644 --- a/src/common/emitter.ts +++ b/src/common/emitter.ts @@ -1,4 +1,10 @@ -import { Callback } from "./types" +import { logger } from "@coder/logger" + +/** + * Event emitter callback. Called with the emitted value and a promise that + * resolves when all emitters have finished. + */ +export type Callback> = (t: T, p: Promise) => R export interface Disposable { dispose(): void @@ -32,8 +38,21 @@ export class Emitter { /** * Emit an event with a value. */ - public emit(value: T): void { - this.listeners.forEach((cb) => cb(value)) + public async emit(value: T): Promise { + let resolve: () => void + const promise = new Promise((r) => (resolve = r)) + + await Promise.all( + this.listeners.map(async (cb) => { + try { + await cb(value, promise) + } catch (error) { + logger.error(error.message) + } + }), + ) + + resolve!() } public dispose(): void { diff --git a/src/common/http.ts b/src/common/http.ts index 4749247d7..c08c8673b 100644 --- a/src/common/http.ts +++ b/src/common/http.ts @@ -8,8 +8,12 @@ export enum HttpCode { ServerError = 500, } +/** + * Represents an error with a message and an HTTP status code. This code will be + * used in the HTTP response. + */ export class HttpError extends Error { - public constructor(message: string, public readonly code: number, public readonly details?: object) { + public constructor(message: string, public readonly status: HttpCode, public readonly details?: object) { super(message) this.name = this.constructor.name } diff --git a/src/common/types.ts b/src/common/types.ts deleted file mode 100644 index a8a0e4c1b..000000000 --- a/src/common/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type Callback = (t: T) => R diff --git a/src/node/app.ts b/src/node/app.ts new file mode 100644 index 000000000..448ec9660 --- /dev/null +++ b/src/node/app.ts @@ -0,0 +1,61 @@ +import { logger } from "@coder/logger" +import express, { Express } from "express" +import { promises as fs } from "fs" +import http from "http" +import * as httpolyglot from "httpolyglot" +import { DefaultedArgs } from "./cli" +import { handleUpgrade } from "./wsRouter" + +/** + * Create an Express app and an HTTP/S server to serve it. + */ +export const createApp = async (args: DefaultedArgs): Promise<[Express, Express, http.Server]> => { + const app = express() + + const server = args.cert + ? httpolyglot.createServer( + { + cert: args.cert && (await fs.readFile(args.cert.value)), + key: args["cert-key"] && (await fs.readFile(args["cert-key"])), + }, + app, + ) + : http.createServer(app) + + await new Promise(async (resolve, reject) => { + server.on("error", reject) + if (args.socket) { + try { + await fs.unlink(args.socket) + } catch (error) { + if (error.code !== "ENOENT") { + logger.error(error.message) + } + } + server.listen(args.socket, resolve) + } else { + // [] is the correct format when using :: but Node errors with them. + server.listen(args.port, args.host.replace(/^\[|\]$/g, ""), resolve) + } + }) + + const wsApp = express() + handleUpgrade(wsApp, server) + + return [app, wsApp, server] +} + +/** + * Get the address of a server as a string (protocol *is* included) while + * ensuring there is one (will throw if there isn't). + */ +export const ensureAddress = (server: http.Server): string => { + const addr = server.address() + if (!addr) { + throw new Error("server has no address") + } + if (typeof addr !== "string") { + return `http://${addr.address}:${addr.port}` + } + return addr +} diff --git a/src/node/app/health.ts b/src/node/app/health.ts deleted file mode 100644 index 48d6897cf..000000000 --- a/src/node/app/health.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { HttpProvider, HttpResponse, Heart, HttpProviderOptions } from "../http" - -/** - * Check the heartbeat. - */ -export class HealthHttpProvider extends HttpProvider { - public constructor(options: HttpProviderOptions, private readonly heart: Heart) { - super(options) - } - - public async handleRequest(): Promise { - return { - cache: false, - mime: "application/json", - content: { - status: this.heart.alive() ? "alive" : "expired", - lastHeartbeat: this.heart.lastHeartbeat, - }, - } - } -} diff --git a/src/node/app/login.ts b/src/node/app/login.ts deleted file mode 100644 index 0fe1e0b6e..000000000 --- a/src/node/app/login.ts +++ /dev/null @@ -1,144 +0,0 @@ -import * as http from "http" -import * as limiter from "limiter" -import * as querystring from "querystring" -import { HttpCode, HttpError } from "../../common/http" -import { AuthType, HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" -import { hash, humanPath } from "../util" - -interface LoginPayload { - password?: string - /** - * Since we must set a cookie with an absolute path, we need to know the full - * base path. - */ - base?: string -} - -/** - * Login HTTP provider. - */ -export class LoginHttpProvider extends HttpProvider { - public constructor( - options: HttpProviderOptions, - private readonly configFile: string, - private readonly envPassword: boolean, - ) { - super(options) - } - - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - if (this.options.auth !== AuthType.Password || !this.isRoot(route)) { - throw new HttpError("Not found", HttpCode.NotFound) - } - switch (route.base) { - case "/": - switch (request.method) { - case "POST": - this.ensureMethod(request, ["GET", "POST"]) - return this.tryLogin(route, request) - default: - this.ensureMethod(request) - if (this.authenticated(request)) { - return { - redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/", - query: { to: undefined }, - } - } - return this.getRoot(route) - } - } - - throw new HttpError("Not found", HttpCode.NotFound) - } - - public async getRoot(route: Route, error?: Error): Promise { - const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/login.html") - response.content = response.content.replace(/{{ERROR}}/, error ? `
    ${error.message}
    ` : "") - let passwordMsg = `Check the config file at ${humanPath(this.configFile)} for the password.` - if (this.envPassword) { - passwordMsg = "Password was set from $PASSWORD." - } - response.content = response.content.replace(/{{PASSWORD_MSG}}/g, passwordMsg) - return this.replaceTemplates(route, response) - } - - private readonly limiter = new RateLimiter() - - /** - * Try logging in. On failure, show the login page with an error. - */ - private async tryLogin(route: Route, request: http.IncomingMessage): Promise { - // Already authenticated via cookies? - const providedPassword = this.authenticated(request) - if (providedPassword) { - return { code: HttpCode.Ok } - } - - try { - if (!this.limiter.try()) { - throw new Error("Login rate limited!") - } - - const data = await this.getData(request) - const payload = data ? querystring.parse(data) : {} - return await this.login(payload, route, request) - } catch (error) { - return this.getRoot(route, error) - } - } - - /** - * Return a cookie if the user is authenticated otherwise throw an error. - */ - private async login(payload: LoginPayload, route: Route, request: http.IncomingMessage): Promise { - const password = this.authenticated(request, { - key: typeof payload.password === "string" ? [hash(payload.password)] : undefined, - }) - - if (password) { - return { - redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/", - query: { to: undefined }, - cookie: - typeof password === "string" - ? { - key: "key", - value: password, - path: payload.base, - } - : undefined, - } - } - - // Only log if it was an actual login attempt. - if (payload && payload.password) { - console.error( - "Failed login attempt", - JSON.stringify({ - xForwardedFor: request.headers["x-forwarded-for"], - remoteAddress: request.connection.remoteAddress, - userAgent: request.headers["user-agent"], - timestamp: Math.floor(new Date().getTime() / 1000), - }), - ) - - throw new Error("Incorrect password") - } - - throw new Error("Missing password") - } -} - -// RateLimiter wraps around the limiter library for logins. -// It allows 2 logins every minute and 12 logins every hour. -class RateLimiter { - private readonly minuteLimiter = new limiter.RateLimiter(2, "minute") - private readonly hourLimiter = new limiter.RateLimiter(12, "hour") - - public try(): boolean { - if (this.minuteLimiter.tryRemoveTokens(1)) { - return true - } - return this.hourLimiter.tryRemoveTokens(1) - } -} diff --git a/src/node/app/proxy.ts b/src/node/app/proxy.ts deleted file mode 100644 index a332cc055..000000000 --- a/src/node/app/proxy.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as http from "http" -import { HttpCode, HttpError } from "../../common/http" -import { HttpProvider, HttpResponse, Route, WsResponse } from "../http" - -/** - * Proxy HTTP provider. - */ -export class ProxyHttpProvider extends HttpProvider { - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - if (!this.authenticated(request)) { - if (this.isRoot(route)) { - return { redirect: "/login", query: { to: route.fullPath } } - } - throw new HttpError("Unauthorized", HttpCode.Unauthorized) - } - - // Ensure there is a trailing slash so relative paths work correctly. - if (this.isRoot(route) && !route.fullPath.endsWith("/")) { - return { - redirect: `${route.fullPath}/`, - } - } - - const port = route.base.replace(/^\//, "") - return { - proxy: { - strip: `${route.providerBase}/${port}`, - port, - }, - } - } - - public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise { - this.ensureAuthenticated(request) - const port = route.base.replace(/^\//, "") - return { - proxy: { - strip: `${route.providerBase}/${port}`, - port, - }, - } - } -} diff --git a/src/node/app/static.ts b/src/node/app/static.ts deleted file mode 100644 index 471d0c987..000000000 --- a/src/node/app/static.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { field, logger } from "@coder/logger" -import * as http from "http" -import * as path from "path" -import { Readable } from "stream" -import * as tarFs from "tar-fs" -import * as zlib from "zlib" -import { HttpProvider, HttpResponse, Route } from "../http" -import { pathToFsPath } from "../util" - -/** - * Static file HTTP provider. Static requests do not require authentication if - * the resource is in the application's directory except requests to serve a - * directory as a tar which always requires authentication. - */ -export class StaticHttpProvider extends HttpProvider { - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - this.ensureMethod(request) - - if (typeof route.query.tar === "string") { - this.ensureAuthenticated(request) - return this.getTarredResource(request, pathToFsPath(route.query.tar)) - } - - const response = await this.getReplacedResource(request, route) - if (!this.isDev) { - response.cache = true - } - return response - } - - /** - * Return a resource with variables replaced where necessary. - */ - protected async getReplacedResource(request: http.IncomingMessage, route: Route): Promise { - // The first part is always the commit (for caching purposes). - const split = route.requestPath.split("/").slice(1) - - const resourcePath = path.resolve("/", ...split) - - // Make sure it's in code-server or a plugin. - const validPaths = [this.rootPath, process.env.PLUGIN_DIR] - if (!validPaths.find((p) => p && resourcePath.startsWith(p))) { - this.ensureAuthenticated(request) - } - - switch (split[split.length - 1]) { - case "manifest.json": { - const response = await this.getUtf8Resource(resourcePath) - return this.replaceTemplates(route, response) - } - } - return this.getResource(resourcePath) - } - - /** - * Tar up and stream a directory. - */ - private async getTarredResource(request: http.IncomingMessage, ...parts: string[]): Promise { - const filePath = path.join(...parts) - let stream: Readable = tarFs.pack(filePath) - const headers: http.OutgoingHttpHeaders = {} - if (request.headers["accept-encoding"] && request.headers["accept-encoding"].includes("gzip")) { - logger.debug("gzipping tar", field("filePath", filePath)) - const compress = zlib.createGzip() - stream.pipe(compress) - stream.on("error", (error) => compress.destroy(error)) - stream.on("close", () => compress.end()) - stream = compress - headers["content-encoding"] = "gzip" - } - return { stream, filePath, mime: "application/x-tar", cache: true, headers } - } -} diff --git a/src/node/app/vscode.ts b/src/node/app/vscode.ts deleted file mode 100644 index ed4f714e5..000000000 --- a/src/node/app/vscode.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { field, logger } from "@coder/logger" -import * as cp from "child_process" -import * as crypto from "crypto" -import * as fs from "fs-extra" -import * as http from "http" -import * as net from "net" -import * as path from "path" -import { - CodeServerMessage, - Options, - StartPath, - VscodeMessage, - VscodeOptions, - WorkbenchOptions, -} from "../../../lib/vscode/src/vs/server/ipc" -import { HttpCode, HttpError } from "../../common/http" -import { arrayify, generateUuid } from "../../common/util" -import { Args } from "../cli" -import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" -import { settings } from "../settings" -import { pathToFsPath } from "../util" - -export class VscodeHttpProvider extends HttpProvider { - private readonly serverRootPath: string - private readonly vsRootPath: string - private _vscode?: Promise - - public constructor(options: HttpProviderOptions, private readonly args: Args) { - super(options) - this.vsRootPath = path.resolve(this.rootPath, "lib/vscode") - this.serverRootPath = path.join(this.vsRootPath, "out/vs/server") - } - - public get running(): boolean { - return !!this._vscode - } - - public async dispose(): Promise { - if (this._vscode) { - const vscode = await this._vscode - vscode.removeAllListeners() - this._vscode = undefined - vscode.kill() - } - } - - private async initialize(options: VscodeOptions): Promise { - const id = generateUuid() - const vscode = await this.fork() - - logger.debug("setting up vs code...") - return new Promise((resolve, reject) => { - vscode.once("message", (message: VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - return message.type === "options" && message.id === id - ? resolve(message.options) - : reject(new Error("Unexpected response during initialization")) - }) - vscode.once("error", reject) - vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) - this.send({ type: "init", id, options }, vscode) - }) - } - - private fork(): Promise { - if (!this._vscode) { - logger.debug("forking vs code...") - const vscode = cp.fork(path.join(this.serverRootPath, "fork")) - vscode.on("error", (error) => { - logger.error(error.message) - this._vscode = undefined - }) - vscode.on("exit", (code) => { - logger.error(`VS Code exited unexpectedly with code ${code}`) - this._vscode = undefined - }) - - this._vscode = new Promise((resolve, reject) => { - vscode.once("message", (message: VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - return message.type === "ready" - ? resolve(vscode) - : reject(new Error("Unexpected response waiting for ready response")) - }) - vscode.once("error", reject) - vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) - }) - } - - return this._vscode - } - - public async handleWebSocket(route: Route, request: http.IncomingMessage, socket: net.Socket): Promise { - if (!this.authenticated(request)) { - throw new Error("not authenticated") - } - - // VS Code expects a raw socket. It will handle all the web socket frames. - // We just need to handle the initial upgrade. - // This magic value is specified by the websocket spec. - const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - const reply = crypto - .createHash("sha1") - .update(request.headers["sec-websocket-key"] + magic) - .digest("base64") - socket.write( - [ - "HTTP/1.1 101 Switching Protocols", - "Upgrade: websocket", - "Connection: Upgrade", - `Sec-WebSocket-Accept: ${reply}`, - ].join("\r\n") + "\r\n\r\n", - ) - - const vscode = await this._vscode - this.send({ type: "socket", query: route.query }, vscode, socket) - } - - private send(message: CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void { - if (!vscode || vscode.killed) { - throw new Error("vscode is not running") - } - vscode.send(message, socket) - } - - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - this.ensureMethod(request) - - switch (route.base) { - case "/": - if (!this.isRoot(route)) { - throw new HttpError("Not found", HttpCode.NotFound) - } else if (!this.authenticated(request)) { - return { redirect: "/login", query: { to: route.providerBase } } - } - try { - return await this.getRoot(request, route) - } catch (error) { - const message = `
    VS Code failed to load.
    ${ - this.isDev - ? `
    It might not have finished compiling.
    ` + - `Check for Finished compilation in the output.` - : "" - }

    ${error}` - return this.getErrorRoot(route, "VS Code failed to load", "500", message) - } - } - - this.ensureAuthenticated(request) - - switch (route.base) { - case "/resource": - case "/vscode-remote-resource": - if (typeof route.query.path === "string") { - return this.getResource(pathToFsPath(route.query.path)) - } - break - case "/webview": - if (/^\/vscode-resource/.test(route.requestPath)) { - return this.getResource(route.requestPath.replace(/^\/vscode-resource(\/file)?/, "")) - } - return this.getResource(this.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", route.requestPath) - } - - throw new HttpError("Not found", HttpCode.NotFound) - } - - private async getRoot(request: http.IncomingMessage, route: Route): Promise { - const remoteAuthority = request.headers.host as string - const { lastVisited } = await settings.read() - const startPath = await this.getFirstPath([ - { url: route.query.workspace, workspace: true }, - { url: route.query.folder, workspace: false }, - this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined, - lastVisited, - ]) - const [response, options] = await Promise.all([ - await this.getUtf8Resource(this.rootPath, "src/browser/pages/vscode.html"), - this.initialize({ - args: this.args, - remoteAuthority, - startPath, - }), - ]) - - settings.write({ - lastVisited: startPath || lastVisited, // If startpath is undefined, then fallback to lastVisited - query: route.query, - }) - - if (!this.isDev) { - response.content = response.content.replace(//g, "") - } - - options.productConfiguration.codeServerVersion = require("../../../package.json").version - - response.content = response.content - .replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`) - .replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`) - .replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`) - .replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`) - return this.replaceTemplates(route, response, { - disableTelemetry: !!this.args["disable-telemetry"], - }) - } - - /** - * Choose the first non-empty path. - */ - private async getFirstPath( - startPaths: Array<{ url?: string | string[]; workspace?: boolean } | undefined>, - ): Promise { - const isFile = async (path: string): Promise => { - try { - const stat = await fs.stat(path) - return stat.isFile() - } catch (error) { - logger.warn(error.message) - return false - } - } - for (let i = 0; i < startPaths.length; ++i) { - const startPath = startPaths[i] - const url = arrayify(startPath && startPath.url).find((p) => !!p) - if (startPath && url) { - return { - url, - // The only time `workspace` is undefined is for the command-line - // argument, in which case it's a path (not a URL) so we can stat it - // without having to parse it. - workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url), - } - } - } - return undefined - } -} diff --git a/src/node/cli.ts b/src/node/cli.ts index e5d069551..23a6bc146 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -4,8 +4,12 @@ import yaml from "js-yaml" import * as os from "os" import * as path from "path" import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc" -import { AuthType } from "./http" -import { generatePassword, humanPath, paths } from "./util" +import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util" + +export enum AuthType { + Password = "password", + None = "none", +} export class Optional { public constructor(public readonly value?: T) {} @@ -22,31 +26,34 @@ export enum LogLevel { export class OptionalString extends Optional {} export interface Args extends VsArgs { - readonly config?: string - readonly auth?: AuthType - readonly password?: string - readonly cert?: OptionalString - readonly "cert-key"?: string - readonly "disable-telemetry"?: boolean - readonly help?: boolean - readonly host?: string - readonly json?: boolean + config?: string + auth?: AuthType + password?: string + cert?: OptionalString + "cert-host"?: string + "cert-key"?: string + "disable-telemetry"?: boolean + help?: boolean + host?: string + json?: boolean log?: LogLevel - readonly open?: boolean - readonly port?: number - readonly "bind-addr"?: string - readonly socket?: string - readonly version?: boolean - readonly force?: boolean - readonly "list-extensions"?: boolean - readonly "install-extension"?: string[] - readonly "show-versions"?: boolean - readonly "uninstall-extension"?: string[] - readonly "proxy-domain"?: string[] - readonly locale?: string - readonly _: string[] - readonly "reuse-window"?: boolean - readonly "new-window"?: boolean + open?: boolean + port?: number + "bind-addr"?: string + socket?: string + version?: boolean + force?: boolean + "list-extensions"?: boolean + "install-extension"?: string[] + "show-versions"?: boolean + "uninstall-extension"?: string[] + "proxy-domain"?: string[] + locale?: string + _: string[] + "reuse-window"?: boolean + "new-window"?: boolean + + link?: OptionalString } interface Option { @@ -63,6 +70,11 @@ interface Option { * Description of the option. Leave blank to hide the option. */ description?: string + + /** + * If marked as beta, the option is not printed unless $CS_BETA is set. + */ + beta?: boolean } type OptionType = T extends boolean @@ -94,7 +106,11 @@ const options: Options> = { cert: { type: OptionalString, path: true, - description: "Path to certificate. Generated if no path is provided.", + description: "Path to certificate. A self signed certificate is generated if none is provided.", + }, + "cert-host": { + type: "string", + description: "Hostname to use when generating a self signed certificate.", }, "cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." }, "disable-telemetry": { type: "boolean", description: "Disable telemetry." }, @@ -130,7 +146,8 @@ const options: Options> = { "install-extension": { type: "string[]", description: - "Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.", + "Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`.\n" + + "To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.", }, "enable-proposed-api": { type: "string[]", @@ -144,17 +161,29 @@ const options: Options> = { "new-window": { type: "boolean", short: "n", - description: "Force to open a new window. (use with open-in)", + description: "Force to open a new window.", }, "reuse-window": { type: "boolean", short: "r", - description: "Force to open a file or folder in an already opened window. (use with open-in)", + description: "Force to open a file or folder in an already opened window.", }, locale: { type: "string" }, log: { type: LogLevel }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, + + link: { + type: OptionalString, + description: ` + Securely bind code-server via Coder Cloud with the passed name. You'll get a URL like + https://myname.coder-cloud.com at which you can easily access your code-server instance. + Authorization is done via GitHub. + This is presently beta and requires being accepted for testing. + See https://github.com/cdr/code-server/discussions/2137 + `, + beta: true, + }, } export const optionDescriptions = (): string[] => { @@ -166,12 +195,32 @@ export const optionDescriptions = (): string[] => { }), { short: 0, long: 0 }, ) - return entries.map( - ([k, v]) => - `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat( - widths.long - k.length, - )} ${v.description}${typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : ""}`, - ) + return entries + .filter(([, v]) => { + // If CS_BETA is set, we show beta options but if not, then we do not want + // to show beta options. + return process.env.CS_BETA || !v.beta + }) + .map(([k, v]) => { + const help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${ + v.short ? `-${v.short}` : " " + } --${k} ` + return ( + help + + v.description + ?.trim() + .split(/\n/) + .map((line, i) => { + line = line.trim() + if (i === 0) { + return " ".repeat(widths.long - k.length) + line + } + return " ".repeat(widths.long + widths.short + 6) + line + }) + .join("\n") + + (typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : "") + ) + }) } export const parse = ( @@ -285,7 +334,46 @@ export const parse = ( args._.push(arg) } - logger.debug("parsed command line", field("args", args)) + // If a cert was provided a key must also be provided. + if (args.cert && args.cert.value && !args["cert-key"]) { + throw new Error("--cert-key is missing") + } + + logger.debug(() => ["parsed command line", field("args", { ...args, password: undefined })]) + + return args +} + +export interface DefaultedArgs extends ConfigArgs { + auth: AuthType + cert?: { + value: string + } + host: string + port: number + "proxy-domain": string[] + verbose: boolean + usingEnvPassword: boolean + "extensions-dir": string + "user-data-dir": string +} + +/** + * Take CLI and config arguments (optional) and return a single set of arguments + * with the defaults set. Arguments from the CLI are prioritized over config + * arguments. + */ +export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promise { + const args = Object.assign({}, configArgs || {}, cliArgs) + + if (!args["user-data-dir"]) { + await copyOldMacOSDataDir() + args["user-data-dir"] = paths.data + } + + if (!args["extensions-dir"]) { + args["extensions-dir"] = path.join(args["user-data-dir"], "extensions") + } // --verbose takes priority over --log and --log takes priority over the // environment variable. @@ -326,22 +414,49 @@ export const parse = ( break } - return args -} - -export async function setDefaults(args: Args): Promise { - args = { ...args } - - if (!args["user-data-dir"]) { - await copyOldMacOSDataDir() - args["user-data-dir"] = paths.data + // Default to using a password. + if (!args.auth) { + args.auth = AuthType.Password } - if (!args["extensions-dir"]) { - args["extensions-dir"] = path.join(args["user-data-dir"], "extensions") + const addr = bindAddrFromAllSources(configArgs || { _: [] }, cliArgs) + args.host = addr.host + args.port = addr.port + + // If we're being exposed to the cloud, we listen on a random address and + // disable auth. + if (args.link) { + args.host = "localhost" + args.port = 0 + args.socket = undefined + args.cert = undefined + args.auth = AuthType.None } - return args + if (args.cert && !args.cert.value) { + const { cert, certKey } = await generateCertificate(args["cert-host"] || "localhost") + args.cert = { + value: cert, + } + args["cert-key"] = certKey + } + + const usingEnvPassword = !!process.env.PASSWORD + if (process.env.PASSWORD) { + args.password = process.env.PASSWORD + } + + // Ensure it's not readable by child processes. + delete process.env.PASSWORD + + // Filter duplicate proxy domains and remove any leading `*.`. + const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, ""))) + args["proxy-domain"] = Array.from(proxyDomains) + + return { + ...args, + usingEnvPassword, + } as DefaultedArgs // TODO: Technically no guarantee this is fulfilled. } async function defaultConfigFile(): Promise { @@ -352,12 +467,16 @@ cert: false ` } +interface ConfigArgs extends Args { + config: string +} + /** * Reads the code-server yaml config file and returns it as Args. * * @param configPath Read the config from configPath instead of $CODE_SERVER_CONFIG or the default. */ -export async function readConfigFile(configPath?: string): Promise { +export async function readConfigFile(configPath?: string): Promise { if (!configPath) { configPath = process.env.CODE_SERVER_CONFIG if (!configPath) { @@ -370,10 +489,6 @@ export async function readConfigFile(configPath?: string): Promise { logger.info(`Wrote default config file to ${humanPath(configPath)}`) } - if (!process.env.CODE_SERVER_PARENT_PID && !process.env.VSCODE_IPC_HOOK_CLI) { - logger.info(`Using config file ${humanPath(configPath)}`) - } - const configFile = await fs.readFile(configPath) const config = yaml.safeLoad(configFile.toString(), { filename: configPath, @@ -399,9 +514,15 @@ export async function readConfigFile(configPath?: string): Promise { } } -function parseBindAddr(bindAddr: string): [string, number] { +function parseBindAddr(bindAddr: string): Addr { const u = new URL(`http://${bindAddr}`) - return [u.hostname, parseInt(u.port, 10)] + return { + host: u.hostname, + // With the http scheme 80 will be dropped so assume it's 80 if missing. + // This means --bind-addr without a port will default to 80 as well + // and not the code-server default. + port: u.port ? parseInt(u.port, 10) : 80, + } } interface Addr { @@ -412,7 +533,7 @@ interface Addr { function bindAddrFromArgs(addr: Addr, args: Args): Addr { addr = { ...addr } if (args["bind-addr"]) { - ;[addr.host, addr.port] = parseBindAddr(args["bind-addr"]) + addr = parseBindAddr(args["bind-addr"]) } if (args.host) { addr.host = args.host @@ -427,16 +548,17 @@ function bindAddrFromArgs(addr: Addr, args: Args): Addr { return addr } -export function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] { +function bindAddrFromAllSources(...argsConfig: Args[]): Addr { let addr: Addr = { host: "localhost", port: 8080, } - addr = bindAddrFromArgs(addr, configArgs) - addr = bindAddrFromArgs(addr, cliArgs) + for (const args of argsConfig) { + addr = bindAddrFromArgs(addr, args) + } - return [addr.host, addr.port] + return addr } async function copyOldMacOSDataDir(): Promise { @@ -453,3 +575,52 @@ async function copyOldMacOSDataDir(): Promise { await fs.copy(oldDataDir, paths.data) } } + +export const shouldRunVsCodeCli = (args: Args): boolean => { + return !!args["list-extensions"] || !!args["install-extension"] || !!args["uninstall-extension"] +} + +/** + * Determine if it looks like the user is trying to open a file or folder in an + * existing instance. The arguments here should be the arguments the user + * explicitly passed on the command line, not defaults or the configuration. + */ +export const shouldOpenInExistingInstance = async (args: Args): Promise => { + // Always use the existing instance if we're running from VS Code's terminal. + if (process.env.VSCODE_IPC_HOOK_CLI) { + return process.env.VSCODE_IPC_HOOK_CLI + } + + const readSocketPath = async (): Promise => { + try { + return await fs.readFile(path.join(os.tmpdir(), "vscode-ipc"), "utf8") + } catch (error) { + if (error.code !== "ENOENT") { + throw error + } + } + return undefined + } + + // If these flags are set then assume the user is trying to open in an + // existing instance since these flags have no effect otherwise. + const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => { + return args[cur as keyof Args] ? prev + 1 : prev + }, 0) + if (openInFlagCount > 0) { + return readSocketPath() + } + + // It's possible the user is trying to spawn another instance of code-server. + // Check if any unrelated flags are set (check against one because `_` always + // exists), that a file or directory was passed, and that the socket is + // active. + if (Object.keys(args).length === 1 && args._.length > 0) { + const socketPath = await readSocketPath() + if (socketPath && (await canConnect(socketPath))) { + return socketPath + } + } + + return undefined +} diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts new file mode 100644 index 000000000..1241bc90b --- /dev/null +++ b/src/node/coder-cloud.ts @@ -0,0 +1,43 @@ +import { logger } from "@coder/logger" +import { spawn } from "child_process" +import path from "path" +import split2 from "split2" + +// https://github.com/cdr/coder-cloud +const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent") + +function runAgent(...args: string[]): Promise { + logger.debug(`running agent with ${args}`) + + const agent = spawn(coderCloudAgent, args, { + stdio: ["inherit", "inherit", "pipe"], + }) + + agent.stderr.pipe(split2()).on("data", (line) => { + line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "") + logger.info(line) + }) + + return new Promise((res, rej) => { + agent.on("error", rej) + + agent.on("close", (code) => { + if (code !== 0) { + rej({ + message: `coder cloud agent exited with ${code}`, + }) + return + } + res() + }) + }) +} + +export function coderCloudBind(csAddr: string, serverName = ""): Promise { + logger.info("Remember --link is a beta feature and requires being accepted for testing") + logger.info("See https://github.com/cdr/code-server/discussions/2137") + // addr needs to be in host:port format. + // So we trim the protocol. + csAddr = csAddr.replace(/^https?:\/\//, "") + return runAgent("bind", `--code-server-addr=${csAddr}`, serverName) +} diff --git a/src/node/constants.ts b/src/node/constants.ts new file mode 100644 index 000000000..d6ba953ea --- /dev/null +++ b/src/node/constants.ts @@ -0,0 +1,13 @@ +import { logger } from "@coder/logger" +import * as path from "path" + +let pkg: { version?: string; commit?: string } = {} +try { + pkg = require("../../package.json") +} catch (error) { + logger.warn(error.message) +} + +export const version = pkg.version || "development" +export const commit = pkg.commit || "development" +export const rootPath = path.resolve(__dirname, "../..") diff --git a/src/node/entry.ts b/src/node/entry.ts index a416ae993..9aa69a65f 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -1,151 +1,175 @@ import { field, logger } from "@coder/logger" import * as cp from "child_process" -import { promises as fs } from "fs" import http from "http" import * as path from "path" import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc" import { plural } from "../common/util" -import { HealthHttpProvider } from "./app/health" -import { LoginHttpProvider } from "./app/login" -import { ProxyHttpProvider } from "./app/proxy" -import { StaticHttpProvider } from "./app/static" -import { UpdateHttpProvider } from "./app/update" -import { VscodeHttpProvider } from "./app/vscode" -import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli" -import { AuthType, HttpServer, HttpServerOptions } from "./http" -import { loadPlugins } from "./plugin" -import { generateCertificate, hash, humanPath, open } from "./util" -import { ipcMain, wrap } from "./wrapper" +import { createApp, ensureAddress } from "./app" +import { + AuthType, + DefaultedArgs, + optionDescriptions, + parse, + readConfigFile, + setDefaults, + shouldOpenInExistingInstance, + shouldRunVsCodeCli, +} from "./cli" +import { coderCloudBind } from "./coder-cloud" +import { commit, version } from "./constants" +import { register } from "./routes" +import { humanPath, isFile, open } from "./util" +import { isChild, wrapper } from "./wrapper" -process.on("uncaughtException", (error) => { - logger.error(`Uncaught exception: ${error.message}`) - if (typeof error.stack !== "undefined") { - logger.error(error.stack) - } -}) - -let pkg: { version?: string; commit?: string } = {} -try { - pkg = require("../../package.json") -} catch (error) { - logger.warn(error.message) +export const runVsCodeCli = (args: DefaultedArgs): void => { + logger.debug("forking vs code cli...") + const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], { + env: { + ...process.env, + CODE_SERVER_PARENT_PID: process.pid.toString(), + }, + }) + vscode.once("message", (message: any) => { + logger.debug("got message from VS Code", field("message", message)) + if (message.type !== "ready") { + logger.error("Unexpected response waiting for ready response", field("type", message.type)) + process.exit(1) + } + const send: CliMessage = { type: "cli", args } + vscode.send(send) + }) + vscode.once("error", (error) => { + logger.error("Got error from VS Code", field("error", error)) + process.exit(1) + }) + vscode.on("exit", (code) => process.exit(code || 0)) } -const version = pkg.version || "development" -const commit = pkg.commit || "development" +export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise => { + const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = { + type: "open", + folderURIs: [], + fileURIs: [], + forceReuseWindow: args["reuse-window"], + forceNewWindow: args["new-window"], + } -const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise => { - if (!args.auth) { - args = { - ...args, - auth: AuthType.Password, + for (let i = 0; i < args._.length; i++) { + const fp = path.resolve(args._[i]) + if (await isFile(fp)) { + pipeArgs.fileURIs.push(fp) + } else { + pipeArgs.folderURIs.push(fp) } } - logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`) + if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) { + logger.error("--new-window can only be used with folder paths") + process.exit(1) + } + if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) { + logger.error("Please specify at least one file or folder") + process.exit(1) + } + + const vscode = http.request( + { + path: "/", + method: "POST", + socketPath, + }, + (response) => { + response.on("data", (message) => { + logger.debug("got message from VS Code", field("message", message.toString())) + }) + }, + ) + vscode.on("error", (error: unknown) => { + logger.error("got error from VS Code", field("error", error)) + }) + vscode.write(JSON.stringify(pipeArgs)) + vscode.end() +} + +const main = async (args: DefaultedArgs): Promise => { + logger.info(`code-server ${version} ${commit}`) + + logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`) logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`) - const envPassword = !!process.env.PASSWORD - const password = args.auth === AuthType.Password && (process.env.PASSWORD || args.password) - if (args.auth === AuthType.Password && !password) { + if (args.auth === AuthType.Password && !args.password) { throw new Error("Please pass in a password via the config file or $PASSWORD") } - const [host, port] = bindAddrFromAllSources(cliArgs, configArgs) - // Spawn the main HTTP server. - const options: HttpServerOptions = { - auth: args.auth, - commit, - host: host, - // The hash does not add any actual security but we do it for obfuscation purposes. - password: password ? hash(password) : undefined, - port: port, - proxyDomains: args["proxy-domain"], - socket: args.socket, - ...(args.cert && !args.cert.value - ? await generateCertificate() - : { - cert: args.cert && args.cert.value, - certKey: args["cert-key"], - }), - } + const [app, wsApp, server] = await createApp(args) + const serverAddress = ensureAddress(server) + await register(app, wsApp, server, args) - if (options.cert && !options.certKey) { - throw new Error("--cert-key is missing") - } - - const httpServer = new HttpServer(options) - httpServer.registerHttpProvider(["/", "/vscode"], VscodeHttpProvider, args) - httpServer.registerHttpProvider("/update", UpdateHttpProvider, false) - httpServer.registerHttpProvider("/proxy", ProxyHttpProvider) - httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword) - httpServer.registerHttpProvider("/static", StaticHttpProvider) - httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart) - - await loadPlugins(httpServer, args) - - ipcMain().onDispose(() => { - httpServer.dispose().then((errors) => { - errors.forEach((error) => logger.error(error.message)) - }) - }) - - logger.info(`code-server ${version} ${commit}`) - const serverAddress = await httpServer.listen() - logger.info(`HTTP server listening on ${serverAddress}`) + logger.info(`Using config file ${humanPath(args.config)}`) + logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`) if (args.auth === AuthType.Password) { - if (envPassword) { + logger.info(" - Authentication is enabled") + if (args.usingEnvPassword) { logger.info(" - Using password from $PASSWORD") } else { logger.info(` - Using password from ${humanPath(args.config)}`) } - logger.info(" - To disable use `--auth none`") } else { - logger.info(" - No authentication") + logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`) } - delete process.env.PASSWORD - if (httpServer.protocol === "https") { - logger.info( - args.cert && args.cert.value - ? ` - Using provided certificate and key for HTTPS` - : ` - Using generated certificate and key for HTTPS`, - ) + if (args.cert) { + logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`) } else { logger.info(" - Not serving HTTPS") } - if (httpServer.proxyDomains.size > 0) { - logger.info(` - ${plural(httpServer.proxyDomains.size, "Proxying the following domain")}:`) - httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`)) + if (args["proxy-domain"].length > 0) { + logger.info(` - ${plural(args["proxy-domain"].length, "Proxying the following domain")}:`) + args["proxy-domain"].forEach((domain) => logger.info(` - *.${domain}`)) } - if (serverAddress && !options.socket && args.open) { + if (args.link) { + try { + await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value) + logger.info(" - Connected to cloud agent") + } catch (err) { + logger.error(err.message) + wrapper.exit(1) + } + } + + if (!args.socket && args.open) { // The web socket doesn't seem to work if browsing with 0.0.0.0. - const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost") - await open(openAddress).catch(console.error) - logger.info(`Opened ${openAddress}`) + const openAddress = serverAddress.replace("://0.0.0.0", "://localhost") + try { + await open(openAddress) + logger.info(`Opened ${openAddress}`) + } catch (error) { + logger.error("Failed to open", field("address", openAddress), field("error", error)) + } } } async function entry(): Promise { - const tryParse = async (): Promise<[Args, Args, Args]> => { - try { - const cliArgs = parse(process.argv.slice(2)) - const configArgs = await readConfigFile(cliArgs.config) - // This prioritizes the flags set in args over the ones in the config file. - let args = Object.assign(configArgs, cliArgs) - args = await setDefaults(args) - return [args, cliArgs, configArgs] - } catch (error) { - console.error(error.message) - process.exit(1) - } + // There's no need to check flags like --help or to spawn in an existing + // instance for the child process because these would have already happened in + // the parent and the child wouldn't have been spawned. We also get the + // arguments from the parent so we don't have to parse twice and to account + // for environment manipulation (like how PASSWORD gets removed to avoid + // leaking to child processes). + if (isChild(wrapper)) { + const args = await wrapper.handshake() + wrapper.preventExit() + return main(args) } - const [args, cliArgs, configArgs] = await tryParse() + const cliArgs = parse(process.argv.slice(2)) + const configArgs = await readConfigFile(cliArgs.config) + const args = await setDefaults(cliArgs, configArgs) + if (args.help) { console.log("code-server", version, commit) console.log("") @@ -155,7 +179,10 @@ async function entry(): Promise { optionDescriptions().forEach((description) => { console.log("", description) }) - } else if (args.version) { + return + } + + if (args.version) { if (args.json) { console.log({ codeServer: version, @@ -165,83 +192,22 @@ async function entry(): Promise { } else { console.log(version, commit) } - process.exit(0) - } else if (args["list-extensions"] || args["install-extension"] || args["uninstall-extension"]) { - logger.debug("forking vs code cli...") - const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], { - env: { - ...process.env, - CODE_SERVER_PARENT_PID: process.pid.toString(), - }, - }) - vscode.once("message", (message: any) => { - logger.debug("Got message from VS Code", field("message", message)) - if (message.type !== "ready") { - logger.error("Unexpected response waiting for ready response") - process.exit(1) - } - const send: CliMessage = { type: "cli", args } - vscode.send(send) - }) - vscode.once("error", (error) => { - logger.error(error.message) - process.exit(1) - }) - vscode.on("exit", (code) => process.exit(code || 0)) - } else if (process.env.VSCODE_IPC_HOOK_CLI) { - const pipeArgs: OpenCommandPipeArgs = { - type: "open", - folderURIs: [], - forceReuseWindow: args["reuse-window"], - forceNewWindow: args["new-window"], - } - const isDir = async (path: string): Promise => { - try { - const st = await fs.stat(path) - return st.isDirectory() - } catch (error) { - return false - } - } - for (let i = 0; i < args._.length; i++) { - const fp = path.resolve(args._[i]) - if (await isDir(fp)) { - pipeArgs.folderURIs.push(fp) - } else { - if (!pipeArgs.fileURIs) { - pipeArgs.fileURIs = [] - } - pipeArgs.fileURIs.push(fp) - } - } - if (pipeArgs.forceNewWindow && pipeArgs.fileURIs && pipeArgs.fileURIs.length > 0) { - logger.error("new-window can only be used with folder paths") - process.exit(1) - } - if (pipeArgs.folderURIs.length === 0 && (!pipeArgs.fileURIs || pipeArgs.fileURIs.length === 0)) { - logger.error("Please specify at least one file or folder argument") - process.exit(1) - } - const vscode = http.request( - { - path: "/", - method: "POST", - socketPath: process.env["VSCODE_IPC_HOOK_CLI"], - }, - (res) => { - res.on("data", (message) => { - logger.debug("Got message from VS Code", field("message", message.toString())) - }) - }, - ) - vscode.on("error", (err) => { - logger.debug("Got error from VS Code", field("error", err)) - }) - vscode.write(JSON.stringify(pipeArgs)) - vscode.end() - } else { - wrap(() => main(args, cliArgs, configArgs)) + return } + + if (shouldRunVsCodeCli(args)) { + return runVsCodeCli(args) + } + + const socketPath = await shouldOpenInExistingInstance(cliArgs) + if (socketPath) { + return openInExistingInstance(args, socketPath) + } + + return wrapper.start(args) } -entry() +entry().catch((error) => { + logger.error(error.message) + wrapper.exit(error) +}) diff --git a/src/node/heart.ts b/src/node/heart.ts new file mode 100644 index 000000000..eed070e4e --- /dev/null +++ b/src/node/heart.ts @@ -0,0 +1,48 @@ +import { logger } from "@coder/logger" +import { promises as fs } from "fs" + +/** + * Provides a heartbeat using a local file to indicate activity. + */ +export class Heart { + private heartbeatTimer?: NodeJS.Timeout + private heartbeatInterval = 60000 + public lastHeartbeat = 0 + + public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise) {} + + public alive(): boolean { + const now = Date.now() + return now - this.lastHeartbeat < this.heartbeatInterval + } + /** + * Write to the heartbeat file if we haven't already done so within the + * timeout and start or reset a timer that keeps running as long as there is + * activity. Failures are logged as warnings. + */ + public beat(): void { + if (this.alive()) { + return + } + + logger.trace("heartbeat") + fs.writeFile(this.heartbeatPath, "").catch((error) => { + logger.warn(error.message) + }) + this.lastHeartbeat = Date.now() + if (typeof this.heartbeatTimer !== "undefined") { + clearTimeout(this.heartbeatTimer) + } + this.heartbeatTimer = setTimeout(() => { + this.isActive() + .then((active) => { + if (active) { + this.beat() + } + }) + .catch((error) => { + logger.warn(error.message) + }) + }, this.heartbeatInterval) + } +} diff --git a/src/node/http.ts b/src/node/http.ts index a8abb94b0..1aa7adb51 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -1,928 +1,144 @@ import { field, logger } from "@coder/logger" -import * as fs from "fs-extra" -import * as http from "http" -import proxy from "http-proxy" -import * as httpolyglot from "httpolyglot" -import * as https from "https" -import * as net from "net" -import * as path from "path" -import * as querystring from "querystring" +import * as express from "express" +import * as expressCore from "express-serve-static-core" +import qs from "qs" import safeCompare from "safe-compare" -import { Readable } from "stream" -import * as tls from "tls" -import * as url from "url" import { HttpCode, HttpError } from "../common/http" -import { arrayify, normalize, Options, plural, split, trimSlashes } from "../common/util" -import { SocketProxyProvider } from "./socket" -import { getMediaMime, paths } from "./util" +import { normalize, Options } from "../common/util" +import { AuthType } from "./cli" +import { commit, rootPath } from "./constants" +import { hash } from "./util" -export type Cookies = { [key: string]: string[] | undefined } -export type PostData = { [key: string]: string | string[] | undefined } - -interface ProxyRequest extends http.IncomingMessage { - base?: string -} - -interface AuthPayload extends Cookies { - key?: string[] -} - -export enum AuthType { - Password = "password", - None = "none", -} - -export type Query = { [key: string]: string | string[] | undefined } - -export interface ProxyOptions { - /** - * A path to strip from from the beginning of the request before proxying - */ - strip?: string - /** - * A path to add to the beginning of the request before proxying. - */ - prepend?: string - /** - * The port to proxy. - */ - port: string -} - -export interface HttpResponse { - /* - * Whether to set cache-control headers for this response. - */ - cache?: boolean - /** - * If the code cannot be determined automatically set it here. The - * defaults are 302 for redirects and 200 for successful requests. For errors - * you should throw an HttpError and include the code there. If you - * use Error it will default to 404 for ENOENT and EISDIR and 500 otherwise. - */ - code?: number - /** - * Content to write in the response. Mutually exclusive with stream. - */ - content?: T - /** - * Cookie to write with the response. - * NOTE: Cookie paths must be absolute. The default is /. - */ - cookie?: { key: string; value: string; path?: string } - /** - * Used to automatically determine the appropriate mime type. - */ - filePath?: string - /** - * Additional headers to include. - */ - headers?: http.OutgoingHttpHeaders - /** - * If the mime type cannot be determined automatically set it here. - */ - mime?: string - /** - * Redirect to this path. This is constructed against the site base (not the - * provider's base). - */ - redirect?: string - /** - * Stream this to the response. Mutually exclusive with content. - */ - stream?: Readable - /** - * Query variables to add in addition to current ones when redirecting. Use - * `undefined` to remove a query variable. - */ - query?: Query - /** - * Indicates the request should be proxied. - */ - proxy?: ProxyOptions -} - -export interface WsResponse { - /** - * Indicates the web socket should be proxied. - */ - proxy?: ProxyOptions +/** + * Replace common variable strings in HTML templates. + */ +export const replaceTemplates = ( + req: express.Request, + content: string, + extraOpts?: Omit, +): string => { + const base = relativeRoot(req) + const options: Options = { + base, + csStaticBase: base + "/static/" + commit + rootPath, + logLevel: logger.level, + ...extraOpts, + } + return content + .replace(/{{TO}}/g, (typeof req.query.to === "string" && req.query.to) || "/") + .replace(/{{BASE}}/g, options.base) + .replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase) + .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`) } /** - * Use when you need to run search and replace on a file's content before - * sending it. + * Throw an error if not authorized. Call `next` if provided. */ -export interface HttpStringFileResponse extends HttpResponse { - content: string - filePath: string -} - -export interface RedirectResponse extends HttpResponse { - redirect: string -} - -export interface HttpServerOptions { - readonly auth?: AuthType - readonly cert?: string - readonly certKey?: string - readonly commit?: string - readonly host?: string - readonly password?: string - readonly port?: number - readonly proxyDomains?: string[] - readonly socket?: string -} - -export interface Route { - /** - * Provider base path part (for /provider/base/path it would be /provider). - */ - providerBase: string - /** - * Base path part (for /provider/base/path it would be /base). - */ - base: string - /** - * Remaining part of the route after factoring out the base and provider base - * (for /provider/base/path it would be /path). It can be blank. - */ - requestPath: string - /** - * Query variables included in the request. - */ - query: querystring.ParsedUrlQuery - /** - * Normalized version of `originalPath`. - */ - fullPath: string - /** - * Original path of the request without any modifications. - */ - originalPath: string -} - -interface ProviderRoute extends Route { - provider: HttpProvider -} - -export interface HttpProviderOptions { - readonly auth: AuthType - readonly commit: string - readonly password?: string -} - -/** - * Provides HTTP responses. This abstract class provides some helpers for - * interpreting, creating, and authenticating responses. - */ -export abstract class HttpProvider { - protected readonly rootPath = path.resolve(__dirname, "../..") - - public constructor(protected readonly options: HttpProviderOptions) {} - - public async dispose(): Promise { - // No default behavior. +export const ensureAuthenticated = (req: express.Request, _?: express.Response, next?: express.NextFunction): void => { + if (!authenticated(req)) { + throw new HttpError("Unauthorized", HttpCode.Unauthorized) } - - /** - * Handle web sockets on the registered endpoint. Normally the provider - * handles the request itself but it can return a response when necessary. The - * default is to throw a 404. - */ - public handleWebSocket( - /* eslint-disable @typescript-eslint/no-unused-vars */ - _route: Route, - _request: http.IncomingMessage, - _socket: net.Socket, - _head: Buffer, - /* eslint-enable @typescript-eslint/no-unused-vars */ - ): Promise { - throw new HttpError("Not found", HttpCode.NotFound) - } - - /** - * Handle requests to the registered endpoint. - */ - public abstract handleRequest(route: Route, request: http.IncomingMessage): Promise - - /** - * Get the base relative to the provided route. For each slash we need to go - * up a directory. For example: - * / => . - * /foo => . - * /foo/ => ./.. - * /foo/bar => ./.. - * /foo/bar/ => ./../.. - */ - public base(route: Route): string { - const depth = (route.originalPath.match(/\//g) || []).length - return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : "")) - } - - /** - * Get error response. - */ - public async getErrorRoot(route: Route, title: string, header: string, body: string): Promise { - const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/error.html") - response.content = response.content - .replace(/{{ERROR_TITLE}}/g, title) - .replace(/{{ERROR_HEADER}}/g, header) - .replace(/{{ERROR_BODY}}/g, body) - return this.replaceTemplates(route, response) - } - - /** - * Replace common templates strings. - */ - protected replaceTemplates( - route: Route, - response: HttpStringFileResponse, - extraOptions?: Omit, - ): HttpStringFileResponse { - const base = this.base(route) - const options: Options = { - base, - csStaticBase: base + "/static/" + this.options.commit + this.rootPath, - logLevel: logger.level, - ...extraOptions, - } - response.content = response.content - .replace(/{{TO}}/g, Array.isArray(route.query.to) ? route.query.to[0] : route.query.to || "/dashboard") - .replace(/{{BASE}}/g, options.base) - .replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase) - .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`) - return response - } - - protected get isDev(): boolean { - return this.options.commit === "development" - } - - /** - * Get a file resource. - * TODO: Would a stream be faster, at least for large files? - */ - protected async getResource(...parts: string[]): Promise { - const filePath = path.join(...parts) - return { content: await fs.readFile(filePath), filePath } - } - - /** - * Get a file resource as a string. - */ - protected async getUtf8Resource(...parts: string[]): Promise { - const filePath = path.join(...parts) - return { content: await fs.readFile(filePath, "utf8"), filePath } - } - - /** - * Helper to error on invalid methods (default GET). - */ - protected ensureMethod(request: http.IncomingMessage, method?: string | string[]): void { - const check = arrayify(method || "GET") - if (!request.method || !check.includes(request.method)) { - throw new HttpError(`Unsupported method ${request.method}`, HttpCode.BadRequest) - } - } - - /** - * Helper to error if not authorized. - */ - public ensureAuthenticated(request: http.IncomingMessage): void { - if (!this.authenticated(request)) { - throw new HttpError("Unauthorized", HttpCode.Unauthorized) - } - } - - /** - * Use the first query value or the default if there isn't one. - */ - protected queryOrDefault(value: string | string[] | undefined, def: string): string { - if (Array.isArray(value)) { - value = value[0] - } - return typeof value !== "undefined" ? value : def - } - - /** - * Return the provided password value if the payload contains the right - * password otherwise return false. If no payload is specified use cookies. - */ - public authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean { - switch (this.options.auth) { - case AuthType.None: - return true - case AuthType.Password: - if (typeof payload === "undefined") { - payload = this.parseCookies(request) - } - if (this.options.password && payload.key) { - for (let i = 0; i < payload.key.length; ++i) { - if (safeCompare(payload.key[i], this.options.password)) { - return payload.key[i] - } - } - } - return false - default: - throw new Error(`Unsupported auth type ${this.options.auth}`) - } - } - - /** - * Parse POST data. - */ - protected getData(request: http.IncomingMessage): Promise { - return request.method === "POST" || request.method === "DELETE" - ? new Promise((resolve, reject) => { - let body = "" - const onEnd = (): void => { - off() // eslint-disable-line @typescript-eslint/no-use-before-define - resolve(body || undefined) - } - const onError = (error: Error): void => { - off() // eslint-disable-line @typescript-eslint/no-use-before-define - reject(error) - } - const onData = (d: Buffer): void => { - body += d - if (body.length > 1e6) { - onError(new HttpError("Payload is too large", HttpCode.LargePayload)) - request.connection.destroy() - } - } - const off = (): void => { - request.off("error", onError) - request.off("data", onError) - request.off("end", onEnd) - } - request.on("error", onError) - request.on("data", onData) - request.on("end", onEnd) - }) - : Promise.resolve(undefined) - } - - /** - * Parse cookies. - */ - protected parseCookies(request: http.IncomingMessage): T { - const cookies: { [key: string]: string[] } = {} - if (request.headers.cookie) { - request.headers.cookie.split(";").forEach((keyValue) => { - const [key, value] = split(keyValue, "=") - if (!cookies[key]) { - cookies[key] = [] - } - cookies[key].push(decodeURI(value)) - }) - } - return cookies as T - } - - /** - * Return true if the route is for the root page. For example /base, /base/, - * or /base/index.html but not /base/path or /base/file.js. - */ - protected isRoot(route: Route): boolean { - return !route.requestPath || route.requestPath === "/index.html" + if (next) { + next() } } /** - * Provides a heartbeat using a local file to indicate activity. + * Return true if authenticated via cookies. */ -export class Heart { - private heartbeatTimer?: NodeJS.Timeout - private heartbeatInterval = 60000 - public lastHeartbeat = 0 - - public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise) {} - - public alive(): boolean { - const now = Date.now() - return now - this.lastHeartbeat < this.heartbeatInterval +export const authenticated = (req: express.Request): boolean => { + switch (req.args.auth) { + case AuthType.None: + return true + case AuthType.Password: + // The password is stored in the cookie after being hashed. + return req.args.password && req.cookies.key && safeCompare(req.cookies.key, hash(req.args.password)) + default: + throw new Error(`Unsupported auth type ${req.args.auth}`) } - /** - * Write to the heartbeat file if we haven't already done so within the - * timeout and start or reset a timer that keeps running as long as there is - * activity. Failures are logged as warnings. - */ - public beat(): void { - if (!this.alive()) { - logger.trace("heartbeat") - fs.outputFile(this.heartbeatPath, "").catch((error) => { - logger.warn(error.message) - }) - this.lastHeartbeat = Date.now() - if (typeof this.heartbeatTimer !== "undefined") { - clearTimeout(this.heartbeatTimer) - } - this.heartbeatTimer = setTimeout(() => { - this.isActive() - .then((active) => { - if (active) { - this.beat() - } - }) - .catch((error) => { - logger.warn(error.message) - }) - }, this.heartbeatInterval) - } - } -} - -export interface HttpProvider0 { - new (options: HttpProviderOptions): T -} - -export interface HttpProvider1 { - new (options: HttpProviderOptions, a1: A1): T -} - -export interface HttpProvider2 { - new (options: HttpProviderOptions, a1: A1, a2: A2): T -} - -export interface HttpProvider3 { - new (options: HttpProviderOptions, a1: A1, a2: A2, a3: A3): T } /** - * An HTTP server. Its main role is to route incoming HTTP requests to the - * appropriate provider for that endpoint then write out the response. It also - * covers some common use cases like redirects and caching. + * Get the relative path that will get us to the root of the page. For each + * slash we need to go up a directory. For example: + * / => . + * /foo => . + * /foo/ => ./.. + * /foo/bar => ./.. + * /foo/bar/ => ./../.. */ -export class HttpServer { - protected readonly server: http.Server | https.Server - private listenPromise: Promise | undefined - public readonly protocol: "http" | "https" - private readonly providers = new Map() - public readonly heart: Heart - private readonly socketProvider = new SocketProxyProvider() - - /** - * Proxy domains are stored here without the leading `*.` - */ - public readonly proxyDomains: Set - - /** - * Provides the actual proxying functionality. - */ - private readonly proxy = proxy.createProxyServer({}) - - public constructor(private readonly options: HttpServerOptions) { - this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, ""))) - this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => { - const connections = await this.getConnections() - logger.trace(plural(connections, `${connections} active connection`)) - return connections !== 0 - }) - this.protocol = this.options.cert ? "https" : "http" - if (this.protocol === "https") { - this.server = httpolyglot.createServer( - { - cert: this.options.cert && fs.readFileSync(this.options.cert), - key: this.options.certKey && fs.readFileSync(this.options.certKey), - }, - this.onRequest, - ) - } else { - this.server = http.createServer(this.onRequest) - } - this.proxy.on("error", (error, _request, response) => { - response.writeHead(HttpCode.ServerError) - response.end(error.message) - }) - // Intercept the response to rewrite absolute redirects against the base path. - this.proxy.on("proxyRes", (response, request: ProxyRequest) => { - if (response.headers.location && response.headers.location.startsWith("/") && request.base) { - response.headers.location = request.base + response.headers.location - } - }) - } - - /** - * Stop and dispose everything. Return an array of disposal errors. - */ - public async dispose(): Promise { - this.socketProvider.stop() - const providers = Array.from(this.providers.values()) - // Catch so all the errors can be seen rather than just the first one. - const responses = await Promise.all(providers.map((p) => p.dispose().catch((e) => e))) - return responses.filter((r): r is Error => typeof r !== "undefined") - } - - public async getConnections(): Promise { - return new Promise((resolve, reject) => { - this.server.getConnections((error, count) => { - return error ? reject(error) : resolve(count) - }) - }) - } - - /** - * Register a provider for a top-level endpoint. - */ - public registerHttpProvider(endpoint: string | string[], provider: HttpProvider0): T - public registerHttpProvider( - endpoint: string | string[], - provider: HttpProvider1, - a1: A1, - ): T - public registerHttpProvider( - endpoint: string | string[], - provider: HttpProvider2, - a1: A1, - a2: A2, - ): T - public registerHttpProvider( - endpoint: string | string[], - provider: HttpProvider3, - a1: A1, - a2: A2, - a3: A3, - ): T - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public registerHttpProvider(endpoint: string | string[], provider: any, ...args: any[]): void { - const p = new provider( - { - auth: this.options.auth || AuthType.None, - commit: this.options.commit, - password: this.options.password, - }, - ...args, - ) - const endpoints = arrayify(endpoint).map(trimSlashes) - endpoints.forEach((endpoint) => { - if (/\//.test(endpoint)) { - throw new Error(`Only top-level endpoints are supported (got ${endpoint})`) - } - const existingProvider = this.providers.get(`/${endpoint}`) - this.providers.set(`/${endpoint}`, p) - if (existingProvider) { - logger.debug(`Overridding existing /${endpoint} provider`) - // If the existing provider isn't registered elsewhere we can dispose. - if (!Array.from(this.providers.values()).find((p) => p === existingProvider)) { - logger.debug(`Disposing existing /${endpoint} provider`) - existingProvider.dispose() - } - } - }) - } - - /** - * Start listening on the specified port. - */ - public listen(): Promise { - if (!this.listenPromise) { - this.listenPromise = new Promise((resolve, reject) => { - this.server.on("error", reject) - this.server.on("upgrade", this.onUpgrade) - const onListen = (): void => resolve(this.address()) - if (this.options.socket) { - this.server.listen(this.options.socket, onListen) - } else { - this.server.listen(this.options.port, this.options.host, onListen) - } - }) - } - return this.listenPromise - } - - /** - * The *local* address of the server. - */ - public address(): string | null { - const address = this.server.address() - const endpoint = - typeof address !== "string" && address !== null - ? (address.address === "::" ? "localhost" : address.address) + ":" + address.port - : address - return endpoint && `${this.protocol}://${endpoint}` - } - - private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise => { - const route = this.parseUrl(request) - if (route.providerBase !== "/healthz") { - this.heart.beat() - } - const write = (payload: HttpResponse): void => { - response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, { - "Content-Type": payload.mime || getMediaMime(payload.filePath), - ...(payload.redirect ? { Location: this.constructRedirect(request, route, payload as RedirectResponse) } : {}), - ...(request.headers["service-worker"] ? { "Service-Worker-Allowed": route.provider.base(route) } : {}), - ...(payload.cache ? { "Cache-Control": "public, max-age=31536000" } : {}), - ...(payload.cookie - ? { - "Set-Cookie": [ - `${payload.cookie.key}=${payload.cookie.value}`, - `Path=${normalize(payload.cookie.path || "/", true)}`, - this.getCookieDomain(request.headers.host || ""), - // "HttpOnly", - "SameSite=lax", - ] - .filter((l) => !!l) - .join(";"), - } - : {}), - ...payload.headers, - }) - if (payload.stream) { - payload.stream.on("error", (error: NodeJS.ErrnoException) => { - response.writeHead(error.code === "ENOENT" ? HttpCode.NotFound : HttpCode.ServerError) - response.end(error.message) - }) - payload.stream.on("close", () => response.end()) - payload.stream.pipe(response) - } else if (typeof payload.content === "string" || payload.content instanceof Buffer) { - response.end(payload.content) - } else if (payload.content && typeof payload.content === "object") { - response.end(JSON.stringify(payload.content)) - } else { - response.end() - } - } - - try { - const payload = (await this.handleRequest(route, request)) || (await route.provider.handleRequest(route, request)) - if (payload.proxy) { - this.doProxy(route, request, response, payload.proxy) - } else { - write(payload) - } - } catch (error) { - let e = error - if (error.code === "ENOENT" || error.code === "EISDIR") { - e = new HttpError("Not found", HttpCode.NotFound) - } - const code = typeof e.code === "number" ? e.code : HttpCode.ServerError - logger.debug("Request error", field("url", request.url), field("code", code), field("error", error)) - if (code >= HttpCode.ServerError) { - logger.error(error.stack) - } - if (request.headers["content-type"] === "application/json") { - write({ - code, - mime: "application/json", - content: { - error: e.message, - ...(e.details || {}), - }, - }) - } else { - write({ - code, - ...(await route.provider.getErrorRoot(route, code, code, e.message)), - }) - } - } - } - - /** - * Handle requests that are always in effect no matter what provider is - * registered at the route. - */ - private async handleRequest(route: ProviderRoute, request: http.IncomingMessage): Promise { - // If we're handling TLS ensure all requests are redirected to HTTPS. - if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) { - return { redirect: route.fullPath } - } - - // Return robots.txt. - if (route.fullPath === "/robots.txt") { - const filePath = path.resolve(__dirname, "../../src/browser/robots.txt") - return { content: await fs.readFile(filePath), filePath } - } - - // Handle proxy domains. - return this.maybeProxy(route, request) - } - - /** - * Given a path that goes from the base, construct a relative redirect URL - * that will get you there considering that the app may be served from an - * unknown base path. If handling TLS, also ensure HTTPS. - */ - private constructRedirect(request: http.IncomingMessage, route: ProviderRoute, payload: RedirectResponse): string { - const query = { - ...route.query, - ...(payload.query || {}), - } - - Object.keys(query).forEach((key) => { - if (typeof query[key] === "undefined") { - delete query[key] - } - }) - - const secure = (request.connection as tls.TLSSocket).encrypted - const redirect = - (this.options.cert && !secure ? `${this.protocol}://${request.headers.host}/` : "") + - normalize(`${route.provider.base(route)}/${payload.redirect}`, true) + - (Object.keys(query).length > 0 ? `?${querystring.stringify(query)}` : "") - logger.debug("redirecting", field("secure", !!secure), field("from", request.url), field("to", redirect)) - return redirect - } - - private onUpgrade = async (request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise => { - try { - this.heart.beat() - socket.on("error", () => socket.destroy()) - - if (this.options.cert && !(socket as tls.TLSSocket).encrypted) { - throw new HttpError("HTTP websocket", HttpCode.BadRequest) - } - - if (!request.headers.upgrade || request.headers.upgrade.toLowerCase() !== "websocket") { - throw new HttpError("HTTP/1.1 400 Bad Request", HttpCode.BadRequest) - } - - const route = this.parseUrl(request) - if (!route.provider) { - throw new HttpError("Not found", HttpCode.NotFound) - } - - // The socket proxy is so we can pass them to child processes (TLS sockets - // can't be transferred so we need an in-between). - const socketProxy = await this.socketProvider.createProxy(socket) - const payload = - this.maybeProxy(route, request) || (await route.provider.handleWebSocket(route, request, socketProxy, head)) - if (payload && payload.proxy) { - this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy) - } - } catch (error) { - socket.destroy(error) - logger.warn(`discarding socket connection: ${error.message}`) - } - } - - /** - * Parse a request URL so we can route it. - */ - private parseUrl(request: http.IncomingMessage): ProviderRoute { - const parse = (fullPath: string): { base: string; requestPath: string } => { - const match = fullPath.match(/^(\/?[^/]*)(.*)$/) - let [, /* ignore */ base, requestPath] = match ? match.map((p) => p.replace(/\/+$/, "")) : ["", "", ""] - if (base.indexOf(".") !== -1) { - // Assume it's a file at the root. - requestPath = base - base = "/" - } else if (base === "") { - // Happens if it's a plain `domain.com`. - base = "/" - } - return { base, requestPath } - } - - const parsedUrl = request.url ? url.parse(request.url, true) : { query: {}, pathname: "" } - const originalPath = parsedUrl.pathname || "/" - const fullPath = normalize(originalPath, true) - const { base, requestPath } = parse(fullPath) - - // Providers match on the path after their base so we need to account for - // that by shifting the next base out of the request path. - let provider = this.providers.get(base) - if (base !== "/" && provider) { - return { ...parse(requestPath), providerBase: base, fullPath, query: parsedUrl.query, provider, originalPath } - } - - // Fall back to the top-level provider. - provider = this.providers.get("/") - if (!provider) { - throw new Error(`No provider for ${base}`) - } - return { base, providerBase: "/", fullPath, requestPath, query: parsedUrl.query, provider, originalPath } - } - - /** - * Proxy a request to the target. - */ - private doProxy( - route: Route, - request: http.IncomingMessage, - response: http.ServerResponse, - options: ProxyOptions, - ): void - /** - * Proxy a web socket to the target. - */ - private doProxy( - route: Route, - request: http.IncomingMessage, - response: { socket: net.Socket; head: Buffer }, - options: ProxyOptions, - ): void - /** - * Proxy a request or web socket to the target. - */ - private doProxy( - route: Route, - request: http.IncomingMessage, - response: http.ServerResponse | { socket: net.Socket; head: Buffer }, - options: ProxyOptions, - ): void { - const port = parseInt(options.port, 10) - if (isNaN(port)) { - throw new HttpError(`"${options.port}" is not a valid number`, HttpCode.BadRequest) - } - - // REVIEW: Absolute redirects need to be based on the subpath but I'm not - // sure how best to get this information to the `proxyRes` event handler. - // For now I'm sticking it on the request object which is passed through to - // the event. - ;(request as ProxyRequest).base = options.strip - - const isHttp = response instanceof http.ServerResponse - const base = options.strip ? route.fullPath.replace(options.strip, "") : route.fullPath - const path = normalize("/" + (options.prepend || "") + "/" + base, true) - const proxyOptions: proxy.ServerOptions = { - changeOrigin: true, - ignorePath: true, - target: `${isHttp ? "http" : "ws"}://127.0.0.1:${port}${path}${ - Object.keys(route.query).length > 0 ? `?${querystring.stringify(route.query)}` : "" - }`, - ws: !isHttp, - } - - if (response instanceof http.ServerResponse) { - this.proxy.web(request, response, proxyOptions) - } else { - this.proxy.ws(request, response.socket, response.head, proxyOptions) - } - } - - /** - * Get the value that should be used for setting a cookie domain. This will - * allow the user to authenticate only once. This will use the highest level - * domain (e.g. `coder.com` over `test.coder.com` if both are specified). - */ - private getCookieDomain(host: string): string | undefined { - const idx = host.lastIndexOf(":") - host = idx !== -1 ? host.substring(0, idx) : host - if ( - // Might be blank/missing, so there's nothing more to do. - !host || - // IP addresses can't have subdomains so there's no value in setting the - // domain for them. Assume anything with a : is ipv6 (valid domain name - // characters are alphanumeric or dashes). - host.includes(":") || - // Assume anything entirely numbers and dots is ipv4 (currently tlds - // cannot be entirely numbers). - !/[^0-9.]/.test(host) || - // localhost subdomains don't seem to work at all (browser bug?). - host.endsWith(".localhost") || - // It might be localhost (or an IP, see above) if it's a proxy and it - // isn't setting the host header to match the access domain. - host === "localhost" - ) { - logger.debug("no valid cookie doman", field("host", host)) - return undefined - } - - this.proxyDomains.forEach((domain) => { - if (host.endsWith(domain) && domain.length < host.length) { - host = domain - } - }) - - logger.debug("got cookie doman", field("host", host)) - return host ? `Domain=${host}` : undefined - } - - /** - * Return a response if the request should be proxied. Anything that ends in a - * proxy domain and has a *single* subdomain should be proxied. Anything else - * should return `undefined` and will be handled as normal. - * - * For example if `coder.com` is specified `8080.coder.com` will be proxied - * but `8080.test.coder.com` and `test.8080.coder.com` will not. - * - * Throw an error if proxying but the user isn't authenticated. - */ - public maybeProxy(route: ProviderRoute, request: http.IncomingMessage): HttpResponse | undefined { - // Split into parts. - const host = request.headers.host || "" - const idx = host.indexOf(":") - const domain = idx !== -1 ? host.substring(0, idx) : host - const parts = domain.split(".") - - // There must be an exact match. - const port = parts.shift() - const proxyDomain = parts.join(".") - if (!port || !this.proxyDomains.has(proxyDomain)) { - return undefined - } - - // Must be authenticated to use the proxy. - route.provider.ensureAuthenticated(request) - - return { - proxy: { - port, - }, - } - } +export const relativeRoot = (req: express.Request): string => { + const depth = (req.originalUrl.split("?", 1)[0].match(/\//g) || []).length + return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : "")) +} + +/** + * Redirect relatively to `/${to}`. Query variables will be preserved. + * `override` will merge with the existing query (use `undefined` to unset). + */ +export const redirect = ( + req: express.Request, + res: express.Response, + to: string, + override: expressCore.Query = {}, +): void => { + const query = Object.assign({}, req.query, override) + Object.keys(override).forEach((key) => { + if (typeof override[key] === "undefined") { + delete query[key] + } + }) + + const relativePath = normalize(`${relativeRoot(req)}/${to}`, true) + const queryString = qs.stringify(query) + const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}` + logger.debug(`redirecting from ${req.originalUrl} to ${redirectPath}`) + res.redirect(redirectPath) +} + +/** + * Get the value that should be used for setting a cookie domain. This will + * allow the user to authenticate once no matter what sub-domain they use to log + * in. This will use the highest level proxy domain (e.g. `coder.com` over + * `test.coder.com` if both are specified). + */ +export const getCookieDomain = (host: string, proxyDomains: string[]): string | undefined => { + const idx = host.lastIndexOf(":") + host = idx !== -1 ? host.substring(0, idx) : host + // If any of these are true we will still set cookies but without an explicit + // `Domain` attribute on the cookie. + if ( + // The host can be be blank or missing so there's nothing we can set. + !host || + // IP addresses can't have subdomains so there's no value in setting the + // domain for them. Assume that anything with a : is ipv6 (valid domain name + // characters are alphanumeric or dashes)... + host.includes(":") || + // ...and that anything entirely numbers and dots is ipv4 (currently tlds + // cannot be entirely numbers). + !/[^0-9.]/.test(host) || + // localhost subdomains don't seem to work at all (browser bug?). A cookie + // set at dev.localhost cannot be read by 8080.dev.localhost. + host.endsWith(".localhost") || + // Domains without at least one dot (technically two since domain.tld will + // become .domain.tld) are considered invalid according to the spec so don't + // set the domain for them. In my testing though localhost is the only + // problem (the browser just doesn't store the cookie at all). localhost has + // an additional problem which is that a reverse proxy might give + // code-server localhost even though the domain is really domain.tld (by + // default NGINX does this). + !host.includes(".") + ) { + logger.debug("no valid cookie doman", field("host", host)) + return undefined + } + + proxyDomains.forEach((domain) => { + if (host.endsWith(domain) && domain.length < host.length) { + host = domain + } + }) + + logger.debug("got cookie doman", field("host", host)) + return host || undefined } diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 54f7f2b76..2c0519ac1 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -1,60 +1,249 @@ -import { field, logger } from "@coder/logger" +import { Logger, field } from "@coder/logger" +import * as express from "express" import * as fs from "fs" import * as path from "path" -import * as util from "util" -import { Args } from "./cli" -import { HttpServer } from "./http" +import * as semver from "semver" +import * as pluginapi from "../../typings/pluginapi" +import { version } from "./constants" +import * as util from "./util" +const fsp = fs.promises -/* eslint-disable @typescript-eslint/no-var-requires */ +interface Plugin extends pluginapi.Plugin { + /** + * These fields are populated from the plugin's package.json + * and now guaranteed to exist. + */ + name: string + version: string -export type Activate = (httpServer: HttpServer, args: Args) => void + /** + * path to the node module on the disk. + */ + modulePath: string +} -export interface Plugin { - activate: Activate +interface Application extends pluginapi.Application { + /* + * Clone of the above without functions. + */ + plugin: Omit } /** - * Intercept imports so we can inject code-server when the plugin tries to - * import it. + * PluginAPI implements the plugin API described in typings/pluginapi.d.ts + * Please see that file for details. */ -const originalLoad = require("module")._load -// eslint-disable-next-line @typescript-eslint/no-explicit-any -require("module")._load = function (request: string, parent: object, isMain: boolean): any { - return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain]) -} +export class PluginAPI { + private readonly plugins = new Map() + private readonly logger: Logger -const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise => { - try { - const plugin: Plugin = require(pluginPath) - plugin.activate(httpServer, args) - logger.debug("Loaded plugin", field("name", path.basename(pluginPath))) - } catch (error) { - if (error.code !== "MODULE_NOT_FOUND") { - logger.warn(error.message) - } else { - logger.error(error.message) - } + public constructor( + logger: Logger, + /** + * These correspond to $CS_PLUGIN_PATH and $CS_PLUGIN respectively. + */ + private readonly csPlugin = "", + private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`, + ) { + this.logger = logger.named("pluginapi") } -} -const _loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { - const pluginPath = path.resolve(__dirname, "../../plugins") - const files = await util.promisify(fs.readdir)(pluginPath, { - withFileTypes: true, - }) - await Promise.all(files.map((file) => loadPlugin(path.join(pluginPath, file.name), httpServer, args))) -} + /** + * applications grabs the full list of applications from + * all loaded plugins. + */ + public async applications(): Promise { + const apps = new Array() + for (const [, p] of this.plugins) { + if (!p.applications) { + continue + } + const pluginApps = await p.applications() -export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { - try { - await _loadPlugins(httpServer, args) - } catch (error) { - if (error.code !== "ENOENT") { - logger.warn(error.message) + // Add plugin key to each app. + apps.push( + ...pluginApps.map((app) => { + app = { ...app, path: path.join(p.routerPath, app.path || "") } + app = { ...app, iconPath: path.join(app.path || "", app.iconPath) } + return { + ...app, + plugin: { + name: p.name, + version: p.version, + modulePath: p.modulePath, + + displayName: p.displayName, + description: p.description, + routerPath: p.routerPath, + homepageURL: p.homepageURL, + }, + } + }), + ) + } + return apps + } + + /** + * mount mounts all plugin routers onto r. + */ + public mount(r: express.Router): void { + for (const [, p] of this.plugins) { + if (!p.router) { + continue + } + r.use(`${p.routerPath}`, p.router()) } } - if (process.env.PLUGIN_DIR) { - await loadPlugin(process.env.PLUGIN_DIR, httpServer, args) + /** + * loadPlugins loads all plugins based on this.csPlugin, + * this.csPluginPath and the built in plugins. + */ + public async loadPlugins(): Promise { + for (const dir of this.csPlugin.split(":")) { + if (!dir) { + continue + } + await this.loadPlugin(dir) + } + + for (const dir of this.csPluginPath.split(":")) { + if (!dir) { + continue + } + await this._loadPlugins(dir) + } + + // Built-in plugins. + await this._loadPlugins(path.join(__dirname, "../../plugins")) + } + + /** + * _loadPlugins is the counterpart to loadPlugins. + * + * It differs in that it loads all plugins in a single + * directory whereas loadPlugins uses all available directories + * as documented. + */ + private async _loadPlugins(dir: string): Promise { + try { + const entries = await fsp.readdir(dir, { withFileTypes: true }) + for (const ent of entries) { + if (!ent.isDirectory()) { + continue + } + await this.loadPlugin(path.join(dir, ent.name)) + } + } catch (err) { + if (err.code !== "ENOENT") { + this.logger.warn(`failed to load plugins from ${q(dir)}: ${err.message}`) + } + } + } + + private async loadPlugin(dir: string): Promise { + try { + const str = await fsp.readFile(path.join(dir, "package.json"), { + encoding: "utf8", + }) + const packageJSON: PackageJSON = JSON.parse(str) + for (const [, p] of this.plugins) { + if (p.name === packageJSON.name) { + this.logger.warn( + `ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`, + ) + return + } + } + const p = this._loadPlugin(dir, packageJSON) + this.plugins.set(p.name, p) + } catch (err) { + if (err.code !== "ENOENT") { + this.logger.warn(`failed to load plugin: ${err.stack}`) + } + } + } + + /** + * _loadPlugin is the counterpart to loadPlugin and actually + * loads the plugin now that we know there is no duplicate + * and that the package.json has been read. + */ + private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { + dir = path.resolve(dir) + + const logger = this.logger.named(packageJSON.name) + logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON)) + + if (!packageJSON.name) { + throw new Error("plugin package.json missing name") + } + if (!packageJSON.version) { + throw new Error("plugin package.json missing version") + } + if (!packageJSON.engines || !packageJSON.engines["code-server"]) { + throw new Error(`plugin package.json missing code-server range like: + "engines": { + "code-server": "^3.7.0" + } +`) + } + if (!semver.satisfies(version, packageJSON.engines["code-server"])) { + throw new Error( + `plugin range ${q(packageJSON.engines["code-server"])} incompatible` + ` with code-server version ${version}`, + ) + } + + const pluginModule = require(dir) + if (!pluginModule.plugin) { + throw new Error("plugin module does not export a plugin") + } + + const p = { + name: packageJSON.name, + version: packageJSON.version, + modulePath: dir, + ...pluginModule.plugin, + } as Plugin + + if (!p.displayName) { + throw new Error("plugin missing displayName") + } + if (!p.description) { + throw new Error("plugin missing description") + } + if (!p.routerPath) { + throw new Error("plugin missing router path") + } + if (!p.routerPath.startsWith("/") || p.routerPath.length < 2) { + throw new Error(`plugin router path ${q(p.routerPath)}: invalid`) + } + if (!p.homepageURL) { + throw new Error("plugin missing homepage") + } + + p.init({ + logger: logger, + }) + + logger.debug("loaded") + + return p } } + +interface PackageJSON { + name: string + version: string + engines: { + "code-server": string + } +} + +function q(s: string | undefined): string { + if (s === undefined) { + s = "undefined" + } + return JSON.stringify(s) +} diff --git a/src/node/proxy.ts b/src/node/proxy.ts new file mode 100644 index 000000000..da430f5b3 --- /dev/null +++ b/src/node/proxy.ts @@ -0,0 +1,16 @@ +import proxyServer from "http-proxy" +import { HttpCode } from "../common/http" + +export const proxy = proxyServer.createProxyServer({}) + +proxy.on("error", (error, _, res) => { + res.writeHead(HttpCode.ServerError) + res.end(error.message) +}) + +// Intercept the response to rewrite absolute redirects against the base path. +proxy.on("proxyRes", (res, req) => { + if (res.headers.location && res.headers.location.startsWith("/") && (req as any).base) { + res.headers.location = (req as any).base + res.headers.location + } +}) diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts new file mode 100644 index 000000000..5c8541fc9 --- /dev/null +++ b/src/node/routes/apps.ts @@ -0,0 +1,17 @@ +import * as express from "express" +import { PluginAPI } from "../plugin" + +/** + * Implements the /api/applications endpoint + * + * See typings/pluginapi.d.ts for details. + */ +export function router(papi: PluginAPI): express.Router { + const router = express.Router() + + router.get("/", async (req, res) => { + res.json(await papi.applications()) + }) + + return router +} diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts new file mode 100644 index 000000000..6b527255a --- /dev/null +++ b/src/node/routes/domainProxy.ts @@ -0,0 +1,89 @@ +import { Request, Router } from "express" +import { HttpCode, HttpError } from "../../common/http" +import { normalize } from "../../common/util" +import { authenticated, ensureAuthenticated, redirect } from "../http" +import { proxy } from "../proxy" +import { Router as WsRouter } from "../wsRouter" + +export const router = Router() + +/** + * Return the port if the request should be proxied. Anything that ends in a + * proxy domain and has a *single* subdomain should be proxied. Anything else + * should return `undefined` and will be handled as normal. + * + * For example if `coder.com` is specified `8080.coder.com` will be proxied + * but `8080.test.coder.com` and `test.8080.coder.com` will not. + */ +const maybeProxy = (req: Request): string | undefined => { + // Split into parts. + const host = req.headers.host || "" + const idx = host.indexOf(":") + const domain = idx !== -1 ? host.substring(0, idx) : host + const parts = domain.split(".") + + // There must be an exact match. + const port = parts.shift() + const proxyDomain = parts.join(".") + if (!port || !req.args["proxy-domain"].includes(proxyDomain)) { + return undefined + } + + return port +} + +router.all("*", (req, res, next) => { + const port = maybeProxy(req) + if (!port) { + return next() + } + + // Must be authenticated to use the proxy. + if (!authenticated(req)) { + // Let the assets through since they're used on the login page. + if (req.path.startsWith("/static/") && req.method === "GET") { + return next() + } + + // Assume anything that explicitly accepts text/html is a user browsing a + // page (as opposed to an xhr request). Don't use `req.accepts()` since + // *every* request that I've seen (in Firefox and Chromium at least) + // includes `*/*` making it always truthy. Even for css/javascript. + if (req.headers.accept && req.headers.accept.includes("text/html")) { + // Let the login through. + if (/\/login\/?/.test(req.path)) { + return next() + } + // Redirect all other pages to the login. + const to = normalize(`${req.baseUrl}${req.path}`) + return redirect(req, res, "login", { + to: to !== "/" ? to : undefined, + }) + } + + // Everything else gets an unauthorized message. + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + } + + proxy.web(req, res, { + ignorePath: true, + target: `http://0.0.0.0:${port}${req.originalUrl}`, + }) +}) + +export const wsRouter = WsRouter() + +wsRouter.ws("*", (req, _, next) => { + const port = maybeProxy(req) + if (!port) { + return next() + } + + // Must be authenticated to use the proxy. + ensureAuthenticated(req) + + proxy.ws(req, req.ws, req.head, { + ignorePath: true, + target: `http://0.0.0.0:${port}${req.originalUrl}`, + }) +}) diff --git a/src/node/routes/health.ts b/src/node/routes/health.ts new file mode 100644 index 000000000..20dab71a5 --- /dev/null +++ b/src/node/routes/health.ts @@ -0,0 +1,10 @@ +import { Router } from "express" + +export const router = Router() + +router.get("/", (req, res) => { + res.json({ + status: req.heart.alive() ? "alive" : "expired", + lastHeartbeat: req.heart.lastHeartbeat, + }) +}) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts new file mode 100644 index 000000000..8a073a319 --- /dev/null +++ b/src/node/routes/index.ts @@ -0,0 +1,170 @@ +import { logger } from "@coder/logger" +import bodyParser from "body-parser" +import cookieParser from "cookie-parser" +import * as express from "express" +import { promises as fs } from "fs" +import http from "http" +import * as path from "path" +import * as tls from "tls" +import { HttpCode, HttpError } from "../../common/http" +import { plural } from "../../common/util" +import { AuthType, DefaultedArgs } from "../cli" +import { rootPath } from "../constants" +import { Heart } from "../heart" +import { replaceTemplates } from "../http" +import { PluginAPI } from "../plugin" +import { getMediaMime, paths } from "../util" +import { WebsocketRequest } from "../wsRouter" +import * as apps from "./apps" +import * as domainProxy from "./domainProxy" +import * as health from "./health" +import * as login from "./login" +import * as proxy from "./pathProxy" +// static is a reserved keyword. +import * as _static from "./static" +import * as update from "./update" +import * as vscode from "./vscode" + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Express { + export interface Request { + args: DefaultedArgs + heart: Heart + } + } +} + +/** + * Register all routes and middleware. + */ +export const register = async ( + app: express.Express, + wsApp: express.Express, + server: http.Server, + args: DefaultedArgs, +): Promise => { + const heart = new Heart(path.join(paths.data, "heartbeat"), async () => { + return new Promise((resolve, reject) => { + server.getConnections((error, count) => { + if (error) { + return reject(error) + } + logger.trace(plural(count, `${count} active connection`)) + resolve(count > 0) + }) + }) + }) + + app.disable("x-powered-by") + wsApp.disable("x-powered-by") + + app.use(cookieParser()) + wsApp.use(cookieParser()) + + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ extended: true })) + + const common: express.RequestHandler = (req, _, next) => { + // /healthz|/healthz/ needs to be excluded otherwise health checks will make + // it look like code-server is always in use. + if (!/^\/healthz\/?$/.test(req.url)) { + heart.beat() + } + + // Add common variables routes can use. + req.args = args + req.heart = heart + + next() + } + + app.use(common) + wsApp.use(common) + + app.use(async (req, res, next) => { + // If we're handling TLS ensure all requests are redirected to HTTPS. + // TODO: This does *NOT* work if you have a base path since to specify the + // protocol we need to specify the whole path. + if (args.cert && !(req.connection as tls.TLSSocket).encrypted) { + return res.redirect(`https://${req.headers.host}${req.originalUrl}`) + } + + // Return robots.txt. + if (req.originalUrl === "/robots.txt") { + const resourcePath = path.resolve(rootPath, "src/browser/robots.txt") + res.set("Content-Type", getMediaMime(resourcePath)) + return res.send(await fs.readFile(resourcePath)) + } + + next() + }) + + app.use("/", domainProxy.router) + wsApp.use("/", domainProxy.wsRouter.router) + + app.use("/", vscode.router) + wsApp.use("/", vscode.wsRouter.router) + app.use("/vscode", vscode.router) + wsApp.use("/vscode", vscode.wsRouter.router) + + app.use("/healthz", health.router) + + if (args.auth === AuthType.Password) { + app.use("/login", login.router) + } + + app.use("/proxy", proxy.router) + wsApp.use("/proxy", proxy.wsRouter.router) + + app.use("/static", _static.router) + app.use("/update", update.router) + + const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH) + await papi.loadPlugins() + papi.mount(app) + app.use("/api/applications", apps.router(papi)) + + app.use(() => { + throw new HttpError("Not Found", HttpCode.NotFound) + }) + + const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => { + if (err.code === "ENOENT" || err.code === "EISDIR") { + err.status = HttpCode.NotFound + } + + const status = err.status ?? err.statusCode ?? 500 + res.status(status) + + // Assume anything that explicitly accepts text/html is a user browsing a + // page (as opposed to an xhr request). Don't use `req.accepts()` since + // *every* request that I've seen (in Firefox and Chromium at least) + // includes `*/*` making it always truthy. Even for css/javascript. + if (req.headers.accept && req.headers.accept.includes("text/html")) { + const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") + res.set("Content-Type", getMediaMime(resourcePath)) + const content = await fs.readFile(resourcePath, "utf8") + res.send( + replaceTemplates(req, content) + .replace(/{{ERROR_TITLE}}/g, status) + .replace(/{{ERROR_HEADER}}/g, status) + .replace(/{{ERROR_BODY}}/g, err.message), + ) + } else { + res.json({ + error: err.message, + ...(err.details || {}), + }) + } + } + + app.use(errorHandler) + + const wsErrorHandler: express.ErrorRequestHandler = async (err, req) => { + logger.error(`${err.message} ${err.stack}`) + ;(req as WebsocketRequest).ws.end() + } + + wsApp.use(wsErrorHandler) +} diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts new file mode 100644 index 000000000..bf1058e5e --- /dev/null +++ b/src/node/routes/login.ts @@ -0,0 +1,95 @@ +import { Router, Request } from "express" +import { promises as fs } from "fs" +import { RateLimiter as Limiter } from "limiter" +import * as path from "path" +import safeCompare from "safe-compare" +import { rootPath } from "../constants" +import { authenticated, getCookieDomain, redirect, replaceTemplates } from "../http" +import { hash, humanPath } from "../util" + +enum Cookie { + Key = "key", +} + +// RateLimiter wraps around the limiter library for logins. +// It allows 2 logins every minute and 12 logins every hour. +class RateLimiter { + private readonly minuteLimiter = new Limiter(2, "minute") + private readonly hourLimiter = new Limiter(12, "hour") + + public try(): boolean { + if (this.minuteLimiter.tryRemoveTokens(1)) { + return true + } + return this.hourLimiter.tryRemoveTokens(1) + } +} + +const getRoot = async (req: Request, error?: Error): Promise => { + const content = await fs.readFile(path.join(rootPath, "src/browser/pages/login.html"), "utf8") + let passwordMsg = `Check the config file at ${humanPath(req.args.config)} for the password.` + if (req.args.usingEnvPassword) { + passwordMsg = "Password was set from $PASSWORD." + } + return replaceTemplates( + req, + content + .replace(/{{PASSWORD_MSG}}/g, passwordMsg) + .replace(/{{ERROR}}/, error ? `
    ${error.message}
    ` : ""), + ) +} + +const limiter = new RateLimiter() + +export const router = Router() + +router.use((req, res, next) => { + const to = (typeof req.query.to === "string" && req.query.to) || "/" + if (authenticated(req)) { + return redirect(req, res, to, { to: undefined }) + } + next() +}) + +router.get("/", async (req, res) => { + res.send(await getRoot(req)) +}) + +router.post("/", async (req, res) => { + try { + if (!limiter.try()) { + throw new Error("Login rate limited!") + } + + if (!req.body.password) { + throw new Error("Missing password") + } + + if (req.args.password && safeCompare(req.body.password, req.args.password)) { + // The hash does not add any actual security but we do it for + // obfuscation purposes (and as a side effect it handles escaping). + res.cookie(Cookie.Key, hash(req.body.password), { + domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]), + path: req.body.base || "/", + sameSite: "lax", + }) + + const to = (typeof req.query.to === "string" && req.query.to) || "/" + return redirect(req, res, to, { to: undefined }) + } + + console.error( + "Failed login attempt", + JSON.stringify({ + xForwardedFor: req.headers["x-forwarded-for"], + remoteAddress: req.connection.remoteAddress, + userAgent: req.headers["user-agent"], + timestamp: Math.floor(new Date().getTime() / 1000), + }), + ) + + throw new Error("Incorrect password") + } catch (error) { + res.send(await getRoot(req, error)) + } +}) diff --git a/src/node/routes/pathProxy.ts b/src/node/routes/pathProxy.ts new file mode 100644 index 000000000..152402195 --- /dev/null +++ b/src/node/routes/pathProxy.ts @@ -0,0 +1,47 @@ +import { Request, Router } from "express" +import qs from "qs" +import { HttpCode, HttpError } from "../../common/http" +import { normalize } from "../../common/util" +import { authenticated, ensureAuthenticated, redirect } from "../http" +import { proxy } from "../proxy" +import { Router as WsRouter } from "../wsRouter" + +export const router = Router() + +const getProxyTarget = (req: Request, rewrite: boolean): string => { + if (rewrite) { + const query = qs.stringify(req.query) + return `http://0.0.0.0:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}` + } + return `http://0.0.0.0:${req.params.port}/${req.originalUrl}` +} + +router.all("/(:port)(/*)?", (req, res) => { + if (!authenticated(req)) { + // If visiting the root (/:port only) redirect to the login page. + if (!req.params[0] || req.params[0] === "/") { + const to = normalize(`${req.baseUrl}${req.path}`) + return redirect(req, res, "login", { + to: to !== "/" ? to : undefined, + }) + } + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + } + + // Absolute redirects need to be based on the subpath when rewriting. + ;(req as any).base = `${req.baseUrl}/${req.params.port}` + + proxy.web(req, res, { + ignorePath: true, + target: getProxyTarget(req, true), + }) +}) + +export const wsRouter = WsRouter() + +wsRouter.ws("/(:port)(/*)?", ensureAuthenticated, (req) => { + proxy.ws(req, req.ws, req.head, { + ignorePath: true, + target: getProxyTarget(req, true), + }) +}) diff --git a/src/node/routes/static.ts b/src/node/routes/static.ts new file mode 100644 index 000000000..22bdd8d24 --- /dev/null +++ b/src/node/routes/static.ts @@ -0,0 +1,69 @@ +import { field, logger } from "@coder/logger" +import { Router } from "express" +import { promises as fs } from "fs" +import * as path from "path" +import { Readable } from "stream" +import * as tarFs from "tar-fs" +import * as zlib from "zlib" +import { HttpCode, HttpError } from "../../common/http" +import { rootPath } from "../constants" +import { authenticated, ensureAuthenticated, replaceTemplates } from "../http" +import { getMediaMime, pathToFsPath } from "../util" + +export const router = Router() + +// The commit is for caching. +router.get("/(:commit)(/*)?", async (req, res) => { + // Used by VS Code to load extensions into the web worker. + const tar = Array.isArray(req.query.tar) ? req.query.tar[0] : req.query.tar + if (typeof tar === "string") { + ensureAuthenticated(req) + let stream: Readable = tarFs.pack(pathToFsPath(tar)) + if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) { + logger.debug("gzipping tar", field("path", tar)) + const compress = zlib.createGzip() + stream.pipe(compress) + stream.on("error", (error) => compress.destroy(error)) + stream.on("close", () => compress.end()) + stream = compress + res.header("content-encoding", "gzip") + } + res.set("Content-Type", "application/x-tar") + stream.on("close", () => res.end()) + return stream.pipe(res) + } + + // If not a tar use the remainder of the path to load the resource. + if (!req.params[0]) { + throw new HttpError("Not Found", HttpCode.NotFound) + } + + const resourcePath = path.resolve(req.params[0]) + + // Make sure it's in code-server if you aren't authenticated. This lets + // unauthenticated users load the login assets. + if (!resourcePath.startsWith(rootPath) && !authenticated(req)) { + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + } + + // Don't cache during development. - can also be used if you want to make a + // static request without caching. + if (req.params.commit !== "development" && req.params.commit !== "-") { + res.header("Cache-Control", "public, max-age=31536000") + } + + // Without this the default is to use the directory the script loaded from. + if (req.headers["service-worker"]) { + res.header("service-worker-allowed", "/") + } + + res.set("Content-Type", getMediaMime(resourcePath)) + + if (resourcePath.endsWith("manifest.json")) { + const content = await fs.readFile(resourcePath, "utf8") + return res.send(replaceTemplates(req, content)) + } + + const content = await fs.readFile(resourcePath) + return res.send(content) +}) diff --git a/src/node/routes/update.ts b/src/node/routes/update.ts new file mode 100644 index 000000000..5c9aa181e --- /dev/null +++ b/src/node/routes/update.ts @@ -0,0 +1,18 @@ +import { Router } from "express" +import { version } from "../constants" +import { ensureAuthenticated } from "../http" +import { UpdateProvider } from "../update" + +export const router = Router() + +const provider = new UpdateProvider() + +router.get("/check", ensureAuthenticated, async (req, res) => { + const update = await provider.getUpdate(req.query.force === "true") + res.json({ + checked: update.checked, + latest: update.version, + current: version, + isLatest: provider.isLatestVersion(update), + }) +}) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts new file mode 100644 index 000000000..9b464f61e --- /dev/null +++ b/src/node/routes/vscode.ts @@ -0,0 +1,105 @@ +import * as crypto from "crypto" +import { Router } from "express" +import { promises as fs } from "fs" +import * as path from "path" +import { commit, rootPath, version } from "../constants" +import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http" +import { getMediaMime, pathToFsPath } from "../util" +import { VscodeProvider } from "../vscode" +import { Router as WsRouter } from "../wsRouter" + +export const router = Router() + +const vscode = new VscodeProvider() + +router.get("/", async (req, res) => { + if (!authenticated(req)) { + return redirect(req, res, "login", { + // req.baseUrl can be blank if already at the root. + to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined, + }) + } + + const [content, options] = await Promise.all([ + await fs.readFile(path.join(rootPath, "src/browser/pages/vscode.html"), "utf8"), + (async () => { + try { + return await vscode.initialize({ args: req.args, remoteAuthority: req.headers.host || "" }, req.query) + } catch (error) { + const devMessage = commit === "development" ? "It might not have finished compiling." : "" + throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`) + } + })(), + ]) + + options.productConfiguration.codeServerVersion = version + + res.send( + replaceTemplates( + req, + // Uncomment prod blocks if not in development. TODO: Would this be + // better as a build step? Or maintain two HTML files again? + commit !== "development" ? content.replace(//g, "") : content, + { + disableTelemetry: !!req.args["disable-telemetry"], + }, + ) + .replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`) + .replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`) + .replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`) + .replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`), + ) +}) + +/** + * TODO: Might currently be unused. + */ +router.get("/resource(/*)?", ensureAuthenticated, async (req, res) => { + if (typeof req.query.path === "string") { + res.set("Content-Type", getMediaMime(req.query.path)) + res.send(await fs.readFile(pathToFsPath(req.query.path))) + } +}) + +/** + * Used by VS Code to load files. + */ +router.get("/vscode-remote-resource(/*)?", ensureAuthenticated, async (req, res) => { + if (typeof req.query.path === "string") { + res.set("Content-Type", getMediaMime(req.query.path)) + res.send(await fs.readFile(pathToFsPath(req.query.path))) + } +}) + +/** + * VS Code webviews use these paths to load files and to load webview assets + * like HTML and JavaScript. + */ +router.get("/webview/*", ensureAuthenticated, async (req, res) => { + res.set("Content-Type", getMediaMime(req.path)) + if (/^vscode-resource/.test(req.params[0])) { + return res.send(await fs.readFile(req.params[0].replace(/^vscode-resource(\/file)?/, ""))) + } + return res.send( + await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])), + ) +}) + +export const wsRouter = WsRouter() + +wsRouter.ws("/", ensureAuthenticated, async (req) => { + const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + const reply = crypto + .createHash("sha1") + .update(req.headers["sec-websocket-key"] + magic) + .digest("base64") + req.ws.write( + [ + "HTTP/1.1 101 Switching Protocols", + "Upgrade: websocket", + "Connection: Upgrade", + `Sec-WebSocket-Accept: ${reply}`, + ].join("\r\n") + "\r\n\r\n", + ) + await vscode.sendWebsocket(req.ws, req.query) +}) diff --git a/src/node/settings.ts b/src/node/settings.ts index d68e8e3bd..5f9427aa1 100644 --- a/src/node/settings.ts +++ b/src/node/settings.ts @@ -1,7 +1,7 @@ import { logger } from "@coder/logger" +import { Query } from "express-serve-static-core" import * as fs from "fs-extra" import * as path from "path" -import { Route } from "./http" import { paths } from "./util" export type Settings = { [key: string]: Settings | string | boolean | number } @@ -58,7 +58,7 @@ export interface CoderSettings extends UpdateSettings { url: string workspace: boolean } - query: Route["query"] + query: Query } /** diff --git a/src/node/socket.ts b/src/node/socket.ts index e5fe66778..ada024831 100644 --- a/src/node/socket.ts +++ b/src/node/socket.ts @@ -4,7 +4,7 @@ import * as path from "path" import * as tls from "tls" import { Emitter } from "../common/emitter" import { generateUuid } from "../common/util" -import { tmpdir } from "./util" +import { canConnect, tmpdir } from "./util" /** * Provides a way to proxy a TLS socket. Can be used when you need to pass a @@ -89,17 +89,6 @@ export class SocketProxyProvider { } public async findFreeSocketPath(basePath: string, maxTries = 100): Promise { - const canConnect = (path: string): Promise => { - return new Promise((resolve) => { - const socket = net.connect(path) - socket.once("error", () => resolve(false)) - socket.once("connect", () => { - socket.destroy() - resolve(true) - }) - }) - } - let i = 0 let path = basePath while ((await canConnect(path)) && i < maxTries) { diff --git a/src/node/app/update.ts b/src/node/update.ts similarity index 70% rename from src/node/app/update.ts rename to src/node/update.ts index a83f578e1..a156aad65 100644 --- a/src/node/app/update.ts +++ b/src/node/update.ts @@ -1,12 +1,10 @@ import { field, logger } from "@coder/logger" import * as http from "http" import * as https from "https" -import * as path from "path" import * as semver from "semver" import * as url from "url" -import { HttpCode, HttpError } from "../../common/http" -import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" -import { settings as globalSettings, SettingsProvider, UpdateSettings } from "../settings" +import { version } from "./constants" +import { settings as globalSettings, SettingsProvider, UpdateSettings } from "./settings" export interface Update { checked: number @@ -18,15 +16,13 @@ export interface LatestResponse { } /** - * HTTP provider for checking updates (does not download/install them). + * Provide update information. */ -export class UpdateHttpProvider extends HttpProvider { +export class UpdateProvider { private update?: Promise private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks. public constructor( - options: HttpProviderOptions, - public readonly enabled: boolean, /** * The URL for getting the latest version of code-server. Should return JSON * that fulfills `LatestResponse`. @@ -37,37 +33,7 @@ export class UpdateHttpProvider extends HttpProvider { * settings will be used. */ private readonly settings: SettingsProvider = globalSettings, - ) { - super(options) - } - - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - this.ensureAuthenticated(request) - this.ensureMethod(request) - - if (!this.isRoot(route)) { - throw new HttpError("Not found", HttpCode.NotFound) - } - - if (!this.enabled) { - throw new Error("update checks are disabled") - } - - switch (route.base) { - case "/check": - case "/": { - const update = await this.getUpdate(route.base === "/check") - return { - content: { - ...update, - isLatest: this.isLatestVersion(update), - }, - } - } - } - - throw new HttpError("Not found", HttpCode.NotFound) - } + ) {} /** * Query for and return the latest update. @@ -89,7 +55,7 @@ export class UpdateHttpProvider extends HttpProvider { if (!update || update.checked + this.updateInterval < now) { const buffer = await this.request(this.latestUrl) const data = JSON.parse(buffer.toString()) as LatestResponse - update = { checked: now, version: data.name } + update = { checked: now, version: data.name.replace(/^v/, "") } await this.settings.write({ update }) } logger.debug("got latest version", field("latest", update.version)) @@ -103,18 +69,13 @@ export class UpdateHttpProvider extends HttpProvider { } } - public get currentVersion(): string { - return require(path.resolve(__dirname, "../../../package.json")).version - } - /** * Return true if the currently installed version is the latest. */ public isLatestVersion(latest: Update): boolean { - const version = this.currentVersion logger.debug("comparing versions", field("current", version), field("latest", latest.version)) try { - return latest.version === version || semver.lt(latest.version, version) + return semver.lte(latest.version, version) } catch (error) { return true } @@ -144,24 +105,22 @@ export class UpdateHttpProvider extends HttpProvider { logger.debug("Making request", field("uri", uri)) const httpx = uri.startsWith("https") ? https : http const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => { - if ( - response.statusCode && - response.statusCode >= 300 && - response.statusCode < 400 && - response.headers.location - ) { + if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) { + return reject(new Error(`${uri}: ${response.statusCode || "500"}`)) + } + + if (response.statusCode >= 300) { ++redirects + response.destroy() if (redirects > maxRedirects) { return reject(new Error("reached max redirects")) } - response.destroy() + if (!response.headers.location) { + return reject(new Error("received redirect with no location header")) + } return request(url.resolve(uri, response.headers.location)) } - if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) { - return reject(new Error(`${uri}: ${response.statusCode || "500"}`)) - } - resolve(response) }) client.on("error", reject) diff --git a/src/node/util.ts b/src/node/util.ts index c0f37f74b..11128e540 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -2,6 +2,7 @@ import * as cp from "child_process" import * as crypto from "crypto" import envPaths from "env-paths" import * as fs from "fs-extra" +import * as net from "net" import * as os from "os" import * as path from "path" import * as util from "util" @@ -53,25 +54,45 @@ export function humanPath(p?: string): string { return p.replace(os.homedir(), "~") } -export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => { - const paths = { - cert: path.join(tmpdir, "self-signed.cert"), - certKey: path.join(tmpdir, "self-signed.key"), - } - const checks = await Promise.all([fs.pathExists(paths.cert), fs.pathExists(paths.certKey)]) +export const generateCertificate = async (hostname: string): Promise<{ cert: string; certKey: string }> => { + const certPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.crt`) + const certKeyPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.key`) + + const checks = await Promise.all([fs.pathExists(certPath), fs.pathExists(certKeyPath)]) if (!checks[0] || !checks[1]) { // Require on demand so openssl isn't required if you aren't going to // generate certificates. const pem = require("pem") as typeof import("pem") const certs = await new Promise((resolve, reject): void => { - pem.createCertificate({ selfSigned: true }, (error, result) => { - return error ? reject(error) : resolve(result) - }) + pem.createCertificate( + { + selfSigned: true, + commonName: hostname, + config: ` +[req] +req_extensions = v3_req + +[ v3_req ] +basicConstraints = CA:true +extendedKeyUsage = serverAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = ${hostname} +`, + }, + (error, result) => { + return error ? reject(error) : resolve(result) + }, + ) }) - await fs.mkdirp(tmpdir) - await Promise.all([fs.writeFile(paths.cert, certs.certificate), fs.writeFile(paths.certKey, certs.serviceKey)]) + await fs.mkdirp(paths.data) + await Promise.all([fs.writeFile(certPath, certs.certificate), fs.writeFile(certKeyPath, certs.serviceKey)]) + } + return { + cert: certPath, + certKey: certKeyPath, } - return paths } export const generatePassword = async (length = 24): Promise => { @@ -246,3 +267,26 @@ export function pathToFsPath(path: string, keepDriveLetterCasing = false): strin } return value } + +/** + * Return a promise that resolves with whether the socket path is active. + */ +export function canConnect(path: string): Promise { + return new Promise((resolve) => { + const socket = net.connect(path) + socket.once("error", () => resolve(false)) + socket.once("connect", () => { + socket.destroy() + resolve(true) + }) + }) +} + +export const isFile = async (path: string): Promise => { + try { + const stat = await fs.stat(path) + return stat.isFile() + } catch (error) { + return false + } +} diff --git a/src/node/vscode.ts b/src/node/vscode.ts new file mode 100644 index 000000000..3c18cdee6 --- /dev/null +++ b/src/node/vscode.ts @@ -0,0 +1,164 @@ +import { logger } from "@coder/logger" +import * as cp from "child_process" +import * as net from "net" +import * as path from "path" +import * as ipc from "../../lib/vscode/src/vs/server/ipc" +import { arrayify, generateUuid } from "../common/util" +import { rootPath } from "./constants" +import { settings } from "./settings" +import { SocketProxyProvider } from "./socket" +import { isFile } from "./util" +import { onMessage, wrapper } from "./wrapper" + +export class VscodeProvider { + public readonly serverRootPath: string + public readonly vsRootPath: string + private _vscode?: Promise + private readonly socketProvider = new SocketProxyProvider() + + public constructor() { + this.vsRootPath = path.resolve(rootPath, "lib/vscode") + this.serverRootPath = path.join(this.vsRootPath, "out/vs/server") + wrapper.onDispose(() => this.dispose()) + } + + public async dispose(): Promise { + this.socketProvider.stop() + if (this._vscode) { + const vscode = await this._vscode + vscode.removeAllListeners() + vscode.kill() + this._vscode = undefined + } + } + + public async initialize( + options: Omit, + query: ipc.Query, + ): Promise { + const { lastVisited } = await settings.read() + const startPath = await this.getFirstPath([ + { url: query.workspace, workspace: true }, + { url: query.folder, workspace: false }, + options.args._ && options.args._.length > 0 + ? { url: path.resolve(options.args._[options.args._.length - 1]) } + : undefined, + lastVisited, + ]) + + settings.write({ + lastVisited: startPath, + query, + }) + + const id = generateUuid() + const vscode = await this.fork() + + logger.debug("setting up vs code...") + + this.send( + { + type: "init", + id, + options: { + ...options, + startPath, + }, + }, + vscode, + ) + + const message = await onMessage( + vscode, + (message): message is ipc.OptionsMessage => { + // There can be parallel initializations so wait for the right ID. + return message.type === "options" && message.id === id + }, + ) + + return message.options + } + + private fork(): Promise { + if (this._vscode) { + return this._vscode + } + + logger.debug("forking vs code...") + const vscode = cp.fork(path.join(this.serverRootPath, "fork")) + + const dispose = () => { + vscode.removeAllListeners() + vscode.kill() + this._vscode = undefined + } + + vscode.on("error", (error: Error) => { + logger.error(error.message) + if (error.stack) { + logger.debug(error.stack) + } + dispose() + }) + + vscode.on("exit", (code) => { + logger.error(`VS Code exited unexpectedly with code ${code}`) + dispose() + }) + + this._vscode = onMessage(vscode, (message): message is ipc.ReadyMessage => { + return message.type === "ready" + }).then(() => vscode) + + return this._vscode + } + + /** + * VS Code expects a raw socket. It will handle all the web socket frames. + */ + public async sendWebsocket(socket: net.Socket, query: ipc.Query): Promise { + const vscode = await this._vscode + // TLS sockets cannot be transferred to child processes so we need an + // in-between. Non-TLS sockets will be returned as-is. + const socketProxy = await this.socketProvider.createProxy(socket) + this.send({ type: "socket", query }, vscode, socketProxy) + } + + private send(message: ipc.CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void { + if (!vscode || vscode.killed) { + throw new Error("vscode is not running") + } + vscode.send(message, socket) + } + + /** + * Choose the first non-empty path from the provided array. + * + * Each array item consists of `url` and an optional `workspace` boolean that + * indicates whether that url is for a workspace. + * + * `url` can be a fully qualified URL or just the path portion. + * + * `url` can also be a query object to make it easier to pass in query + * variables directly but anything that isn't a string or string array is not + * valid and will be ignored. + */ + private async getFirstPath( + startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>, + ): Promise { + for (let i = 0; i < startPaths.length; ++i) { + const startPath = startPaths[i] + const url = arrayify(startPath && startPath.url).find((p) => !!p) + if (startPath && url && typeof url === "string") { + return { + url, + // The only time `workspace` is undefined is for the command-line + // argument, in which case it's a path (not a URL) so we can stat it + // without having to parse it. + workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url), + } + } + } + return undefined + } +} diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index ba459efd1..f6f84e2bd 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -1,11 +1,70 @@ -import { field, logger } from "@coder/logger" +import { field, Logger, logger } from "@coder/logger" import * as cp from "child_process" import * as path from "path" import * as rfs from "rotating-file-stream" import { Emitter } from "../common/emitter" +import { DefaultedArgs } from "./cli" import { paths } from "./util" -interface HandshakeMessage { +const timeoutInterval = 10000 // 10s, matches VS Code's timeouts. + +/** + * Listen to a single message from a process. Reject if the process errors, + * exits, or times out. + * + * `fn` is a function that determines whether the message is the one we're + * waiting for. + */ +export function onMessage( + proc: cp.ChildProcess | NodeJS.Process, + fn: (message: M) => message is T, + customLogger?: Logger, +): Promise { + return new Promise((resolve, reject) => { + const cleanup = () => { + proc.off("error", onError) + proc.off("exit", onExit) + proc.off("message", onMessage) + clearTimeout(timeout) + } + + const timeout = setTimeout(() => { + cleanup() + reject(new Error("timed out")) + }, timeoutInterval) + + const onError = (error: Error) => { + cleanup() + reject(error) + } + + const onExit = (code: number) => { + cleanup() + reject(new Error(`exited unexpectedly with code ${code}`)) + } + + const onMessage = (message: M) => { + ;(customLogger || logger).trace("got message", field("message", message)) + if (fn(message)) { + cleanup() + resolve(message) + } + } + + proc.on("message", onMessage) + // NodeJS.Process doesn't have `error` but binding anyway shouldn't break + // anything. It does have `exit` but the types aren't working. + ;(proc as cp.ChildProcess).on("error", onError) + ;(proc as cp.ChildProcess).on("exit", onExit) + }) +} + +interface ParentHandshakeMessage { + type: "handshake" + args: DefaultedArgs +} + +interface ChildHandshakeMessage { type: "handshake" } @@ -14,9 +73,10 @@ interface RelaunchMessage { version: string } -export type Message = RelaunchMessage | HandshakeMessage +type ChildMessage = RelaunchMessage | ChildHandshakeMessage +type ParentMessage = ParentHandshakeMessage -export class ProcessError extends Error { +class ProcessError extends Error { public constructor(message: string, public readonly code: number | undefined) { super(message) this.name = this.constructor.name @@ -25,52 +85,55 @@ export class ProcessError extends Error { } /** - * Allows the wrapper and inner processes to communicate. + * Wrapper around a process that tries to gracefully exit when a process exits + * and provides a way to prevent `process.exit`. */ -export class IpcMain { - private readonly _onMessage = new Emitter() - public readonly onMessage = this._onMessage.event - private readonly _onDispose = new Emitter() - public readonly onDispose = this._onDispose.event - public readonly processExit: (code?: number) => never +abstract class Process { + /** + * Emit this to trigger a graceful exit. + */ + protected readonly _onDispose = new Emitter() - public constructor(public readonly parentPid?: number) { + /** + * Emitted when the process is about to be disposed. + */ + public readonly onDispose = this._onDispose.event + + /** + * Uniquely named logger for the process. + */ + public abstract logger: Logger + + public constructor() { process.on("SIGINT", () => this._onDispose.emit("SIGINT")) process.on("SIGTERM", () => this._onDispose.emit("SIGTERM")) process.on("exit", () => this._onDispose.emit(undefined)) - // Ensure we control when the process exits. - this.processExit = process.exit - process.exit = function (code?: number) { - logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`) - } as (code?: number) => never - - this.onDispose((signal) => { + this.onDispose((signal, wait) => { // Remove listeners to avoid possibly triggering disposal again. process.removeAllListeners() - // Let any other handlers run first then exit. - logger.debug(`${parentPid ? "inner process" : "wrapper"} ${process.pid} disposing`, field("code", signal)) - setTimeout(() => this.exit(0), 0) + // Try waiting for other handlers to run first then exit. + this.logger.debug("disposing", field("code", signal)) + wait.then(() => this.exit(0)) + setTimeout(() => this.exit(0), 5000) }) + } - // Kill the inner process if the parent dies. This is for the case where the - // parent process is forcefully terminated and cannot clean up. - if (parentPid) { - setInterval(() => { - try { - // process.kill throws an exception if the process doesn't exist. - process.kill(parentPid, 0) - } catch (_) { - // Consider this an error since it should have been able to clean up - // the child process unless it was forcefully killed. - logger.error(`parent process ${parentPid} died`) - this._onDispose.emit(undefined) - } - }, 5000) + /** + * Ensure control over when the process exits. + */ + public preventExit(): void { + ;(process.exit as any) = (code?: number) => { + this.logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`) } } + private readonly processExit: (code?: number) => never = process.exit + + /** + * Will always exit even if normal exit is being prevented. + */ public exit(error?: number | ProcessError): never { if (error && typeof error !== "number") { this.processExit(typeof error.code === "number" ? error.code : 1) @@ -78,48 +141,59 @@ export class IpcMain { this.processExit(error) } } +} - public handshake(child?: cp.ChildProcess): Promise { - return new Promise((resolve, reject) => { - const target = child || process - const onMessage = (message: Message): void => { - logger.debug( - `${child ? "wrapper" : "inner process"} ${process.pid} received message from ${ - child ? child.pid : this.parentPid - }`, - field("message", message), - ) - if (message.type === "handshake") { - target.removeListener("message", onMessage) - target.on("message", (msg) => this._onMessage.emit(msg)) - // The wrapper responds once the inner process starts the handshake. - if (child) { - if (!target.send) { - throw new Error("child not spawned with IPC") - } - target.send({ type: "handshake" }) - } - resolve() - } +/** + * Child process that will clean up after itself if the parent goes away and can + * perform a handshake with the parent and ask it to relaunch. + */ +class ChildProcess extends Process { + public logger = logger.named(`child:${process.pid}`) + + public constructor(private readonly parentPid: number) { + super() + + // Kill the inner process if the parent dies. This is for the case where the + // parent process is forcefully terminated and cannot clean up. + setInterval(() => { + try { + // process.kill throws an exception if the process doesn't exist. + process.kill(this.parentPid, 0) + } catch (_) { + // Consider this an error since it should have been able to clean up + // the child process unless it was forcefully killed. + this.logger.error(`parent process ${parentPid} died`) + this._onDispose.emit(undefined) } - target.on("message", onMessage) - if (child) { - child.once("error", reject) - child.once("exit", (code) => { - reject(new ProcessError(`Unexpected exit with code ${code}`, code !== null ? code : undefined)) - }) - } else { - // The inner process initiates the handshake. - this.send({ type: "handshake" }) - } - }) + }, 5000) } + /** + * Initiate the handshake and wait for a response from the parent. + */ + public async handshake(): Promise { + this.send({ type: "handshake" }) + const message = await onMessage( + process, + (message): message is ParentHandshakeMessage => { + return message.type === "handshake" + }, + this.logger, + ) + return message.args + } + + /** + * Notify the parent process that it should relaunch the child. + */ public relaunch(version: string): void { this.send({ type: "relaunch", version }) } - private send(message: Message): void { + /** + * Send a message to the parent. + */ + private send(message: ChildMessage): void { if (!process.send) { throw new Error("not spawned with IPC") } @@ -127,34 +201,32 @@ export class IpcMain { } } -let _ipcMain: IpcMain -export const ipcMain = (): IpcMain => { - if (!_ipcMain) { - _ipcMain = new IpcMain( - typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" - ? parseInt(process.env.CODE_SERVER_PARENT_PID) - : undefined, - ) - } - return _ipcMain -} - -export interface WrapperOptions { - maxMemory?: number - nodeOptions?: string -} - /** - * Provides a way to wrap a process for the purpose of updating the running - * instance. + * Parent process wrapper that spawns the child process and performs a handshake + * with it. Will relaunch the child if it receives a SIGUSR1 or is asked to by + * the child. If the child otherwise exits the parent will also exit. */ -export class WrapperProcess { - private process?: cp.ChildProcess +export class ParentProcess extends Process { + public logger = logger.named(`parent:${process.pid}`) + + private child?: cp.ChildProcess private started?: Promise private readonly logStdoutStream: rfs.RotatingFileStream private readonly logStderrStream: rfs.RotatingFileStream - public constructor(private currentVersion: string, private readonly options?: WrapperOptions) { + protected readonly _onChildMessage = new Emitter() + protected readonly onChildMessage = this._onChildMessage.event + + private args?: DefaultedArgs + + public constructor(private currentVersion: string) { + super() + + process.on("SIGUSR1", async () => { + this.logger.info("Received SIGUSR1; hotswapping") + this.relaunch() + }) + const opts = { size: "10M", maxFiles: 10, @@ -162,112 +234,135 @@ export class WrapperProcess { this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts) this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts) - ipcMain().onDispose(() => { - if (this.process) { - this.process.removeAllListeners() - this.process.kill() - } + this.onDispose(() => { + this.disposeChild() }) - ipcMain().onMessage((message) => { + this.onChildMessage((message) => { switch (message.type) { case "relaunch": - logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`) + this.logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`) this.currentVersion = message.version this.relaunch() break default: - logger.error(`Unrecognized message ${message}`) + this.logger.error(`Unrecognized message ${message}`) break } }) + } - process.on("SIGUSR1", async () => { - logger.info("Received SIGUSR1; hotswapping") - this.relaunch() - }) + private disposeChild(): void { + this.started = undefined + if (this.child) { + this.child.removeAllListeners() + this.child.kill() + } } private async relaunch(): Promise { - this.started = undefined - if (this.process) { - this.process.removeAllListeners() - this.process.kill() - } + this.disposeChild() try { - await this.start() + this.started = this._start() + await this.started } catch (error) { - logger.error(error.message) - ipcMain().exit(typeof error.code === "number" ? error.code : 1) + this.logger.error(error.message) + this.exit(typeof error.code === "number" ? error.code : 1) } } - public start(): Promise { + public start(args: DefaultedArgs): Promise { + // Store for relaunches. + this.args = args if (!this.started) { - this.started = this.spawn().then((child) => { - // Log both to stdout and to the log directory. - if (child.stdout) { - child.stdout.pipe(this.logStdoutStream) - child.stdout.pipe(process.stdout) - } - if (child.stderr) { - child.stderr.pipe(this.logStderrStream) - child.stderr.pipe(process.stderr) - } - logger.debug(`spawned inner process ${child.pid}`) - ipcMain() - .handshake(child) - .then(() => { - child.once("exit", (code) => { - logger.debug(`inner process ${child.pid} exited unexpectedly`) - ipcMain().exit(code || 0) - }) - }) - this.process = child - }) + this.started = this._start() } return this.started } - private async spawn(): Promise { - // Flags to pass along to the Node binary. - let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${(this.options && this.options.nodeOptions) || ""}` - if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) { - nodeOptions += ` --max_old_space_size=${(this.options && this.options.maxMemory) || 2048}` + private async _start(): Promise { + const child = this.spawn() + this.child = child + + // Log both to stdout and to the log directory. + if (child.stdout) { + child.stdout.pipe(this.logStdoutStream) + child.stdout.pipe(process.stdout) + } + if (child.stderr) { + child.stderr.pipe(this.logStderrStream) + child.stderr.pipe(process.stderr) } + this.logger.debug(`spawned inner process ${child.pid}`) + + await this.handshake(child) + + child.once("exit", (code) => { + this.logger.debug(`inner process ${child.pid} exited unexpectedly`) + this.exit(code || 0) + }) + } + + private spawn(): cp.ChildProcess { // Use spawn (instead of fork) to use the new binary in case it was updated. return cp.spawn(process.argv[0], process.argv.slice(1), { env: { ...process.env, CODE_SERVER_PARENT_PID: process.pid.toString(), - NODE_OPTIONS: nodeOptions, + NODE_OPTIONS: `--max-old-space-size=2048 ${process.env.NODE_OPTIONS || ""}`, }, stdio: ["ipc"], }) } + + /** + * Wait for a handshake from the child then reply. + */ + private async handshake(child: cp.ChildProcess): Promise { + if (!this.args) { + throw new Error("started without args") + } + await onMessage( + child, + (message): message is ChildHandshakeMessage => { + return message.type === "handshake" + }, + this.logger, + ) + this.send(child, { type: "handshake", args: this.args }) + } + + /** + * Send a message to the child. + */ + private send(child: cp.ChildProcess, message: ParentMessage): void { + child.send(message) + } +} + +/** + * Process wrapper. + */ +export const wrapper = + typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" + ? new ChildProcess(parseInt(process.env.CODE_SERVER_PARENT_PID)) + : new ParentProcess(require("../../package.json").version) + +export function isChild(proc: ChildProcess | ParentProcess): proc is ChildProcess { + return proc instanceof ChildProcess } // It's possible that the pipe has closed (for example if you run code-server // --version | head -1). Assume that means we're done. if (!process.stdout.isTTY) { - process.stdout.on("error", () => ipcMain().exit()) + process.stdout.on("error", () => wrapper.exit()) } -export const wrap = (fn: () => Promise): void => { - if (ipcMain().parentPid) { - ipcMain() - .handshake() - .then(() => fn()) - .catch((error: ProcessError): void => { - logger.error(error.message) - ipcMain().exit(error) - }) - } else { - const wrapper = new WrapperProcess(require("../../package.json").version) - wrapper.start().catch((error) => { - logger.error(error.message) - ipcMain().exit(error) - }) +// Don't let uncaught exceptions crash the process. +process.on("uncaughtException", (error) => { + wrapper.logger.error(`Uncaught exception: ${error.message}`) + if (typeof error.stack !== "undefined") { + wrapper.logger.error(error.stack) } -} +}) diff --git a/src/node/wsRouter.ts b/src/node/wsRouter.ts new file mode 100644 index 000000000..8787d6f4f --- /dev/null +++ b/src/node/wsRouter.ts @@ -0,0 +1,57 @@ +import * as express from "express" +import * as expressCore from "express-serve-static-core" +import * as http from "http" +import * as net from "net" + +export const handleUpgrade = (app: express.Express, server: http.Server): void => { + server.on("upgrade", (req, socket, head) => { + socket.pause() + + req.ws = socket + req.head = head + req._ws_handled = false + + // Send the request off to be handled by Express. + ;(app as any).handle(req, new http.ServerResponse(req), () => { + if (!req._ws_handled) { + socket.end("HTTP/1.1 404 Not Found\r\n\r\n") + } + }) + }) +} + +export interface WebsocketRequest extends express.Request { + ws: net.Socket + head: Buffer +} + +interface InternalWebsocketRequest extends WebsocketRequest { + _ws_handled: boolean +} + +export type WebSocketHandler = ( + req: WebsocketRequest, + res: express.Response, + next: express.NextFunction, +) => void | Promise + +export class WebsocketRouter { + public readonly router = express.Router() + + public ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void { + this.router.get( + route, + ...handlers.map((handler) => { + const wrapped: express.Handler = (req, res, next) => { + ;(req as InternalWebsocketRequest)._ws_handled = true + return handler(req as WebsocketRequest, res, next) + } + return wrapped + }), + ) + } +} + +export function Router(): WebsocketRouter { + return new WebsocketRouter() +} diff --git a/test/cli.test.ts b/test/cli.test.ts index f4f6c8849..6b1e96c2f 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,20 +1,37 @@ -import { logger, Level } from "@coder/logger" +import { Level, logger } from "@coder/logger" import * as assert from "assert" +import * as fs from "fs-extra" +import * as net from "net" +import * as os from "os" import * as path from "path" -import { parse } from "../src/node/cli" +import { Args, parse, setDefaults, shouldOpenInExistingInstance } from "../src/node/cli" +import { paths, tmpdir } from "../src/node/util" -describe("cli", () => { +type Mutable = { + -readonly [P in keyof T]: T[P] +} + +describe("parser", () => { beforeEach(() => { delete process.env.LOG_LEVEL + delete process.env.PASSWORD }) - // The parser will always fill these out. + // The parser should not set any defaults so the caller can determine what + // values the user actually set. These are only set after explicitly calling + // `setDefaults`. const defaults = { - _: [], + auth: "password", + host: "localhost", + port: 8080, + "proxy-domain": [], + usingEnvPassword: false, + "extensions-dir": path.join(paths.data, "extensions"), + "user-data-dir": paths.data, } - it("should set defaults", () => { - assert.deepEqual(parse([]), defaults) + it("should parse nothing", () => { + assert.deepEqual(parse([]), { _: [] }) }) it("should parse all available options", () => { @@ -69,7 +86,7 @@ describe("cli", () => { help: true, host: "0.0.0.0", json: true, - log: "trace", + log: "error", open: true, port: 8081, socket: path.resolve("mumble"), @@ -83,19 +100,20 @@ describe("cli", () => { it("should work with short options", () => { assert.deepEqual(parse(["-vvv", "-v"]), { - ...defaults, - log: "trace", + _: [], verbose: true, version: true, }) - assert.equal(process.env.LOG_LEVEL, "trace") - assert.equal(logger.level, Level.Trace) }) - it("should use log level env var", () => { + it("should use log level env var", async () => { + const args = parse([]) + assert.deepEqual(args, { _: [] }) + process.env.LOG_LEVEL = "debug" - assert.deepEqual(parse([]), { + assert.deepEqual(await setDefaults(args), { ...defaults, + _: [], log: "debug", verbose: false, }) @@ -103,8 +121,9 @@ describe("cli", () => { assert.equal(logger.level, Level.Debug) process.env.LOG_LEVEL = "trace" - assert.deepEqual(parse([]), { + assert.deepEqual(await setDefaults(args), { ...defaults, + _: [], log: "trace", verbose: true, }) @@ -113,9 +132,16 @@ describe("cli", () => { }) it("should prefer --log to env var and --verbose to --log", async () => { + let args = parse(["--log", "info"]) + assert.deepEqual(args, { + _: [], + log: "info", + }) + process.env.LOG_LEVEL = "debug" - assert.deepEqual(parse(["--log", "info"]), { + assert.deepEqual(await setDefaults(args), { ...defaults, + _: [], log: "info", verbose: false, }) @@ -123,17 +149,26 @@ describe("cli", () => { assert.equal(logger.level, Level.Info) process.env.LOG_LEVEL = "trace" - assert.deepEqual(parse(["--log", "info"]), { + assert.deepEqual(await setDefaults(args), { ...defaults, + _: [], log: "info", verbose: false, }) assert.equal(process.env.LOG_LEVEL, "info") assert.equal(logger.level, Level.Info) + args = parse(["--log", "info", "--verbose"]) + assert.deepEqual(args, { + _: [], + log: "info", + verbose: true, + }) + process.env.LOG_LEVEL = "warn" - assert.deepEqual(parse(["--log", "info", "--verbose"]), { + assert.deepEqual(await setDefaults(args), { ...defaults, + _: [], log: "trace", verbose: true, }) @@ -141,9 +176,12 @@ describe("cli", () => { assert.equal(logger.level, Level.Trace) }) - it("should ignore invalid log level env var", () => { + it("should ignore invalid log level env var", async () => { process.env.LOG_LEVEL = "bogus" - assert.deepEqual(parse([]), defaults) + assert.deepEqual(await setDefaults(parse([])), { + _: [], + ...defaults, + }) }) it("should error if value isn't provided", () => { @@ -166,7 +204,7 @@ describe("cli", () => { it("should not error if the value is optional", () => { assert.deepEqual(parse(["--cert"]), { - ...defaults, + _: [], cert: { value: undefined, }, @@ -177,7 +215,7 @@ describe("cli", () => { assert.throws(() => parse(["--socket", "--socket-path-value"]), /--socket requires a value/) // If you actually had a path like this you would do this instead: assert.deepEqual(parse(["--socket", "./--socket-path-value"]), { - ...defaults, + _: [], socket: path.resolve("--socket-path-value"), }) assert.throws(() => parse(["--cert", "--socket-path-value"]), /Unknown option --socket-path-value/) @@ -185,7 +223,6 @@ describe("cli", () => { it("should allow positional arguments before options", () => { assert.deepEqual(parse(["foo", "test", "--auth", "none"]), { - ...defaults, _: ["foo", "test"], auth: "none", }) @@ -193,12 +230,150 @@ describe("cli", () => { it("should support repeatable flags", () => { assert.deepEqual(parse(["--proxy-domain", "*.coder.com"]), { - ...defaults, + _: [], "proxy-domain": ["*.coder.com"], }) assert.deepEqual(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), { - ...defaults, + _: [], "proxy-domain": ["*.coder.com", "test.com"], }) }) + + it("should enforce cert-key with cert value or otherwise generate one", async () => { + const args = parse(["--cert"]) + assert.deepEqual(args, { + _: [], + cert: { + value: undefined, + }, + }) + assert.throws(() => parse(["--cert", "test"]), /--cert-key is missing/) + assert.deepEqual(await setDefaults(args), { + _: [], + ...defaults, + cert: { + value: path.join(paths.data, "localhost.crt"), + }, + "cert-key": path.join(paths.data, "localhost.key"), + }) + }) + + it("should override with --link", async () => { + const args = parse("--cert test --cert-key test --socket test --host 0.0.0.0 --port 8888 --link test".split(" ")) + assert.deepEqual(await setDefaults(args), { + _: [], + ...defaults, + auth: "none", + host: "localhost", + link: { + value: "test", + }, + port: 0, + cert: undefined, + "cert-key": path.resolve("test"), + socket: undefined, + }) + }) + + it("should use env var password", async () => { + process.env.PASSWORD = "test" + const args = parse([]) + assert.deepEqual(args, { + _: [], + }) + + assert.deepEqual(await setDefaults(args), { + ...defaults, + _: [], + password: "test", + usingEnvPassword: true, + }) + }) + + it("should filter proxy domains", async () => { + const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"]) + assert.deepEqual(args, { + _: [], + "proxy-domain": ["*.coder.com", "coder.com", "coder.org"], + }) + + assert.deepEqual(await setDefaults(args), { + ...defaults, + _: [], + "proxy-domain": ["coder.com", "coder.org"], + }) + }) +}) + +describe("cli", () => { + let args: Mutable = { _: [] } + const testDir = path.join(tmpdir, "tests/cli") + const vscodeIpcPath = path.join(os.tmpdir(), "vscode-ipc") + + before(async () => { + await fs.remove(testDir) + await fs.mkdirp(testDir) + }) + + beforeEach(async () => { + delete process.env.VSCODE_IPC_HOOK_CLI + args = { _: [] } + await fs.remove(vscodeIpcPath) + }) + + it("should use existing if inside code-server", async () => { + process.env.VSCODE_IPC_HOOK_CLI = "test" + assert.strictEqual(await shouldOpenInExistingInstance(args), "test") + + args.port = 8081 + args._.push("./file") + assert.strictEqual(await shouldOpenInExistingInstance(args), "test") + }) + + it("should use existing if --reuse-window is set", async () => { + args["reuse-window"] = true + assert.strictEqual(await shouldOpenInExistingInstance(args), undefined) + + await fs.writeFile(vscodeIpcPath, "test") + assert.strictEqual(await shouldOpenInExistingInstance(args), "test") + + args.port = 8081 + assert.strictEqual(await shouldOpenInExistingInstance(args), "test") + }) + + it("should use existing if --new-window is set", async () => { + args["new-window"] = true + assert.strictEqual(await shouldOpenInExistingInstance(args), undefined) + + await fs.writeFile(vscodeIpcPath, "test") + assert.strictEqual(await shouldOpenInExistingInstance(args), "test") + + args.port = 8081 + assert.strictEqual(await shouldOpenInExistingInstance(args), "test") + }) + + it("should use existing if no unrelated flags are set, has positional, and socket is active", async () => { + assert.strictEqual(await shouldOpenInExistingInstance(args), undefined) + + args._.push("./file") + assert.strictEqual(await shouldOpenInExistingInstance(args), undefined) + + const socketPath = path.join(testDir, "socket") + await fs.writeFile(vscodeIpcPath, socketPath) + assert.strictEqual(await shouldOpenInExistingInstance(args), undefined) + + await new Promise((resolve) => { + const server = net.createServer(() => { + // Close after getting the first connection. + server.close() + }) + server.once("listening", () => resolve(server)) + server.listen(socketPath) + }) + + assert.strictEqual(await shouldOpenInExistingInstance(args), socketPath) + + args.port = 8081 + assert.strictEqual(await shouldOpenInExistingInstance(args), undefined) + }) }) diff --git a/test/plugin.test.ts b/test/plugin.test.ts new file mode 100644 index 000000000..305cf041a --- /dev/null +++ b/test/plugin.test.ts @@ -0,0 +1,62 @@ +import { logger } from "@coder/logger" +import * as express from "express" +import * as fs from "fs" +import { describe } from "mocha" +import * as path from "path" +import * as supertest from "supertest" +import { PluginAPI } from "../src/node/plugin" +import * as apps from "../src/node/routes/apps" +const fsp = fs.promises + +/** + * Use $LOG_LEVEL=debug to see debug logs. + */ +describe("plugin", () => { + let papi: PluginAPI + let app: express.Application + let agent: supertest.SuperAgentTest + + before(async () => { + papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") + await papi.loadPlugins() + + app = express.default() + papi.mount(app) + + app.use("/api/applications", apps.router(papi)) + + agent = supertest.agent(app) + }) + + it("/api/applications", async () => { + await agent.get("/api/applications").expect(200, [ + { + name: "Test App", + version: "4.0.0", + + description: "This app does XYZ.", + iconPath: "/test-plugin/test-app/icon.svg", + homepageURL: "https://example.com", + path: "/test-plugin/test-app", + + plugin: { + name: "test-plugin", + version: "1.0.0", + modulePath: path.join(__dirname, "test-plugin"), + + displayName: "Test Plugin", + description: "Plugin used in code-server tests.", + routerPath: "/test-plugin", + homepageURL: "https://example.com", + }, + }, + ]) + }) + + it("/test-plugin/test-app", async () => { + const indexHTML = await fsp.readFile(path.join(__dirname, "test-plugin/public/index.html"), { + encoding: "utf8", + }) + await agent.get("/test-plugin/test-app").expect(200, indexHTML) + }) +}) diff --git a/test/socket.test.ts b/test/socket.test.ts index 7d4de985f..b1e974ad0 100644 --- a/test/socket.test.ts +++ b/test/socket.test.ts @@ -45,7 +45,7 @@ describe("SocketProxyProvider", () => { } before(async () => { - const cert = await generateCertificate() + const cert = await generateCertificate("localhost") const options = { cert: fs.readFileSync(cert.cert), key: fs.readFileSync(cert.certKey), diff --git a/test/test-plugin/.gitignore b/test/test-plugin/.gitignore new file mode 100644 index 000000000..1fcb1529f --- /dev/null +++ b/test/test-plugin/.gitignore @@ -0,0 +1 @@ +out diff --git a/test/test-plugin/Makefile b/test/test-plugin/Makefile new file mode 100644 index 000000000..d01aa80a8 --- /dev/null +++ b/test/test-plugin/Makefile @@ -0,0 +1,6 @@ +out/index.js: src/index.ts + # Typescript always emits, even on errors. + yarn build || rm out/index.js + +node_modules: package.json yarn.lock + yarn diff --git a/test/test-plugin/package.json b/test/test-plugin/package.json new file mode 100644 index 000000000..55c474e3d --- /dev/null +++ b/test/test-plugin/package.json @@ -0,0 +1,19 @@ +{ + "private": true, + "name": "test-plugin", + "version": "1.0.0", + "engines": { + "code-server": "^3.7.0" + }, + "main": "out/index.js", + "devDependencies": { + "@types/express": "^4.17.8", + "typescript": "^4.0.5" + }, + "scripts": { + "build": "tsc" + }, + "dependencies": { + "express": "^4.17.1" + } +} diff --git a/test/test-plugin/public/icon.svg b/test/test-plugin/public/icon.svg new file mode 100644 index 000000000..25b9cf047 --- /dev/null +++ b/test/test-plugin/public/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/test-plugin/public/index.html b/test/test-plugin/public/index.html new file mode 100644 index 000000000..3485f18e5 --- /dev/null +++ b/test/test-plugin/public/index.html @@ -0,0 +1,10 @@ + + + + + Test Plugin + + +

    Welcome to the test plugin!

    + + diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts new file mode 100644 index 000000000..fb1869447 --- /dev/null +++ b/test/test-plugin/src/index.ts @@ -0,0 +1,39 @@ +import * as express from "express" +import * as fspath from "path" +import * as pluginapi from "../../../typings/pluginapi" + +export const plugin: pluginapi.Plugin = { + displayName: "Test Plugin", + routerPath: "/test-plugin", + homepageURL: "https://example.com", + description: "Plugin used in code-server tests.", + + init(config) { + config.logger.debug("test-plugin loaded!") + }, + + router() { + const r = express.Router() + r.get("/test-app", (req, res) => { + res.sendFile(fspath.resolve(__dirname, "../public/index.html")) + }) + r.get("/goland/icon.svg", (req, res) => { + res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) + }) + return r + }, + + applications() { + return [ + { + name: "Test App", + version: "4.0.0", + iconPath: "/icon.svg", + path: "/test-app", + + description: "This app does XYZ.", + homepageURL: "https://example.com", + }, + ] + }, +} diff --git a/test/test-plugin/tsconfig.json b/test/test-plugin/tsconfig.json new file mode 100644 index 000000000..0956ead88 --- /dev/null +++ b/test/test-plugin/tsconfig.json @@ -0,0 +1,69 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./out" /* Redirect output structure to the directory. */, + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/test/test-plugin/yarn.lock b/test/test-plugin/yarn.lock new file mode 100644 index 000000000..c77db2f7e --- /dev/null +++ b/test/test-plugin/yarn.lock @@ -0,0 +1,435 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@*": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" + integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@^4.17.8": + version "4.17.8" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" + integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/mime@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + +"@types/node@*": + version "14.14.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.6.tgz#146d3da57b3c636cc0d1769396ce1cfa8991147f" + integrity sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw== + +"@types/qs@*": + version "6.9.5" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/serve-static@*": + version "1.13.6" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.6.tgz#866b1b8dec41c36e28c7be40ac725b88be43c5c1" + integrity sha512-nuRJmv7jW7VmCVTn+IgYDkkbbDGyIINOeu/G0d74X3lm6E5KfMeQPJhxIt1ayQeQB3cSxvYs1RA/wipYoFB4EA== + dependencies: + "@types/mime" "*" + "@types/node" "*" + +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@~2.1.24: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" + integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= diff --git a/test/update.test.ts b/test/update.test.ts index 7e4b80f21..29c558f56 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -2,13 +2,11 @@ import * as assert from "assert" import * as fs from "fs-extra" import * as http from "http" import * as path from "path" -import { LatestResponse, UpdateHttpProvider } from "../src/node/app/update" -import { AuthType } from "../src/node/http" import { SettingsProvider, UpdateSettings } from "../src/node/settings" +import { LatestResponse, UpdateProvider } from "../src/node/update" import { tmpdir } from "../src/node/util" -describe("update", () => { - return +describe.skip("update", () => { let version = "1.0.0" let spy: string[] = [] const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { @@ -35,22 +33,14 @@ describe("update", () => { const jsonPath = path.join(tmpdir, "tests/updates/update.json") const settings = new SettingsProvider(jsonPath) - let _provider: UpdateHttpProvider | undefined - const provider = (): UpdateHttpProvider => { + let _provider: UpdateProvider | undefined + const provider = (): UpdateProvider => { if (!_provider) { const address = server.address() if (!address || typeof address === "string" || !address.port) { throw new Error("unexpected address") } - _provider = new UpdateHttpProvider( - { - auth: AuthType.None, - commit: "test", - }, - true, - `http://${address.address}:${address.port}/latest`, - settings, - ) + _provider = new UpdateProvider(`http://${address.address}:${address.port}/latest`, settings) } return _provider } @@ -154,14 +144,10 @@ describe("update", () => { }) it("should not reject if unable to fetch", async () => { - const options = { - auth: AuthType.None, - commit: "test", - } - let provider = new UpdateHttpProvider(options, true, "invalid", settings) + let provider = new UpdateProvider("invalid", settings) await assert.doesNotReject(() => provider.getUpdate(true)) - provider = new UpdateHttpProvider(options, true, "http://probably.invalid.dev.localhost/latest", settings) + provider = new UpdateProvider("http://probably.invalid.dev.localhost/latest", settings) await assert.doesNotReject(() => provider.getUpdate(true)) }) }) diff --git a/tsconfig.json b/tsconfig.json index ac3a1df52..0db0b1908 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,8 @@ "tsBuildInfoFile": "./.cache/tsbuildinfo", "incremental": true, "rootDir": "./src", - "typeRoots": ["./node_modules/@types", "./typings"] + "typeRoots": ["./node_modules/@types", "./typings"], + "downlevelIteration": true }, "include": ["./src/**/*.ts"] } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts new file mode 100644 index 000000000..06ce35fb4 --- /dev/null +++ b/typings/pluginapi.d.ts @@ -0,0 +1,189 @@ +/** + * This file describes the code-server plugin API for adding new applications. + */ +import { Logger } from "@coder/logger" +import * as express from "express" + +/** + * Overlay + * + * The homepage of code-server will launch into VS Code. However, there will be an overlay + * button that when clicked, will show all available applications with their names, + * icons and provider plugins. When one clicks on an app's icon, they will be directed + * to // to access the application. + */ + +/** + * Plugins + * + * Plugins are just node modules that contain a top level export "plugin" that implements + * the Plugin interface. + * + * 1. code-server uses $CS_PLUGIN to find plugins. + * + * e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load + * /tmp/will and /tmp/teffen as plugins. + * + * 2. code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in + * $CS_PLUGIN_PATH with a package.json where the engine is code-server is + * a valid plugin. + * + * e.g. CS_PLUGIN_PATH=/tmp/nhooyr:/tmp/ash will cause code-server to search + * /tmp/nhooyr and then /tmp/ash for plugins. + * + * CS_PLUGIN_PATH defaults to + * ~/.local/share/code-server/plugins:/usr/share/code-server/plugins + * if unset. + * + * + * 3. Built in plugins are loaded from __dirname/../plugins + * + * Plugins are required as soon as they are found and then initialized. + * See the Plugin interface for details. + * + * If two plugins are found with the exact same name, then code-server will + * use the first one and emit a warning. + * + */ + +/** + * Programmability + * + * There is also a /api/applications endpoint to allow programmatic access to all + * available applications. It could be used to create a custom application dashboard + * for example. An important difference with the API is that all application paths + * will be absolute (i.e have the plugin path prepended) so that they may be used + * directly. + * + * Example output: + * + * [ + * { + * "name": "Test App", + * "version": "4.0.0", + * "iconPath": "/test-plugin/test-app/icon.svg", + * "path": "/test-plugin/test-app", + * "description": "This app does XYZ.", + * "homepageURL": "https://example.com", + * "plugin": { + * "name": "test-plugin", + * "version": "1.0.0", + * "modulePath": "/Users/nhooyr/src/cdr/code-server/test/test-plugin", + * "displayName": "Test Plugin", + * "description": "Plugin used in code-server tests.", + * "routerPath": "/test-plugin", + * "homepageURL": "https://example.com" + * } + * } + * ] + */ + +/** + * Your plugin module must have a top level export "plugin" that implements this interface. + * + * The plugin's router will be mounted at / + */ +export interface Plugin { + /** + * name is used as the plugin's unique identifier. + * No two plugins may share the same name. + * + * Fetched from package.json. + */ + readonly name?: string + + /** + * The version for the plugin in the overlay. + * + * Fetched from package.json. + */ + readonly version?: string + + /** + * Name used in the overlay. + */ + readonly displayName: string + + /** + * Used in overlay. + * Should be a full sentence describing the plugin. + */ + readonly description: string + + /** + * The path at which the plugin router is to be registered. + */ + readonly routerPath: string + + /** + * Link to plugin homepage. + */ + readonly homepageURL: string + + /** + * init is called so that the plugin may initialize itself with the config. + */ + init(config: PluginConfig): void + + /** + * Returns the plugin's router. + * + * Mounted at / + * + * If not present, the plugin provides no routes. + */ + router?(): express.Router + + /** + * code-server uses this to collect the list of applications that + * the plugin can currently provide. + * It is called when /api/applications is hit or the overlay needs to + * refresh the list of applications + * + * Ensure this is as fast as possible. + * + * If not present, the plugin provides no applications. + */ + applications?(): Application[] | Promise +} + +/** + * PluginConfig contains the configuration required for initializing + * a plugin. + */ +export interface PluginConfig { + /** + * All plugin logs should be logged via this logger. + */ + readonly logger: Logger +} + +/** + * Application represents a user accessible application. + */ +export interface Application { + readonly name: string + readonly version: string + + /** + * When the user clicks on the icon in the overlay, they will be + * redirected to // + * where the application should be accessible. + * + * If undefined, then / is used. + */ + readonly path?: string + + readonly description?: string + + /** + * The path at which the icon for this application can be accessed. + * /// + */ + readonly iconPath: string + + /** + * Link to application homepage. + */ + readonly homepageURL: string +} diff --git a/yarn.lock b/yarn.lock index 68221a85d..c2bae13fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,28 +9,24 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" - integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ== - dependencies: - browserslist "^4.12.0" - invariant "^2.2.4" - semver "^5.5.0" +"@babel/compat-data@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.0.tgz#443aea07a5aeba7942cb067de6b8272f2ab36b9e" + integrity sha512-jAbCtMANC9ptXxbSVXIqV/3H0bkh7iyyv6JS5lu10av45bcc2QmDNJXkASZCFwbBt75Q0AEq/BB+bNa3x1QgYQ== "@babel/core@>=7.9.0", "@babel/core@^7.4.4": - version "7.11.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.4.tgz#4301dfdfafa01eeb97f1896c5501a3f0655d4229" - integrity sha512-5deljj5HlqRXN+5oJTY7Zs37iH3z3b++KjiKtIsJy1NrjOOVSEaJHEetLBhyu0aQOSNNZ/0IuEAan9GzRuDXHg== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.0.tgz#e42e07a086e978cdd4c61f4078d8230fb817cc86" + integrity sha512-iV7Gwg0DePKvdDZZWRTkj4MW+6/AbVWd4ZCg+zk8H1RVt5xBpUZS6vLQWwb3pyLg4BFTaGiQCPoJ4Ibmbne4fA== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.4" - "@babel/helper-module-transforms" "^7.11.0" + "@babel/generator" "^7.12.0" + "@babel/helper-module-transforms" "^7.12.0" "@babel/helpers" "^7.10.4" - "@babel/parser" "^7.11.4" + "@babel/parser" "^7.12.0" "@babel/template" "^7.10.4" - "@babel/traverse" "^7.11.0" - "@babel/types" "^7.11.0" + "@babel/traverse" "^7.12.0" + "@babel/types" "^7.12.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" @@ -40,12 +36,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.11.0", "@babel/generator@^7.11.4", "@babel/generator@^7.4.4": - version "7.11.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.4.tgz#1ec7eec00defba5d6f83e50e3ee72ae2fee482be" - integrity sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g== +"@babel/generator@^7.12.0", "@babel/generator@^7.4.4": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.0.tgz#91a45f1c18ca8d895a35a04da1a4cf7ea3f37f98" + integrity sha512-8lnf4QcyiQMf5XQp47BltuMTocsOh6P0z/vueEh8GzhmWWlDbdvOoI5Ziddg0XYhmnx35HyByUW51/9NprF8cA== dependencies: - "@babel/types" "^7.11.0" + "@babel/types" "^7.12.0" jsesc "^2.5.1" source-map "^0.5.0" @@ -65,13 +61,13 @@ "@babel/types" "^7.10.4" "@babel/helper-builder-react-jsx-experimental@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.5.tgz#f35e956a19955ff08c1258e44a515a6d6248646b" - integrity sha512-Buewnx6M4ttG+NLkKyt7baQn7ScC/Td+e99G914fRU8fGIUivDDgVIQeDHFa5e4CRSJQt58WpNHhsAZgtzVhsg== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.0.tgz#e8655888d0d36fd2a15c02decf77923fc18e95cd" + integrity sha512-AFzu6ib4i56olCtulkbIifcTay0O5tv8ZVK8hZMzrpu+YjsIDEcesF1DMqqTzV65clu3X61aE7qeHcJsY/gmnA== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-module-imports" "^7.10.4" - "@babel/types" "^7.10.5" + "@babel/types" "^7.12.0" "@babel/helper-builder-react-jsx@^7.10.4": version "7.10.4" @@ -81,37 +77,36 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-compilation-targets@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" - integrity sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ== +"@babel/helper-compilation-targets@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.0.tgz#c477d89a1f4d626c8149b9b88802f78d66d0c99a" + integrity sha512-NbDFJNjDgxE7IkrHp5gq2+Tr8bEdCLKYN90YDQEjMiTMUAFAcShNkaH8kydcmU0mEQTiQY0Ydy/+1xfS2OCEnw== dependencies: - "@babel/compat-data" "^7.10.4" + "@babel/compat-data" "^7.12.0" + "@babel/helper-validator-option" "^7.12.0" browserslist "^4.12.0" - invariant "^2.2.4" - levenary "^1.1.1" semver "^5.5.0" "@babel/helper-create-class-features-plugin@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" - integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.0.tgz#f3f2fc77bacc89e59ce6764daeabc1fb23e79a05" + integrity sha512-9tD1r9RK928vxvxcoNK8/7uwT7Q2DJZP1dnJmyMAJPwOF0yr8PPwqdpyw33lUpCfrJ765bOs5XNa4KSfUDWFSw== dependencies: "@babel/helper-function-name" "^7.10.4" - "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-member-expression-to-functions" "^7.12.0" "@babel/helper-optimise-call-expression" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.0" "@babel/helper-split-export-declaration" "^7.10.4" "@babel/helper-create-regexp-features-plugin@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" - integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.0.tgz#858cef57039f3b3a9012273597288a71e1dff8ca" + integrity sha512-YBqH+3wLcom+tko8/JLgRcG8DMqORgmjqNRNI751gTioJSZHWFybO1mRoLtJtWIlYSHY+zT9LqqnbbK1c3KIVQ== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-regex" "^7.10.4" - regexpu-core "^4.7.0" + regexpu-core "^4.7.1" "@babel/helper-define-map@^7.10.4": version "7.10.5" @@ -152,12 +147,12 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" - integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== +"@babel/helper-member-expression-to-functions@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.0.tgz#48f605fa801764f3e5b2e301e49d35fe1820c4f3" + integrity sha512-I0d/bgzgzgLsJMk7UZ0TN2KV3OGjC/t/9Saz8PKb9jrcEAXhgjGysOgp4PDKydIKjUv/gj2St4ae+ov8l+T9Xg== dependencies: - "@babel/types" "^7.11.0" + "@babel/types" "^7.12.0" "@babel/helper-module-imports@^7.10.4": version "7.10.4" @@ -166,17 +161,19 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" - integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.0.tgz#8ac7d9e8716f94549a42e577c5429391950e33f3" + integrity sha512-1ZTMoCiLSzTJLbq7mSaTHki4oIrBIf/dUbzdhwTrvtMU3ZNVKwQmGae3gSiqppo7G8HAgnXmc43rfEaD8yYLLQ== dependencies: "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.0" "@babel/helper-simple-access" "^7.10.4" "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/helper-validator-identifier" "^7.10.4" "@babel/template" "^7.10.4" - "@babel/types" "^7.11.0" + "@babel/traverse" "^7.12.0" + "@babel/types" "^7.12.0" lodash "^4.17.19" "@babel/helper-optimise-call-expression@^7.10.4": @@ -208,15 +205,15 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-replace-supers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" - integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== +"@babel/helper-replace-supers@^7.10.4", "@babel/helper-replace-supers@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.0.tgz#98d3f3eb779752e59c7422ab387c9b444323be60" + integrity sha512-9kycFdq2c9e7PXZOr2z/ZqTFF9OzFu287iFwYS+CiDVPuoTCfY8hoTsIqNQNetQjlqoRsRyJFrMG1uhGAR4EEw== dependencies: - "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.12.0" "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/traverse" "^7.12.0" + "@babel/types" "^7.12.0" "@babel/helper-simple-access@^7.10.4": version "7.10.4" @@ -245,6 +242,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== +"@babel/helper-validator-option@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.0.tgz#1d1fc48a9b69763da61b892774b0df89aee1c969" + integrity sha512-NRfKaAQw/JCMsTFUdJI6cp4MoJGGVBRQTRSiW1nwlGldNqzjB9jqWI0SZqQksC724dJoKqwG+QqfS9ib7SoVsw== + "@babel/helper-wrap-function@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" @@ -273,10 +275,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.4", "@babel/parser@^7.4.4": - version "7.11.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.4.tgz#6fa1a118b8b0d80d0267b719213dc947e88cc0ca" - integrity sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA== +"@babel/parser@^7.10.4", "@babel/parser@^7.12.0", "@babel/parser@^7.4.4": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.0.tgz#2ad388f3960045b22f9b7d4bf85e80b15a1c9e3a" + integrity sha512-dYmySMYnlus2jwl7JnnajAj11obRStZoW9cG04wh4ZuhozDn11tDUrhHcUZ9iuNHqALAhh60XqNaYXpvuuE/Gg== "@babel/plugin-proposal-async-generator-functions@^7.10.4": version "7.10.5" @@ -303,10 +305,10 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-export-namespace-from@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" - integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg== +"@babel/plugin-proposal-export-namespace-from@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.0.tgz#08b0f8100bbae1199a5f5294f38a1b0b8d8402fc" + integrity sha512-ao43U2ptSe+mIZAQo2nBV5Wx2Ie3i2XbLt8jCXZpv+bvLY1Twv0lak4YZ1Ps5OwbeLMAl3iOVScgGMOImBae1g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" @@ -319,26 +321,26 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" - integrity sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q== +"@babel/plugin-proposal-logical-assignment-operators@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.0.tgz#830d8ff4984d800b2824e8eac0005ecb7430328e" + integrity sha512-dssjXHzdMQal4q6GCSwDTVPEbyBLdd9+7aSlzAkQbrGEKq5xG8pvhQ7u2ktUrCLRmzQphZnSzILBL5ta4xSRlA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" - integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.0.tgz#d82174a531305df4d7079ce3782269b35b810b82" + integrity sha512-JpNWix2VP2ue31r72fKytTE13nPX1fxl1mudfTaTwcDhl3iExz5NZjQBq012b/BQ6URWoc/onI73pZdYlAfihg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" - integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA== +"@babel/plugin-proposal-numeric-separator@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.0.tgz#76de244152abaf2e72800ab0aebc9771f6de3e9a" + integrity sha512-iON65YmIy/IpEgteYJ4HfO2q30SLdIxiyjNNlsSjSl0tUxLhSH9PljE5r6sczwdW64ZZzznYNcezdcROB+rDDw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" @@ -360,10 +362,10 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" - integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== +"@babel/plugin-proposal-optional-chaining@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.0.tgz#0159b549f165016fc9f284b8607a58a37a3b71fe" + integrity sha512-CXu9aw32FH/MksqdKvhpiH8pSvxnXJ33E7I7BGNE9VzNRpWgpNzvPpds/tW9E0pjmX9+D1zAHRyHbtyeTboo2g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" @@ -620,14 +622,15 @@ "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" - integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== +"@babel/plugin-transform-modules-systemjs@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.0.tgz#bca842db6980cfc98ae7d0f2c907c9b1df3f874e" + integrity sha512-h2fDMnwRwBiNMmTGAWqUo404Z3oLbrPE6hyATecyIbsEsrbM5gjLbfKQLb6hjiouMlGHH+yliYBbc4NPgWKE/g== dependencies: "@babel/helper-hoist-variables" "^7.10.4" - "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-module-transforms" "^7.12.0" "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-identifier" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-umd@^7.10.4": @@ -753,25 +756,26 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/preset-env@^7.4.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" - integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.0.tgz#7d2d0c4f4a14ca0fd7d905a741070ab4745177b7" + integrity sha512-jSIHvHSuF+hBUIrvA2/61yIzhH+ceLOXGLTH1nwPvQlso/lNxXsoE/nvrCzY5M77KRzhKegB1CvdhWPZmYDZ5A== dependencies: - "@babel/compat-data" "^7.11.0" - "@babel/helper-compilation-targets" "^7.10.4" + "@babel/compat-data" "^7.12.0" + "@babel/helper-compilation-targets" "^7.12.0" "@babel/helper-module-imports" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-option" "^7.12.0" "@babel/plugin-proposal-async-generator-functions" "^7.10.4" "@babel/plugin-proposal-class-properties" "^7.10.4" "@babel/plugin-proposal-dynamic-import" "^7.10.4" - "@babel/plugin-proposal-export-namespace-from" "^7.10.4" + "@babel/plugin-proposal-export-namespace-from" "^7.12.0" "@babel/plugin-proposal-json-strings" "^7.10.4" - "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" - "@babel/plugin-proposal-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-logical-assignment-operators" "^7.12.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.0" + "@babel/plugin-proposal-numeric-separator" "^7.12.0" "@babel/plugin-proposal-object-rest-spread" "^7.11.0" "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" - "@babel/plugin-proposal-optional-chaining" "^7.11.0" + "@babel/plugin-proposal-optional-chaining" "^7.12.0" "@babel/plugin-proposal-private-methods" "^7.10.4" "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" @@ -802,7 +806,7 @@ "@babel/plugin-transform-member-expression-literals" "^7.10.4" "@babel/plugin-transform-modules-amd" "^7.10.4" "@babel/plugin-transform-modules-commonjs" "^7.10.4" - "@babel/plugin-transform-modules-systemjs" "^7.10.4" + "@babel/plugin-transform-modules-systemjs" "^7.12.0" "@babel/plugin-transform-modules-umd" "^7.10.4" "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" "@babel/plugin-transform-new-target" "^7.10.4" @@ -819,17 +823,15 @@ "@babel/plugin-transform-unicode-escapes" "^7.10.4" "@babel/plugin-transform-unicode-regex" "^7.10.4" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.11.0" + "@babel/types" "^7.12.0" browserslist "^4.12.0" core-js-compat "^3.6.2" - invariant "^2.2.2" - levenary "^1.1.1" semver "^5.5.0" "@babel/preset-modules@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72" - integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg== + version "0.1.4" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e" + integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" @@ -838,9 +840,9 @@ esutils "^2.0.2" "@babel/runtime@^7.4.4", "@babel/runtime@^7.8.4": - version "7.11.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" - integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.0.tgz#98bd7666186969c04be893d747cf4a6c6c8fa6b0" + integrity sha512-lS4QLXQ2Vbw2ubfQjeQcn+BZgZ5+ROHW9f+DWjEp5Y+NHYmkRGKqHSJ1tuhbUauKu2nhZNTBIvsIQ8dXfY5Gjw== dependencies: regenerator-runtime "^0.13.4" @@ -853,25 +855,25 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.4.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" - integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== +"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.0", "@babel/traverse@^7.4.4": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.0.tgz#ed31953d6e708cdd34443de2fcdb55f72cdfb266" + integrity sha512-ZU9e79xpOukCNPkQ1UzR4gJKCruGckr6edd8v8lmKpSk8iakgUIvb+5ZtaKKV9f7O+x5r+xbMDDIbzVpUoiIuw== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.0" + "@babel/generator" "^7.12.0" "@babel/helper-function-name" "^7.10.4" "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.11.0" - "@babel/types" "^7.11.0" + "@babel/parser" "^7.12.0" + "@babel/types" "^7.12.0" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.4.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" - integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== +"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.12.0", "@babel/types@^7.4.4": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.0.tgz#b6b49f425ee59043fbc89c61b11a13d5eae7b5c6" + integrity sha512-ggIyFmT2zMaYRheOfPDQ4gz7QqV3B+t2rjqjbttDJxMcb7/LukvWCmlIl1sWcOxrvwpTDd+z0OytzqsbGeb3/g== dependencies: "@babel/helper-validator-identifier" "^7.10.4" lodash "^4.17.19" @@ -882,6 +884,22 @@ resolved "https://registry.yarnpkg.com/@coder/logger/-/logger-1.1.16.tgz#ee5b1b188f680733f35c11b065bbd139d618c1e1" integrity sha512-X6VB1++IkosYY6amRAiMvuvCf12NA4+ooX+gOuu5bJIkdjmh4Lz7QpJcWRdgxesvo1msriDDr9E/sDbIWf6vsQ== +"@eslint/eslintrc@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.1.3.tgz#7d1a2b2358552cc04834c0979bd4275362e37085" + integrity sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + lodash "^4.17.19" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + "@iarna/toml@^2.2.0": version "2.2.5" resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" @@ -962,7 +980,7 @@ "@parcel/utils" "^1.11.0" physical-cpu-count "^2.0.0" -"@stylelint/postcss-css-in-js@^0.37.1": +"@stylelint/postcss-css-in-js@^0.37.2": version "0.37.2" resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2" integrity sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA== @@ -995,25 +1013,52 @@ traverse "^0.6.6" unified "^6.1.6" -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/body-parser@*", "@types/body-parser@^1.19.0": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" -"@types/eslint-visitor-keys@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" - integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/cookie-parser@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.2.tgz#e4d5c5ffda82b80672a88a4281aaceefb1bd9df5" + integrity sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg== + dependencies: + "@types/express" "*" + +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== "@types/express-serve-static-core@*": - version "4.17.9" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz#2d7b34dcfd25ec663c25c85d76608f8b249667f1" - integrity sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA== + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" + integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" +"@types/express@*", "@types/express@^4.17.8": + version "4.17.8" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" + integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/fs-extra@^8.0.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068" @@ -1034,15 +1079,20 @@ integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww== "@types/json-schema@^7.0.3": - version "7.0.5" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" - integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + version "7.0.6" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" + integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/mime@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + "@types/minimist@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" @@ -1054,9 +1104,9 @@ integrity sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg== "@types/node@*", "@types/node@^12.12.7": - version "12.12.54" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.54.tgz#a4b58d8df3a4677b6c08bfbc94b7ad7a7a5f82d1" - integrity sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w== + version "12.12.67" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.67.tgz#4f86badb292e822e3b13730a1f9713ed2377f789" + integrity sha512-R48tgL2izApf+9rYNH+3RBMbRpPeW3N8f0I9HMhggeq4UXwBDqumJ14SDs4ctTMhG11pIOduZ4z3QWGOiMc9Vg== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1088,9 +1138,9 @@ integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== "@types/qs@*": - version "6.9.4" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a" - integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ== + version "6.9.5" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== "@types/range-parser@*": version "1.2.3" @@ -1103,9 +1153,39 @@ integrity sha512-1ri+LJhh0gRxIa37IpGytdaW7yDEHeJniBSMD1BmitS07R1j63brcYCzry+l0WJvGdEKQNQ7DYXO2epgborWPw== "@types/semver@^7.1.0": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.3.tgz#3ad6ed949e7487e7bda6f886b4a2434a2c3d7b1a" - integrity sha512-jQxClWFzv9IXdLdhSaTf16XI3NYe6zrEbckSpb5xhKfPbWgIyAY0AFyWWWfaiDcBuj3UHmMkCIwSRqpKMTZL2Q== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb" + integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ== + +"@types/serve-static@*": + version "1.13.5" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.5.tgz#3d25d941a18415d3ab092def846e135a08bbcf53" + integrity sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +"@types/split2@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@types/split2/-/split2-2.1.6.tgz#b095c9e064853824b22c67993d99b066777402b1" + integrity sha512-ddaFSOMuy2Rp97l6q/LEteQygvTQJuEZ+SRhxFKR0uXGsdbFDqX/QF2xoGcOqLQ8XV91v01SnAv2vpgihNgW/Q== + dependencies: + "@types/node" "*" + +"@types/superagent@*": + version "4.1.10" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.10.tgz#5e2cc721edf58f64fe9b819f326ee74803adee86" + integrity sha512-xAgkb2CMWUMCyVc/3+7iQfOEBE75NvuZeezvmixbUw3nmENf2tCnQkW5yQLTYqvXUQ+R6EXxdqKKbal2zM5V/g== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.10": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.10.tgz#630d79b4d82c73e043e43ff777a9ca98d457cab7" + integrity sha512-Xt8TbEyZTnD5Xulw95GLMOkmjGICrOQyJ2jqgkSjAUR3mm7pAIzSR0NFBaMcwlzVvlpCjNwbATcWWwjNiZiFrQ== + dependencies: + "@types/superagent" "*" "@types/tar-fs@^2.0.0": version "2.0.0" @@ -1128,76 +1208,94 @@ integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== "@types/ws@^7.2.6": - version "7.2.6" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.6.tgz#516cbfb818310f87b43940460e065eb912a4178d" - integrity sha512-Q07IrQUSNpr+cXU4E4LtkSIBPie5GLZyyMC1QtQYRLWz701+XcoVygGUZgvLqElq1nU4ICldMYPnexlBsg3dqQ== + version "7.2.7" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.7.tgz#362ad1a1d62721bdb725e72c8cccf357078cf5a3" + integrity sha512-UUFC/xxqFLP17hTva8/lVT0SybLUrfSD9c+iapKb0fEiC8uoDbA+xuZ3pAN603eW+bY8ebSMLm9jXdIPnD0ZgA== dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz#7e061338a1383f59edc204c605899f93dc2e2c8f" - integrity sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ== +"@typescript-eslint/eslint-plugin@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.7.0.tgz#85c9bbda00c0cb604d3c241f7bc7fb171a2d3479" + integrity sha512-li9aiSVBBd7kU5VlQlT1AqP0uWGDK6JYKUQ9cVDnOg34VNnd9t4jr0Yqc/bKxJr/tDCPDaB4KzoSFN9fgVxe/Q== dependencies: - "@typescript-eslint/experimental-utils" "3.10.1" + "@typescript-eslint/experimental-utils" "4.7.0" + "@typescript-eslint/scope-manager" "4.7.0" debug "^4.1.1" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686" - integrity sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw== +"@typescript-eslint/experimental-utils@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.7.0.tgz#8d1058c38bec3d3bbd9c898a1c32318d80faf3c5" + integrity sha512-cymzovXAiD4EF+YoHAB5Oh02MpnXjvyaOb+v+BdpY7lsJXZQN34oIETeUwVT2XfV9rSNpXaIcknDLfupO/tUoA== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/typescript-estree" "3.10.1" + "@typescript-eslint/scope-manager" "4.7.0" + "@typescript-eslint/types" "4.7.0" + "@typescript-eslint/typescript-estree" "4.7.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.10.1.tgz#1883858e83e8b442627e1ac6f408925211155467" - integrity sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw== +"@typescript-eslint/parser@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.7.0.tgz#44bdab0f788b478178368baa65d3365fdc63da1c" + integrity sha512-+meGV8bMP1sJHBI2AFq1GeTwofcGiur8LoIr6v+rEmD9knyCqDlrQcFHR0KDDfldHIFDU/enZ53fla6ReF4wRw== dependencies: - "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "3.10.1" - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/typescript-estree" "3.10.1" - eslint-visitor-keys "^1.1.0" - -"@typescript-eslint/types@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" - integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ== - -"@typescript-eslint/typescript-estree@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853" - integrity sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w== - dependencies: - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/visitor-keys" "3.10.1" + "@typescript-eslint/scope-manager" "4.7.0" + "@typescript-eslint/types" "4.7.0" + "@typescript-eslint/typescript-estree" "4.7.0" debug "^4.1.1" - glob "^7.1.6" + +"@typescript-eslint/scope-manager@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.7.0.tgz#2115526085fb72723ccdc1eeae75dec7126220ed" + integrity sha512-ILITvqwDJYbcDCROj6+Ob0oCKNg3SH46iWcNcTIT9B5aiVssoTYkhKjxOMNzR1F7WSJkik4zmuqve5MdnA0DyA== + dependencies: + "@typescript-eslint/types" "4.7.0" + "@typescript-eslint/visitor-keys" "4.7.0" + +"@typescript-eslint/types@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.7.0.tgz#5e95ef5c740f43d942542b35811f87b62fccca69" + integrity sha512-uLszFe0wExJc+I7q0Z/+BnP7wao/kzX0hB5vJn4LIgrfrMLgnB2UXoReV19lkJQS1a1mHWGGODSxnBx6JQC3Sg== + +"@typescript-eslint/typescript-estree@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.7.0.tgz#539531167f05ba20eb0b6785567076679e29d393" + integrity sha512-5XZRQznD1MfUmxu1t8/j2Af4OxbA7EFU2rbo0No7meb46eHgGkSieFdfV6omiC/DGIBhH9H9gXn7okBbVOm8jw== + dependencies: + "@typescript-eslint/types" "4.7.0" + "@typescript-eslint/visitor-keys" "4.7.0" + debug "^4.1.1" + globby "^11.0.1" is-glob "^4.0.1" lodash "^4.17.15" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931" - integrity sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ== +"@typescript-eslint/visitor-keys@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.7.0.tgz#6783824f22acfc49e754970ed21b88ac03b80e6f" + integrity sha512-aDJDWuCRsf1lXOtignlfiPODkzSxxop7D0rZ91L6ZuMlcMCSh0YyK+gAfo5zN/ih6WxMwhoXgJWC3cWQdaKC+A== dependencies: - eslint-visitor-keys "^1.1.0" + "@typescript-eslint/types" "4.7.0" + eslint-visitor-keys "^2.0.0" abab@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.4.tgz#6dfa57b417ca06d21b2478f0e638302f99c2405c" - integrity sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ== + version "2.0.5" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" + integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" acorn-globals@^4.3.0: version "4.3.4" @@ -1208,9 +1306,9 @@ acorn-globals@^4.3.0: acorn-walk "^6.0.1" acorn-jsx@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" - integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== acorn-walk@^6.0.1: version "6.2.0" @@ -1218,19 +1316,19 @@ acorn-walk@^6.0.1: integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== acorn@^6.0.1, acorn@^6.0.4: - version "6.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" - integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^7.1.1, acorn@^7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" - integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3: - version "6.12.4" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" - integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -1286,12 +1384,11 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - "@types/color-name" "^1.1.1" color-convert "^2.0.1" ansi-to-html@^0.6.4: @@ -1349,6 +1446,11 @@ array-equal@^1.0.0: resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= +array-flatten@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" + integrity sha1-Qmu52oQJDBg42BLIFQryCoMx4pY= + array-includes@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" @@ -1431,6 +1533,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + async-each@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" @@ -1456,7 +1563,7 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -autoprefixer@^9.8.0: +autoprefixer@^9.8.6: version "9.8.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== @@ -1565,7 +1672,7 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^4.0.1: +bl@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== @@ -1584,6 +1691,22 @@ bn.js@^5.1.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== +body-parser@1.19.0, body-parser@^1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + boolbase@^1.0.0, boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -1712,14 +1835,14 @@ browserify-zlib@^0.2.0: pako "~1.0.5" browserslist@^4.0.0, browserslist@^4.1.0, browserslist@^4.12.0, browserslist@^4.8.5: - version "4.14.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.0.tgz#2908951abfe4ec98737b72f34c3bcedc8d43b000" - integrity sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ== + version "4.14.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" + integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== dependencies: - caniuse-lite "^1.0.30001111" - electron-to-chromium "^1.3.523" - escalade "^3.0.2" - node-releases "^1.1.60" + caniuse-lite "^1.0.30001135" + electron-to-chromium "^1.3.571" + escalade "^3.1.0" + node-releases "^1.1.61" buffer-alloc-unsafe@^1.1.0: version "1.1.0" @@ -1776,6 +1899,11 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -1844,10 +1972,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111: - version "1.0.30001118" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001118.tgz#116a9a670e5264aec895207f5e918129174c6f62" - integrity sha512-RNKPLojZo74a0cP7jFMidQI7nvLER40HgNfgKQEJ2PFm225L0ectUungNQoK3Xk3StQcFbpBPNEvoWD59436Hg== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135: + version "1.0.30001148" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001148.tgz#dc97c7ed918ab33bf8706ddd5e387287e015d637" + integrity sha512-E66qcd0KMKZHNJQt9hiLZGE3J4zuTqE1OnU53miEVtylFbwOEmeA5OsRu90noZful+XGSQOni1aT2tiqu/9yYw== caseless@~0.12.0: version "0.12.0" @@ -2058,28 +2186,28 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" - integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== +color-string@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" + integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" color@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" - integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" + integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== dependencies: color-convert "^1.9.1" - color-string "^1.5.2" + color-string "^1.5.4" colorette@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -2101,7 +2229,7 @@ commander@^5.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -component-emitter@^1.2.1: +component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -2136,6 +2264,18 @@ contains-path@^0.1.0: resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + convert-source-map@^1.5.1, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -2143,6 +2283,29 @@ convert-source-map@^1.5.1, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +cookie-parser@^1.4.5: + version "1.4.5" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49" + integrity sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw== + dependencies: + cookie "0.4.0" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +cookiejar@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" + integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -2176,16 +2339,16 @@ cosmiconfig@^5.0.0: js-yaml "^3.13.1" parse-json "^4.0.0" -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== dependencies: "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" + import-fresh "^3.2.1" parse-json "^5.0.0" path-type "^4.0.0" - yaml "^1.7.2" + yaml "^1.10.0" create-ecdh@^4.0.0: version "4.0.4" @@ -2325,9 +2488,9 @@ css-tree@1.0.0-alpha.39: source-map "^0.6.1" css-what@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39" - integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg== + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== cssesc@^3.0.0: version "3.0.0" @@ -2452,13 +2615,27 @@ debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== dependencies: ms "^2.1.1" +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + dependencies: + ms "2.1.2" + decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -2608,9 +2785,9 @@ domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== domelementtype@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" - integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== + version "2.0.2" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.2.tgz#f3b6e549201e46f588b59463dd77187131fe6971" + integrity sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA== domexception@^1.0.1: version "1.0.1" @@ -2635,9 +2812,9 @@ domutils@^1.5.1, domutils@^1.7.0: domelementtype "1" dot-prop@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" - integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== dependencies: is-obj "^2.0.0" @@ -2671,10 +2848,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.523: - version "1.3.549" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.549.tgz#bf500c8eb75a7286a895e34f41aa144384ac613b" - integrity sha512-q09qZdginlqDH3+Y1P6ch5UDTW8nZ1ijwMkxFs15J/DAWOwqolIx8HZH1UP0vReByBigk/dPlU22xS1MaZ+kpQ== +electron-to-chromium@^1.3.571: + version "1.3.581" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.581.tgz#7f796fe92cdc18f5013769dc6f45f4536315a183" + integrity sha512-ALORbI23YkYJoVJWusSdmTq8vXH3TLFzniILE47uZkZOim135ZhoTCM7QlIuvmK78As5kLdANfy7kDIQvJ+iPw== elliptic@^6.5.3: version "6.5.3" @@ -2751,19 +2928,37 @@ error-ex@^1.2.0, error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5: - version "1.17.6" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" - integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== + version "1.17.7" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" + integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" has "^1.0.3" has-symbols "^1.0.1" - is-callable "^1.2.0" - is-regex "^1.1.0" - object-inspect "^1.7.0" + is-callable "^1.2.2" + is-regex "^1.1.1" + object-inspect "^1.8.0" object-keys "^1.1.1" - object.assign "^4.1.0" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-abstract@^1.18.0-next.0: + version "1.18.0-next.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" + integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-negative-zero "^2.0.0" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" string.prototype.trimend "^1.0.1" string.prototype.trimstart "^1.0.1" @@ -2799,10 +2994,10 @@ es6-promisify@^6.0.0: resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.1.1.tgz#46837651b7b06bf6fff893d03f29393668d01621" integrity sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg== -escalade@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" - integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ== +escalade@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== escape-html@~1.0.3: version "1.0.3" @@ -2844,13 +3039,13 @@ escodegen@~1.9.0: source-map "~0.6.1" eslint-config-prettier@^6.0.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1" - integrity sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA== + version "6.12.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.12.0.tgz#9eb2bccff727db1c52104f0b49e87ea46605a0d2" + integrity sha512-9jWPlFlgNwRUYVoujvWTQ1aMO8o6648r+K7qU7K5Jmkbyqav1fuEZC0COYpGBxyiAJb65Ra9hrmFx19xRGwXWw== dependencies: get-stdin "^6.0.0" -eslint-import-resolver-node@^0.3.3: +eslint-import-resolver-node@^0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== @@ -2867,16 +3062,16 @@ eslint-module-utils@^2.6.0: pkg-dir "^2.0.0" eslint-plugin-import@^2.18.2: - version "2.22.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e" - integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg== + version "2.22.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" + integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== dependencies: array-includes "^3.1.1" array.prototype.flat "^1.2.3" contains-path "^0.1.0" debug "^2.6.9" doctrine "1.5.0" - eslint-import-resolver-node "^0.3.3" + eslint-import-resolver-node "^0.3.4" eslint-module-utils "^2.6.0" has "^1.0.3" minimatch "^3.0.4" @@ -2892,12 +3087,12 @@ eslint-plugin-prettier@^3.1.0: dependencies: prettier-linter-helpers "^1.0.0" -eslint-scope@^5.0.0, eslint-scope@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" - integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== +eslint-scope@^5.0.0, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: - esrecurse "^4.1.0" + esrecurse "^4.3.0" estraverse "^4.1.1" eslint-utils@^2.0.0, eslint-utils@^2.1.0: @@ -2912,22 +3107,28 @@ eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== +eslint-visitor-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" + integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + eslint@^7.7.0: - version "7.7.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.7.0.tgz#18beba51411927c4b64da0a8ceadefe4030d6073" - integrity sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg== + version "7.11.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.11.0.tgz#aaf2d23a0b5f1d652a08edacea0c19f7fadc0b3b" + integrity sha512-G9+qtYVCHaDi1ZuWzBsOWo2wSwd70TXnU6UHA3cTYHp7gCTXZcpggWFoUVAMRarg68qtPoNfFbzPh+VdOgmwmw== dependencies: "@babel/code-frame" "^7.0.0" + "@eslint/eslintrc" "^0.1.3" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.0.1" doctrine "^3.0.0" enquirer "^2.3.5" - eslint-scope "^5.1.0" + eslint-scope "^5.1.1" eslint-utils "^2.1.0" - eslint-visitor-keys "^1.3.0" - espree "^7.2.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.0" esquery "^1.2.0" esutils "^2.0.2" file-entry-cache "^5.0.1" @@ -2954,7 +3155,7 @@ eslint@^7.7.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^7.2.0: +espree@^7.3.0: version "7.3.0" resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== @@ -2980,19 +3181,19 @@ esquery@^1.2.0: dependencies: estraverse "^5.1.0" -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: - estraverse "^4.1.0" + estraverse "^5.2.0" -estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -estraverse@^5.1.0: +estraverse@^5.1.0, estraverse@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== @@ -3008,9 +3209,9 @@ etag@~1.8.1: integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= eventemitter3@^4.0.0: - version "4.0.6" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.6.tgz#1258f6fa51b4908aadc2cd624fcd6e64f99f49d6" - integrity sha512-s3GJL04SQoM+gn2c14oyqxvZ3Pcq7cduSDqy3sBFXx6UPSUmgVYwQM9zwkTn9je0lrfg0gHEwR42pF3Q2dCQkQ== + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== events@^3.0.0: version "3.2.0" @@ -3045,6 +3246,43 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +express@^5.0.0-alpha.8: + version "5.0.0-alpha.8" + resolved "https://registry.yarnpkg.com/express/-/express-5.0.0-alpha.8.tgz#b9dd3a568eab791e3391db47f9e6ab91e61b13fe" + integrity sha512-PL8wTLgaNOiq7GpXt187/yWHkrNSfbr4H0yy+V0fpqJt5wpUzBi9DprAkwGKBFOqWHylJ8EyPy34V5u9YArfng== + dependencies: + accepts "~1.3.7" + array-flatten "2.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "3.1.0" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-is-absolute "1.0.1" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + router "2.0.0-alpha.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -3121,7 +3359,7 @@ fast-glob@^2.2.2: merge2 "^1.2.3" micromatch "^3.1.10" -fast-glob@^3.1.1: +fast-glob@^3.1.1, fast-glob@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== @@ -3143,6 +3381,16 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-safe-stringify@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" + integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== + +fastest-levenshtein@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" + integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== + fastparse@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" @@ -3196,6 +3444,19 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + find-up@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -3236,9 +3497,9 @@ flat-cache@^2.0.1: write "1.0.3" flat@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" - integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== + version "4.1.1" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.1.tgz#a392059cc382881ff98642f5da4dde0a959f309b" + integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== dependencies: is-buffer "~2.0.3" @@ -3267,6 +3528,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" + integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -3281,6 +3551,16 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs= +formidable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -3400,7 +3680,7 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= -glob@7.1.6, glob@^7.0.0, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@7.1.6, glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -3676,6 +3956,17 @@ htmlparser2@~3.9.2: inherits "^2.0.1" readable-stream "^2.0.2" +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-errors@~1.7.2: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" @@ -3750,7 +4041,7 @@ import-fresh@^2.0.0: caller-path "^2.0.0" resolve-from "^3.0.0" -import-fresh@^3.0.0, import-fresh@^3.1.0: +import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== @@ -3806,12 +4097,10 @@ ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -invariant@^2.2.2, invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== is-absolute-url@^2.0.0: version "2.1.0" @@ -3894,10 +4183,10 @@ is-buffer@^2.0.0, is-buffer@~2.0.3: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== -is-callable@^1.1.4, is-callable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" - integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== +is-callable@^1.1.4, is-callable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" + integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== is-color-stop@^1.0.0: version "1.1.0" @@ -4016,6 +4305,11 @@ is-map@^2.0.1: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== +is-negative-zero@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" + integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -4050,7 +4344,7 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-regex@^1.1.0: +is-regex@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== @@ -4166,7 +4460,7 @@ iterate-value@^1.0.0: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -4232,9 +4526,9 @@ json-parse-better-errors@^1.0.1: integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== json-parse-even-better-errors@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.0.tgz#371873c5ffa44304a6ba12419bcfa95f404ae081" - integrity sha512-o3aP+RsWDJZayj1SbHNQAI8x0v3T3SKiGoZlNYfbUP1S3omJQ6i9CnqADqkSPaOAxwua4/1YWx5CM7oiChJt2Q== + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-schema-traverse@^0.4.1: version "0.4.1" @@ -4327,18 +4621,6 @@ leaked-handles@^5.2.0: weakmap-shim "^1.1.0" xtend "^4.0.0" -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -levenary@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77" - integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ== - dependencies: - leven "^3.1.0" - levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -4425,7 +4707,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -4449,13 +4731,6 @@ longest-streak@^2.0.1: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - magic-string@^0.22.4: version "0.22.5" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e" @@ -4542,10 +4817,15 @@ mdn-data@2.0.6: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== -meow@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.0.tgz#50ecbcdafa16f8b58fb7eb9675b933f6473b3a59" - integrity sha512-kq5F0KVteskZ3JdfyQFivJEj2RaA8NFsS4+r9DaMKLcUHpk5OcHS3Q0XkCXONB1mZRPsu/Y/qImKri0nwSEZog== +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +meow@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306" + integrity sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA== dependencies: "@types/minimist" "^1.2.0" camelcase-keys "^6.2.2" @@ -4559,6 +4839,11 @@ meow@^7.0.1: type-fest "^0.13.1" yargs-parser "^18.1.3" +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + merge-source-map@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.0.4.tgz#a5de46538dae84d4114cc5ea02b4772a6346701f" @@ -4571,6 +4856,11 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +methods@1.1.2, methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -4611,7 +4901,7 @@ mime-db@1.44.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== @@ -4623,6 +4913,11 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^2.4.6: + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -4705,9 +5000,9 @@ mkdirp@^1.0.3: integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== mocha@^8.1.2: - version "8.1.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.2.tgz#d67fad13300e4f5cd48135a935ea566f96caf827" - integrity sha512-I8FRAcuACNMLQn3lS4qeWLxXqLvGf6r2CaLstDpZmMUUSmvW6Cnm1AuHxgbc7ctZVRcfwspCRbDHymPsi3dkJw== + version "8.1.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.3.tgz#5e93f873e35dfdd69617ea75f9c68c2ca61c2ac5" + integrity sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw== dependencies: ansi-colors "4.1.1" browser-stdout "1.3.1" @@ -4751,9 +5046,9 @@ ms@2.1.2, ms@^2.1.1: integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== nan@^2.12.1: - version "2.14.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" - integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== + version "2.14.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" + integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== nanomatch@^1.2.9: version "1.2.13" @@ -4777,6 +5072,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -4821,10 +5121,10 @@ node-libs-browser@^2.0.0: util "^0.11.0" vm-browserify "^1.0.1" -node-releases@^1.1.60: - version "1.1.60" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" - integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== +node-releases@^1.1.61: + version "1.1.63" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.63.tgz#db6dbb388544c31e888216304e8fd170efee3ff5" + integrity sha512-ukW3iCfQaoxJkSPN+iK7KznTeqDGVJatAEuXsJERYHa9tn/KaT5lBdIyxQjLEVTzSkyjJEuQ17/vaEjrOauDkg== normalize-html-whitespace@^1.0.0: version "1.0.0" @@ -4904,7 +5204,7 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.7.0: +object-inspect@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== @@ -4926,7 +5226,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@4.1.0, object.assign@^4.1.0: +object.assign@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== @@ -4936,6 +5236,16 @@ object.assign@4.1.0, object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" +object.assign@^4.1.0, object.assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" + integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.18.0-next.0" + has-symbols "^1.0.1" + object-keys "^1.1.1" + object.getownpropertydescriptors@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" @@ -5241,7 +5551,7 @@ parse5@5.1.0: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== -parseurl@~1.3.3: +parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -5271,7 +5581,7 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: +path-is-absolute@1.0.1, path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= @@ -5291,6 +5601,11 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + path-type@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" @@ -5362,9 +5677,9 @@ posix-character-classes@^0.1.0: integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= postcss-calc@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.3.tgz#d65cca92a3c52bf27ad37a5f732e0587b74f1623" - integrity sha512-IB/EAEmZhIMEIhG7Ov4x+l47UaXOS1n2f4FBUk/aKllQhtSCxWhTzn0nJgkqN7fo/jcWySvWTSB6Syk9L+31bA== + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e" + integrity sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg== dependencies: postcss "^7.0.27" postcss-selector-parser "^6.0.2" @@ -5639,16 +5954,6 @@ postcss-reduce-transforms@^4.0.2: postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-reporter@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-6.0.1.tgz#7c055120060a97c8837b4e48215661aafb74245f" - integrity sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw== - dependencies: - chalk "^2.4.1" - lodash "^4.17.11" - log-symbols "^2.2.0" - postcss "^7.0.7" - postcss-resolve-nested-selector@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" @@ -5676,7 +5981,7 @@ postcss-scss@^2.1.1: dependencies: postcss "^7.0.6" -postcss-selector-parser@6.0.2, postcss-selector-parser@^6.0.2: +postcss-selector-parser@6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== @@ -5694,6 +5999,16 @@ postcss-selector-parser@^3.0.0: indexes-of "^1.0.1" uniq "^1.0.1" +postcss-selector-parser@^6.0.2: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" + integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + util-deprecate "^1.0.2" + postcss-svgo@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" @@ -5737,7 +6052,7 @@ postcss@6.0.1: source-map "^0.5.6" supports-color "^3.2.3" -postcss@7.0.32, postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.11, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.6, postcss@^7.0.7: +postcss@7.0.32: version "7.0.32" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== @@ -5755,6 +6070,15 @@ postcss@^6.0.1: source-map "^0.6.1" supports-color "^5.4.0" +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.11, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.6: + version "7.0.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" + integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + posthtml-parser@^0.4.0, posthtml-parser@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.4.2.tgz#a132bbdf0cd4bc199d34f322f5c1599385d7c6c1" @@ -5783,9 +6107,9 @@ posthtml@^0.11.2: posthtml-render "^1.1.5" posthtml@^0.13.1: - version "0.13.3" - resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.13.3.tgz#9702d745108d532a9d5808985e0dafd81b09f7bd" - integrity sha512-5NL2bBc4ihAyoYnY0EAQrFQbJNE1UdvgC1wjYts0hph7jYeU2fa5ki3/9U45ce9V6M1vLMEgUX2NXe/bYL+bCQ== + version "0.13.4" + resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.13.4.tgz#ad81b3fa62b85f81ccdb5710f4ec375a4ed94934" + integrity sha512-i2oTo/+dwXGC6zaAQSF6WZEQSbEqu10hsvg01DWzGAfZmy31Iiy9ktPh9nnXDfZiYytjxTIvxoK4TI0uk4QWpw== dependencies: posthtml-parser "^0.5.0" posthtml-render "^1.2.3" @@ -5808,9 +6132,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^2.0.5: - version "2.1.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6" - integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw== + version "2.1.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" + integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== process-nextick-args@~2.0.0: version "2.0.1" @@ -5843,6 +6167,14 @@ promise.allsettled@1.0.2: function-bind "^1.1.1" iterate-value "^1.0.0" +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -5898,6 +6230,16 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@^6.9.4: + version "6.9.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" + integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -5947,6 +6289,16 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + read-pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -5996,7 +6348,7 @@ readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.3, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -6071,10 +6423,10 @@ regexpp@^3.0.0, regexpp@^3.1.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== -regexpu-core@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938" - integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ== +regexpu-core@^4.7.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" + integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== dependencies: regenerate "^1.4.0" regenerate-unicode-properties "^8.2.0" @@ -6334,12 +6686,25 @@ rotating-file-stream@^2.1.1: resolved "https://registry.yarnpkg.com/rotating-file-stream/-/rotating-file-stream-2.1.3.tgz#4b3cc8f56ae70b3e30ccdb4ee6b14d95e66b02bb" integrity sha512-zZ4Tkngxispo7DgiTqX0s4ChLtM3qET6iYsDA9tmgDEqJ3BFgRq/ZotsKEDAYQt9pAn9JwwqT27CSwQt3CTxNg== +router@2.0.0-alpha.1: + version "2.0.0-alpha.1" + resolved "https://registry.yarnpkg.com/router/-/router-2.0.0-alpha.1.tgz#9188213b972215e03ef830e0ac77837870085f6d" + integrity sha512-fz/T/qLkJM6RTtbqGqA1+uZ88ejqJoPyKeJAeXPYjebA7HzV/UyflH4gXWqW/Y6SERnp4kDwNARjqy6se3PcOw== + dependencies: + array-flatten "2.1.1" + debug "3.1.0" + methods "~1.1.2" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + setprototypeof "1.1.0" + utils-merge "1.0.1" + run-parallel@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.2.0: +safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -6421,7 +6786,7 @@ serialize-to-js@^3.0.0: resolved "https://registry.yarnpkg.com/serialize-to-js/-/serialize-to-js-3.1.1.tgz#b3e77d0568ee4a60bfe66287f991e104d3a1a4ac" integrity sha512-F+NGU0UHMBO4Q965tjw7rvieNVjlH6Lqi2emq/Lc9LUURYJbiCzmpi4Cy1OOjjVPtxu0c+NE85LU6968Wko5ZA== -serve-static@^1.12.4: +serve-static@1.14.1, serve-static@^1.12.4: version "1.14.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== @@ -6451,6 +6816,11 @@ setimmediate@^1.0.4: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + setprototypeof@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" @@ -6519,6 +6889,15 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -6605,9 +6984,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" - integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + version "3.0.6" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" + integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== specificity@^0.4.1: version "0.4.1" @@ -6621,6 +7000,13 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +split2@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -6827,7 +7213,7 @@ strip-json-comments@3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== -strip-json-comments@^3.1.0: +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -6859,18 +7245,20 @@ stylelint-config-recommended@^3.0.0: integrity sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ== stylelint@^13.0.0: - version "13.6.1" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.6.1.tgz#cc1d76338116d55e8ff2be94c4a4386c1239b878" - integrity sha512-XyvKyNE7eyrqkuZ85Citd/Uv3ljGiuYHC6UiztTR6sWS9rza8j3UeQv/eGcQS9NZz/imiC4GKdk1EVL3wst5vw== + version "13.7.2" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.7.2.tgz#6f3c58eea4077680ed0ceb0d064b22b100970486" + integrity sha512-mmieorkfmO+ZA6CNDu1ic9qpt4tFvH2QUB7vqXgrMVHe5ENU69q7YDq0YUg/UHLuCsZOWhUAvcMcLzLDIERzSg== dependencies: - "@stylelint/postcss-css-in-js" "^0.37.1" + "@stylelint/postcss-css-in-js" "^0.37.2" "@stylelint/postcss-markdown" "^0.36.1" - autoprefixer "^9.8.0" + autoprefixer "^9.8.6" balanced-match "^1.0.0" chalk "^4.1.0" - cosmiconfig "^6.0.0" + cosmiconfig "^7.0.0" debug "^4.1.1" execall "^2.0.0" + fast-glob "^3.2.4" + fastest-levenshtein "^1.0.12" file-entry-cache "^5.0.1" get-stdin "^8.0.0" global-modules "^2.0.0" @@ -6881,18 +7269,16 @@ stylelint@^13.0.0: import-lazy "^4.0.0" imurmurhash "^0.1.4" known-css-properties "^0.19.0" - leven "^3.1.0" - lodash "^4.17.15" + lodash "^4.17.20" log-symbols "^4.0.0" mathml-tag-names "^2.1.3" - meow "^7.0.1" + meow "^7.1.1" micromatch "^4.0.2" normalize-selector "^0.2.0" postcss "^7.0.32" postcss-html "^0.36.0" postcss-less "^3.1.4" postcss-media-query-parser "^0.2.3" - postcss-reporter "^6.0.1" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^4.0.2" postcss-sass "^0.4.4" @@ -6908,7 +7294,7 @@ stylelint@^13.0.0: style-search "^0.1.0" sugarss "^2.0.0" svg-tags "^1.0.0" - table "^5.4.6" + table "^6.0.1" v8-compile-cache "^2.1.1" write-file-atomic "^3.0.3" @@ -6919,7 +7305,32 @@ sugarss@^2.0.0: dependencies: postcss "^7.0.2" -supports-color@7.1.0, supports-color@^7.1.0: +superagent@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-6.1.0.tgz#09f08807bc41108ef164cfb4be293cebd480f4a6" + integrity sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.2" + debug "^4.1.1" + fast-safe-stringify "^2.0.7" + form-data "^3.0.0" + formidable "^1.2.2" + methods "^1.1.2" + mime "^2.4.6" + qs "^6.9.4" + readable-stream "^3.6.0" + semver "^7.3.2" + +supertest@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.0.1.tgz#f6b54370de85c45d6557192c8d7df604ca2c9e18" + integrity sha512-8yDNdm+bbAN/jeDdXsRipbq9qMpVF7wRsbwLgsANHqdjPsCoecmlTuqEcLQMGpmojFBhxayZ0ckXmLXYq7e+0g== + dependencies: + methods "1.1.2" + superagent "6.1.0" + +supports-color@7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== @@ -6952,6 +7363,13 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + svg-tags@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" @@ -6981,7 +7399,7 @@ symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -table@^5.2.3, table@^5.4.6: +table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== @@ -6991,6 +7409,16 @@ table@^5.2.3, table@^5.4.6: slice-ansi "^2.1.0" string-width "^3.0.0" +table@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/table/-/table-6.0.3.tgz#e5b8a834e37e27ad06de2e0fda42b55cfd8a0123" + integrity sha512-8321ZMcf1B9HvVX/btKv8mMZahCjn2aYrDlpqHaBFCfnox64edeH9kEid0vTLTRR8gWR2A20aDgeuTTea4sVtw== + dependencies: + ajv "^6.12.4" + lodash "^4.17.20" + slice-ansi "^4.0.0" + string-width "^4.2.0" + tar-fs@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" @@ -7002,11 +7430,11 @@ tar-fs@^2.0.0: tar-stream "^2.0.0" tar-stream@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.3.tgz#1e2022559221b7866161660f118255e20fa79e41" - integrity sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA== + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" + integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== dependencies: - bl "^4.0.1" + bl "^4.0.3" end-of-stream "^1.4.1" fs-constants "^1.0.0" inherits "^2.0.3" @@ -7186,9 +7614,9 @@ tsconfig-paths@^3.9.0: strip-bom "^3.0.0" tslib@^1.8.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tsutils@^3.17.1: version "3.17.1" @@ -7243,6 +7671,14 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -7442,6 +7878,11 @@ universalify@^1.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + unquote@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" @@ -7466,9 +7907,9 @@ update-section@^0.3.0: integrity sha1-RY8Xgg03gg3GDiC4bZQ5GwASMVg= uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + version "4.4.0" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== dependencies: punycode "^2.1.0" @@ -7490,7 +7931,7 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -7519,6 +7960,11 @@ util@^0.11.0: dependencies: inherits "2.0.3" +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -7537,6 +7983,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + vendors@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" @@ -7775,7 +8226,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.7.2: +yaml@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== @@ -7849,9 +8300,9 @@ yargs@^14.2.3: yargs-parser "^15.0.1" yarn@^1.22.4: - version "1.22.4" - resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.4.tgz#01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e" - integrity sha512-oYM7hi/lIWm9bCoDMEWgffW8aiNZXCWeZ1/tGy0DWrN6vmzjCXIKu2Y21o8DYVBUtiktwKcNoxyGl/2iKLUNGA== + version "1.22.10" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.10.tgz#c99daa06257c80f8fa2c3f1490724e394c26b18c" + integrity sha512-IanQGI9RRPAN87VGTF7zs2uxkSyQSrSPsju0COgbsKQOOXr5LtcVPeyXWgwVa0ywG3d8dg6kSYKGBuYK021qeA== yn@3.1.1: version "3.1.1"