├── .dockerignore ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── extension_bug.md │ ├── feature_request.md │ └── question.md └── pull_request_template.md ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── build └── tasks.ts ├── doc ├── admin │ └── install │ │ ├── aws.md │ │ ├── digitalocean.md │ │ └── google_cloud.md ├── assets │ ├── aws_ubuntu.png │ ├── chrome_confirm.png │ ├── chrome_warning.png │ ├── cli.png │ ├── ide.png │ ├── logo-horizontal.png │ └── server-password-modal.png ├── security │ └── ssl.md └── self-hosted │ └── index.md ├── package.json ├── packages ├── app │ ├── browser │ │ ├── package.json │ │ ├── src │ │ │ ├── app.html │ │ │ ├── app.scss │ │ │ └── app.ts │ │ ├── webpack.config.js │ │ └── yarn.lock │ ├── chrome │ │ ├── icon_128.png │ │ ├── manifest.json │ │ ├── package.json │ │ ├── src │ │ │ ├── background.ts │ │ │ ├── chome.ts │ │ │ ├── content.ts │ │ │ └── index.html │ │ ├── webpack.config.js │ │ └── yarn.lock │ └── common │ │ ├── package.json │ │ ├── src │ │ ├── app.scss │ │ ├── app.tsx │ │ ├── connection.ts │ │ ├── containers.tsx │ │ ├── fonts │ │ │ ├── AktivGroteskBold.eot │ │ │ ├── AktivGroteskBold.ttf │ │ │ ├── AktivGroteskBold.woff │ │ │ ├── AktivGroteskBold.woff2 │ │ │ ├── AktivGroteskMedium.eot │ │ │ ├── AktivGroteskMedium.ttf │ │ │ ├── AktivGroteskMedium.woff │ │ │ ├── AktivGroteskMedium.woff2 │ │ │ ├── AktivGroteskRegular.eot │ │ │ ├── AktivGroteskRegular.ttf │ │ │ ├── AktivGroteskRegular.woff │ │ │ └── AktivGroteskRegular.woff2 │ │ ├── storage.ts │ │ └── tooltip.scss │ │ └── yarn.lock ├── disposable │ ├── package.json │ ├── src │ │ ├── disposable.ts │ │ └── index.ts │ └── yarn.lock ├── dns │ ├── .gcloudignore │ ├── Dockerfile │ ├── app.yaml │ ├── package.json │ ├── src │ │ ├── dns.ts │ │ └── words.ts │ ├── webpack.config.js │ └── yarn.lock ├── events │ ├── package.json │ ├── src │ │ ├── events.ts │ │ └── index.ts │ └── yarn.lock ├── ide-api │ ├── README.md │ ├── api.d.ts │ ├── package.json │ └── yarn.lock ├── ide │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── fill │ │ │ ├── child_process.ts │ │ │ ├── client.ts │ │ │ ├── clipboard.ts │ │ │ ├── dialog.scss │ │ │ ├── dialog.ts │ │ │ ├── electron.ts │ │ │ ├── empty.ts │ │ │ ├── fs.ts │ │ │ ├── net.ts │ │ │ ├── notification.ts │ │ │ ├── os.ts │ │ │ ├── path.js │ │ │ └── util.ts │ │ ├── index.ts │ │ ├── retry.ts │ │ └── upload.ts │ ├── test │ │ ├── child_process.test.ts │ │ ├── forker.js │ │ ├── fs.test.ts │ │ └── net.test.ts │ └── yarn.lock ├── logger │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── logger.test.ts │ │ └── logger.ts │ ├── tsconfig.build.json │ ├── webpack.config.js │ └── yarn.lock ├── package.json ├── protocol │ ├── package.json │ ├── scripts │ │ └── generate_proto.sh │ ├── src │ │ ├── browser │ │ │ └── client.ts │ │ ├── common │ │ │ ├── connection.ts │ │ │ ├── helpers.ts │ │ │ └── util.ts │ │ ├── index.ts │ │ ├── node │ │ │ ├── evaluate.ts │ │ │ └── server.ts │ │ └── proto │ │ │ ├── client.proto │ │ │ ├── client_pb.d.ts │ │ │ ├── client_pb.js │ │ │ ├── index.ts │ │ │ ├── node.proto │ │ │ ├── node_pb.d.ts │ │ │ ├── node_pb.js │ │ │ ├── vscode.proto │ │ │ ├── vscode_pb.d.ts │ │ │ └── vscode_pb.js │ ├── test │ │ ├── evaluate.test.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ └── server.test.ts │ └── yarn.lock ├── requirefs │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── requirefs.ts │ │ └── tarReader.ts │ ├── test │ │ ├── .gitignore │ │ ├── lib │ │ │ ├── chained-1.js │ │ │ ├── chained-2.js │ │ │ ├── chained-3.js │ │ │ ├── customModule.js │ │ │ ├── individual.js │ │ │ ├── nodeResolve.js │ │ │ ├── node_modules │ │ │ │ └── frogger │ │ │ │ │ └── index.js │ │ │ ├── scope.js │ │ │ ├── subfolder.js │ │ │ └── subfolder │ │ │ │ ├── goingUp.js │ │ │ │ └── oranges.js │ │ ├── requirefs.bench.ts │ │ ├── requirefs.test.ts │ │ └── requirefs.util.ts │ └── yarn.lock ├── runner │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── runner.ts │ └── yarn.lock ├── server │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── scripts │ │ └── nexe.js │ ├── src │ │ ├── cli.ts │ │ ├── constants.ts │ │ ├── fill.ts │ │ ├── ipc.ts │ │ ├── modules.ts │ │ ├── portScanner.ts │ │ ├── server.ts │ │ └── vscode │ │ │ ├── bootstrapFork.ts │ │ │ └── sharedProcess.ts │ ├── webpack.config.js │ └── yarn.lock ├── tsconfig.json ├── tunnel │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── common.ts │ │ └── server.ts │ └── yarn.lock ├── vscode │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── dialog.scss │ │ ├── dialog.ts │ │ ├── fill │ │ │ ├── amd.ts │ │ │ ├── codeEditor.ts │ │ │ ├── css.js │ │ │ ├── dom.ts │ │ │ ├── environmentService.ts │ │ │ ├── graceful-fs.ts │ │ │ ├── iconv-lite.ts │ │ │ ├── labels.ts │ │ │ ├── menuRegistry.ts │ │ │ ├── mouseEvent.ts │ │ │ ├── native-keymap.ts │ │ │ ├── native-watchdog.ts │ │ │ ├── node-pty.ts │ │ │ ├── package.ts │ │ │ ├── paste.ts │ │ │ ├── paths.ts │ │ │ ├── platform.ts │ │ │ ├── product.ts │ │ │ ├── ripgrep.ts │ │ │ ├── spdlog.ts │ │ │ ├── stdioElectron.ts │ │ │ ├── storageDatabase.ts │ │ │ ├── vscodeTextmate.ts │ │ │ ├── windowsService.ts │ │ │ ├── workbenchRegistry.ts │ │ │ ├── workspacesService.ts │ │ │ └── zip.ts │ │ ├── index.ts │ │ ├── vscode-coder.svg │ │ ├── vscode.scss │ │ └── workbench.ts │ ├── test │ │ └── node-pty.test.ts │ ├── webpack.bootstrap.config.js │ └── yarn.lock ├── web │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── index.html │ │ ├── index.scss │ │ └── index.ts │ ├── webpack.config.js │ └── yarn.lock └── yarn.lock ├── rules ├── src │ ├── curlyStatementNewlinesRule.ts │ └── noBlockPaddingRule.ts └── tsconfig.json ├── scripts ├── build.sh ├── dummy.js ├── install-packages.ts ├── test-setup.js ├── vscode.patch ├── webpack.client.config.js ├── webpack.general.config.js └── webpack.node.config.js ├── tsconfig.json ├── tslint.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @coderasher @kylecarbs 2 | Dockerfile @nhooyr 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report problems and unexpected behavior. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | 12 | - `code-server` version: 13 | - OS Version: 14 | 15 | #### Steps to Reproduce 16 | 17 | 1. 18 | 2. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/extension_bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Extension Bug 3 | about: Report problems and unexpected behavior with extensions. 4 | title: '' 5 | labels: 'extension-specific' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | - `code-server` version: 12 | - OS Version: 13 | - Extension: 14 | 15 | #### Steps to Reproduce 16 | 17 | 1. 18 | 2. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | #### Description 12 | 13 | 14 | 15 | #### Related Issues 16 | 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Describe in detail the problem you had and how this PR fixes it 4 | 5 | ### Is there an open issue you can link to? 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | dist 4 | out 5 | .DS_Store 6 | release 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8.10.0 4 | env: 5 | - VERSION="1.32.0-$TRAVIS_BUILD_NUMBER" 6 | matrix: 7 | include: 8 | - os: linux 9 | dist: ubuntu 10 | - os: osx 11 | before_install: 12 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install libxkbfile-dev 13 | libsecret-1-dev; fi 14 | script: 15 | - scripts/build.sh 16 | before_deploy: 17 | - echo "$VERSION" "$TRAVIS_COMMIT" 18 | - git config --local user.name "$USER_NAME" 19 | - git config --local user.email "$USER_EMAIL" 20 | - git tag "$VERSION" "$TRAVIS_COMMIT" 21 | - yarn task package "$VERSION" 22 | deploy: 23 | provider: releases 24 | file_glob: true 25 | draft: true 26 | tag_name: "$VERSION" 27 | target_commitish: "$TRAVIS_COMMIT" 28 | name: "$VERSION" 29 | skip_cleanup: true 30 | api_key: 31 | secure: YL/x24KjYjgYXPcJWk3FV7FGxI79Mh6gBECQEcdlf3fkLEoKFVgzHBoUNWrFPzyR4tgLyWNAgcpD9Lkme1TRWTom7UPjXcwMNyLcLa+uec7ciSAnYD9ntLTpiCuPDD1u0LtRGclSi/EHQ+F8YVq+HZJpXTsJeAmOmihma3GVbGKSZr+BRum+0YZSG4w+o4TOlYzw/4bLWS52MogZcwpjd+hemBbgXLuGU2ziKv2vEKCZFbEeA16II4x1WLI4mutDdCeh7+3aLzGLwDa49NxtsVYNjyNFF75JhCTCNA55e2YMiLz9Uq69IXe/mi5F7xUaFfhIqqLNyKBnKeEOzu3dYnc+8n3LjnQ+00PmkF05nx9kBn3UfV1kwQGh6QbyDmTtBP07rtUMyI14aeQqHjxsaVRdMnwj9Q2DjXRr8UDqESZF0rmK3pHCXS2fBhIzLE8tLVW5Heiba2pQRFMHMZW+KBE97FzcFh7is90Ait3T8enfcd/PWFPYoBejDAdjwxwOkezh5N5ZkYquEfDYuWrFi6zRFCktsruaAcA+xGtTf9oilBBzUqu8Ie+YFWH5me83xakcblJWdaW/D2rLJAJH3m6LFm8lBqyUgDX5t/etob6CpDuYHu5D1J3XINOj/+aLAcadq6qlh70PMZS3zYffUu3JlzaD2amlSHIT8b5YXFc= 32 | file: 33 | - release/*.tar.gz 34 | - release/*.zip 35 | on: 36 | repo: codercom/code-server 37 | branch: master 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.15.0 2 | 3 | # Install VS Code's deps. These are the only two it seems we need. 4 | RUN apt-get update && apt-get install -y \ 5 | libxkbfile-dev \ 6 | libsecret-1-dev 7 | 8 | # Ensure latest yarn. 9 | RUN npm install -g yarn@1.13 10 | 11 | WORKDIR /src 12 | COPY . . 13 | 14 | # In the future, we can use https://github.com/yarnpkg/rfcs/pull/53 to make yarn use the node_modules 15 | # directly which should be fast as it is slow because it populates its own cache every time. 16 | RUN yarn && yarn task build:server:binary 17 | 18 | # We deploy with ubuntu so that devs have a familiar environemnt. 19 | FROM ubuntu:18.10 20 | WORKDIR /root/project 21 | COPY --from=0 /src/packages/server/cli-linux-x64 /usr/local/bin/code-server 22 | EXPOSE 8443 23 | RUN apt-get update && apt-get install -y \ 24 | openssl \ 25 | net-tools 26 | RUN apt-get install -y locales && \ 27 | locale-gen en_US.UTF-8 28 | # We unfortunately cannot use update-locale because docker will not use the env variables 29 | # configured in /etc/default/locale so we need to set it manually. 30 | ENV LANG=en_US.UTF-8 31 | # Unfortunately `.` does not work with code-server. 32 | CMD code-server $PWD 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 Coder Technologies Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # code-server 2 | 3 | [!["Open Issues"](https://img.shields.io/github/issues-raw/codercom/code-server.svg)](https://github.com/codercom/code-server/issues) 4 | [!["Latest Release"](https://img.shields.io/github/release/codercom/code-server.svg)](https://github.com/codercom/code-server/releases/latest) 5 | [![MIT license](https://img.shields.io/badge/license-MIT-green.svg)](#) 6 | [![Discord](https://discordapp.com/api/guilds/463752820026376202/widget.png)](https://discord.gg/zxSwN8Z) 7 | 8 | `code-server` is [VS Code](https://github.com/Microsoft/vscode) running on a remote server, accessible through the browser. 9 | 10 | Try it out: 11 | ```bash 12 | docker run -p 127.0.0.1:8443:8443 -v "${PWD}:/root/project" codercom/code-server code-server --allow-http --no-auth 13 | ``` 14 | 15 | - Code on your Chromebook, tablet, and laptop with a consistent dev environment. 16 | - If you have a Windows or Mac workstation, more easily develop for Linux. 17 | - Take advantage of large cloud servers to speed up tests, compilations, downloads, and more. 18 | - Preserve battery life when you're on the go. 19 | - All intensive computation runs on your server. 20 | - You're no longer running excess instances of Chrome. 21 | 22 | ![Screenshot](/doc/assets/ide.png) 23 | 24 | ## Getting Started 25 | 26 | ### Hosted 27 | 28 | [Try `code-server` now](https://coder.com/signup) for free at coder.com. 29 | 30 | ### Docker 31 | 32 | See docker oneliner mentioned above. Dockerfile is at [/Dockerfile](/Dockerfile). 33 | 34 | ### Binaries 35 | 36 | 1. [Download a binary](https://github.com/codercom/code-server/releases) (Linux and OSX supported. Windows coming soon) 37 | 2. Start the binary with the project directory as the first argument 38 | 39 | ``` 40 | code-server 41 | ``` 42 | > You will be prompted to enter the password shown in the CLI 43 | `code-server` should now be running at https://localhost:8443. 44 | 45 | > code-server uses a self-signed SSL certificate that may prompt your browser to ask you some additional questions before you proceed. Please [read here](doc/self-hosted/index.md) for more information. 46 | 47 | For detailed instructions and troubleshooting, see the [self-hosted quick start guide](doc/self-hosted/index.md). 48 | 49 | Quickstart guides for [Google Cloud](doc/admin/install/google_cloud.md), [AWS](doc/admin/install/aws.md), and [Digital Ocean](doc/admin/install/digitalocean.md). 50 | 51 | How to [secure your setup](/doc/security/ssl.md). 52 | 53 | ## Development 54 | 55 | ### Known Issues 56 | 57 | - Creating custom VS Code extensions and debugging them doesn't work. 58 | 59 | ### Future 60 | 61 | - Windows support. 62 | - Electron and ChromeOS applications to bridge the gap between local<->remote. 63 | - Run VS Code unit tests against our builds to ensure features work as expected. 64 | 65 | ## Contributing 66 | 67 | Development guides are coming soon. 68 | 69 | ## License 70 | 71 | [MIT](LICENSE) 72 | 73 | ## Enterprise 74 | 75 | Visit [our enterprise page](https://coder.com/enterprise) for more information about our enterprise offering. 76 | 77 | ## Commercialization 78 | 79 | If you would like to commercialize code-server, please contact contact@coder.com. 80 | -------------------------------------------------------------------------------- /doc/admin/install/aws.md: -------------------------------------------------------------------------------- 1 | # Deploy on AWS 2 | 3 | This tutorial shows you how to deploy `code-server` on an EC2 AWS instance. 4 | 5 | If you're just starting out, we recommend [installing code-server locally](../../self-hosted/index.md). It takes only a few minutes and lets you try out all of the features. You can also try out the IDE on a container hosted [by Coder](http://coder.com/signup) 6 | 7 | --- 8 | 9 | ## Deploy to EC2 10 | 11 | ### Use the AWS wizard 12 | 13 | - Click **Launch Instance** from your [EC2 dashboard](https://console.aws.amazon.com/ec2/v2/home). 14 | - Select the Ubuntu Server 16.04 LTS (HVM), SSD Volume Type (`ami-0f9cf087c1f27d9b1)` at this time of writing) 15 | - Select an appropriate instance size (we recommend t2.medium/large, depending on team size and number of repositories/languages enabled), then **Next: Configure Instance Details** 16 | - Select **Next: ...** until you get to the **Configure Security Group** page, then add the default **HTTP** rule (port range "80", source "0.0.0.0/0, ::/0") 17 | > Rules with source of 0.0.0.0/0 allow all IP addresses to access your instance. We recommend setting [security group rules](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-network-security.html?icmpid=docs_ec2_console) to allow access from known IP addresses only. 18 | - Click **Launch** 19 | - You will be prompted to create a key pair 20 | > A key pair consists of a public key that AWS stores, and a private key file that you store. Together, they allow you to connect to your instance securely. For Windows AMIs, the private key file is required to obtain the password used to log into your instance. For Linux AMIs, the private key file allows you to securely SSH into your instance. 21 | - From the dropdown choose "create a new pair", give the key pair a name 22 | - Click **Download Key Pair** 23 | > This is necessary before you proceed. A `.pem` file will be downloaded. make sure you store is in a safe location because it can't be retrieved once we move on. 24 | - Finally, click **Launch Instances** 25 | --- 26 | ### SSH Into EC2 Instance 27 | - First head to your [EC2 dashboard](https://console.aws.amazon.com/ec2/v2/home) and choose instances from the left panel 28 | - In the description of your EC2 instance copy the public DNS (iPv4) address using the copy to clipboard button 29 | - Open a terminal on your computer and use the following command to SSH into your EC2 instance 30 | ``` 31 | ssh i "path/to/your/keypair.pem" ubuntu@(paste the public DNS here) 32 | ``` 33 | >example: `ssh -i "/Users/John/Downloads/TestInstance.pem" ubuntu@ec2-3-45-678-910.compute-1.amazonaws.co` 34 | - You should see a prompt for your EC2 instance like so 35 | - At this point it is time to download the `code-server` binary. We will of course want the linux version. Make sure you copy the link for the latest linux version on our [releases page](https://github.com/codercom/code-server/releases) 36 | - With the URL in the clipboard, run: 37 | ``` 38 | wget https://github.com/codercom/code-server/releases/download/0.1.4/code-server-linux 39 | ``` 40 | - If you run into any permission errors, make the binary executable by running: 41 | ``` 42 | chmod +x code-server-linux 43 | ``` 44 | > To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md) 45 | - Finally, run 46 | ``` 47 | sudo ./code-server-linux -p 80 48 | ``` 49 | - When you visit the public IP for your AWS instance, you will be greeted with this page. Code-server is using a self-signed SSL certificate for easy setup. To proceed to the IDE, click **"Advanced"** 50 | - Then click **"proceed anyway"** 51 | 52 | > For instructions on how to keep the server running after you end your SSH session please checkout [how to use systemd](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) to start linux based services if they are killed 53 | 54 | > The `-p 80` flag is necessary in order to make the IDE accessible from the public IP of your instance (also available from the description in the instances page. 55 | 56 | --- 57 | > NOTE: If you get stuck or need help, [file an issue](https://github.com/codercom/code-server/issues/new?&title=Improve+self-hosted+quickstart+guide), [tweet (@coderhq)](https://twitter.com/coderhq) or [email](mailto:support@coder.com?subject=Self-hosted%20quickstart%20guide). -------------------------------------------------------------------------------- /doc/admin/install/digitalocean.md: -------------------------------------------------------------------------------- 1 | # Deploy on DigitalOcean 2 | 3 | This tutorial shows you how to deploy `code-server` to a single node running on DigitalOcean. 4 | 5 | If you're just starting out, we recommend [installing code-server locally](../../self-hosted/index.md). It takes only a few minutes and lets you try out all of the features. You can also try out the IDE on a container hosted [by Coder](http://coder.com/signup) 6 | 7 | --- 8 | 9 | ## Use the "Create Droplets" wizard 10 | 11 | [Open your DigitalOcean dashboard](https://cloud.digitalocean.com/droplets/new) to create a new droplet 12 | 13 | - **Choose an image -** Select the **Distributions** tab and then choose Ubuntu 14 | - **Choose a size -** We recommend at least 4GB RAM and 2 CPU, more depending on team size and number of repositories/languages enabled. 15 | - Launch your instance 16 | - Open a terminal on your computer and SSH into your instance 17 | > example: ssh root@203.0.113.0 18 | - Once in the SSH session, visit code-server [releases page](https://github.com/codercom/code-server/releases/) and copy the link to the download for the latest linux release 19 | - In the shell run the below command with the URL from your clipboard 20 | ``` 21 | wget https://github.com/codercom/code-server/releases/download/0.1.4/code-server-linux 22 | ``` 23 | - If you run into any permission errors when attempting to run the binary: 24 | ``` 25 | chmod +x code-server-linux 26 | ``` 27 | > To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md) 28 | - Finally start the code-server 29 | ``` 30 | sudo ./code-server-linux -p80 31 | ``` 32 | > For instructions on how to keep the server running after you end your SSH session please checkout [how to use systemd](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) to start linux based services if they are killed 33 | - When you visit the public IP for your Digital Ocean instance, you will be greeted with this page. Code-server is using a self-signed SSL certificate for easy setup. To proceed to the IDE, click **"Advanced"** 34 | - Then click **"proceed anyway"** 35 | 36 | --- 37 | > NOTE: If you get stuck or need help, [file an issue](https://github.com/codercom/code-server/issues/new?&title=Improve+self-hosted+quickstart+guide), [tweet (@coderhq)](https://twitter.com/coderhq) or [email](mailto:support@coder.com?subject=Self-hosted%20quickstart%20guide). -------------------------------------------------------------------------------- /doc/admin/install/google_cloud.md: -------------------------------------------------------------------------------- 1 | # Deploy on Google Cloud 2 | 3 | This tutorial shows you how to deploy `code-server` to a single node running on Google Cloud. 4 | 5 | If you're just starting out, we recommend [installing code-server locally](../../self-hosted/index.md). It takes only a few minutes and lets you try out all of the features. You can also try out the IDE on a container hosted [by Coder](http://coder.com/signup) 6 | 7 | --- 8 | 9 | ## Deploy to Google Cloud VM 10 | > Pre-requisite: Please [set up Google Cloud SDK](https://cloud.google.com/sdk/docs/) on your local machine 11 | 12 | - [Open your Google Cloud console](https://console.cloud.google.com/compute/instances) to create a new VM instance and click **Create Instance** 13 | - Choose an appropriate machine type (we recommend 2 vCPU and 7.5 GB RAM, more depending on team size and number of repositories/languages enabled) 14 | - Choose Ubuntu 16.04 LTS as your boot disk 15 | - Check the boxes for **Allow HTTP traffic** and **Allow HTTPS traffic** in the **Firewall** section 16 | - Create your VM, and **take note** of it's public IP address. 17 | - Copy the link to download the latest Linux binary from our [releases page](https://github.com/codercom/code-server/releases) 18 | 19 | --- 20 | 21 | ## Final Steps 22 | 23 | 1. SSH into your Google Cloud VM 24 | ``` 25 | gcloud compute ssh --zone [region] [instance name] 26 | ``` 27 | 2. Download the binary using the link we copied to clipboard 28 | ``` 29 | wget https://github.com/codercom/code-server/releases/download/0.1.4/code-server-linux 30 | ``` 31 | 3. Make the binary executable if you run into any errors regarding permission: 32 | ``` 33 | chmod +x code-server-linux 34 | ``` 35 | > To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md) 36 | 4. Start the code-server 37 | ``` 38 | sudo ./code-server-linux -p 80 39 | ``` 40 | > For instructions on how to keep the server running after you end your SSH session please checkout [how to use systemd](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) to start linux based services if they are killed 41 | 5. Access code-server from the public IP of your Google Cloud instance we noted earlier in your browser. 42 | > example: 32.32.32.234 43 | 6. You will be greeted with this page. Code-server is using a self-signed SSL certificate for easy setup. To proceed to the IDE, click **"Advanced"** 44 | 7. Then click **"proceed anyway"** 45 | --- 46 | > NOTE: If you get stuck or need help, [file an issue](https://github.com/codercom/code-server/issues/new?&title=Improve+self-hosted+quickstart+guide), [tweet (@coderhq)](https://twitter.com/coderhq) or [email](mailto:support@coder.com?subject=Self-hosted%20quickstart%20guide). -------------------------------------------------------------------------------- /doc/assets/aws_ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/doc/assets/aws_ubuntu.png -------------------------------------------------------------------------------- /doc/assets/chrome_confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/doc/assets/chrome_confirm.png -------------------------------------------------------------------------------- /doc/assets/chrome_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/doc/assets/chrome_warning.png -------------------------------------------------------------------------------- /doc/assets/cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/doc/assets/cli.png -------------------------------------------------------------------------------- /doc/assets/ide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/doc/assets/ide.png -------------------------------------------------------------------------------- /doc/assets/logo-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/doc/assets/logo-horizontal.png -------------------------------------------------------------------------------- /doc/assets/server-password-modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/doc/assets/server-password-modal.png -------------------------------------------------------------------------------- /doc/security/ssl.md: -------------------------------------------------------------------------------- 1 | # Generate a self-signed certificate 🔒 2 | 3 | code-server has the ability to secure your connection between client and server using SSL/TSL certificates. By default, the server will start with an unencrypted connection. We recommend Self-signed TLS/SSL certificates for personal of code-server or within an organization. 4 | 5 | This guide will show you how to create a self-signed certificate and start code-server using your certificate/key. 6 | 7 | ## TLS / HTTPS 8 | 9 | You can specify any location that you want to save the certificate and key. In this example, we will navigate to the root directory, create a folder called `certs` and cd into it. 10 | 11 | ```shell 12 | mkdir ~/certs && cd ~/certs 13 | ``` 14 | 15 | If you don't already have a TLS certificate and key, you can generate them with the command below. They will be placed in `~/certs` 16 | 17 | ```shell 18 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ~/certs/MyKey.key -out ~/certs/MyCertificate.crt 19 | ``` 20 | 21 | You will be prompted to add some identifying information about your organization 22 | ```shell 23 | You are about to be asked to enter information that will be incorporated 24 | into your certificate request. 25 | What you are about to enter is what is called a Distinguished Name or a DN. 26 | There are quite a few fields but you can leave some blank 27 | For some fields there will be a default value, 28 | If you enter '.', the field will be left blank. 29 | ----- 30 | Country Name (2 letter code) [AU]:US 31 | State or Province Name (full name) [Some-State]:TX 32 | Locality Name (eg, city) []:Austin 33 | Organization Name (eg, company) [Coder Technologies]:Coder 34 | Organizational Unit Name (eg, section) []:Docs 35 | Common Name (e.g. server FQDN or YOUR name) []:hostname.example.com 36 | Email Address []:admin@example.com 37 | ``` 38 | >If you already have a TLS certificate and key, you can simply reference them in the `--cert` and `--cert-key` flags when launching code-server 39 | 40 | 41 | ## Starting code-server with certificate and key 42 | 43 | 1. At the end of the path to your binary, add the following flags followed by the path to your certificate and key like so. Then press enter to run code-server. 44 | ```shell 45 | ./code-server --cert=~/certs/MyCertificate.crt --cert-key=~/certs/MyKey.key 46 | ``` 47 | 2. After that you will be running a secure code-server. 48 | 49 | > You will know your connection is secure if the lines `WARN No certificate specified. This could be insecure. WARN Documentation on securing your setup: https://coder.com/docs` no longer appear. 50 | 51 | ## Other options 52 | 53 | For larger organizations you may wish to rely on a Certificate Authority as opposed to a self-signed certificate. For more information on generating free and open certificates for your site, please check out EFF's [certbot](https://certbot.eff.org/). Certbot is a cli to generate certificates using [LetsEncrypt](https://letsencrypt.org/). 54 | -------------------------------------------------------------------------------- /doc/self-hosted/index.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | [code-server](https://coder.com) is used by developers at Azure, Google, Reddit, and more to give them access to VS Code in the browser. 4 | 5 | ## Quickstart guide 6 | 7 | > NOTE: If you get stuck or need help, [file an issue](https://github.com/codercom/code-server/issues/new?&title=Improve+self-hosted+quickstart+guide), [tweet (@coderhq)](https://twitter.com/coderhq) or [email](mailto:support@coder.com?subject=Self-hosted%20quickstart%20guide). 8 | 9 | This document pertains to Coder specific implementations of VS Code. For documentation on how to use VS Code itself, please refer to the official [documentation for VS Code](https://code.visualstudio.com/docs) 10 | 11 | It takes just a few minutes to get your own self-hosted server running. If you've got a machine running macOS, Windows, or Linux, you're ready to start the binary which listens on port `8443` by default. 12 | 13 | 18 | 19 | 20 | 1. Visit [the releases](https://github.com/codercom/code-server/releases) page and download the latest cli for your operating system 21 | 2. Double click the executable to run in the current directory 22 | 3. Copy the password that appears in the cli 23 | 4. In your browser navigate to `localhost:8443` 24 | 5. Paste the password from the cli into the login window 25 | > NOTE: Be careful with your password as sharing it will grant those users access to your server's file system 26 | 27 | ### Things to know 28 | - When you visit the IP for your code-server, you will be greeted with this page. Code-server is using a self-signed SSL certificate for easy setup. To proceed to the IDE, click **"Advanced"** 29 | - Then click **"proceed anyway"** 30 | 31 | ## Usage 32 |
code-server --help
33 | 34 | code-server can be ran with a number of arguments to customize your working directory, host, port, and SSL certificate. 35 | 36 | ``` 37 | USAGE 38 | $ code-server [WORKDIR] 39 | 40 | ARGUMENTS 41 | WORKDIR [default: (directory to binary)] Specify working dir 42 | 43 | OPTIONS 44 | -d, --data-dir=data-dir 45 | -h, --host=host [default: 0.0.0.0] 46 | -o, --open Open in browser on startup 47 | -p, --port=port [default: 8443] Port to bind on 48 | -v, --version show CLI version 49 | --allow-http 50 | --cert=cert 51 | --cert-key=cert-key 52 | --help show CLI help 53 | --no-auth 54 | --password=password 55 | ``` 56 | 57 | ### Data directory 58 | Use `code-server -d (path/to/directory)` or `code-server --data-dir=(path/to/directory)`, excluding the parentheses to specify the root folder that VS Code will start in 59 | 60 | ### Host 61 | By default, code-server will use `0.0.0.0` as its address. This can be changed by using `code-server -h` or `code-server --host=` followed by the address you want to use. 62 | > Example: `code-server -h 127.0.0.1` 63 | 64 | ### Open 65 | You can have the server automatically open the VS Code in your browser on startup by using the `code server -o` or `code-server --open` flags 66 | 67 | ### Port 68 | By default, code-server will use `8443` as its port. This can be changed by using `code-server -p` or `code-server --port=` followed by the port you want to use. 69 | > Example: `code-server -p 9000` 70 | 71 | ### Cert and Cert Key 72 | To encrypt the traffic between the browser and server use `code-server --cert=` followed by the path to your `.cer` file. Additionally, you can use certificate keys with `code-server --cert-key` followed by the path to your `.key` file. 73 | > Example (certificate and key): `code-server --cert /etc/letsencrypt/live/example.com/fullchain.cer --cert-key /etc/letsencrypt/live/example.com/fullchain.key` 74 | 75 | > To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md) 76 | 77 | ### Help 78 | Use `code-server -h` or `code-server --help` to view the usage for the cli. This is also shown at the beginning of this section. 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/code-server", 3 | "repository": "https://github.com/codercom/code-server", 4 | "author": "Coder", 5 | "license": "MIT", 6 | "description": "Run VS Code remotely.", 7 | "scripts": { 8 | "build:rules": "cd ./rules && tsc -p .", 9 | "packages:install": "cd ./packages && yarn", 10 | "postinstall": "npm-run-all --parallel packages:install build:rules", 11 | "start": "cd ./packages/server && yarn start", 12 | "task": "ts-node -r tsconfig-paths/register build/tasks.ts", 13 | "test": "cd ./packages && yarn test" 14 | }, 15 | "devDependencies": { 16 | "@types/fs-extra": "^5.0.4", 17 | "@types/node": "^10.12.18", 18 | "@types/trash": "^4.3.1", 19 | "cross-env": "^5.2.0", 20 | "crypto-browserify": "^3.12.0", 21 | "css-loader": "^2.1.0", 22 | "file-loader": "^3.0.1", 23 | "fork-ts-checker-webpack-plugin": "^0.5.2", 24 | "fs-extra": "^7.0.1", 25 | "happypack": "^5.0.1", 26 | "html-webpack-plugin": "^3.2.0", 27 | "http-browserify": "^1.7.0", 28 | "ignore-loader": "^0.1.2", 29 | "mini-css-extract-plugin": "^0.5.0", 30 | "node-sass": "^4.11.0", 31 | "npm-run-all": "^4.1.5", 32 | "path-browserify": "^1.0.0", 33 | "preload-webpack-plugin": "^3.0.0-beta.2", 34 | "sass-loader": "^7.1.0", 35 | "string-replace-loader": "^2.1.1", 36 | "style-loader": "^0.23.1", 37 | "ts-loader": "^5.3.3", 38 | "ts-node": "^7.0.1", 39 | "tsconfig-paths": "^3.8.0", 40 | "tslint": "^5.12.1", 41 | "typescript": "^3.2.2", 42 | "typescript-tslint-plugin": "^0.2.1", 43 | "uglifyjs-webpack-plugin": "^2.1.1", 44 | "webpack": "^4.28.4", 45 | "webpack-bundle-analyzer": "^3.0.3", 46 | "webpack-cli": "^3.2.1", 47 | "webpack-dev-middleware": "^3.5.0", 48 | "webpack-dev-server": "^3.1.14", 49 | "webpack-hot-middleware": "^2.24.3", 50 | "write-file-webpack-plugin": "^4.5.0" 51 | }, 52 | "dependencies": { 53 | "node-loader": "^0.6.0", 54 | "trash": "^4.3.0", 55 | "webpack-merge": "^4.2.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/app/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/app", 3 | "scripts": { 4 | "start": "node ../../../node_modules/webpack-dev-server/bin/webpack-dev-server.js --config ./webpack.config.js", 5 | "build": "node ../../../node_modules/webpack/bin/webpack.js --config ./webpack.config.js" 6 | }, 7 | "dependencies": { 8 | "@material/checkbox": "^0.44.1", 9 | "@material/textfield": "^0.44.1", 10 | "material-components-web": "^0.44.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/app/browser/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Authenticate: code-server 7 | 8 | 9 | 10 |
11 |
12 | <- Back
13 |

