Skip to content

Commit

Permalink
feat: support attributes for ESM external module (#8422)
Browse files Browse the repository at this point in the history
  • Loading branch information
fi3ework authored Nov 15, 2024
1 parent bd78e52 commit 49a9974
Show file tree
Hide file tree
Showing 12 changed files with 129 additions and 81 deletions.
46 changes: 35 additions & 11 deletions crates/rspack_core/src/external_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use crate::{
to_identifier, AsyncDependenciesBlockIdentifier, BuildContext, BuildInfo, BuildMeta,
BuildMetaExportsType, BuildResult, ChunkInitFragments, ChunkUkey, CodeGenerationDataUrl,
CodeGenerationResult, Compilation, ConcatenationScope, Context, DependenciesBlock, DependencyId,
ExternalType, FactoryMeta, InitFragmentExt, InitFragmentKey, InitFragmentStage, LibIdentOptions,
Module, ModuleType, NormalInitFragment, RuntimeGlobals, RuntimeSpec, SourceType,
ExternalType, FactoryMeta, ImportAttributes, InitFragmentExt, InitFragmentKey, InitFragmentStage,
LibIdentOptions, Module, ModuleType, NormalInitFragment, RuntimeGlobals, RuntimeSpec, SourceType,
StaticExportsDependency, StaticExportsSpec, NAMESPACE_OBJECT_EXPORT,
};
use crate::{ChunkGraph, ModuleGraph};
Expand Down Expand Up @@ -112,12 +112,24 @@ fn get_source_for_commonjs(module_and_specifiers: &ExternalRequestValue) -> Stri
fn get_source_for_import(
module_and_specifiers: &ExternalRequestValue,
compilation: &Compilation,
attributes: &Option<ImportAttributes>,
) -> String {
format!(
"{}({})",
compilation.options.output.import_function_name,
serde_json::to_string(module_and_specifiers.primary()).expect("invalid json to_string")
)
format!("{}({})", compilation.options.output.import_function_name, {
let attributes_str = if let Some(attributes) = attributes {
format!(
", {{ with: {} }}",
serde_json::to_string(attributes).expect("invalid json to_string")
)
} else {
String::new()
};

format!(
"{}{}",
serde_json::to_string(module_and_specifiers.primary()).expect("invalid json to_string"),
attributes_str
)
})
}

/**
Expand Down Expand Up @@ -178,6 +190,7 @@ pub type MetaExternalType = Option<ExternalTypeEnum>;
#[derive(Debug)]
pub struct DependencyMeta {
pub external_type: MetaExternalType,
pub attributes: Option<ImportAttributes>,
}

impl ExternalModule {
Expand Down Expand Up @@ -300,7 +313,7 @@ impl ExternalModule {
"import" if let Some(request) = request => format!(
"{} = {};",
get_namespace_object_export(concatenation_scope, supports_const),
get_source_for_import(request, compilation)
get_source_for_import(request, compilation, &self.dependency_meta.attributes)
),
"var" | "promise" | "const" | "let" | "assign" if let Some(request) = request => format!(
"{} = {};",
Expand All @@ -313,9 +326,20 @@ impl ExternalModule {
chunk_init_fragments.push(
NormalInitFragment::new(
format!(
"import * as __WEBPACK_EXTERNAL_MODULE_{}__ from {};\n",
"import * as __WEBPACK_EXTERNAL_MODULE_{}__ from {}{};\n",
id.clone(),
json_stringify(request.primary())
json_stringify(request.primary()),
{
let meta = &self.dependency_meta.attributes;
if let Some(meta) = meta {
format!(
" with {}",
serde_json::to_string(meta).expect("json stringify failed"),
)
} else {
String::new()
}
},
),
InitFragmentStage::StageESMImports,
0,
Expand Down Expand Up @@ -344,7 +368,7 @@ impl ExternalModule {
format!(
"{} = {};",
get_namespace_object_export(concatenation_scope, supports_const),
get_source_for_import(request, compilation)
get_source_for_import(request, compilation, &self.dependency_meta.attributes)
)
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/rspack_plugin_externals/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ impl ExternalsPlugin {
}

let dependency_meta: DependencyMeta = DependencyMeta {
attributes: dependency.get_attributes().cloned(),
external_type: {
if dependency
.as_any()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,23 @@ impl DependencyTemplate for ModernModuleImportDependency {
};

if let Some(request_and_external_type) = request_and_external_type.0 {
let attributes_str = if let Some(attributes) = &self.attributes {
format!(
", {{ with: {} }}",
serde_json::to_string(attributes).expect("invalid json to_string")
)
} else {
String::new()
};

source.replace(
self.range.start,
self.range.end,
format!(
"import({})",
"import({}{})",
serde_json::to_string(request_and_external_type.primary())
.expect("invalid json to_string")
.expect("invalid json to_string"),
attributes_str
)
.as_str(),
None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ async fn finish_modules(&self, compilation: &mut Compilation) -> Result<()> {
external_module.request.clone(),
external_module.external_type.clone(),
import_dependency.range.clone(),
None,
import_dependency.get_attributes().cloned(),
);

deps_to_replace.push((
Expand Down
11 changes: 9 additions & 2 deletions crates/rspack_plugin_library/src/module_library_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::hash::Hash;
use rspack_core::rspack_sources::{ConcatSource, RawSource, SourceExt};
use rspack_core::{
property_access, to_identifier, ApplyContext, ChunkUkey, Compilation, CompilationParams,
CompilerCompilation, CompilerOptions, LibraryOptions, ModuleGraph, ModuleIdentifier, Plugin,
PluginContext,
CompilerCompilation, CompilerOptions, ExportInfoProvided, LibraryOptions, ModuleGraph,
ModuleIdentifier, Plugin, PluginContext,
};
use rspack_error::{error_bail, Result};
use rspack_hash::RspackHash;
Expand Down Expand Up @@ -76,6 +76,13 @@ fn render_startup(
}
let exports_info = module_graph.get_exports_info(module);
for export_info in exports_info.ordered_exports(&module_graph) {
if !(matches!(
export_info.provided(&module_graph),
Some(ExportInfoProvided::True)
)) {
continue;
};

let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey);
let info_name = export_info.name(&module_graph).expect("should have name");
let used_name = export_info
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ it("modern-module-dynamic-import-runtime", () => {
expect(initialChunk).toContain('import * as __WEBPACK_EXTERNAL_MODULE_angular_alias__ from "angular-alias"');
expect(initialChunk).toContain('const reactNs = await import("react-alias")');
expect(initialChunk).toContain('const vueNs = await import("vue-alias")');
expect(initialChunk).toContain('const jqueryNs = await import("jquery-alias", { with: {"type":"url"} })');

expect(asyncChunk).toContain('const litNs = await import("lit-alias")');
expect(asyncChunk).toContain('const solidNs = await import("solid-alias")');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export const main = async () => {
const dyn = await import('./dyn.js') // lazy dynamic import
const reactNs = await import('react') // 'module' + 'import' externalized
const vueNs = await import('vue') // 'import' externalized
console.log(angular, react, reactNs, vueNs, dyn)
const jqueryNs = await import('jquery', { with: { type: 'url' } }) // import attributes should be preserved
console.log(angular, react, reactNs, vueNs, dyn, jqueryNs)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = [{
svelte: 'svelte-alias',
lit: 'lit-alias',
solid: 'solid-alias',
jquery: 'jquery-alias',
},
externalsType: 'module-import',
experiments: {
Expand Down
118 changes: 62 additions & 56 deletions tests/webpack-test/ConfigTestCases.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ const describeCases = config => {
}
if (esmMode === "unlinked") return esm;
return (async () => {
if (esmMode === "unlinked") return esm;
await esm.link(
async (specifier, referencingModule) => {
return await asModule(
Expand Down Expand Up @@ -597,64 +598,69 @@ const describeCases = config => {
? ns.default
: ns;
})();
} else {
if (p in requireCache) {
return requireCache[p].exports;
}
const m = {
exports: {}
};
requireCache[p] = m;
const moduleScope = {
...baseModuleScope,
require: _require.bind(
null,
path.dirname(p),
options
),
importScripts: url => {
expect(url).toMatch(
/^https:\/\/test\.cases\/path\//
);
_require(
outputDirectory,
options,
`.${url.slice(
"https://test.cases/path".length
)}`
);
},
module: m,
exports: m.exports,
__dirname: path.dirname(p),
__filename: p,
_globalAssign: { expect }
};
if (testConfig.moduleScope) {
testConfig.moduleScope(moduleScope);
}
if (!runInNewContext)
content = `Object.assign(global, _globalAssign); ${content}`;
const args = Object.keys(moduleScope);
const argValues = args.map(arg => moduleScope[arg]);
const code = `(function(${args.join(
", "
)}) {${content}\n})`;
}
const isJSON = p.endsWith(".json");
if (isJSON) {
return JSON.parse(content);
}

let oldCurrentScript = document.currentScript;
document.currentScript = new CurrentScript(subPath);
const fn = runInNewContext
? vm.runInNewContext(code, globalContext, p)
: vm.runInThisContext(code, p);
fn.call(
testConfig.nonEsmThis
? testConfig.nonEsmThis(module)
: m.exports,
...argValues
);
document.currentScript = oldCurrentScript;
return m.exports;
if (p in requireCache) {
return requireCache[p].exports;
}
const m = {
exports: {}
};
requireCache[p] = m;

const moduleScope = {
...baseModuleScope,
require: _require.bind(
null,
path.dirname(p),
options
),
importScripts: url => {
expect(url).toMatch(
/^https:\/\/test\.cases\/path\//
);
_require(
outputDirectory,
options,
`.${url.slice(
"https://test.cases/path".length
)}`
);
},
module: m,
exports: m.exports,
__dirname: path.dirname(p),
__filename: p,
_globalAssign: { expect }
};
if (testConfig.moduleScope) {
testConfig.moduleScope(moduleScope);
}
if (!runInNewContext)
content = `Object.assign(global, _globalAssign); ${content}`;
const args = Object.keys(moduleScope);
const argValues = args.map(arg => moduleScope[arg]);
const code = `(function(${args.join(
", "
)}) {${content}\n})`;

let oldCurrentScript = document.currentScript;
document.currentScript = new CurrentScript(subPath);
const fn = runInNewContext
? vm.runInNewContext(code, globalContext, p)
: vm.runInThisContext(code, p);
fn.call(
testConfig.nonEsmThis
? testConfig.nonEsmThis(module)
: m.exports,
...argValues
);
document.currentScript = oldCurrentScript;
return m.exports;
} else if (
testConfig.modules &&
module in testConfig.modules
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@

// TODO: Should create a issue for this test
// TODO: _painic
module.exports = () => {
// return /^v(2[2-9])/.test(process.version);
return false;
};
module.exports = () => /^v(2[2-9])/.test(process.version);
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ module.exports = {
"./eager.json": "import ./eager.json",
"./weak.json": "import ./weak.json",
"./pkg.json": "import ./pkg.json",
"./pkg": "import ./pkg",
"./pkg": "import ./pkg.json",
"./re-export.json": "module ./re-export.json",
"./re-export-directly.json": "module ./re-export-directly.json"
}
Expand Down
3 changes: 3 additions & 0 deletions tests/webpack-test/helpers/pkg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"foo": "bar"
}

2 comments on commit 49a9974

@rspack-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Ran ecosystem CI: Open

suite result
modernjs ✅ success
_selftest ✅ success
rspress ✅ success
rslib ✅ success
rsbuild ❌ failure
examples ✅ success
devserver ✅ success

@rspack-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Benchmark detail: Open

Name Base (2024-11-15 b8db757) Current Change
10000_big_production-mode + exec 43.9 s ± 1.16 s 44.4 s ± 1.18 s +1.22 %
10000_development-mode + exec 1.82 s ± 41 ms 1.78 s ± 25 ms -2.11 %
10000_development-mode_hmr + exec 646 ms ± 16 ms 641 ms ± 3.5 ms -0.75 %
10000_production-mode + exec 2.41 s ± 31 ms 2.43 s ± 30 ms +0.60 %
arco-pro_development-mode + exec 1.79 s ± 77 ms 1.78 s ± 66 ms -0.31 %
arco-pro_development-mode_hmr + exec 430 ms ± 1.2 ms 430 ms ± 0.87 ms +0.01 %
arco-pro_production-mode + exec 3.14 s ± 86 ms 3.19 s ± 65 ms +1.59 %
arco-pro_production-mode_generate-package-json-webpack-plugin + exec 3.2 s ± 116 ms 3.21 s ± 112 ms +0.36 %
threejs_development-mode_10x + exec 1.58 s ± 16 ms 1.58 s ± 12 ms -0.16 %
threejs_development-mode_10x_hmr + exec 772 ms ± 3.5 ms 775 ms ± 8.3 ms +0.40 %
threejs_production-mode_10x + exec 4.98 s ± 37 ms 4.97 s ± 34 ms -0.13 %

Please sign in to comment.