363 lines
10 KiB
TypeScript
363 lines
10 KiB
TypeScript
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;
|
|
}
|