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 7 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
26 changes: 26 additions & 0 deletions code/lib/cli/src/autoblock/block-react-script-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createBlocker } from './types';
import { dedent } from 'ts-dedent';
import { gte } from 'semver';

export const blocker = createBlocker({
id: 'storiesMdxUsage',
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
async check({ packageManager }) {
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
const version = await packageManager.getVersion('react-scripts');
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
if (version && gte(version, '5.0.0')) {
return false;
}
return { version };
},
message(options, data) {
return `Found react-script version: ${data.version}, please upgrade to latest.`;
},
log() {
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.
`;
},
});
31 changes: 31 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,31 @@
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) {
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
return { files };
},
message(options, data) {
return `Found ${data.files.length} stories.mdx files, these must be migrated.`;
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
},
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 7.0.0.
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
Please see the migration guide for more information:
https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#story-store-v7
ndelangen marked this conversation as resolved.
Show resolved Hide resolved

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');
});
82 changes: 82 additions & 0 deletions code/lib/cli/src/autoblock/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { AutoblockOptions, Blocker } from './types';
import { logger } from '@storybook/node-logger';
import chalk from 'chalk';
import boxen from 'boxen';
import { writeFile } from 'node:fs/promises';

const excludesFalse = <T>(x: T | false): x is T => x !== false;

const blockers: () => BlockerModule<any>[] = () => [
// add/remove blockers here
import('./block-storystorev6'),
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved
];

type BlockerModule<T> = Promise<{ blocker: Blocker<T> }>;

export const autoblock = async (
options: AutoblockOptions,
list: BlockerModule<any>[] = blockers()
) => {
if (list.length === 0) {
return null;
}

logger.info('Checking for upgrade blockers...');

const out = await Promise.all(
list.map(async (i) => {
const { blocker } = await i;
const result = await blocker.check(options);
if (result) {
return {
id: blocker.id,
value: true,
message: blocker.message(options, result),
log: blocker.log(options, result),
};
} else {
return false;
}
})
);

const faults = out.filter(excludesFalse);

if (faults.length > 0) {
const LOG_FILE_NAME = 'migration-storybook.log';
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved

const messages = {
welcome: `Blocking your upgrade because of the following issues:`,
reminder: chalk.yellow('Fix the above issues and try running the upgrade command again.'),
logfile: chalk.yellow(`You can find more details in ./${LOG_FILE_NAME}.`),
};
const borderColor = '#FC521F';

logger.plain('Oh no..');
logger.plain(
boxen(
[messages.welcome]
.concat(faults.map((i) => i.message))
.concat([messages.reminder])
.concat([messages.logfile])
.join('\n\n'),
{ borderStyle: 'round', padding: 1, borderColor }
)
);

await writeFile(
LOG_FILE_NAME,
faults.map((i) => '(' + i.id + '):\n' + i.log).join('\n\n----\n\n'),
{
encoding: 'utf-8',
}
);

return faults[0].id;
}

logger.plain('No blockers found.');
logger.line();

return null;
};
42 changes: 42 additions & 0 deletions code/lib/cli/src/autoblock/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { JsPackageManager, PackageJson } from '@storybook/core-common';
import type { StorybookConfig } from '@storybook/types';

export interface AutoblockOptions {
packageManager: JsPackageManager;
packageJson: PackageJson;
mainConfig: StorybookConfig;
mainConfigPath: string;
configDir: string;
}

export interface Blocker<T> {
/**
* A unique string to identify the blocker with.
*/
id: string;
/**
* Check if the blocker should block.
*
* @param context
* @returns A truthy value to activate the block, return false to proceed.
*/
check: (options: AutoblockOptions) => Promise<T | false>;
/**
* Format a message to be printed to the log-file.
* @param context
* @param data returned from the check method.
* @returns The string to print to the terminal.
*/
message: (options: AutoblockOptions, data: T) => string;
/**
* Format a message to be printed to the log-file.
* @param context
* @param data returned from the check method.
* @returns The string to print to the log-file.
*/
log: (options: AutoblockOptions, data: T) => string;
}

export function createBlocker<T>(block: Blocker<T>) {
return block;
}
Loading
Loading