import { ExecutorContext, joinPathFragments, logger, parseTargetString, readTargetOptions, TargetConfiguration, } from '@nrwl/devkit'; import { checkAndCleanWithSemver } from '@nrwl/workspace/src/utilities/version-utils'; import 'dotenv/config'; import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; import { gte, lt } from 'semver'; import { findOrCreateConfig, readCurrentWorkspaceStorybookVersionFromExecutor, } from '../utils/utilities'; import { StorybookBuilderOptions } from './build-storybook/build-storybook.impl'; import { CommonNxStorybookConfig } from './models'; import { StorybookExecutorOptions } from './storybook/storybook.impl'; export interface NodePackage { name: string; version: string; } export function getStorybookFrameworkPath(uiFramework) { const serverOptionsPaths = { '@storybook/angular': '@storybook/angular/dist/ts3.9/server/options', '@storybook/react': '@storybook/react/dist/cjs/server/options', '@storybook/html': '@storybook/html/dist/cjs/server/options', '@storybook/vue': '@storybook/vue/dist/cjs/server/options', '@storybook/vue3': '@storybook/vue3/dist/cjs/server/options', '@storybook/web-components': '@storybook/web-components/dist/cjs/server/options', '@storybook/svelte': '@storybook/svelte/dist/cjs/server/options', }; if (isStorybookV62onwards(uiFramework)) { return serverOptionsPaths[uiFramework]; } else { return `${uiFramework}/dist/server/options`; } } function isStorybookV62onwards(uiFramework) { const storybookPackageVersion = require(join( uiFramework, 'package.json' )).version; return gte(storybookPackageVersion, '6.2.0-rc.4'); } // see: https://github.com/storybookjs/storybook/pull/12565 // TODO: this should really be passed as a param to the CLI rather than env export function setStorybookAppProject( context: ExecutorContext, leadStorybookProject: string ) { let leadingProject: string; // for libs we check whether the build config should be fetched // from some app if ( context.workspace.projects[context.projectName].projectType === 'library' ) { // we have a lib so let's try to see whether the app has // been set from which we want to get the build config if (leadStorybookProject) { leadingProject = leadStorybookProject; } else { // do nothing return; } } else { // ..for apps we just use the app target itself leadingProject = context.projectName; } process.env.STORYBOOK_ANGULAR_PROJECT = leadingProject; } export function runStorybookSetupCheck(options: CommonNxStorybookConfig) { webpackFinalPropertyCheck(options); reactWebpack5Check(options); } function reactWebpack5Check(options: CommonNxStorybookConfig) { if (options.uiFramework === '@storybook/react') { let storybookConfigFilePath = joinPathFragments( options.config.configFolder, 'main.js' ); if (!existsSync(storybookConfigFilePath)) { storybookConfigFilePath = joinPathFragments( options.config.configFolder, 'main.ts' ); } if (!existsSync(storybookConfigFilePath)) { // looks like there's no main config file, so skip return; } // check whether the current Storybook configuration has the webpack 5 builder enabled const storybookConfig = readFileSync(storybookConfigFilePath, { encoding: 'utf8', }); if ( !storybookConfig.match(/builder: ('webpack5'|"webpack5"|`webpack5`)/g) ) { // storybook needs to be upgraded to webpack 5 logger.warn(` It looks like you use Webpack 5 but your Storybook setup is not configured to leverage that and thus falls back to Webpack 4. Make sure you upgrade your Storybook config to use Webpack 5. - https://gist.github.com/shilman/8856ea1786dcd247139b47b270912324#upgrade `); } } } function webpackFinalPropertyCheck(options: CommonNxStorybookConfig) { let placesToCheck = [ { path: joinPathFragments('.storybook', 'webpack.config.js'), result: false, }, { path: joinPathFragments(options.config.configFolder, 'webpack.config.js'), result: false, }, ]; placesToCheck = placesToCheck .map((entry) => { return { ...entry, result: existsSync(entry.path), }; }) .filter((x) => x.result === true); if (placesToCheck.length > 0) { logger.warn( ` You have a webpack.config.js files in your Storybook configuration: ${placesToCheck.map((x) => `- "${x.path}"`).join('\n ')} Consider switching to the "webpackFinal" property declared in "main.js" instead. ${ options.uiFramework === '@storybook/react' ? 'https://nx.dev/storybook/migrate-webpack-final-react' : 'https://nx.dev/storybook/migrate-webpack-final-angular' } ` ); } } export function resolveCommonStorybookOptionMapper( builderOptions: CommonNxStorybookConfig, frameworkOptions: any, context: ExecutorContext ) { const storybookConfig = findOrCreateConfig(builderOptions.config, context); const storybookOptions = { workspaceRoot: context.root, configDir: storybookConfig, ...frameworkOptions, frameworkPresets: [...(frameworkOptions.frameworkPresets || [])], watch: false, }; if ( builderOptions.uiFramework === '@storybook/angular' && // just for new 6.4 with Angular isStorybookGTE6_4() ) { let buildProjectName; let targetName = 'build'; // default let targetOptions = null; if (builderOptions.projectBuildConfig) { const targetString = normalizeTargetString( builderOptions.projectBuildConfig, targetName ); const { project, target, configuration } = parseTargetString(targetString); // set the extracted target name targetName = target; buildProjectName = project; targetOptions = readTargetOptions( { project, target, configuration }, context ); storybookOptions.angularBrowserTarget = targetString; } else { const { storybookBuildTarget, storybookTarget, buildTarget } = findStorybookAndBuildTargets( context?.workspace?.projects?.[context.projectName]?.targets ); throw new Error( ` No projectBuildConfig was provided. To fix this, you can try one of the following options: 1. You can run the ${ context.targetName ? context.targetName : storybookTarget } executor by providing the projectBuildConfig flag as follows: nx ${context.targetName ? context.targetName : storybookTarget} ${ context.projectName } --projectBuildConfig=${context.projectName}${ !buildTarget && storybookBuildTarget ? `:${storybookBuildTarget}` : '' } 2. In your project configuration, under the "${ context.targetName ? context.targetName : storybookTarget }" target options, you can set the "projectBuildConfig" property to the name of the project of which you want to use the build configuration for Storybook. ` ); } const project = context.workspace.projects[buildProjectName]; const angularDevkitCompatibleLogger = { ...logger, createChild() { return angularDevkitCompatibleLogger; }, }; // construct a builder object for Storybook storybookOptions.angularBuilderContext = { target: { ...project.targets[targetName], project: buildProjectName, }, workspaceRoot: context.cwd, getProjectMetadata: () => { return project; }, getTargetOptions: () => { return targetOptions; }, logger: angularDevkitCompatibleLogger, }; // Add watch to angularBuilderOptions for Storybook to merge configs correctly storybookOptions.angularBuilderOptions = { watch: true, }; } else { // keep the backwards compatibility setStorybookAppProject(context, builderOptions.projectBuildConfig); } return storybookOptions; } function normalizeTargetString( appName: string, defaultTarget: string = 'build' ) { if (appName.includes(':')) { return appName; } return `${appName}:${defaultTarget}`; } function isStorybookGTE6_4() { const storybookVersion = readCurrentWorkspaceStorybookVersionFromExecutor(); return gte( checkAndCleanWithSemver('@storybook/core', storybookVersion), '6.4.0-rc.1' ); } export function isStorybookLT6() { const storybookVersion = readCurrentWorkspaceStorybookVersionFromExecutor(); return lt( checkAndCleanWithSemver('@storybook/core', storybookVersion), '6.0.0' ); } export function findStorybookAndBuildTargets(targets: { [targetName: string]: TargetConfiguration; }): { storybookBuildTarget?: string; storybookTarget?: string; buildTarget?: string; } { const returnObject: { storybookBuildTarget?: string; storybookTarget?: string; buildTarget?: string; } = {}; Object.entries(targets).forEach(([target, targetConfig]) => { if (targetConfig.executor === '@nrwl/storybook:storybook') { returnObject.storybookTarget = target; } if (targetConfig.executor === '@nrwl/storybook:build') { returnObject.storybookBuildTarget = target; } /** * Not looking for '@nrwl/angular:ng-packagr-lite', only * looking for '@angular-devkit/build-angular:browser' * because the '@nrwl/angular:ng-packagr-lite' executor * does not support styles and extra options, so the user * will be forced to switch to build-storybook to add extra options. * * So we might as well use the build-storybook by default to * avoid any errors. */ if (targetConfig.executor === '@angular-devkit/build-angular:browser') { returnObject.buildTarget = target; } }); return returnObject; } export function normalizeAngularBuilderStylesOptions( builderOptions: StorybookBuilderOptions | StorybookExecutorOptions, uiFramework: | '@storybook/angular' | '@storybook/react' | '@storybook/html' | '@storybook/web-components' | '@storybook/vue' | '@storybook/vue3' | '@storybook/svelte' | '@storybook/react-native' ): StorybookBuilderOptions | StorybookExecutorOptions { if (uiFramework !== '@storybook/angular') { if (builderOptions.styles) { delete builderOptions.styles; } if (builderOptions.stylePreprocessorOptions) { delete builderOptions.stylePreprocessorOptions; } } return builderOptions; }