@atcute/lex-cli 2.6.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +17 -12
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +20 -15
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +29 -24
- package/dist/commands/pull.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -2
- package/dist/config.js.map +1 -1
- package/dist/formatter.d.ts +2 -0
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +71 -60
- package/dist/formatter.js.map +1 -1
- package/dist/lsp-client.d.ts +20 -0
- package/dist/lsp-client.d.ts.map +1 -0
- package/dist/lsp-client.js +194 -0
- package/dist/lsp-client.js.map +1 -0
- package/package.json +5 -4
- package/src/commands/export.ts +18 -14
- package/src/commands/generate.ts +25 -20
- package/src/commands/pull.ts +31 -27
- package/src/config.ts +5 -2
- package/src/formatter.ts +80 -68
- package/src/lsp-client.ts +283 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import * as url from 'node:url';
|
|
3
|
+
import { getUtf8Length } from '@atcute/uint8array';
|
|
4
|
+
// #endregion
|
|
5
|
+
// #region text edits
|
|
6
|
+
const applyTextEdits = (code, edits) => {
|
|
7
|
+
if (edits.length === 0) {
|
|
8
|
+
return code;
|
|
9
|
+
}
|
|
10
|
+
// build line start offsets
|
|
11
|
+
const lineStarts = [0];
|
|
12
|
+
for (let i = 0; i < code.length; i++) {
|
|
13
|
+
if (code[i] === '\n') {
|
|
14
|
+
lineStarts.push(i + 1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const positionToOffset = (pos) => {
|
|
18
|
+
return (lineStarts[pos.line] ?? code.length) + pos.character;
|
|
19
|
+
};
|
|
20
|
+
// sort edits in reverse document order so earlier positions stay valid
|
|
21
|
+
const sorted = edits.toSorted((a, b) => {
|
|
22
|
+
const lineDiff = b.range.start.line - a.range.start.line;
|
|
23
|
+
if (lineDiff !== 0) {
|
|
24
|
+
return lineDiff;
|
|
25
|
+
}
|
|
26
|
+
return b.range.start.character - a.range.start.character;
|
|
27
|
+
});
|
|
28
|
+
let result = code;
|
|
29
|
+
for (const edit of sorted) {
|
|
30
|
+
const start = positionToOffset(edit.range.start);
|
|
31
|
+
const end = positionToOffset(edit.range.end);
|
|
32
|
+
result = result.slice(0, start) + edit.newText + result.slice(end);
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
// #endregion
|
|
37
|
+
const inferLanguageId = (filepath) => {
|
|
38
|
+
if (filepath.endsWith('.ts') || filepath.endsWith('.tsx')) {
|
|
39
|
+
return 'typescript';
|
|
40
|
+
}
|
|
41
|
+
if (filepath.endsWith('.json')) {
|
|
42
|
+
return 'json';
|
|
43
|
+
}
|
|
44
|
+
if (filepath.endsWith('.md') || filepath.endsWith('.markdown')) {
|
|
45
|
+
return 'markdown';
|
|
46
|
+
}
|
|
47
|
+
return 'typescript';
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* creates an LSP client that communicates with a formatter over stdio
|
|
51
|
+
* @param command shell command to spawn the LSP server
|
|
52
|
+
* @param root project root for LSP rootUri
|
|
53
|
+
* @returns an initialized LSP client ready for formatting
|
|
54
|
+
*/
|
|
55
|
+
export const createLspClient = async (command, root) => {
|
|
56
|
+
const child = spawn('sh', ['-c', command], {
|
|
57
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
58
|
+
});
|
|
59
|
+
// prevent EPIPE crash when child exits mid-write; actual errors
|
|
60
|
+
// are handled by the close/error handlers below
|
|
61
|
+
child.stdin.on('error', () => { });
|
|
62
|
+
// #region JSON-RPC framing
|
|
63
|
+
const pending = new Map();
|
|
64
|
+
let nextId = 1;
|
|
65
|
+
let exited = false;
|
|
66
|
+
const sendMessage = (message) => {
|
|
67
|
+
if (exited) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const json = JSON.stringify(message);
|
|
71
|
+
const byteLength = getUtf8Length(json);
|
|
72
|
+
child.stdin.write(`Content-Length: ${byteLength}\r\n\r\n${json}`);
|
|
73
|
+
};
|
|
74
|
+
const sendRequest = (method, params) => {
|
|
75
|
+
if (exited) {
|
|
76
|
+
return Promise.reject(new Error(`LSP server has already exited`));
|
|
77
|
+
}
|
|
78
|
+
const id = nextId++;
|
|
79
|
+
const deferred = Promise.withResolvers();
|
|
80
|
+
pending.set(id, deferred);
|
|
81
|
+
sendMessage({ jsonrpc: '2.0', id, method, params });
|
|
82
|
+
return deferred.promise;
|
|
83
|
+
};
|
|
84
|
+
const sendNotification = (method, params) => {
|
|
85
|
+
sendMessage({ jsonrpc: '2.0', method, params });
|
|
86
|
+
};
|
|
87
|
+
// incremental message parser
|
|
88
|
+
const HEADER_SEPARATOR = Buffer.from('\r\n\r\n');
|
|
89
|
+
let buffer = Buffer.alloc(0);
|
|
90
|
+
let contentLength = -1;
|
|
91
|
+
const processBuffer = () => {
|
|
92
|
+
while (true) {
|
|
93
|
+
if (contentLength === -1) {
|
|
94
|
+
const separatorIndex = buffer.indexOf(HEADER_SEPARATOR);
|
|
95
|
+
if (separatorIndex === -1) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const header = buffer.toString('utf8', 0, separatorIndex);
|
|
99
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
100
|
+
buffer = buffer.subarray(separatorIndex + 4);
|
|
101
|
+
if (!match) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
contentLength = parseInt(match[1], 10);
|
|
105
|
+
}
|
|
106
|
+
if (buffer.length < contentLength) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
const body = buffer.toString('utf8', 0, contentLength);
|
|
110
|
+
buffer = buffer.subarray(contentLength);
|
|
111
|
+
contentLength = -1;
|
|
112
|
+
let message;
|
|
113
|
+
try {
|
|
114
|
+
message = JSON.parse(body);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// dispatch responses to pending requests, ignore everything else
|
|
120
|
+
if (message.id != null) {
|
|
121
|
+
const entry = pending.get(message.id);
|
|
122
|
+
if (entry) {
|
|
123
|
+
pending.delete(message.id);
|
|
124
|
+
if (message.error) {
|
|
125
|
+
entry.reject(new Error(`LSP error ${message.error.code}: ${message.error.message}`));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
entry.resolve(message.result);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
child.stdout.on('data', (chunk) => {
|
|
135
|
+
buffer = buffer.length > 0 ? Buffer.concat([buffer, chunk]) : chunk;
|
|
136
|
+
processBuffer();
|
|
137
|
+
});
|
|
138
|
+
const rejectPending = (error) => {
|
|
139
|
+
for (const [, entry] of pending) {
|
|
140
|
+
entry.reject(error);
|
|
141
|
+
}
|
|
142
|
+
pending.clear();
|
|
143
|
+
};
|
|
144
|
+
child.on('error', (err) => {
|
|
145
|
+
exited = true;
|
|
146
|
+
rejectPending(err);
|
|
147
|
+
});
|
|
148
|
+
child.on('close', (exitCode) => {
|
|
149
|
+
exited = true;
|
|
150
|
+
rejectPending(new Error(`LSP server exited unexpectedly with code ${exitCode}`));
|
|
151
|
+
});
|
|
152
|
+
// #endregion
|
|
153
|
+
// #region initialize handshake
|
|
154
|
+
const rootUri = url.pathToFileURL(root).href;
|
|
155
|
+
await sendRequest('initialize', {
|
|
156
|
+
processId: process.pid,
|
|
157
|
+
clientInfo: { name: 'lex-cli' },
|
|
158
|
+
rootUri,
|
|
159
|
+
capabilities: {},
|
|
160
|
+
});
|
|
161
|
+
sendNotification('initialized');
|
|
162
|
+
// #endregion
|
|
163
|
+
return {
|
|
164
|
+
async formatDocument(code, filepath) {
|
|
165
|
+
const uri = url.pathToFileURL(filepath).href;
|
|
166
|
+
const languageId = inferLanguageId(filepath);
|
|
167
|
+
sendNotification('textDocument/didOpen', {
|
|
168
|
+
textDocument: { uri, languageId, version: 1, text: code },
|
|
169
|
+
});
|
|
170
|
+
const edits = (await sendRequest('textDocument/formatting', {
|
|
171
|
+
textDocument: { uri },
|
|
172
|
+
options: { tabSize: 2, insertSpaces: false },
|
|
173
|
+
}));
|
|
174
|
+
sendNotification('textDocument/didClose', {
|
|
175
|
+
textDocument: { uri },
|
|
176
|
+
});
|
|
177
|
+
if (!edits || edits.length === 0) {
|
|
178
|
+
return code;
|
|
179
|
+
}
|
|
180
|
+
return applyTextEdits(code, edits);
|
|
181
|
+
},
|
|
182
|
+
async dispose() {
|
|
183
|
+
if (!exited) {
|
|
184
|
+
await sendRequest('shutdown', null);
|
|
185
|
+
sendNotification('exit');
|
|
186
|
+
}
|
|
187
|
+
if (!exited) {
|
|
188
|
+
child.kill();
|
|
189
|
+
await new Promise((resolve) => child.on('close', resolve));
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=lsp-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lsp-client.js","sourceRoot":"","sources":["../src/lsp-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atcute/lex-cli",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "cli tool to generate type definitions for atcute",
|
|
5
5
|
"license": "0BSD",
|
|
6
6
|
"repository": {
|
|
@@ -31,13 +31,14 @@
|
|
|
31
31
|
"prettier": "^3.8.1",
|
|
32
32
|
"@atcute/identity": "^1.1.4",
|
|
33
33
|
"@atcute/identity-resolver": "^1.2.2",
|
|
34
|
+
"@atcute/lexicons": "^1.2.10",
|
|
34
35
|
"@atcute/lexicon-doc": "^2.1.2",
|
|
35
|
-
"@atcute/lexicon-resolver": "^0.1.6"
|
|
36
|
-
"@atcute/lexicons": "^1.2.9"
|
|
36
|
+
"@atcute/lexicon-resolver": "^0.1.6"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^25.5.2",
|
|
40
|
-
"tschema": "^3.2.0"
|
|
40
|
+
"tschema": "^3.2.0",
|
|
41
|
+
"@atcute/uint8array": "^1.1.1"
|
|
41
42
|
},
|
|
42
43
|
"scripts": {
|
|
43
44
|
"build": "pnpm run generate:schema && tsgo",
|
package/src/commands/export.ts
CHANGED
|
@@ -73,23 +73,27 @@ export const runExport = async (args: ExportCommand): Promise<void> => {
|
|
|
73
73
|
const outdir = path.resolve(config.root, exportConfig.outdir);
|
|
74
74
|
const formatter = await createFormatter(config.formatter, config.root);
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
try {
|
|
77
|
+
// load lexicons from files
|
|
78
|
+
const loaded = await loadLexicons(files, config.root);
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
if (loaded.length === 0) {
|
|
81
|
+
console.warn(pc.yellow(`warning: no lexicons found to export`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
// clean output directory if requested
|
|
86
|
+
if (exportConfig.clean) {
|
|
87
|
+
await fs.rm(outdir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
await fs.mkdir(outdir, { recursive: true });
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
// write each lexicon as JSON
|
|
93
|
+
await Promise.all(loaded.map(({ nsid, doc }) => writeLexicon(outdir, nsid, doc, formatter)));
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
console.log(pc.green(`exported ${loaded.length} lexicon(s) to ${outdir}`));
|
|
96
|
+
} finally {
|
|
97
|
+
await formatter.dispose();
|
|
98
|
+
}
|
|
95
99
|
};
|
package/src/commands/generate.ts
CHANGED
|
@@ -150,25 +150,30 @@ export const runGenerate = async (args: GenerateCommand): Promise<void> => {
|
|
|
150
150
|
|
|
151
151
|
const outdir = path.join(config.root, config.outdir);
|
|
152
152
|
const formatter = await createFormatter(config.formatter, config.root);
|
|
153
|
-
const pending: Promise<void>[] = [];
|
|
154
|
-
|
|
155
|
-
for (const file of generateLexiconApi({
|
|
156
|
-
documents: documents,
|
|
157
|
-
mappings: allMappings,
|
|
158
|
-
modules: {
|
|
159
|
-
importSuffix: config.modules?.importSuffix ?? '.js',
|
|
160
|
-
},
|
|
161
|
-
})) {
|
|
162
|
-
const filename = path.join(outdir, file.filename);
|
|
163
|
-
|
|
164
|
-
pending.push(
|
|
165
|
-
(async () => {
|
|
166
|
-
const formatted = await formatter.format(file.code, filename);
|
|
167
|
-
await fs.mkdir(path.dirname(filename), { recursive: true });
|
|
168
|
-
await fs.writeFile(filename, formatted);
|
|
169
|
-
})(),
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
153
|
|
|
173
|
-
|
|
154
|
+
try {
|
|
155
|
+
const pending: Promise<void>[] = [];
|
|
156
|
+
|
|
157
|
+
for (const file of generateLexiconApi({
|
|
158
|
+
documents: documents,
|
|
159
|
+
mappings: allMappings,
|
|
160
|
+
modules: {
|
|
161
|
+
importSuffix: config.modules?.importSuffix ?? '.js',
|
|
162
|
+
},
|
|
163
|
+
})) {
|
|
164
|
+
const filename = path.join(outdir, file.filename);
|
|
165
|
+
|
|
166
|
+
pending.push(
|
|
167
|
+
(async () => {
|
|
168
|
+
const formatted = await formatter.format(file.code, filename);
|
|
169
|
+
await fs.mkdir(path.dirname(filename), { recursive: true });
|
|
170
|
+
await fs.writeFile(filename, formatted);
|
|
171
|
+
})(),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await Promise.all(pending);
|
|
176
|
+
} finally {
|
|
177
|
+
await formatter.dispose();
|
|
178
|
+
}
|
|
174
179
|
};
|
package/src/commands/pull.ts
CHANGED
|
@@ -187,40 +187,44 @@ export const runPull = async (args: PullCommand): Promise<void> => {
|
|
|
187
187
|
const outdir = path.resolve(config.root, pullConfig.outdir);
|
|
188
188
|
const formatter = await createFormatter(config.formatter, config.root);
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
190
|
+
try {
|
|
191
|
+
const seen = new Map<string, SourceLocation>();
|
|
192
|
+
const collected: PulledLexicon[] = [];
|
|
193
|
+
const sourceRevisions: SourceRevision[] = [];
|
|
193
194
|
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
for (const source of pullConfig.sources) {
|
|
196
|
+
const result = await pullSource(source);
|
|
196
197
|
|
|
197
|
-
|
|
198
|
+
sourceRevisions.push({ source, rev: result.rev });
|
|
198
199
|
|
|
199
|
-
|
|
200
|
-
|
|
200
|
+
for (const [nsid, entry] of result.pulled) {
|
|
201
|
+
const existing = seen.get(nsid);
|
|
201
202
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
203
|
+
if (existing) {
|
|
204
|
+
console.error(pc.bold(pc.red(`duplicate lexicon "${nsid}"`)));
|
|
205
|
+
console.error(`- found ${entry.location.relativePath} from ${entry.location.sourceDescription}`);
|
|
206
|
+
console.error(` at ${entry.location.absolutePath}`);
|
|
207
|
+
console.error(`- already found ${existing.relativePath} from ${existing.sourceDescription}`);
|
|
208
|
+
console.error(` at ${existing.absolutePath}`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
210
211
|
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
seen.set(nsid, entry.location);
|
|
213
|
+
collected.push(entry);
|
|
214
|
+
}
|
|
213
215
|
}
|
|
214
|
-
}
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
if (pullConfig.clean) {
|
|
218
|
+
await fs.rm(outdir, { recursive: true, force: true });
|
|
219
|
+
}
|
|
219
220
|
|
|
220
|
-
|
|
221
|
+
await fs.mkdir(outdir, { recursive: true });
|
|
221
222
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
await Promise.all([
|
|
224
|
+
...collected.map((entry) => writeLexicon(outdir, entry.nsid, entry.doc, formatter)),
|
|
225
|
+
writeSourceReadme(outdir, sourceRevisions, formatter),
|
|
226
|
+
]);
|
|
227
|
+
} finally {
|
|
228
|
+
await formatter.dispose();
|
|
229
|
+
}
|
|
226
230
|
};
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
|
-
import { availableParallelism } from 'node:os';
|
|
3
2
|
import * as path from 'node:path';
|
|
4
3
|
import * as url from 'node:url';
|
|
5
4
|
|
|
@@ -72,7 +71,11 @@ const formatterConfigSchema = v.union(
|
|
|
72
71
|
concurrency: v
|
|
73
72
|
.number()
|
|
74
73
|
.assert((value) => Number.isInteger(value) && value > 0, `must be a positive integer`)
|
|
75
|
-
.optional(() =>
|
|
74
|
+
.optional(() => 1),
|
|
75
|
+
}),
|
|
76
|
+
v.object({
|
|
77
|
+
type: v.literal('lsp'),
|
|
78
|
+
command: v.string().assert((value) => value.length > 0, `must not be empty`),
|
|
76
79
|
}),
|
|
77
80
|
);
|
|
78
81
|
|
package/src/formatter.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { availableParallelism } from 'node:os';
|
|
3
2
|
|
|
4
3
|
import type { FormatterConfig } from './config.ts';
|
|
4
|
+
import { createLspClient } from './lsp-client.ts';
|
|
5
5
|
|
|
6
6
|
/** formats source code */
|
|
7
7
|
export interface Formatter {
|
|
@@ -12,6 +12,8 @@ export interface Formatter {
|
|
|
12
12
|
* @returns formatted code
|
|
13
13
|
*/
|
|
14
14
|
format(code: string, filepath: string): Promise<string>;
|
|
15
|
+
/** releases any resources held by the formatter */
|
|
16
|
+
dispose(): Promise<void>;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
const inferPrettierParser = (filepath: string): string => {
|
|
@@ -74,72 +76,82 @@ class Semaphore {
|
|
|
74
76
|
* @returns a formatter instance
|
|
75
77
|
*/
|
|
76
78
|
export const createFormatter = async (config: FormatterConfig, root: string): Promise<Formatter> => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
79
|
+
switch (config.type) {
|
|
80
|
+
case 'prettier': {
|
|
81
|
+
const prettier = await import('prettier');
|
|
82
|
+
const prettierConfig = await prettier.resolveConfig(root, { editorconfig: true });
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
async format(code, filepath) {
|
|
86
|
+
return prettier.format(code, { ...prettierConfig, parser: inferPrettierParser(filepath) });
|
|
87
|
+
},
|
|
88
|
+
async dispose() {},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
case 'command': {
|
|
92
|
+
// the template uses {filepath} as a placeholder, which is passed as a
|
|
93
|
+
// positional argument to sh to avoid shell injection via filenames
|
|
94
|
+
const shellCmd = config.command.replaceAll('{filepath}', '"$1"');
|
|
95
|
+
const semaphore = new Semaphore(config.concurrency);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
async format(code, filepath) {
|
|
99
|
+
const lock = await semaphore.acquire();
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
return await new Promise<string>((resolve, reject) => {
|
|
103
|
+
const child = spawn('sh', ['-c', shellCmd, 'sh', filepath], {
|
|
104
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const stdoutChunks: Buffer[] = [];
|
|
108
|
+
const stderrChunks: Buffer[] = [];
|
|
109
|
+
|
|
110
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
111
|
+
stdoutChunks.push(chunk);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
115
|
+
stderrChunks.push(chunk);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
child.on('error', reject);
|
|
119
|
+
|
|
120
|
+
child.on('close', (exitCode: number | null) => {
|
|
121
|
+
if (exitCode !== 0) {
|
|
122
|
+
const stderr = Buffer.concat(stderrChunks).toString();
|
|
123
|
+
reject(new Error(`formatter exited with code ${exitCode}:\n${stderr}`));
|
|
124
|
+
} else {
|
|
125
|
+
resolve(Buffer.concat(stdoutChunks).toString());
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
child.stdin.end(code);
|
|
130
|
+
});
|
|
131
|
+
} finally {
|
|
132
|
+
lock.release();
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
async dispose() {},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
case 'lsp': {
|
|
139
|
+
const client = await createLspClient(config.command, root);
|
|
140
|
+
const semaphore = new Semaphore(1);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
async format(code, filepath) {
|
|
144
|
+
const lock = await semaphore.acquire();
|
|
145
|
+
try {
|
|
146
|
+
return await client.formatDocument(code, filepath);
|
|
147
|
+
} finally {
|
|
148
|
+
lock.release();
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
async dispose() {
|
|
152
|
+
await client.dispose();
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
131
156
|
}
|
|
132
|
-
|
|
133
|
-
const semaphore = new Semaphore(concurrency);
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
async format(code, filepath) {
|
|
137
|
-
const lock = await semaphore.acquire();
|
|
138
|
-
try {
|
|
139
|
-
return await inner.format(code, filepath);
|
|
140
|
-
} finally {
|
|
141
|
-
lock.release();
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
157
|
};
|