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

Migration: Add auto-automigration for merged packages #30753

Merged
merged 31 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
29421c9
Refactor consolidated-imports fix with improved error handling and Ba…
ndelangen Mar 3, 2025
557c3f0
improve automigration
ndelangen Mar 5, 2025
efd8fad
Merge branch 'next' into norbert/automigrate-cpc-complaince
ndelangen Mar 5, 2025
9bcd854
Fix: Dynamically import p-limit in consolidated imports
ndelangen Mar 5, 2025
ded0706
Fix: Correct p-limit import syntax in consolidated imports
ndelangen Mar 5, 2025
ab2c87a
Refactor: Extract consolidated packages into a separate helper module
ndelangen Mar 5, 2025
ecb0ad8
Feature: Add consolidated package.json automigration fix
ndelangen Mar 5, 2025
b34c50a
Refactor: Remove unused import in consolidated package.json automigra…
ndelangen Mar 5, 2025
0064b85
Automigrate: Add consolidated package.json fix to allFixes array
ndelangen Mar 5, 2025
495a98a
Merge branch 'next' into norbert/automigrate-cpc-complaince
ndelangen Mar 5, 2025
ea3aeb0
Refactor: Consolidate import and package.json migration fixes
ndelangen Mar 6, 2025
13bfc76
Merge branch 'next' into norbert/automigrate-cpc-complaince
ndelangen Mar 6, 2025
2eee9fd
Remove consolidated package.json migration fix
ndelangen Mar 6, 2025
34fc807
Automigrate: Add gitignore support to file globbing
ndelangen Mar 6, 2025
aac6929
Automigrate: Remove Storybook version addition to devDependencies
ndelangen Mar 6, 2025
6af03c2
Refactor: Simplify consolidated imports transformation
ndelangen Mar 6, 2025
1047465
Automigrate: Add custom glob pattern support for consolidated imports
ndelangen Mar 6, 2025
5e91a64
Automigrate: Add dependency installation for consolidated imports
ndelangen Mar 6, 2025
42f8105
Automigrate: Add '@storybook/test' to consolidated packages
ndelangen Mar 6, 2025
0bedebf
Automigrate: Add documentation for consolidated packages mapping
ndelangen Mar 6, 2025
c3bc0d2
Automigrate: Dynamically import p-limit for consolidated imports
ndelangen Mar 6, 2025
feeeab6
Automigrate: Remove dependencies field from BaseFix type
ndelangen Mar 6, 2025
a8450d7
Update code/lib/cli-storybook/src/automigrate/fixes/consolidated-impo…
ndelangen Mar 6, 2025
f0c282f
Migration: Document dropped support for legacy Storybook packages in …
ndelangen Mar 6, 2025
d7a667f
Update MIGRATION.md
ndelangen Mar 7, 2025
7d25490
Merge branch 'next' into norbert/automigrate-cpc-complaince
valentinpalkovic Mar 7, 2025
2213e7f
Update @storybook/test package mapping
valentinpalkovic Mar 7, 2025
0519e60
Support dot folders and adjust regex to support sub-paths
valentinpalkovic Mar 7, 2025
46e4387
Merge branch 'norbert/automigrate-cpc-complaince' into norbert/manual…
ndelangen Mar 7, 2025
773405d
Update MIGRATION.md
valentinpalkovic Mar 9, 2025
c657af9
Merge pull request #30766 from storybookjs/norbert/manual-cpc-migration
valentinpalkovic Mar 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';

import { describe, expect, it, vi } from 'vitest';

import { dedent } from 'ts-dedent';

import {
consolidatedImports,
transformImportFiles,
transformPackageJsonFiles,
} from './consolidated-imports';

vi.mock('node:fs/promises');
vi.mock('globby', () => ({
globby: vi.fn(),
}));

const mockPackageJson = {
dependencies: {
'@storybook/react': '^7.0.0',
'@storybook/core-common': '^7.0.0',
react: '^18.0.0',
},
devDependencies: {
'@storybook/addon-essentials': '^7.0.0',
'@storybook/manager-api': '^7.0.0',
typescript: '^5.0.0',
},
};

const mockRunOptions = {
packageManager: {
retrievePackageJson: async () => mockPackageJson,
} as any,
mainConfig: {} as any,
mainConfigPath: 'main.ts',
packageJson: mockPackageJson,
};

const setupGlobby = async (files: string[]) => {
// eslint-disable-next-line depend/ban-dependencies
const { globby } = await import('globby');
vi.mocked(globby).mockResolvedValueOnce(files);
};

const setupCheck = async (packageJsonContents: string, packageJsonFiles: string[]) => {
vi.mocked(readFile).mockImplementation(async (path: any) => {
const filePath = path.toString();
if (filePath.endsWith('package.json')) {
return packageJsonContents;
}
return '';
});
await setupGlobby(packageJsonFiles);

return consolidatedImports.check({
...mockRunOptions,
storybookVersion: '8.0.0',
});
};

describe('check', () => {
it('should call globby with correct patterns for package.json files', async () => {
const filePath = 'test/package.json';
const contents = JSON.stringify(mockPackageJson);

await setupCheck(contents, [filePath]);

// eslint-disable-next-line depend/ban-dependencies
const { globby } = await import('globby');
expect(globby).toHaveBeenCalledWith(
['**/package.json'],
expect.objectContaining({
ignore: ['**/node_modules/**'],
})
);
});

it('should detect consolidated packages in package.json', async () => {
const contents = JSON.stringify(mockPackageJson);
const filePath = 'test/package.json';

const result = await setupCheck(contents, [filePath]);
expect(result).toMatchObject({
packageJsonFiles: [filePath],
});
});

it('should not detect non-consolidated packages in package.json', async () => {
const packageJsonWithoutConsolidated = {
dependencies: {
react: '^18.0.0',
},
devDependencies: {
typescript: '^5.0.0',
},
};
const contents = JSON.stringify(packageJsonWithoutConsolidated);
const filePath = 'test/package.json';

const result = await setupCheck(contents, [filePath]);
expect(result).toBeNull();
});
});

