'use strict';

var errors = require('@backstage/errors');
var cliCommon = require('@backstage/cli-common');
var child_process = require('child_process');
var util = require('util');
var fs = require('fs-extra');
var path = require('path');
var getPackages = require('@manypkg/get-packages');
var parsers = require('@yarnpkg/parsers');
var zod = require('zod');

function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }

var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
var path__default = /*#__PURE__*/_interopDefaultCompat(path);

const paths = cliCommon.findPaths(__dirname);

const execFile = util.promisify(child_process.execFile);

async function runGit(...args) {
  var _a, _b;
  try {
    const { stdout } = await execFile("git", args, {
      shell: true,
      cwd: paths.targetRoot
    });
    return stdout.trim().split(/\r\n|\r|\n/);
  } catch (error) {
    errors.assertError(error);
    if (error.stderr || typeof error.code === "number") {
      const stderr = (_a = error.stderr) == null ? void 0 : _a.toString("utf8");
      const msg = (_b = stderr == null ? void 0 : stderr.trim()) != null ? _b : `with exit code ${error.code}`;
      throw new Error(`git ${args[0]} failed, ${msg}`);
    }
    throw new errors.ForwardedError("Unknown execution error", error);
  }
}
class GitUtils {
  /**
   * Returns a sorted list of all files that have changed since the merge base
   * of the provided `ref` and HEAD, as well as all files that are not tracked by git.
   */
  static async listChangedFiles(ref) {
    if (!ref) {
      throw new Error("ref is required");
    }
    let diffRef = ref;
    try {
      const [base] = await runGit("merge-base", "HEAD", ref);
      diffRef = base;
    } catch {
    }
    const tracked = await runGit("diff", "--name-only", diffRef);
    const untracked = await runGit(
      "ls-files",
      "--others",
      "--exclude-standard"
    );
    return Array.from(/* @__PURE__ */ new Set([...tracked, ...untracked]));
  }
  /**
   * Returns the contents of a file at a specific ref.
   */
  static async readFileAtRef(path, ref) {
    let showRef = ref;
    try {
      const [base] = await runGit("merge-base", "HEAD", ref);
      showRef = base;
    } catch {
    }
    const { stdout } = await execFile("git", ["show", `${showRef}:${path}`], {
      shell: true,
      cwd: paths.targetRoot,
      maxBuffer: 1024 * 1024 * 50
    });
    return stdout;
  }
}

async function isMonoRepo() {
  var _a;
  const rootPackageJsonPath = paths.resolveTargetRoot("package.json");
  try {
    const pkg = await fs__default.default.readJson(rootPackageJsonPath);
    return Boolean((_a = pkg == null ? void 0 : pkg.workspaces) == null ? void 0 : _a.packages);
  } catch (error) {
    return false;
  }
}

