fbredius/storybook

View on GitHub
lib/cli/src/add.ts

Summary

Maintainability
A
1 hr
Test Coverage
import path from 'path';
import fs from 'fs';
import { sync as spawnSync } from 'cross-spawn';
import { commandLog } from './helpers';
import { JsPackageManager, JsPackageManagerFactory, PackageJson } from './js-package-manager';

const logger = console;
export const storybookAddonScope = '@storybook/addon-';

const isAddon = async (packageManager: JsPackageManager, name: string) => {
  try {
    await packageManager.latestVersion(name);
    return true;
  } catch (e) {
    return false;
  }
};

const isStorybookAddon = async (packageManager: JsPackageManager, name: string) =>
  isAddon(packageManager, `${storybookAddonScope}${name}`);

export const getPackageName = (addonName: string, isOfficialAddon: boolean) =>
  isOfficialAddon ? storybookAddonScope + addonName : addonName;

export const getInstalledStorybookVersion = (packageJson: PackageJson) =>
  packageJson.devDependencies[
    // This only considers the first occurrence.
    Object.keys(packageJson.devDependencies).find((devDep) => /@storybook/.test(devDep))
  ] || false;

export const getPackageArg = (
  addonName: string,
  isOfficialAddon: boolean,
  packageJson: PackageJson
) => {
  if (isOfficialAddon) {
    const addonNameNoTag = addonName.split('@')[0];
    const installedStorybookVersion = getInstalledStorybookVersion(packageJson);
    return installedStorybookVersion
      ? `${addonNameNoTag}@${getInstalledStorybookVersion(packageJson)}`
      : addonName;
  }
  return addonName;
};

const installAddon = (
  packageManager: JsPackageManager,
  addonName: string,
  isOfficialAddon: boolean
) => {
  const prepareDone = commandLog(`Preparing to install the ${addonName} Storybook addon`);
  prepareDone();
  logger.log();

  const packageArg = getPackageArg(
    addonName,
    isOfficialAddon,
    packageManager.retrievePackageJson()
  );

  logger.log();
  const installDone = commandLog(`Installing the ${addonName} Storybook addon`);

  try {
    packageManager.addDependencies({}, [packageArg]);
  } catch (e) {
    installDone(
      `Something went wrong installing the addon: "${getPackageName(addonName, isOfficialAddon)}"`
    );
    logger.log();
    process.exit(1);
  }
  installDone();
};

export const addStorybookAddonToFile = (
  addonName: string,
  addonsFile: string[],
  isOfficialAddon: boolean
) => {
  const addonNameNoTag = addonName.split('@')[0];
  const alreadyRegistered = addonsFile.find((line) => line.includes(`${addonNameNoTag}/register`));

  if (alreadyRegistered) {
    return addonsFile;
  }

  const latestImportIndex = addonsFile.reduce(
    (prev, curr, currIndex) =>
      curr.startsWith('import') && curr.includes('register') ? currIndex : prev,
    -1
  );

  return [
    ...addonsFile.slice(0, latestImportIndex + 1),
    `import '${getPackageName(addonNameNoTag, isOfficialAddon)}/register';`,
    ...addonsFile.slice(latestImportIndex + 1),
  ];
};

const LEGACY_CONFIGS = ['addons', 'config', 'presets'];

const postinstallAddon = async (addonName: string, isOfficialAddon: boolean) => {
  let skipMsg = null;
  if (!isOfficialAddon) {
    skipMsg = 'unofficial addon';
  } else if (!fs.existsSync('.storybook')) {
    skipMsg = 'no .storybook config';
  } else {
    skipMsg = 'no codmods found';
    LEGACY_CONFIGS.forEach((config) => {
      try {
        const codemod = require.resolve(
          `${getPackageName(addonName, isOfficialAddon)}/postinstall/${config}.js`
        );
        commandLog(`Running postinstall script for ${addonName}`)();
        let configFile = path.join('.storybook', `${config}.ts`);
        if (!fs.existsSync(configFile)) {
          configFile = path.join('.storybook', `${config}.js`);
          if (!fs.existsSync(configFile)) {
            fs.writeFileSync(configFile, '', 'utf8');
          }
        }
        spawnSync('npx', ['jscodeshift', '-t', codemod, configFile], {
          stdio: 'inherit',
        });
        skipMsg = null;
      } catch (err) {
        // resolve failed, skip
      }
    });
  }

  if (skipMsg) {
    commandLog(`Skipping postinstall for ${addonName}, ${skipMsg}`)();
  }
};

export async function add(
  addonName: string,
  options: { useNpm: boolean; skipPostinstall: boolean }
) {
  const packageManager = JsPackageManagerFactory.getPackageManager(options.useNpm);

  const addonCheckDone = commandLog(`Verifying that ${addonName} is an addon`);
  const isOfficialAddon = await isStorybookAddon(packageManager, addonName);
  if (!isOfficialAddon) {
    if (!(await isAddon(packageManager, addonName))) {
      addonCheckDone(`The provided package was not a Storybook addon: ${addonName}.`);
      return;
    }
  }
  addonCheckDone();
  installAddon(packageManager, addonName, isOfficialAddon);
  if (!options.skipPostinstall) {
    await postinstallAddon(addonName, isOfficialAddon);
  }
}