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 3 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);
}
},
};
95 changes: 95 additions & 0 deletions code/lib/cli/src/automigrate/helpers/eslintPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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) {
const allDependencies = await packageManager.getAllDependencies();
const packageJson = await packageManager.retrievePackageJson();
let eslintConfigFile;
yannbf marked this conversation as resolved.
Show resolved Hide resolved

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 detected that you're using ESLint. Storybook provides a plugin that gives the best experience with Storybook and helps follow best practices: ${chalk.yellow(
yannbf marked this conversation as resolved.
Show resolved Hide resolved
'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.

82 changes: 56 additions & 26 deletions code/lib/cli/src/babel-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,55 @@ 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}`);

Expand Down Expand Up @@ -52,48 +101,29 @@ 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(', ')})`,
message: `Shall we install the required dependencies now? (${dependencies.join(', ')})`,
});

if (runInstall) {
yannbf marked this conversation as resolved.
Show resolved Hide resolved
logger.info(`Installing dependencies...`);

const packageManager = JsPackageManagerFactory.getPackageManager();

await packageManager.addDependencies({ installAsDevDependencies: true }, added);
await packageManager.addDependencies({ installAsDevDependencies: true }, dependencies);
} else {
logger.info(
`⚠️ Please remember to install the required dependencies yourself: (${added.join(', ')})`
`⚠️ Please remember to install the required dependencies yourself: (${dependencies.join(
', '
)})`
);
}
};
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