UNPKG

storybook-builder-rsbuild

Version:
740 lines (725 loc) 24.4 kB
import { __commonJS, __toESM, __require } from './chunk-TTFRSOOU.mjs'; import { createServer } from 'net'; import path, { join, resolve, parse, dirname, isAbsolute } from 'path'; import * as rsbuildReal from '@rsbuild/core'; import { loadConfig, mergeRsbuildConfig } from '@rsbuild/core'; import fs2 from 'fs-extra'; import sirv from 'sirv'; import { corePath } from 'storybook/core-path'; import { normalizeStories, loadPreviewOrConfigFile, getBuilderOptions, readTemplate, resolveAddonName, getPresets, stringifyProcessEnvs, isPreservingSymlinks } from 'storybook/internal/common'; import { WebpackInvocationError } from 'storybook/internal/server-errors'; import { pluginTypeCheck } from '@rsbuild/plugin-type-check'; import { webpack } from '@storybook/addon-docs/dist/preset'; import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'; import { pluginHtmlMinifierTerser } from 'rsbuild-plugin-html-minifier-terser'; import { globalsNameReferenceMap } from 'storybook/internal/preview/globals'; import { dedent } from 'ts-dedent'; import fs from 'fs'; import { webpackIncludeRegexp } from '@storybook/core-webpack'; import findCacheDirectory from 'find-cache-dir'; import { readFile } from 'fs/promises'; // ../../node_modules/.pnpm/pretty-hrtime@1.0.3/node_modules/pretty-hrtime/index.js var require_pretty_hrtime = __commonJS({ "../../node_modules/.pnpm/pretty-hrtime@1.0.3/node_modules/pretty-hrtime/index.js"(exports, module) { var minimalDesc = ["h", "min", "s", "ms", "\u03BCs", "ns"]; var verboseDesc = ["hour", "minute", "second", "millisecond", "microsecond", "nanosecond"]; var convert = [60 * 60, 60, 1, 1e6, 1e3, 1]; module.exports = function(source, opts) { var verbose, precise, i, spot, sourceAtStep, valAtStep, decimals, strAtStep, results, totalSeconds; verbose = false; precise = false; if (opts) { verbose = opts.verbose || false; precise = opts.precise || false; } if (!Array.isArray(source) || source.length !== 2) { return ""; } if (typeof source[0] !== "number" || typeof source[1] !== "number") { return ""; } if (source[1] < 0) { totalSeconds = source[0] + source[1] / 1e9; source[0] = parseInt(totalSeconds); source[1] = parseFloat((totalSeconds % 1).toPrecision(9)) * 1e9; } results = ""; for (i = 0; i < 6; i++) { spot = i < 3 ? 0 : 1; sourceAtStep = source[spot]; if (i !== 3 && i !== 0) { sourceAtStep = sourceAtStep % convert[i - 1]; } if (i === 2) { sourceAtStep += source[1] / 1e9; } valAtStep = sourceAtStep / convert[i]; if (valAtStep >= 1) { if (verbose) { valAtStep = Math.floor(valAtStep); } if (!precise) { decimals = valAtStep >= 10 ? 0 : 2; strAtStep = valAtStep.toFixed(decimals); } else { strAtStep = valAtStep.toString(); } if (strAtStep.indexOf(".") > -1 && strAtStep[strAtStep.length - 1] === "0") { strAtStep = strAtStep.replace(/\.?0+$/, ""); } if (results) { results += " "; } results += strAtStep; if (verbose) { results += " " + verboseDesc[i]; if (strAtStep !== "1") { results += "s"; } } else { results += " " + minimalDesc[i]; } if (!verbose) { break; } } } return results; }; } }); // src/index.ts var import_pretty_hrtime = __toESM(require_pretty_hrtime()); // ../../node_modules/.pnpm/slash@5.1.0/node_modules/slash/index.js function slash(path2) { const isExtendedLengthPath = path2.startsWith("\\\\?\\"); if (isExtendedLengthPath) { return path2; } return path2.replace(/\\/g, "/"); } var getVirtualModules = async (options) => { const virtualModules = {}; const cwd = process.cwd(); const workingDir = options.cache ? ( // TODO: This is a hard code cache dir, as Rspack doesn't support virtual modules now. // Remove this when Rspack supports virtual modules. findCacheDirectory({ name: "storybook-rsbuild-builder", create: true }) ) : process.cwd(); if (!fs.existsSync(workingDir)) { fs.mkdirSync(workingDir, { recursive: true }); } const isProd = options.configType === "PRODUCTION"; const nonNormalizedStories = await options.presets.apply("stories", []); const entries = []; const stories = normalizeStories(nonNormalizedStories, { configDir: options.configDir, workingDir }); const realPathRelativeToCwd = path.relative(workingDir, cwd).split(path.sep).join(path.posix.sep); const previewAnnotations = [ ...(await options.presets.apply( "previewAnnotations", [], options )).map((entry) => { if (typeof entry === "object") { return entry.absolute; } return slash(entry); }), loadPreviewOrConfigFile(options) ].filter(Boolean); const storiesFilename = "storybook-stories.js"; const storiesPath = resolve(join(workingDir, storiesFilename)); const builderOptions = await getBuilderOptions(options); const needPipelinedImport = !!builderOptions.lazyCompilation && !isProd; virtualModules[storiesPath] = toImportFn(stories, realPathRelativeToCwd, { needPipelinedImport }); const configEntryPath = resolve(join(workingDir, "storybook-config-entry.js")); virtualModules[configEntryPath] = (await readTemplate( __require.resolve( "storybook-builder-rsbuild/templates/virtualModuleModernEntry.js" ) )).replaceAll(`'{{storiesFilename}}'`, `'./${storiesFilename}'`).replaceAll( `'{{previewAnnotations}}'`, previewAnnotations.filter(Boolean).map((entry) => `'${entry}'`).join(",") ).replaceAll( `'{{previewAnnotations_requires}}'`, previewAnnotations.filter(Boolean).map((entry) => `require('${entry}')`).join(",") ).replace(/\\/g, "\\\\"); entries.push(configEntryPath); for (const [key, value] of Object.entries(virtualModules)) { fs.writeFileSync(key, value); } return { virtualModules, entries }; }; function toImportFnPart(specifier) { const { directory, importPathMatcher } = specifier; return dedent` async (path) => { if (!${importPathMatcher}.exec(path)) { return; } const pathRemainder = path.substring(${directory.length + 1}); return import( /* webpackChunkName: "[request]" */ /* webpackInclude: ${webpackIncludeRegexp(specifier)} */ '${directory}/' + pathRemainder ); } `; } function toImportFn(stories, relativeOffset, { needPipelinedImport } = {}) { let pipelinedImport = "const pipeline = (x) => x();"; if (needPipelinedImport) { pipelinedImport = ` const importPipeline = ${importPipeline}; const pipeline = importPipeline(); `; } return dedent` ${pipelinedImport} const importers = [ ${stories.map(toImportFnPart).join(",\n")} ]; export async function importFn(path) { const offset = '${relativeOffset}'; for (let i = 0; i < importers.length; i++) { const pathWithOffset = buildPath(offset, path) const moduleExports = await pipeline(() => importers[i](pathWithOffset)); if (moduleExports) { return moduleExports; } } } function buildPath(offset, path) { if(path.startsWith('./')) { return offset + '/' + path.substring(2); } else { return offset + '/' + path; } } `; } function importPipeline() { let importGate = Promise.resolve(); return async (importFn) => { await importGate; const moduleExportsPromise = importFn(); importGate = importGate.then(async () => { await moduleExportsPromise; }); return moduleExportsPromise; }; } // src/preview/iframe-rsbuild.config.ts var getAbsolutePath = (input) => dirname(__require.resolve(join(input, "package.json"))); var maybeGetAbsolutePath = (input) => { try { return getAbsolutePath(input); } catch (e) { return false; } }; var builtInResolveExtensions = [ ".mjs", ".js", ".jsx", ".ts", ".tsx", ".json", ".cjs" ]; var managerAPIPath = maybeGetAbsolutePath("@storybook/manager-api"); var componentsPath = maybeGetAbsolutePath("@storybook/components"); var globalPath = maybeGetAbsolutePath("@storybook/global"); var routerPath = maybeGetAbsolutePath("@storybook/router"); var themingPath = maybeGetAbsolutePath("@storybook/theming"); var storybookPaths = { ...managerAPIPath ? { "@storybook/manager-api": managerAPIPath } : {}, ...componentsPath ? { "@storybook/components": componentsPath } : {}, ...globalPath ? { "@storybook/global": globalPath } : {}, ...routerPath ? { "@storybook/router": routerPath } : {}, ...themingPath ? { "@storybook/theming": themingPath } : {} }; var iframe_rsbuild_config_default = async (options, extraWebpackConfig) => { const { rsbuildConfigPath, addonDocs } = await getBuilderOptions(options); const appliedDocsWebpack = await webpack({}, { ...options, ...addonDocs }); const { outputDir = join(".", "public"), quiet, packageJson, configType, presets, previewUrl, typescriptOptions, features } = options; const isProd = configType === "PRODUCTION"; const workingDir = process.cwd(); const [ coreOptions, frameworkOptions, envs, logLevel, headHtmlSnippet, bodyHtmlSnippet, template, docsOptions, entries, nonNormalizedStories, _modulesCount, build2, tagsOptions ] = await Promise.all([ presets.apply("core"), presets.apply("frameworkOptions"), presets.apply("env"), presets.apply("logLevel", void 0), presets.apply("previewHead"), presets.apply("previewBody"), presets.apply("previewMainTemplate"), presets.apply("docs"), presets.apply("entries", []), presets.apply("stories", []), options.cache?.get("modulesCount", 1e3), options.presets.apply("build"), presets.apply("tags", {}) ]); const stories = normalizeStories(nonNormalizedStories, { configDir: options.configDir, workingDir }); const shouldCheckTs = typescriptOptions.check && !typescriptOptions.skipCompiler; const tsCheckOptions = typescriptOptions.checkOptions || {}; const builderOptions = await getBuilderOptions(options); const lazyCompilationConfig = builderOptions.lazyCompilation && !isProd ? { lazyCompilation: { entries: false } } : {}; if (!template) { throw new Error(dedent` Storybook's Webpack5 builder requires a template to be specified. Somehow you've ended up with a falsy value for the template option. Please file an issue at https://github.com/storybookjs/storybook with a reproduction. `); } const externals = globalsNameReferenceMap; if (build2?.test?.disableBlocks) { externals["@storybook/blocks"] = "__STORYBOOK_BLOCKS_EMPTY_MODULE__"; } const { virtualModules: _virtualModules, entries: dynamicEntries } = await getVirtualModules(options); if (!options.cache) { throw new Error("Cache is required"); } let contentFromConfig = {}; const { content } = await loadConfig({ cwd: workingDir, path: rsbuildConfigPath }); const { environments, ...withoutEnv } = content; if (content.environments) { const envCount = Object.keys(content.environments).length; if (envCount === 0) { contentFromConfig = withoutEnv; } else if (envCount === 1) { contentFromConfig = mergeRsbuildConfig( withoutEnv, content.environments[0] ); } else { const userEnv = builderOptions.environment; if (typeof userEnv !== "string") { throw new Error( "You must specify an environment when there are multiple environments in the Rsbuild config." ); } if (Object.keys(content.environments).includes(userEnv)) { contentFromConfig = mergeRsbuildConfig( withoutEnv, content.environments[userEnv] ); } else { throw new Error( `The specified environment "${userEnv}" is not found in the Rsbuild config.` ); } } } else { contentFromConfig = content; } contentFromConfig.source ??= {}; contentFromConfig.source.entry = {}; const resourceFilename = isProd ? "static/media/[name].[contenthash:8][ext]" : "static/media/[path][name][ext]"; const rsbuildConfig = mergeRsbuildConfig(contentFromConfig, { output: { cleanDistPath: false, assetPrefix: "/", dataUriLimit: { media: 1e4 }, sourceMap: { js: options.build?.test?.disableSourcemaps ? false : "cheap-module-source-map", css: !options.build?.test?.disableSourcemaps }, distPath: { root: resolve(process.cwd(), outputDir) }, filename: { js: isProd ? "[name].[contenthash:8].iframe.bundle.js" : "[name].iframe.bundle.js", image: resourceFilename, font: resourceFilename, media: resourceFilename }, externals }, server: { // Storybook will handle public directory itself, disable Rsbuild's public dir // feature to prevent overwriting Storybook's public directory. publicDir: false }, dev: { assetPrefix: "/", progressBar: !quiet }, source: { // TODO: Rspack doesn't support virtual modules yet, use cache dir instead // we needed to explicitly set the module in `node_modules` to be compiled include: [/[\\/]node_modules[\\/].*[\\/]storybook-config-entry\.js/], alias: { ...storybookPaths }, entry: { // to avoid `It's not allowed to load an initial chunk on demand. The chunk name "main" is already used by an entrypoint` of main: [...entries ?? [], ...dynamicEntries] }, define: { ...stringifyProcessEnvs(envs), NODE_ENV: JSON.stringify(process.env.NODE_ENV) } }, performance: { chunkSplit: { strategy: "custom", splitChunks: { chunks: "all" } } }, plugins: [ shouldCheckTs ? pluginTypeCheck(tsCheckOptions) : null, pluginHtmlMinifierTerser(() => ({ collapseWhitespace: true, removeComments: true, removeRedundantAttributes: true, removeScriptTypeAttributes: false, removeStyleLinkTypeAttributes: true, useShortDoctype: true })) ].filter(Boolean), tools: { rspack: (config, { addRules, appendPlugins, rspack, mergeConfig }) => { addRules({ test: /\.stories\.([tj])sx?$|(stories|story)\.mdx$/, exclude: /node_modules/, enforce: "post", use: [ { loader: __require.resolve( "storybook-builder-rsbuild/loaders/export-order-loader" ) } ] }); config.resolve ??= {}; config.resolve.symlinks = !isPreservingSymlinks(); config.resolve.extensions = Array.from( /* @__PURE__ */ new Set([ ...config.resolve.extensions ?? [], ...builtInResolveExtensions ]) ); config.watchOptions = { ignored: /node_modules/ }; config.ignoreWarnings = [ ...config.ignoreWarnings || [], /export '\S+' was not found in 'global'/, /export '\S+' was not found in '@storybook\/global'/ ]; config.resolve ??= {}; config.resolve.fallback ??= { stream: false, path: __require.resolve("path-browserify"), assert: __require.resolve("browser-assert"), util: __require.resolve("util"), url: __require.resolve("url"), fs: false, constants: __require.resolve("constants-browserify") }; config.optimization ??= {}; config.optimization.runtimeChunk = true; config.optimization.usedExports = options.build?.test?.disableTreeShaking ? false : isProd; config.optimization.moduleIds = "named"; config.module ??= {}; config.module.parser ??= {}; config.module.parser.javascript ??= {}; config.module.parser.javascript.exportsPresence = false; appendPlugins( [ new rspack.ProvidePlugin({ process: __require.resolve("process/browser.js") }), new CaseSensitivePathsPlugin() ].filter(Boolean) ); config.experiments ??= {}; config.experiments = { ...config.experiments, ...lazyCompilationConfig }; return mergeConfig(config, extraWebpackConfig, appliedDocsWebpack); }, htmlPlugin: { filename: "iframe.html", // FIXME: `none` isn't a known option chunksSortMode: "none", alwaysWriteToDisk: true, inject: false, template, templateParameters: { version: packageJson?.version ?? "0.0.0-storybook-rsbuild-unknown-version", globals: { CONFIG_TYPE: configType, LOGLEVEL: logLevel, FRAMEWORK_OPTIONS: frameworkOptions, CHANNEL_OPTIONS: coreOptions.channelOptions, FEATURES: features, PREVIEW_URL: previewUrl, STORIES: stories.map((specifier) => ({ ...specifier, importPathMatcher: specifier.importPathMatcher.source })), DOCS_OPTIONS: docsOptions, TAGS_OPTIONS: tagsOptions, ...build2?.test?.disableBlocks ? { __STORYBOOK_BLOCKS_EMPTY_MODULE__: {} } : {} }, headHtmlSnippet, bodyHtmlSnippet } } } }); return rsbuildConfig; }; var getIsReactVersion18or19 = async (options) => { const { legacyRootApi } = await options.presets.apply( "frameworkOptions" ) || {}; if (legacyRootApi) { return false; } const resolvedReact = await options.presets.apply( "resolvedReact", {} ); const reactDom = resolvedReact.reactDom || dirname(__require.resolve("react-dom/package.json")); if (!isAbsolute(reactDom)) { return false; } const { version } = JSON.parse( await readFile(join(reactDom, "package.json"), "utf-8") ); return version.startsWith("18") || version.startsWith("19") || version.startsWith("0.0.0"); }; var applyReactShims = async (config, options) => { const isReactVersion18 = await getIsReactVersion18or19(options); if (isReactVersion18) { return void 0; } return { source: { alias: { "@storybook/react-dom-shim": "@storybook/react-dom-shim/dist/react-16" } } }; }; // src/index.ts var printDuration = (startTime) => (0, import_pretty_hrtime.default)(process.hrtime(startTime)).replace(" ms", " milliseconds").replace(" s", " seconds").replace(" m", " minutes"); var executor = { get: async (options) => { const rsbuildInstance = await options.presets.apply("rsbuildInstance") || rsbuildReal; return rsbuildInstance; } }; var isObject = (val) => val != null && typeof val === "object" && Array.isArray(val) === false; function nonNullables(value) { return value !== void 0; } var rsbuild = async (_, options) => { const { presets } = options; const webpackAddons = await presets.apply("webpackAddons"); const resolvedWebpackAddons = (webpackAddons ?? []).map((preset) => { const addonOptions = isObject(preset) ? preset.options || void 0 : void 0; const name = isObject(preset) ? preset.name : preset; return resolveAddonName(options.configDir, name, addonOptions); }).filter(nonNullables); const { apply } = await getPresets(resolvedWebpackAddons, options); const webpackAddonsConfig = await apply( "webpackFinal", // TODO: using empty webpack config as base for now. It's better to using the composed rspack // config in `iframe-rsbuild.config.ts` as base config. But when `tools.rspack` is an async function, // the following `tools.rspack` raise an ` Promises are not supported` error. { output: {}, module: {}, plugins: [], resolve: {}, devServer: {}, optimization: {}, performance: {}, externals: {}, experiments: {}, node: {}, stats: {}, entry: {} }, options ); let defaultConfig = await iframe_rsbuild_config_default(options, webpackAddonsConfig); const shimsConfig = await applyReactShims(defaultConfig, options); defaultConfig = rsbuildReal.mergeRsbuildConfig( defaultConfig, shimsConfig ); const finalDefaultConfig = await presets.apply( "rsbuildFinal", defaultConfig, options ); return finalDefaultConfig; }; var getConfig = async (options) => { const { presets } = options; const typescriptOptions = await presets.apply("typescript", {}, options); const frameworkOptions = await presets.apply("frameworkOptions"); return rsbuild({}, { ...options, typescriptOptions, frameworkOptions }); }; var server; async function bail() { return server?.close(); } var start = async ({ startTime, options, router, server: storybookServer, channel }) => { const { createRsbuild } = await executor.get(options); const config = await getConfig(options); const rsbuildBuild = await createRsbuild({ cwd: process.cwd(), rsbuildConfig: { ...config, server: { ...config.server, port: await getRandomPort(options.host), host: options.host, htmlFallback: false, printUrls: false } } }); const rsbuildServer = await rsbuildBuild.createDevServer(); const waitFirstCompileDone = new Promise((resolve3) => { rsbuildBuild.onDevCompileDone(({ stats: stats2, isFirstCompile }) => { if (!isFirstCompile) { return; } resolve3(stats2); }); }); server = rsbuildServer; if (!rsbuildBuild) { throw new WebpackInvocationError({ // eslint-disable-next-line local-rules/no-uncategorized-errors error: new Error("Missing Rsbuild build instance at runtime!") }); } const previewResolvedDir = join(corePath, "dist/preview"); const previewDirOrigin = previewResolvedDir; router.use( "/sb-preview", sirv(previewDirOrigin, { maxAge: 3e5, dev: true, immutable: true }) ); router.use(rsbuildServer.middlewares); rsbuildServer.connectWebSocket({ server: storybookServer }); const stats = await waitFirstCompileDone; await server.afterListen(); return { bail, stats, totalTime: process.hrtime(startTime) }; }; var build = async ({ options }) => { const { createRsbuild } = await executor.get(options); const config = await getConfig(options); const rsbuildBuild = await createRsbuild({ cwd: process.cwd(), rsbuildConfig: config }); const previewResolvedDir = join(corePath, "dist/preview"); const previewDirOrigin = previewResolvedDir; const previewDirTarget = join(options.outputDir || "", "sb-preview"); let stats; rsbuildBuild.onAfterBuild((params) => { stats = params.stats; }); const previewFiles = fs2.copy(previewDirOrigin, previewDirTarget, { filter: (src) => { const { ext } = parse(src); if (ext) { return ext === ".js"; } return true; } }); rsbuildBuild.onAfterBuild((params) => { stats = params.stats; }); const [{ close }] = await Promise.all([rsbuildBuild.build(), previewFiles]); await close(); return stats; }; var corePresets = [join(__dirname, "./preview-preset.js")]; var previewMainTemplate = () => __require.resolve("storybook-builder-rsbuild/templates/preview.ejs"); function getRandomPort(host) { return new Promise((resolve3, reject) => { const server2 = createServer(); server2.unref(); server2.on("error", reject); server2.listen({ port: 0, host }, () => { const { port } = server2.address(); server2.close(() => { resolve3(port); }); }); }); } export { bail, build, corePresets, executor, getConfig, getVirtualModules, importPipeline, previewMainTemplate, printDuration, start, toImportFn, toImportFnPart };