testMirrorWritesBack
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/); }