Merge remote-tracking branch 'origin/next' into valentin/move-addon-actions-into-core

This commit is contained in:
Valentin Palkovic 2025-03-10 08:48:01 +01:00
commit 43287545e2
6 changed files with 585 additions and 12 deletions

View File

@ -878,10 +878,11 @@ workflows:
parallelism: 4
requires:
- create-sandboxes
- bench-sandboxes:
parallelism: 5
requires:
- create-sandboxes
# TODO: don't forget to reenable this
# - bench-sandboxes:
# parallelism: 5
# requires:
# - create-sandboxes
- test-ui-testing-module:
requires:
- build
@ -965,10 +966,11 @@ workflows:
- test-init-features:
requires:
- build
- bench-sandboxes:
parallelism: 5
requires:
- create-sandboxes
# TODO: don't forget to reenable this
# - bench-sandboxes:
# parallelism: 5
# requires:
# - create-sandboxes
# TODO: reenable once we find out the source of flakyness
# - test-runner-dev:
# parallelism: 4
@ -1059,10 +1061,11 @@ workflows:
# --smoke-test is not supported for the angular builder right now
# - "angular-cli"
- "lit-vite-ts"
- bench-sandboxes:
parallelism: 5
requires:
- create-sandboxes
# TODO: don't forget to reenable this
# - bench-sandboxes:
# parallelism: 5
# requires:
# - create-sandboxes
# TODO: reenable once we find out the source of flakyness
# - test-runner-dev:

View File

@ -2,6 +2,7 @@
- [From version 8.x to 9.0.0](#from-version-8x-to-900)
- [Actions addon moved to core](#actions-addon-moved-to-core)
- [Dropped support for legacy packages](#dropped-support-for-legacy-packages)
- [Dropped support for TypeScript \< 4.9](#dropped-support-for-typescript--49)
- [Test addon renamed from experimental to stable](#test-addon-renamed-from-experimental-to-stable)
- [From version 8.5.x to 8.6.x](#from-version-85x-to-86x)
@ -443,6 +444,45 @@ The actions addon has been moved from `@storybook/addon-actions` to Storybook co
Furthermore, we have deprecated the usage of `withActions` from `@storybook/addon-actions` and we will remove it in Storybook v10. Please file an issue if you need this API.
### Dropped support for legacy packages
The following packages are no longer published as part of `9.0.0`:
The following packages have been consolidated into the main `storybook` package:
| Old Package | New Path |
| ---------------------- | --------------------- |
| @storybook/manager-api | storybook/manager-api |
| @storybook/preview-api | storybook/preview-api |
| @storybook/theming | storybook/theming |
| @storybook/test | storybook/test |
Please un-install these packages, and ensure you have the `storybook` package installed.
Replace any imports with the path listed in the second column.
Additionally the following packages were also consolidated and placed under a `/internal` sub-path, to indicate they are for internal usage only.
If you're depending on these packages, they will continue to work for `9.0`, but they will likely be removed in `10.0`.
| Old Package | New Path |
| -------------------------- | ---------------------------------- |
| @storybook/channels | storybook/internal/channels |
| @storybook/client-logger | storybook/internal/client-logger |
| @storybook/core-common | storybook/internal/common |
| @storybook/core-events | storybook/internal/core-events |
| @storybook/csf-tools | storybook/internal/csf-tools |
| @storybook/docs-tools | storybook/internal/docs-tools |
| @storybook/node-logger | storybook/internal/node-logger |
| @storybook/router | storybook/internal/router |
| @storybook/telemetry | storybook/internal/telemetry |
| @storybook/types | storybook/internal/types |
| @storybook/manager | storybook/internal/manager |
| @storybook/preview | storybook/internal/preview |
| @storybook/core-server | storybook/internal/core-server |
| @storybook/builder-manager | storybook/internal/builder-manager |
| @storybook/components | storybook/internal/components |
Addon authors may continue to use the internal packages, there is currently not yet any replacement.
### Dropped support for TypeScript < 4.9
Storybook now requires TypeScript 4.9 or later. If you're using an older version of TypeScript, you'll need to upgrade to continue using Storybook.

View File

@ -0,0 +1,298 @@
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.not.stringContaining('"@storybook/core-common": "^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 import declarations with sub-paths', async () => {
const sourceContents = dedent`
import { other } from '@storybook/theming/create';
`;
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/theming/create'`)
);
});
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),
});
});
});

