Skip to content

Commit 711e692

Browse files
authored
Add support for loading local processors
* This adds support for loading a local processor (such as `rehype`) from the file system (`node_modules/`) in the project * By default, it now shows a warningh notification when that processor can’t be found locally, instead of throwing an error * Optionally, a default processor can be passed when creating the language server, which will be used if a local processor can’t be found, instead of the previously mentioned warning notification Reviewed-by: Remco Haszing <[email protected]> Closes GH-23. Related to: remarkjs/remark-language-server#3.
1 parent f222a65 commit 711e692

9 files changed

+266
-21
lines changed

lib/index.js

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,39 @@
33
* @typedef {import('unist').Position} UnistPosition
44
* @typedef {import('vfile-message').VFileMessage} VFileMessage
55
* @typedef {import('vscode-languageserver').Connection} Connection
6-
* @typedef {Partial<Pick<
7-
* import('unified-engine').Options,
6+
* @typedef {import('unified-engine').Options} EngineOptions
7+
* @typedef {Pick<
8+
* EngineOptions,
89
* | 'ignoreName'
910
* | 'packageField'
1011
* | 'pluginPrefix'
1112
* | 'plugins'
12-
* | 'processor'
1313
* | 'rcName'
14-
* >>} Options
14+
* >} EngineFields
15+
*
16+
* @typedef LanguageServerFields
17+
* @property {string} processorName
18+
* The package ID of the expected processor (example: `'remark'`).
19+
* Will be loaded from the local workspace.
20+
* @property {string} [processorSpecifier='default']
21+
* The specifier to get the processor on the resolved module.
22+
* For example, remark uses the specifier `remark` to expose its processor and
23+
* a default export can be requested by passing `'default'` (the default).
24+
* @property {EngineOptions['processor']} [defaultProcessor]
25+
* Optional fallback processor to use if `processorName` can’t be found
26+
* locally in `node_modules`.
27+
* This can be used to ship a processor with your package, to be used if no
28+
* processor is found locally.
29+
* If this isn’t passed, a warning is shown if `processorName` can’t be found.
30+
*
31+
* @typedef {EngineFields & LanguageServerFields} Options
1532
*/
1633

1734
import {PassThrough} from 'node:stream'
35+
import process from 'node:process'
1836
import {URL, pathToFileURL} from 'node:url'
1937

