UNPKG

@module-federation/storybook-addon

Version:

Storybook addon to consume remote module federated apps/components

128 lines 6.38 kB
import fs from 'fs'; import { dirname, join } from 'path'; import * as process from 'process'; import VirtualModulesPlugin from 'webpack-virtual-modules'; import { container, } from 'webpack'; import { normalizeStories } from '@storybook/core/common'; import withModuleFederation from '../utils/with-module-federation.js'; import { correctImportPath } from '../utils/correctImportPath.js'; // TODO: Only reserve `storybook/internal/node-logger` in next major release. // Dynamic import for logger to support multiple Storybook versions async function getLogger() { try { // Try high version first (since Storybook 9) const mod = await import('storybook/internal/node-logger'); return mod.logger; } catch { // Fallback to low version const mod = await import('@storybook/node-logger'); return mod.logger; } } const { ModuleFederationPlugin } = container; export { withModuleFederation }; export const webpack = async (webpackConfig, options) => { const { plugins = [], context: webpackContext } = webpackConfig; const { moduleFederationConfig, presets, nxModuleFederationConfig } = options; const context = webpackContext || process.cwd(); // Get logger instance (supports multiple Storybook versions) const logger = await getLogger(); // Detect webpack version. More about storybook webpack config https://storybook.js.org/docs/react/addons/writing-presets#webpack const webpackVersion = await presets.apply('webpackVersion'); logger.info(`=> [MF] Webpack ${webpackVersion} version detected`); if (webpackVersion !== '5') { throw new Error('Webpack 5 required: Configure Storybook to use the webpack5 builder'); } if (nxModuleFederationConfig) { logger.info(`=> [MF] Detect NX configuration`); const wmf = await withModuleFederation(nxModuleFederationConfig); webpackConfig = { ...webpackConfig, ...wmf(webpackConfig), }; } if (moduleFederationConfig) { logger.info(`=> [MF] Push Module Federation plugin`); // @ts-ignore enhanced add new remoteType 'module-import' plugins.push(new ModuleFederationPlugin(moduleFederationConfig)); } const entries = await presets.apply('entries'); const bootstrap = entries.map((entryFile) => `import '${correctImportPath(context, entryFile)}';`); const index = plugins.findIndex((plugin) => plugin?.constructor.name === 'VirtualModulesPlugin'); if (index !== -1) { logger.info(`=> [MF] Detected plugin VirtualModulesPlugin`); const plugin = plugins[index]; const virtualEntries = plugin.getModuleList('static'); const virtualEntriesPaths = Object.keys(virtualEntries); logger.info(`=> [MF] Write files from VirtualModulesPlugin`); for (const virtualEntryPath of virtualEntriesPaths) { const nodeModulesPath = '/node_modules/'; const filePathFromProjectRootDir = virtualEntryPath.replace(context, ''); let sourceCode = virtualEntries[virtualEntryPath]; let finalPath = virtualEntryPath; let finalDir = dirname(virtualEntryPath); // If virtual file is not in directory node_modules, move file in directory node_modules/.cache/storybook if (!filePathFromProjectRootDir.startsWith(nodeModulesPath)) { finalPath = join(context, nodeModulesPath, '.cache', 'storybook', filePathFromProjectRootDir); finalDir = dirname(finalPath); // Fix storybook stories' path in virtual module if ( // For storybook version before 7 filePathFromProjectRootDir === '/generated-stories-entry.cjs' || // For storybook version 7 filePathFromProjectRootDir === '/storybook-stories.js') { const isStorybookVersion7 = filePathFromProjectRootDir === '/storybook-stories.js'; const nonNormalizedStories = await presets.apply('stories'); const stories = normalizeStories(nonNormalizedStories, { configDir: options.configDir, workingDir: context, }); // For each story fix the import path stories.forEach((story) => { // Go up 3 times because the file was moved in /node_modules/.cache/storybook const newDirectory = join('..', '..', '..', story.directory); // Adding trailing slash for story directory in storybook v7 const oldSrc = isStorybookVersion7 ? `'${story.directory}/'` : `'${story.directory}'`; const newSrc = isStorybookVersion7 ? `'${newDirectory}/'` : `'${newDirectory}'`; // Fix story directory sourceCode = sourceCode.replace(oldSrc, newSrc); }); } } if (!fs.existsSync(finalDir)) { fs.mkdirSync(finalDir, { recursive: true }); } fs.writeFileSync(finalPath, sourceCode); bootstrap.push(`import '${correctImportPath(context, finalPath)}';`); } } /** * Create a new VirtualModulesPlugin plugin to fix error "Shared module is not available for eager consumption" * Entry file content is moved in bootstrap file. More details in the webpack documentation: * https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption * */ const virtualModulePlugin = new VirtualModulesPlugin({ './__entry.js': `import('./__bootstrap.js');`, './__bootstrap.js': bootstrap.join('\n'), }); let action = 'Push'; if (index === -1) { plugins.push(virtualModulePlugin); } else { plugins[index] = virtualModulePlugin; action = 'Replace'; } logger.info(`=> [MF] ${action} plugin VirtualModulesPlugin to bootstrap entry point`); return { ...webpackConfig, entry: ['./__entry.js'], plugins, }; }; //# sourceMappingURL=storybook-addon.js.map