Skip to content

Commit

Permalink
Merge pull request #25934 from storybookjs/norbert/upgrade-auto-blockers
Browse files Browse the repository at this point in the history
CLI: Refactor to add autoblockers
  • Loading branch information
valentinpalkovic committed Feb 13, 2024
2 parents 3ae39b6 + 016ac00 commit ece84b9
Show file tree
Hide file tree
Showing 21 changed files with 887 additions and 227 deletions.
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({
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() });
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
},
};
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

0 comments on commit ece84b9

Please sign in to comment.