diff --git a/lib/cli/src/automigrate/fixes/npm7.test.ts b/lib/cli/src/automigrate/fixes/npm7.test.ts new file mode 100644 index 000000000000..83346ccdfaf1 --- /dev/null +++ b/lib/cli/src/automigrate/fixes/npm7.test.ts @@ -0,0 +1,47 @@ +import { NPMProxy } from '../../js-package-manager/NPMProxy'; +import { npm7 } from './npm7'; + +const mockExecuteCommand = jest.fn(); +class MockedNPMProxy extends NPMProxy { + executeCommand(...args) { + return mockExecuteCommand(...args); + } +} + +function mockExecuteResults(map: Record) { + mockExecuteCommand.mockImplementation((command, args) => { + const commandString = `${command} ${args.join(' ')}`; + if (map[commandString]) return map[commandString]; + + throw new Error(`Unexpected execution of '${commandString}'`); + }); +} + +describe('npm7 fix', () => { + describe('npm < 7', () => { + it('does not match', async () => { + mockExecuteResults({ 'npm --version': '6.0.0' }); + expect(await npm7.check({ packageManager: new MockedNPMProxy() })).toEqual(null); + }); + }); + + describe('npm 7+', () => { + it('matches if config is not installed', async () => { + mockExecuteResults({ + 'npm --version': '7.0.0', + 'npm config get legacy-peer-deps --location=project': 'false', + }); + expect(await npm7.check({ packageManager: new MockedNPMProxy() })).toEqual({ + npmVersion: '7.0.0', + }); + }); + + it('does not match if config is installed', async () => { + mockExecuteResults({ + 'npm --version': '7.0.0', + 'npm config get legacy-peer-deps --location=project': 'true', + }); + expect(await npm7.check({ packageManager: new MockedNPMProxy() })).toEqual(null); + }); + }); +}); diff --git a/lib/cli/src/automigrate/fixes/npm7.ts b/lib/cli/src/automigrate/fixes/npm7.ts new file mode 100644 index 000000000000..74f40e6723d8 --- /dev/null +++ b/lib/cli/src/automigrate/fixes/npm7.ts @@ -0,0 +1,41 @@ +import chalk from 'chalk'; +import dedent from 'ts-dedent'; +import { Fix } from '../types'; +import { NPMProxy } from '../../js-package-manager/NPMProxy'; + +interface Npm7RunOptions { + npmVersion: string; +} + +/** + * Is the user using npm7+? If so create a .npmrc with legacy-peer-deps=true + */ +export const npm7: Fix = { + id: 'npm7', + + async check({ packageManager }) { + if (packageManager.type !== 'npm') return null; + + const npmVersion = (packageManager as NPMProxy).getNpmVersion(); + if ((packageManager as NPMProxy).needsLegacyPeerDeps(npmVersion)) { + return { npmVersion }; + } + return null; + }, + + prompt({ npmVersion }) { + const npmFormatted = chalk.cyan(`npm ${npmVersion}`); + return dedent` + We've detected you are running ${npmFormatted} which has peer dependency semantics which Storybook is incompatible with. + + In order to work with Storybook's package structure, you'll need to run \`npm\` with the + \`--legacy-peer-deps=true\` flag. We can generate an \`.npmrc\` which will do that automatically. + + More info: ${chalk.yellow('https://github.com/storybookjs/storybook/issues/18298')} + `; + }, + + async run({ packageManager }) { + (packageManager as NPMProxy).setLegacyPeerDeps(); + }, +}; diff --git a/lib/cli/src/js-package-manager/NPMProxy.ts b/lib/cli/src/js-package-manager/NPMProxy.ts index 052bb6fcd255..a75d7d2b69fd 100644 --- a/lib/cli/src/js-package-manager/NPMProxy.ts +++ b/lib/cli/src/js-package-manager/NPMProxy.ts @@ -18,10 +18,28 @@ export class NPMProxy extends JsPackageManager { return `npm run ${command}`; } + getNpmVersion(): string { + return this.executeCommand('npm', ['--version']); + } + + hasLegacyPeerDeps() { + return ( + this.executeCommand('npm', ['config', 'get', 'legacy-peer-deps', '--location=project']) === + 'true' + ); + } + + setLegacyPeerDeps() { + this.executeCommand('npm', ['config', 'set', 'legacy-peer-deps=true', '--location=project']); + } + + needsLegacyPeerDeps(version: string) { + return semver.gte(version, '7.0.0') && !this.hasLegacyPeerDeps(); + } + getInstallArgs(): string[] { if (!this.installArgs) { - const version = this.executeCommand('npm', ['--version']); - this.installArgs = semver.gte(version, '7.0.0') + this.installArgs = this.needsLegacyPeerDeps(this.getNpmVersion()) ? ['install', '--legacy-peer-deps'] : ['install']; }