Skip to content

CLI: Fix throwing in readonly environments #31785

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

Open
wants to merge 2 commits into
base: next
Choose a base branch
from
Open
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
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
4 changes: 2 additions & 2 deletions code/addons/vitest/src/postinstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import {
extractProperFrameworkName,
formatFileContent,
getProjectRoot,
isCI,
loadAllPresets,
loadMainConfig,
scanAndTransformFiles,
serverResolve,
transformImportFiles,
validateFrameworkName,
versions,
} from 'storybook/internal/common';
import { readConfig, writeConfig } from 'storybook/internal/csf-tools';
import { colors, logger } from 'storybook/internal/node-logger';
Expand Down Expand Up @@ -71,7 +71,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 @@ -210,10 +215,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 @@ -258,7 +259,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 @@ -286,9 +287,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 @@ -370,10 +369,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 @@ -407,7 +406,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 @@ -431,7 +430,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}`}`);
}
}
}
21 changes: 21 additions & 0 deletions code/core/src/common/utils/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,24 @@ 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;
}
Comment on lines +78 to +80
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant right.. do we think it's better for brevity? Should this function be unit tested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm you mean that the strings 'TRUE' and '1' are truthy anyways, so Boolean(input) would return true anyway? I hadn't thought of that. I should remove it, that should also make it more obvious that 'blah' also makes it true.

I can add tests.

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 const isCI = optionalEnvToBoolean(process.env.CI);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it OK for this file to read process.env? It's never used in a scenario where one of those might be undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No the storybook/internal/common entrypoint is only usable in Node, not in the browser.

17 changes: 1 addition & 16 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 @@ -171,22 +172,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
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) {
if (isCI) {
return undefined;
}

Expand Down
4 changes: 3 additions & 1 deletion code/core/src/node-logger/logger/log-tracker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { promises as fs } from 'node:fs';
import path, { join } from 'node:path';

import { isCI } from 'storybook/internal/common';

import { cleanLog } from '../../../../lib/cli-storybook/src/automigrate/helpers/cleanLog';
import type { LogLevel } from './logger';

Expand Down Expand Up @@ -97,7 +99,7 @@ class LogTracker {
await fs.writeFile(filePath, logContent, 'utf-8');
this.#logs = [];

return process.env.CI ? filePath : path.relative(process.cwd(), filePath);
return isCI ? filePath : path.relative(process.cwd(), filePath);
}
}

Expand Down
5 changes: 4 additions & 1 deletion code/core/src/node-logger/prompts/prompt-config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { optionalEnvToBoolean } from '../../common/utils/envs';
import type { PromptProvider } from './prompt-provider-base';
import { ClackPromptProvider } from './prompt-provider-clack';
import { PromptsPromptProvider } from './prompt-provider-prompts';
Expand All @@ -9,7 +10,9 @@ const PROVIDERS = {
prompts: new PromptsPromptProvider(),
} as const;

let currentPromptLibrary: PromptLibrary = process.env.USE_CLACK === 'true' ? 'clack' : 'prompts';
let currentPromptLibrary: PromptLibrary = optionalEnvToBoolean(process.env.USE_CLACK)
? 'clack'
: 'prompts';

export const setPromptLibrary = (library: PromptLibrary): void => {
currentPromptLibrary = library;
Expand Down
12 changes: 0 additions & 12 deletions code/core/src/server-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,15 +566,3 @@ export class IncompatiblePostCssConfigError extends StorybookError {
});
}
}

export class SavingGlobalSettingsFileError extends StorybookError {
constructor(public data: { filePath: string; error: Error | unknown }) {
super({
category: Category.CORE_SERVER,
code: 1,
message: dedent`
Unable to save global settings file to ${data.filePath}
${data.error && `Reason: ${data.error}`}`,
});
}
}
5 changes: 3 additions & 2 deletions code/core/src/telemetry/storybook-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getProjectRoot,
getStorybookConfiguration,
getStorybookInfo,
isCI,
loadMainConfig,
versions,
} from 'storybook/internal/common';
Expand Down Expand Up @@ -57,10 +58,10 @@ export const computeStorybookMetadata = async ({
mainConfig?: StorybookConfig & Record<string, any>;
configDir: string;
}): Promise<StorybookMetadata> => {
const settings = await globalSettings();
const settings = isCI ? undefined : await globalSettings();
const metadata: Partial<StorybookMetadata> = {
generatedAt: new Date().getTime(),
userSince: settings.value.userSince,
userSince: settings?.value.userSince,
hasCustomBabel: false,
hasCustomWebpack: false,
hasStaticDirs: false,
Expand Down
4 changes: 3 additions & 1 deletion code/core/src/telemetry/telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/// <reference types="node" />
import * as os from 'node:os';

import { isCI } from 'storybook/internal/common';

import retry from 'fetch-retry';
import { nanoid } from 'nanoid';

Expand Down Expand Up @@ -45,7 +47,7 @@ const getOperatingSystem = (): 'Windows' | 'macOS' | 'Linux' | `Other: ${string}
// by the app. currently:
// - cliVersion
const globalContext = {
inCI: Boolean(process.env.CI),
inCI: isCI,
isTTY: process.stdout.isTTY,
platform: getOperatingSystem(),
nodeVersion: process.versions.node,
Expand Down
Loading
Loading