Skip to content

Commit 49cebac

Browse files
fix: skip missing exports behind a named re-export (#22144)
* fix: skip missing exports behind a named re-export * fix: check the whole target path of a re-export for a missing export A `getTarget` path keeps every name when it ends in a property access into a provided export, so looking only at its first name emits an export the target module never provides. Claude-Session: https://claude.ai/code/session_01MLuo9ywuw2n2ymCY8YcqTA * docs: shorten the re-export guard comment to two lines Claude-Session: https://claude.ai/code/session_01MLuo9ywuw2n2ymCY8YcqTA --------- Co-authored-by: alexander-akait <[email protected]>
1 parent a588538 commit 49cebac

11 files changed

Lines changed: 92 additions & 32 deletions

File tree

‎.changeset/190-on-demand-exports-codegen-data.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"webpack": patch
33
---
44

5-
Fix loss of export definitions in the module library under cache.
5+
Fix module library exports lost under cache or missing behind a re-export.

‎lib/ExportsInfo.js‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1671,7 +1671,9 @@ class ExportInfo {
16711671
}
16721672

16731673
/**
1674-
* Returns the target, undefined when there is no target, false when no target is valid.
1674+
* Walks the reexport chain and returns the first target whose module passes
1675+
* `validTargetModuleFilter`. Returns undefined when there is no target, false
1676+
* when the chain ends without a valid module.
16751677
* @param {ModuleGraph} moduleGraph the module graph
16761678
* @param {ValidTargetModuleFilter} validTargetModuleFilter a valid target module
16771679
* @returns {TargetItemWithoutConnection | null | undefined | false} the target, undefined when there is no target, false when no target is valid
@@ -1736,7 +1738,9 @@ class ExportInfo {
17361738
}
17371739

17381740
/**
1739-
* Returns the target.
1741+
* Resolves the reexport chain as far as `resolveTargetFilter` allows and
1742+
* returns where it ends. Returns undefined when there is no target, the
1743+
* targets disagree, or the chain is circular.
17401744
* @param {ModuleGraph} moduleGraph the module graph
17411745
* @param {ResolveTargetFilter} resolveTargetFilter filter function to further resolve target
17421746
* @returns {TargetItemWithConnection | undefined} the target

‎lib/library/ModuleLibraryPlugin.js‎

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -651,48 +651,45 @@ class ModuleLibraryPlugin extends AbstractLibraryPlugin {
651651
}
652652

653653
// Try to find all known exports of the entry module
654-
outer: for (const exportInfo of exportInfos) {
654+
for (const exportInfo of exportInfos) {
655655
if (!exportInfo.provided) continue;
656656

657657
const originalName = exportInfo.name;
658658
// Skip rendering the default export in some cases
659659
if (skipRenderDefaultExport && originalName === "default") continue;
660660

661661
// Try to find all exports from the reexported modules
662-
const target = exportInfo.findTarget(moduleGraph, (_m) => true);
663-
// A namespace object target (`export * as ns from "./module"`) has no
664-
// export name, so there is nothing that could be missing
665-
if (target && target.export) {
666-
const targetExport = target.export[0];
667-
const reexportsInfo = moduleGraph.getExportsInfo(target.module);
668-
for (const reexportInfo of reexportsInfo.orderedExports) {
669-
if (
670-
reexportInfo.provided === false &&
671-
reexportInfo.name !== "default" &&
672-
reexportInfo.name === targetExport
673-
) {
674-
continue outer;
675-
}
676-
}
662+
const target = exportInfo.getTarget(moduleGraph);
663+
664+
// A re-export is provided even when its target lacks the export, and
665+
// the path can end in a property access, so every name is looked up.
666+
if (
667+
target &&
668+
target.export &&
669+
target.export[target.export.length - 1] !== "default" &&
670+
moduleGraph
671+
.getExportsInfo(target.module)
672+
.isExportProvided(target.export) === false
673+
) {
674+
continue;
677675
}
678676

679-
const externalTarget = exportInfo.getTarget(moduleGraph);
680677
if (
681-
externalTarget &&
682-
externalTarget.module instanceof ExternalModule &&
683-
externalTarget.export &&
684-
externalTarget.export.length === 1 &&
685-
codeGenerationResults.has(externalTarget.module, chunk.runtime)
678+
target &&
679+
target.module instanceof ExternalModule &&
680+
target.export &&
681+
target.export.length === 1 &&
682+
codeGenerationResults.has(target.module, chunk.runtime)
686683
) {
687684
const reexport = codeGenerationResults.getData(
688-
externalTarget.module,
685+
target.module,
689686
chunk.runtime,
690687
"externalReexport"
691688
);
692689
if (reexport) {
693690
if (!bindingsTakenOver) needExportsDeclaration = true;
694691
result.add(
695-
`export { ${toExportName(externalTarget.export[0])} as ${toExportName(originalName)} } ${reexport};\n`
692+
`export { ${toExportName(target.export[0])} as ${toExportName(originalName)} } ${reexport};\n`
696693
);
697694
alreadyRenderedExports.add(originalName);
698695
continue;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"outer": {
3+
"present": 1
4+
}
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { present } from "./mid";
2+
3+
export { present, missing } from "./mid";
4+
5+
it("should not reexport a name missing behind a property path", () => {
6+
expect(present).toBe(1);
7+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
exports.present = require("./data.json").outer.present;
2+
exports.missing = require("./data.json").outer.missing;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use strict";
2+
3+
/** @import { Compilation } from "../../../../" */
4+
5+
/** @type {import("../../../../").Configuration} */
6+
module.exports = {
7+
mode: "none",
8+
entry: { main: "./index.js" },
9+
output: {
10+
module: true,
11+
library: {
12+
type: "module"
13+
},
14+
chunkFormat: "module"
15+
},
16+
optimization: {
17+
concatenateModules: true
18+
},
19+
plugins: [
20+
function apply() {
21+
/**
22+
* @param {Compilation} compilation compilation
23+
*/
24+
const handler = (compilation) => {
25+
compilation.hooks.afterProcessAssets.tap("testcase", (assets) => {
26+
const source = assets["bundle0.mjs"].source();
27+
expect(source).toMatch(/export \{[^}]*\bas present\b/);
28+
// `missing` sits behind a property path into a provided export,
29+
// so only the last name of the path says it is not there
30+
expect(source).not.toMatch(/export \{[^}]*\bas missing\b/);
31+
});
32+
};
33+
this.hooks.compilation.tap("testcase", handler);
34+
}
35+
]
36+
};

‎test/configCases/library/module-reexport-type/index.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { value, T } from './re-export'
2+
import { NamedT } from './re-export-named'
23
import logo from './file.png';
34

45
type MyType = string;
56

6-
export { logo, value, T, MyType }
7+
export { logo, value, T, NamedT, MyType }
78

89
it("should not reexport type", function () {
910
expect(value).toBe(1)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { T as NamedT } from './export'

‎test/configCases/library/module-reexport-type/webpack.config.js‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ module.exports = {
99
ignoreWarnings: [
1010
(warning) => {
1111
// when using swc-loader or `transpileOnly: true` with ts-loader, the warning is expected
12-
expect(warning.message).toContain(
13-
"export 'T' (reexported as 'T') was not found in './re-export' (possible exports: value)"
12+
expect(warning.message).toMatch(
13+
/export 'T' \(reexported as '(T|NamedT)'\) was not found in '\.\/(re-export|export)' \(possible exports: value\)/
1414
);
1515
return true;
1616
}
@@ -54,6 +54,9 @@ module.exports = {
5454
expect(source).toContain(
5555
"export { file_namespaceObject as logo, value };"
5656
);
57+
// `NamedT` crosses a named re-export, which is marked provided itself:
58+
// only the end of the chain says the erased type is missing
59+
expect(source).not.toMatch(/^export .*\b(T|NamedT)\b/m);
5760
});
5861
};
5962
this.hooks.compilation.tap("testcase", handler);

0 commit comments

Comments
 (0)