View File

@ -0,0 +1,203 @@
import { readFile, writeFile } from 'node:fs/promises';
import { commonGlobOptions, getProjectRoot } from 'storybook/internal/common';
import prompts from 'prompts';
import { dedent } from 'ts-dedent';
import { consolidatedPackages } from '../helpers/consolidated-packages';
import type { Fix, RunOptions } from '../types';
export interface ConsolidatedOptions {
packageJsonFiles: string[];
}
function transformPackageJson(content: string): string | null {
const packageJson = JSON.parse(content);
let hasChanges = false;
// Check dependencies
if (packageJson.dependencies) {
for (const [dep, version] of Object.entries(packageJson.dependencies)) {
if (dep in consolidatedPackages) {
delete packageJson.dependencies[dep];
hasChanges = true;
}
}
}
// Check devDependencies
if (packageJson.devDependencies) {
for (const [dep, version] of Object.entries(packageJson.devDependencies)) {
if (dep in consolidatedPackages) {
delete packageJson.devDependencies[dep];
hasChanges = true;
}
}
}
return hasChanges ? JSON.stringify(packageJson, null, 2) : null;
}
function transformImports(source: string) {
let hasChanges = false;
let transformed = source;
for (const [from, to] of Object.entries(consolidatedPackages)) {
// Match the package name when it's inside either single or double quotes
const regex = new RegExp(`(['"])${from}(.*)\\1`, 'g');
if (regex.test(transformed)) {
transformed = transformed.replace(regex, `$1${to}$2$1`);
hasChanges = true;
}
}
return hasChanges ? transformed : null;
}
export const transformPackageJsonFiles = async (files: string[], dryRun: boolean) => {
const errors: Array<{ file: string; error: Error }> = [];
const { default: pLimit } = await import('p-limit');
const limit = pLimit(10);
await Promise.all(
files.map((file) =>
limit(async () => {
try {
const contents = await readFile(file, 'utf-8');
const transformed = transformPackageJson(contents);
if (!dryRun && transformed) {
await writeFile(file, transformed);
}
} catch (error) {
errors.push({ file, error: error as Error });
}
})
)
);
return errors;
};
export const transformImportFiles = async (files: string[], dryRun: boolean) => {
const errors: Array<{ file: string; error: Error }> = [];
const { default: pLimit } = await import('p-limit');
const limit = pLimit(10);
await Promise.all(
files.map((file) =>
limit(async () => {
try {
const contents = await readFile(file, 'utf-8');
const transformed = transformImports(contents);
if (!dryRun && transformed) {
await writeFile(file, transformed);
}
} catch (error) {
errors.push({ file, error: error as Error });
}
})
)
);
return errors;
};
export const consolidatedImports: Fix<ConsolidatedOptions> = {
id: 'consolidated-imports',
versionRange: ['^8.0.0', '^9.0.0-0'],
check: async () => {
const projectRoot = getProjectRoot();
// eslint-disable-next-line depend/ban-dependencies
const globby = (await import('globby')).globby;
const packageJsonFiles = await globby(['**/package.json'], {
...commonGlobOptions(''),
ignore: ['**/node_modules/**'],
cwd: projectRoot,
gitignore: true,
});
// check if any of the package.json files have consolidated packages
const hasConsolidatedDependencies = await Promise.all(
packageJsonFiles.map(async (file) => {
const contents = await readFile(file, 'utf-8');
const packageJson = JSON.parse(contents);
return (
Object.keys(packageJson.dependencies || {}).some((dep) => dep in consolidatedPackages) ||
Object.keys(packageJson.devDependencies || {}).some((dep) => dep in consolidatedPackages)
);
})
).then((results) => results.some(Boolean));
if (!hasConsolidatedDependencies) {
return null;
}
return {
packageJsonFiles,
};
},
prompt: (result: ConsolidatedOptions) => {
return dedent`
Found package.json files that contain consolidated Storybook packages that need to be updated:
${result.packageJsonFiles.map((file) => `- ${file}`).join('\n')}
These packages have been consolidated into the main storybook package and should be removed.
The main storybook package will be added to devDependencies if not already present.
Would you like to:
1. Update these package.json files
2. Scan your codebase and update any imports from these consolidated packages
This will ensure your project is properly updated to use the new consolidated package structure.
`;
},
run: async (options: RunOptions<ConsolidatedOptions>) => {
const { result, dryRun = false } = options;
const { packageJsonFiles } = result;
const errors: Array<{ file: string; error: Error }> = [];
const packageJsonErrors = await transformPackageJsonFiles(packageJsonFiles, dryRun);
errors.push(...packageJsonErrors);
const projectRoot = getProjectRoot();
const defaultGlob = '**/*.{mjs,cjs,js,jsx,ts,tsx}';
// Find all files matching the glob pattern
const { glob } = await prompts({
type: 'text',
name: 'glob',
message: 'Enter a custom glob pattern to scan (or press enter to use default):',
initial: defaultGlob,
});
// eslint-disable-next-line depend/ban-dependencies
const globby = (await import('globby')).globby;
const sourceFiles = await globby([glob], {
...commonGlobOptions(''),
ignore: ['**/node_modules/**'],
dot: true,
cwd: projectRoot,
});
const importErrors = await transformImportFiles(sourceFiles, dryRun);
errors.push(...importErrors);
if (errors.length > 0) {
// eslint-disable-next-line local-rules/no-uncategorized-errors
throw new Error(
`Failed to process ${errors.length} files:\n${errors
.map(({ file, error }) => `- ${file}: ${error.message}`)
.join('\n')}`
);
}
if (!dryRun && result.packageJsonFiles.length > 0) {
await options.packageManager.installDependencies();
}
},
};

