sverweij/dependency-cruiser

View on GitHub
src/cli/index.mjs

Summary

Maintainability
Test Coverage
import { join } from "node:path";
import picomatch from "picomatch";
import set from "lodash/set.js";
import isInstalledGlobally from "is-installed-globally";
import chalk from "chalk";

import assertFileExistence from "./utl/assert-file-existence.mjs";
import normalizeCliOptions from "./normalize-cli-options.mjs";
import { write } from "./utl/io.mjs";
import setUpCliFeedbackListener from "./listeners/cli-feedback.mjs";
import setUpPerformanceLogListener from "./listeners/performance-log/index.mjs";
import setUpNDJSONListener from "./listeners/ndjson.mjs";
import cruise from "#main/cruise.mjs";
import { INFO, bus } from "#utl/bus.mjs";

async function extractResolveOptions(pCruiseOptions) {
  let lResolveOptions = {};
  const lWebPackConfigFileName =
    pCruiseOptions?.ruleSet?.options?.webpackConfig?.fileName ?? null;

  if (lWebPackConfigFileName) {
    const { default: extractWebpackResolveConfig } = await import(
      "#config-utl/extract-webpack-resolve-config.mjs"
    );
    lResolveOptions = await extractWebpackResolveConfig(
      lWebPackConfigFileName,
      pCruiseOptions?.ruleSet?.options?.webpackConfig?.env ?? null,
      pCruiseOptions?.ruleSet?.options?.webpackConfig?.arguments ?? null,
    );
  }
  return lResolveOptions;
}

async function addKnownViolations(pCruiseOptions) {
  if (pCruiseOptions.knownViolationsFile) {
    const { default: extractKnownViolations } = await import(
      "#config-utl/extract-known-violations.mjs"
    );
    const lKnownViolations = await extractKnownViolations(
      pCruiseOptions.knownViolationsFile,
    );

    // Check against json schema is already done in src/main/options/validate
    // so here we can just concentrate on the io
    let lCruiseOptions = structuredClone(pCruiseOptions);
    set(lCruiseOptions, "ruleSet.options.knownViolations", lKnownViolations);
    return lCruiseOptions;
  }
  return pCruiseOptions;
}

async function extractTSConfigOptions(pCruiseOptions) {
  let lReturnValue = {};
  const lTSConfigFileName =
    pCruiseOptions?.ruleSet?.options?.tsConfig?.fileName ?? null;

  if (lTSConfigFileName) {
    const { default: extractTSConfig } = await import(
      "#config-utl/extract-ts-config.mjs"
    );
    lReturnValue = extractTSConfig(lTSConfigFileName);
  }

  return lReturnValue;
}

async function extractBabelConfigOptions(pCruiseOptions) {
  let lReturnValue = {};
  const lBabelConfigFileName =
    pCruiseOptions?.ruleSet?.options?.babelConfig?.fileName ?? null;
  if (lBabelConfigFileName) {
    const { default: extractBabelConfig } = await import(
      "#config-utl/extract-babel-config.mjs"
    );
    lReturnValue = extractBabelConfig(lBabelConfigFileName);
  }

  return lReturnValue;
}

function setUpListener(pCruiseOptions) {
  const lString2Listener = new Map([
    ["cli-feedback", setUpCliFeedbackListener],
    ["performance-log", setUpPerformanceLogListener],
    ["ndjson", setUpNDJSONListener],
  ]);
  const lListenerID =
    pCruiseOptions?.progress ??
    pCruiseOptions?.ruleSet?.options?.progress?.type;

  const lListenerFunction = lString2Listener.get(lListenerID);
  /* c8 ignore next 6 */
  if (Boolean(lListenerFunction)) {
    lListenerFunction(
      bus,
      pCruiseOptions?.ruleSet?.options?.progress?.maximumLevel ?? INFO,
    );
  }
}

async function runCruise(pFileDirectoryArray, pCruiseOptions) {
  const lCruiseOptions = await addKnownViolations(
    await normalizeCliOptions(pCruiseOptions),
  );

  pFileDirectoryArray
    .filter((pFileOrDirectory) => !picomatch.scan(pFileOrDirectory).isGlob)
    .map((pFileOrDirectory) =>
      lCruiseOptions?.ruleSet?.options?.baseDir
        ? join(lCruiseOptions.ruleSet.options.baseDir, pFileOrDirectory)
        : pFileOrDirectory,
    )
    .forEach(assertFileExistence);

  setUpListener(lCruiseOptions);

  bus.emit("start");
  const [lResolveOptions, tsConfig, babelConfig] = await Promise.all([
    extractResolveOptions(lCruiseOptions),
    extractTSConfigOptions(lCruiseOptions),
    extractBabelConfigOptions(lCruiseOptions),
  ]);
  const lReportingResult = await cruise(
    pFileDirectoryArray,
    lCruiseOptions,
    lResolveOptions,
    { tsConfig, babelConfig },
  );

  bus.progress("cli: writing results", { complete: 1 });
  bus.emit("write-start");
  write(lCruiseOptions.outputTo, lReportingResult.output);

  return lReportingResult.exitCode;
}

/**
 *
 * @param {string[]} pFileDirectoryArray
 * @param {import("../../types/options.mjs").ICruiseOptions} lCruiseOptions
 * @param {{stdout: NodeJS.WritableStream, stderr: NodeJS.WritableStream}=} pStreams
 * @returns {number}
 */
// eslint-disable-next-line complexity
export default async function executeCli(
  pFileDirectoryArray,
  pCruiseOptions,
  pStreams,
) {
  const lStreams = {
    stdout: process.stdout,
    stderr: process.stderr,
    ...(pStreams || {}),
  };
  let lCruiseOptions = pCruiseOptions || {};
  let lExitCode = 0;

  try {
    /* c8 ignore start */
    if (isInstalledGlobally) {
      lStreams.stderr.write(
        `\n  ${chalk.yellow(
          "WARNING",
        )}: You're running a globally installed dependency-cruiser.\n\n` +
          `           We recommend to ${chalk.bold.italic.underline(
            "install and run it as a local devDependency",
          )} in\n` +
          `           your project instead. There it has your project's environment and\n` +
          `           transpilers at its disposal. That will ensure it can find e.g.\n` +
          `           TypeScript, Vue or Svelte modules and dependencies.\n\n`,
      );
    }
    /* c8 ignore stop */
    if (lCruiseOptions.info === true) {
      const { default: formatMetaInfo } = await import(
        "./format-meta-info.mjs"
      );
      lStreams.stdout.write(await formatMetaInfo());
    } else if (lCruiseOptions.init) {
      const { default: initConfig } = await import("./init-config/index.mjs");
      initConfig(lCruiseOptions.init, null, lStreams);
    } else {
      lExitCode = await runCruise(pFileDirectoryArray, lCruiseOptions);
    }
  } catch (pError) {
    lStreams.stderr.write(`\n  ${chalk.red("ERROR")}: ${pError.message}\n`);
    bus.emit("end");
    lExitCode = 1;
  }
  bus.emit("end");
  return lExitCode;
}