testMirrorWritesBack

tcmts.cli.test.e2e.mirror.testMirrorWritesBack · #xjiqghbg · term

a mirror: edits, additions, removals and renames made in tcm are written back to the files

import { okIn, plainEnv, tcmBin, tempDir } from "#codebase/tcmts/cli/test/e2e/harness";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";

export const testMirrorWritesBack = () => {
  const root = tempDir("mirror");
  const project = path.join(root, "proj");
  const dir = path.join(root, "cb");
  const tcm = (...args: string[]) => okIn(dir, ...args);
  const put = (file: string, text: string) => {
    fs.mkdirSync(path.dirname(path.join(project, file)), { recursive: true });
    fs.writeFileSync(path.join(project, file), text);
  };
  const read = (file: string) => fs.readFileSync(path.join(project, file), "utf8");
  put("package.json", JSON.stringify({ type: "module" }));
  put(
    "src/math.ts",
    `// Arithmetic, as the rest of the project needs it.\nimport fs from "node:fs";\n\n/** Twice \`x\`. */\nexport function double(x: number): number {\n  return x * 2;\n}\n\n// a constant nobody else uses\nconst SCALE = 3;\n\nexport const triple = (x: number): number => x * SCALE;\nexport const exists = (p: string) => fs.existsSync(p);\nexport const unused = 1;\n`,
  );
  put("src/use.ts", `import { double, triple } from "./math.ts";\nimport * as math from "./math.ts";\n\nexport const quad = (x: number) => double(double(x));\nexport const both = (x: number) => ({ double, t: triple(x), via: math.double(x) });\n`);
  fs.mkdirSync(dir);
  tcm("init");
  fs.rmSync(path.join(dir, "scratch.ts"));
  assert.match(tcm("import", project, "--into", "calc", "--mirror"), /written back to/);
  assert.match(tcm("mirror", "--check"), /the files hash to what the codebase holds/);

  // an edit made through a scratch file replaces the declaration where it is; the rest of the file is untouched
  tcm("edit", "calc.math.double");
  const scratch = path.join(dir, "scratch.ts");
  fs.writeFileSync(scratch, fs.readFileSync(scratch, "utf8").replace("return x * 2;", "return x + x;"));
  assert.match(tcm("update"), /mirror: wrote src\/math\.ts/);
  const edited = read("src/math.ts");
  assert.match(edited, /return x \+ x;/);
  assert.match(edited, /^\/\/ Arithmetic, as the rest of the project needs it\.\nimport fs from "node:fs";/, "comments and imports stay as written");
  assert.match(edited, /\/\/ a constant nobody else uses\nconst SCALE = 3;/);
  assert.equal(read("src/use.ts").includes("double(double(x))"), true, "dependents re-hashed by the update keep their text");

  // a rename is made at every reference: named imports, namespace imports, shorthand properties
  tcm("mv", "calc.math.double", "calc.math.twice");
  assert.match(read("src/math.ts"), /export function twice\(x: number\): number \{/);
  assert.equal(read("src/use.ts"), `import { twice, triple } from "./math.ts";\nimport * as math from "./math.ts";\n\nexport const quad = (x: number) => twice(twice(x));\nexport const both = (x: number) => ({ double: twice, t: triple(x), via: math.twice(x) });\n`);

  // a new definition goes to its namespace's file, with what it uses imported; a removed one is deleted
  fs.writeFileSync(scratch, `// tcm: namespace calc.use\nimport { triple } from "#codebase/calc/math";\nexport const nine = () => triple(3);\n`);
  tcm("add");
  assert.match(read("src/use.ts"), /\nexport const nine = \(\) => triple\(3\);\n$/);
  tcm("rm", "calc.math.unused");
  assert.doesNotMatch(read("src/math.ts"), /unused/);
  assert.match(tcm("mirror", "--check"), /the files hash to what the codebase holds/);

  // files changed outside tcm (by hand, a git pull or checkout) are imported before the next command
  put("src/use.ts", read("src/use.ts") + "export const ten = () => 10;\n");
  fs.writeFileSync(scratch, `// tcm: namespace calc.math\nexport const half = (x: number) => x / 2;\n`);
  assert.match(tcm("add"), /mirror: src\/use\.ts changed outside tcm; importing it[\s\S]*\+ new 1/);
  assert.match(tcm("ls", "calc.use"), /calc\.use\.ten/, "the edit is in the codebase");
  assert.match(read("src/math.ts"), /export const half/, "and the command's change is in the files");
  // a definition gone from the files goes from the codebase
  put("src/use.ts", read("src/use.ts").replace("export const ten = () => 10;\n", ""));
  const gone = tcm("ls", "calc.use");
  assert.match(gone, /- removed +calc\.use\.ten \(gone from the files\)/);
  assert.doesNotMatch(gone, /^calc\.use\.ten/m);
  // files that don't import yet (an edit half made) wait, and so does writing back to them; what the codebase
  // changed meanwhile is written once they import
  put("src/use.ts", read("src/use.ts") + "export const eleven = () => 11 +;\n");
  fs.writeFileSync(scratch, `// tcm: namespace calc.math\nexport const halve = (x: number) => x / 2;\n`);
  const waiting = tcm("add");
  assert.match(waiting, /TS1109: Expression expected[\s\S]*nothing was imported/);
  assert.match(waiting, /mirror: src\/use\.ts changed outside tcm and doesn't import yet/);
  assert.doesNotMatch(read("src/math.ts"), /halve/);
  fs.writeFileSync(scratch, `// tcm: namespace calc.use\nexport const nine = () => 9;\n`);
  tcm("update");
  put("src/use.ts", read("src/use.ts").replace("11 +;", "11;"));
  assert.match(tcm("ls", "calc.use"), /calc\.use\.eleven/);
  assert.match(read("src/math.ts"), /export const halve/, "write-back resumes");
  assert.match(read("src/use.ts"), /export const nine = \(\) => 9;\nexport const eleven = \(\) => 11;\n$/, "with the codebase's change to a definition the files hadn't changed");
  assert.match(tcm("mirror", "--check"), /the files hash to what the codebase holds/);
  // what importing them says goes to stderr: a command's output is its own
  put("src/use.ts", read("src/use.ts").replace("() => 11", "() => 12"));
  const ran = spawnSync(process.execPath, [tcmBin(), "run", "calc.use.eleven"], { cwd: dir, encoding: "utf8", env: plainEnv() });
  assert.equal(ran.stdout, "12\n");
  assert.match(ran.stderr, /mirror: src\/use\.ts changed outside tcm; importing it/);

  // an import a change stops using goes with it
  assert.match(read("src/math.ts"), /^import fs from "node:fs";$/m);
  tcm("edit", "--force", "calc.math.exists");
  fs.writeFileSync(scratch, fs.readFileSync(scratch, "utf8").replace("fs.existsSync(p)", "p.length > 0"));
  tcm("update");
  assert.doesNotMatch(read("src/math.ts"), /node:fs/);
  assert.match(read("src/math.ts"), /^\/\/ Arithmetic, as the rest of the project needs it\.\n\n/, "and only it");
  assert.match(tcm("mirror", "--check"), /the files hash to what the codebase holds/);
}

uses