mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-09 00:19:13 +08:00
Add experimental-addon-test automigration
This commit is contained in:
parent
34c730c8af
commit
eefe2c709f
@ -0,0 +1,303 @@
|
||||
/* eslint-disable depend/ban-dependencies */
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { JsPackageManager } from 'storybook/internal/common';
|
||||
import type { StorybookConfig } from 'storybook/internal/types';
|
||||
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import dedent from 'ts-dedent';
|
||||
|
||||
import { addonExperimentalTest } from './addon-experimental-test';
|
||||
|
||||
// Mock filesystem and globby
|
||||
vi.mock('fs', async (importOriginal) => {
|
||||
const mod = (await importOriginal()) as any;
|
||||
return {
|
||||
...mod,
|
||||
readFileSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// mock picocolors yellow and cyan
|
||||
vi.mock('picocolors', () => {
|
||||
return {
|
||||
default: {
|
||||
cyan: (str: string) => str,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the dynamic import of globby
|
||||
vi.mock('globby', () => ({
|
||||
globbySync: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockFiles: Record<string, string> = {
|
||||
'.storybook/test-setup.ts': `
|
||||
import { setup } from '@storybook/experimental-addon-test';
|
||||
// Setup code here
|
||||
`,
|
||||
'.storybook/main.ts': `
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
addons: ['@storybook/experimental-addon-test'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
`,
|
||||
'vitest.setup.ts': `
|
||||
import { setup } from '@storybook/experimental-addon-test';
|
||||
// Vitest setup
|
||||
`,
|
||||
'vite.config.ts': `
|
||||
import { defineConfig } from 'vite';
|
||||
import { test } from '@storybook/experimental-addon-test';
|
||||
|
||||
export default defineConfig({
|
||||
// Some config
|
||||
});
|
||||
`,
|
||||
};
|
||||
|
||||
const checkAddonExperimentalTest = async ({
|
||||
packageManager = {},
|
||||
mainConfig = {},
|
||||
storybookVersion = '8.0.0',
|
||||
files = Object.keys(mockFiles),
|
||||
}: {
|
||||
packageManager?: Partial<JsPackageManager>;
|
||||
mainConfig?: Partial<StorybookConfig>;
|
||||
storybookVersion?: string;
|
||||
files?: string[];
|
||||
}) => {
|
||||
// Mock the globbySync function from the globby module
|
||||
const globbyModule = await import('globby');
|
||||
(globbyModule.globbySync as any).mockReturnValue(files);
|
||||
|
||||
return addonExperimentalTest.check({
|
||||
packageManager: packageManager as any,
|
||||
storybookVersion,
|
||||
mainConfig: mainConfig as any,
|
||||
});
|
||||
};
|
||||
|
||||
describe('addon-experimental-test fix', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// @ts-expect-error Ignore
|
||||
vi.mocked(readFileSync).mockImplementation((file: string) => {
|
||||
if (mockFiles[file]) {
|
||||
return mockFiles[file];
|
||||
}
|
||||
throw new Error(`File not found: ${file}`);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('check function', () => {
|
||||
it('should return null if @storybook/experimental-addon-test is not installed', async () => {
|
||||
const packageManager = {
|
||||
getPackageVersion: () => Promise.resolve(null),
|
||||
};
|
||||
await expect(checkAddonExperimentalTest({ packageManager })).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('should find files containing @storybook/experimental-addon-test', async () => {
|
||||
const packageManager = {
|
||||
getPackageVersion: (packageName: string) => {
|
||||
if (packageName === '@storybook/experimental-addon-test') {
|
||||
return Promise.resolve('8.6.0');
|
||||
}
|
||||
if (packageName === 'storybook') {
|
||||
return Promise.resolve('9.0.0');
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
};
|
||||
|
||||
const result = await checkAddonExperimentalTest({ packageManager });
|
||||
expect(result).toEqual({
|
||||
matchingFiles: [
|
||||
'.storybook/test-setup.ts',
|
||||
'.storybook/main.ts',
|
||||
'vitest.setup.ts',
|
||||
'vite.config.ts',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('prompt function', () => {
|
||||
it('should render properly with few files', () => {
|
||||
const matchingFiles = ['.storybook/test-setup.ts', '.storybook/main.ts'];
|
||||
|
||||
const promptResult = addonExperimentalTest.prompt({ matchingFiles });
|
||||
expect(promptResult).toMatchInlineSnapshot(dedent`
|
||||
"We've detected you're using @storybook/experimental-addon-test, which is now available as a stable addon.
|
||||
|
||||
We can automatically migrate your project to use @storybook/addon-test instead.
|
||||
|
||||
This will update 2 file(s) and your package.json:
|
||||
- .storybook/test-setup.ts
|
||||
- .storybook/main.ts"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render properly with many files', () => {
|
||||
const matchingFiles = [
|
||||
'.storybook/test-setup.ts',
|
||||
'.storybook/main.ts',
|
||||
'vitest.setup.ts',
|
||||
'vite.config.ts',
|
||||
'.storybook/preview.ts',
|
||||
'.storybook/preview.js',
|
||||
'.storybook/main.js',
|
||||
];
|
||||
|
||||
const promptResult = addonExperimentalTest.prompt({ matchingFiles });
|
||||
expect(promptResult).toMatchInlineSnapshot(dedent`
|
||||
"We've detected you're using @storybook/experimental-addon-test, which is now available as a stable addon.
|
||||
|
||||
We can automatically migrate your project to use @storybook/addon-test instead.
|
||||
|
||||
This will update 7 file(s) and your package.json:
|
||||
- .storybook/test-setup.ts
|
||||
- .storybook/main.ts
|
||||
- vitest.setup.ts
|
||||
- vite.config.ts
|
||||
- .storybook/preview.ts
|
||||
... and 2 more files"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('run function', () => {
|
||||
it('should replace @storybook/experimental-addon-test in files', async () => {
|
||||
const packageManager = {
|
||||
getPackageVersion: (packageName: string) => {
|
||||
if (packageName === '@storybook/experimental-addon-test') {
|
||||
return Promise.resolve('8.6.0');
|
||||
}
|
||||
if (packageName === 'storybook') {
|
||||
return Promise.resolve('9.0.0');
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
retrievePackageJson: () =>
|
||||
Promise.resolve({
|
||||
dependencies: {},
|
||||
devDependencies: {
|
||||
'@storybook/experimental-addon-test': '8.6.0',
|
||||
},
|
||||
}),
|
||||
removeDependencies: vi.fn(() => Promise.resolve()),
|
||||
addDependencies: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
const matchingFiles = ['.storybook/test-setup.ts', '.storybook/main.ts', 'vitest.setup.ts'];
|
||||
|
||||
await addonExperimentalTest.run?.({
|
||||
result: {
|
||||
matchingFiles,
|
||||
hasPackageJsonDependency: true,
|
||||
},
|
||||
packageManager: packageManager as any,
|
||||
dryRun: false,
|
||||
} as any);
|
||||
|
||||
// Check that each file was read and written with the replacement
|
||||
expect(readFileSync).toHaveBeenCalledTimes(3);
|
||||
expect(writeFileSync).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Verify writeFileSync was called with replaced content
|
||||
matchingFiles.forEach((file) => {
|
||||
expect(writeFileSync).toHaveBeenCalledWith(
|
||||
file,
|
||||
expect.stringContaining('@storybook/addon-test'),
|
||||
'utf-8'
|
||||
);
|
||||
});
|
||||
|
||||
// Verify package dependencies were updated
|
||||
expect(packageManager.removeDependencies).toHaveBeenCalledWith({}, [
|
||||
'@storybook/experimental-addon-test',
|
||||
]);
|
||||
|
||||
expect(packageManager.addDependencies).toHaveBeenCalledWith(
|
||||
{ installAsDevDependencies: true },
|
||||
['@storybook/addon-test@9.0.0']
|
||||
);
|
||||
});
|
||||
|
||||
it('should replace @storybook/experimental-addon-test in files (dependency)', async () => {
|
||||
const packageManager = {
|
||||
getPackageVersion: (packageName: string) => {
|
||||
if (packageName === '@storybook/experimental-addon-test') {
|
||||
return Promise.resolve('8.6.0');
|
||||
}
|
||||
if (packageName === 'storybook') {
|
||||
return Promise.resolve('9.0.0');
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
retrievePackageJson: () =>
|
||||
Promise.resolve({
|
||||
dependencies: {
|
||||
'@storybook/experimental-addon-test': '8.6.0',
|
||||
},
|
||||
devDependencies: {},
|
||||
}),
|
||||
removeDependencies: vi.fn(() => Promise.resolve()),
|
||||
addDependencies: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
const matchingFiles = ['.storybook/test-setup.ts', '.storybook/main.ts', 'vitest.setup.ts'];
|
||||
|
||||
await addonExperimentalTest.run?.({
|
||||
result: {
|
||||
matchingFiles,
|
||||
hasPackageJsonDependency: true,
|
||||
},
|
||||
packageManager: packageManager as any,
|
||||
dryRun: false,
|
||||
} as any);
|
||||
|
||||
expect(packageManager.addDependencies).toHaveBeenCalledWith(
|
||||
{ installAsDevDependencies: false },
|
||||
['@storybook/addon-test@9.0.0']
|
||||
);
|
||||
});
|
||||
|
||||
it('should not modify files or dependencies in dry run mode', async () => {
|
||||
const packageManager = {
|
||||
getPackageVersion: () => Promise.resolve('0.2.0'),
|
||||
removeDependencies: vi.fn(),
|
||||
addDependencies: vi.fn(),
|
||||
};
|
||||
|
||||
const matchingFiles = ['.storybook/test-setup.ts'];
|
||||
|
||||
await addonExperimentalTest.run?.({
|
||||
result: {
|
||||
matchingFiles,
|
||||
hasPackageJsonDependency: true,
|
||||
},
|
||||
packageManager: packageManager as any,
|
||||
dryRun: true,
|
||||
} as any);
|
||||
|
||||
// Files should be read but not written in dry run mode
|
||||
expect(readFileSync).toHaveBeenCalledTimes(1);
|
||||
expect(writeFileSync).not.toHaveBeenCalled();
|
||||
|
||||
// Package dependencies should not be modified in dry run mode
|
||||
expect(packageManager.removeDependencies).not.toHaveBeenCalled();
|
||||
expect(packageManager.addDependencies).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,123 @@
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import picocolors from 'picocolors';
|
||||
import { dedent } from 'ts-dedent';
|
||||
|
||||
import type { Fix } from '../types';
|
||||
|
||||
const logger = console;
|
||||
|
||||
interface AddonExperimentalTestOptions {
|
||||
matchingFiles: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This fix migrates users from @storybook/experimental-addon-test to @storybook/addon-test
|
||||
*
|
||||
* It will:
|
||||
*
|
||||
* - Replace all instances of @storybook/experimental-addon-test with @storybook/addon-test in all
|
||||
* project files
|
||||
* - Update package.json dependencies if needed
|
||||
*/
|
||||
export const addonExperimentalTest: Fix<AddonExperimentalTestOptions> = {
|
||||
id: 'addon-experimental-test',
|
||||
|
||||
versionRange: ['*', '*'],
|
||||
|
||||
promptType: 'auto',
|
||||
|
||||
async check({ packageManager }) {
|
||||
const experimentalAddonTestVersion = await packageManager.getPackageVersion(
|
||||
'@storybook/experimental-addon-test'
|
||||
);
|
||||
|
||||
if (!experimentalAddonTestVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dynamically import fast-glob to find files
|
||||
// eslint-disable-next-line depend/ban-dependencies
|
||||
const { globbySync } = await import('globby');
|
||||
|
||||
// Find all files that contain @storybook/experimental-addon-test
|
||||
const matchingFiles = globbySync(
|
||||
['**/.storybook/**/*.*', '**/vitest.*.{js,ts,mjs,cjs}', '**/vite.config.{js,ts,mjs,cjs}'],
|
||||
{
|
||||
ignore: ['**/node_modules/**', '**/dist/**'],
|
||||
}
|
||||
);
|
||||
|
||||
const filesWithExperimentalAddon = [];
|
||||
|
||||
for (const file of matchingFiles) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
if (content.includes('@storybook/experimental-addon-test')) {
|
||||
filesWithExperimentalAddon.push(file);
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip files that can't be read
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
matchingFiles: filesWithExperimentalAddon,
|
||||
};
|
||||
},
|
||||
|
||||
prompt({ matchingFiles }) {
|
||||
const fileCount = matchingFiles.length;
|
||||
const fileList = matchingFiles
|
||||
.slice(0, 5)
|
||||
.map((file) => ` - ${picocolors.cyan(file)}`)
|
||||
.join('\n');
|
||||
const hasMoreFiles = fileCount > 5;
|
||||
|
||||
return dedent`
|
||||
We've detected you're using ${picocolors.cyan('@storybook/experimental-addon-test')}, which is now available as a stable addon.
|
||||
|
||||
We can automatically migrate your project to use ${picocolors.cyan('@storybook/addon-test')} instead.
|
||||
|
||||
This will update ${fileCount} file(s) and your package.json:
|
||||
${fileList}${hasMoreFiles ? `\n ... and ${fileCount - 5} more files` : ''}
|
||||
`;
|
||||
},
|
||||
|
||||
async run({ result: { matchingFiles }, packageManager, dryRun }) {
|
||||
// Update all files that contain @storybook/experimental-addon-test
|
||||
for (const file of matchingFiles) {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const updatedContent = content.replace(
|
||||
/@storybook\/experimental-addon-test/g,
|
||||
'@storybook/addon-test'
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
writeFileSync(file, updatedContent, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
// Update package.json if needed
|
||||
if (!dryRun) {
|
||||
const packageJson = await packageManager.retrievePackageJson();
|
||||
const devDependencies = packageJson.devDependencies ?? {};
|
||||
const storybookVersion = await packageManager.getPackageVersion('storybook');
|
||||
const isExperimentalAddonTestDevDependency = Object.keys(devDependencies).includes(
|
||||
'@storybook/experimental-addon-test'
|
||||
);
|
||||
|
||||
await packageManager.removeDependencies({}, ['@storybook/experimental-addon-test']);
|
||||
await packageManager.addDependencies(
|
||||
{ installAsDevDependencies: isExperimentalAddonTestDevDependency },
|
||||
[`@storybook/addon-test@${storybookVersion}`]
|
||||
);
|
||||
}
|
||||
|
||||
// Log success message instead of returning it
|
||||
logger.info(dedent`
|
||||
✅ Successfully migrated from ${picocolors.cyan('@storybook/experimental-addon-test')} to ${picocolors.cyan('@storybook/addon-test')}
|
||||
✅ Updated package.json dependency
|
||||
✅ Updated ${matchingFiles.length} file(s)
|
||||
`);
|
||||
},
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { csfFactories } from '../../codemod/csf-factories';
|
||||
import type { CommandFix, Fix } from '../types';
|
||||
import { addonA11yAddonTest } from './addon-a11y-addon-test';
|
||||
import { addonExperimentalTest } from './addon-experimental-test';
|
||||
import { addonPostCSS } from './addon-postcss';
|
||||
import { addonsAPI } from './addons-api';
|
||||
import { angularBuilders } from './angular-builders';
|
||||
@ -68,6 +69,7 @@ export const allFixes: Fix[] = [
|
||||
autodocsTags,
|
||||
initialGlobals,
|
||||
addonA11yAddonTest,
|
||||
addonExperimentalTest,
|
||||
];
|
||||
|
||||
export const initFixes: Fix[] = [eslintPlugin];
|
||||
|
Loading…
x
Reference in New Issue
Block a user