From 835feb515f874f9bad97fdd53e0b9ded8636aa18 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Wed, 10 Jan 2024 16:59:36 +0800 Subject: [PATCH 1/2] CLI: Add addon `remove` command --- code/lib/cli/src/generate.ts | 9 +++++ code/lib/cli/src/remove.ts | 46 ++++++++++++++++++++++ code/lib/csf-tools/src/ConfigFile.test.ts | 48 +++++++++++++++++++++++ code/lib/csf-tools/src/ConfigFile.ts | 28 +++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 code/lib/cli/src/remove.ts diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index 5ee5c9d89abf..be5d8cc5ed80 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -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'; @@ -67,6 +68,14 @@ command('add ') .option('-s --skip-postinstall', 'Skip package specific postinstall config modifications') .action((addonName: string, options: any) => add(addonName, options)); +command('remove ') + .description('Remove an addon from your Storybook') + .option( + '--package-manager ', + 'Force package manager for installing dependencies' + ) + .action((addonName: string, options: any) => remove(addonName, options)); + command('babelrc') .description('generate the default storybook babel config into your current working directory') .action(() => generateStorybookBabelConfigInCWD()); diff --git a/code/lib/cli/src/remove.ts b/code/lib/cli/src/remove.ts new file mode 100644 index 000000000000..47c556eb578f --- /dev/null +++ b/code/lib/cli/src/remove.ts @@ -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.`); + } +} diff --git a/code/lib/csf-tools/src/ConfigFile.test.ts b/code/lib/csf-tools/src/ConfigFile.test.ts index 2ae59f8a75ac..c79dbce188e9 100644 --- a/code/lib/csf-tools/src/ConfigFile.test.ts +++ b/code/lib/csf-tools/src/ConfigFile.test.ts @@ -1139,4 +1139,52 @@ 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('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'` + ); + }); + }); }); diff --git a/code/lib/csf-tools/src/ConfigFile.ts b/code/lib/csf-tools/src/ConfigFile.ts index eb2921b4e06d..45eae49ee61d 100644 --- a/code/lib/csf-tools/src/ConfigFile.ts +++ b/code/lib/csf-tools/src/ConfigFile.ts @@ -506,6 +506,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 false; + }); + 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 From 7ca83ab560f6a4e401d5dbb5a6153438beb21cf5 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Wed, 10 Jan 2024 19:40:10 +0800 Subject: [PATCH 2/2] ConfigFile: Fix PNP wrapping logic --- code/lib/csf-tools/src/ConfigFile.test.ts | 22 ++++++++++++++++++++++ code/lib/csf-tools/src/ConfigFile.ts | 14 +++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/code/lib/csf-tools/src/ConfigFile.test.ts b/code/lib/csf-tools/src/ConfigFile.test.ts index c79dbce188e9..5fc75ff41f26 100644 --- a/code/lib/csf-tools/src/ConfigFile.test.ts +++ b/code/lib/csf-tools/src/ConfigFile.test.ts @@ -1163,6 +1163,28 @@ describe('ConfigFile', () => { 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 { diff --git a/code/lib/csf-tools/src/ConfigFile.ts b/code/lib/csf-tools/src/ConfigFile.ts index 45eae49ee61d..410bbcc1fee7 100644 --- a/code/lib/csf-tools/src/ConfigFile.ts +++ b/code/lib/csf-tools/src/ConfigFile.ts @@ -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' } @@ -409,6 +419,8 @@ export class ConfigFile { ) { if (t.isStringLiteral(prop.value)) { value = prop.value.value; + } else { + value = this._getPnpWrappedValue(prop.value); } } @@ -522,7 +534,7 @@ export class ConfigFile { const name = this._getPresetValue(element, 'name'); return name === value; } - return false; + return this._getPnpWrappedValue(element as t.Node) === value; }); if (index >= 0) { current.elements.splice(index, 1);