storybook/lib/core-server/src/build-dev.ts
2022-02-22 16:56:51 +08:00

179 lines
6.1 KiB
TypeScript

import { logger, instance as npmLog } from '@storybook/node-logger';
import {
CLIOptions,
LoadOptions,
BuilderOptions,
resolvePathInStorybookCache,
loadAllPresets,
Options,
cache,
StorybookConfig,
} from '@storybook/core-common';
import dedent from 'ts-dedent';
import prompts from 'prompts';
import global from 'global';
import path from 'path';
import { storybookDevServer } from './dev-server';
import { getDevCli } from './cli';
import { getReleaseNotesData, getReleaseNotesFailedState } from './utils/release-notes';
import { outputStats } from './utils/output-stats';
import { outputStartupInformation } from './utils/output-startup-information';
import { updateCheck } from './utils/update-check';
import { getServerPort, getServerChannelUrl } from './utils/server-address';
import { getPreviewBuilder } from './utils/get-preview-builder';
import { getManagerBuilder } from './utils/get-manager-builder';
export async function buildDevStandalone(options: CLIOptions & LoadOptions & BuilderOptions) {
const { packageJson, versionUpdates, releaseNotes } = options;
const { version, name = '' } = packageJson;
// updateInfo and releaseNotesData are cached, so this is typically pretty fast
const [port, versionCheck, releaseNotesData] = await Promise.all([
getServerPort(options.port),
versionUpdates
? updateCheck(version)
: Promise.resolve({ success: false, data: {}, time: Date.now() }),
releaseNotes
? getReleaseNotesData(version, cache)
: Promise.resolve(getReleaseNotesFailedState(version)),
]);
if (!options.ci && !options.smokeTest && options.port != null && port !== options.port) {
const { shouldChangePort } = await prompts({
type: 'confirm',
initial: true,
name: 'shouldChangePort',
message: `Port ${options.port} is not available. Would you like to run Storybook on port ${port} instead?`,
});
if (!shouldChangePort) process.exit(1);
}
/* eslint-disable no-param-reassign */
options.port = port;
options.versionCheck = versionCheck;
options.releaseNotesData = releaseNotesData;
options.configType = 'DEVELOPMENT';
options.configDir = path.resolve(options.configDir);
options.outputDir = options.smokeTest
? resolvePathInStorybookCache('public')
: path.resolve(options.outputDir || resolvePathInStorybookCache('public'));
options.serverChannelUrl = getServerChannelUrl(port, options);
/* eslint-enable no-param-reassign */
const previewBuilder = await getPreviewBuilder(options.configDir);
const managerBuilder = await getManagerBuilder(options.configDir);
const presets = loadAllPresets({
corePresets: [
require.resolve('./presets/common-preset'),
...managerBuilder.corePresets,
...previewBuilder.corePresets,
require.resolve('./presets/babel-cache-preset'),
],
overridePresets: previewBuilder.overridePresets,
...options,
});
const features = await presets.apply<StorybookConfig['features']>('features');
global.FEATURES = features;
const fullOptions: Options = {
...options,
presets,
features,
};
const { address, networkAddress, managerResult, previewResult } = await storybookDevServer(
fullOptions
);
const previewTotalTime = previewResult && previewResult.totalTime;
const managerTotalTime = managerResult && managerResult.totalTime;
const previewStats = previewResult && previewResult.stats;
const managerStats = managerResult && managerResult.stats;
if (options.webpackStatsJson) {
const target = options.webpackStatsJson === true ? options.outputDir : options.webpackStatsJson;
await outputStats(target, previewStats, managerStats);
}
if (options.smokeTest) {
// @ts-ignore
const managerWarnings = (managerStats && managerStats.toJson().warnings) || [];
if (managerWarnings.length > 0)
logger.warn(`manager: ${JSON.stringify(managerWarnings, null, 2)}`);
// I'm a little reticent to import webpack types in this file :shrug:
// @ts-ignore
const previewWarnings = (previewStats && previewStats.toJson().warnings) || [];
if (previewWarnings.length > 0)
logger.warn(`preview: ${JSON.stringify(previewWarnings, null, 2)}`);
process.exit(
managerWarnings.length > 0 || (previewWarnings.length > 0 && !options.ignorePreview) ? 1 : 0
);
return;
}
// Get package name and capitalize it e.g. @storybook/react -> React
const packageName = name.split('@storybook/').length > 1 ? name.split('@storybook/')[1] : name;
const frameworkName = packageName.charAt(0).toUpperCase() + packageName.slice(1);
outputStartupInformation({
updateInfo: versionCheck,
version,
name: frameworkName,
address,
networkAddress,
managerTotalTime,
previewTotalTime,
});
}
export async function buildDev(loadOptions: LoadOptions) {
const cliOptions = await getDevCli(loadOptions.packageJson);
try {
await buildDevStandalone({
...cliOptions,
...loadOptions,
configDir: loadOptions.configDir || cliOptions.configDir || './.storybook',
configType: 'DEVELOPMENT',
ignorePreview: !!cliOptions.previewUrl && !cliOptions.forceBuildPreview,
docsMode: !!cliOptions.docs,
cache,
});
} catch (error) {
// this is a weird bugfix, somehow 'node-pre-gyp' is polluting the npmLog header
npmLog.heading = '';
if (error instanceof Error) {
if ((error as any).error) {
logger.error((error as any).error);
} else if ((error as any).stats && (error as any).stats.compilation.errors) {
(error as any).stats.compilation.errors.forEach((e: any) => logger.plain(e));
} else {
logger.error(error as any);
}
} else if (error.compilation?.errors) {
error.compilation.errors.forEach((e: any) => logger.plain(e));
}
logger.line();
logger.warn(
error.close
? dedent`
FATAL broken build!, will close the process,
Fix the error below and restart storybook.
`
: dedent`
Broken build, fix the error above.
You may need to refresh the browser.
`
);
logger.line();
process.exit(1);
}
}