const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
const SPECIAL_OBJECT_KEYS = [
  `__metadata`,
  `version`,
  `resolution`,
  `dependencies`,
  `peerDependencies`,
  `dependenciesMeta`,
  `peerDependenciesMeta`,
  `binaries`
];
class Lockfile {
  constructor(packages, data) {
    this.packages = packages;
    this.data = data;
  }
  /**
   * Load a {@link Lockfile} from a file path.
   */
  static async load(path) {
    const lockfileContents = await fs__default.default.readFile(path, "utf8");
    return Lockfile.parse(lockfileContents);
  }
  /**
   * Parse lockfile contents into a {@link Lockfile}.
   *
   * @public
   */
  static parse(content) {
    var _a;
    let data;
    try {
      data = parsers.parseSyml(content);
    } catch (err) {
      throw new Error(`Failed yarn.lock parse, ${err}`);
    }
    const packages = /* @__PURE__ */ new Map();
    for (const [key, value] of Object.entries(data)) {
      if (SPECIAL_OBJECT_KEYS.includes(key))
        continue;
      const [, name, ranges] = (_a = ENTRY_PATTERN.exec(key)) != null ? _a : [];
      if (!name) {
        throw new Error(`Failed to parse yarn.lock entry '${key}'`);
      }
      let queries = packages.get(name);
      if (!queries) {
        queries = [];
        packages.set(name, queries);
      }
      for (let range of ranges.split(/\s*,\s*/)) {
        if (range.startsWith(`${name}@`)) {
          range = range.slice(`${name}@`.length);
        }
        if (range.startsWith("npm:")) {
          range = range.slice("npm:".length);
        }
        queries.push({ range, version: value.version, dataKey: key });
      }
    }
    return new Lockfile(packages, data);
  }
  /**
   * Creates a simplified dependency graph from the lockfile data, where each
   * key is a package, and the value is a set of all packages that it depends on
   * across all versions.
   */
  createSimplifiedDependencyGraph() {
    const graph = /* @__PURE__ */ new Map();
    for (const [name, entries] of this.packages) {
      const dependencies = new Set(
        entries.flatMap((e) => {
          var _a, _b;
          const data = this.data[e.dataKey];
          return [
            ...Object.keys((_a = data == null ? void 0 : data.dependencies) != null ? _a : {}),
            ...Object.keys((_b = data == null ? void 0 : data.peerDependencies) != null ? _b : {})
          ];
        })
      );
      graph.set(name, dependencies);
    }
    return graph;
  }
  /**
   * Diff with another lockfile, returning entries that have been
   * added, changed, and removed compared to the other lockfile.
   */
  diff(otherLockfile) {
    var _a;
    const diff = {
      added: new Array(),
      changed: new Array(),
      removed: new Array()
    };
    const remainingOldNames = new Set(this.packages.keys());
    for (const [name, otherQueries] of otherLockfile.packages) {
      remainingOldNames.delete(name);
      const thisQueries = this.packages.get(name);
      if (!thisQueries) {
        diff.removed.push(...otherQueries.map((q) => ({ name, range: q.range })));
        continue;
      }
      const remainingOldRanges = new Set(thisQueries.map((q) => q.range));
      for (const otherQuery of otherQueries) {
        remainingOldRanges.delete(otherQuery.range);
        const thisQuery = thisQueries.find((q) => q.range === otherQuery.range);
        if (!thisQuery) {
          diff.removed.push({ name, range: otherQuery.range });
          continue;
        }
        const otherPkg = otherLockfile.data[otherQuery.dataKey];
        const thisPkg = this.data[thisQuery.dataKey];
        if (otherPkg && thisPkg) {
          const thisCheck = thisPkg.integrity || thisPkg.checksum;
          const otherCheck = otherPkg.integrity || otherPkg.checksum;
          if (thisCheck !== otherCheck) {
            diff.changed.push({ name, range: otherQuery.range });
          }
        }
      }
      for (const thisRange of remainingOldRanges) {
        diff.added.push({ name, range: thisRange });
      }
    }
    for (const name of remainingOldNames) {
      const queries = (_a = this.packages.get(name)) != null ? _a : [];
      diff.added.push(...queries.map((q) => ({ name, range: q.range })));
    }
    return diff;
  }
}

