Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/docs/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ const optimizeViteDeps = [
'@storybook/addon-docs',
'@storybook/addon-docs/blocks',
'@storybook/addon-docs > @mdx-js/react',
'@storybook/addon-docs > @storybook/react-dom-shim',
'react-dom/client',
'react/jsx-runtime',
];

export { webpackX as webpack, docsX as docs, optimizeViteDeps };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, expect, it } from 'vitest';

import { generateModernIframeScriptCodeFromPreviews } from './codegen-modern-iframe-script';
import { generateAddonSetupCode } from './codegen-set-addon-channel';
import { optimizeViteDeps } from './preset';

describe('generateModernIframeScriptCodeFromPreviews', () => {
it('handle one annotation', async () => {
Expand Down Expand Up @@ -131,3 +133,60 @@ describe('generateModernIframeScriptCodeFromPreviews', () => {
`);
});
});

/**
* Extract bare package import specifiers from a block of generated JavaScript/TypeScript code.
* Captures both `import ... from 'pkg'` and `import 'pkg'` forms, excluding:
*
* - Relative paths (start with `.`)
* - Virtual module IDs (start with `virtual:`)
* - Absolute paths (start with `/`)
*/
function extractPackageImports(code: string): string[] {
const importRegex = /import\s+(?:[^'"]*\s+from\s+)?['"]([^'"]+)['"]/g;
const specifiers = new Set<string>();
for (const match of code.matchAll(importRegex)) {
const specifier = match[1];
if (
!specifier.startsWith('.') &&
!specifier.startsWith('virtual:') &&
!specifier.startsWith('/')
) {
specifiers.add(specifier);
}
}
return [...specifiers];
}

describe('optimizeDeps coverage for virtual module imports', () => {
it('every package imported in virtual module code is either in optimizeViteDeps or known to be discovered via entry crawling', async () => {
// Collect all code generated for virtual modules — Vite's dep scanner never sees
// the contents of virtual modules, so any package imported there must be
// pre-bundled explicitly via optimizeViteDeps.
const iframeCode = await generateModernIframeScriptCodeFromPreviews({ frameworkName: 'test' });
const addonCode = await generateAddonSetupCode();
const allVirtualModuleCode = [iframeCode, addonCode].join('\n');

const packageImports = extractPackageImports(allVirtualModuleCode);

// These packages are also imported in real source files (preview annotations, renderer
// previews, addon previews) that ARE added as optimizeDeps entries, so Vite discovers
// them via entry crawling. No explicit optimizeViteDeps entry is required.
const discoveredViaEntries = new Set([
'storybook/preview-api', // Imported in many renderer/addon preview files
'storybook/internal/channels', // Imported in addon preview files
]);

const notCovered = packageImports.filter(
(pkg) => !discoveredViaEntries.has(pkg) && !optimizeViteDeps.includes(pkg)
);

expect(
notCovered,
`The following packages are imported in virtual module code but are NOT covered.\n` +
`They must be added to optimizeViteDeps in builder-vite/src/preset.ts, OR added to\n` +
`the discoveredViaEntries set in this test if they appear in real source entry files.\n` +
`Uncovered: ${notCovered.join(', ')}`
).toHaveLength(0);
});
});
118 changes: 0 additions & 118 deletions code/builders/builder-vite/src/constants.ts

This file was deleted.

30 changes: 0 additions & 30 deletions code/builders/builder-vite/src/optimizeDeps.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { loadPreviewOrConfigFile } from 'storybook/internal/common';
import type { StoryIndexGenerator } from 'storybook/internal/core-server';
import type { Options, StoryIndex } from 'storybook/internal/types';
import type { Options, PreviewAnnotation, StoryIndex } from 'storybook/internal/types';

import { resolve } from 'pathe';
import { type Plugin } from 'vite';

import { processPreviewAnnotation } from '../utils/process-preview-annotation';
import { getUniqueImportPaths } from '../utils/unique-import-paths';

/** A Vite plugin that configures dependency optimization for Storybook's dev server. */
Expand All @@ -15,25 +18,37 @@ export function storybookOptimizeDepsPlugin(options: Options): Plugin {
return;
}

const [extraOptimizeDeps, storyIndexGenerator] = await Promise.all([
options.presets.apply('optimizeViteDeps', []),
const projectRoot = resolve(options.configDir, '..');

const [extraOptimizeDeps, storyIndexGenerator, previewAnnotations] = await Promise.all([
options.presets.apply<string[]>('optimizeViteDeps', []),
options.presets.apply<StoryIndexGenerator>('storyIndexGenerator'),
options.presets.apply<PreviewAnnotation[]>('previewAnnotations', [], options),
]);

const index: StoryIndex = await storyIndexGenerator.getIndex();

// Include the user's preview file and all addon/framework/renderer preview annotations
// as optimizer entries so Vite can discover all transitive CJS dependencies automatically.
const previewOrConfigFile = loadPreviewOrConfigFile({ configDir: options.configDir });
const previewAnnotationEntries = [...previewAnnotations, previewOrConfigFile]
.filter((path): path is PreviewAnnotation => path !== undefined)
.map((path) => processPreviewAnnotation(path, projectRoot));

return {
optimizeDeps: {
// Story file paths as entry points for the optimizer
// Story files + preview annotation files as entry points for the dep optimizer.
// Vite will crawl these to discover all transitive CJS dependencies that need
// pre-bundling, removing the need for a hard-coded include list.
entries: [
...(typeof config.optimizeDeps?.entries === 'string'
? [config.optimizeDeps.entries]
: []),
: (config.optimizeDeps?.entries ?? [])),
...getUniqueImportPaths(index),
...previewAnnotationEntries,
],
// Known CJS dependencies that need to be pre-compiled to ESM,
// plus any extra deps from Storybook presets.
include: [...extraOptimizeDeps, ...(config.optimizeDeps?.include || [])],
// Extra deps explicitly included by Storybook presets (e.g. framework-specific packages).
include: [...extraOptimizeDeps, ...(config.optimizeDeps?.include ?? [])],
},
};
},
Expand Down
2 changes: 2 additions & 0 deletions code/builders/builder-vite/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { storybookSanitizeEnvs } from './plugins/storybook-runtime-plugin';
import { viteInjectMockerRuntime } from './plugins/vite-inject-mocker/plugin';
import { viteMockPlugin } from './plugins/vite-mock/plugin';

export const optimizeViteDeps: string[] = ['storybook/internal/preview/runtime'];

/**
* Preset that provides the core Storybook Vite plugins shared between `@storybook/builder-vite` and
* `@storybook/addon-vitest`.
Expand Down
8 changes: 0 additions & 8 deletions code/builders/builder-vite/src/vite-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,15 @@ import { dedent } from 'ts-dedent';
import type { InlineConfig, ServerOptions } from 'vite';

import { createViteLogger } from './logger';
import { getOptimizeDeps } from './optimizeDeps';
import { commonConfig } from './vite-config';

export async function createViteServer(options: Options, devServer: Server) {
const { presets } = options;

const commonCfg = await commonConfig(options, 'development');

const optimizeDeps = await getOptimizeDeps(commonCfg);

const config: InlineConfig & { server: ServerOptions } = {
...commonCfg,
// Set up dev server
optimizeDeps: {
...commonCfg.optimizeDeps,
include: [...(commonCfg.optimizeDeps?.include || []), ...optimizeDeps.include],
},
server: {
middlewareMode: true,
hmr: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ describe('ghostStoriesChannel', () => {
stats: {
globMatchCount: 0,
candidateAnalysisDuration: 0,
totalRunDuration: 0,
totalRunDuration: expect.any(Number),
analyzedCount: 0,
avgComplexity: 0,
candidateCount: 0,
Expand Down
8 changes: 4 additions & 4 deletions code/lib/cli-storybook/src/sandbox-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ export const baseTemplates = {
renderer: '@storybook/html',
builder: '@storybook/builder-vite',
},
skipTasks: ['e2e-tests', 'bench', 'vitest-integration'],
skipTasks: ['e2e-tests', 'bench'],
initOptions: {
type: ProjectType.HTML,
},
Expand All @@ -597,7 +597,7 @@ export const baseTemplates = {
renderer: '@storybook/html',
builder: '@storybook/builder-vite',
},
skipTasks: ['e2e-tests', 'bench', 'vitest-integration'],
skipTasks: ['e2e-tests', 'bench'],
initOptions: {
type: ProjectType.HTML,
},
Expand Down Expand Up @@ -693,7 +693,7 @@ export const baseTemplates = {
useCsfFactory: true,
},
// Remove smoke-test from the list once https://github.com/storybookjs/storybook/issues/19351 is fixed.
skipTasks: ['smoke-test', 'e2e-tests', 'bench', 'vitest-integration'],
skipTasks: ['smoke-test', 'e2e-tests', 'bench'],
},
'lit-vite/default-ts': {
name: 'Lit Latest (Vite | TypeScript)',
Expand All @@ -708,7 +708,7 @@ export const baseTemplates = {
useCsfFactory: true,
},
// Remove smoke-test from the list once https://github.com/storybookjs/storybook/issues/19351 is fixed.
skipTasks: ['smoke-test', 'e2e-tests', 'bench', 'vitest-integration'],
skipTasks: ['smoke-test', 'e2e-tests', 'bench'],
},
'lit-rsbuild/default-ts': {
name: 'Web Components Latest (RsBuild | TypeScript)',
Expand Down
15 changes: 14 additions & 1 deletion code/lib/react-dom-shim/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,20 @@ export const webpackFinal = async (config: any, options: Options) => {

export const viteFinal = async (config: any, options: Options) => {
const isReactVersion18 = await getIsReactVersion18or19(options);

const optimizeDeps = {
...(config?.optimizeDeps ?? {}),
include: [
...(config.optimizeDeps?.include || []),
...(isReactVersion18 ? ['react-dom/client'] : []),
],
};

if (isReactVersion18) {
return config;
return {
...config,
optimizeDeps,
};
}

const alias = Array.isArray(config.resolve?.alias)
Expand All @@ -66,6 +78,7 @@ export const viteFinal = async (config: any, options: Options) => {

return {
...config,
optimizeDeps,
resolve: {
...config.resolve,
alias,
Expand Down
Loading
Loading