CLI: add new frameworks format automigration

This commit is contained in:
Yann Braga 2022-08-10 07:44:32 +02:00
parent 1d80c8ee62
commit 27d58ad701
3 changed files with 403 additions and 0 deletions

View File

@ -7,6 +7,7 @@ import { eslintPlugin } from './eslint-plugin';
import { builderVite } from './builder-vite';
import { npm7 } from './npm7';
import { sbScripts } from './sb-scripts';
import { newFrameworks } from './new-frameworks';
import { Fix } from '../types';
export * from '../types';
@ -20,4 +21,5 @@ export const fixes: Fix[] = [
builderVite,
npm7,
sbScripts,
newFrameworks,
];

View File

@ -0,0 +1,180 @@
/* eslint-disable no-underscore-dangle */
import path from 'path';
import { JsPackageManager } from '../../js-package-manager';
import { newFrameworks } from './new-frameworks';
// eslint-disable-next-line global-require, jest/no-mocks-import
jest.mock('fs-extra', () => require('../../../../../__mocks__/fs-extra'));
const checkNewFrameworks = async ({ packageJson, main }) => {
if (main) {
// eslint-disable-next-line global-require
require('fs-extra').__setMockFiles({
[path.join('.storybook', 'main.js')]: `module.exports = ${JSON.stringify(main)};`,
});
}
const packageManager = {
retrievePackageJson: () => ({ dependencies: {}, devDependencies: {}, ...packageJson }),
} as JsPackageManager;
return newFrameworks.check({ packageManager });
};
describe('new-frameworks fix', () => {
describe('should no-op', () => {
it('in sb < 7', async () => {
const packageJson = { dependencies: { '@storybook/vue': '^6.2.0' } };
await expect(
checkNewFrameworks({
packageJson,
main: {},
})
).resolves.toBeFalsy();
});
it('in sb 7 with no main', async () => {
const packageJson = { dependencies: { '@storybook/vue': '^7.0.0' } };
await expect(
checkNewFrameworks({
packageJson,
main: undefined,
})
).resolves.toBeFalsy();
});
it('in sb 7 with no framework field in main', async () => {
const packageJson = { dependencies: { '@storybook/vue': '^7.0.0' } };
await expect(
checkNewFrameworks({
packageJson,
main: {},
})
).resolves.toBeFalsy();
});
it('in sb 7 with no builder', async () => {
const packageJson = { dependencies: { '@storybook/vue': '^7.0.0' } };
await expect(
checkNewFrameworks({
packageJson,
main: {
framework: '@storybook/vue',
},
})
).resolves.toBeFalsy();
});
it('in sb 7 with unsupported package', async () => {
const packageJson = { dependencies: { '@storybook/riot': '^7.0.0' } };
await expect(
checkNewFrameworks({
packageJson,
main: {
framework: '@storybook/riot',
core: {
builder: 'webpack5',
},
},
})
).resolves.toBeFalsy();
});
// TODO: once we have vite frameworks e.g. @storybook/react-vite, then we should remove this test
it('in sb 7 with vite', async () => {
const packageJson = { dependencies: { '@storybook/react': '^7.0.0' } };
await expect(
checkNewFrameworks({
packageJson,
main: {
framework: '@storybook/react',
core: {
builder: '@storybook/builder-vite',
},
},
})
).resolves.toBeFalsy();
});
});
describe('sb >= 7', () => {
describe('new-frameworks dependency', () => {
it('should update to @storybook/react-webpack5', async () => {
const packageJson = {
dependencies: {
'@storybook/react': '^7.0.0-alpha.0',
'@storybook/builder-webpack5': '^6.5.9',
'@storybook/manager-webpack5': '^6.5.9',
},
};
await expect(
checkNewFrameworks({
packageJson,
main: {
framework: '@storybook/react',
core: {
builder: {
name: 'webpack5',
options: {
lazyCompilation: true,
},
},
},
reactOptions: {
fastRefresh: true,
},
},
})
).resolves.toEqual(
expect.objectContaining({
frameworkPackage: '@storybook/react',
dependenciesToAdd: ['@storybook/react-webpack5'],
dependenciesToRemove: [
'@storybook/react',
'@storybook/builder-webpack5',
'@storybook/manager-webpack5',
],
frameworkOptions: {
fastRefresh: true,
},
builderInfo: {
name: 'webpack5',
options: {
lazyCompilation: true,
},
},
})
);
});
});
// TODO: enable this once Vite is supported
// eslint-disable-next-line jest/no-disabled-tests
it.skip('should update to @storybook/react-vite', async () => {
const packageJson = {
dependencies: {
'@storybook/react': '^7.0.0-alpha.0',
'@storybook/builder-vite': '^0.0.2',
},
};
await expect(
checkNewFrameworks({
packageJson,
main: {
framework: '@storybook/react',
core: {
builder: '@storybook/builder-vite',
},
},
})
).resolves.toEqual(
expect.objectContaining({
dependenciesToAdd: '@storybook/react-vite',
dependenciesToRemove: [
'@storybook/react',
'storybook-builder-vite',
'@storybook/builder-vite',
],
})
);
});
});
});

View File

