Skip to content

Commit

Permalink
Init VSCode Extension (#3858)
Browse files Browse the repository at this point in the history
Summary:
This PR aims to scaffold out an MVP for the Relay LSP VSCode Extension.

## High Level Changes
- Added new `vscode-extension` in the root dir containing all the extension code.
- We're using typescript in this new directory. Spoke this over with captbaritone and it seems like we're cool with it.
  - Main benefit is we get all the types published by the VSCode team for free without having to wait for published flow defs.
- Added a basic LSP Client in `extension.ts` which looks for a relay binary and starts it up. Things seem to be working well in the Coinbase codebase.
- Added a `launch.json` so contributors can easily test the extension with `F5`. It also lets you see the debug output from the LSP Server directly in the opened window. It's really sweet.

## Follow Ups
I left a bunch of TODOs in the source if you want some more insight.

- [ ] Use VSCode config instead of environment variables from the launch json
- [ ] Support VSCode workspaces (don't use `workspace.rootPath`). Not exactly sure how to handle this but will be looking at the flow extension for more guidance.
- [ ] Better error message handling
- [ ] Status Bar support
- [x] Fix go to definition on fields / types
- [ ] Add command to manually restart the LSP Client.
- [x] Add a Github Action to do type checking / build check / prettier / linting

Pull Request resolved: #3858

Test Plan:
Imported from GitHub, without a `Test Plan:` line.

**Static Docs Preview: relay**
|[Full Site](https://our.intern.facebook.com/intern/staticdocs/eph/D35408177/V5/relay/)|

|**Modified Pages**|

**Static Docs Preview: relay**
|[Full Site](https://our.intern.facebook.com/intern/staticdocs/eph/D35408177/V6/relay/)|

|**Modified Pages**|

Reviewed By: alunyov

Differential Revision: D35408177

Pulled By: captbaritone

fbshipit-source-id: 26db9dcd0c6e03e955c4f07898d4737eae5322f7
  • Loading branch information
tbezman authored and facebook-github-bot committed Apr 6, 2022
1 parent d80203a commit 4c8a6df
Show file tree
Hide file tree
Showing 12 changed files with 1,734 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ name: CI
on: [push, pull_request]

jobs:
vscode-extension-lint:
name: VSCode Extension Lint
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./vscode-extension
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile --ignore-scripts
- name: ESLint
run: yarn run lint
- name: Prettier
run: yarn run prettier-check
- name: Typecheck
run: yarn run typecheck

js-tests:
name: JS Tests (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions packages/relay-compiler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

const path = require('path');

// We copy this binary resolution in the VSCode extension
// If this changes, please update accordingly in here
// https://github.com/facebook/relay/blob/main/vscode-extension/src/utils.ts
let binary;
if (process.platform === 'darwin' && process.arch === 'x64') {
binary = path.join(__dirname, 'macos-x64', 'relay');
Expand Down
14 changes: 14 additions & 0 deletions vscode-extension/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"root": true,
"ignorePatterns": "out/*",
"parserOptions": {
"project": "./tsconfig.json"
},
"extends": ["airbnb-base", "airbnb-typescript/base"],
"rules": {
"operator-linebreak": "off",
"@typescript-eslint/object-curly-spacing": "off",
"import/prefer-default-export": "off",
"no-await-in-loop": "off"
}
}
2 changes: 2 additions & 0 deletions vscode-extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out/
tsconfig.tsbuildinfo
19 changes: 19 additions & 0 deletions vscode-extension/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Relay Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"preLaunchTask": "ts-build",
"env": {
// Use this if you want to use a local binary
// Otherwise we'll look in your node modules
// "RELAY_BINARY_PATH": "/path/to/your/binary",
"RELAY_LSP_LOG_LEVEL": "debug"
}
}
]
}
5 changes: 5 additions & 0 deletions vscode-extension/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"files.exclude": {
"out": true
}
}
17 changes: 17 additions & 0 deletions vscode-extension/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "ts-build",
"type": "typescript",
"tsconfig": "tsconfig.json",
"problemMatcher": ["$tsc"],
"group": {
"kind": "build",
"isDefault": false
}
}
]
}
68 changes: 68 additions & 0 deletions vscode-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"name": "relay",
"displayName": "Relay GraphQL",
"version": "0.0.1",
"description": "Relay LSP Client for VSCode",
"main": "./out/extension.js",
"activationEvents": [
"onLanguage:plaintext",
"onLanguage:javascript",
"onLanguage:javascriptreact",
"onLanguage:typescript",
"onLanguage:typescriptreact"
],
"contributes": {
"configuration": {
"type": "object",
"title": "Relay",
"properties": {
"relay.outputLevel": {
"scope": "window",
"type": "string",
"default": "quiet-with-errors",
"enum": [
"quiet",
"quiet-with-errors",
"verbose",
"debug"
],
"description": "Controls what is logged to the Output Channel."
},
"relay.pathToRelay": {
"scope": "window",
"type": "string",
"description": "Absolute path to the relay binary. If not provided, the extension will look in the nearest node_modules directory"
}
}
}
},
"scripts": {
"typecheck": "tsc --noEmit",
"prettier-check": "prettier -c .",
"lint": "eslint --max-warnings 0 ."
},
"engines": {
"vscode": "^1.65.0"
},
"prettier": {
"bracketSameLine": true,
"bracketSpacing": false,
"singleQuote": true,
"trailingComma": "all"
},
"dependencies": {
"vscode-languageclient": "^7.0.0"
},
"devDependencies": {
"@types/node": "^17.0.23",
"@types/vscode": "^1.65.0",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.12.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
"prettier": "^2.6.2",
"typescript": "^4.6.3"
}
}
141 changes: 141 additions & 0 deletions vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {workspace, window} from 'vscode';

