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

Vitest: Implement add command for vitest addon #28920

Merged
merged 30 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
46a838f
Implement add command for vitest addon
kasperpeulen Aug 19, 2024
3cee65e
Better texts
kasperpeulen Aug 19, 2024
1a4a5e7
Improve glob
kasperpeulen Aug 19, 2024
f4db6c0
Add more log lines
kasperpeulen Aug 19, 2024
c98d4c2
Support adding workspace files if vitest.config.ts exists
kasperpeulen Aug 20, 2024
480f404
Update unit tests
kasperpeulen Aug 20, 2024
e9070eb
Update code/addons/vitest/src/postinstall.ts
kasperpeulen Aug 20, 2024
675fdb8
Get correct path
kasperpeulen Aug 20, 2024
dbd4844
Add comment about isolation
kasperpeulen Aug 21, 2024
419b81c
Rollback
kasperpeulen Aug 21, 2024
71ad827
Fix
kasperpeulen Aug 21, 2024
718b879
Improve messages
kasperpeulen Aug 21, 2024
a26e267
Validate framework
kasperpeulen Aug 21, 2024
ce006bf
Update code/addons/vitest/src/postinstall.ts
kasperpeulen Aug 22, 2024
76734a0
Update code/addons/vitest/src/postinstall.ts
kasperpeulen Aug 22, 2024
78d544e
Update code/addons/vitest/src/postinstall.ts
kasperpeulen Aug 22, 2024
b45a9b4
Update code/addons/vitest/src/postinstall.ts
kasperpeulen Aug 22, 2024
d8a5c72
Update code/addons/vitest/src/postinstall.ts
kasperpeulen Aug 22, 2024
cc42862
Correct message
kasperpeulen Aug 22, 2024
3d68903
Merge branch 'next' into kasper/vitest-add
kasperpeulen Aug 22, 2024
b80775b
Change isolate message
kasperpeulen Aug 22, 2024
ce2afff
Merge remote-tracking branch 'origin/kasper/vitest-add' into kasper/v…
kasperpeulen Aug 22, 2024
4d80b1f
Fix code style
kasperpeulen Aug 22, 2024
0a35a32
Fix more codestyle
kasperpeulen Aug 22, 2024
f1031cb
Merge branch 'next' into kasper/vitest-add
kasperpeulen Aug 22, 2024
61338bf
Fix default export
kasperpeulen Aug 23, 2024
cd8f1f2
Fix
kasperpeulen Aug 23, 2024
c17df9f
Make renderer nullable
kasperpeulen Aug 23, 2024
9630148
Fix type issues
kasperpeulen Aug 23, 2024
dc9432a
Throw errors in renderToCanvas in portable stories
kasperpeulen Aug 23, 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
3 changes: 3 additions & 0 deletions code/addons/vitest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
},
"devDependencies": {
"@vitest/browser": "^2.0.0",
"find-up": "^7.0.0",
"tinyrainbow": "^1.2.0",
"ts-dedent": "^2.2.0",
"vitest": "^2.0.0"
},
"peerDependencies": {
Expand Down
251 changes: 249 additions & 2 deletions code/addons/vitest/src/postinstall.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,250 @@
export default async function postinstall(context: any) {
console.log('[addon-vitest] postinstall with', context);
import { existsSync } from 'node:fs';
import * as fs from 'node:fs/promises';
import { writeFile } from 'node:fs/promises';
import { dirname, join, relative, resolve } from 'node:path';
import * as path from 'node:path';

import {
JsPackageManagerFactory,
extractProperFrameworkName,
loadAllPresets,
loadMainConfig,
validateFrameworkName,
} from 'storybook/internal/common';
import { logger } from 'storybook/internal/node-logger';

import { findUp } from 'find-up';
import c from 'tinyrainbow';
import dedent from 'ts-dedent';

import { type PostinstallOptions } from '../../../lib/cli-storybook/src/add';

const extensions = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'];

export default async function postInstall(options: PostinstallOptions) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to do any version checks here? Given that these packages will only fully work in Storybook 8.3? Specially sveltekit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this postinstall will only be available in 8.3 right?

const packageManager = JsPackageManagerFactory.getPackageManager({
force: options.packageManager,
});

const info = await getFrameworkInfo(options);
kasperpeulen marked this conversation as resolved.
Show resolved Hide resolved

if (
info.frameworkPackageName !== '@storybook/nextjs' &&
info.builderPackageName !== '@storybook/builder-vite'
) {
logger.info(
'The Vitest addon can only be used with a Vite-based Storybook framework or Next.js.'
);
return;
}

const annotationsImport = [
'@storybook/nextjs',
'@storybook/experimental-nextjs-vite',
'@storybook/sveltekit',
].includes(info.frameworkPackageName)
? info.frameworkPackageName
: ['@storybook/react', '@storybook/svelte', '@storybook/vue3'].includes(
kasperpeulen marked this conversation as resolved.
Show resolved Hide resolved
info.rendererPackageName
)
? info.rendererPackageName
: null;

if (!annotationsImport) {
logger.info('The Vitest addon cannot yet be used with: ' + info.frameworkPackageName);
return;
}

const vitestInfo = getVitestPluginInfo(info.frameworkPackageName);

const packages = ['vitest@latest', '@vitest/browser@latest', 'playwright@latest'];
yannbf marked this conversation as resolved.
Show resolved Hide resolved
yannbf marked this conversation as resolved.
Show resolved Hide resolved

logger.info(
dedent`
We detected that you're using Next.js.
We will configure the vite-plugin-storybook-nextjs plugin to allow you to run tests in Vitest.
`
);

if (info.frameworkPackageName === '@storybook/nextjs') {
packages.push('vite-plugin-storybook-nextjs@latest');
}
logger.info(c.bold('Installing packages...'));
logger.info(packages.join(', '));
await packageManager.addDependencies({ installAsDevDependencies: true }, packages);
kasperpeulen marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if this or any of the other operations fail? Should we wrap in a try/catch to provide some narrative about the failure? Perhaps we may want to add telemetry to track these?


logger.info(c.bold('Executing npx playwright install chromium --with-deps ...'));
await packageManager.executeCommand({
command: 'npx',
args: ['playwright', 'install', 'chromium', '--with-deps'],
});

logger.info(c.bold('Writing .storybook/vitest.setup.ts file...'));

const previewExists = extensions
.map((ext) => path.resolve(path.join(options.configDir, `preview${ext}`)))
.some((config) => existsSync(config));

await writeFile(
resolve(options.configDir, 'vitest.setup.ts'),
kasperpeulen marked this conversation as resolved.
Show resolved Hide resolved
dedent`
import { beforeAll } from 'vitest'
import { setProjectAnnotations } from '${annotationsImport}'
${previewExists ? `import * as projectAnnotations from './preview'` : ''}

const project = setProjectAnnotations(${previewExists ? 'projectAnnotations' : '[]'})

beforeAll(project.beforeAll)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure the snippet matches what we have in the docs for portable stories, or change the docs for portable stories to match the snippet

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean?

import { beforeAll } from 'vitest';
import { render as testingLibraryRender } from '@testing-library/svelte';
import { setProjectAnnotations } from '@storybook/svelte';
// 👇 Import the exported annotations, if any, from the addons you're using; otherwise remove this
import * as addonAnnotations from 'my-addon/preview';
import * as previewAnnotations from './.storybook/preview';
 
const annotations = setProjectAnnotations([
  previewAnnotations,
  addonAnnotations,
  // You MUST provide this option to use portable stories with vitest
  { testingLibraryRender },
]);
 
// Run Storybook's beforeAll hook
beforeAll(annotations.beforeAll);

I can remove the testingLibraryRender indeed. Do you mean that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kylegach Let's discuss this for another PR

`
);

const configFiles = extensions.map((ext) => 'vitest.config' + ext);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should do this mapping where extensions are defined as well?


const rootConfig = await findUp(configFiles, {
cwd: process.cwd(),
kasperpeulen marked this conversation as resolved.
Show resolved Hide resolved
});

if (rootConfig) {
const extname = rootConfig ? path.extname(rootConfig) : '.ts';
const browserWorkspaceFile = resolve(dirname(rootConfig), `vitest.workspace${extname}`);
if (existsSync(browserWorkspaceFile)) {
logger.info(
dedent`
We can not automatically setup the plugin when you use Vitest with workspaces.
Please refer to the documentation to complete the setup manually:
https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual
`
);
} else {
logger.info(c.bold('Writing vitest.workspace.ts file...'));
await writeFile(
browserWorkspaceFile,
dedent`
import { defineWorkspace } from 'vitest/config';
import { storybookTest } from '@storybook/experimental-addon-vitest/plugin';
${vitestInfo.frameworkPluginImport ? vitestInfo.frameworkPluginImport + '\n' : ''}
export default defineWorkspace([
'${relative(dirname(browserWorkspaceFile), rootConfig)}',
{
plugins: [
storybookTest(),${vitestInfo.frameworkPluginCall ? '\n' + vitestInfo.frameworkPluginCall : ''}
],
test: {
include: ['**/*.stories.?(m)[jt]s?(x)'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: The glob pattern for stories files might be too permissive

browser: {
enabled: true,
name: 'chromium',
provider: 'playwright',
headless: true,
},
// Disabling isolation is faster and is similar to how tests are isolated in storybook itself.
// Consider removing this if you are seeing problems with your tests.
isolate: false,
kasperpeulen marked this conversation as resolved.
Show resolved Hide resolved
setupFiles: ['./.storybook/vitest.setup.ts'],
},
},
]);
`
);
}
} else {
logger.info(c.bold('Writing vitest.config.ts file...'));
await writeFile(
resolve('vitest.config.ts'),
dedent`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these code snippets should probably be in a separate file somewhere, rather than inline.

import { defineConfig } from "vitest/config";
import { storybookTest } from "@storybook/experimental-addon-vitest/plugin";
${vitestInfo.frameworkPluginImport ? vitestInfo.frameworkPluginImport + '\n' : ''}
export default defineConfig({
plugins: [
storybookTest(),${vitestInfo.frameworkPluginCall ? '\n' + vitestInfo.frameworkPluginCall : ''}
],
test: {
include: ['**/*.stories.?(m)[jt]s?(x)'],
browser: {
enabled: true,
name: 'chromium',
provider: 'playwright',
headless: true,
},
// Disabling isolation is faster and is similar to how tests are isolated in storybook itself.
// Consider removing this, if you have flaky tests.
isolate: false,
setupFiles: ['./.storybook/vitest.setup.ts'],
},
});
`
);
}

logger.info(
dedent`
The Vitest addon is now configured and you're ready to run your tests!
Check the documentation for more information about its features and options at:
https://storybook.js.org/docs/writing-tests/test-runner-with-vitest
`
);
}

const getVitestPluginInfo = (framework: string) => {
let frameworkPluginImport = '';
let frameworkPluginCall = '';

if (framework === '@storybook/nextjs') {
frameworkPluginImport = "import vitePluginNext from 'vite-plugin-storybook-nextjs'";
frameworkPluginCall = 'vitePluginNext()';
}

if (framework === '@storybook/sveltekit') {
frameworkPluginImport = "import { storybookSveltekitPlugin } from '@storybook/sveltekit/vite'";
frameworkPluginCall = 'storybookSveltekitPlugin()';
}

return { frameworkPluginImport, frameworkPluginCall };
};

async function getFrameworkInfo({ configDir, packageManager: pkgMgr }: PostinstallOptions) {
const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr });
const packageJson = await packageManager.retrievePackageJson();

