prettier_server.js

  1const { Buffer } = require("buffer");
  2const fs = require("fs");
  3const path = require("path");
  4const { once } = require("events");
  5
  6const prettierContainerPath = process.argv[2];
  7if (prettierContainerPath == null || prettierContainerPath.length == 0) {
  8  process.stderr.write(
  9    `Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`,
 10  );
 11  process.exit(1);
 12}
 13fs.stat(prettierContainerPath, (err, stats) => {
 14  if (err) {
 15    process.stderr.write(`Path '${prettierContainerPath}' does not exist\n`);
 16    process.exit(1);
 17  }
 18
 19  if (!stats.isDirectory()) {
 20    process.stderr.write(
 21      `Path '${prettierContainerPath}' exists but is not a directory\n`,
 22    );
 23    process.exit(1);
 24  }
 25});
 26const prettierPath = path.join(prettierContainerPath, "node_modules/prettier");
 27
 28class Prettier {
 29  constructor(path, prettier, config) {
 30    this.path = path;
 31    this.prettier = prettier;
 32    this.config = config;
 33  }
 34}
 35
 36(async () => {
 37  let prettier;
 38  let config;
 39  try {
 40    prettier = await loadPrettier(prettierPath);
 41    config = (await prettier.resolveConfig(prettierPath)) || {};
 42  } catch (e) {
 43    process.stderr.write(`Failed to load prettier: ${e}\n`);
 44    process.exit(1);
 45  }
 46  process.stderr.write(
 47    `Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`,
 48  );
 49  process.stdin.resume();
 50  handleBuffer(new Prettier(prettierPath, prettier, config));
 51})();
 52
 53async function handleBuffer(prettier) {
 54  for await (const messageText of readStdin()) {
 55    let message;
 56    try {
 57      message = JSON.parse(messageText);
 58    } catch (e) {
 59      sendResponse(makeError(`Failed to parse message '${messageText}': ${e}`));
 60      continue;
 61    }
 62    // allow concurrent request handling by not `await`ing the message handling promise (async function)
 63    handleMessage(message, prettier).catch((e) => {
 64      const errorMessage = message;
 65      if ((errorMessage.params || {}).text !== undefined) {
 66        errorMessage.params.text = "..snip..";
 67      }
 68      sendResponse({
 69        id: message.id,
 70        ...makeError(
 71          `error during message '${JSON.stringify(errorMessage)}' handling: ${e}`,
 72        ),
 73      });
 74    });
 75  }
 76}
 77
 78const headerSeparator = "\r\n";
 79const contentLengthHeaderName = "Content-Length";
 80
 81async function* readStdin() {
 82  let buffer = Buffer.alloc(0);
 83  let streamEnded = false;
 84  process.stdin.on("end", () => {
 85    streamEnded = true;
 86  });
 87  process.stdin.on("data", (data) => {
 88    buffer = Buffer.concat([buffer, data]);
 89  });
 90
 91  async function handleStreamEnded(errorMessage) {
 92    sendResponse(makeError(errorMessage));
 93    buffer = Buffer.alloc(0);
 94    messageLength = null;
 95    await once(process.stdin, "readable");
 96    streamEnded = false;
 97  }
 98
 99  try {
100    let headersLength = null;
101    let messageLength = null;
102    main_loop: while (true) {
103      if (messageLength === null) {
104        while (buffer.indexOf(`${headerSeparator}${headerSeparator}`) === -1) {
105          if (streamEnded) {
106            await handleStreamEnded(
107              "Unexpected end of stream: headers not found",
108            );
109            continue main_loop;
110          } else if (buffer.length > contentLengthHeaderName.length * 10) {
111            await handleStreamEnded(
112              `Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`,
113            );
114            continue main_loop;
115          }
116          await once(process.stdin, "readable");
117        }
118        const headers = buffer
119          .subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`))
120          .toString("ascii");
121        const contentLengthHeader = headers
122          .split(headerSeparator)
123          .map((header) => header.split(":"))
124          .filter((header) => header[2] === undefined)
125          .filter((header) => (header[1] || "").length > 0)
126          .find(
127            (header) => (header[0] || "").trim() === contentLengthHeaderName,
128          );
129        const contentLength = (contentLengthHeader || [])[1];
130        if (contentLength === undefined) {
131          await handleStreamEnded(
132            `Missing or incorrect ${contentLengthHeaderName} header: ${headers}`,
133          );
134          continue main_loop;
135        }
136        headersLength = headers.length + headerSeparator.length * 2;
137        messageLength = parseInt(contentLength, 10);
138      }
139
140      while (buffer.length < headersLength + messageLength) {
141        if (streamEnded) {
142          await handleStreamEnded(
143            `Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`,
144          );
145          continue main_loop;
146        }
147        await once(process.stdin, "readable");
148      }
149
150      const messageEnd = headersLength + messageLength;
151      const message = buffer.subarray(headersLength, messageEnd);
152      buffer = buffer.subarray(messageEnd);
153      headersLength = null;
154      messageLength = null;
155      yield message.toString("utf8");
156    }
157  } catch (e) {
158    sendResponse(makeError(`Error reading stdin: ${e}`));
159  } finally {
160    process.stdin.off("data", () => {});
161  }
162}
163
164async function handleMessage(message, prettier) {
165  const { method, id, params } = message;
166  if (method === undefined) {
167    throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
168  } else if (method == "initialized") {
169    return;
170  }
171
172  if (id === undefined) {
173    throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
174  }
175
176  if (method === "prettier/format") {
177    if (params === undefined || params.text === undefined) {
178      throw new Error(
179        `Message params.text is undefined: ${JSON.stringify(message)}`,
180      );
181    }
182    if (params.options === undefined) {
183      throw new Error(
184        `Message params.options is undefined: ${JSON.stringify(message)}`,
185      );
186    }
187
188    let resolvedConfig = {};
189    if (params.options.filepath) {
190      resolvedConfig =
191        (await prettier.prettier.resolveConfig(params.options.filepath)) || {};
192    }
193
194    // Marking the params.options.filepath as undefined makes
195    // prettier.format() work even if no filepath is set.
196    if (params.options.filepath === null) {
197      params.options.filepath = undefined;
198    }
199
200    const plugins =
201      Array.isArray(resolvedConfig?.plugins) &&
202      resolvedConfig.plugins.length > 0
203        ? resolvedConfig.plugins
204        : params.options.plugins;
205
206    const options = {
207      ...(params.options.prettierOptions || prettier.config),
208      ...resolvedConfig,
209      plugins,
210      parser: params.options.parser,
211      filepath: params.options.filepath,
212    };
213    process.stderr.write(
214      `Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${
215        params.options.filepath || ""
216      }' with options: ${JSON.stringify(options)}\n`,
217    );
218    const formattedText = await prettier.prettier.format(params.text, options);
219    sendResponse({ id, result: { text: formattedText } });
220  } else if (method === "prettier/clear_cache") {
221    prettier.prettier.clearConfigCache();
222    prettier.config =
223      (await prettier.prettier.resolveConfig(prettier.path)) || {};
224    sendResponse({ id, result: null });
225  } else if (method === "initialize") {
226    sendResponse({
227      id,
228      result: {
229        capabilities: {},
230      },
231    });
232  } else {
233    throw new Error(`Unknown method: ${method}`);
234  }
235}
236
237function makeError(message) {
238  return {
239    error: {
240      code: -32600, // invalid request code
241      message,
242    },
243  };
244}
245
246function sendResponse(response) {
247  const responsePayloadString = JSON.stringify({
248    jsonrpc: "2.0",
249    ...response,
250  });
251  const headers = `${contentLengthHeaderName}: ${Buffer.byteLength(
252    responsePayloadString,
253  )}${headerSeparator}${headerSeparator}`;
254  process.stdout.write(headers + responsePayloadString);
255}
256
257function loadPrettier(prettierPath) {
258  return new Promise((resolve, reject) => {
259    fs.access(prettierPath, fs.constants.F_OK, (err) => {
260      if (err) {
261        reject(`Path '${prettierPath}' does not exist.Error: ${err}`);
262      } else {
263        try {
264          resolve(require(prettierPath));
265        } catch (err) {
266          reject(
267            `Error requiring prettier module from path '${prettierPath}'.Error: ${err}`,
268          );
269        }
270      }
271    });
272  });
273}