Skip to content

Commit d268b1a

Browse files
committed
fix(mf): resolve version from parent package for secondary entry points
When a secondary entry point (e.g. `@mui/material/styles`) has its own `package.json` without a `version` field, ProvideSharedPlugin now walks up the directory tree to find the parent package's version instead of emitting a warning. This matches the behavior expected for packages that use the secondary entry point pattern (MUI, Emotion, Apollo Client, etc). The fix validates the relationship by checking that the shared key exactly equals `<parent_name>/<relative_path>` and stops at the `node_modules` boundary. Fixes: webpack/webpack#15864 Ref: webpack/webpack#13457 Made-with: Cursor Signed-off-by: David Festal <dfestal@redhat.com>
1 parent f15f7f4 commit d268b1a

File tree

7 files changed

+92
-5
lines changed

7 files changed

+92
-5
lines changed

crates/rspack_plugin_mf/src/sharing/provide_shared_plugin.rs

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{
22
fmt,
3+
path::Path,
34
sync::{Arc, LazyLock},
45
};
56

@@ -101,6 +102,41 @@ impl ProvideSharedPlugin {
101102
)
102103
}
103104

105+
/// For secondary entry points (e.g. `@mui/material/styles`) whose own
106+
/// `package.json` has no `version`, walk up to the parent package and use
107+
/// its version — but only when the shared key matches
108+
/// `<parent_name>/<relative_path>`.
109+
fn find_parent_package_version(description_path: &Path, key: &str) -> Option<String> {
110+
let entry_dir = description_path.parent()?;
111+
let mut search_dir = entry_dir.parent();
112+
113+
while let Some(dir) = search_dir {
114+
if dir.file_name().is_some_and(|n| n == "node_modules") {
115+
break;
116+
}
117+
118+
let parent_pkg = dir.join("package.json");
119+
if parent_pkg.exists() {
120+
if let Ok(data) = std::fs::read(&parent_pkg)
121+
&& let Ok(parent) = serde_json::from_slice::<serde_json::Value>(&data)
122+
&& let Some(parent_name) = parent.get("name").and_then(|n| n.as_str())
123+
&& let Some(parent_version) = parent.get("version").and_then(|v| v.as_str())
124+
{
125+
if let Ok(rel) = entry_dir.strip_prefix(dir) {
126+
let rel_posix: String = rel.components().map(|c| c.as_os_str().to_string_lossy()).collect::<Vec<_>>().join("/");
127+
let expected_key = format!("{parent_name}/{rel_posix}");
128+
if key == expected_key {
129+
return Some(parent_version.to_string());
130+
}
131+
}
132+
}
133+
}
134+
search_dir = dir.parent();
135+
}
136+
137+
None
138+
}
139+
104140
#[allow(clippy::too_many_arguments)]
105141
pub async fn provide_shared_module(
106142
&self,
@@ -134,16 +170,21 @@ impl ProvideSharedPlugin {
134170
},
135171
);
136172
} else if let Some(description) = resource_data.description() {
137-
if let Some(description) = description.json().as_object()
138-
&& let Some(version) = description.get("version")
139-
&& let Some(version) = version.as_str()
140-
{
173+
let version = description
174+
.json()
175+
.as_object()
176+
.and_then(|d| d.get("version"))
177+
.and_then(|v| v.as_str())
178+
.map(|v| v.to_string())
179+
.or_else(|| Self::find_parent_package_version(description.path(), key));
180+
181+
if let Some(version) = version {
141182
self.resolved_provide_map.write().await.insert(
142183
resource.to_string(),
143184
VersionedProvideOptions {
144185
share_key: share_key.to_string(),
145186
share_scope: share_scope.clone(),
146-
version: ProvideVersion::Version(version.to_string()),
187+
version: ProvideVersion::Version(version),
147188
eager,
148189
singleton,
149190
strict_version,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
it("should provide a secondary entry point using the parent package version", async () => {
2+
const styles = await import("@scope/pkg/styles");
3+
expect(styles).toEqual(
4+
expect.objectContaining({
5+
default: "pkg-styles"
6+
})
7+
);
8+
9+
await __webpack_init_sharing__("default");
10+
expect(Object.keys(__webpack_share_scopes__.default)).toContain("@scope/pkg/styles");
11+
expect(Object.keys(__webpack_share_scopes__.default["@scope/pkg/styles"])).toContain("1.2.3");
12+
});
13+
14+
it("should provide the root package normally", async () => {
15+
const pkg = await import("@scope/pkg");
16+
expect(pkg).toEqual(
17+
expect.objectContaining({
18+
default: "pkg-root"
19+
})
20+
);
21+
});

tests/rspack-test/configCases/sharing/share-plugin-secondary-entry-point/node_modules/@scope/pkg/index.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/rspack-test/configCases/sharing/share-plugin-secondary-entry-point/node_modules/@scope/pkg/package.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/rspack-test/configCases/sharing/share-plugin-secondary-entry-point/node_modules/@scope/pkg/styles/index.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/rspack-test/configCases/sharing/share-plugin-secondary-entry-point/node_modules/@scope/pkg/styles/package.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// eslint-disable-next-line node/no-unpublished-require
2+
const { SharePlugin } = require("@rspack/core").sharing;
3+
4+
/** @type {import("@rspack/core").Configuration} */
5+
module.exports = {
6+
mode: "development",
7+
devtool: false,
8+
plugins: [
9+
new SharePlugin({
10+
shared: {
11+
"@scope/pkg": {},
12+
"@scope/pkg/styles": {}
13+
}
14+
})
15+
]
16+
};

0 commit comments

Comments
 (0)