View File

@ -9,6 +9,7 @@ import { angularBuildersMultiproject } from './angular-builders-multiproject';
import { autodocsTags } from './autodocs-tags';
import { autodocsTrue } from './autodocs-true';
import { builderVite } from './builder-vite';
import { consolidatedImports } from './consolidated-imports';
import { cra5 } from './cra5';
import { eslintPlugin } from './eslint-plugin';
import { initialGlobals } from './initial-globals';
@ -67,6 +68,7 @@ export const allFixes: Fix[] = [
autodocsTags,
initialGlobals,
addonA11yAddonTest,
consolidatedImports,
addonExperimentalTest,
];

View File

@ -0,0 +1,27 @@
/**
* Consolidated packages are packages that have been merged into the main storybook package. This
* object maps the old package name to the new package name.
*/
export const consolidatedPackages = {
'@storybook/channels': 'storybook/internal/channels',
'@storybook/client-logger': 'storybook/internal/client-logger',
'@storybook/core-common': 'storybook/internal/common',
'@storybook/core-events': 'storybook/internal/core-events',
'@storybook/csf-tools': 'storybook/internal/csf-tools',
'@storybook/docs-tools': 'storybook/internal/docs-tools',
'@storybook/node-logger': 'storybook/internal/node-logger',
'@storybook/preview-api': 'storybook/internal/preview-api',
'@storybook/router': 'storybook/internal/router',
'@storybook/telemetry': 'storybook/internal/telemetry',
'@storybook/theming': 'storybook/internal/theming',
'@storybook/types': 'storybook/internal/types',
'@storybook/manager-api': 'storybook/internal/manager-api',
'@storybook/manager': 'storybook/internal/manager',
'@storybook/preview': 'storybook/internal/preview',
'@storybook/core-server': 'storybook/internal/core-server',
'@storybook/builder-manager': 'storybook/internal/builder-manager',
'@storybook/components': 'storybook/internal/components',
'@storybook/test': 'storybook/test',
} as const;
export type ConsolidatedPackage = keyof typeof consolidatedPackages;