import {
CloseAction,
ErrorAction,
LanguageClient,
LanguageClientOptions,
RevealOutputChannelOn,
ServerOptions,
} from 'vscode-languageclient/node';
import {findRelayBinary} from './utils';

let client: LanguageClient;

export async function activate() {
const outputChannel = window.createOutputChannel('Relay Language Server');

// TODO: Support multi folder workspaces by not using rootPath.
// Maybe initialize a client once for each workspace?
const relayBinary =
// TODO: Use VSCode config instead of process.env
process.env.RELAY_BINARY_PATH ??
(await findRelayBinary(workspace.rootPath));

// TODO: Use VSCode config instead of process.env
const outputLevel = process.env.RELAY_LSP_LOG_LEVEL ?? 'debug';

if (!relayBinary) {
outputChannel.appendLine(
"Could not find relay binary in path. Maybe you're not inside of a project with relay installed.",
);

return;
}

outputChannel.appendLine(`Using relay binary: ${relayBinary}`);

const serverOptions: ServerOptions = {
command: relayBinary,
args: ['lsp', `--output=${outputLevel}`],
};

// Options to control the language client
const clientOptions: LanguageClientOptions = {
markdown: {
isTrusted: true,
},
documentSelector: [
{scheme: 'file', language: 'javascript'},
{scheme: 'file', language: 'typescript'},
{scheme: 'file', language: 'typescriptreact'},
{scheme: 'file', language: 'javascriptreact'},
],

outputChannel,

// Since we use stderr for debug logs, the "Something went wrong" popup
// in VSCode shows up a lot. This tells vscode not to show it in any case.
revealOutputChannelOn: RevealOutputChannelOn.Never,

initializationFailedHandler: (error) => {
outputChannel.appendLine(`initializationFailedHandler ${error}`);

return true;
},

errorHandler: {
// This happens when the LSP server stops running.
// e.g. Could not find relay config.
// e.g. watchman was not installed.
//
// TODO: Figure out the best way to handle this `closed` event
//
// Some of these messages are worth surfacing and others are not
// e.g. "Watchman is not installed" is important to surface to the user
// but "No relay config found" is not relevant since the user is likely
// just in a workspace where they don't have a relay config.
//
// We already bail early if there is no relay binary found.
// So maybe we should just show all of these messages since it would
// be weird if you had a relay binary in your node modules but no relay
// config could be found. 🤷 for now.
closed() {
window
.showWarningMessage(
'Relay LSP client connection got closed unexpectedly.',
'Go to output',
'Ignore',
)
.then((selected) => {
if (selected === 'Go to output') {
client.outputChannel.show();
}
});

return CloseAction.DoNotRestart;
},
// This `error` callback should probably never happen. 🙏
error() {
window
.showWarningMessage(
'An error occurred while writing/reading to/from the relay lsp connection',
'Go to output',
'Ignore',
)
.then((selected) => {
if (selected === 'Go to output') {
client.outputChannel.show();
}
});

return ErrorAction.Continue;
},
},
};

// Create the language client and start the client.
client = new LanguageClient(
'RelayLanguageClient',
'Relay Language Client',
serverOptions,
clientOptions,
);

// Start the client. This will also launch the server
client.start();
}

export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
}
85 changes: 85 additions & 0 deletions vscode-extension/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import * as path from 'path';
import * as fs from 'fs/promises';

async function exists(file: string): Promise<boolean> {
return fs
.stat(file)
.then(() => true)
.catch(() => false);
}

// This is derived from the relay-compiler npm package.
// If you update this, please update accordingly here
// https://github.com/facebook/relay/blob/main/packages/relay-compiler/index.js
function getBinaryPathRelativeToPackageJson() {
let binaryPathRelativeToPackageJson;
if (process.platform === 'darwin' && process.arch === 'x64') {
binaryPathRelativeToPackageJson = path.join('macos-x64', 'relay');
} else if (process.platform === 'darwin' && process.arch === 'arm64') {
binaryPathRelativeToPackageJson = path.join('macos-arm64', 'relay');
} else if (process.platform === 'linux' && process.arch === 'x64') {
binaryPathRelativeToPackageJson = path.join('linux-x64', 'relay');
} else if (process.platform === 'win32' && process.arch === 'x64') {
binaryPathRelativeToPackageJson = path.join('win-x64', 'relay.exe');
} else {
binaryPathRelativeToPackageJson = null;
}

if (binaryPathRelativeToPackageJson) {
return path.join(
'.',
'node_modules',
'relay-compiler',
binaryPathRelativeToPackageJson,
);
}

return null;
}

export async function findRelayBinary(
rootPath: string,
): Promise<string | null> {
const binaryPathRelativeToPackageJson = getBinaryPathRelativeToPackageJson();

let counter = 0;
let currentPath = rootPath;

// eslint-disable-next-line no-constant-condition
while (true) {
if (counter >= 5000) {
throw new Error(
'Could not find Relay binary after 5000 traversals. This is likely a bug in the extension code and should be reported to https://github.com/facebook/relay/issues',
);
}

counter += 1;

const possibleBinaryPath = path.join(
currentPath,
binaryPathRelativeToPackageJson,
);

if (await exists(possibleBinaryPath)) {
return possibleBinaryPath;
}

const nextPath = path.normalize(path.join(currentPath, '..'));

// Eventually we'll get to `/` and get stuck in a loop.
if (nextPath === currentPath) {
break;
} else {
currentPath = nextPath;
}
}

return null;
}
Loading

0 comments on commit 4c8a6df

Please sign in to comment.