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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 10.2.1

- Builder-Webpack5: Fix @vitest/mocker resolution issue - [#33315](https://github.com/storybookjs/storybook/pull/33315), thanks @valentinpalkovic!
- CLI: Add init telemetry for CLI integrations - [#33603](https://github.com/storybookjs/storybook/pull/33603), thanks @shilman!

## 10.2.0

> Improved UI and story authoring ergonomics
Expand Down
1 change: 0 additions & 1 deletion code/builders/builder-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
],
"dependencies": {
"@storybook/csf-plugin": "workspace:*",
"@vitest/mocker": "3.2.4",
"ts-dedent": "^2.0.0"
},
"devDependencies": {
Expand Down
74 changes: 16 additions & 58 deletions code/builders/builder-vite/src/plugins/vite-inject-mocker/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,35 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

import { resolvePackageDir } from 'storybook/internal/common';
import type { ResolvedConfig } from 'vite';

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

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

const entryCode = dedent`
<script type="module" src=".${entryPath}"></script>
`;

let server: ViteDevServer;
const ENTRY_PATH = '/vite-inject-mocker-entry.js';

export const viteInjectMockerRuntime = (options: {
previewConfigPath?: string | null;
}): import('vite').Plugin => {
// Get the actual file path so Vite can resolve relative imports
const mockerRuntimePath = fileURLToPath(
import.meta.resolve('storybook/internal/mocking-utils/mocker-runtime')
);

let viteConfig: ResolvedConfig;

return {
name: 'vite:storybook-inject-mocker-runtime',
enforce: 'pre',
buildStart() {
if (viteConfig.command === 'build') {
this.emitFile({
type: 'chunk',
id: join(
resolvePackageDir('storybook'),
'assets',
'server',
'mocker-runtime.template.js'
),
fileName: entryPath.slice(1),
id: mockerRuntimePath,
fileName: ENTRY_PATH.slice(1),
});
}
},
config() {
return {
optimizeDeps: {
include: ['@vitest/mocker', '@vitest/mocker/browser'],
},
resolve: {
// Aliasing necessary for package managers like pnpm, since resolving modules from a virtual module
// leads to errors, if the imported module is not a dependency of the project.
// By resolving the module to the real path, we can avoid this issue.
alias: {
'@vitest/mocker/browser': fileURLToPath(import.meta.resolve('@vitest/mocker/browser')),
'@vitest/mocker': fileURLToPath(import.meta.resolve('@vitest/mocker')),
},
},
};
},
configResolved(config) {
viteConfig = config;
},
configureServer(server_) {
server = server_;
configureServer(server) {
if (options.previewConfigPath) {
server.watcher.on('change', (file) => {
if (file === options.previewConfigPath) {
Expand All @@ -69,31 +41,17 @@ export const viteInjectMockerRuntime = (options: {
});
}
},
resolveId: {
filter: {
id: [exactRegex(entryPath)],
},
handler(id) {
if (exactRegex(id).test(entryPath)) {
return id;
}
return null;
},
},
async load(id) {
if (exactRegex(id).test(entryPath)) {
return readFileSync(
join(resolvePackageDir('storybook'), 'assets', 'server', 'mocker-runtime.template.js'),
'utf-8'
);
resolveId(source) {
if (source === ENTRY_PATH) {
return mockerRuntimePath;
}

return null;
return undefined;
},
transformIndexHtml(html: string) {
const headTag = html.match(/<head[^>]*>/);

if (headTag) {
const entryCode = `<script type="module" src="${ENTRY_PATH}"></script>`;
const headTagIndex = html.indexOf(headTag[0]);
const newHtml =
html.slice(0, headTagIndex + headTag[0].length) +
Expand Down
2 changes: 1 addition & 1 deletion code/builders/builder-vite/src/plugins/vite-mock/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { readFileSync } from 'node:fs';
import {
babelParser,
extractMockCalls,
findMockRedirect,
getAutomockCode,
getRealPath,
rewriteSbMockImportCalls,
} from 'storybook/internal/mocking-utils';
import { logger } from 'storybook/internal/node-logger';
import type { CoreConfig } from 'storybook/internal/types';

import { findMockRedirect } from '@vitest/mocker/redirect';
import { normalize } from 'pathe';
import type { Plugin, ResolvedConfig } from 'vite';

Expand Down
1 change: 0 additions & 1 deletion code/builders/builder-webpack5/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
],
"dependencies": {
"@storybook/core-webpack": "workspace:*",
"@vitest/mocker": "3.2.4",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"cjs-module-lexer": "^1.2.3",
"css-loader": "^7.1.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { fileURLToPath } from 'node:url';
import {
babelParser,
extractMockCalls,
findMockRedirect,
getIsExternal,
resolveExternalModule,
resolveWithExtensions,
} from 'storybook/internal/mocking-utils';

import { findMockRedirect } from '@vitest/mocker/redirect';
import type { Compiler } from 'webpack';

// --- Type Definitions ---
Expand Down
10 changes: 10 additions & 0 deletions code/core/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,16 @@ const config: BuildEntries = {
entryPoint: './src/manager/globals-runtime.ts',
dts: false,
},
/**
* It is required to be a runtime entry point, because it is used to inject the mocker runtime
* into the preview iframe in builder-vite and builder-webpack5. To guarantee that the mocker
* runtime is transpiled correctly, code splitting needs to be disabled for this entry point.
*/
{
exportEntries: ['./internal/mocking-utils/mocker-runtime'],
entryPoint: './src/mocking-utils/mocker-runtime.js',
dts: false,
},
],
globalizedRuntime: [
{
Expand Down
2 changes: 2 additions & 0 deletions code/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"types": "./dist/mocking-utils/index.d.ts",
"default": "./dist/mocking-utils/index.js"
},
"./internal/mocking-utils/mocker-runtime": "./dist/mocking-utils/mocker-runtime.js",
"./internal/node-logger": {
"types": "./dist/node-logger/index.d.ts",
"default": "./dist/node-logger/index.js"
Expand Down Expand Up @@ -255,6 +256,7 @@
"@types/react-syntax-highlighter": "11.0.5",
"@types/semver": "^7.7.1",
"@types/ws": "^8",
"@vitest/mocker": "3.2.4",
"@vitest/utils": "^3.2.4",
"@yarnpkg/fslib": "2.10.3",
"@yarnpkg/libzip": "2.3.0",
Expand Down
1 change: 1 addition & 0 deletions code/core/src/mocking-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './extract';
export * from './resolve';
export * from './esmWalker';
export * from './runtime';
export * from './redirect';
3 changes: 3 additions & 0 deletions code/core/src/mocking-utils/redirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Re-export findMockRedirect from @vitest/mocker/redirect
// This allows builders to use it without depending on @vitest/mocker directly
export { findMockRedirect } from '@vitest/mocker/redirect';
39 changes: 12 additions & 27 deletions code/core/src/mocking-utils/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
import { resolvePackageDir } from 'storybook/internal/common';

import { buildSync } from 'esbuild';
import { join } from 'pathe';

const runtimeTemplatePath = join(
resolvePackageDir('storybook'),
'assets',
'server',
'mocker-runtime.template.js'
);

export function getMockerRuntime() {
// Use esbuild to bundle the runtime script and its dependencies (`@vitest/mocker`, etc.)
// into a single, self-contained string of code.
const bundleResult = buildSync({
entryPoints: [runtimeTemplatePath],
bundle: true,
write: false, // Return the result in memory instead of writing to disk
format: 'esm',
target: 'es2020',
external: ['msw/browser', 'msw/core/http'],
});

const runtimeScriptContent = bundleResult.outputFiles[0].text;

return runtimeScriptContent;
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';

/**
* Returns the bundled mocker runtime script content. This is used by builders (webpack5, vite,
* etc.) to inject the mocker runtime into the preview iframe.
*/
export function getMockerRuntime(): string {
return readFileSync(
fileURLToPath(import.meta.resolve('storybook/internal/mocking-utils/mocker-runtime')),
'utf-8'
);
}
7 changes: 5 additions & 2 deletions code/lib/cli-storybook/src/bin/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ const handleCommandFailure =
try {
const logFile = await logTracker.writeToFile(logFilePath);
logger.log(`Debug logs are written to: ${logFile}`);
} catch {}
} catch (e) {
logger.error('Error writing debug logs to file');
logger.error(String(e));
}
logger.outro('');
process.exit(1);
};
Expand Down Expand Up @@ -247,7 +250,7 @@ command('sandbox [filterValue]')
.action((filterValue, options) => {
logger.intro(`Creating a Storybook sandbox...`);
sandbox({ filterValue, ...options })
.catch(handleCommandFailure)
.catch(handleCommandFailure(options.logfile))
.finally(() => {
logger.outro('Done!');
});
Expand Down
33 changes: 22 additions & 11 deletions code/lib/cli-storybook/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,24 @@ export const sandbox = async ({
const packageManager = JsPackageManagerFactory.getPackageManager({
force: pkgMgr,
});
const latestVersion = (await packageManager.latestVersion('storybook'))!;
const latestVersion = (await packageManager.latestVersion('storybook')) ?? '0.0.0';
const nextVersion = (await packageManager.latestVersion('storybook@next')) ?? '0.0.0';

logger.debug(`latestVersion: ${latestVersion}`);
logger.debug(`nextVersion: ${nextVersion}`);

const currentVersion = versions.storybook;
const isPrerelease = prerelease(currentVersion);
const isOutdated = lt(currentVersion, isPrerelease ? nextVersion : latestVersion);

const downloadType = !isOutdated && init ? 'after-storybook' : 'before-storybook';
const branch = isPrerelease ? 'next' : 'main';

logger.debug(`isPrerelease: ${isPrerelease}`);
logger.debug(`isOutdated: ${isOutdated}`);
logger.debug(`downloadType: ${downloadType}`);
logger.debug(`branch: ${branch}`);

const messages = {
welcome: `Creating a Storybook ${picocolors.bold(currentVersion)} sandbox..`,
notLatest: picocolors.red(dedent`
Expand All @@ -71,16 +80,18 @@ export const sandbox = async ({
prerelease: picocolors.yellow('This is a pre-release version.'),
};

logger.logBox(
[messages.welcome]
.concat(isOutdated && !isPrerelease ? [messages.notLatest] : [])
.concat(init && (isOutdated || isPrerelease) ? [messages.longInitTime] : [])
.concat(isPrerelease ? [messages.prerelease] : [])
.join('\n'),
{
rounded: true,
}
);
try {
logger.logBox(
[messages.welcome]
.concat(isOutdated && !isPrerelease ? [messages.notLatest] : [])
.concat(init && (isOutdated || isPrerelease) ? [messages.longInitTime] : [])
.concat(isPrerelease ? [messages.prerelease] : [])
.join('\n'),
{
rounded: true,
}
);
} catch {}

if (!selectedConfig) {
const filterRegex = new RegExp(`^${filterValue || ''}`, 'i');
Expand Down
51 changes: 51 additions & 0 deletions code/lib/create-storybook/src/services/VersionService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,57 @@ describe('VersionService', () => {

expect(integration).toBeUndefined();
});

it('should detect create-rsbuild command', () => {
const ancestry = [{ command: 'npx create-rsbuild' }, { command: 'node /usr/local/bin/npm' }];

const integration = versionService.getCliIntegrationFromAncestry(ancestry as any);

expect(integration).toBe('create-rsbuild');
});

it('should detect create rsbuild with version specifier', () => {
const ancestry = [{ command: 'npx create-rsbuild@1.0.0 init' }];

const integration = versionService.getCliIntegrationFromAncestry(ancestry as any);

expect(integration).toBe('create-rsbuild');
});

it('should detect "create rsbuild" with space instead of dash', () => {
const ancestry = [{ command: 'npm create rsbuild -- my-app' }];

const integration = versionService.getCliIntegrationFromAncestry(ancestry as any);

expect(integration).toBe('create-rsbuild');
});

it('should NOT detect creatersbuild without separator', () => {
const ancestry = [{ command: 'npx creatersbuild' }];

const integration = versionService.getCliIntegrationFromAncestry(ancestry as any);

expect(integration).toBeUndefined();
});

it('should detect @tanstack/start command', () => {
const ancestry = [{ command: 'npx @tanstack/start@latest create my-app' }];

const integration = versionService.getCliIntegrationFromAncestry(ancestry as any);

expect(integration).toBe('@tanstack/start');
});

it('should detect @tanstack/start in middle of command chain', () => {
const ancestry = [
{ command: 'pnpm @tanstack/start init' },
{ command: 'node /usr/local/bin/pnpm' },
];

const integration = versionService.getCliIntegrationFromAncestry(ancestry as any);

expect(integration).toBe('@tanstack/start');
});
});

describe('getVersionInfo', () => {
Expand Down
Loading
Loading