Skip to content

Commit

Permalink
Merge pull request #16550 from storybookjs/feature/eslint-plugin-auto…
Browse files Browse the repository at this point in the history
…migrate

CLI: Add eslint-plugin-storybook to automigrate
  • Loading branch information
shilman authored Nov 11, 2021
2 parents dca9290 + 1df03aa commit dd560fd
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 1 deletion.
119 changes: 119 additions & 0 deletions lib/cli/src/automigrate/fixes/eslint-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* eslint-disable no-underscore-dangle */
import dedent from 'ts-dedent';
import { JsPackageManager } from '../../js-package-manager';
import { eslintPlugin } from './eslint-plugin';

// eslint-disable-next-line global-require, jest/no-mocks-import
jest.mock('fs-extra', () => require('../../../../../__mocks__/fs-extra'));

const checkEslint = async ({
packageJson,
main = {},
hasEslint = true,
eslintExtension = 'js',
}) => {
// eslint-disable-next-line global-require
require('fs-extra').__setMockFiles({
'.storybook/main.js': !main ? null : `module.exports = ${JSON.stringify(main)};`,
[`.eslintrc.${eslintExtension}`]: !hasEslint
? null
: dedent(`
module.exports = {
extends: ['plugin:react/recommended', 'airbnb-typescript', 'plugin:prettier/recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: 'module',
project: 'tsconfig.eslint.json',
},
plugins: ['react', '@typescript-eslint'],
rules: {
'some/rule': 'warn',
},
}
`),
});
const packageManager = {
retrievePackageJson: () => ({ dependencies: {}, devDependencies: {}, ...packageJson }),
} as JsPackageManager;
return eslintPlugin.check({ packageManager });
};

describe('eslint-plugin fix', () => {
describe('should skip migration when', () => {
it('project does not have eslint installed', async () => {
const packageJson = { dependencies: {} };

await expect(
checkEslint({
packageJson,
})
).resolves.toBeFalsy();
});

it('project already contains eslint-plugin-storybook dependency', async () => {
const packageJson = { dependencies: { 'eslint-plugin-storybook': '^0.0.0' } };

await expect(
checkEslint({
packageJson,
})
).resolves.toBeFalsy();
});
});

describe('when project does not contain eslint-plugin-storybook but has eslint installed', () => {
const packageJson = { dependencies: { '@storybook/react': '^6.2.0', eslint: '^7.0.0' } };

describe('should no-op and warn when', () => {
it('main.js is not found', async () => {
const loggerSpy = jest.spyOn(console, 'warn').mockImplementationOnce(jest.fn);
const result = await checkEslint({
packageJson,
main: null,
hasEslint: false,
});

expect(loggerSpy).toHaveBeenCalledWith('Unable to find storybook main.js config, skipping');

await expect(result).toBeFalsy();
});

it('.eslintrc is not found', async () => {
const loggerSpy = jest.spyOn(console, 'warn').mockImplementationOnce(jest.fn);
const result = await checkEslint({
packageJson,
hasEslint: false,
});

expect(loggerSpy).toHaveBeenCalledWith('Unable to find .eslintrc config file, skipping');

await expect(result).toBeFalsy();
});
});

describe('should install eslint plugin', () => {
it('when .eslintrc is using a supported extension', async () => {
await expect(
checkEslint({
packageJson,
})
).resolves.toMatchObject({
unsupportedExtension: undefined,
});
});

it('when .eslintrc is using unsupported extension', async () => {
await expect(
checkEslint({
packageJson,
eslintExtension: 'yml',
})
).resolves.toMatchObject({ unsupportedExtension: 'yml' });
});
});
});
});
107 changes: 107 additions & 0 deletions lib/cli/src/automigrate/fixes/eslint-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import chalk from 'chalk';
import dedent from 'ts-dedent';
import { ConfigFile, readConfig, writeConfig } from '@storybook/csf-tools';

import { findEslintFile, SUPPORTED_ESLINT_EXTENSIONS } from '../helpers/getEslintInfo';
import { getStorybookInfo } from '../helpers/getStorybookInfo';

import type { Fix } from '../types';

const logger = console;

interface EslintPluginRunOptions {
main: ConfigFile;
eslintFile: string;
unsupportedExtension?: string;
}