@ -0,0 +1,221 @@
import chalk from 'chalk';
import dedent from 'ts-dedent';
import semver from '@storybook/semver';
import { ConfigFile, readConfig, writeConfig } from '@storybook/csf-tools';
import { getStorybookInfo } from '@storybook/core-common';
import type { Fix } from '../types';
import type { PackageJsonWithDepsAndDevDeps } from '../../js-package-manager';
const logger = console;
const packagesMap = {
'@storybook/react': {
webpack5: '@storybook/react-webpack5',
vite: '@storybook/react-vite',
},
'@storybook/preact': {
webpack5: '@storybook/preact-webpack5',
},
'@storybook/server': {
webpack5: '@storybook/server-webpack5',
},
'@storybook/angular': {
webpack5: '@storybook/angular-webpack5',
},
'@storybook/vue': {
webpack5: '@storybook/vue-webpack5',
vite: '@storybook/vue-vite',
},
'@storybook/vue3': {
webpack5: '@storybook/vue3-webpack5',
vite: '@storybook/vue3-vite',
},
'@storybook/svelte': {
webpack5: '@storybook/svelte-webpack5',
vite: '@storybook/svelte-vite',
},
'@storybook/web-components': {
webpack5: '@storybook/web-components-webpack5',
},
'@storybook/html': {
webpack5: '@storybook/html-webpack5',
},
};
interface NewFrameworkRunOptions {
main: ConfigFile;
packageJson: PackageJsonWithDepsAndDevDeps;
dependenciesToAdd: string[];
dependenciesToRemove: string[];
frameworkPackage: keyof typeof packagesMap;
frameworkOptions: Record<string, any>;
builderInfo: {
name: string;
options: Record<string, any>;
};
}
export const getBuilder = (builder: string | { name: string }) => {
if (typeof builder === 'string') {
return builder.includes('vite') ? 'vite' : 'webpack5';
}
return builder.name.includes('vite') ? 'vite' : 'webpack5';
};
export const getFrameworkOptions = (framework: string, main: ConfigFile) => {
const frameworkOptions = main.getFieldValue([`${framework}Options`]);
return frameworkOptions || {};
};
/**
* Does the user have separate framework and builders (e.g. @storybook/react + core.builder -> webpack5?
*
* If so:
* - Remove the dependencies (@storybook/react + @storybook/builder-webpack5 + @storybook/manager-webpack5)
* - Install the correct new package e.g. (@storybook/react-webpack5)
* - Update the main config to use the new framework
* -- moving core.builder into framework.options.builder
* -- moving frameworkOptions (e.g. reactOptions) into framework.options
*/
export const newFrameworks: Fix<NewFrameworkRunOptions> = {
id: 'newFrameworks',
async check({ packageManager }) {
const packageJson = packageManager.retrievePackageJson();
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
const config = getStorybookInfo(packageJson);
const { mainConfig, version: storybookVersion, framework } = config;
if (!mainConfig) {
logger.warn('Unable to find storybook main.js config, skipping');
return null;
}
const storybookCoerced = storybookVersion && semver.coerce(storybookVersion)?.version;
if (!storybookCoerced) {
logger.warn(dedent`
Unable to determine storybook version, skipping ${chalk.cyan('newFrameworks')} fix.
🤔 Are you running automigrate from your project directory?
`);
return null;
}
if (!semver.gte(storybookCoerced, '7.0.0')) {
console.log('lower than 7.0.0!');
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);
if (!main) {
console.log('no main');
return null;
}
const frameworkPackage = main.getFieldValue(['framework']) as keyof typeof packagesMap;
const builder = main.getFieldValue(['core', 'builder']);
if (!frameworkPackage || !builder) {
console.log('no framework or no builder, skipping');
return null;
}
const supportedPackages = Object.keys(packagesMap);
if (!supportedPackages.includes(frameworkPackage)) {
console.log('no supported package, skipping');
return null;
}
const builderInfo = {
name: getBuilder(builder),
options: main.getFieldValue(['core', 'builder', 'options']) || {},
} as const;
// TODO: once we have vite frameworks e.g. @storybook/react-vite, then we support it here
// and remove ['storybook-builder-vite', '@storybook/builder-vite'] from deps
if (builderInfo.name === 'vite') {
return null;
}
const frameworkOptions = getFrameworkOptions(framework, main);
const dependenciesToRemove = [
frameworkPackage,
'@storybook/builder-webpack5',
'@storybook/manager-webpack5',
].filter((dep) => allDeps[dep]);
const newFrameworkPackage = packagesMap[frameworkPackage][builderInfo.name];
const dependenciesToAdd = [newFrameworkPackage];
return {
main,
dependenciesToAdd,
dependenciesToRemove,
frameworkPackage: newFrameworkPackage,
frameworkOptions,
builderInfo,
packageJson,
};
},
prompt() {
return dedent`
We've detected you are using an older format of Storybook frameworks and builders.
In Storybook 7, frameworks also specify the builder to be used.
We can remove the dependencies that are no longer needed and install the new framework that already includes the builder.
More info: ${chalk.yellow(
'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#framework-field-mandatory'
)}
`;
},
async run({
result: {
dependenciesToAdd,
dependenciesToRemove,
main,
frameworkPackage,
frameworkOptions,
builderInfo,
packageJson,
},
packageManager,
dryRun,
}) {
logger.info(`✅ Removing legacy dependencies: ${dependenciesToRemove.join(', ')}`);
if (!dryRun) {
packageManager.removeDependencies({ skipInstall: true, packageJson }, dependenciesToRemove);
}
logger.info(`✅ Installing new dependencies: ${dependenciesToAdd.join(', ')}`);
if (!dryRun) {
packageManager.addDependencies({ installAsDevDependencies: true }, dependenciesToAdd);
}
if (!dryRun) {
logger.info(`✅ Updating framework field in main.js`);
const currentCore = main.getFieldValue(['core']);
main.setFieldValue(['framework', 'name'], frameworkPackage);
main.setFieldValue(['framework', 'options'], frameworkOptions);
main.setFieldValue(['framework', 'options', 'builder'], builderInfo.options);
delete currentCore.builder;
if (Object.keys(currentCore).length === 0) {
// TODO: this should delete the field instead
main.setFieldValue(['core'], {});
} else {
main.setFieldValue(['core'], currentCore);
}
await writeConfig(main);
}
},
};