code-server

14 |

15 | Enter server password 16 |

17 |
18 | 19 | 20 |
21 |
22 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/app/browser/src/app.scss: -------------------------------------------------------------------------------- 1 | @import url("https://use.typekit.net/vzk7ygg.css"); 2 | 3 | html, body { 4 | background-color: #FFFFFF; 5 | min-height: 100%; 6 | } 7 | 8 | body { 9 | font-family: 'aktiv-grotesk'; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | height: calc(100vh - 20px); 14 | margin: 0; 15 | padding: 10px; 16 | --mdc-theme-primary: #AAADA1; 17 | --mdc-theme-secondary: #AAADA1; 18 | 19 | &.in-app { 20 | .back { 21 | pointer-events: all; 22 | opacity: 1; 23 | } 24 | } 25 | } 26 | 27 | .login { 28 | box-shadow: 0 18px 80px 10px rgba(69, 65, 78, 0.08); 29 | max-width: 328px; 30 | width: 100%; 31 | padding: 40px; 32 | border-radius: 5px; 33 | position: relative; 34 | color: #575962; 35 | 36 | .title { 37 | margin-bottom: 0px; 38 | font-size: 12px; 39 | font-weight: 500; 40 | letter-spacing: 1.5px; 41 | line-height: 15px; 42 | margin-bottom: 5px; 43 | margin-top: 0px; 44 | text-align: center; 45 | text-transform: uppercase; 46 | } 47 | 48 | .subtitle { 49 | text-align: center; 50 | margin: 0; 51 | font-size: 19px; 52 | font-weight: bold; 53 | line-height: 25px; 54 | margin-bottom: 45px; 55 | } 56 | 57 | .mdc-text-field { 58 | width: 100%; 59 | background: none !important; 60 | 61 | &::before { 62 | background: none !important; 63 | } 64 | } 65 | 66 | .mdc-form-field { 67 | text-align: left; 68 | font-size: 12px; 69 | color: #797E84; 70 | margin-top: 16px; 71 | } 72 | 73 | .mdc-button { 74 | border-radius: 24px; 75 | padding-left: 75px; 76 | padding-right: 75px; 77 | padding-top: 15px; 78 | padding-bottom: 15px; 79 | height: 48px; 80 | margin: 0 auto; 81 | display: block; 82 | box-shadow: 0 12px 17px 2px rgba(171,173,163,0.14), 0 5px 22px 4px rgba(171,173,163,0.12), 0 7px 8px -4px rgba(171,173,163,0.2); 83 | margin-top: 40px; 84 | } 85 | } 86 | 87 | .mdc-text-field--focused:not(.mdc-text-field--disabled) .mdc-floating-label { 88 | color: var(--mdc-theme-primary); 89 | } 90 | 91 | .mdc-floating-label--float-above { 92 | transform: translateY(-70%) scale(0.75); 93 | } 94 | 95 | .mdc-text-field:not(.mdc-text-field--disabled):not(.mdc-text-field--outlined):not(.mdc-text-field--textarea) .mdc-text-field__input, .mdc-text-field:not(.mdc-text-field--disabled):not(.mdc-text-field--outlined):not(.mdc-text-field--textarea) .mdc-text-field__input:hover { 96 | border-bottom-color: #EBEDF2; 97 | } 98 | 99 | .back { 100 | position: absolute; 101 | top: -50px; 102 | left: -50px; 103 | font-weight: bold; 104 | opacity: 0; 105 | pointer-events: none; 106 | 107 | // transition: 500ms opacity ease; 108 | } 109 | -------------------------------------------------------------------------------- /packages/app/browser/src/app.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { MDCTextField } from "@material/textfield"; 3 | //@ts-ignore 4 | import { MDCCheckbox } from "@material/checkbox"; 5 | import "material-components-web/dist/material-components-web.css"; 6 | import "./app.scss"; 7 | 8 | document.querySelectorAll(".mdc-text-field").forEach((d) => new MDCTextField(d)); 9 | document.querySelectorAll(".mdc-checkbox").forEach((d) => new MDCCheckbox(d)); 10 | 11 | window.addEventListener("message", (event) => { 12 | if (event.data === "app") { 13 | document.body.classList.add("in-app"); 14 | 15 | const back = document.querySelector(".back")!; 16 | back.addEventListener("click", () => { 17 | (event.source as Window).postMessage("back", event.origin); 18 | }); 19 | } 20 | }); 21 | 22 | const password = document.getElementById("password") as HTMLInputElement; 23 | const submit = document.getElementById("submit") as HTMLButtonElement; 24 | if (!submit) { 25 | throw new Error("No submit button found"); 26 | } 27 | submit.addEventListener("click", () => { 28 | document.cookie = `password=${password.value}`; 29 | location.reload(); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/app/browser/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const merge = require("webpack-merge"); 4 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 5 | 6 | const root = path.resolve(__dirname, "../../.."); 7 | 8 | module.exports = merge( 9 | require(path.join(root, "scripts/webpack.client.config.js"))({ 10 | entry: path.join(root, "packages/app/browser/src/app.ts"), 11 | template: path.join(root, "packages/app/browser/src/app.html"), 12 | }), { 13 | output: { 14 | path: path.join(__dirname, "out"), 15 | }, 16 | }, 17 | ); 18 | -------------------------------------------------------------------------------- /packages/app/chrome/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/chrome/icon_128.png -------------------------------------------------------------------------------- /packages/app/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Coder", 4 | "version": "1", 5 | "icons": { 6 | "128": "icon_128.png" 7 | }, 8 | "permissions": [ 9 | "storage", 10 | "webview", 11 | "http://*/*", 12 | "https://*/*" 13 | ], 14 | "app": { 15 | "background": { 16 | "scripts": [ 17 | "out/background.js" 18 | ] 19 | }, 20 | "content": { 21 | "scripts": [ 22 | "out/content.js" 23 | ] 24 | } 25 | }, 26 | "commands": { 27 | "toggle-feature-foo": { 28 | "suggested_key": { 29 | "default": "Ctrl+W" 30 | }, 31 | "description": "Toggle feature foo", 32 | "global": true 33 | } 34 | }, 35 | "sockets": { 36 | "tcpServer": { 37 | "listen": [ 38 | "" 39 | ] 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /packages/app/chrome/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/chrome-app", 3 | "dependencies": { 4 | "@types/chrome": "^0.0.79" 5 | }, 6 | "scripts": { 7 | "build": "../../../node_modules/.bin/webpack --config ./webpack.config.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/app/chrome/src/background.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // tslint:disable-next-line:no-any 4 | const chromeApp = (chrome).app; 5 | 6 | chromeApp.runtime.onLaunched.addListener(() => { 7 | chromeApp.window.create("src/index.html", { 8 | outerBounds: { 9 | width: 400, 10 | height: 500, 11 | }, 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/app/chrome/src/chome.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { TcpHost, TcpServer, TcpConnection } from "@coder/app/common/src/app"; 3 | import { Event, Emitter } from "@coder/events/src"; 4 | 5 | export const tcpHost: TcpHost = { 6 | listen(host: string, port: number): Promise { 7 | const socketApi: { 8 | readonly tcpServer: { 9 | create(props: {}, cb: (createInfo: { readonly socketId: number }) => void): void; 10 | listen(socketId: number, address: string, port: number, callback: (result: number) => void): void; 11 | disconnect(socketId: number, callback: () => void): void; 12 | 13 | readonly onAccept: { 14 | addListener(callback: (info: { readonly socketId: number; readonly clientSocketId: number }) => void): void; 15 | }; 16 | }; 17 | readonly tcp: { 18 | readonly onReceive: { 19 | addListener(callback: (info: { readonly socketId: number; readonly data: ArrayBuffer; }) => void): void; 20 | }; 21 | close(socketId: number, callback?: () => void): void; 22 | send(socketId: number, data: ArrayBuffer, callback?: () => void): void; 23 | setPaused(socketId: number, value: boolean): void; 24 | }; 25 | // tslint:disable-next-line:no-any 26 | } = (chrome).sockets; 27 | 28 | return new Promise((resolve, reject): void => { 29 | socketApi.tcpServer.create({}, (createInfo) => { 30 | const serverSocketId = createInfo.socketId; 31 | socketApi.tcpServer.listen(serverSocketId, host, port, (result) => { 32 | if (result < 0) { 33 | return reject("Failed to listen: " + chrome.runtime.lastError); 34 | } 35 | 36 | const connectionEmitter = new Emitter(); 37 | 38 | socketApi.tcpServer.onAccept.addListener((info) => { 39 | if (info.socketId !== serverSocketId) { 40 | return; 41 | } 42 | 43 | const dataEmitter = new Emitter(); 44 | 45 | socketApi.tcp.onReceive.addListener((recvInfo) => { 46 | if (recvInfo.socketId !== info.clientSocketId) { 47 | return; 48 | } 49 | 50 | dataEmitter.emit(recvInfo.data); 51 | }); 52 | 53 | socketApi.tcp.setPaused(info.clientSocketId, false); 54 | 55 | connectionEmitter.emit({ 56 | send: (data): Promise => { 57 | return new Promise((res): void => { 58 | socketApi.tcp.send(info.clientSocketId, data, () => { 59 | res(); 60 | }); 61 | }); 62 | }, 63 | close: (): Promise => { 64 | return new Promise((res): void => { 65 | socketApi.tcp.close(info.clientSocketId, () => { 66 | res(); 67 | }); 68 | }); 69 | }, 70 | get onData(): Event { 71 | return dataEmitter.event; 72 | }, 73 | }); 74 | }); 75 | 76 | resolve({ 77 | get onConnection(): Event { 78 | return connectionEmitter.event; 79 | }, 80 | close: (): Promise => { 81 | return new Promise((res): void => { 82 | socketApi.tcpServer.disconnect(serverSocketId, () => { 83 | res(); 84 | }); 85 | }); 86 | }, 87 | }); 88 | }); 89 | }); 90 | }); 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /packages/app/chrome/src/content.ts: -------------------------------------------------------------------------------- 1 | import { create } from "@coder/app/common/src/app"; 2 | import { tcpHost } from "./chome"; 3 | 4 | create({ 5 | storage: { 6 | get: (key: string): Promise => { 7 | return new Promise((resolve, reject): void => { 8 | try { 9 | chrome.storage.sync.get(key, (items) => { 10 | resolve(items[key]); 11 | }); 12 | } catch (ex) { 13 | reject(ex); 14 | } 15 | }); 16 | }, 17 | set: (key: string, value: T): Promise => { 18 | return new Promise((resolve, reject): void => { 19 | try { 20 | chrome.storage.sync.set({ 21 | [key]: value, 22 | }, () => { 23 | resolve(); 24 | }); 25 | } catch (ex) { 26 | reject(ex); 27 | } 28 | }); 29 | }, 30 | }, 31 | tcp: tcpHost, 32 | node: document.getElementById("main") as HTMLDivElement, 33 | }); 34 | -------------------------------------------------------------------------------- /packages/app/chrome/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/app/chrome/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const merge = require("webpack-merge"); 4 | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; 5 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 6 | const prod = process.env.NODE_ENV === "production"; 7 | 8 | module.exports = [ 9 | merge(require(path.join(__dirname, "../../../scripts", "webpack.general.config.js"))(), { 10 | devtool: "none", 11 | mode: "development", 12 | target: "web", 13 | output: { 14 | path: path.join(__dirname, "out"), 15 | filename: "background.js", 16 | }, 17 | entry: [ 18 | "./packages/app/chrome/src/background.ts" 19 | ], 20 | plugins: [ 21 | ] 22 | }), 23 | merge(require(path.join(__dirname, "../../../scripts", "webpack.general.config.js"))(), { 24 | devtool: "none", 25 | mode: "development", 26 | target: "web", 27 | output: { 28 | path: path.join(__dirname, "out"), 29 | filename: "content.js", 30 | }, 31 | entry: [ 32 | "./packages/app/chrome/src/content.ts" 33 | ], 34 | plugins: [ 35 | ] 36 | }), 37 | ]; 38 | -------------------------------------------------------------------------------- /packages/app/chrome/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/chrome@^0.0.79": 6 | version "0.0.79" 7 | resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.79.tgz#1c83b35bd9b21b6204fb56e4816a1ea65dc013e5" 8 | integrity sha512-4+Xducpig6lpwVX65Hk8KSZwRoURHXMDbd38SDNcV8TBaw4xyJki39fjB1io2h7ip+BsyFvgTm9OxR5qneLPiA== 9 | dependencies: 10 | "@types/filesystem" "*" 11 | 12 | "@types/filesystem@*": 13 | version "0.0.29" 14 | resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.29.tgz#ee3748eb5be140dcf980c3bd35f11aec5f7a3748" 15 | integrity sha512-85/1KfRedmfPGsbK8YzeaQUyV1FQAvMPMTuWFQ5EkLd2w7szhNO96bk3Rh/SKmOfd9co2rCLf0Voy4o7ECBOvw== 16 | dependencies: 17 | "@types/filewriter" "*" 18 | 19 | "@types/filewriter@*": 20 | version "0.0.28" 21 | resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.28.tgz#c054e8af4d9dd75db4e63abc76f885168714d4b3" 22 | integrity sha1-wFTor02d11205jq8dviFFocU1LM= 23 | -------------------------------------------------------------------------------- /packages/app/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/app-common", 3 | "main": "src/app.ts", 4 | "dependencies": { 5 | "material-components-web": "^0.44.0", 6 | "react": "^16.8.1", 7 | "react-dom": "^16.8.1" 8 | }, 9 | "devDependencies": { 10 | "@types/react": "^16.8.2", 11 | "@types/react-dom": "^16.8.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/app/common/src/app.tsx: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { MDCTextField } from "@material/textfield"; 3 | import { TcpHost } from "./connection"; 4 | import { StorageProvider } from "./storage"; 5 | import "material-components-web/dist/material-components-web.css"; 6 | import "./app.scss"; 7 | import "./tooltip.scss"; 8 | 9 | import * as React from "react"; 10 | import { render } from "react-dom"; 11 | import { Main } from "./containers"; 12 | 13 | export * from "./connection"; 14 | export interface App { 15 | readonly tcp: TcpHost; 16 | readonly storage: StorageProvider; 17 | readonly node: HTMLElement; 18 | } 19 | 20 | export interface RegisteredServer { 21 | readonly host: "coder" | "self"; 22 | readonly hostname: string; 23 | readonly name: string; 24 | } 25 | 26 | export const create = async (app: App): Promise => { 27 | let servers = await app.storage.get("servers"); 28 | if (!servers) { 29 | servers = []; 30 | } 31 | 32 | render(
, app.node); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/app/common/src/connection.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "@coder/events"; 2 | import { TunnelCloseEvent } from "@coder/tunnel/src/client"; 3 | 4 | export interface TcpHost { 5 | listen(host: string, port: number): Promise; 6 | } 7 | 8 | export interface TcpServer { 9 | readonly onConnection: Event; 10 | close(): Promise; 11 | } 12 | 13 | export interface TcpConnection { 14 | readonly onData: Event; 15 | send(data: ArrayBuffer): Promise; 16 | close(): Promise; 17 | } 18 | -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskBold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/common/src/fonts/AktivGroteskBold.eot -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/common/src/fonts/AktivGroteskBold.ttf -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/common/src/fonts/AktivGroteskBold.woff -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/common/src/fonts/AktivGroteskBold.woff2 -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskMedium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/common/src/fonts/AktivGroteskMedium.eot -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskMedium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/common/src/fonts/AktivGroteskMedium.ttf -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskMedium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/common/src/fonts/AktivGroteskMedium.woff -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskMedium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/common/src/fonts/AktivGroteskMedium.woff2 -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskRegular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/common/src/fonts/AktivGroteskRegular.eot -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskRegular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/common/src/fonts/AktivGroteskRegular.ttf -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskRegular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/common/src/fonts/AktivGroteskRegular.woff -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskRegular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/code-server/36c05ed335e30721764865b397a6c47abc138e6d/packages/app/common/src/fonts/AktivGroteskRegular.woff2 -------------------------------------------------------------------------------- /packages/app/common/src/storage.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface StorageProvider { 3 | set(key: string, value: T): Promise; 4 | get(key: string): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /packages/app/common/src/tooltip.scss: -------------------------------------------------------------------------------- 1 | .md-tooltip { 2 | position: relative; 3 | } 4 | 5 | .md-tooltip-content { 6 | position: absolute; 7 | bottom: -35px; 8 | left: 50%; 9 | padding: 7px; 10 | transform: translateX(-50%) scale(0); 11 | transition: transform 0.15s cubic-bezier(0, 0, 0.2, 1); 12 | transform-origin: top; 13 | background: rgba(67, 67, 67, 0.97); 14 | color: white; 15 | letter-spacing: 0.3px; 16 | border-radius: 3px; 17 | font-size: 12px; 18 | font-weight: 500; 19 | z-index: 2; 20 | } 21 | 22 | .md-tooltip:hover .md-tooltip-content { 23 | transform: translateX(-50%) scale(1); 24 | } 25 | -------------------------------------------------------------------------------- /packages/disposable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/disposable", 3 | "main": "src/index.ts" 4 | } 5 | -------------------------------------------------------------------------------- /packages/disposable/src/disposable.ts: -------------------------------------------------------------------------------- 1 | export interface IDisposable { 2 | dispose(): void; 3 | } 4 | -------------------------------------------------------------------------------- /packages/disposable/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./disposable"; 2 | -------------------------------------------------------------------------------- /packages/disposable/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/dns/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | src 16 | 17 | # Node.js dependencies: 18 | node_modules/ -------------------------------------------------------------------------------- /packages/dns/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | COPY out/main.js /main.js 4 | COPY package.json /package.json 5 | RUN yarn 6 | ENV NODE_ENV production 7 | 8 | CMD ["node", "/main.js"] -------------------------------------------------------------------------------- /packages/dns/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs10 2 | service: cdrdns 3 | network: 4 | forwarded_ports: 5 | - 53/udp -------------------------------------------------------------------------------- /packages/dns/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/dns", 3 | "main": "out/main.js", 4 | "scripts": { 5 | "build": "../../node_modules/.bin/webpack --config ./webpack.config.js" 6 | }, 7 | "dependencies": { 8 | "node-named": "^0.0.1" 9 | }, 10 | "devDependencies": { 11 | "ip-address": "^5.8.9", 12 | "@types/ip-address": "^5.8.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/dns/src/dns.ts: -------------------------------------------------------------------------------- 1 | import { field, logger } from "@coder/logger"; 2 | import * as http from "http"; 3 | //@ts-ignore 4 | import * as named from "node-named"; 5 | import * as ip from "ip-address"; 6 | import { words, wordKeys } from "./words"; 7 | 8 | import * as dgram from "dgram"; 9 | 10 | const oldCreate = dgram.createSocket; 11 | 12 | // tslint:disable-next-line:no-any 13 | (dgram).createSocket = (_: any, callback: any): dgram.Socket => { 14 | return oldCreate("udp4", callback); 15 | }; 16 | 17 | interface DnsQuery { 18 | name(): string; 19 | // tslint:disable-next-line:no-any 20 | addAnswer(domain: string, target: any, ttl: number): void; 21 | } 22 | 23 | const dnsServer: { 24 | listen(port: number, host: string, callback: () => void): void; 25 | on(event: "query", callback: (query: DnsQuery) => void): void; 26 | send(query: DnsQuery): void; 27 | } = named.createServer(); 28 | 29 | const isDev = process.env.NODE_ENV !== "production"; 30 | const dnsPort = isDev ? 9999 : 53; 31 | dnsServer.listen(dnsPort, "0.0.0.0", () => { 32 | logger.info("DNS server started", field("port", dnsPort)); 33 | }); 34 | 35 | dnsServer.on("query", (query) => { 36 | const domain = query.name(); 37 | const reqParts = domain.split("."); 38 | if (reqParts.length < 2) { 39 | dnsServer.send(query); 40 | logger.info("Invalid request", field("request", domain)); 41 | 42 | return; 43 | } 44 | const allWords = reqParts.shift()!; 45 | if (allWords.length > 16) { 46 | dnsServer.send(query); 47 | logger.info("Invalid request", field("request", domain)); 48 | 49 | return; 50 | } 51 | const wordParts = allWords.split(/(?=[A-Z])/); 52 | const ipParts: string[] = []; 53 | // Should be left with HowAreYouNow 54 | for (let i = 0; i < wordParts.length; i++) { 55 | const part = wordParts[i]; 56 | if (part.length > 4) { 57 | dnsServer.send(query); 58 | logger.info("Words too long", field("request", domain)); 59 | 60 | return; 61 | } 62 | const ipPart = words[part.toLowerCase()]; 63 | if (typeof ipPart === "undefined") { 64 | dnsServer.send(query); 65 | logger.info("Word not found in index", field("part", part), field("request", domain)); 66 | 67 | return; 68 | } 69 | ipParts.push(ipPart.toString()); 70 | } 71 | 72 | const address = new ip.Address4(ipParts.join(".")); 73 | 74 | if (address.isValid()) { 75 | logger.info("Responded with valid address query", field("address", address.address), field("request", domain)); 76 | query.addAnswer(domain, new named.ARecord(address.address), 99999); 77 | } else { 78 | logger.warn("Received invalid request", field("request", domain)); 79 | } 80 | 81 | dnsServer.send(query); 82 | }); 83 | 84 | const httpServer = http.createServer((request, response) => { 85 | const remoteAddr = request.connection.remoteAddress; 86 | if (!remoteAddr) { 87 | response.writeHead(422); 88 | response.end(); 89 | 90 | return; 91 | } 92 | const hostHeader = request.headers.host; 93 | if (!hostHeader) { 94 | response.writeHead(422); 95 | response.end(); 96 | 97 | return; 98 | } 99 | const host = remoteAddr.split(".").map(p => wordKeys[Number.parseInt(p, 10)]).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(""); 100 | logger.info("Resolved host", field("remote-addr", remoteAddr), field("host", host)); 101 | response.writeHead(200); 102 | response.write(`${host}.${hostHeader}`); 103 | response.end(); 104 | }); 105 | 106 | const httpPort = isDev ? 3000 : 80; 107 | httpServer.listen(httpPort, "0.0.0.0", () => { 108 | logger.info("HTTP server started", field("port", httpPort)); 109 | }); 110 | -------------------------------------------------------------------------------- /packages/dns/src/words.ts: -------------------------------------------------------------------------------- 1 | export const words: { readonly [key: string]: number } = { 2 | term: 0, 3 | salt: 1, 4 | barn: 2, 5 | corn: 3, 6 | went: 4, 7 | feel: 5, 8 | rest: 6, 9 | will: 7, 10 | pale: 8, 11 | cave: 9, 12 | dirt: 10, 13 | time: 11, 14 | in: 12, 15 | pie: 13, 16 | star: 14, 17 | iron: 15, 18 | door: 16, 19 | tone: 17, 20 | want: 18, 21 | task: 19, 22 | zoo: 20, 23 | nor: 21, 24 | fall: 22, 25 | tell: 23, 26 | noon: 24, 27 | new: 25, 28 | per: 26, 29 | end: 27, 30 | arm: 28, 31 | been: 29, 32 | wolf: 30, 33 | port: 31, 34 | beat: 32, 35 | pour: 33, 36 | far: 34, 37 | may: 35, 38 | tie: 36, 39 | moon: 37, 40 | duck: 38, 41 | us: 39, 42 | led: 40, 43 | met: 41, 44 | bank: 42, 45 | day: 43, 46 | due: 44, 47 | both: 45, 48 | pet: 46, 49 | gate: 47, 50 | pain: 48, 51 | rock: 49, 52 | fill: 50, 53 | open: 51, 54 | thus: 52, 55 | mark: 53, 56 | our: 54, 57 | loud: 55, 58 | wife: 56, 59 | say: 57, 60 | flag: 58, 61 | as: 59, 62 | ride: 60, 63 | once: 61, 64 | sun: 62, 65 | duty: 63, 66 | pure: 64, 67 | made: 65, 68 | gulf: 66, 69 | pig: 67, 70 | fish: 68, 71 | name: 69, 72 | army: 70, 73 | have: 71, 74 | ill: 72, 75 | meal: 73, 76 | ago: 74, 77 | late: 75, 78 | view: 76, 79 | atom: 77, 80 | pen: 78, 81 | mud: 79, 82 | tail: 80, 83 | sink: 81, 84 | cow: 82, 85 | rear: 83, 86 | fur: 84, 87 | go: 85, 88 | suit: 86, 89 | come: 87, 90 | fear: 88, 91 | also: 89, 92 | sail: 90, 93 | row: 91, 94 | lay: 92, 95 | noun: 93, 96 | hat: 94, 97 | am: 95, 98 | mail: 96, 99 | keep: 97, 100 | drop: 98, 101 | than: 99, 102 | weak: 100, 103 | by: 101, 104 | who: 102, 105 | fire: 103, 106 | good: 104, 107 | sick: 105, 108 | care: 106, 109 | pink: 107, 110 | lady: 108, 111 | war: 109, 112 | sets: 110, 113 | swam: 111, 114 | well: 112, 115 | shoe: 113, 116 | bent: 114, 117 | fuel: 115, 118 | wet: 116, 119 | fog: 117, 120 | land: 118, 121 | lead: 119, 122 | tax: 120, 123 | deal: 121, 124 | verb: 122, 125 | take: 123, 126 | save: 124, 127 | gift: 125, 128 | had: 126, 129 | gold: 127, 130 | slow: 128, 131 | drew: 129, 132 | lamp: 130, 133 | roof: 131, 134 | hung: 132, 135 | wild: 133, 136 | able: 134, 137 | girl: 135, 138 | warn: 136, 139 | were: 137, 140 | know: 138, 141 | camp: 139, 142 | milk: 140, 143 | neck: 141, 144 | aid: 142, 145 | fair: 143, 146 | bell: 144, 147 | dig: 145, 148 | hope: 146, 149 | wood: 147, 150 | away: 148, 151 | cook: 149, 152 | just: 150, 153 | form: 151, 154 | food: 152, 155 | hall: 153, 156 | mind: 154, 157 | for: 155, 158 | card: 156, 159 | half: 157, 160 | sat: 158, 161 | now: 159, 162 | team: 160, 163 | rush: 161, 164 | face: 162, 165 | wire: 163, 166 | such: 164, 167 | tool: 165, 168 | make: 166, 169 | fat: 167, 170 | hold: 168, 171 | inch: 169, 172 | bill: 170, 173 | mean: 171, 174 | tide: 172, 175 | burn: 173, 176 | talk: 174, 177 | tape: 175, 178 | hard: 176, 179 | mine: 177, 180 | on: 178, 181 | year: 179, 182 | rich: 180, 183 | sum: 181, 184 | yes: 182, 185 | baby: 183, 186 | wide: 184, 187 | how: 185, 188 | clay: 186, 189 | car: 187, 190 | here: 188, 191 | cent: 189, 192 | bowl: 190, 193 | post: 191, 194 | said: 192, 195 | see: 193, 196 | raw: 194, 197 | foot: 195, 198 | life: 196, 199 | bar: 197, 200 | from: 198, 201 | path: 199, 202 | meat: 200, 203 | show: 201, 204 | sent: 202, 205 | wait: 203, 206 | mice: 204, 207 | ten: 205, 208 | pot: 206, 209 | nice: 207, 210 | idea: 208, 211 | or: 209, 212 | onto: 210, 213 | rose: 211, 214 | your: 212, 215 | this: 213, 216 | cat: 214, 217 | bet: 215, 218 | took: 216, 219 | hang: 217, 220 | very: 218, 221 | bend: 219, 222 | mix: 220, 223 | base: 221, 224 | jack: 222, 225 | her: 223, 226 | leg: 224, 227 | own: 225, 228 | book: 226, 229 | love: 227, 230 | dawn: 228, 231 | deer: 229, 232 | hit: 230, 233 | rain: 231, 234 | gas: 232, 235 | eat: 233, 236 | tube: 234, 237 | case: 235, 238 | pipe: 236, 239 | get: 237, 240 | joy: 238, 241 | ever: 239, 242 | nest: 240, 243 | home: 241, 244 | egg: 242, 245 | pack: 243, 246 | hand: 244, 247 | cold: 245, 248 | hot: 246, 249 | frog: 247, 250 | peep: 248, 251 | seed: 249, 252 | rawr: 250, 253 | top: 251, 254 | meow: 252, 255 | bark: 253, 256 | eel: 254, 257 | swap: 255, 258 | }; 259 | 260 | export const wordKeys = Object.keys(words); 261 | -------------------------------------------------------------------------------- /packages/dns/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const merge = require("webpack-merge"); 3 | 4 | const root = path.resolve(__dirname, "../.."); 5 | 6 | module.exports = merge( 7 | require(path.join(root, "scripts/webpack.node.config.js"))({ 8 | // Options. 9 | }), { 10 | externals: { 11 | "node-named": "commonjs node-named", 12 | }, 13 | output: { 14 | path: path.join(__dirname, "out"), 15 | filename: "main.js", 16 | }, 17 | entry: [ 18 | "./packages/dns/src/dns.ts" 19 | ], 20 | }, 21 | ); 22 | -------------------------------------------------------------------------------- /packages/dns/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/ip-address@^5.8.2": 6 | version "5.8.2" 7 | resolved "https://registry.yarnpkg.com/@types/ip-address/-/ip-address-5.8.2.tgz#5e413c477f78b3a264745eac937538a6e6e0c1f6" 8 | integrity sha512-LFlDGRjJDnahfPyNCZGXvlaevSmZTi/zDxjTdXeTs8TQ9pQkNZKbCWaJXW29a3bGPRsASqeO+jGgZlaTUi9jTw== 9 | dependencies: 10 | "@types/jsbn" "*" 11 | 12 | "@types/jsbn@*": 13 | version "1.2.29" 14 | resolved "https://registry.yarnpkg.com/@types/jsbn/-/jsbn-1.2.29.tgz#28229bc0262c704a1506c3ed69a7d7e115bd7832" 15 | integrity sha512-2dVz9LTEGWVj9Ov9zaDnpvqHFV+W4bXtU0EUEGAzWfdRNO3dlUuosdHpENI6/oQW+Kejn0hAjk6P/czs9h/hvg== 16 | 17 | bunyan@0.7.0: 18 | version "0.7.0" 19 | resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-0.7.0.tgz#921065e70c936fe302a740e2c5605775beea2f42" 20 | integrity sha1-khBl5wyTb+MCp0DixWBXdb7qL0I= 21 | 22 | "coffee-script@>= 1.1.1": 23 | version "1.12.7" 24 | resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.7.tgz#c05dae0cb79591d05b3070a8433a98c9a89ccc53" 25 | integrity sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw== 26 | 27 | ip-address@^5.8.9: 28 | version "5.8.9" 29 | resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-5.8.9.tgz#6379277c23fc5adb20511e4d23ec2c1bde105dfd" 30 | integrity sha512-7ay355oMN34iXhET1BmCJVsHjOTSItEEIIpOs38qUC23AIhOy+xIPnkrTuEFjeLMrTJ7m8KMXWgWfy/2Vn9sDw== 31 | dependencies: 32 | jsbn "1.1.0" 33 | lodash.find "^4.6.0" 34 | lodash.max "^4.0.1" 35 | lodash.merge "^4.6.0" 36 | lodash.padstart "^4.6.1" 37 | lodash.repeat "^4.1.0" 38 | sprintf-js "1.1.0" 39 | 40 | ipaddr.js@0.1.1: 41 | version "0.1.1" 42 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-0.1.1.tgz#28c6a7c116a021c555544f906ab1ad540b1d635a" 43 | integrity sha1-KManwRagIcVVVE+QarGtVAsdY1o= 44 | dependencies: 45 | coffee-script ">= 1.1.1" 46 | 47 | jsbn@1.1.0: 48 | version "1.1.0" 49 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" 50 | integrity sha1-sBMHyym2GKHtJux56RH4A8TaAEA= 51 | 52 | lodash.find@^4.6.0: 53 | version "4.6.0" 54 | resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1" 55 | integrity sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E= 56 | 57 | lodash.max@^4.0.1: 58 | version "4.0.1" 59 | resolved "https://registry.yarnpkg.com/lodash.max/-/lodash.max-4.0.1.tgz#8735566c618b35a9f760520b487ae79658af136a" 60 | integrity sha1-hzVWbGGLNan3YFILSHrnllivE2o= 61 | 62 | lodash.merge@^4.6.0: 63 | version "4.6.1" 64 | resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" 65 | integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ== 66 | 67 | lodash.padstart@^4.6.1: 68 | version "4.6.1" 69 | resolved "https://registry.yarnpkg.com/lodash.padstart/-/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b" 70 | integrity sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs= 71 | 72 | lodash.repeat@^4.1.0: 73 | version "4.1.0" 74 | resolved "https://registry.yarnpkg.com/lodash.repeat/-/lodash.repeat-4.1.0.tgz#fc7de8131d8c8ac07e4b49f74ffe829d1f2bec44" 75 | integrity sha1-/H3oEx2MisB+S0n3T/6CnR8r7EQ= 76 | 77 | node-named@^0.0.1: 78 | version "0.0.1" 79 | resolved "https://registry.yarnpkg.com/node-named/-/node-named-0.0.1.tgz#3607b434cf237ab99440f5ff6d19c05e3a93e217" 80 | integrity sha1-Nge0NM8jermUQPX/bRnAXjqT4hc= 81 | dependencies: 82 | bunyan "0.7.0" 83 | ipaddr.js "0.1.1" 84 | 85 | sprintf-js@1.1.0: 86 | version "1.1.0" 87 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.0.tgz#cffcaf702daf65ea39bb4e0fa2b299cec1a1be46" 88 | integrity sha1-z/yvcC2vZeo5u04PorKZzsGhvkY= 89 | -------------------------------------------------------------------------------- /packages/events/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/events", 3 | "main": "./src/index.ts" 4 | } 5 | -------------------------------------------------------------------------------- /packages/events/src/events.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from "@coder/disposable"; 2 | 3 | export interface Event { 4 | (listener: (e: T) => void): IDisposable; 5 | } 6 | 7 | /** 8 | * Emitter typecasts for a single event type. 9 | */ 10 | export class Emitter { 11 | private listeners = void>>[]; 12 | 13 | public get event(): Event { 14 | return (cb: (e: T) => void): IDisposable => { 15 | if (this.listeners) { 16 | this.listeners.push(cb); 17 | } 18 | 19 | return { 20 | dispose: (): void => { 21 | if (this.listeners) { 22 | const i = this.listeners.indexOf(cb); 23 | if (i !== -1) { 24 | this.listeners.splice(i, 1); 25 | } 26 | } 27 | }, 28 | }; 29 | }; 30 | } 31 | 32 | /** 33 | * Emit an event with a value. 34 | */ 35 | public emit(value: T): void { 36 | if (this.listeners) { 37 | this.listeners.forEach((t) => t(value)); 38 | } 39 | } 40 | 41 | /** 42 | * Dispose the current events. 43 | */ 44 | public dispose(): void { 45 | this.listeners = []; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/events/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./events"; 2 | -------------------------------------------------------------------------------- /packages/events/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/ide-api/README.md: -------------------------------------------------------------------------------- 1 | # ide-api 2 | 3 | Provides window listeners for interfacing with the IDE. 4 | 5 | Created for content-scripts. -------------------------------------------------------------------------------- /packages/ide-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/ide-api", 3 | "version": "1.0.2", 4 | "typings": "api.d.ts", 5 | "author": "Coder", 6 | "license": "MIT", 7 | "description": "API for interfacing with the API created for content-scripts" 8 | } -------------------------------------------------------------------------------- /packages/ide-api/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/ide/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/ide", 3 | "description": "Browser-based IDE client abstraction.", 4 | "main": "src/index.ts", 5 | "dependencies": {}, 6 | "devDependencies": { 7 | "@types/rimraf": "^2.0.2", 8 | "rimraf": "^2.6.3" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/ide/src/fill/client.ts: -------------------------------------------------------------------------------- 1 | import { Emitter } from "@coder/events"; 2 | import { field, logger } from "@coder/logger"; 3 | import { Client, ReadWriteConnection } from "@coder/protocol"; 4 | import { retry } from "../retry"; 5 | 6 | /** 7 | * A connection based on a web socket. Automatically reconnects and buffers 8 | * messages during connection. 9 | */ 10 | class WebsocketConnection implements ReadWriteConnection { 11 | private activeSocket: WebSocket | undefined; 12 | private readonly messageBuffer = []; 13 | private readonly socketTimeoutDelay = 60 * 1000; 14 | private readonly retryName = "Socket"; 15 | private isUp: boolean = false; 16 | private closed: boolean = false; 17 | 18 | private readonly messageEmitter = new Emitter(); 19 | private readonly closeEmitter = new Emitter(); 20 | private readonly upEmitter = new Emitter(); 21 | private readonly downEmitter = new Emitter(); 22 | 23 | public readonly onUp = this.upEmitter.event; 24 | public readonly onClose = this.closeEmitter.event; 25 | public readonly onDown = this.downEmitter.event; 26 | public readonly onMessage = this.messageEmitter.event; 27 | 28 | public constructor() { 29 | retry.register(this.retryName, () => this.connect()); 30 | retry.block(this.retryName); 31 | retry.run(this.retryName); 32 | } 33 | 34 | public send(data: Buffer | Uint8Array): void { 35 | if (this.closed) { 36 | throw new Error("web socket is closed"); 37 | } 38 | if (!this.activeSocket || this.activeSocket.readyState !== this.activeSocket.OPEN) { 39 | this.messageBuffer.push(data); 40 | } else { 41 | this.activeSocket.send(data); 42 | } 43 | } 44 | 45 | public close(): void { 46 | this.closed = true; 47 | this.dispose(); 48 | this.closeEmitter.emit(); 49 | } 50 | 51 | /** 52 | * Connect to the server. 53 | */ 54 | private async connect(): Promise { 55 | const socket = await this.openSocket(); 56 | 57 | socket.addEventListener("message", (event: MessageEvent) => { 58 | this.messageEmitter.emit(event.data); 59 | }); 60 | 61 | socket.addEventListener("close", (event) => { 62 | if (this.isUp) { 63 | this.isUp = false; 64 | this.downEmitter.emit(undefined); 65 | } 66 | logger.warn( 67 | "Web socket closed", 68 | field("code", event.code), 69 | field("reason", event.reason), 70 | field("wasClean", event.wasClean), 71 | ); 72 | if (!this.closed) { 73 | retry.block(this.retryName); 74 | retry.run(this.retryName); 75 | } 76 | }); 77 | 78 | // Send any messages that were queued while we were waiting to connect. 79 | while (this.messageBuffer.length > 0) { 80 | socket.send(this.messageBuffer.shift()!); 81 | } 82 | 83 | if (!this.isUp) { 84 | this.isUp = true; 85 | this.upEmitter.emit(undefined); 86 | } 87 | } 88 | 89 | /** 90 | * Open a web socket, disposing the previous connection if any. 91 | */ 92 | private async openSocket(): Promise { 93 | this.dispose(); 94 | const socket = new WebSocket( 95 | `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}`, 96 | ); 97 | socket.binaryType = "arraybuffer"; 98 | this.activeSocket = socket; 99 | 100 | const socketWaitTimeout = window.setTimeout(() => { 101 | socket.close(); 102 | }, this.socketTimeoutDelay); 103 | 104 | await new Promise((resolve, reject): void => { 105 | const onClose = (): void => { 106 | clearTimeout(socketWaitTimeout); 107 | socket.removeEventListener("close", onClose); 108 | reject(); 109 | }; 110 | socket.addEventListener("close", onClose); 111 | 112 | socket.addEventListener("open", async () => { 113 | clearTimeout(socketWaitTimeout); 114 | resolve(); 115 | }); 116 | }); 117 | 118 | return socket; 119 | } 120 | 121 | /** 122 | * Dispose the current connection. 123 | */ 124 | private dispose(): void { 125 | if (this.activeSocket) { 126 | this.activeSocket.close(); 127 | } 128 | } 129 | } 130 | 131 | // Global instance so all fills can use the same client. 132 | export const client = new Client(new WebsocketConnection()); 133 | -------------------------------------------------------------------------------- /packages/ide/src/fill/dialog.scss: -------------------------------------------------------------------------------- 1 | .msgbox { 2 | padding-top: 25px; 3 | padding-left: 40px; 4 | padding-right: 40px; 5 | padding-bottom: 25px; 6 | background: #242424; 7 | -webkit-box-shadow: 0px 0px 10px -3px rgba(0,0,0,0.75); 8 | -moz-box-shadow: 0px 0px 10px -3px rgba(0,0,0,0.75); 9 | box-shadow: 0px 0px 10px -3px rgba(0,0,0,0.75); 10 | border-radius: 3px; 11 | } 12 | 13 | .msgbox.input { 14 | max-width: 500px; 15 | width: 100%; 16 | } 17 | 18 | .msgbox > .input { 19 | background: #141414; 20 | border: none; 21 | box-sizing: border-box; 22 | margin-bottom: 25px; 23 | padding: 10px; 24 | width: 100%; 25 | } 26 | 27 | .msgbox > .msg { 28 | font-size: 16px; 29 | font-weight: bold; 30 | } 31 | 32 | .msgbox > .detail { 33 | font-size: 14px; 34 | margin-top: 5px; 35 | } 36 | 37 | .msgbox > .errors { 38 | margin-bottom: 25px; 39 | } 40 | 41 | .msgbox > .errors { 42 | color: #f44747; 43 | } 44 | 45 | .msgbox > .button-wrapper { 46 | display: flex; 47 | flex-direction: row; 48 | justify-content: space-between; 49 | } 50 | 51 | .msgbox > .button-wrapper > button { 52 | flex: 1; 53 | border-radius: 2px; 54 | padding: 10px; 55 | color: white; 56 | background: #3d3d3d; 57 | border: 0px; 58 | cursor: pointer; 59 | opacity: 0.8; 60 | } 61 | 62 | .msgbox > .button-wrapper > button:hover { 63 | opacity: 1; 64 | } 65 | 66 | .msgbox > .button-wrapper > button:not(:last-child) { 67 | margin-right: 8px; 68 | } 69 | 70 | .msgbox-overlay { 71 | align-items: center; 72 | background: rgba(0, 0, 0, 0.4); 73 | bottom: 0; 74 | display: flex; 75 | justify-content: center; 76 | left: 0; 77 | opacity: 0; 78 | position: absolute; 79 | right: 0; 80 | top: 0; 81 | transition: 300ms opacity ease; 82 | z-index: 15; 83 | } 84 | -------------------------------------------------------------------------------- /packages/ide/src/fill/empty.ts: -------------------------------------------------------------------------------- 1 | export = {}; 2 | -------------------------------------------------------------------------------- /packages/ide/src/fill/notification.ts: -------------------------------------------------------------------------------- 1 | import { logger, field } from "@coder/logger"; 2 | 3 | export interface INotificationHandle { 4 | close(): void; 5 | updateMessage(message: string): void; 6 | updateButtons(buttons: INotificationButton[]): void; 7 | } 8 | 9 | export enum Severity { 10 | Ignore = 0, 11 | Info = 1, 12 | Warning = 2, 13 | Error = 3, 14 | } 15 | 16 | export interface INotificationButton { 17 | label: string; 18 | run(): void; 19 | } 20 | 21 | /** 22 | * Optional notification service. 23 | */ 24 | export interface INotificationService { 25 | error(error: Error): void; 26 | prompt(severity: Severity, message: string, buttons: INotificationButton[], onCancel: () => void): INotificationHandle; 27 | } 28 | 29 | export interface IProgress { 30 | /** 31 | * Report progress, which should be the completed percentage from 0 to 100. 32 | */ 33 | report(progress: number): void; 34 | } 35 | 36 | export interface IProgressService { 37 | /** 38 | * Start a new progress bar that resolves & disappears when the task finishes. 39 | */ 40 | start(title: string, task: (progress: IProgress) => Promise, onCancel: () => void): Promise; 41 | } 42 | 43 | /** 44 | * Console-based notification service. 45 | */ 46 | export class NotificationService implements INotificationService { 47 | public error(error: Error): void { 48 | logger.error(error.message, field("error", error)); 49 | } 50 | 51 | public prompt(severity: Severity, message: string, _buttons: INotificationButton[], _onCancel: () => void): INotificationHandle { 52 | switch (severity) { 53 | case Severity.Info: logger.info(message); break; 54 | case Severity.Warning: logger.warn(message); break; 55 | case Severity.Error: logger.error(message); break; 56 | } 57 | 58 | return { 59 | close: (): void => undefined, 60 | updateMessage: (): void => undefined, 61 | updateButtons: (): void => undefined, 62 | }; 63 | } 64 | } 65 | 66 | /** 67 | * Console-based progress service. 68 | */ 69 | export class ProgressService implements IProgressService { 70 | public start(title: string, task: (progress: IProgress) => Promise): Promise { 71 | logger.info(title); 72 | 73 | return task({ 74 | report: (progress): void => { 75 | logger.info(`${title} progress: ${progress}`); 76 | }, 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/ide/src/fill/os.ts: -------------------------------------------------------------------------------- 1 | import { OperatingSystem, InitData } from "@coder/protocol"; 2 | import { client } from "./client"; 3 | 4 | class OS { 5 | private _homedir: string | undefined; 6 | private _tmpdir: string | undefined; 7 | private _platform: NodeJS.Platform | undefined; 8 | 9 | public constructor() { 10 | client.initData.then((d) => this.initialize(d)); 11 | } 12 | 13 | public homedir(): string { 14 | if (typeof this._homedir === "undefined") { 15 | throw new Error("trying to access homedir before it has been set"); 16 | } 17 | 18 | return this._homedir; 19 | } 20 | 21 | public tmpdir(): string { 22 | if (typeof this._tmpdir === "undefined") { 23 | throw new Error("trying to access tmpdir before it has been set"); 24 | } 25 | 26 | return this._tmpdir; 27 | } 28 | 29 | public initialize(data: InitData): void { 30 | this._homedir = data.homeDirectory; 31 | this._tmpdir = data.tmpDirectory; 32 | switch (data.os) { 33 | case OperatingSystem.Windows: this._platform = "win32"; break; 34 | case OperatingSystem.Mac: this._platform = "darwin"; break; 35 | default: this._platform = "linux"; break; 36 | } 37 | process.platform = this._platform; 38 | } 39 | 40 | public release(): string { 41 | return "Unknown"; 42 | } 43 | 44 | public platform(): NodeJS.Platform { 45 | if (typeof this._platform === "undefined") { 46 | throw new Error("trying to access platform before it has been set"); 47 | } 48 | 49 | return this._platform; 50 | } 51 | } 52 | 53 | export = new OS(); 54 | -------------------------------------------------------------------------------- /packages/ide/src/fill/util.ts: -------------------------------------------------------------------------------- 1 | export * from "../../../../node_modules/util"; 2 | import { implementation } from "../../../../node_modules/util.promisify"; 3 | 4 | export const promisify = implementation; 5 | -------------------------------------------------------------------------------- /packages/ide/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | export * from "./fill/clipboard"; 3 | export * from "./fill/notification"; 4 | export * from "./retry"; 5 | export * from "./upload"; 6 | -------------------------------------------------------------------------------- /packages/ide/test/child_process.test.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from "child_process"; 2 | import * as path from "path"; 3 | import { Readable } from "stream"; 4 | import * as util from "util"; 5 | import { createClient } from "@coder/protocol/test"; 6 | 7 | const client = createClient(); 8 | jest.mock("../src/fill/client", () => ({ client })); 9 | const cp = require("../src/fill/child_process") as typeof import("child_process"); 10 | 11 | describe("child_process", () => { 12 | const getStdout = async (proc: ChildProcess): Promise => { 13 | return new Promise((r): Readable => proc.stdout.on("data", r)) 14 | .then((s) => s.toString()); 15 | }; 16 | 17 | describe("exec", () => { 18 | it("should get exec stdout", async () => { 19 | await expect(util.promisify(cp.exec)("echo test", { encoding: "utf8" })) 20 | .resolves.toEqual({ 21 | stdout: "test\n", 22 | stderr: "", 23 | }); 24 | }); 25 | }); 26 | 27 | describe("spawn", () => { 28 | it("should get spawn stdout", async () => { 29 | const proc = cp.spawn("echo", ["test"]); 30 | await expect(Promise.all([ 31 | getStdout(proc), 32 | new Promise((r): ChildProcess => proc.on("exit", r)), 33 | ]).then((values) => values[0])).resolves.toEqual("test\n"); 34 | }); 35 | 36 | it("should cat", async () => { 37 | const proc = cp.spawn("cat", []); 38 | expect(proc.pid).toBe(-1); 39 | proc.stdin.write("banana"); 40 | await expect(getStdout(proc)).resolves.toBe("banana"); 41 | 42 | proc.stdin.end(); 43 | proc.kill(); 44 | 45 | expect(proc.pid).toBeGreaterThan(-1); 46 | await new Promise((r): ChildProcess => proc.on("exit", r)); 47 | }); 48 | 49 | it("should print env", async () => { 50 | const proc = cp.spawn("env", [], { 51 | env: { hi: "donkey" }, 52 | }); 53 | 54 | await expect(getStdout(proc)).resolves.toContain("hi=donkey\n"); 55 | }); 56 | }); 57 | 58 | describe("fork", () => { 59 | it("should echo messages", async () => { 60 | const proc = cp.fork(path.join(__dirname, "forker.js")); 61 | 62 | proc.send({ bananas: true }); 63 | 64 | await expect(new Promise((r): ChildProcess => proc.on("message", r))) 65 | .resolves.toMatchObject({ 66 | bananas: true, 67 | }); 68 | 69 | proc.kill(); 70 | 71 | await new Promise((r): ChildProcess => proc.on("exit", r)); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /packages/ide/test/forker.js: -------------------------------------------------------------------------------- 1 | process.on("message", (data) => { 2 | process.send(data); 3 | }); 4 | -------------------------------------------------------------------------------- /packages/ide/test/net.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as nativeNet from "net"; 3 | import * as os from "os"; 4 | import * as path from "path"; 5 | import * as util from "util"; 6 | import * as rimraf from "rimraf"; 7 | import { createClient } from "@coder/protocol/test"; 8 | 9 | const client = createClient(); 10 | jest.mock("../src/fill/client", () => ({ client })); 11 | const net = require("../src/fill/net") as typeof import("net"); 12 | 13 | describe("net", () => { 14 | let i = 0; 15 | const coderDir = path.join(os.tmpdir(), "coder", "net"); 16 | const tmpFile = (): string => path.join(coderDir, `socket.${i++}`); 17 | 18 | beforeAll(async () => { 19 | try { 20 | await util.promisify(fs.mkdir)(path.dirname(coderDir)); 21 | } catch (error) { 22 | if (error.code !== "EEXIST" && error.code !== "EISDIR") { 23 | throw error; 24 | } 25 | } 26 | await util.promisify(rimraf)(coderDir); 27 | await util.promisify(fs.mkdir)(coderDir); 28 | }); 29 | 30 | describe("Socket", () => { 31 | const socketPath = tmpFile(); 32 | let server: nativeNet.Server; 33 | 34 | beforeAll(async () => { 35 | await new Promise((r): void => { 36 | server = nativeNet.createServer().listen(socketPath, r); 37 | }); 38 | }); 39 | 40 | afterAll(() => { 41 | server.close(); 42 | }); 43 | 44 | it("should connect", async () => { 45 | await new Promise((resolve): void => { 46 | const socket = net.createConnection(socketPath, () => { 47 | socket.end(); 48 | socket.addListener("close", () => { 49 | resolve(); 50 | }); 51 | }); 52 | }); 53 | 54 | await new Promise((resolve): void => { 55 | const socket = new net.Socket(); 56 | socket.connect(socketPath, () => { 57 | socket.end(); 58 | socket.addListener("close", () => { 59 | resolve(); 60 | }); 61 | }); 62 | }); 63 | }); 64 | 65 | it("should get data", (done) => { 66 | server.once("connection", (socket: nativeNet.Socket) => { 67 | socket.write("hi how r u"); 68 | }); 69 | 70 | const socket = net.createConnection(socketPath); 71 | 72 | socket.addListener("data", (data) => { 73 | expect(data.toString()).toEqual("hi how r u"); 74 | socket.end(); 75 | socket.addListener("close", () => { 76 | done(); 77 | }); 78 | }); 79 | }); 80 | 81 | it("should send data", (done) => { 82 | const clientSocket = net.createConnection(socketPath); 83 | clientSocket.write(Buffer.from("bananas")); 84 | server.once("connection", (socket: nativeNet.Socket) => { 85 | socket.addListener("data", (data) => { 86 | expect(data.toString()).toEqual("bananas"); 87 | socket.end(); 88 | clientSocket.addListener("end", () => { 89 | done(); 90 | }); 91 | }); 92 | }); 93 | }); 94 | }); 95 | 96 | describe("Server", () => { 97 | it("should listen", (done) => { 98 | const s = net.createServer(); 99 | s.on("listening", () => s.close()); 100 | s.on("close", () => done()); 101 | s.listen(tmpFile()); 102 | }); 103 | 104 | it("should get connection", async () => { 105 | let constructorListener: (() => void) | undefined; 106 | const s = net.createServer(() => { 107 | if (constructorListener) { 108 | constructorListener(); 109 | } 110 | }); 111 | 112 | const socketPath = tmpFile(); 113 | s.listen(socketPath); 114 | 115 | const makeConnection = async (): Promise => { 116 | net.createConnection(socketPath); 117 | await Promise.all([ 118 | new Promise((resolve): void => { 119 | constructorListener = resolve; 120 | }), 121 | new Promise((resolve): void => { 122 | s.once("connection", (socket) => { 123 | socket.destroy(); 124 | resolve(); 125 | }); 126 | }), 127 | ]); 128 | }; 129 | 130 | await makeConnection(); 131 | await makeConnection(); 132 | 133 | s.close(); 134 | await new Promise((r): nativeNet.Server => s.on("close", r)); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /packages/logger/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.build.json 3 | webpack.config.js 4 | yarn.lock -------------------------------------------------------------------------------- /packages/logger/README.md: -------------------------------------------------------------------------------- 1 | # Logger 2 | 3 | Beautiful logging inspired by https://github.com/uber-go/zap. 4 | 5 | - Built for node and the browser 6 | - Zero dependencies 7 | - Uses groups in the browser to reduce clutter 8 | 9 | ## Example Usage 10 | 11 | ```javascript 12 | import { field, logger } from "@coder/logger"; 13 | 14 | logger.info("Loading container", 15 | field("container_id", container.id_str), 16 | field("organization_id", organization.id_str)); 17 | ``` 18 | 19 | ## Formatting 20 | 21 | By default the logger uses a different formatter depending on whether it detects 22 | it is running in the browser or not. A custom formatter can be set: 23 | 24 | ```javascript 25 | import { logger, Formatter } from "@coder/logger"; 26 | 27 | class MyFormatter extends Formatter { 28 | // implementation ... 29 | } 30 | 31 | logger.formatter = new MyFormatter(); 32 | ``` 33 | -------------------------------------------------------------------------------- /packages/logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/logger", 3 | "description": "Beautiful logging inspired by https://github.com/uber-go/zap.", 4 | "scripts": { 5 | "build": "tsc -p tsconfig.build.json && cp ./out/packages/logger/src/* ./out && rm -rf out/packages && ../../node_modules/.bin/webpack --config ./webpack.config.js", 6 | "postinstall": "if [ ! -d out ];then npm run build; fi" 7 | }, 8 | "version": "1.0.3", 9 | "main": "out/main.js", 10 | "types": "out/index.d.ts", 11 | "author": "Coder", 12 | "license": "MIT" 13 | } 14 | -------------------------------------------------------------------------------- /packages/logger/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./logger"; 2 | -------------------------------------------------------------------------------- /packages/logger/src/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { field, logger, BrowserFormatter, Time } from "./logger"; 2 | 3 | describe("Logger", () => { 4 | it("should use server formatter", () => { 5 | logger.info("test", field("key", "value"), field("time", new Time(100, Date.now()))); 6 | logger.named("name").debug("test name"); 7 | logger.named("another name").warn("another test name"); 8 | }); 9 | 10 | it("should use browser formatter", () => { 11 | logger.formatter = new BrowserFormatter(); 12 | logger.info("test", field("key", "value"), field("time", new Time(100, Date.now()))); 13 | logger.named("name").debug("test name"); 14 | logger.named("another name").warn("another test name"); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/logger/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "declarationDir": "out", 5 | "declaration": true, 6 | "emitDeclarationOnly": true 7 | } 8 | } -------------------------------------------------------------------------------- /packages/logger/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const merge = require("webpack-merge"); 3 | 4 | module.exports = merge(require(path.join(__dirname, "../../scripts", "webpack.general.config.js"))(), { 5 | devtool: "none", 6 | mode: "production", 7 | target: "node", 8 | output: { 9 | path: path.join(__dirname, "out"), 10 | filename: "main.js", 11 | libraryTarget: "commonjs", 12 | }, 13 | entry: [ 14 | "./packages/logger/src/index.ts" 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /packages/logger/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "postinstall": "../node_modules/.bin/ts-node ../scripts/install-packages.ts", 4 | "test": "jest" 5 | }, 6 | "devDependencies": { 7 | "@types/jest": "^23.3.12", 8 | "jest": "^23.6.0", 9 | "ts-jest": "^23.10.5" 10 | }, 11 | "dependencies": { 12 | "xmlhttprequest": "1.8.0" 13 | }, 14 | "jest": { 15 | "moduleFileExtensions": [ 16 | "ts", 17 | "tsx", 18 | "js", 19 | "json" 20 | ], 21 | "setupFiles": [ 22 | "/../scripts/test-setup.js" 23 | ], 24 | "moduleNameMapper": { 25 | "^.+\\.(s?css|png|svg)$": "/../scripts/dummy.js", 26 | "@coder/ide/src/fill/evaluation": "/ide/src/fill/evaluation", 27 | "@coder/ide/src/fill/client": "/ide/src/fill/client", 28 | "@coder/(.*)/test": "/$1/test", 29 | "@coder/(.*)": "/$1/src" 30 | }, 31 | "transform": { 32 | "^.+\\.tsx?$": "ts-jest" 33 | }, 34 | "testPathIgnorePatterns": [ 35 | "/node_modules/", 36 | "/logger/" 37 | ], 38 | "testRegex": ".*\\.test\\.tsx?" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/protocol/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/protocol", 3 | "main": "src/index.ts", 4 | "dependencies": { 5 | "express": "^4.16.4", 6 | "google-protobuf": "^3.6.1", 7 | "node-pty-prebuilt": "^0.7.6", 8 | "spdlog": "^0.7.2", 9 | "tslib": "^1.9.3", 10 | "ws": "^6.1.2" 11 | }, 12 | "devDependencies": { 13 | "@types/google-protobuf": "^3.2.7", 14 | "@types/text-encoding": "^0.0.35", 15 | "text-encoding": "^0.7.0", 16 | "ts-protoc-gen": "^0.8.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/protocol/scripts/generate_proto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | protoc --plugin="protoc-gen-ts=./node_modules/.bin/protoc-gen-ts" --js_out="import_style=commonjs,binary:./src/proto" --ts_out="./src/proto" ./src/proto/*.proto --proto_path="./src/proto" -------------------------------------------------------------------------------- /packages/protocol/src/common/connection.ts: -------------------------------------------------------------------------------- 1 | export interface SendableConnection { 2 | send(data: Buffer | Uint8Array): void; 3 | } 4 | 5 | export interface ReadWriteConnection extends SendableConnection { 6 | onMessage(cb: (data: Uint8Array | Buffer) => void): void; 7 | onClose(cb: () => void): void; 8 | close(): void; 9 | } 10 | 11 | export enum OperatingSystem { 12 | Windows, 13 | Linux, 14 | Mac, 15 | } 16 | 17 | export interface InitData { 18 | readonly os: OperatingSystem; 19 | readonly dataDirectory: string; 20 | readonly workingDirectory: string; 21 | readonly homeDirectory: string; 22 | readonly tmpDirectory: string; 23 | readonly shell: string; 24 | readonly builtInExtensionsDirectory: string; 25 | } 26 | 27 | export interface SharedProcessData { 28 | readonly socketPath: string; 29 | readonly logPath: string; 30 | } 31 | -------------------------------------------------------------------------------- /packages/protocol/src/common/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return true if we're in a browser environment (including web workers). 3 | */ 4 | export const isBrowserEnvironment = (): boolean => { 5 | return typeof process === "undefined" || typeof process.stdout === "undefined"; 6 | }; 7 | 8 | /** 9 | * Escape a path. This prevents any issues with file names that have quotes, 10 | * spaces, braces, etc. 11 | */ 12 | export const escapePath = (path: string): string => { 13 | return `'${path.replace(/'/g, "'\\''")}'`; 14 | }; 15 | 16 | export type IEncodingOptions = { 17 | encoding?: string | null; 18 | flag?: string; 19 | mode?: string; 20 | persistent?: boolean; 21 | recursive?: boolean; 22 | } | string | undefined | null; 23 | 24 | // tslint:disable-next-line no-any 25 | export type IEncodingOptionsCallback = IEncodingOptions | ((err: NodeJS.ErrnoException, ...args: any[]) => void); 26 | 27 | /** 28 | * Stringify an event argument. isError is because although methods like 29 | * `fs.stat` are supposed to throw Error objects, they currently throw regular 30 | * objects when running tests through Jest. 31 | */ 32 | export const stringify = (arg: any, isError?: boolean): string => { // tslint:disable-line no-any 33 | if (arg instanceof Error || isError) { 34 | // Errors don't stringify at all. They just become "{}". 35 | return JSON.stringify({ 36 | type: "Error", 37 | data: { 38 | message: arg.message, 39 | stack: arg.stack, 40 | code: (arg as NodeJS.ErrnoException).code, 41 | }, 42 | }); 43 | } else if (arg instanceof Uint8Array) { 44 | // With stringify, these get turned into objects with each index becoming a 45 | // key for some reason. Then trying to do something like write that data 46 | // results in [object Object] being written. Stringify them like a Buffer 47 | // instead. 48 | return JSON.stringify({ 49 | type: "Buffer", 50 | data: Array.from(arg), 51 | }); 52 | } 53 | 54 | return JSON.stringify(arg); 55 | }; 56 | /** 57 | * Parse an event argument. 58 | */ 59 | export const parse = (arg: string): any => { // tslint:disable-line no-any 60 | const convert = (value: any): any => { // tslint:disable-line no-any 61 | if (value && value.data && value.type) { 62 | switch (value.type) { 63 | // JSON.stringify turns a Buffer into an object but JSON.parse doesn't 64 | // turn it back, it just remains an object. 65 | case "Buffer": 66 | if (Array.isArray(value.data)) { 67 | return Buffer.from(value); 68 | } 69 | break; 70 | // Errors apparently can't be stringified, so we do something similar to 71 | // what happens to buffers and stringify them as regular objects. 72 | case "Error": 73 | if (value.data.message) { 74 | const error = new Error(value.data.message); 75 | // TODO: Can we set the stack? Doing so seems to make it into an 76 | // "invalid object". 77 | if (typeof value.data.code !== "undefined") { 78 | (error as NodeJS.ErrnoException).code = value.data.code; 79 | } 80 | // tslint:disable-next-line no-any 81 | (error as any).originalStack = value.data.stack; 82 | 83 | return error; 84 | } 85 | break; 86 | } 87 | } 88 | 89 | if (value && typeof value === "object") { 90 | Object.keys(value).forEach((key) => { 91 | value[key] = convert(value[key]); 92 | }); 93 | } 94 | 95 | return value; 96 | }; 97 | 98 | return arg ? convert(JSON.parse(arg)) : arg; 99 | }; 100 | -------------------------------------------------------------------------------- /packages/protocol/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./browser/client"; 2 | export * from "./common/connection"; 3 | export * from "./common/helpers"; 4 | export * from "./common/util"; 5 | -------------------------------------------------------------------------------- /packages/protocol/src/proto/client.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | import "node.proto"; 3 | import "vscode.proto"; 4 | 5 | message ClientMessage { 6 | oneof msg { 7 | // node.proto 8 | NewEvalMessage new_eval = 11; 9 | EvalEventMessage eval_event = 12; 10 | 11 | Ping ping = 13; 12 | } 13 | } 14 | 15 | message ServerMessage { 16 | oneof msg { 17 | // node.proto 18 | EvalFailedMessage eval_failed = 13; 19 | EvalDoneMessage eval_done = 14; 20 | EvalEventMessage eval_event = 15; 21 | 22 | WorkingInitMessage init = 16; 23 | 24 | // vscode.proto 25 | SharedProcessActiveMessage shared_process_active = 17; 26 | 27 | Pong pong = 18; 28 | } 29 | } 30 | 31 | message WorkingInitMessage { 32 | string home_directory = 1; 33 | string tmp_directory = 2; 34 | string data_directory = 3; 35 | string working_directory = 4; 36 | enum OperatingSystem { 37 | Windows = 0; 38 | Linux = 1; 39 | Mac = 2; 40 | } 41 | OperatingSystem operating_system = 5; 42 | string shell = 6; 43 | string builtin_extensions_dir = 7; 44 | } 45 | -------------------------------------------------------------------------------- /packages/protocol/src/proto/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client_pb"; 2 | export * from "./node_pb"; 3 | export * from "./vscode_pb"; 4 | -------------------------------------------------------------------------------- /packages/protocol/src/proto/node.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message NewEvalMessage { 4 | uint64 id = 1; 5 | string function = 2; 6 | repeated string args = 3; 7 | // Timeout in ms 8 | uint32 timeout = 4; 9 | // Create active eval message. 10 | // Allows for dynamic communication for an eval 11 | bool active = 5; 12 | } 13 | 14 | message EvalEventMessage { 15 | uint64 id = 1; 16 | string event = 2; 17 | repeated string args = 3; 18 | } 19 | 20 | message EvalFailedMessage { 21 | uint64 id = 1; 22 | string response = 2; 23 | } 24 | 25 | message EvalDoneMessage { 26 | uint64 id = 1; 27 | string response = 2; 28 | } 29 | 30 | message Ping {} 31 | 32 | message Pong {} 33 | -------------------------------------------------------------------------------- /packages/protocol/src/proto/vscode.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // Sent when a shared process becomes active 4 | message SharedProcessActiveMessage { 5 | string socket_path = 1; 6 | string log_path = 2; 7 | } -------------------------------------------------------------------------------- /packages/protocol/src/proto/vscode_pb.d.ts: -------------------------------------------------------------------------------- 1 | // package: 2 | // file: vscode.proto 3 | 4 | import * as jspb from "google-protobuf"; 5 | 6 | export class SharedProcessActiveMessage extends jspb.Message { 7 | getSocketPath(): string; 8 | setSocketPath(value: string): void; 9 | 10 | getLogPath(): string; 11 | setLogPath(value: string): void; 12 | 13 | serializeBinary(): Uint8Array; 14 | toObject(includeInstance?: boolean): SharedProcessActiveMessage.AsObject; 15 | static toObject(includeInstance: boolean, msg: SharedProcessActiveMessage): SharedProcessActiveMessage.AsObject; 16 | static extensions: {[key: number]: jspb.ExtensionFieldInfo}; 17 | static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; 18 | static serializeBinaryToWriter(message: SharedProcessActiveMessage, writer: jspb.BinaryWriter): void; 19 | static deserializeBinary(bytes: Uint8Array): SharedProcessActiveMessage; 20 | static deserializeBinaryFromReader(message: SharedProcessActiveMessage, reader: jspb.BinaryReader): SharedProcessActiveMessage; 21 | } 22 | 23 | export namespace SharedProcessActiveMessage { 24 | export type AsObject = { 25 | socketPath: string, 26 | logPath: string, 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /packages/protocol/test/evaluate.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "./helpers"; 2 | 3 | describe("Evaluate", () => { 4 | const client = createClient(); 5 | 6 | it("should transfer string", async () => { 7 | const value = await client.evaluate(() => { 8 | return "hi"; 9 | }); 10 | 11 | expect(value).toEqual("hi"); 12 | }, 100); 13 | 14 | it("should compute from string", async () => { 15 | const start = "ban\%\$\"``a,,,,asdasd"; 16 | const value = await client.evaluate((_helper, a) => { 17 | return a; 18 | }, start); 19 | 20 | expect(value).toEqual(start); 21 | }, 100); 22 | 23 | it("should compute from object", async () => { 24 | const value = await client.evaluate((_helper, arg) => { 25 | return arg.bananas * 2; 26 | }, { bananas: 1 }); 27 | 28 | expect(value).toEqual(2); 29 | }, 100); 30 | 31 | it("should transfer object", async () => { 32 | const value = await client.evaluate(() => { 33 | return { alpha: "beta" }; 34 | }); 35 | 36 | expect(value.alpha).toEqual("beta"); 37 | }, 100); 38 | 39 | it("should require", async () => { 40 | const value = await client.evaluate(() => { 41 | const fs = require("fs") as typeof import("fs"); 42 | 43 | return Object.keys(fs).filter((f) => f === "readFileSync"); 44 | }); 45 | 46 | expect(value[0]).toEqual("readFileSync"); 47 | }, 100); 48 | 49 | it("should resolve with promise", async () => { 50 | const value = await client.evaluate(async () => { 51 | await new Promise((r): number => setTimeout(r, 100)); 52 | 53 | return "donkey"; 54 | }); 55 | 56 | expect(value).toEqual("donkey"); 57 | }, 250); 58 | 59 | it("should do active process", (done) => { 60 | const runner = client.run((ae) => { 61 | ae.on("first", () => { 62 | ae.emit("first:response"); 63 | ae.on("second", () => ae.emit("second:response")); 64 | }); 65 | 66 | const disposeCallbacks = void>>[]; 67 | const dispose = (): void => { 68 | disposeCallbacks.forEach((cb) => cb()); 69 | ae.emit("disposed"); 70 | }; 71 | 72 | return { 73 | onDidDispose: (cb: () => void): number => disposeCallbacks.push(cb), 74 | dispose, 75 | }; 76 | }); 77 | 78 | runner.emit("first"); 79 | runner.on("first:response", () => runner.emit("second")); 80 | runner.on("second:response", () => client.dispose()); 81 | 82 | runner.on("disposed", () => done()); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /packages/protocol/test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Emitter } from "@coder/events"; 2 | import { Client } from "../src/browser/client"; 3 | import { Server, ServerOptions } from "../src/node/server"; 4 | 5 | export const createClient = (serverOptions?: ServerOptions): Client => { 6 | const s2c = new Emitter(); 7 | const c2s = new Emitter(); 8 | const closeCallbacks = void>>[]; 9 | 10 | // tslint:disable-next-line no-unused-expression 11 | new Server({ 12 | close: (): void => closeCallbacks.forEach((cb) => cb()), 13 | onClose: (cb: () => void): number => closeCallbacks.push(cb), 14 | onMessage: (cb): void => { 15 | c2s.event((d) => cb(d)); 16 | }, 17 | send: (data): NodeJS.Timer => setTimeout(() => s2c.emit(data), 0), 18 | }, serverOptions); 19 | 20 | const client = new Client({ 21 | close: (): void => closeCallbacks.forEach((cb) => cb()), 22 | onClose: (cb: () => void): number => closeCallbacks.push(cb), 23 | onMessage: (cb): void => { 24 | s2c.event((d) => cb(d)); 25 | }, 26 | send: (data): NodeJS.Timer => setTimeout(() => c2s.emit(data), 0), 27 | }); 28 | 29 | return client; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/protocol/test/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./helpers"; 2 | -------------------------------------------------------------------------------- /packages/protocol/test/server.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "./helpers"; 2 | 3 | describe("Server", () => { 4 | const dataDirectory = "/tmp/example"; 5 | const workingDirectory = "/working/dir"; 6 | const builtInExtensionsDirectory = "/tmp/example"; 7 | const client = createClient({ 8 | dataDirectory, 9 | workingDirectory, 10 | builtInExtensionsDirectory, 11 | }); 12 | 13 | it("should get init msg", (done) => { 14 | client.initData.then((data) => { 15 | expect(data.dataDirectory).toEqual(dataDirectory); 16 | expect(data.workingDirectory).toEqual(workingDirectory); 17 | expect(data.builtInExtensionsDirectory).toEqual(builtInExtensionsDirectory); 18 | done(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/requirefs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requirefs", 3 | "description": "", 4 | "main": "src/index.ts", 5 | "scripts": { 6 | "benchmark": "ts-node ./test/*.bench.ts" 7 | }, 8 | "dependencies": { 9 | "jszip": "2.6.0", 10 | "path": "0.12.7", 11 | "resolve": "1.8.1" 12 | }, 13 | "devDependencies": { 14 | "@types/benchmark": "^1.0.31", 15 | "@types/jszip": "3.1.4", 16 | "@types/resolve": "0.0.8", 17 | "benchmark": "^2.1.4", 18 | "text-encoding": "0.6.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/requirefs/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./requirefs"; 2 | -------------------------------------------------------------------------------- /packages/requirefs/test/.gitignore: -------------------------------------------------------------------------------- 1 | !lib/node_modules 2 | *.tar 3 | *.zip 4 | -------------------------------------------------------------------------------- /packages/requirefs/test/lib/chained-1.js: -------------------------------------------------------------------------------- 1 | exports = require("./chained-2"); -------------------------------------------------------------------------------- /packages/requirefs/test/lib/chained-2.js: -------------------------------------------------------------------------------- 1 | exports = require("./chained-3"); -------------------------------------------------------------------------------- /packages/requirefs/test/lib/chained-3.js: -------------------------------------------------------------------------------- 1 | exports.text = "moo"; -------------------------------------------------------------------------------- /packages/requirefs/test/lib/customModule.js: -------------------------------------------------------------------------------- 1 | exports = require("donkey"); -------------------------------------------------------------------------------- /packages/requirefs/test/lib/individual.js: -------------------------------------------------------------------------------- 1 | exports.frog = "hi"; 2 | -------------------------------------------------------------------------------- /packages/requirefs/test/lib/nodeResolve.js: -------------------------------------------------------------------------------- 1 | const frogger = require("frogger"); 2 | 3 | exports = frogger; -------------------------------------------------------------------------------- /packages/requirefs/test/lib/node_modules/frogger/index.js: -------------------------------------------------------------------------------- 1 | exports.banana = "potato"; -------------------------------------------------------------------------------- /packages/requirefs/test/lib/scope.js: -------------------------------------------------------------------------------- 1 | exports = coder.test; -------------------------------------------------------------------------------- /packages/requirefs/test/lib/subfolder.js: -------------------------------------------------------------------------------- 1 | exports.orangeColor = require("./subfolder/oranges").orange; -------------------------------------------------------------------------------- /packages/requirefs/test/lib/subfolder/goingUp.js: -------------------------------------------------------------------------------- 1 | exports = require("../individual"); -------------------------------------------------------------------------------- /packages/requirefs/test/lib/subfolder/oranges.js: -------------------------------------------------------------------------------- 1 | exports.orange = "blue"; -------------------------------------------------------------------------------- /packages/requirefs/test/requirefs.bench.ts: -------------------------------------------------------------------------------- 1 | import * as benchmark from "benchmark"; 2 | import { performance } from "perf_hooks"; 3 | import { TestCaseArray, isMac } from "./requirefs.util"; 4 | 5 | const files = [ 6 | "./individual.js", "./chained-1", "./subfolder", 7 | "./subfolder/goingUp", "./nodeResolve", 8 | ]; 9 | const toBench = new TestCaseArray(); 10 | 11 | // Limits the amount of time taken for each test, 12 | // but increases uncertainty. 13 | benchmark.options.maxTime = 0.5; 14 | 15 | let suite = new benchmark.Suite(); 16 | let _start = 0; 17 | const addMany = (names: string[]): benchmark.Suite => { 18 | for (let name of names) { 19 | for (let file of files) { 20 | suite = suite.add(`${name} -> ${file}`, async () => { 21 | let rfs = await toBench.byName(name).rfs; 22 | rfs.require(file); 23 | }); 24 | } 25 | } 26 | _start = performance.now(); 27 | return suite; 28 | } 29 | // Returns mean time per operation, in microseconds (10^-6s). 30 | const mean = (c: any): number => { 31 | return Number((c.stats.mean * 10e+5).toFixed(5)); 32 | }; 33 | 34 | // Swap out the tar command for gtar, when on MacOS. 35 | let testNames = ["zip", "bsdtar", isMac ? "gtar" : "tar"]; 36 | addMany(testNames).on("cycle", (event: benchmark.Event) => { 37 | console.log(String(event.target) + ` (~${mean(event.target)} μs/op)`); 38 | }).on("complete", () => { 39 | const slowest = suite.filter("slowest").shift(); 40 | const fastest = suite.filter("fastest").shift(); 41 | console.log(`===\nFastest is ${fastest.name} with ~${mean(fastest)} μs/op`); 42 | if (slowest.name !== fastest.name) { 43 | console.log(`Slowest is ${slowest.name} with ~${mean(slowest)} μs/op`); 44 | } 45 | const d = ((performance.now() - _start)/1000).toFixed(2); 46 | console.log(`Benchmark took ${d} s`); 47 | }) 48 | .run({ "async": true }); 49 | -------------------------------------------------------------------------------- /packages/requirefs/test/requirefs.test.ts: -------------------------------------------------------------------------------- 1 | import { RequireFS } from "../src/requirefs"; 2 | import { TestCaseArray, isMac } from "./requirefs.util"; 3 | 4 | const toTest = new TestCaseArray(); 5 | 6 | describe("requirefs", () => { 7 | for (let i = 0; i < toTest.length(); i++) { 8 | const testCase = toTest.byID(i); 9 | if (!isMac && testCase.name === "gtar") { 10 | break; 11 | } 12 | if (isMac && testCase.name === "tar") { 13 | break; 14 | } 15 | 16 | describe(testCase.name, () => { 17 | let rfs: RequireFS; 18 | beforeAll(async () => { 19 | rfs = await testCase.rfs; 20 | }); 21 | 22 | it("should parse individual module", () => { 23 | expect(rfs.require("./individual.js").frog).toEqual("hi"); 24 | }); 25 | 26 | it("should parse chained modules", () => { 27 | expect(rfs.require("./chained-1").text).toEqual("moo"); 28 | }); 29 | 30 | it("should parse through subfolders", () => { 31 | expect(rfs.require("./subfolder").orangeColor).toEqual("blue"); 32 | }); 33 | 34 | it("should be able to move up directories", () => { 35 | expect(rfs.require("./subfolder/goingUp").frog).toEqual("hi"); 36 | }); 37 | 38 | it("should resolve node_modules", () => { 39 | expect(rfs.require("./nodeResolve").banana).toEqual("potato"); 40 | }); 41 | 42 | it("should access global scope", () => { 43 | // tslint:disable-next-line no-any for testing 44 | (window as any).coder = { 45 | test: "hi", 46 | }; 47 | expect(rfs.require("./scope")).toEqual("hi"); 48 | }); 49 | 50 | it("should find custom module", () => { 51 | rfs.provide("donkey", "ok"); 52 | expect(rfs.require("./customModule")).toEqual("ok"); 53 | }); 54 | }); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /packages/requirefs/test/requirefs.util.ts: -------------------------------------------------------------------------------- 1 | import * as cp from "child_process"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import * as os from "os"; 5 | import { fromTar, RequireFS, fromZip } from "../src/requirefs"; 6 | 7 | export const isMac = os.platform() === "darwin"; 8 | 9 | /** 10 | * Encapsulates a RequireFS Promise and the 11 | * name of the test case it will be used in. 12 | */ 13 | interface TestCase { 14 | rfs: Promise; 15 | name: string; 16 | } 17 | 18 | /** 19 | * TestCaseArray allows tests and benchmarks to share 20 | * test cases while limiting redundancy. 21 | */ 22 | export class TestCaseArray { 23 | private cases: Array = []; 24 | 25 | constructor(cases?: Array) { 26 | if (!cases) { 27 | this.cases = TestCaseArray.defaults(); 28 | return 29 | } 30 | this.cases = cases; 31 | } 32 | 33 | /** 34 | * Returns default test cases. MacOS users need to have `gtar` binary 35 | * in order to run GNU-tar tests and benchmarks. 36 | */ 37 | public static defaults(): Array { 38 | let cases: Array = [ 39 | TestCaseArray.newCase("cd lib && zip -r ../lib.zip ./*", "lib.zip", async (c) => fromZip(c), "zip"), 40 | TestCaseArray.newCase("cd lib && bsdtar cvf ../lib.tar ./*", "lib.tar", async (c) => fromTar(c), "bsdtar"), 41 | ]; 42 | if (isMac) { 43 | const gtarInstalled: boolean = cp.execSync("which tar").length > 0; 44 | if (gtarInstalled) { 45 | cases.push(TestCaseArray.newCase("cd lib && gtar cvf ../lib.tar ./*", "lib.tar", async (c) => fromTar(c), "gtar")); 46 | } else { 47 | throw new Error("failed to setup gtar test case, gtar binary is necessary to test GNU-tar on MacOS"); 48 | } 49 | } else { 50 | cases.push(TestCaseArray.newCase("cd lib && tar cvf ../lib.tar ./*", "lib.tar", async (c) => fromTar(c), "tar")); 51 | } 52 | return cases; 53 | }; 54 | 55 | /** 56 | * Returns a test case prepared with the provided RequireFS Promise. 57 | * @param command Command to run immediately. For setup. 58 | * @param targetFile File to be read and handled by prepare function. 59 | * @param prepare Run on target file contents before test. 60 | * @param name Test case name. 61 | */ 62 | public static newCase(command: string, targetFile: string, prepare: (content: Uint8Array) => Promise, name: string): TestCase { 63 | cp.execSync(command, { cwd: __dirname }); 64 | const content = fs.readFileSync(path.join(__dirname, targetFile)); 65 | return { 66 | name, 67 | rfs: prepare(content), 68 | }; 69 | } 70 | 71 | /** 72 | * Returns updated TestCaseArray instance, with a new test case. 73 | * @see TestCaseArray.newCase 74 | */ 75 | public add(command: string, targetFile: string, prepare: (content: Uint8Array) => Promise, name: string): TestCaseArray { 76 | this.cases.push(TestCaseArray.newCase(command, targetFile, prepare, name)); 77 | return this; 78 | }; 79 | 80 | /** 81 | * Gets a test case by index. 82 | * @param id Test case index. 83 | */ 84 | public byID(id: number): TestCase { 85 | if (!this.cases[id]) { 86 | if (id < 0 || id >= this.cases.length) { 87 | throw new Error(`test case index "${id}" out of bounds`); 88 | } 89 | throw new Error(`test case at index "${id}" not found`); 90 | } 91 | return this.cases[id]; 92 | } 93 | 94 | /** 95 | * Gets a test case by name. 96 | * @param name Test case name. 97 | */ 98 | public byName(name: string): TestCase { 99 | let c = this.cases.find((c) => c.name === name); 100 | if (!c) { 101 | throw new Error(`test case "${name}" not found`); 102 | } 103 | return c; 104 | } 105 | 106 | /** 107 | * Gets the number of test cases. 108 | */ 109 | public length(): number { 110 | return this.cases.length; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /packages/requirefs/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/benchmark@^1.0.31": 6 | version "1.0.31" 7 | resolved "https://registry.yarnpkg.com/@types/benchmark/-/benchmark-1.0.31.tgz#2dd3514e93396f362ba5551a7c9ff0da405c1d38" 8 | integrity sha512-F6fVNOkGEkSdo/19yWYOwVKGvzbTeWkR/XQYBKtGBQ9oGRjBN9f/L4aJI4sDcVPJO58Y1CJZN8va9V2BhrZapA== 9 | 10 | "@types/jszip@3.1.4": 11 | version "3.1.4" 12 | resolved "https://registry.yarnpkg.com/@types/jszip/-/jszip-3.1.4.tgz#9b81e3901a6988e9459ac27abf483e6b892251af" 13 | integrity sha512-UaVbz4buRlBEolZYrxqkrGDOypugYlbqGNrUFB4qBaexrLypTH0jyvaF5jolNy5D+5C4kKV1WJ3Yx9cn/JH8oA== 14 | dependencies: 15 | "@types/node" "*" 16 | 17 | "@types/node@*": 18 | version "10.11.3" 19 | resolved "https://registry.yarnpkg.com/@types/node/-/node-10.11.3.tgz#c055536ac8a5e871701aa01914be5731539d01ee" 20 | integrity sha512-3AvcEJAh9EMatxs+OxAlvAEs7OTy6AG94mcH1iqyVDwVVndekLxzwkWQ/Z4SDbY6GO2oyUXyWW8tQ4rENSSQVQ== 21 | 22 | "@types/resolve@0.0.8": 23 | version "0.0.8" 24 | resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" 25 | integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ== 26 | dependencies: 27 | "@types/node" "*" 28 | 29 | benchmark@^2.1.4: 30 | version "2.1.4" 31 | resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.4.tgz#09f3de31c916425d498cc2ee565a0ebf3c2a5629" 32 | integrity sha1-CfPeMckWQl1JjMLuVloOvzwqVik= 33 | dependencies: 34 | lodash "^4.17.4" 35 | platform "^1.3.3" 36 | 37 | inherits@2.0.3: 38 | version "2.0.3" 39 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 40 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 41 | 42 | jszip@2.6.0: 43 | version "2.6.0" 44 | resolved "https://registry.yarnpkg.com/jszip/-/jszip-2.6.0.tgz#7fb3e9c2f11c8a9840612db5dabbc8cf3a7534b7" 45 | integrity sha1-f7PpwvEciphAYS212rvIzzp1NLc= 46 | dependencies: 47 | pako "~1.0.0" 48 | 49 | lodash@^4.17.4: 50 | version "4.17.11" 51 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" 52 | integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== 53 | 54 | pako@~1.0.0: 55 | version "1.0.6" 56 | resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" 57 | integrity sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg== 58 | 59 | path-parse@^1.0.5: 60 | version "1.0.6" 61 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" 62 | integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== 63 | 64 | path@0.12.7: 65 | version "0.12.7" 66 | resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" 67 | integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8= 68 | dependencies: 69 | process "^0.11.1" 70 | util "^0.10.3" 71 | 72 | platform@^1.3.3: 73 | version "1.3.5" 74 | resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444" 75 | integrity sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q== 76 | 77 | process@^0.11.1: 78 | version "0.11.10" 79 | resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" 80 | integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= 81 | 82 | resolve@1.8.1: 83 | version "1.8.1" 84 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" 85 | integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA== 86 | dependencies: 87 | path-parse "^1.0.5" 88 | 89 | text-encoding@0.6.4: 90 | version "0.6.4" 91 | resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" 92 | integrity sha1-45mpgiV6J22uQou5KEXLcb3CbRk= 93 | 94 | util@^0.10.3: 95 | version "0.10.4" 96 | resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" 97 | integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== 98 | dependencies: 99 | inherits "2.0.3" 100 | -------------------------------------------------------------------------------- /packages/runner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/runner", 3 | "main": "src/index.ts" 4 | } -------------------------------------------------------------------------------- /packages/runner/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./runner"; 2 | -------------------------------------------------------------------------------- /packages/runner/src/runner.ts: -------------------------------------------------------------------------------- 1 | import * as cp from "child_process"; 2 | import {field, Logger, logger, time} from "@coder/logger"; 3 | 4 | export interface CommandResult { 5 | readonly exitCode: number; 6 | readonly stdout: string; 7 | readonly stderr: string; 8 | } 9 | 10 | const execute = (command: string, args: string[] = [], options: cp.SpawnOptions, logger: Logger): Promise => { 11 | let resolve: (result: CommandResult) => void; 12 | const prom = new Promise((res): void => { 13 | resolve = res; 14 | }); 15 | 16 | const stdout: string[] = []; 17 | const stderr: string[] = []; 18 | const complete = (exitCode: number): void => { 19 | resolve({ 20 | stderr: stderr.join(""), 21 | stdout: stdout.join(""), 22 | exitCode, 23 | }); 24 | }; 25 | logger.info(`Executing '${command} ${JSON.stringify(args)}'`, field("options", options)); 26 | const proc = cp.spawn(command, args.length > 0 ? args : [], options); 27 | proc.on("close", (code) => { 28 | complete(code); 29 | }); 30 | proc.on("exit", (code) => { 31 | complete(code!); 32 | }); 33 | proc.stdout.on("data", (d) => { 34 | stdout.push(d.toString()); 35 | logger.debug("stdio", field("stdout", d.toString())); 36 | }); 37 | proc.stderr.on("data", (d) => { 38 | stderr.push(d.toString()); 39 | logger.debug("stdio", field("stderr", d.toString())); 40 | }); 41 | 42 | return prom; 43 | }; 44 | 45 | // tslint:disable-next-line no-any 46 | export type TaskFunction = (runner: Runner, ...args: any[]) => void | Promise; 47 | 48 | export interface Runner { 49 | cwd: string; 50 | 51 | execute(command: string, args?: string[], env?: object): Promise; 52 | } 53 | 54 | export interface Task { 55 | readonly name: string; 56 | readonly func: TaskFunction; 57 | } 58 | 59 | const tasks = new Map(); 60 | const activated = new Map>(); 61 | 62 | export const register = (name: string, func: TaskFunction): () => void | Promise => { 63 | if (tasks.has(name)) { 64 | throw new Error(`Task "${name}" already registered`); 65 | } 66 | 67 | tasks.set(name, { 68 | name, 69 | func, 70 | }); 71 | 72 | return (): void | Promise => { 73 | return run(name); 74 | }; 75 | }; 76 | 77 | export const run = (name: string = process.argv[2]): void | Promise => { 78 | const task = tasks.get(name); 79 | if (!task) { 80 | logger.error("Task not found.", field("name", name), field("available", Array.from(tasks.keys()))); 81 | 82 | return process.exit(1); 83 | } 84 | if (activated.has(name)) { 85 | return activated.get(name); 86 | } 87 | let cwd: string = process.cwd(); 88 | const log = logger.named(name); 89 | const timer = time(Number.MAX_SAFE_INTEGER); 90 | let outputTimer: NodeJS.Timer | undefined; 91 | log.info("Starting..."); 92 | const prom = task.func({ 93 | set cwd(path: string) { 94 | cwd = path; 95 | }, 96 | execute(command: string, args: string[] = [], env?: object): Promise { 97 | const prom = execute(command, args, { 98 | cwd, 99 | env: env as NodeJS.ProcessEnv, 100 | }, log); 101 | 102 | return prom.then((result: CommandResult) => { 103 | if (result.exitCode != 0) { 104 | log.error("failed", 105 | field("exitCode", result.exitCode), 106 | field("stdout", result.stdout), 107 | field("stderr", result.stderr) 108 | ); 109 | } 110 | 111 | return result; 112 | }); 113 | }, 114 | }, ...process.argv.slice(3)); 115 | 116 | if (prom) { 117 | activated.set(name, prom); 118 | 119 | const doOutput = (): void => { 120 | outputTimer = setTimeout(() => { 121 | log.info("Still running..."); 122 | doOutput(); 123 | }, 60 * 1000 * 5); 124 | }; 125 | doOutput(); 126 | 127 | prom.then(() => { 128 | if (outputTimer) { 129 | clearTimeout(outputTimer); 130 | } 131 | log.info("Completed!", field("time", timer)); 132 | }).catch((ex) => { 133 | activated.delete(name); 134 | log.error(`Failed: ${ex.message}`); 135 | log.error(`Stack: ${ex.stack}`); 136 | 137 | return process.exit(1); 138 | }); 139 | } 140 | 141 | return prom; 142 | }; 143 | -------------------------------------------------------------------------------- /packages/runner/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/server/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | cli* 3 | !cli.ts 4 | build 5 | resources 6 | 7 | # This file is generated when the binary is created. 8 | # We want to use the parent tsconfig so we can ignore it. 9 | tsconfig.json 10 | -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 | # server 2 | 3 | ## Endpoints 4 | 5 | ### `/tunnel/` 6 | 7 | Tunnels a TCP connection over WebSockets. Implemented for proxying connections from a remote machine locally. 8 | 9 | ### `/ports` 10 | 11 | Watches for open ports. Implemented for tunneling ports on the remote server. 12 | 13 | ### `/resource/` 14 | 15 | Reads files on GET. 16 | Writes files on POST. -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "main": "./out/cli.js", 4 | "bin": "./out/cli.js", 5 | "files": [], 6 | "scripts": { 7 | "start": "node --max-old-space-size=32384 --require ts-node/register --require tsconfig-paths/register src/cli.ts", 8 | "build": "rm -rf ./out && ../../node_modules/.bin/cross-env CLI=true UV_THREADPOOL_SIZE=100 node --max-old-space-size=32384 ../../node_modules/webpack/bin/webpack.js --config ./webpack.config.js", 9 | "build:nexe": "node scripts/nexe.js" 10 | }, 11 | "dependencies": { 12 | "@oclif/config": "^1.10.4", 13 | "@oclif/errors": "^1.2.2", 14 | "@oclif/plugin-help": "^2.1.4", 15 | "express": "^4.16.4", 16 | "express-static-gzip": "^1.1.3", 17 | "httpolyglot": "^0.1.2", 18 | "mime-types": "^2.1.21", 19 | "node-netstat": "^1.6.0", 20 | "pem": "^1.14.1", 21 | "promise.prototype.finally": "^3.1.0", 22 | "ws": "^6.1.2", 23 | "xhr2": "^0.1.4" 24 | }, 25 | "devDependencies": { 26 | "@types/express": "^4.16.0", 27 | "@types/fs-extra": "^5.0.4", 28 | "@types/mime-types": "^2.1.0", 29 | "@types/opn": "^5.1.0", 30 | "@types/pem": "^1.9.4", 31 | "@types/ws": "^6.0.1", 32 | "fs-extra": "^7.0.1", 33 | "nexe": "^2.0.0-rc.34", 34 | "opn": "^5.4.0", 35 | "string-replace-webpack-plugin": "^0.1.3", 36 | "ts-node": "^7.0.1", 37 | "tsconfig-paths": "^3.7.0", 38 | "typescript": "^3.2.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/server/scripts/nexe.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const fse = require("fs-extra"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | const nexePath = require.resolve("nexe"); 7 | const shimPath = path.join(path.dirname(nexePath), "lib/steps/shim.js"); 8 | let shimContent = fs.readFileSync(shimPath).toString(); 9 | const replaceString = `global.nativeFs = { existsSync: originalExistsSync, readFile: originalReadFile, readFileSync: originalReadFileSync, createReadStream: originalCreateReadStream, readdir: originalReaddir, readdirSync: originalReaddirSync, statSync: originalStatSync, stat: originalStat, realpath: originalRealpath, realpathSync: originalRealpathSync };`; 10 | shimContent = shimContent.replace(/compiler\.options\.resources\.length[\s\S]*wrap\("(.*\\n)"/g, (om, a) => { 11 | return om.replace(a, `${a}${replaceString}`); 12 | }); 13 | fs.writeFileSync(shimPath, shimContent); 14 | 15 | const nexe = require("nexe"); 16 | 17 | const target = `${os.platform()}-${os.arch()}`; 18 | nexe.compile({ 19 | debugBundle: true, 20 | input: path.join(__dirname, "../out/cli.js"), 21 | output: `cli-${target}`, 22 | targets: [target], 23 | /** 24 | * To include native extensions, do NOT install node_modules for each one. They 25 | * are not required as each extension is built using webpack. 26 | */ 27 | resources: [ 28 | path.join(__dirname, "../package.json"), 29 | path.join(__dirname, "../build/**/*"), 30 | ], 31 | }); 32 | -------------------------------------------------------------------------------- /packages/server/src/constants.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | export const isCli = typeof process.env.CLI !== "undefined" && process.env.CLI !== "false"; 4 | export const serveStatic = typeof process.env.SERVE_STATIC !== "undefined" && process.env.SERVE_STATIC !== "false"; 5 | export const buildDir = process.env.BUILD_DIR ? path.resolve(process.env.BUILD_DIR) : ""; 6 | -------------------------------------------------------------------------------- /packages/server/src/ipc.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import { ChildProcess } from "child_process"; 3 | 4 | export interface IpcMessage { 5 | readonly event: string; 6 | readonly args: any[]; // tslint:disable-line no-any 7 | } 8 | 9 | export class StdioIpcHandler extends EventEmitter { 10 | private isListening: boolean = false; 11 | 12 | public constructor( 13 | private readonly childProcess?: ChildProcess, 14 | ) { 15 | super(); 16 | } 17 | 18 | // tslint:disable-next-line no-any 19 | public on(event: string, cb: (...args: any[]) => void): this { 20 | this.listen(); 21 | 22 | return super.on(event, cb); 23 | } 24 | 25 | // tslint:disable-next-line no-any 26 | public once(event: string, cb: (...args: any[]) => void): this { 27 | this.listen(); 28 | 29 | return super.once(event, cb); 30 | } 31 | 32 | // tslint:disable-next-line no-any 33 | public addListener(event: string, cb: (...args: any[]) => void): this { 34 | this.listen(); 35 | 36 | return super.addListener(event, cb); 37 | } 38 | 39 | // tslint:disable-next-line no-any 40 | public send(event: string, ...args: any[]): void { 41 | const msg: IpcMessage = { 42 | event, 43 | args, 44 | }; 45 | const d = JSON.stringify(msg); 46 | if (this.childProcess) { 47 | this.childProcess.stdin.write(d + "\n"); 48 | } else { 49 | process.stdout.write(d); 50 | } 51 | } 52 | 53 | private listen(): void { 54 | if (this.isListening) { 55 | return; 56 | } 57 | // tslint:disable-next-line no-any 58 | const onData = (data: any): void => { 59 | try { 60 | const d = JSON.parse(data.toString()) as IpcMessage; 61 | this.emit(d.event, ...d.args); 62 | } catch (ex) { 63 | if (!this.childProcess) { 64 | process.stderr.write(`Failed to parse incoming data: ${ex.message}`); 65 | } 66 | } 67 | }; 68 | if (this.childProcess) { 69 | this.childProcess.stdout.resume(); 70 | this.childProcess.stdout.on("data", onData); 71 | } else { 72 | process.stdin.resume(); 73 | process.stdin.on("data", onData); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/server/src/modules.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import * as os from "os"; 4 | import { isCli, buildDir } from "./constants"; 5 | 6 | /** 7 | * Handling of native modules within the CLI 8 | */ 9 | export const setup = (dataDirectory: string): void => { 10 | path.resolve(dataDirectory, "dependencies").split(path.sep).reduce((parentDir, childDir) => { 11 | const currentDir = path.join(parentDir, childDir); 12 | try { 13 | fs.mkdirSync(currentDir); 14 | } catch (ex) { 15 | if (ex.code !== "EEXIST" && ex.code !== "EISDIR" && ex.code !== "ENOENT") { 16 | throw ex; 17 | } 18 | } 19 | 20 | return currentDir; 21 | }, os.platform() === "win32" ? undefined! : path.sep); // Might need path.sep here for linux. Having it for windows causes an error because \C:\Users ... 22 | 23 | const unpackModule = (moduleName: string, markExecutable: boolean = false): void => { 24 | const memFile = path.join(isCli ? buildDir! : path.join(__dirname, ".."), "build/dependencies", moduleName); 25 | const diskFile = path.join(dataDirectory, "dependencies", moduleName); 26 | if (!fs.existsSync(diskFile)) { 27 | fs.writeFileSync(diskFile, fs.readFileSync(memFile)); 28 | } 29 | if (markExecutable) { 30 | fs.chmodSync(diskFile, "755"); 31 | } 32 | }; 33 | 34 | /** 35 | * We need to unpack node-pty and patch its `loadNative` function to require our unpacked pty.node 36 | * If pty.node isn't unpacked a SIGSEGV is thrown and the application exits. The exact reasoning 37 | * for this is unknown ATM, but this patch works around it. 38 | */ 39 | unpackModule("pty.node"); 40 | unpackModule("spdlog.node"); 41 | unpackModule("rg", true); 42 | // const nodePtyUtils = require("../../protocol/node_modules/node-pty-prebuilt/lib/utils") as typeof import("../../protocol/node_modules/node-pty-prebuilt/src/utils"); 43 | // tslint:disable-next-line:no-any 44 | // nodePtyUtils.loadNative = (modName: string): any => { 45 | // return (typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require)(path.join(dataDirectory, "dependencies", modName + ".node")); 46 | // }; 47 | (global).RIPGREP_LOCATION = path.join(dataDirectory, "dependencies", "rg"); 48 | (global).NODEPTY_LOCATION = path.join(dataDirectory, "dependencies", "pty.node"); 49 | // tslint:disable-next-line:no-any 50 | (global).SPDLOG_LOCATION = path.join(dataDirectory, "dependencies", "spdlog.node"); 51 | // tslint:disable-next-line:no-unused-expression 52 | require("../../protocol/node_modules/node-pty-prebuilt/lib/index") as typeof import("../../protocol/node_modules/node-pty-prebuilt/src/index"); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/server/src/portScanner.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import * as netstat from "node-netstat"; 3 | import { Event, Emitter } from "@coder/events"; 4 | 5 | export interface PortScanner { 6 | readonly ports: ReadonlyArray; 7 | 8 | readonly onAdded: Event>; 9 | readonly onRemoved: Event>; 10 | 11 | dispose(): void; 12 | } 13 | 14 | /** 15 | * Creates a disposable port scanner. 16 | * Will scan local ports and emit events when ports are added or removed. 17 | * Currently only scans TCP ports. 18 | */ 19 | export const createPortScanner = (scanInterval: number = 250): PortScanner => { 20 | const ports = new Map(); 21 | 22 | const addEmitter = new Emitter(); 23 | const removeEmitter = new Emitter(); 24 | 25 | const scan = (onCompleted: (err?: Error) => void): void => { 26 | const scanTime = Date.now(); 27 | const added: number[] = []; 28 | netstat({ 29 | done: (err: Error): void => { 30 | const removed: number[] = []; 31 | ports.forEach((value, key) => { 32 | if (value !== scanTime) { 33 | // Remove port 34 | removed.push(key); 35 | ports.delete(key); 36 | } 37 | }); 38 | if (removed.length > 0) { 39 | removeEmitter.emit(removed); 40 | } 41 | 42 | if (added.length > 0) { 43 | addEmitter.emit(added); 44 | } 45 | 46 | onCompleted(err); 47 | }, 48 | filter: { 49 | state: "LISTEN", 50 | }, 51 | }, (data: { 52 | readonly protocol: string; 53 | readonly local: { 54 | readonly port: number; 55 | readonly address: string; 56 | }; 57 | }) => { 58 | // https://en.wikipedia.org/wiki/Registered_port 59 | if (data.local.port <= 1023 || data.local.port >= 49151) { 60 | return; 61 | } 62 | // Only forward TCP ports 63 | if (!data.protocol.startsWith("tcp")) { 64 | return; 65 | } 66 | 67 | if (!ports.has(data.local.port)) { 68 | added.push(data.local.port); 69 | } 70 | ports.set(data.local.port, scanTime); 71 | }); 72 | }; 73 | 74 | let lastTimeout: NodeJS.Timer | undefined; 75 | let disposed: boolean = false; 76 | 77 | const doInterval = (): void => { 78 | scan(() => { 79 | if (disposed) { 80 | return; 81 | } 82 | lastTimeout = setTimeout(doInterval, scanInterval); 83 | }); 84 | }; 85 | 86 | doInterval(); 87 | 88 | return { 89 | get ports(): number[] { 90 | return Array.from(ports.keys()); 91 | }, 92 | get onAdded(): Event { 93 | return addEmitter.event; 94 | }, 95 | get onRemoved(): Event { 96 | return removeEmitter.event; 97 | }, 98 | dispose(): void { 99 | if (typeof lastTimeout !== "undefined") { 100 | clearTimeout(lastTimeout); 101 | } 102 | disposed = true; 103 | }, 104 | }; 105 | }; 106 | -------------------------------------------------------------------------------- /packages/server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const merge = require("webpack-merge"); 4 | 5 | const root = path.resolve(__dirname, "../.."); 6 | 7 | module.exports = merge( 8 | require(path.join(root, "scripts/webpack.node.config.js"))({ 9 | // Config options. 10 | }), { 11 | output: { 12 | filename: "cli.js", 13 | path: path.join(__dirname, "out"), 14 | libraryTarget: "commonjs", 15 | }, 16 | node: { 17 | console: false, 18 | global: false, 19 | process: false, 20 | Buffer: false, 21 | __filename: false, 22 | __dirname: false, 23 | setImmediate: false 24 | }, 25 | resolve: { 26 | alias: { 27 | "node-pty": "node-pty-prebuilt", 28 | }, 29 | }, 30 | externals: ["tslib", "trash"], 31 | entry: "./packages/server/src/cli.ts", 32 | plugins: [ 33 | new webpack.DefinePlugin({ 34 | "process.env.BUILD_DIR": `"${__dirname.replace(/\\/g, "\\\\")}"`, 35 | "process.env.CLI": `"${process.env.CLI ? "true" : "false"}"`, 36 | }), 37 | ], 38 | }, 39 | ); 40 | -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/tunnel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/tunnel", 3 | "main": "src/tunnel.ts" 4 | } -------------------------------------------------------------------------------- /packages/tunnel/src/client.ts: -------------------------------------------------------------------------------- 1 | import { Event, Emitter } from "@coder/events"; 2 | import { TunnelCloseCode } from "./common"; 3 | 4 | export interface TunnelCloseEvent { 5 | readonly code: TunnelCloseCode; 6 | readonly reason: string; 7 | } 8 | 9 | export interface ClientConnection { 10 | readonly onData: Event; 11 | readonly onClose: Event; 12 | send(data: ArrayBuffer): void; 13 | } 14 | 15 | export const forward = (connectionUrl: string): Promise => { 16 | return new Promise((resolve, reject): void => { 17 | const socket = new WebSocket(connectionUrl); 18 | const closeEmitter = new Emitter(); 19 | const dataEmitter = new Emitter(); 20 | const connection: ClientConnection = { 21 | get onClose(): Event { 22 | return closeEmitter.event; 23 | }, 24 | get onData(): Event { 25 | return dataEmitter.event; 26 | }, 27 | send(data: ArrayBuffer): void { 28 | socket.send(data); 29 | }, 30 | }; 31 | socket.binaryType = "arraybuffer"; 32 | socket.addEventListener("message", (event) => { 33 | dataEmitter.emit(event.data); 34 | }); 35 | socket.addEventListener("error", (event) => { 36 | reject("uncertain"); 37 | }); 38 | socket.addEventListener("open", () => { 39 | resolve(connection); 40 | }); 41 | socket.addEventListener("close", (event) => { 42 | closeEmitter.emit({ 43 | code: event.code, 44 | reason: event.reason, 45 | }); 46 | }); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /packages/tunnel/src/common.ts: -------------------------------------------------------------------------------- 1 | export enum TunnelCloseCode { 2 | Normal = 1000, 3 | Error = 4000, 4 | ConnectionRefused = 4001, 5 | } 6 | -------------------------------------------------------------------------------- /packages/tunnel/src/server.ts: -------------------------------------------------------------------------------- 1 | import * as net from "net"; 2 | import { TunnelCloseCode } from "./common"; 3 | 4 | export interface WS { 5 | addEventListener(event: "message", cb: (event: { 6 | // tslint:disable-next-line:no-any 7 | readonly data: any; 8 | }) => void): void; 9 | addEventListener(event: "close", cb: () => void): void; 10 | binaryType: string; 11 | close(code: number, reason?: string): void; 12 | // tslint:disable-next-line:no-any 13 | send(data: any): void; 14 | } 15 | 16 | export const handle = async (socket: WS, port: number): Promise => { 17 | const hosts = [ 18 | "127.0.0.1", 19 | "::", // localhost 20 | ]; 21 | 22 | let localSocket: net.Socket | undefined; 23 | for (let i = 0; i < hosts.length; i++) { 24 | if (localSocket) { 25 | break; 26 | } 27 | localSocket = await new Promise((resolve, reject): void => { 28 | const socket = net.connect({ 29 | host: hosts[i], 30 | port, 31 | }, () => { 32 | // Connected 33 | resolve(socket); 34 | }); 35 | socket.on("error", (err: Error & { readonly code: string }) => { 36 | if (err.code === "ECONNREFUSED") { 37 | resolve(undefined); 38 | } 39 | }); 40 | }); 41 | } 42 | if (!localSocket) { 43 | socket.close(TunnelCloseCode.ConnectionRefused); 44 | 45 | return; 46 | } 47 | socket.binaryType = "arraybuffer"; 48 | socket.addEventListener("message", (event) => localSocket!.write(Buffer.from(event.data))); 49 | socket.addEventListener("close", () => localSocket!.end()); 50 | localSocket.on("data", (data) => socket.send(data)); 51 | localSocket.on("error", (err) => socket.close(TunnelCloseCode.Error, err.toString())); 52 | localSocket.on("close", () => socket.close(TunnelCloseCode.Normal)); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/tunnel/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/vscode/.gitignore: -------------------------------------------------------------------------------- 1 | bin -------------------------------------------------------------------------------- /packages/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/vscode", 3 | "description": "VS Code implementation of the browser-based IDE client.", 4 | "main": "src/index.ts", 5 | "scripts": { 6 | "build:bootstrap-fork": "../../node_modules/.bin/cross-env UV_THREADPOOL_SIZE=100 node --max-old-space-size=32384 ../../node_modules/webpack/bin/webpack.js --config ./webpack.bootstrap.config.js" 7 | }, 8 | "dependencies": { 9 | "iconv-lite": "^0.4.24", 10 | "onigasm": "^2.2.1", 11 | "spdlog": "^0.7.2", 12 | "string-replace-loader": "^2.1.1", 13 | "tar-stream": "^2.0.1" 14 | }, 15 | "devDependencies": { 16 | "@types/tar-stream": "^1.6.0", 17 | "vscode-textmate": "^4.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/vscode/src/client.ts: -------------------------------------------------------------------------------- 1 | import { IdeClient } from "@coder/ide"; 2 | import { client as ideClientInstance } from "@coder/ide/src/fill/client"; 3 | import Severity from "vs/base/common/severity"; 4 | import { INotificationService } from "vs/platform/notification/common/notification"; 5 | import { IStatusbarService, StatusbarAlignment } from "vs/platform/statusbar/common/statusbar"; 6 | import * as paths from "./fill/paths"; 7 | import "./vscode.scss"; 8 | import { MenuId, MenuRegistry } from "vs/platform/actions/common/actions"; 9 | import { CommandsRegistry } from "vs/platform/commands/common/commands"; 10 | // NOTE: shouldn't import anything from VS Code here or anything that will 11 | // depend on a synchronous fill like `os`. 12 | 13 | class VSClient extends IdeClient { 14 | protected initialize(): Promise { 15 | return this.task("Start workbench", 1000, async (data, sharedData) => { 16 | paths._paths.initialize(data, sharedData); 17 | process.env.SHELL = data.shell; 18 | // At this point everything should be filled, including `os`. `os` also 19 | // relies on `initData` but it listens first so it initialize before this 20 | // callback, meaning we are safe to include everything from VS Code now. 21 | const { workbench } = require("./workbench") as typeof import("./workbench"); 22 | await workbench.initialize(); 23 | 24 | // tslint:disable-next-line:no-any 25 | const getService = (id: any): T => workbench.serviceCollection.get(id) as T; 26 | window.ide = { 27 | client: ideClientInstance, 28 | workbench: { 29 | commandRegistry: CommandsRegistry, 30 | // tslint:disable-next-line:no-any 31 | menuRegistry: MenuRegistry as any, 32 | // tslint:disable-next-line:no-any 33 | statusbarService: getService(IStatusbarService) as any, 34 | notificationService: getService(INotificationService), 35 | }, 36 | 37 | // @ts-ignore 38 | // tslint:disable-next-line:no-any 39 | MenuId: MenuId as any, 40 | // tslint:disable-next-line:no-any 41 | Severity: Severity as any, 42 | // @ts-ignore 43 | // tslint:disable-next-line:no-any 44 | StatusbarAlignment: StatusbarAlignment as any, 45 | }; 46 | 47 | const event = new CustomEvent("ide-ready"); 48 | // tslint:disable-next-line:no-any 49 | (event).ide = window.ide; 50 | window.dispatchEvent(event); 51 | }, this.initData, this.sharedProcessData); 52 | } 53 | } 54 | 55 | export const client = new VSClient(); 56 | -------------------------------------------------------------------------------- /packages/vscode/src/dialog.scss: -------------------------------------------------------------------------------- 1 | .dialog { 2 | --primary: #2A2E37; 3 | --border: black; 4 | --faded: #a0a1a5; 5 | --header-background: #161616; 6 | --header-foreground: white; 7 | --list-active-selection-background: rgb(0, 120, 160); 8 | --list-active-selection-foreground: white; 9 | --list-hover-background: rgb(36, 39, 46); 10 | font-family: inherit; 11 | box-shadow: 0 18px 80px 10px rgba(0, 0, 0, 0.1); 12 | background-color: var(--primary); 13 | display: flex; 14 | flex-direction: column; 15 | user-select: none; 16 | overflow: hidden; 17 | border-radius: 5px; 18 | 19 | .monaco-tl-twistie { 20 | display: none; 21 | } 22 | 23 | .title { 24 | background-color: var(--header-background); 25 | color: var(--header-foreground); 26 | padding: 1px; 27 | font-size: 11px; 28 | font-weight: normal; 29 | text-transform: uppercase; 30 | white-space: nowrap; 31 | padding-left: 10px; 32 | } 33 | 34 | .nav { 35 | display: flex; 36 | flex-direction: row; 37 | padding: 4px; 38 | padding-top: 8px; 39 | padding-bottom: 8px; 40 | border-bottom: 1px solid var(--border); 41 | min-height: 32px; 42 | } 43 | 44 | .path { 45 | display: flex; 46 | flex-direction: row; 47 | 48 | .path-part { 49 | padding: 5px; 50 | border-radius: 3px; 51 | font-size: 1.2em; 52 | cursor: pointer; 53 | 54 | &:not(:first-child) { 55 | margin-left: 5px; 56 | } 57 | 58 | &.active { 59 | font-weight: bold; 60 | color: var(--list-active-selection-foreground); 61 | } 62 | } 63 | } 64 | 65 | .dialog-grid { 66 | display: grid; 67 | grid-template-columns: 2fr 0.2fr 0.8fr; 68 | } 69 | 70 | .headings { 71 | padding: 8px; 72 | font-size: 12px; 73 | } 74 | 75 | .file-area { 76 | flex: 1; 77 | display: flex; 78 | flex-direction: column; 79 | overflow: hidden; 80 | 81 | .dialog-entry { 82 | cursor: pointer; 83 | font-size: 1.2em; 84 | padding: 0px; 85 | padding-left: 8px; 86 | padding-right: 8px; 87 | 88 | .dialog-entry-info { 89 | display: flex; 90 | flex-direction: row; 91 | } 92 | 93 | .dialog-entry-icon { 94 | width: 16px; 95 | height: 19px; 96 | margin-right: 5px; 97 | } 98 | 99 | &:hover { 100 | background-color: var(--list-hover-background); 101 | } 102 | 103 | &.active { 104 | background-color: var(--list-active-selection-background); 105 | color: var(--list-active-selection-foreground); 106 | } 107 | } 108 | } 109 | 110 | .buttons { 111 | display: flex; 112 | flex-direction: row; 113 | padding: 10px; 114 | position: relative; 115 | background: var(--primary); 116 | border-top: 1px solid var(--border); 117 | 118 | button:first-child { 119 | margin-left: auto; 120 | margin-right: 10px; 121 | } 122 | 123 | button { 124 | background: transparent; 125 | outline: none; 126 | border: 0; 127 | color: var(--faded); 128 | padding: 10px; 129 | padding-left: 18px; 130 | padding-right: 18px; 131 | transition: 150ms background ease, 150ms color ease; 132 | cursor: pointer; 133 | border-radius: 5px; 134 | 135 | &:hover { 136 | background: var(--titlebar); 137 | color: white; 138 | } 139 | } 140 | } 141 | } 142 | 143 | .monaco-shell .monaco-tree.focused.no-focused-item:focus:before, .monaco-shell .monaco-list:not(.element-focused):focus:before { 144 | display: none; 145 | } 146 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/amd.ts: -------------------------------------------------------------------------------- 1 | import { URI } from "vs/base/common/uri"; 2 | 3 | export const getPathFromAmdModule = (_: typeof require, relativePath: string): string => { 4 | if (process.mainModule && process.mainModule.filename) { 5 | const index = process.mainModule.filename.lastIndexOf("/"); 6 | 7 | return process.mainModule.filename.slice(0, index); 8 | } 9 | 10 | return relativePath ? URI.file(relativePath).fsPath : ""; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/codeEditor.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import * as editor from "vs/editor/browser/services/codeEditorServiceImpl"; 3 | import { IDecorationRenderOptions } from "vs/editor/common/editorCommon"; 4 | 5 | /** 6 | * This converts icon paths for decorations to the correct URL. 7 | */ 8 | abstract class CodeEditorServiceImpl extends editor.CodeEditorServiceImpl { 9 | public registerDecorationType(key: string, options: IDecorationRenderOptions, parentTypeKey?: string): void { 10 | super.registerDecorationType(key, options ? { 11 | ...options, 12 | gutterIconPath: options.gutterIconPath && options.gutterIconPath.scheme === "file" ? { 13 | ...options.gutterIconPath, 14 | scheme: location.protocol.replace(":", ""), 15 | authority: location.host, 16 | path: join("/resource", options.gutterIconPath.path), 17 | } :options.gutterIconPath, 18 | } : {}, parentTypeKey); 19 | } 20 | } 21 | 22 | const target = editor as typeof editor; 23 | target.CodeEditorServiceImpl = CodeEditorServiceImpl; 24 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/css.js: -------------------------------------------------------------------------------- 1 | module.exports = function(source) { 2 | if (this.resourcePath.endsWith(".ts")) { 3 | this.resourcePath = this.resourcePath.replace(".ts", ".css"); 4 | } 5 | return `module.exports = require("${this.resourcePath.replace(/\\/g, "\\\\")}");`; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/dom.ts: -------------------------------------------------------------------------------- 1 | import * as dom from "vs/base/browser/dom"; 2 | import { IDisposable } from "vs/base/common/lifecycle"; 3 | 4 | // Firefox has no implementation of toElement. 5 | if (!("toElement" in MouseEvent.prototype)) { 6 | Object.defineProperty(MouseEvent.prototype, "toElement", { 7 | get: function (): EventTarget | null { 8 | // @ts-ignore 9 | const event = this as MouseEvent; 10 | switch (event.type) { 11 | case "mouseup": 12 | case "focusin": 13 | case "mousenter": 14 | case "mouseover": 15 | case "dragenter": 16 | return event.target; 17 | default: 18 | return event.relatedTarget; 19 | } 20 | }, 21 | }); 22 | } 23 | 24 | const _addDisposableListener = dom.addDisposableListener; 25 | // tslint:disable-next-line no-any 26 | const addDisposableListener = (node: Element | Window | Document, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable => { 27 | return _addDisposableListener(node, type === "mousewheel" ? "wheel" : type, handler, useCapture); 28 | }; 29 | 30 | const target = dom as typeof dom; 31 | target.addDisposableListener = addDisposableListener; 32 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/environmentService.ts: -------------------------------------------------------------------------------- 1 | import * as paths from "./paths"; 2 | import * as environment from "vs/platform/environment/node/environmentService"; 3 | 4 | export class EnvironmentService extends environment.EnvironmentService { 5 | public get sharedIPCHandle(): string { 6 | return paths.getSocketPath() || super.sharedIPCHandle; 7 | } 8 | } 9 | 10 | const target = environment as typeof environment; 11 | target.EnvironmentService = EnvironmentService; 12 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/graceful-fs.ts: -------------------------------------------------------------------------------- 1 | export const gracefulify = (): void => undefined; 2 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/iconv-lite.ts: -------------------------------------------------------------------------------- 1 | import * as iconv from "../../node_modules/iconv-lite"; 2 | import { Transform, TransformCallback } from "stream"; 3 | 4 | class IconvLiteDecoderStream extends Transform { 5 | // tslint:disable-next-line no-any 6 | private conv: any; 7 | private encoding: string; 8 | 9 | public constructor(options: { encoding: string }) { 10 | super(options); 11 | // tslint:disable-next-line no-any 12 | this.conv = (iconv as any).getDecoder(options.encoding, undefined); 13 | this.encoding = options.encoding; 14 | } 15 | 16 | // tslint:disable-next-line no-any 17 | public _transform(chunk: any, _encoding: string, done: TransformCallback): void { 18 | if (!Buffer.isBuffer(chunk)) { 19 | return done(new Error("Iconv decoding stream needs buffers as its input.")); 20 | } 21 | try { 22 | const res = this.conv.write(chunk); 23 | if (res && res.length) { 24 | this.push(res, this.encoding); 25 | } 26 | done(); 27 | } catch (error) { 28 | done(error); 29 | } 30 | } 31 | 32 | public _flush(done: TransformCallback): void { 33 | try { 34 | const res = this.conv.end(); 35 | if (res && res.length) { 36 | this.push(res, this.encoding); 37 | } 38 | done(); 39 | } catch (error) { 40 | done(error); 41 | } 42 | } 43 | 44 | // tslint:disable-next-line no-any 45 | public collect(cb: (error: Error | null, response?: any) => void): this { 46 | let res = ""; 47 | this.on("error", cb); 48 | this.on("data", (chunk) => res += chunk); 49 | this.on("end", () => { 50 | cb(null, res); 51 | }); 52 | 53 | return this; 54 | } 55 | } 56 | 57 | const decodeStream = (encoding: string): NodeJS.ReadWriteStream => { 58 | return new IconvLiteDecoderStream({ encoding }); 59 | }; 60 | 61 | const target = iconv as typeof iconv; 62 | target.decodeStream = decodeStream; 63 | 64 | export = target; 65 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/labels.ts: -------------------------------------------------------------------------------- 1 | import * as labels from "vs/base/common/labels"; 2 | 3 | // Disable all mnemonics for now until we implement it. 4 | const target = labels as typeof labels; 5 | target.mnemonicMenuLabel = (label: string, forceDisable?: boolean): string => { 6 | return label.replace(/\(&&\w\)|&&/g, ""); 7 | }; 8 | target.mnemonicButtonLabel = (label: string): string => { 9 | return label.replace(/\(&&\w\)|&&/g, ""); 10 | }; 11 | target.unmnemonicLabel = (label: string): string => { return label; }; 12 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/menuRegistry.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "@coder/logger"; 2 | import { IDisposable } from "vs/base/common/lifecycle"; 3 | import * as actions from "vs/platform/actions/common/actions"; 4 | import { ToggleDevToolsAction } from "vs/workbench/electron-browser/actions/developerActions"; 5 | 6 | // Intercept appending menu items so we can skip items that won't work. 7 | const originalAppend = actions.MenuRegistry.appendMenuItem.bind(actions.MenuRegistry); 8 | actions.MenuRegistry.appendMenuItem = (id: actions.MenuId, item: actions.IMenuItem | actions.ISubmenuItem): IDisposable => { 9 | if (actions.isIMenuItem(item)) { 10 | switch (item.command.id) { 11 | case ToggleDevToolsAction.ID: // There appears to be no way to toggle this programmatically. 12 | logger.debug(`Skipping unsupported menu item ${item.command.id}`); 13 | 14 | return { 15 | dispose: (): void => undefined, 16 | }; 17 | } 18 | } 19 | 20 | return originalAppend(id, item); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/mouseEvent.ts: -------------------------------------------------------------------------------- 1 | import * as mouse from "vs/base/browser/mouseEvent"; 2 | 3 | /** 4 | * Fix the wheel event for Firefox. 5 | */ 6 | class StandardWheelEvent extends mouse.StandardWheelEvent { 7 | public constructor(event: mouse.IMouseWheelEvent | null) { 8 | super( 9 | event, 10 | (-(event as any as MouseWheelEvent).deltaX || 0) / 3, // tslint:disable-line no-any 11 | (-(event as any as MouseWheelEvent).deltaY || 0) / 3, // tslint:disable-line no-any 12 | ); 13 | } 14 | } 15 | 16 | const target = mouse as typeof mouse; 17 | target.StandardWheelEvent = StandardWheelEvent; 18 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/native-keymap.ts: -------------------------------------------------------------------------------- 1 | class NativeKeymap { 2 | public getCurrentKeyboardLayout(): null { 3 | return null; 4 | } 5 | 6 | public getKeyMap(): undefined[] { 7 | return []; 8 | } 9 | } 10 | 11 | export = new NativeKeymap(); 12 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/native-watchdog.ts: -------------------------------------------------------------------------------- 1 | class Watchdog { 2 | public start(): void { 3 | // No action required. 4 | } 5 | } 6 | 7 | export = new Watchdog(); 8 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/node-pty.ts: -------------------------------------------------------------------------------- 1 | import { client } from "@coder/ide/src/fill/client"; 2 | import { EventEmitter } from "events"; 3 | import * as nodePty from "node-pty"; 4 | import { ActiveEvalHelper } from "@coder/protocol"; 5 | import { logger } from "@coder/logger"; 6 | 7 | /** 8 | * Implementation of nodePty for the browser. 9 | */ 10 | class Pty implements nodePty.IPty { 11 | private readonly emitter = new EventEmitter(); 12 | private readonly ae: ActiveEvalHelper; 13 | private _pid = -1; 14 | private _process = ""; 15 | 16 | public constructor(file: string, args: string[] | string, options: nodePty.IPtyForkOptions) { 17 | this.ae = client.run((ae, file, args, options) => { 18 | ae.preserveEnv(options); 19 | 20 | const ptyProc = ae.modules.pty.spawn(file, args, options); 21 | 22 | let process = ptyProc.process; 23 | ae.emit("process", process); 24 | ae.emit("pid", ptyProc.pid); 25 | 26 | const timer = setInterval(() => { 27 | if (ptyProc.process !== process) { 28 | process = ptyProc.process; 29 | ae.emit("process", process); 30 | } 31 | }, 200); 32 | 33 | ptyProc.on("exit", (code, signal) => { 34 | clearTimeout(timer); 35 | ae.emit("exit", code, signal); 36 | }); 37 | 38 | ptyProc.on("data", (data) => ae.emit("data", data)); 39 | 40 | ae.on("resize", (cols: number, rows: number) => ptyProc.resize(cols, rows)); 41 | ae.on("write", (data: string) => ptyProc.write(data)); 42 | ae.on("kill", (signal: string) => ptyProc.kill(signal)); 43 | 44 | return { 45 | onDidDispose: (cb): void => ptyProc.on("exit", cb), 46 | dispose: (): void => { 47 | ptyProc.kill(); 48 | setTimeout(() => ptyProc.kill("SIGKILL"), 5000); // Double tap. 49 | }, 50 | }; 51 | }, file, args, options); 52 | 53 | this.ae.on("error", (error) => logger.error(error.message)); 54 | 55 | this.ae.on("pid", (pid) => this._pid = pid); 56 | this.ae.on("process", (process) => this._process = process); 57 | 58 | this.ae.on("exit", (code, signal) => this.emitter.emit("exit", code, signal)); 59 | this.ae.on("data", (data) => this.emitter.emit("data", data)); 60 | } 61 | 62 | public get pid(): number { 63 | return this._pid; 64 | } 65 | 66 | public get process(): string { 67 | return this._process; 68 | } 69 | 70 | // tslint:disable-next-line no-any 71 | public on(event: string, listener: (...args: any[]) => void): void { 72 | this.emitter.on(event, listener); 73 | } 74 | 75 | public resize(columns: number, rows: number): void { 76 | this.ae.emit("resize", columns, rows); 77 | } 78 | 79 | public write(data: string): void { 80 | this.ae.emit("write", data); 81 | } 82 | 83 | public kill(signal?: string): void { 84 | this.ae.emit("kill", signal); 85 | } 86 | } 87 | 88 | const ptyType: typeof nodePty = { 89 | spawn: (file: string, args: string[] | string, options: nodePty.IPtyForkOptions): nodePty.IPty => { 90 | return new Pty(file, args, options); 91 | }, 92 | }; 93 | 94 | module.exports = ptyType; 95 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/package.ts: -------------------------------------------------------------------------------- 1 | import * as packageJson from "../../../../lib/vscode/package.json"; 2 | export default { name: "vscode", version: packageJson.version }; 3 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/paste.ts: -------------------------------------------------------------------------------- 1 | import * as nls from "vs/nls"; 2 | import { Action } from "vs/base/common/actions"; 3 | import { TERMINAL_COMMAND_ID } from "vs/workbench/contrib/terminal/common/terminalCommands"; 4 | import { ITerminalService } from "vs/workbench/contrib/terminal/common/terminal"; 5 | import * as actions from "vs/workbench/contrib/terminal/electron-browser/terminalActions"; 6 | import * as instance from "vs/workbench/contrib/terminal/electron-browser/terminalInstance"; 7 | import { client } from "../client"; 8 | 9 | const getLabel = (key: string, enabled: boolean): string => { 10 | return enabled 11 | ? nls.localize(key, "Paste") 12 | : nls.localize(`${key}WithKeybind`, "Paste (must use keybind)"); 13 | }; 14 | 15 | export class PasteAction extends Action { 16 | private static readonly KEY = "paste"; 17 | 18 | public constructor() { 19 | super( 20 | "editor.action.clipboardPasteAction", 21 | getLabel(PasteAction.KEY, client.clipboard.isEnabled), 22 | undefined, 23 | client.clipboard.isEnabled, 24 | async (): Promise => client.clipboard.paste(), 25 | ); 26 | 27 | client.clipboard.onPermissionChange((enabled) => { 28 | this.label = getLabel(PasteAction.KEY, enabled); 29 | this.enabled = enabled; 30 | }); 31 | } 32 | } 33 | 34 | class TerminalPasteAction extends Action { 35 | private static readonly KEY = "workbench.action.terminal.paste"; 36 | 37 | public static readonly ID = TERMINAL_COMMAND_ID.PASTE; 38 | public static readonly LABEL = nls.localize("workbench.action.terminal.paste", "Paste into Active Terminal"); 39 | public static readonly SHORT_LABEL = getLabel(TerminalPasteAction.KEY, client.clipboard.isEnabled); 40 | 41 | public constructor( 42 | id: string, label: string, 43 | @ITerminalService private terminalService: ITerminalService, 44 | ) { 45 | super(id, label); 46 | client.clipboard.onPermissionChange((enabled) => { 47 | this._setLabel(getLabel(TerminalPasteAction.KEY, enabled)); 48 | }); 49 | this._setLabel(getLabel(TerminalPasteAction.KEY, client.clipboard.isEnabled)); 50 | } 51 | 52 | public run(): Promise { 53 | const instance = this.terminalService.getActiveOrCreateInstance(); 54 | if (instance) { 55 | // tslint:disable-next-line no-any it will return a promise (see below) 56 | return (instance as any).paste(); 57 | } 58 | 59 | return Promise.resolve(); 60 | } 61 | } 62 | 63 | class TerminalInstance extends instance.TerminalInstance { 64 | public async paste(): Promise { 65 | this.focus(); 66 | if (client.clipboard.isEnabled) { 67 | const text = await client.clipboard.readText(); 68 | this.sendText(text, false); 69 | } else { 70 | document.execCommand("paste"); 71 | } 72 | } 73 | } 74 | 75 | const actionsTarget = actions as typeof actions; 76 | // @ts-ignore TODO: don't ignore it. 77 | actionsTarget.TerminalPasteAction = TerminalPasteAction; 78 | 79 | const instanceTarget = instance as typeof instance; 80 | instanceTarget.TerminalInstance = TerminalInstance; 81 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/paths.ts: -------------------------------------------------------------------------------- 1 | import { InitData, SharedProcessData } from "@coder/protocol"; 2 | 3 | class Paths { 4 | private _appData: string | undefined; 5 | private _defaultUserData: string | undefined; 6 | private _socketPath: string | undefined; 7 | private _builtInExtensionsDirectory: string | undefined; 8 | private _workingDirectory: string | undefined; 9 | 10 | public get appData(): string { 11 | if (typeof this._appData === "undefined") { 12 | throw new Error("trying to access appData before it has been set"); 13 | } 14 | 15 | return this._appData; 16 | } 17 | 18 | public get defaultUserData(): string { 19 | if (typeof this._defaultUserData === "undefined") { 20 | throw new Error("trying to access defaultUserData before it has been set"); 21 | } 22 | 23 | return this._defaultUserData; 24 | } 25 | 26 | public get socketPath(): string { 27 | if (typeof this._socketPath === "undefined") { 28 | throw new Error("trying to access socketPath before it has been set"); 29 | } 30 | 31 | return this._socketPath; 32 | } 33 | 34 | public get builtInExtensionsDirectory(): string { 35 | if (!this._builtInExtensionsDirectory) { 36 | throw new Error("trying to access builtin extensions directory before it has been set"); 37 | } 38 | 39 | return this._builtInExtensionsDirectory; 40 | } 41 | 42 | public get workingDirectory(): string { 43 | if (!this._workingDirectory) { 44 | throw new Error("trying to access working directory before it has been set"); 45 | } 46 | 47 | return this._workingDirectory; 48 | } 49 | 50 | public initialize(data: InitData, sharedData: SharedProcessData): void { 51 | process.env.VSCODE_LOGS = sharedData.logPath; 52 | this._appData = data.dataDirectory; 53 | this._defaultUserData = data.dataDirectory; 54 | this._socketPath = sharedData.socketPath; 55 | this._builtInExtensionsDirectory = data.builtInExtensionsDirectory; 56 | this._workingDirectory = data.workingDirectory; 57 | } 58 | } 59 | 60 | export const _paths = new Paths(); 61 | export const getAppDataPath = (): string => _paths.appData; 62 | export const getDefaultUserDataPath = (): string => _paths.defaultUserData; 63 | export const getWorkingDirectory = (): string => _paths.workingDirectory; 64 | export const getBuiltInExtensionsDirectory = (): string => _paths.builtInExtensionsDirectory; 65 | export const getSocketPath = (): string => _paths.socketPath; 66 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/platform.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os"; 2 | import * as platform from "vs/base/common/platform"; 3 | import * as browser from "vs/base/browser/browser"; 4 | 5 | // tslint:disable no-any to override const 6 | 7 | // Use en instead of en-US since that's vscode default and it uses 8 | // that to determine whether to output aliases which will be redundant. 9 | if (platform.locale === "en-US") { 10 | (platform as any).locale = "en"; 11 | } 12 | if (platform.language === "en-US") { 13 | (platform as any).language = "en"; 14 | } 15 | 16 | // Use the server's platform instead of the client's. For example, this affects 17 | // how VS Code handles paths (and more) because different platforms give 18 | // different results. We'll have to counter for things that shouldn't change, 19 | // like keybindings. 20 | (platform as any).isLinux = os.platform() === "linux"; 21 | (platform as any).isWindows = os.platform() === "win32"; 22 | (platform as any).isMacintosh = os.platform() === "darwin"; 23 | (platform as any).platform = os.platform() === "linux" ? platform.Platform.Linux : os.platform() === "win32" ? platform.Platform.Windows : platform.Platform.Mac; 24 | 25 | // This is used for keybindings, and in one place to choose between \r\n and \n 26 | // (which we change to use platform.isWindows instead). 27 | (platform as any).OS = (browser.isMacintosh ? platform.OperatingSystem.Macintosh : (browser.isWindows ? platform.OperatingSystem.Windows : platform.OperatingSystem.Linux)); 28 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/product.ts: -------------------------------------------------------------------------------- 1 | import { IProductConfiguration } from "vs/platform/product/node/product"; 2 | 3 | const product = { 4 | nameShort: "code-server", 5 | nameLong: "code-server", 6 | dataFolderName: ".code-server", 7 | extensionsGallery: { 8 | serviceUrl: global && global.process && global.process.env.SERVICE_URL 9 | || process.env.SERVICE_URL 10 | || "https://v1.extapi.coder.com", 11 | }, 12 | extensionExecutionEnvironments: { 13 | "wayou.vscode-todo-highlight": "worker", 14 | "vscodevim.vim": "worker", 15 | "coenraads.bracket-pair-colorizer": "worker", 16 | }, 17 | fetchUrl: "", 18 | } as IProductConfiguration; 19 | 20 | if (process.env['VSCODE_DEV']) { 21 | product.nameShort += ' Dev'; 22 | product.nameLong += ' Dev'; 23 | product.dataFolderName += '-dev'; 24 | } 25 | 26 | export default product; 27 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/ripgrep.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | // tslint:disable-next-line:no-any 4 | module.exports.rgPath = (global).RIPGREP_LOCATION || path.join(__dirname, "../bin/rg"); 5 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/spdlog.ts: -------------------------------------------------------------------------------- 1 | import { RotatingLogger as NodeRotatingLogger } from "spdlog"; 2 | import { logger } from "@coder/logger"; 3 | import { client } from "@coder/ide/src/fill/client"; 4 | 5 | const ae = client.run((ae) => { 6 | const loggers = new Map(); 7 | 8 | ae.on("new", (id: number, name: string, filePath: string, fileSize: number, fileCount: number) => { 9 | const logger = new ae.modules.spdlog.RotatingLogger(name, filePath, fileSize, fileCount); 10 | loggers.set(id, logger); 11 | }); 12 | 13 | ae.on("clearFormatters", (id: number) => loggers.get(id)!.clearFormatters()); 14 | ae.on("critical", (id: number, message: string) => loggers.get(id)!.critical(message)); 15 | ae.on("debug", (id: number, message: string) => loggers.get(id)!.debug(message)); 16 | ae.on("drop", (id: number) => loggers.get(id)!.drop()); 17 | ae.on("errorLog", (id: number, message: string) => loggers.get(id)!.error(message)); 18 | ae.on("flush", (id: number) => loggers.get(id)!.flush()); 19 | ae.on("info", (id: number, message: string) => loggers.get(id)!.info(message)); 20 | ae.on("setAsyncMode", (bufferSize: number, flushInterval: number) => ae.modules.spdlog.setAsyncMode(bufferSize, flushInterval)); 21 | ae.on("setLevel", (id: number, level: number) => loggers.get(id)!.setLevel(level)); 22 | ae.on("trace", (id: number, message: string) => loggers.get(id)!.trace(message)); 23 | ae.on("warn", (id: number, message: string) => loggers.get(id)!.warn(message)); 24 | 25 | const disposeCallbacks = void>>[]; 26 | 27 | return { 28 | onDidDispose: (cb): number => disposeCallbacks.push(cb), 29 | dispose: (): void => { 30 | loggers.forEach((logger) => logger.flush()); 31 | loggers.clear(); 32 | disposeCallbacks.forEach((cb) => cb()); 33 | }, 34 | }; 35 | }); 36 | 37 | const spdLogger = logger.named("spdlog"); 38 | ae.on("close", () => spdLogger.error("session closed prematurely")); 39 | ae.on("error", (error: Error) => spdLogger.error(error.message)); 40 | 41 | let id = 0; 42 | export class RotatingLogger implements NodeRotatingLogger { 43 | private readonly id = id++; 44 | 45 | public constructor(name: string, filePath: string, fileSize: number, fileCount: number) { 46 | ae.emit("new", this.id, name, filePath, fileSize, fileCount); 47 | } 48 | 49 | public trace(message: string): void { ae.emit("trace", this.id, message); } 50 | public debug(message: string): void { ae.emit("debug", this.id, message); } 51 | public info(message: string): void { ae.emit("info", this.id, message); } 52 | public warn(message: string): void { ae.emit("warn", this.id, message); } 53 | public error(message: string): void { ae.emit("errorLog", this.id, message); } 54 | public critical(message: string): void { ae.emit("critical", this.id, message); } 55 | public setLevel(level: number): void { ae.emit("setLevel", this.id, level); } 56 | public clearFormatters(): void { ae.emit("clearFormatters", this.id); } 57 | public flush(): void { ae.emit("flush", this.id); } 58 | public drop(): void { ae.emit("drop", this.id); } 59 | } 60 | 61 | export const setAsyncMode = (bufferSize: number, flushInterval: number): void => { 62 | ae.emit("setAsyncMode", bufferSize, flushInterval); 63 | }; 64 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/stdioElectron.ts: -------------------------------------------------------------------------------- 1 | import { StdioIpcHandler } from "@coder/server/src/ipc"; 2 | import { IpcRenderer } from "electron"; 3 | 4 | // TODO: Commenting out for now since the electron fill includes the client code 5 | // and tries to connect to the web socket. The fill also likely wouldn't work 6 | // since it assumes it is running on the client. Could we proxy all methods to 7 | // the client? It might not matter since we intercept everything before sending 8 | // to the shared process. 9 | // export * from "@coder/ide/src/fill/electron"; 10 | 11 | class StdioIpcRenderer extends StdioIpcHandler implements IpcRenderer { 12 | // tslint:disable-next-line no-any 13 | public sendTo(_windowId: number, _channel: string, ..._args: any[]): void { 14 | throw new Error("Method not implemented."); 15 | } 16 | 17 | // tslint:disable-next-line no-any 18 | public sendToHost(_channel: string, ..._args: any[]): void { 19 | throw new Error("Method not implemented."); 20 | } 21 | 22 | public eventNames(): string[] { 23 | return super.eventNames() as string[]; 24 | } 25 | } 26 | 27 | export const ipcRenderer = new StdioIpcRenderer(); 28 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/storageDatabase.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile, mkdir } from "fs"; 2 | import * as path from "path"; 3 | import { promisify } from "util"; 4 | import { IDisposable } from "@coder/disposable"; 5 | import { logger, field } from "@coder/logger"; 6 | import { Event } from "vs/base/common/event"; 7 | import * as workspaceStorage from "vs/base/node/storage"; 8 | import * as globalStorage from "vs/platform/storage/node/storageIpc"; 9 | import { IStorageService, WillSaveStateReason } from "vs/platform/storage/common/storage"; 10 | import * as paths from "./paths"; 11 | import { workbench } from "../workbench"; 12 | 13 | class StorageDatabase implements workspaceStorage.IStorageDatabase { 14 | public readonly onDidChangeItemsExternal = Event.None; 15 | private readonly items = new Map(); 16 | private fetched: boolean = false; 17 | private readonly path: string; 18 | 19 | public constructor(path: string) { 20 | this.path = path.replace(/\.vscdb$/, ".json"); 21 | logger.debug("Setting up storage", field("path", this.path)); 22 | window.addEventListener("unload", () => { 23 | if (!navigator.sendBeacon) { 24 | throw new Error("cannot save state"); 25 | } 26 | 27 | this.triggerFlush(WillSaveStateReason.SHUTDOWN); 28 | navigator.sendBeacon(`/resource${this.path}`, this.content); 29 | }); 30 | } 31 | 32 | public async getItems(): Promise> { 33 | if (this.fetched) { 34 | return this.items; 35 | } 36 | try { 37 | const contents = await promisify(readFile)(this.path, "utf8"); 38 | const json = JSON.parse(contents); 39 | Object.keys(json).forEach((key) => { 40 | this.items.set(key, json[key]); 41 | }); 42 | } catch (error) { 43 | if (error.code !== "ENOENT") { 44 | throw error; 45 | } 46 | } 47 | 48 | this.fetched = true; 49 | 50 | return this.items; 51 | } 52 | 53 | public updateItems(request: workspaceStorage.IUpdateRequest): Promise { 54 | if (request.insert) { 55 | request.insert.forEach((value, key) => { 56 | if (key === "colorThemeData") { 57 | localStorage.setItem("colorThemeData", value); 58 | } 59 | 60 | this.items.set(key, value); 61 | }); 62 | } 63 | 64 | if (request.delete) { 65 | request.delete.forEach(key => this.items.delete(key)); 66 | } 67 | 68 | return this.save(); 69 | } 70 | 71 | public close(): Promise { 72 | return Promise.resolve(); 73 | } 74 | 75 | public checkIntegrity(): Promise { 76 | return Promise.resolve("ok"); 77 | } 78 | 79 | private async save(): Promise { 80 | try { 81 | await promisify(mkdir)(path.dirname(this.path)); 82 | } catch (ex) {} 83 | 84 | return promisify(writeFile)(this.path, this.content); 85 | } 86 | 87 | private triggerFlush(reason: WillSaveStateReason = WillSaveStateReason.NONE): boolean { 88 | // tslint:disable-next-line:no-any 89 | const storageService = workbench.serviceCollection.get(IStorageService) as any; 90 | if (reason === WillSaveStateReason.SHUTDOWN && storageService.close) { 91 | storageService.close(); 92 | 93 | return true; 94 | } 95 | if (storageService._onWillSaveState) { 96 | storageService._onWillSaveState.fire({ reason }); 97 | 98 | return true; 99 | } 100 | 101 | return false; 102 | } 103 | 104 | private get content(): string { 105 | const json: { [key: string]: string } = {}; 106 | this.items.forEach((value, key) => { 107 | json[key] = value; 108 | }); 109 | 110 | return JSON.stringify(json); 111 | } 112 | } 113 | 114 | class GlobalStorageDatabase extends StorageDatabase implements IDisposable { 115 | public constructor() { 116 | super(path.join(paths.getAppDataPath(), "globalStorage", "state.vscdb")); 117 | } 118 | 119 | public dispose(): void { 120 | // Nothing to do. 121 | } 122 | } 123 | 124 | const workspaceTarget = workspaceStorage as typeof workspaceStorage; 125 | // @ts-ignore TODO: don't ignore it. 126 | workspaceTarget.SQLiteStorageDatabase = StorageDatabase; 127 | 128 | const globalTarget = globalStorage as typeof globalStorage; 129 | // @ts-ignore TODO: don't ignore it. 130 | globalTarget.GlobalStorageDatabaseChannelClient = GlobalStorageDatabase; 131 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/vscodeTextmate.ts: -------------------------------------------------------------------------------- 1 | import * as vscodeTextmate from "../../../../lib/vscode/node_modules/vscode-textmate"; 2 | 3 | const target = vscodeTextmate as typeof vscodeTextmate; 4 | 5 | target.Registry = class Registry extends vscodeTextmate.Registry { 6 | public constructor(opts: vscodeTextmate.RegistryOptions) { 7 | super({ 8 | ...opts, 9 | getOnigLib: (): Promise => { 10 | return new Promise((res, rej) => { 11 | const onigasm = require("onigasm"); 12 | const wasmUrl = require("!!file-loader!onigasm/lib/onigasm.wasm"); 13 | 14 | return fetch(wasmUrl).then(resp => resp.arrayBuffer()).then(buffer => { 15 | return onigasm.loadWASM(buffer); 16 | }).then(() => { 17 | res({ 18 | createOnigScanner: function (patterns) { return new onigasm.OnigScanner(patterns); }, 19 | createOnigString: function (s) { return new onigasm.OnigString(s); }, 20 | }); 21 | }).catch(reason => rej(reason)); 22 | }); 23 | }, 24 | }); 25 | } 26 | }; 27 | 28 | enum StandardTokenType { 29 | Other = 0, 30 | Comment = 1, 31 | String = 2, 32 | RegEx = 4, 33 | } 34 | 35 | // tslint:disable-next-line no-any to override const 36 | (target as any).StandardTokenType = StandardTokenType; 37 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/workbenchRegistry.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "@coder/logger"; 2 | import { IDisposable } from "vs/base/common/lifecycle"; 3 | import { Registry } from "vs/platform/registry/common/platform"; 4 | import { IWorkbenchActionRegistry, Extensions } from "vs/workbench/common/actions"; 5 | import { SyncActionDescriptor } from "vs/platform/actions/common/actions"; 6 | import { ContextKeyExpr } from "vs/platform/contextkey/common/contextkey"; 7 | import { ToggleDevToolsAction } from "vs/workbench/electron-browser/actions/developerActions"; 8 | import { TerminalPasteAction } from "vs/workbench/contrib/terminal/electron-browser/terminalActions"; 9 | import { KEYBINDING_CONTEXT_TERMINAL_FOCUS } from "vs/workbench/contrib/terminal/common/terminal"; 10 | import { KeyCode, KeyMod } from "vs/base/common/keyCodes"; 11 | import { workbench } from "../workbench"; 12 | 13 | // Intercept adding workbench actions so we can skip actions that won't work or 14 | // modify actions that need different conditions, keybindings, etc. 15 | const registry = Registry.as(Extensions.WorkbenchActions); 16 | const originalRegister = registry.registerWorkbenchAction.bind(registry); 17 | registry.registerWorkbenchAction = (descriptor: SyncActionDescriptor, alias: string, category?: string, when?: ContextKeyExpr): IDisposable => { 18 | switch (descriptor.id) { 19 | case ToggleDevToolsAction.ID: // There appears to be no way to toggle this programmatically. 20 | logger.debug(`Skipping unsupported workbench action ${descriptor.id}`); 21 | 22 | return { 23 | dispose: (): void => undefined, 24 | }; 25 | 26 | case TerminalPasteAction.ID: // Modify the Windows keybinding and add our context key. 27 | // tslint:disable-next-line no-any override private 28 | (descriptor as any)._keybindings = { 29 | primary: KeyMod.CtrlCmd | KeyCode.KEY_V, 30 | linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V }, 31 | win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V }, 32 | mac: { primary: 0 }, 33 | }; 34 | // tslint:disable-next-line no-any override private 35 | (descriptor as any)._keybindingContext = ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, workbench.clipboardContextKey); 36 | } 37 | 38 | return originalRegister(descriptor, alias, category, when); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/workspacesService.ts: -------------------------------------------------------------------------------- 1 | import { URI } from "vs/base/common/uri"; 2 | import { IEnvironmentService } from "vs/platform/environment/common/environment"; 3 | import { ILogService } from "vs/platform/log/common/log"; 4 | import { IWorkspaceFolderCreationData, IWorkspaceIdentifier, IWorkspacesService } from "vs/platform/workspaces/common/workspaces"; 5 | import { WorkspacesMainService } from "vs/platform/workspaces/electron-main/workspacesMainService"; 6 | import * as workspacesIpc from "vs/platform/workspaces/node/workspacesIpc"; 7 | import { workbench } from "../workbench"; 8 | 9 | /** 10 | * Instead of going to the shared process, we'll directly run these methods on 11 | * the client. This setup means we can only control the current window. 12 | */ 13 | class WorkspacesService implements IWorkspacesService { 14 | // tslint:disable-next-line:no-any 15 | public _serviceBrand: any; 16 | 17 | public createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[] | undefined): Promise { 18 | const mainService = new WorkspacesMainService( 19 | workbench.serviceCollection.get(IEnvironmentService) as IEnvironmentService, 20 | workbench.serviceCollection.get(ILogService) as ILogService, 21 | ); 22 | 23 | // lib/vscode/src/vs/platform/workspaces/node/workspacesIpc.ts 24 | const rawFolders: IWorkspaceFolderCreationData[] = folders!; 25 | if (Array.isArray(rawFolders)) { 26 | folders = rawFolders.map(rawFolder => { 27 | return { 28 | uri: URI.revive(rawFolder.uri), // convert raw URI back to real URI 29 | name: rawFolder.name!, 30 | } as IWorkspaceFolderCreationData; 31 | }); 32 | } 33 | 34 | return mainService.createUntitledWorkspace(folders); 35 | } 36 | } 37 | 38 | const target = workspacesIpc as typeof workspacesIpc; 39 | // @ts-ignore TODO: don't ignore it. 40 | target.WorkspacesChannelClient = WorkspacesService; 41 | -------------------------------------------------------------------------------- /packages/vscode/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | -------------------------------------------------------------------------------- /packages/vscode/src/vscode.scss: -------------------------------------------------------------------------------- 1 | // These use -webkit-margin-before/after which don't work. 2 | .monaco-workbench > .part > .title > .title-label h2, 3 | .monaco-panel-view .panel > .panel-header h3.title { 4 | margin-top: 0; 5 | margin-bottom: 0; 6 | } 7 | 8 | .monaco-icon-label > .monaco-icon-label-description-container { 9 | margin-right: auto; 10 | } 11 | 12 | .monaco-icon-label > .decorations-wrapper { 13 | display: flex; 14 | flex-direction: row; 15 | padding-right: 12px; 16 | } 17 | 18 | .monaco-icon-label::after { 19 | margin-left: initial; 20 | } 21 | 22 | // We don't have rating data. 23 | .extension-ratings { 24 | display: none !important; 25 | } 26 | 27 | // Using @supports to keep the Firefox fixes completely separate from vscode's 28 | // CSS that is tailored for Chrome. 29 | @supports (-moz-appearance:none) { 30 | // Fix buttons getting cut off on notifications. 31 | .monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-buttons-container .monaco-button.monaco-text-button { 32 | max-width: 100%; 33 | width: auto; 34 | } 35 | 36 | .monaco-shell .screen-reader-detected-explanation .buttons a, 37 | .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink, 38 | .monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-buttons-container .monaco-button { 39 | max-width: -moz-fit-content; 40 | } 41 | 42 | .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit, 43 | .explorer-viewlet .panel-header .count, 44 | .extensions-viewlet > .extensions .extension > .details > .header-container > .header > .version, 45 | .debug-viewlet .debug-call-stack .stack-frame .label { 46 | min-width: -moz-fit-content; 47 | } 48 | } 49 | 50 | .window-appicon { 51 | background-image: url(./vscode-coder.svg) !important; 52 | background-size: 56px !important; 53 | width: 56px !important; 54 | margin-right: 4px; 55 | } 56 | -------------------------------------------------------------------------------- /packages/vscode/test/node-pty.test.ts: -------------------------------------------------------------------------------- 1 | import { IPty } from "node-pty"; 2 | import { createClient } from "@coder/protocol/test"; 3 | 4 | const client = createClient(); 5 | jest.mock("../../ide/src/fill/client", () => ({ client })); 6 | const pty = require("../src/fill/node-pty") as typeof import("node-pty"); 7 | 8 | describe("node-pty", () => { 9 | /** 10 | * Returns a function that when called returns a promise that resolves with 11 | * the next chunk of data from the process. 12 | */ 13 | const promisifyData = (proc: IPty): (() => Promise) => { 14 | // Use a persistent callback instead of creating it in the promise since 15 | // otherwise we could lose data that comes in while no promise is listening. 16 | let onData: (() => void) | undefined; 17 | let buffer: string | undefined; 18 | proc.on("data", (data) => { 19 | // Remove everything that isn't a letter, number, or $ to avoid issues 20 | // with ANSI escape codes printing inside the test output. 21 | buffer = (buffer || "") + data.toString().replace(/[^a-zA-Z0-9$]/g, ""); 22 | if (onData) { 23 | onData(); 24 | } 25 | }); 26 | 27 | return (): Promise => new Promise((resolve): void => { 28 | onData = (): void => { 29 | if (typeof buffer !== "undefined") { 30 | const data = buffer; 31 | buffer = undefined; 32 | onData = undefined; 33 | resolve(data); 34 | } 35 | }; 36 | onData(); 37 | }); 38 | }; 39 | 40 | it("should create shell", async () => { 41 | // Setting the config file to something that shouldn't exist so the test 42 | // isn't affected by custom configuration. 43 | const proc = pty.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], { 44 | cols: 100, 45 | rows: 10, 46 | }); 47 | 48 | const getData = promisifyData(proc); 49 | 50 | // First it outputs @hostname:cwd 51 | expect((await getData()).length).toBeGreaterThan(1); 52 | 53 | // Then it seems to overwrite that with a shorter prompt in the format of 54 | // [hostname@user]$ 55 | expect((await getData())).toContain("$"); 56 | 57 | proc.kill(); 58 | 59 | await new Promise((resolve): void => { 60 | proc.on("exit", resolve); 61 | }); 62 | }); 63 | 64 | it("should resize", async () => { 65 | // Requires the `tput lines` cmd to be available. 66 | // Setting the config file to something that shouldn't exist so the test 67 | // isn't affected by custom configuration. 68 | const proc = pty.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], { 69 | cols: 10, 70 | rows: 10, 71 | }); 72 | 73 | const getData = promisifyData(proc); 74 | 75 | // We've already tested these first two bits of output; see shell test. 76 | await getData(); 77 | await getData(); 78 | 79 | proc.write("tput lines\n"); 80 | expect(await getData()).toContain("tput"); 81 | 82 | expect((await getData()).trim()).toContain("10"); 83 | proc.resize(10, 50); 84 | 85 | // The prompt again. 86 | await getData(); 87 | await getData(); 88 | 89 | proc.write("tput lines\n"); 90 | expect(await getData()).toContain("tput"); 91 | 92 | expect((await getData())).toContain("50"); 93 | 94 | proc.kill(); 95 | await new Promise((resolve): void => { 96 | proc.on("exit", resolve); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /packages/vscode/webpack.bootstrap.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const merge = require("webpack-merge"); 3 | 4 | const root = path.resolve(__dirname, "../.."); 5 | const fills = path.join(root, "packages/ide/src/fill"); 6 | const vsFills = path.join(root, "packages/vscode/src/fill"); 7 | 8 | module.exports = merge( 9 | require(path.join(root, "scripts/webpack.node.config.js"))({ 10 | typescriptCompilerOptions: { 11 | target: "es6", 12 | }, 13 | }), { 14 | entry: path.join(root, "lib/vscode/src/bootstrap-fork.js"), 15 | mode: "development", 16 | output: { 17 | chunkFilename: "[name].bundle.js", 18 | path: path.resolve(__dirname, "out"), 19 | publicPath: "/", 20 | filename: "bootstrap-fork.js", 21 | libraryTarget: "commonjs", 22 | globalObject: "this", 23 | }, 24 | // Due to the dynamic `require.context` we add to `loader.js` Webpack tries 25 | // to include way too much. We can modify what Webpack imports in this case 26 | // (I believe), but for now ignore some things. 27 | module: { 28 | rules: [{ 29 | test: /\.(txt|d\.ts|perf\.data\.js|jxs|scpt|exe|sh|less|html|s?css|qwoff|md|svg|png|ttf|woff|eot|woff2)$/, 30 | use: [{ 31 | loader: "ignore-loader", 32 | }], 33 | }, { 34 | test: /test|tsconfig/, 35 | use: [{ 36 | loader: "ignore-loader", 37 | }], 38 | }, { 39 | test: /((\\|\/)vs(\\|\/)code(\\|\/)electron-main(\\|\/))|((\\|\/)test(\\|\/))|(OSSREADME\.json$)|\/browser\//, 40 | use: [{ 41 | loader: "ignore-loader", 42 | }], 43 | }], 44 | noParse: /(\\|\/)test(\\|\/)|\.test\.jsx?|\.test\.tsx?|tsconfig.+\.json$/, 45 | }, 46 | resolve: { 47 | alias: { 48 | "gc-signals": path.join(fills, "empty.ts"), 49 | "node-pty": path.resolve(fills, "empty.ts"), 50 | "windows-mutex": path.resolve(fills, "empty.ts"), 51 | "windows-process-tree": path.resolve(fills, "empty.ts"), 52 | "vscode-windows-registry": path.resolve(fills, "empty.ts"), 53 | "vscode-sqlite3": path.resolve(fills, "empty.ts"), 54 | "vs/base/browser/browser": path.resolve(fills, "empty.ts"), 55 | 56 | "electron": path.join(vsFills, "stdioElectron.ts"), 57 | "vscode-ripgrep": path.join(vsFills, "ripgrep.ts"), 58 | "native-keymap": path.join(vsFills, "native-keymap.ts"), 59 | "native-watchdog": path.join(vsFills, "native-watchdog.ts"), 60 | "vs/base/common/amd": path.resolve(vsFills, "amd.ts"), 61 | "vs/base/node/paths": path.resolve(vsFills, "paths.ts"), 62 | "vs/platform/product/node/package": path.resolve(vsFills, "package.ts"), 63 | "vs/platform/product/node/product": path.resolve(vsFills, "product.ts"), 64 | "vs/base/node/zip": path.resolve(vsFills, "zip.ts"), 65 | "vs": path.resolve(root, "lib/vscode/src/vs"), 66 | }, 67 | }, 68 | resolveLoader: { 69 | alias: { 70 | "vs/css": path.resolve(vsFills, "css.js"), 71 | }, 72 | }, 73 | } 74 | ); 75 | -------------------------------------------------------------------------------- /packages/web/.gitignore: -------------------------------------------------------------------------------- 1 | out -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/web", 3 | "scripts": { 4 | "build": "../../node_modules/.bin/cross-env UV_THREADPOOL_SIZE=100 node --max-old-space-size=32384 ../../node_modules/webpack/bin/webpack.js --config ./webpack.config.js" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | code-server 7 | 8 | 9 | 10 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/web/src/index.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | width: 100%; 5 | } 6 | 7 | #overlay { 8 | background: rgba(0, 0, 0, 0.2); 9 | bottom: 0; 10 | left: 0; 11 | position: absolute; 12 | right: 0; 13 | top: 0; 14 | } 15 | 16 | #overlay { 17 | align-items: center; 18 | background-color: #252526; 19 | bottom: 0; 20 | display: flex; 21 | flex-direction: column; 22 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 23 | justify-content: center; 24 | left: 0; 25 | opacity: 1; 26 | position: absolute; 27 | right: 0; 28 | top: 0; 29 | transition: 150ms opacity ease; 30 | z-index: 2; 31 | } 32 | 33 | #overlay > .message { 34 | color: white; 35 | margin-top: 10px; 36 | opacity: 0.5; 37 | } 38 | 39 | #overlay.error > .message { 40 | color: white; 41 | opacity: 0.3; 42 | } 43 | 44 | #overlay > .activitybar { 45 | background-color: rgb(44, 44, 44); 46 | bottom: 0; 47 | height: 100%; 48 | left: 0; 49 | position: absolute; 50 | top: 0; 51 | width: 50px; 52 | } 53 | 54 | #overlay > .activitybar svg { 55 | fill: white; 56 | margin-left: 2px; 57 | margin-top: 2px; 58 | opacity: 0.3; 59 | } 60 | 61 | #overlay.error > #status { 62 | opacity: 0; 63 | } 64 | 65 | #overlay>.statusbar { 66 | background-color: rgb(0, 122, 204); 67 | bottom: 0; 68 | cursor: default; 69 | height: 22px; 70 | left: 0; 71 | position: absolute; 72 | right: 0; 73 | } 74 | 75 | #logo { 76 | transform-style: preserve-3d; 77 | } 78 | 79 | #logo > svg { 80 | fill: rgb(0, 122, 204); 81 | opacity: 1; 82 | width: 100px; 83 | } 84 | 85 | #status { 86 | background: rgba(255, 255, 255, 0.1); 87 | border-radius: 5px; 88 | box-shadow: 0px 2px 10px -2px rgba(0, 0, 0, 0.75); 89 | color: white; 90 | font-size: 0.9em; 91 | margin-top: 15px; 92 | min-width: 100px; 93 | position: relative; 94 | transition: 300ms opacity ease; 95 | } 96 | 97 | #progress { 98 | background: rgba(0, 0, 0, 0.2); 99 | border-bottom-left-radius: 5px; 100 | border-bottom-right-radius: 5px; 101 | bottom: 0; 102 | height: 3px; 103 | left: 0; 104 | overflow: hidden; 105 | position: absolute; 106 | right: 0; 107 | } 108 | 109 | @-moz-keyframes statusProgress { 110 | 0% { 111 | background-position: 0% 50% 112 | } 113 | 114 | 50% { 115 | background-position: 100% 50% 116 | } 117 | 118 | 100% { 119 | background-position: 0% 50% 120 | } 121 | } 122 | 123 | @keyframes statusProgress { 124 | 0% { 125 | background-position: 0% 50% 126 | } 127 | 128 | 50% { 129 | background-position: 100% 50% 130 | } 131 | 132 | 100% { 133 | background-position: 0% 50% 134 | } 135 | } 136 | 137 | #fill { 138 | animation: statusProgress 2s ease infinite; 139 | background-size: 400% 400%; 140 | background: linear-gradient(270deg, #007acc, #0016cc); 141 | height: 100%; 142 | transition: 500ms width ease; 143 | width: 0%; 144 | } 145 | 146 | .reload-button { 147 | background-color: #007acc; 148 | border-radius: 2px; 149 | cursor: pointer; 150 | margin-top: 10px; 151 | padding: 6px 10px; 152 | } 153 | -------------------------------------------------------------------------------- /packages/web/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./index.scss"; 2 | import "@coder/vscode"; 3 | -------------------------------------------------------------------------------- /packages/web/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const merge = require("webpack-merge"); 3 | 4 | const root = path.resolve(__dirname, "../.."); 5 | const fills = path.join(root, "packages/ide/src/fill"); 6 | const vsFills = path.join(root, "packages/vscode/src/fill"); 7 | 8 | module.exports = merge( 9 | require(path.join(root, "scripts/webpack.client.config.js"))({ 10 | entry: path.join(root, "packages/web/src/index.ts"), 11 | template: path.join(root, "packages/web/src/index.html"), 12 | typescriptCompilerOptions: { 13 | "target": "es5", 14 | "lib": ["dom", "esnext"], 15 | "importHelpers": true, 16 | }, 17 | }, 18 | ), { 19 | output: { 20 | chunkFilename: "[name]-[hash:6].bundle.js", 21 | path: path.join(__dirname, "out"), 22 | filename: "[hash:6].bundle.js", 23 | }, 24 | node: { 25 | module: "empty", 26 | crypto: "empty", 27 | tls: "empty", 28 | }, 29 | resolve: { 30 | alias: { 31 | "gc-signals": path.join(fills, "empty.ts"), 32 | "selenium-webdriver": path.join(fills, "empty.ts"), 33 | "vscode": path.join(fills, "empty.ts"), 34 | "vscode-fsevents": path.join(fills, "empty.ts"), 35 | "vscode-windows-registry": path.resolve(fills, "empty.ts"), 36 | "vsda": path.join(fills, "empty.ts"), 37 | "windows-foreground-love": path.join(fills, "empty.ts"), 38 | "windows-mutex": path.join(fills, "empty.ts"), 39 | "windows-process-tree": path.join(fills, "empty.ts"), 40 | "vscode-sqlite3": path.join(fills, "empty.ts"), 41 | "tls": path.join(fills, "empty.ts"), 42 | "native-is-elevated": path.join(fills, "empty.ts"), 43 | "dns": path.join(fills, "empty.ts"), 44 | "console": path.join(fills, "empty.ts"), 45 | "readline": path.join(fills, "empty.ts"), 46 | "oniguruma": path.join(fills, "empty.ts"), 47 | 48 | // Webpack includes path-browserify but not the latest version, so 49 | // path.posix and path.parse are undefined (among other things possibly). 50 | // Also if we don't provide the full path, the code in vscode will import 51 | // from vscode's node_modules which is the wrong version. 52 | "path": path.join(fills, "path.js"), 53 | "crypto": "crypto-browserify", 54 | "http": "http-browserify", 55 | 56 | "child_process": path.join(fills, "child_process.ts"), 57 | "os": path.join(fills, "os.ts"), 58 | "fs": path.join(fills, "fs.ts"), 59 | "net": path.join(fills, "net.ts"), 60 | "util": path.join(fills, "util.ts"), 61 | "electron": path.join(fills, "electron.ts"), 62 | 63 | "native-keymap": path.join(vsFills, "native-keymap.ts"), 64 | "node-pty": path.join(vsFills, "node-pty.ts"), 65 | "graceful-fs": path.join(vsFills, "graceful-fs.ts"), 66 | "spdlog": path.join(vsFills, "spdlog.ts"), 67 | "native-watchdog": path.join(vsFills, "native-watchdog.ts"), 68 | "iconv-lite": path.join(vsFills, "iconv-lite.ts"), 69 | 70 | // This seems to be in the wrong place? 71 | "vs/workbench/contrib/codeEditor/electron-browser/media/WordWrap_16x.svg": "vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/WordWrap_16x.svg", 72 | 73 | "vs/base/node/paths": path.join(vsFills, "paths.ts"), 74 | "vs/base/common/amd": path.join(vsFills, "amd.ts"), 75 | "vs/platform/product/node/package": path.resolve(vsFills, "package.ts"), 76 | "vs/platform/product/node/product": path.resolve(vsFills, "product.ts"), 77 | "vs/base/node/zip": path.resolve(vsFills, "zip.ts"), 78 | "vs": path.join(root, "lib", "vscode", "src", "vs"), 79 | }, 80 | }, 81 | resolveLoader: { 82 | alias: { 83 | "vs/css": path.join(vsFills, "css.js"), 84 | }, 85 | }, 86 | }); 87 | -------------------------------------------------------------------------------- /packages/web/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /rules/src/curlyStatementNewlinesRule.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import * as Lint from "tslint"; 3 | 4 | /** 5 | * Curly statement newlines rule. 6 | */ 7 | export class Rule extends Lint.Rules.AbstractRule { 8 | public static FAILURE_STRING = "Curly statements must separate with newlines"; 9 | 10 | /** 11 | * Apply the rule. 12 | */ 13 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 14 | return this.applyWithWalker(new CurlyStatementNewlinesWalker(sourceFile, this.getOptions())); 15 | } 16 | } 17 | 18 | /** 19 | * Curly statement newlines walker. 20 | */ 21 | class CurlyStatementNewlinesWalker extends Lint.RuleWalker { 22 | /** 23 | * Visit if statements. 24 | */ 25 | public visitIfStatement(node: ts.IfStatement): void { 26 | const splitLength = node.getFullText().trim().split("\n").length; 27 | if (splitLength <= 2) { 28 | this.addFailureAt(node.getStart(), node.getWidth(), Rule.FAILURE_STRING); 29 | } 30 | 31 | super.visitIfStatement(node); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /rules/src/noBlockPaddingRule.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import * as Lint from "tslint"; 3 | 4 | /** 5 | * Rule for disallowing blank lines around the content of blocks. 6 | */ 7 | export class Rule extends Lint.Rules.AbstractRule { 8 | public static BEFORE_FAILURE_STRING = "Blocks must not start with blank lines"; 9 | public static AFTER_FAILURE_STRING = "Blocks must not end with blank lines"; 10 | 11 | /** 12 | * Apply the rule. 13 | */ 14 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 15 | return this.applyWithWalker(new NoBlockPaddingWalker(sourceFile, this.getOptions())); 16 | } 17 | } 18 | 19 | /** 20 | * Walker for checking block padding. 21 | */ 22 | class NoBlockPaddingWalker extends Lint.RuleWalker { 23 | /** 24 | * Apply this rule to interfaces. 25 | */ 26 | public visitInterfaceDeclaration(node: ts.InterfaceDeclaration): void { 27 | this.visitBlockNode(node); 28 | super.visitInterfaceDeclaration(node); 29 | } 30 | 31 | /** 32 | * Apply this rule to classes. 33 | */ 34 | public visitClassDeclaration(node: ts.ClassDeclaration): void { 35 | this.visitBlockNode(node); 36 | super.visitClassDeclaration(node); 37 | } 38 | 39 | /** 40 | * Add failures to blank lines surrounding a block's content. 41 | */ 42 | private visitBlockNode(node: ts.ClassDeclaration | ts.InterfaceDeclaration): void { 43 | const sourceFile = node.getSourceFile(); 44 | const children = node.getChildren(); 45 | 46 | const openBraceIndex = children.findIndex((n) => n.kind === ts.SyntaxKind.OpenBraceToken); 47 | if (openBraceIndex !== -1) { 48 | const nextToken = children[openBraceIndex + 1]; 49 | if (nextToken) { 50 | const startLine = this.getStartIncludingComments(sourceFile, nextToken); 51 | const openBraceToken = children[openBraceIndex]; 52 | if (ts.getLineAndCharacterOfPosition(sourceFile, openBraceToken.getEnd()).line + 1 < startLine) { 53 | this.addFailureAt(openBraceToken.getEnd(), openBraceToken.getEnd(), Rule.BEFORE_FAILURE_STRING); 54 | } 55 | } 56 | } 57 | 58 | const closeBraceIndex = children.findIndex((n) => n.kind === ts.SyntaxKind.CloseBraceToken); 59 | if (closeBraceIndex >= 2) { 60 | const previousToken = children[closeBraceIndex - 1]; 61 | if (previousToken) { 62 | let endLine = ts.getLineAndCharacterOfPosition(sourceFile, previousToken.getEnd()).line; 63 | const closeBraceToken = children[closeBraceIndex]; 64 | if (this.getStartIncludingComments(sourceFile, closeBraceToken) > endLine + 1) { 65 | this.addFailureAt(closeBraceToken.getStart(), closeBraceToken.getStart(), Rule.AFTER_FAILURE_STRING); 66 | } 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * getStart() doesn't account for comments while this does. 73 | */ 74 | private getStartIncludingComments(sourceFile: ts.SourceFile, node: ts.Node): number { 75 | // This gets the line the node starts on without counting comments. 76 | let startLine = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart()).line; 77 | 78 | // Adjust the start line for the comments. 79 | const comments = ts.getLeadingCommentRanges(sourceFile.text, node.pos) || []; 80 | comments.forEach((c) => { 81 | const commentStartLine = ts.getLineAndCharacterOfPosition(sourceFile, c.pos).line; 82 | if (commentStartLine < startLine) { 83 | startLine = commentStartLine; 84 | } 85 | }); 86 | 87 | return startLine; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /rules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "declaration": false, 6 | "rootDir": "./src", 7 | "outDir": "./dist" 8 | }, 9 | "include": [ 10 | "." 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | yarn task build:server:binary 5 | -------------------------------------------------------------------------------- /scripts/dummy.js: -------------------------------------------------------------------------------- 1 | // This is for ignoring CSS and images when running tests with Jest. 2 | -------------------------------------------------------------------------------- /scripts/install-packages.ts: -------------------------------------------------------------------------------- 1 | import { exec, execSync } from "child_process"; 2 | import { existsSync, readdirSync } from "fs"; 3 | import * as os from "os"; 4 | import { join, resolve } from "path"; 5 | import { logger, field } from "../packages/logger/src/logger"; 6 | 7 | /** 8 | * Install dependencies for a single package. 9 | */ 10 | const doInstall = (pkg: string, path: string): Promise => { 11 | logger.info(`Installing "${pkg}" dependencies...`); 12 | 13 | return new Promise((resolve): void => { 14 | exec("yarn --network-concurrency 1", { 15 | cwd: path, 16 | maxBuffer: 1024 * 1024 * 10, 17 | }, (error, stdout, stderr) => { 18 | if (error) { 19 | logger.error( 20 | `Failed to install "${pkg}" dependencies`, 21 | field("error", error), 22 | field("stdout", stdout), 23 | field("stderr", stderr), 24 | ); 25 | process.exit(1); 26 | } 27 | 28 | logger.info(`Successfully grabbed \"${pkg}\" dependencies!`); 29 | resolve(); 30 | }); 31 | }); 32 | }; 33 | 34 | /** 35 | * Install dependencies for all packages. 36 | */ 37 | const handlePackages = async (dir: string): Promise => { 38 | const dirs = readdirSync(dir); 39 | for (let i = 0; i < dirs.length; i++) { 40 | const pkg = dirs[i]; 41 | const pkgDir = join(dir, pkg); 42 | const pkgJsonPath = join(pkgDir, "package.json"); 43 | if (existsSync(pkgJsonPath)) { 44 | const ip = doInstall(pkg, pkgDir); 45 | if (os.platform() === "win32") { 46 | await ip; 47 | } 48 | } 49 | } 50 | }; 51 | 52 | handlePackages(resolve(__dirname, "..", "packages")).then(() => { 53 | return handlePackages(resolve(__dirname, "..", "packages", "app")); 54 | }); 55 | -------------------------------------------------------------------------------- /scripts/test-setup.js: -------------------------------------------------------------------------------- 1 | global.requestAnimationFrame = (cb) => { 2 | setTimeout(cb, 0); 3 | }; 4 | -------------------------------------------------------------------------------- /scripts/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const merge = require("webpack-merge"); 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | const PreloadWebpackPlugin = require("preload-webpack-plugin"); 6 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 7 | // const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); 8 | 9 | const root = path.join(__dirname, ".."); 10 | const prod = process.env.NODE_ENV === "production"; 11 | 12 | module.exports = (options = {}) => merge( 13 | require("./webpack.general.config")(options), { 14 | devtool: prod ? "source-map" : "cheap-module-eval-source-map", 15 | mode: prod ? "production" : "development", 16 | entry: prod ? options.entry : [ 17 | "webpack-hot-middleware/client?reload=true&quiet=true", 18 | options.entry, 19 | ], 20 | module: { 21 | rules: [{ 22 | test: /\.s?css$/, 23 | // This is required otherwise it'll fail to resolve CSS in common. 24 | include: root, 25 | use: [{ 26 | loader: MiniCssExtractPlugin.loader, 27 | }, { 28 | loader: "css-loader", 29 | }, { 30 | loader: "sass-loader", 31 | }], 32 | }, { 33 | test: /\.(svg|png|ttf|woff|eot|woff2)$/, 34 | use: [{ 35 | loader: "file-loader", 36 | options: { 37 | name: "[path][name].[ext]", 38 | }, 39 | }], 40 | }], 41 | }, 42 | plugins: [ 43 | new MiniCssExtractPlugin({ 44 | filename: "[name].css", 45 | chunkFilename: "[id].css", 46 | }), 47 | new HtmlWebpackPlugin({ 48 | template: options.template, 49 | }), 50 | new PreloadWebpackPlugin({ 51 | rel: "preload", 52 | as: "script", 53 | }), 54 | ].concat(prod ? [] : [ 55 | new webpack.HotModuleReplacementPlugin(), 56 | ]), 57 | target: "web", 58 | }); 59 | -------------------------------------------------------------------------------- /scripts/webpack.general.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const os = require("os"); 3 | const environment = process.env.NODE_ENV || "development"; 4 | const HappyPack = require("happypack"); 5 | const webpack = require("webpack"); 6 | 7 | const root = path.join(__dirname, ".."); 8 | 9 | module.exports = (options = {}) => ({ 10 | context: root, 11 | devtool: "none", 12 | externals: ["fsevents"], 13 | module: { 14 | rules: [{ 15 | loader: "string-replace-loader", 16 | test: /\.(j|t)s/, 17 | options: { 18 | multiple: [{ 19 | // These will be handled by file-loader. We need the location because 20 | // they are parsed as URIs and will throw errors if not fully formed. 21 | // The !! prefix causes it to ignore other loaders (doesn't work). 22 | search: "require\\.toUrl\\(", 23 | replace: "location.protocol + '//' + location.host + '/' + require('!!file-loader?name=[path][name].[ext]!' + ", 24 | flags: "g", 25 | }, { 26 | search: "require\\.__\\$__nodeRequire", 27 | replace: "require", 28 | flags: "g", 29 | }, { 30 | search: "\\.attributes\\[([^\\]]+)\\] = ([^;]+)", 31 | replace: ".setAttribute($1, $2)", 32 | flags: "g", 33 | }], 34 | }, 35 | }, { 36 | test: /\.node$/, 37 | use: "node-loader", 38 | }, { 39 | use: [{ 40 | loader: "happypack/loader?id=ts", 41 | }], 42 | test: /(^.?|\.[^d]|[^.]d|[^.][^d])\.tsx?$/, 43 | }, { 44 | test: /\.wasm$/, 45 | type: "javascript/auto", 46 | }, { 47 | // Fixes spdlog. 48 | test: /spdlog(\\|\/)index\.js/, 49 | loader: "string-replace-loader", 50 | options: { 51 | multiple: [{ 52 | search: "const spdlog.*;", 53 | replace: "const spdlog = __non_webpack_require__(global.SPDLOG_LOCATION);", 54 | flags: "g", 55 | }], 56 | }, 57 | }, { 58 | // This is required otherwise it attempts to require("package.json") 59 | test: /@oclif(\\|\/)command(\\|\/)lib(\\|\/)index\.js/, 60 | loader: "string-replace-loader", 61 | options: { 62 | multiple: [{ 63 | search: "checkNodeVersion\\(\\);", 64 | replace: "", 65 | flags: "g", 66 | }], 67 | }, 68 | }, { 69 | test: /node\-pty\-prebuilt(\\|\/)lib(\\|\/)index\.js/, 70 | loader: "string-replace-loader", 71 | options: { 72 | multiple: [{ 73 | search: "exports\\.native.*;", 74 | replace: "exports.native = null;", 75 | flags: "g", 76 | }], 77 | }, 78 | }, { 79 | test: /node\-pty\-prebuilt(\\|\/)lib(\\|\/).*\.js/, 80 | loader: "string-replace-loader", 81 | options: { 82 | multiple: [{ 83 | search: "var pty = .*pty\.node.*;", 84 | replace: "var pty = __non_webpack_require__(global.NODEPTY_LOCATION);", 85 | flags: "g", 86 | }], 87 | }, 88 | }], 89 | }, 90 | resolve: { 91 | alias: { 92 | "@coder": path.join(root, "packages"), 93 | }, 94 | extensions: [".js", ".jsx", ".ts", ".tsx", ".json", ".css"], 95 | mainFiles: [ 96 | "index", 97 | "src/index", 98 | ], 99 | }, 100 | resolveLoader: { 101 | modules: [ 102 | path.join(root, "node_modules"), 103 | ], 104 | }, 105 | plugins: [ 106 | new HappyPack({ 107 | id: "ts", 108 | threads: Math.max(os.cpus().length - 1, 1), 109 | loaders: [{ 110 | path: "ts-loader", 111 | query: { 112 | happyPackMode: true, 113 | compilerOptions: options.typescriptCompilerOptions, 114 | }, 115 | }], 116 | }), 117 | new webpack.DefinePlugin({ 118 | "process.env.NODE_ENV": `"${environment}"`, 119 | "process.env.LOG_LEVEL": `"${process.env.LOG_LEVEL || ""}"`, 120 | "process.env.SERVICE_URL": `"${process.env.SERVICE_URL || ""}"`, 121 | "process.env.VERSION": `"${process.env.VERSION || ""}"`, 122 | }), 123 | ], 124 | stats: { 125 | all: false, // Fallback for options not defined. 126 | errors: true, 127 | warnings: true, 128 | }, 129 | }); 130 | -------------------------------------------------------------------------------- /scripts/webpack.node.config.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | 3 | module.exports = (options = {}) => merge( 4 | require("./webpack.general.config")(options), { 5 | devtool: "none", 6 | mode: "production", 7 | target: "node", 8 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "baseUrl": ".", 6 | "rootDir": ".", 7 | "jsx": "react", 8 | "outDir": "dist", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "resolveJsonModule": true, 13 | "experimentalDecorators": true, 14 | "plugins": [ 15 | { 16 | "name": "tslint-language-service" 17 | } 18 | ], 19 | "paths": { 20 | "@coder/*": [ 21 | "./packages/*" 22 | ], 23 | "vs/*": [ 24 | "./lib/vscode/src/vs/*" 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": "./rules/dist", 3 | "rules": { 4 | "only-arrow-functions": true, 5 | "curly-statement-newlines": true, 6 | "no-block-padding": true, 7 | "adjacent-overload-signatures": true, 8 | "align": true, 9 | "await-promise": [true, "Thenable"], 10 | "class-name": true, 11 | "eofline": true, 12 | "import-spacing": true, 13 | "indent": [true, "tabs"], 14 | "no-angle-bracket-type-assertion": false, 15 | "no-bitwise": false, 16 | "no-any": true, 17 | "newline-before-return": true, 18 | "no-console": true, 19 | "no-duplicate-imports": true, 20 | "no-consecutive-blank-lines": true, 21 | "no-empty": true, 22 | "no-floating-promises": true, 23 | "no-return-await": true, 24 | "no-var-keyword": true, 25 | "no-trailing-whitespace": true, 26 | "no-redundant-jsdoc": true, 27 | "no-implicit-dependencies": false, 28 | "no-boolean-literal-compare": true, 29 | "prefer-readonly": true, 30 | "deprecation": true, 31 | "semicolon": true, 32 | "one-line": [ 33 | true, 34 | "check-catch", 35 | "check-finally", 36 | "check-else", 37 | "check-whitespace", 38 | "check-open-brace" 39 | ], 40 | "completed-docs": { 41 | "options": [ 42 | true, 43 | "enums", 44 | "functions", 45 | "methods", 46 | "classes" 47 | ], 48 | "severity": "warning" 49 | }, 50 | "no-unused-expression": [ 51 | true, 52 | "allow-fast-null-checks" 53 | ], 54 | "curly": [ 55 | true 56 | ], 57 | "quotemark": [ 58 | true, 59 | "double", 60 | "avoid-escape", 61 | "avoid-template" 62 | ], 63 | "trailing-comma": [ 64 | true, 65 | { 66 | "multiline": "always", 67 | "singleline": "never", 68 | "esSpecCompliant": true 69 | } 70 | ], 71 | "space-before-function-paren": [ 72 | false, 73 | "always" 74 | ], 75 | "member-access": [ 76 | true, 77 | "check-accessor", 78 | "check-constructor", 79 | "check-parameter-property" 80 | ], 81 | "typedef": [ 82 | true, 83 | "call-signature", 84 | "arrow-call-signature", 85 | "parameter", 86 | "property-declaration" 87 | ] 88 | } 89 | } 90 | --------------------------------------------------------------------------------