Skip to content
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

CLI: Run storybook dev as part of storybook init #22928

Merged
merged 12 commits into from
Jun 13, 2023
3 changes: 3 additions & 0 deletions code/frameworks/angular/src/builders/start-storybook/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export type StorybookBuilderOptions = JsonObject & {
| 'ci'
| 'quiet'
| 'disableTelemetry'
| 'initialPath'
| 'open'
| 'docs'
>;
Expand Down Expand Up @@ -98,6 +99,7 @@ const commandBuilder: BuilderHandlerFn<StorybookBuilderOptions> = (options, cont
sslKey,
disableTelemetry,
assets,
initialPath,
open,
} = options;

Expand All @@ -123,6 +125,7 @@ const commandBuilder: BuilderHandlerFn<StorybookBuilderOptions> = (options, cont
...(assets ? { assets } : {}),
},
tsConfig,
initialPath,
open,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"initialPath": {
"type": "string",
"description": "URL path to be appended when visiting Storybook for the first time"
}
},
"additionalProperties": false,
Expand Down
16 changes: 12 additions & 4 deletions code/lib/cli/src/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { sync as readUpSync } from 'read-pkg-up';
import { logger, instance as npmLog } from '@storybook/node-logger';
import { buildDevStandalone, withTelemetry } from '@storybook/core-server';
import { cache } from '@storybook/core-common';
import type { CLIOptions } from '@storybook/types';

