@@ -10,14 +10,11 @@ import {
1010 type WebpackError ,
1111 default as webpack ,
1212} from "webpack" ;
13- import type webpackMerge from "webpack-merge" ;
1413
15- import { type CLIPlugin as CLIPluginClass } from "./plugins/cli-plugin.js" ;
1614import {
1715 type Argument ,
1816 type Argv ,
1917 type BasicPrimitive ,
20- type CLIPluginOptions ,
2118 type CallableWebpackConfiguration ,
2219 type CommandAction ,
2320 type DynamicImport ,
@@ -26,7 +23,6 @@ import {
2623 type IWebpackCLI ,
2724 type ImportLoaderError ,
2825 type Instantiable ,
29- type JsonExt ,
3026 type LoadableWebpackConfiguration ,
3127 type ModuleName ,
3228 type PackageInstallOptions ,
@@ -73,6 +69,11 @@ const WEBPACK_DEV_SERVER_PACKAGE = WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM
7369 : "webpack-dev-server" ;
7470
7571const EXIT_SIGNALS = [ "SIGINT" , "SIGTERM" ] ;
72+ const DEFAULT_CONFIGURATION_FILES = [
73+ "webpack.config" ,
74+ ".webpack/webpack.config" ,
75+ ".webpack/webpackfile" ,
76+ ] ;
7677
7778interface Information {
7879 Binaries ?: string [ ] ;
@@ -83,6 +84,25 @@ interface Information {
8384 npmPackages ?: string | string [ ] ;
8485}
8586
87+ type LoadConfigOption = PotentialPromise < Configuration > ;
88+
89+ class ConfigurationLoadingError extends Error {
90+ name = "ConfigurationLoadingError" ;
91+
92+ constructor ( errors : [ unknown , unknown ] ) {
93+ const message1 = errors [ 0 ] instanceof Error ? errors [ 0 ] . message : String ( errors [ 0 ] ) ;
94+ const message2 = util . stripVTControlCharacters (
95+ errors [ 1 ] instanceof Error ? errors [ 1 ] . message : String ( errors [ 1 ] ) ,
96+ ) ;
97+ const message =
98+ `▶ ESM (\`import\`) failed:\n ${ message1 . split ( "\n" ) . join ( "\n " ) } \n\n▶ CJS (\`require\`) failed:\n ${ message2 . split ( "\n" ) . join ( "\n " ) } ` . trim ( ) ;
99+
100+ super ( message ) ;
101+
102+ this . stack = "" ;
103+ }
104+ }
105+
86106class WebpackCLI implements IWebpackCLI {
87107 colors : WebpackCLIColors ;
88108
@@ -348,7 +368,7 @@ class WebpackCLI implements IWebpackCLI {
348368 }
349369
350370 if ( needInstall ) {
351- const { sync } = require ( "cross-spawn" ) ;
371+ const { sync } = await import ( "cross-spawn" ) ;
352372
353373 try {
354374 sync ( packageManager , commandArguments , { stdio : "inherit" } ) ;
@@ -364,6 +384,7 @@ class WebpackCLI implements IWebpackCLI {
364384 process . exit ( 2 ) ;
365385 }
366386
387+ // TODO remove me in the next major release
367388 async tryRequireThenImport < T > (
368389 module : ModuleName ,
369390 handleError = true ,
@@ -539,7 +560,7 @@ class WebpackCLI implements IWebpackCLI {
539560
540561 defaultInformation . npmPackages = `{${ defaultPackages . map ( ( item ) => `*${ item } *` ) . join ( "," ) } }` ;
541562
542- const envinfo = await this . tryRequireThenImport < typeof import ( "envinfo" ) > ( "envinfo" , false ) ;
563+ const envinfo = await import ( "envinfo" ) ;
543564
544565 let info = await envinfo . run ( defaultInformation , envinfoConfig ) ;
545566
@@ -1094,8 +1115,8 @@ class WebpackCLI implements IWebpackCLI {
10941115 return options ;
10951116 }
10961117
1097- async loadWebpack ( handleError = true ) {
1098- return this . tryRequireThenImport < typeof webpack > ( WEBPACK_PACKAGE , handleError ) ;
1118+ async loadWebpack ( ) : Promise < typeof webpack > {
1119+ return require ( WEBPACK_PACKAGE ) ;
10991120 }
11001121
11011122 async run ( args : Parameters < WebpackCLICommand [ "parseOptions" ] > [ 0 ] , parseOptions : ParseOptions ) {
@@ -1287,7 +1308,7 @@ class WebpackCLI implements IWebpackCLI {
12871308 } ;
12881309
12891310 // Register own exit
1290- this . program . exitOverride ( async ( error ) => {
1311+ this . program . exitOverride ( ( error ) => {
12911312 if ( error . exitCode === 0 ) {
12921313 process . exit ( 0 ) ;
12931314 }
@@ -1325,10 +1346,10 @@ class WebpackCLI implements IWebpackCLI {
13251346 process . exit ( 2 ) ;
13261347 }
13271348
1328- const levenshtein = require ( "fastest-levenshtein" ) ;
1349+ const { distance } = require ( "fastest-levenshtein" ) ;
13291350
13301351 for ( const option of ( command as WebpackCLICommand ) . options ) {
1331- if ( ! option . hidden && levenshtein . distance ( name , option . long ?. slice ( 2 ) ) < 3 ) {
1352+ if ( ! option . hidden && distance ( name , option . long ?. slice ( 2 ) as string ) < 3 ) {
13321353 this . logger . error ( `Did you mean '--${ option . name ( ) } '?` ) ;
13331354 }
13341355 }
@@ -1761,11 +1782,10 @@ class WebpackCLI implements IWebpackCLI {
17611782 } else {
17621783 this . logger . error ( `Unknown command or entry '${ operand } '` ) ;
17631784
1764- const levenshtein = require ( "fastest-levenshtein" ) ;
1785+ const { distance } = await import ( "fastest-levenshtein" ) ;
17651786
17661787 const found = knownCommands . find (
1767- ( commandOptions ) =>
1768- levenshtein . distance ( operand , getCommandName ( commandOptions . name ) ) < 3 ,
1788+ ( commandOptions ) => distance ( operand , getCommandName ( commandOptions . name ) ) < 3 ,
17691789 ) ;
17701790
17711791 if ( found ) {
@@ -1789,30 +1809,41 @@ class WebpackCLI implements IWebpackCLI {
17891809 await this . program . parseAsync ( args , parseOptions ) ;
17901810 }
17911811
1792- async loadConfig ( options : Partial < WebpackDevServerOptions > ) {
1793- const disableInterpret =
1794- typeof options . disableInterpret !== "undefined" && options . disableInterpret ;
1812+ async #loadConfigurationFile(
1813+ configPath : string ,
1814+ disableInterpret = false ,
1815+ ) : Promise < LoadConfigOption | LoadConfigOption [ ] | undefined > {
1816+ let pkg : LoadConfigOption | LoadConfigOption [ ] | undefined ;
17951817
1796- const interpret = require ( "interpret" ) ;
1818+ let loadingError ;
17971819
1798- const loadConfigByPath = async (
1799- configPath : string ,
1800- argv : Argv = { } ,
1801- ) : Promise < { options : Configuration | Configuration [ ] ; path : string } > => {
1820+ try {
1821+ // eslint-disable-next-line no-eval
1822+ pkg = ( await eval ( `import("${ pathToFileURL ( configPath ) } ")` ) ) . default ;
1823+ } catch ( err ) {
1824+ if ( this . isValidationError ( err ) || process . env ?. WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG ) {
1825+ throw err ;
1826+ }
1827+
1828+ loadingError = err ;
1829+ }
1830+
1831+ // Fallback logic when we can't use `import(...)`
1832+ if ( loadingError ) {
1833+ const { jsVariants, extensions } = await import ( "interpret" ) ;
18021834 const ext = path . extname ( configPath ) . toLowerCase ( ) ;
1803- let interpreted = Object . keys ( interpret . jsVariants ) . find ( ( variant ) => variant === ext ) ;
1804- // Fallback `.cts` to `.ts`
1805- // TODO implement good `.mts` support after https://github.com/gulpjs/rechoir/issues/43
1806- // For ESM and `.mts` you need to use: 'NODE_OPTIONS="--loader ts-node/esm" webpack-cli --config ./webpack.config.mts'
1835+
1836+ let interpreted = Object . keys ( jsVariants ) . find ( ( variant ) => variant === ext ) ;
1837+
18071838 if ( ! interpreted && ext . endsWith ( ".cts" ) ) {
1808- interpreted = interpret . jsVariants [ ".ts" ] ;
1839+ interpreted = jsVariants [ ".ts" ] as string ;
18091840 }
18101841
18111842 if ( interpreted && ! disableInterpret ) {
1812- const rechoir : Rechoir = require ( "rechoir" ) ;
1843+ const rechoir : Rechoir = ( await import ( "rechoir" ) ) . default ;
18131844
18141845 try {
1815- rechoir . prepare ( interpret . extensions , configPath ) ;
1846+ rechoir . prepare ( extensions , configPath ) ;
18161847 } catch ( error ) {
18171848 if ( ( error as RechoirError ) ?. failures ) {
18181849 this . logger . error ( `Unable load '${ configPath } '` ) ;
@@ -1823,52 +1854,59 @@ class WebpackCLI implements IWebpackCLI {
18231854 this . logger . error ( "Please install one of them" ) ;
18241855 process . exit ( 2 ) ;
18251856 }
1826-
18271857 this . logger . error ( error ) ;
18281858 process . exit ( 2 ) ;
18291859 }
18301860 }
18311861
1832- let options : LoadableWebpackConfiguration | LoadableWebpackConfiguration [ ] ;
1862+ try {
1863+ pkg = require ( configPath ) ;
1864+ } catch ( err ) {
1865+ if ( this . isValidationError ( err ) ) {
1866+ throw err ;
1867+ }
1868+
1869+ throw new ConfigurationLoadingError ( [ loadingError , err ] ) ;
1870+ }
1871+ }
1872+
1873+ // To handle `babel`/`module.exports.default = {};`
1874+ if ( pkg && typeof pkg === "object" && "default" in pkg ) {
1875+ pkg = pkg . default as LoadConfigOption | LoadConfigOption [ ] | undefined ;
1876+ }
18331877
1834- type LoadConfigOption = PotentialPromise < Configuration > ;
1878+ if ( ! pkg ) {
1879+ this . logger . warn (
1880+ `Default export is missing or nullish at (from ${ configPath } ). Webpack will run with an empty configuration. Please double-check that this is what you want. If you want to run webpack with an empty config, \`export {}\`/\`module.exports = {};\` to remove this warning.` ,
1881+ ) ;
1882+ }
18351883
1836- let moduleType : "unknown" | "commonjs" | "esm" = "unknown" ;
1884+ return pkg || { } ;
1885+ }
18371886
1838- switch ( ext ) {
1839- case ".cjs" :
1840- case ".cts" :
1841- moduleType = "commonjs" ;
1842- break ;
1843- case ".mjs" :
1844- case ".mts" :
1845- moduleType = "esm" ;
1846- break ;
1847- }
1887+ async loadConfig ( options : Partial < WebpackDevServerOptions > ) {
1888+ const disableInterpret =
1889+ typeof options . disableInterpret !== "undefined" && options . disableInterpret ;
1890+
1891+ const loadConfigByPath = async (
1892+ configPath : string ,
1893+ argv : Argv = { } ,
1894+ ) : Promise < { options : Configuration | Configuration [ ] ; path : string } > => {
1895+ let options : LoadableWebpackConfiguration | LoadableWebpackConfiguration [ ] | undefined ;
18481896
18491897 try {
1850- options = await this . tryRequireThenImport < LoadConfigOption | LoadConfigOption [ ] > (
1851- configPath ,
1852- false ,
1853- moduleType ,
1854- ) ;
1898+ options = await this . #loadConfigurationFile( configPath , disableInterpret ) ;
18551899 } catch ( error ) {
1856- this . logger . error ( `Failed to load '${ configPath } ' config` ) ;
1857-
1858- if ( this . isValidationError ( error ) ) {
1859- this . logger . error ( error . message ) ;
1900+ if ( error instanceof ConfigurationLoadingError ) {
1901+ this . logger . error ( `Failed to load '${ configPath } ' config\n${ error . message } ` ) ;
18601902 } else {
1903+ this . logger . error ( `Failed to load '${ configPath } ' config` ) ;
18611904 this . logger . error ( error ) ;
18621905 }
18631906
18641907 process . exit ( 2 ) ;
18651908 }
18661909
1867- if ( ! options ) {
1868- this . logger . error ( `Failed to load '${ configPath } ' config. Unable to find default export.` ) ;
1869- process . exit ( 2 ) ;
1870- }
1871-
18721910 if ( Array . isArray ( options ) ) {
18731911 // reassign the value to assert type
18741912 const optionsArray : LoadableWebpackConfiguration [ ] = options ;
@@ -1950,6 +1988,7 @@ class WebpackCLI implements IWebpackCLI {
19501988 }
19511989 }
19521990 } else {
1991+ const interpret = await import ( "interpret" ) ;
19531992 // Prioritize popular extensions first to avoid unnecessary fs calls
19541993 const extensions = new Set ( [
19551994 ".js" ,
@@ -1962,7 +2001,7 @@ class WebpackCLI implements IWebpackCLI {
19622001 ] ) ;
19632002 // Order defines the priority, in decreasing order
19642003 const defaultConfigFiles = new Set (
1965- [ "webpack.config" , ".webpack/webpack.config" , ".webpack/webpackfile" ] . flatMap ( ( filename ) =>
2004+ DEFAULT_CONFIGURATION_FILES . flatMap ( ( filename ) =>
19662005 [ ...extensions ] . map ( ( ext ) => path . resolve ( filename + ext ) ) ,
19672006 ) ,
19682007 ) ;
@@ -2035,7 +2074,7 @@ class WebpackCLI implements IWebpackCLI {
20352074 ) ,
20362075 ) ;
20372076
2038- const merge = await this . tryRequireThenImport < typeof webpackMerge > ( "webpack-merge" ) ;
2077+ const { merge } = await import ( "webpack-merge" ) ;
20392078 const loadedOptions = loadedConfigs . flatMap ( ( config ) => config . options ) ;
20402079
20412080 if ( loadedOptions . length > 0 ) {
@@ -2108,7 +2147,7 @@ class WebpackCLI implements IWebpackCLI {
21082147 }
21092148
21102149 if ( options . merge ) {
2111- const merge = await this . tryRequireThenImport < typeof webpackMerge > ( "webpack-merge" ) ;
2150+ const { merge } = await import ( "webpack-merge" ) ;
21122151
21132152 // we can only merge when there are multiple configurations
21142153 // either by passing multiple configs by flags or passing a
@@ -2161,10 +2200,7 @@ class WebpackCLI implements IWebpackCLI {
21612200 process . exit ( 2 ) ;
21622201 }
21632202
2164- const CLIPlugin =
2165- await this . tryRequireThenImport < Instantiable < CLIPluginClass , [ CLIPluginOptions ] > > (
2166- "./plugins/cli-plugin" ,
2167- ) ;
2203+ const CLIPlugin = ( await import ( "./plugins/cli-plugin.js" ) ) . default ;
21682204
21692205 const internalBuildConfig = ( item : Configuration ) => {
21702206 const originalWatchValue = item . watch ;
@@ -2407,9 +2443,9 @@ class WebpackCLI implements IWebpackCLI {
24072443 let createStringifyChunked : typeof stringifyChunked ;
24082444
24092445 if ( options . json ) {
2410- const jsonExt = await this . tryRequireThenImport < JsonExt > ( "@discoveryjs/json-ext" ) ;
2446+ const { stringifyChunked } = await import ( "@discoveryjs/json-ext" ) ;
24112447
2412- createStringifyChunked = jsonExt . stringifyChunked ;
2448+ createStringifyChunked = stringifyChunked ;
24132449 }
24142450
24152451 const callback : WebpackCallback = ( error , stats ) : void => {
0 commit comments