Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 8 additions & 48 deletions crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::{
sync::{Arc, LazyLock, OnceLock},
};

use camino::Utf8Path;
use regex::Regex;
use rspack_cacheable::cacheable;
use rspack_core::{
Expand All @@ -14,14 +13,12 @@ use rspack_core::{
RuntimeGlobals, RuntimeModule,
};
use rspack_error::{Diagnostic, Result, error};
use rspack_fs::ReadableFileSystem;
use rspack_hook::{plugin, plugin_hook};
use rspack_util::fx_hash::FxHashSet;
use rustc_hash::FxHashMap;

use super::{
consume_shared_module::ConsumeSharedModule,
consume_shared_runtime_module::ConsumeSharedRuntimeModule,
consume_shared_runtime_module::ConsumeSharedRuntimeModule, get_description_file,
};
use crate::ShareScope;

Expand Down Expand Up @@ -106,41 +103,8 @@ pub async fn resolve_matched_configs(
}
}

pub async fn get_description_file(
fs: Arc<dyn ReadableFileSystem>,
mut dir: &Utf8Path,
satisfies_description_file_data: Option<impl Fn(Option<serde_json::Value>) -> bool>,
) -> (Option<serde_json::Value>, Option<Vec<String>>) {
let description_filename = "package.json";
let mut checked_file_paths = FxHashSet::default();

loop {
let description_file = dir.join(description_filename);

let data = fs.read(&description_file).await;

if let Ok(data) = data
&& let Ok(data) = serde_json::from_slice::<serde_json::Value>(&data)
{
if satisfies_description_file_data
.as_ref()
.is_some_and(|f| !f(Some(data.clone())))
{
checked_file_paths.insert(description_file.to_string());
} else {
return (Some(data), None);
}
}
if let Some(parent) = dir.parent() {
dir = parent;
} else {
return (None, Some(checked_file_paths.into_iter().collect()));
}
}
}

