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 @@ -50,6 +50,7 @@ export type StorybookBuilderOptions = JsonObject & {
| 'ci'
| 'quiet'
| 'disableTelemetry'
| 'initialPath'
>;

export type StorybookBuilderOutput = JsonObject & BuilderOutput & {};
Expand Down Expand Up @@ -93,6 +94,7 @@ const commandBuilder: BuilderHandlerFn<StorybookBuilderOptions> = (options, cont
sslKey,
disableTelemetry,
assets,
initialPath,
} = options;

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

return standaloneOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,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
59 changes: 51 additions & 8 deletions code/lib/cli/src/initiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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 +257,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 +268,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 @@ -319,9 +320,53 @@ async function doInitiate(options: CommandOptions, pkg: PackageJson): Promise<vo

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`]);
const shouldRunDev =
projectType !== ProjectType.REACT_NATIVE &&
process.env.CI !== 'true' &&
process.env.IN_STORYBOOK_SANDBOX !== 'true';
if (shouldRunDev) {
logger.log('\nRunning Storybook');
Copy link
Member Author

Choose a reason for hiding this comment

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

Running Storybook automatically. To run it manually, use the following command: xyz

Copy link
Member Author

Choose a reason for hiding this comment

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

add it in a box

Copy link
Member Author

Choose a reason for hiding this comment

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

@shilman @jonniebigodes here's how it looks like after the change:

For Vite:
image

For Angular Webpack (the most verbose we have):
image

Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

Looks great! I'm sure we can clean all this up if we rework the CLI, but for now I'd say ship it!


process.on('SIGINT', () => {
logger.log('\nTo run your Storybook again, type:\n');
const storybookCommand =
projectType === ProjectType.ANGULAR
? `ng run ${installResult.projectName}:storybook`
: packageManager.getRunStorybookCommand();
codeLog([storybookCommand]);
logger.log();
});

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,
});
}
}
} else if (projectType === ProjectType.REACT_NATIVE) {
logger.log();
logger.log(chalk.yellow('NOTE: installation is not 100% automated.\n'));
Expand All @@ -335,10 +380,8 @@ async function doInitiate(options: CommandOptions, pkg: PackageJson): Promise<vo
} else {
logger.log('\nTo run your Storybook, type:\n');
codeLog([packageManager.getRunStorybookCommand()]);
logger.log();
}

// 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