Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
7c44d0a
Add documentation on how we develop features and release Storybook
vanessayuenn Jun 4, 2025
075da24
WIP
kylegach Jun 12, 2025
5c0be3e
Tweaks
kylegach Jun 12, 2025
e226b18
Merge branch 'next' into vy/add-release-docs
kylegach Jun 12, 2025
3f6a06c
Update sidebar titles
kylegach Jun 12, 2025
e6a2935
Improve clarity of supported versions
kylegach Jun 20, 2025
67bb672
Merge branch 'next' into vy/add-release-docs
kylegach Jun 20, 2025
37ac8d2
WIP
valentinpalkovic Jun 24, 2025
539f149
Build-Mode: Module automocking manifest generation
valentinpalkovic Jun 30, 2025
2fa1747
Support build mode for mocking
valentinpalkovic Jul 7, 2025
5de7c3e
removing labels from type unions
grantralls Jul 7, 2025
f532b91
adding union type to button for manual tests
grantralls Jul 7, 2025
cea8ce1
remove trailing comma
grantralls Jul 7, 2025
6b63b02
adding comments and fixing secondary story
grantralls Jul 7, 2025
4c4a30b
Merge branch 'fix-svelte-extract-arg-types' of github.com:grantralls/…
grantralls Jul 7, 2025
f27837d
adding explanation for empty flat mapping
grantralls Jul 8, 2025
74b1ea4
Support mocking in build and serve mode without msw
valentinpalkovic Jul 9, 2025
09d0526
Invalidate mocks and affected files on change
valentinpalkovic Jul 9, 2025
ae7ccbb
Cleanup
valentinpalkovic Jul 9, 2025
6ed46dc
Fix type assertion for global CHANNEL_OPTIONS
valentinpalkovic Jul 9, 2025
d421269
Merge remote-tracking branch 'origin/next' into valentin/storybook-mock
valentinpalkovic Jul 9, 2025
507b895
Remove msw dependency
valentinpalkovic Jul 9, 2025
452b1b1
Revert accidential addition of import
valentinpalkovic Jul 9, 2025
f3a7a3f
Fix build
valentinpalkovic Jul 9, 2025
64db30c
Update lock file
valentinpalkovic Jul 9, 2025
b7f804d
Merge branch 'next' into valentin/storybook-mock
valentinpalkovic Jul 10, 2025
51739bd
Dynamically load vite-based dependencies
valentinpalkovic Jul 10, 2025
85da36e
Renamings
valentinpalkovic Jul 10, 2025
94d6004
Renamings
valentinpalkovic Jul 10, 2025
969c6b6
Further renamings
valentinpalkovic Jul 10, 2025
d0134d0
Remove unused imports
valentinpalkovic Jul 10, 2025
2117325
Fix pnpm related issues where dependencies in virtual files cannot be…
valentinpalkovic Jul 10, 2025
0be57b8
Readd storybook/test global
valentinpalkovic Jul 10, 2025
a6e7285
Ignore ts error due to wrong types
valentinpalkovic Jul 10, 2025
ddeb032
Finetune stories
valentinpalkovic Jul 10, 2025
2d8331d
Simplify configuration
valentinpalkovic Jul 10, 2025
0467d3d
Move storybook vitest config to root
valentinpalkovic Jul 10, 2025
8a16325
Fix indentation
valentinpalkovic Jul 10, 2025
d8bfaf6
Merge remote-tracking branch 'origin/next' into valentin/storybook-mock
valentinpalkovic Jul 10, 2025
37a70c5
Merge branch 'next' into vy/add-release-docs
vanessayuenn Jul 11, 2025
f243db7
Update features.mdx
vanessayuenn Jul 11, 2025
ba39684
Add telemetry
valentinpalkovic Jul 11, 2025
7f9ccd1
Add E2E tests
valentinpalkovic Jul 11, 2025
0200199
Optimize @vitest/spy dependency
valentinpalkovic Jul 11, 2025
1bc7f56
Fix event checker
valentinpalkovic Jul 11, 2025
2529308
Implement module mocking for Webpack
valentinpalkovic Jul 13, 2025
a7f833f
Update event log checker to find main event by event type when noBoot…
valentinpalkovic Jul 13, 2025
8757901
Enable mocking E2E tests for all frameworks
valentinpalkovic Jul 13, 2025
ec1721e
Update package.json
valentinpalkovic Jul 13, 2025
ddcc52e
Refactor sandbox-parts.ts to always include mocking setup for Vitest …
valentinpalkovic Jul 13, 2025
0d381f1
Add temporary resolution field for rollup
valentinpalkovic Jul 13, 2025
e5cd418
Adjust event-log-checker to consider mocking event
valentinpalkovic Jul 14, 2025
91374d1
Don't setup mocks for bench sandboxes
valentinpalkovic Jul 14, 2025
e5b3b3b
Add rollup version resolution to package.json
valentinpalkovic Jul 14, 2025
4bfdfee
Update sandbox task conditions to check for 'Bench' instead of 'bench'
valentinpalkovic Jul 14, 2025
c806bf0
Rewrite mock modules to be framework agnostic
valentinpalkovic Jul 14, 2025
198b15c
Adjust sb-module-mocking e2e tests
valentinpalkovic Jul 14, 2025
f84e398
Fix mocking when preserveSymlink is true
valentinpalkovic Jul 14, 2025
c00e0c2
Refactor event-log-checker to exclude mocking events from log assertions
valentinpalkovic Jul 14, 2025
3c9376d
Merge branch 'next' into fix-svelte-extract-arg-types
grantralls Jul 14, 2025
368b902
Simplify mock stories
valentinpalkovic Jul 14, 2025
6c006cf
Fix module mocking stories
valentinpalkovic Jul 14, 2025
c7b2846
Transform NodeModuleMocking into JavaScript
valentinpalkovic Jul 14, 2025
77f6c71
Update storybook mock stories to use 'text' instead of 'object' for a…
valentinpalkovic Jul 14, 2025
e7699cc
Update CHANGELOG.md for v9.0.17 [skip ci]
storybook-bot Jul 15, 2025
2770930
Fix Windows-related issues
valentinpalkovic Jul 15, 2025
200c6d3
Change transform order in viteMockPlugin from 'post' to 'pre'
valentinpalkovic Jul 15, 2025
1c0e813
Fix creation of react-webpack/prerelease-ts in linked mode
valentinpalkovic Jul 16, 2025
73f030d
Merge branch 'next' into fix-svelte-extract-arg-types
JReinhold Jul 16, 2025
a5d24df
Implement watcher for mocking in Webpack-based projects
valentinpalkovic Jul 16, 2025
5b4f063
Use babel for parsing mocks
valentinpalkovic Jul 16, 2025
668f2d2
Improve error logging in viteMockPlugin to include error details
valentinpalkovic Jul 16, 2025
6d5499b
Revert change
valentinpalkovic Jul 16, 2025
b7d3a49
reverting component & story changes
grantralls Jul 16, 2025
477ca9e
reverting component & story changes
grantralls Jul 16, 2025
628f30c
Merge pull request #31980 from grantralls/fix-svelte-extract-arg-types
JReinhold Jul 16, 2025
bb66c83
Support sb.mock(import(x), ...) syntax
valentinpalkovic Jul 17, 2025
7ac5042
Use spyOn from storybook/test
valentinpalkovic Jul 17, 2025
f54c196
Use proper type for sb.mock
valentinpalkovic Jul 17, 2025
4ecd08f
Remove unnecessary try/catch block
valentinpalkovic Jul 17, 2025
88961b7
Add Todo comments
valentinpalkovic Jul 17, 2025
e47c289
Remove unnecessary code
valentinpalkovic Jul 17, 2025
9ffd81d
Telemetry: Add nodeLinker to telemetry
valentinpalkovic Jul 18, 2025
e0b28ed
Add tests
valentinpalkovic Jul 18, 2025
ec50290
Correct type
valentinpalkovic Jul 18, 2025
f7c3e91
Merge pull request #32072 from storybookjs/valentin/add-node-linker-t…
valentinpalkovic Jul 18, 2025
d358bfc
Cleanup
valentinpalkovic Jul 18, 2025
7f7556c
Merge branch 'next' into valentin/storybook-mock
valentinpalkovic Jul 18, 2025
2bce95e
Merge pull request #31987 from storybookjs/valentin/storybook-mock
valentinpalkovic Jul 18, 2025
ea3843a
ignore 0.x packages in compatibility checks
yannbf Jul 18, 2025
e243936
docs: remove deprecated vscode extension
Joe-Moran Jul 18, 2025
20f98c6
Merge branch 'next' into vy/add-release-docs
kylegach Jul 18, 2025
9f17aa1
Merge pull request #31662 from storybookjs/vy/add-release-docs
kylegach Jul 18, 2025
7520398
Merge branch 'next' into patch-3
kylegach Jul 18, 2025
b987886
Merge pull request #32078 from Joe-Moran/patch-3
kylegach Jul 18, 2025
2510bf8
Merge pull request #32077 from storybookjs/yann/fix-doctor-reports
yannbf Jul 21, 2025
ea1a760
Write changelog for 9.1.0-alpha.9 [skip ci]
storybook-bot Jul 21, 2025
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
Prev Previous commit
Next Next commit
Build-Mode: Module automocking manifest generation
  • Loading branch information
