Skip to content

Commit

Permalink
Support multiple workspaces/clients in Parcel for VSCode (#9278)
Browse files Browse the repository at this point in the history
* Add vscode workspace setting

Disable js/ts validation for the workspace
(packages that have .tsconfig should still work)

* WIP: detect lsp server in reporter

If the server isn't running, the reporter should do nothing.

* Update todo doc

* Add ideas to todo doc

* Fix kitchen-sync example

* support multiple workspaces (#9265)

* WIP: lsp sentinel watcher

* garbage

* f

* add initial sentinel check to watch

* remove event emitter from reporter

* update README, add reporter README

* support multiple LSP clients

- changed reporter project root to used process.cwd
- only add client when workspace root matches project root

* remove generated files

* remove other generated files

* move vscode-extension-TODO into extension dir

* clean up

* remove unused import

---------

Co-authored-by: Celina Peralta <cperalta@atlassian.com>

* remove examples changes

* revert html example changes

* move development info to CONTRIBUTING.md

* Remove log

Co-authored-by: Eric Eldredge <lettertwo@gmail.com>

* Update packages/reporters/lsp-reporter/src/LspReporter.js

Co-authored-by: Eric Eldredge <lettertwo@gmail.com>

* Update packages/reporters/lsp-reporter/src/LspReporter.js

Co-authored-by: Eric Eldredge <lettertwo@gmail.com>

* Update packages/utils/parcel-lsp/src/LspServer.ts

Co-authored-by: Eric Eldredge <lettertwo@gmail.com>

* Update packages/utils/parcel-lsp/src/LspServer.ts

Co-authored-by: Eric Eldredge <lettertwo@gmail.com>

* Update packages/reporters/lsp-reporter/src/LspReporter.js

Co-authored-by: Eric Eldredge <lettertwo@gmail.com>

* Update packages/utils/parcel-lsp/src/LspServer.ts

Co-authored-by: Eric Eldredge <lettertwo@gmail.com>

* Update packages/reporters/lsp-reporter/src/LspReporter.js

Co-authored-by: Eric Eldredge <lettertwo@gmail.com>

* Update packages/utils/parcel-lsp/src/LspServer.ts

Co-authored-by: Eric Eldredge <lettertwo@gmail.com>

* add sentinel file cleanup

* Apply suggestions from code review

* linting

---------

Co-authored-by: Eric Eldredge <lettertwo@gmail.com>
Co-authored-by: Brian Tedder <6571474+imbrian@users.noreply.github.com>
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
  • Loading branch information
4 people authored Oct 23, 2023
1 parent 808edd8 commit e92ba6c
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 68 deletions.
4 changes: 2 additions & 2 deletions packages/examples/kitchen-sink/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@
"@parcel/reporter-sourcemap-visualiser": "2.10.0",
"parcel": "2.10.0"
},
"browser": "dist/legacy/index.html",
"browserModern": "dist/modern/index.html",
"targets": {
"browserModern": {
"distDir": "dist/modern",
"engines": {
"browsers": [
"last 1 Chrome version"
]
}
},
"browser": {
"distDir": "dist/legacy",
"engines": {
"browsers": [
"> 0.25%"
Expand Down
13 changes: 13 additions & 0 deletions packages/reporters/lsp-reporter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# LSP Reporter

This reporter is for sending diagnostics to a running [LSP server](../../utils/parcel-lsp/). This is inteded to be used alongside the Parcel VS Code extension.

It creates an IPC server for responding to requests for diagnostics from the LSP server, and pushes diagnostics to the LSP server.

## Usage

This reporter is run with Parcel build, watch, and serve commands by passing `@parcel/reporter-lsp` to the `--reporter` option.

```sh
parcel serve --reporter @parcel/reporter-lsp
```
134 changes: 90 additions & 44 deletions packages/reporters/lsp-reporter/src/LspReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
normalizeFilePath,
parcelSeverityToLspSeverity,
} from './utils';
import type {FSWatcher} from 'fs';

const lookupPid: Query => Program[] = promisify(ps.lookup);

Expand Down Expand Up @@ -67,58 +68,103 @@ let bundleGraphDeferrable =
let bundleGraph: Promise<?BundleGraph<PackagedBundle>> =
bundleGraphDeferrable.promise;

export default (new Reporter({
async report({event, options}) {
switch (event.type) {
case 'watchStart': {
await fs.promises.mkdir(BASEDIR, {recursive: true});

// For each existing file, check if the pid matches a running process.
// If no process matches, delete the file, assuming it was orphaned
// by a process that quit unexpectedly.
for (let filename of fs.readdirSync(BASEDIR)) {
if (filename.endsWith('.json')) continue;
let pid = parseInt(filename.slice('parcel-'.length), 10);
let resultList = await lookupPid({pid});
if (resultList.length > 0) continue;
fs.unlinkSync(path.join(BASEDIR, filename));
ignoreFail(() =>
fs.unlinkSync(path.join(BASEDIR, filename + '.json')),
);
}
let watchStarted = false;
let lspStarted = false;
let watchStartPromise;

server = await createServer(SOCKET_FILE, connection => {
// console.log('got connection');
connections.push(connection);
connection.onClose(() => {
connections = connections.filter(c => c !== connection);
});
const LSP_SENTINEL_FILENAME = 'lsp-server';
const LSP_SENTINEL_FILE = path.join(BASEDIR, LSP_SENTINEL_FILENAME);

connection.onRequest(RequestDocumentDiagnostics, async uri => {
let graph = await bundleGraph;
if (!graph) return;
async function watchLspActive(): Promise<FSWatcher> {
// Check for lsp-server when reporter is first started
try {
await fs.promises.access(LSP_SENTINEL_FILE, fs.constants.F_OK);
lspStarted = true;
} catch {
//
}

return getDiagnosticsUnusedExports(graph, uri);
return fs.watch(BASEDIR, (eventType: string, filename: string) => {
switch (eventType) {
case 'rename':
if (filename === LSP_SENTINEL_FILENAME) {
fs.access(LSP_SENTINEL_FILE, fs.constants.F_OK, err => {
if (err) {
lspStarted = false;
} else {
lspStarted = true;
}
});
}
}
});
}

connection.onRequest(RequestImporters, async params => {
let graph = await bundleGraph;
if (!graph) return null;
async function doWatchStart() {
await fs.promises.mkdir(BASEDIR, {recursive: true});

// For each existing file, check if the pid matches a running process.
// If no process matches, delete the file, assuming it was orphaned
// by a process that quit unexpectedly.
for (let filename of fs.readdirSync(BASEDIR)) {
if (filename.endsWith('.json')) continue;
let pid = parseInt(filename.slice('parcel-'.length), 10);
let resultList = await lookupPid({pid});
if (resultList.length > 0) continue;
fs.unlinkSync(path.join(BASEDIR, filename));
ignoreFail(() => fs.unlinkSync(path.join(BASEDIR, filename + '.json')));
}

return getImporters(graph, params);
});
server = await createServer(SOCKET_FILE, connection => {
// console.log('got connection');
connections.push(connection);
connection.onClose(() => {
connections = connections.filter(c => c !== connection);
});

sendDiagnostics();
});
await fs.promises.writeFile(
META_FILE,
JSON.stringify({
projectRoot: options.projectRoot,
pid: process.pid,
argv: process.argv,
}),
);
connection.onRequest(RequestDocumentDiagnostics, async uri => {
let graph = await bundleGraph;
if (!graph) return;

return getDiagnosticsUnusedExports(graph, uri);
});

connection.onRequest(RequestImporters, async params => {
let graph = await bundleGraph;
if (!graph) return null;

return getImporters(graph, params);
});

sendDiagnostics();
});
await fs.promises.writeFile(
META_FILE,
JSON.stringify({
projectRoot: process.cwd(),
pid: process.pid,
argv: process.argv,
}),
);
}

watchLspActive();

export default (new Reporter({
async report({event, options}) {
if (event.type === 'watchStart') {
watchStarted = true;
}

if (watchStarted && lspStarted) {
if (!watchStartPromise) {
watchStartPromise = doWatchStart();
}
await watchStartPromise;
}

switch (event.type) {
case 'watchStart': {
break;
}

Expand Down
43 changes: 32 additions & 11 deletions packages/utils/parcel-lsp/src/LspServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,15 @@ import {
RequestImporters,
} from '@parcel/lsp-protocol';

const connection = createConnection(ProposedFeatures.all);
type Metafile = {
projectRoot: string;
pid: typeof process['pid'];
argv: typeof process['argv'];
};

const connection = createConnection(ProposedFeatures.all);
const WORKSPACE_ROOT = process.cwd();
const LSP_SENTINEL_FILENAME = 'lsp-server';
// Create a simple text document manager.
// const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

Expand Down Expand Up @@ -220,9 +227,12 @@ function findClient(document: DocumentUri): Client | undefined {
return bestClient;
}

function createClient(metafilepath: string) {
let metafile = JSON.parse(fs.readFileSync(metafilepath, 'utf8'));
function parseMetafile(filepath: string) {
const file = fs.readFileSync(filepath, 'utf-8');
return JSON.parse(file);
}

function createClient(metafilepath: string, metafile: Metafile) {
let socketfilepath = metafilepath.slice(0, -5);
let [reader, writer] = createServerPipeTransport(socketfilepath);
let client = createMessageConnection(reader, writer);
Expand Down Expand Up @@ -263,27 +273,34 @@ function createClient(metafilepath: string) {
});

client.onClose(() => {
clients.delete(metafile);
clients.delete(JSON.stringify(metafile));
sendDiagnosticsRefresh();
return Promise.all(
[...uris].map(uri => connection.sendDiagnostics({uri, diagnostics: []})),
);
});

sendDiagnosticsRefresh();
clients.set(metafile, result);
clients.set(JSON.stringify(metafile), result);
}

// Take realpath because to have consistent cache keys on macOS (/var -> /private/var)
const BASEDIR = path.join(fs.realpathSync(os.tmpdir()), 'parcel-lsp');
fs.mkdirSync(BASEDIR, {recursive: true});

fs.writeFileSync(path.join(BASEDIR, LSP_SENTINEL_FILENAME), '');

// Search for currently running Parcel processes in the parcel-lsp dir.
// Create an IPC client connection for each running process.
for (let filename of fs.readdirSync(BASEDIR)) {
if (!filename.endsWith('.json')) continue;
let filepath = path.join(BASEDIR, filename);
createClient(filepath);
// console.log('connected initial', filepath);
const contents = parseMetafile(filepath);
const {projectRoot} = contents;

if (WORKSPACE_ROOT === projectRoot) {
createClient(filepath, contents);
}
}

// Watch for new Parcel processes in the parcel-lsp dir, and disconnect the
Expand All @@ -295,15 +312,19 @@ watcher.subscribe(BASEDIR, async (err, events) => {

for (let event of events) {
if (event.type === 'create' && event.path.endsWith('.json')) {
createClient(event.path);
// console.log('connected watched', event.path);
const file = fs.readFileSync(event.path, 'utf-8');
const contents = parseMetafile(file);
const {projectRoot} = contents;

if (WORKSPACE_ROOT === projectRoot) {
createClient(event.path, contents);
}
} else if (event.type === 'delete' && event.path.endsWith('.json')) {
let existing = clients.get(event.path);
// console.log('existing', event.path, existing);
console.log('existing', event.path, existing);
if (existing) {
clients.delete(event.path);
existing.connection.end();
// console.log('disconnected watched', event.path);
}
}
}
Expand Down
35 changes: 35 additions & 0 deletions packages/utils/parcelforvscode/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## Development Debugging

1. Go to the Run and Debug menu in VSCode
2. Select "Launch Parcel for VSCode Extension"
3. Specify in which project to run the Extension Development Host in `launch.json`:

```
{
"version": "0.2.0",
"configurations": [
{
"args": [
"${workspaceFolder}/packages/examples/kitchen-sink", // Change this project
"--extensionDevelopmentPath=${workspaceFolder}/packages/utils/parcelforvscode"
],
"name": "Launch Parcel for VSCode Extension",
"outFiles": [
"${workspaceFolder}/packages/utils/parcelforvscode/out/**/*.js"
],
"preLaunchTask": "Watch VSCode Extension",
"request": "launch",
"type": "extensionHost"
}
]
}
```

4. Run a Parcel command (e.g. `parcel server --reporter @parcel/reporter-lsp`) in the Extension Host window.
5. Diagnostics should appear in the Extension Host window in the Problems panel (Shift + CMD + m).
6. Output from the extension should be available in the Output panel (Shift + CMD + u) in the launching window.

## Packaging

1. Run `yarn package`. The output is a `.vsix` file.
2. Run `code --install-extension parcel-for-vscode-<version>.vsix`
10 changes: 10 additions & 0 deletions packages/utils/parcelforvscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type {ExtensionContext} from 'vscode';

import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';

import {
LanguageClient,
LanguageClientOptions,
Expand Down Expand Up @@ -48,5 +51,12 @@ export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}

const LSP_SENTINEL_FILEPATH = path.join(fs.realpathSync(os.tmpdir()), 'parcel-lsp', 'lsp-server');

if (fs.existsSync(LSP_SENTINEL_FILEPATH)) {
fs.rmSync(LSP_SENTINEL_FILEPATH);
}

return client.stop();
}
32 changes: 32 additions & 0 deletions packages/utils/parcelforvscode/vscode-extension-TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Packages:

- [@parcel/reporter-lsp](./packages/reporters/lsp-reporter/)
- [parcel-for-vscode](./packages/utils/parcelforvscode/)
- [@parcel/lsp](./packages/utils/parcel-lsp/)
- [@parcel/lsp-protocol](./packages/utils/parcel-lsp-protocol)

TODO:

- [x] need to not wait for connections
- [x] language server shuts down and kills our process when the extension is closed
- [x] handle the case where parcel is started after the extension is running
- [x] handle the case where extension is started while parcel is running
- [x] support multiple parcels
- [x] show prior diagnostics on connection
- [x] only connect to parcels that match the workspace
- [ ] show parcel diagnostic hints
- [ ] implement quick fixes (requires Parcel changes?)
- [x] cleanup LSP server sentinel when server shuts down
- [x] support multiple LSP servers (make sure a workspace only sees errors from its server)
- [x] cleanup the lsp reporter's server detection (make async, maybe use file watcher)
- [ ] make @parcel/reporter-lsp part of default config or otherwise always installed
(or, move the reporter's behavior into core)

Ideas:

- a `parcel lsp` cli command to replace/subsume the standalone `@parcel/lsp` server
- this could take on the complexities of decision making like automatically
starting a Parcel build if one isn’t running, or sharing an LSP server
for the same parcel project with multiple workspaces/instances, etc.
- integrating the behavior of `@parcel/reporter-lsp` into core
or otherwise having the reporter be 'always on' or part of default config
11 changes: 0 additions & 11 deletions vscode-extension-TODO.md

This file was deleted.

0 comments on commit e92ba6c

Please sign in to comment.