-
-
Notifications
You must be signed in to change notification settings - Fork 9.8k
CLI: Improve support for upgrading Storybook in monorepos #31557
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
Conversation
CLI: Add prompts abstraction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
192 files reviewed, 33 comments
Edit PR Review Bot Settings | Greptile
| const cleanLog = (str) => { | ||
| const pattern = [ | ||
| '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', | ||
| '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', | ||
| ].join('|'); | ||
|
|
||
| return str.replace(new RegExp(pattern, 'g'), ''); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider making the regex pattern a constant to avoid recompiling it on every function call. This would improve performance when cleanLog is called frequently.
| const cleanLog = (str) => { | |
| const pattern = [ | |
| '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', | |
| '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', | |
| ].join('|'); | |
| return str.replace(new RegExp(pattern, 'g'), ''); | |
| }; | |
| const ANSI_PATTERN = new RegExp([ | |
| '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', | |
| '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', | |
| ].join('|'), 'g'); | |
| const cleanLog = (str) => { | |
| return str.replace(ANSI_PATTERN, ''); | |
| }; |
| import { | ||
| getIncompatiblePackagesSummary, | ||
| getIncompatibleStorybookPackages, | ||
| } from '../../../../lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider using direct imports instead of importing through 4 directory levels with '../../../../'
| const storyFilePath = | ||
| doesStoryFileExist(join(cwd, dir), storyFileName) && componentExportCount > 1 | ||
| ? join(cwd, dir, alternativeStoryFileNameWithExtension) | ||
| : join(cwd, dir, storyFileNameWithExtension); | ||
| doesStoryFileExist(join(getProjectRoot(), dir), storyFileName) && componentExportCount > 1 | ||
| ? join(getProjectRoot(), dir, alternativeStoryFileNameWithExtension) | ||
| : join(getProjectRoot(), dir, storyFileNameWithExtension); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Repetitive getProjectRoot() calls could be optimized by storing result in a variable
| const storyFilePath = | |
| doesStoryFileExist(join(cwd, dir), storyFileName) && componentExportCount > 1 | |
| ? join(cwd, dir, alternativeStoryFileNameWithExtension) | |
| : join(cwd, dir, storyFileNameWithExtension); | |
| doesStoryFileExist(join(getProjectRoot(), dir), storyFileName) && componentExportCount > 1 | |
| ? join(getProjectRoot(), dir, alternativeStoryFileNameWithExtension) | |
| : join(getProjectRoot(), dir, storyFileNameWithExtension); | |
| const projectRoot = getProjectRoot(); | |
| const storyFilePath = | |
| doesStoryFileExist(join(projectRoot, dir), storyFileName) && componentExportCount > 1 | |
| ? join(projectRoot, dir, alternativeStoryFileNameWithExtension) | |
| : join(projectRoot, dir, storyFileNameWithExtension); |
| TaskLogOptions, | ||
| TextPromptOptions, | ||
| } from './prompt-provider-base'; | ||
| import { asyncLocalStorage } from './storage'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: asyncLocalStorage is imported but never used
| const allDependencies = packageManager.getAllDependencies(); | ||
| const { packageJson } = packageManager.primaryPackageJson; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: getAllDependencies() called without await but appears to be synchronous now - verify this change is intentional
| const configFile = join(getProjectRoot(), monorepoConfigs[monorepo]); | ||
| return existsSync(configFile); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: getProjectRoot() called multiple times may introduce overhead if expensive. Consider storing result in a variable.
| function getEnvFromTerminal(key: string): string { | ||
| return execaSync('echo', [`$${key}`], { shell: true }).stdout.trim(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Shell command execution can fail in restricted environments. Consider using process.env directly as a safer fallback.
| it('throws an error if command output is not a valid JSON', async () => { | ||
| vi.spyOn(yarn1Proxy, 'executeCommand').mockResolvedValueOnce('NOT A JSON'); | ||
| vi.spyOn(yarn1Proxy, 'executeCommand').mockReturnValue( | ||
| Promise.resolve({ stdout: 'NOT A JSON' }) as any | ||
| ); | ||
|
|
||
| await expect(yarn1Proxy.latestVersion('storybook')).rejects.toThrow(); | ||
| await expect(yarn1Proxy.latestVersion('storybook')).resolves.toBe(null); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Error handling behavior changed from throwing to returning null. Ensure this change was intentional and documented, as it affects error handling in dependent code
| it('throws an error if command output is not a valid JSON', async () => { | |
| vi.spyOn(yarn1Proxy, 'executeCommand').mockResolvedValueOnce('NOT A JSON'); | |
| vi.spyOn(yarn1Proxy, 'executeCommand').mockReturnValue( | |
| Promise.resolve({ stdout: 'NOT A JSON' }) as any | |
| ); | |
| await expect(yarn1Proxy.latestVersion('storybook')).rejects.toThrow(); | |
| await expect(yarn1Proxy.latestVersion('storybook')).resolves.toBe(null); | |
| }); | |
| it('returns null if command output is not a valid JSON', async () => { | |
| vi.spyOn(yarn1Proxy, 'executeCommand').mockReturnValue( | |
| Promise.resolve({ stdout: 'NOT A JSON' }) as any | |
| ); | |
| await expect(yarn1Proxy.latestVersion('storybook')).resolves.toBe(null); | |
| }); |
| migration: any, | ||
| { glob, dryRun, list, rename, parser, configDir: userSpecifiedConfigDir }: CLIOptions | ||
| ) { | ||
| export async function migrate(migration: any, { glob, dryRun, list, rename, parser }: CLIOptions) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: The configDir option is defined in CLIOptions but not destructured in the function parameters
| if (level === 'prompt') { | ||
| level = 'info'; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Mutating level parameter can cause unexpected behavior. Consider using a new variable
…ents CLI: Improve support for upgrading Storybook in monorepos (cherry picked from commit 9c2f2fd)
|
@valentinpalkovic can you please add a manual testing section, so we can do QA? |
|
This feature was already properly QAed for 9.1. It was missed to remove the |
Closes #31517
What I did
This PR completely revamps our upgrade process to introduce better mono repository and introduces a visually improved CLI experience for the upgrade.
Improved upgrade experience for mono-repositories
Storybook's upgrade CLI (
npx storybook@<next|latest> upgrade) should now always be executed in your project root. The CLI automatically detects all of your Storybook projects. If the Storybook projects are truly encapsulated, you can choose to upgrade one by one. Otherwise, we will upgrade all of your Storybooks at once.Upgrade: Slimed down messaging and collecting results
The previous upgrade experience was packed with a lot of messages and links. Also, the upgrade CLI was shining in a variety of colors. We have reduced messaging to a minimum and outsourced debugging messages, warnings and errors to a debug log.
Additionally: We have also introduced new flags to introduce different log levels and to write all upgrade logs to a log file:
--write-logs: Write all debug logs to a file at the end of the run--loglevel: <trace | debug | info | warn | error | silent> Define log level (default: "info")Improved the
doctorintegration inside of upgradeThe doctor command was fine-tuned to collect results from multiple projects simultaneously. A user upgrading multiple Storybooks at once will get exact information of what kind of conflicts they have for a particular Storybook.
Introducing the technical foundation to adjust other parts of our CLI
We have revived the
@storybook/node-loggerpackage to encapsulate tooling for logging. Via a simple mechanism, we can switch the internal implementation of our loggers and prompt functions from thepromptandboxenlibrary to the new modern packageclack.Checklist for Contributors
Testing
The changes in this PR are covered in the following automated tests:
Manual testing
This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!
Documentation
MIGRATION.MD
Checklist for Maintainers
When this PR is ready for testing, make sure to add
ci:normal,ci:mergedorci:dailyGH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found incode/lib/cli-storybook/src/sandbox-templates.tsMake sure this PR contains one of the labels below:
Available labels
bug: Internal changes that fixes incorrect behavior.maintenance: User-facing maintenance tasks.dependencies: Upgrading (sometimes downgrading) dependencies.build: Internal-facing build tooling & test updates. Will not show up in release changelog.cleanup: Minor cleanup style change. Will not show up in release changelog.documentation: Documentation only changes. Will not show up in release changelog.feature request: Introducing a new feature.BREAKING CHANGE: Changes that break compatibility in some way with current major version.other: Changes that don't fit in the above categories.🦋 Canary release
This pull request has been released as version
0.0.0-pr-31557-sha-a944b962. Try it out in a new sandbox by runningnpx [email protected] sandboxor in an existing project withnpx [email protected] upgrade.More information
0.0.0-pr-31557-sha-a944b962valentin/monorepo-enhancementsa944b9621749567133)To request a new release of this pull request, mention the
@storybookjs/coreteam.core team members can create a new canary release here or locally with
gh workflow run --repo storybookjs/storybook canary-release-pr.yml --field pr=31557Greptile Summary
Comprehensive overhaul of Storybook's upgrade process to improve monorepo support and CLI experience, focusing on centralized logging and user interaction improvements.
@storybook/node-loggerpackage--write-logsflag