Skip to content

Release: Patch 9.1.1 #32175

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

Merged
merged 7 commits into from
Aug 4, 2025
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 9.1.1

- CLI: Fix throwing in readonly environments - [#31785](https://github.com/storybookjs/storybook/pull/31785), thanks @JReinhold!
- Onboarding: Tweak referral wording in survey - [#32185](https://github.com/storybookjs/storybook/pull/32185), thanks @shilman!
- Telemetry: Send index stats on dev exit - [#32168](https://github.com/storybookjs/storybook/pull/32168), thanks @shilman!

## 9.1.0

Storybook 9.1 is packed with new features and improvements to enhance accessibility, streamline testing, and make your development workflow even smoother!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const IntentSurvey = ({
},
},
referrer: {
label: 'How did you learn about Storybook?',
label: 'How did you discover Storybook?',
type: 'select',
required: true,
options: shuffleObject({
Expand Down
4 changes: 2 additions & 2 deletions code/addons/vitest/src/postinstall-logger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isCI } from 'storybook/internal/common';
import { colors, logger } from 'storybook/internal/node-logger';

const fancy =
process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color';
const fancy = process.platform !== 'win32' || isCI() || process.env.TERM === 'xterm-256color';

export const step = colors.gray('›');
export const info = colors.blue(fancy ? 'ℹ' : 'i');
Expand Down
3 changes: 2 additions & 1 deletion code/addons/vitest/src/postinstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
extractProperFrameworkName,
formatFileContent,
getProjectRoot,
isCI,
loadAllPresets,
loadMainConfig,
scanAndTransformFiles,
Expand Down Expand Up @@ -79,7 +80,7 @@ export default async function postInstall(options: PostinstallOptions) {

const hasCustomWebpackConfig = !!config.getFieldNode(['webpackFinal']);

const isInteractive = process.stdout.isTTY && !process.env.CI;
const isInteractive = process.stdout.isTTY && !isCI();

if (info.frameworkPackageName === '@storybook/nextjs' && !hasCustomWebpackConfig) {
const out =
Expand Down
29 changes: 14 additions & 15 deletions code/addons/vitest/src/vitest-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DEFAULT_FILES_PATTERN,
getInterpretedFile,
normalizeStories,
optionalEnvToBoolean,
resolvePathInStorybookCache,
validateConfigurationFiles,
} from 'storybook/internal/common';
Expand Down Expand Up @@ -123,14 +124,18 @@ export const storybookTest = async (options?: UserOptions): Promise<Plugin[]> =>
},
} as InternalOptions;

if (process.env.DEBUG) {
if (optionalEnvToBoolean(process.env.DEBUG)) {
finalOptions.debug = true;
}

// To be accessed by the global setup file
process.env.__STORYBOOK_URL__ = finalOptions.storybookUrl;
process.env.__STORYBOOK_SCRIPT__ = finalOptions.storybookScript;

// We signal the test runner that we are not running it via Storybook
// We are overriding the environment variable to 'true' if vitest runs via @storybook/addon-vitest's backend
const isVitestStorybook = optionalEnvToBoolean(process.env.VITEST_STORYBOOK);

const directories = {
configDir: finalOptions.configDir,
workingDir: WORKING_DIR,
Expand Down Expand Up @@ -212,10 +217,6 @@ export const storybookTest = async (options?: UserOptions): Promise<Plugin[]> =>
// plugin.name?.startsWith('vitest:browser')
// )

// We signal the test runner that we are not running it via Storybook
// We are overriding the environment variable to 'true' if vitest runs via @storybook/addon-vitest's backend
const vitestStorybook = process.env.VITEST_STORYBOOK ?? 'false';

const testConfig = nonMutableInputConfig.test;
finalOptions.vitestRoot =
testConfig?.dir || testConfig?.root || nonMutableInputConfig.root || process.cwd();
Expand Down Expand Up @@ -260,7 +261,7 @@ export const storybookTest = async (options?: UserOptions): Promise<Plugin[]> =>
// To be accessed by the setup file
__STORYBOOK_URL__: finalOptions.storybookUrl,

VITEST_STORYBOOK: vitestStorybook,
VITEST_STORYBOOK: isVitestStorybook ? 'true' : 'false',
__VITEST_INCLUDE_TAGS__: finalOptions.tags.include.join(','),
__VITEST_EXCLUDE_TAGS__: finalOptions.tags.exclude.join(','),
__VITEST_SKIP_TAGS__: finalOptions.tags.skip.join(','),
Expand Down Expand Up @@ -288,9 +289,7 @@ export const storybookTest = async (options?: UserOptions): Promise<Plugin[]> =>
getInitialGlobals: () => {
const envConfig = JSON.parse(process.env.VITEST_STORYBOOK_CONFIG ?? '{}');

const shouldRunA11yTests = process.env.VITEST_STORYBOOK
? (envConfig.a11y ?? false)
: true;
const shouldRunA11yTests = isVitestStorybook ? (envConfig.a11y ?? false) : true;

return {
a11y: {
Expand Down Expand Up @@ -373,10 +372,10 @@ export const storybookTest = async (options?: UserOptions): Promise<Plugin[]> =>
configureVitest(context) {
context.vitest.config.coverage.exclude.push('storybook-static');

const disableTelemetryVar =
process.env.STORYBOOK_DISABLE_TELEMETRY &&
process.env.STORYBOOK_DISABLE_TELEMETRY !== 'false';
if (!core?.disableTelemetry && !disableTelemetryVar) {
if (
!core?.disableTelemetry &&
!optionalEnvToBoolean(process.env.STORYBOOK_DISABLE_TELEMETRY)
) {
// NOTE: we start telemetry immediately but do not wait on it. Typically it should complete
// before the tests do. If not we may miss the event, we are OK with that.
telemetry(
Expand Down Expand Up @@ -410,7 +409,7 @@ export const storybookTest = async (options?: UserOptions): Promise<Plugin[]> =>
}
},
async transform(code, id) {
if (process.env.VITEST !== 'true') {
if (!optionalEnvToBoolean(process.env.VITEST)) {
return code;
}

Expand All @@ -434,7 +433,7 @@ export const storybookTest = async (options?: UserOptions): Promise<Plugin[]> =>
// When running tests via the Storybook UI, we need
// to find the right project to run, thus we override
// with a unique identifier using the path to the config dir
if (process.env.VITEST_STORYBOOK) {
if (isVitestStorybook) {
const projectName = `storybook:${normalize(finalOptions.configDir)}`;
plugins.push({
name: 'storybook:workspace-name-override',
Expand Down
7 changes: 3 additions & 4 deletions code/core/src/cli/bin/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getEnvConfig, parseList } from 'storybook/internal/common';
import { getEnvConfig, optionalEnvToBoolean, parseList } from 'storybook/internal/common';
import { logTracker, logger } from 'storybook/internal/node-logger';
import { addToGlobalContext } from 'storybook/internal/telemetry';

Expand All @@ -22,8 +22,7 @@ const command = (name: string) =>
.option(
'--disable-telemetry',
'Disable sending telemetry data',
// default value is false, but if the user sets STORYBOOK_DISABLE_TELEMETRY, it can be true
process.env.STORYBOOK_DISABLE_TELEMETRY && process.env.STORYBOOK_DISABLE_TELEMETRY !== 'false'
optionalEnvToBoolean(process.env.STORYBOOK_DISABLE_TELEMETRY)
)
.option('--debug', 'Get more logs in debug mode', false)
.option('--enable-crash-reports', 'Enable sending crash reports to telemetry data')
Expand Down Expand Up @@ -151,7 +150,7 @@ command('build')
await build({
...options,
packageJson: pkg,
test: !!options.test || process.env.SB_TESTBUILD === 'true',
test: !!options.test || optionalEnvToBoolean(process.env.SB_TESTBUILD),
}).catch(() => process.exit(1));
});

Expand Down
14 changes: 10 additions & 4 deletions code/core/src/cli/globalSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,22 @@ describe('Settings', () => {
);
});

it('throws error if write fails', async () => {
it('logs warning if write fails', async () => {
vi.mocked(fs.writeFile).mockRejectedValue(new Error('Write error'));

await expect(settings.save()).rejects.toThrow('Unable to save global settings');
await expect(settings.save()).resolves.toBeUndefined();
expect(console.warn).toHaveBeenCalledWith(
'Unable to save global settings file to /test/settings.json\nReason: Write error'
);
});

it('throws error if directory creation fails', async () => {
it('logs warning if directory creation fails', async () => {
vi.mocked(fs.mkdir).mockRejectedValue(new Error('Directory creation error'));

await expect(settings.save()).rejects.toThrow('Unable to save global settings');
await expect(settings.save()).resolves.toBeUndefined();
expect(console.warn).toHaveBeenCalledWith(
'Unable to save global settings file to /test/settings.json\nReason: Directory creation error'
);
});
});
});
10 changes: 4 additions & 6 deletions code/core/src/cli/globalSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import fs from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';

import { dedent } from 'ts-dedent';
import { z } from 'zod';

import { SavingGlobalSettingsFileError } from '../server-errors';

const DEFAULT_SETTINGS_PATH = join(homedir(), '.storybook', 'settings.json');

const VERSION = 1;
Expand Down Expand Up @@ -71,10 +70,9 @@ export class Settings {
await fs.mkdir(dirname(this.filePath), { recursive: true });
await fs.writeFile(this.filePath, JSON.stringify(this.value, null, 2));
} catch (err) {
throw new SavingGlobalSettingsFileError({
filePath: this.filePath,
error: err,
});
console.warn(dedent`
Unable to save global settings file to ${this.filePath}
${err && `Reason: ${(err as Error).message ?? err}`}`);
}
}
}
30 changes: 26 additions & 4 deletions code/core/src/common/utils/envs.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Needed for Angular sandbox running without --no-link option. Do NOT convert to @ts-expect-error!
import { getEnvironment } from 'lazy-universal-dotenv';

import { nodePathsToArray } from './paths';

// Load environment variables starts with STORYBOOK_ to the client side.

export function loadEnvs(options: { production?: boolean } = {}): {
export async function loadEnvs(options: { production?: boolean } = {}): Promise<{
stringified: Record<string, string>;
raw: Record<string, string>;
} {
}> {
const { getEnvironment } = await import('lazy-universal-dotenv');
const defaultNodeEnv = options.production ? 'production' : 'development';

const env: Record<string, string | undefined> = {
Expand Down Expand Up @@ -67,3 +66,26 @@ export const stringifyProcessEnvs = (raw: Record<string, string>): Record<string
// envs['process.env'] = JSON.stringify(raw);
return envs;
};

export const optionalEnvToBoolean = (input: string | undefined): boolean | undefined => {
if (input === undefined) {
return undefined;
}
if (input.toUpperCase() === 'FALSE' || input === '0') {
return false;
}
if (input.toUpperCase() === 'TRUE' || input === '1') {
return true;
}
return Boolean(input);
};

/**
* Consistently determine if we are in a CI environment
*
* Doing Boolean(process.env.CI) or !process.env.CI is not enough, because users might set CI=false
* or CI=0, which would be truthy, and thus return true in those cases.
*/
export function isCI(): boolean | undefined {
return optionalEnvToBoolean(process.env.CI);
}
23 changes: 23 additions & 0 deletions code/core/src/core-server/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import compression from '@polka/compression';
import polka from 'polka';
import invariant from 'tiny-invariant';

import { telemetry } from '../telemetry';
import type { StoryIndexGenerator } from './utils/StoryIndexGenerator';
import { doTelemetry } from './utils/doTelemetry';
import { getManagerBuilder, getPreviewBuilder } from './utils/get-builders';
Expand All @@ -19,6 +20,7 @@ import { openInBrowser } from './utils/open-in-browser';
import { getServerAddresses } from './utils/server-address';
import { getServer } from './utils/server-init';
import { useStatics } from './utils/server-statics';
import { summarizeIndex } from './utils/summarizeIndex';

export async function storybookDevServer(options: Options) {
const [server, core] = await Promise.all([getServer(options), options.presets.apply('core')]);
Expand Down Expand Up @@ -130,5 +132,26 @@ export async function storybookDevServer(options: Options) {
// Now the preview has successfully started, we can count this as a 'dev' event.
doTelemetry(app, core, initializedStoryIndexGenerator, options);

async function cancelTelemetry() {
const payload = { eventType: 'dev' };
try {
const generator = await initializedStoryIndexGenerator;
const indexAndStats = await generator?.getIndexAndStats();
// compute stats so we can get more accurate story counts
if (indexAndStats) {
Object.assign(payload, {
storyIndex: summarizeIndex(indexAndStats.storyIndex),
storyStats: indexAndStats.stats,
});
}
} catch (err) {}
await telemetry('canceled', payload, { immediate: true });
process.exit(0);
}

if (!core?.disableTelemetry) {
process.on('SIGINT', cancelTelemetry);
}

return { previewResult, managerResult, address, networkAddress };
}
20 changes: 3 additions & 17 deletions code/core/src/core-server/presets/common-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises';
import { dirname, isAbsolute, join } from 'node:path';

import type { Channel } from 'storybook/internal/channels';
import { optionalEnvToBoolean } from 'storybook/internal/common';
import {
JsPackageManagerFactory,
type RemoveAddonOptions,
Expand Down Expand Up @@ -143,7 +144,8 @@ export const previewHead = async (base: any, { configDir, presets }: Options) =>
};

export const env = async () => {
return loadEnvs({ production: true }).raw;
const { raw } = await loadEnvs({ production: true });
return raw;
};

export const previewBody = async (base: any, { configDir, presets }: Options) => {
Expand All @@ -164,22 +166,6 @@ export const typescript = () => ({
},
});

const optionalEnvToBoolean = (input: string | undefined): boolean | undefined => {
if (input === undefined) {
return undefined;
}
if (input.toUpperCase() === 'FALSE') {
return false;
}
if (input.toUpperCase() === 'TRUE') {
return true;
}
if (typeof input === 'string') {
return true;
}
return undefined;
};

/** This API is used by third-parties to access certain APIs in a Node environment */
export const experimental_serverAPI = (extension: Record<string, Function>, options: Options) => {
let removeAddon = removeAddonBase;
Expand Down
3 changes: 2 additions & 1 deletion code/core/src/core-server/stores/status.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { optionalEnvToBoolean } from '../../common/utils/envs';
import { createStatusStore } from '../../shared/status-store';
import { UNIVERSAL_STATUS_STORE_OPTIONS } from '../../shared/status-store';
import { UniversalStore } from '../../shared/universal-store';
Expand All @@ -12,7 +13,7 @@ const statusStore = createStatusStore({
before it was ready.
This will be fixed when we do the planned UniversalStore v0.2.
*/
leader: process.env.VITEST_CHILD_PROCESS !== 'true',
leader: !optionalEnvToBoolean(process.env.VITEST_CHILD_PROCESS),
}),
environment: 'server',
});
Expand Down
3 changes: 2 additions & 1 deletion code/core/src/core-server/stores/test-provider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { optionalEnvToBoolean } from '../../common/utils/envs';
import { createTestProviderStore } from '../../shared/test-provider-store';
import { UNIVERSAL_TEST_PROVIDER_STORE_OPTIONS } from '../../shared/test-provider-store';
import { UniversalStore } from '../../shared/universal-store';
Expand All @@ -12,7 +13,7 @@ const testProviderStore = createTestProviderStore({
before it was ready.
This will be fixed when we do the planned UniversalStore v0.2.
*/
leader: process.env.VITEST_CHILD_PROCESS !== 'true',
leader: !optionalEnvToBoolean(process.env.VITEST_CHILD_PROCESS),
}),
});

Expand Down
1 change: 1 addition & 0 deletions code/core/src/core-server/utils/server-address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getServerAddresses, getServerChannelUrl, getServerPort } from './server

vi.mock('node:os', () => ({
default: { release: () => '' },
platform: 'darwin',
constants: {
signals: {},
},
Expand Down
4 changes: 2 additions & 2 deletions code/core/src/core-server/withTelemetry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HandledError, cache, loadAllPresets } from 'storybook/internal/common';
import { HandledError, cache, isCI, loadAllPresets } from 'storybook/internal/common';
import { logger } from 'storybook/internal/node-logger';
import { getPrecedingUpgrade, oneWayHash, telemetry } from 'storybook/internal/telemetry';
import type { EventType } from 'storybook/internal/telemetry';
Expand All @@ -14,7 +14,7 @@ type TelemetryOptions = {
};

const promptCrashReports = async () => {
if (process.env.CI || !process.stdout.isTTY) {
if (isCI() || !process.stdout.isTTY) {
return undefined;
}

Expand Down
Loading
Loading