20-
import {unified} from 'unified'
38+
import {loadPlugin} from 'load-plugin'
2139
import {engine} from 'unified-engine'
2240
import {VFile} from 'vfile'
2341
import {
@@ -140,7 +158,9 @@ export function configureUnifiedLanguageServer(
140158
packageField,
141159
pluginPrefix,
142160
plugins,
143-
processor = unified(),
161+
processorName,
162+
processorSpecifier = 'default',
163+
defaultProcessor,
144164
rcName
145165
}
146166
) {
@@ -152,7 +172,48 @@ export function configureUnifiedLanguageServer(
152172
* @param {boolean} alwaysStringify
153173
* @returns {Promise<VFile[]>}
154174
*/
155-
function processDocuments(textDocuments, alwaysStringify = false) {
175+
async function processDocuments(textDocuments, alwaysStringify = false) {
176+
/** @type {EngineOptions['processor']} */
177+
let processor
178+
179+
try {
180+
// @ts-expect-error: assume we load a unified processor.
181+
processor = await loadPlugin(processorName, {
182+
cwd: process.cwd(),
183+
key: processorSpecifier
184+
})
185+
} catch (error) {
186+
const exception = /** @type {NodeJS.ErrnoException} */ (error)
187+
188+
// Pass other funky errors through.
189+
/* c8 ignore next 3 */
190+
if (exception.code !== 'ERR_MODULE_NOT_FOUND') {
191+
throw error
192+
}
193+
194+
if (!defaultProcessor) {
195+
connection.window.showInformationMessage(
196+
'Cannot turn on language server without `' +
197+
processorName +
198+
'` locally. Run `npm install ' +
199+
processorName +
200+
'` to enable it'
201+
)
202+
return []
203+
}
204+
205+
const problem = new Error(
206+
'Cannot find `' +
207+
processorName +
208+
'` locally but using `defaultProcessor`, original error:\n' +
209+
exception.stack
210+
)
211+
212+
connection.console.log(String(problem))
213+
214+
processor = defaultProcessor
215+
}
216+
156217
return new Promise((resolve, reject) => {
157218
engine(
158219
{
@@ -179,12 +240,13 @@ export function configureUnifiedLanguageServer(
179240
} else {
180241
resolve((context && context.files) || [])
181242
}
182-
/* c8 ignore stop */
183243
}
184244
)
185245
})
186246
}
187247

248+
/* c8 ignore stop */
249+
188250
/**
189251
* Process various LSP text documents using unified and send back the
190252
* resulting messages as diagnostics.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
],
3535
"dependencies": {
3636
"@types/unist": "^2.0.0",
37-
"unified": "^10.0.0",
37+
"load-plugin": "^4.0.0",
3838
"unified-engine": "^9.0.0",
3939
"vfile": "^5.0.0",
4040
"vfile-message": "^3.0.0",
@@ -51,6 +51,7 @@
5151
"tape": "^5.0.0",
5252
"type-coverage": "^2.0.0",
5353
"typescript": "^4.0.0",
54+
"unified": "^10.0.0",
5455
"xo": "^0.47.0"
5556
},
5657
"scripts": {

readme.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,10 @@ createUnifiedLanguageServer({
9393
ignoreName: '.remarkignore',
9494
packageField: 'remarkConfig',
9595
pluginPrefix: 'remark',
96-
processor: remark,
97-
rcName: '.remarkrc'
96+
rcName: '.remarkrc',
97+
processorName: 'remark',
98+
processorSpecifier: 'remark',
99+
defaultProcessor: remark
98100
})
99101
```
100102

@@ -111,28 +113,47 @@ Create a language server for a unified ecosystem.
111113

112114
Configuration for `unified-engine` and the language server.
113115

116+
###### `options.processorName`
117+
118+
The package ID of the expected processor (`string`, required, example:
119+
`'remark'`).
120+
Will be loaded from the local workspace.
121+
122+
###### `options.processorSpecifier`
123+
124+
The specifier to get the processor on the resolved module (`string`, optional,
125+
default: `'default'`).
126+
For example, remark uses the specifier `remark` to expose its processor and
127+
a default export can be requested by passing `'default'` (the default).
128+
129+
###### `options.defaultProcessor`
130+
131+
Optional fallback processor to use if `processorName` can’t be found
132+
locally in `node_modules` ([`Unified`][unified], optional).
133+
This can be used to ship a processor with your package, to be used if no
134+
processor is found locally.
135+
If this isn’t passed, a warning is shown if `processorName` can’t be found.
136+
114137
###### `options.ignoreName`
115138

116-
Name of ignore files to load (`string`, optional)
139+
Name of ignore files to load (`string`, optional).
117140

118141
###### `options.packageField`
119142

120143
Property at which configuration can be found in package.json files (`string`,
121-
optional)
144+
optional).
122145

123146
###### `options.pluginPrefix`
124147

125-
Optional prefix to use when searching for plugins (`string`, optional)
148+
Optional prefix to use when searching for plugins (`string`, optional).
126149

127150
###### `options.plugins`
128151

129-
Plugins to use by default (`Array|Object`, optional)
130-
131-
Typically this contains 2 plugins named `*-parse` and `*-stringify`.
152+
Plugins to use by default (`Array|Object`, optional).
132153

133154
###### `options.rcName`
134155

135-
Name of configuration files to load (`string`, optional)
156+
Name of configuration files to load (`string`, optional).
136157

137158
## Examples
138159

test/index.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,148 @@ test('`textDocument/didOpen`, `textDocument/didClose` (and diagnostics)', async
205205
t.end()
206206
})
207207

208+
test('uninstalled processor so `window/showMessageRequest`', async (t) => {
209+
const stdin = new PassThrough()
210+
const promise = execa('node', ['missing-package.js', '--stdio'], {
211+
cwd: fileURLToPath(new URL('.', import.meta.url)),
212+
input: stdin,
213+
timeout
214+
})
215+
216+
stdin.write(
217+
toMessage({
218+
method: 'initialize',
219+
id: 0,
220+
/** @type {import('vscode-languageserver').InitializeParams} */
221+
params: {
222+
processId: null,
223+
rootUri: null,
224+
capabilities: {},
225+
workspaceFolders: null
226+
}
227+
})
228+
)
229+
230+
await sleep(delay)
231+
232+
stdin.write(
233+
toMessage({
234+
method: 'textDocument/didOpen',
235+
/** @type {import('vscode-languageserver').DidOpenTextDocumentParams} */
236+
params: {
237+
textDocument: {
238+
uri: new URL('lsp.md', import.meta.url).href,
239+
languageId: 'markdown',
240+
version: 1,
241+
text: '# hi'
242+
}
243+
}
244+
})
245+
)
246+
247+
await sleep(delay)
248+
249+
assert(promise.stdout)
250+
promise.stdout.on('data', () => setImmediate(() => stdin.end()))
251+
252+
try {
253+
await promise
254+
t.fail('should reject')
255+
} catch (error) {
256+
const exception = /** @type {ExecError} */ (error)
257+
const messages = fromMessages(exception.stdout)
258+
t.equal(messages.length, 2, 'should emit messages')
259+
const parameters = messages[1].params
260+
261+
t.deepEqual(
262+
parameters,
263+
{
264+
type: 3,
265+
message:
266+
'Cannot turn on language server without `xxx-missing-yyy` locally. Run `npm install xxx-missing-yyy` to enable it',
267+
actions: []
268+
},
269+
'should emit a `window/showMessageRequest` when the processor can’t be found locally'
270+
)
271+
}
272+
273+
t.end()
274+
})
275+
276+
test('uninstalled processor w/ `defaultProcessor`', async (t) => {
277+
const stdin = new PassThrough()
278+
const promise = execa(
279+
'node',
280+
['missing-package-with-default.js', '--stdio'],
281+
{
282+
cwd: fileURLToPath(new URL('.', import.meta.url)),
283+
input: stdin,
284+
timeout
285+
}
286+
)
287+
288+
stdin.write(
289+
toMessage({
290+
method: 'initialize',
291+
id: 0,
292+
/** @type {import('vscode-languageserver').InitializeParams} */
293+
params: {
294+
processId: null,
295+
rootUri: null,
296+
capabilities: {},
297+
workspaceFolders: null
298+
}
299+
})
300+
)
301+
302+
await sleep(delay)
303+
304+
stdin.write(
305+
toMessage({
306+
method: 'textDocument/didOpen',
307+
/** @type {import('vscode-languageserver').DidOpenTextDocumentParams} */
308+
params: {
309+
textDocument: {
310+
uri: new URL('lsp.md', import.meta.url).href,
311+
languageId: 'markdown',
312+
version: 1,
313+
text: '# hi'
314+
}
315+
}
316+
})
317+
)
318+
319+
await sleep(delay)
320+
321+
assert(promise.stdout)
322+
promise.stdout.on('data', () => setImmediate(() => stdin.end()))
323+
324+
try {
325+
await promise
326+
t.fail('should reject')
327+
} catch (error) {
328+
const exception = /** @type {ExecError} */ (error)
329+
const messages = fromMessages(exception.stdout)
330+
t.equal(messages.length, 3, 'should emit messages')
331+
332+
const parameters =
333+
/** @type {import('vscode-languageserver').LogMessageParams} */ (
334+
messages[1].params
335+
)
336+
337+
t.deepEqual(
338+
cleanStack(parameters.message, 2).replace(
339+
/(imported from )[^\r\n]+/,
340+
'$1zzz'
341+
),
342+
"Error: Cannot find `xxx-missing-yyy` locally but using `defaultProcessor`, original error:\nError [ERR_MODULE_NOT_FOUND]: Cannot find package 'xxx-missing-yyy' imported from zzz",
343+
'should work w/ `defaultProcessor`'
344+
)
345+
}
346+
347+
t.end()
348+
})
349+
208350
test('`textDocument/formatting`', async (t) => {
209351
const stdin = new PassThrough()
210352

test/missing-package-with-default.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {remark} from 'remark'
2+
import {createUnifiedLanguageServer} from '../index.js'
3+
4+
createUnifiedLanguageServer({
5+
processorName: 'xxx-missing-yyy',
6+
defaultProcessor: remark
7+
})

test/missing-package.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {createUnifiedLanguageServer} from '../index.js'
2+
3+
createUnifiedLanguageServer({
4+
processorName: 'xxx-missing-yyy'
5+
})

test/remark-with-error.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {createUnifiedLanguageServer} from '../index.js'
22

33
createUnifiedLanguageServer({
4-
plugins: ['remark-parse', 'remark-stringify', './one-error.js']
4+
processorName: 'remark',
5+
processorSpecifier: 'remark',
6+
plugins: ['./one-error.js']
57
})

test/remark-with-warnings.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {createUnifiedLanguageServer} from '../index.js'
22

33
createUnifiedLanguageServer({
4-
plugins: ['remark-parse', 'remark-stringify', './lots-of-warnings.js']
4+
processorName: 'remark',
5+
processorSpecifier: 'remark',
6+
plugins: ['./lots-of-warnings.js']
57
})

test/remark.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import {createUnifiedLanguageServer} from '../index.js'
22

3-
createUnifiedLanguageServer({plugins: ['remark-parse', 'remark-stringify']})
3+
createUnifiedLanguageServer({
4+
processorName: 'remark',
5+
processorSpecifier: 'remark'
6+
})

0 commit comments

Comments
 (0)