Merge pull request #19312 from storybookjs/shilman/fix-sb-add

CLI: Update sb add for main.js
This commit is contained in:
Michael Shilman 2022-10-03 14:38:07 +08:00 committed by GitHub
commit c05ee07dbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 55 additions and 317 deletions

View File

@ -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`);
});
});

View File

@ -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);
}
}