Skip to content

Commit 49a9974

Browse files
authored
feat: support attributes for ESM external module (#8422)
1 parent bd78e52 commit 49a9974

File tree

12 files changed

+129
-81
lines changed

12 files changed

+129
-81
lines changed

crates/rspack_core/src/external_module.rs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use crate::{
1313
to_identifier, AsyncDependenciesBlockIdentifier, BuildContext, BuildInfo, BuildMeta,
1414
BuildMetaExportsType, BuildResult, ChunkInitFragments, ChunkUkey, CodeGenerationDataUrl,
1515
CodeGenerationResult, Compilation, ConcatenationScope, Context, DependenciesBlock, DependencyId,
16-
ExternalType, FactoryMeta, InitFragmentExt, InitFragmentKey, InitFragmentStage, LibIdentOptions,
17-
Module, ModuleType, NormalInitFragment, RuntimeGlobals, RuntimeSpec, SourceType,
16+
ExternalType, FactoryMeta, ImportAttributes, InitFragmentExt, InitFragmentKey, InitFragmentStage,
17+
LibIdentOptions, Module, ModuleType, NormalInitFragment, RuntimeGlobals, RuntimeSpec, SourceType,
1818
StaticExportsDependency, StaticExportsSpec, NAMESPACE_OBJECT_EXPORT,
1919
};
2020
use crate::{ChunkGraph, ModuleGraph};
@@ -112,12 +112,24 @@ fn get_source_for_commonjs(module_and_specifiers: &ExternalRequestValue) -> Stri
112112
fn get_source_for_import(
113113
module_and_specifiers: &ExternalRequestValue,
114114
compilation: &Compilation,
115+
attributes: &Option<ImportAttributes>,
115116
) -> String {
116-
format!(
117-
"{}({})",
118-
compilation.options.output.import_function_name,
119-
serde_json::to_string(module_and_specifiers.primary()).expect("invalid json to_string")
120-
)
117+
format!("{}({})", compilation.options.output.import_function_name, {
118+
let attributes_str = if let Some(attributes) = attributes {
119+
format!(
120+
", {{ with: {} }}",
121+
serde_json::to_string(attributes).expect("invalid json to_string")
122+
)
123+
} else {
124+
String::new()
125+
};
126+
127+
format!(
128+
"{}{}",
129+
serde_json::to_string(module_and_specifiers.primary()).expect("invalid json to_string"),
130+
attributes_str
131+
)
132+
})
121133
}
122134

123135
/**
@@ -178,6 +190,7 @@ pub type MetaExternalType = Option<ExternalTypeEnum>;
178190
#[derive(Debug)]
179191
pub struct DependencyMeta {
180192
pub external_type: MetaExternalType,
193+
pub attributes: Option<ImportAttributes>,
181194
}
182195

183196
impl ExternalModule {
@@ -300,7 +313,7 @@ impl ExternalModule {
300313
"import" if let Some(request) = request => format!(
301314
"{} = {};",
302315
get_namespace_object_export(concatenation_scope, supports_const),
303-
get_source_for_import(request, compilation)
316+
get_source_for_import(request, compilation, &self.dependency_meta.attributes)
304317
),
305318
"var" | "promise" | "const" | "let" | "assign" if let Some(request) = request => format!(
306319
"{} = {};",
@@ -313,9 +326,20 @@ impl ExternalModule {
313326
chunk_init_fragments.push(
314327
NormalInitFragment::new(
315328
format!(
316-
"import * as __WEBPACK_EXTERNAL_MODULE_{}__ from {};\n",
329+
"import * as __WEBPACK_EXTERNAL_MODULE_{}__ from {}{};\n",
317330
id.clone(),
318-
json_stringify(request.primary())
331+
json_stringify(request.primary()),
332+
{
333+
let meta = &self.dependency_meta.attributes;
334+
if let Some(meta) = meta {
335+
format!(
336+
" with {}",
337+
serde_json::to_string(meta).expect("json stringify failed"),
338+
)
339+
} else {
340+
String::new()
341+
}
342+
},
319343
),
320344
InitFragmentStage::StageESMImports,
321345
0,
@@ -344,7 +368,7 @@ impl ExternalModule {
344368
format!(
345369
"{} = {};",
346370
get_namespace_object_export(concatenation_scope, supports_const),
347-
get_source_for_import(request, compilation)
371+
get_source_for_import(request, compilation, &self.dependency_meta.attributes)
348372
)
349373
}
350374
}

crates/rspack_plugin_externals/src/plugin.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ impl ExternalsPlugin {
103103
}
104104

105105
let dependency_meta: DependencyMeta = DependencyMeta {
106+
attributes: dependency.get_attributes().cloned(),
106107
external_type: {
107108
if dependency
108109
.as_any()

crates/rspack_plugin_library/src/modern_module/import_dependency.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,23 @@ impl DependencyTemplate for ModernModuleImportDependency {
9797
};
9898

9999
if let Some(request_and_external_type) = request_and_external_type.0 {
100+
let attributes_str = if let Some(attributes) = &self.attributes {
101+
format!(
102+
", {{ with: {} }}",
103+
serde_json::to_string(attributes).expect("invalid json to_string")
104+
)
105+
} else {
106+
String::new()
107+
};
108+
100109
source.replace(
101110
self.range.start,
102111
self.range.end,
103112
format!(
104-
"import({})",
113+
"import({}{})",
105114
serde_json::to_string(request_and_external_type.primary())
106-
.expect("invalid json to_string")
115+
.expect("invalid json to_string"),
116+
attributes_str
107117
)
108118
.as_str(),
109119
None,

crates/rspack_plugin_library/src/modern_module_library_plugin.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ async fn finish_modules(&self, compilation: &mut Compilation) -> Result<()> {
249249
external_module.request.clone(),
250250
external_module.external_type.clone(),
251251
import_dependency.range.clone(),
252-
None,
252+
import_dependency.get_attributes().cloned(),
253253
);
254254

255255
deps_to_replace.push((

crates/rspack_plugin_library/src/module_library_plugin.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ use std::hash::Hash;
33
use rspack_core::rspack_sources::{ConcatSource, RawSource, SourceExt};
44
use rspack_core::{
55
property_access, to_identifier, ApplyContext, ChunkUkey, Compilation, CompilationParams,
6-
CompilerCompilation, CompilerOptions, LibraryOptions, ModuleGraph, ModuleIdentifier, Plugin,
7-
PluginContext,
6+
CompilerCompilation, CompilerOptions, ExportInfoProvided, LibraryOptions, ModuleGraph,
7+
ModuleIdentifier, Plugin, PluginContext,
88
};
99
use rspack_error::{error_bail, Result};
1010
use rspack_hash::RspackHash;
@@ -76,6 +76,13 @@ fn render_startup(
7676
}
7777
let exports_info = module_graph.get_exports_info(module);
7878
for export_info in exports_info.ordered_exports(&module_graph) {
79+
if !(matches!(
80+
export_info.provided(&module_graph),
81+
Some(ExportInfoProvided::True)
82+
)) {
83+
continue;
84+
};
85+
7986
let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey);
8087
let info_name = export_info.name(&module_graph).expect("should have name");
8188
let used_name = export_info

packages/rspack-test-tools/tests/configCases/library/modern-module-dynamic-import-runtime/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ it("modern-module-dynamic-import-runtime", () => {
1111
expect(initialChunk).toContain('import * as __WEBPACK_EXTERNAL_MODULE_angular_alias__ from "angular-alias"');
1212
expect(initialChunk).toContain('const reactNs = await import("react-alias")');
1313
expect(initialChunk).toContain('const vueNs = await import("vue-alias")');
14+
expect(initialChunk).toContain('const jqueryNs = await import("jquery-alias", { with: {"type":"url"} })');
1415

1516
expect(asyncChunk).toContain('const litNs = await import("lit-alias")');
1617
expect(asyncChunk).toContain('const solidNs = await import("solid-alias")');

packages/rspack-test-tools/tests/configCases/library/modern-module-dynamic-import-runtime/main.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export const main = async () => {
55
const dyn = await import('./dyn.js') // lazy dynamic import
66
const reactNs = await import('react') // 'module' + 'import' externalized
77
const vueNs = await import('vue') // 'import' externalized
8-
console.log(angular, react, reactNs, vueNs, dyn)
8+
const jqueryNs = await import('jquery', { with: { type: 'url' } }) // import attributes should be preserved
9+
console.log(angular, react, reactNs, vueNs, dyn, jqueryNs)
910
}

packages/rspack-test-tools/tests/configCases/library/modern-module-dynamic-import-runtime/rspack.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module.exports = [{
2121
svelte: 'svelte-alias',
2222
lit: 'lit-alias',
2323
solid: 'solid-alias',
24+
jquery: 'jquery-alias',
2425
},
2526
externalsType: 'module-import',
2627
experiments: {

tests/webpack-test/ConfigTestCases.template.js

Lines changed: 62 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ const describeCases = config => {
567567
}
568568
if (esmMode === "unlinked") return esm;
569569
return (async () => {
570+
if (esmMode === "unlinked") return esm;
570571
await esm.link(
571572
async (specifier, referencingModule) => {
572573
return await asModule(
@@ -597,64 +598,69 @@ const describeCases = config => {
597598
? ns.default
598599
: ns;
599600
})();
600-
} else {
601-
if (p in requireCache) {
602-
return requireCache[p].exports;
603-
}
604-
const m = {
605-
exports: {}
606-
};
607-
requireCache[p] = m;
608-
const moduleScope = {
609-
...baseModuleScope,
610-
require: _require.bind(
611-
null,
612-
path.dirname(p),
613-
options
614-
),
615-
importScripts: url => {
616-
expect(url).toMatch(
617-
/^https:\/\/test\.cases\/path\//
618-
);
619-
_require(
620-
outputDirectory,
621-
options,
622-
`.${url.slice(
623-
"https://test.cases/path".length
624-
)}`
625-
);
626-
},
627-
module: m,
628-
exports: m.exports,
629-
__dirname: path.dirname(p),
630-
__filename: p,
631-
_globalAssign: { expect }
632-
};
633-
if (testConfig.moduleScope) {
634-
testConfig.moduleScope(moduleScope);
635-
}
636-
if (!runInNewContext)
637-
content = `Object.assign(global, _globalAssign); ${content}`;
638-
const args = Object.keys(moduleScope);
639-
const argValues = args.map(arg => moduleScope[arg]);
640-
const code = `(function(${args.join(
641-
", "
642-
)}) {${content}\n})`;
601+
}
602+
const isJSON = p.endsWith(".json");
603+
if (isJSON) {
604+
return JSON.parse(content);
605+
}
643606

644-
let oldCurrentScript = document.currentScript;
645-
document.currentScript = new CurrentScript(subPath);
646-
const fn = runInNewContext
647-
? vm.runInNewContext(code, globalContext, p)
648-
: vm.runInThisContext(code, p);
649-
fn.call(
650-
testConfig.nonEsmThis
651-
? testConfig.nonEsmThis(module)
652-
: m.exports,
653-
...argValues
654-
);
655-
document.currentScript = oldCurrentScript;
656-
return m.exports;
607+
if (p in requireCache) {
608+
return requireCache[p].exports;
657609
}
610+
const m = {
611+
exports: {}
612+
};
613+
requireCache[p] = m;
614+
615+
const moduleScope = {
616+
...baseModuleScope,
617+
require: _require.bind(
618+
null,
619+
path.dirname(p),
620+
options
621+
),
622+
importScripts: url => {
623+
expect(url).toMatch(
624+
/^https:\/\/test\.cases\/path\//
625+
);
626+
_require(
627+
outputDirectory,
628+
options,
629+
`.${url.slice(
630+
"https://test.cases/path".length
631+
)}`
632+
);
633+
},
634+
module: m,
635+
exports: m.exports,
636+
__dirname: path.dirname(p),
637+
__filename: p,
638+
_globalAssign: { expect }
639+
};
640+
if (testConfig.moduleScope) {
641+
testConfig.moduleScope(moduleScope);
642+
}
643+
if (!runInNewContext)
644+
content = `Object.assign(global, _globalAssign); ${content}`;
645+
const args = Object.keys(moduleScope);
646+
const argValues = args.map(arg => moduleScope[arg]);
647+
const code = `(function(${args.join(
648+
", "
649+
)}) {${content}\n})`;
650+
651+
let oldCurrentScript = document.currentScript;
652+
document.currentScript = new CurrentScript(subPath);
653+
const fn = runInNewContext
654+
? vm.runInNewContext(code, globalContext, p)
655+
: vm.runInThisContext(code, p);
656+
fn.call(
657+
testConfig.nonEsmThis
658+
? testConfig.nonEsmThis(module)
659+
: m.exports,
660+
...argValues
661+
);
662+
document.currentScript = oldCurrentScript;
663+
return m.exports;
658664
} else if (
659665
testConfig.modules &&
660666
module in testConfig.modules
Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1 @@
1-
2-
// TODO: Should create a issue for this test
3-
// TODO: _painic
4-
module.exports = () => {
5-
// return /^v(2[2-9])/.test(process.version);
6-
return false;
7-
};
1+
module.exports = () => /^v(2[2-9])/.test(process.version);

0 commit comments

Comments
 (0)