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

CLI: Refactor to add autoblockers #25934

Merged
merged 31 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
923deb7
refactor cli to add autoblockers
ndelangen Feb 6, 2024
aeac067
add tests & fix some review comments
ndelangen Feb 7, 2024
1c3aa9b
fix migrate command that calls the changed runFixes fn
ndelangen Feb 7, 2024
da51fee
cleanup
ndelangen Feb 7, 2024
8af5a52
cleanup
ndelangen Feb 7, 2024
d4ca826
remove the types from jsdoc
ndelangen Feb 7, 2024
ab58f40
the best i can make it.. #bikeshedding
ndelangen Feb 7, 2024
668c3a8
expand scope of autoblock for react-script to also block vue2
ndelangen Feb 8, 2024
2bfea5f
add a step in between commander & calling the automigrate function so…
ndelangen Feb 8, 2024
332b8da
fix incorrect logic on detecting stories.mdx
ndelangen Feb 8, 2024
c20c3d8
make it say 8.0.0
ndelangen Feb 8, 2024
9ca5d50
check for the INSTALLED version, not the latest
ndelangen Feb 8, 2024
b12052c
add a automigration for a old vite version and another one for ensur…
ndelangen Feb 8, 2024
30bed62
add migration note, and early return
ndelangen Feb 8, 2024
274049b
Merge branch 'next' into norbert/upgrade-auto-blockers
ndelangen Feb 8, 2024
cf4bdb5
Merge branch 'next' into norbert/upgrade-auto-blockers
ndelangen Feb 8, 2024
3b20982
use getFrameworkPackageName
ndelangen Feb 9, 2024
a57d58b
Merge branch 'next' into norbert/upgrade-auto-blockers
ndelangen Feb 9, 2024
b8e1a75
performed some manual testing and found some issues
ndelangen Feb 9, 2024
fb144f2
fix review comment
ndelangen Feb 12, 2024
bd369e5
Merge branch 'next' into norbert/upgrade-auto-blockers
ndelangen Feb 12, 2024
68e6cb0
add more autoblockers for dependencies & add a --force flag to skip a…
ndelangen Feb 12, 2024
369704b
move nodeversion check to autoblock
ndelangen Feb 12, 2024
0003c40
remove from list
ndelangen Feb 12, 2024
6344a4a
Apply suggestions from code review
ndelangen Feb 12, 2024
a4d4087
apply https://github.com/storybookjs/storybook/pull/25934#discussion_…
ndelangen Feb 12, 2024
243a60d
add back the removed renderer mapping
ndelangen Feb 13, 2024
d79043f
Update code/lib/cli/src/autoblock/block-stories-mdx.ts
ndelangen Feb 13, 2024
2aec740
fixes
ndelangen Feb 13, 2024
6164216
Merge branch 'norbert/upgrade-auto-blockers' of https://github.com/st…
ndelangen Feb 13, 2024
016ac00
Merge branch 'next' into norbert/upgrade-auto-blockers
ndelangen Feb 13, 2024
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
64 changes: 63 additions & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
- [Removal of `storiesOf`-API](#removal-of-storiesof-api)
- [Removed deprecated shim packages](#removed-deprecated-shim-packages)
- [Framework-specific Vite plugins have to be explicitly added](#framework-specific-vite-plugins-have-to-be-explicitly-added)
- [For React:](#for-react)
- [For Vue:](#for-vue)
- [For Svelte (without Sveltekit):](#for-svelte-without-sveltekit)
- [For Preact:](#for-preact)
- [For Solid:](#for-solid)
- [For Qwik:](#for-qwik)
- [TurboSnap Vite plugin is no longer needed](#turbosnap-vite-plugin-is-no-longer-needed)
- [Implicit actions can not be used during rendering (for example in the play function)](#implicit-actions-can-not-be-used-during-rendering-for-example-in-the-play-function)
- [MDX related changes](#mdx-related-changes)
Expand Down Expand Up @@ -469,16 +475,72 @@ This section explains the rationale, and the required changed you might have to
In Storybook 7, we would automatically add frameworks-specific Vite plugins, e.g. `@vitejs/plugin-react` if not installed.
In Storybook 8 those plugins have to be added explicitly in the user's `vite.config.ts`:

#### For React:

```ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});
```

#### For Vue:

```ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
plugins: [vue()],
});
```

#### For Svelte (without Sveltekit):

```ts
import { defineConfig } from "vite";
import svelte from "@sveltejs/vite-plugin-svelte";

export default defineConfig({
plugins: [svelte()],
});
```

#### For Preact:

```ts
import { defineConfig } from "vite";
import preact from "@preact/preset-vite";

export default defineConfig({
plugins: [preact()],
});
```

#### For Solid:

```ts
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";

export default defineConfig({
plugins: [solid()],
});
```

#### For Qwik:

```ts
import { defineConfig } from "vite";
import qwik from "vite-plugin-qwik";

export default defineConfig({
plugins: [qwik()],
});
```

### TurboSnap Vite plugin is no longer needed

At least in build mode, `builder-vite` now supports the `--webpack-stats-json` flag and will output `preview-stats.json`.
Expand Down
92 changes: 92 additions & 0 deletions code/lib/cli/src/autoblock/block-dependencies-versions.ts
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.
`;
}
},
});
25 changes: 25 additions & 0 deletions code/lib/cli/src/autoblock/block-node-version.ts
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({
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
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
`;
},
});
33 changes: 33 additions & 0 deletions code/lib/cli/src/autoblock/block-stories-mdx.ts
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() });
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved
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
`;
},
});
40 changes: 40 additions & 0 deletions code/lib/cli/src/autoblock/block-storystorev6.ts
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
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
},
};

You need to remove the storyStoreV7 property.
`;
},
});
109 changes: 109 additions & 0 deletions code/lib/cli/src/autoblock/index.test.ts
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');
});
Loading
Loading