diff --git a/app/angular/src/server/angular-cli-webpack-12.2.x.d.ts b/app/angular/src/server/angular-cli-webpack-12.2.x.d.ts index d0253b25442..b2f79e4d963 100644 --- a/app/angular/src/server/angular-cli-webpack-12.2.x.d.ts +++ b/app/angular/src/server/angular-cli-webpack-12.2.x.d.ts @@ -1 +1,7 @@ -export declare function getWebpackConfig(baseConfig: any, options: any): any; +import { JsonObject } from '@angular-devkit/core'; +import { BuilderContext } from '@angular-devkit/architect'; + +export declare function getWebpackConfig( + baseConfig: any, + options: { builderOptions: JsonObject; builderContext: BuilderContext } +): any; diff --git a/app/angular/src/server/angular-cli-webpack-12.2.x.js b/app/angular/src/server/angular-cli-webpack-12.2.x.js index a72c7f2f9a3..3f1d69e5863 100644 --- a/app/angular/src/server/angular-cli-webpack-12.2.x.js +++ b/app/angular/src/server/angular-cli-webpack-12.2.x.js @@ -1,5 +1,3 @@ -const { targetFromTargetString } = require('@angular-devkit/architect'); - // Private angular devkit stuff const { generateI18nBrowserWebpackConfigFromContext, @@ -7,53 +5,47 @@ const { const { getCommonConfig, getStylesConfig, - getTypescriptWorkerPlugin, + getTypeScriptConfig, } = require('@angular-devkit/build-angular/src/webpack/configs'); const { filterOutStylingRules } = require('./utils/filter-out-styling-rules'); /** - * Extract wepack config from angular-cli 12.2.x + * Extract webpack config from angular-cli 12.2.x * ⚠️ This file is in JavaScript to not use TypeScript. Because current storybook TypeScript version is not compatible with Angular CLI. * FIXME: Try another way with TypeScript on future storybook version (7 maybe 🤞) * * @param {*} baseConfig Previous webpack config from storybook - * @param {*} options PresetOptions + * @param {*} options { builderOptions, builderContext } */ -exports.getWebpackConfig = async (baseConfig, options) => { - const builderContext = options.angularBuilderContext; - const target = options.angularBrowserTarget; - - let targetOptions = {}; - - if (target) { - targetOptions = await builderContext.getTargetOptions(targetFromTargetString(target)); - } - - const tsConfig = options.tsConfig ?? targetOptions.tsConfig; - +exports.getWebpackConfig = async (baseConfig, { builderOptions, builderContext }) => { + /** + * Get angular-cli Webpack config + */ const { config: cliConfig } = await generateI18nBrowserWebpackConfigFromContext( { - // Default required options + // Default options index: 'noop-index', main: 'noop-main', outputPath: 'noop-out', - // Target options to override - ...targetOptions, + // Options provided by user + ...builderOptions, // Fixed options optimization: false, namedChunks: false, progress: false, - tsConfig, buildOptimizer: false, aot: false, }, builderContext, - (wco) => [getCommonConfig(wco), getStylesConfig(wco), getTypescriptWorkerPlugin(wco)] + (wco) => [getCommonConfig(wco), getStylesConfig(wco), getTypeScriptConfig(wco)] ); + /** + * Merge baseConfig Webpack with angular-cli Webpack + */ const entry = [ ...baseConfig.entry, ...(cliConfig.entry.styles ?? []), @@ -63,6 +55,12 @@ exports.getWebpackConfig = async (baseConfig, options) => { // Don't use storybooks styling rules because we have to use rules created by @angular-devkit/build-angular // because @angular-devkit/build-angular created rules have include/exclude for global style files. const rulesExcludingStyles = filterOutStylingRules(baseConfig); + const module = { + ...baseConfig.module, + rules: [...cliConfig.module.rules, ...rulesExcludingStyles], + }; + + const plugins = [...(cliConfig.plugins ?? []), ...baseConfig.plugins]; const resolve = { ...baseConfig.resolve, @@ -72,11 +70,8 @@ exports.getWebpackConfig = async (baseConfig, options) => { return { ...baseConfig, entry, - module: { - ...baseConfig.module, - rules: [...cliConfig.module.rules, ...rulesExcludingStyles], - }, - plugins: [...(cliConfig.plugins ?? []), ...baseConfig.plugins], + module, + plugins, resolve, resolveLoader: cliConfig.resolveLoader, }; diff --git a/app/angular/src/server/angular-cli-webpack-13.x.x.d.ts b/app/angular/src/server/angular-cli-webpack-13.x.x.d.ts index d0253b25442..b2f79e4d963 100644 --- a/app/angular/src/server/angular-cli-webpack-13.x.x.d.ts +++ b/app/angular/src/server/angular-cli-webpack-13.x.x.d.ts @@ -1 +1,7 @@ -export declare function getWebpackConfig(baseConfig: any, options: any): any; +import { JsonObject } from '@angular-devkit/core'; +import { BuilderContext } from '@angular-devkit/architect'; + +export declare function getWebpackConfig( + baseConfig: any, + options: { builderOptions: JsonObject; builderContext: BuilderContext } +): any; diff --git a/app/angular/src/server/angular-cli-webpack-13.x.x.js b/app/angular/src/server/angular-cli-webpack-13.x.x.js index a72c7f2f9a3..0455e07047f 100644 --- a/app/angular/src/server/angular-cli-webpack-13.x.x.js +++ b/app/angular/src/server/angular-cli-webpack-13.x.x.js @@ -1,5 +1,3 @@ -const { targetFromTargetString } = require('@angular-devkit/architect'); - // Private angular devkit stuff const { generateI18nBrowserWebpackConfigFromContext, @@ -13,40 +11,31 @@ const { const { filterOutStylingRules } = require('./utils/filter-out-styling-rules'); /** - * Extract wepack config from angular-cli 12.2.x + * Extract webpack config from angular-cli 13.x.x * ⚠️ This file is in JavaScript to not use TypeScript. Because current storybook TypeScript version is not compatible with Angular CLI. * FIXME: Try another way with TypeScript on future storybook version (7 maybe 🤞) * * @param {*} baseConfig Previous webpack config from storybook - * @param {*} options PresetOptions + * @param {*} options { builderOptions, builderContext } */ -exports.getWebpackConfig = async (baseConfig, options) => { - const builderContext = options.angularBuilderContext; - const target = options.angularBrowserTarget; - - let targetOptions = {}; - - if (target) { - targetOptions = await builderContext.getTargetOptions(targetFromTargetString(target)); - } - - const tsConfig = options.tsConfig ?? targetOptions.tsConfig; - +exports.getWebpackConfig = async (baseConfig, { builderOptions, builderContext }) => { + /** + * Get angular-cli Webpack config + */ const { config: cliConfig } = await generateI18nBrowserWebpackConfigFromContext( { - // Default required options + // Default options index: 'noop-index', main: 'noop-main', outputPath: 'noop-out', - // Target options to override - ...targetOptions, + // Options provided by user + ...builderOptions, // Fixed options optimization: false, namedChunks: false, progress: false, - tsConfig, buildOptimizer: false, aot: false, }, @@ -54,6 +43,9 @@ exports.getWebpackConfig = async (baseConfig, options) => { (wco) => [getCommonConfig(wco), getStylesConfig(wco), getTypescriptWorkerPlugin(wco)] ); + /** + * Merge baseConfig Webpack with angular-cli Webpack + */ const entry = [ ...baseConfig.entry, ...(cliConfig.entry.styles ?? []), @@ -63,6 +55,12 @@ exports.getWebpackConfig = async (baseConfig, options) => { // Don't use storybooks styling rules because we have to use rules created by @angular-devkit/build-angular // because @angular-devkit/build-angular created rules have include/exclude for global style files. const rulesExcludingStyles = filterOutStylingRules(baseConfig); + const module = { + ...baseConfig.module, + rules: [...cliConfig.module.rules, ...rulesExcludingStyles], + }; + + const plugins = [...(cliConfig.plugins ?? []), ...baseConfig.plugins]; const resolve = { ...baseConfig.resolve, @@ -72,11 +70,8 @@ exports.getWebpackConfig = async (baseConfig, options) => { return { ...baseConfig, entry, - module: { - ...baseConfig.module, - rules: [...cliConfig.module.rules, ...rulesExcludingStyles], - }, - plugins: [...(cliConfig.plugins ?? []), ...baseConfig.plugins], + module, + plugins, resolve, resolveLoader: cliConfig.resolveLoader, }; diff --git a/app/angular/src/server/angular-cli-webpack-older.ts b/app/angular/src/server/angular-cli-webpack-older.ts new file mode 100644 index 00000000000..5fc68958aea --- /dev/null +++ b/app/angular/src/server/angular-cli-webpack-older.ts @@ -0,0 +1,146 @@ +import webpack from 'webpack'; +import { logger } from '@storybook/node-logger'; +import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; +import { targetFromTargetString, Target } from '@angular-devkit/architect'; + +import { workspaces } from '@angular-devkit/core'; +import { + findAngularProjectTarget, + getDefaultProjectName, + readAngularWorkspaceConfig, +} from './angular-read-workspace'; +import { + AngularCliWebpackConfig, + extractAngularCliWebpackConfig, +} from './angular-devkit-build-webpack'; +import { filterOutStylingRules } from './utils/filter-out-styling-rules'; +import { PresetOptions } from './options'; + +/** + * Old way currently support version lower than 12.2.x + */ +export async function getWebpackConfig(baseConfig: webpack.Configuration, options: PresetOptions) { + logger.info('=> Loading angular-cli config for angular <=12.2.0'); + const dirToSearch = process.cwd(); + + // Read angular workspace + let workspaceConfig; + try { + workspaceConfig = await readAngularWorkspaceConfig(dirToSearch); + } catch (error) { + logger.error( + `=> Could not find angular workspace config (angular.json) on this path "${dirToSearch}"` + ); + logger.info(`=> Fail to load angular-cli config. Using base config`); + return baseConfig; + } + + // Find angular project target + let project: workspaces.ProjectDefinition; + let target: workspaces.TargetDefinition; + let confName: string; + try { + // Default behavior when `angularBrowserTarget` are not explicitly defined to null + if (options.angularBrowserTarget !== null) { + const browserTarget = options.angularBrowserTarget + ? targetFromTargetString(options.angularBrowserTarget) + : ({ + configuration: undefined, + project: getDefaultProjectName(workspaceConfig), + target: 'build', + } as Target); + + const fondProject = findAngularProjectTarget( + workspaceConfig, + browserTarget.project, + browserTarget.target + ); + project = fondProject.project; + target = fondProject.target; + confName = browserTarget.configuration; + + logger.info( + `=> Using angular project "${browserTarget.project}:${browserTarget.target}${ + confName ? `:${confName}` : '' + }" for configuring Storybook` + ); + } + // Start storybook when only tsConfig is provided. + if (options.angularBrowserTarget === null && options.tsConfig) { + logger.info(`=> Using default angular project with "tsConfig:${options.tsConfig}"`); + + project = { root: '', extensions: {}, targets: undefined }; + target = { builder: '', options: { tsConfig: options.tsConfig } }; + } + } catch (error) { + logger.error(`=> Could not find angular project: ${error.message}`); + logger.info(`=> Fail to load angular-cli config. Using base config`); + return baseConfig; + } + + // Use angular-cli to get some webpack config + let angularCliWebpackConfig: AngularCliWebpackConfig; + try { + angularCliWebpackConfig = await extractAngularCliWebpackConfig( + dirToSearch, + project, + target, + confName + ); + logger.info(`=> Using angular-cli webpack config`); + } catch (error) { + logger.error(`=> Could not get angular cli webpack config`); + throw error; + } + + return mergeAngularCliWebpackConfig(angularCliWebpackConfig, baseConfig); +} + +function mergeAngularCliWebpackConfig( + { cliCommonWebpackConfig, cliStyleWebpackConfig, tsConfigPath }: AngularCliWebpackConfig, + baseConfig: webpack.Configuration +) { + // Don't use storybooks styling rules because we have to use rules created by @angular-devkit/build-angular + // because @angular-devkit/build-angular created rules have include/exclude for global style files. + const rulesExcludingStyles = filterOutStylingRules(baseConfig); + + // styleWebpackConfig.entry adds global style files to the webpack context + const entry = [ + ...(baseConfig.entry as string[]), + ...Object.values(cliStyleWebpackConfig.entry).reduce((acc, item) => acc.concat(item), []), + ]; + + const module = { + ...baseConfig.module, + rules: [...cliStyleWebpackConfig.module.rules, ...rulesExcludingStyles], + }; + + // We use cliCommonConfig plugins to serve static assets files. + const plugins = [ + ...cliStyleWebpackConfig.plugins, + ...cliCommonWebpackConfig.plugins, + ...baseConfig.plugins, + ]; + + const resolve = { + ...baseConfig.resolve, + modules: Array.from( + new Set([...baseConfig.resolve.modules, ...cliCommonWebpackConfig.resolve.modules]) + ), + plugins: [ + new TsconfigPathsPlugin({ + configFile: tsConfigPath, + mainFields: ['browser', 'module', 'main'], + }), + ], + }; + + return { + ...baseConfig, + entry, + module, + plugins, + resolve, + resolveLoader: cliCommonWebpackConfig.resolveLoader, + }; +} diff --git a/app/angular/src/server/framework-preset-angular-cli.ts b/app/angular/src/server/framework-preset-angular-cli.ts index 673d9f538de..75e593810ca 100644 --- a/app/angular/src/server/framework-preset-angular-cli.ts +++ b/app/angular/src/server/framework-preset-angular-cli.ts @@ -1,174 +1,126 @@ import webpack from 'webpack'; import { logger } from '@storybook/node-logger'; -import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; -import { targetFromTargetString, Target } from '@angular-devkit/architect'; +import { targetFromTargetString, BuilderContext } from '@angular-devkit/architect'; import { sync as findUpSync } from 'find-up'; import semver from '@storybook/semver'; -import { workspaces } from '@angular-devkit/core'; -import { - findAngularProjectTarget, - getDefaultProjectName, - readAngularWorkspaceConfig, -} from './angular-read-workspace'; -import { - AngularCliWebpackConfig, - extractAngularCliWebpackConfig, -} from './angular-devkit-build-webpack'; +import { logging, JsonObject } from '@angular-devkit/core'; import { moduleIsAvailable } from './utils/module-is-available'; -import { filterOutStylingRules } from './utils/filter-out-styling-rules'; import { getWebpackConfig as getWebpackConfig12_2_x } from './angular-cli-webpack-12.2.x'; import { getWebpackConfig as getWebpackConfig13_x_x } from './angular-cli-webpack-13.x.x'; +import { getWebpackConfig as getWebpackConfigOlder } from './angular-cli-webpack-older'; import { PresetOptions } from './options'; export async function webpackFinal(baseConfig: webpack.Configuration, options: PresetOptions) { - /** - * Find angular version and use right getWebpackConfig - * - * ⚠️ Only work with angular storybook builder - */ - const packageJson = await import(findUpSync('package.json', { cwd: options.configDir })); - const angularCliVersion = semver.coerce(packageJson.devDependencies['@angular/cli'])?.version; - - const isNg12_2_x = semver.satisfies(angularCliVersion, '12.2.x'); - if (isNg12_2_x && options.angularBuilderContext) { - return getWebpackConfig12_2_x(baseConfig, options); - } - - const isNg13_x_x = semver.satisfies(angularCliVersion, '13.x.x'); - if (isNg13_x_x && options.angularBuilderContext) { - return getWebpackConfig13_x_x(baseConfig, options); - } - - /** - * Classic way currently support version lower than 12.2.x - */ - const dirToSearch = process.cwd(); - if (!moduleIsAvailable('@angular-devkit/build-angular')) { logger.info('=> Using base config because "@angular-devkit/build-angular" is not installed'); return baseConfig; } - logger.info('=> Loading angular-cli config'); - // Read angular workspace - let workspaceConfig; - try { - workspaceConfig = await readAngularWorkspaceConfig(dirToSearch); - } catch (error) { - logger.error( - `=> Could not find angular workspace config (angular.json) on this path "${dirToSearch}"` - ); - logger.info(`=> Fail to load angular-cli config. Using base config`); - return baseConfig; - } + const packageJson = await import(findUpSync('package.json', { cwd: options.configDir })); + const angularCliVersion = semver.coerce(packageJson.devDependencies['@angular/cli'])?.version; - // Find angular project target - let project: workspaces.ProjectDefinition; - let target: workspaces.TargetDefinition; - let confName: string; - try { - // Default behavior when `angularBrowserTarget` are not explicitly defined to null - if (options.angularBrowserTarget !== null) { - const browserTarget = options.angularBrowserTarget - ? targetFromTargetString(options.angularBrowserTarget) - : ({ - configuration: undefined, - project: getDefaultProjectName(workspaceConfig), - target: 'build', - } as Target); + /** + * Ordered array to use the specific getWebpackConfig according to some condition like angular-cli version + */ + const webpackGetterByVersions: { + info: string; + condition: boolean; + getWebpackConfig( + baseConfig: webpack.Configuration, + options: PresetOptions + ): Promise | webpack.Configuration; + }[] = [ + { + info: '=> Loading angular-cli config for angular >= 13.0.0', + condition: semver.satisfies(angularCliVersion, '>=13.0.0'), + getWebpackConfig: async (_baseConfig, _options) => { + const builderContext = getBuilderContext(_options); + const builderOptions = await getBuilderOptions(_options, builderContext); - const fondProject = findAngularProjectTarget( - workspaceConfig, - browserTarget.project, + return getWebpackConfig13_x_x(_baseConfig, { + builderOptions, + builderContext, + }); + }, + }, + { + info: '=> Loading angular-cli config for angular 12.2.x', + condition: semver.satisfies(angularCliVersion, '12.2.x') && options.angularBuilderContext, + getWebpackConfig: async (_baseConfig, _options) => { + const builderContext = getBuilderContext(_options); + const builderOptions = await getBuilderOptions(_options, builderContext); + + return getWebpackConfig12_2_x(_baseConfig, { + builderOptions, + builderContext, + }); + }, + }, + { + info: '=> Loading angular-cli config for angular lower than 12.2.0', + condition: true, + getWebpackConfig: getWebpackConfigOlder, + }, + ]; + + const webpackGetter = webpackGetterByVersions.find((wg) => wg.condition); + + logger.info(webpackGetter.info); + return Promise.resolve(webpackGetter.getWebpackConfig(baseConfig, options)); +} + +/** + * Get Builder Context + * If storybook is not start by angular builder create dumb BuilderContext + */ +function getBuilderContext(options: PresetOptions): BuilderContext { + return ( + options.angularBuilderContext ?? + (({ + target: { project: 'noop-project', builder: '', options: {} }, + workspaceRoot: process.cwd(), + getProjectMetadata: () => ({}), + getTargetOptions: () => ({}), + logger: new logging.Logger('Storybook'), + } as unknown) as BuilderContext) + ); +} + +/** + * Get builder options + * Merge target options from browser target and from storybook options + */ +async function getBuilderOptions( + options: PresetOptions, + builderContext: BuilderContext +): Promise { + /** + * Get Browser Target options + */ + let browserTargetOptions: JsonObject = {}; + if (options.angularBrowserTarget) { + const browserTarget = targetFromTargetString(options.angularBrowserTarget); + + logger.info( + `=> Using angular browser target options from "${browserTarget.project}:${ browserTarget.target - ); - project = fondProject.project; - target = fondProject.target; - confName = browserTarget.configuration; - - logger.info( - `=> Using angular project "${browserTarget.project}:${browserTarget.target}${ - confName ? `:${confName}` : '' - }" for configuring Storybook` - ); - } - // Start storybook when only tsConfig is provided. - if (options.angularBrowserTarget === null && options.tsConfig) { - logger.info(`=> Using default angular project with "tsConfig:${options.tsConfig}"`); - - project = { root: '', extensions: {}, targets: undefined }; - target = { builder: '', options: { tsConfig: options.tsConfig } }; - } - } catch (error) { - logger.error(`=> Could not find angular project: ${error.message}`); - logger.info(`=> Fail to load angular-cli config. Using base config`); - return baseConfig; - } - - // Use angular-cli to get some webpack config - let angularCliWebpackConfig: AngularCliWebpackConfig; - try { - angularCliWebpackConfig = await extractAngularCliWebpackConfig( - dirToSearch, - project, - target, - confName + }${browserTarget.configuration ? `:${browserTarget.configuration}` : ''}"` ); - logger.info(`=> Using angular-cli webpack config`); - } catch (error) { - logger.error(`=> Could not get angular cli webpack config`); - throw error; + browserTargetOptions = await builderContext.getTargetOptions(browserTarget); } - return mergeAngularCliWebpackConfig(angularCliWebpackConfig, baseConfig); -} - -function mergeAngularCliWebpackConfig( - { cliCommonWebpackConfig, cliStyleWebpackConfig, tsConfigPath }: AngularCliWebpackConfig, - baseConfig: webpack.Configuration -) { - // Don't use storybooks styling rules because we have to use rules created by @angular-devkit/build-angular - // because @angular-devkit/build-angular created rules have include/exclude for global style files. - const rulesExcludingStyles = filterOutStylingRules(baseConfig); - - // styleWebpackConfig.entry adds global style files to the webpack context - const entry = [ - ...(baseConfig.entry as string[]), - ...Object.values(cliStyleWebpackConfig.entry).reduce((acc, item) => acc.concat(item), []), - ]; - - const module = { - ...baseConfig.module, - rules: [...cliStyleWebpackConfig.module.rules, ...rulesExcludingStyles], - }; - - // We use cliCommonConfig plugins to serve static assets files. - const plugins = [ - ...cliStyleWebpackConfig.plugins, - ...cliCommonWebpackConfig.plugins, - ...baseConfig.plugins, - ]; - - const resolve = { - ...baseConfig.resolve, - modules: Array.from( - new Set([...baseConfig.resolve.modules, ...cliCommonWebpackConfig.resolve.modules]) - ), - plugins: [ - new TsconfigPathsPlugin({ - configFile: tsConfigPath, - mainFields: ['browser', 'module', 'main'], - }), - ], - }; - - return { - ...baseConfig, - entry, - module, - plugins, - resolve, - resolveLoader: cliCommonWebpackConfig.resolveLoader, - }; + /** + * Merge target options from browser target options and from storybook options + */ + const builderOptions = { + ...browserTargetOptions, + tsConfig: + options.tsConfig ?? + browserTargetOptions.tsConfig ?? + findUpSync('tsconfig.json', { cwd: options.configDir }), + }; + logger.info(`=> Using angular project with "tsConfig:${builderOptions.tsConfig}"`); + + return builderOptions; }