import path from 'path' ; import { isDev , isProd , removeLeadingSlash , getEntryOptions , generateMetaTags , removeTailSlash , findExists , } from '@modern-js/utils' ; import HtmlWebpackPlugin from 'html-webpack-plugin' ; import { IAppContext , NormalizedConfig } from '@modern-js/core' ; import { WebpackManifestPlugin } from 'webpack-manifest-plugin' ; import webpack , { DefinePlugin , HotModuleReplacementPlugin , ProvidePlugin , } from 'webpack' ; import nodeLibsBrowser from 'node-libs-browser' ; import { Entrypoint } from '@modern-js/types' ; import CopyPlugin from 'copy-webpack-plugin' ; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' ; import { RouteManifest } from '../plugins/route-manifest-plugin' ; import { InlineChunkHtmlPlugin } from '../plugins/inline-html-chunk-plugin' ; import { AppIconPlugin } from '../plugins/app-icon-plugin' ; import { ICON_EXTENSIONS } from '../utils/constants' ; import { BaseWebpackConfig } from './base' ; class ClientWebpackConfig extends BaseWebpackConfig { htmlFilename : ( name : string ) => string ; constructor ( appContext , options ) { super ( appContext , options ) ; this . htmlFilename = ( name ) => removeLeadingSlash ( `${ this . options . output . htmlPath ! }/${ this . options . output . disableHtmlFolder ? name : `${ name }/index` }.html` ) ; } name ( ) { this . chain . name ( 'client' ) ; } entry ( ) { super . entry ( ) ; const entrypoints = Object . keys ( this . chain . entryPoints . entries ( ) || { } ) ; for ( const name of entrypoints ) { if ( this . options . output . polyfill !== 'off' ) { this . chain . entry ( name ) . prepend ( require . resolve ( 'regenerator-runtime/runtime' ) ) . prepend ( require . resolve ( 'core-js' ) ) ; } } } resolve ( ) { super . resolve ( ) ; this . chain . resolve . modules . add ( path . resolve ( __dirname , '../../../../node_modules' ) ) ; if ( ! this . options . output . disableNodePolyfill ) { this . chain . resolve . merge ( { fallback : Object . keys ( nodeLibsBrowser ) . reduce < Record < string , string | false > > ( ( previous , name ) => { if ( nodeLibsBrowser [ name ] ) { previous [ name ] = nodeLibsBrowser [ name ] ; } else { previous [ name ] = false ; } return previous ; } , { } ) , } ) ; } } private useDefinePlugin ( ) { const { envVars , globalVars } = this . options . source || { } ; this . chain . plugin ( 'define' ) . use ( DefinePlugin , [ { ... [ 'NODE_ENV' , 'BUILD_MODE' , ... ( envVars || [ ] ) ] . reduce < Record < string , string > > ( ( memo , name ) => { memo [ `process.env.${ name }` ] = JSON . stringify ( process . env [ name ] ) ; return memo ; } , { } ) , ... Object . keys ( globalVars || { } ) . reduce < Record < string , string > > ( ( memo , name ) => { memo [ name ] = globalVars ? JSON . stringify ( globalVars [ name ] ) : '' ; return memo ; } , { } ) , } , ] ) ; } plugins ( ) { super . plugins ( ) ; this . useDefinePlugin ( ) ; isDev ( ) && this . chain . plugin ( 'hmr' ) . use ( HotModuleReplacementPlugin ) ; const { entrypoints = [ ] , packageName } = this . appContext as IAppContext & { entrypoints : Entrypoint [ ] ; } ; for ( const { entryName } of entrypoints ) { this . chain . plugin ( `html-${ entryName }` ) . use ( HtmlWebpackPlugin , [ { filename : this . htmlFilename ( entryName ) , chunks : [ entryName ] , template : this . appContext . htmlTemplates [ entryName ] , minify : ! isProd ( ) ? false : { collapseWhitespace : true , removeComments : false , removeRedundantAttributes : true , removeScriptTypeAttributes : true , removeStyleLinkTypeAttributes : true , useShortDoctype : true , } , favicon : getEntryOptions < string | undefined > ( entryName , this . options . output . favicon , this . options . output . faviconByEntries , packageName ) || findExists ( ICON_EXTENSIONS . map ( ext => path . resolve ( this . appContext . appDirectory , this . options . source . configDir ! , `favicon.${ ext }` ) ) ) , inject : getEntryOptions ( entryName , this . options . output . inject , this . options . output . injectByEntries , packageName ) , templateParameters : ( compilation , assets , assetTags , pluginOptions ) => { let stats ; return { entryName , get webpack ( ) { return stats || ( stats = compilation . getStats ( ) . toJson ( ) ) ; } , webpackConfig : compilation . options , htmlWebpackPlugin : { tags : assetTags , files : assets , options : pluginOptions , } , title : getEntryOptions < string | undefined > ( entryName , this . options . output . title , this . options . output . titleByEntries , packageName ) , mountId : this . options . output . mountId ! , assetPrefix : removeTailSlash ( this . chain . output . get ( 'publicPath' ) ) , meta : generateMetaTags ( getEntryOptions ( entryName , this . options . output . meta , this . options . output . metaByEntries , packageName ) ) , ... getEntryOptions < Record < string , unknown > | undefined > ( entryName , this . options . output . templateParameters , this . options . output . templateParametersByEntries , packageName ) , } ; } , } , ] ) ; } const appIcon = findExists ( ICON_EXTENSIONS . map ( ext => path . resolve ( this . appContext . appDirectory , this . options . source . configDir ! , `icon.${ ext }` ) ) ) ; if ( appIcon ) { this . chain . plugin ( 'app-icon' ) . use ( AppIconPlugin , [ HtmlWebpackPlugin , appIcon ] ) ; } this . chain . plugin ( 'webpack-manifest' ) . use ( WebpackManifestPlugin , [ { fileName : 'asset-manifest.json' , publicPath : this . chain . output . get ( 'publicPath' ) , generate : ( seed , files , entries ) => { const manifestFiles = files . reduce ( ( manifest , file ) => { manifest [ file . name ] = file . path ; return manifest ; } , seed ) ; const entrypointFiles = Object . keys ( entries ) . reduce < string [ ] > ( ( previous , name ) => previous . concat ( entries [ name ] . filter ( fileName => ! fileName . endsWith ( '.map' ) ) ) , [ ] ) ; return { files : manifestFiles , entrypoints : entrypointFiles , } as any ; } , } , ] ) ; const configDir = path . resolve ( this . appDirectory , this . options . source . configDir ! ) ; this . chain . plugin ( 'copy' ) . use ( CopyPlugin , [ { patterns : [ ... ( ( this . options . output . copy as any ) || [ ] ) , { from : '**/*' , to : 'public' , context : path . posix . join ( configDir . replace ( /\\/g , '/' ) , 'public' ) , noErrorOnMissing : true , transform : ( content , absoluteFrom ) => { if ( ! /\.html?$/ . test ( absoluteFrom ) ) { return content ; } return require ( 'lodash.template' ) ( content . toString ( 'utf8' ) ) ( { assetPrefix : removeTailSlash ( this . chain . output . get ( 'publicPath' ) ) , } ) ; } , } , { from : '**/*' , to : 'upload' , context : path . posix . join ( configDir . replace ( /\\/g , '/' ) , 'upload' ) , noErrorOnMissing : true , } , ] , } , ] ) ; this . chain . plugin ( 'route-manitest' ) . use ( RouteManifest , [ { options : this . options , appContext : this . appContext , } , ] ) ; if ( isProd ( ) ) { const { disableInlineRuntimeChunk , enableInlineStyles , enableInlineScripts , } = this . options . output ; this . chain . plugin ( 'inline-html' ) . use ( InlineChunkHtmlPlugin , [ HtmlWebpackPlugin , [ enableInlineScripts && /\.js$/ , enableInlineStyles && /\.css$/ , ! disableInlineRuntimeChunk && /runtime-.+[.]js$/ , ] . filter ( Boolean ) as RegExp [ ] , ] ) ; } if ( ! this . options . output . disableNodePolyfill ) { this . chain . plugin ( 'node-polyfill-provide' ) . use ( ProvidePlugin , [ { Buffer : [ nodeLibsBrowser . buffer , 'Buffer' ] , console : [ nodeLibsBrowser . console ] , process : [ nodeLibsBrowser . process ] , } , ] ) ; } if ( this . options . cliOptions ?. analyze ) { this . chain . plugin ( 'bundle-analyze' ) . use ( BundleAnalyzerPlugin , [ { analyzerMode : 'static' , openAnalyzer : false , reportFilename : 'report.html' , } , ] ) ; } } } export { ClientWebpackConfig } ;