codemods/selectors/edit.selector.ts
codemods/selectors/edit.selector.ts is a file in GovLab Extension Host. 68 lines of code and 15 definitions.
import type { Edit } from "../../types/codemod.types.ts";
import { extname } from "node:path";
import { readFileSync } from "node:fs";
import { syntaxRefusal } from "../strings/codemod.strings.ts";
import ts from "typescript";
import { writeVerbatim } from "@govlab/canonical-write";
const JSON_EXTENSION = ".json";
export const parsesAsJson = function parsesAsJson(content: string): boolean {
try {
JSON.parse(content);
return true;
} catch (error) {
if (error instanceof SyntaxError) {
return false;
}
throw error;
}
};
const overlaps = function overlaps(left: Edit, right: Edit): boolean {
return left.start < right.end && right.start < left.end;
};
export const disjoint = function disjoint(edits: readonly Edit[]): Edit[] {
const kept: Edit[] = [];
for (const edit of edits) {
if (!kept.some((other) => overlaps(edit, other))) {
kept.push(edit);
}
}
return kept;
};
const syntaxErrors = function syntaxErrors(target: string, content: string): number {
if (extname(target) === JSON_EXTENSION) {
return parsesAsJson(content) ? 0 : 1;
}
const result = ts.transpileModule(content, {
compilerOptions: { target: ts.ScriptTarget.ESNext },
fileName: target,
reportDiagnostics: true,
});
return (result.diagnostics ?? []).length;
};
export const splice = function splice(content: string, edits: readonly Edit[]): string {
let out = content;
for (const edit of edits) {
out = out.slice(0, edit.start) + edit.replacement + out.slice(edit.end);
}
return out;
};
export const applyEdits = function applyEdits(fileName: string, edits: readonly Edit[]): number {
if (edits.length === 0) {
return 0;
}
const ordered = disjoint([...edits].sort((a, b) => b.start - a.start));
const original = readFileSync(fileName, "utf8");
const next = splice(original, ordered);
if (syntaxErrors(fileName, next) > syntaxErrors(fileName, original)) {
throw new Error(syntaxRefusal(fileName));
}
writeVerbatim(fileName, next);
return ordered.length;
};
export const applyByFile = function applyByFile(byFile: ReadonlyMap<string, Edit[]>): number {
let applied = 0;
for (const [fileName, edits] of byFile) {
applied += applyEdits(fileName, edits);
}
return applied;
};