-
-
Notifications
You must be signed in to change notification settings - Fork 9.3k
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
Changes from 25 commits
46a838f
3cee65e
1a4a5e7
f4db6c0
c98d4c2
480f404
e9070eb
675fdb8
dbd4844
419b81c
71ad827
718b879
a26e267
ce006bf
76734a0
78d544e
b45a9b4
d8a5c72
cc42862
3d68903
b80775b
ce2afff
4d80b1f
0a35a32
f1031cb
61338bf
cd8f1f2
c17df9f
9630148
dc9432a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) { | ||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should do this mapping where |
||
|
||
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)'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They will get an error earlier when we validate the framework: I think this can only happen if you have a mal-configured community framework. So I think it is very unlikely. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, in the |
||
} | ||
|
||
const builderPackageJson = await fs.readFile( | ||
`${typeof builder === 'string' ? builder : builder.name}/package.json`, | ||
'utf8' | ||
); | ||
Comment on lines
+237
to
+240
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
}; | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?