function printError(error: any) {
// this is a weird bugfix, somehow 'node-pre-gyp' is polluting the npmLog header
Expand Down Expand Up @@ -35,7 +36,7 @@ function printError(error: any) {
logger.line();
}

export const dev = async (cliOptions: any) => {
export const dev = async (cliOptions: CLIOptions) => {
process.env.NODE_ENV = process.env.NODE_ENV || 'development';

const options = {
Expand All @@ -45,8 +46,15 @@ export const dev = async (cliOptions: any) => {
ignorePreview: !!cliOptions.previewUrl && !cliOptions.forceBuildPreview,
cache,
packageJson: readUpSync({ cwd: __dirname }).packageJson,
};
await withTelemetry('dev', { cliOptions, presetOptions: options, printError }, () =>
buildDevStandalone(options)
} as Parameters<typeof buildDevStandalone>[0];

await withTelemetry(
'dev',
{
cliOptions,
presetOptions: options as Parameters<typeof withTelemetry>[1]['presetOptions'],
printError,
},
() => buildDevStandalone(options)
);
};
4 changes: 4 additions & 0 deletions code/lib/cli/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ command('dev')
)
.option('--force-build-preview', 'Build the preview iframe even if you are using --preview-url')
.option('--docs', 'Build a documentation-only site using addon-docs')
.option(
'--initial-path [path]',
yannbf marked this conversation as resolved.
Show resolved Hide resolved
'URL path to be appended when visiting Storybook for the first time'
)
.action(async (options) => {
logger.setLevel(program.loglevel);
consoleLogger.log(chalk.bold(`${pkg.name} v${pkg.version}`) + chalk.reset('\n'));
Expand Down
72 changes: 59 additions & 13 deletions code/lib/cli/src/initiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { telemetry } from '@storybook/telemetry';
import { withTelemetry } from '@storybook/core-server';

import dedent from 'ts-dedent';
import boxen from 'boxen';
import { installableProjectTypes, ProjectType } from './project_types';
import {
detect,
Expand Down Expand Up @@ -37,6 +38,7 @@ import { JsPackageManagerFactory, useNpmWarning } from './js-package-manager';
import type { NpmOptions } from './NpmOptions';
import type { CommandOptions } from './generators/types';
import { HandledError } from './HandledError';
import { dev } from './dev';

const logger = console;

Expand Down Expand Up @@ -256,7 +258,7 @@ async function doInitiate(options: CommandOptions, pkg: PackageJson): Promise<vo
updateCheckInterval: 1000 * 60 * 60, // every hour (we could increase this later on.)
});

let projectType;
let projectType: ProjectType;
const projectTypeProvided = options.type;
const infoText = projectTypeProvided
? `Installing Storybook for user specified project type: ${projectTypeProvided}`
Expand All @@ -267,7 +269,7 @@ async function doInitiate(options: CommandOptions, pkg: PackageJson): Promise<vo

if (projectTypeProvided) {
if (installableProjectTypes.includes(projectTypeProvided)) {
projectType = projectTypeProvided.toUpperCase();
projectType = projectTypeProvided.toUpperCase() as ProjectType;
} else {
done(`The provided project type was not recognized by Storybook: ${projectTypeProvided}`);
logger.log(`\nThe project types currently supported by Storybook are:\n`);
Expand Down Expand Up @@ -317,12 +319,7 @@ async function doInitiate(options: CommandOptions, pkg: PackageJson): Promise<vo
telemetry('init', { projectType });
}

logger.log('\nFor more information visit:', chalk.cyan('https://storybook.js.org'));

if (projectType === ProjectType.ANGULAR) {
logger.log('\nTo run your Storybook, type:\n');
codeLog([`ng run ${installResult.projectName}:storybook`]);
} else if (projectType === ProjectType.REACT_NATIVE) {
if (projectType === ProjectType.REACT_NATIVE) {
logger.log();
logger.log(chalk.yellow('NOTE: installation is not 100% automated.\n'));
logger.log(`To quickly run Storybook, replace contents of your app entry with:\n`);
Expand All @@ -333,12 +330,61 @@ async function doInitiate(options: CommandOptions, pkg: PackageJson): Promise<vo
logger.log(chalk.cyan('https://github.com/storybookjs/react-native'));
logger.log();
} else {
logger.log('\nTo run your Storybook, type:\n');
codeLog([packageManager.getRunStorybookCommand()]);
const storybookCommand =
projectType === ProjectType.ANGULAR
? `ng run ${installResult.projectName}:storybook`
: packageManager.getRunStorybookCommand();
logger.log(
boxen(
dedent`
Storybook was successfully installed in your project! 🎉
To run Storybook manually, run ${chalk.yellow(
chalk.bold(storybookCommand)
)}. CTRL+C to stop.

Wanna know more about Storybook? Check out ${chalk.cyan('https://storybook.js.org/')}
Having trouble or want to chat? Join us at ${chalk.cyan('https://discord.gg/storybook/')}
`,
{ borderStyle: 'round', padding: 1, borderColor: '#F1618C' }
)
);

const shouldRunDev = process.env.CI !== 'true' && process.env.IN_STORYBOOK_SANDBOX !== 'true';
if (shouldRunDev) {
logger.log('\nRunning Storybook');

switch (projectType) {
case ProjectType.ANGULAR: {
try {
// for angular specifically, we have to run the `ng` command, and to stream the output
// it has to be a sync command.
packageManager.runPackageCommandSync(
`ng run ${installResult.projectName}:storybook`,
['--quiet'],
undefined,
'inherit'
);
} catch (e) {
if (e.message.includes('Command failed with exit code 129')) {
// catch ctrl + c error
} else {
throw e;
}
}
break;
}

default: {
await dev({
...options,
port: 6006,
open: true,
quiet: true,
});
}
}
}
}

// Add a new line for the clear visibility.
logger.log();
}

export async function initiate(options: CommandOptions, pkg: PackageJson): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions code/lib/core-server/src/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export async function storybookDevServer(options: Options) {

app.use(router);

const { port, host } = options;
const { port, host, initialPath } = options;
const proto = options.https ? 'https' : 'http';
const { address, networkAddress } = getServerAddresses(port, host, proto);
const { address, networkAddress } = getServerAddresses(port, host, proto, initialPath);

const listening = new Promise<void>((resolve, reject) => {
// @ts-expect-error (Following line doesn't match TypeScript signature at all 🤔)
Expand Down
81 changes: 81 additions & 0 deletions code/lib/core-server/src/utils/server-address.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import detectPort from 'detect-port';
import { getServerAddresses, getServerPort, getServerChannelUrl } from './server-address';

jest.mock('ip');
jest.mock('detect-port');
jest.mock('@storybook/node-logger');

describe('getServerAddresses', () => {
const port = 3000;
const host = 'localhost';
const proto = 'http';

it('should return server addresses without initial path by default', () => {
const expectedAddress = `${proto}://localhost:${port}/`;
const expectedNetworkAddress = `${proto}://${host}:${port}/`;

const result = getServerAddresses(port, host, proto);

expect(result.address).toBe(expectedAddress);
expect(result.networkAddress).toBe(expectedNetworkAddress);
});

it('should return server addresses with initial path', () => {
const initialPath = '/foo/bar';

const expectedAddress = `${proto}://localhost:${port}/?path=/foo/bar`;
const expectedNetworkAddress = `${proto}://${host}:${port}/?path=/foo/bar`;

const result = getServerAddresses(port, host, proto, initialPath);

expect(result.address).toBe(expectedAddress);
expect(result.networkAddress).toBe(expectedNetworkAddress);
});

it('should return server addresses with initial path and add slash if missing', () => {
const initialPath = 'foo/bar';

const expectedAddress = `${proto}://localhost:${port}/?path=/foo/bar`;
const expectedNetworkAddress = `${proto}://${host}:${port}/?path=/foo/bar`;

const result = getServerAddresses(port, host, proto, initialPath);

expect(result.address).toBe(expectedAddress);
expect(result.networkAddress).toBe(expectedNetworkAddress);
});
});

describe('getServerPort', () => {
const port = 3000;

it('should resolve with a free port', async () => {
const expectedFreePort = 4000;

(detectPort as jest.Mock).mockResolvedValue(expectedFreePort);

const result = await getServerPort(port);

expect(result).toBe(expectedFreePort);
});
});

describe('getServerChannelUrl', () => {
const port = 3000;
it('should return WebSocket URL with HTTP', () => {
const options = { https: false };
const expectedUrl = `ws://localhost:${port}/storybook-server-channel`;

const result = getServerChannelUrl(port, options);

expect(result).toBe(expectedUrl);
});

it('should return WebSocket URL with HTTPS', () => {
const options = { https: true };
const expectedUrl = `wss://localhost:${port}/storybook-server-channel`;

const result = getServerChannelUrl(port, options);

expect(result).toBe(expectedUrl);
});
});
22 changes: 19 additions & 3 deletions code/lib/core-server/src/utils/server-address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,26 @@ import ip from 'ip';
import { logger } from '@storybook/node-logger';
import detectFreePort from 'detect-port';

export function getServerAddresses(port: number, host: string, proto: string) {
export function getServerAddresses(
port: number,
host: string,
proto: string,
initialPath?: string
) {
const address = new URL(`${proto}://localhost:${port}/`);
const networkAddress = new URL(`${proto}://${host || ip.address()}:${port}/`);

if (initialPath) {
const searchParams = `?path=${decodeURIComponent(
initialPath.startsWith('/') ? initialPath : `/${initialPath}`
)}`;
address.search = searchParams;
networkAddress.search = searchParams;
}

return {
address: `${proto}://localhost:${port}/`,
networkAddress: `${proto}://${host || ip.address()}:${port}/`,
address: address.href,
networkAddress: networkAddress.href,
};
}

Expand Down
1 change: 1 addition & 0 deletions code/lib/types/src/modules/core-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export interface CLIOptions {
disableTelemetry?: boolean;
enableCrashReports?: boolean;
host?: string;
initialPath?: string;
/**
* @deprecated Use 'staticDirs' Storybook Configuration option instead
*/
Expand Down
3 changes: 3 additions & 0 deletions scripts/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@ async function runTask(task: Task, details: TemplateDetails, optionValues: Passe
const controllers: AbortController[] = [];

async function run() {
// useful for other scripts to know whether they're running in the creation of a sandbox in the monorepo
process.env.IN_STORYBOOK_SANDBOX = 'true';

const allOptionValues = await getOptionsOrPrompt('yarn task', options);

const { task: taskKey, startFrom, junit, ...optionValues } = allOptionValues;
Expand Down
12 changes: 7 additions & 5 deletions scripts/tasks/sandbox-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '../utils/yarn';
import { exec } from '../utils/exec';
import type { ConfigFile } from '../../code/lib/csf-tools';
import storybookPackages from '../../code/lib/cli/src/versions';
import { writeConfig } from '../../code/lib/csf-tools';
import { filterExistsInCodeDir } from '../utils/filterExistsInCodeDir';
import { findFirstPath } from '../utils/paths';
Expand Down Expand Up @@ -430,8 +431,6 @@ export const addStories: Task['run'] = async (
});
}

console.log({ sandboxSpecificStoriesFolder, storiesVariantFolder });

if (
await pathExists(
resolve(CODE_DIRECTORY, frameworkPath, join('template', storiesVariantFolder))
Expand Down Expand Up @@ -473,9 +472,12 @@ export const addStories: Task['run'] = async (
);

const addonDirs = await Promise.all(
[...mainAddons, ...extraAddons].map(async (addon) =>
workspacePath('addon', `@storybook/addon-${addon}`)
)
[...mainAddons, ...extraAddons]
// only include addons that are in the monorepo
.filter((addon: string) =>
Object.keys(storybookPackages).find((pkg: string) => pkg === `@storybook/addon-${addon}`)
)
.map(async (addon) => workspacePath('addon', `@storybook/addon-${addon}`))
);

if (isCoreRenderer) {
Expand Down