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