mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-31 05:03:21 +08:00
Merge pull request #19312 from storybookjs/shilman/fix-sb-add
CLI: Update sb add for main.js
This commit is contained in:
commit
c05ee07dbf
@ -1,209 +0,0 @@
|
||||
import {
|
||||
addStorybookAddonToFile,
|
||||
storybookAddonScope,
|
||||
getPackageName,
|
||||
getInstalledStorybookVersion,
|
||||
getPackageArg,
|
||||
} from './add';
|
||||
|
||||
describe('addStorybookAddonToFile should correctly register an Storybook addon', () => {
|
||||
test('to an empty array', () => {
|
||||
expect(addStorybookAddonToFile('addon-name', [], true)).toEqual([
|
||||
`import '${storybookAddonScope}addon-name/manager';`,
|
||||
]);
|
||||
});
|
||||
|
||||
test('to an empty file', () => {
|
||||
expect(addStorybookAddonToFile('addon-name', [''], true)).toEqual([
|
||||
`import '${storybookAddonScope}addon-name/manager';`,
|
||||
'',
|
||||
]);
|
||||
});
|
||||
|
||||
test('to an addons file with existing addons registered', () => {
|
||||
expect(
|
||||
addStorybookAddonToFile(
|
||||
'addon-name',
|
||||
[
|
||||
"import '@storybook/addon-actions/manager';",
|
||||
"import '@storybook/addon-links/manager';",
|
||||
'',
|
||||
],
|
||||
true
|
||||
)
|
||||
).toEqual([
|
||||
`import '${storybookAddonScope}addon-name/manager';`,
|
||||
"import '@storybook/addon-actions/manager';",
|
||||
"import '@storybook/addon-links/manager';",
|
||||
'',
|
||||
]);
|
||||
});
|
||||
|
||||
test('to an addons file with more than only imports', () => {
|
||||
expect(
|
||||
addStorybookAddonToFile(
|
||||
'addon-name',
|
||||
[
|
||||
"import '@storybook/addon-links/manager';",
|
||||
"import '@storybook/addon-actions/manager';",
|
||||
'',
|
||||
'//some other stuff',
|
||||
'',
|
||||
'and more stuff',
|
||||
'',
|
||||
],
|
||||
true
|
||||
)
|
||||
).toEqual([
|
||||
`import '${storybookAddonScope}addon-name/manager';`,
|
||||
"import '@storybook/addon-links/manager';",
|
||||
"import '@storybook/addon-actions/manager';",
|
||||
'',
|
||||
'//some other stuff',
|
||||
'',
|
||||
'and more stuff',
|
||||
'',
|
||||
]);
|
||||
});
|
||||
|
||||
test('to an addon file with it already being installed by not duplicating it', () => {
|
||||
expect(
|
||||
addStorybookAddonToFile(
|
||||
'addon-name',
|
||||
[
|
||||
"import '@storybook/addon-actions/manager';",
|
||||
"import '@storybook/addon-links/manager';",
|
||||
`import '${storybookAddonScope}addon-name/manager';`,
|
||||
'',
|
||||
],
|
||||
true
|
||||
)
|
||||
).toEqual([
|
||||
"import '@storybook/addon-actions/manager';",
|
||||
"import '@storybook/addon-links/manager';",
|
||||
`import '${storybookAddonScope}addon-name/manager';`,
|
||||
'',
|
||||
]);
|
||||
});
|
||||
|
||||
test('to an addons file if it is not an official addon', () => {
|
||||
expect(
|
||||
addStorybookAddonToFile(
|
||||
'addon-name',
|
||||
[
|
||||
"import '@storybook/addon-actions/manager';",
|
||||
"import '@storybook/addon-links/manager';",
|
||||
'',
|
||||
],
|
||||
false
|
||||
)
|
||||
).toEqual([
|
||||
`import 'addon-name/manager';`,
|
||||
"import '@storybook/addon-actions/manager';",
|
||||
"import '@storybook/addon-links/manager';",
|
||||
'',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPackageName should correctly return the full package name', () => {
|
||||
test('on a normal addon', () => {
|
||||
const name = 'normal-addon';
|
||||
expect(getPackageName(name, false)).toBe(name);
|
||||
});
|
||||
test('on an official addon', () => {
|
||||
const name = 'official-addon';
|
||||
expect(getPackageName(name, true)).toBe(storybookAddonScope + name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInstalledStorybookVersion should return the correct Storybook version', () => {
|
||||
test('when single official Storybook package is installed', () => {
|
||||
expect(
|
||||
getInstalledStorybookVersion({
|
||||
devDependencies: {
|
||||
'@storybook/react': '^4.0.0-alpha.22',
|
||||
},
|
||||
})
|
||||
).toBe('^4.0.0-alpha.22');
|
||||
});
|
||||
|
||||
test('when no official Storybook package is installed', () => {
|
||||
expect(
|
||||
getInstalledStorybookVersion({
|
||||
devDependencies: {
|
||||
'random package': '^4.0.0-alpha.22',
|
||||
},
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('when an unofficial package with "storybook" in its name is installed', () => {
|
||||
expect(
|
||||
getInstalledStorybookVersion({
|
||||
devDependencies: {
|
||||
'not-storybook': '^4.0.0-alpha.22',
|
||||
},
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPackageArg returns the correct package argument to install', () => {
|
||||
const officialAddonName = 'knob';
|
||||
const randomAddonName = 'random';
|
||||
const officialAddonNameWithTag = `${officialAddonName}@alpha`;
|
||||
const randomAddonNameWithTag = `${randomAddonName}@latest`;
|
||||
|
||||
test('when it is an official Storybook addon without any Storybook package installed', () => {
|
||||
expect(
|
||||
getPackageArg(officialAddonName, true, {
|
||||
devDependencies: {},
|
||||
})
|
||||
).toBe(officialAddonName);
|
||||
});
|
||||
|
||||
test('when it is a random addon without any Storybook package installed', () => {
|
||||
expect(
|
||||
getPackageArg(randomAddonName, true, {
|
||||
devDependencies: {},
|
||||
})
|
||||
).toBe(randomAddonName);
|
||||
});
|
||||
|
||||
test('when it is a random addon with tag without any Storybook package installed', () => {
|
||||
expect(
|
||||
getPackageArg(randomAddonNameWithTag, true, {
|
||||
devDependencies: {},
|
||||
})
|
||||
).toBe(randomAddonNameWithTag);
|
||||
});
|
||||
|
||||
test('when it is an official addon with tag without any Storybook package installed', () => {
|
||||
expect(
|
||||
getPackageArg(officialAddonNameWithTag, true, {
|
||||
devDependencies: {},
|
||||
})
|
||||
).toBe(officialAddonNameWithTag);
|
||||
});
|
||||
|
||||
test('when it is an official addon with tag with a Storybook package installed', () => {
|
||||
expect(
|
||||
getPackageArg(officialAddonNameWithTag, true, {
|
||||
devDependencies: {
|
||||
'@storybook/html': '^4.0.0-alpha.21',
|
||||
},
|
||||
})
|
||||
).toBe(`${officialAddonName}@^4.0.0-alpha.21`);
|
||||
});
|
||||
|
||||
test('when it is an official addon with a Storybook package installed', () => {
|
||||
expect(
|
||||
getPackageArg(officialAddonName, true, {
|
||||
devDependencies: {
|
||||
'@storybook/html': '^4.0.0-alpha.21',
|
||||
},
|
||||
})
|
||||
).toBe(`${officialAddonName}@^4.0.0-alpha.21`);
|
||||
});
|
||||
});
|
@ -1,102 +1,14 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { sync as spawnSync } from 'cross-spawn';
|
||||
|
||||
import { getStorybookInfo } from '@storybook/core-common';
|
||||
import { readConfig, writeConfig } from '@storybook/csf-tools';
|
||||
|
||||
import { commandLog } from './helpers';
|
||||
import { JsPackageManager, JsPackageManagerFactory, PackageJson } from './js-package-manager';
|
||||
import { JsPackageManagerFactory } from './js-package-manager';
|
||||
|
||||
const logger = console;
|
||||
export const storybookAddonScope = '@storybook/addon-';
|
||||
|
||||
const isAddon = async (packageManager: JsPackageManager, name: string) => {
|
||||
try {
|
||||
await packageManager.latestVersion(name);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isStorybookAddon = async (packageManager: JsPackageManager, name: string) =>
|
||||
isAddon(packageManager, `${storybookAddonScope}${name}`);
|
||||
|
||||
export const getPackageName = (addonName: string, isOfficialAddon: boolean) =>
|
||||
isOfficialAddon ? storybookAddonScope + addonName : addonName;
|
||||
|
||||
export const getInstalledStorybookVersion = (packageJson: PackageJson) =>
|
||||
packageJson.devDependencies[
|
||||
// This only considers the first occurrence.
|
||||
Object.keys(packageJson.devDependencies).find((devDep) => /@storybook/.test(devDep))
|
||||
] || false;
|
||||
|
||||
export const getPackageArg = (
|
||||
addonName: string,
|
||||
isOfficialAddon: boolean,
|
||||
packageJson: PackageJson
|
||||
) => {
|
||||
if (isOfficialAddon) {
|
||||
const addonNameNoTag = addonName.split('@')[0];
|
||||
const installedStorybookVersion = getInstalledStorybookVersion(packageJson);
|
||||
return installedStorybookVersion
|
||||
? `${addonNameNoTag}@${getInstalledStorybookVersion(packageJson)}`
|
||||
: addonName;
|
||||
}
|
||||
return addonName;
|
||||
};
|
||||
|
||||
const installAddon = (
|
||||
packageManager: JsPackageManager,
|
||||
addonName: string,
|
||||
isOfficialAddon: boolean
|
||||
) => {
|
||||
const prepareDone = commandLog(`Preparing to install the ${addonName} Storybook addon`);
|
||||
prepareDone();
|
||||
logger.log();
|
||||
|
||||
const packageArg = getPackageArg(
|
||||
addonName,
|
||||
isOfficialAddon,
|
||||
packageManager.retrievePackageJson()
|
||||
);
|
||||
|
||||
logger.log();
|
||||
const installDone = commandLog(`Installing the ${addonName} Storybook addon`);
|
||||
|
||||
try {
|
||||
packageManager.addDependencies({}, [packageArg]);
|
||||
} catch (e) {
|
||||
installDone(
|
||||
`Something went wrong installing the addon: "${getPackageName(addonName, isOfficialAddon)}"`
|
||||
);
|
||||
logger.log();
|
||||
process.exit(1);
|
||||
}
|
||||
installDone();
|
||||
};
|
||||
|
||||
export const addStorybookAddonToFile = (
|
||||
addonName: string,
|
||||
addonsFile: string[],
|
||||
isOfficialAddon: boolean
|
||||
) => {
|
||||
const addonNameNoTag = addonName.split('@')[0];
|
||||
const alreadyRegistered = addonsFile.find((line) => line.includes(`${addonNameNoTag}/manager`));
|
||||
|
||||
if (alreadyRegistered) {
|
||||
return addonsFile;
|
||||
}
|
||||
|
||||
const latestImportIndex = addonsFile.reduce(
|
||||
(prev, curr, currIndex) =>
|
||||
curr.startsWith('import') && curr.includes('register') ? currIndex : prev,
|
||||
-1
|
||||
);
|
||||
|
||||
return [
|
||||
...addonsFile.slice(0, latestImportIndex + 1),
|
||||
`import '${getPackageName(addonNameNoTag, isOfficialAddon)}/manager';`,
|
||||
...addonsFile.slice(latestImportIndex + 1),
|
||||
];
|
||||
};
|
||||
|
||||
const LEGACY_CONFIGS = ['addons', 'config', 'presets'];
|
||||
|
||||
@ -137,23 +49,58 @@ const postinstallAddon = async (addonName: string, isOfficialAddon: boolean) =>
|
||||
}
|
||||
};
|
||||
|
||||
export async function add(
|
||||
addonName: string,
|
||||
options: { useNpm: boolean; skipPostinstall: boolean }
|
||||
) {
|
||||
const packageManager = JsPackageManagerFactory.getPackageManager(options.useNpm);
|
||||
const getVersionSpecifier = (addon: string) => {
|
||||
const groups = /^(...*)@(.*)$/.exec(addon);
|
||||
return groups ? [groups[1], groups[2]] : [addon, undefined];
|
||||
};
|
||||
|
||||
const addonCheckDone = commandLog(`Verifying that ${addonName} is an addon`);
|
||||
const isOfficialAddon = await isStorybookAddon(packageManager, addonName);
|
||||
if (!isOfficialAddon) {
|
||||
if (!(await isAddon(packageManager, addonName))) {
|
||||
addonCheckDone(`The provided package was not a Storybook addon: ${addonName}.`);
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Install the given addon package and add it to main.js
|
||||
*
|
||||
* Usage:
|
||||
* - sb add @storybook/addon-docs
|
||||
* - sb add @storybook/addon-interactions@7.0.1
|
||||
*
|
||||
* If there is no version specifier and it's a storybook addon,
|
||||
* it will try to use the version specifier matching your current
|
||||
* Storybook install version.
|
||||
*/
|
||||
export async function add(addon: string, options: { useNpm: boolean; skipPostinstall: boolean }) {
|
||||
const packageManager = JsPackageManagerFactory.getPackageManager(options.useNpm);
|
||||
const packageJson = packageManager.retrievePackageJson();
|
||||
const [addonName, versionSpecifier] = getVersionSpecifier(addon);
|
||||
|
||||
const { mainConfig, version: storybookVersion } = getStorybookInfo(packageJson);
|
||||
if (!mainConfig) {
|
||||
logger.error('Unable to find storybook main.js config');
|
||||
return;
|
||||
}
|
||||
addonCheckDone();
|
||||
installAddon(packageManager, addonName, isOfficialAddon);
|
||||
const main = await readConfig(mainConfig);
|
||||
const addons = main.getFieldValue(['addons']);
|
||||
if (addons && !Array.isArray(addons)) {
|
||||
logger.error('Expected addons array in main.js config');
|
||||
}
|
||||
|
||||
logger.log(`Verifying ${addonName}`);
|
||||
const latestVersion = packageManager.latestVersion(addonName);
|
||||
if (!latestVersion) {
|
||||
logger.error(`Unknown addon ${addonName}`);
|
||||
}
|
||||
|
||||
// add to package.json
|
||||
const isStorybookAddon = addonName.startsWith('@storybook/');
|
||||
const version = versionSpecifier || (isStorybookAddon ? storybookVersion : latestVersion);
|
||||
const addonWithVersion = `${addonName}@${version}`;
|
||||
logger.log(`Installing ${addonWithVersion}`);
|
||||
packageManager.addDependencies({ installAsDevDependencies: true }, [addonWithVersion]);
|
||||
|
||||
// add to main.js
|
||||
logger.log(`Adding '${addon}' to main.js addons field.`);
|
||||
const updatedAddons = [...(addons || []), addonName];
|
||||
main.setFieldValue(['addons'], updatedAddons);
|
||||
await writeConfig(main);
|
||||
|
||||
if (!options.skipPostinstall) {
|
||||
await postinstallAddon(addonName, isOfficialAddon);
|
||||
await postinstallAddon(addon, isStorybookAddon);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user