Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Detach automigrate command from storybook init #22523

Merged
merged 6 commits into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions code/lib/cli/src/automigrate/fixes/eslint-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('eslint-plugin fix', () => {
await expect(
checkEslint({
packageJson,
hasEslint: false,
})
).resolves.toBeFalsy();
});
Expand Down
37 changes: 9 additions & 28 deletions code/lib/cli/src/automigrate/fixes/eslint-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import chalk from 'chalk';
import { dedent } from 'ts-dedent';
import { readConfig, writeConfig } from '@storybook/csf-tools';
import { readFile, readJson, writeJson } from 'fs-extra';
import detectIndent from 'detect-indent';

import { findEslintFile, SUPPORTED_ESLINT_EXTENSIONS } from '../helpers/getEslintInfo';
import {
configureEslintPlugin,
extractEslintInfo,
findEslintFile,
SUPPORTED_ESLINT_EXTENSIONS,
} from '../helpers/eslintPlugin';

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

Expand All @@ -25,12 +27,9 @@ export const eslintPlugin: Fix<EslintPluginRunOptions> = {
id: 'eslintPlugin',

async check({ packageManager }) {
const allDependencies = await packageManager.getAllDependencies();
const { hasEslint, isStorybookPluginInstalled } = await extractEslintInfo(packageManager);

const eslintPluginStorybook = allDependencies['eslint-plugin-storybook'];
const eslintDependency = allDependencies.eslint;

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

Expand Down Expand Up @@ -82,26 +81,8 @@ export const eslintPlugin: Fix<EslintPluginRunOptions> = {
return;
}

logger.info(`✅ Adding Storybook plugin to ${eslintFile}`);
if (!dryRun) {
if (eslintFile.endsWith('json')) {
const eslintConfig = (await readJson(eslintFile)) as { extends?: string[] };
const existingConfigValue = Array.isArray(eslintConfig.extends)
? eslintConfig.extends
: [eslintConfig.extends];
eslintConfig.extends = [...(existingConfigValue || []), 'plugin:storybook/recommended'];

const eslintFileContents = await readFile(eslintFile, 'utf8');
const spaces = detectIndent(eslintFileContents).amount || 2;
await writeJson(eslintFile, eslintConfig, { spaces });
} else {
const eslint = await readConfig(eslintFile);
const extendsConfig = eslint.getFieldValue(['extends']) || [];
const existingConfigValue = Array.isArray(extendsConfig) ? extendsConfig : [extendsConfig];
eslint.setFieldValue(['extends'], [...existingConfigValue, 'plugin:storybook/recommended']);

await writeConfig(eslint);
}
await configureEslintPlugin(eslintFile, packageManager);
}
},
};
11 changes: 5 additions & 6 deletions code/lib/cli/src/automigrate/fixes/missing-babelrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,19 @@ export const missingBabelRc: Fix<MissingBabelRcOptions> = {

If your project does not have a babel configuration file, we can generate one that's equivalent to the 6.x defaults for you. Keep in mind that this can affect your project if it uses babel, and you may need to make additional changes based on your projects needs.

We can create a ${chalk.blue(
'.babelrc.json'
)} file with some basic configuration and add any necessary package devDependencies.

${chalk.bold(
'Note:'
)} This automatic setup doesn't work in a monorepo, see the babel documentation for how to setup babel manually:
)} After installing the necessary presets, if it does not work in a monorepo, see the babel documentation for reference:
${chalk.yellow('https://babeljs.io/docs')}

We can create a ${chalk.blue(
'.babelrc.json'
)} file with some basic configuration and add any necessary package devDependencies.

Please see the migration guide for more information:
${chalk.yellow(
'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#babel-mode-v7-exclusively'
)}

`;
},
async run() {
Expand Down
99 changes: 99 additions & 0 deletions code/lib/cli/src/automigrate/helpers/eslintPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import fse, { readFile, readJson, writeJson } from 'fs-extra';

import { dedent } from 'ts-dedent';
import detectIndent from 'detect-indent';
import { readConfig, writeConfig } from '@storybook/csf-tools';
import prompts from 'prompts';
import chalk from 'chalk';
import type { JsPackageManager } from '../../js-package-manager';
import { paddedLog } from '../../helpers';

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

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

export async function extractEslintInfo(packageManager: JsPackageManager): Promise<{
hasEslint: boolean;
isStorybookPluginInstalled: boolean;
eslintConfigFile: string | null;
}> {
const allDependencies = await packageManager.getAllDependencies();
const packageJson = await packageManager.retrievePackageJson();
let eslintConfigFile: string | null = null;

try {
eslintConfigFile = findEslintFile();
} catch (err) {
//
}

const isStorybookPluginInstalled = !!allDependencies['eslint-plugin-storybook'];
const hasEslint = allDependencies.eslint || eslintConfigFile || packageJson.eslintConfig;
return { hasEslint, isStorybookPluginInstalled, eslintConfigFile };
}

export async function configureEslintPlugin(eslintFile: string, packageManager: JsPackageManager) {
if (eslintFile) {
paddedLog(`Configuring Storybook ESLint plugin at ${eslintFile}`);
if (eslintFile.endsWith('json')) {
const eslintConfig = (await readJson(eslintFile)) as { extends?: string[] };
const existingConfigValue = Array.isArray(eslintConfig.extends)
? eslintConfig.extends
: [eslintConfig.extends];
eslintConfig.extends = [...(existingConfigValue || []), 'plugin:storybook/recommended'];

const eslintFileContents = await readFile(eslintFile, 'utf8');
const spaces = detectIndent(eslintFileContents).amount || 2;
await writeJson(eslintFile, eslintConfig, { spaces });
} else {
const eslint = await readConfig(eslintFile);
const extendsConfig = eslint.getFieldValue(['extends']) || [];
const existingConfigValue = Array.isArray(extendsConfig) ? extendsConfig : [extendsConfig];
eslint.setFieldValue(['extends'], [...existingConfigValue, 'plugin:storybook/recommended']);

await writeConfig(eslint);
}
} else {
paddedLog(`Configuring eslint-plugin-storybook in your package.json`);
const packageJson = await packageManager.retrievePackageJson();
await packageManager.writePackageJson({
...packageJson,
eslintConfig: {
...packageJson.eslintConfig,
extends: [...(packageJson.eslintConfig?.extends || []), 'plugin:storybook/recommended'],
},
});
}
}

export const suggestESLintPlugin = async (): Promise<boolean> => {
const { shouldInstall } = await prompts({
type: 'confirm',
name: 'shouldInstall',
message: dedent`
We have detected that you're using ESLint. Storybook provides a plugin that gives the best experience with Storybook and helps follow best practices: ${chalk.yellow(
'https://github.com/storybookjs/eslint-plugin-storybook#readme'
)}

