-
-
Notifications
You must be signed in to change notification settings - Fork 9.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #25934 from storybookjs/norbert/upgrade-auto-blockers
CLI: Refactor to add autoblockers
- Loading branch information
Showing
21 changed files
with
887 additions
and
227 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { createBlocker } from './types'; | ||
import { dedent } from 'ts-dedent'; | ||
import { lt } from 'semver'; | ||
|
||
const minimalVersionsMap = { | ||
'@angular/core': '15.0.0', | ||
'react-scripts': '5.0.0', | ||
next: '13.5.0', | ||
preact: '10.0.0', | ||
svelte: '4.0.0', | ||
vue: '3.0.0', | ||
}; | ||
|
||
type Result = { | ||
installedVersion: string | undefined; | ||
packageName: keyof typeof minimalVersionsMap; | ||
minimumVersion: string; | ||
}; | ||
const typedKeys = <TKey extends string>(obj: Record<TKey, any>) => Object.keys(obj) as TKey[]; | ||
|
||
export const blocker = createBlocker({ | ||
id: 'dependenciesVersions', | ||
async check({ packageManager }) { | ||
const list = await Promise.all( | ||
typedKeys(minimalVersionsMap).map(async (packageName) => ({ | ||
packageName, | ||
installedVersion: await packageManager.getPackageVersion(packageName), | ||
minimumVersion: minimalVersionsMap[packageName], | ||
})) | ||
); | ||
|
||
return list.reduce<false | Result>((acc, { installedVersion, minimumVersion, packageName }) => { | ||
if (acc) { | ||
return acc; | ||
} | ||
if (packageName && installedVersion && lt(installedVersion, minimumVersion)) { | ||
return { | ||
installedVersion, | ||
packageName, | ||
minimumVersion, | ||
}; | ||
} | ||
return acc; | ||
}, false); | ||
}, | ||
message(options, data) { | ||
return `Found ${data.packageName} version: ${data.installedVersion}, please upgrade to ${data.minimumVersion} or higher.`; | ||
}, | ||
log(options, data) { | ||
switch (data.packageName) { | ||
case 'react-scripts': | ||
return dedent` | ||
Support react-script < 5.0.0 has been removed. | ||
Please see the migration guide for more information: | ||
https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#create-react-app-dropped-cra4-support | ||
Upgrade to the latest version of react-scripts. | ||
`; | ||
case 'vue': | ||
return dedent` | ||
Support for Vue 2 has been removed. | ||
Please see the migration guide for more information: | ||
https://v3-migration.vuejs.org/ | ||
Please upgrade to the latest version of Vue. | ||
`; | ||
case '@angular/core': | ||
return dedent` | ||
Support for Angular < 15 has been removed. | ||
Please see the migration guide for more information: | ||
https://angular.io/guide/update-to-version-15 | ||
Please upgrade to the latest version of Angular. | ||
`; | ||
case 'next': | ||
return dedent` | ||
Support for Next.js < 13.5 has been removed. | ||
Please see the migration guide for more information: | ||
https://nextjs.org/docs/pages/building-your-application/upgrading/version-13 | ||
Please upgrade to the latest version of Next.js. | ||
`; | ||
default: | ||
return dedent` | ||
Support for ${data.packageName} version < ${data.minimumVersion} has been removed. | ||
Storybook 8 needs minimum version of ${data.minimumVersion}, but you had version ${data.installedVersion}. | ||
Please update this dependency. | ||
`; | ||
} | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { createBlocker } from './types'; | ||
import { dedent } from 'ts-dedent'; | ||
import { lt } from 'semver'; | ||
|
||
export const blocker = createBlocker({ | ||
id: 'minimumNode16', | ||
async check() { | ||
const nodeVersion = process.versions.node; | ||
if (nodeVersion && lt(nodeVersion, '18.0.0')) { | ||
return { nodeVersion }; | ||
} | ||
return false; | ||
}, | ||
message(options, data) { | ||
return `Please use Node.js v18 or higher.`; | ||
}, | ||
log(options, data) { | ||
return dedent` | ||
We've detected you're using Node.js v${data.nodeVersion}. | ||
Storybook needs Node.js 18 or higher. | ||
https://nodejs.org/en/download | ||
`; | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { createBlocker } from './types'; | ||
import { dedent } from 'ts-dedent'; | ||
import { glob } from 'glob'; | ||
|
||
export const blocker = createBlocker({ | ||
id: 'storiesMdxUsage', | ||
async check() { | ||
const files = await glob('**/*.stories.mdx', { cwd: process.cwd() }); | ||
if (files.length === 0) { | ||
return false; | ||
} | ||
return { files }; | ||
}, | ||
message(options, data) { | ||
return `Found ${data.files.length} stories.mdx ${ | ||
data.files.length === 1 ? 'file' : 'files' | ||
}, these must be migrated.`; | ||
}, | ||
log() { | ||
return dedent` | ||
Support for *.stories.mdx files has been removed. | ||
Please see the migration guide for more information: | ||
https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#dropping-support-for-storiesmdx-csf-in-mdx-format-and-mdx1-support | ||
Storybook will also require you to use MDX 3.0.0 or later. | ||
Check the migration guide for more information: | ||
https://mdxjs.com/blog/v3/ | ||
Manually run the migration script to convert your stories.mdx files to CSF format documented here: | ||
https://storybook.js.org/docs/migration-guide#storiesmdx-to-mdxcsf | ||
`; | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { relative } from 'path'; | ||
import { createBlocker } from './types'; | ||
import { dedent } from 'ts-dedent'; | ||
import type { StorybookConfigRaw } from '@storybook/types'; | ||
|
||
export const blocker = createBlocker({ | ||
id: 'storyStoreV7removal', | ||
async check({ mainConfig }) { | ||
const features = (mainConfig as any as StorybookConfigRaw)?.features; | ||
if (features === undefined) { | ||
return false; | ||
} | ||
if (Object.hasOwn(features, 'storyStoreV7')) { | ||
return true; | ||
} | ||
return false; | ||
}, | ||
message(options, data) { | ||
const mainConfigPath = relative(process.cwd(), options.mainConfigPath); | ||
return `StoryStoreV7 feature must be removed from ${mainConfigPath}`; | ||
}, | ||
log() { | ||
return dedent` | ||
StoryStoreV7 feature must be removed from your Storybook configuration. | ||
This feature was removed in Storybook 8.0.0. | ||
Please see the migration guide for more information: | ||
https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#storystorev6-and-storiesof-is-deprecated | ||
In your Storybook configuration file you have this code: | ||
export default = { | ||
features: { | ||
storyStoreV7: false, <--- remove this line | ||
}, | ||
}; | ||
You need to remove the storyStoreV7 property. | ||
`; | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { expect, test, vi } from 'vitest'; | ||
import { autoblock } from './index'; | ||
import { JsPackageManagerFactory } from '@storybook/core-common'; | ||
import { createBlocker } from './types'; | ||
import { writeFile as writeFileRaw } from 'node:fs/promises'; | ||
import { logger } from '@storybook/node-logger'; | ||
|
||
vi.mock('node:fs/promises', () => ({ | ||
writeFile: vi.fn(), | ||
})); | ||
vi.mock('boxen', () => ({ | ||
default: vi.fn((x) => x), | ||
})); | ||
vi.mock('@storybook/node-logger', () => ({ | ||
logger: { | ||
info: vi.fn(), | ||
line: vi.fn(), | ||
plain: vi.fn(), | ||
}, | ||
})); | ||
|
||
const writeFile = vi.mocked(writeFileRaw); | ||
|
||
const blockers = { | ||
alwaysPass: createBlocker({ | ||
id: 'alwaysPass', | ||
check: async () => false, | ||
message: () => 'Always pass', | ||
log: () => 'Always pass', | ||
}), | ||
alwaysFail: createBlocker({ | ||
id: 'alwaysFail', | ||
check: async () => ({ bad: true }), | ||
message: () => 'Always fail', | ||
log: () => '...', | ||
}), | ||
alwaysFail2: createBlocker({ | ||
id: 'alwaysFail2', | ||
check: async () => ({ disaster: true }), | ||
message: () => 'Always fail 2', | ||
log: () => '...', | ||
}), | ||
} as const; | ||
|
||
const baseOptions: Parameters<typeof autoblock>[0] = { | ||
configDir: '.storybook', | ||
mainConfig: { | ||
stories: [], | ||
}, | ||
mainConfigPath: '.storybook/main.ts', | ||
packageJson: { | ||
dependencies: {}, | ||
devDependencies: {}, | ||
}, | ||
packageManager: JsPackageManagerFactory.getPackageManager({ force: 'npm' }), | ||
}; | ||
|
||
test('with empty list', async () => { | ||
const result = await autoblock({ ...baseOptions }, []); | ||
expect(result).toBe(null); | ||
expect(logger.plain).not.toHaveBeenCalledWith(expect.stringContaining('No blockers found')); | ||
}); | ||
|
||
test('all passing', async () => { | ||
const result = await autoblock({ ...baseOptions }, [ | ||
Promise.resolve({ blocker: blockers.alwaysPass }), | ||
Promise.resolve({ blocker: blockers.alwaysPass }), | ||
]); | ||
expect(result).toBe(null); | ||
expect(logger.plain).toHaveBeenCalledWith(expect.stringContaining('No blockers found')); | ||
}); | ||
|
||
test('1 fail', async () => { | ||
const result = await autoblock({ ...baseOptions }, [ | ||
Promise.resolve({ blocker: blockers.alwaysPass }), | ||
Promise.resolve({ blocker: blockers.alwaysFail }), | ||
]); | ||
expect(writeFile).toHaveBeenCalledWith( | ||
expect.any(String), | ||
expect.stringContaining('alwaysFail'), | ||
expect.any(Object) | ||
); | ||
expect(result).toBe('alwaysFail'); | ||
expect(logger.plain).toHaveBeenCalledWith(expect.stringContaining('Oh no..')); | ||
|
||
expect(writeFile.mock.calls[0][1]).toMatchInlineSnapshot(` | ||
"(alwaysFail): | ||
..." | ||
`); | ||
}); | ||
|
||
test('multiple fails', async () => { | ||
const result = await autoblock({ ...baseOptions }, [ | ||
Promise.resolve({ blocker: blockers.alwaysPass }), | ||
Promise.resolve({ blocker: blockers.alwaysFail }), | ||
Promise.resolve({ blocker: blockers.alwaysFail2 }), | ||
]); | ||
expect(writeFile.mock.calls[0][1]).toMatchInlineSnapshot(` | ||
"(alwaysFail): | ||
... | ||
---- | ||
(alwaysFail2): | ||
..." | ||
`); | ||
|
||
expect(result).toBe('alwaysFail'); | ||
}); |
Oops, something went wrong.