Skip to content

Commit

Permalink
Merge pull request #11074 from storybookjs/refactor-cli
Browse files Browse the repository at this point in the history
CLI: refactor to simplify works with multiple package managers
  • Loading branch information
ndelangen authored Jun 10, 2020
2 parents 7c05d42 + b9ed269 commit e28d0fe
Show file tree
Hide file tree
Showing 44 changed files with 1,143 additions and 892 deletions.
1 change: 0 additions & 1 deletion lib/cli/src/NpmOptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export type NpmOptions = {
useYarn: boolean;
skipInstall?: boolean;
installAsDevDependencies?: boolean;
};
5 changes: 0 additions & 5 deletions lib/cli/src/PackageJson.ts

This file was deleted.

54 changes: 21 additions & 33 deletions lib/cli/src/add.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
import path from 'path';
import fs from 'fs';
import { sync as spawnSync } from 'cross-spawn';
import { hasYarn } from './has_yarn';
import { latestVersion } from './latest_version';
import { commandLog, getPackageJson } from './helpers';
import { PackageJson } from './PackageJson';
import { commandLog } from './helpers';
import { JsPackageManager, JsPackageManagerFactory, PackageJson } from './js-package-manager';

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

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

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

export const getPackageName = (addonName: string, isOfficialAddon: boolean) =>
isOfficialAddon ? storybookAddonScope + addonName : addonName;
Expand All @@ -51,29 +44,26 @@ export const getPackageArg = (
};

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

let result;
const packageArg = getPackageArg(addonName, isOfficialAddon, getPackageJson());
if (npmOptions.useYarn) {
result = spawnSync('yarn', ['add', packageArg, '--dev'], {
stdio: 'inherit',
});
} else {
result = spawnSync('npm', ['install', packageArg, '--save-dev'], {
stdio: 'inherit',
});
}
const packageArg = getPackageArg(
addonName,
isOfficialAddon,
packageManager.retrievePackageJson()
);

