Skip to content
Merged
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 10.2.6

- Addon-Vitest: Skip postinstall setup when configured - [#33712](https://github.com/storybookjs/storybook/pull/33712), thanks @valentinpalkovic!
- Addon-Vitest: Support vite/vitest config with deferred export - [#33755](https://github.com/storybookjs/storybook/pull/33755), thanks @valentinpalkovic!
- CLI: Support addon-vitest setup when --skip-install is passed - [#33718](https://github.com/storybookjs/storybook/pull/33718), thanks @valentinpalkovic!
- Manager: Update logic to use base path instead of full pathname - [#33686](https://github.com/storybookjs/storybook/pull/33686), thanks @JSMike!

## 10.2.5

- Angular: fix --loglevel options in docs and descriptions - [#33726](https://github.com/storybookjs/storybook/pull/33726), thanks @theRuslan!
Expand Down
10 changes: 7 additions & 3 deletions code/addons/a11y/src/postinstall.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { JsPackageManagerFactory } from 'storybook/internal/common';
import { JsPackageManagerFactory, versions } from 'storybook/internal/common';

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

export default async function postinstall(options: PostinstallOptions) {
const args = ['storybook', 'automigrate', 'addon-a11y-addon-test'];
const args = [
options.skipInstall ? `storybook@${versions.storybook}` : `storybook`,
'automigrate',
'addon-a11y-addon-test',
];

args.push('--loglevel', 'silent');
args.push('--skip-doctor');
Expand All @@ -25,5 +29,5 @@ export default async function postinstall(options: PostinstallOptions) {
configDir: options.configDir,
});

await jsPackageManager.runPackageCommand({ args });
await jsPackageManager.runPackageCommand({ args, useRemotePkg: !!options.skipInstall });
}
49 changes: 49 additions & 0 deletions code/addons/vitest/src/postinstall.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, it } from 'vitest';

import { isConfigAlreadySetup } from './postinstall';

describe('postinstall helpers', () => {
it('detects a fully configured Vitest config with addon plugin', () => {
const config = `
import { defineConfig } from 'vitest/config';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';

export default defineConfig({
test: {
projects: [
{
extends: true,
plugins: [storybookTest({ configDir: '.storybook' })],
test: {
setupFiles: ['./.storybook/vitest.setup.ts'],
},
},
],
},
});
`;

expect(isConfigAlreadySetup('/project/vitest.config.ts', config)).toBe(true);
});

it('returns false when storybookTest plugin is not used', () => {
const config = `
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
projects: [
{
extends: true,
test: {
setupFiles: ['./.storybook/vitest.setup.ts'],
},
},
],
},
});
`;

expect(isConfigAlreadySetup('/project/vitest.config.ts', config)).toBe(false);
});
});
115 changes: 91 additions & 24 deletions code/addons/vitest/src/postinstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ import { existsSync } from 'node:fs';
import * as fs from 'node:fs/promises';
import { writeFile } from 'node:fs/promises';

import { babelParse, generate } from 'storybook/internal/babel';
import { babelParse, generate, traverse } from 'storybook/internal/babel';
import { AddonVitestService } from 'storybook/internal/cli';
import {
JsPackageManagerFactory,
formatFileContent,
getProjectRoot,
getStorybookInfo,
versions,
} from 'storybook/internal/common';
import { CLI_COLORS } from 'storybook/internal/node-logger';
import type { StorybookError } from 'storybook/internal/server-errors';
import {
AddonVitestPostinstallConfigUpdateError,
AddonVitestPostinstallError,
AddonVitestPostinstallExistingSetupFileError,
AddonVitestPostinstallFailedAddonA11yError,
AddonVitestPostinstallPrerequisiteCheckError,
AddonVitestPostinstallWorkspaceUpdateError,
Expand All @@ -33,6 +33,7 @@ import { loadTemplate, updateConfigFile, updateWorkspaceFile } from './updateVit

const ADDON_NAME = '@storybook/addon-vitest' as const;
const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.cts', '.mts', '.cjs', '.mjs'];
const STORYBOOK_TEST_PLUGIN_SOURCE = `${ADDON_NAME}/vitest-plugin`;

const addonA11yName = '@storybook/addon-a11y';

Expand Down Expand Up @@ -161,6 +162,7 @@ export default async function postInstall(options: PostinstallOptions) {
if (!options.skipInstall) {
await addonVitestService.installPlaywright({
yes: options.yes,
useRemotePkg: !!options.skipInstall,
});
} else {
logger.warn(dedent`
Expand All @@ -174,17 +176,13 @@ export default async function postInstall(options: PostinstallOptions) {
allDeps.typescript || findFile('tsconfig', [...EXTENSIONS, '.json']) ? 'ts' : 'js';

const vitestSetupFile = resolve(options.configDir, `vitest.setup.${fileExtension}`);
const existingSetupFile =
EXTENSIONS.map((ext) => resolve(options.configDir, `vitest.setup${ext}`)).find(existsSync) ||
null;

if (existsSync(vitestSetupFile)) {
const errorMessage = dedent`
Found an existing Vitest setup file:
${vitestSetupFile}
Please refer to the documentation to complete the setup manually:
https://storybook.js.org/docs/next/${DOCUMENTATION_LINK}#manual-setup-advanced
`;
logger.line();
logger.error(`${errorMessage}\n`);
errors.push(new AddonVitestPostinstallExistingSetupFileError({ filePath: vitestSetupFile }));
if (existingSetupFile) {
logger.step(`Found existing Vitest setup file, reusing:`);
logger.log(`${existingSetupFile}\n`);
} else {
logger.step(`Creating a Vitest setup file for Storybook:`);
logger.log(`${vitestSetupFile}\n`);
Expand Down Expand Up @@ -233,26 +231,35 @@ export default async function postInstall(options: PostinstallOptions) {

const getTemplateName = () => {
if (isVitest4OrNewer) {
return 'vitest.config.4.template.ts';
return 'vitest.config.4.template';
} else if (isVitest3_2To4) {
return 'vitest.config.3.2.template.ts';
return 'vitest.config.3.2.template';
}
return 'vitest.config.template.ts';
return 'vitest.config.template';
};

// If there's an existing workspace file, we update that file to include the Storybook Addon Vitest plugin.
// We assume the existing workspaces include the Vite(st) config, so we won't add it.
if (vitestWorkspaceFile) {
const workspaceTemplate = await loadTemplate('vitest.workspace.template.ts', {
const workspaceFileContent = await fs.readFile(vitestWorkspaceFile, 'utf8');
const alreadyConfigured = isConfigAlreadySetup(vitestWorkspaceFile, workspaceFileContent);

if (alreadyConfigured) {
logger.step(
CLI_COLORS.success('Vitest for Storybook is already properly configured. Skipping setup.')
);
return;
}

const workspaceTemplate = await loadTemplate('vitest.workspace.template', {
EXTENDS_WORKSPACE: viteConfigFile
? relative(dirname(vitestWorkspaceFile), viteConfigFile)
: '',
CONFIG_DIR: options.configDir,
SETUP_FILE: relative(dirname(vitestWorkspaceFile), vitestSetupFile),
SETUP_FILE: relative(dirname(vitestWorkspaceFile), existingSetupFile ?? vitestSetupFile),
}).then((t) => t.replace(`\n 'ROOT_CONFIG',`, '').replace(/\s+extends: '',/, ''));
const workspaceFile = await fs.readFile(vitestWorkspaceFile, 'utf8');
const source = babelParse(workspaceTemplate);
const target = babelParse(workspaceFile);
const target = babelParse(workspaceFileContent);

const updated = updateWorkspaceFile(source, target);
if (updated) {
Expand Down Expand Up @@ -290,18 +297,24 @@ export default async function postInstall(options: PostinstallOptions) {

const templateName = getTemplateName();

if (templateName) {
const alreadyConfigured = isConfigAlreadySetup(rootConfig, configFile);

if (templateName && !alreadyConfigured) {
const configTemplate = await loadTemplate(templateName, {
CONFIG_DIR: options.configDir,
SETUP_FILE: relative(dirname(rootConfig), vitestSetupFile),
SETUP_FILE: relative(dirname(rootConfig), existingSetupFile ?? vitestSetupFile),
});

const source = babelParse(configTemplate);
target = babelParse(configFile);
updated = updateConfigFile(source, target);
}

if (target && updated) {
if (alreadyConfigured) {
logger.step(
CLI_COLORS.success('Vitest for Storybook is already properly configured. Skipping setup.')
);
} else if (target && updated) {
logger.step(`Updating your ${vitestConfigFile ? 'Vitest' : 'Vite'} config file:`);
logger.log(` ${rootConfig}`);

Expand Down Expand Up @@ -347,7 +360,7 @@ export default async function postInstall(options: PostinstallOptions) {
if (a11yAddon) {
try {
const command = [
'storybook',
options.skipInstall ? `storybook@${versions.storybook}` : `storybook`,
'automigrate',
'addon-a11y-addon-test',
'--loglevel',
Expand All @@ -370,7 +383,12 @@ export default async function postInstall(options: PostinstallOptions) {

await prompt.executeTask(
// TODO: Remove stdio: 'ignore' once we have a way to log the output of the command properly
() => packageManager.runPackageCommand({ args: command, stdio: 'ignore' }),
() =>
packageManager.runPackageCommand({
args: command,
stdio: 'ignore',
useRemotePkg: !!options.skipInstall,
}),
{
intro: 'Setting up a11y addon for @storybook/addon-vitest',
error: 'Failed to setup a11y addon for @storybook/addon-vitest',
Expand Down Expand Up @@ -412,3 +430,52 @@ export default async function postInstall(options: PostinstallOptions) {
throw new AddonVitestPostinstallError({ errors });
}
}

function isStorybookTestPluginSource(value: string) {
return value === STORYBOOK_TEST_PLUGIN_SOURCE;
}

export function isConfigAlreadySetup(_configPath: string, configContent: string) {
let ast: ReturnType<typeof babelParse>;
try {
ast = babelParse(configContent);
} catch (e) {
return false;
}

const pluginIdentifiers = new Set<string>();

traverse(ast, {
ImportDeclaration(path) {
const source = path.node.source.value;
if (typeof source === 'string' && isStorybookTestPluginSource(source)) {
path.node.specifiers.forEach((specifier) => {
if ('local' in specifier && specifier.local?.name) {
pluginIdentifiers.add(specifier.local.name);
}
});
}
},
});

let pluginReferenced = false;

traverse(ast, {
CallExpression(path) {
if (pluginReferenced) {
path.stop();
return;
}
const callee = path.node.callee;
if (
callee.type === 'Identifier' &&
(pluginIdentifiers.has(callee.name) || callee.name === 'storybookTest')
) {
pluginReferenced = true;
path.stop();
}
},
});

return pluginReferenced;
}
5 changes: 5 additions & 0 deletions code/addons/vitest/src/typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}

declare module '*?raw' {
const content: string;
export default content;
}
Loading
Loading