From a71266a6f133f23f41cd9d7600ae01443acbce6f Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 8 Oct 2020 15:06:43 +0200 Subject: [PATCH 01/27] Create, use and publish a prebuilt manager. --- .gitignore | 1 + lib/core/package.json | 2 +- lib/core/src/server/build-dev.js | 11 ++- lib/core/src/server/build-static.js | 7 +- lib/core/src/server/dev-server.js | 136 +++++++++++++++++---------- package.json | 1 + scripts/build-manager-config/main.js | 3 + scripts/build-manager.js | 11 +++ 8 files changed, 114 insertions(+), 58 deletions(-) create mode 100644 scripts/build-manager-config/main.js create mode 100644 scripts/build-manager.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/lib/core/package.json b/lib/core/package.json index 69cc436a7553..6a774ef2139f 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/**/*" diff --git a/lib/core/src/server/build-dev.js b/lib/core/src/server/build-dev.js index cd519a42d4ca..f5afc922587c 100644 --- a/lib/core/src/server/build-dev.js +++ b/lib/core/src/server/build-dev.js @@ -272,11 +272,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( diff --git a/lib/core/src/server/build-static.js b/lib/core/src/server/build-static.js index a9ce584dabb0..af6ba8cbc440 100644 --- a/lib/core/src/server/build-static.js +++ b/lib/core/src/server/build-static.js @@ -192,7 +192,12 @@ export async function buildStaticStandalone(options) { shelljs.cp('-r', dllPath, path.join(outputDir, 'sb_dll')); 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/dev-server.js b/lib/core/src/server/dev-server.js index 7c2192facd95..7745e6d0fd2a 100644 --- a/lib/core/src/server/dev-server.js +++ b/lib/core/src/server/dev-server.js @@ -1,6 +1,7 @@ import path from 'path'; -import { Router } from 'express'; +import express, { Router } from 'express'; import webpack from 'webpack'; +import { pathExists } from 'fs-extra'; import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware'; @@ -10,6 +11,8 @@ import { getMiddleware } from './utils/middleware'; import { logConfig } from './logConfig'; import loadConfig from './config'; import loadManagerConfig from './manager/manager-config'; +import { getInterpretedFile } from './utils/interpret-files'; +import { loadManagerOrAddonsFile } from './utils/load-manager-or-addons-file'; import { resolvePathInStorybookCache } from './utils/resolve-path-in-sb-cache'; const dllPath = path.join(__dirname, '../../dll'); @@ -22,7 +25,34 @@ let resolved = false; const router = new Router(); -export default function (options) { +const usePrebuiltManager = async ({ configDir }) => { + const prebuiltDir = path.join(__dirname, '../../prebuilt'); + const prebuiltIndex = path.join(prebuiltDir, 'index.html'); + + const hasPrebuiltManager = await pathExists(prebuiltIndex); + if (!hasPrebuiltManager) return false; + + const hasManagerConfig = !!loadManagerOrAddonsFile({ configDir }); + if (hasManagerConfig) return false; + + const mainConfigFile = getInterpretedFile(path.resolve(configDir, 'main')); + if (!mainConfigFile) return false; + + // eslint-disable-next-line global-require, import/no-dynamic-require + const { addons, refs, managerBabel, managerWebpack } = require(mainConfigFile); + if (refs || managerBabel || managerWebpack) return false; + if (addons && addons.some((addon) => addon !== '@storybook/addon-essentials')) return false; + + logger.info('=> Using prebuilt manager'); + router.use('/', express.static(prebuiltDir, { index: false })); + router.get('/', (request, response) => { + response.set('Content-Type', 'text/html'); + response.sendFile(prebuiltIndex); + }); + return true; +}; + +export default async function (options) { const configDir = path.resolve(options.configDir); const outputDir = options.smokeTest ? resolvePathInStorybookCache('public') @@ -33,61 +63,65 @@ export default function (options) { let managerTotalTime; let previewTotalTime; - const managerPromise = loadManagerConfig({ - configType, - outputDir, - configDir, - cache, - corePresets: [require.resolve('./manager/manager-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 - ); - - router.get(/\/static\/media\/.*\..*/, (request, response, next) => { - response.set('Cache-Control', `public, max-age=31536000`); - next(); - }); + const prebuiltManager = await usePrebuiltManager({ configDir }); + + const managerPromise = prebuiltManager + ? Promise.resolve({}) + : loadManagerConfig({ + configType, + outputDir, + configDir, + cache, + corePresets: [require.resolve('./manager/manager-preset.js')], + ...options, + }).then(async (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, + }; - router.use(managerDevMiddlewareInstance); + const managerDevMiddlewareInstance = webpackDevMiddleware( + managerCompiler, + devMiddlewareOptions + ); - return new Promise((resolve, reject) => { - managerDevMiddlewareInstance.waitUntilValid((stats) => { - managerTotalTime = process.hrtime(startTime); + router.get(/\/static\/media\/.*\..*/, (request, response, next) => { + response.set('Cache-Control', `public, max-age=31536000`); + next(); + }); - if (!stats) { - reject(new Error('no stats after building preview')); - } else if (stats.hasErrors()) { - reject(stats); - } else { - resolve(stats); - } + router.use(managerDevMiddlewareInstance); + + return new Promise((resolve, reject) => { + managerDevMiddlewareInstance.waitUntilValid((stats) => { + managerTotalTime = process.hrtime(startTime); + + if (!stats) { + reject(new Error('no stats after building preview')); + } else if (stats.hasErrors()) { + reject(stats); + } else { + resolve(stats); + } + }); + }); }); - }); - }); const previewPromise = options.ignorePreview - ? new Promise((resolve) => resolve()) + ? Promise.resolve({}) : loadConfig({ configType, outputDir, diff --git a/package.json b/package.json index c4a78f0650fc..28a3dab18a44 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/build-manager-config/main.js b/scripts/build-manager-config/main.js new file mode 100644 index 000000000000..0e72c49503da --- /dev/null +++ b/scripts/build-manager-config/main.js @@ -0,0 +1,3 @@ +module.exports = { + addons: [{ name: '@storybook/addon-essentials' }], +}; diff --git a/scripts/build-manager.js b/scripts/build-manager.js new file mode 100644 index 000000000000..570eea93b02c --- /dev/null +++ b/scripts/build-manager.js @@ -0,0 +1,11 @@ +const { buildStaticStandalone } = require('../lib/core/dist/server/build-static'); + +const options = { + managerOnly: true, + outputDir: './lib/core/prebuilt', + configDir: './scripts/build-manager-config', +}; + +process.env.NODE_ENV = 'production'; + +buildStaticStandalone(options); From af65fb050c3672fc9221536feb1a23034ebc4a3b Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 8 Oct 2020 22:35:14 +0200 Subject: [PATCH 02/27] Refactor dev server and add manager caching. --- lib/core/package.json | 1 + lib/core/src/server/build-dev.js | 1 + lib/core/src/server/cli/dev.js | 1 + lib/core/src/server/dev-server.js | 293 +++++++++++++++--------------- 4 files changed, 147 insertions(+), 149 deletions(-) diff --git a/lib/core/package.json b/lib/core/package.json index 6a774ef2139f..9eb172a2d690 100644 --- a/lib/core/package.json +++ b/lib/core/package.json @@ -121,6 +121,7 @@ "shelljs": "^0.8.3", "stable": "^0.1.8", "style-loader": "^1.2.1", + "telejson": "^5.0.2", "terser-webpack-plugin": "^3.0.0", "ts-dedent": "^1.1.1", "unfetch": "^4.1.0", diff --git a/lib/core/src/server/build-dev.js b/lib/core/src/server/build-dev.js index f5afc922587c..8747f4e2e92a 100644 --- a/lib/core/src/server/build-dev.js +++ b/lib/core/src/server/build-dev.js @@ -458,5 +458,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/cli/dev.js b/lib/core/src/server/cli/dev.js index 224cffc6750b..81878eed56e7 100644 --- a/lib/core/src/server/cli/dev.js +++ b/lib/core/src/server/cli/dev.js @@ -34,6 +34,7 @@ async function getCLI(packageJson) { true ) .option('--no-dll', 'Do not use dll reference') + .option('--no-manager-cache', 'Do not cache the manager UI') .option('--debug-webpack', 'Display final webpack configurations for debugging purposes') .option( '--preview-url [string]', diff --git a/lib/core/src/server/dev-server.js b/lib/core/src/server/dev-server.js index 7745e6d0fd2a..8e30f07dc2f8 100644 --- a/lib/core/src/server/dev-server.js +++ b/lib/core/src/server/dev-server.js @@ -3,6 +3,7 @@ import express, { Router } from 'express'; import webpack from 'webpack'; import { pathExists } from 'fs-extra'; +import { stringify } from 'telejson'; import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware'; @@ -21,15 +22,23 @@ 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; +}; const router = new Router(); -const usePrebuiltManager = async ({ configDir }) => { - const prebuiltDir = path.join(__dirname, '../../prebuilt'); - const prebuiltIndex = path.join(prebuiltDir, 'index.html'); - - const hasPrebuiltManager = await pathExists(prebuiltIndex); +const canUsePrebuiltManager = async ({ prebuiltDir, configDir }) => { + const hasPrebuiltManager = await pathExists(path.join(prebuiltDir, 'index.html')); if (!hasPrebuiltManager) return false; const hasManagerConfig = !!loadManagerOrAddonsFile({ configDir }); @@ -43,13 +52,94 @@ const usePrebuiltManager = async ({ configDir }) => { if (refs || managerBabel || managerWebpack) return false; if (addons && addons.some((addon) => addon !== '@storybook/addon-essentials')) return false; - logger.info('=> Using prebuilt manager'); - router.use('/', express.static(prebuiltDir, { index: false })); + return true; +}; + +const useCachedManager = (cacheDir) => { + const indexFile = path.join(cacheDir, 'index.html'); + router.use('/', express.static(cacheDir, { index: false })); router.get('/', (request, response) => { response.set('Content-Type', 'text/html'); - response.sendFile(prebuiltIndex); + response.sendFile(indexFile); }); - return true; +}; + +const startManager = async ({ managerConfig, startTime }) => { + if (!managerConfig) { + return { managerStats: {}, managerTotalTime: 0 }; + } + + const middleware = webpackDevMiddleware(webpack(managerConfig), { + 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 ({ configType, outputDir, options, startTime }) => { + if (options.ignorePreview) { + return { previewStats: {}, previewTotalTime: 0 }; + } + + const previewConfig = await loadConfig({ + configType, + outputDir, + cache, + corePresets: [require.resolve('./preview/preview-preset.js')], + overridePresets: [require.resolve('./preview/custom-webpack-preset.js')], + ...options, + }); + + if (options.debugWebpack) { + logConfig('Preview webpack config', previewConfig, logger); + } + + const compiler = webpack(previewConfig); + 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, + }); + + router.use(previewProcess); + router.use(webpackHotMiddleware(compiler)); + + 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) }; }; export default async function (options) { @@ -58,147 +148,53 @@ export default async function (options) { ? resolvePathInStorybookCache('public') : path.resolve(options.outputDir || resolvePathInStorybookCache('public')); const configType = 'DEVELOPMENT'; - const startTime = process.hrtime(); - let managerTotalTime; - let previewTotalTime; - - const prebuiltManager = await usePrebuiltManager({ configDir }); - - const managerPromise = prebuiltManager - ? Promise.resolve({}) - : loadManagerConfig({ - configType, - outputDir, - configDir, - cache, - corePresets: [require.resolve('./manager/manager-preset.js')], - ...options, - }).then(async (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 - ); - - router.get(/\/static\/media\/.*\..*/, (request, response, next) => { - response.set('Cache-Control', `public, max-age=31536000`); - next(); - }); - - router.use(managerDevMiddlewareInstance); - - return new Promise((resolve, reject) => { - managerDevMiddlewareInstance.waitUntilValid((stats) => { - managerTotalTime = process.hrtime(startTime); - - if (!stats) { - reject(new Error('no stats after building preview')); - } else if (stats.hasErrors()) { - reject(stats); - } else { - resolve(stats); - } - }); - }); - }); - - const previewPromise = options.ignorePreview - ? Promise.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); - } - - // remove the leading '/' - let { publicPath } = previewConfig.output; - if (publicPath[0] === '/') { - publicPath = publicPath.slice(1); - } - - 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 - ); - - 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(); + + let usesPrebuiltManager = false; + if (options.managerCache) { + const prebuiltDir = path.join(__dirname, '../../prebuilt'); + if (await canUsePrebuiltManager({ prebuiltDir, configDir })) { + logger.info('=> Using prebuilt manager'); + useCachedManager(prebuiltDir); + usesPrebuiltManager = true; + } + } + + let managerConfig; + if (!usesPrebuiltManager) { + managerConfig = await loadManagerConfig({ + configType, + outputDir, + configDir, + cache, + corePresets: [require.resolve('./manager/manager-preset.js')], + ...options, + }); + + if (options.debugWebpack) { + logConfig('Manager webpack config', managerConfig, logger); + } + + if (options.managerCache) { + const cachedConfig = await options.cache.get('managerConfig'); + const configString = stringify(managerConfig); + await options.cache.set('managerConfig', configString); + if (configString === cachedConfig) { + logger.info('=> Using cached manager'); + useCachedManager(managerConfig.output.path); + usesPrebuiltManager = true; } - previewProcess.close(); - logger.warn('force closed preview build'); - } catch (err) { - logger.warn('Unable to close preview build!'); } - }); + } - return Promise.all([managerPromise, previewPromise]).then(([managerStats, previewStats]) => { + getMiddleware(configDir)(router); + + // Build the manager and preview in parallel. + // Bail if the manager fails, but continue if the preview fails. + return Promise.all([ + startManager({ managerConfig, startTime }).catch(bailPreview), + startPreview({ configType, outputDir, options, startTime }), + ]).then(([managerResult, previewResult]) => { router.get('/', (request, response) => { response.set('Content-Type', 'text/html'); response.sendFile(path.join(`${outputDir}/index.html`)); @@ -211,7 +207,6 @@ export default async function (options) { response.set('Content-Type', 'text/html'); response.sendFile(path.join(`${dllPath}/${request.params[0]}`)); }); - - return { previewStats, managerStats, managerTotalTime, previewTotalTime, router }; + return { ...managerResult, ...previewResult, router }; }); } From 0fb34feb137090adcdeb4ff9641e408ca677faf6 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 8 Oct 2020 22:36:05 +0200 Subject: [PATCH 03/27] Consistent log messages. --- lib/core/src/server/preview/custom-webpack-preset.js | 2 +- lib/core/src/server/preview/entries.js | 2 +- lib/core/src/server/utils/load-manager-or-addons-file.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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..a954ae84f8c9 100644 --- a/lib/core/src/server/preview/entries.js +++ b/lib/core/src/server/preview/entries.js @@ -71,7 +71,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/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) { From 8cd9ad1edd4b2d993fd9a2484247db412aa77516 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 9 Oct 2020 12:16:47 +0200 Subject: [PATCH 04/27] Move server logic to dev-server.js and have it boot as soon as the manager is ready. --- lib/core/src/server/build-dev.js | 169 +++--------------------------- lib/core/src/server/dev-server.js | 142 ++++++++++++++++++++++--- 2 files changed, 142 insertions(+), 169 deletions(-) diff --git a/lib/core/src/server/build-dev.js b/lib/core/src/server/build-dev.js index 8747f4e2e92a..411f965cba2c 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; @@ -303,24 +215,12 @@ 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([ + const [port, updateInfo, releaseNotesData] = await Promise.all([ getFreePort(options.port), versionUpdates ? updateCheck(version) @@ -330,11 +230,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', @@ -348,58 +243,26 @@ export async function buildDevStandalone(options) { } } - // 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); - } - - 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; } @@ -411,10 +274,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 = ''; diff --git a/lib/core/src/server/dev-server.js b/lib/core/src/server/dev-server.js index 8e30f07dc2f8..97b3cbe0f789 100644 --- a/lib/core/src/server/dev-server.js +++ b/lib/core/src/server/dev-server.js @@ -1,13 +1,18 @@ -import path from 'path'; +import { logger } from '@storybook/node-logger'; +import open from 'better-opn'; import express, { Router } from 'express'; -import webpack from 'webpack'; -import { pathExists } from 'fs-extra'; - +import { pathExists, readFile } from 'fs-extra'; +import http from 'http'; +import https from 'https'; +import ip from 'ip'; +import path from 'path'; import { stringify } from 'telejson'; +import dedent from 'ts-dedent'; +import favicon from 'serve-favicon'; +import webpack 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'; @@ -16,6 +21,7 @@ import { getInterpretedFile } from './utils/interpret-files'; import { loadManagerOrAddonsFile } from './utils/load-manager-or-addons-file'; import { resolvePathInStorybookCache } from './utils/resolve-path-in-sb-cache'; +const defaultFavIcon = require.resolve('./public/favicon.ico'); const dllPath = path.join(__dirname, '../../dll'); const cache = {}; @@ -35,6 +41,78 @@ const bailPreview = (e) => { 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 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 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} .` + ); + app.use(staticEndpoint, express.static(localStaticPath, { index: false })); + + const faviconPath = path.resolve(localStaticPath, 'favicon.ico'); + + if (await pathExists(faviconPath)) { + hasCustomFavicon = true; + app.use(favicon(faviconPath)); + } + }) + ); + } + + if (!hasCustomFavicon) { + app.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(); const canUsePrebuiltManager = async ({ prebuiltDir, configDir }) => { @@ -142,7 +220,22 @@ const startPreview = async ({ configType, outputDir, options, startTime }) => { return { previewStats, previewTotalTime: process.hrtime(startTime) }; }; -export default async function (options) { +export async function storybookDevServer(options) { + const app = express(); + const server = await getServer(app, options); + + if (typeof options.extendServer === 'function') { + options.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); + const configDir = path.resolve(options.configDir); const outputDir = options.smokeTest ? resolvePathInStorybookCache('public') @@ -183,18 +276,20 @@ export default async function (options) { logger.info('=> Using cached manager'); useCachedManager(managerConfig.output.path); usesPrebuiltManager = true; + managerConfig = null; } } } getMiddleware(configDir)(router); + app.use(router); - // Build the manager and preview in parallel. - // Bail if the manager fails, but continue if the preview fails. - return Promise.all([ - startManager({ managerConfig, startTime }).catch(bailPreview), - startPreview({ configType, outputDir, options, startTime }), - ]).then(([managerResult, previewResult]) => { + const { port, host } = options; + const proto = options.https ? 'https' : 'http'; + const address = `${proto}://${host || 'localhost'}:${port}/`; + const networkAddress = `${proto}://${ip.address()}:${port}/`; + + const startServer = async (result) => { router.get('/', (request, response) => { response.set('Content-Type', 'text/html'); response.sendFile(path.join(`${outputDir}/index.html`)); @@ -207,6 +302,25 @@ export default async function (options) { response.set('Content-Type', 'text/html'); response.sendFile(path.join(`${dllPath}/${request.params[0]}`)); }); - return { ...managerResult, ...previewResult, router }; - }); + + await new Promise((resolve, reject) => { + server.listen({ port, host }, (error) => (error ? reject(error) : resolve())); + }); + + if (!options.ci) { + openInBrowser(address); + } + + return result; + }; + + // 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 [managerResult, previewResult] = await Promise.all([ + startManager({ managerConfig, startTime }).then(startServer).catch(bailPreview), + startPreview({ configType, outputDir, options, startTime }), + ]); + + return { ...managerResult, ...previewResult, address, networkAddress }; } From 0a4e5bcc4534a4987525ae32e10147fa72f2452b Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 9 Oct 2020 14:42:38 +0200 Subject: [PATCH 05/27] Nicer log messages. --- app/react/src/server/framework-preset-react.ts | 2 +- lib/core/src/server/preview/entries.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/react/src/server/framework-preset-react.ts b/app/react/src/server/framework-preset-react.ts index c2180d367dd9..7171ae8714a1 100644 --- a/app/react/src/server/framework-preset-react.ts +++ b/app/react/src/server/framework-preset-react.ts @@ -43,7 +43,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/lib/core/src/server/preview/entries.js b/lib/core/src/server/preview/entries.js index a954ae84f8c9..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`)); } From 6dbbebaaf323a360aba9188dc347a10f1d9c1544 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 9 Oct 2020 14:43:16 +0200 Subject: [PATCH 06/27] Minor cleanup. --- lib/core/src/server/build-dev.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/core/src/server/build-dev.js b/lib/core/src/server/build-dev.js index 411f965cba2c..6d58f673c2ab 100644 --- a/lib/core/src/server/build-dev.js +++ b/lib/core/src/server/build-dev.js @@ -220,6 +220,7 @@ export async function buildDevStandalone(options) { const { packageJson, versionUpdates, releaseNotes } = options; const { version } = packageJson; + // updateInfo and releaseNotesData are cached, so this is typically pretty fast const [port, updateInfo, releaseNotesData] = await Promise.all([ getFreePort(options.port), versionUpdates @@ -237,10 +238,7 @@ 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); - } + if (!shouldChangePort) process.exit(1); } /* eslint-disable no-param-reassign */ From 3fba7e603e3c84715f855c39a3612b498d90cc73 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 9 Oct 2020 14:43:56 +0200 Subject: [PATCH 07/27] Make sure we start building the preview as soon as possible. --- lib/core/src/server/dev-server.js | 83 +++++++++++++++++-------------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/lib/core/src/server/dev-server.js b/lib/core/src/server/dev-server.js index 97b3cbe0f789..cdb94eab2ec1 100644 --- a/lib/core/src/server/dev-server.js +++ b/lib/core/src/server/dev-server.js @@ -142,7 +142,44 @@ const useCachedManager = (cacheDir) => { }); }; -const startManager = async ({ managerConfig, startTime }) => { +const startManager = async ({ + startTime, + options, + configType, + outputDir, + configDir, + skipBuilding, +}) => { + let managerConfig; + if (!skipBuilding) { + // 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, logger); + } + + if (options.managerCache) { + const cachedConfig = await options.cache.get('managerConfig'); + const configString = stringify(managerConfig); + options.cache.set('managerConfig', configString); + if (configString === cachedConfig) { + logger.info('=> Using cached manager'); + useCachedManager(managerConfig.output.path); + managerConfig = null; + } + } else { + options.cache.remove('managerConfig'); + } + } + if (!managerConfig) { return { managerStats: {}, managerTotalTime: 0 }; } @@ -173,7 +210,7 @@ const startManager = async ({ managerConfig, startTime }) => { return { managerStats, managerTotalTime: process.hrtime(startTime) }; }; -const startPreview = async ({ configType, outputDir, options, startTime }) => { +const startPreview = async ({ startTime, options, configType, outputDir }) => { if (options.ignorePreview) { return { previewStats: {}, previewTotalTime: 0 }; } @@ -243,41 +280,13 @@ export async function storybookDevServer(options) { const configType = 'DEVELOPMENT'; const startTime = process.hrtime(); - let usesPrebuiltManager = false; + let skipBuilding = false; if (options.managerCache) { const prebuiltDir = path.join(__dirname, '../../prebuilt'); if (await canUsePrebuiltManager({ prebuiltDir, configDir })) { logger.info('=> Using prebuilt manager'); useCachedManager(prebuiltDir); - usesPrebuiltManager = true; - } - } - - let managerConfig; - if (!usesPrebuiltManager) { - managerConfig = await loadManagerConfig({ - configType, - outputDir, - configDir, - cache, - corePresets: [require.resolve('./manager/manager-preset.js')], - ...options, - }); - - if (options.debugWebpack) { - logConfig('Manager webpack config', managerConfig, logger); - } - - if (options.managerCache) { - const cachedConfig = await options.cache.get('managerConfig'); - const configString = stringify(managerConfig); - await options.cache.set('managerConfig', configString); - if (configString === cachedConfig) { - logger.info('=> Using cached manager'); - useCachedManager(managerConfig.output.path); - usesPrebuiltManager = true; - managerConfig = null; - } + skipBuilding = true; } } @@ -317,10 +326,12 @@ export async function storybookDevServer(options) { // 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 [managerResult, previewResult] = await Promise.all([ - startManager({ managerConfig, startTime }).then(startServer).catch(bailPreview), - startPreview({ configType, outputDir, options, startTime }), + const [previewResult, managerResult] = await Promise.all([ + startPreview({ startTime, options, configType, outputDir }), + startManager({ startTime, options, configType, outputDir, configDir, skipBuilding }) + .then(startServer) + .catch(bailPreview), ]); - return { ...managerResult, ...previewResult, address, networkAddress }; + return { ...previewResult, ...managerResult, address, networkAddress }; } From 9a48cfb791bc30869205535656cbb7a0bc36d409 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 9 Oct 2020 14:57:33 +0200 Subject: [PATCH 08/27] Show refs as loading while main SB is loading. Disable keyboard shortcuts while loading. --- lib/ui/src/components/sidebar/Explorer.stories.tsx | 2 ++ lib/ui/src/components/sidebar/Explorer.tsx | 7 +++++-- lib/ui/src/components/sidebar/Refs.tsx | 5 ++--- lib/ui/src/components/sidebar/Sidebar.stories.tsx | 11 +++++++++++ lib/ui/src/components/sidebar/Sidebar.tsx | 1 + lib/ui/src/components/sidebar/useHighlighted.ts | 6 ++++-- 6 files changed, 25 insertions(+), 7 deletions(-) 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( = 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 d2aae7f8ceb5..32f8c852d530 100644 --- a/lib/ui/src/components/sidebar/Sidebar.tsx +++ b/lib/ui/src/components/sidebar/Sidebar.tsx @@ -138,6 +138,7 @@ export const Sidebar: FunctionComponent = React.memo( diff --git a/lib/ui/src/components/sidebar/useHighlighted.ts b/lib/ui/src/components/sidebar/useHighlighted.ts index 75511a42039c..ae79b35b0d44 100644 --- a/lib/ui/src/components/sidebar/useHighlighted.ts +++ b/lib/ui/src/components/sidebar/useHighlighted.ts @@ -15,6 +15,7 @@ import { cycle, isAncestor, scrollIntoView } from './utils'; export interface HighlightedProps { containerRef: MutableRefObject; + 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]; }; From 6b3caaebef8196af4ef2a8f6104f9a33c2901c4f Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Mon, 12 Oct 2020 19:50:35 +0200 Subject: [PATCH 09/27] Show compilation progress in the browser. --- lib/components/src/Loader/Loader.stories.tsx | 65 +++++---- lib/components/src/Loader/Loader.tsx | 121 ++++++++++++++++- lib/core/src/server/dev-server.js | 135 +++++++++++++------ 3 files changed, 249 insertions(+), 72 deletions(-) diff --git a/lib/components/src/Loader/Loader.stories.tsx b/lib/components/src/Loader/Loader.stories.tsx index 30da9c305fe7..2373feff6046 100644 --- a/lib/components/src/Loader/Loader.stories.tsx +++ b/lib/components/src/Loader/Loader.stories.tsx @@ -1,33 +1,46 @@ 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.storyName = 'infinite state'; +InfiniteState.decorators = [withBackground]; + +export const SizeAdjusted = () => ; +SizeAdjusted.storyName = 'size adjusted'; +SizeAdjusted.decorators = [withBackground]; + +export const ProgressBar = () => ( + +); +ProgressBar.storyName = 'progress bar'; diff --git a/lib/components/src/Loader/Loader.tsx b/lib/components/src/Loader/Loader.tsx index c822d5fc533f..1f302a433d79 100644 --- a/lib/components/src/Loader/Loader.tsx +++ b/lib/components/src/Loader/Loader.tsx @@ -1,5 +1,6 @@ -import React, { FunctionComponent, ComponentProps } from 'react'; -import { styled } from '@storybook/theming'; +import { EventSource } from 'global'; +import React, { FunctionComponent, useEffect, useState } from 'react'; +import { styled, keyframes } from '@storybook/theming'; import { rotate360 } from '../shared/animation'; const LoaderWrapper = styled.div<{ size?: number }>(({ size = 32 }) => ({ @@ -25,6 +26,116 @@ 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%', + 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', + marginTop: '0.75rem', + fontSize: `${theme.typography.size.s1}px`, + color: theme.barTextColor, +})); + +const ellipsis = keyframes` + from { content: "..." } + 33% { content: "." } + 66% { content: ".." } + to { content: "..." } +`; + +const Ellipsis = styled.span({ + '&::after': { + content: "'...'", + animation: `${ellipsis} 1s linear infinite`, + display: 'inline-block', + width: '1em', + height: 'auto', + }, +}); + +interface LoaderProps { + progress?: { + value: number; + message: string; + modules?: { + complete: number; + total: number; + }; + }; + size?: number; +} + +export const PureLoader: FunctionComponent = ({ progress, size, ...props }) => + progress ? ( + + + + + + {progress.message} + {progress.modules && ` ${progress.modules.complete} / ${progress.modules.total} modules`} + {progress.value < 1 && } + + + ) : ( + + ); + +export const Loader: FunctionComponent = (props) => { + const [progress, setProgress] = useState(undefined); + + useEffect(() => { + const eventSource = new EventSource('/progress'); + eventSource.onmessage = (event: any) => { + try { + setProgress(JSON.parse(event.data)); + } catch (e) { + // do nothing + } + }; + return () => eventSource.close(); + }, []); + + return ; +}; diff --git a/lib/core/src/server/dev-server.js b/lib/core/src/server/dev-server.js index cdb94eab2ec1..8d0a8ee4a534 100644 --- a/lib/core/src/server/dev-server.js +++ b/lib/core/src/server/dev-server.js @@ -6,10 +6,11 @@ import http from 'http'; import https from 'https'; import ip from 'ip'; import path from 'path'; +import prettyTime from 'pretty-hrtime'; import { stringify } from 'telejson'; import dedent from 'ts-dedent'; import favicon from 'serve-favicon'; -import webpack from 'webpack'; +import webpack, { ProgressPlugin } from 'webpack'; import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware'; @@ -28,6 +29,7 @@ const cache = {}; let previewProcess; let previewReject; + const bailPreview = (e) => { if (previewReject) previewReject(); if (previewProcess) { @@ -65,7 +67,7 @@ async function getServer(app, options) { return https.createServer(sslOptions, app); } -async function applyStatic(app, options) { +async function useStatics(app, options) { const { staticDir } = options; let hasCustomFavicon = false; @@ -142,6 +144,58 @@ const useCachedManager = (cacheDir) => { }); }; +const printDuration = (startTime) => + prettyTime(process.hrtime(startTime)) + .replace(' ms', ' milliseconds') + .replace(' s', ' seconds') + .replace(' m', ' minutes'); + +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); + + 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, @@ -184,7 +238,8 @@ const startManager = async ({ return { managerStats: {}, managerTotalTime: 0 }; } - const middleware = webpackDevMiddleware(webpack(managerConfig), { + const compiler = webpack(managerConfig); + const middleware = webpackDevMiddleware(compiler, { publicPath: managerConfig.output.publicPath, writeToDisk: true, watchOptions: { @@ -229,8 +284,9 @@ const startPreview = async ({ startTime, options, configType, outputDir }) => { } const compiler = webpack(previewConfig); - const { publicPath } = previewConfig.output; + await useProgressReporting(compiler, options, startTime); + const { publicPath } = previewConfig.output; previewProcess = webpackDevMiddleware(compiler, { publicPath: publicPath[0] === '/' ? publicPath.slice(1) : publicPath, watchOptions: { @@ -261,6 +317,13 @@ export async function storybookDevServer(options) { const app = express(); const server = await getServer(app, options); + 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(); + if (typeof options.extendServer === 'function') { options.extendServer(server); } @@ -271,24 +334,20 @@ export async function storybookDevServer(options) { next(); }); - await applyStatic(app, options); - - 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(); + await useStatics(app, options); - let skipBuilding = false; - if (options.managerCache) { - const prebuiltDir = path.join(__dirname, '../../prebuilt'); - if (await canUsePrebuiltManager({ prebuiltDir, configDir })) { - logger.info('=> Using prebuilt manager'); - useCachedManager(prebuiltDir); - skipBuilding = true; - } - } + app.get('/', (request, response) => { + response.set('Content-Type', 'text/html'); + response.sendFile(path.join(`${outputDir}/index.html`)); + }); + app.get(/\/sb_dll\/(.+\.js)$/, (request, response) => { + response.set('Content-Type', 'text/javascript'); + response.sendFile(path.join(`${dllPath}/${request.params[0]}`)); + }); + app.get(/\/sb_dll\/(.+\.LICENCE)$/, (request, response) => { + response.set('Content-Type', 'text/html'); + response.sendFile(path.join(`${dllPath}/${request.params[0]}`)); + }); getMiddleware(configDir)(router); app.use(router); @@ -298,28 +357,22 @@ export async function storybookDevServer(options) { const address = `${proto}://${host || 'localhost'}:${port}/`; const networkAddress = `${proto}://${ip.address()}:${port}/`; - const startServer = async (result) => { - 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]}`)); - }); - - await new Promise((resolve, reject) => { - server.listen({ port, host }, (error) => (error ? reject(error) : resolve())); - }); + await new Promise((resolve, reject) => { + server.listen({ port, host }, (error) => (error ? reject(error) : resolve())); + }); - if (!options.ci) { - openInBrowser(address); + let skipBuilding = false; + if (options.managerCache) { + const prebuiltDir = path.join(__dirname, '../../prebuilt'); + if (await canUsePrebuiltManager({ prebuiltDir, configDir })) { + logger.info('=> Using prebuilt manager'); + useCachedManager(prebuiltDir); + skipBuilding = true; } + } + const openBrowser = async (result) => { + if (!options.ci) openInBrowser(address); return result; }; @@ -329,7 +382,7 @@ export async function storybookDevServer(options) { const [previewResult, managerResult] = await Promise.all([ startPreview({ startTime, options, configType, outputDir }), startManager({ startTime, options, configType, outputDir, configDir, skipBuilding }) - .then(startServer) + .then(openBrowser) .catch(bailPreview), ]); From b667c2942b7a2a6906cd96cb711db804d04cb2a9 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 13 Oct 2020 23:40:48 +0200 Subject: [PATCH 10/27] Only animate ellipsis after 1s of inactivity. --- lib/components/src/Loader/Loader.tsx | 51 ++++++++++++++++------------ 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/components/src/Loader/Loader.tsx b/lib/components/src/Loader/Loader.tsx index 1f302a433d79..d8972463aee8 100644 --- a/lib/components/src/Loader/Loader.tsx +++ b/lib/components/src/Loader/Loader.tsx @@ -73,6 +73,7 @@ const Ellipsis = styled.span({ '&::after': { content: "'...'", animation: `${ellipsis} 1s linear infinite`, + animationDelay: '1s', display: 'inline-block', width: '1em', height: 'auto', @@ -91,28 +92,33 @@ interface LoaderProps { size?: number; } -export const PureLoader: FunctionComponent = ({ progress, size, ...props }) => - progress ? ( - - - - - - {progress.message} - {progress.modules && ` ${progress.modules.complete} / ${progress.modules.total} modules`} - {progress.value < 1 && } - - - ) : ( +export const PureLoader: FunctionComponent = ({ progress, size, ...props }) => { + if (progress) { + const { value, modules } = progress; + let { message } = progress; + if (modules) message += ` ${modules.complete} / ${modules.total} modules`; + return ( + + + + + + {message} + {value < 1 && } + + + ); + } + return ( = ({ progress, size, ... {...props} /> ); +}; export const Loader: FunctionComponent = (props) => { const [progress, setProgress] = useState(undefined); From fd6dbbf7c5cae7fd758ac738706aee1def573dec Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Mon, 26 Oct 2020 15:54:57 +0100 Subject: [PATCH 11/27] Fix DeepScan issues. --- lib/components/src/Loader/Loader.stories.tsx | 1 - lib/core/src/server/dev-server.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/components/src/Loader/Loader.stories.tsx b/lib/components/src/Loader/Loader.stories.tsx index 2373feff6046..eae94d126946 100644 --- a/lib/components/src/Loader/Loader.stories.tsx +++ b/lib/components/src/Loader/Loader.stories.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { storiesOf } from '@storybook/react'; import { PureLoader as Loader } from './Loader'; const withBackground = (storyFn) => ( diff --git a/lib/core/src/server/dev-server.js b/lib/core/src/server/dev-server.js index 8d0a8ee4a534..4aa8fdafd604 100644 --- a/lib/core/src/server/dev-server.js +++ b/lib/core/src/server/dev-server.js @@ -217,7 +217,7 @@ const startManager = async ({ }); if (options.debugWebpack) { - logConfig('Manager webpack config', managerConfig, logger); + logConfig('Manager webpack config', managerConfig); } if (options.managerCache) { @@ -280,7 +280,7 @@ const startPreview = async ({ startTime, options, configType, outputDir }) => { }); if (options.debugWebpack) { - logConfig('Preview webpack config', previewConfig, logger); + logConfig('Preview webpack config', previewConfig); } const compiler = webpack(previewConfig); From 3c38f894315b1f09aa16ef322a4a70f33240fd52 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Mon, 26 Oct 2020 16:48:17 +0100 Subject: [PATCH 12/27] Fix types. --- lib/components/src/Loader/Loader.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/components/src/Loader/Loader.tsx b/lib/components/src/Loader/Loader.tsx index d8972463aee8..a9e07a35ffc8 100644 --- a/lib/components/src/Loader/Loader.tsx +++ b/lib/components/src/Loader/Loader.tsx @@ -1,5 +1,5 @@ import { EventSource } from 'global'; -import React, { FunctionComponent, useEffect, useState } from 'react'; +import React, { ComponentProps, FunctionComponent, useEffect, useState } from 'react'; import { styled, keyframes } from '@storybook/theming'; import { rotate360 } from '../shared/animation'; @@ -92,7 +92,9 @@ interface LoaderProps { size?: number; } -export const PureLoader: FunctionComponent = ({ progress, size, ...props }) => { +export const PureLoader: FunctionComponent< + LoaderProps & ComponentProps +> = ({ progress, size, ...props }) => { if (progress) { const { value, modules } = progress; let { message } = progress; @@ -129,7 +131,7 @@ export const PureLoader: FunctionComponent = ({ progress, size, ... ); }; -export const Loader: FunctionComponent = (props) => { +export const Loader: FunctionComponent> = (props) => { const [progress, setProgress] = useState(undefined); useEffect(() => { From 61ea0a167fa5c1a588f9aeb612c2786e33bc2a72 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Mon, 26 Oct 2020 16:57:13 +0100 Subject: [PATCH 13/27] Fix missing isLoading prop. --- lib/ui/src/components/sidebar/Refs.stories.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/ui/src/components/sidebar/Refs.stories.tsx b/lib/ui/src/components/sidebar/Refs.stories.tsx index 0b7eb1bc60f3..4d8fdb234604 100644 --- a/lib/ui/src/components/sidebar/Refs.stories.tsx +++ b/lib/ui/src/components/sidebar/Refs.stories.tsx @@ -119,6 +119,7 @@ const refs: Record = { 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 = () => ( Date: Wed, 28 Oct 2020 17:35:27 +0100 Subject: [PATCH 14/27] Consider default addons and auto refs when deciding to use the prebuilt manager. Avoid conflicting express routes. --- lib/cli/src/generators/baseGenerator.ts | 1 + lib/core/src/server/build-static.js | 2 +- lib/core/src/server/dev-server.js | 97 +++++++++---------- lib/core/src/server/manager/manager-config.js | 2 +- scripts/build-manager-config/main.js | 3 +- scripts/build-manager.js | 10 +- 6 files changed, 55 insertions(+), 60 deletions(-) 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/core/src/server/build-static.js b/lib/core/src/server/build-static.js index af6ba8cbc440..366962de352c 100644 --- a/lib/core/src/server/build-static.js +++ b/lib/core/src/server/build-static.js @@ -164,7 +164,7 @@ async function buildPreview(configType, outputDir, packageJson, options) { function prepareFilesStructure(outputDir, defaultFavIcon) { // clear the output dir - logger.info('clean outputDir..'); + logger.info('=> Cleaning outputDir..'); shelljs.rm('-rf', outputDir); // create output directory if not exists diff --git a/lib/core/src/server/dev-server.js b/lib/core/src/server/dev-server.js index 4aa8fdafd604..439935573c32 100644 --- a/lib/core/src/server/dev-server.js +++ b/lib/core/src/server/dev-server.js @@ -17,7 +17,7 @@ import webpackHotMiddleware from 'webpack-hot-middleware'; import { getMiddleware } from './utils/middleware'; import { logConfig } from './logConfig'; import loadConfig from './config'; -import loadManagerConfig from './manager/manager-config'; +import loadManagerConfig, { getAutoRefs } from './manager/manager-config'; import { getInterpretedFile } from './utils/interpret-files'; import { loadManagerOrAddonsFile } from './utils/load-manager-or-addons-file'; import { resolvePathInStorybookCache } from './utils/resolve-path-in-sb-cache'; @@ -67,7 +67,7 @@ async function getServer(app, options) { return https.createServer(sslOptions, app); } -async function useStatics(app, options) { +async function useStatics(router, options) { const { staticDir } = options; let hasCustomFavicon = false; @@ -86,20 +86,20 @@ async function useStatics(app, options) { logger.info( `=> Loading static files from: ${localStaticPath} and serving at ${staticEndpoint} .` ); - app.use(staticEndpoint, express.static(localStaticPath, { index: false })); + router.use(staticEndpoint, express.static(localStaticPath, { index: false })); const faviconPath = path.resolve(localStaticPath, 'favicon.ico'); if (await pathExists(faviconPath)) { hasCustomFavicon = true; - app.use(favicon(faviconPath)); + router.use(favicon(faviconPath)); } }) ); } if (!hasCustomFavicon) { - app.use(favicon(defaultFavIcon)); + router.use(favicon(defaultFavIcon)); } } @@ -117,7 +117,9 @@ function openInBrowser(address) { const router = new Router(); -const canUsePrebuiltManager = async ({ prebuiltDir, configDir }) => { +const canUsePrebuiltManager = async ({ prebuiltDir, configDir, options }) => { + if (!options.managerCache) return false; + const hasPrebuiltManager = await pathExists(path.join(prebuiltDir, 'index.html')); if (!hasPrebuiltManager) return false; @@ -127,21 +129,23 @@ const canUsePrebuiltManager = async ({ prebuiltDir, configDir }) => { const mainConfigFile = getInterpretedFile(path.resolve(configDir, 'main')); if (!mainConfigFile) return false; + // Addons automatically installed when running `sb init` (see baseGenerator.ts) + const defaultAddons = ['@storybook/addon-links', '@storybook/addon-essentials']; + // Addons we can safely ignore because they don't affect the manager + const ignoredAddons = ['@storybook/preset-create-react-app', ...defaultAddons]; + // eslint-disable-next-line global-require, import/no-dynamic-require const { addons, refs, managerBabel, managerWebpack } = require(mainConfigFile); - if (refs || managerBabel || managerWebpack) return false; - if (addons && addons.some((addon) => addon !== '@storybook/addon-essentials')) return false; + if (!addons || refs || managerBabel || managerWebpack) return false; + if (defaultAddons.some((addon) => !addons.includes(addon))) return false; + if (addons.some((addon) => !ignoredAddons.includes(addon))) return false; - return true; -}; + // 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; -const useCachedManager = (cacheDir) => { - const indexFile = path.join(cacheDir, 'index.html'); - router.use('/', express.static(cacheDir, { index: false })); - router.get('/', (request, response) => { - response.set('Content-Type', 'text/html'); - response.sendFile(indexFile); - }); + logger.info('=> Using prebuilt manager'); + return true; }; const printDuration = (startTime) => @@ -202,10 +206,10 @@ const startManager = async ({ configType, outputDir, configDir, - skipBuilding, + usePrebuilt, }) => { let managerConfig; - if (!skipBuilding) { + if (!usePrebuilt) { // this is pretty slow managerConfig = await loadManagerConfig({ configType, @@ -221,12 +225,11 @@ const startManager = async ({ } if (options.managerCache) { - const cachedConfig = await options.cache.get('managerConfig'); const configString = stringify(managerConfig); + const cachedConfig = await options.cache.get('managerConfig'); options.cache.set('managerConfig', configString); - if (configString === cachedConfig) { + if (configString === cachedConfig && (await pathExists(outputDir))) { logger.info('=> Using cached manager'); - useCachedManager(managerConfig.output.path); managerConfig = null; } } else { @@ -334,20 +337,8 @@ export async function storybookDevServer(options) { next(); }); - await useStatics(app, options); - - app.get('/', (request, response) => { - response.set('Content-Type', 'text/html'); - response.sendFile(path.join(`${outputDir}/index.html`)); - }); - app.get(/\/sb_dll\/(.+\.js)$/, (request, response) => { - response.set('Content-Type', 'text/javascript'); - response.sendFile(path.join(`${dllPath}/${request.params[0]}`)); - }); - app.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); @@ -361,28 +352,32 @@ export async function storybookDevServer(options) { server.listen({ port, host }, (error) => (error ? reject(error) : resolve())); }); - let skipBuilding = false; - if (options.managerCache) { - const prebuiltDir = path.join(__dirname, '../../prebuilt'); - if (await canUsePrebuiltManager({ prebuiltDir, configDir })) { - logger.info('=> Using prebuilt manager'); - useCachedManager(prebuiltDir); - skipBuilding = true; - } - } + const prebuiltDir = path.join(__dirname, '../../prebuilt'); + const usePrebuilt = await canUsePrebuiltManager({ prebuiltDir, configDir, options }); - const openBrowser = async (result) => { - if (!options.ci) openInBrowser(address); - return result; - }; + // Manager static files + router.use('/', express.static(usePrebuilt ? prebuiltDir : outputDir)); + + // 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, skipBuilding }) - .then(openBrowser) + startManager({ startTime, options, configType, outputDir, configDir, usePrebuilt }) + .then((result) => { + if (!options.ci) openInBrowser(address); + return result; + }) .catch(bailPreview), ]); 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/scripts/build-manager-config/main.js b/scripts/build-manager-config/main.js index 0e72c49503da..5fc25e4e72f6 100644 --- a/scripts/build-manager-config/main.js +++ b/scripts/build-manager-config/main.js @@ -1,3 +1,4 @@ module.exports = { - addons: [{ name: '@storybook/addon-essentials' }], + // 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 index 570eea93b02c..b3107346ee67 100644 --- a/scripts/build-manager.js +++ b/scripts/build-manager.js @@ -1,11 +1,9 @@ const { buildStaticStandalone } = require('../lib/core/dist/server/build-static'); -const options = { +process.env.NODE_ENV = 'production'; + +buildStaticStandalone({ managerOnly: true, outputDir: './lib/core/prebuilt', configDir: './scripts/build-manager-config', -}; - -process.env.NODE_ENV = 'production'; - -buildStaticStandalone(options); +}); From 9d4264b9c9c3125b23349abc2b202ad50571c213 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 29 Oct 2020 10:00:04 +0100 Subject: [PATCH 15/27] Use trash instead of . --- lib/core/package.json | 1 + lib/core/src/server/build-static.js | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/core/package.json b/lib/core/package.json index 2bcf91ba4048..237a945065ef 100644 --- a/lib/core/package.json +++ b/lib/core/package.json @@ -123,6 +123,7 @@ "style-loader": "^1.2.1", "telejson": "^5.0.2", "terser-webpack-plugin": "^3.0.0", + "trash": "^6.1.1", "ts-dedent": "^1.1.1", "unfetch": "^4.1.0", "url-loader": "^4.0.0", diff --git a/lib/core/src/server/build-static.js b/lib/core/src/server/build-static.js index 366962de352c..099e1106047b 100644 --- a/lib/core/src/server/build-static.js +++ b/lib/core/src/server/build-static.js @@ -2,6 +2,7 @@ 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'; @@ -162,10 +163,10 @@ async function buildPreview(configType, outputDir, packageJson, options) { return compilePreview(previewConfig, previewStartTime); } -function prepareFilesStructure(outputDir, defaultFavIcon) { +async function prepareFilesStructure(outputDir, defaultFavIcon) { // clear the output dir logger.info('=> Cleaning outputDir..'); - shelljs.rm('-rf', outputDir); + await trash(outputDir); // create output directory if not exists shelljs.mkdir('-p', outputDir); @@ -184,7 +185,7 @@ export async function buildStaticStandalone(options) { const dllPath = path.join(__dirname, '../../dll/*'); const defaultFavIcon = require.resolve('./public/favicon.ico'); - prepareFilesStructure(outputDir, defaultFavIcon); + await prepareFilesStructure(outputDir, defaultFavIcon); await copyAllStaticFiles(staticDir, outputDir); From e418511096f692440115606d9f6d3e727f37795a Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 29 Oct 2020 10:11:08 +0100 Subject: [PATCH 16/27] Add manager task to bootstrap. --- lib/core/src/server/build-static.js | 2 +- scripts/bootstrap.js | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/core/src/server/build-static.js b/lib/core/src/server/build-static.js index 099e1106047b..34ee56f82475 100644 --- a/lib/core/src/server/build-static.js +++ b/lib/core/src/server/build-static.js @@ -35,7 +35,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); 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'], }; From c668449638de8aab951f26b4dc4feb531535504f Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 29 Oct 2020 11:33:31 +0100 Subject: [PATCH 17/27] Example doesn't have a public directory. --- examples/vue-cli/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/vue-cli/package.json b/examples/vue-cli/package.json index 5e9ec8cf5cb8..b43be40f49e1 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", From a1b6344d7efc56254ec3088452399b3c63bacfcb Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 29 Oct 2020 11:34:43 +0100 Subject: [PATCH 18/27] Use prebuilt when building static storybook. --- lib/core/package.json | 1 + lib/core/src/server/build-static.js | 32 ++++----- lib/core/src/server/dev-server.js | 47 ++---------- lib/core/src/server/utils/prebuilt-manager.js | 44 ++++++++++++ yarn.lock | 71 ++++++++++++++++++- 5 files changed, 134 insertions(+), 61 deletions(-) create mode 100644 lib/core/src/server/utils/prebuilt-manager.js diff --git a/lib/core/package.json b/lib/core/package.json index 237a945065ef..cc5d73eabf37 100644 --- a/lib/core/package.json +++ b/lib/core/package.json @@ -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", diff --git a/lib/core/src/server/build-static.js b/lib/core/src/server/build-static.js index 34ee56f82475..6ae5b9c4d186 100644 --- a/lib/core/src/server/build-static.js +++ b/lib/core/src/server/build-static.js @@ -1,3 +1,4 @@ +import cpy from 'cpy'; import fs from 'fs-extra'; import path from 'path'; import webpack from 'webpack'; @@ -10,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..'); @@ -163,18 +165,6 @@ async function buildPreview(configType, outputDir, packageJson, options) { return compilePreview(previewConfig, previewStartTime); } -async function prepareFilesStructure(outputDir, defaultFavIcon) { - // clear the output dir - logger.info('=> Cleaning outputDir..'); - await trash(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; @@ -182,17 +172,23 @@ 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'); - await 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')); - - await buildManager(configType, outputDir, configDir, options); + 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); + } if (options.managerOnly) { logger.info(`=> Not building preview`); diff --git a/lib/core/src/server/dev-server.js b/lib/core/src/server/dev-server.js index 439935573c32..30a8ce7aacd8 100644 --- a/lib/core/src/server/dev-server.js +++ b/lib/core/src/server/dev-server.js @@ -17,10 +17,9 @@ import webpackHotMiddleware from 'webpack-hot-middleware'; import { getMiddleware } from './utils/middleware'; import { logConfig } from './logConfig'; import loadConfig from './config'; -import loadManagerConfig, { getAutoRefs } from './manager/manager-config'; -import { getInterpretedFile } from './utils/interpret-files'; -import { loadManagerOrAddonsFile } from './utils/load-manager-or-addons-file'; +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'); @@ -117,37 +116,6 @@ function openInBrowser(address) { const router = new Router(); -const canUsePrebuiltManager = async ({ prebuiltDir, configDir, options }) => { - if (!options.managerCache) return false; - - 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; - - // Addons automatically installed when running `sb init` (see baseGenerator.ts) - const defaultAddons = ['@storybook/addon-links', '@storybook/addon-essentials']; - // Addons we can safely ignore because they don't affect the manager - const ignoredAddons = ['@storybook/preset-create-react-app', ...defaultAddons]; - - // eslint-disable-next-line global-require, import/no-dynamic-require - const { addons, refs, managerBabel, managerWebpack } = require(mainConfigFile); - if (!addons || refs || managerBabel || managerWebpack) return false; - if (defaultAddons.some((addon) => !addons.includes(addon))) return false; - if (addons.some((addon) => !ignoredAddons.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 true; -}; - const printDuration = (startTime) => prettyTime(process.hrtime(startTime)) .replace(' ms', ' milliseconds') @@ -206,10 +174,10 @@ const startManager = async ({ configType, outputDir, configDir, - usePrebuilt, + prebuiltDir, }) => { let managerConfig; - if (!usePrebuilt) { + if (!prebuiltDir) { // this is pretty slow managerConfig = await loadManagerConfig({ configType, @@ -352,11 +320,10 @@ export async function storybookDevServer(options) { server.listen({ port, host }, (error) => (error ? reject(error) : resolve())); }); - const prebuiltDir = path.join(__dirname, '../../prebuilt'); - const usePrebuilt = await canUsePrebuiltManager({ prebuiltDir, configDir, options }); + const prebuiltDir = await getPrebuiltDir({ configDir, options }); // Manager static files - router.use('/', express.static(usePrebuilt ? prebuiltDir : outputDir)); + router.use('/', express.static(prebuiltDir || outputDir)); // TODO remove when we drop DLLs router.get(/\/sb_dll\/(.+\.js)$/, (request, response) => { @@ -373,7 +340,7 @@ export async function storybookDevServer(options) { // 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, usePrebuilt }) + startManager({ startTime, options, configType, outputDir, configDir, prebuiltDir }) .then((result) => { if (!options.ci) openInBrowser(address); return result; 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..baa0b480f249 --- /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'; + +// 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; + + // eslint-disable-next-line global-require, import/no-dynamic-require + const { addons, refs, managerBabel, managerWebpack } = require(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/yarn.lock b/yarn.lock index 7de619184617..9b08dba6281b 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== From 908e2f567a9f9f237690de9cd9ed8954b785b934 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 29 Oct 2020 11:45:21 +0100 Subject: [PATCH 19/27] Drop story names. --- lib/components/src/Loader/Loader.stories.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/components/src/Loader/Loader.stories.tsx b/lib/components/src/Loader/Loader.stories.tsx index eae94d126946..8a979527ce76 100644 --- a/lib/components/src/Loader/Loader.stories.tsx +++ b/lib/components/src/Loader/Loader.stories.tsx @@ -32,14 +32,11 @@ export default { }; export const InfiniteState = () => ; -InfiniteState.storyName = 'infinite state'; InfiniteState.decorators = [withBackground]; export const SizeAdjusted = () => ; -SizeAdjusted.storyName = 'size adjusted'; SizeAdjusted.decorators = [withBackground]; export const ProgressBar = () => ( ); -ProgressBar.storyName = 'progress bar'; From 13f65c069743b4866d54354f604817d971746a92 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 29 Oct 2020 12:07:05 +0100 Subject: [PATCH 20/27] Don't lint the prebuilt files. --- .eslintignore | 1 + 1 file changed, 1 insertion(+) 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 From 7be47ba4932bf12c8ba65d060e9aa5c27dbcf8a5 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 30 Oct 2020 10:20:45 +0100 Subject: [PATCH 21/27] Use serverRequire so that TypeScript gets interpreted. --- lib/core/src/server/utils/interpret-files.js | 4 ++-- lib/core/src/server/utils/interpret-files.test.js | 11 +++++++++++ lib/core/src/server/utils/prebuilt-manager.js | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) 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/prebuilt-manager.js b/lib/core/src/server/utils/prebuilt-manager.js index baa0b480f249..cf82feb44b59 100644 --- a/lib/core/src/server/utils/prebuilt-manager.js +++ b/lib/core/src/server/utils/prebuilt-manager.js @@ -4,6 +4,7 @@ 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']; @@ -29,8 +30,7 @@ export const getPrebuiltDir = async ({ configDir, options }) => { const mainConfigFile = getInterpretedFile(path.resolve(configDir, 'main')); if (!mainConfigFile) return false; - // eslint-disable-next-line global-require, import/no-dynamic-require - const { addons, refs, managerBabel, managerWebpack } = require(mainConfigFile); + 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; From 6eddd079a0e858a0544eb1b01779482a7551fcfa Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 30 Oct 2020 11:19:45 +0100 Subject: [PATCH 22/27] Show message when connection is closed by the server. --- lib/components/src/Loader/Loader.stories.tsx | 2 + lib/components/src/Loader/Loader.tsx | 47 +++++++++++++++----- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/lib/components/src/Loader/Loader.stories.tsx b/lib/components/src/Loader/Loader.stories.tsx index 8a979527ce76..a3f8db8a1cac 100644 --- a/lib/components/src/Loader/Loader.stories.tsx +++ b/lib/components/src/Loader/Loader.stories.tsx @@ -40,3 +40,5 @@ 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 a9e07a35ffc8..f6e4aeaaa9cc 100644 --- a/lib/components/src/Loader/Loader.tsx +++ b/lib/components/src/Loader/Loader.tsx @@ -1,6 +1,7 @@ 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 }) => ({ @@ -62,6 +63,11 @@ const ProgressMessage = styled.div(({ theme }) => ({ color: theme.barTextColor, })); +const ErrorIcon = styled(Icons)({ + width: 20, + height: 20, +}); + const ellipsis = keyframes` from { content: "..." } 33% { content: "." } @@ -80,21 +86,33 @@ const Ellipsis = styled.span({ }, }); -interface LoaderProps { - progress?: { - value: number; - message: string; - modules?: { - complete: number; - total: number; - }; +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, size, ...props }) => { +> = ({ progress, error, size, ...props }) => { + if (error) { + return ( + + + {error.message} + + ); + } + if (progress) { const { value, modules } = progress; let { message } = progress; @@ -120,6 +138,7 @@ export const PureLoader: FunctionComponent< ); } + return ( > = (props) => { const [progress, setProgress] = useState(undefined); + const [error, setError] = useState(undefined); useEffect(() => { const eventSource = new EventSource('/progress'); @@ -140,11 +160,16 @@ export const Loader: FunctionComponent> = (pro try { setProgress(JSON.parse(event.data)); } catch (e) { - // do nothing + setError(e); + eventSource.close(); } }; + eventSource.onerror = () => { + setError(new Error('Connection closed')); + eventSource.close(); + }; return () => eventSource.close(); }, []); - return ; + return ; }; From effba9e3677f367b1d00d274c6be7e891bdde758 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 30 Oct 2020 13:01:12 +0100 Subject: [PATCH 23/27] Nicer icon. --- lib/components/src/Loader/Loader.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/components/src/Loader/Loader.tsx b/lib/components/src/Loader/Loader.tsx index f6e4aeaaa9cc..b38bc9638861 100644 --- a/lib/components/src/Loader/Loader.tsx +++ b/lib/components/src/Loader/Loader.tsx @@ -40,6 +40,7 @@ const ProgressWrapper = styled.div({ const ProgressTrack = styled.div(({ theme }) => ({ position: 'relative', width: '80%', + marginBottom: '0.75rem', maxWidth: 300, height: 5, borderRadius: 5, @@ -58,15 +59,16 @@ const ProgressBar = styled.div(({ theme }) => ({ const ProgressMessage = styled.div(({ theme }) => ({ minHeight: '2em', - marginTop: '0.75rem', fontSize: `${theme.typography.size.s1}px`, color: theme.barTextColor, })); -const ErrorIcon = styled(Icons)({ +const ErrorIcon = styled(Icons)(({ theme }) => ({ width: 20, height: 20, -}); + marginBottom: '0.5rem', + color: theme.color.mediumdark, +})); const ellipsis = keyframes` from { content: "..." } @@ -107,7 +109,7 @@ export const PureLoader: FunctionComponent< if (error) { return ( - + {error.message} ); From fa5c843aa953369be8c85a3947f312d04a5c40ef Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 30 Oct 2020 15:01:15 +0100 Subject: [PATCH 24/27] Hide addon panel while loading. --- lib/ui/src/components/preview/preview.tsx | 7 +++---- lib/ui/src/index.tsx | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-) 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/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 ( From ac446513aa9812e2ba064e67853acda1d138b5c7 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 30 Oct 2020 15:16:55 +0100 Subject: [PATCH 25/27] Make sure we don't show the Connection closed message when the progress completes succesfully. --- lib/components/src/Loader/Loader.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/components/src/Loader/Loader.tsx b/lib/components/src/Loader/Loader.tsx index b38bc9638861..912d5a3c448a 100644 --- a/lib/components/src/Loader/Loader.tsx +++ b/lib/components/src/Loader/Loader.tsx @@ -158,16 +158,18 @@ export const Loader: FunctionComponent> = (pro useEffect(() => { const eventSource = new EventSource('/progress'); + let lastProgress: Progress; eventSource.onmessage = (event: any) => { try { - setProgress(JSON.parse(event.data)); + lastProgress = JSON.parse(event.data); + setProgress(lastProgress); } catch (e) { setError(e); eventSource.close(); } }; eventSource.onerror = () => { - setError(new Error('Connection closed')); + if (lastProgress?.value !== 1) setError(new Error('Connection closed')); eventSource.close(); }; return () => eventSource.close(); From 1b48c989edba7584e4e317b2bbcebd82d613ee65 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Fri, 30 Oct 2020 22:50:20 +0800 Subject: [PATCH 26/27] Don't collect coverage on prebuilt bundles --- jest.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/jest.config.js b/jest.config.js index ca0a8d3cd3fb..f6b696d64be5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -71,6 +71,7 @@ module.exports = { '/__mocks__ /', '/__testfixtures__/', '^.*\\.stories\\.[jt]sx?$', + '/prebuilt/', ], globals: { DOCS_MODE: false, From 62f8691984111a23afc27ad98b8b26002b67b6a9 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 30 Oct 2020 15:54:56 +0100 Subject: [PATCH 27/27] Also ignore prebuilt as potential test path. --- jest.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index f6b696d64be5..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,12 +67,12 @@ module.exports = { '/node_modules/', '/cli/test/', '/dist/', + '/prebuilt/', '/generators/', '/dll/', '/__mocks__ /', '/__testfixtures__/', '^.*\\.stories\\.[jt]sx?$', - '/prebuilt/', ], globals: { DOCS_MODE: false,