logger.log();
const installDone = commandLog(`Installing the ${addonName} Storybook addon`);
if (result.status !== 0) {

try {
packageManager.addDependencies({}, [packageArg]);
} catch (e) {
installDone(
`Something went wrong installing the addon: "${getPackageName(addonName, isOfficialAddon)}"`
);
Expand Down Expand Up @@ -150,20 +140,18 @@ export async function add(
addonName: string,
options: { useNpm: boolean; skipPostinstall: boolean }
) {
const useYarn = Boolean(options.useNpm !== true) && hasYarn();
const npmOptions = {
useYarn,
};
const packageManager = JsPackageManagerFactory.getPackageManager(options.useNpm);

const addonCheckDone = commandLog(`Verifying that ${addonName} is an addon`);
const isOfficialAddon = await isStorybookAddon(addonName, npmOptions);
const isOfficialAddon = await isStorybookAddon(packageManager, addonName);
if (!isOfficialAddon) {
if (!(await isAddon(addonName, npmOptions))) {
if (!(await isAddon(packageManager, addonName))) {
addonCheckDone(`The provided package was not a Storybook addon: ${addonName}.`);
return;
}
}
addonCheckDone();
installAddon(addonName, npmOptions, isOfficialAddon);
installAddon(packageManager, addonName, isOfficialAddon);
if (!options.skipPostinstall) {
await postinstallAddon(addonName, isOfficialAddon);
}
Expand Down
16 changes: 10 additions & 6 deletions lib/cli/src/detect.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import fs from 'fs';

import { getBowerJson, getPackageJson } from './helpers';
import { getBowerJson } from './helpers';
import { isStorybookInstalled, detectFrameworkPreset, detect, detectLanguage } from './detect';
import { ProjectType, SUPPORTED_FRAMEWORKS, SupportedLanguage } from './project_types';
import { readPackageJson } from './js-package-manager';

jest.mock('./helpers', () => ({
getBowerJson: jest.fn(),
getPackageJson: jest.fn(),
}));

jest.mock('./js-package-manager', () => ({
readPackageJson: jest.fn(),
}));

jest.mock('fs', () => ({
Expand Down Expand Up @@ -209,18 +213,18 @@ const MOCK_FRAMEWORK_FILES = [

describe('Detect', () => {
it(`should return type HTML if html option is passed`, () => {
(getPackageJson as jest.Mock).mockImplementation(() => true);
(readPackageJson as jest.Mock).mockImplementation(() => true);
expect(detect({ html: true })).toBe(ProjectType.HTML);
});

it(`should return type UNDETECTED if neither packageJson or bowerJson exist`, () => {
(getPackageJson as jest.Mock).mockImplementation(() => false);
(readPackageJson as jest.Mock).mockImplementation(() => false);
(getBowerJson as jest.Mock).mockImplementation(() => false);
expect(detect()).toBe(ProjectType.UNDETECTED);
});

it(`should return language typescript if the dependency is present`, () => {
(getPackageJson as jest.Mock).mockImplementation(() => ({
(readPackageJson as jest.Mock).mockImplementation(() => ({
dependencies: {
typescript: '1.0.0',
},
Expand All @@ -229,7 +233,7 @@ describe('Detect', () => {
});

it(`should return language javascript by default`, () => {
(getPackageJson as jest.Mock).mockImplementation(() => true);
(readPackageJson as jest.Mock).mockImplementation(() => true);
expect(detectLanguage()).toBe(SupportedLanguage.JAVASCRIPT);
});

Expand Down
10 changes: 5 additions & 5 deletions lib/cli/src/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
TemplateConfiguration,
TemplateMatcher,
} from './project_types';
import { getBowerJson, getPackageJson } from './helpers';
import { PackageJson } from './PackageJson';
import { getBowerJson } from './helpers';
import { PackageJson, readPackageJson } from './js-package-manager';

const hasDependency = (packageJson: PackageJson, name: string) => {
return !!packageJson.dependencies?.[name] || !!packageJson.devDependencies?.[name];
Expand Down Expand Up @@ -55,7 +55,7 @@ export function detectFrameworkPreset(packageJson = {}) {
return result ? result.preset : ProjectType.UNDETECTED;
}

export function isStorybookInstalled(dependencies: PackageJson, force?: boolean) {
export function isStorybookInstalled(dependencies: PackageJson | false, force?: boolean) {
if (!dependencies) {
return false;
}
Expand Down Expand Up @@ -83,7 +83,7 @@ export function isStorybookInstalled(dependencies: PackageJson, force?: boolean)

export function detectLanguage() {
let language = SupportedLanguage.JAVASCRIPT;
const packageJson = getPackageJson();
const packageJson = readPackageJson();
const bowerJson = getBowerJson();
if (!packageJson && !bowerJson) {
return language;
Expand All @@ -97,7 +97,7 @@ export function detectLanguage() {
}

export function detect(options: { force?: boolean; html?: boolean } = {}) {
const packageJson = getPackageJson();
const packageJson = readPackageJson();
const bowerJson = getBowerJson();

if (!packageJson && !bowerJson) {
Expand Down
41 changes: 17 additions & 24 deletions lib/cli/src/generators/ANGULAR/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,17 @@ import {
getAngularAppTsConfigJson,
getAngularAppTsConfigPath,
} from './angular-helpers';
import {
retrievePackageJson,
getVersionedPackages,
writePackageJson,
getBabelDependencies,
installDependencies,
writeFileAsJson,
copyTemplate,
} from '../../helpers';
import { getBabelDependencies, writeFileAsJson, copyTemplate } from '../../helpers';
import { StoryFormat } from '../../project_types';
import { NpmOptions } from '../../NpmOptions';
import { Generator, GeneratorOptions } from '../Generator';
import { JsPackageManager } from '../../js-package-manager';

async function addDependencies(npmOptions: NpmOptions, { storyFormat }: GeneratorOptions) {
async function addDependencies(
packageManager: JsPackageManager,
npmOptions: NpmOptions,
{ storyFormat }: GeneratorOptions
) {
const packages = [
'@storybook/angular',
'@storybook/addon-actions',
Expand All @@ -30,22 +27,18 @@ async function addDependencies(npmOptions: NpmOptions, { storyFormat }: Generato
packages.push('@storybook/addon-docs');
}

const versionedPackages = await getVersionedPackages(npmOptions, ...packages);

const packageJson = await retrievePackageJson();

packageJson.dependencies = packageJson.dependencies || {};
packageJson.devDependencies = packageJson.devDependencies || {};
const versionedPackages = await packageManager.getVersionedPackages(...packages);

packageJson.scripts = packageJson.scripts || {};
packageJson.scripts.storybook = 'start-storybook -p 6006';
packageJson.scripts['build-storybook'] = 'build-storybook';
const packageJson = packageManager.retrievePackageJson();

writePackageJson(packageJson);
const babelDependencies = await getBabelDependencies(packageManager, packageJson);

const babelDependencies = await getBabelDependencies(npmOptions, packageJson);
packageManager.addDependencies({ ...npmOptions, packageJson }, [
...versionedPackages,
...babelDependencies,
]);

installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]);
packageManager.addStorybookCommandInScripts();
}

function editAngularAppTsConfig() {
Expand All @@ -64,7 +57,7 @@ function editAngularAppTsConfig() {
writeFileAsJson(getAngularAppTsConfigPath(), tsConfigJson);
}

const generator: Generator = async (npmOptions, { storyFormat }) => {
const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => {
if (!isDefaultProjectSet()) {
throw new Error(
'Could not find a default project in your Angular workspace.\nSet a defaultProject in your angular.json and re-run the installation.'
Expand All @@ -73,7 +66,7 @@ const generator: Generator = async (npmOptions, { storyFormat }) => {

copyTemplate(__dirname, storyFormat);

await addDependencies(npmOptions, { storyFormat });
await addDependencies(packageManager, npmOptions, { storyFormat });
editAngularAppTsConfig();
editStorybookTsConfig(path.resolve('./.storybook/tsconfig.json'));
};
Expand Down
36 changes: 15 additions & 21 deletions lib/cli/src/generators/AURELIA/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
import {
getVersionedPackages,
writePackageJson,
getBabelDependencies,
installDependencies,
getPackageJson,
writeFileAsJson,
copyTemplate,
readFileAsJson,
} from '../../helpers';
import { getBabelDependencies, writeFileAsJson, copyTemplate, readFileAsJson } from '../../helpers';
import { Generator } from '../Generator';
import { StoryFormat } from '../../project_types';

Expand All @@ -26,7 +17,11 @@ function addStorybookExcludeGlobToTsConfig() {
tsConfigJson.exclude = [...exclude, glob];
writeFileAsJson('tsconfig.json', tsConfigJson);
}
const generator: Generator = async (npmOptions, { storyFormat = StoryFormat.CSF }) => {
const generator: Generator = async (
packageManager,
npmOptions,
{ storyFormat = StoryFormat.CSF }
) => {
copyTemplate(__dirname, storyFormat);
const packages = [
'@storybook/aurelia',
Expand All @@ -45,17 +40,16 @@ const generator: Generator = async (npmOptions, { storyFormat = StoryFormat.CSF
packages.push('@storybook/addon-docs');
}

const versionedPackages = await getVersionedPackages(npmOptions, ...packages);
const packageJson = getPackageJson();
packageJson.dependencies = packageJson.dependencies || {};
packageJson.devDependencies = packageJson.devDependencies || {};
packageJson.scripts = packageJson.scripts || {};
packageJson.scripts.storybook = 'start-storybook -p 6006';
packageJson.scripts['build-storybook'] = 'build-storybook';
writePackageJson(packageJson);
const versionedPackages = await packageManager.getVersionedPackages(...packages);
const packageJson = packageManager.retrievePackageJson();
addStorybookExcludeGlobToTsConfig();
const babelDependencies = await getBabelDependencies(npmOptions, packageJson);
installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]);
const babelDependencies = await getBabelDependencies(packageManager, packageJson);
packageManager.addDependencies({ ...npmOptions, packageJson }, [
...versionedPackages,
...babelDependencies,
]);

packageManager.addStorybookCommandInScripts();
};

export default generator;
36 changes: 8 additions & 28 deletions lib/cli/src/generators/EMBER/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import {
getVersions,
retrievePackageJson,
writePackageJson,
getBabelDependencies,
installDependencies,
copyTemplate,
} from '../../helpers';
import { getBabelDependencies, copyTemplate } from '../../helpers';
import { Generator } from '../Generator';

const generator: Generator = async (npmOptions, { storyFormat }) => {
const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => {
const [
storybookVersion,
babelPluginEmberModulePolyfillVersion,
babelPluginHtmlBarsInlinePrecompileVersion,
linksVersion,
actionsVersion,
addonsVersion,
] = await getVersions(
npmOptions,
] = await packageManager.getVersions(
'@storybook/ember',
// babel-plugin-ember-modules-api-polyfill is a peerDep of @storybook/ember
'babel-plugin-ember-modules-api-polyfill',
Expand All @@ -30,24 +22,10 @@ const generator: Generator = async (npmOptions, { storyFormat }) => {

copyTemplate(__dirname, storyFormat);

const packageJson = await retrievePackageJson();
const packageJson = packageManager.retrievePackageJson();
const babelDependencies = await getBabelDependencies(packageManager, packageJson);

packageJson.dependencies = packageJson.dependencies || {};
packageJson.devDependencies = packageJson.devDependencies || {};

packageJson.scripts = {
...packageJson.scripts,
...{
storybook: 'start-storybook -p 6006 -s dist',
'build-storybook': 'build-storybook -s dist',
},
};

writePackageJson(packageJson);

const babelDependencies = await getBabelDependencies(npmOptions, packageJson);

installDependencies({ ...npmOptions, packageJson }, [
packageManager.addDependencies({ ...npmOptions, packageJson }, [
`@storybook/ember@${storybookVersion}`,
`@storybook/addon-actions@${actionsVersion}`,
`@storybook/addon-links@${linksVersion}`,
Expand All @@ -56,6 +34,8 @@ const generator: Generator = async (npmOptions, { storyFormat }) => {
`babel-plugin-htmlbars-inline-precompile@${babelPluginHtmlBarsInlinePrecompileVersion}`,
...babelDependencies,
]);

packageManager.addStorybookCommandInScripts();
};

export default generator;
Loading

0 comments on commit e28d0fe

Please sign in to comment.