diff --git a/lib/cli/src/NpmOptions.ts b/lib/cli/src/NpmOptions.ts index f397f5f2f7a6..1f2114144623 100644 --- a/lib/cli/src/NpmOptions.ts +++ b/lib/cli/src/NpmOptions.ts @@ -1,5 +1,4 @@ export type NpmOptions = { - useYarn: boolean; skipInstall?: boolean; installAsDevDependencies?: boolean; }; diff --git a/lib/cli/src/PackageJson.ts b/lib/cli/src/PackageJson.ts deleted file mode 100644 index 2acfc43a55b5..000000000000 --- a/lib/cli/src/PackageJson.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type PackageJson = { - dependencies?: Record; - devDependencies?: Record; - peerDependencies?: Record; -}; diff --git a/lib/cli/src/add.ts b/lib/cli/src/add.ts index 01d82738c583..300a23039668 100644 --- a/lib/cli/src/add.ts +++ b/lib/cli/src/add.ts @@ -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; @@ -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)}"` ); @@ -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); } diff --git a/lib/cli/src/detect.test.ts b/lib/cli/src/detect.test.ts index 98bb4a026ffd..ae652a6a00d0 100644 --- a/lib/cli/src/detect.test.ts +++ b/lib/cli/src/detect.test.ts @@ -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', () => ({ @@ -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', }, @@ -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); }); diff --git a/lib/cli/src/detect.ts b/lib/cli/src/detect.ts index 9fccbacb74d8..05528cb8767a 100644 --- a/lib/cli/src/detect.ts +++ b/lib/cli/src/detect.ts @@ -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]; @@ -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; } @@ -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; @@ -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) { diff --git a/lib/cli/src/generators/ANGULAR/index.ts b/lib/cli/src/generators/ANGULAR/index.ts index cc9d5921366f..9ab5196a0192 100644 --- a/lib/cli/src/generators/ANGULAR/index.ts +++ b/lib/cli/src/generators/ANGULAR/index.ts @@ -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', @@ -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() { @@ -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.' @@ -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')); }; diff --git a/lib/cli/src/generators/AURELIA/index.ts b/lib/cli/src/generators/AURELIA/index.ts index 3f7362f13759..04ec62b89ed8 100644 --- a/lib/cli/src/generators/AURELIA/index.ts +++ b/lib/cli/src/generators/AURELIA/index.ts @@ -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'; @@ -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', @@ -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; diff --git a/lib/cli/src/generators/EMBER/index.ts b/lib/cli/src/generators/EMBER/index.ts index 874e81e6e2e7..36fb1c1ef533 100644 --- a/lib/cli/src/generators/EMBER/index.ts +++ b/lib/cli/src/generators/EMBER/index.ts @@ -1,14 +1,7 @@ -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, @@ -16,8 +9,7 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { 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', @@ -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}`, @@ -56,6 +34,8 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { `babel-plugin-htmlbars-inline-precompile@${babelPluginHtmlBarsInlinePrecompileVersion}`, ...babelDependencies, ]); + + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/Generator.ts b/lib/cli/src/generators/Generator.ts index b6143c0c92dc..7d081f55c9a2 100644 --- a/lib/cli/src/generators/Generator.ts +++ b/lib/cli/src/generators/Generator.ts @@ -1,8 +1,13 @@ import { NpmOptions } from '../NpmOptions'; import { StoryFormat } from '../project_types'; +import { JsPackageManager } from '../js-package-manager'; export type GeneratorOptions = { storyFormat: StoryFormat; }; -export type Generator = (npmOptions: NpmOptions, options: GeneratorOptions) => Promise; +export type Generator = ( + packageManager: JsPackageManager, + npmOptions: NpmOptions, + options: GeneratorOptions +) => Promise; diff --git a/lib/cli/src/generators/HTML/index.ts b/lib/cli/src/generators/HTML/index.ts index 25d850192b6c..eb96ae0ff49f 100755 --- a/lib/cli/src/generators/HTML/index.ts +++ b/lib/cli/src/generators/HTML/index.ts @@ -1,38 +1,25 @@ -import { - retrievePackageJson, - getVersionedPackages, - writePackageJson, - getBabelDependencies, - installDependencies, - copyTemplate, -} from '../../helpers'; +import { getBabelDependencies, copyTemplate } from '../../helpers'; import { StoryFormat } from '../../project_types'; import { Generator } from '../Generator'; -const generator: Generator = async (npmOptions, { storyFormat }) => { +const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => { const packages = ['@storybook/html', '@storybook/addon-actions', '@storybook/addon-links']; - - const versionedPackages = await getVersionedPackages(npmOptions, ...packages); if (storyFormat === StoryFormat.MDX) { packages.push('@storybook/addon-docs'); } - copyTemplate(__dirname, storyFormat); - - const packageJson = await retrievePackageJson(); + const versionedPackages = await packageManager.getVersionedPackages(...packages); - 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); + copyTemplate(__dirname, storyFormat); - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); + const packageJson = packageManager.retrievePackageJson(); + const babelDependencies = await getBabelDependencies(packageManager, packageJson); + packageManager.addDependencies({ ...npmOptions, packageJson }, [ + ...versionedPackages, + ...babelDependencies, + ]); - installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/MARIONETTE/index.ts b/lib/cli/src/generators/MARIONETTE/index.ts index e3ce805931a8..c29c891b00ed 100644 --- a/lib/cli/src/generators/MARIONETTE/index.ts +++ b/lib/cli/src/generators/MARIONETTE/index.ts @@ -1,35 +1,22 @@ import fse from 'fs-extra'; import path from 'path'; -import { - getVersion, - writePackageJson, - getBabelDependencies, - installDependencies, - retrievePackageJson, -} from '../../helpers'; +import { getBabelDependencies } from '../../helpers'; import { Generator } from '../Generator'; -const generator: Generator = async (npmOptions) => { - const storybookVersion = await getVersion(npmOptions, '@storybook/marionette'); +const generator: Generator = async (packageManager, npmOptions) => { + const storybookVersion = await packageManager.getVersion('@storybook/marionette'); fse.copySync(path.resolve(__dirname, 'template/'), '.', { overwrite: true }); - const packageJson = await retrievePackageJson(); + const packageJson = packageManager.retrievePackageJson(); - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; + const babelDependencies = await getBabelDependencies(packageManager, packageJson); - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; - - writePackageJson(packageJson); - - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); - - installDependencies({ ...npmOptions, packageJson }, [ + packageManager.addDependencies({ ...npmOptions, packageJson }, [ `@storybook/marionette@${storybookVersion}`, ...babelDependencies, ]); + + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/MARKO/index.ts b/lib/cli/src/generators/MARKO/index.ts index b8a3bc0e40e1..f28c62ff86d1 100644 --- a/lib/cli/src/generators/MARKO/index.ts +++ b/lib/cli/src/generators/MARKO/index.ts @@ -1,16 +1,12 @@ -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 [storybookVersion, addonActionVersion, addonKnobsVersion] = await getVersions( - npmOptions, +const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => { + const [ + storybookVersion, + addonActionVersion, + addonKnobsVersion, + ] = await packageManager.getVersions( '@storybook/marko', '@storybook/addon-actions', '@storybook/addon-knobs' @@ -18,25 +14,18 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { copyTemplate(__dirname, storyFormat); - const packageJson = await retrievePackageJson(); + const packageJson = packageManager.retrievePackageJson(); - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; + const babelDependencies = await getBabelDependencies(packageManager, packageJson); - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; - - writePackageJson(packageJson); - - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); - - installDependencies({ ...npmOptions, packageJson }, [ + packageManager.addDependencies({ ...npmOptions, packageJson }, [ `@storybook/marko@${storybookVersion}`, `@storybook/addon-actions@${addonActionVersion}`, `@storybook/addon-knobs@${addonKnobsVersion}`, ...babelDependencies, ]); + + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/METEOR/index.ts b/lib/cli/src/generators/METEOR/index.ts index 6420a01f0b03..95ae01834906 100644 --- a/lib/cli/src/generators/METEOR/index.ts +++ b/lib/cli/src/generators/METEOR/index.ts @@ -1,16 +1,10 @@ import fs from 'fs'; import JSON5 from 'json5'; -import { - getVersions, - retrievePackageJson, - writePackageJson, - getBabelDependencies, - installDependencies, - copyTemplate, -} from '../../helpers'; +import { getBabelDependencies, copyTemplate } from '../../helpers'; import { Generator } from '../Generator'; +import { writePackageJson } from '../../js-package-manager'; -const generator: Generator = async (npmOptions, { storyFormat }) => { +const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => { const [ storybookVersion, actionsVersion, @@ -21,8 +15,7 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { reactDomVersion, presetEnvVersion, presetReactVersion, - ] = await getVersions( - npmOptions, + ] = await packageManager.getVersions( '@storybook/react', '@storybook/addon-actions', '@storybook/addon-links', @@ -36,11 +29,7 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { copyTemplate(__dirname, storyFormat); - const packageJson = await retrievePackageJson(); - - packageJson.devDependencies = packageJson.devDependencies || {}; - packageJson.scripts = packageJson.scripts || {}; - packageJson.dependencies = packageJson.dependencies || {}; + const packageJson = packageManager.retrievePackageJson(); const devDependencies = [ `@storybook/react@${storybookVersion}`, @@ -70,12 +59,9 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { fs.writeFileSync('.babelrc', JSON.stringify(babelrc, null, 2), 'utf8'); - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; - writePackageJson(packageJson); - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); + const babelDependencies = await getBabelDependencies(packageManager, packageJson); // add react packages. const dependencies = []; @@ -87,13 +73,18 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { } if (dependencies.length > 0) { - installDependencies( + packageManager.addDependencies( { ...npmOptions, packageJson, installAsDevDependencies: false }, dependencies ); } - installDependencies({ ...npmOptions, packageJson }, [...devDependencies, ...babelDependencies]); + packageManager.addDependencies({ ...npmOptions, packageJson }, [ + ...devDependencies, + ...babelDependencies, + ]); + + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/MITHRIL/index.ts b/lib/cli/src/generators/MITHRIL/index.ts index d6bff9454643..329b521c9d7f 100644 --- a/lib/cli/src/generators/MITHRIL/index.ts +++ b/lib/cli/src/generators/MITHRIL/index.ts @@ -1,22 +1,14 @@ -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, actionsVersion, linksVersion, knobsVersion, addonsVersion, - ] = await getVersions( - npmOptions, + ] = await packageManager.getVersions( '@storybook/mithril', '@storybook/addon-actions', '@storybook/addon-links', @@ -26,20 +18,11 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { copyTemplate(__dirname, storyFormat); - const packageJson = await retrievePackageJson(); + const packageJson = packageManager.retrievePackageJson(); - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; + const babelDependencies = await getBabelDependencies(packageManager, packageJson); - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; - - writePackageJson(packageJson); - - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); - - installDependencies({ ...npmOptions, packageJson }, [ + packageManager.addDependencies({ ...npmOptions, packageJson }, [ `@storybook/mithril@${storybookVersion}`, `@storybook/addon-actions@${actionsVersion}`, `@storybook/addon-links@${linksVersion}`, @@ -47,6 +30,8 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { `@storybook/addons@${addonsVersion}`, ...babelDependencies, ]); + + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/PREACT/index.ts b/lib/cli/src/generators/PREACT/index.ts index 42c9eb416c9d..f6ceceac04d8 100644 --- a/lib/cli/src/generators/PREACT/index.ts +++ b/lib/cli/src/generators/PREACT/index.ts @@ -1,16 +1,13 @@ -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 [storybookVersion, actionsVersion, linksVersion, addonsVersion] = await getVersions( - npmOptions, +const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => { + const [ + storybookVersion, + actionsVersion, + linksVersion, + addonsVersion, + ] = await packageManager.getVersions( '@storybook/preact', '@storybook/addon-actions', '@storybook/addon-links', @@ -19,26 +16,19 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { copyTemplate(__dirname, storyFormat); - const packageJson = await retrievePackageJson(); + const packageJson = packageManager.retrievePackageJson(); - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; + const babelDependencies = await getBabelDependencies(packageManager, packageJson); - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; - - writePackageJson(packageJson); - - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); - - installDependencies({ ...npmOptions, packageJson }, [ + packageManager.addDependencies({ ...npmOptions, packageJson }, [ `@storybook/preact@${storybookVersion}`, `@storybook/addon-actions@${actionsVersion}`, `@storybook/addon-links@${linksVersion}`, `@storybook/addons@${addonsVersion}`, ...babelDependencies, ]); + + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/RAX/index.ts b/lib/cli/src/generators/RAX/index.ts index 4ac18e259ab9..ed0b4bdfa3cb 100644 --- a/lib/cli/src/generators/RAX/index.ts +++ b/lib/cli/src/generators/RAX/index.ts @@ -1,22 +1,15 @@ -import { - getVersions, - retrievePackageJson, - writePackageJson, - getBabelDependencies, - installDependencies, - copyTemplate, -} from '../../helpers'; +import { getBabelDependencies, copyTemplate } from '../../helpers'; import { Generator } from '../Generator'; +import { writePackageJson } from '../../js-package-manager'; -const generator: Generator = async (npmOptions, { storyFormat }) => { +const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => { const [ storybookVersion, actionsVersion, linksVersion, addonsVersion, latestRaxVersion, - ] = await getVersions( - npmOptions, + ] = await packageManager.getVersions( '@storybook/rax', '@storybook/addon-actions', '@storybook/addon-links', @@ -26,10 +19,7 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { copyTemplate(__dirname, storyFormat); - const packageJson = await retrievePackageJson(); - - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; + const packageJson = packageManager.retrievePackageJson(); const raxVersion = packageJson.dependencies.rax || latestRaxVersion; @@ -42,21 +32,19 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { packageJson.dependencies['rax-text'] = packageJson.dependencies['rax-text'] || raxVersion; packageJson.dependencies['rax-view'] = packageJson.dependencies['rax-view'] || raxVersion; - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; - writePackageJson(packageJson); - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); + const babelDependencies = await getBabelDependencies(packageManager, packageJson); - installDependencies({ ...npmOptions, packageJson }, [ + packageManager.addDependencies({ ...npmOptions, packageJson }, [ `@storybook/rax@${storybookVersion}`, `@storybook/addon-actions@${actionsVersion}`, `@storybook/addon-links@${linksVersion}`, `@storybook/addons@${addonsVersion}`, ...babelDependencies, ]); + + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/REACT/index.ts b/lib/cli/src/generators/REACT/index.ts index 586dac39814b..2a6606bc4d2b 100644 --- a/lib/cli/src/generators/REACT/index.ts +++ b/lib/cli/src/generators/REACT/index.ts @@ -1,15 +1,8 @@ -import { - getVersionedPackages, - retrievePackageJson, - writePackageJson, - getBabelDependencies, - installDependencies, - copyTemplate, -} from '../../helpers'; +import { getBabelDependencies, copyTemplate } from '../../helpers'; import { StoryFormat } from '../../project_types'; import { Generator } from '../Generator'; -const generator: Generator = async (npmOptions, { storyFormat }) => { +const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => { const packages = [ '@storybook/react', '@storybook/addon-actions', @@ -20,24 +13,20 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { packages.push('@storybook/addon-docs'); } - const versionedPackages = await getVersionedPackages(npmOptions, ...packages); + const versionedPackages = await packageManager.getVersionedPackages(...packages); copyTemplate(__dirname, storyFormat); - const packageJson = await retrievePackageJson(); + const packageJson = packageManager.retrievePackageJson(); - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; + const babelDependencies = await getBabelDependencies(packageManager, packageJson); - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; + packageManager.addDependencies({ ...npmOptions, packageJson }, [ + ...versionedPackages, + ...babelDependencies, + ]); - writePackageJson(packageJson); - - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); - - installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/REACT_NATIVE/index.ts b/lib/cli/src/generators/REACT_NATIVE/index.ts index aad02bcb16c6..a47856934c78 100644 --- a/lib/cli/src/generators/REACT_NATIVE/index.ts +++ b/lib/cli/src/generators/REACT_NATIVE/index.ts @@ -1,24 +1,22 @@ import shell from 'shelljs'; import chalk from 'chalk'; -import { - getVersions, - retrievePackageJson, - writePackageJson, - paddedLog, - getBabelDependencies, - installDependencies, - copyTemplate, -} from '../../helpers'; +import { paddedLog, getBabelDependencies, copyTemplate } from '../../helpers'; import { NpmOptions } from '../../NpmOptions'; import { GeneratorOptions } from '../Generator'; +import { JsPackageManager, writePackageJson } from '../../js-package-manager'; export default async ( + packageManager: JsPackageManager, npmOptions: NpmOptions, installServer: boolean, { storyFormat }: GeneratorOptions ) => { - const [storybookVersion, addonsVersion, actionsVersion, linksVersion] = await getVersions( - npmOptions, + const [ + storybookVersion, + addonsVersion, + actionsVersion, + linksVersion, + ] = await packageManager.getVersions( '@storybook/react-native', '@storybook/addons', '@storybook/addon-actions', @@ -45,7 +43,7 @@ export default async ( } } - const packageJson = await retrievePackageJson(); + const packageJson = packageManager.retrievePackageJson(); packageJson.dependencies = packageJson.dependencies || {}; packageJson.devDependencies = packageJson.devDependencies || {}; @@ -75,7 +73,10 @@ export default async ( writePackageJson(packageJson); - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); + const babelDependencies = await getBabelDependencies(packageManager, packageJson); - installDependencies({ ...npmOptions, packageJson }, [...devDependencies, ...babelDependencies]); + packageManager.addDependencies({ ...npmOptions, packageJson }, [ + ...devDependencies, + ...babelDependencies, + ]); }; diff --git a/lib/cli/src/generators/REACT_SCRIPTS/index.ts b/lib/cli/src/generators/REACT_SCRIPTS/index.ts index cc43eafdb98e..bba68ba18b73 100644 --- a/lib/cli/src/generators/REACT_SCRIPTS/index.ts +++ b/lib/cli/src/generators/REACT_SCRIPTS/index.ts @@ -1,17 +1,10 @@ import path from 'path'; import fs from 'fs'; -import { - retrievePackageJson, - getVersionedPackages, - writePackageJson, - getBabelDependencies, - installDependencies, - copyTemplate, -} from '../../helpers'; +import { getBabelDependencies, copyTemplate } from '../../helpers'; import { StoryFormat } from '../../project_types'; import { Generator } from '../Generator'; -const generator: Generator = async (npmOptions, { storyFormat }) => { +const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => { const packages = [ '@storybook/react', '@storybook/preset-create-react-app', @@ -24,30 +17,23 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { packages.push('@storybook/addon-docs'); } - const versionedPackages = await getVersionedPackages(npmOptions, ...packages); + const versionedPackages = await packageManager.getVersionedPackages(...packages); copyTemplate(__dirname, storyFormat); - const packageJson = await retrievePackageJson(); + const packageJson = packageManager.retrievePackageJson(); - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; + const babelDependencies = await getBabelDependencies(packageManager, packageJson); - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 9009'; - packageJson.scripts['build-storybook'] = 'build-storybook'; + packageManager.addDependencies({ ...npmOptions, packageJson }, [ + ...versionedPackages, + ...babelDependencies, + ]); - if (fs.existsSync(path.resolve('./public'))) { - // has a public folder and add support to it. - packageJson.scripts.storybook += ' -s public'; - packageJson.scripts['build-storybook'] += ' -s public'; - } - - writePackageJson(packageJson); - - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); - - installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); + packageManager.addStorybookCommandInScripts({ + port: 9009, + staticFolder: fs.existsSync(path.resolve('./public')) ? 'public' : undefined, + }); }; export default generator; diff --git a/lib/cli/src/generators/RIOT/index.ts b/lib/cli/src/generators/RIOT/index.ts index 2c624431018a..404eb4aeacf6 100644 --- a/lib/cli/src/generators/RIOT/index.ts +++ b/lib/cli/src/generators/RIOT/index.ts @@ -1,22 +1,15 @@ -import { - getVersions, - retrievePackageJson, - writePackageJson, - getBabelDependencies, - installDependencies, - copyTemplate, -} from '../../helpers'; +import { getBabelDependencies, copyTemplate } from '../../helpers'; import { Generator } from '../Generator'; +import { writePackageJson } from '../../js-package-manager'; -const generator: Generator = async (npmOptions, { storyFormat }) => { +const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => { const [ storybookVersion, actionsVersion, linksVersion, addonsVersion, tagLoaderVersion, - ] = await getVersions( - npmOptions, + ] = await packageManager.getVersions( '@storybook/riot', '@storybook/addon-actions', '@storybook/addon-links', @@ -26,7 +19,7 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { copyTemplate(__dirname, storyFormat); - const packageJson = await retrievePackageJson(); + const packageJson = packageManager.retrievePackageJson(); packageJson.dependencies = packageJson.dependencies || {}; packageJson.devDependencies = packageJson.devDependencies || {}; @@ -40,18 +33,20 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { if ( !packageJson.devDependencies['riot-tag-loader'] && !packageJson.dependencies['riot-tag-loader'] - ) + ) { dependencies.push(`riot-tag-loader@${tagLoaderVersion}`); - - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; + } writePackageJson(packageJson); - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); + const babelDependencies = await getBabelDependencies(packageManager, packageJson); + + packageManager.addDependencies({ ...npmOptions, packageJson }, [ + ...dependencies, + ...babelDependencies, + ]); - installDependencies({ ...npmOptions, packageJson }, [...dependencies, ...babelDependencies]); + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/SFC_VUE/index.ts b/lib/cli/src/generators/SFC_VUE/index.ts index 0f85dbce6b12..df945f683faa 100644 --- a/lib/cli/src/generators/SFC_VUE/index.ts +++ b/lib/cli/src/generators/SFC_VUE/index.ts @@ -1,15 +1,8 @@ -import { - retrievePackageJson, - getVersionedPackages, - writePackageJson, - getBabelDependencies, - installDependencies, - copyTemplate, -} from '../../helpers'; +import { getBabelDependencies, copyTemplate } from '../../helpers'; import { StoryFormat } from '../../project_types'; import { Generator } from '../Generator'; -const generator: Generator = async (npmOptions, { storyFormat }) => { +const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => { const packages = [ '@storybook/vue', '@storybook/addon-actions', @@ -19,24 +12,20 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { if (storyFormat === StoryFormat.MDX) { packages.push('@storybook/addon-docs'); } - const versionedPackages = await getVersionedPackages(npmOptions, ...packages); + const versionedPackages = await packageManager.getVersionedPackages(...packages); copyTemplate(__dirname, storyFormat); - const packageJson = await retrievePackageJson(); + const packageJson = packageManager.retrievePackageJson(); - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; + const babelDependencies = await getBabelDependencies(packageManager, packageJson); - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; + packageManager.addDependencies({ ...npmOptions, packageJson }, [ + ...versionedPackages, + ...babelDependencies, + ]); - writePackageJson(packageJson); - - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); - - installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/SVELTE/index.ts b/lib/cli/src/generators/SVELTE/index.ts index 9da7bfa15463..646e234961c6 100644 --- a/lib/cli/src/generators/SVELTE/index.ts +++ b/lib/cli/src/generators/SVELTE/index.ts @@ -1,14 +1,7 @@ -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, actionsVersion, @@ -16,8 +9,7 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { addonsVersion, svelte, svelteLoader, - ] = await getVersions( - npmOptions, + ] = await packageManager.getVersions( '@storybook/svelte', '@storybook/addon-actions', '@storybook/addon-links', @@ -28,20 +20,11 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { copyTemplate(__dirname, storyFormat); - const packageJson = await retrievePackageJson(); + const packageJson = packageManager.retrievePackageJson(); - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; + const babelDependencies = await getBabelDependencies(packageManager, packageJson); - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; - - writePackageJson(packageJson); - - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); - - installDependencies({ ...npmOptions, packageJson }, [ + packageManager.addDependencies({ ...npmOptions, packageJson }, [ `@storybook/svelte@${storybookVersion}`, `@storybook/addon-actions@${actionsVersion}`, `@storybook/addon-links@${linksVersion}`, @@ -50,6 +33,8 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { `svelte-loader@${svelteLoader}`, ...babelDependencies, ]); + + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/UPDATE_PACKAGE_ORGANIZATIONS/index.ts b/lib/cli/src/generators/UPDATE_PACKAGE_ORGANIZATIONS/index.ts index ab3a1e0fcf79..0bd448f3f2f5 100644 --- a/lib/cli/src/generators/UPDATE_PACKAGE_ORGANIZATIONS/index.ts +++ b/lib/cli/src/generators/UPDATE_PACKAGE_ORGANIZATIONS/index.ts @@ -2,35 +2,27 @@ import path from 'path'; import { sync as spawnSync } from 'cross-spawn'; import { packageNames } from '@storybook/codemod'; -import { - getBabelDependencies, - getPackageJson, - getVersion, - getVersions, - installDependencies, - writePackageJson, -} from '../../helpers'; -import { PackageJson } from '../../PackageJson'; +import { getBabelDependencies } from '../../helpers'; import { NpmOptions } from '../../NpmOptions'; +import { JsPackageManager, PackageJson, writePackageJson } from '../../js-package-manager'; async function updatePackage( + packageManager: JsPackageManager, devDependencies: PackageJson['devDependencies'], oldName: string, - newName: string, - npmOptions: NpmOptions + newName: string ) { if (devDependencies[oldName]) { delete devDependencies[oldName]; - devDependencies[newName] = await getVersion(npmOptions, newName); + devDependencies[newName] = await packageManager.getVersion(newName); } } -async function updatePackageJson(npmOptions: NpmOptions) { - const packageJson = getPackageJson(); +async function updatePackageJson(packageManager: JsPackageManager, npmOptions: NpmOptions) { + const packageJson = packageManager.retrievePackageJson(); const { devDependencies } = packageJson; - const [actionsVersion, linksVersion] = await getVersions( - npmOptions, + const [actionsVersion, linksVersion] = await packageManager.getVersions( '@storybook/addon-actions', '@storybook/addon-links' ); @@ -41,7 +33,7 @@ async function updatePackageJson(npmOptions: NpmOptions) { await Promise.all( Object.keys(packageNames).map((oldName) => { const newName = packageNames[oldName]; - return updatePackage(devDependencies, oldName, newName, npmOptions); + return updatePackage(packageManager, devDependencies, oldName, newName); }) ); @@ -51,10 +43,10 @@ async function updatePackageJson(npmOptions: NpmOptions) { writePackageJson(packageJson); - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); + const babelDependencies = await getBabelDependencies(packageManager, packageJson); if (babelDependencies.length > 0) { - installDependencies({ ...npmOptions, packageJson }, babelDependencies); + packageManager.addDependencies({ ...npmOptions, packageJson }, babelDependencies); } } @@ -76,7 +68,7 @@ function updateSourceCode(parser: string) { }); } -export default async (parser: string, npmOptions: NpmOptions) => { - await updatePackageJson(npmOptions); +export default async (packageManager: JsPackageManager, parser: string, npmOptions: NpmOptions) => { + await updatePackageJson(packageManager, npmOptions); updateSourceCode(parser); }; diff --git a/lib/cli/src/generators/VUE/index.ts b/lib/cli/src/generators/VUE/index.ts index d35af935858a..0efb2d56bb0f 100644 --- a/lib/cli/src/generators/VUE/index.ts +++ b/lib/cli/src/generators/VUE/index.ts @@ -1,17 +1,13 @@ import { - retrievePackageJson, - getVersion, - getVersionedPackages, - writePackageJson, getBabelDependencies, - installDependencies, addToDevDependenciesIfNotPresent, copyTemplate, } from '../../helpers'; import { StoryFormat } from '../../project_types'; import { Generator } from '../Generator'; +import { writePackageJson } from '../../js-package-manager'; -const generator: Generator = async (npmOptions, { storyFormat }) => { +const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => { const packages = [ '@storybook/vue', '@storybook/addon-actions', @@ -22,14 +18,11 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { if (storyFormat === StoryFormat.MDX) { packages.push('@storybook/addon-docs'); } - const versionedPackages = await getVersionedPackages(npmOptions, ...packages); + const versionedPackages = await packageManager.getVersionedPackages(...packages); copyTemplate(__dirname, storyFormat); - const packageJson = await retrievePackageJson(); - - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; + const packageJson = packageManager.retrievePackageJson(); const packageBabelCoreVersion = packageJson.dependencies['babel-core'] || packageJson.devDependencies['babel-core']; @@ -41,18 +34,19 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { addToDevDependenciesIfNotPresent( packageJson, '@babel/core', - await getVersion(npmOptions, '@babel/core') + await packageManager.getVersion('@babel/core') ); } - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; - writePackageJson(packageJson); - 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; diff --git a/lib/cli/src/generators/WEB-COMPONENTS/index.ts b/lib/cli/src/generators/WEB-COMPONENTS/index.ts index 373eac327a67..03ced0f5e0c3 100755 --- a/lib/cli/src/generators/WEB-COMPONENTS/index.ts +++ b/lib/cli/src/generators/WEB-COMPONENTS/index.ts @@ -1,16 +1,10 @@ import fse from 'fs-extra'; import path from 'path'; -import { - getVersionedPackages, - retrievePackageJson, - writePackageJson, - getBabelDependencies, - installDependencies, -} from '../../helpers'; +import { getBabelDependencies } from '../../helpers'; import { StoryFormat } from '../../project_types'; import { Generator } from '../Generator'; -const generator: Generator = async (npmOptions, { storyFormat }) => { +const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => { const packages = [ '@storybook/web-components', '@storybook/addon-actions', @@ -18,7 +12,7 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { 'lit-html', ]; - const versionedPackages = await getVersionedPackages(npmOptions, ...packages); + const versionedPackages = await packageManager.getVersionedPackages(...packages); fse.copySync(path.resolve(__dirname, 'template/'), '.', { overwrite: true }); @@ -26,20 +20,16 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { // TODO: handle adding of docs mode } - const packageJson = await retrievePackageJson(); + const packageJson = packageManager.retrievePackageJson(); - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; + const babelDependencies = await getBabelDependencies(packageManager, packageJson); - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; + packageManager.addDependencies({ ...npmOptions, packageJson }, [ + ...versionedPackages, + ...babelDependencies, + ]); - writePackageJson(packageJson); - - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); - - installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/generators/WEBPACK_REACT/index.ts b/lib/cli/src/generators/WEBPACK_REACT/index.ts index 75bec7324ed2..5c030def1ee4 100644 --- a/lib/cli/src/generators/WEBPACK_REACT/index.ts +++ b/lib/cli/src/generators/WEBPACK_REACT/index.ts @@ -1,15 +1,8 @@ -import { - retrievePackageJson, - getVersionedPackages, - writePackageJson, - getBabelDependencies, - installDependencies, - copyTemplate, -} from '../../helpers'; +import { getBabelDependencies, copyTemplate } from '../../helpers'; import { StoryFormat } from '../../project_types'; import { Generator } from '../Generator'; -const generator: Generator = async (npmOptions, { storyFormat }) => { +const generator: Generator = async (packageManager, npmOptions, { storyFormat }) => { const packages = [ '@storybook/react', '@storybook/addon-actions', @@ -19,24 +12,20 @@ const generator: Generator = async (npmOptions, { storyFormat }) => { if (storyFormat === StoryFormat.MDX) { packages.push('@storybook/addon-docs'); } - const versionedPackages = await getVersionedPackages(npmOptions, ...packages); + const versionedPackages = await packageManager.getVersionedPackages(...packages); copyTemplate(__dirname, storyFormat); - const packageJson = await retrievePackageJson(); + const packageJson = packageManager.retrievePackageJson(); - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; + const babelDependencies = await getBabelDependencies(packageManager, packageJson); - packageJson.scripts = packageJson.scripts || {}; - packageJson.scripts.storybook = 'start-storybook -p 6006'; - packageJson.scripts['build-storybook'] = 'build-storybook'; + packageManager.addDependencies({ ...npmOptions, packageJson }, [ + ...versionedPackages, + ...babelDependencies, + ]); - writePackageJson(packageJson); - - const babelDependencies = await getBabelDependencies(npmOptions, packageJson); - - installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); + packageManager.addStorybookCommandInScripts(); }; export default generator; diff --git a/lib/cli/src/has_yarn.ts b/lib/cli/src/has_yarn.ts deleted file mode 100644 index 62991684dd1a..000000000000 --- a/lib/cli/src/has_yarn.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { sync } from 'cross-spawn'; -import path from 'path'; -import findUp from 'find-up'; - -export function hasYarn() { - const yarnAvailable = sync('yarn', ['--version']); - const npmAvailable = sync('npm', ['--version']); - - const lockFile = findUp.sync(['yarn.lock', 'package-lock.json']); - const hasYarnLock = lockFile && path.basename(lockFile) === 'yarn.lock'; - - if (yarnAvailable.status === 0 && (hasYarnLock || npmAvailable.status !== 0)) { - return true; - } - return false; -} - -export function hasYarn2() { - const yarnVersion = sync('yarn', ['--version']) - // @ts-ignore - .output.toString('utf8') - .replace(/,/g, '') - .replace(/"/g, ''); - - return !/^1\.+/.test(yarnVersion); -} diff --git a/lib/cli/src/helpers.test.ts b/lib/cli/src/helpers.test.ts index f71887a352f7..675733fe7597 100644 --- a/lib/cli/src/helpers.test.ts +++ b/lib/cli/src/helpers.test.ts @@ -17,14 +17,6 @@ jest.mock('path', () => ({ resolve: jest.fn((_, p) => p), })); -jest.mock('./npm_init', () => ({ - npmInit: jest.fn(), -})); - -jest.mock('./has_yarn', () => ({ - hasYarn2: jest.fn(() => false), -})); - describe('Helpers', () => { describe('copyTemplate', () => { it(`should fall back to ${StoryFormat.CSF} diff --git a/lib/cli/src/helpers.ts b/lib/cli/src/helpers.ts index 63eca144909c..9f32d3c4facc 100644 --- a/lib/cli/src/helpers.ts +++ b/lib/cli/src/helpers.ts @@ -3,85 +3,14 @@ import path from 'path'; import fs from 'fs'; import fse from 'fs-extra'; import chalk from 'chalk'; -import { sync as spawnSync } from 'cross-spawn'; -import { gt, satisfies } from '@storybook/semver'; +import { satisfies } from '@storybook/semver'; import stripJsonComments from 'strip-json-comments'; -import { latestVersion } from './latest_version'; -import { npmInit } from './npm_init'; import { StoryFormat } from './project_types'; -import { PackageJson } from './PackageJson'; -import { NpmOptions } from './NpmOptions'; -import { hasYarn2 } from './has_yarn'; - -// Cannot be `import` as it's not under TS root dir -const { version, devDependencies } = require('../package.json'); +import { JsPackageManager, PackageJson, PackageJsonWithDepsAndDevDeps } from './js-package-manager'; const logger = console; -export async function getVersion(npmOptions: NpmOptions, packageName: string, constraint?: any) { - let current; - if (packageName === '@storybook/cli') { - current = version; - } else if (/storybook/.test(packageName)) { - current = devDependencies[packageName]; - } - - let latest; - try { - latest = await latestVersion(npmOptions, packageName, constraint); - } catch (e) { - if (current) { - logger.warn(`\n ${chalk.yellow(e.message)}`); - return current; - } - - logger.error(`\n ${chalk.red(e.message)}`); - process.exit(1); - } - - const versionToUse = - current && (!constraint || satisfies(current, constraint)) && gt(current, latest) - ? current - : latest; - return `^${versionToUse}`; -} - -export function getVersions(npmOptions: NpmOptions, ...packageNames: string[]) { - return Promise.all(packageNames.map((packageName) => getVersion(npmOptions, packageName))); -} - -export function getVersionedPackages(npmOptions: NpmOptions, ...packageNames: string[]) { - return Promise.all( - packageNames.map( - async (packageName) => `${packageName}@${await getVersion(npmOptions, packageName)}` - ) - ); -} - -export function getPackageJson() { - const packageJsonPath = path.resolve('package.json'); - if (!fs.existsSync(packageJsonPath)) { - return false; - } - - const jsonContent = fs.readFileSync(packageJsonPath, 'utf8'); - return JSON.parse(jsonContent); -} - -export async function retrievePackageJson() { - const existing = getPackageJson(); - if (existing) { - return existing; - } - - // npmInit will create a new package.json file - npmInit(); - - // read the newly created package.json file - return getPackageJson() || {}; -} - export function getBowerJson() { const bowerJsonPath = path.resolve('bower.json'); if (!fs.existsSync(bowerJsonPath)) { @@ -113,13 +42,6 @@ export const writeFileAsJson = (jsonPath: string, content: unknown) => { return true; }; -export function writePackageJson(packageJson: object) { - const content = `${JSON.stringify(packageJson, null, 2)}\n`; - const packageJsonPath = path.resolve('package.json'); - - fs.writeFileSync(packageJsonPath, content, 'utf8'); -} - export const commandLog = (message: string) => { process.stdout.write(chalk.cyan(' • ') + message); @@ -182,110 +104,22 @@ export function codeLog(codeLines: string[], leftPadAmount?: number) { logger.log(finalResult); } -export function installDepsFromPackageJson(options: NpmOptions) { - let done = commandLog('Preparing to install dependencies'); - done(); - logger.log(); - - let result; - if (options.useYarn) { - result = spawnSync('yarn', { stdio: 'inherit' }); - } else { - result = spawnSync('npm', ['install'], { stdio: 'inherit' }); - } - - logger.log(); - done = commandLog('Installing dependencies'); - if (result.status !== 0) { - done('An error occurred while installing dependencies.'); - process.exit(1); - } - done(); -} - -/** - * Add dependencies to a project using `yarn add` or `npm install`. - * - * @param {Object} options contains `useYarn`, `runInstall` and `installAsDevDependencies` which we use to determine how we install packages. - * @param {Array} dependencies contains a list of packages to add. - * @example - * installDependencies(options, [ - * `@storybook/react@${storybookVersion}`, - * `@storybook/addon-actions@${actionsVersion}`, - * `@storybook/addon-links@${linksVersion}`, - * `@storybook/addons@${addonsVersion}`, - * ]); - */ -export function installDependencies( - options: NpmOptions & { packageJson: PackageJson }, - dependencies: string[] -) { - const { skipInstall } = options; - - if (skipInstall) { - const { packageJson } = options; - - const dependenciesMap = dependencies.reduce((acc, dep) => { - const idx = dep.lastIndexOf('@'); - const packageName = dep.slice(0, idx); - const packageVersion = dep.slice(idx + 1); - - return { ...acc, [packageName]: packageVersion }; - }, {}); - - if (options.installAsDevDependencies) { - packageJson.devDependencies = { - ...packageJson.devDependencies, - ...dependenciesMap, - }; - } else { - packageJson.dependencies = { - ...packageJson.dependencies, - ...dependenciesMap, - }; - } - - writePackageJson(packageJson); - } else { - const spawnCommand = options.useYarn ? 'yarn' : 'npm'; - const installCommand = options.useYarn ? 'add' : 'install'; - - const installArgs = [installCommand, ...dependencies]; - - if (options.installAsDevDependencies) { - installArgs.push('-D'); - } - - if (options.useYarn && !hasYarn2()) { - installArgs.push('--ignore-workspace-root-check'); - } - - const dependencyResult = spawnSync(spawnCommand, installArgs, { - stdio: 'inherit', - }); - - if (dependencyResult.status !== 0) { - logger.error('An error occurred while installing dependencies.'); - logger.log(dependencyResult); - process.exit(1); - } - } -} - /** * Detect if any babel dependencies need to be added to the project - * @param {Object} npmOptions Passed along to `latestVersion` and `getVersion` * @param {Object} packageJson The current package.json so we can inspect its contents * @returns {Array} Contains the packages and versions that need to be installed * @example - * const babelDependencies = await getBabelDependencies(npmOptions, packageJson); + * const babelDependencies = await getBabelDependencies(packageManager, npmOptions, packageJson); * // you can then spread the result when using installDependencies * installDependencies(npmOptions, [ * `@storybook/react@${storybookVersion}`, * ...babelDependencies, * ]); */ -export async function getBabelDependencies(npmOptions: NpmOptions, packageJson: PackageJson) { +export async function getBabelDependencies( + packageManager: JsPackageManager, + packageJson: PackageJsonWithDepsAndDevDeps +) { const dependenciesToAdd = []; let babelLoaderVersion = '^8.0.0-0'; @@ -294,12 +128,11 @@ export async function getBabelDependencies(npmOptions: NpmOptions, packageJson: if (!babelCoreVersion) { if (!packageJson.dependencies['@babel/core'] && !packageJson.devDependencies['@babel/core']) { - const babelCoreInstallVersion = await getVersion(npmOptions, '@babel/core'); + const babelCoreInstallVersion = await packageManager.getVersion('@babel/core'); dependenciesToAdd.push(`@babel/core@${babelCoreInstallVersion}`); } } else { - const latestCompatibleBabelVersion = await latestVersion( - npmOptions, + const latestCompatibleBabelVersion = await packageManager.latestVersion( 'babel-core', babelCoreVersion ); @@ -310,8 +143,7 @@ export async function getBabelDependencies(npmOptions: NpmOptions, packageJson: } if (!packageJson.dependencies['babel-loader'] && !packageJson.devDependencies['babel-loader']) { - const babelLoaderInstallVersion = await getVersion( - npmOptions, + const babelLoaderInstallVersion = await packageManager.getVersion( 'babel-loader', babelLoaderVersion ); diff --git a/lib/cli/src/initiate.ts b/lib/cli/src/initiate.ts index edaf6127e75e..a204d606a33f 100644 --- a/lib/cli/src/initiate.ts +++ b/lib/cli/src/initiate.ts @@ -2,20 +2,13 @@ import { UpdateNotifier, IPackage } from 'update-notifier'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { detect, isStorybookInstalled, detectLanguage } from './detect'; -import { hasYarn } from './has_yarn'; import { installableProjectTypes, ProjectType, StoryFormat, SupportedLanguage, } from './project_types'; -import { - commandLog, - codeLog, - paddedLog, - installDepsFromPackageJson, - getPackageJson, -} from './helpers'; +import { commandLog, codeLog, paddedLog } from './helpers'; import angularGenerator from './generators/ANGULAR'; import emberGenerator from './generators/EMBER'; import meteorGenerator from './generators/METEOR'; @@ -36,6 +29,8 @@ import preactGenerator from './generators/PREACT'; import svelteGenerator from './generators/SVELTE'; import raxGenerator from './generators/RAX'; import { warn } from './warn'; +import { JsPackageManagerFactory, readPackageJson } from './js-package-manager'; +import { NpmOptions } from './NpmOptions'; const logger = console; @@ -51,10 +46,9 @@ type CommandOptions = { }; const installStorybook = (projectType: ProjectType, options: CommandOptions): Promise => { - const useYarn = Boolean(options.useNpm !== true) && hasYarn(); + const packageManager = JsPackageManagerFactory.getPackageManager(options.useNpm); - const npmOptions = { - useYarn, + const npmOptions: NpmOptions = { installAsDevDependencies: true, skipInstall: options.skipInstall, }; @@ -69,15 +63,13 @@ const installStorybook = (projectType: ProjectType, options: CommandOptions): Pr storyFormat: options.storyFormat || defaultStoryFormat, }; - const runStorybookCommand = useYarn ? 'yarn storybook' : 'npm run storybook'; - const end = () => { if (!options.skipInstall) { - installDepsFromPackageJson(npmOptions); + packageManager.installDependencies(); } logger.log('\nTo run your storybook, type:\n'); - codeLog([runStorybookCommand]); + codeLog([packageManager.getRunStorybookCommand()]); logger.log('\nFor more information visit:', chalk.cyan('https://storybook.js.org')); // Add a new line for the clear visibility. @@ -100,18 +92,18 @@ const installStorybook = (projectType: ProjectType, options: CommandOptions): Pr return Promise.resolve(); case ProjectType.UPDATE_PACKAGE_ORGANIZATIONS: - return updateOrganisationsGenerator(options.parser, npmOptions) + return updateOrganisationsGenerator(packageManager, options.parser, npmOptions) .then(() => null) // commmandLog doesn't like to see output .then(commandLog('Upgrading your project to the new storybook packages.')) .then(end); case ProjectType.REACT_SCRIPTS: - return reactScriptsGenerator(npmOptions, generatorOptions) + return reactScriptsGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Create React App" based project')) .then(end); case ProjectType.REACT: - return reactGenerator(npmOptions, generatorOptions) + return reactGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "React" app')) .then(end); @@ -128,7 +120,9 @@ const installStorybook = (projectType: ProjectType, options: CommandOptions): Pr }, ]) as Promise<{ server: boolean }>) ) - .then(({ server }) => reactNativeGenerator(npmOptions, server, generatorOptions)) + .then(({ server }) => + reactNativeGenerator(packageManager, npmOptions, server, generatorOptions) + ) .then(commandLog('Adding storybook support to your "React Native" app')) .then(end) .then(() => { @@ -142,82 +136,82 @@ const installStorybook = (projectType: ProjectType, options: CommandOptions): Pr } case ProjectType.METEOR: - return meteorGenerator(npmOptions, generatorOptions) + return meteorGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Meteor" app')) .then(end); case ProjectType.WEBPACK_REACT: - return webpackReactGenerator(npmOptions, generatorOptions) + return webpackReactGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Webpack React" app')) .then(end); case ProjectType.REACT_PROJECT: - return reactGenerator(npmOptions, generatorOptions) + return reactGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "React" library')) .then(end); case ProjectType.SFC_VUE: - return sfcVueGenerator(npmOptions, generatorOptions) + return sfcVueGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Single File Components Vue" app')) .then(end); case ProjectType.VUE: - return vueGenerator(npmOptions, generatorOptions) + return vueGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Vue" app')) .then(end); case ProjectType.ANGULAR: - return angularGenerator(npmOptions, generatorOptions) + return angularGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Angular" app')) .then(end); case ProjectType.EMBER: - return emberGenerator(npmOptions, generatorOptions) + return emberGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Ember" app')) .then(end); case ProjectType.MITHRIL: - return mithrilGenerator(npmOptions, generatorOptions) + return mithrilGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Mithril" app')) .then(end); case ProjectType.MARIONETTE: - return marionetteGenerator(npmOptions, generatorOptions) + return marionetteGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Marionette.js" app')) .then(end); case ProjectType.MARKO: - return markoGenerator(npmOptions, generatorOptions) + return markoGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Marko" app')) .then(end); case ProjectType.HTML: - return htmlGenerator(npmOptions, generatorOptions) + return htmlGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "HTML" app')) .then(end); case ProjectType.WEB_COMPONENTS: - return webComponentsGenerator(npmOptions, generatorOptions) + return webComponentsGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "web components" app')) .then(end); case ProjectType.RIOT: - return riotGenerator(npmOptions, generatorOptions) + return riotGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "riot.js" app')) .then(end); case ProjectType.PREACT: - return preactGenerator(npmOptions, generatorOptions) + return preactGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Preact" app')) .then(end); case ProjectType.SVELTE: - return svelteGenerator(npmOptions, generatorOptions) + return svelteGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Svelte" app')) .then(end); case ProjectType.RAX: - return raxGenerator(npmOptions, generatorOptions) + return raxGenerator(packageManager, npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Rax" app')) .then(end); @@ -286,7 +280,7 @@ export default function (options: CommandOptions, pkg: IPackage): Promise try { if (projectTypeProvided) { if (installableProjectTypes.includes(options.type)) { - const storybookInstalled = isStorybookInstalled(getPackageJson(), options.force); + const storybookInstalled = isStorybookInstalled(readPackageJson(), options.force); projectType = storybookInstalled ? ProjectType.ALREADY_HAS_STORYBOOK : options.type.toUpperCase(); diff --git a/lib/cli/src/js-package-manager/JsPackageManager.ts b/lib/cli/src/js-package-manager/JsPackageManager.ts new file mode 100644 index 000000000000..eeb52bfa4be7 --- /dev/null +++ b/lib/cli/src/js-package-manager/JsPackageManager.ts @@ -0,0 +1,231 @@ +import chalk from 'chalk'; +import { gt, satisfies } from '@storybook/semver'; +import { sync as spawnSync } from 'cross-spawn'; +import { commandLog } from '../helpers'; +import { PackageJson, PackageJsonWithDepsAndDevDeps } from './PackageJson'; +import { readPackageJson, writePackageJson } from './PackageJsonHelper'; + +const logger = console; +// Cannot be `import` as it's not under TS root dir +const { storybookCLIVersion, devDependencies } = require('../../package.json'); + +export abstract class JsPackageManager { + public abstract initPackageJson(): void; + + public abstract getRunStorybookCommand(): string; + + /** + * Install dependencies listed in `package.json` + */ + public installDependencies(): void { + let done = commandLog('Preparing to install dependencies'); + done(); + logger.log(); + + logger.log(); + done = commandLog('Installing dependencies'); + + try { + this.runInstall(); + } catch (e) { + done('An error occurred while installing dependencies.'); + process.exit(1); + } + done(); + } + + public retrievePackageJson(): PackageJsonWithDepsAndDevDeps { + let packageJson = readPackageJson(); + if (!packageJson) { + // It will create a new package.json file + this.initPackageJson(); + + // read the newly created package.json file + packageJson = readPackageJson() || {}; + } + + return { + ...packageJson, + dependencies: { ...packageJson.dependencies }, + devDependencies: { ...packageJson.devDependencies }, + }; + } + + /** + * Add dependencies to a project using `yarn add` or `npm install`. + * + * @param {Object} options contains `skipInstall`, `packageJson` and `installAsDevDependencies` which we use to determine how we install packages. + * @param {Array} dependencies contains a list of packages to add. + * @example + * addDependencies(options, [ + * `@storybook/react@${storybookVersion}`, + * `@storybook/addon-actions@${actionsVersion}`, + * `@storybook/addon-links@${linksVersion}`, + * `@storybook/addons@${addonsVersion}`, + * ]); + */ + public addDependencies( + options: { + skipInstall?: boolean; + installAsDevDependencies?: boolean; + packageJson?: PackageJson; + }, + dependencies: string[] + ): void { + const { skipInstall } = options; + + if (skipInstall) { + const { packageJson } = options; + + const dependenciesMap = dependencies.reduce((acc, dep) => { + const idx = dep.lastIndexOf('@'); + const packageName = dep.slice(0, idx); + const packageVersion = dep.slice(idx + 1); + + return { ...acc, [packageName]: packageVersion }; + }, {}); + + if (options.installAsDevDependencies) { + packageJson.devDependencies = { + ...packageJson.devDependencies, + ...dependenciesMap, + }; + } else { + packageJson.dependencies = { + ...packageJson.dependencies, + ...dependenciesMap, + }; + } + + writePackageJson(packageJson); + } else { + try { + this.runAddDeps(dependencies, options.installAsDevDependencies); + } catch (e) { + logger.error('An error occurred while installing dependencies.'); + logger.log(e.message); + process.exit(1); + } + } + } + + /** + * Return an array of strings matching following format: `@` + * + * @param packageNames + */ + public getVersionedPackages(...packageNames: string[]): Promise { + return Promise.all( + packageNames.map( + async (packageName) => `${packageName}@${await this.getVersion(packageName)}` + ) + ); + } + + /** + * Return an array of string standing for the latest version of the input packages. + * To be able to identify which version goes with which package the order of the input array is keep. + * + * @param packageNames + */ + public getVersions(...packageNames: string[]): Promise { + return Promise.all(packageNames.map((packageName) => this.getVersion(packageName))); + } + + public async getVersion(packageName: string, constraint?: string): Promise { + let current; + if (packageName === '@storybook/cli') { + current = storybookCLIVersion; + } else if (/storybook/.test(packageName)) { + current = devDependencies[packageName]; + } + + let latest; + try { + latest = await this.latestVersion(packageName, constraint); + } catch (e) { + if (current) { + logger.warn(`\n ${chalk.yellow(e.message)}`); + return current; + } + + logger.error(`\n ${chalk.red(e.message)}`); + process.exit(1); + } + + const versionToUse = + current && (!constraint || satisfies(current, constraint)) && gt(current, latest) + ? current + : latest; + return `^${versionToUse}`; + } + + /** + * Get the latest version of the package available on npmjs.com. + * If constraint is set then it returns a version satisfying it, otherwise the latest version available is returned. + * + * @param packageName Name of the package + * @param constraint Version range to use to constraint the returned version + */ + public async latestVersion(packageName: string, constraint?: string): Promise { + if (!constraint) { + return this.runGetVersions(packageName, false); + } + + const versions = await this.runGetVersions(packageName, true); + + // Get the latest version satisfying the constraint + return versions.reverse().find((version) => satisfies(version, constraint)); + } + + public addStorybookCommandInScripts(options?: { port: number; staticFolder?: string }) { + const packageJson = this.retrievePackageJson(); + + const sbPort = options?.port ?? 6006; + const storybookCmd = options?.staticFolder + ? `start-storybook -p ${sbPort} -s ${options.staticFolder}` + : `start-storybook -p ${sbPort}`; + + const buildStorybookCmd = options?.staticFolder + ? `build-storybook -s ${options.staticFolder}` + : 'build-storybook'; + + writePackageJson({ + ...packageJson, + scripts: { + ...packageJson.scripts, + storybook: storybookCmd, + 'build-storybook': buildStorybookCmd, + }, + }); + } + + protected abstract runInstall(): void; + + protected abstract runAddDeps(dependencies: string[], installAsDevDependencies: boolean): void; + + /** + * Get the latest or all versions of the input package available on npmjs.com + * + * @param packageName Name of the package + * @param fetchAllVersions Should return + */ + protected abstract runGetVersions( + packageName: string, + fetchAllVersions: T + ): // Use generic and conditional type to force `string[]` if fetchAllVersions is true and `string` if false + Promise; + + public executeCommand(command: string, args: string[], stdio?: 'pipe' | 'inherit'): string { + const commandResult = spawnSync(command, args, { + stdio: stdio ?? 'pipe', + encoding: 'utf-8', + }); + + if (commandResult.status !== 0) { + throw new Error(commandResult.stderr ?? ''); + } + + return commandResult.stdout ?? ''; + } +} diff --git a/lib/cli/src/js-package-manager/JsPackageManagerFactory.test.ts b/lib/cli/src/js-package-manager/JsPackageManagerFactory.test.ts new file mode 100644 index 000000000000..8bbe8481ecc7 --- /dev/null +++ b/lib/cli/src/js-package-manager/JsPackageManagerFactory.test.ts @@ -0,0 +1,136 @@ +import { sync as spawnSync } from 'cross-spawn'; +import { sync as findUpSync } from 'find-up'; +import { JsPackageManagerFactory } from './JsPackageManagerFactory'; +import { NPMProxy } from './NPMProxy'; +import { Yarn1Proxy } from './Yarn1Proxy'; +import { Yarn2Proxy } from './Yarn2Proxy'; + +jest.mock('cross-spawn'); +const spawnSyncMock = spawnSync as jest.Mock; + +jest.mock('find-up'); +const findUpSyncMock = (findUpSync as unknown) as jest.Mock; +findUpSyncMock.mockReturnValue(undefined); + +describe('JsPackageManagerFactory', () => { + describe('getPackageManager', () => { + describe('return an NPM proxy', () => { + it('when `useNpm` option is used', () => { + expect(JsPackageManagerFactory.getPackageManager(true)).toBeInstanceOf(NPMProxy); + }); + + it('when NPM command is ok, Yarn is ok, there is no `yarn.lock` file', () => { + spawnSyncMock.mockImplementation((command) => { + return command === 'yarn' + ? { + // Yarn is ok + status: 0, + output: '1.22.4', + } + : { + // NPM is ok + status: 0, + output: '6.5.12', + }; + }); + + // There is no yarn.lock + findUpSyncMock.mockImplementation((file) => (file === 'yarn.lock' ? undefined : '')); + + expect(JsPackageManagerFactory.getPackageManager(false)).toBeInstanceOf(NPMProxy); + }); + }); + + describe('return a Yarn 1 proxy', () => { + it('when Yarn command is ok, Yarn version is <2, NPM is ko', () => { + spawnSyncMock.mockImplementation((command) => { + return command === 'yarn' + ? { + // Yarn is ok + status: 0, + output: '1.22.4', + } + : { + // NPM is ko + status: 1, + }; + }); + + // there is no + findUpSyncMock.mockReturnValue(undefined); + + expect(JsPackageManagerFactory.getPackageManager(false)).toBeInstanceOf(Yarn1Proxy); + }); + + it('when Yarn command is ok, Yarn version is <2, NPM is ok, there is a `yarn.lock` file', () => { + spawnSyncMock.mockImplementation((command) => { + return command === 'yarn' + ? { + // Yarn is ok + status: 0, + output: '1.22.4', + } + : { + // NPM is ok + status: 0, + output: '6.5.12', + }; + }); + + // There is a yarn.lock + findUpSyncMock.mockImplementation((file) => + file === 'yarn.lock' ? '/Users/johndoe/Documents/yarn.lock' : undefined + ); + + expect(JsPackageManagerFactory.getPackageManager(false)).toBeInstanceOf(Yarn1Proxy); + }); + }); + + describe('return a Yarn 2 proxy', () => { + it('when Yarn command is ok, Yarn version is >=2, NPM is ko', () => { + spawnSyncMock.mockImplementation((command) => { + return command === 'yarn' + ? { + // Yarn is ok + status: 0, + output: '2.0.0-rc.33', + } + : { + // NPM is ko + status: 1, + }; + }); + + expect(JsPackageManagerFactory.getPackageManager(false)).toBeInstanceOf(Yarn2Proxy); + }); + + it('when Yarn command is ok, Yarn version is >=2, NPM is ok, there is a `yarn.lock` file', () => { + spawnSyncMock.mockImplementation((command) => { + return command === 'yarn' + ? { + // Yarn is ok + status: 0, + output: '2.0.0-rc.33', + } + : { + // NPM is ok + status: 0, + output: '6.5.12', + }; + }); + + // There is a yarn.lock + findUpSyncMock.mockImplementation((file) => + file === 'yarn.lock' ? '/Users/johndoe/Documents/yarn.lock' : undefined + ); + + expect(JsPackageManagerFactory.getPackageManager(false)).toBeInstanceOf(Yarn2Proxy); + }); + }); + + it('throws an error if Yarn is ko and NPM is ko', () => { + spawnSyncMock.mockReturnValue({ status: 1 }); + expect(() => JsPackageManagerFactory.getPackageManager(false)).toThrow(); + }); + }); +}); diff --git a/lib/cli/src/js-package-manager/JsPackageManagerFactory.ts b/lib/cli/src/js-package-manager/JsPackageManagerFactory.ts new file mode 100644 index 000000000000..6a7c9d38840b --- /dev/null +++ b/lib/cli/src/js-package-manager/JsPackageManagerFactory.ts @@ -0,0 +1,46 @@ +import { sync as spawnSync } from 'cross-spawn'; +import { sync as findUpSync } from 'find-up'; +import { NPMProxy } from './NPMProxy'; +import { JsPackageManager } from './JsPackageManager'; +import { Yarn2Proxy } from './Yarn2Proxy'; +import { Yarn1Proxy } from './Yarn1Proxy'; + +export class JsPackageManagerFactory { + public static getPackageManager(forceNpmUsage = false): JsPackageManager { + if (forceNpmUsage) { + return new NPMProxy(); + } + + const yarnVersion = getYarnVersion(); + const hasYarnLockFile = findUpSync('yarn.lock'); + + const hasNPMCommand = hasNPM(); + + if (yarnVersion && (hasYarnLockFile || !hasNPMCommand)) { + return yarnVersion === 1 ? new Yarn1Proxy() : new Yarn2Proxy(); + } + + if (hasNPMCommand) { + return new NPMProxy(); + } + + throw new Error('Unable to find a usable package manager within NPM, Yarn and Yarn 2'); + } +} + +function hasNPM() { + const npmVersionCommand = spawnSync('npm', ['--version']); + return npmVersionCommand.status === 0; +} + +function getYarnVersion(): 1 | 2 | undefined { + const yarnVersionCommand = spawnSync('yarn', ['--version']); + + if (yarnVersionCommand.status !== 0) { + return undefined; + } + + const yarnVersion = yarnVersionCommand.output.toString().replace(/,/g, '').replace(/"/g, ''); + + return /^1\.+/.test(yarnVersion) ? 1 : 2; +} diff --git a/lib/cli/src/js-package-manager/NPMProxy.test.ts b/lib/cli/src/js-package-manager/NPMProxy.test.ts new file mode 100644 index 000000000000..e152732bab9e --- /dev/null +++ b/lib/cli/src/js-package-manager/NPMProxy.test.ts @@ -0,0 +1,81 @@ +import { NPMProxy } from './NPMProxy'; + +describe('NPM Proxy', () => { + let npmProxy: NPMProxy; + + beforeEach(() => { + npmProxy = new NPMProxy(); + }); + + describe('initPackageJson', () => { + it('should run `npm init -y`', () => { + const executeCommandSpy = jest.spyOn(npmProxy, 'executeCommand').mockReturnValue(''); + + npmProxy.initPackageJson(); + + expect(executeCommandSpy).toHaveBeenCalledWith('npm', ['init', '-y']); + }); + }); + + describe('installDependencies', () => { + it('should run `npm install`', () => { + const executeCommandSpy = jest.spyOn(npmProxy, 'executeCommand').mockReturnValue(''); + + npmProxy.installDependencies(); + + expect(executeCommandSpy).toHaveBeenCalledWith('npm', ['install'], expect.any(String)); + }); + }); + + describe('addDependencies', () => { + it('with devDep it should run `npm install -D @storybook/addons`', () => { + const executeCommandSpy = jest.spyOn(npmProxy, 'executeCommand').mockReturnValue(''); + + npmProxy.addDependencies({ installAsDevDependencies: true }, ['@storybook/addons']); + + expect(executeCommandSpy).toHaveBeenCalledWith( + 'npm', + ['install', '-D', '@storybook/addons'], + expect.any(String) + ); + }); + }); + + describe('latestVersion', () => { + it('without contraint it returns the latest version', async () => { + const executeCommandSpy = jest.spyOn(npmProxy, 'executeCommand').mockReturnValue('"5.3.19"'); + + const version = await npmProxy.latestVersion('@storybook/addons'); + + expect(executeCommandSpy).toHaveBeenCalledWith('npm', [ + 'info', + '@storybook/addons', + 'version', + '--json', + ]); + expect(version).toEqual('5.3.19'); + }); + + it('with contraint it returns the latest version satisfying the constraint', async () => { + const executeCommandSpy = jest + .spyOn(npmProxy, 'executeCommand') + .mockReturnValue('["4.25.3","5.3.19","6.0.0-beta.23"]'); + + const version = await npmProxy.latestVersion('@storybook/addons', '5.X'); + + expect(executeCommandSpy).toHaveBeenCalledWith('npm', [ + 'info', + '@storybook/addons', + 'versions', + '--json', + ]); + expect(version).toEqual('5.3.19'); + }); + + it('throws an error if command output is not a valid JSON', async () => { + jest.spyOn(npmProxy, 'executeCommand').mockReturnValue('NOT A JSON'); + + await expect(npmProxy.latestVersion('@storybook/addons')).rejects.toThrow(); + }); + }); +}); diff --git a/lib/cli/src/js-package-manager/NPMProxy.ts b/lib/cli/src/js-package-manager/NPMProxy.ts new file mode 100644 index 000000000000..a8e4765954ee --- /dev/null +++ b/lib/cli/src/js-package-manager/NPMProxy.ts @@ -0,0 +1,47 @@ +import { JsPackageManager } from './JsPackageManager'; + +export class NPMProxy extends JsPackageManager { + initPackageJson() { + return this.executeCommand('npm', ['init', '-y']); + } + + getRunStorybookCommand(): string { + return 'npm run storybook'; + } + + protected runInstall(): void { + this.executeCommand('npm', ['install'], 'inherit'); + } + + protected runAddDeps(dependencies: string[], installAsDevDependencies: boolean): void { + let args = [...dependencies]; + + if (installAsDevDependencies) { + args = ['-D', ...args]; + } + + this.executeCommand('npm', ['install', ...args], 'inherit'); + } + + protected runGetVersions( + packageName: string, + fetchAllVersions: T + ): Promise { + const args = [fetchAllVersions ? 'versions' : 'version', '--json']; + + const commandResult = this.executeCommand('npm', ['info', packageName, ...args]); + + try { + const parsedOutput = JSON.parse(commandResult); + + if (parsedOutput.error) { + // FIXME: improve error handling + throw new Error(parsedOutput.error.summary); + } else { + return parsedOutput; + } + } catch (e) { + throw new Error(`Unable to find versions of ${packageName} using yarn`); + } + } +} diff --git a/lib/cli/src/js-package-manager/PackageJson.ts b/lib/cli/src/js-package-manager/PackageJson.ts new file mode 100644 index 000000000000..3a1cf2c9d2a1 --- /dev/null +++ b/lib/cli/src/js-package-manager/PackageJson.ts @@ -0,0 +1,9 @@ +export type PackageJson = { + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + scripts?: Record; +}; + +export type PackageJsonWithDepsAndDevDeps = PackageJson & + Required>; diff --git a/lib/cli/src/js-package-manager/PackageJsonHelper.ts b/lib/cli/src/js-package-manager/PackageJsonHelper.ts new file mode 100644 index 000000000000..17fa72935f0e --- /dev/null +++ b/lib/cli/src/js-package-manager/PackageJsonHelper.ts @@ -0,0 +1,20 @@ +import path from 'path'; +import fs from 'fs'; +import { PackageJson } from './PackageJson'; + +export function readPackageJson(): PackageJson | false { + const packageJsonPath = path.resolve('package.json'); + if (!fs.existsSync(packageJsonPath)) { + return false; + } + + const jsonContent = fs.readFileSync(packageJsonPath, 'utf8'); + return JSON.parse(jsonContent); +} + +export function writePackageJson(packageJson: PackageJson) { + const content = `${JSON.stringify(packageJson, null, 2)}\n`; + const packageJsonPath = path.resolve('package.json'); + + fs.writeFileSync(packageJsonPath, content, 'utf8'); +} diff --git a/lib/cli/src/js-package-manager/Yarn1Proxy.test.ts b/lib/cli/src/js-package-manager/Yarn1Proxy.test.ts new file mode 100644 index 000000000000..0db351c79bff --- /dev/null +++ b/lib/cli/src/js-package-manager/Yarn1Proxy.test.ts @@ -0,0 +1,83 @@ +import { Yarn1Proxy } from './Yarn1Proxy'; + +describe('Yarn 1 Proxy', () => { + let yarn1Proxy: Yarn1Proxy; + + beforeEach(() => { + yarn1Proxy = new Yarn1Proxy(); + }); + + describe('initPackageJson', () => { + it('should run `yarn init -y`', () => { + const executeCommandSpy = jest.spyOn(yarn1Proxy, 'executeCommand').mockReturnValue(''); + + yarn1Proxy.initPackageJson(); + + expect(executeCommandSpy).toHaveBeenCalledWith('yarn', ['init', '-y']); + }); + }); + + describe('installDependencies', () => { + it('should run `yarn`', () => { + const executeCommandSpy = jest.spyOn(yarn1Proxy, 'executeCommand').mockReturnValue(''); + + yarn1Proxy.installDependencies(); + + expect(executeCommandSpy).toHaveBeenCalledWith('yarn', [], expect.any(String)); + }); + }); + + describe('addDependencies', () => { + it('with devDep it should run `yarn install -D --ignore-workspace-root-check @storybook/addons`', () => { + const executeCommandSpy = jest.spyOn(yarn1Proxy, 'executeCommand').mockReturnValue(''); + + yarn1Proxy.addDependencies({ installAsDevDependencies: true }, ['@storybook/addons']); + + expect(executeCommandSpy).toHaveBeenCalledWith( + 'yarn', + ['add', '-D', '--ignore-workspace-root-check', '@storybook/addons'], + expect.any(String) + ); + }); + }); + + describe('latestVersion', () => { + it('without contraint it returns the latest version', async () => { + const executeCommandSpy = jest + .spyOn(yarn1Proxy, 'executeCommand') + .mockReturnValue('{"type":"inspect","data":"5.3.19"}'); + + const version = await yarn1Proxy.latestVersion('@storybook/addons'); + + expect(executeCommandSpy).toHaveBeenCalledWith('yarn', [ + 'info', + '@storybook/addons', + 'version', + '--json', + ]); + expect(version).toEqual('5.3.19'); + }); + + it('with contraint it returns the latest version satisfying the constraint', async () => { + const executeCommandSpy = jest + .spyOn(yarn1Proxy, 'executeCommand') + .mockReturnValue('{"type":"inspect","data":["4.25.3","5.3.19","6.0.0-beta.23"]}'); + + const version = await yarn1Proxy.latestVersion('@storybook/addons', '5.X'); + + expect(executeCommandSpy).toHaveBeenCalledWith('yarn', [ + 'info', + '@storybook/addons', + 'versions', + '--json', + ]); + expect(version).toEqual('5.3.19'); + }); + + it('throws an error if command output is not a valid JSON', async () => { + jest.spyOn(yarn1Proxy, 'executeCommand').mockReturnValue('NOT A JSON'); + + await expect(yarn1Proxy.latestVersion('@storybook/addons')).rejects.toThrow(); + }); + }); +}); diff --git a/lib/cli/src/js-package-manager/Yarn1Proxy.ts b/lib/cli/src/js-package-manager/Yarn1Proxy.ts new file mode 100644 index 000000000000..af09bf197214 --- /dev/null +++ b/lib/cli/src/js-package-manager/Yarn1Proxy.ts @@ -0,0 +1,44 @@ +import { JsPackageManager } from './JsPackageManager'; + +export class Yarn1Proxy extends JsPackageManager { + initPackageJson() { + return this.executeCommand('yarn', ['init', '-y']); + } + + getRunStorybookCommand(): string { + return 'yarn storybook'; + } + + protected runInstall(): void { + this.executeCommand('yarn', [], 'inherit'); + } + + protected runAddDeps(dependencies: string[], installAsDevDependencies: boolean): void { + let args = ['--ignore-workspace-root-check', ...dependencies]; + + if (installAsDevDependencies) { + args = ['-D', ...args]; + } + + this.executeCommand('yarn', ['add', ...args], 'inherit'); + } + + protected runGetVersions( + packageName: string, + fetchAllVersions: T + ): Promise { + const args = [fetchAllVersions ? 'versions' : 'version', '--json']; + + const commandResult = this.executeCommand('yarn', ['info', packageName, ...args]); + + try { + const parsedOutput = JSON.parse(commandResult); + if (parsedOutput.type === 'inspect') { + return parsedOutput.data; + } + throw new Error(`Unable to find versions of ${packageName} using yarn`); + } catch (e) { + throw new Error(`Unable to find versions of ${packageName} using yarn`); + } + } +} diff --git a/lib/cli/src/js-package-manager/Yarn2Proxy.test.ts b/lib/cli/src/js-package-manager/Yarn2Proxy.test.ts new file mode 100644 index 000000000000..3c3fc17ba799 --- /dev/null +++ b/lib/cli/src/js-package-manager/Yarn2Proxy.test.ts @@ -0,0 +1,89 @@ +import { Yarn2Proxy } from './Yarn2Proxy'; + +describe('Yarn 1 Proxy', () => { + let yarn2Proxy: Yarn2Proxy; + + beforeEach(() => { + yarn2Proxy = new Yarn2Proxy(); + }); + + describe('initPackageJson', () => { + it('should run `yarn init`', () => { + const executeCommandSpy = jest.spyOn(yarn2Proxy, 'executeCommand').mockReturnValue(''); + + yarn2Proxy.initPackageJson(); + + expect(executeCommandSpy).toHaveBeenCalledWith('yarn', ['init']); + }); + }); + + describe('installDependencies', () => { + it('should run `yarn`', () => { + const executeCommandSpy = jest.spyOn(yarn2Proxy, 'executeCommand').mockReturnValue(''); + + yarn2Proxy.installDependencies(); + + expect(executeCommandSpy).toHaveBeenCalledWith('yarn', [], expect.any(String)); + }); + }); + + describe('addDependencies', () => { + it('with devDep it should run `yarn install -D @storybook/addons`', () => { + const executeCommandSpy = jest.spyOn(yarn2Proxy, 'executeCommand').mockReturnValue(''); + + yarn2Proxy.addDependencies({ installAsDevDependencies: true }, ['@storybook/addons']); + + expect(executeCommandSpy).toHaveBeenCalledWith( + 'yarn', + ['add', '-D', '@storybook/addons'], + expect.any(String) + ); + }); + }); + + describe('latestVersion', () => { + it('without contraint it returns the latest version', async () => { + const executeCommandSpy = jest + .spyOn(yarn2Proxy, 'executeCommand') + .mockReturnValue('{"name":"@storybook/addons","version":"5.3.19"}'); + + const version = await yarn2Proxy.latestVersion('@storybook/addons'); + + expect(executeCommandSpy).toHaveBeenCalledWith('yarn', [ + 'npm', + 'info', + '@storybook/addons', + '--fields', + 'version', + '--json', + ]); + expect(version).toEqual('5.3.19'); + }); + + it('with contraint it returns the latest version satisfying the constraint', async () => { + const executeCommandSpy = jest + .spyOn(yarn2Proxy, 'executeCommand') + .mockReturnValue( + '{"name":"@storybook/addons","versions":["4.25.3","5.3.19","6.0.0-beta.23"]}' + ); + + const version = await yarn2Proxy.latestVersion('@storybook/addons', '5.X'); + + expect(executeCommandSpy).toHaveBeenCalledWith('yarn', [ + 'npm', + 'info', + '@storybook/addons', + '--fields', + 'versions', + '--json', + ]); + expect(version).toEqual('5.3.19'); + }); + + it('throws an error if command output is not a valid JSON', async () => { + jest.spyOn(yarn2Proxy, 'executeCommand').mockReturnValue('NOT A JSON'); + + await expect(yarn2Proxy.latestVersion('@storybook/addons')).rejects.toThrow(); + }); + }); +}); diff --git a/lib/cli/src/js-package-manager/Yarn2Proxy.ts b/lib/cli/src/js-package-manager/Yarn2Proxy.ts new file mode 100644 index 000000000000..2b7783bb58be --- /dev/null +++ b/lib/cli/src/js-package-manager/Yarn2Proxy.ts @@ -0,0 +1,42 @@ +import { JsPackageManager } from './JsPackageManager'; + +export class Yarn2Proxy extends JsPackageManager { + initPackageJson() { + return this.executeCommand('yarn', ['init']); + } + + getRunStorybookCommand(): string { + return 'yarn storybook'; + } + + protected runInstall(): void { + this.executeCommand('yarn', [], 'inherit'); + } + + protected runAddDeps(dependencies: string[], installAsDevDependencies: boolean): void { + let args = [...dependencies]; + + if (installAsDevDependencies) { + args = ['-D', ...args]; + } + + this.executeCommand('yarn', ['add', ...args], 'inherit'); + } + + protected runGetVersions( + packageName: string, + fetchAllVersions: T + ): Promise { + const field = fetchAllVersions ? 'versions' : 'version'; + const args = ['--fields', field, '--json']; + + const commandResult = this.executeCommand('yarn', ['npm', 'info', packageName, ...args]); + + try { + const parsedOutput = JSON.parse(commandResult); + return parsedOutput[field]; + } catch (e) { + throw new Error(`Unable to find versions of ${packageName} using yarn 2`); + } + } +} diff --git a/lib/cli/src/js-package-manager/index.ts b/lib/cli/src/js-package-manager/index.ts new file mode 100644 index 000000000000..13cc52f4ed04 --- /dev/null +++ b/lib/cli/src/js-package-manager/index.ts @@ -0,0 +1,4 @@ +export * from './JsPackageManagerFactory'; +export * from './JsPackageManager'; +export * from './PackageJson'; +export * from './PackageJsonHelper'; diff --git a/lib/cli/src/latest_version.ts b/lib/cli/src/latest_version.ts deleted file mode 100644 index 7015aa68c287..000000000000 --- a/lib/cli/src/latest_version.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { spawn, sync } from 'cross-spawn'; -import { satisfies } from '@storybook/semver'; -import { hasYarn2 } from './has_yarn'; - -/** - * Get the latest version of the package available on npmjs.com. - * If constraint is set then it returns a version satisfying it, otherwise the latest version available is returned. - * - * @param {Object} npmOptions Object containing a `useYarn: boolean` attribute - * @param {string} packageName Name of the package - * @param {Object} constraint Version range to use to constraint the returned version - * @return {Promise} Promise resolved with a version - */ -export async function latestVersion( - npmOptions: { useYarn: boolean }, - packageName: string, - constraint?: any -): Promise { - let versions: string | string[]; - - // TODO: Refactor things to hide the package manager details: - // Create a `PackageManager` interface that expose some functions like `version`, `add` etc - // and then create classes that handle the npm/yarn/yarn2 specific behavior - if (npmOptions.useYarn) { - if (hasYarn2()) { - versions = await spawnVersionsWithYarn2(packageName, constraint); - } else { - versions = await spawnVersionsWithYarn(packageName, constraint); - } - } else { - versions = await spawnVersionsWithNpm(packageName, constraint); - } - - if (!constraint) { - return versions as string; - } - - return (versions as string[]).reverse().find((version) => satisfies(version, constraint)); -} - -/** - * Get latest version(s) of the package available on npmjs.com using NPM - * - * @param {string} packageName Name of the package - * @param {Object} constraint Version range to use to constraint the returned version - * @returns {Promise>} versions Promise resolved with a version or an array of versions - */ -function spawnVersionsWithNpm(packageName: string, constraint: any): Promise { - return new Promise((resolve, reject) => { - const command = spawn( - 'npm', - ['info', packageName, constraint ? 'versions' : 'version', '--json', '--silent'], - { - cwd: process.cwd(), - env: process.env, - stdio: 'pipe', - } - ); - - command.stdout.on('data', (data) => { - try { - const info = JSON.parse(data); - if (info.error) { - reject(new Error(info.error.summary)); - } else { - resolve(info); - } - } catch (e) { - reject(new Error(`Unable to find versions of ${packageName} using npm`)); - } - }); - }); -} - -/** - * Get latest version(s) of the package available on npmjs.com using Yarn - * - * @param {string} packageName Name of the package - * @param {Object} constraint Version range to use to constraint the returned version - * @returns {Promise>} versions Promise resolved with a version or an array of versions - */ -function spawnVersionsWithYarn(packageName: string, constraint: any): Promise { - return new Promise((resolve, reject) => { - const command = spawn( - 'yarn', - ['info', packageName, constraint ? 'versions' : 'version', '--json', '--silent'], - { - cwd: process.cwd(), - env: process.env, - stdio: 'pipe', - } - ); - - command.stdout.on('data', (data) => { - try { - const info = JSON.parse(data); - if (info.type === 'inspect') { - resolve(info.data); - } - } catch (e) { - reject(new Error(`Unable to find versions of ${packageName} using yarn`)); - } - }); - - command.stderr.on('data', (data) => { - const info = JSON.parse(data); - if (info.type === 'error') { - reject(new Error(info.data)); - } - }); - }); -} - -/** - * Get latest version(s) of the package available on npmjs.com using Yarn 2 a.k.a Berry - * - * @param {string} packageName Name of the package - * @param {Object} constraint Version range to use to constraint the returned version - * @returns {Promise>} versions Promise resolved with a version or an array of versions - */ -async function spawnVersionsWithYarn2( - packageName: string, - constraint: any -): Promise { - const field = constraint ? 'versions' : 'version'; - - const commandResult = sync('yarn', ['npm', 'info', packageName, '--fields', field, '--json'], { - cwd: process.cwd(), - env: process.env, - stdio: 'pipe', - encoding: 'utf-8', - }); - - if (commandResult.status !== 0) { - throw new Error(commandResult.stderr.toString()); - } - - try { - const parsedOutput = JSON.parse(commandResult.stdout.toString()); - return parsedOutput[field]; - } catch (e) { - throw new Error(`Unable to find versions of ${packageName} using yarn 2`); - } -} diff --git a/lib/cli/src/npm_init.ts b/lib/cli/src/npm_init.ts deleted file mode 100644 index f409557809fc..000000000000 --- a/lib/cli/src/npm_init.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { sync } from 'cross-spawn'; -import { hasYarn } from './has_yarn'; - -const packageManager = hasYarn() ? 'yarn' : 'npm'; - -export function npmInit(): string { - const results = sync(packageManager, ['init', '-y'], { - cwd: process.cwd(), - env: process.env, - stdio: 'pipe', - encoding: 'utf-8', - }); - return results.stdout; -}