creevey
Version:
Cross-browser screenshot testing tool for Storybook with fancy UI Runner
300 lines (240 loc) • 15.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = compile;
var _fs = require("fs");
var _path = _interopRequireDefault(require("path"));
var _webpack = _interopRequireDefault(require("webpack"));
var _webpackNodeExternals = _interopRequireDefault(require("webpack-node-externals"));
var _utils = require("../../utils");
var _helpers = require("../../storybook/helpers");
var _types = require("../../../types");
var _messages = require("../../messages");
var _logger = require("../../logger");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
let isInitiated = false;
let dumpStats = _types.noop;
function handleWebpackBuild(error, stats) {
dumpStats(stats);
if (error || !stats || stats.hasErrors()) {
(0, _messages.emitWebpackMessage)({
type: isInitiated ? 'rebuild failed' : 'fail'
});
console.error('=> Failed to build the Storybook preview bundle');
if (error) return console.error(error.message);
if (stats && (stats.hasErrors() || stats.hasWarnings())) {
const {
warnings,
errors
} = stats.toJson();
errors.forEach(e => console.error(e));
warnings.forEach(e => console.error(e));
return;
}
}
stats.toJson().warnings.forEach(e => console.warn(e));
if (!isInitiated) {
isInitiated = true;
(0, _messages.emitWebpackMessage)({
type: 'success'
});
} else {
(0, _messages.emitWebpackMessage)({
type: 'rebuild succeeded'
});
}
return;
}
async function applyMdxLoader(config, areAddonsRemoved, loader) {
const {
mdxLoaders
} = await Promise.resolve().then(() => _interopRequireWildcard(require('./mdx-loader')));
mdxLoaders.splice(1, 0, loader);
const mdxRegexps = (0, _helpers.isStorybookVersionLessThan)(6, 2) ? [/\.(stories|story).mdx$/, /\.(stories|story)\.mdx$/] : [/(stories|story)\.mdx$/]; // NOTE replace md/mdx to null loader
const mdRegexps = [/\.md$/, /\.mdx$/];
if (areAddonsRemoved) {
var _config$module2;
mdRegexps.forEach(test => {
var _config$module;
return (_config$module = config.module) === null || _config$module === void 0 ? void 0 : _config$module.rules.push({
test,
exclude: /(stories|story)\.mdx$/,
use: require.resolve('null-loader')
});
});
(_config$module2 = config.module) === null || _config$module2 === void 0 ? void 0 : _config$module2.rules.push({
test: /(stories|story)\.mdx$/,
use: mdxLoaders
});
} else {
var _config$module3, _config$module4, _config$module$rules$, _config$module5;
// NOTE Exclude addons' entry points
config.entry = Array.isArray(config.entry) ? config.entry.filter(entry => !/@storybook(\/|\\)addon/.test(entry)) : config.entry;
(_config$module3 = config.module) === null || _config$module3 === void 0 ? void 0 : _config$module3.rules.filter(rule => mdRegexps.some(test => {
var _rule$test;
return ((_rule$test = rule.test) === null || _rule$test === void 0 ? void 0 : _rule$test.toString()) == test.toString();
})).forEach(rule => rule.use = require.resolve('null-loader'));
(_config$module4 = config.module) === null || _config$module4 === void 0 ? void 0 : _config$module4.rules.filter(rule => mdxRegexps.some(test => {
var _rule$test2;
return ((_rule$test2 = rule.test) === null || _rule$test2 === void 0 ? void 0 : _rule$test2.toString()) == test.toString();
})).forEach(rule => rule.use = mdxLoaders); // NOTE Exclude source-loader
config.module = { ...config.module,
rules: (_config$module$rules$ = (_config$module5 = config.module) === null || _config$module5 === void 0 ? void 0 : _config$module5.rules.filter(rule => !(typeof rule.loader == 'string' && /@storybook(\/|\\)source-loader/.test(rule.loader)))) !== null && _config$module$rules$ !== void 0 ? _config$module$rules$ : []
};
}
}
async function getWebpackConfigForStorybook_pre6_2(framework, configDir, outputDir) {
const {
default: storybookFrameworkOptions
} = await Promise.resolve(`${(0, _helpers.resolveFromStorybook)(`@storybook/${framework}/dist/server/options`)}`).then(s => _interopRequireWildcard(require(s))); // eslint-disable-next-line node/no-missing-import, @typescript-eslint/no-unsafe-assignment, import/no-unresolved
const {
default: getConfig
} = await Promise.resolve(`${(0, _helpers.resolveFromStorybook)('@storybook/core/dist/server/config')}`).then(s => _interopRequireWildcard(require(s))); // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return getConfig({
// NOTE: 6.1 storybook don't support quite any more. But we still have older versions
quiet: true,
configType: 'PRODUCTION',
outputDir,
cache: {},
// eslint-disable-next-line node/no-missing-require
corePresets: [(0, _helpers.resolveFromStorybook)('@storybook/core/dist/server/preview/preview-preset')],
overridePresets: [...((0, _helpers.hasDocsAddon)() ? [require.resolve('./mdx-loader')] : []), // eslint-disable-next-line node/no-missing-require
(0, _helpers.resolveFromStorybook)('@storybook/core/dist/server/preview/custom-webpack-preset')],
...storybookFrameworkOptions,
configDir
});
}
async function getWebpackConfigForStorybook_6_2(framework, configDir, outputDir) {
const {
default: storybookFrameworkOptions
} = await Promise.resolve(`${(0, _helpers.resolveFromStorybook)(`@storybook/${framework}/dist/cjs/server/options`)}`).then(s => _interopRequireWildcard(require(s)));
const options = {
quiet: true,
configType: 'PRODUCTION',
outputDir,
configDir,
...storybookFrameworkOptions
}; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const {
getPreviewBuilder
} = await Promise.resolve(`${(0, _helpers.resolveFromStorybook)('@storybook/core-server/dist/cjs/utils/get-preview-builder')}`).then(s => _interopRequireWildcard(require(s))); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const {
loadAllPresets
} = await Promise.resolve(`${(0, _helpers.resolveFromStorybook)('@storybook/core-common')}`).then(s => _interopRequireWildcard(require(s))); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const builder = await getPreviewBuilder(configDir); // NOTE: Copy-paste from storybook/lib/core-server/src/build-dev.ts
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const presets = loadAllPresets({
corePresets: [// eslint-disable-next-line node/no-missing-require
(0, _helpers.resolveFromStorybook)('@storybook/core-server/dist/cjs/presets/common-preset'), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
...builder.corePresets, // eslint-disable-next-line node/no-missing-require
(0, _helpers.resolveFromStorybook)('@storybook/core-server/dist/cjs/presets/babel-cache-preset')],
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
overridePresets: [...((0, _helpers.hasDocsAddon)() ? [require.resolve('./mdx-loader')] : []), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
...builder.overridePresets],
...options
}); // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
return builder.getConfig({ ...options,
presets
});
}
async function removeAddons(configDir) {
const storybookUtilsPath = (0, _helpers.isStorybookVersionLessThan)(6, 2) ? '@storybook/core/dist/server/utils' : '@storybook/core-common/dist/cjs/utils';
const serverRequireModule = (0, _helpers.isStorybookVersionLessThan)(6, 2) ? 'server-require' : 'interpret-require';
try {
var _config$core;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const {
getInterpretedFile
} = await Promise.resolve(`${(0, _helpers.resolveFromStorybook)(`${storybookUtilsPath}/interpret-files`)}`).then(s => _interopRequireWildcard(require(s))); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const {
serverRequire
} = await Promise.resolve(`${(0, _helpers.resolveFromStorybook)(`${storybookUtilsPath}/${serverRequireModule}`)}`).then(s => _interopRequireWildcard(require(s))); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const mainConfigFile = (0, _helpers.isStorybookVersion)(6) ? _path.default.join(configDir, 'main') : // eslint-disable-next-line @typescript-eslint/no-unsafe-call
getInterpretedFile(_path.default.join(configDir, 'main')); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const config = serverRequire(mainConfigFile);
if (((_config$core = config.core) === null || _config$core === void 0 ? void 0 : _config$core.builder) == 'webpack5') {
_logger.logger.warn("Be aware Creevey doesn't fully support webpack@5, some feature might not work well");
}
if (config.addons && config.stories) {
config.addons = [];
return true;
}
} catch (_) {
/* noop */
}
return false;
}
async function compile(config, {
debug,
ui
}) {
var _storybookWebpackConf, _storybookWebpackConf2, _storybookWebpackConf3, _extensions$map, _storybookWebpackConf4, _storybookWebpackConf5, _storybookWebpackConf6;
const storybookFramework = (0, _helpers.getStorybookFramework)();
const outputDir = _path.default.join((0, _utils.getCreeveyCache)(), 'storybook');
try {
(0, _fs.rmdirSync)(outputDir, {
recursive: true
});
} catch (_) {
/* noop */
}
const creeveyLoader = {
loader: require.resolve('./creevey-loader'),
options: {
debug,
storybookDir: config.storybookDir
}
}; // NOTE Remove addons by monkey patching, only for new config file (main.js)
const areAddonsRemoved = await removeAddons(config.storybookDir);
const getWebpackConfig = (0, _helpers.isStorybookVersionLessThan)(6, 2) ? getWebpackConfigForStorybook_pre6_2 : getWebpackConfigForStorybook_6_2;
const storybookWebpackConfig = await getWebpackConfig(storybookFramework, config.storybookDir, outputDir);
const extensions = (_storybookWebpackConf = (_storybookWebpackConf2 = storybookWebpackConfig.resolve) === null || _storybookWebpackConf2 === void 0 ? void 0 : _storybookWebpackConf2.extensions) !== null && _storybookWebpackConf !== void 0 ? _storybookWebpackConf : _utils.extensions;
delete storybookWebpackConfig.optimization;
storybookWebpackConfig.devtool = false;
storybookWebpackConfig.performance = false;
storybookWebpackConfig.profile = debug;
storybookWebpackConfig.mode = 'development';
storybookWebpackConfig.target = 'node';
storybookWebpackConfig.output = { ...storybookWebpackConfig.output,
filename: 'main.js'
}; // NOTE Add hack to allow stories HMR work in nodejs
Array.isArray(storybookWebpackConfig.entry) && storybookWebpackConfig.entry.unshift(require.resolve('./dummy-hmr')); // NOTE apply creevey loader to output from mdx loader
if ((0, _helpers.hasDocsAddon)()) await applyMdxLoader(storybookWebpackConfig, areAddonsRemoved, creeveyLoader); // NOTE Add creevey-loader to cut off all unnecessary code except stories meta and tests
(_storybookWebpackConf3 = storybookWebpackConfig.module) === null || _storybookWebpackConf3 === void 0 ? void 0 : _storybookWebpackConf3.rules.unshift({
enforce: 'pre',
test: new RegExp(`\\.(${(_extensions$map = extensions.map(x => x.slice(1))) === null || _extensions$map === void 0 ? void 0 : _extensions$map.join('|')})$`),
exclude: /node_modules/,
use: creeveyLoader
});
const aliases = (_storybookWebpackConf4 = (_storybookWebpackConf5 = storybookWebpackConfig.resolve) === null || _storybookWebpackConf5 === void 0 ? void 0 : _storybookWebpackConf5.alias) !== null && _storybookWebpackConf4 !== void 0 ? _storybookWebpackConf4 : {};
const excluded = ['@storybook/addons', '@storybook/api', '@storybook/channel-postmessage', '@storybook/channels', '@storybook/client-api', '@storybook/client-logger', '@storybook/components', '@storybook/core-events', '@storybook/router', '@storybook/semver', '@storybook/theming']; // NOTE Exclude from bundle all modules from node_modules
storybookWebpackConfig.externals = [...Object.entries(aliases).filter(([alias]) => excluded.includes(alias)).map(([, aliasPath]) => ({
[aliasPath]: `commonjs ${aliasPath}`
})), // NOTE Replace `@storybook/${framework}` to ../../storybook.ts
{
[`@storybook/${storybookFramework}`]: `commonjs ${require.resolve('../../storybook/entry')}`
}, (0, _webpackNodeExternals.default)({
includeAbsolutePaths: true,
allowlist: /(webpack|dummy-hmr|generated-stories-entry|generated-config-entry|generated-other-entry)/
}), // TODO Don't work well with monorepos
(0, _webpackNodeExternals.default)({
modulesDir: (0, _helpers.resolveFromStorybook)('@storybook/core').split('@storybook')[0],
includeAbsolutePaths: true,
allowlist: /(webpack|dummy-hmr|generated-stories-entry|generated-config-entry|generated-other-entry)/
})]; // NOTE Exclude some plugins
const excludedPlugins = ['DocgenPlugin', 'ForkTsCheckerWebpackPlugin'];
storybookWebpackConfig.plugins = (_storybookWebpackConf6 = storybookWebpackConfig.plugins) === null || _storybookWebpackConf6 === void 0 ? void 0 : _storybookWebpackConf6.filter(plugin => !excludedPlugins.includes(plugin.constructor.name));
const storybookWebpackCompiler = (0, _webpack.default)(storybookWebpackConfig);
if (debug) {
dumpStats = stats => (0, _fs.writeFile)(_path.default.join(config.reportDir, 'stats.json'), JSON.stringify(stats.toJson(), null, 2), _types.noop);
}
if (ui) {
const watcher = storybookWebpackCompiler.watch({}, handleWebpackBuild);
(0, _messages.subscribeOn)('shutdown', () => watcher.close(_types.noop));
} else {
storybookWebpackCompiler.run(handleWebpackBuild);
}
}