// src/actions/incremental.ts
import { createHash } from "node:crypto";
import {
  existsSync,
  lstatSync,
  mkdirSync,
  readdirSync,
  readFileSync as readFileSync2,
  readlinkSync,
  rmSync,
  writeFileSync
} from "node:fs";
import path from "node:path";
import * as yaml from "yaml";

// src/lib/initCwd.ts
function INIT_CWD() {
  return process.env.INIT_CWD ?? process.cwd();
}

// src/lib/toolchainVersion.ts
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
var require2 = createRequire(import.meta.url);
function bundledToolchainVersion() {
  const pkgPath = require2.resolve("@xylabs/toolchain/package.json");
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
  if (!(pkg.version != null && pkg.version.length > 0)) throw new Error("Could not read @xylabs/toolchain version");
  return pkg.version;
}

// src/actions/incremental.ts
var snapshotVersion = 1;
var dependencyFields = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
var workspaceDependencyFields = ["dependencies", "devDependencies", "peerDependencies"];
var ignoredInputDirs = /* @__PURE__ */ new Set([
  ".cache",
  ".git",
  ".next",
  ".turbo",
  "build",
  "coverage",
  "dist",
  "docs",
  "node_modules"
]);
var ignoredInputFiles = /* @__PURE__ */ new Set([".eslintcache"]);
var outputDirs = ["build", "dist"];
function toPosix(value) {
  return value.split(path.sep).join("/");
}
function normalizeLocation(value) {
  const normalized = toPosix(value).replace(/\/$/u, "");
  return normalized === "" ? "." : normalized;
}
function stableValue(value) {
  if (Array.isArray(value)) return value.map(stableValue);
  if (Boolean(value) && typeof value === "object") {
    const entries = Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b)).map(([key, entryValue]) => [key, stableValue(entryValue)]);
    return Object.fromEntries(entries);
  }
  return value;
}
function hashContent(value) {
  return createHash("sha256").update(value).digest("hex");
}
function hashJson(value) {
  return hashContent(JSON.stringify(stableValue(value)));
}
function readJsonFile(filePath) {
  try {
    return JSON.parse(readFileSync2(filePath, "utf8"));
  } catch {
    return {};
  }
}
function readPackageJson(rootDir, workspace) {
  return readJsonFile(path.resolve(rootDir, workspace.location, "package.json"));
}
function isStringRecord(value) {
  if (value == null || typeof value !== "object") return false;
  return Object.values(value).every((entry) => typeof entry === "string");
}
function packageDeps(pkg, field) {
  const value = pkg[field];
  return isStringRecord(value) ? value : {};
}
function selectedDeps(pkg, fields) {
  const deps = {};
  for (const field of fields) {
    const fieldEntries = Object.entries(packageDeps(pkg, field));
    for (const [name, range] of fieldEntries) {
      Reflect.set(deps, `${field}:${name}`, range);
    }
  }
  return deps;
}
function workspaceDependencyNames(pkg, workspaceNames) {
  const names = /* @__PURE__ */ new Set();
  for (const field of workspaceDependencyFields) {
    const fieldEntries = Object.entries(packageDeps(pkg, field));
    for (const [name, range] of fieldEntries) {
      if (workspaceNames.has(name) && range.startsWith("workspace:")) {
        names.add(name);
      }
    }
  }
  return [...names].toSorted((a, b) => a.localeCompare(b));
}
function externalDeps(pkg, workspaceNames) {
  const deps = {};
  const depEntries = Object.entries(selectedDeps(pkg, dependencyFields));
  for (const [key, range] of depEntries) {
    const name = key.slice(key.indexOf(":") + 1);
    if (!workspaceNames.has(name) && !range.startsWith("workspace:")) {
      deps[key] = range;
    }
  }
  return deps;
}
function shouldHashRootFile(name) {
  return name === "package.json" || name === "pnpm-workspace.yaml" || name === ".npmrc" || name === ".yarnrc" || name === ".yarnrc.yml" || /^tsconfig(?:\..+)?\.json$/u.test(name) || /^(?:eslint|vite|vitest|xy|tsup)\.config\./u.test(name);
}
function rootConfigFiles(rootDir) {
  return readdirSync(rootDir, { withFileTypes: true }).filter((entry) => entry.isFile() && shouldHashRootFile(entry.name)).map((entry) => entry.name).toSorted((a, b) => a.localeCompare(b));
}
function collectFiles(dir, baseDir = dir) {
  if (!existsSync(dir)) return [];
  const files = [];
  const entries = readdirSync(dir, { withFileTypes: true });
  for (const entry of entries) {
    if (entry.isDirectory()) {
      if (!ignoredInputDirs.has(entry.name)) {
        files.push(...collectFiles(path.join(dir, entry.name), baseDir));
      }
      continue;
    }
    if (!entry.isFile() && !entry.isSymbolicLink()) continue;
    if (ignoredInputFiles.has(entry.name)) continue;
    const relativePath = path.relative(baseDir, path.join(dir, entry.name));
    files.push(toPosix(relativePath));
  }
  return files.toSorted((a, b) => a.localeCompare(b));
}
function collectOutputFiles(workspaceDir) {
  const files = [];
  for (const outputDir of outputDirs) {
    const abs = path.join(workspaceDir, outputDir);
    if (existsSync(abs)) {
      files.push(...collectFiles(abs, workspaceDir).map((file) => normalizeLocation(file)));
    }
  }
  return files.toSorted((a, b) => a.localeCompare(b));
}
function hashFiles(baseDir, files) {
  const hash = createHash("sha256");
  hash.update(`count:${files.length}\0`);
  const sortedFiles = files.toSorted((a, b) => a.localeCompare(b));
  for (const file of sortedFiles) {
    const abs = path.resolve(baseDir, file);
    hash.update(file);
    hash.update("\0");
    try {
      const stat = lstatSync(abs);
      if (stat.isSymbolicLink()) {
        hash.update("symlink");
        hash.update(readlinkSync(abs));
      } else if (stat.isFile()) {
        hash.update(readFileSync2(abs));
      }
    } catch {
      hash.update("missing");
    }
    hash.update("\0");
  }
  return hash.digest("hex");
}
function workspaceInputHash(rootDir, workspace) {
  if (workspace.location === ".") {
    return hashFiles(rootDir, rootConfigFiles(rootDir));
  }
  const workspaceDir = path.resolve(rootDir, workspace.location);
  return hashFiles(workspaceDir, collectFiles(workspaceDir));
}
function workspaceOutputHash(rootDir, workspace) {
  const workspaceDir = path.resolve(rootDir, workspace.location);
  return hashFiles(workspaceDir, collectOutputFiles(workspaceDir));
}
function rootHash(rootDir) {
  return hashFiles(rootDir, rootConfigFiles(rootDir));
}
function lockfileHash(rootDir, pm) {
  const lockfilePath = path.resolve(rootDir, pm.lockfile);
  return existsSync(lockfilePath) ? hashContent(readFileSync2(lockfilePath)) : "";
}
function readPnpmLockfileImporters(rootDir, pm) {
  if (pm.name !== "pnpm") return {};
  const lockfilePath = path.resolve(rootDir, pm.lockfile);
  if (!existsSync(lockfilePath)) return {};
  try {
    const parsed = yaml.parse(readFileSync2(lockfilePath, "utf8"));
    if (!Boolean(parsed) || typeof parsed !== "object") return {};
    const importers = parsed.importers;
    if (!Boolean(importers) || typeof importers !== "object" || Array.isArray(importers)) return {};
    const hashes = {};
    const sortedImporters = Object.entries(importers).toSorted(([a], [b]) => a.localeCompare(b));
    for (const [name, value] of sortedImporters) {
      hashes[normalizeLocation(name)] = hashJson(value);
    }
    return hashes;
  } catch {
    return {};
  }
}
function workspaceGraphHash(workspaceDeps) {
  return hashJson(workspaceDeps);
}
function safeToolchainVersion() {
  try {
    return bundledToolchainVersion();
  } catch {
    return "unknown";
  }
}
function createCurrentSnapshot({
  action,
  options,
  pm,
  workspaces
}) {
  const rootDir = INIT_CWD();
  const workspaceNames = new Set(workspaces.map((workspace) => workspace.name));
  const workspaceDeps = {};
  const fingerprints = {};
  const sortedWorkspaces = workspaces.toSorted((a, b) => a.name.localeCompare(b.name));
  for (const workspace of sortedWorkspaces) {
    const pkg = readPackageJson(rootDir, workspace);
    const packageJsonPath = path.resolve(rootDir, workspace.location, "package.json");
    const deps = workspaceDependencyNames(pkg, workspaceNames);
    workspaceDeps[workspace.name] = deps;
    fingerprints[workspace.name] = {
      externalDepsHash: hashJson(externalDeps(pkg, workspaceNames)),
      inputHash: workspaceInputHash(rootDir, workspace),
      location: normalizeLocation(workspace.location),
      name: workspace.name,
      outputHash: workspaceOutputHash(rootDir, workspace),
      packageJsonHash: existsSync(packageJsonPath) ? hashContent(readFileSync2(packageJsonPath)) : "",
      workspaceDepsHash: hashJson(deps)
    };
  }
  return {
    action,
    lockfileHash: lockfileHash(rootDir, pm),
    lockfileImporters: readPnpmLockfileImporters(rootDir, pm),
    optionsHash: options === void 0 ? void 0 : hashJson(options),
    packageManager: pm.name,
    rootHash: rootHash(rootDir),
    toolchainVersion: safeToolchainVersion(),
    version: snapshotVersion,
    workspaceGraphHash: workspaceGraphHash(workspaceDeps),
    workspaces: fingerprints
  };
}
function snapshotPath(rootDir, action) {
  return path.resolve(rootDir, ".xy/cache/incremental", `${action}.json`);
}
function readSnapshot(action) {
  const filePath = snapshotPath(INIT_CWD(), action);
  if (!existsSync(filePath)) return void 0;
  try {
    const parsed = JSON.parse(readFileSync2(filePath, "utf8"));
    return parsed.version === snapshotVersion && parsed.action === action ? parsed : void 0;
  } catch {
    return void 0;
  }
}
function didWorkspaceNamesChange(previous, current) {
  const previousEntries = Object.values(previous.workspaces).map((workspace) => `${workspace.name}\0${workspace.location}`).toSorted((a, b) => a.localeCompare(b));
  const currentEntries = Object.values(current.workspaces).map((workspace) => `${workspace.name}\0${workspace.location}`).toSorted((a, b) => a.localeCompare(b));
  return previousEntries.join("\n") !== currentEntries.join("\n");
}
function createDependentsMap(workspaces) {
  const rootDir = INIT_CWD();
  const workspaceNames = new Set(workspaces.map((workspace) => workspace.name));
  const dependents = /* @__PURE__ */ new Map();
  for (const workspace of workspaces) {
    dependents.set(workspace.name, /* @__PURE__ */ new Set());
  }
  for (const workspace of workspaces) {
    const deps = workspaceDependencyNames(readPackageJson(rootDir, workspace), workspaceNames);
    for (const dep of deps) {
      dependents.get(dep)?.add(workspace.name);
    }
  }
  return dependents;
}
function expandWithDependents(workspaceNames, workspaces) {
  const dependents = createDependentsMap(workspaces);
  const queue = [...workspaceNames];
  for (const name of queue) {
    const nameDependents = dependents.get(name) ?? [];
    for (const dependent of nameDependents) {
      if (!workspaceNames.has(dependent)) {
        workspaceNames.add(dependent);
        queue.push(dependent);
      }
    }
  }
  return [...workspaceNames].toSorted((a, b) => a.localeCompare(b));
}
function resolvePnpmLockfileImpact(previous, current, workspaces) {
  const byLocation = new Map(workspaces.map((workspace) => [normalizeLocation(workspace.location), workspace.name]));
  const allImporters = /* @__PURE__ */ new Set([
    ...Object.keys(previous.lockfileImporters),
    ...Object.keys(current.lockfileImporters)
  ]);
  const changedImporters = [...allImporters].filter((importer) => previous.lockfileImporters[importer] !== current.lockfileImporters[importer]).toSorted((a, b) => a.localeCompare(b));
  if (changedImporters.length === 0) {
    return { full: "lockfile changed outside workspace importers", workspaceNames: [] };
  }
  const affected = /* @__PURE__ */ new Set();
  for (const importer of changedImporters) {
    if (importer === ".") {
      return { full: "root lockfile importer changed", workspaceNames: [] };
    }
    const workspaceName = byLocation.get(importer);
    if (!(workspaceName != null && workspaceName.length > 0)) {
      return { full: `lockfile importer changed for unknown workspace: ${importer}`, workspaceNames: [] };
    }
    affected.add(workspaceName);
  }
  return { workspaceNames: [...affected].toSorted((a, b) => a.localeCompare(b)) };
}
function resolveLockfileImpact(previous, current, workspaces) {
  if (previous.lockfileHash === current.lockfileHash) return { workspaceNames: [] };
  if (current.packageManager !== "pnpm") {
    return { full: `${current.packageManager} lockfile changed`, workspaceNames: [] };
  }
  return resolvePnpmLockfileImpact(previous, current, workspaces);
}
function fullPlan(action, current, previous, reason) {
  return {
    action,
    affectedWorkspaceNames: Object.keys(current.workspaces).toSorted((a, b) => a.localeCompare(b)),
    current,
    full: true,
    previous,
    reason
  };
}
function compatibilityReason(previous, current) {
  if (previous.version !== snapshotVersion) return "snapshot version changed";
  if (previous.packageManager !== current.packageManager) return "package manager changed";
  if (previous.toolchainVersion !== current.toolchainVersion) return "toolchain version changed";
  if (previous.optionsHash !== current.optionsHash) return `${current.action} options changed`;
  if (didWorkspaceNamesChange(previous, current)) return "workspace set changed";
  if (previous.rootHash !== current.rootHash) return "root configuration changed";
  return void 0;
}
function isTrackingOutputs(action) {
  return action !== "lint";
}
function isExpandingDependents(action) {
  return action !== "lint";
}
function didWorkspaceChange(action, previous, current) {
  return previous.inputHash !== current.inputHash || isTrackingOutputs(action) && previous.outputHash !== current.outputHash || previous.packageJsonHash !== current.packageJsonHash || previous.workspaceDepsHash !== current.workspaceDepsHash || previous.externalDepsHash !== current.externalDepsHash;
}
function collectChangedWorkspaceNames(action, previous, current) {
  const affected = /* @__PURE__ */ new Set();
  for (const [name, workspace] of Object.entries(current.workspaces)) {
    const prior = previous.workspaces[name];
    if (prior === void 0) return { affected, fullReason: `workspace added: ${name}` };
    if (didWorkspaceChange(action, prior, workspace)) affected.add(name);
  }
  if (previous.workspaceGraphHash !== current.workspaceGraphHash) {
    for (const [name, workspace] of Object.entries(current.workspaces)) {
      if (previous.workspaces[name]?.workspaceDepsHash !== workspace.workspaceDepsHash) affected.add(name);
    }
  }
  return { affected };
}
function createIncrementalPlan(params) {
  const current = createCurrentSnapshot(params);
  const previous = readSnapshot(params.action);
  const {
    action,
    isVerbose,
    workspaces
  } = params;
  if (!previous) return fullPlan(action, current, previous, "no previous snapshot");
  const incompatible = compatibilityReason(previous, current);
  if (incompatible != null && incompatible.length > 0) return fullPlan(action, current, previous, incompatible);
  const { affected, fullReason } = collectChangedWorkspaceNames(action, previous, current);
  if (fullReason != null && fullReason.length > 0) return fullPlan(action, current, previous, fullReason);
  const lockImpact = resolveLockfileImpact(previous, current, workspaces);
  if (lockImpact.full != null && lockImpact.full.length > 0) return fullPlan(action, current, previous, lockImpact.full);
  for (const workspaceName of lockImpact.workspaceNames) {
    affected.add(workspaceName);
  }
  const affectedWorkspaceNames = isExpandingDependents(action) ? expandWithDependents(affected, workspaces) : [...affected].toSorted((a, b) => a.localeCompare(b));
  if ((isVerbose ?? false) && affectedWorkspaceNames.length > 0) {
    console.log(`Incremental ${action}: ${affectedWorkspaceNames.join(", ")}`);
  }
  return {
    action,
    affectedWorkspaceNames,
    current,
    full: false,
    previous
  };
}
function writeIncrementalSnapshot(params) {
  const rootDir = INIT_CWD();
  const filePath = snapshotPath(rootDir, params.action);
  mkdirSync(path.dirname(filePath), { recursive: true });
  writeFileSync(filePath, `${JSON.stringify(createCurrentSnapshot(params), null, 2)}
`);
}
function clearIncrementalSnapshot(action) {
  const filePath = snapshotPath(INIT_CWD(), action);
  if (!existsSync(filePath)) return;
  rmSync(filePath, { force: true });
}
export {
  clearIncrementalSnapshot,
  createIncrementalPlan,
  writeIncrementalSnapshot
};
//# sourceMappingURL=incremental.mjs.map