pub fn get_required_version_from_description_file(
data: serde_json::Value,
data: &serde_json::Value,
package_name: &str,
) -> Option<ConsumeVersion> {
let data = data.as_object()?;
Expand Down Expand Up @@ -269,15 +233,11 @@ impl ConsumeSharedPlugin {
let (data, checked_description_file_paths) = get_description_file(
fs,
context.as_path(),
Some(|data: Option<serde_json::Value>| {
if let Some(data) = data {
let name_matches = data.get("name").and_then(|n| n.as_str()) == Some(package_name);
let version_matches = get_required_version_from_description_file(data, package_name)
.is_some_and(|version| matches!(version, ConsumeVersion::Version(_)));
name_matches || version_matches
} else {
false
}
Some(|data: &serde_json::Value| {
let name_matches = data.get("name").and_then(|n| n.as_str()) == Some(package_name);
let version_matches = get_required_version_from_description_file(data, package_name)
.is_some_and(|version| matches!(version, ConsumeVersion::Version(_)));
name_matches || version_matches
}),
)
.await;
Expand All @@ -289,7 +249,7 @@ impl ConsumeSharedPlugin {
// Package self-referencing
return None;
}
return get_required_version_from_description_file(data, package_name);
return get_required_version_from_description_file(&data, package_name);
} else {
if let Some(file_paths) = checked_description_file_paths
&& !file_paths.is_empty()
Expand Down
84 changes: 84 additions & 0 deletions crates/rspack_plugin_mf/src/sharing/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};

use camino::{Utf8Path, Utf8PathBuf};
use rspack_fs::ReadableFileSystem;
use rustc_hash::FxHashSet;

pub mod collect_shared_entry_plugin;
pub mod consume_shared_fallback_dependency;
pub mod consume_shared_module;
Expand All @@ -14,3 +23,78 @@ pub mod shared_container_plugin;
pub mod shared_container_runtime_module;
pub mod shared_used_exports_optimizer_plugin;
pub mod shared_used_exports_optimizer_runtime_module;

const DESCRIPTION_FILE_NAME: &str = "package.json";

fn is_node_modules_dir(dir: &Path) -> bool {
dir.file_name().is_some_and(|name| name == "node_modules")
}

fn collect_description_file_paths(mut dir: &Path) -> Vec<PathBuf> {
let mut description_file_paths = Vec::new();

loop {
if is_node_modules_dir(dir) {
break;
}

description_file_paths.push(dir.join(DESCRIPTION_FILE_NAME));

if let Some(parent) = dir.parent() {
dir = parent;
} else {
break;
}
}

description_file_paths
}

fn find_ancestor_description_data<T>(
start_dir: &Path,
mut matcher: impl FnMut(&Path, &serde_json::Value) -> Option<T>,
) -> Option<T> {
for description_file in collect_description_file_paths(start_dir) {
if let Ok(data) = std::fs::read(&description_file)
&& let Ok(data) = serde_json::from_slice::<serde_json::Value>(&data)
&& let Some(dir) = description_file.parent()
&& let Some(value) = matcher(dir, &data)
{
return Some(value);
}
}

None
}

async fn get_description_file(
fs: Arc<dyn ReadableFileSystem>,
dir: &Utf8Path,
satisfies_description_file_data: Option<impl Fn(&serde_json::Value) -> bool>,
) -> (Option<serde_json::Value>, Option<Vec<String>>) {
let mut checked_file_paths = FxHashSet::default();

for description_file in collect_description_file_paths(dir.as_std_path()) {
let description_file = Utf8PathBuf::from_path_buf(description_file)
.expect("description file path should remain utf8");
let data = fs.read(&description_file).await;

if let Ok(data) = data
&& let Ok(data) = serde_json::from_slice::<serde_json::Value>(&data)
{
if satisfies_description_file_data
.as_ref()
.is_some_and(|f| !f(&data))
{
checked_file_paths.insert(description_file.to_string());
} else {
return (Some(data), None);
}
}
}

let mut checked_file_paths = checked_file_paths.into_iter().collect::<Vec<_>>();
checked_file_paths.sort_unstable();

(None, Some(checked_file_paths))
}
46 changes: 40 additions & 6 deletions crates/rspack_plugin_mf/src/sharing/provide_shared_plugin.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{
fmt,
path::Path,
sync::{Arc, LazyLock},
};

Expand All @@ -16,7 +17,7 @@ use rustc_hash::FxHashMap;
use tokio::sync::RwLock;

use super::{
provide_shared_dependency::ProvideSharedDependency,
find_ancestor_description_data, provide_shared_dependency::ProvideSharedDependency,
provide_shared_module_factory::ProvideSharedModuleFactory,
};
use crate::{ConsumeVersion, ShareScope};
Expand Down Expand Up @@ -101,6 +102,34 @@ impl ProvideSharedPlugin {
)
}

/// For secondary entry points (e.g. `@mui/material/styles`) whose own
/// `package.json` has no `version`, walk up to the parent package and use
/// its version — but only when the shared key matches
/// `<parent_name>/<relative_path>`.
fn find_parent_package_version(description_path: &Path, share_key: &str) -> Option<String> {
let entry_dir = if description_path
.file_name()
.is_some_and(|name| name == "package.json")
{
description_path.parent()?
} else {
description_path
};

find_ancestor_description_data(entry_dir, |dir, parent| {
let parent_name = parent.get("name").and_then(|n| n.as_str())?;
let parent_version = parent.get("version").and_then(|v| v.as_str())?;
let rel = entry_dir.strip_prefix(dir).ok()?;
let rel_posix: String = rel
.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/");
let expected_key = format!("{parent_name}/{rel_posix}");
(share_key == expected_key).then(|| parent_version.to_string())
})
}

#[allow(clippy::too_many_arguments)]
pub async fn provide_shared_module(
&self,
Expand Down Expand Up @@ -134,16 +163,21 @@ impl ProvideSharedPlugin {
},
);
} else if let Some(description) = resource_data.description() {
if let Some(description) = description.json().as_object()
&& let Some(version) = description.get("version")
&& let Some(version) = version.as_str()
{
let version = description
.json()
.as_object()
.and_then(|d| d.get("version"))
.and_then(|v| v.as_str())
.map(|v| v.to_string())
.or_else(|| Self::find_parent_package_version(description.path(), share_key));

if let Some(version) = version {
self.resolved_provide_map.write().await.insert(
resource.to_string(),
VersionedProvideOptions {
share_key: share_key.to_string(),
share_scope: share_scope.clone(),
version: ProvideVersion::Version(version.to_string()),
version: ProvideVersion::Version(version),
eager,
singleton,
strict_version,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
it("should provide a secondary entry point using the parent package version", async () => {
const styles = await import("@scope/pkg/styles");
expect(styles).toEqual(
expect.objectContaining({
default: "pkg-styles"
})
);

await __webpack_init_sharing__("default");
expect(Object.keys(__webpack_share_scopes__.default)).toContain("@scope/pkg/styles");
expect(Object.keys(__webpack_share_scopes__.default["@scope/pkg/styles"])).toContain("1.2.3");
});

it("should provide the root package normally", async () => {
const pkg = await import("@scope/pkg");
expect(pkg).toEqual(
expect.objectContaining({
default: "pkg-root"
})
);
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"@scope/pkg": "^1.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// eslint-disable-next-line node/no-unpublished-require
const { SharePlugin } = require('@rspack/core').sharing;

/** @type {import("@rspack/core").Configuration} */
module.exports = {
mode: 'development',
devtool: false,
plugins: [
new SharePlugin({
shared: {
'@scope/pkg': {},
'@scope/pkg/styles': {},
},
}),
],
};
Loading