have a webpack 4 & 5 version of a somewhat minimized builder package

This commit is contained in:
Norbert de Langen 2021-02-04 15:54:39 +01:00
parent eba4c621d9
commit 165732527a
No known key found for this signature in database
GPG Key ID: 976651DA156C2825
46 changed files with 72 additions and 3635 deletions

View File

@ -1 +0,0 @@
module.exports = require('./dist/cjs/client').default;

View File

@ -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']);
});
});

View File

@ -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<VersionCheck> => {
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<ReleaseNotesData> => {
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,
});
}

View File

@ -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);
});
}

View File

@ -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<CommanderStatic & DevCliOptions> {
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 <dir-names>', '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 <ca>',
'Provide an SSL certificate authority. (Optional with --https, required if using a self-signed certificate)',
parseList
)
.option('--ssl-cert <cert>', 'Provide an SSL certificate. (Required with --https)')
.option('--ssl-key <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 };
}

View File

@ -1,2 +0,0 @@
export * from './dev';
export * from './prod';

View File

@ -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 <dir-names>', '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 };
}

View File

@ -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<string, any>, configEnv: Record<string, any>): 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();
}
}

View File

@ -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 || [];
}

View File

@ -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']);
});
});

View File

@ -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<Configuration> {
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<Configuration> = 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;

View File

@ -1,23 +0,0 @@
import ip from 'ip';
import { getServerAddresses } from './dev-server';
jest.mock('ip');
const mockedIp = ip as jest.Mocked<typeof ip>;
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/');
});
});

View File

@ -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<typeof webpackDevMiddleware>;
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<ManagerResult> => {
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<typeof webpackDevMiddleware>[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<PreviewResult> => {
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<typeof webpackDevMiddleware>[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 };
}

View File

@ -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<typeof webpackDevMiddleware>;
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<PreviewResult> => {
if (options.ignorePreview) {
return {};
}
if (options.debugWebpack) {
logConfig('Preview webpack config', previewConfig);
}
const compiler = webpack(previewConfig);
await useProgressReporting(compiler, options, startTime);
const middlewareOptions: Parameters<typeof webpackDevMiddleware>[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')];

View File

@ -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/],
});

View File

@ -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<Ref[]> => {
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<Configuration> {
const typescriptOptions = await presets.apply('typescript', { ...typeScriptDefaults }, options);
const babelOptions = await presets.apply('babel', {}, { ...options, typescriptOptions });
const definedRefs: Record<string, any> | 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<string, Ref> = {};
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<Configuration> = 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;

View File

@ -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<Configuration> {
return createDevConfig(options);
}
export async function managerEntries(
installedAddons: string[],
options: { managerEntry: string; configDir: string }
): Promise<string[]> {
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';

View File

@ -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<Configuration> => {
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,
},
}),
]
: [],
},
};
};

View File

@ -1,5 +0,0 @@
import { addons } from '@storybook/addons';
addons.setConfig({
refs: '{{refs}}',
});

View File

@ -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: {},
},
]);
});
});

View File

@ -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<string, any> =>
val != null && typeof val === 'object' && Array.isArray(val) === false;
const isFunction = (val: unknown): val is Function => typeof val === 'function';
function resolvePresetFunction<T = any>(
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<any> {
const presetResult = new Promise((resolve) => resolve(config));
if (!presets.length) {
return presetResult;
}
return presets.reduce((accumulationPromise: Promise<unknown>, { 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;

View File

@ -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) => ({

View File

@ -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')),
];

View File

@ -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<string, string> = [
'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',
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@ -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;

View File

@ -5,10 +5,7 @@ const interpolate = (string: string, data: Record<string, string> = {}) =>
Object.entries(data).reduce((acc, [k, v]) => acc.replace(new RegExp(`%${k}%`, 'g'), v), string);
export function getPreviewBodyHtml(configDirPath: string, interpolations?: Record<string, string>) {
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<string, string>) {
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<string, string>) {
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);
}

View File

@ -1,46 +0,0 @@
<style>
html, body {
overflow: hidden;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
* {
box-sizing: border-box;
}
</style>
<script>
/* globals window */
/* eslint-disable no-underscore-dangle */
try {
if (window.top !== window) {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.top.__REACT_DEVTOOLS_GLOBAL_HOOK__;
}
} catch (e) {
// eslint-disable-next-line no-console
console.warn('unable to connect to top frame for connecting dev tools');
}
window.onerror = function onerror(message, source, line, column, err) {
if (window.CONFIG_TYPE !== 'DEVELOPMENT') return;
// eslint-disable-next-line no-var, vars-on-top
var xhr = new window.XMLHttpRequest();
xhr.open('POST', '/runtime-error');
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
xhr.send(
JSON.stringify({
/* eslint-disable object-shorthand */
message: message,
source: source,
line: line,
column: column,
error: err && { message: err.message, name: err.name, stack: err.stack },
origin: 'manager',
/* eslint-enable object-shorthand */
})
);
};
</script>

View File

@ -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<string, Ref>;
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<TransformOptions>;
apply(extension: 'babel', config: {}, args: any): Promise<TransformOptions>;
apply(extension: 'entries', config: [], args: any): Promise<unknown>;
apply(extension: 'stories', config: [], args: any): Promise<unknown>;
apply(
extension: 'webpack',
config: {},
args: { babelOptions?: TransformOptions } & any
): Promise<Configuration>;
apply(extension: 'managerEntries', config: [], args: any): Promise<string[]>;
apply(extension: 'refs', config: [], args: any): Promise<unknown>;
apply(
extension: 'managerWebpack',
config: {},
args: { babelOptions?: TransformOptions } & ManagerWebpackOptions
): Promise<Configuration>;
apply(extension: string, config: unknown, args: unknown): Promise<unknown>;
}
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<string, string>;
devDependencies?: Record<string, string>;
}
// 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[];
}

View File

@ -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",
],
},
}
`;

View File

@ -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);
});

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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;

View File

@ -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 () => {};
}

View File

@ -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<string | false> => {
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;
};

View File

@ -1 +0,0 @@
export const resolveFile = require.resolve;

View File

@ -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'));
});
});

View File

@ -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 };
};

View File

@ -1,88 +0,0 @@
import mock from 'mock-fs';
import { getPreviewHeadHtml, getPreviewBodyHtml } from './template';
const HEAD_HTML_CONTENTS = '<script>console.log("custom script!");</script>';
const BASE_HTML_CONTENTS = '<script>console.log("base script!");</script>';
const BASE_BODY_HTML_CONTENTS = '<div>story contents</div>';
const BODY_HTML_CONTENTS = '<div>custom body contents</div>';
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);
});
});
});

View File

@ -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();
}
}

View File

@ -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<TypescriptOptions>;
/**
* Modify or return a custom Webpack config.
*/
webpackFinal?: (
config: Configuration,
options: StorybookOptions
) => Configuration | Promise<Configuration>;
}
/**
* 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;
}

View File

@ -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<boolean>;
ensureBasePath(): Promise<void>;
get(key: string, defaultValue?: any): Promise<any | typeof defaultValue>;
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<void>;
clear(): Promise<void>;
save(): Promise<{ paths: string[] }>;
load(): Promise<{ files: Array<{ path: string, value: any }> }>;
}
function create(options: Options): FileSystemCache;
export = create;
}
declare module 'global';

View File

@ -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';