Would you like to install it?
`,
initial: true,
});

return shouldInstall;
};
20 changes: 0 additions & 20 deletions code/lib/cli/src/automigrate/helpers/getEslintInfo.ts

This file was deleted.

103 changes: 55 additions & 48 deletions code/lib/cli/src/babel-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,63 @@ import { writeFile, pathExists } from 'fs-extra';
import { logger } from '@storybook/node-logger';
import path from 'path';
import prompts from 'prompts';
import chalk from 'chalk';
import { JsPackageManagerFactory } from './js-package-manager';

export const generateStorybookBabelConfigInCWD = async () => {
const target = process.cwd();
return generateStorybookBabelConfig({ target });
};

export const getBabelPresets = ({ typescript, jsx }: { typescript: boolean; jsx: boolean }) => {
const dependencies = ['@babel/preset-env'];

if (typescript) {
dependencies.push('@babel/preset-typescript');
}

if (jsx) {
dependencies.push('@babel/preset-react');
}

return dependencies;
};

export const writeBabelConfigFile = async ({
location,
typescript,
jsx,
}: {
location?: string;
typescript: boolean;
jsx: boolean;
}) => {
const fileLocation = location || path.join(process.cwd(), '.babelrc.json');

const presets: (string | [string, any])[] = [['@babel/preset-env', { targets: { chrome: 100 } }]];

if (typescript) {
presets.push('@babel/preset-typescript');
}

if (jsx) {
presets.push('@babel/preset-react');
}

const contents = JSON.stringify(
{
sourceType: 'unambiguous',
presets,
plugins: [],
},
null,
2
);

await writeFile(fileLocation, contents);
};

export const generateStorybookBabelConfig = async ({ target }: { target: string }) => {
logger.info(`Generating the storybook default babel config at ${target}`);
logger.info(`Generating the Storybook default babel config at ${target}`);

const fileName = '.babelrc.json';
const location = path.join(target, fileName);
Expand All @@ -31,12 +79,6 @@ export const generateStorybookBabelConfig = async ({ target }: { target: string
}
}

logger.info(
`The config will contain ${chalk.yellow(
'@babel/preset-env'
)} and you will be prompted for additional presets, if you wish to add them depending on your project needs.`
);

const { typescript, jsx } = await prompts([
{
type: 'confirm',
Expand All @@ -52,48 +94,13 @@ export const generateStorybookBabelConfig = async ({ target }: { target: string
},
]);

const added = ['@babel/preset-env'];
const presets: (string | [string, any])[] = [['@babel/preset-env', { targets: { chrome: 100 } }]];

if (typescript) {
added.push('@babel/preset-typescript');
presets.push('@babel/preset-typescript');
}

if (jsx) {
added.push('@babel/preset-react');
presets.push('@babel/preset-react');
}

const contents = JSON.stringify(
{
sourceType: 'unambiguous',
presets,
plugins: [],
},
null,
2
);
const dependencies = getBabelPresets({ typescript, jsx });

logger.info(`Writing file to ${location}`);
await writeFile(location, contents);
await writeBabelConfigFile({ location, typescript, jsx });

const { runInstall } = await prompts({
type: 'confirm',
initial: true,
name: 'runInstall',
message: `Shall we install the required dependencies now? (${added.join(', ')})`,
});
const packageManager = JsPackageManagerFactory.getPackageManager();

if (runInstall) {
logger.info(`Installing dependencies...`);

const packageManager = JsPackageManagerFactory.getPackageManager();

await packageManager.addDependencies({ installAsDevDependencies: true }, added);
} else {
logger.info(
`⚠️ Please remember to install the required dependencies yourself: (${added.join(', ')})`
);
}
logger.info(`Installing dependencies (${dependencies.join(', ')})`);
await packageManager.addDependencies({ installAsDevDependencies: true }, dependencies);
};
3 changes: 1 addition & 2 deletions code/lib/cli/src/generators/REACT_SCRIPTS/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => {
extraAddons,
extraPackages,
staticDir: fs.existsSync(path.resolve('./public')) ? 'public' : undefined,
addBabel: false,
addESLint: true,
skipBabel: true,
extraMain,
});
};
Expand Down