diff --git a/packages/examples/kitchen-sink/package.json b/packages/examples/kitchen-sink/package.json index 42112c6d671..47c042abf30 100644 --- a/packages/examples/kitchen-sink/package.json +++ b/packages/examples/kitchen-sink/package.json @@ -16,10 +16,9 @@ "@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" @@ -27,6 +26,7 @@ } }, "browser": { + "distDir": "dist/legacy", "engines": { "browsers": [ "> 0.25%" diff --git a/packages/reporters/lsp-reporter/README.md b/packages/reporters/lsp-reporter/README.md new file mode 100644 index 00000000000..f9846f82390 --- /dev/null +++ b/packages/reporters/lsp-reporter/README.md @@ -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 +``` diff --git a/packages/reporters/lsp-reporter/src/LspReporter.js b/packages/reporters/lsp-reporter/src/LspReporter.js index 70b8f81bdca..6574496be52 100644 --- a/packages/reporters/lsp-reporter/src/LspReporter.js +++ b/packages/reporters/lsp-reporter/src/LspReporter.js @@ -36,6 +36,7 @@ import { normalizeFilePath, parcelSeverityToLspSeverity, } from './utils'; +import type {FSWatcher} from 'fs'; const lookupPid: Query => Program[] = promisify(ps.lookup); @@ -67,58 +68,103 @@ let bundleGraphDeferrable = let bundleGraph: Promise> = 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 { + // 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; } diff --git a/packages/utils/parcel-lsp/src/LspServer.ts b/packages/utils/parcel-lsp/src/LspServer.ts index 3b71455d806..b78a288c285 100644 --- a/packages/utils/parcel-lsp/src/LspServer.ts +++ b/packages/utils/parcel-lsp/src/LspServer.ts @@ -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 = new TextDocuments(TextDocument); @@ -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); @@ -263,7 +273,7 @@ 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: []})), @@ -271,19 +281,26 @@ function createClient(metafilepath: string) { }); 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 @@ -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); } } } diff --git a/packages/utils/parcelforvscode/CONTRIBUTING.md b/packages/utils/parcelforvscode/CONTRIBUTING.md new file mode 100644 index 00000000000..975a9e5dda7 --- /dev/null +++ b/packages/utils/parcelforvscode/CONTRIBUTING.md @@ -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-.vsix` diff --git a/packages/utils/parcelforvscode/src/extension.ts b/packages/utils/parcelforvscode/src/extension.ts index d1f35274651..61ab0c153e9 100644 --- a/packages/utils/parcelforvscode/src/extension.ts +++ b/packages/utils/parcelforvscode/src/extension.ts @@ -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, @@ -48,5 +51,12 @@ export function deactivate(): Thenable | 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(); } diff --git a/packages/utils/parcelforvscode/vscode-extension-TODO.md b/packages/utils/parcelforvscode/vscode-extension-TODO.md new file mode 100644 index 00000000000..5999ed2e022 --- /dev/null +++ b/packages/utils/parcelforvscode/vscode-extension-TODO.md @@ -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 diff --git a/vscode-extension-TODO.md b/vscode-extension-TODO.md deleted file mode 100644 index ecee839edcf..00000000000 --- a/vscode-extension-TODO.md +++ /dev/null @@ -1,11 +0,0 @@ -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 -- [ ] only connect to parcels that match the workspace -- [ ] show parcel diagnostic hints -- [ ] implement quick fixes (requires Parcel changes?)