55 type RsbuildConfig ,
66 type RsbuildPlugin ,
77 type RsbuildPlugins ,
8+ type Rspack ,
89 defineConfig as defineRsbuildConfig ,
910 loadConfig as loadRsbuildConfig ,
1011 mergeRsbuildConfig ,
@@ -38,6 +39,7 @@ import type {
3839 DeepRequired ,
3940 ExcludesFalse ,
4041 Format ,
42+ JsRedirect ,
4143 LibConfig ,
4244 LibOnlyConfig ,
4345 PkgJson ,
@@ -50,6 +52,7 @@ import type {
5052 RslibConfigAsyncFn ,
5153 RslibConfigExport ,
5254 RslibConfigSyncFn ,
55+ RspackResolver ,
5356 Shims ,
5457 Syntax ,
5558} from './types' ;
@@ -63,6 +66,7 @@ import {
6366 isIntermediateOutputFormat ,
6467 isObject ,
6568 nodeBuiltInModules ,
69+ normalizeSlash ,
6670 omit ,
6771 pick ,
6872 readPackageJson ,
@@ -956,64 +960,113 @@ const composeEntryConfig = async (
956960 } ;
957961} ;
958962
959- const composeBundleConfig = (
963+ const composeBundlelessExternalConfig = (
960964 jsExtension : string ,
961965 redirect : Redirect ,
962966 cssModulesAuto : CssLoaderOptionsAuto ,
963967 bundle : boolean ,
964- ) : RsbuildConfig => {
965- if ( bundle ) return { } ;
968+ ) : {
969+ config : RsbuildConfig ;
970+ resolvedJsRedirect ?: DeepRequired < JsRedirect > ;
971+ } => {
972+ if ( bundle ) return { config : { } } ;
973+
974+ const isStyleRedirected = redirect . style ?? true ;
975+ const jsRedirectPath = redirect . js ?. path ?? true ;
976+ const jsRedirectExtension = redirect . js ?. extension ?? true ;
966977
967- const isStyleRedirect = redirect . style ?? true ;
978+ let resolver : RspackResolver | undefined ;
968979
969980 return {
970- output : {
971- externals : [
972- ( data : any , callback : any ) => {
973- // Issuer is not empty string when the module is imported by another module.
974- // Prevent from externalizing entry modules here.
975- if ( data . contextInfo . issuer ) {
976- // Node.js ECMAScript module loader does no extension searching.
977- // Add a file extension according to autoExtension config
978- // when data.request is a relative path and do not have an extension.
979- // If data.request already have an extension, we replace it with new extension
980- // This may result in a change in semantics,
981- // user should use copy to keep origin file or use another separate entry to deal this
982- let request : string = data . request ;
983-
984- const cssExternal = cssExternalHandler (
985- request ,
986- callback ,
987- jsExtension ,
988- cssModulesAuto ,
989- isStyleRedirect ,
990- ) ;
991-
992- if ( cssExternal !== false ) {
993- return cssExternal ;
981+ resolvedJsRedirect : {
982+ path : jsRedirectPath ,
983+ extension : jsRedirectExtension ,
984+ } ,
985+ config : {
986+ output : {
987+ externals : [
988+ async ( data , callback ) => {
989+ const { request, getResolve, context, contextInfo } = data ;
990+ if ( ! request || ! getResolve || ! context || ! contextInfo ) {
991+ return callback ( ) ;
992+ }
993+
994+ if ( ! resolver ) {
995+ resolver = ( await getResolve ( ) ) as RspackResolver ;
994996 }
995997
996- if ( request [ 0 ] === '.' ) {
997- const ext = extname ( request ) ;
998+ // Issuer is not empty string when the module is imported by another module.
999+ // Prevent from externalizing entry modules here.
1000+ if ( contextInfo . issuer ) {
1001+ let resolvedRequest : string = request ;
1002+
1003+ const cssExternal = cssExternalHandler (
1004+ resolvedRequest ,
1005+ callback ,
1006+ jsExtension ,
1007+ cssModulesAuto ,
1008+ isStyleRedirected ,
1009+ ) ;
1010+
1011+ if ( cssExternal !== false ) {
1012+ return cssExternal ;
1013+ }
1014+
1015+ if ( jsRedirectPath ) {
1016+ try {
1017+ resolvedRequest = await resolver ( context , resolvedRequest ) ;
1018+ } catch ( e ) {
1019+ // Do nothing, fallthrough to other external matches.
1020+ logger . debug (
1021+ `Failed to resolve ${ resolvedRequest } with resolver` ,
1022+ ) ;
1023+ }
1024+
1025+ resolvedRequest = normalizeSlash (
1026+ path . relative (
1027+ path . dirname ( contextInfo . issuer ) ,
1028+ resolvedRequest ,
1029+ ) ,
1030+ ) ;
1031+
1032+ // Requests that fall through here cannot be matched by any other externals config ahead.
1033+ // Treat all these requests as relative import of source code. Node.js won't add the
1034+ // leading './' to the relative path resolved by `path.relative`. So add manually it here.
1035+ if ( resolvedRequest [ 0 ] !== '.' ) {
1036+ resolvedRequest = `./${ resolvedRequest } ` ;
1037+ }
1038+ }
9981039
999- if ( ext ) {
1000- if ( JS_EXTENSIONS_PATTERN . test ( request ) ) {
1001- request = request . replace ( / \. [ ^ . ] + $ / , jsExtension ) ;
1040+ // Node.js ECMAScript module loader does no extension searching.
1041+ // Add a file extension according to autoExtension config
1042+ // when data.request is a relative path and do not have an extension.
1043+ // If data.request already have an extension, we replace it with new extension
1044+ // This may result in a change in semantics,
1045+ // user should use copy to keep origin file or use another separate entry to deal this
1046+ if ( jsRedirectExtension ) {
1047+ const ext = extname ( resolvedRequest ) ;
1048+ if ( ext ) {
1049+ if ( JS_EXTENSIONS_PATTERN . test ( resolvedRequest ) ) {
1050+ resolvedRequest = resolvedRequest . replace (
1051+ / \. [ ^ . ] + $ / ,
1052+ jsExtension ,
1053+ ) ;
1054+ } else {
1055+ // If it does not match jsExtensionsPattern, we should do nothing, eg: ./foo.png
1056+ return callback ( ) ;
1057+ }
10021058 } else {
1003- // If it does not match jsExtensionsPattern, we should do nothing, eg: ./foo.png
1004- return callback ( ) ;
1059+ resolvedRequest = `${ resolvedRequest } ${ jsExtension } ` ;
10051060 }
1006- } else {
1007- // TODO: add redirect.extension option
1008- request = `${ request } ${ jsExtension } ` ;
10091061 }
1062+
1063+ return callback ( undefined , resolvedRequest ) ;
10101064 }
10111065
1012- return callback ( null , request ) ;
1013- }
1014- callback ( ) ;
1015- } ,
1016- ] ,
1066+ callback ( ) ;
1067+ } ,
1068+ ] as Rspack . ExternalItem [ ] ,
1069+ } ,
10171070 } ,
10181071 } ;
10191072} ;
@@ -1054,17 +1107,15 @@ const composeDtsConfig = async (
10541107} ;
10551108
10561109const composeTargetConfig = (
1057- target : RsbuildConfigOutputTarget ,
1110+ userTarget : RsbuildConfigOutputTarget ,
10581111 format : Format ,
10591112) : {
10601113 config : RsbuildConfig ;
1114+ externalsConfig : RsbuildConfig ;
10611115 target : RsbuildConfigOutputTarget ;
10621116} => {
1063- let defaultTarget = target ;
1064- if ( ! defaultTarget ) {
1065- defaultTarget = format === 'mf' ? 'web' : 'node' ;
1066- }
1067- switch ( defaultTarget ) {
1117+ const target = userTarget ?? ( format === 'mf' ? 'web' : 'node' ) ;
1118+ switch ( target ) {
10681119 case 'web' :
10691120 return {
10701121 config : {
@@ -1075,6 +1126,7 @@ const composeTargetConfig = (
10751126 } ,
10761127 } ,
10771128 target : 'web' ,
1129+ externalsConfig : { } ,
10781130 } ;
10791131 case 'node' :
10801132 return {
@@ -1084,15 +1136,19 @@ const composeTargetConfig = (
10841136 target : [ 'node' ] ,
10851137 } ,
10861138 } ,
1139+ output : {
1140+ target : 'node' ,
1141+ } ,
1142+ } ,
1143+ target : 'node' ,
1144+ externalsConfig : {
10871145 output : {
10881146 // When output.target is 'node', Node.js's built-in will be treated as externals of type `node-commonjs`.
10891147 // Simply override the built-in modules to make them external.
10901148 // https://github.com/webpack/webpack/blob/dd44b206a9c50f4b4cb4d134e1a0bd0387b159a3/lib/node/NodeTargetPlugin.js#L81
10911149 externals : nodeBuiltInModules ,
1092- target : 'node' ,
10931150 } ,
10941151 } ,
1095- target : 'node' ,
10961152 } ;
10971153 // TODO: Support `neutral` target, however Rsbuild don't list it as an option in the target field.
10981154 // case 'neutral':
@@ -1104,7 +1160,7 @@ const composeTargetConfig = (
11041160 // },
11051161 // };
11061162 default :
1107- throw new Error ( `Unsupported platform: ${ defaultTarget } ` ) ;
1163+ throw new Error ( `Unsupported platform: ${ target } ` ) ;
11081164 }
11091165} ;
11101166
@@ -1189,7 +1245,7 @@ async function composeLibRsbuildConfig(
11891245 externalHelpers ,
11901246 pkgJson ,
11911247 ) ;
1192- const externalsConfig = composeExternalsConfig (
1248+ const userExternalsConfig = composeExternalsConfig (
11931249 format ! ,
11941250 config . output ?. externals ,
11951251 ) ;
@@ -1198,16 +1254,17 @@ async function composeLibRsbuildConfig(
11981254 jsExtension,
11991255 dtsExtension,
12001256 } = composeAutoExtensionConfig ( config , autoExtension , pkgJson ) ;
1201- const bundleConfig = composeBundleConfig (
1257+ const { config : bundlelessExternalConfig } = composeBundlelessExternalConfig (
12021258 jsExtension ,
12031259 redirect ,
12041260 cssModulesAuto ,
12051261 bundle ,
12061262 ) ;
1207- const { config : targetConfig , target } = composeTargetConfig (
1208- config . output ?. target ,
1209- format ! ,
1210- ) ;
1263+ const {
1264+ config : targetConfig ,
1265+ externalsConfig : targetExternalsConfig ,
1266+ target,
1267+ } = composeTargetConfig ( config . output ?. target , format ! ) ;
12111268 const syntaxConfig = composeSyntaxConfig ( target , config ?. syntax ) ;
12121269 const autoExternalConfig = composeAutoExternalConfig ( {
12131270 format : format ! ,
@@ -1231,7 +1288,7 @@ async function composeLibRsbuildConfig(
12311288 const externalsWarnConfig = composeExternalsWarnConfig (
12321289 format ! ,
12331290 autoExternalConfig ?. output ?. externals ,
1234- externalsConfig ?. output ?. externals ,
1291+ userExternalsConfig ?. output ?. externals ,
12351292 ) ;
12361293 const minifyConfig = composeMinifyConfig ( config ) ;
12371294 const bannerFooterConfig = composeBannerFooterConfig ( banner , footer ) ;
@@ -1243,15 +1300,23 @@ async function composeLibRsbuildConfig(
12431300 return mergeRsbuildConfig (
12441301 formatConfig ,
12451302 shimsConfig ,
1303+ syntaxConfig ,
12461304 externalHelpersConfig ,
1247- // externalsWarnConfig should before other externals config
1248- externalsWarnConfig ,
1249- externalsConfig ,
1250- autoExternalConfig ,
12511305 autoExtensionConfig ,
1252- syntaxConfig ,
1253- bundleConfig ,
12541306 targetConfig ,
1307+ // #region Externals configs
1308+ // The order of the externals config should come in the following order:
1309+ // 1. `externalsWarnConfig` should come before other externals config to touch the externalized modules first.
1310+ // 2. The externals config in `bundlelessExternalConfig` should present after other externals config as
1311+ // it relies on other externals config to bail out the externalized modules first then resolve
1312+ // the correct path for relative imports.
1313+ // 3. `userExternalsConfig` should present later to override the externals config of the ahead ones.
1314+ externalsWarnConfig ,
1315+ autoExternalConfig ,
1316+ targetExternalsConfig ,
1317+ userExternalsConfig ,
1318+ bundlelessExternalConfig ,
1319+ // #endregion
12551320 entryConfig ,
12561321 cssConfig ,
12571322 assetConfig ,
0 commit comments