class PackageGraph extends Map {
  /**
   * Lists all local packages in a monorepo.
   */
  static async listTargetPackages() {
    const { packages } = await getPackages.getPackages(paths.targetDir);
    return packages;
  }
  /**
   * Creates a package graph from a list of local packages.
   */
  static fromPackages(packages) {
    const graph = new PackageGraph();
    for (const pkg of packages) {
      const name = pkg.packageJson.name;
      const existingPkg = graph.get(name);
      if (existingPkg) {
        throw new Error(
          `Duplicate package name '${name}' at ${pkg.dir} and ${existingPkg.dir}`
        );
      }
      graph.set(name, {
        name,
        dir: pkg.dir,
        packageJson: pkg.packageJson,
        allLocalDependencies: /* @__PURE__ */ new Map(),
        publishedLocalDependencies: /* @__PURE__ */ new Map(),
        localDependencies: /* @__PURE__ */ new Map(),
        localDevDependencies: /* @__PURE__ */ new Map(),
        localOptionalDependencies: /* @__PURE__ */ new Map(),
        allLocalDependents: /* @__PURE__ */ new Map(),
        publishedLocalDependents: /* @__PURE__ */ new Map(),
        localDependents: /* @__PURE__ */ new Map(),
        localDevDependents: /* @__PURE__ */ new Map(),
        localOptionalDependents: /* @__PURE__ */ new Map()
      });
    }
    for (const node of graph.values()) {
      for (const depName of Object.keys(node.packageJson.dependencies || {})) {
        const depPkg = graph.get(depName);
        if (depPkg) {
          node.allLocalDependencies.set(depName, depPkg);
          node.publishedLocalDependencies.set(depName, depPkg);
          node.localDependencies.set(depName, depPkg);
          depPkg.allLocalDependents.set(node.name, node);
          depPkg.publishedLocalDependents.set(node.name, node);
          depPkg.localDependents.set(node.name, node);
        }
      }
      for (const depName of Object.keys(
        node.packageJson.devDependencies || {}
      )) {
        const depPkg = graph.get(depName);
        if (depPkg) {
          node.allLocalDependencies.set(depName, depPkg);
          node.localDevDependencies.set(depName, depPkg);
          depPkg.allLocalDependents.set(node.name, node);
          depPkg.localDevDependents.set(node.name, node);
        }
      }
      for (const depName of Object.keys(
        node.packageJson.optionalDependencies || {}
      )) {
        const depPkg = graph.get(depName);
        if (depPkg) {
          node.allLocalDependencies.set(depName, depPkg);
          node.publishedLocalDependencies.set(depName, depPkg);
          node.localOptionalDependencies.set(depName, depPkg);
          depPkg.allLocalDependents.set(node.name, node);
          depPkg.publishedLocalDependents.set(node.name, node);
          depPkg.localOptionalDependents.set(node.name, node);
        }
      }
    }
    return graph;
  }
  /**
   * Traverses the package graph and collects a set of package names.
   *
   * The traversal starts at the provided list names, and continues
   * throughout all the names returned by the `collectFn`, which is
   * called once for each seen package.
   */
  collectPackageNames(startingPackageNames, collectFn) {
    const targets = /* @__PURE__ */ new Set();
    const searchNames = startingPackageNames.slice();
    while (searchNames.length) {
      const name = searchNames.pop();
      if (targets.has(name)) {
        continue;
      }
      const node = this.get(name);
      if (!node) {
        throw new Error(`Package '${name}' not found`);
      }
      targets.add(name);
      const collected = collectFn(node);
      if (collected) {
        searchNames.push(...collected);
      }
    }
    return targets;
  }
  /**
   * Lists all packages that have changed since a given git ref.
   *
   * @remarks
   *
   * If the `analyzeLockfile` option is set to true, the change detection will
   * also consider changes to the dependency management lockfile.
   */
  async listChangedPackages(options) {
    var _a, _b;
    const changedFiles = await GitUtils.listChangedFiles(options.ref);
    const dirMap = new Map(
      Array.from(this.values()).map((pkg) => [
        // relative from root, convert to posix, and add a / at the end
        path__default.default.relative(paths.targetRoot, pkg.dir).split(path__default.default.sep).join(path__default.default.posix.sep) + path__default.default.posix.sep,
        pkg
      ])
    );
    const packageDirs = Array.from(dirMap.keys());
    const result = new Array();
    let searchIndex = 0;
    changedFiles.sort();
    packageDirs.sort();
    for (const packageDir of packageDirs) {
      while (searchIndex < changedFiles.length && changedFiles[searchIndex] < packageDir) {
        searchIndex += 1;
      }
      if ((_a = changedFiles[searchIndex]) == null ? void 0 : _a.startsWith(packageDir)) {
        searchIndex += 1;
        result.push(dirMap.get(packageDir));
        while ((_b = changedFiles[searchIndex]) == null ? void 0 : _b.startsWith(packageDir)) {
          searchIndex += 1;
        }
      }
    }
    if (changedFiles.includes("yarn.lock") && options.analyzeLockfile) {
      let thisLockfile;
      let otherLockfile;
      try {
        thisLockfile = await Lockfile.load(
          paths.resolveTargetRoot("yarn.lock")
        );
        otherLockfile = Lockfile.parse(
          await GitUtils.readFileAtRef("yarn.lock", options.ref)
        );
      } catch (error) {
        console.warn(
          `Failed to read lockfiles, assuming all packages have changed, ${error}`
        );
        return Array.from(this.values());
      }
      const diff = thisLockfile.diff(otherLockfile);
      const graph = thisLockfile.createSimplifiedDependencyGraph();
      {
        const otherGraph = thisLockfile.createSimplifiedDependencyGraph();
        for (const [name, dependencies] of otherGraph) {
          const node = graph.get(name);
          if (node) {
            dependencies.forEach((d) => node.add(d));
          } else {
            graph.set(name, dependencies);
          }
        }
      }
      const changedPackages = new Set(
        [...diff.added, ...diff.changed, ...diff.removed].map((e) => e.name)
      );
      let changed = false;
      do {
        changed = false;
        for (const [name, dependencies] of graph) {
          if (changedPackages.has(name)) {
            continue;
          }
          for (const dep of dependencies) {
            if (changedPackages.has(dep)) {
              changed = true;
              changedPackages.add(name);
              break;
            }
          }
        }
      } while (changed);
      for (const node of this.values()) {
        if (changedPackages.has(node.name) && !result.includes(node)) {
          result.push(node);
        }
      }
    }
    return result;
  }
}

