Skip to content

Commit

Permalink
Merge pull request #25538 from storybookjs/shlman/add-cli-remove-command
Browse files Browse the repository at this point in the history
CLI: Add addon `remove` command
  • Loading branch information
shilman authored Jan 10, 2024
2 parents b29d2a7 + 12ce91e commit 4b4efed
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 0 deletions.
9 changes: 9 additions & 0 deletions code/lib/cli/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import invariant from 'tiny-invariant';
import type { CommandOptions } from './generators/types';
import { initiate } from './initiate';
import { add } from './add';
import { remove } from './remove';
import { migrate } from './migrate';
import { upgrade, type UpgradeOptions } from './upgrade';
import { sandbox } from './sandbox';
Expand Down Expand Up @@ -66,6 +67,14 @@ command('add <addon>')
.option('-s --skip-postinstall', 'Skip package specific postinstall config modifications')
.action((addonName: string, options: any) => add(addonName, options));

command('remove <addon>')
.description('Remove an addon from your Storybook')
.option(
'--package-manager <npm|pnpm|yarn1|yarn2>',
'Force package manager for installing dependencies'
)
.action((addonName: string, options: any) => remove(addonName, options));

command('upgrade')
.description('Upgrade your Storybook packages to the latest')
.option(
Expand Down
46 changes: 46 additions & 0 deletions code/lib/cli/src/remove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { getStorybookInfo } from '@storybook/core-common';
import { readConfig, writeConfig } from '@storybook/csf-tools';
import dedent from 'ts-dedent';

import { JsPackageManagerFactory, type PackageManagerName } from './js-package-manager';

const logger = console;

/**
* Remove the given addon package and remove it from main.js
*
* Usage:
* - sb remove @storybook/addon-links
*/
export async function remove(addon: string, options: { packageManager: PackageManagerName }) {
const { packageManager: pkgMgr } = options;

const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr });
const packageJson = await packageManager.retrievePackageJson();
const { mainConfig, configDir } = getStorybookInfo(packageJson);

if (typeof configDir === 'undefined') {
throw new Error(dedent`
Unable to find storybook config directory
`);
}

if (!mainConfig) {
logger.error('Unable to find storybook main.js config');
return;
}
const main = await readConfig(mainConfig);

// remove from package.json
logger.log(`Uninstalling ${addon}`);
await packageManager.removeDependencies({ packageJson }, [addon]);

// add to main.js
logger.log(`Removing '${addon}' from main.js addons field.`);
try {
main.removeEntryFromArray(['addons'], addon);
await writeConfig(main);
} catch (err) {
logger.warn(`Failed to remove '${addon}' from main.js addons field.`);
}
}
70 changes: 70 additions & 0 deletions code/lib/csf-tools/src/ConfigFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1139,4 +1139,74 @@ describe('ConfigFile', () => {
`);
});
});

describe('removeEntryFromArray', () => {
it('removes a string literal entry', () => {
const source = dedent`
export default {
addons: ['a', 'b', 'c'],
}
`;
const config = loadConfig(source).parse();
config.removeEntryFromArray(['addons'], 'b');
expect(config.getFieldValue(['addons'])).toMatchInlineSnapshot(`a,c`);
});

it('removes a preset-style object entry', () => {
const source = dedent`
export default {
addons: ['a', { name: 'b', options: {} }, 'c'],
}
`;
const config = loadConfig(source).parse();
config.removeEntryFromArray(['addons'], 'b');
expect(config.getFieldValue(['addons'])).toMatchInlineSnapshot(`a,c`);
});

it('removes a pnp-wrapped string entry', () => {
const source = dedent`
export default {
addons: ['a', getAbsolutePath('b'), 'c'],
}
`;
const config = loadConfig(source).parse();
config.removeEntryFromArray(['addons'], 'b');
expect(config.getFieldValue(['addons'])).toMatchInlineSnapshot(`a,c`);
});

it('removes a pnp-wrapped object entry', () => {
const source = dedent`
export default {
addons: ['a', { name: getAbsolutePath('b'), options: {} }, 'c'],
}
`;
const config = loadConfig(source).parse();
config.removeEntryFromArray(['addons'], 'b');
expect(config.getFieldValue(['addons'])).toMatchInlineSnapshot(`a,c`);
});

it('throws when entry is missing', () => {
const source = dedent`
export default {
addons: ['a', { name: 'b', options: {} }, 'c'],
}
`;
const config = loadConfig(source).parse();
expect(() => config.removeEntryFromArray(['addons'], 'x')).toThrowErrorMatchingInlineSnapshot(
`Error: Could not find 'x' in array at 'addons'`
);
});

it('throws when target array is not an arral', () => {
const source = dedent`
export default {
addons: {},
}
`;
const config = loadConfig(source).parse();
expect(() => config.removeEntryFromArray(['addons'], 'x')).toThrowErrorMatchingInlineSnapshot(
`Error: Expected array at 'addons', got 'ObjectExpression'`
);
});
});
});
40 changes: 40 additions & 0 deletions code/lib/csf-tools/src/ConfigFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,16 @@ export class ConfigFile {
return pathNames;
}

_getPnpWrappedValue(node: t.Node) {
if (t.isCallExpression(node)) {
const arg = node.arguments[0];
if (t.isStringLiteral(arg)) {
return arg.value;
}
}
return undefined;
}

/**
* Given a node and a fallback property, returns a **non-evaluated** string value of the node.
* 1. { node: 'value' }
Expand All @@ -409,6 +419,8 @@ export class ConfigFile {
) {
if (t.isStringLiteral(prop.value)) {
value = prop.value.value;
} else {
value = this._getPnpWrappedValue(prop.value);
}
}

Expand Down Expand Up @@ -506,6 +518,34 @@ export class ConfigFile {
}
}

/**
* Specialized helper to remove addons or other array entries
* that can either be strings or objects with a name property.
*/
removeEntryFromArray(path: string[], value: string) {
const current = this.getFieldNode(path);
if (!current) return;
if (t.isArrayExpression(current)) {
const index = current.elements.findIndex((element) => {
if (t.isStringLiteral(element)) {
return element.value === value;
}
if (t.isObjectExpression(element)) {
const name = this._getPresetValue(element, 'name');
return name === value;
}
return this._getPnpWrappedValue(element as t.Node) === value;
});
if (index >= 0) {
current.elements.splice(index, 1);
} else {
throw new Error(`Could not find '${value}' in array at '${path.join('.')}'`);
}
} else {
throw new Error(`Expected array at '${path.join('.')}', got '${current.type}'`);
}
}

_inferQuotes() {
if (!this._quotes) {
// first 500 tokens for efficiency
Expand Down

0 comments on commit 4b4efed

Please sign in to comment.