/**
* Does the user not have eslint-plugin-storybook installed?
*
* If so:
* - Install it, and if possible configure it
*/
export const eslintPlugin: Fix<EslintPluginRunOptions> = {
id: 'eslintPlugin',

async check({ packageManager }) {
const packageJson = packageManager.retrievePackageJson();
const { dependencies, devDependencies } = packageJson;

const eslintPluginStorybook =
dependencies['eslint-plugin-storybook'] || devDependencies['eslint-plugin-storybook'];
const eslintDependency = dependencies.eslint || devDependencies.eslint;

if (eslintPluginStorybook || !eslintDependency) {
return null;
}

const config = getStorybookInfo(packageJson);

const { mainConfig } = config;
if (!mainConfig) {
logger.warn('Unable to find storybook main.js config, skipping');
return null;
}

let eslintFile;
let unsupportedExtension;
try {
eslintFile = findEslintFile();
} catch (err) {
unsupportedExtension = err.message;
}

if (!eslintFile && !unsupportedExtension) {
logger.warn('Unable to find .eslintrc config file, skipping');
return null;
}

// If in the future the eslint plugin has a framework option, using main to extract the framework field will be very useful
const main = await readConfig(mainConfig);

return { eslintFile, main, unsupportedExtension };
},

prompt() {
return dedent`
We've detected you are not using our eslint-plugin.
In order to have the best experience with Storybook and follow best practices, we advise you to install eslint-plugin-storybook.
More info: ${chalk.yellow('https://github.com/storybookjs/eslint-plugin-storybook#readme')}
`;
},

async run({ result: { eslintFile, unsupportedExtension }, packageManager, dryRun }) {
const deps = [`eslint-plugin-storybook`];

logger.info(`✅ Adding dependencies: ${deps}`);
if (!dryRun) packageManager.addDependencies({ installAsDevDependencies: true }, deps);

if (!dryRun && unsupportedExtension) {
logger.warn(
dedent(`
⚠️ The plugin was successfuly installed but failed to configure.
Found an .eslintrc config file with an unsupported automigration format: ${unsupportedExtension}.
Supported formats for automigration are: ${SUPPORTED_ESLINT_EXTENSIONS.join(', ')}.
Please refer to https://github.com/storybookjs/eslint-plugin-storybook#usage to finish setting up the plugin manually.
`)
);

return;
}

const eslint = await readConfig(eslintFile);
logger.info(`✅ Configuring eslint rules in ${eslint.fileName}`);

if (!dryRun) {
logger.info(`✅ Adding Storybook to extends list`);
const extendsConfig = eslint.getFieldValue(['extends']) || [];
eslint.setFieldValue(['extends'], [...extendsConfig, 'plugin:storybook/recommended']);
await writeConfig(eslint);
}
},
};
3 changes: 2 additions & 1 deletion lib/cli/src/automigrate/fixes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { cra5 } from './cra5';
import { webpack5 } from './webpack5';
import { angular12 } from './angular12';
import { mainjsFramework } from './mainjsFramework';
import { eslintPlugin } from './eslint-plugin';
import { Fix } from '../types';

export * from '../types';
export const fixes: Fix[] = [cra5, webpack5, angular12, mainjsFramework];
export const fixes: Fix[] = [cra5, webpack5, angular12, mainjsFramework, eslintPlugin];
20 changes: 20 additions & 0 deletions lib/cli/src/automigrate/helpers/getEslintInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import fse from 'fs-extra';

export const SUPPORTED_ESLINT_EXTENSIONS = ['js', 'cjs'];
const UNSUPPORTED_ESLINT_EXTENSIONS = ['yaml', 'yml', 'json'];

export const findEslintFile = () => {
const filePrefix = '.eslintrc';
const unsupportedExtension = UNSUPPORTED_ESLINT_EXTENSIONS.find((ext: string) =>
fse.existsSync(`${filePrefix}.${ext}`)
);

if (unsupportedExtension) {
throw new Error(unsupportedExtension);
}

const extension = SUPPORTED_ESLINT_EXTENSIONS.find((ext: string) =>
fse.existsSync(`${filePrefix}.${ext}`)
);
return extension ? `${filePrefix}.${extension}` : null;
};

0 comments on commit dd560fd

Please sign in to comment.