mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-04 21:11:07 +08:00
add nextjs framework automigration
This commit is contained in:
parent
8063a0ed6f
commit
2b4e3a7bb9
@ -9,6 +9,7 @@ import { eslintPlugin } from './eslint-plugin';
|
||||
import { builderVite } from './builder-vite';
|
||||
import { sbScripts } from './sb-scripts';
|
||||
import { sbBinary } from './sb-binary';
|
||||
import { nextjsFramework } from './nextjs-framework';
|
||||
import { newFrameworks } from './new-frameworks';
|
||||
import { removedGlobalClientAPIs } from './remove-global-client-apis';
|
||||
import { mdx1to2 } from './mdx-1-to-2';
|
||||
@ -30,6 +31,7 @@ export const fixes: Fix[] = [
|
||||
sbBinary,
|
||||
sbScripts,
|
||||
newFrameworks,
|
||||
nextjsFramework,
|
||||
removedGlobalClientAPIs,
|
||||
mdx1to2,
|
||||
docsPageAutomatic,
|
||||
|
163
code/lib/cli/src/automigrate/fixes/nextjs-framework.test.ts
Normal file
163
code/lib/cli/src/automigrate/fixes/nextjs-framework.test.ts
Normal file
@ -0,0 +1,163 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import path from 'path';
|
||||
import { JsPackageManager } from '../../js-package-manager';
|
||||
import { nextjsFramework } from './nextjs-framework';
|
||||
|
||||
// eslint-disable-next-line global-require, jest/no-mocks-import
|
||||
jest.mock('fs-extra', () => require('../../../../../__mocks__/fs-extra'));
|
||||
|
||||
const checkNextjsFramework = 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 nextjsFramework.check({ packageManager });
|
||||
};
|
||||
|
||||
describe('nextjs-framework fix', () => {
|
||||
describe('should no-op', () => {
|
||||
it('in sb < 7', async () => {
|
||||
const packageJson = { dependencies: { '@storybook/react': '^6.2.0' } };
|
||||
await expect(
|
||||
checkNextjsFramework({
|
||||
packageJson,
|
||||
main: {},
|
||||
})
|
||||
).resolves.toBeFalsy();
|
||||
});
|
||||
|
||||
it('in sb 7 with no main', async () => {
|
||||
const packageJson = { dependencies: { '@storybook/react': '^7.0.0' } };
|
||||
await expect(
|
||||
checkNextjsFramework({
|
||||
packageJson,
|
||||
main: undefined,
|
||||
})
|
||||
).resolves.toBeFalsy();
|
||||
});
|
||||
|
||||
it('in sb 7 with no framework field in main', async () => {
|
||||
const packageJson = { dependencies: { '@storybook/react': '^7.0.0' } };
|
||||
await expect(
|
||||
checkNextjsFramework({
|
||||
packageJson,
|
||||
main: {},
|
||||
})
|
||||
).resolves.toBeFalsy();
|
||||
});
|
||||
|
||||
it('in sb 7 in non-nextjs projects', async () => {
|
||||
const packageJson = { dependencies: { '@storybook/react': '^7.0.0' } };
|
||||
await expect(
|
||||
checkNextjsFramework({
|
||||
packageJson,
|
||||
main: {
|
||||
framework: '@storybook/react',
|
||||
},
|
||||
})
|
||||
).resolves.toBeFalsy();
|
||||
});
|
||||
|
||||
it('in sb 7 with unsupported package', async () => {
|
||||
const packageJson = { dependencies: { '@storybook/riot': '^7.0.0' } };
|
||||
await expect(
|
||||
checkNextjsFramework({
|
||||
packageJson,
|
||||
main: {
|
||||
framework: '@storybook/riot',
|
||||
core: {
|
||||
builder: 'webpack5',
|
||||
},
|
||||
},
|
||||
})
|
||||
).resolves.toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sb >= 7', () => {
|
||||
it('should update from @storybook/react-webpack5 to @storybook/nextjs', async () => {
|
||||
const packageJson = {
|
||||
dependencies: {
|
||||
'@storybook/react': '^7.0.0-alpha.0',
|
||||
'@storybook/react-webpack5': '^7.0.0-alpha.0',
|
||||
next: '^12.0.0',
|
||||
},
|
||||
};
|
||||
await expect(
|
||||
checkNextjsFramework({
|
||||
packageJson,
|
||||
main: {
|
||||
framework: '@storybook/react-webpack5',
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(expect.objectContaining({}));
|
||||
});
|
||||
|
||||
it('should remove legacy addons', async () => {
|
||||
const packageJson = {
|
||||
dependencies: {
|
||||
'@storybook/react': '^7.0.0-alpha.0',
|
||||
'@storybook/react-webpack5': '^7.0.0-alpha.0',
|
||||
next: '^12.0.0',
|
||||
'storybook-addon-next': '^1.0.0',
|
||||
'storybook-addon-next-router': '^1.0.0',
|
||||
},
|
||||
};
|
||||
await expect(
|
||||
checkNextjsFramework({
|
||||
packageJson,
|
||||
main: {
|
||||
framework: '@storybook/react-webpack5',
|
||||
addons: ['storybook-addon-next', 'storybook-addon-next-router'],
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
addonsToRemove: ['storybook-addon-next', 'storybook-addon-next-router'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should move nextjs addon options to frameworkOptions', async () => {
|
||||
const packageJson = {
|
||||
dependencies: {
|
||||
'@storybook/react': '^7.0.0-alpha.0',
|
||||
'@storybook/react-webpack5': '^7.0.0-alpha.0',
|
||||
next: '^12.0.0',
|
||||
'storybook-addon-next': '^1.0.0',
|
||||
},
|
||||
};
|
||||
await expect(
|
||||
checkNextjsFramework({
|
||||
packageJson,
|
||||
main: {
|
||||
framework: { name: '@storybook/react-webpack5', options: { fastRefresh: true } },
|
||||
addons: [
|
||||
{
|
||||
name: 'storybook-addon-next',
|
||||
options: {
|
||||
nextConfigPath: '../next.config.js',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
addonsToRemove: ['storybook-addon-next'],
|
||||
frameworkOptions: {
|
||||
fastRefresh: true,
|
||||
nextConfigPath: '../next.config.js',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it.todo('should just warn for @storybook/react-vite users');
|
||||
});
|
||||
});
|
178
code/lib/cli/src/automigrate/fixes/nextjs-framework.ts
Normal file
178
code/lib/cli/src/automigrate/fixes/nextjs-framework.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import chalk from 'chalk';
|
||||
import dedent from 'ts-dedent';
|
||||
import semver from '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';
|
||||
import { getStorybookVersionSpecifier } from '../../helpers';
|
||||
|
||||
const logger = console;
|
||||
|
||||
interface NextjsFrameworkRunOptions {
|
||||
main: ConfigFile;
|
||||
packageJson: PackageJsonWithDepsAndDevDeps;
|
||||
addonsToRemove: string[];
|
||||
frameworkOptions: Record<string, any>;
|
||||
}
|
||||
|
||||
type Addon = string | { name: string; options?: Record<string, any> };
|
||||
|
||||
export const getNextjsAddonOptions = (addons: Addon[]) => {
|
||||
const nextjsAddon = addons?.find((addon) =>
|
||||
typeof addon === 'string'
|
||||
? addon === 'storybook-addon-next'
|
||||
: addon.name === 'storybook-addon-next'
|
||||
);
|
||||
|
||||
if (!nextjsAddon || typeof nextjsAddon === 'string') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return nextjsAddon.options || {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Does the user have a nextjs project but is not using the @storybook/nextjs framework package?
|
||||
*
|
||||
* If so:
|
||||
* - Remove the dependencies if webpack (@storybook/react-webpack5)
|
||||
* - Install the nextjs package (@storybook/nextjs)
|
||||
* - Uninstall existing legacy addons: storybook-addon-next and storybook-addon-next-router
|
||||
* - Update StorybookConfig type import (if it exists) from react-webpack5 to nextjs
|
||||
* - Update the main config to use the new framework
|
||||
* -- removing legacy addons: storybook-addon-next and storybook-addon-next-router
|
||||
* -- moving storybook-addon-next options into frameworkOptions
|
||||
*/
|
||||
export const nextjsFramework: Fix<NextjsFrameworkRunOptions> = {
|
||||
id: 'nextjsFramework',
|
||||
|
||||
async check({ packageManager }) {
|
||||
const packageJson = packageManager.retrievePackageJson();
|
||||
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
||||
|
||||
if (!allDeps.next) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { mainConfig, version: storybookVersion } = getStorybookInfo(packageJson);
|
||||
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('nextjsFramework')} fix.
|
||||
🤔 Are you running automigrate from your project directory?
|
||||
`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!semver.gte(storybookCoerced, '7.0.0')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const main = await readConfig(mainConfig);
|
||||
|
||||
const frameworkPackage = main.getFieldValue(['framework']);
|
||||
|
||||
if (!frameworkPackage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const frameworkPackageName =
|
||||
typeof frameworkPackage === 'string' ? frameworkPackage : frameworkPackage.name;
|
||||
// we only migrate from react-webpack5 projects
|
||||
if (frameworkPackageName !== '@storybook/react-webpack5') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const addonOptions = getNextjsAddonOptions(main.getFieldValue(['addons']));
|
||||
const frameworkOptions = main.getFieldValue(['framework', 'options']) || {};
|
||||
|
||||
const addonsToRemove = ['storybook-addon-next', 'storybook-addon-next-router'].filter(
|
||||
(dep) => allDeps[dep]
|
||||
);
|
||||
|
||||
return {
|
||||
main,
|
||||
addonsToRemove,
|
||||
frameworkOptions: {
|
||||
...frameworkOptions,
|
||||
...addonOptions,
|
||||
},
|
||||
packageJson,
|
||||
};
|
||||
},
|
||||
|
||||
prompt({ addonsToRemove }) {
|
||||
let addonsMessage = '';
|
||||
|
||||
if (addonsToRemove.length > 0) {
|
||||
addonsMessage = `
|
||||
This package also supports features provided by the following packages, which can now be removed:
|
||||
${addonsToRemove.map((dep) => `- ${chalk.cyan(dep)}`).join(', ')}
|
||||
`;
|
||||
}
|
||||
|
||||
return dedent`
|
||||
We've detected you are using Storybook in a Next.js project.
|
||||
|
||||
In Storybook 7, we introduced a new framework package for Next.js projects: @storybook/nextjs.
|
||||
|
||||
This package is a replacement for @storybook/react-webpack5 and provides a better experience for Next.js users.
|
||||
${addonsMessage}
|
||||
To learn more about this change, see: ${chalk.yellow(
|
||||
'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#nextjs-framework'
|
||||
)}
|
||||
`;
|
||||
},
|
||||
|
||||
async run({
|
||||
result: { addonsToRemove, main, frameworkOptions, packageJson },
|
||||
packageManager,
|
||||
dryRun,
|
||||
}) {
|
||||
const dependenciesToRemove = [...addonsToRemove, '@storybook/react-webpack5'];
|
||||
if (dependenciesToRemove.length > 0) {
|
||||
logger.info(`✅ Removing redundant packages: ${dependenciesToRemove.join(', ')}`);
|
||||
if (!dryRun) {
|
||||
packageManager.removeDependencies({ skipInstall: true, packageJson }, dependenciesToRemove);
|
||||
|
||||
const existingAddons = main.getFieldValue(['addons']) as Addon[];
|
||||
const updatedAddons = existingAddons.filter((addon) => {
|
||||
if (typeof addon === 'string') {
|
||||
return !addonsToRemove.includes(addon);
|
||||
}
|
||||
|
||||
if (addon.name) {
|
||||
return !addonsToRemove.includes(addon.name);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
main.setFieldValue(['addons'], updatedAddons);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`✅ Installing new dependencies: @storybook/nextjs`);
|
||||
if (!dryRun) {
|
||||
const versionToInstall = getStorybookVersionSpecifier(packageJson);
|
||||
packageManager.addDependencies(
|
||||
{ installAsDevDependencies: true, skipInstall: true, packageJson },
|
||||
[`@storybook/nextjs@${versionToInstall}`]
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`✅ Updating framework field in main.js`);
|
||||
if (!dryRun) {
|
||||
main.setFieldValue(['framework', 'options'], frameworkOptions);
|
||||
main.setFieldValue(['framework', 'name'], '@storybook/nextjs');
|
||||
|
||||
await writeConfig(main);
|
||||
}
|
||||
},
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user