valentinpalkovic committed Jun 30, 2025
commit 539f149faf498f2cd071b362e050cd2c4aca44db
3 changes: 2 additions & 1 deletion code/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ import * as templatePreview from '../core/template/stories/preview';
import '../renderers/react/template/components/index';
import { isChromatic } from './isChromatic';

sb.mock('../core/src/test/stories/ModuleMocking.utils');
sb.mock('../core/src/test/stories/ModuleMocking.utils', { spy: true });
sb.mock('../core/src/test/stories/ModuleAutoMocking.utils');

const { document } = global;
globalThis.CONFIG_TYPE = 'DEVELOPMENT';
Expand Down
2 changes: 2 additions & 0 deletions code/core/src/core-server/presets/common-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { initializeSaveStory } from '../utils/save-story/save-story';
import { parseStaticDir } from '../utils/server-statics';
import { type OptionsWithRequiredCache, initializeWhatsNew } from '../utils/whats-new';
import { viteInjectMockerRuntime } from './vitePlugins/vite-inject-mocker/plugin';
import { viteMockBuildManifestPlugin } from './vitePlugins/vite-mock-build-manifest/plugin';

const interpolate = (string: string, data: Record<string, string> = {}) =>
Object.entries(data).reduce((acc, [k, v]) => acc.replace(new RegExp(`%${k}%`, 'g'), v), string);
Expand Down Expand Up @@ -340,6 +341,7 @@ export const viteFinal = async (
utilsObjectNames: ['sb'],
},
}),
...(previewConfigPath ? [viteMockBuildManifestPlugin({ previewConfigPath })] : []),
],
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MockerRegistry } from '@vitest/mocker';
import type { MockedModule } from '@vitest/mocker';

