Skip to content

Instantly share code, notes, and snippets.

@weswigham
Created June 6, 2019 00:26
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save weswigham/f477373b6124cc43df5356c75f66911b to your computer and use it in GitHub Desktop.
Save weswigham/f477373b6124cc43df5356c75f66911b to your computer and use it in GitHub Desktop.
Quick and dirty gist for using the compiler API to apply all available import fixes for a project
// Usage: node ./apply-all-import-fixes.js ./path/to/index.ts
import * as ts from "typescript/lib/tsserverlibrary";
import * as fs from "fs";
import * as path from "path";
class Logger implements ts.server.Logger { // tslint:disable-line no-unnecessary-qualifier
private fd = -1;
private seq = 0;
private inGroup = false;
private firstInGroup = true;
constructor(private readonly logFilename: string,
private readonly traceToConsole: boolean,
private readonly level: ts.server.LogLevel) {
if (this.logFilename) {
try {
this.fd = fs.openSync(this.logFilename, "w");
}
catch (_) {
// swallow the error and keep logging disabled if file cannot be opened
}
}
}
static padStringRight(str: string, padding: string) {
return (str + padding).slice(0, padding.length);
}
close() {
if (this.fd >= 0) {
fs.close(this.fd, () => void 0);
}
}
getLogFileName() {
return this.logFilename;
}
perftrc(s: string) {
this.msg(s, ts.server.Msg.Perf);
}
info(s: string) {
this.msg(s, ts.server.Msg.Info);
}
err(s: string) {
this.msg(s, ts.server.Msg.Err);
}
startGroup() {
this.inGroup = true;
this.firstInGroup = true;
}
endGroup() {
this.inGroup = false;
}
loggingEnabled() {
return !!this.logFilename || this.traceToConsole;
}
hasLevel(level: ts.server.LogLevel) {
return this.loggingEnabled() && this.level >= level;
}
msg(s: string, type: ts.server.Msg = ts.server.Msg.Err) {
if (!this.canWrite) return;
s = `[${new Date().getUTCDate()}] ${s}\n`;
if (!this.inGroup || this.firstInGroup) {
const prefix = Logger.padStringRight(type + " " + this.seq.toString(), " ");
s = prefix + s;
}
this.write(s);
if (!this.inGroup) {
this.seq++;
}
}
private get canWrite() {
return this.fd >= 0 || this.traceToConsole;
}
private write(s: string) {
if (this.fd >= 0) {
const buf = Buffer.from(s);
// tslint:disable-next-line no-null-keyword
fs.writeSync(this.fd, buf, 0, buf.length, /*position*/ null!); // TODO: GH#18217
}
if (this.traceToConsole) {
console.warn(s);
}
}
}
const projectService = new ts.server.ProjectService({
host: ts.sys as ts.server.ServerHost,
typingsInstaller: ts.server.nullTypingsInstaller,
logger: new Logger("./log.log", true, ts.server.LogLevel.normal),
cancellationToken: ts.server.nullCancellationToken,
useInferredProjectPerProjectRoot: true,
useSingleInferredProject: false
});
projectService.openClientFile(path.join(process.cwd(), process.argv[2]));
const lshost = projectService.configuredProjects.values().next().value;
const ls = lshost.getLanguageService();
const formatSettings = ts.getDefaultFormatCodeSettings("\n");
for (const file of ls.getProgram().getSourceFiles()) {
const diags = ls.getSemanticDiagnostics(file.fileName);
const allfixes: ts.FileTextChanges[] = [];
for (const diag of diags) {
if (diag.code !== 2304) continue; // Code for "cannot find name" errors
const fixes = ls.getCodeFixesAtPosition(file.fileName, diag.start, diag.start + diag.length, [diag.code], formatSettings, { quotePreference: "double" });
const fix = fixes[0];
allfixes.push(...fix.changes);
}
const newFileContent = applyEdits(file.text, file.fileName, allfixes);
fs.writeFileSync(file.fileName, newFileContent);
}
function applyEdits(text: string, textFilename: string, edits: ReadonlyArray<ts.FileTextChanges>): string {
for (const { fileName, textChanges } of edits) {
if (fileName !== textFilename) {
continue;
}
for (let i = textChanges.length - 1; i >= 0; i--) {
const { newText, span: { start, length } } = textChanges[i];
text = text.slice(0, start) + newText + text.slice(start + length);
}
}
return text;
}
@karlhorky
Copy link

Thanks for this @weswigham, this is great!

I'm trying to do something similar - I would like to write a command line program to call the SQL formatting functionality in typescript-sql-tagged-template-plugin (when SQL is in inline tagged template literals).

I tried combining your program here with some code to run getFormattingEditsForDocument, but it doesn't seem to be running the corresponding getFormattingEditsFor methods in the plugin 🤔 :

https://github.com/karlhorky/ts-language-service-plugin-formatting-cli

Is it maybe related to the following comment from this Stack Overflow question:

You have to set serverHost.require or else the service can't load plugins and they'll be skipped. I copied TS's internal implementation from src/server/server.ts

I'm not completely versed with TS internals, so I don't know what I should do with this information, but I guess if it seems like a good lead, I can investigate further...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment