save

tcmts.cli.main.save · #jpu6xqae · term · runs on the server

uses node:path

Save a scratch file (tcm add, tcm update). Its history entry is named by what it changes (update lib.f, lib.g (+3); a conflict it settles: resolve lib.k (merged)), or how.command; how.mates are paths it holds only as the rest of a cycle, saved as they are. A definition how.respell has names for (by path) is stored with its references written so (see respelled); every definition is stored as it reads alone (spelledAlone), whatever names the file had to give what it uses. A function or class declared find_1 where the codebase has it as find isn't saved: its name is part of its hash.

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { LINT_CONFIG } from "#codebase/tcmts/cli/lint";
import {
  apply, changedCode, cycleWarnings, entryName, fileRows, formattedRows, keptAs, printScratchLint, respelled,
  spelledAlone, textChanged, withTheirCycles,
} from "#codebase/tcmts/cli/main";
import type { Row } from "#codebase/tcmts/cli/main";
import { conflictMarkers, resolvedEntry } from "#codebase/tcmts/cli/merge";
import { red, yellow } from "#codebase/tcmts/cli/style";
import fs from "node:fs";
import path_1 from "node:path";

export function save(
  cb: Codebase,
  file: string,
  mode: "add" | "update",
  into?: string,
  how: {
    command?: string;
    mates?: ReadonlySet<string>;
    respell?: ReadonlyMap<string, ReadonlyMap<string, string>>;
  } = {},
): boolean {
  // (saved over a conflicted name, it resolves it: the entry says so, and how)
  const command = (rows: Row[]) => {
    if (how.command) return how.command;
    const named = rows.filter(
      (r) =>
        !r.mate &&
        r.status !== "identical" &&
        (mode === "update" || (r.status !== "update" && r.status !== "conflict")),
    );
    const resolved = named.filter((r) => r.status === "conflict");
    const rest = named.filter((r) => r.status !== "conflict").map((r) => r.path);
    if (!resolved.length) return entryName(mode, rest, `${mode} ${path_1.basename(file)}`);
    return `${resolvedEntry(
      cb,
      resolved.map((r) => ({ path: r.path, kind: r.def.kind, hash: r.def.hash })),
    )}${rest.length ? `; ${entryName(mode, rest)}` : ""}`;
  };
  // a conflict merged with markers left in it isn't what either side, or anyone, means yet
  const marker = conflictMarkers(fs.readFileSync(file, "utf8"));
  if (marker) {
    console.log(
      red(
        `${path_1.basename(file)}:${marker} still has conflict markers: keep what each side should, take the markers out, then save it`,
      ),
    );
    return false;
  }
  // a file kept as written (Markdown, CSS, an image) is a definition of its own
  const format = keptAs(cb, file, into);
  if (format !== undefined) {
    const rows = fileRows(cb, file, into, format);
    return !!rows && apply(cb, rows, mode, command(rows), false);
  }
  const whole = withTheirCycles(cb, file, into);
  if (!whole) return false;
  const { a, text } = whole;
  const rows = whole.rows.map((r) => (how.mates?.has(r.path) ? { ...r, mate: true } : r));
  // lint what's being saved: warnings are shown, errors (rules set to "error" in lint.config) stop the save
  const saving = changedCode(cb, rows);
  if (saving.length && printScratchLint(cb, file, saving)) {
    console.log(
      red(
        `\n${path_1.basename(file)} has lint errors; nothing was saved. Fix them, or set the rule to "warn" in ${LINT_CONFIG}.`,
      ),
    );
    return false;
  }
  // a function's or class's name is part of its hash (code can read it, as `.name`): one written with another's,
  // as a file holding both has to, would be another function
  for (const r of rows) {
    if (r.mate || !r.def.nominal || (r.status !== "update" && r.status !== "conflict")) continue;
    const was = cb.store
      .hashesAt(r.path, r.def.kind)
      .map((h) => cb.store.getDef(h)?.nameHint)
      .find(
        (n) =>
          n !== undefined &&
          /^_\d+$/.test(r.def.nameHint.slice(n.length)) &&
          r.def.nameHint.startsWith(n),
      );
    if (was === undefined) continue;
    console.log(
      red(
        `${r.path} is declared ${r.def.nameHint} in ${path_1.basename(file)}, and ${was} in the codebase: a function's or class's name is part of its hash, so saved so it would be another definition. Nothing was saved: declare it ${was} (in a file of its own, if another here is declared ${was} too).`,
      ),
    );
    return false;
  }
  cb.lockPackages(a.packages);
  const analyzed = text ?? fs.readFileSync(file, "utf8");
  // a cycle it makes or grows, said before it's saved (it's saved all the same)
  for (const w of cycleWarnings(cb, rows)) console.log(yellow(`warning: ${w}`));
  return apply(
    cb,
    // the same code written differently (a comment changed): the codebase takes the new text, as formatting does
    formattedRows(cb, file, into, a, rows, text)
      .map((r) => {
        const names = how.respell?.get(r.path);
        return names ? { ...r, def: respelled(r.def, names) } : r;
      })
      .map((r, _, all) => spelledAlone(cb, file, analyzed, r, all))
      .map((r) =>
        !r.mate && r.status === "identical" && textChanged(cb, r.def)
          ? {
              ...r,
              status: "text" as const,
              note: "comments, layout or import names changed; the code is the same",
            }
          : r,
      ),
    mode,
    command(rows),
    false,
  );
}

uses

used by