describe('transformPackageJsonFiles', () => {
it('should transform package.json files', async () => {
const contents = JSON.stringify(mockPackageJson);
const filePath = 'test/package.json';

vi.mocked(readFile).mockResolvedValueOnce(contents);

const errors = await transformPackageJsonFiles([filePath], false);

expect(errors).toHaveLength(0);
expect(writeFile).toHaveBeenCalledWith(
filePath,
expect.stringContaining('"storybook": "^8.0.0"')
);
});

it('should not write files in dry run mode', async () => {
const contents = JSON.stringify(mockPackageJson);
const filePath = 'test/package.json';

vi.mocked(readFile).mockResolvedValueOnce(contents);

const errors = await transformPackageJsonFiles([filePath], true);

expect(errors).toHaveLength(0);
expect(writeFile).not.toHaveBeenCalled();
});

it('should handle file read errors', async () => {
const filePath = 'test/package.json';
vi.mocked(readFile).mockRejectedValueOnce(new Error('Failed to read file'));

const errors = await transformPackageJsonFiles([filePath], false);

expect(errors).toHaveLength(1);
expect(errors[0]).toMatchObject({
file: filePath,
error: expect.any(Error),
});
});

it('should handle file write errors', async () => {
const contents = JSON.stringify(mockPackageJson);
const filePath = 'test/package.json';
vi.mocked(readFile).mockResolvedValueOnce(contents);
vi.mocked(writeFile).mockRejectedValueOnce(new Error('Failed to write file'));

const errors = await transformPackageJsonFiles([filePath], false);

expect(errors).toHaveLength(1);
expect(errors[0]).toMatchObject({
file: filePath,
error: expect.any(Error),
});
});
});

describe('transformImportFiles', () => {
it('should transform import declarations', async () => {
const sourceContents = dedent`
import { something } from '@storybook/components';
import { other } from '@storybook/core-common';
`;
const sourceFiles = [join('src', 'test.ts')];

vi.mocked(readFile).mockResolvedValueOnce(sourceContents);

const errors = await transformImportFiles(sourceFiles, false);

expect(errors).toHaveLength(0);
expect(writeFile).toHaveBeenCalledWith(
sourceFiles[0],
expect.stringContaining('from "storybook/internal/components"')
);
});

it('should transform require calls', async () => {
const sourceContents = dedent`
const something = require('@storybook/components');
const other = require('@storybook/core-common');
`;
const sourceFiles = [join('src', 'test.ts')];

vi.mocked(readFile).mockResolvedValueOnce(sourceContents);

const errors = await transformImportFiles(sourceFiles, false);

expect(errors).toHaveLength(0);
expect(writeFile).toHaveBeenCalledWith(
sourceFiles[0],
expect.stringContaining('require("storybook/internal/components")')
);
});

it('should handle mixed import styles', async () => {
const sourceContents = dedent`
import { something } from '@storybook/components';
const other = require('@storybook/core-common');
`;
const sourceFiles = [join('src', 'test.ts')];

vi.mocked(readFile).mockResolvedValueOnce(sourceContents);

const errors = await transformImportFiles(sourceFiles, false);

expect(errors).toHaveLength(0);
expect(writeFile).toHaveBeenCalledWith(
sourceFiles[0],
expect.stringContaining('from "storybook/internal/components"')
);
expect(writeFile).toHaveBeenCalledWith(
sourceFiles[0],
expect.stringContaining('require("storybook/internal/common")')
);
});

it('should not transform non-consolidated package imports', async () => {
const sourceContents = `
import { something } from '@storybook/other-package';
const other = require('some-other-package');
`;
const sourceFiles = [join('src', 'test.ts')];

vi.mocked(readFile).mockResolvedValueOnce(sourceContents);

const errors = await transformImportFiles(sourceFiles, false);

expect(errors).toHaveLength(0);
expect(writeFile).not.toHaveBeenCalledWith(sourceFiles[0], expect.any(String));
});

it('should not write files in dry run mode', async () => {
const sourceContents = dedent`
import { something } from '@storybook/components';
`;
const sourceFiles = [join('src', 'test.ts')];

vi.mocked(readFile).mockResolvedValueOnce(sourceContents);

const errors = await transformImportFiles(sourceFiles, true);

expect(errors).toHaveLength(0);
expect(writeFile).not.toHaveBeenCalled();
});

it('should handle file read errors', async () => {
const sourceFiles = [join('src', 'test.ts')];
vi.mocked(readFile).mockRejectedValueOnce(new Error('Failed to read file'));

const errors = await transformImportFiles(sourceFiles, false);

expect(errors).toHaveLength(1);
expect(errors[0]).toMatchObject({
file: sourceFiles[0],
error: expect.any(Error),
});
});

it('should handle file write errors', async () => {
const sourceContents = dedent`
import { something } from '@storybook/components';
`;
const sourceFiles = [join('src', 'test.ts')];
vi.mocked(readFile).mockResolvedValueOnce(sourceContents);
vi.mocked(writeFile).mockRejectedValueOnce(new Error('Failed to write file'));

const errors = await transformImportFiles(sourceFiles, false);

expect(errors).toHaveLength(1);
expect(errors[0]).toMatchObject({
file: sourceFiles[0],
error: expect.any(Error),
});
});
});
Loading
Loading