// This interceptor will be used in the production build.
// It relies on a pre-generated manifest.
export class ModuleMockerBuildInterceptor {
private manifestPromise: Promise<any>;
private mocks = new MockerRegistry();

constructor(options) {
// Fetch the manifest we created during the build
this.manifestPromise = fetch('/mock-manifest.json').then((res) => res.json());
}

async register(module: MockedModule): Promise<void> {
this.mocks.add(module);

// Here, we can override the default import behavior
// This is the most complex part. We might need to integrate with MSW
// to redirect requests for mocked modules to our pre-built automock assets.
}

// ... other methods
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';

import { exactRegex } from '@rolldown/pluginutils';
import { dedent } from 'ts-dedent';
import type { ViteDevServer } from 'vite';
import type { ResolvedConfig, ViteDevServer } from 'vite';

const entryPath = '/vite-inject-mocker-entry.js';

Expand All @@ -12,88 +12,94 @@ const entryCode = dedent`

const __STORYBOOK_GLOBAL_THIS_ACCESSOR__ = '__vitest_mocker__';

const runtimeCode = () => dedent`
import { ModuleMockerServerInterceptor } from "@vitest/mocker/browser";
import { registerModuleMocker } from "@vitest/mocker/register";
globalThis.__STORYBOOK_MOCKER__ = registerModuleMocker((globalThisAccessor) => new ModuleMockerServerInterceptor(globalThisAccessor));
const runtimeCode = (command: ResolvedConfig['command']) => {
if (command === 'serve') {
return dedent`
import { ModuleMockerServerInterceptor } from "@vitest/mocker/browser";
import { registerModuleMocker } from "@vitest/mocker/register";
globalThis.__STORYBOOK_MOCKER__ = registerModuleMocker((globalThisAccessor) => new ModuleMockerServerInterceptor(globalThisAccessor));

if (import.meta.hot) {
import.meta.hot.on('invalidate-mocker', (payload) => {
globalThis.${__STORYBOOK_GLOBAL_THIS_ACCESSOR__}.invalidate();
});
if (import.meta.hot) {
import.meta.hot.on('invalidate-mocker', (payload) => {
globalThis.${__STORYBOOK_GLOBAL_THIS_ACCESSOR__}.invalidate();
});
}
`;
} else {
return dedent`
// For 'build', we'll use a custom interceptor that works with a pre-built manifest
const { ModuleMockerBuildInterceptor } = await import('./ModuleMockerBuildInterceptor'); // We will create this
const { registerModuleMocker } = await import('@vitest/mocker/register');
globalThis.__STORYBOOK_MOCKER__ = registerModuleMocker(
(accessor) => new ModuleMockerBuildInterceptor({ globalThisAccessor: accessor })
`;
}
`;
};

let server: ViteDevServer;

export const viteInjectMockerRuntime = (options: {
previewConfigPath?: string | null;
}): import('vite').Plugin => ({
name: 'vite:inject-mocker-runtime',
config() {
return {
resolve: {
// external: ['msw/browser', 'msw/core/http'],
}): import('vite').Plugin => {
let viteConfig: ResolvedConfig;

return {
name: 'vite:inject-mocker-runtime',
configResolved(config) {
viteConfig = config;
},
configureServer(server_) {
server = server_;
if (options.previewConfigPath) {
server.watcher.on('change', (file) => {
if (file === options.previewConfigPath) {
server.ws.send({
type: 'custom',
event: 'invalidate-mocker',
});
}
});
}
},
resolveId: {
filter: {
id: [exactRegex(entryPath)],
},
};
},
configureServer(server_) {
server = server_;
if (options.previewConfigPath) {
server.watcher.on('change', (file) => {
if (file === options.previewConfigPath) {
server.ws.send({
type: 'custom',
event: 'invalidate-mocker',
});
handler(id) {
if (exactRegex(id).test(entryPath)) {
return id;
}
});
}
},
resolveId: {
filter: {
id: [exactRegex(entryPath)],
return null;
},
},
handler(id) {
async load(id) {
if (exactRegex(id).test(entryPath)) {
return id;
return runtimeCode(viteConfig.command);
}

if (id.includes('@vitest/mocker/dist/register.js')) {
const content = await readFile(require.resolve('@vitest/mocker/dist/register.js'), 'utf-8');
const result = content
.replace(
/__VITEST_GLOBAL_THIS_ACCESSOR__/g,
JSON.stringify(__STORYBOOK_GLOBAL_THIS_ACCESSOR__)
)
.replace('__VITEST_MOCKER_ROOT__', JSON.stringify(server.config.root));
return result;
}
return null;
},
},
async load(id) {
if (exactRegex(id).test(entryPath)) {
return runtimeCode();
}
transformIndexHtml(html: string) {
const headTag = html.match(/<head[^>]*>/);

if (id.includes('@vitest/mocker/dist/register.js')) {
console.log(id);
if (!server) {
// mocker doesn't work during build
return 'export {}';
if (headTag) {
const headTagIndex = html.indexOf(headTag[0]);
const newHtml =
html.slice(0, headTagIndex + headTag[0].length) +
entryCode +
html.slice(headTagIndex + headTag[0].length);
return newHtml;
}

const content = await readFile(require.resolve('@vitest/mocker/dist/register.js'), 'utf-8');
const result = content
.replace(
/__VITEST_GLOBAL_THIS_ACCESSOR__/g,
JSON.stringify(__STORYBOOK_GLOBAL_THIS_ACCESSOR__)
)
.replace('__VITEST_MOCKER_ROOT__', JSON.stringify(server.config.root));
return result;
}
return null;
},
transformIndexHtml(html: string) {
const headTag = html.match(/<head[^>]*>/);

if (headTag) {
const headTagIndex = html.indexOf(headTag[0]);
const newHtml =
html.slice(0, headTagIndex + headTag[0].length) +
entryCode +
html.slice(headTagIndex + headTag[0].length);
return newHtml;
}
},
});
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import crypto from 'node:crypto';
import { readFile } from 'node:fs/promises';

import { automockModule, findMockRedirect } from '@vitest/mocker/node';
import { walk } from 'estree-walker';
import type { Plugin, ResolvedConfig } from 'vite';

interface MockBuildManifestPluginOptions {
/** The absolute path to the preview.tsx file where mocks are defined. */
previewConfigPath: string;
}

const VIRTUAL_AUTOMOCK_PREFIX = 'virtual:automock:';

/**
* A Vite plugin that runs only during build. It scans the preview.tsx file for `sb.mock()` calls,
* processes them, and creates a manifest for runtime use.
*/
export function viteMockBuildManifestPlugin(options: MockBuildManifestPluginOptions): Plugin {
let viteConfig: ResolvedConfig;

// Temporary maps to hold chunk references during the build.
const redirectChunkRefs = new Map<string, string>();
const automockChunkRefs = new Map<string, string>();

return {
name: 'storybook:mock-build-manifest',
apply: 'build',

buildStart() {
redirectChunkRefs.clear();
automockChunkRefs.clear();
},

configResolved(config) {
viteConfig = config;
},

async transform(code, id) {
// We only care about the specific `preview.tsx` file where mocks are defined.
if (id !== options.previewConfigPath) {
return null;
}

const ast = this.parse(code);
const mockCalls: Array<{ path: string; isFactoryMock: boolean }> = [];

// Use an AST walker to find all `sb.mock()` expressions.
walk(ast as any, {
enter(node) {
if (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'sb' &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'mock'
) {
if (node.arguments.length > 0 && node.arguments[0].type === 'Literal') {
mockCalls.push({
path: node.arguments[0].value as string,
isFactoryMock: node.arguments.length > 1,
});
}
}
},
});

if (mockCalls.length === 0) {
return null;
}

for (const call of mockCalls) {
// Factory mocks (e.g., `sb.mock('path', () => ({ ... }))`) are handled at runtime
// because the factory function needs to exist in the browser's memory with its scope.
// We only need to process path-only mocks here.
if (call.isFactoryMock) {
continue;
}

const resolved = await this.resolve(call.path, id);
if (!resolved) {
this.warn(`[vitest-mocker] Could not resolve mock path "${call.path}" in ${id}`);
continue;
}

const resolvedId = resolved.id;

// Check for a corresponding file in a `__mocks__` directory.
const redirectPath = findMockRedirect(viteConfig.root, resolvedId, null);

if (redirectPath) {
const chunkRef = this.emitFile({
type: 'chunk',
id: redirectPath,
importer: options.previewConfigPath,
});

// The manifest maps the original module ID to the URL of the newly created asset.
redirectChunkRefs.set(resolvedId, chunkRef);
} else {
// If there's no redirect, it's an automock. We'll generate the mocked version.
const virtualId = `${VIRTUAL_AUTOMOCK_PREFIX}${resolvedId}`;

const chunkRef = this.emitFile({
type: 'chunk',
id: virtualId, // The entry point is our virtual module ID.
importer: options.previewConfigPath,
});

// The manifest maps the original module ID to the URL of the automocked asset.
automockChunkRefs.set(resolvedId, chunkRef);
}
}

// We return null because we don't need to change the preview.tsx file itself.
// All our work is done by calling `this.emitFile`.
return null;
},

// The resolveId and load hooks are used to provide the source for our virtual automock modules.
resolveId(id) {
if (id.startsWith(VIRTUAL_AUTOMOCK_PREFIX)) {
return id;
}
return null;
},

async load(id) {
if (id.startsWith(VIRTUAL_AUTOMOCK_PREFIX)) {
const originalId = id.slice(VIRTUAL_AUTOMOCK_PREFIX.length);
const originalCode = await readFile(originalId, 'utf-8');
// Generate the mocked source code. Vite will transform this code after we return it.
const mocked = automockModule(originalCode, 'automock', this.parse, {
globalThisAccessor: JSON.stringify('__vitest_mocker__'),
});
return mocked.toString();
}
return null;
},

generateBundle() {
// This hook runs after all chunks have been generated and filenames are finalized.
const manifest = {
redirects: {} as Record<string, string>,
automocks: {} as Record<string, string>,
};

// Resolve the final URLs for our __mocks__ redirects.
for (const [originalId, chunkRef] of redirectChunkRefs.entries()) {
const finalFileName = this.getFileName(chunkRef);
manifest.redirects[originalId] = `/${finalFileName}`;
}

// Resolve the final URLs for our automocked modules.
for (const [originalId, chunkRef] of automockChunkRefs.entries()) {
const finalFileName = this.getFileName(chunkRef);
manifest.automocks[originalId] = `/${finalFileName}`;
}

// Emit the final manifest file.
this.emitFile({
type: 'asset',
fileName: 'mock-manifest.json',
source: JSON.stringify(manifest, null, 2),
});
},
};
}
Loading