storybook/lib/core/src/server/build-dev.js
Norbert de Langen ac0a42bec1 FIX linting
2019-07-07 00:42:18 +02:00

373 lines
9.6 KiB
JavaScript

import express from 'express';
import https from 'https';
import http from 'http';
import ip from 'ip';
import favicon from 'serve-favicon';
import path from 'path';
import fs from 'fs-extra';
import chalk from 'chalk';
import { logger, colors, instance as npmLog } from '@storybook/node-logger';
import fetch from 'node-fetch';
import Cache from 'file-system-cache';
import findCacheDir from 'find-cache-dir';
import open from 'open';
import boxen from 'boxen';
import semver from 'semver';
import { stripIndents } from 'common-tags';
import Table from 'cli-table3';
import prettyTime from 'pretty-hrtime';
import inquirer from 'inquirer';
import detectFreePort from 'detect-port';
import storybook from './dev-server';
import { getDevCli } from './cli';
const defaultFavIcon = require.resolve('./public/favicon.ico');
const cacheDir = findCacheDir({ name: 'storybook' });
const cache = Cache({
basePath: cacheDir,
ns: 'storybook', // Optional. A grouping namespace for items.
});
const writeStats = async (name, stats) => {
await fs.writeFile(
path.join(cacheDir, `${name}-stats.json`),
JSON.stringify(stats.toJson(), null, 2),
'utf8'
);
};
const getFreePort = port =>
detectFreePort(port).catch(error => {
logger.error(error);
process.exit(-1);
});
async function getServer(app, options) {
if (!options.https) {
return http.createServer(app);
}
if (!options.sslCert) {
logger.error('Error: --ssl-cert is required with --https');
process.exit(-1);
}
if (!options.sslKey) {
logger.error('Error: --ssl-key is required with --https');
process.exit(-1);
}
const sslOptions = {
ca: await Promise.all((options.sslCa || []).map(ca => fs.readFile(ca, 'utf-8'))),
cert: await fs.readFile(options.sslCert, 'utf-8'),
key: await fs.readFile(options.sslKey, 'utf-8'),
};
return https.createServer(sslOptions, app);
}
async function applyStatic(app, options) {
const { staticDir } = options;
let hasCustomFavicon = false;
if (staticDir && staticDir.length) {
await Promise.all(
staticDir.map(async dir => {
const staticPath = path.resolve(dir);
if (await !fs.exists(staticPath)) {
logger.error(`Error: no such directory to load static files: ${staticPath}`);
process.exit(-1);
}
logger.info(`=> Loading static files from: ${staticPath} .`);
app.use(express.static(staticPath, { index: false }));
const faviconPath = path.resolve(staticPath, 'favicon.ico');
if (await fs.exists(faviconPath)) {
hasCustomFavicon = true;
app.use(favicon(faviconPath));
}
})
);
}
if (!hasCustomFavicon) {
app.use(favicon(defaultFavIcon));
}
}
const updateCheck = async version => {
let result;
const time = Date.now();
try {
const fromCache = await cache.get('lastUpdateCheck', { success: false, time: 0 });
// if last check was more then 24h ago
if (time - 86400000 > fromCache.time) {
const fromFetch = await Promise.race([
fetch(`https://storybook.js.org/versions.json?current=${version}`),
// if fetch is too slow, we won't wait for it
new Promise((res, rej) => global.setTimeout(rej, 1500)),
]);
const data = await fromFetch.json();
result = { success: true, data, time };
await cache.set('lastUpdateCheck', result);
} else {
result = fromCache;
}
} catch (error) {
result = { success: false, error, time };
}
return result;
};
function listenToServer(server, listenAddr) {
let serverResolve = () => {};
let serverReject = () => {};
const serverListening = new Promise((resolve, reject) => {
serverResolve = resolve;
serverReject = reject;
});
server.listen(...listenAddr, error => {
if (error) {
serverReject(error);
} else {
serverResolve();
}
});
return serverListening;
}
function createUpdateMessage(updateInfo, version) {
let updateMessage;
try {
updateMessage =
updateInfo.success && semver.lt(version, updateInfo.data.latest.version)
? stripIndents`
${colors.orange(
`A new version (${chalk.bold(updateInfo.data.latest.version)}) is available!`
)}
${chalk.gray('Read full changelog here:')} ${chalk.gray.underline('https://git.io/fhFYe')}
`
: '';
} catch (e) {
updateMessage = '';
}
return updateMessage;
}
function outputStartupInformation(options) {
const {
updateInfo,
version,
address,
networkAddress,
managerTotalTime,
previewTotalTime,
} = options;
const updateMessage = createUpdateMessage(updateInfo, version);
const serveMessage = new Table({
chars: {
top: '',
'top-mid': '',
'top-left': '',
'top-right': '',
bottom: '',
'bottom-mid': '',
'bottom-left': '',
'bottom-right': '',
left: '',
'left-mid': '',
mid: '',
'mid-mid': '',
right: '',
'right-mid': '',
middle: '',
},
paddingLeft: 0,
paddingRight: 0,
paddingTop: 0,
paddingBottom: 0,
});
serveMessage.push(
['Local:', chalk.cyan(address)],
['On your network:', chalk.cyan(networkAddress)]
);
const timeStatement = previewTotalTime
? `${chalk.underline(prettyTime(managerTotalTime))} for manager and ${chalk.underline(
prettyTime(previewTotalTime)
)} for preview`
: `${chalk.underline(prettyTime(managerTotalTime))}`;
// eslint-disable-next-line no-console
console.log(
boxen(
stripIndents`
${colors.green(`Storybook ${chalk.bold(version)} started`)}
${chalk.gray(timeStatement)}
${serveMessage.toString()}${updateMessage ? `\n\n${updateMessage}` : ''}
`,
{ borderStyle: 'round', padding: 1, borderColor: '#F1618C' }
)
);
}
async function outputStats(previewStats, managerStats) {
if (previewStats) {
await writeStats('preview', previewStats);
}
await writeStats('manager', managerStats);
logger.info(`stats written to => ${chalk.cyan(path.join(cacheDir, '[name].json'))}`);
}
function openInBrowser(address) {
open(address).catch(() => {
logger.error(stripIndents`
Could not open ${address} inside a browser. If you're running this command inside a
docker container or on a CI, you need to pass the '--ci' flag to prevent opening a
browser by default.
`);
});
}
export async function buildDevStandalone(options) {
try {
const { host, extendServer } = options;
const port = await getFreePort(options.port);
if (!options.ci && !options.smokeTest && options.port != null && port !== options.port) {
const { shouldChangePort } = await inquirer.prompt({
type: 'confirm',
default: 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);
}
}
// Used with `app.listen` below
const listenAddr = [port];
if (host) {
listenAddr.push(host);
}
const app = express();
const server = await getServer(app, options);
if (typeof extendServer === 'function') {
extendServer(server);
}
await applyStatic(app, options);
const {
router: storybookMiddleware,
previewStats,
managerStats,
managerTotalTime,
previewTotalTime,
} = await storybook(options);
app.use(storybookMiddleware);
const serverListening = listenToServer(server, listenAddr);
const { version } = options.packageJson;
const [updateInfo] = await Promise.all([updateCheck(version), serverListening]);
const proto = options.https ? 'https' : 'http';
const address = `${proto}://${options.host || 'localhost'}:${port}/`;
const networkAddress = `${proto}://${ip.address()}:${port}/`;
outputStartupInformation({
updateInfo,
version,
address,
networkAddress,
managerTotalTime,
previewTotalTime,
});
if (options.smokeTest) {
await outputStats(previewStats, managerStats);
let warning = 0;
if (!options.ignorePreview) {
warning += previewStats.toJson().warnings.length;
}
warning += managerStats.toJson().warnings.length;
process.exit(warning ? 1 : 0);
} else if (!options.ci) {
openInBrowser(address);
}
} catch (error) {
// this is a weird bugfix, somehow 'node-pre-gyp' is poluting the npmLog header
npmLog.heading = '';
logger.line();
logger.warn(
error.close
? stripIndents`
FATAL broken build!, will close the process,
Fix the error below and restart storybook.
`
: stripIndents`
Broken build, fix the error below.
You may need to refresh the browser.
`
);
logger.line();
if (error instanceof Error) {
if (error.error) {
logger.error(error.error);
} else if (error.stats && error.stats.compilation.errors) {
error.stats.compilation.errors.forEach(e => logger.plain(e));
} else {
logger.error(error);
}
if (error.close) {
process.exit(1);
}
}
if (options.smokeTest) {
process.exit(1);
}
}
}
export async function buildDev({ packageJson, ...loadOptions }) {
const cliOptions = await getDevCli(packageJson);
await buildDevStandalone({
...cliOptions,
...loadOptions,
packageJson,
configDir: loadOptions.configDir || cliOptions.configDir || './.storybook',
ignorePreview: !!cliOptions.previewUrl,
docsMode: !!cliOptions.docs,
});
}