diff --git a/.eslintignore b/.eslintignore
index a2f20ebea5a3..43ef35b0efa4 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -7,6 +7,7 @@ docs/public
storybook-static
built-storybooks
lib/cli/test
+lib/core/prebuilt
lib/codemod/src/transforms/__testfixtures__
scripts/storage
*.bundle.js
diff --git a/.gitignore b/.gitignore
index 834e9ba0455b..3a78a59470fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@ integration/__image_snapshots__/__diff_output__
/examples/cra-kitchen-sink/src/__image_snapshots__/__diff_output__/
lib/*.jar
lib/**/dll
+lib/core/prebuilt
/false
/addons/docs/common/config-*
built-storybooks
diff --git a/app/react/src/server/framework-preset-react.ts b/app/react/src/server/framework-preset-react.ts
index a24e228da3dd..68da6e308361 100644
--- a/app/react/src/server/framework-preset-react.ts
+++ b/app/react/src/server/framework-preset-react.ts
@@ -59,7 +59,7 @@ export async function webpackFinal(config: Configuration, options: StorybookOpti
return config;
}
- logger.info('=> Using React fast refresh feature.');
+ logger.info('=> Using React fast refresh');
return {
...config,
plugins: [...(config.plugins || []), new ReactRefreshWebpackPlugin()],
diff --git a/examples/vue-cli/package.json b/examples/vue-cli/package.json
index 925713ab8f83..f3dca42aa73c 100644
--- a/examples/vue-cli/package.json
+++ b/examples/vue-cli/package.json
@@ -4,9 +4,9 @@
"private": true,
"scripts": {
"build": "vue-cli-service build",
- "build-storybook": "build-storybook -s public",
+ "build-storybook": "build-storybook",
"serve": "vue-cli-service serve",
- "storybook": "start-storybook -p 9009 -s public"
+ "storybook": "start-storybook -p 9009"
},
"dependencies": {
"core-js": "^3.6.4",
diff --git a/jest.config.js b/jest.config.js
index ca0a8d3cd3fb..cde713563907 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -49,6 +49,7 @@ module.exports = {
testPathIgnorePatterns: [
'/node_modules/',
'/dist/',
+ '/prebuilt/',
'addon-jest.test.js',
'/cli/test/',
'/examples/cra-kitchen-sink/src/*',
@@ -66,6 +67,7 @@ module.exports = {
'/node_modules/',
'/cli/test/',
'/dist/',
+ '/prebuilt/',
'/generators/',
'/dll/',
'/__mocks__ /',
diff --git a/lib/cli/src/generators/baseGenerator.ts b/lib/cli/src/generators/baseGenerator.ts
index cb3b9ce0fc25..fbe5ed7ef9cc 100644
--- a/lib/cli/src/generators/baseGenerator.ts
+++ b/lib/cli/src/generators/baseGenerator.ts
@@ -44,6 +44,7 @@ export async function baseGenerator(
};
// added to main.js
+ // make sure to update `canUsePrebuiltManager` in dev-server.js and build-manager-config/main.js when this list changes
const addons = ['@storybook/addon-links', '@storybook/addon-essentials'];
// added to package.json
const addonPackages = [...addons, '@storybook/addon-actions'];
diff --git a/lib/components/src/Loader/Loader.stories.tsx b/lib/components/src/Loader/Loader.stories.tsx
index 30da9c305fe7..a3f8db8a1cac 100644
--- a/lib/components/src/Loader/Loader.stories.tsx
+++ b/lib/components/src/Loader/Loader.stories.tsx
@@ -1,33 +1,44 @@
import React from 'react';
-import { storiesOf } from '@storybook/react';
-import { Loader } from './Loader';
+import { PureLoader as Loader } from './Loader';
-storiesOf('Basics/Loader', module)
- .addDecorator((storyFn) => (
-
(
+
+
-
- {storyFn()}
-
- ))
- .add('infinite state', () =>
)
- .add('size adjusted', () =>
);
+ />
+ {storyFn()}
+
+);
+
+export default {
+ title: 'Basics/Loader',
+};
+
+export const InfiniteState = () => ;
+InfiniteState.decorators = [withBackground];
+
+export const SizeAdjusted = () => ;
+SizeAdjusted.decorators = [withBackground];
+
+export const ProgressBar = () => (
+
+);
+
+export const ProgressError = () => ;
diff --git a/lib/components/src/Loader/Loader.tsx b/lib/components/src/Loader/Loader.tsx
index c822d5fc533f..912d5a3c448a 100644
--- a/lib/components/src/Loader/Loader.tsx
+++ b/lib/components/src/Loader/Loader.tsx
@@ -1,5 +1,7 @@
-import React, { FunctionComponent, ComponentProps } from 'react';
-import { styled } from '@storybook/theming';
+import { EventSource } from 'global';
+import React, { ComponentProps, FunctionComponent, useEffect, useState } from 'react';
+import { styled, keyframes } from '@storybook/theming';
+import { Icons } from '../icon/icon';
import { rotate360 } from '../shared/animation';
const LoaderWrapper = styled.div<{ size?: number }>(({ size = 32 }) => ({
@@ -25,6 +27,153 @@ const LoaderWrapper = styled.div<{ size?: number }>(({ size = 32 }) => ({
mixBlendMode: 'difference',
}));
-export const Loader: FunctionComponent> = (props) => (
-
-);
+const ProgressWrapper = styled.div({
+ position: 'absolute',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: '100%',
+ height: '100%',
+});
+
+const ProgressTrack = styled.div(({ theme }) => ({
+ position: 'relative',
+ width: '80%',
+ marginBottom: '0.75rem',
+ maxWidth: 300,
+ height: 5,
+ borderRadius: 5,
+ background: `${theme.color.secondary}33`,
+ overflow: 'hidden',
+ cursor: 'progress',
+}));
+
+const ProgressBar = styled.div(({ theme }) => ({
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ height: '100%',
+ background: theme.color.secondary,
+}));
+
+const ProgressMessage = styled.div(({ theme }) => ({
+ minHeight: '2em',
+ fontSize: `${theme.typography.size.s1}px`,
+ color: theme.barTextColor,
+}));
+
+const ErrorIcon = styled(Icons)(({ theme }) => ({
+ width: 20,
+ height: 20,
+ marginBottom: '0.5rem',
+ color: theme.color.mediumdark,
+}));
+
+const ellipsis = keyframes`
+ from { content: "..." }
+ 33% { content: "." }
+ 66% { content: ".." }
+ to { content: "..." }
+`;
+
+const Ellipsis = styled.span({
+ '&::after': {
+ content: "'...'",
+ animation: `${ellipsis} 1s linear infinite`,
+ animationDelay: '1s',
+ display: 'inline-block',
+ width: '1em',
+ height: 'auto',
+ },
+});
+
+interface Progress {
+ value: number;
+ message: string;
+ modules?: {
+ complete: number;
+ total: number;
+ };
+}
+
+interface LoaderProps {
+ progress?: Progress;
+ error?: Error;
+ size?: number;
+}
+
+export const PureLoader: FunctionComponent<
+ LoaderProps & ComponentProps
+> = ({ progress, error, size, ...props }) => {
+ if (error) {
+ return (
+
+
+ {error.message}
+
+ );
+ }
+
+ if (progress) {
+ const { value, modules } = progress;
+ let { message } = progress;
+ if (modules) message += ` ${modules.complete} / ${modules.total} modules`;
+ return (
+
+
+
+
+
+ {message}
+ {value < 1 && }
+
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export const Loader: FunctionComponent> = (props) => {
+ const [progress, setProgress] = useState(undefined);
+ const [error, setError] = useState(undefined);
+
+ useEffect(() => {
+ const eventSource = new EventSource('/progress');
+ let lastProgress: Progress;
+ eventSource.onmessage = (event: any) => {
+ try {
+ lastProgress = JSON.parse(event.data);
+ setProgress(lastProgress);
+ } catch (e) {
+ setError(e);
+ eventSource.close();
+ }
+ };
+ eventSource.onerror = () => {
+ if (lastProgress?.value !== 1) setError(new Error('Connection closed'));
+ eventSource.close();
+ };
+ return () => eventSource.close();
+ }, []);
+
+ return ;
+};
diff --git a/lib/core/package.json b/lib/core/package.json
index c7f856e21be0..4b48d47c6855 100644
--- a/lib/core/package.json
+++ b/lib/core/package.json
@@ -20,8 +20,8 @@
"files": [
"dist/**/*",
"dll/**/*",
+ "prebuilt/**/*",
"types/**/*",
- "README.md",
"*.js",
"*.d.ts",
"ts3.4/**/*"
@@ -83,6 +83,7 @@
"cli-table3": "0.6.0",
"commander": "^5.0.0",
"core-js": "^3.0.1",
+ "cpy": "^8.1.1",
"css-loader": "^3.5.3",
"detect-port": "^1.3.0",
"dotenv-webpack": "^1.7.0",
@@ -121,7 +122,9 @@
"shelljs": "^0.8.4",
"stable": "^0.1.8",
"style-loader": "^1.2.1",
+ "telejson": "^5.0.2",
"terser-webpack-plugin": "^3.0.0",
+ "trash": "^6.1.1",
"ts-dedent": "^2.0.0",
"unfetch": "^4.1.0",
"url-loader": "^4.0.0",
diff --git a/lib/core/src/server/build-dev.js b/lib/core/src/server/build-dev.js
index cd519a42d4ca..6d58f673c2ab 100644
--- a/lib/core/src/server/build-dev.js
+++ b/lib/core/src/server/build-dev.js
@@ -1,15 +1,8 @@
-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 open from 'better-opn';
import boxen from 'boxen';
import semver from '@storybook/semver';
import dedent from 'ts-dedent';
@@ -18,11 +11,10 @@ import prettyTime from 'pretty-hrtime';
import inquirer from 'inquirer';
import detectFreePort from 'detect-port';
-import storybook from './dev-server';
+import { storybookDevServer } from './dev-server';
import { getDevCli } from './cli';
import { resolvePathInStorybookCache } from './utils/resolve-path-in-sb-cache';
-const defaultFavIcon = require.resolve('./public/favicon.ico');
const cache = Cache({
basePath: resolvePathInStorybookCache('dev-server'),
ns: 'storybook', // Optional. A grouping namespace for items.
@@ -42,66 +34,6 @@ const getFreePort = (port) =>
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 [currentStaticDir, staticEndpoint] = dir.split(':').concat('/');
- const localStaticPath = path.resolve(currentStaticDir);
-
- if (await !fs.exists(localStaticPath)) {
- logger.error(`Error: no such directory to load static files: ${localStaticPath}`);
- process.exit(-1);
- }
-
- logger.info(
- `=> Loading static files from: ${localStaticPath} and serving at ${staticEndpoint} .`
- );
- app.use(staticEndpoint, express.static(localStaticPath, { index: false }));
-
- const faviconPath = path.resolve(localStaticPath, '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();
@@ -187,26 +119,6 @@ export const getReleaseNotesData = async (currentVersionToParse, fileSystemCache
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;
@@ -272,11 +184,12 @@ function outputStartupInformation(options) {
['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))}`;
+ const timeStatement = [
+ managerTotalTime && `${chalk.underline(prettyTime(managerTotalTime))} for manager`,
+ previewTotalTime && `${chalk.underline(prettyTime(previewTotalTime))} for preview`,
+ ]
+ .filter(Boolean)
+ .join(' and ');
// eslint-disable-next-line no-console
console.log(
@@ -302,24 +215,13 @@ async function outputStats(previewStats, managerStats) {
);
}
-function openInBrowser(address) {
- try {
- open(address);
- } catch (error) {
- logger.error(dedent`
- 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, packageJson, versionUpdates, releaseNotes } = options;
+ const { packageJson, versionUpdates, releaseNotes } = options;
const { version } = packageJson;
- const [port, versionCheck, releaseNotesData] = await Promise.all([
+ // updateInfo and releaseNotesData are cached, so this is typically pretty fast
+ const [port, updateInfo, releaseNotesData] = await Promise.all([
getFreePort(options.port),
versionUpdates
? updateCheck(version)
@@ -329,11 +231,6 @@ export async function buildDevStandalone(options) {
: Promise.resolve(getReleaseNotesFailedState(version)),
]);
- /* eslint-disable no-param-reassign */
- options.versionCheck = versionCheck;
- options.releaseNotesData = releaseNotesData;
- /* eslint-enable no-param-reassign */
-
if (!options.ci && !options.smokeTest && options.port != null && port !== options.port) {
const { shouldChangePort } = await inquirer.prompt({
type: 'confirm',
@@ -341,64 +238,29 @@ export async function buildDevStandalone(options) {
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);
+ if (!shouldChangePort) process.exit(1);
}
- const app = express();
- const server = await getServer(app, options);
-
- if (typeof extendServer === 'function') {
- extendServer(server);
- }
-
- app.use((req, res, next) => {
- res.header('Access-Control-Allow-Origin', '*');
- res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
- next();
- });
-
- await applyStatic(app, options);
+ /* eslint-disable no-param-reassign */
+ options.port = port;
+ options.versionCheck = updateInfo;
+ options.releaseNotesData = releaseNotesData;
+ /* eslint-enable no-param-reassign */
const {
- router: storybookMiddleware,
+ address,
+ networkAddress,
previewStats,
managerStats,
managerTotalTime,
previewTotalTime,
- } = await storybook(options);
-
- app.use(storybookMiddleware);
-
- const serverListening = listenToServer(server, listenAddr);
-
- const [updateInfo] = await Promise.all([Promise.resolve(versionCheck), serverListening]);
-
- const proto = options.https ? 'https' : 'http';
- const address = `${proto}://${options.host || 'localhost'}:${port}/`;
- const networkAddress = `${proto}://${ip.address()}:${port}/`;
+ } = await storybookDevServer(options);
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);
+ const managerWarnings = managerStats.toJson().warnings.length > 0;
+ const previewWarnings = !options.ignorePreview && previewStats.toJson().warnings.length > 0;
+ process.exit(managerWarnings || previewWarnings ? 1 : 0);
return;
}
@@ -410,10 +272,6 @@ export async function buildDevStandalone(options) {
managerTotalTime,
previewTotalTime,
});
-
- if (!options.ci) {
- openInBrowser(address);
- }
} catch (error) {
// this is a weird bugfix, somehow 'node-pre-gyp' is polluting the npmLog header
npmLog.heading = '';
@@ -457,5 +315,6 @@ export async function buildDev({ packageJson, ...loadOptions }) {
configDir: loadOptions.configDir || cliOptions.configDir || './.storybook',
ignorePreview: !!cliOptions.previewUrl,
docsMode: !!cliOptions.docs,
+ cache,
});
}
diff --git a/lib/core/src/server/build-static.js b/lib/core/src/server/build-static.js
index a9ce584dabb0..6ae5b9c4d186 100644
--- a/lib/core/src/server/build-static.js
+++ b/lib/core/src/server/build-static.js
@@ -1,7 +1,9 @@
+import cpy from 'cpy';
import fs from 'fs-extra';
import path from 'path';
import webpack from 'webpack';
import shelljs from 'shelljs';
+import trash from 'trash';
import { logger } from '@storybook/node-logger';
@@ -9,6 +11,7 @@ import { getProdCli } from './cli';
import loadConfig from './config';
import loadManagerConfig from './manager/manager-config';
import { logConfig } from './logConfig';
+import { getPrebuiltDir } from './utils/prebuilt-manager';
async function compileManager(managerConfig, managerStartTime) {
logger.info('=> Compiling manager..');
@@ -34,7 +37,7 @@ async function compileManager(managerConfig, managerStartTime) {
return;
}
- logger.trace({ message: '=> manager built', time: process.hrtime(managerStartTime) });
+ logger.trace({ message: '=> Manager built', time: process.hrtime(managerStartTime) });
stats.toJson(managerConfig.stats).warnings.forEach((e) => logger.warn(e));
resolve(stats);
@@ -162,18 +165,6 @@ async function buildPreview(configType, outputDir, packageJson, options) {
return compilePreview(previewConfig, previewStartTime);
}
-function prepareFilesStructure(outputDir, defaultFavIcon) {
- // clear the output dir
- logger.info('clean outputDir..');
- shelljs.rm('-rf', outputDir);
-
- // create output directory if not exists
- shelljs.mkdir('-p', outputDir);
- shelljs.mkdir('-p', path.join(outputDir, 'sb_dll'));
-
- shelljs.cp(defaultFavIcon, outputDir);
-}
-
export async function buildStaticStandalone(options) {
const { staticDir, configDir, packageJson } = options;
@@ -181,18 +172,29 @@ export async function buildStaticStandalone(options) {
const outputDir = path.isAbsolute(options.outputDir)
? options.outputDir
: path.join(process.cwd(), options.outputDir);
- const dllPath = path.join(__dirname, '../../dll/*');
+
const defaultFavIcon = require.resolve('./public/favicon.ico');
- prepareFilesStructure(outputDir, defaultFavIcon);
+ logger.info(`=> Cleaning outputDir ${outputDir}`);
+ await trash(outputDir, { glob: false });
+ await cpy(defaultFavIcon, outputDir);
await copyAllStaticFiles(staticDir, outputDir);
- logger.info(`=> Copying prebuild dll's..`);
- shelljs.cp('-r', dllPath, path.join(outputDir, 'sb_dll'));
+ const prebuiltDir = await getPrebuiltDir({ configDir, options });
+ if (prebuiltDir) {
+ await cpy('**', outputDir, { cwd: prebuiltDir, parents: true });
+ } else {
+ logger.info(`=> Copying prebuilt dll's..`);
+ await cpy(path.join(__dirname, '../../dll/*'), path.join(outputDir, 'sb_dll'));
+ await buildManager(configType, outputDir, configDir, options);
+ }
- await buildManager(configType, outputDir, configDir, options);
- await buildPreview(configType, outputDir, packageJson, options);
+ if (options.managerOnly) {
+ logger.info(`=> Not building preview`);
+ } else {
+ await buildPreview(configType, outputDir, packageJson, options);
+ }
logger.info(`=> Output directory: ${outputDir}`);
}
diff --git a/lib/core/src/server/cli/dev.js b/lib/core/src/server/cli/dev.js
index f70b68e59b1d..e7bcb8471ac8 100644
--- a/lib/core/src/server/cli/dev.js
+++ b/lib/core/src/server/cli/dev.js
@@ -33,6 +33,7 @@ async function getCLI(packageJson) {
'Suppress automatic redirects to the release notes after upgrading',
true
)
+ .option('--no-manager-cache', 'Do not cache the manager UI')
.option('--no-dll', 'Do not use dll references (no-op)')
.option('--docs-dll', 'Use Docs dll reference (legacy)')
.option('--ui-dll', 'Use UI dll reference (legacy)')
diff --git a/lib/core/src/server/dev-server.js b/lib/core/src/server/dev-server.js
index 7c2192facd95..30a8ce7aacd8 100644
--- a/lib/core/src/server/dev-server.js
+++ b/lib/core/src/server/dev-server.js
@@ -1,183 +1,352 @@
+import { logger } from '@storybook/node-logger';
+import open from 'better-opn';
+import express, { Router } from 'express';
+import { pathExists, readFile } from 'fs-extra';
+import http from 'http';
+import https from 'https';
+import ip from 'ip';
import path from 'path';
-import { Router } from 'express';
-import webpack from 'webpack';
-
+import prettyTime from 'pretty-hrtime';
+import { stringify } from 'telejson';
+import dedent from 'ts-dedent';
+import favicon from 'serve-favicon';
+import webpack, { ProgressPlugin } from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
-import { logger } from '@storybook/node-logger';
import { getMiddleware } from './utils/middleware';
import { logConfig } from './logConfig';
import loadConfig from './config';
import loadManagerConfig from './manager/manager-config';
import { resolvePathInStorybookCache } from './utils/resolve-path-in-sb-cache';
+import { getPrebuiltDir } from './utils/prebuilt-manager';
+const defaultFavIcon = require.resolve('./public/favicon.ico');
const dllPath = path.join(__dirname, '../../dll');
const cache = {};
let previewProcess;
let previewReject;
-let resolved = false;
+
+const bailPreview = (e) => {
+ if (previewReject) previewReject();
+ if (previewProcess) {
+ try {
+ previewProcess.close();
+ logger.warn('Force closed preview build');
+ } catch (err) {
+ logger.warn('Unable to close preview build!');
+ }
+ }
+ throw e;
+};
+
+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) => readFile(ca, 'utf-8'))),
+ cert: await readFile(options.sslCert, 'utf-8'),
+ key: await readFile(options.sslKey, 'utf-8'),
+ };
+
+ return https.createServer(sslOptions, app);
+}
+
+async function useStatics(router, options) {
+ const { staticDir } = options;
+
+ let hasCustomFavicon = false;
+
+ if (staticDir && staticDir.length) {
+ await Promise.all(
+ staticDir.map(async (dir) => {
+ const [currentStaticDir, staticEndpoint] = dir.split(':').concat('/');
+ const localStaticPath = path.resolve(currentStaticDir);
+
+ if (!(await pathExists(localStaticPath))) {
+ logger.error(`Error: no such directory to load static files: ${localStaticPath}`);
+ process.exit(-1);
+ }
+
+ logger.info(
+ `=> Loading static files from: ${localStaticPath} and serving at ${staticEndpoint} .`
+ );
+ router.use(staticEndpoint, express.static(localStaticPath, { index: false }));
+
+ const faviconPath = path.resolve(localStaticPath, 'favicon.ico');
+
+ if (await pathExists(faviconPath)) {
+ hasCustomFavicon = true;
+ router.use(favicon(faviconPath));
+ }
+ })
+ );
+ }
+
+ if (!hasCustomFavicon) {
+ router.use(favicon(defaultFavIcon));
+ }
+}
+
+function openInBrowser(address) {
+ try {
+ open(address);
+ } catch (error) {
+ logger.error(dedent`
+ 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.
+ `);
+ }
+}
const router = new Router();
-export default function (options) {
- const configDir = path.resolve(options.configDir);
- const outputDir = options.smokeTest
- ? resolvePathInStorybookCache('public')
- : path.resolve(options.outputDir || resolvePathInStorybookCache('public'));
- const configType = 'DEVELOPMENT';
+const printDuration = (startTime) =>
+ prettyTime(process.hrtime(startTime))
+ .replace(' ms', ' milliseconds')
+ .replace(' s', ' seconds')
+ .replace(' m', ' minutes');
- const startTime = process.hrtime();
- let managerTotalTime;
- let previewTotalTime;
+const useProgressReporting = async (compiler, options, startTime) => {
+ let value = 0;
+ let totalModules;
+ let reportProgress = () => {};
+
+ router.get('/progress', (request, response) => {
+ response.setHeader('Cache-Control', 'no-cache');
+ response.setHeader('Content-Type', 'text/event-stream');
+ response.setHeader('Connection', 'keep-alive');
+ response.flushHeaders();
+
+ const close = () => response.end();
+ response.on('close', close);
- const managerPromise = loadManagerConfig({
+ reportProgress = (progress) => {
+ if (response.writableEnded) return;
+ response.write(`data: ${JSON.stringify(progress)}\n\n`);
+ if (progress.value === 1) close();
+ };
+ });
+
+ const handler = (newValue, message, arg3) => {
+ value = Math.max(newValue, value); // never go backwards
+ const progress = { value, message: message.charAt(0).toUpperCase() + message.slice(1) };
+ if (message === 'building') {
+ const counts = arg3.match(/(\d+)\/(\d+)/) || [];
+ const complete = parseInt(counts[1], 10);
+ const total = parseInt(counts[2], 10);
+ if (!Number.isNaN(complete) && !Number.isNaN(total)) {
+ progress.modules = { complete, total };
+ totalModules = total;
+ }
+ }
+ if (value === 1) {
+ options.cache.set('modulesCount', totalModules);
+ if (!progress.message) {
+ progress.message = `Completed in ${printDuration(startTime)}.`;
+ }
+ }
+ reportProgress(progress);
+ };
+
+ const modulesCount = (await options.cache.get('modulesCount')) || 1000;
+ new ProgressPlugin({ handler, modulesCount }).apply(compiler);
+};
+
+const startManager = async ({
+ startTime,
+ options,
+ configType,
+ outputDir,
+ configDir,
+ prebuiltDir,
+}) => {
+ let managerConfig;
+ if (!prebuiltDir) {
+ // this is pretty slow
+ managerConfig = await loadManagerConfig({
+ configType,
+ outputDir,
+ configDir,
+ cache,
+ corePresets: [require.resolve('./manager/manager-preset.js')],
+ ...options,
+ });
+
+ if (options.debugWebpack) {
+ logConfig('Manager webpack config', managerConfig);
+ }
+
+ if (options.managerCache) {
+ const configString = stringify(managerConfig);
+ const cachedConfig = await options.cache.get('managerConfig');
+ options.cache.set('managerConfig', configString);
+ if (configString === cachedConfig && (await pathExists(outputDir))) {
+ logger.info('=> Using cached manager');
+ managerConfig = null;
+ }
+ } else {
+ options.cache.remove('managerConfig');
+ }
+ }
+
+ if (!managerConfig) {
+ return { managerStats: {}, managerTotalTime: 0 };
+ }
+
+ const compiler = webpack(managerConfig);
+ const middleware = webpackDevMiddleware(compiler, {
+ publicPath: managerConfig.output.publicPath,
+ writeToDisk: true,
+ watchOptions: {
+ aggregateTimeout: 2000,
+ ignored: /node_modules/,
+ },
+ // this actually causes 0 (regular) output from wdm & webpack
+ logLevel: 'warn',
+ clientLogLevel: 'warning',
+ noInfo: true,
+ });
+
+ router.get(/\/static\/media\/.*\..*/, (request, response, next) => {
+ response.set('Cache-Control', `public, max-age=31536000`);
+ next();
+ });
+
+ router.use(middleware);
+
+ const managerStats = await new Promise((resolve) => middleware.waitUntilValid(resolve));
+ if (!managerStats) throw new Error('no stats after building preview');
+ if (managerStats.hasErrors()) throw managerStats;
+ return { managerStats, managerTotalTime: process.hrtime(startTime) };
+};
+
+const startPreview = async ({ startTime, options, configType, outputDir }) => {
+ if (options.ignorePreview) {
+ return { previewStats: {}, previewTotalTime: 0 };
+ }
+
+ const previewConfig = await loadConfig({
configType,
outputDir,
- configDir,
cache,
- corePresets: [require.resolve('./manager/manager-preset.js')],
+ corePresets: [require.resolve('./preview/preview-preset.js')],
+ overridePresets: [require.resolve('./preview/custom-webpack-preset.js')],
...options,
- }).then((config) => {
- if (options.debugWebpack) {
- logConfig('Manager webpack config', config, logger);
- }
- const managerCompiler = webpack(config);
-
- const devMiddlewareOptions = {
- publicPath: config.output.publicPath,
- writeToDisk: !!options.smokeTest,
- watchOptions: {
- aggregateTimeout: 2000,
- ignored: /node_modules/,
- },
- // this actually causes 0 (regular) output from wdm & webpack
- logLevel: 'warn',
- clientLogLevel: 'warning',
- noInfo: true,
- };
+ });
- const managerDevMiddlewareInstance = webpackDevMiddleware(
- managerCompiler,
- devMiddlewareOptions
- );
+ if (options.debugWebpack) {
+ logConfig('Preview webpack config', previewConfig);
+ }
- router.get(/\/static\/media\/.*\..*/, (request, response, next) => {
- response.set('Cache-Control', `public, max-age=31536000`);
- next();
- });
+ const compiler = webpack(previewConfig);
+ await useProgressReporting(compiler, options, startTime);
- router.use(managerDevMiddlewareInstance);
+ const { publicPath } = previewConfig.output;
+ previewProcess = webpackDevMiddleware(compiler, {
+ publicPath: publicPath[0] === '/' ? publicPath.slice(1) : publicPath,
+ watchOptions: {
+ aggregateTimeout: 1,
+ ignored: /node_modules/,
+ ...(previewConfig.watchOptions || {}),
+ },
+ // this actually causes 0 (regular) output from wdm & webpack
+ logLevel: 'warn',
+ clientLogLevel: 'warning',
+ noInfo: true,
+ ...previewConfig.devServer,
+ });
- return new Promise((resolve, reject) => {
- managerDevMiddlewareInstance.waitUntilValid((stats) => {
- managerTotalTime = process.hrtime(startTime);
+ router.use(previewProcess);
+ router.use(webpackHotMiddleware(compiler));
- if (!stats) {
- reject(new Error('no stats after building preview'));
- } else if (stats.hasErrors()) {
- reject(stats);
- } else {
- resolve(stats);
- }
- });
- });
+ const previewStats = await new Promise((resolve, reject) => {
+ previewProcess.waitUntilValid(resolve);
+ previewReject = reject;
});
+ if (!previewStats) throw new Error('no stats after building preview');
+ if (previewStats.hasErrors()) throw previewStats;
+ return { previewStats, previewTotalTime: process.hrtime(startTime) };
+};
- const previewPromise = options.ignorePreview
- ? new Promise((resolve) => resolve())
- : loadConfig({
- configType,
- outputDir,
- cache,
- corePresets: [require.resolve('./preview/preview-preset.js')],
- overridePresets: [require.resolve('./preview/custom-webpack-preset.js')],
- ...options,
- }).then((previewConfig) => {
- if (options.debugWebpack) {
- logConfig('Preview webpack config', previewConfig, logger);
- }
+export async function storybookDevServer(options) {
+ const app = express();
+ const server = await getServer(app, options);
- // remove the leading '/'
- let { publicPath } = previewConfig.output;
- if (publicPath[0] === '/') {
- publicPath = publicPath.slice(1);
- }
+ const configDir = path.resolve(options.configDir);
+ const outputDir = options.smokeTest
+ ? resolvePathInStorybookCache('public')
+ : path.resolve(options.outputDir || resolvePathInStorybookCache('public'));
+ const configType = 'DEVELOPMENT';
+ const startTime = process.hrtime();
- const previewCompiler = webpack(previewConfig);
-
- const devMiddlewareOptions = {
- publicPath: previewConfig.output.publicPath,
- watchOptions: {
- aggregateTimeout: 1,
- ignored: /node_modules/,
- ...(previewConfig.watchOptions || {}),
- },
- // this actually causes 0 (regular) output from wdm & webpack
- logLevel: 'warn',
- clientLogLevel: 'warning',
- noInfo: true,
- ...previewConfig.devServer,
- };
-
- const previewDevMiddlewareInstance = webpackDevMiddleware(
- previewCompiler,
- devMiddlewareOptions
- );
+ if (typeof options.extendServer === 'function') {
+ options.extendServer(server);
+ }
- router.use(previewDevMiddlewareInstance);
- router.use(webpackHotMiddleware(previewCompiler));
-
- return new Promise((resolve, reject) => {
- previewReject = reject;
- previewDevMiddlewareInstance.waitUntilValid((stats) => {
- resolved = true;
- previewTotalTime = process.hrtime(startTime);
-
- if (!stats) {
- reject(new Error('no stats after building preview'));
- } else if (stats.hasErrors()) {
- reject(stats);
- } else {
- resolve(stats);
- }
- });
- previewProcess = previewDevMiddlewareInstance;
- });
- });
-
- // custom middleware
- const middlewareFn = getMiddleware(configDir);
- middlewareFn(router);
-
- managerPromise.catch((e) => {
- try {
- if (!resolved) {
- previewReject();
- }
- previewProcess.close();
- logger.warn('force closed preview build');
- } catch (err) {
- logger.warn('Unable to close preview build!');
- }
+ app.use((req, res, next) => {
+ res.header('Access-Control-Allow-Origin', '*');
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
+ next();
});
- return Promise.all([managerPromise, previewPromise]).then(([managerStats, previewStats]) => {
- router.get('/', (request, response) => {
- response.set('Content-Type', 'text/html');
- response.sendFile(path.join(`${outputDir}/index.html`));
- });
- router.get(/\/sb_dll\/(.+\.js)$/, (request, response) => {
- response.set('Content-Type', 'text/javascript');
- response.sendFile(path.join(`${dllPath}/${request.params[0]}`));
- });
- router.get(/\/sb_dll\/(.+\.LICENCE)$/, (request, response) => {
- response.set('Content-Type', 'text/html');
- response.sendFile(path.join(`${dllPath}/${request.params[0]}`));
- });
+ // User's own static files
+ await useStatics(router, options);
+
+ getMiddleware(configDir)(router);
+ app.use(router);
+
+ const { port, host } = options;
+ const proto = options.https ? 'https' : 'http';
+ const address = `${proto}://${host || 'localhost'}:${port}/`;
+ const networkAddress = `${proto}://${ip.address()}:${port}/`;
+
+ await new Promise((resolve, reject) => {
+ server.listen({ port, host }, (error) => (error ? reject(error) : resolve()));
+ });
+
+ const prebuiltDir = await getPrebuiltDir({ configDir, options });
+
+ // Manager static files
+ router.use('/', express.static(prebuiltDir || outputDir));
- return { previewStats, managerStats, managerTotalTime, previewTotalTime, router };
+ // TODO remove when we drop DLLs
+ router.get(/\/sb_dll\/(.+\.js)$/, (request, response) => {
+ response.set('Content-Type', 'text/javascript');
+ response.sendFile(path.join(`${dllPath}/${request.params[0]}`));
});
+ router.get(/\/sb_dll\/(.+\.LICENCE)$/, (request, response) => {
+ response.set('Content-Type', 'text/html');
+ response.sendFile(path.join(`${dllPath}/${request.params[0]}`));
+ });
+
+ // Build the manager and preview in parallel.
+ // Start the server (and open the browser) as soon as the manager is ready.
+ // Bail if the manager fails, but continue if the preview fails.
+ const [previewResult, managerResult] = await Promise.all([
+ startPreview({ startTime, options, configType, outputDir }),
+ startManager({ startTime, options, configType, outputDir, configDir, prebuiltDir })
+ .then((result) => {
+ if (!options.ci) openInBrowser(address);
+ return result;
+ })
+ .catch(bailPreview),
+ ]);
+
+ return { ...previewResult, ...managerResult, address, networkAddress };
}
diff --git a/lib/core/src/server/manager/manager-config.js b/lib/core/src/server/manager/manager-config.js
index 0c6086de8d26..63a51756231a 100644
--- a/lib/core/src/server/manager/manager-config.js
+++ b/lib/core/src/server/manager/manager-config.js
@@ -12,7 +12,7 @@ import loadPresets from '../presets';
import loadCustomPresets from '../common/custom-presets';
import { typeScriptDefaults } from '../config/defaults';
-const getAutoRefs = async (options) => {
+export const getAutoRefs = async (options) => {
const location = await findUp('package.json', { cwd: options.configDir });
const directory = path.dirname(location);
diff --git a/lib/core/src/server/preview/custom-webpack-preset.js b/lib/core/src/server/preview/custom-webpack-preset.js
index 7d82edfbfc8f..7679bd99025c 100644
--- a/lib/core/src/server/preview/custom-webpack-preset.js
+++ b/lib/core/src/server/preview/custom-webpack-preset.js
@@ -25,6 +25,6 @@ export async function webpack(config, options) {
return customConfig({ config: finalDefaultConfig, mode: configType });
}
- logger.info('=> Using default Webpack setup.');
+ logger.info('=> Using default Webpack setup');
return finalDefaultConfig;
}
diff --git a/lib/core/src/server/preview/entries.js b/lib/core/src/server/preview/entries.js
index d48687fe892a..d5003d85e2e2 100644
--- a/lib/core/src/server/preview/entries.js
+++ b/lib/core/src/server/preview/entries.js
@@ -44,13 +44,15 @@ export async function createPreviewEntry(options) {
const other = await presets.apply('config', [], options);
const stories = await presets.apply('stories', [], options);
- if (configs.length) {
- logger.info(`=> Loading config/preview file in "${configDir}".`);
+ if (configs.length > 0) {
+ const noun = configs.length === 1 ? 'file' : 'files';
+ logger.info(`=> Loading ${configs.length} config ${noun} in "${configDir}"`);
entries.push(...configs.map((filename) => `${filename}-generated-config-entry.js`));
}
- if (other && other.length) {
- logger.info(`=> Loading config/preview file in "${configDir}".`);
+ if (other && other.length > 0) {
+ const noun = other.length === 1 ? 'file' : 'files';
+ logger.info(`=> Loading ${other.length} other ${noun} in "${configDir}"`);
entries.push(...other.map((filename) => `${filename}-generated-other-entry.js`));
}
@@ -71,7 +73,7 @@ export async function createPreviewEntry(options) {
see: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#correct-globs-in-mainjs
`);
} else {
- logger.info(`=> Adding stories defined in "${path.join(configDir, 'main.js')}".`);
+ logger.info(`=> Adding stories defined in "${path.join(configDir, 'main.js')}"`);
}
}
diff --git a/lib/core/src/server/utils/interpret-files.js b/lib/core/src/server/utils/interpret-files.js
index a44231d70740..f92df25d4d13 100644
--- a/lib/core/src/server/utils/interpret-files.js
+++ b/lib/core/src/server/utils/interpret-files.js
@@ -16,12 +16,12 @@ const possibleExtensions = sortExtensions();
export function getInterpretedFile(pathToFile) {
return possibleExtensions
- .map((ext) => `${pathToFile}${ext}`)
+ .map((ext) => (pathToFile.endsWith(ext) ? pathToFile : `${pathToFile}${ext}`))
.find((candidate) => fs.existsSync(candidate));
}
export function getInterpretedFileWithExt(pathToFile) {
return possibleExtensions
- .map((ext) => ({ path: `${pathToFile}${ext}`, ext }))
+ .map((ext) => ({ path: pathToFile.endsWith(ext) ? pathToFile : `${pathToFile}${ext}`, ext }))
.find((candidate) => fs.existsSync(candidate.path));
}
diff --git a/lib/core/src/server/utils/interpret-files.test.js b/lib/core/src/server/utils/interpret-files.test.js
index 05ff066e435c..f8759ddc20f7 100644
--- a/lib/core/src/server/utils/interpret-files.test.js
+++ b/lib/core/src/server/utils/interpret-files.test.js
@@ -23,6 +23,17 @@ describe('interpret-files', () => {
expect(file).toEqual('path/to/file.js');
});
+ it('will interpret file even if extension is already present', () => {
+ mock({
+ 'path/to/file.js': 'js file contents',
+ 'path/to/file.ts': 'ts file contents',
+ });
+
+ const file = getInterpretedFile('path/to/file.js');
+
+ expect(file).toEqual('path/to/file.js');
+ });
+
it('will return undefined if there is no candidate of a file in fs', () => {
mock({
'path/to/file.js': 'js file contents',
diff --git a/lib/core/src/server/utils/load-manager-or-addons-file.js b/lib/core/src/server/utils/load-manager-or-addons-file.js
index 90adea9a4296..391864848aa9 100644
--- a/lib/core/src/server/utils/load-manager-or-addons-file.js
+++ b/lib/core/src/server/utils/load-manager-or-addons-file.js
@@ -11,7 +11,7 @@ export function loadManagerOrAddonsFile({ configDir }) {
const storybookCustomManagerPath = getInterpretedFile(path.resolve(configDir, 'manager'));
if (storybookCustomAddonsPath || storybookCustomManagerPath) {
- logger.info('=> Loading custom manager config.');
+ logger.info('=> Loading custom manager config');
}
if (storybookCustomAddonsPath && storybookCustomManagerPath) {
diff --git a/lib/core/src/server/utils/prebuilt-manager.js b/lib/core/src/server/utils/prebuilt-manager.js
new file mode 100644
index 000000000000..cf82feb44b59
--- /dev/null
+++ b/lib/core/src/server/utils/prebuilt-manager.js
@@ -0,0 +1,44 @@
+import { logger } from '@storybook/node-logger';
+import { pathExists } from 'fs-extra';
+import path from 'path';
+import { getAutoRefs } from '../manager/manager-config';
+import { getInterpretedFile } from './interpret-files';
+import { loadManagerOrAddonsFile } from './load-manager-or-addons-file';
+import { serverRequire } from './server-require';
+
+// Addons automatically installed when running `sb init` (see baseGenerator.ts)
+export const DEFAULT_ADDONS = ['@storybook/addon-links', '@storybook/addon-essentials'];
+
+// Addons we can safely ignore because they don't affect the manager
+export const IGNORED_ADDONS = [
+ '@storybook/preset-create-react-app',
+ '@storybook/preset-scss',
+ '@storybook/preset-typescript',
+ ...DEFAULT_ADDONS,
+];
+
+export const getPrebuiltDir = async ({ configDir, options }) => {
+ if (options.managerCache === false) return false;
+
+ const prebuiltDir = path.join(__dirname, '../../../prebuilt');
+ const hasPrebuiltManager = await pathExists(path.join(prebuiltDir, 'index.html'));
+ if (!hasPrebuiltManager) return false;
+
+ const hasManagerConfig = !!loadManagerOrAddonsFile({ configDir });
+ if (hasManagerConfig) return false;
+
+ const mainConfigFile = getInterpretedFile(path.resolve(configDir, 'main'));
+ if (!mainConfigFile) return false;
+
+ const { addons, refs, managerBabel, managerWebpack } = serverRequire(mainConfigFile);
+ if (!addons || refs || managerBabel || managerWebpack) return false;
+ if (DEFAULT_ADDONS.some((addon) => !addons.includes(addon))) return false;
+ if (addons.some((addon) => !IGNORED_ADDONS.includes(addon))) return false;
+
+ // Auto refs will not be listed in the config, so we have to verify there aren't any
+ const autoRefs = await getAutoRefs({ configDir });
+ if (autoRefs.length > 0) return false;
+
+ logger.info('=> Using prebuilt manager');
+ return prebuiltDir;
+};
diff --git a/lib/ui/src/components/preview/preview.tsx b/lib/ui/src/components/preview/preview.tsx
index 91b4b0cbae37..e4af5249a741 100644
--- a/lib/ui/src/components/preview/preview.tsx
+++ b/lib/ui/src/components/preview/preview.tsx
@@ -60,10 +60,9 @@ const createCanvas = (id: string, baseUrl = 'iframe.html', withLoader = true): A
...defaultWrappers,
]);
- const isLoading = !!(
- (!story && !(storiesFailed || storiesConfigured)) ||
- (story && refId && refs[refId] && !refs[refId].ready)
- );
+ const isLoading = story
+ ? !!refs[refId] && !refs[refId].ready
+ : !storiesFailed && !storiesConfigured;
return (
diff --git a/lib/ui/src/components/sidebar/Explorer.stories.tsx b/lib/ui/src/components/sidebar/Explorer.stories.tsx
index c8e5be5aba18..d576069ac47d 100644
--- a/lib/ui/src/components/sidebar/Explorer.stories.tsx
+++ b/lib/ui/src/components/sidebar/Explorer.stories.tsx
@@ -68,6 +68,7 @@ export const Simple = () => (
);
@@ -76,6 +77,7 @@ export const WithRefs = () => (
);
diff --git a/lib/ui/src/components/sidebar/Explorer.tsx b/lib/ui/src/components/sidebar/Explorer.tsx
index 343e9d83b0d5..2ffc967bd49c 100644
--- a/lib/ui/src/components/sidebar/Explorer.tsx
+++ b/lib/ui/src/components/sidebar/Explorer.tsx
@@ -5,18 +5,20 @@ import { CombinedDataset, Selection } from './types';
import { useHighlighted } from './useHighlighted';
export interface ExplorerProps {
+ isLoading: boolean;
+ isBrowsing: boolean;
dataset: CombinedDataset;
selected: Selection;
- isBrowsing: boolean;
}
export const Explorer: FunctionComponent = React.memo(
- ({ isBrowsing, dataset, selected }) => {
+ ({ isLoading, isBrowsing, dataset, selected }) => {
const containerRef = useRef(null);
// Track highlighted nodes, keep it in sync with props and enable keyboard navigation
const [highlighted, setHighlighted] = useHighlighted({
containerRef,
+ isLoading, // only enable keyboard navigation when ready
isBrowsing, // only enable keyboard navigation when tree is visible
dataset,
selected,
@@ -28,6 +30,7 @@ export const Explorer: FunctionComponent = React.memo(
[ = {
export const Optimized = () => (
][ (
export const IsEmpty = () => (
][ (
export const StartInjectedUnknown = () => (
][ (
export const StartInjectedLoading = () => (
][ (
export const StartInjectedReady = () => (
][ (
export const Versions = () => (
][ (
export const VersionsMissingCurrent = () => (
][ (
export const Errored = () => (
][ (
export const Auth = () => (
][ (
export const Long = () => (
][ = React.memo((props) =>
stories,
id: refId,
title = refId,
+ isLoading: isLoadingMain,
isBrowsing,
selectedStoryId,
highlightedItemId,
@@ -108,10 +110,7 @@ export const Ref: FunctionComponent = React.memo((props) =>
const indicatorRef = useRef(null);
const isMain = refId === DEFAULT_REF_ID;
-
- const isLoadingMain = !ready && isMain;
const isLoadingInjected = type === 'auto-inject' && !ready;
-
const isLoading = isLoadingMain || isLoadingInjected || type === 'unknown';
const isError = !!error;
const isEmpty = !isLoading && length === 0;
diff --git a/lib/ui/src/components/sidebar/Sidebar.stories.tsx b/lib/ui/src/components/sidebar/Sidebar.stories.tsx
index b8122d598e50..664105b76907 100644
--- a/lib/ui/src/components/sidebar/Sidebar.stories.tsx
+++ b/lib/ui/src/components/sidebar/Sidebar.stories.tsx
@@ -71,3 +71,14 @@ export const WithRefs = () => (
refs={refs}
/>
);
+
+export const LoadingWithRefs = () => (
+
+);
diff --git a/lib/ui/src/components/sidebar/Sidebar.tsx b/lib/ui/src/components/sidebar/Sidebar.tsx
index cd8502359b30..f1c3a284ee38 100644
--- a/lib/ui/src/components/sidebar/Sidebar.tsx
+++ b/lib/ui/src/components/sidebar/Sidebar.tsx
@@ -121,7 +121,12 @@ export const Sidebar: FunctionComponent = React.memo(
>
{({ query, results, isBrowsing, getMenuProps, getItemProps, highlightedIndex }) => (
-
+
;
+ isLoading: boolean;
isBrowsing: boolean;
dataset: CombinedDataset;
selected: Selection;
@@ -25,6 +26,7 @@ const fromSelection = (selection: Selection): Highlight =>
export const useHighlighted = ({
containerRef,
+ isLoading,
isBrowsing,
dataset,
selected,
@@ -66,7 +68,7 @@ export const useHighlighted = ({
useEffect(() => {
const menuElement = document.getElementById('storybook-explorer-menu');
const navigateTree = throttle((event) => {
- if (!isBrowsing || !event.key || !containerRef || !containerRef.current) return;
+ if (isLoading || !isBrowsing || !event.key || !containerRef || !containerRef.current) return;
if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
const target = event.target as Element;
@@ -92,7 +94,7 @@ export const useHighlighted = ({
document.addEventListener('keydown', navigateTree);
return () => document.removeEventListener('keydown', navigateTree);
- }, [isBrowsing, highlightedRef, highlightElement]);
+ }, [isLoading, isBrowsing, highlightedRef, highlightElement]);
return [highlighted, setHighlighted];
};
diff --git a/lib/ui/src/index.tsx b/lib/ui/src/index.tsx
index c6939ec3e0df..1a670e47cecd 100644
--- a/lib/ui/src/index.tsx
+++ b/lib/ui/src/index.tsx
@@ -46,13 +46,16 @@ export const Root: FunctionComponent = ({ provider, history }) => (
{({ state, api }: Combo) => {
const panelCount = Object.keys(api.getPanels()).length;
const story = api.getData(state.storyId, state.refId);
+ const isLoading = story
+ ? !!state.refs[state.refId] && !state.refs[state.refId].ready
+ : !state.storiesFailed && !state.storiesConfigured;
return (
diff --git a/package.json b/package.json
index b83d7e3cb638..12107b4e784e 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"await-serve-storybooks": "wait-on http://localhost:8001",
"bootstrap": "node ./scripts/bootstrap.js",
"build": "node ./scripts/build-package.js",
+ "build-manager": "node ./scripts/build-manager.js",
"build-packs": "lerna exec --scope '@storybook/*' -- \\$LERNA_ROOT_PATH/scripts/build-pack.sh \\$LERNA_ROOT_PATH/packs",
"build-storybooks": "cross-env STORYBOOK_DISPLAY_WARNING=true DISPLAY_WARNING=true node -r esm ./scripts/build-storybooks.js",
"changelog": "pr-log --sloppy --cherry-pick",
diff --git a/scripts/bootstrap.js b/scripts/bootstrap.js
index 3222a8056f30..a13cede2e9fc 100755
--- a/scripts/bootstrap.js
+++ b/scripts/bootstrap.js
@@ -65,7 +65,7 @@ function run() {
command: () => {
log.info(prefix, 'yarn workspace');
},
- pre: ['install', 'build', 'dll'],
+ pre: ['install', 'build', 'manager', 'dll'],
order: 1,
}),
reset: createTask({
@@ -104,6 +104,15 @@ function run() {
},
order: 2,
}),
+ manager: createTask({
+ name: `Generate prebuilt manager UI ${chalk.gray('(manager)')}`,
+ defaultValue: false,
+ option: '--manager',
+ command: () => {
+ spawn('yarn build-manager');
+ },
+ order: 3,
+ }),
dll: createTask({
name: `Generate DLL ${chalk.gray('(dll)')}`,
defaultValue: false,
@@ -114,7 +123,7 @@ function run() {
spawn('lerna run createDlls --scope "@storybook/ui" --scope "@storybook/addon-docs"');
}, 5000);
},
- order: 3,
+ order: 4,
}),
packs: createTask({
name: `Build tarballs of packages ${chalk.gray('(build-packs)')}`,
@@ -148,7 +157,7 @@ function run() {
const groups = {
main: ['core'],
- buildtasks: ['install', 'build', 'dll', 'packs'],
+ buildtasks: ['install', 'build', 'manager', 'dll', 'packs'],
devtasks: ['dev', 'registry', 'reset'],
};
diff --git a/scripts/build-manager-config/main.js b/scripts/build-manager-config/main.js
new file mode 100644
index 000000000000..5fc25e4e72f6
--- /dev/null
+++ b/scripts/build-manager-config/main.js
@@ -0,0 +1,4 @@
+module.exports = {
+ // Should be kept in sync with addons listed in `baseGenerator.ts`
+ addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
+};
diff --git a/scripts/build-manager.js b/scripts/build-manager.js
new file mode 100644
index 000000000000..b3107346ee67
--- /dev/null
+++ b/scripts/build-manager.js
@@ -0,0 +1,9 @@
+const { buildStaticStandalone } = require('../lib/core/dist/server/build-static');
+
+process.env.NODE_ENV = 'production';
+
+buildStaticStandalone({
+ managerOnly: true,
+ outputDir: './lib/core/prebuilt',
+ configDir: './scripts/build-manager-config',
+});
diff --git a/yarn.lock b/yarn.lock
index 5d6b5fbf68cc..276a1c816be4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12489,6 +12489,31 @@ cp-file@^6.1.0:
pify "^4.0.1"
safe-buffer "^5.0.1"
+cp-file@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-7.0.0.tgz#b9454cfd07fe3b974ab9ea0e5f29655791a9b8cd"
+ integrity sha512-0Cbj7gyvFVApzpK/uhCtQ/9kE9UnYpxMzaq5nQQC/Dh4iaj5fxp7iEFIullrYwzj8nf0qnsI1Qsx34hAeAebvw==
+ dependencies:
+ graceful-fs "^4.1.2"
+ make-dir "^3.0.0"
+ nested-error-stacks "^2.0.0"
+ p-event "^4.1.0"
+
+cpy@^8.1.1:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/cpy/-/cpy-8.1.1.tgz#066ed4c6eaeed9577df96dae4db9438c1a90df62"
+ integrity sha512-vqHT+9o67sMwJ5hUd/BAOYeemkU+MuFRsK2c36Xc3eefQpAsp1kAsyDxEDcc5JS1+y9l/XHPrIsVTcyGGmkUUQ==
+ dependencies:
+ arrify "^2.0.1"
+ cp-file "^7.0.0"
+ globby "^9.2.0"
+ has-glob "^1.0.0"
+ junk "^3.1.0"
+ nested-error-stacks "^2.1.0"
+ p-all "^2.1.0"
+ p-filter "^2.1.0"
+ p-map "^3.0.0"
+
create-ecdh@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff"
@@ -17825,6 +17850,13 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+has-glob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-glob/-/has-glob-1.0.0.tgz#9aaa9eedbffb1ba3990a7b0010fb678ee0081207"
+ integrity sha1-mqqe7b/7G6OZCnsAEPtnjuAIEgc=
+ dependencies:
+ is-glob "^3.0.0"
+
has-symbol-support-x@^1.4.1:
version "1.4.2"
resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455"
@@ -19324,7 +19356,7 @@ is-glob@^2.0.0, is-glob@^2.0.1:
dependencies:
is-extglob "^1.0.0"
-is-glob@^3.1.0:
+is-glob@^3.0.0, is-glob@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
@@ -21848,6 +21880,11 @@ junk@^1.0.1:
resolved "https://registry.yarnpkg.com/junk/-/junk-1.0.3.tgz#87be63488649cbdca6f53ab39bec9ccd2347f592"
integrity sha1-h75jSIZJy9ym9Tqzm+yczSNH9ZI=
+junk@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
+ integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
+
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
@@ -24361,7 +24398,7 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
-nested-error-stacks@^2.0.0:
+nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61"
integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==
@@ -25282,6 +25319,13 @@ override-require@^1.1.1:
resolved "https://registry.yarnpkg.com/override-require/-/override-require-1.1.1.tgz#6ae22fadeb1f850ffb0cf4c20ff7b87e5eb650df"
integrity sha1-auIvresfhQ/7DPTCD/e4fl62UN8=
+p-all@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/p-all/-/p-all-2.1.0.tgz#91419be56b7dee8fe4c5db875d55e0da084244a0"
+ integrity sha512-HbZxz5FONzz/z2gJfk6bFca0BCiSRF8jU3yCsWOen/vR6lZjfPOu/e7L3uFzTW1i0H8TlC3vqQstEJPQL4/uLA==
+ dependencies:
+ p-map "^2.0.0"
+
p-cancelable@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0"
@@ -25314,6 +25358,20 @@ p-each-series@^2.1.0:
resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48"
integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==
+p-event@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5"
+ integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==
+ dependencies:
+ p-timeout "^3.1.0"
+
+p-filter@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-2.1.0.tgz#1b1472562ae7a0f742f0f3d3d3718ea66ff9c09c"
+ integrity sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==
+ dependencies:
+ p-map "^2.0.0"
+
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
@@ -25431,6 +25489,13 @@ p-timeout@^2.0.1:
dependencies:
p-finally "^1.0.0"
+p-timeout@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
+ integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
+ dependencies:
+ p-finally "^1.0.0"
+
p-try@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
@@ -28553,7 +28618,7 @@ react@^15.4.2:
object-assign "^4.1.0"
prop-types "^15.5.10"
-"react@^16.8.3 || ^17.0.0", react@^16.8.3, react@^16.9.17:
+react@^16.8.3, "react@^16.8.3 || ^17.0.0", react@^16.9.17:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
]