From 165732527ab56ad601efe42a941bb0859ed2c4d6 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 4 Feb 2021 15:54:39 +0100 Subject: [PATCH] have a webpack 4 & 5 version of a somewhat minimized builder package --- lib/builder-webpack4/client.js | 1 - lib/builder-webpack4/src/build-dev.test.ts | 80 --- lib/builder-webpack4/src/build-dev.ts | 348 ------------- lib/builder-webpack4/src/build-static.ts | 219 -------- lib/builder-webpack4/src/cli/dev.ts | 103 ---- lib/builder-webpack4/src/cli/index.ts | 2 - lib/builder-webpack4/src/cli/prod.ts | 59 --- lib/builder-webpack4/src/cli/utils.ts | 35 -- .../src/common/custom-presets.ts | 17 - .../src/{config => common}/utils.ts | 0 lib/builder-webpack4/src/config.test.ts | 21 - lib/builder-webpack4/src/config.ts | 64 --- lib/builder-webpack4/src/dev-server.test.ts | 23 - lib/builder-webpack4/src/dev-server.ts | 406 --------------- lib/builder-webpack4/src/index.ts | 71 ++- .../src/{logConfig.ts => logger.ts} | 0 .../src/manager/babel-loader-manager.ts | 24 - .../src/manager/manager-config.ts | 177 ------- .../src/manager/manager-preset.ts | 33 -- .../src/manager/manager-webpack.config.ts | 185 ------- .../src/manager/virtualModuleRef.template.ts | 5 - lib/builder-webpack4/src/presets.test.ts | 493 ------------------ lib/builder-webpack4/src/presets.ts | 263 ---------- .../src/preview/babel-loader-preview.ts | 2 +- lib/builder-webpack4/src/preview/entries.ts | 2 +- .../src/preview/iframe-webpack.config.ts | 17 +- lib/builder-webpack4/src/public/favicon.ico | Bin 32988 -> 0 bytes lib/builder-webpack4/src/standalone.ts | 27 - .../src/{utils => }/template.ts | 26 +- .../src/templates/base-manager-head.html | 46 -- lib/builder-webpack4/src/types.ts | 116 +---- .../merge-webpack-config.test.ts.snap | 150 ------ .../src/utils/interpret-files.test.ts | 48 -- .../src/utils/load-manager-or-addons-file.ts | 24 - .../src/utils/merge-webpack-config.test.ts | 93 ---- .../src/utils/merge-webpack-config.ts | 81 --- lib/builder-webpack4/src/utils/middleware.ts | 14 - .../src/utils/prebuilt-manager.ts | 50 -- .../src/utils/resolve-file.ts | 1 - .../src/utils/static-files.test.ts | 84 --- .../src/utils/static-files.ts | 24 - .../src/utils/template.test.ts | 88 ---- .../src/utils/validate-configuration-files.ts | 70 --- lib/builder-webpack4/types/index.ts | 79 --- lib/builder-webpack4/typings.d.ts | 34 +- lib/core-server/src/dev-server.ts | 2 +- 46 files changed, 72 insertions(+), 3635 deletions(-) delete mode 100644 lib/builder-webpack4/client.js delete mode 100644 lib/builder-webpack4/src/build-dev.test.ts delete mode 100644 lib/builder-webpack4/src/build-dev.ts delete mode 100644 lib/builder-webpack4/src/build-static.ts delete mode 100644 lib/builder-webpack4/src/cli/dev.ts delete mode 100644 lib/builder-webpack4/src/cli/index.ts delete mode 100644 lib/builder-webpack4/src/cli/prod.ts delete mode 100644 lib/builder-webpack4/src/cli/utils.ts delete mode 100644 lib/builder-webpack4/src/common/custom-presets.ts rename lib/builder-webpack4/src/{config => common}/utils.ts (100%) delete mode 100644 lib/builder-webpack4/src/config.test.ts delete mode 100644 lib/builder-webpack4/src/config.ts delete mode 100644 lib/builder-webpack4/src/dev-server.test.ts delete mode 100644 lib/builder-webpack4/src/dev-server.ts rename lib/builder-webpack4/src/{logConfig.ts => logger.ts} (100%) delete mode 100644 lib/builder-webpack4/src/manager/babel-loader-manager.ts delete mode 100644 lib/builder-webpack4/src/manager/manager-config.ts delete mode 100644 lib/builder-webpack4/src/manager/manager-preset.ts delete mode 100644 lib/builder-webpack4/src/manager/manager-webpack.config.ts delete mode 100644 lib/builder-webpack4/src/manager/virtualModuleRef.template.ts delete mode 100644 lib/builder-webpack4/src/presets.test.ts delete mode 100644 lib/builder-webpack4/src/presets.ts delete mode 100644 lib/builder-webpack4/src/public/favicon.ico delete mode 100644 lib/builder-webpack4/src/standalone.ts rename lib/builder-webpack4/src/{utils => }/template.ts (57%) delete mode 100644 lib/builder-webpack4/src/templates/base-manager-head.html delete mode 100644 lib/builder-webpack4/src/utils/__snapshots__/merge-webpack-config.test.ts.snap delete mode 100644 lib/builder-webpack4/src/utils/interpret-files.test.ts delete mode 100644 lib/builder-webpack4/src/utils/load-manager-or-addons-file.ts delete mode 100644 lib/builder-webpack4/src/utils/merge-webpack-config.test.ts delete mode 100644 lib/builder-webpack4/src/utils/merge-webpack-config.ts delete mode 100644 lib/builder-webpack4/src/utils/middleware.ts delete mode 100644 lib/builder-webpack4/src/utils/prebuilt-manager.ts delete mode 100644 lib/builder-webpack4/src/utils/resolve-file.ts delete mode 100644 lib/builder-webpack4/src/utils/static-files.test.ts delete mode 100644 lib/builder-webpack4/src/utils/static-files.ts delete mode 100644 lib/builder-webpack4/src/utils/template.test.ts delete mode 100644 lib/builder-webpack4/src/utils/validate-configuration-files.ts delete mode 100644 lib/builder-webpack4/types/index.ts diff --git a/lib/builder-webpack4/client.js b/lib/builder-webpack4/client.js deleted file mode 100644 index c0c71ee796d..00000000000 --- a/lib/builder-webpack4/client.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/cjs/client').default; diff --git a/lib/builder-webpack4/src/build-dev.test.ts b/lib/builder-webpack4/src/build-dev.test.ts deleted file mode 100644 index 87bca239429..00000000000 --- a/lib/builder-webpack4/src/build-dev.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { getReleaseNotesData, RELEASE_NOTES_CACHE_KEY } from './build-dev'; - -describe('getReleaseNotesData', () => { - it('handles errors gracefully', async () => { - const version = '4.0.0'; - // The cache is missing necessary functions. This will cause an error. - const cache = {}; - - expect(await getReleaseNotesData(version, cache)).toEqual({ - currentVersion: version, - showOnFirstLaunch: false, - success: false, - }); - }); - - it('does not show the release notes on first build', async () => { - const version = '4.0.0'; - const set = jest.fn((...args: any[]) => Promise.resolve()); - const cache = { get: () => Promise.resolve([]), set }; - - expect(await getReleaseNotesData(version, cache)).toEqual({ - currentVersion: version, - showOnFirstLaunch: false, - success: true, - }); - expect(set).toHaveBeenCalledWith(RELEASE_NOTES_CACHE_KEY, ['4.0.0']); - }); - - it('shows the release notes after upgrading a major version', async () => { - const version = '4.0.0'; - const set = jest.fn((...args: any[]) => Promise.resolve()); - const cache = { get: () => Promise.resolve(['3.0.0']), set }; - - expect(await getReleaseNotesData(version, cache)).toEqual({ - currentVersion: version, - showOnFirstLaunch: true, - success: true, - }); - expect(set).toHaveBeenCalledWith(RELEASE_NOTES_CACHE_KEY, ['3.0.0', '4.0.0']); - }); - - it('shows the release notes after upgrading a minor version', async () => { - const version = '4.1.0'; - const set = jest.fn((...args: any[]) => Promise.resolve()); - const cache = { get: () => Promise.resolve(['4.0.0']), set }; - - expect(await getReleaseNotesData(version, cache)).toEqual({ - currentVersion: version, - showOnFirstLaunch: true, - success: true, - }); - expect(set).toHaveBeenCalledWith(RELEASE_NOTES_CACHE_KEY, ['4.0.0', '4.1.0']); - }); - - it('transforms patch versions to the closest major.minor version', async () => { - const version = '4.0.1'; - const set = jest.fn((...args: any[]) => Promise.resolve()); - const cache = { get: () => Promise.resolve(['4.0.0']), set }; - - expect(await getReleaseNotesData(version, cache)).toEqual({ - currentVersion: '4.0.0', - showOnFirstLaunch: false, - success: true, - }); - expect(set).not.toHaveBeenCalled(); - }); - - it('does not show release notes when downgrading', async () => { - const version = '3.0.0'; - const set = jest.fn((...args: any[]) => Promise.resolve()); - const cache = { get: () => Promise.resolve(['4.0.0']), set }; - - expect(await getReleaseNotesData(version, cache)).toEqual({ - currentVersion: '3.0.0', - showOnFirstLaunch: false, - success: true, - }); - expect(set).toHaveBeenCalledWith(RELEASE_NOTES_CACHE_KEY, ['4.0.0', '3.0.0']); - }); -}); diff --git a/lib/builder-webpack4/src/build-dev.ts b/lib/builder-webpack4/src/build-dev.ts deleted file mode 100644 index c8c96069af4..00000000000 --- a/lib/builder-webpack4/src/build-dev.ts +++ /dev/null @@ -1,348 +0,0 @@ -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 boxen from 'boxen'; -import semver from '@storybook/semver'; -import dedent from 'ts-dedent'; -import Table from 'cli-table3'; -import prettyTime from 'pretty-hrtime'; -import prompts from 'prompts'; -import detectFreePort from 'detect-port'; - -import { Stats } from 'webpack'; -import { storybookDevServer } from './dev-server'; -import { DevCliOptions, getDevCli } from './cli'; -import { resolvePathInStorybookCache } from './utils/resolve-path-in-sb-cache'; -import { ReleaseNotesData, VersionCheck, PackageJson, LoadOptions } from './types'; - -const { STORYBOOK_VERSION_BASE = 'https://storybook.js.org' } = process.env; - -const cache = Cache({ - basePath: resolvePathInStorybookCache('dev-server'), - ns: 'storybook', // Optional. A grouping namespace for items. -}); - -const writeStats = async (name: string, stats: Stats) => { - const filePath = resolvePathInStorybookCache(`public/${name}-stats.json`); - await fs.writeFile(filePath, JSON.stringify(stats.toJson(), null, 2), 'utf8'); - return filePath; -}; - -const getFreePort = (port: number) => - detectFreePort(port).catch((error) => { - logger.error(error); - process.exit(-1); - }); - -const updateCheck = async (version: string): Promise => { - let result; - const time = Date.now(); - try { - const fromCache = await cache.get('lastUpdateCheck', { success: false, time: 0 }); - - // if last check was more then 24h ago - if (time - 86400000 > fromCache.time) { - const fromFetch: any = await Promise.race([ - fetch(`${STORYBOOK_VERSION_BASE}/versions.json?current=${version}`), - // if fetch is too slow, we won't wait for it - new Promise((res, rej) => global.setTimeout(rej, 1500)), - ]); - const data = await fromFetch.json(); - result = { success: true, data, time }; - await cache.set('lastUpdateCheck', result); - } else { - result = fromCache; - } - } catch (error) { - result = { success: false, error, time }; - } - return result; -}; - -// We only expect to have release notes available for major and minor releases. -// For this reason, we convert the actual version of the build here so that -// every place that relies on this data can reference the version of the -// release notes that we expect to use. -const getReleaseNotesVersion = (version: string): string => { - const { major, minor } = semver.parse(version); - const { version: releaseNotesVersion } = semver.coerce(`${major}.${minor}`); - return releaseNotesVersion; -}; - -const getReleaseNotesFailedState = (version: string) => { - return { - success: false, - currentVersion: getReleaseNotesVersion(version), - showOnFirstLaunch: false, - }; -}; - -export const RELEASE_NOTES_CACHE_KEY = 'releaseNotesData'; - -export const getReleaseNotesData = async ( - currentVersionToParse: string, - fileSystemCache: any -): Promise => { - let result; - try { - const fromCache = (await fileSystemCache.get('releaseNotesData', []).catch(() => {})) || []; - const releaseNotesVersion = getReleaseNotesVersion(currentVersionToParse); - const versionHasNotBeenSeen = !fromCache.includes(releaseNotesVersion); - - if (versionHasNotBeenSeen) { - await fileSystemCache.set('releaseNotesData', [...fromCache, releaseNotesVersion]); - } - - const sortedHistory = semver.sort(fromCache); - const highestVersionSeenInThePast = sortedHistory.slice(-1)[0]; - - let isUpgrading = false; - let isMajorOrMinorDiff = false; - - if (highestVersionSeenInThePast) { - isUpgrading = semver.gt(releaseNotesVersion, highestVersionSeenInThePast); - const versionDiff = semver.diff(releaseNotesVersion, highestVersionSeenInThePast); - isMajorOrMinorDiff = versionDiff === 'major' || versionDiff === 'minor'; - } - - result = { - success: true, - showOnFirstLaunch: - versionHasNotBeenSeen && - // Only show the release notes if this is not the first time Storybook - // has been built. - !!highestVersionSeenInThePast && - isUpgrading && - isMajorOrMinorDiff, - currentVersion: releaseNotesVersion, - }; - } catch (e) { - result = getReleaseNotesFailedState(currentVersionToParse); - } - return result; -}; - -function createUpdateMessage(updateInfo: VersionCheck, version: string): string { - let updateMessage; - - try { - const suffix = semver.prerelease(updateInfo.data.latest.version) ? '--prerelease' : ''; - const upgradeCommand = `npx sb@latest upgrade ${suffix}`.trim(); - updateMessage = - updateInfo.success && semver.lt(version, updateInfo.data.latest.version) - ? dedent` - ${colors.orange( - `A new version (${chalk.bold(updateInfo.data.latest.version)}) is available!` - )} - - ${chalk.gray('Upgrade now:')} ${colors.green(upgradeCommand)} - - ${chalk.gray('Read full changelog:')} ${chalk.gray.underline('https://git.io/fhFYe')} - ` - : ''; - } catch (e) { - updateMessage = ''; - } - return updateMessage; -} - -function outputStartupInformation(options: { - updateInfo: VersionCheck; - version: string; - address: string; - networkAddress: string; - managerTotalTime?: [number, number]; - previewTotalTime?: [number, number]; -}) { - const { - updateInfo, - version, - address, - networkAddress, - managerTotalTime, - previewTotalTime, - } = options; - - const updateMessage = createUpdateMessage(updateInfo, version); - - const serveMessage = new Table({ - chars: { - top: '', - 'top-mid': '', - 'top-left': '', - 'top-right': '', - bottom: '', - 'bottom-mid': '', - 'bottom-left': '', - 'bottom-right': '', - left: '', - 'left-mid': '', - mid: '', - 'mid-mid': '', - right: '', - 'right-mid': '', - middle: '', - }, - // @ts-ignore - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - paddingBottom: 0, - }); - - serveMessage.push( - ['Local:', chalk.cyan(address)], - ['On your network:', chalk.cyan(networkAddress)] - ); - - 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( - boxen( - dedent` - ${colors.green(`Storybook ${chalk.bold(version)} started`)} - ${chalk.gray(timeStatement)} - - ${serveMessage.toString()}${updateMessage ? `\n\n${updateMessage}` : ''} - `, - { borderStyle: 'round', padding: 1, borderColor: '#F1618C' } as any - ) - ); -} - -async function outputStats(previewStats: Stats, managerStats: Stats) { - if (previewStats) { - const filePath = await writeStats('preview', previewStats); - logger.info(`=> preview stats written to ${chalk.cyan(filePath)}`); - } - if (managerStats) { - const filePath = await writeStats('manager', managerStats); - logger.info(`=> manager stats written to ${chalk.cyan(filePath)}`); - } -} - -export async function buildDevStandalone( - options: DevCliOptions & - LoadOptions & { - packageJson: PackageJson; - ignorePreview: boolean; - docsMode: boolean; - configDir: string; - cache: any; - } -) { - try { - 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 - ? updateCheck(version) - : Promise.resolve({ success: false, data: {}, time: Date.now() }), - releaseNotes - ? getReleaseNotesData(version, cache) - : Promise.resolve(getReleaseNotesFailedState(version)), - ]); - - if (!options.ci && !options.smokeTest && options.port != null && port !== options.port) { - const { shouldChangePort } = await prompts({ - type: 'confirm', - initial: true, - name: 'shouldChangePort', - message: `Port ${options.port} is not available. Would you like to run Storybook on port ${port} instead?`, - }); - if (!shouldChangePort) process.exit(1); - } - - /* eslint-disable no-param-reassign */ - options.port = port; - // @ts-ignore - options.versionCheck = updateInfo; - // @ts-ignore - options.releaseNotesData = releaseNotesData; - /* eslint-enable no-param-reassign */ - - const { - address, - networkAddress, - previewStats, - managerStats, - managerTotalTime, - previewTotalTime, - } = await storybookDevServer(options); - - if (options.smokeTest) { - await outputStats(previewStats, managerStats); - const hasManagerWarnings = managerStats && managerStats.toJson().warnings.length > 0; - const hasPreviewWarnings = previewStats && previewStats.toJson().warnings.length > 0; - process.exit(hasManagerWarnings || (hasPreviewWarnings && !options.ignorePreview) ? 1 : 0); - return; - } - - outputStartupInformation({ - updateInfo, - version, - address, - networkAddress, - managerTotalTime, - previewTotalTime, - }); - } catch (error) { - // this is a weird bugfix, somehow 'node-pre-gyp' is polluting the npmLog header - npmLog.heading = ''; - - if (error instanceof Error) { - if ((error as any).error) { - logger.error((error as any).error); - } else if ((error as any).stats && (error as any).stats.compilation.errors) { - (error as any).stats.compilation.errors.forEach((e: any) => logger.plain(e)); - } else { - logger.error(error as any); - } - } - logger.line(); - logger.warn( - error.close - ? dedent` - FATAL broken build!, will close the process, - Fix the error below and restart storybook. - ` - : dedent` - Broken build, fix the error above. - You may need to refresh the browser. - ` - ); - logger.line(); - - if (options.smokeTest || (error && error.close)) { - process.exit(1); - } - } -} - -export async function buildDev({ - packageJson, - ...loadOptions -}: { packageJson: PackageJson } & LoadOptions) { - const cliOptions = await getDevCli(packageJson); - - await buildDevStandalone({ - ...cliOptions, - ...loadOptions, - packageJson, - configDir: (loadOptions as any).configDir || cliOptions.configDir || './.storybook', - ignorePreview: !!cliOptions.previewUrl, - docsMode: !!cliOptions.docs, - cache, - }); -} diff --git a/lib/builder-webpack4/src/build-static.ts b/lib/builder-webpack4/src/build-static.ts deleted file mode 100644 index 36d16c7f42f..00000000000 --- a/lib/builder-webpack4/src/build-static.ts +++ /dev/null @@ -1,219 +0,0 @@ -import chalk from 'chalk'; -import cpy from 'cpy'; -import fs from 'fs-extra'; -import path from 'path'; -import webpack, { Configuration } from 'webpack'; - -import { logger } from '@storybook/node-logger'; - -import { getProdCli } from './cli'; -import loadConfig from './config'; -import loadManagerConfig from './manager/manager-config'; -import { logConfig } from './logConfig'; -import { getPrebuiltDir } from './utils/prebuilt-manager'; -import { parseStaticDir } from './utils/static-files'; - -async function compileManager(managerConfig: Configuration, managerStartTime: [number, number]) { - logger.info('=> Compiling manager..'); - - return new Promise((resolve, reject) => { - webpack(managerConfig).run((error, stats) => { - if (error || !stats || stats.hasErrors()) { - logger.error('=> Failed to build the manager'); - - if (error) { - logger.error(error.message); - } - - if (stats && (stats.hasErrors() || stats.hasWarnings())) { - const { warnings, errors } = stats.toJson(managerConfig.stats); - - errors.forEach((e: string) => logger.error(e)); - warnings.forEach((e: string) => logger.error(e)); - } - - process.exitCode = 1; - reject(error || stats); - return; - } - - logger.trace({ message: '=> Manager built', time: process.hrtime(managerStartTime) }); - stats.toJson(managerConfig.stats).warnings.forEach((e: string) => logger.warn(e)); - - resolve(stats); - }); - }); -} - -async function watchPreview(previewConfig: any) { - logger.info('=> Compiling preview in watch mode..'); - - return new Promise(() => { - webpack(previewConfig).watch( - { - aggregateTimeout: 1, - }, - (error, stats) => { - if (!error) { - const statsConfig = previewConfig.stats ? previewConfig.stats : { colors: true }; - - // eslint-disable-next-line no-console - console.log(stats.toString(statsConfig)); - } else { - logger.error(error.message); - } - } - ); - }); -} - -async function compilePreview(previewConfig: Configuration, previewStartTime: [number, number]) { - logger.info('=> Compiling preview..'); - - return new Promise((resolve, reject) => { - webpack(previewConfig).run((error, stats) => { - if (error || !stats || stats.hasErrors()) { - logger.error('=> Failed to build the preview'); - process.exitCode = 1; - - if (error) { - logger.error(error.message); - return reject(error); - } - - if (stats && (stats.hasErrors() || stats.hasWarnings())) { - const { warnings, errors } = stats.toJson(previewConfig.stats); - - errors.forEach((e: string) => logger.error(e)); - warnings.forEach((e: string) => logger.error(e)); - return reject(stats); - } - } - - logger.trace({ message: '=> Preview built', time: process.hrtime(previewStartTime) }); - if (stats) { - stats.toJson(previewConfig.stats).warnings.forEach((e: string) => logger.warn(e)); - } - - return resolve(stats); - }); - }); -} - -async function copyAllStaticFiles(staticDirs: any[] | undefined, outputDir: string) { - if (staticDirs && staticDirs.length > 0) { - await Promise.all( - staticDirs.map(async (dir) => { - try { - const { staticDir, staticPath, targetDir } = await parseStaticDir(dir); - const targetPath = path.join(outputDir, targetDir); - logger.info(chalk`=> Copying static files: {cyan ${staticDir}} => {cyan ${targetDir}}`); - - // Storybook's own files should not be overwritten, so we skip such files if we find them - const skipPaths = ['index.html', 'iframe.html'].map((f) => path.join(targetPath, f)); - await fs.copy(staticPath, targetPath, { filter: (_, dest) => !skipPaths.includes(dest) }); - } catch (e) { - logger.error(e.message); - process.exit(-1); - } - }) - ); - } -} - -async function buildManager(configType: any, outputDir: string, configDir: string, options: any) { - logger.info('=> Building manager..'); - const managerStartTime = process.hrtime(); - - logger.info('=> Loading manager config..'); - const managerConfig = await loadManagerConfig({ - ...options, - configType, - outputDir, - configDir, - corePresets: [require.resolve('./manager/manager-preset.js')], - }); - - if (options.debugWebpack) { - logConfig('Manager webpack config', managerConfig); - } - - return compileManager(managerConfig, managerStartTime); -} - -async function buildPreview(configType: any, outputDir: string, packageJson: any, options: any) { - const { watch, debugWebpack } = options; - - logger.info('=> Building preview..'); - const previewStartTime = process.hrtime(); - - logger.info('=> Loading preview config..'); - const previewConfig = await loadConfig({ - ...options, - configType, - outputDir, - packageJson, - corePresets: [require.resolve('./preview/preview-preset.js')], - overridePresets: [require.resolve('./preview/custom-webpack-preset.js')], - }); - - if (debugWebpack) { - logConfig('Preview webpack config', previewConfig); - } - - if (watch) { - return watchPreview(previewConfig); - } - - return compilePreview(previewConfig, previewStartTime); -} - -export async function buildStaticStandalone(options: any) { - const { staticDir, configDir, packageJson } = options; - - const configType = 'PRODUCTION'; - const outputDir = path.isAbsolute(options.outputDir) - ? options.outputDir - : path.join(process.cwd(), options.outputDir); - - const defaultFavIcon = require.resolve('./public/favicon.ico'); - - logger.info(chalk`=> Cleaning outputDir: {cyan ${outputDir}}`); - if (outputDir === '/') throw new Error("Won't remove directory '/'. Check your outputDir!"); - await fs.emptyDir(outputDir); - - await cpy(defaultFavIcon, outputDir); - await copyAllStaticFiles(staticDir, outputDir); - - const prebuiltDir = await getPrebuiltDir({ configDir, options }); - if (prebuiltDir) { - await cpy('**', outputDir, { cwd: prebuiltDir, parents: true }); - } else { - await buildManager(configType, outputDir, configDir, options); - } - - if (options.managerOnly) { - logger.info(`=> Not building preview`); - } else { - await buildPreview(configType, outputDir, packageJson, options); - } - - logger.info(`=> Output directory: ${outputDir}`); -} - -export function buildStatic({ packageJson, ...loadOptions }: any) { - const cliOptions = getProdCli(packageJson); - - return buildStaticStandalone({ - ...cliOptions, - ...loadOptions, - packageJson, - configDir: loadOptions.configDir || cliOptions.configDir || './.storybook', - outputDir: loadOptions.outputDir || cliOptions.outputDir || './storybook-static', - ignorePreview: !!cliOptions.previewUrl, - docsMode: !!cliOptions.docs, - }).catch((e) => { - logger.error(e); - process.exit(1); - }); -} diff --git a/lib/builder-webpack4/src/cli/dev.ts b/lib/builder-webpack4/src/cli/dev.ts deleted file mode 100644 index 8857220f165..00000000000 --- a/lib/builder-webpack4/src/cli/dev.ts +++ /dev/null @@ -1,103 +0,0 @@ -import program, { CommanderStatic } from 'commander'; -import chalk from 'chalk'; -import { logger } from '@storybook/node-logger'; -import { parseList, getEnvConfig, checkDeprecatedFlags } from './utils'; - -export interface DevCliOptions { - port?: number; - host?: string; - staticDir?: string[]; - configDir?: string; - https?: boolean; - sslCa?: string[]; - sslCert?: string; - sslKey?: string; - smokeTest?: boolean; - ci?: boolean; - loglevel?: string; - quiet?: boolean; - versionUpdates?: boolean; - releaseNotes?: boolean; - dll?: boolean; - docs?: boolean; - docsDll?: boolean; - uiDll?: boolean; - debugWebpack?: boolean; - previewUrl?: string; -} - -export async function getDevCli(packageJson: { - version: string; - name: string; -}): Promise { - process.env.NODE_ENV = process.env.NODE_ENV || 'development'; - - program - .version(packageJson.version) - .option('-p, --port [number]', 'Port to run Storybook', (str) => parseInt(str, 10)) - .option('-h, --host [string]', 'Host to run Storybook') - .option('-s, --static-dir ', 'Directory where to load static files from', parseList) - .option('-c, --config-dir [dir-name]', 'Directory where to load Storybook configurations from') - .option( - '--https', - 'Serve Storybook over HTTPS. Note: You must provide your own certificate information.' - ) - .option( - '--ssl-ca ', - 'Provide an SSL certificate authority. (Optional with --https, required if using a self-signed certificate)', - parseList - ) - .option('--ssl-cert ', 'Provide an SSL certificate. (Required with --https)') - .option('--ssl-key ', 'Provide an SSL key. (Required with --https)') - .option('--smoke-test', 'Exit after successful start') - .option('--ci', "CI mode (skip interactive prompts, don't open browser)") - .option('--loglevel [level]', 'Control level of logging during build') - .option('--quiet', 'Suppress verbose build output') - .option('--no-version-updates', 'Suppress update check', true) - .option( - '--no-release-notes', - '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)') - .option('--debug-webpack', 'Display final webpack configurations for debugging purposes') - .option( - '--preview-url [string]', - 'Disables the default storybook preview and lets your use your own' - ) - .option('--docs', 'Build a documentation-only site using addon-docs') - .parse(process.argv); - - logger.setLevel(program.loglevel); - - // Workaround the `-h` shorthand conflict. - // Output the help if `-h` is called without any value. - // See storybookjs/storybook#5360 - program.on('option:host', (value) => { - if (!value) { - program.help(); - } - }); - - logger.info(chalk.bold(`${packageJson.name} v${packageJson.version}`) + chalk.reset('\n')); - - // The key is the field created in `program` variable for - // each command line argument. Value is the env variable. - getEnvConfig(program, { - port: 'SBCONFIG_PORT', - host: 'SBCONFIG_HOSTNAME', - staticDir: 'SBCONFIG_STATIC_DIR', - configDir: 'SBCONFIG_CONFIG_DIR', - ci: 'CI', - }); - - if (typeof program.port === 'string' && program.port.length > 0) { - program.port = parseInt(program.port, 10); - } - - checkDeprecatedFlags(program as DevCliOptions); - return { ...program }; -} diff --git a/lib/builder-webpack4/src/cli/index.ts b/lib/builder-webpack4/src/cli/index.ts deleted file mode 100644 index 825ba73331d..00000000000 --- a/lib/builder-webpack4/src/cli/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './dev'; -export * from './prod'; diff --git a/lib/builder-webpack4/src/cli/prod.ts b/lib/builder-webpack4/src/cli/prod.ts deleted file mode 100644 index 1f7c35b8097..00000000000 --- a/lib/builder-webpack4/src/cli/prod.ts +++ /dev/null @@ -1,59 +0,0 @@ -import program, { CommanderStatic } from 'commander'; -import chalk from 'chalk'; -import { logger } from '@storybook/node-logger'; -import { parseList, getEnvConfig, checkDeprecatedFlags } from './utils'; - -export interface ProdCliOptions { - staticDir?: string[]; - outputDir?: string; - configDir?: string; - watch?: boolean; - quiet?: boolean; - loglevel?: string; - dll?: boolean; - docsDll?: boolean; - uiDll?: boolean; - debugWebpack?: boolean; - previewUrl?: string; - docs?: boolean; -} - -export function getProdCli(packageJson: { - version: string; - name: string; -}): CommanderStatic & ProdCliOptions { - process.env.NODE_ENV = process.env.NODE_ENV || 'production'; - - program - .version(packageJson.version) - .option('-s, --static-dir ', 'Directory where to load static files from', parseList) - .option('-o, --output-dir [dir-name]', 'Directory where to store built files') - .option('-c, --config-dir [dir-name]', 'Directory where to load Storybook configurations from') - .option('-w, --watch', 'Enable watch mode') - .option('--quiet', 'Suppress verbose build output') - .option('--loglevel [level]', 'Control level of logging during build') - .option('--no-dll', 'Do not use dll reference (no-op)') - .option('--docs-dll', 'Use Docs dll reference (legacy)') - .option('--ui-dll', 'Use UI dll reference (legacy)') - .option('--debug-webpack', 'Display final webpack configurations for debugging purposes') - .option( - '--preview-url [string]', - 'Disables the default storybook preview and lets your use your own' - ) - .option('--docs', 'Build a documentation-only site using addon-docs') - .parse(process.argv); - - logger.setLevel(program.loglevel); - logger.info(chalk.bold(`${packageJson.name} v${packageJson.version}\n`)); - - // The key is the field created in `program` variable for - // each command line argument. Value is the env variable. - getEnvConfig(program, { - staticDir: 'SBCONFIG_STATIC_DIR', - outputDir: 'SBCONFIG_OUTPUT_DIR', - configDir: 'SBCONFIG_CONFIG_DIR', - }); - - checkDeprecatedFlags(program as ProdCliOptions); - return { ...program }; -} diff --git a/lib/builder-webpack4/src/cli/utils.ts b/lib/builder-webpack4/src/cli/utils.ts deleted file mode 100644 index 03e1baae0d3..00000000000 --- a/lib/builder-webpack4/src/cli/utils.ts +++ /dev/null @@ -1,35 +0,0 @@ -import deprecate from 'util-deprecate'; -import dedent from 'ts-dedent'; - -export function parseList(str: string): string[] { - return str.split(','); -} - -export function getEnvConfig(program: Record, configEnv: Record): void { - Object.keys(configEnv).forEach((fieldName) => { - const envVarName = configEnv[fieldName]; - const envVarValue = process.env[envVarName]; - if (envVarValue) { - program[fieldName] = envVarValue; // eslint-disable-line - } - }); -} - -const warnDLLsDeprecated = deprecate( - () => {}, - dedent` - DLL-related CLI flags are deprecated, see: - - https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-dll-flags - ` -); - -export function checkDeprecatedFlags(options: { - dll?: boolean; - uiDll?: boolean; - docsDll?: boolean; -}) { - if (!options.dll || options.uiDll || options.docsDll) { - warnDLLsDeprecated(); - } -} diff --git a/lib/builder-webpack4/src/common/custom-presets.ts b/lib/builder-webpack4/src/common/custom-presets.ts deleted file mode 100644 index c5b0916316d..00000000000 --- a/lib/builder-webpack4/src/common/custom-presets.ts +++ /dev/null @@ -1,17 +0,0 @@ -import path from 'path'; -import { serverRequire, serverResolve } from '../utils/server-require'; -import validateConfigurationFiles from '../utils/validate-configuration-files'; -import { PresetConfig } from '../types'; - -export default function loadCustomPresets({ configDir }: { configDir: string }): PresetConfig[] { - validateConfigurationFiles(configDir); - - const presets = serverRequire(path.resolve(configDir, 'presets')); - const main = serverRequire(path.resolve(configDir, 'main')); - - if (main) { - return [serverResolve(path.resolve(configDir, 'main'))]; - } - - return presets || []; -} diff --git a/lib/builder-webpack4/src/config/utils.ts b/lib/builder-webpack4/src/common/utils.ts similarity index 100% rename from lib/builder-webpack4/src/config/utils.ts rename to lib/builder-webpack4/src/common/utils.ts diff --git a/lib/builder-webpack4/src/config.test.ts b/lib/builder-webpack4/src/config.test.ts deleted file mode 100644 index 44cf29c13ea..00000000000 --- a/lib/builder-webpack4/src/config.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { filterPresetsConfig } from './config'; - -describe('filterPresetsConfig', () => { - it('string config', () => { - expect( - filterPresetsConfig(['@storybook/preset-scss', '@storybook/preset-typescript']) - ).toEqual(['@storybook/preset-scss']); - }); - - it('windows paths', () => { - expect(filterPresetsConfig(['a', '@storybook\\preset-typescript'])).toEqual(['a']); - }); - - it('object config', () => { - const tsConfig = { - name: '@storybook/preset-typescript', - options: { foo: 1 }, - }; - expect(filterPresetsConfig([tsConfig, 'a'])).toEqual(['a']); - }); -}); diff --git a/lib/builder-webpack4/src/config.ts b/lib/builder-webpack4/src/config.ts deleted file mode 100644 index 533108afabc..00000000000 --- a/lib/builder-webpack4/src/config.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { logger } from '@storybook/node-logger'; -import { Configuration } from 'webpack'; -import loadPresets from './presets'; -import loadCustomPresets from './common/custom-presets'; -import { typeScriptDefaults } from './config/defaults'; -import { PresetConfig, Presets, PresetsOptions, StorybookConfigOptions } from './types'; - -async function getPreviewWebpackConfig( - options: StorybookConfigOptions & { presets: Presets }, - presets: Presets -): Promise { - const typescriptOptions = await presets.apply('typescript', { ...typeScriptDefaults }, options); - const babelOptions = await presets.apply('babel', {}, { ...options, typescriptOptions }); - const entries = await presets.apply('entries', [], options); - const stories = await presets.apply('stories', [], options); - const frameworkOptions = await presets.apply(`${options.framework}Options`, {}, options); - - return presets.apply( - 'webpack', - {}, - { - ...options, - babelOptions, - entries, - stories, - typescriptOptions, - [`${options.framework}Options`]: frameworkOptions, - } - ); -} - -export function filterPresetsConfig(presetsConfig: PresetConfig[]): PresetConfig[] { - return presetsConfig.filter((preset) => { - const presetName = typeof preset === 'string' ? preset : preset.name; - return !/@storybook[\\\\/]preset-typescript/.test(presetName); - }); -} -const loadConfig: ( - options: PresetsOptions & StorybookConfigOptions -) => Promise = async (options: PresetsOptions & StorybookConfigOptions) => { - const { corePresets = [], frameworkPresets = [], overridePresets = [], ...restOptions } = options; - - const presetsConfig: PresetConfig[] = [ - ...corePresets, - require.resolve('./common/babel-cache-preset'), - ...frameworkPresets, - ...loadCustomPresets(options), - ...overridePresets, - ]; - - // Remove `@storybook/preset-typescript` and add a warning if in use. - const filteredPresetConfig = filterPresetsConfig(presetsConfig); - if (filteredPresetConfig.length < presetsConfig.length) { - logger.warn( - 'Storybook now supports TypeScript natively. You can safely remove `@storybook/preset-typescript`.' - ); - } - - const presets = loadPresets(filteredPresetConfig, restOptions); - - return getPreviewWebpackConfig({ ...restOptions, presets }, presets); -}; - -export default loadConfig; diff --git a/lib/builder-webpack4/src/dev-server.test.ts b/lib/builder-webpack4/src/dev-server.test.ts deleted file mode 100644 index 7339fabe3d9..00000000000 --- a/lib/builder-webpack4/src/dev-server.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import ip from 'ip'; -import { getServerAddresses } from './dev-server'; - -jest.mock('ip'); -const mockedIp = ip as jest.Mocked; - -describe('getServerAddresses', () => { - beforeEach(() => { - mockedIp.address.mockReturnValue('192.168.0.5'); - }); - - it('builds addresses with a specified host', () => { - const { address, networkAddress } = getServerAddresses(9009, '192.168.89.89', 'http'); - expect(address).toEqual('http://localhost:9009/'); - expect(networkAddress).toEqual('http://192.168.89.89:9009/'); - }); - - it('builds addresses with local IP when host is not specified', () => { - const { address, networkAddress } = getServerAddresses(9009, '', 'http'); - expect(address).toEqual('http://localhost:9009/'); - expect(networkAddress).toEqual('http://192.168.0.5:9009/'); - }); -}); diff --git a/lib/builder-webpack4/src/dev-server.ts b/lib/builder-webpack4/src/dev-server.ts deleted file mode 100644 index 6d5c086bc3e..00000000000 --- a/lib/builder-webpack4/src/dev-server.ts +++ /dev/null @@ -1,406 +0,0 @@ -import { logger } from '@storybook/node-logger'; -import open from 'better-opn'; -import chalk from 'chalk'; -import express, { 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 prettyTime from 'pretty-hrtime'; -import { stringify } from 'telejson'; -import dedent from 'ts-dedent'; -import favicon from 'serve-favicon'; -import webpack, { Compiler, ProgressPlugin, Stats } from 'webpack'; -import webpackDevMiddleware from 'webpack-dev-middleware'; -import webpackHotMiddleware from 'webpack-hot-middleware'; - -import { FileSystemCache } from 'file-system-cache'; -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'; -import { parseStaticDir } from './utils/static-files'; -import { ManagerResult, PreviewResult } from './types'; - -const defaultFavIcon = require.resolve('./public/favicon.ico'); - -const cache = {}; - -let previewProcess: ReturnType; -let previewReject: (reason?: any) => void; - -const bailPreview = (e: Error) => { - 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: Express, - options: { - https?: boolean; - sslCert?: string; - sslKey?: string; - sslCa?: string[]; - } -) { - 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: any, options: { staticDir?: string[] }) { - let hasCustomFavicon = false; - - if (options.staticDir && options.staticDir.length > 0) { - await Promise.all( - options.staticDir.map(async (dir) => { - try { - const { staticDir, staticPath, targetEndpoint } = await parseStaticDir(dir); - logger.info( - chalk`=> Serving static files from {cyan ${staticDir}} at {cyan ${targetEndpoint}}` - ); - router.use(targetEndpoint, express.static(staticPath, { index: false })); - - if (!hasCustomFavicon && targetEndpoint === '/') { - const faviconPath = path.join(staticPath, 'favicon.ico'); - if (await pathExists(faviconPath)) { - hasCustomFavicon = true; - router.use(favicon(faviconPath)); - } - } - } catch (e) { - logger.warn(e.message); - } - }) - ); - } - - if (!hasCustomFavicon) { - router.use(favicon(defaultFavIcon)); - } -} - -function openInBrowser(address: string) { - 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. - `); - } -} - -// @ts-ignore -const router: Router = new Router(); - -const printDuration = (startTime: [number, number]) => - prettyTime(process.hrtime(startTime)) - .replace(' ms', ' milliseconds') - .replace(' s', ' seconds') - .replace(' m', ' minutes'); - -const useProgressReporting = async ( - compiler: Compiler, - options: any, - startTime: [number, number] -) => { - let value = 0; - let totalModules: number; - let reportProgress: (progress?: { - value?: number; - message: string; - modules?: any; - }) => void = () => {}; - - router.get('/progress', (request, response) => { - let closed = false; - const close = () => { - closed = true; - response.end(); - }; - response.on('close', close); - - if (closed || response.writableEnded) return; - response.setHeader('Cache-Control', 'no-cache'); - response.setHeader('Content-Type', 'text/event-stream'); - response.setHeader('Connection', 'keep-alive'); - response.flushHeaders(); - - reportProgress = (progress: any) => { - if (closed || response.writableEnded) return; - response.write(`data: ${JSON.stringify(progress)}\n\n`); - if (progress.value === 1) close(); - }; - }); - - const handler = (newValue: number, message: string, arg3: any) => { - value = Math.max(newValue, value); // never go backwards - const progress = { value, message: message.charAt(0).toUpperCase() + message.slice(1) }; - if (message === 'building') { - // arg3 undefined in webpack5 - const counts = (arg3 && arg3.match(/(\d+)\/(\d+)/)) || []; - const complete = parseInt(counts[1], 10); - const total = parseInt(counts[2], 10); - if (!Number.isNaN(complete) && !Number.isNaN(total)) { - (progress as any).modules = { complete, total }; - totalModules = total; - } - } - - if (value === 1) { - if (options.cache) { - options.cache.set('modulesCount', totalModules); - } - - if (!progress.message) { - progress.message = `Completed in ${printDuration(startTime)}.`; - } - } - reportProgress(progress); - }; - - const modulesCount = (await options.cache?.get('modulesCount').catch(() => {})) || 1000; - new ProgressPlugin({ handler, modulesCount }).apply(compiler); -}; - -const useManagerCache = async (fsc: FileSystemCache, managerConfig: webpack.Configuration) => { - // Drop the `cache` property because it'll change as a result of writing to the cache. - const { cache: _, ...baseConfig } = managerConfig; - const configString = stringify(baseConfig); - const cachedConfig = await fsc.get('managerConfig').catch(() => {}); - await fsc.set('managerConfig', configString); - return configString === cachedConfig; -}; - -const clearManagerCache = async (fsc: FileSystemCache) => { - if (fsc && fsc.fileExists('managerConfig')) { - await fsc.remove('managerConfig'); - return true; - } - return false; -}; - -const startManager = async ({ - startTime, - options, - configType, - outputDir, - configDir, - prebuiltDir, -}: any): Promise => { - 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.cache) { - if (options.managerCache) { - const [useCache, hasOutput] = await Promise.all([ - // must run even if outputDir doesn't exist, otherwise the 2nd run won't use cache - useManagerCache(options.cache, managerConfig), - pathExists(outputDir), - ]); - if (useCache && hasOutput && !options.smokeTest) { - logger.info('=> Using cached manager'); - managerConfig = null; - } - } else if (!options.smokeTest && (await clearManagerCache(options.cache))) { - logger.info('=> Cleared cached manager config'); - } - } - } - - if (!managerConfig) { - return {}; - } - - const compiler = webpack(managerConfig); - const middewareOptions: Parameters[1] = { - publicPath: managerConfig.output?.publicPath as string, - writeToDisk: true, - }; - const middleware = webpackDevMiddleware(compiler, middewareOptions); - - router.get(/\/static\/media\/.*\..*/, (request, response, next) => { - response.set('Cache-Control', `public, max-age=31536000`); - next(); - }); - - // Used to report back any client-side (runtime) errors - router.post('/runtime-error', express.json(), (request, response) => { - if (request.body?.error || request.body?.message) { - logger.error('Runtime error! Check your browser console.'); - logger.error(request.body.error?.stack || request.body.message || request.body); - if (request.body.origin === 'manager') clearManagerCache(options.cache); - } - response.sendStatus(200); - }); - - router.use(middleware); - - const managerStats: Stats = await new Promise((resolve) => middleware.waitUntilValid(resolve)); - if (!managerStats) { - await clearManagerCache(options.cache); - throw new Error('no stats after building manager'); - } - if (managerStats.hasErrors()) { - await clearManagerCache(options.cache); - throw managerStats; - } - return { managerStats, managerTotalTime: process.hrtime(startTime) }; -}; - -const startPreview = async ({ - startTime, - options, - configType, - outputDir, -}: any): Promise => { - if (options.ignorePreview) { - return {}; - } - - 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); - } - - const compiler = webpack(previewConfig); - await useProgressReporting(compiler, options, startTime); - - const middewareOptions: Parameters[1] = { - publicPath: previewConfig.output?.publicPath as string, - writeToDisk: true, - }; - previewProcess = webpackDevMiddleware(compiler, middewareOptions); - - router.use(previewProcess as any); - router.use(webpackHotMiddleware(compiler)); - - const previewStats: Stats = 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 function getServerAddresses(port: number, host: string, proto: string) { - return { - address: `${proto}://localhost:${port}/`, - networkAddress: `${proto}://${host || ip.address()}:${port}/`, - }; -} - -export async function storybookDevServer(options: any) { - 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); - } - - 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(); - }); - - // 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, networkAddress } = getServerAddresses(port, host, proto); - - await new Promise((resolve, reject) => { - // FIXME: Following line doesn't match TypeScript signature at all 🤔 - // @ts-ignore - server.listen({ port, host }, (error: Error) => (error ? reject(error) : resolve())); - }); - - const prebuiltDir = await getPrebuiltDir({ configDir, options }); - - // Manager static files - router.use('/', express.static(prebuiltDir || outputDir)); - - // 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 }) - // TODO #13083 Restore this when compiling the preview is fast enough - // .then((result) => { - // if (!options.ci && !options.smokeTest) openInBrowser(address); - // return result; - // }) - .catch(bailPreview), - ]); - - // TODO #13083 Remove this when compiling the preview is fast enough - if (!options.ci && !options.smokeTest) openInBrowser(networkAddress); - - return { ...previewResult, ...managerResult, address, networkAddress }; -} diff --git a/lib/builder-webpack4/src/index.ts b/lib/builder-webpack4/src/index.ts index 168b9bc4bf5..0e2fe5fcb18 100644 --- a/lib/builder-webpack4/src/index.ts +++ b/lib/builder-webpack4/src/index.ts @@ -1,16 +1,61 @@ -const defaultWebpackConfig = require('./preview/base-webpack.config'); -const serverUtils = require('./utils/template'); -const buildStatic = require('./build-static'); -const buildDev = require('./build-dev'); -const toRequireContext = require('./preview/to-require-context'); +import webpack, { Stats } from 'webpack'; +import webpackDevMiddleware from 'webpack-dev-middleware'; +import webpackHotMiddleware from 'webpack-hot-middleware'; +import { logger } from '@storybook/node-logger'; +import { logConfig } from './logger'; +import { PreviewResult } from './types'; -const managerPreset = require.resolve('./manager/manager-preset'); +let previewProcess: ReturnType; +let previewReject: (reason?: any) => void; -module.exports = { - managerPreset, - ...defaultWebpackConfig, - ...buildStatic, - ...buildDev, - ...serverUtils, - ...toRequireContext, +export const start = async ({ + startTime, + options, + useProgressReporting, + router, + config: previewConfig, +}: any): Promise => { + if (options.ignorePreview) { + return {}; + } + + if (options.debugWebpack) { + logConfig('Preview webpack config', previewConfig); + } + + const compiler = webpack(previewConfig); + await useProgressReporting(compiler, options, startTime); + + const middlewareOptions: Parameters[1] = { + publicPath: previewConfig.output?.publicPath as string, + writeToDisk: true, + }; + previewProcess = webpackDevMiddleware(compiler, middlewareOptions); + + router.use(previewProcess as any); + router.use(webpackHotMiddleware(compiler)); + + const previewStats: Stats = 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 const bail = (e: Error) => { + 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; +}; + +export const corePresets = [require.resolve('./preview/preview-preset.js')]; +export const overridePresets = [require.resolve('./preview/custom-webpack-preset.js')]; diff --git a/lib/builder-webpack4/src/logConfig.ts b/lib/builder-webpack4/src/logger.ts similarity index 100% rename from lib/builder-webpack4/src/logConfig.ts rename to lib/builder-webpack4/src/logger.ts diff --git a/lib/builder-webpack4/src/manager/babel-loader-manager.ts b/lib/builder-webpack4/src/manager/babel-loader-manager.ts deleted file mode 100644 index 01597f0044d..00000000000 --- a/lib/builder-webpack4/src/manager/babel-loader-manager.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { RuleSetRule } from 'webpack'; -import { includePaths } from '../config/utils'; -import { plugins, presets } from '../common/babel'; - -export const babelLoader: () => RuleSetRule = () => ({ - test: /\.(mjs|tsx?|jsx?)$/, - use: [ - { - loader: require.resolve('babel-loader'), - options: { - sourceType: 'unambiguous', - presets: [...presets, require.resolve('@babel/preset-react')], - plugins: [ - ...plugins, - // Should only be done on manager. Template literals are not meant to be - // transformed for frameworks like ember - require.resolve('@babel/plugin-transform-template-literals'), - ], - }, - }, - ], - include: includePaths, - exclude: [/node_modules/, /dist/], -}); diff --git a/lib/builder-webpack4/src/manager/manager-config.ts b/lib/builder-webpack4/src/manager/manager-config.ts deleted file mode 100644 index a22b124b3e3..00000000000 --- a/lib/builder-webpack4/src/manager/manager-config.ts +++ /dev/null @@ -1,177 +0,0 @@ -import path from 'path'; -import fs from 'fs-extra'; -import findUp from 'find-up'; -import resolveFrom from 'resolve-from'; -import fetch from 'node-fetch'; -import deprecate from 'util-deprecate'; -import dedent from 'ts-dedent'; - -import { logger } from '@storybook/node-logger'; - -import { Configuration } from 'webpack'; -import loadPresets from '../presets'; -import loadCustomPresets from '../common/custom-presets'; -import { typeScriptDefaults } from '../config/defaults'; -import { Presets, PresetsOptions, Ref, StorybookConfigOptions } from '../types'; - -export const getAutoRefs = async ( - options: { configDir: string }, - disabledRefs: string[] = [] -): Promise => { - const location = await findUp('package.json', { cwd: options.configDir }); - const directory = path.dirname(location); - - const { dependencies, devDependencies } = await fs.readJSON(location); - const deps = Object.keys({ ...dependencies, ...devDependencies }).filter( - (dep) => !disabledRefs.includes(dep) - ); - - const list = await Promise.all( - deps.map(async (d) => { - try { - const l = resolveFrom(directory, path.join(d, 'package.json')); - - const { storybook, name, version } = await fs.readJSON(l); - - if (storybook?.url) { - return { id: name, ...storybook, version }; - } - } catch { - logger.warn(`unable to find package.json for ${d}`); - return undefined; - } - return undefined; - }) - ); - - return list.filter(Boolean); -}; - -const checkRef = (url: string) => - fetch(`${url}/iframe.html`).then( - ({ ok }) => ok, - () => false - ); - -const stripTrailingSlash = (url: string) => url.replace(/\/$/, ''); - -const toTitle = (input: string) => { - const result = input - .replace(/[A-Z]/g, (f) => ` ${f}`) - .replace(/[-_][A-Z]/gi, (f) => ` ${f.toUpperCase()}`) - .replace(/-/g, ' ') - .replace(/_/g, ' '); - - return `${result.substring(0, 1).toUpperCase()}${result.substring(1)}`.trim(); -}; - -const deprecatedDefinedRefDisabled = deprecate( - () => {}, - dedent` - Deprecated parameter: disabled => disable - - https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-package-composition-disabled-parameter - ` -); - -async function getManagerWebpackConfig( - options: StorybookConfigOptions & { presets: Presets }, - presets: Presets -): Promise { - const typescriptOptions = await presets.apply('typescript', { ...typeScriptDefaults }, options); - const babelOptions = await presets.apply('babel', {}, { ...options, typescriptOptions }); - - const definedRefs: Record | undefined = await presets.apply( - 'refs', - undefined, - options - ); - - let disabledRefs: string[] = []; - if (definedRefs) { - disabledRefs = Object.entries(definedRefs) - .filter(([key, value]) => { - const { disable, disabled } = value; - - if (disable || disabled) { - if (disabled) { - deprecatedDefinedRefDisabled(); - } - - delete definedRefs[key]; // Also delete the ref that is disabled in definedRefs - - return true; - } - - return false; - }) - .map((ref) => ref[0]); - } - - const autoRefs = await getAutoRefs(options, disabledRefs); - const entries = await presets.apply('managerEntries', [], options); - - const refs: Record = {}; - - if (autoRefs && autoRefs.length) { - autoRefs.forEach(({ id, url, title, version }) => { - refs[id.toLowerCase()] = { - id: id.toLowerCase(), - url: stripTrailingSlash(url), - title, - version, - }; - }); - } - - if (definedRefs) { - Object.entries(definedRefs).forEach(([key, value]) => { - const url = typeof value === 'string' ? value : value.url; - const rest = - typeof value === 'string' - ? { title: toTitle(key) } - : { ...value, title: value.title || toTitle(value.key || key) }; - - refs[key.toLowerCase()] = { - id: key.toLowerCase(), - ...rest, - url: stripTrailingSlash(url), - }; - }); - } - - if (autoRefs || definedRefs) { - entries.push(path.resolve(path.join(options.configDir, `generated-refs.js`))); - - // verify the refs are publicly reachable, if they are not we'll require stories.json at runtime, otherwise the ref won't work - await Promise.all( - Object.entries(refs).map(async ([k, value]) => { - const ok = await checkRef(value.url); - - refs[k] = { ...value, type: ok ? 'server-checked' : 'unknown' }; - }) - ); - } - - return presets.apply('managerWebpack', {}, { ...options, babelOptions, entries, refs }); -} - -const loadConfig: ( - options: PresetsOptions & StorybookConfigOptions -) => Promise = async (options: PresetsOptions & StorybookConfigOptions) => { - const { corePresets = [], frameworkPresets = [], overridePresets = [], ...restOptions } = options; - - const presetsConfig = [ - ...corePresets, - require.resolve('../common/babel-cache-preset.js'), - ...frameworkPresets, - ...loadCustomPresets(options), - ...overridePresets, - ]; - - const presets = loadPresets(presetsConfig, restOptions); - - return getManagerWebpackConfig({ ...restOptions, presets }, presets); -}; - -export default loadConfig; diff --git a/lib/builder-webpack4/src/manager/manager-preset.ts b/lib/builder-webpack4/src/manager/manager-preset.ts deleted file mode 100644 index b3afdd1ae55..00000000000 --- a/lib/builder-webpack4/src/manager/manager-preset.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Configuration } from 'webpack'; -import { loadManagerOrAddonsFile } from '../utils/load-manager-or-addons-file'; -import createDevConfig from './manager-webpack.config'; -import { ManagerWebpackOptions } from '../types'; - -export async function managerWebpack( - _: Configuration, - options: ManagerWebpackOptions -): Promise { - return createDevConfig(options); -} - -export async function managerEntries( - installedAddons: string[], - options: { managerEntry: string; configDir: string } -): Promise { - const { managerEntry = '@storybook/core-client/dist/esm/manager' } = options; - const entries = [require.resolve('../common/polyfills')]; - - if (installedAddons && installedAddons.length) { - entries.push(...installedAddons); - } - - const managerConfig = loadManagerOrAddonsFile(options); - if (managerConfig) { - entries.push(managerConfig); - } - - entries.push(require.resolve(managerEntry)); - return entries; -} - -export * from '../common/common-preset'; diff --git a/lib/builder-webpack4/src/manager/manager-webpack.config.ts b/lib/builder-webpack4/src/manager/manager-webpack.config.ts deleted file mode 100644 index e3d2d2a4fb8..00000000000 --- a/lib/builder-webpack4/src/manager/manager-webpack.config.ts +++ /dev/null @@ -1,185 +0,0 @@ -import path from 'path'; -import fse from 'fs-extra'; -import { DefinePlugin, Configuration } from 'webpack'; -import Dotenv from 'dotenv-webpack'; -import HtmlWebpackPlugin from 'html-webpack-plugin'; -import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'; -import PnpWebpackPlugin from 'pnp-webpack-plugin'; -import VirtualModulePlugin from 'webpack-virtual-modules'; -import TerserWebpackPlugin from 'terser-webpack-plugin'; - -import themingPaths from '@storybook/theming/paths'; -import uiPaths from '@storybook/ui/paths'; - -import readPackage from 'read-pkg-up'; -import { getManagerHeadHtml } from '../utils/template'; -import { loadEnv } from '../config/utils'; - -import { babelLoader } from './babel-loader-manager'; -import { resolvePathInStorybookCache } from '../utils/resolve-path-in-sb-cache'; -import { es6Transpiler } from '../common/es6Transpiler'; -import { ManagerWebpackOptions } from '../types'; - -export default async ({ - configDir, - configType, - docsMode, - entries, - refs, - outputDir, - previewUrl, - versionCheck, - releaseNotesData, - presets, -}: ManagerWebpackOptions): Promise => { - const { raw, stringified } = loadEnv(); - const logLevel = await presets.apply('logLevel', undefined); - const headHtmlSnippet = await presets.apply( - 'managerHead', - getManagerHeadHtml(configDir, process.env) - ); - const isProd = configType === 'PRODUCTION'; - const refsTemplate = fse.readFileSync(path.join(__dirname, 'virtualModuleRef.template.js'), { - encoding: 'utf8', - }); - const { - packageJson: { version }, - } = await readPackage({ cwd: __dirname }); - - // @ts-ignore - // eslint-disable-next-line import/no-extraneous-dependencies - const { BundleAnalyzerPlugin } = await import('webpack-bundle-analyzer').catch(() => ({})); - - return { - name: 'manager', - mode: isProd ? 'production' : 'development', - bail: isProd, - devtool: false, - entry: entries, - output: { - path: outputDir, - filename: '[name].[chunkhash].bundle.js', - publicPath: '', - }, - watchOptions: { - aggregateTimeout: 2000, - ignored: /node_modules/, - }, - plugins: [ - refs - ? new VirtualModulePlugin({ - [path.resolve(path.join(configDir, `generated-refs.js`))]: refsTemplate.replace( - `'{{refs}}'`, - JSON.stringify(refs) - ), - }) - : null, - new HtmlWebpackPlugin({ - filename: `index.html`, - // FIXME: `none` isn't a known option - chunksSortMode: 'none' as any, - alwaysWriteToDisk: true, - inject: false, - templateParameters: (compilation, files, options) => ({ - compilation, - files, - options, - version, - globals: { - CONFIG_TYPE: configType, - LOGLEVEL: logLevel, - VERSIONCHECK: JSON.stringify(versionCheck), - RELEASE_NOTES_DATA: JSON.stringify(releaseNotesData), - DOCS_MODE: docsMode, // global docs mode - PREVIEW_URL: previewUrl, // global preview URL - }, - headHtmlSnippet, - }), - template: require.resolve(`../templates/index.ejs`), - }), - new CaseSensitivePathsPlugin(), - new Dotenv({ silent: true }), - // graphql sources check process variable - new DefinePlugin({ - 'process.env': stringified, - NODE_ENV: JSON.stringify(process.env.NODE_ENV), - }), - isProd && - BundleAnalyzerPlugin && - new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false }), - ].filter(Boolean), - module: { - rules: [ - babelLoader(), - es6Transpiler(), - { - test: /\.css$/, - use: [ - require.resolve('style-loader'), - { - loader: require.resolve('css-loader'), - options: { - importLoaders: 1, - }, - }, - ], - }, - { - test: /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/, - loader: require.resolve('file-loader'), - options: { - name: 'static/media/[name].[hash:8].[ext]', - }, - }, - { - test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)(\?.*)?$/, - loader: require.resolve('url-loader'), - options: { - limit: 10000, - }, - }, - ], - }, - resolve: { - extensions: ['.mjs', '.js', '.jsx', '.json', '.cjs', '.ts', '.tsx'], - modules: ['node_modules'].concat((raw.NODE_PATH as string[]) || []), - mainFields: ['module', 'main'], - alias: { - ...themingPaths, - ...uiPaths, - }, - plugins: [ - // Transparently resolve packages via PnP when needed; noop otherwise - PnpWebpackPlugin, - ], - }, - resolveLoader: { - plugins: [PnpWebpackPlugin.moduleLoader(module)], - }, - recordsPath: resolvePathInStorybookCache('public/records.json'), - performance: { - hints: false, - }, - optimization: { - splitChunks: { - chunks: 'all', - }, - runtimeChunk: true, - sideEffects: true, - usedExports: true, - concatenateModules: true, - minimizer: isProd - ? [ - new TerserWebpackPlugin({ - parallel: true, - terserOptions: { - mangle: false, - sourceMap: true, - keep_fnames: true, - }, - }), - ] - : [], - }, - }; -}; diff --git a/lib/builder-webpack4/src/manager/virtualModuleRef.template.ts b/lib/builder-webpack4/src/manager/virtualModuleRef.template.ts deleted file mode 100644 index a7ac896f419..00000000000 --- a/lib/builder-webpack4/src/manager/virtualModuleRef.template.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { addons } from '@storybook/addons'; - -addons.setConfig({ - refs: '{{refs}}', -}); diff --git a/lib/builder-webpack4/src/presets.test.ts b/lib/builder-webpack4/src/presets.test.ts deleted file mode 100644 index 0dadb6b981b..00000000000 --- a/lib/builder-webpack4/src/presets.test.ts +++ /dev/null @@ -1,493 +0,0 @@ -function wrapPreset(basePresets: any): { babel: Function; webpack: Function } { - return { - babel: async (config: any, args: any) => basePresets.apply('babel', config, args), - webpack: async (config: any, args: any) => basePresets.apply('webpack', config, args), - }; -} - -function mockPreset(name: string, mockPresetObject: any) { - jest.mock(name, () => mockPresetObject, { virtual: true }); -} - -jest.mock('@storybook/node-logger', () => ({ - logger: { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }, -})); - -jest.mock('resolve-from', () => (l: any, name: string) => { - const KNOWN_FILES = [ - '@storybook/addon-actions/register', - './local/preset', - './local/addons', - '/absolute/preset', - '/absolute/addons', - '@storybook/addon-docs/preset', - '@storybook/addon-essentials', - '@storybook/addon-knobs/register', - '@storybook/addon-notes/register-panel', - '@storybook/preset-create-react-app', - '@storybook/preset-typescript', - 'addon-bar/preset.js', - 'addon-baz/register.js', - 'addon-foo/register.js', - ]; - - if (KNOWN_FILES.includes(name)) { - return name; - } - throw new Error(`cannot resolve ${name}`); -}); - -describe('presets', () => { - it('does not throw when there is no preset file', async () => { - const getPresets = jest.requireActual('./presets').default; - let presets; - - async function testPresets() { - presets = wrapPreset(getPresets()); - await presets.webpack(); - await presets.babel(); - } - - await expect(testPresets()).resolves.toBeUndefined(); - - expect(presets).toBeDefined(); - }); - - it('does not throw when presets are empty', async () => { - const getPresets = jest.requireActual('./presets').default; - const presets = wrapPreset(getPresets([])); - - async function testPresets() { - await presets.webpack(); - await presets.babel(); - } - - await expect(testPresets()).resolves.toBeUndefined(); - }); - - it('does not throw when preset can not be loaded', async () => { - const getPresets = jest.requireActual('./presets').default; - const presets = wrapPreset(getPresets(['preset-foo'])); - - async function testPresets() { - await presets.webpack(); - await presets.babel(); - } - - await expect(testPresets()).resolves.toBeUndefined(); - }); - - it('loads and applies presets when they are combined in another preset', async () => { - mockPreset('preset-foo', { - foo: (exec: string[]) => exec.concat('foo'), - }); - - mockPreset('preset-bar', { - foo: (exec: string[]) => exec.concat('bar'), - }); - - mockPreset('preset-got', [ - 'preset-dracarys', - { name: 'preset-valar', options: { custom: 'morghulis' } }, - ]); - - mockPreset('preset-dracarys', { - foo: (exec: string[]) => exec.concat('dracarys'), - }); - - mockPreset('preset-valar', { - foo: (exec: string[], options: any) => exec.concat(`valar ${options.custom}`), - }); - - const getPresets = jest.requireActual('./presets').default; - const presets = getPresets(['preset-foo', 'preset-got', 'preset-bar']); - - const result = await presets.apply('foo', []); - - expect(result).toEqual(['foo', 'dracarys', 'valar morghulis', 'bar']); - }); - - it('loads and applies presets when they are declared as a string', async () => { - const mockPresetFooExtendWebpack = jest.fn(); - const mockPresetBarExtendBabel = jest.fn(); - - mockPreset('preset-foo', { - webpack: mockPresetFooExtendWebpack, - }); - - mockPreset('preset-bar', { - babel: mockPresetBarExtendBabel, - }); - - const getPresets = jest.requireActual('./presets').default; - const presets = wrapPreset(getPresets(['preset-foo', 'preset-bar'], {})); - - async function testPresets() { - await presets.webpack(); - await presets.babel(); - } - - await expect(testPresets()).resolves.toBeUndefined(); - - expect(mockPresetFooExtendWebpack).toHaveBeenCalled(); - expect(mockPresetBarExtendBabel).toHaveBeenCalled(); - }); - - it('loads and applies presets when they are declared as an object without props', async () => { - const mockPresetFooExtendWebpack = jest.fn(); - const mockPresetBarExtendBabel = jest.fn(); - - mockPreset('preset-foo', { - webpack: mockPresetFooExtendWebpack, - }); - - mockPreset('preset-bar', { - babel: mockPresetBarExtendBabel, - }); - - const getPresets = jest.requireActual('./presets').default; - const presets = wrapPreset(getPresets([{ name: 'preset-foo' }, { name: 'preset-bar' }])); - - async function testPresets() { - await presets.webpack(); - await presets.babel(); - } - - await expect(testPresets()).resolves.toBeUndefined(); - - expect(mockPresetFooExtendWebpack).toHaveBeenCalled(); - expect(mockPresetBarExtendBabel).toHaveBeenCalled(); - }); - - it('loads and applies presets when they are declared as an object with props', async () => { - const mockPresetFooExtendWebpack = jest.fn(); - const mockPresetBarExtendBabel = jest.fn(); - - mockPreset('preset-foo', { - webpack: mockPresetFooExtendWebpack, - }); - - mockPreset('preset-bar', { - babel: mockPresetBarExtendBabel, - }); - - const getPresets = jest.requireActual('./presets').default; - const presets = wrapPreset( - getPresets([ - { name: 'preset-foo', options: { foo: 1 } }, - { name: 'preset-bar', options: { bar: 'a' } }, - ]) - ); - - async function testPresets() { - await presets.webpack({}); - await presets.babel({}); - } - - await expect(testPresets()).resolves.toBeUndefined(); - - expect(mockPresetFooExtendWebpack).toHaveBeenCalledWith(expect.anything(), { - foo: 1, - presetsList: expect.anything(), - }); - expect(mockPresetBarExtendBabel).toHaveBeenCalledWith(expect.anything(), { - bar: 'a', - presetsList: expect.anything(), - }); - }); - - it('loads and applies presets when they are declared as a string and as an object', async () => { - const mockPresetFooExtendWebpack = jest.fn(); - const mockPresetBarExtendBabel = jest.fn(); - - mockPreset('preset-foo', { - webpack: mockPresetFooExtendWebpack, - }); - - mockPreset('preset-bar', { - babel: mockPresetBarExtendBabel, - }); - - const getPresets = jest.requireActual('./presets').default; - const presets = wrapPreset( - getPresets([ - 'preset-foo', - { - name: 'preset-bar', - options: { - bar: 'a', - }, - }, - ]) - ); - - async function testPresets() { - await presets.webpack({}); - await presets.babel({}); - } - - await expect(testPresets()).resolves.toBeUndefined(); - - expect(mockPresetFooExtendWebpack).toHaveBeenCalled(); - expect(mockPresetBarExtendBabel).toHaveBeenCalledWith(expect.anything(), { - bar: 'a', - presetsList: expect.arrayContaining([ - expect.objectContaining({ name: 'preset-foo' }), - expect.objectContaining({ name: 'preset-bar' }), - ]), - }); - }); - - it('applies presets in chain', async () => { - const mockPresetFooExtendWebpack = jest.fn((...args: any[]) => ({})); - const mockPresetBarExtendWebpack = jest.fn((...args: any[]) => ({})); - - mockPreset('preset-foo', { - webpack: mockPresetFooExtendWebpack, - }); - - mockPreset('preset-bar', { - webpack: mockPresetBarExtendWebpack, - }); - - const getPresets = jest.requireActual('./presets').default; - const presets = wrapPreset( - getPresets([ - 'preset-foo', - { - name: 'preset-bar', - options: { - bar: 'a', - presetsList: expect.arrayContaining([ - expect.objectContaining({ name: 'preset-foo' }), - expect.objectContaining({ name: 'preset-bar' }), - ]), - }, - }, - ]) - ); - - async function testPresets() { - await presets.webpack(); - await presets.babel(); - } - - await expect(testPresets()).resolves.toBeUndefined(); - - expect(mockPresetFooExtendWebpack).toHaveBeenCalled(); - expect(mockPresetBarExtendWebpack).toHaveBeenCalledWith(expect.anything(), { - bar: 'a', - presetsList: expect.arrayContaining([ - expect.objectContaining({ name: 'preset-foo' }), - expect.objectContaining({ name: 'preset-bar' }), - ]), - }); - }); - - it('allows for presets to export presets array', async () => { - const getPresets = jest.requireActual('./presets').default; - const input = {}; - const mockPresetBar = jest.fn((...args: any[]) => input); - - mockPreset('preset-foo', { - presets: ['preset-bar'], - }); - - mockPreset('preset-bar', { - bar: mockPresetBar, - }); - - const presets = getPresets(['preset-foo']); - - const output = await presets.apply('bar'); - - expect(mockPresetBar).toHaveBeenCalledWith(undefined, expect.any(Object)); - - expect(input).toBe(output); - }); - - it('allows for presets to export presets fn', async () => { - const getPresets = jest.requireActual('./presets').default; - const input = {}; - const storybookOptions = { a: 1 }; - const presetOptions = { b: 2 }; - const mockPresetBar = jest.fn((...args: any[]) => input); - const mockPresetFoo = jest.fn((...args: any[]) => ['preset-bar']); - - mockPreset('preset-foo', { - presets: mockPresetFoo, - }); - - mockPreset('preset-bar', { - bar: mockPresetBar, - }); - - const presets = getPresets([{ name: 'preset-foo', options: { b: 2 } }], storybookOptions); - - const output = await presets.apply('bar'); - - expect(mockPresetFoo).toHaveBeenCalledWith({ ...storybookOptions, ...presetOptions }); - expect(mockPresetBar).toHaveBeenCalledWith(undefined, expect.any(Object)); - - expect(input).toBe(output); - }); - - afterEach(() => { - jest.resetModules(); - }); -}); - -describe('resolveAddonName', () => { - const { resolveAddonName } = jest.requireActual('./presets'); - - it('should resolve packages with metadata (relative path)', () => { - mockPreset('./local/preset', { - presets: [], - }); - expect(resolveAddonName({}, './local/preset')).toEqual({ - name: './local/preset', - type: 'presets', - }); - }); - - it('should resolve packages with metadata (absolute path)', () => { - mockPreset('/absolute/preset', { - presets: [], - }); - expect(resolveAddonName({}, '/absolute/preset')).toEqual({ - name: '/absolute/preset', - type: 'presets', - }); - }); - - it('should resolve packages without metadata', () => { - expect(resolveAddonName({}, '@storybook/preset-create-react-app')).toEqual({ - name: '@storybook/preset-create-react-app', - type: 'presets', - }); - }); - - it('should resolve managerEntries', () => { - expect(resolveAddonName({}, '@storybook/addon-actions/register')).toEqual({ - name: '@storybook/addon-actions/register', - type: 'managerEntries', - }); - }); - - it('should resolve presets', () => { - expect(resolveAddonName({}, '@storybook/addon-docs')).toEqual({ - name: '@storybook/addon-docs/preset', - type: 'presets', - }); - }); - - it('should resolve preset packages', () => { - expect(resolveAddonName({}, '@storybook/addon-essentials')).toEqual({ - name: '@storybook/addon-essentials', - type: 'presets', - }); - }); - - it('should error on invalid inputs', () => { - expect(() => resolveAddonName({}, null)).toThrow(); - }); -}); - -describe('loadPreset', () => { - mockPreset('@storybook/preset-typescript', {}); - mockPreset('@storybook/addon-docs/preset', {}); - mockPreset('@storybook/addon-actions/register', {}); - mockPreset('addon-foo/register.js', {}); - mockPreset('addon-bar/preset', {}); - mockPreset('addon-baz/register.js', {}); - mockPreset('@storybook/addon-notes/register-panel', {}); - - const { loadPreset } = jest.requireActual('./presets'); - - it('should resolve all addons & presets in correct order', () => { - const loaded = loadPreset( - { - name: '', - type: 'managerEntries', - presets: ['@storybook/preset-typescript'], - addons: [ - '@storybook/addon-docs', - '@storybook/addon-actions/register', - 'addon-foo/register.js', - 'addon-bar', - 'addon-baz/register.tsx', - '@storybook/addon-notes/register-panel', - ], - }, - 0, - {} - ); - expect(loaded).toEqual([ - { - name: '@storybook/preset-typescript', - options: {}, - preset: {}, - }, - { - name: '@storybook/addon-docs/preset', - options: {}, - preset: {}, - }, - { - name: '@storybook/addon-actions/register_additionalManagerEntries', - options: {}, - preset: { - managerEntries: ['@storybook/addon-actions/register'], - }, - }, - { - name: 'addon-foo/register.js_additionalManagerEntries', - options: {}, - preset: { - managerEntries: ['addon-foo/register.js'], - }, - }, - // should be there, but some file mocking problem is causing it to not resolve - // { - // name: 'addon-bar', - // options: {}, - // preset: {}, - // }, - { - name: 'addon-baz/register.tsx_additionalManagerEntries', - options: {}, - preset: { - managerEntries: ['addon-baz/register.tsx'], - }, - }, - { - name: '@storybook/addon-notes/register-panel_additionalManagerEntries', - options: {}, - preset: { - managerEntries: ['@storybook/addon-notes/register-panel'], - }, - }, - { - name: { - presets: ['@storybook/preset-typescript'], - addons: [ - '@storybook/addon-docs', - '@storybook/addon-actions/register', - 'addon-foo/register.js', - 'addon-bar', - 'addon-baz/register.tsx', - '@storybook/addon-notes/register-panel', - ], - name: '', - type: 'managerEntries', - }, - options: {}, - preset: {}, - }, - ]); - }); -}); diff --git a/lib/builder-webpack4/src/presets.ts b/lib/builder-webpack4/src/presets.ts deleted file mode 100644 index 8b778a4a98f..00000000000 --- a/lib/builder-webpack4/src/presets.ts +++ /dev/null @@ -1,263 +0,0 @@ -import dedent from 'ts-dedent'; -import { join } from 'path'; -import { logger } from '@storybook/node-logger'; -import resolveFrom from 'resolve-from'; -import { LoadedPreset, PresetConfig, Presets, StorybookConfigOptions } from './types'; - -const isObject = (val: unknown): val is Record => - val != null && typeof val === 'object' && Array.isArray(val) === false; -const isFunction = (val: unknown): val is Function => typeof val === 'function'; - -function resolvePresetFunction( - input: T[] | Function, - presetOptions: any, - storybookOptions: StorybookConfigOptions -): T[] { - if (isFunction(input)) { - return input({ ...storybookOptions, ...presetOptions }); - } - if (Array.isArray(input)) { - return input; - } - - return []; -} - -/** - * Parse an addon into either a managerEntries or a preset. Throw on invalid input. - * - * Valid inputs: - * - '@storybook/addon-actions/register' - * => { type: 'managerEntries', item } - * - * - '@storybook/addon-docs/preset' - * => { type: 'presets', item } - * - * - '@storybook/addon-docs' - * => { type: 'presets', item: '@storybook/addon-docs/preset' } - * - * - { name: '@storybook/addon-docs(/preset)?', options: { ... } } - * => { type: 'presets', item: { name: '@storybook/addon-docs/preset', options } } - */ -export const resolveAddonName = (configDir: string, name: string) => { - let path; - - if (name.startsWith('.')) { - path = resolveFrom(configDir, name); - } else if (name.startsWith('/')) { - path = name; - } else if (name.match(/\/(preset|register(-panel)?)(\.(js|ts|tsx|jsx))?$/)) { - path = name; - } - - // when user provides full path, we don't need to do anything - if (path) { - return { - name: path, - // Accept `register`, `register.js`, `require.resolve('foo/register'), `register-panel` - type: path.match(/register(-panel)?(\.(js|ts|tsx|jsx))?$/) ? 'managerEntries' : 'presets', - }; - } - - try { - return { - name: resolveFrom(configDir, join(name, 'preset')), - type: 'presets', - }; - // eslint-disable-next-line no-empty - } catch (err) {} - - try { - return { - name: resolveFrom(configDir, join(name, 'register')), - type: 'managerEntries', - }; - // eslint-disable-next-line no-empty - } catch (err) {} - - return { - name: resolveFrom(configDir, name), - type: 'presets', - }; -}; - -const map = ({ configDir }: { configDir: string }) => (item: any) => { - try { - if (isObject(item)) { - const { name } = resolveAddonName(configDir, item.name); - return { ...item, name }; - } - const { name, type } = resolveAddonName(configDir, item); - if (type === 'managerEntries') { - return { - name: `${name}_additionalManagerEntries`, - type, - managerEntries: [name], - }; - } - return resolveAddonName(configDir, name); - } catch (err) { - logger.error( - `Addon value should end in /register OR it should be a valid preset https://storybook.js.org/docs/presets/introduction/\n${item}` - ); - } - return undefined; -}; - -function interopRequireDefault(filePath: string) { - // eslint-disable-next-line global-require,import/no-dynamic-require - const result = require(filePath); - - const isES6DefaultExported = - typeof result === 'object' && result !== null && typeof result.default !== 'undefined'; - - return isES6DefaultExported ? result.default : result; -} - -function getContent(input: any) { - if (input.type === 'managerEntries') { - const { type, name, ...rest } = input; - return rest; - } - const name = input.name ? input.name : input; - - return interopRequireDefault(name); -} - -export function loadPreset( - input: PresetConfig, - level: number, - storybookOptions: StorybookConfigOptions -): LoadedPreset[] { - try { - // @ts-ignores - const name: string = input.name ? input.name : input; - // @ts-ignore - const presetOptions = input.options ? input.options : {}; - - let contents = getContent(input); - - if (typeof contents === 'function') { - // allow the export of a preset to be a function, that gets storybookOptions - contents = contents(storybookOptions, presetOptions); - } - - if (Array.isArray(contents)) { - const subPresets = contents; - return loadPresets(subPresets, level + 1, storybookOptions); - } - - if (isObject(contents)) { - const { addons: addonsInput, presets: presetsInput, ...rest } = contents; - - const subPresets = resolvePresetFunction(presetsInput, presetOptions, storybookOptions); - const subAddons = resolvePresetFunction(addonsInput, presetOptions, storybookOptions); - - return [ - ...loadPresets([...subPresets], level + 1, storybookOptions), - ...loadPresets( - [...subAddons.map(map(storybookOptions))].filter(Boolean), - level + 1, - storybookOptions - ), - { - name, - preset: rest, - options: presetOptions, - }, - ]; - } - - throw new Error(dedent` - ${input} is not a valid preset - `); - } catch (e) { - const warning = - level > 0 - ? ` Failed to load preset: ${JSON.stringify(input)} on level ${level}` - : ` Failed to load preset: ${JSON.stringify(input)}`; - - logger.warn(warning); - logger.error(e); - - return []; - } -} - -function loadPresets( - presets: PresetConfig[], - level: number, - storybookOptions: StorybookConfigOptions -): LoadedPreset[] { - if (!presets || !Array.isArray(presets) || !presets.length) { - return []; - } - - if (!level) { - logger.info('=> Loading presets'); - } - - return presets.reduce((acc, preset) => { - const loaded = loadPreset(preset, level, storybookOptions); - return acc.concat(loaded); - }, []); -} - -function applyPresets( - presets: LoadedPreset[], - extension: string, - config: any, - args: any, - storybookOptions: StorybookConfigOptions -): Promise { - const presetResult = new Promise((resolve) => resolve(config)); - - if (!presets.length) { - return presetResult; - } - - return presets.reduce((accumulationPromise: Promise, { preset, options }) => { - const change = preset[extension]; - - if (!change) { - return accumulationPromise; - } - - if (typeof change === 'function') { - const extensionFn = change; - const context = { - preset, - combinedOptions: { ...storybookOptions, ...args, ...options, presetsList: presets }, - }; - - return accumulationPromise.then((newConfig) => - extensionFn.call(context.preset, newConfig, context.combinedOptions) - ); - } - - return accumulationPromise.then((newConfig) => { - if (Array.isArray(newConfig) && Array.isArray(change)) { - return [...newConfig, ...change]; - } - if (isObject(newConfig) && isObject(change)) { - return { ...newConfig, ...change }; - } - return change; - }); - }, presetResult); -} - -function getPresets( - presets: PresetConfig[], - // @ts-ignore - storybookOptions: StorybookConfigOptions = {} -): Presets { - const loadedPresets: LoadedPreset[] = loadPresets(presets, 0, storybookOptions); - - return { - apply: async (extension: string, config: any, args = {}) => - applyPresets(loadedPresets, extension, config, args, storybookOptions), - }; -} - -export default getPresets; diff --git a/lib/builder-webpack4/src/preview/babel-loader-preview.ts b/lib/builder-webpack4/src/preview/babel-loader-preview.ts index b2dcd9fc098..3b42ee44ac8 100644 --- a/lib/builder-webpack4/src/preview/babel-loader-preview.ts +++ b/lib/builder-webpack4/src/preview/babel-loader-preview.ts @@ -1,4 +1,4 @@ -import { includePaths } from '../config/utils'; +import { includePaths } from '../common/utils'; import { useBaseTsSupport } from '../config/useBaseTsSupport'; export const createBabelLoader = (options: any, framework: string) => ({ diff --git a/lib/builder-webpack4/src/preview/entries.ts b/lib/builder-webpack4/src/preview/entries.ts index 8b092a5038a..ce4d7970f9d 100644 --- a/lib/builder-webpack4/src/preview/entries.ts +++ b/lib/builder-webpack4/src/preview/entries.ts @@ -35,7 +35,7 @@ const getMainConfigs = (options: { configDir: string }) => { export async function createPreviewEntry(options: { configDir: string; presets: any }) { const { configDir, presets } = options; const entries = [ - require.resolve('../common/polyfills'), + require.resolve('./polyfills'), require.resolve('./globals'), path.resolve(path.join(configDir, 'storybook-init-framework-entry.js')), ]; diff --git a/lib/builder-webpack4/src/preview/iframe-webpack.config.ts b/lib/builder-webpack4/src/preview/iframe-webpack.config.ts index c143b497c6e..d45dae97519 100644 --- a/lib/builder-webpack4/src/preview/iframe-webpack.config.ts +++ b/lib/builder-webpack4/src/preview/iframe-webpack.config.ts @@ -15,12 +15,12 @@ import FilterWarningsPlugin from 'webpack-filter-warnings-plugin'; import themingPaths from '@storybook/theming/paths'; import { createBabelLoader } from './babel-loader-preview'; -import { es6Transpiler } from '../common/es6Transpiler'; -import { nodeModulesPaths, loadEnv } from '../config/utils'; -import { getPreviewHeadHtml, getPreviewBodyHtml } from '../utils/template'; import { toRequireContextString } from './to-require-context'; import { useBaseTsSupport } from '../config/useBaseTsSupport'; +import { getPreviewBodyHtml, getPreviewHeadHtml } from '../template'; +import { loadEnv, nodeModulesPaths } from '../common/utils'; +import { es6Transpiler } from '../common/es6Transpiler'; const storybookPaths: Record = [ 'addons', @@ -71,13 +71,10 @@ export default async ({ const { raw, stringified } = loadEnv({ production: true }); const babelLoader = createBabelLoader(babelOptions, framework); const isProd = configType === 'PRODUCTION'; - const entryTemplate = await fse.readFile( - // TODO ANDREW maybe something simpler - path.join(__dirname, '../../esm/preview', 'virtualModuleEntry.template.js'), - { - encoding: 'utf8', - } - ); + // TODO FIX ME - does this need to be ESM? + const entryTemplate = await fse.readFile(path.join(__dirname, 'virtualModuleEntry.template.js'), { + encoding: 'utf8', + }); const storyTemplate = await fse.readFile(path.join(__dirname, 'virtualModuleStory.template.js'), { encoding: 'utf8', }); diff --git a/lib/builder-webpack4/src/public/favicon.ico b/lib/builder-webpack4/src/public/favicon.ico deleted file mode 100644 index 428500fde188122fe8a6b07197f4c03f4e93640a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32988 zcmeHP36vDY86Faor@5nfxexMk`C<+d^Dr27QQ4W`K#-M--8Ponvnf z0R=V!8iRluG!eXopn#&HphN^g5k(g807PNC`_K1vO)WjWGt)IoBfO;hy`JvrqpH5Y z{=2F=7V8l^AlA2U49A|aLvf79VzHh*^Z!2;i~SMTKL7dr|AS(&2?JxXPvN@z`~k7p zLqlS*L-7tCi>0vhdbIt|QlLj_uzYy@VEI^ULJp3_q{se zo=ZA&>yysIxwy3`o3NLslh%vbxV^r<#%^!!Z!g95Q^KY}_INp@^#tjiOLX+hCS^cV zt#xukQqDq~F3KeBtLl?>T`p<2;(Q|7dUH16{39E8?$0HyM}TKZHfcYLwysIX?F~3? z!M+V`-kC|*PJN9?W35O_e-YH-?xS!VePZFxYrNmW9nUsnZ^Cf?4>82Ev? z7jZXZXB)$8XIOas<&Z}})0tu52i_~j3<#%R{O#OZ|6TlXdwMwd?F{8`Z?$!Alp7}g zT#eoQe~lk@hx%-9^wZudGtB-^C+r(T!ygw(SBs219{X|XSl18Na6L>~goz(EditK= zC*Et178&|ebtKltpTnN~0=5);4t6C~rS1>=nF#5zr?dzYe&C!M7F;v^2Tc-FFhl&c$Ua5e^K=e?W6dl;yUyGLzwu} zN#|brTgGjHFGKz&_UTuPyu3^*+S*?dxvL30(O2;WeIw(#_X7S*!dgIo&$JDcYxeo$ zvnzt#<>6J2Dfyu6R7!&|`#)E0Jx>4C1izN`We;?L)2$2S1?mXu2b22sGhyOKT=O)2 zeqDCCBX!)pTZ`{=k z`v9Hj#aQkA1Clv2{88 zwCU8>uRK>4M(0i25_xk2cx7wnrNEOZe^mZMxpwG#Vd2mILJoodx0!hbU3Re}d4JT& zBJaHu2*b69eiq-GG78d?d3Hbv2zqf<%4?jlvI6*k9T_O|C z7P-2&;^*P8ZM2tfyc+bT$dWtFbwF77k@NT(_#N}#0I%2I z=esKx2EV)fK@mRdt+&*9Soj-HkZ(c%budp=sxDrquXjf#7{kg(f#E2W1%1KyNC)!H zE6)bMyX^k=AAhF5e9z6z1>uKnnq41^;rT~^0km>yi84vI8-6Q7xd_@GyW{ zmm;)N)CISvvDYC68VcPA+Kf3hZ^Oa!^c5HbuX(Z%W4V8A=POw$Cx`l8Sn{7qN}>tA zzX^WUO&Ty>p`IddbLY9%^QkI@opCPhzv=e+iz=ox^?}2}55M>4mBfCU{{AD|ef1HF zKbFohMZNnu#>4x<|HT-}$)u$pkLiC`&Y|DQ6U4Cnjop|`&*)P!o;K}!wk@pwhweLb zPjX+B^HHb3zArLGTlm~NZ;OmQEid;0Hpts2A>u#3yZFiPw_Mr1J|`7+AU0^b5IO|5 zHOT&Wv4`3JnYexVDER39@`L^l1@_qug?(AczxK5v*Po-XLkCw}e^~vWOUST_#y-$i z^89ThD?wZM?34deIbqt3O8Z^+OIY|Zo?g+|7yY2|XNv54w-A%bKjwx14!cM_Pkq;& zeh(dg(rz*CfA}@@?N%)|LA(2z_mD4o0C^L}1eA4+VNZ@>^*>^t38wN-8~0b}t4cP; zbYq6(<6-2?_oUCg^a=|<=ABG6)&G?Hg)`02ckER0(lvO8J|S~k?0fP)kM23{@jrQr zex(;*(0N$+v(@$tmK9VV<5&m((*%DVc$c}Xx7&mJe-U`(%ENcllzsh=Z29--H!^R;9HYiq z?idz+%wc-K#Q!IMwhn^N0)H)NqmCd?>-H1>od&+b_^AGCit1}pqckdo0a@h}*z91iPe{E&?_qJ|= zd<}pOpp8I$6SRlL|Iq(NV_!P|Z<5O)k1$ureEHa4DP30{J;ukM2G3L#=1t4dAPoHH z9{$9K(ut0hCSyNr3+ew?mLdiw%)j9tS zV@34qH@~9BK^EMi#!2YoyJLhE$h)5}!oUyQeW?4pjo;J$RF#qm>PPlz=lsWkC?9o@ zPV8^$`BHse>bf6$X#JnSdX!qU&x4~_U6o#A9^ak+3SX8-cV3HujFT^1JLqW-bA*fu z{CyNg{{wrHzJJ*GyF)MHd-frbnZwmM5^2G)wSQt>)J>Omjf=y;58fZhwKSEsH|*!@ zz%O;xw)56T3swQS!byr}7R};6WhW>*-g-mAKgc-u>ETIlJHj}p+>fZT?R6_UkU3Ht z<3YXMR%bcn1#ewp;79KNo3Q_u$=pA0-C>^-f2zuI)-JV*TG5%qo!xUP^8FA7ey*K? z|NHx2b+>mOImeoM&`Wn0 ze^~3EYUSI&dm6A^1Wea}Ct5HktPSh=W@MAjJ%|w((k~5ss}>Yu*Z-#d0oajy##&OB|ERP1PGe0+m1*1ehvzugHrHQ14c`4yWA%IA z%Efmb-jM7#9_wKT3_C(jUwV)ICt$wTw(S<|ZI0vIf&IR=^Q}ixRUOZP_g)51ZiKu( z4gL##{pr8fSH5kgRO(*poxR! zlW2zV2L1Ad`V-}_M(7&YnxkQR5{M@TBKA5Ry81lCu9qmCBlVDvap|N?#r2uFgfk}_ zmwC|Lk3rWght6JwSa&_vI&8wV`ON(UH6mAIozqflZ)JR}bvtxndaWPh9FrU#>qozy z9E3lekSB`P4_a_MhCVUJBpYE1Iljp83C5qCe_AeG$=~q<(%w0&=^N@}l2k3@~d%@j5w=%)fE` z0XnD4*t${u&=1fjZ&L1c{p2^!QTA>e-1TT9 ziOWy5G@Em1;<88&}B;Ao#akR$3qX~ZKyq=kAd#5)yNFSVYmzcBW#v^n0?tBKc zZS~{oy7!m!<0vnc)(_o%DDXSp`k7a$hwdQ%GY3E6*NmIgxcrLw-dff9PRz%qUU22m zBLgP*o09Sa)=hc$ww*G-+yv*pael*X;74z)>TSis8Qtf9=)T_MeSiJ@&s0!Uq3OD2Tj*hOuMA3?lrDIY3Ka)^Ihsb>eLOZ zicw+rwFf5`$8K6b=s&ht{aXKlJ}xhCK8E(cxZZ+xfb+?;4@jD^PVa8Q|IGjR`ML!r05Q z`yJO_vOhR4i?V1^Kk-ijCO%6&TxtG~Ic?0Xakjl-J9@1Auh5;O1Tl2E{xE?Y6YR2_$UN&xTrO!$^)x0ql zvI#%#>{4|w#=HUc%Oq}REI^-7`-Sw4SU&nYQ>y$rX*2SR&CMj1w z+VK^%-<_Yre&hT|##o!zVLs&xA`_v{Xooo8KyUPUXouW>4xm4E7<8&der8{}wgfnc zg|W@lKZ$HvuS-@(_C5Q_gnwY8ze^ja%jbEjO0{{(?80*SkF@7ElgL9R<->*?PTI3v zK2OZV@DhAr|Hhbs{X-v4KhJl{(I2|~NbCLsUjK8nwmAC!*_5_t* zhwn_%wz7P-o4CDx74t02X>lIu6L+cH1J~SoW4dzfr(NPWiAx*sKaX2LW4$fDP2@M= zPx=OW+XVSwAF(|_?MMF(%KH*--j6!X`z<8SeR*_xVXo?#dzHRp*+J#we!<#A`qDx3 z$T?R)`(HujgAdO0u6-e9VqlxK&!XwVH4Tij{Ns@2=nwzrQr_2Cd8A%p8I)bF_ojZH zGe+e!c%3?mF)nxV^JvI?Dg6&$V+6+mz2)n3_Mx^flvn;Ie@!}1t=VQ?X8c)@2_>IJ z?F(Y?bFM@A>|@$TV$r*{m*g+?6My3p;&OuGab`>q38A zN=*13;b`XZ`CT%MA3j7I4WDmi>KJ?dgtM(J7#n++m0W)&+7Ft7J|*pYal6^|DvImT z+D8s*`;8oo^RLQI`R=$O(kI<-R~P?;eiA;#?2f6J_(>c$<##sd_hjHRg3peG?sdO0 zi?$EJ`EQ{EE(R|S1r7T6TgUH{HP__*8t6NWy-EXk6*(%^H~D^V^{yB!B;JK){R#nQ`*+CPtmr5Z;G<*c>fjlFL{z<@tn)gZynHXG6(M3e6)w} zV;;*4Z~gR_ajhM`pNu{g-+_P}anm_Oo^viKWxLq>81wr~T|(X(4%zUop<%qm?}u?? zPLw=OKK0k%P%U4CkMT|&dH-C@U%y@Hkcl|gc{}P>`eDWDuZzpqVS_tpXQ&&l{ISSA z<9E+@Bpuv!J5|Nscl#VXN*zBO&+r=`dZR68e)_83l#$}^*C*tMw4W)w%Q@oi=AaG8 zc`*OU?=Mi7&}J8FKk7e%{uOm1ze&K&Mjvp^wf-gx*DG<2O0oA*cVGIK)Jvo}{TKf> z?NU{KvG+4^tA@Nsy+r=uv+Mt(%AvjInlRFycDWVx`1_A}u<-+xerdq{5$FRi|8fmB z^TAW#t4ufr?d(^`zm&J88v9A;l{MHmqA$1A^|LLO0ouw)@YRxA6{1-eLN^~>C_kE) zAMPs$!2kMqW`KOI4&St9ewe!Jnj@qZ_HzhIxM0#5a@EMHuJWtPh>j5jA__zlh$s+I zAfiA-frtVT1tJPW6o@DgQ6Qo~M1hC`5d|U&L==c95K$naKtzFv0ucow3Pco$C=gK~ QqCiA}hyoD>_J0ce7gEZOiU0rr diff --git a/lib/builder-webpack4/src/standalone.ts b/lib/builder-webpack4/src/standalone.ts deleted file mode 100644 index f3452eb6c6a..00000000000 --- a/lib/builder-webpack4/src/standalone.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { buildStaticStandalone } from './build-static'; -import { buildDevStandalone } from './build-dev'; - -async function build(options: any = {}, frameworkOptions: any = {}) { - const { mode = 'dev' } = options; - - const commonOptions = { - ...options, - ...frameworkOptions, - frameworkPresets: [ - ...(options.frameworkPresets || []), - ...(frameworkOptions.frameworkPresets || []), - ], - }; - - if (mode === 'dev') { - return buildDevStandalone(commonOptions); - } - - if (mode === 'static') { - return buildStaticStandalone(commonOptions); - } - - throw new Error(`'mode' parameter should be either 'dev' or 'static'`); -} - -export default build; diff --git a/lib/builder-webpack4/src/utils/template.ts b/lib/builder-webpack4/src/template.ts similarity index 57% rename from lib/builder-webpack4/src/utils/template.ts rename to lib/builder-webpack4/src/template.ts index d86fb03ae55..d37cf222b2b 100644 --- a/lib/builder-webpack4/src/utils/template.ts +++ b/lib/builder-webpack4/src/template.ts @@ -5,10 +5,7 @@ const interpolate = (string: string, data: Record = {}) => Object.entries(data).reduce((acc, [k, v]) => acc.replace(new RegExp(`%${k}%`, 'g'), v), string); export function getPreviewBodyHtml(configDirPath: string, interpolations?: Record) { - const base = fs.readFileSync( - path.resolve(__dirname, '../templates/base-preview-body.html'), - 'utf8' - ); + const base = fs.readFileSync(path.resolve(__dirname, 'templates/base-preview-body.html'), 'utf8'); const bodyHtmlPath = path.resolve(configDirPath, 'preview-body.html'); let result = base; @@ -21,10 +18,7 @@ export function getPreviewBodyHtml(configDirPath: string, interpolations?: Recor } export function getPreviewHeadHtml(configDirPath: string, interpolations?: Record) { - const base = fs.readFileSync( - path.resolve(__dirname, '../templates/base-preview-head.html'), - 'utf8' - ); + const base = fs.readFileSync(path.resolve(__dirname, 'templates/base-preview-head.html'), 'utf8'); const headHtmlPath = path.resolve(configDirPath, 'preview-head.html'); let result = base; @@ -35,19 +29,3 @@ export function getPreviewHeadHtml(configDirPath: string, interpolations?: Recor return interpolate(result, interpolations); } - -export function getManagerHeadHtml(configDirPath: string, interpolations: Record) { - const base = fs.readFileSync( - path.resolve(__dirname, '../templates/base-manager-head.html'), - 'utf8' - ); - const scriptPath = path.resolve(configDirPath, 'manager-head.html'); - - let result = base; - - if (fs.existsSync(scriptPath)) { - result += fs.readFileSync(scriptPath, 'utf8'); - } - - return interpolate(result, interpolations); -} diff --git a/lib/builder-webpack4/src/templates/base-manager-head.html b/lib/builder-webpack4/src/templates/base-manager-head.html deleted file mode 100644 index 8141504c0f2..00000000000 --- a/lib/builder-webpack4/src/templates/base-manager-head.html +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/lib/builder-webpack4/src/types.ts b/lib/builder-webpack4/src/types.ts index bcd4062089f..03f7c7253ef 100644 --- a/lib/builder-webpack4/src/types.ts +++ b/lib/builder-webpack4/src/types.ts @@ -1,120 +1,6 @@ -import { Configuration, Stats } from 'webpack'; -import { TransformOptions } from '@babel/core'; -import { typeScriptDefaults } from './config/defaults'; - -/** - * ⚠️ This file contains internal WIP types they MUST NOT be exported outside this package for now! - */ -export interface ManagerWebpackOptions { - configDir: any; - configType?: string; - docsMode?: boolean; - entries: string[]; - refs: Record; - uiDll: boolean; - dll: boolean; - outputDir?: string; - cache: boolean; - previewUrl?: string; - versionCheck: VersionCheck; - releaseNotesData: ReleaseNotesData; - presets: any; -} - -export interface Presets { - apply( - extension: 'typescript', - config: typeof typeScriptDefaults, - args: StorybookConfigOptions & { presets: Presets } - ): Promise; - apply(extension: 'babel', config: {}, args: any): Promise; - apply(extension: 'entries', config: [], args: any): Promise; - apply(extension: 'stories', config: [], args: any): Promise; - apply( - extension: 'webpack', - config: {}, - args: { babelOptions?: TransformOptions } & any - ): Promise; - apply(extension: 'managerEntries', config: [], args: any): Promise; - apply(extension: 'refs', config: [], args: any): Promise; - apply( - extension: 'managerWebpack', - config: {}, - args: { babelOptions?: TransformOptions } & ManagerWebpackOptions - ): Promise; - apply(extension: string, config: unknown, args: unknown): Promise; -} - -export interface LoadedPreset { - name: string; - preset: any; - options: any; -} - -export interface StorybookConfigOptions { - configType: 'DEVELOPMENT' | 'PRODUCTION'; - outputDir?: string; - configDir: string; - cache?: any; - framework: string; -} - -export interface PresetsOptions { - corePresets: string[]; - overridePresets: string[]; - frameworkPresets: string[]; -} - -export type PresetConfig = - | string - | { - name: string; - options?: unknown; - }; - -export interface Ref { - id: string; - url: string; - title: string; - version: string; - type?: string; -} - -export interface VersionCheck { - success: boolean; - data?: any; - error?: any; - time: number; -} - -export interface ReleaseNotesData { - success: boolean; - currentVersion: string; - showOnFirstLaunch: boolean; -} +import { Stats } from 'webpack'; export interface PreviewResult { previewStats?: Stats; previewTotalTime?: [number, number]; } - -export interface ManagerResult { - managerStats?: Stats; - managerTotalTime?: [number, number]; -} - -// TODO: this is a generic interface that we can share across multiple SB packages (like @storybook/cli) -export interface PackageJson { - name: string; - version: string; - dependencies?: Record; - devDependencies?: Record; -} - -// TODO: This could be exported to the outside world and used in `options.ts` file of each `@storybook/APP` -// like it's described in docs/api/new-frameworks.md -export interface LoadOptions { - packageJson: PackageJson; - framework: string; - frameworkPresets: string[]; -} diff --git a/lib/builder-webpack4/src/utils/__snapshots__/merge-webpack-config.test.ts.snap b/lib/builder-webpack4/src/utils/__snapshots__/merge-webpack-config.test.ts.snap deleted file mode 100644 index ddc929b507a..00000000000 --- a/lib/builder-webpack4/src/utils/__snapshots__/merge-webpack-config.test.ts.snap +++ /dev/null @@ -1,150 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`mergeConfigs merges partial custom config 1`] = ` -Object { - "devtool": "source-map", - "entry": Object { - "bundle": "index.js", - }, - "module": Object { - "rules": Array [ - Object { - "use": "r1", - }, - Object { - "use": "r2", - }, - ], - }, - "optimization": Object { - "runtimeChunk": true, - "splitChunks": Object { - "chunks": "all", - }, - }, - "output": Object { - "filename": "[name].js", - }, - "plugins": Array [ - "p1", - "p2", - "p3", - ], - "resolve": Object { - "alias": Object { - "A1": "src/B1", - "A2": "src/B2", - }, - "enforceExtension": true, - "extensions": Array [ - ".js", - ".json", - ".ts", - ".tsx", - ], - }, -} -`; - -exports[`mergeConfigs merges successfully if custom config is empty 1`] = ` -Object { - "devtool": "source-map", - "entry": Object { - "bundle": "index.js", - }, - "module": Object { - "rules": Array [ - Object { - "use": "r1", - }, - Object { - "use": "r2", - }, - ], - }, - "optimization": Object { - "runtimeChunk": true, - "splitChunks": Object { - "chunks": "all", - }, - }, - "output": Object { - "filename": "[name].js", - }, - "plugins": Array [ - "p1", - "p2", - ], - "resolve": Object { - "alias": Object { - "A1": "src/B1", - "A2": "src/B2", - }, - "enforceExtension": true, - "extensions": Array [ - ".js", - ".json", - ], - }, -} -`; - -exports[`mergeConfigs merges two full configs in one 1`] = ` -Object { - "devtool": "source-map", - "entry": Object { - "bundle": "index.js", - }, - "module": Object { - "noParse": /jquery\\|lodash/, - "rules": Array [ - Object { - "use": "r1", - }, - Object { - "use": "r2", - }, - Object { - "use": "r3", - }, - Object { - "use": "r4", - }, - ], - }, - "optimization": Object { - "minimizer": Array [ - "banana", - ], - "runtimeChunk": true, - "splitChunks": Object { - "chunks": "all", - }, - }, - "output": Object { - "filename": "[name].js", - }, - "plugins": Array [ - "p1", - "p2", - "p3", - "p4", - ], - "profile": true, - "resolve": Object { - "alias": Object { - "A1": "src/B1", - "A2": "src/B2", - "A3": "src/B3", - "A4": "src/B4", - }, - "enforceExtension": false, - "extensions": Array [ - ".js", - ".json", - ".ts", - ".tsx", - ], - }, -} -`; diff --git a/lib/builder-webpack4/src/utils/interpret-files.test.ts b/lib/builder-webpack4/src/utils/interpret-files.test.ts deleted file mode 100644 index f8759ddc20f..00000000000 --- a/lib/builder-webpack4/src/utils/interpret-files.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import mock from 'mock-fs'; -import { getInterpretedFile } from './interpret-files'; - -describe('interpret-files', () => { - it('will interpret file as file.ts when it exists in fs', () => { - mock({ - 'path/to/file.ts': 'ts file contents', - }); - - const file = getInterpretedFile('path/to/file'); - - expect(file).toEqual('path/to/file.ts'); - }); - - it('will interpret file as file.js when both are in fs', () => { - mock({ - 'path/to/file.js': 'js file contents', - 'path/to/file.ts': 'ts file contents', - }); - - const file = getInterpretedFile('path/to/file'); - - 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', - }); - - const file = getInterpretedFile('path/to/file2'); - - expect(file).toBeUndefined(); - }); - - afterEach(mock.restore); -}); diff --git a/lib/builder-webpack4/src/utils/load-manager-or-addons-file.ts b/lib/builder-webpack4/src/utils/load-manager-or-addons-file.ts deleted file mode 100644 index e89c8b04ea3..00000000000 --- a/lib/builder-webpack4/src/utils/load-manager-or-addons-file.ts +++ /dev/null @@ -1,24 +0,0 @@ -import path from 'path'; -import { logger } from '@storybook/node-logger'; -import dedent from 'ts-dedent'; - -import { getInterpretedFile } from './interpret-files'; - -export function loadManagerOrAddonsFile({ configDir }: { configDir: string }) { - const storybookCustomAddonsPath = getInterpretedFile(path.resolve(configDir, 'addons')); - const storybookCustomManagerPath = getInterpretedFile(path.resolve(configDir, 'manager')); - - if (storybookCustomAddonsPath || storybookCustomManagerPath) { - logger.info('=> Loading custom manager config'); - } - - if (storybookCustomAddonsPath && storybookCustomManagerPath) { - throw new Error(dedent` - You have both a "addons.js" and a "manager.js", remove the "addons.js" file from your configDir (${path.resolve( - configDir, - 'addons' - )})`); - } - - return storybookCustomManagerPath || storybookCustomAddonsPath; -} diff --git a/lib/builder-webpack4/src/utils/merge-webpack-config.test.ts b/lib/builder-webpack4/src/utils/merge-webpack-config.test.ts deleted file mode 100644 index c75213f7b64..00000000000 --- a/lib/builder-webpack4/src/utils/merge-webpack-config.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Configuration } from 'webpack'; -import { mergeConfigs } from './merge-webpack-config'; - -const config: Configuration = { - devtool: 'source-map', - entry: { - bundle: 'index.js', - }, - output: { - filename: '[name].js', - }, - module: { - rules: [{ use: 'r1' }, { use: 'r2' }], - }, - // For snapshot readability purposes `plugins` attribute doesn't match the correct type - // @ts-expect-errors - plugins: ['p1', 'p2'], - resolve: { - enforceExtension: true, - extensions: ['.js', '.json'], - alias: { - A1: 'src/B1', - A2: 'src/B2', - }, - }, - optimization: { - splitChunks: { - chunks: 'all', - }, - runtimeChunk: true, - }, -}; - -describe('mergeConfigs', () => { - it('merges two full configs in one', () => { - const customConfig: Configuration = { - profile: true, - entry: { - bundle: 'should_not_be_merged.js', - }, - output: { - filename: 'should_not_be_merged.js', - }, - module: { - noParse: /jquery|lodash/, - rules: [{ use: 'r3' }, { use: 'r4' }], - }, - // For snapshot readability purposes `plugins` attribute doesn't match the correct type - // @ts-expect-errors - plugins: ['p3', 'p4'], - resolve: { - enforceExtension: false, - extensions: ['.ts', '.tsx'], - alias: { - A3: 'src/B3', - A4: 'src/B4', - }, - }, - optimization: { - // For snapshot readability purposes `minimizer` attribute doesn't match the correct type - // @ts-expect-errors - minimizer: ['banana'], - }, - }; - - const result = mergeConfigs(config, customConfig); - - expect(result).toMatchSnapshot(); - }); - - it('merges partial custom config', () => { - const customConfig: Configuration = { - // For snapshot readability purposes `plugins` attribute doesn't match the correct type - // @ts-expect-errors - plugins: ['p3'], - resolve: { - extensions: ['.ts', '.tsx'], - }, - }; - - const result = mergeConfigs(config, customConfig); - - expect(result).toMatchSnapshot(); - }); - - it('merges successfully if custom config is empty', () => { - const customConfig = {}; - - const result = mergeConfigs(config, customConfig); - - expect(result).toMatchSnapshot(); - }); -}); diff --git a/lib/builder-webpack4/src/utils/merge-webpack-config.ts b/lib/builder-webpack4/src/utils/merge-webpack-config.ts deleted file mode 100644 index a7dcacecf5e..00000000000 --- a/lib/builder-webpack4/src/utils/merge-webpack-config.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Configuration } from 'webpack'; - -function plugins( - { plugins: defaultPlugins = [] }: Configuration, - { plugins: customPlugins = [] }: Configuration -): Configuration['plugins'] { - return [...defaultPlugins, ...customPlugins]; -} - -function rules( - { rules: defaultRules = [] }: Configuration['module'], - { rules: customRules = [] }: Configuration['module'] -): Configuration['module']['rules'] { - return [...defaultRules, ...customRules]; -} - -function extensions( - { extensions: defaultExtensions = [] }: Configuration['resolve'], - { extensions: customExtensions = [] }: Configuration['resolve'] -): Configuration['resolve']['extensions'] { - return [...defaultExtensions, ...customExtensions]; -} - -function alias( - { alias: defaultAlias = {} }: Configuration['resolve'], - { alias: customAlias = {} }: Configuration['resolve'] -): Configuration['resolve']['alias'] { - return { - ...defaultAlias, - ...customAlias, - }; -} - -function module( - { module: defaultModule = { rules: [] } }: Configuration, - { module: customModule = { rules: [] } }: Configuration -): Configuration['module'] { - return { - ...defaultModule, - ...customModule, - rules: rules(defaultModule, customModule), - }; -} - -function resolve( - { resolve: defaultResolve = {} }: Configuration, - { resolve: customResolve = {} }: Configuration -): Configuration['resolve'] { - return { - ...defaultResolve, - ...customResolve, - alias: alias(defaultResolve, customResolve), - extensions: extensions(defaultResolve, customResolve), - }; -} - -function optimization( - { optimization: defaultOptimization = {} }: Configuration, - { optimization: customOptimization = {} }: Configuration -): Configuration['optimization'] { - return { - ...defaultOptimization, - ...customOptimization, - }; -} - -export function mergeConfigs(config: Configuration, customConfig: Configuration): Configuration { - return { - // We'll always load our configurations after the custom config. - // So, we'll always load the stuff we need. - ...customConfig, - ...config, - devtool: customConfig.devtool || config.devtool, - plugins: plugins(config, customConfig), - module: module(config, customConfig), - resolve: resolve(config, customConfig), - optimization: optimization(config, customConfig), - }; -} - -export default mergeConfigs; diff --git a/lib/builder-webpack4/src/utils/middleware.ts b/lib/builder-webpack4/src/utils/middleware.ts deleted file mode 100644 index b0f37bd82e2..00000000000 --- a/lib/builder-webpack4/src/utils/middleware.ts +++ /dev/null @@ -1,14 +0,0 @@ -import path from 'path'; -import fs from 'fs'; - -export function getMiddleware(configDir: string) { - const middlewarePath = path.resolve(configDir, 'middleware.js'); - if (fs.existsSync(middlewarePath)) { - let middlewareModule = require(middlewarePath); // eslint-disable-line - if (middlewareModule.__esModule) { // eslint-disable-line - middlewareModule = middlewareModule.default; - } - return middlewareModule; - } - return () => {}; -} diff --git a/lib/builder-webpack4/src/utils/prebuilt-manager.ts b/lib/builder-webpack4/src/utils/prebuilt-manager.ts deleted file mode 100644 index 860a7eefad3..00000000000 --- a/lib/builder-webpack4/src/utils/prebuilt-manager.ts +++ /dev/null @@ -1,50 +0,0 @@ -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, -}: { - configDir: string; - options: { managerCache?: boolean; smokeTest?: boolean }; -}): Promise => { - if (options.managerCache === false || options.smokeTest) 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: string) => !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/builder-webpack4/src/utils/resolve-file.ts b/lib/builder-webpack4/src/utils/resolve-file.ts deleted file mode 100644 index b5d262be8df..00000000000 --- a/lib/builder-webpack4/src/utils/resolve-file.ts +++ /dev/null @@ -1 +0,0 @@ -export const resolveFile = require.resolve; diff --git a/lib/builder-webpack4/src/utils/static-files.test.ts b/lib/builder-webpack4/src/utils/static-files.test.ts deleted file mode 100644 index a034e5593c0..00000000000 --- a/lib/builder-webpack4/src/utils/static-files.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import fs from 'fs-extra'; -import path from 'path'; -import { parseStaticDir } from './static-files'; - -fs.pathExists = jest.fn().mockReturnValue(true); - -describe('parseStaticDir', () => { - it('returns the static dir/path and default target', async () => { - await expect(parseStaticDir('public')).resolves.toEqual({ - staticDir: './public', - staticPath: path.resolve('public'), - targetDir: './', - targetEndpoint: '/', - }); - - await expect(parseStaticDir('foo/bar')).resolves.toEqual({ - staticDir: './foo/bar', - staticPath: path.resolve('foo/bar'), - targetDir: './', - targetEndpoint: '/', - }); - }); - - it('returns the static dir/path and custom target', async () => { - await expect(parseStaticDir('public:/custom-endpoint')).resolves.toEqual({ - staticDir: './public', - staticPath: path.resolve('public'), - targetDir: './custom-endpoint', - targetEndpoint: '/custom-endpoint', - }); - - await expect(parseStaticDir('foo/bar:/custom-endpoint')).resolves.toEqual({ - staticDir: './foo/bar', - staticPath: path.resolve('foo/bar'), - targetDir: './custom-endpoint', - targetEndpoint: '/custom-endpoint', - }); - }); - - it('supports absolute file paths', async () => { - await expect(parseStaticDir('/foo/bar')).resolves.toEqual({ - staticDir: '/foo/bar', - staticPath: '/foo/bar', - targetDir: './', - targetEndpoint: '/', - }); - - await expect(parseStaticDir('C:\\foo\\bar')).resolves.toEqual({ - staticDir: expect.any(String), // can't test this properly on unix - staticPath: path.resolve('C:\\foo\\bar'), - targetDir: './', - targetEndpoint: '/', - }); - }); - - it('supports absolute file paths with custom endpoint', async () => { - await expect(parseStaticDir('/foo/bar:/custom-endpoint')).resolves.toEqual({ - staticDir: '/foo/bar', - staticPath: '/foo/bar', - targetDir: './custom-endpoint', - targetEndpoint: '/custom-endpoint', - }); - - await expect(parseStaticDir('C:\\foo\\bar:/custom-endpoint')).resolves.toEqual({ - staticDir: expect.any(String), // can't test this properly on unix - staticPath: path.resolve('C:\\foo\\bar'), - targetDir: './custom-endpoint', - targetEndpoint: '/custom-endpoint', - }); - }); - - it('pins relative endpoint at root', async () => { - const normal = await parseStaticDir('public:relative-endpoint'); - expect(normal.targetEndpoint).toBe('/relative-endpoint'); - - const windows = await parseStaticDir('C:\\public:relative-endpoint'); - expect(windows.targetEndpoint).toBe('/relative-endpoint'); - }); - - it('checks that the path exists', async () => { - fs.pathExists = jest.fn().mockReturnValueOnce(false); - await expect(parseStaticDir('nonexistent')).rejects.toThrow(path.resolve('nonexistent')); - }); -}); diff --git a/lib/builder-webpack4/src/utils/static-files.ts b/lib/builder-webpack4/src/utils/static-files.ts deleted file mode 100644 index 205f066cec3..00000000000 --- a/lib/builder-webpack4/src/utils/static-files.ts +++ /dev/null @@ -1,24 +0,0 @@ -import path from 'path'; -import { pathExists } from 'fs-extra'; -import dedent from 'ts-dedent'; -import chalk from 'chalk'; - -export const parseStaticDir = async (arg: string) => { - // Split on ':' only if not followed by '\', for Windows compatibility (e.g. 'C:\some\dir') - const [rawDir, target = '/'] = arg.split(/:(?!\\)/); - const staticDir = path.isAbsolute(rawDir) ? rawDir : `./${rawDir}`; - const staticPath = path.resolve(staticDir); - const targetDir = target.replace(/^\/?/, './'); - const targetEndpoint = targetDir.substr(1); - - if (!(await pathExists(staticPath))) { - throw new Error( - dedent(chalk` - Failed to load static files, no such directory: {cyan ${staticPath}} - Make sure this directory exists, or omit the {bold -s (--static-dir)} option. - `) - ); - } - - return { staticDir, staticPath, targetDir, targetEndpoint }; -}; diff --git a/lib/builder-webpack4/src/utils/template.test.ts b/lib/builder-webpack4/src/utils/template.test.ts deleted file mode 100644 index dfc7da05dad..00000000000 --- a/lib/builder-webpack4/src/utils/template.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import mock from 'mock-fs'; -import { getPreviewHeadHtml, getPreviewBodyHtml } from './template'; - -const HEAD_HTML_CONTENTS = ''; -const BASE_HTML_CONTENTS = ''; - -const BASE_BODY_HTML_CONTENTS = '
story contents
'; -const BODY_HTML_CONTENTS = '
custom body contents
'; - -describe('server.getPreviewHeadHtml', () => { - describe('when .storybook/preview-head.html does not exist', () => { - beforeEach(() => { - mock({ - [`${__dirname}/../templates/base-preview-head.html`]: BASE_HTML_CONTENTS, - config: {}, - }); - }); - - afterEach(() => { - mock.restore(); - }); - - it('return an empty string', () => { - const result = getPreviewHeadHtml('./config'); - expect(result).toEqual(BASE_HTML_CONTENTS); - }); - }); - - describe('when .storybook/preview-head.html exists', () => { - beforeEach(() => { - mock({ - [`${__dirname}/../templates/base-preview-head.html`]: BASE_HTML_CONTENTS, - config: { - 'preview-head.html': HEAD_HTML_CONTENTS, - }, - }); - }); - - afterEach(() => { - mock.restore(); - }); - - it('return the contents of the file', () => { - const result = getPreviewHeadHtml('./config'); - expect(result).toEqual(BASE_HTML_CONTENTS + HEAD_HTML_CONTENTS); - }); - }); -}); - -describe('server.getPreviewBodyHtml', () => { - describe('when .storybook/preview-body.html does not exist', () => { - beforeEach(() => { - mock({ - [`${__dirname}/../templates/base-preview-body.html`]: BASE_BODY_HTML_CONTENTS, - config: {}, - }); - }); - - afterEach(() => { - mock.restore(); - }); - - it('return an empty string', () => { - const result = getPreviewBodyHtml('./config'); - expect(result).toEqual(BASE_BODY_HTML_CONTENTS); - }); - }); - - describe('when .storybook/preview-body.html exists', () => { - beforeEach(() => { - mock({ - [`${__dirname}/../templates/base-preview-body.html`]: BASE_BODY_HTML_CONTENTS, - config: { - 'preview-body.html': BODY_HTML_CONTENTS, - }, - }); - }); - - afterEach(() => { - mock.restore(); - }); - - it('return the contents of the file', () => { - const result = getPreviewBodyHtml('./config'); - expect(result).toEqual(BODY_HTML_CONTENTS + BASE_BODY_HTML_CONTENTS); - }); - }); -}); diff --git a/lib/builder-webpack4/src/utils/validate-configuration-files.ts b/lib/builder-webpack4/src/utils/validate-configuration-files.ts deleted file mode 100644 index ba19be9b361..00000000000 --- a/lib/builder-webpack4/src/utils/validate-configuration-files.ts +++ /dev/null @@ -1,70 +0,0 @@ -import dedent from 'ts-dedent'; -import deprecate from 'util-deprecate'; -import glob from 'glob'; -import path from 'path'; - -import { boost } from './interpret-files'; - -const warnLegacyConfigurationFiles = deprecate( - () => {}, - dedent` - Configuration files such as "config", "presets" and "addons" are deprecated and will be removed in Storybook 7.0. - Read more about it in the migration guide: https://github.com/storybookjs/storybook/blob/master/MIGRATION.md#to-mainjs-configuration - ` -); - -const errorMixingConfigFiles = (first: string, second: string, configDir: string) => { - const firstPath = path.resolve(configDir, first); - const secondPath = path.resolve(configDir, second); - throw new Error(dedent` - You have mixing configuration files: - ${firstPath} - ${secondPath} - "${first}" and "${second}" cannot coexist. - Please check the documentation for migration steps: https://github.com/storybookjs/storybook/blob/master/MIGRATION.md#to-mainjs-configuration - `); -}; - -export default function validateConfigurationFiles(configDir: string) { - const extensionsPattern = `{${Array.from(boost).join(',')}}`; - const exists = (file: string) => - !!glob.sync(path.resolve(configDir, `${file}${extensionsPattern}`)).length; - - const main = exists('main'); - const config = exists('config'); - - if (!main && !config) { - throw new Error(dedent` - No configuration files have been found in your configDir (${path.resolve(configDir)}). - Storybook needs either a "main" or "config" file. - `); - } - - if (main && config) { - throw new Error(dedent` - You have both a "main" and a "config". Please remove the "config" file from your configDir (${path.resolve( - configDir, - 'config' - )})`); - } - - const presets = exists('presets'); - if (main && presets) { - errorMixingConfigFiles('main', 'presets', configDir); - } - - const preview = exists('preview'); - if (preview && config) { - errorMixingConfigFiles('preview', 'config', configDir); - } - - const addons = exists('addons'); - const manager = exists('manager'); - if (manager && addons) { - errorMixingConfigFiles('manager', 'addons', configDir); - } - - if (presets || config || addons) { - warnLegacyConfigurationFiles(); - } -} diff --git a/lib/builder-webpack4/types/index.ts b/lib/builder-webpack4/types/index.ts deleted file mode 100644 index 05fefb16730..00000000000 --- a/lib/builder-webpack4/types/index.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; -import type { PluginOptions } from 'react-docgen-typescript-plugin'; -import { Configuration } from 'webpack'; - -type Preset = string | { name: string }; - -/** - * The interface for Storybook configuration in `main.ts` files. - */ -export interface StorybookConfig { - /** - * Sets the addons you want to use with Storybook. - * - * @example `['@storybook/addon-essentials']` or `[{ name: '@storybook/addon-essentials', options: { backgrounds: false } }]` - */ - addons?: Array< - | string - | { - name: string; - options?: any; - } - >; - /** - * Tells Storybook where to find stories. - * - * @example `['./src/*.stories.@(j|t)sx?']` - */ - stories: string[]; - /** - * Controls how Storybook handles TypeScript files. - */ - typescript?: Partial; - /** - * Modify or return a custom Webpack config. - */ - webpackFinal?: ( - config: Configuration, - options: StorybookOptions - ) => Configuration | Promise; -} - -/** - * The internal options object, used by Storybook frameworks and addons. - */ -export interface StorybookOptions { - configType: 'DEVELOPMENT' | 'PRODUCTION'; - presetsList: Preset[]; - typescriptOptions: TypescriptOptions; - [key: string]: any; -} - -/** - * Options for TypeScript usage within Storybook. - */ -export interface TypescriptOptions { - /** - * Enables type checking within Storybook. - * - * @default `false` - */ - check: boolean; - /** - * Configures `fork-ts-checker-webpack-plugin` - */ - checkOptions?: ForkTsCheckerWebpackPlugin['options']; - /** - * Sets the type of Docgen when working with React and TypeScript - * - * @default `'react-docgen-typescript'` - */ - reactDocgen: 'react-docgen-typescript' | 'react-docgen' | false; - /** - * Configures `react-docgen-typescript-plugin` - * - * @default - * @see https://github.com/storybookjs/storybook/blob/next/lib/builder-webpack4/src/config/defaults.js#L4-L6 - */ - reactDocgenTypescriptOptions: PluginOptions; -} diff --git a/lib/builder-webpack4/typings.d.ts b/lib/builder-webpack4/typings.d.ts index 94f3fdaa253..d6299bfb8a3 100644 --- a/lib/builder-webpack4/typings.d.ts +++ b/lib/builder-webpack4/typings.d.ts @@ -1,36 +1,4 @@ -declare module 'global'; -declare module '@storybook/semver'; -declare module 'unfetch/dist/unfetch'; declare module 'lazy-universal-dotenv'; declare module 'pnp-webpack-plugin'; declare module '@storybook/theming/paths'; -declare module '@storybook/ui/paths'; -declare module 'better-opn'; -declare module '@storybook/ui'; - -declare module 'file-system-cache' { - export interface Options { - basePath?: string; - ns?: string | string[]; - extension?: string; - } - - export declare class FileSystemCache { - constructor(options: Options); - path(key: string): string; - fileExists(key: string): Promise; - ensureBasePath(): Promise; - get(key: string, defaultValue?: any): Promise; - getSync(key: string, defaultValue?: any): any | typeof defaultValue; - set(key: string, value: any): Promise<{ path: string }> - setSync(key: string, value: any): this; - remove(key: string): Promise; - clear(): Promise; - save(): Promise<{ paths: string[] }>; - load(): Promise<{ files: Array<{ path: string, value: any }> }>; - } - - function create(options: Options): FileSystemCache; - - export = create; -} +declare module 'global'; diff --git a/lib/core-server/src/dev-server.ts b/lib/core-server/src/dev-server.ts index 40b56a25582..617abbd251a 100644 --- a/lib/core-server/src/dev-server.ts +++ b/lib/core-server/src/dev-server.ts @@ -15,7 +15,7 @@ import webpack, { Compiler, ProgressPlugin, Stats } from 'webpack'; import webpackDevMiddleware from 'webpack-dev-middleware'; import Cache, { FileSystemCache } from 'file-system-cache'; -import * as previewBuilder from '@storybook/builder-webpack5'; +import * as previewBuilder from '@storybook/builder-webpack4'; import { getMiddleware } from './utils/middleware'; import { logConfig } from './logConfig'; import loadManagerConfig from './manager/manager-config';