Skip to content

Commit e92ba6c

Browse files
celinanperaltalettertwoimbrianmischnic
authored
Support multiple workspaces/clients in Parcel for VSCode (#9278)
* 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 <[email protected]> * remove examples changes * revert html example changes * move development info to CONTRIBUTING.md * Remove log Co-authored-by: Eric Eldredge <[email protected]> * Update packages/reporters/lsp-reporter/src/LspReporter.js Co-authored-by: Eric Eldredge <[email protected]> * Update packages/reporters/lsp-reporter/src/LspReporter.js Co-authored-by: Eric Eldredge <[email protected]> * Update packages/utils/parcel-lsp/src/LspServer.ts Co-authored-by: Eric Eldredge <[email protected]> * Update packages/utils/parcel-lsp/src/LspServer.ts Co-authored-by: Eric Eldredge <[email protected]> * Update packages/reporters/lsp-reporter/src/LspReporter.js Co-authored-by: Eric Eldredge <[email protected]> * Update packages/utils/parcel-lsp/src/LspServer.ts Co-authored-by: Eric Eldredge <[email protected]> * Update packages/reporters/lsp-reporter/src/LspReporter.js Co-authored-by: Eric Eldredge <[email protected]> * Update packages/utils/parcel-lsp/src/LspServer.ts Co-authored-by: Eric Eldredge <[email protected]> * add sentinel file cleanup * Apply suggestions from code review * linting --------- Co-authored-by: Eric Eldredge <[email protected]> Co-authored-by: Brian Tedder <[email protected]> Co-authored-by: Niklas Mischkulnig <[email protected]>
1 parent 808edd8 commit e92ba6c

File tree

8 files changed

+214
-68
lines changed

8 files changed

+214
-68
lines changed

packages/examples/kitchen-sink/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@
1616
"@parcel/reporter-sourcemap-visualiser": "2.10.0",
1717
"parcel": "2.10.0"
1818
},
19-
"browser": "dist/legacy/index.html",
20-
"browserModern": "dist/modern/index.html",
2119
"targets": {
2220
"browserModern": {
21+
"distDir": "dist/modern",
2322
"engines": {
2423
"browsers": [
2524
"last 1 Chrome version"
2625
]
2726
}
2827
},
2928
"browser": {
29+
"distDir": "dist/legacy",
3030
"engines": {
3131
"browsers": [
3232
"> 0.25%"
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# LSP Reporter
2+
3+
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.
4+
5+
It creates an IPC server for responding to requests for diagnostics from the LSP server, and pushes diagnostics to the LSP server.
6+
7+
## Usage
8+
9+
This reporter is run with Parcel build, watch, and serve commands by passing `@parcel/reporter-lsp` to the `--reporter` option.
10+
11+
```sh
12+
parcel serve --reporter @parcel/reporter-lsp
13+
```

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

+90-44
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
normalizeFilePath,
3737
parcelSeverityToLspSeverity,
3838
} from './utils';
39+
import type {FSWatcher} from 'fs';
3940

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

@@ -67,58 +68,103 @@ let bundleGraphDeferrable =
6768
let bundleGraph: Promise<?BundleGraph<PackagedBundle>> =
6869
bundleGraphDeferrable.promise;
6970

70-
export default (new Reporter({
71-
async report({event, options}) {
72-
switch (event.type) {
73-
case 'watchStart': {
74-
await fs.promises.mkdir(BASEDIR, {recursive: true});
75-
76-
// For each existing file, check if the pid matches a running process.
77-
// If no process matches, delete the file, assuming it was orphaned
78-
// by a process that quit unexpectedly.
79-
for (let filename of fs.readdirSync(BASEDIR)) {
80-
if (filename.endsWith('.json')) continue;
81-
let pid = parseInt(filename.slice('parcel-'.length), 10);
82-
let resultList = await lookupPid({pid});
83-
if (resultList.length > 0) continue;
84-
fs.unlinkSync(path.join(BASEDIR, filename));
85-
ignoreFail(() =>
86-
fs.unlinkSync(path.join(BASEDIR, filename + '.json')),
87-
);
88-
}
71+
let watchStarted = false;
72+
let lspStarted = false;
73+
let watchStartPromise;
8974

90-
server = await createServer(SOCKET_FILE, connection => {
91-
// console.log('got connection');
92-
connections.push(connection);
93-
connection.onClose(() => {
94-
connections = connections.filter(c => c !== connection);
95-
});
75+
const LSP_SENTINEL_FILENAME = 'lsp-server';
76+
const LSP_SENTINEL_FILE = path.join(BASEDIR, LSP_SENTINEL_FILENAME);
9677

97-
connection.onRequest(RequestDocumentDiagnostics, async uri => {
98-
let graph = await bundleGraph;
99-
if (!graph) return;
78+
async function watchLspActive(): Promise<FSWatcher> {
79+
// Check for lsp-server when reporter is first started
80+
try {
81+
await fs.promises.access(LSP_SENTINEL_FILE, fs.constants.F_OK);
82+
lspStarted = true;
83+
} catch {
84+
//
85+
}
10086

101-
return getDiagnosticsUnusedExports(graph, uri);
87+
return fs.watch(BASEDIR, (eventType: string, filename: string) => {
88+
switch (eventType) {
89+
case 'rename':
90+
if (filename === LSP_SENTINEL_FILENAME) {
91+
fs.access(LSP_SENTINEL_FILE, fs.constants.F_OK, err => {
92+
if (err) {
93+
lspStarted = false;
94+
} else {
95+
lspStarted = true;
96+
}
10297
});
98+
}
99+
}
100+
});
101+
}
103102

104-
connection.onRequest(RequestImporters, async params => {
105-
let graph = await bundleGraph;
106-
if (!graph) return null;
103+
async function doWatchStart() {
104+
await fs.promises.mkdir(BASEDIR, {recursive: true});
105+
106+
// For each existing file, check if the pid matches a running process.
107+
// If no process matches, delete the file, assuming it was orphaned
108+
// by a process that quit unexpectedly.
109+
for (let filename of fs.readdirSync(BASEDIR)) {
110+
if (filename.endsWith('.json')) continue;
111+
let pid = parseInt(filename.slice('parcel-'.length), 10);
112+
let resultList = await lookupPid({pid});
113+
if (resultList.length > 0) continue;
114+
fs.unlinkSync(path.join(BASEDIR, filename));
115+
ignoreFail(() => fs.unlinkSync(path.join(BASEDIR, filename + '.json')));
116+
}
107117

108-
return getImporters(graph, params);
109-
});
118+
server = await createServer(SOCKET_FILE, connection => {
119+
// console.log('got connection');
120+
connections.push(connection);
121+
connection.onClose(() => {
122+
connections = connections.filter(c => c !== connection);
123+
});
110124

111-
sendDiagnostics();
112-
});
113-
await fs.promises.writeFile(
114-
META_FILE,
115-
JSON.stringify({
116-
projectRoot: options.projectRoot,
117-
pid: process.pid,
118-
argv: process.argv,
119-
}),
120-
);
125+
connection.onRequest(RequestDocumentDiagnostics, async uri => {
126+
let graph = await bundleGraph;
127+
if (!graph) return;
128+
129+
return getDiagnosticsUnusedExports(graph, uri);
130+
});
131+
132+
connection.onRequest(RequestImporters, async params => {
133+
let graph = await bundleGraph;
134+
if (!graph) return null;
135+
136+
return getImporters(graph, params);
137+
});
138+
139+
sendDiagnostics();
140+
});
141+
await fs.promises.writeFile(
142+
META_FILE,
143+
JSON.stringify({
144+
projectRoot: process.cwd(),
145+
pid: process.pid,
146+
argv: process.argv,
147+
}),
148+
);
149+
}
150+
151+
watchLspActive();
121152

153+
export default (new Reporter({
154+
async report({event, options}) {
155+
if (event.type === 'watchStart') {
156+
watchStarted = true;
157+
}
158+
159+
if (watchStarted && lspStarted) {
160+
if (!watchStartPromise) {
161+
watchStartPromise = doWatchStart();
162+
}
163+
await watchStartPromise;
164+
}
165+
166+
switch (event.type) {
167+
case 'watchStart': {
122168
break;
123169
}
124170

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

+32-11
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,15 @@ import {
3636
RequestImporters,
3737
} from '@parcel/lsp-protocol';
3838

39-
const connection = createConnection(ProposedFeatures.all);
39+
type Metafile = {
40+
projectRoot: string;
41+
pid: typeof process['pid'];
42+
argv: typeof process['argv'];
43+
};
4044

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

@@ -220,9 +227,12 @@ function findClient(document: DocumentUri): Client | undefined {
220227
return bestClient;
221228
}
222229

223-
function createClient(metafilepath: string) {
224-
let metafile = JSON.parse(fs.readFileSync(metafilepath, 'utf8'));
230+
function parseMetafile(filepath: string) {
231+
const file = fs.readFileSync(filepath, 'utf-8');
232+
return JSON.parse(file);
233+
}
225234

235+
function createClient(metafilepath: string, metafile: Metafile) {
226236
let socketfilepath = metafilepath.slice(0, -5);
227237
let [reader, writer] = createServerPipeTransport(socketfilepath);
228238
let client = createMessageConnection(reader, writer);
@@ -263,27 +273,34 @@ function createClient(metafilepath: string) {
263273
});
264274

265275
client.onClose(() => {
266-
clients.delete(metafile);
276+
clients.delete(JSON.stringify(metafile));
267277
sendDiagnosticsRefresh();
268278
return Promise.all(
269279
[...uris].map(uri => connection.sendDiagnostics({uri, diagnostics: []})),
270280
);
271281
});
272282

273283
sendDiagnosticsRefresh();
274-
clients.set(metafile, result);
284+
clients.set(JSON.stringify(metafile), result);
275285
}
276286

277287
// Take realpath because to have consistent cache keys on macOS (/var -> /private/var)
278288
const BASEDIR = path.join(fs.realpathSync(os.tmpdir()), 'parcel-lsp');
279289
fs.mkdirSync(BASEDIR, {recursive: true});
290+
291+
fs.writeFileSync(path.join(BASEDIR, LSP_SENTINEL_FILENAME), '');
292+
280293
// Search for currently running Parcel processes in the parcel-lsp dir.
281294
// Create an IPC client connection for each running process.
282295
for (let filename of fs.readdirSync(BASEDIR)) {
283296
if (!filename.endsWith('.json')) continue;
284297
let filepath = path.join(BASEDIR, filename);
285-
createClient(filepath);
286-
// console.log('connected initial', filepath);
298+
const contents = parseMetafile(filepath);
299+
const {projectRoot} = contents;
300+
301+
if (WORKSPACE_ROOT === projectRoot) {
302+
createClient(filepath, contents);
303+
}
287304
}
288305

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

296313
for (let event of events) {
297314
if (event.type === 'create' && event.path.endsWith('.json')) {
298-
createClient(event.path);
299-
// console.log('connected watched', event.path);
315+
const file = fs.readFileSync(event.path, 'utf-8');
316+
const contents = parseMetafile(file);
317+
const {projectRoot} = contents;
318+
319+
if (WORKSPACE_ROOT === projectRoot) {
320+
createClient(event.path, contents);
321+
}
300322
} else if (event.type === 'delete' && event.path.endsWith('.json')) {
301323
let existing = clients.get(event.path);
302-
// console.log('existing', event.path, existing);
324+
console.log('existing', event.path, existing);
303325
if (existing) {
304326
clients.delete(event.path);
305327
existing.connection.end();
306-
// console.log('disconnected watched', event.path);
307328
}
308329
}
309330
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
## Development Debugging
2+
3+
1. Go to the Run and Debug menu in VSCode
4+
2. Select "Launch Parcel for VSCode Extension"
5+
3. Specify in which project to run the Extension Development Host in `launch.json`:
6+
7+
```
8+
{
9+
"version": "0.2.0",
10+
"configurations": [
11+
{
12+
"args": [
13+
"${workspaceFolder}/packages/examples/kitchen-sink", // Change this project
14+
"--extensionDevelopmentPath=${workspaceFolder}/packages/utils/parcelforvscode"
15+
],
16+
"name": "Launch Parcel for VSCode Extension",
17+
"outFiles": [
18+
"${workspaceFolder}/packages/utils/parcelforvscode/out/**/*.js"
19+
],
20+
"preLaunchTask": "Watch VSCode Extension",
21+
"request": "launch",
22+
"type": "extensionHost"
23+
}
24+
]
25+
}
26+
```
27+
28+
4. Run a Parcel command (e.g. `parcel server --reporter @parcel/reporter-lsp`) in the Extension Host window.
29+
5. Diagnostics should appear in the Extension Host window in the Problems panel (Shift + CMD + m).
30+
6. Output from the extension should be available in the Output panel (Shift + CMD + u) in the launching window.
31+
32+
## Packaging
33+
34+
1. Run `yarn package`. The output is a `.vsix` file.
35+
2. Run `code --install-extension parcel-for-vscode-<version>.vsix`

packages/utils/parcelforvscode/src/extension.ts

+10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import type {ExtensionContext} from 'vscode';
33

44
import * as vscode from 'vscode';
55
import * as path from 'path';
6+
import * as fs from 'fs';
7+
import * as os from 'os';
8+
69
import {
710
LanguageClient,
811
LanguageClientOptions,
@@ -48,5 +51,12 @@ export function deactivate(): Thenable<void> | undefined {
4851
if (!client) {
4952
return undefined;
5053
}
54+
55+
const LSP_SENTINEL_FILEPATH = path.join(fs.realpathSync(os.tmpdir()), 'parcel-lsp', 'lsp-server');
56+
57+
if (fs.existsSync(LSP_SENTINEL_FILEPATH)) {
58+
fs.rmSync(LSP_SENTINEL_FILEPATH);
59+
}
60+
5161
return client.stop();
5262
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Packages:
2+
3+
- [@parcel/reporter-lsp](./packages/reporters/lsp-reporter/)
4+
- [parcel-for-vscode](./packages/utils/parcelforvscode/)
5+
- [@parcel/lsp](./packages/utils/parcel-lsp/)
6+
- [@parcel/lsp-protocol](./packages/utils/parcel-lsp-protocol)
7+
8+
TODO:
9+
10+
- [x] need to not wait for connections
11+
- [x] language server shuts down and kills our process when the extension is closed
12+
- [x] handle the case where parcel is started after the extension is running
13+
- [x] handle the case where extension is started while parcel is running
14+
- [x] support multiple parcels
15+
- [x] show prior diagnostics on connection
16+
- [x] only connect to parcels that match the workspace
17+
- [ ] show parcel diagnostic hints
18+
- [ ] implement quick fixes (requires Parcel changes?)
19+
- [x] cleanup LSP server sentinel when server shuts down
20+
- [x] support multiple LSP servers (make sure a workspace only sees errors from its server)
21+
- [x] cleanup the lsp reporter's server detection (make async, maybe use file watcher)
22+
- [ ] make @parcel/reporter-lsp part of default config or otherwise always installed
23+
(or, move the reporter's behavior into core)
24+
25+
Ideas:
26+
27+
- a `parcel lsp` cli command to replace/subsume the standalone `@parcel/lsp` server
28+
- this could take on the complexities of decision making like automatically
29+
starting a Parcel build if one isn’t running, or sharing an LSP server
30+
for the same parcel project with multiple workspaces/instances, etc.
31+
- integrating the behavior of `@parcel/reporter-lsp` into core
32+
or otherwise having the reporter be 'always on' or part of default config

vscode-extension-TODO.md

-11
This file was deleted.

0 commit comments

Comments
 (0)