const config = await loadMainConfig({ configDir, noCache: true });
const { framework } = config;

const frameworkName = typeof framework === 'string' ? framework : framework?.name;
validateFrameworkName(frameworkName);
const frameworkPackageName = extractProperFrameworkName(frameworkName);

console.log(configDir);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to be removed.

const presets = await loadAllPresets({
corePresets: [join(frameworkName, 'preset')],
overridePresets: [
require.resolve('@storybook/core/core-server/presets/common-override-preset'),
kasperpeulen marked this conversation as resolved.
Show resolved Hide resolved
],
configDir,
packageJson,
isCritical: true,
});

const core = await presets.apply('core', {});

const { builder, renderer } = core;

if (!builder || !renderer) {
throw new Error('Could not detect your Storybook framework.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please tell the user what to do in this situation, I'd assume they passed the wrong config dir?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They will get an error earlier when we validate the framework:
validateFrameworkName(frameworkName);

I think this can only happen if you have a mal-configured community framework. So I think it is very unlikely.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, in the dev command, we don't throw anything here. As typescript is okay with it.

}

const builderPackageJson = await fs.readFile(
`${typeof builder === 'string' ? builder : builder.name}/package.json`,
'utf8'
);
Comment on lines +237 to +240
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Wrap this in a try/catch to handle potential file read errors

Comment on lines +237 to +240
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Wrap this in a try/catch to handle potential file read errors

const builderPackageName = JSON.parse(builderPackageJson).name;

const rendererPackageJson = await fs.readFile(`${renderer}/package.json`, 'utf8');
const rendererPackageName = JSON.parse(rendererPackageJson).name;

return {
frameworkPackageName,
builderPackageName,
rendererPackageName,
};
}
1 change: 1 addition & 0 deletions code/lib/cli-storybook/src/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ describe('add (extra)', () => {

expect(MockedPostInstall.postinstallAddon).toHaveBeenCalledWith('@storybook/addon-docs', {
packageManager: 'npm',
configDir: '.storybook',
});
});
});
3 changes: 2 additions & 1 deletion code/lib/cli-storybook/src/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { postinstallAddon } from './postinstallAddon';

export interface PostinstallOptions {
packageManager: PackageManagerName;
configDir: string;
}

/**
Expand Down Expand Up @@ -139,7 +140,7 @@ export async function add(
await writeConfig(main);

if (!skipPostinstall && isCoreAddon(addonName)) {
await postinstallAddon(addonName, { packageManager: packageManager.type });
await postinstallAddon(addonName, { packageManager: packageManager.type, configDir });
}
}
function isValidVersion(version: string) {
Expand Down
3 changes: 3 additions & 0 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6132,6 +6132,9 @@ __metadata:
dependencies:
"@storybook/csf": "npm:^0.1.11"
"@vitest/browser": "npm:^2.0.0"
find-up: "npm:^7.0.0"
tinyrainbow: "npm:^1.2.0"
ts-dedent: "npm:^2.2.0"
vitest: "npm:^2.0.0"
peerDependencies:
"@vitest/browser": ^2.0.0
Expand Down