const packageRoleInfos = [
  {
    role: "frontend",
    platform: "web",
    output: ["bundle"]
  },
  {
    role: "backend",
    platform: "node",
    output: ["bundle"]
  },
  {
    role: "cli",
    platform: "node",
    output: ["cjs"]
  },
  {
    role: "web-library",
    platform: "web",
    output: ["types", "esm"]
  },
  {
    role: "node-library",
    platform: "node",
    output: ["types", "cjs"]
  },
  {
    role: "common-library",
    platform: "common",
    output: ["types", "esm", "cjs"]
  },
  {
    role: "frontend-plugin",
    platform: "web",
    output: ["types", "esm"]
  },
  {
    role: "frontend-plugin-module",
    platform: "web",
    output: ["types", "esm"]
  },
  {
    role: "backend-plugin",
    platform: "node",
    output: ["types", "cjs"]
  },
  {
    role: "backend-plugin-module",
    platform: "node",
    output: ["types", "cjs"]
  }
];
const readSchema = zod.z.object({
  name: zod.z.string().optional(),
  backstage: zod.z.object({
    role: zod.z.string().optional()
  }).optional()
});
const detectionSchema = zod.z.object({
  name: zod.z.string().optional(),
  scripts: zod.z.object({
    start: zod.z.string().optional(),
    build: zod.z.string().optional()
  }).optional(),
  publishConfig: zod.z.object({
    main: zod.z.string().optional(),
    types: zod.z.string().optional(),
    module: zod.z.string().optional()
  }).optional(),
  main: zod.z.string().optional(),
  types: zod.z.string().optional(),
  module: zod.z.string().optional()
});
class PackageRoles {
  /**
   * Get the associated info for a package role.
   */
  static getRoleInfo(role) {
    const roleInfo = packageRoleInfos.find((r) => r.role === role);
    if (!roleInfo) {
      throw new Error(`Unknown package role '${role}'`);
    }
    return roleInfo;
  }
  /**
   * Given package JSON data, get the package role.
   */
  static getRoleFromPackage(pkgJson) {
    const pkg = readSchema.parse(pkgJson);
    if (pkg.backstage) {
      const { role } = pkg.backstage;
      if (!role) {
        throw new Error(
          `Package ${pkg.name} must specify a role in the "backstage" field`
        );
      }
      return this.getRoleInfo(role).role;
    }
    return void 0;
  }
  /**
   * Attempt to detect the role of a package from its package.json.
   */
  static detectRoleFromPackage(pkgJson) {
    var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
    const pkg = detectionSchema.parse(pkgJson);
    if ((_b = (_a = pkg.scripts) == null ? void 0 : _a.start) == null ? void 0 : _b.includes("app:serve")) {
      return "frontend";
    }
    if ((_d = (_c = pkg.scripts) == null ? void 0 : _c.build) == null ? void 0 : _d.includes("backend:bundle")) {
      return "backend";
    }
    if (((_e = pkg.name) == null ? void 0 : _e.includes("plugin-")) && ((_f = pkg.name) == null ? void 0 : _f.includes("-backend-module-"))) {
      return "backend-plugin-module";
    }
    if (((_g = pkg.name) == null ? void 0 : _g.includes("plugin-")) && ((_h = pkg.name) == null ? void 0 : _h.includes("-module-"))) {
      return "frontend-plugin-module";
    }
    if ((_j = (_i = pkg.scripts) == null ? void 0 : _i.start) == null ? void 0 : _j.includes("plugin:serve")) {
      return "frontend-plugin";
    }
    if ((_l = (_k = pkg.scripts) == null ? void 0 : _k.start) == null ? void 0 : _l.includes("backend:dev")) {
      return "backend-plugin";
    }
    const mainEntry = ((_m = pkg.publishConfig) == null ? void 0 : _m.main) || pkg.main;
    const moduleEntry = ((_n = pkg.publishConfig) == null ? void 0 : _n.module) || pkg.module;
    const typesEntry = ((_o = pkg.publishConfig) == null ? void 0 : _o.types) || pkg.types;
    if (typesEntry) {
      if (mainEntry && moduleEntry) {
        return "common-library";
      }
      if (moduleEntry || (mainEntry == null ? void 0 : mainEntry.endsWith(".esm.js"))) {
        return "web-library";
      }
      if (mainEntry) {
        return "node-library";
      }
    } else if (mainEntry) {
      return "cli";
    }
    return void 0;
  }
}

exports.GitUtils = GitUtils;
exports.Lockfile = Lockfile;
exports.PackageGraph = PackageGraph;
exports.PackageRoles = PackageRoles;
exports.isMonoRepo = isMonoRepo;
//# sourceMappingURL=index.cjs.js.map
