Visitar URL original
fix: use configured external targets in ESM star reexports by bjohansebas · Pull Request #22079 · webpack/webpack · GitHub
Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/013-esm-reexport-external-target.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"webpack": patch
---

Use configured external requests in ESM star reexports; reject property paths.
11 changes: 11 additions & 0 deletions lib/ExternalModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,17 @@ class ExternalModule extends Module {
return this._resolveExternalType(this.externalType) !== "module";
}

/**
* The request this external resolves to for its own type, with the record form
* narrowed to its entry. An array is a module specifier plus a property path.
* @returns {string | string[] | undefined} the resolved request
*/
getResolvedRequest() {
return /** @type {string | string[] | undefined} */ (
this._getRequestAndExternalType().request
);
}

/**
* Get request and external type.
* @private
Expand Down
44 changes: 44 additions & 0 deletions lib/errors/ExternalStarReexportError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Alexander Akait @alexander-akait
*/

"use strict";

const WebpackError = require("./WebpackError");

/** @import ExternalModule from "../ExternalModule" */
/** @import Module from "../Module" */

/**
* Error raised when a `module` library reexports everything from an external
* whose request names a property path instead of a module.
*/
class ExternalStarReexportError extends WebpackError {
/**
* Captures the external and the library entry reexporting from it.
* @param {ExternalModule} externalModule the reexported external
* @param {Module} module the library entry module
*/
constructor(externalModule, module) {
const request = /** @type {string[]} */ (
externalModule.getResolvedRequest()
);
super(
`Can't 'export * from ${JSON.stringify(
externalModule.userRequest
)}': the external is configured as ${JSON.stringify(
request
)}, which is a property of ${JSON.stringify(
request[0]
)} and has no module specifier to reexport. Reexport the names explicitly or configure the external without a property path.`
);

/** @type {string} */
this.name = "ExternalStarReexportError";
/** @type {Module} */
this.module = module;
}
}

module.exports = ExternalStarReexportError;
3 changes: 1 addition & 2 deletions lib/html/HtmlModulesPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,10 @@ const getCssModulesPlugin = memoize(() => require("../css/CssModulesPlugin"));
/** @typedef {import("../../declarations/WebpackOptions").OutputHtmlOptions["favicon"]} FaviconOption */
/** @typedef {import("../../declarations/WebpackOptions").OutputHtmlOptions["manifest"]} ManifestOption */
/** @typedef {Exclude<HtmlFaviconIcon, string>} FaviconIcon */
/** @import Chunk from "../Chunk" */
/** @import Chunk, { ChunkFilenameTemplate } from "../Chunk" */
/** @import ChunkGraph from "../ChunkGraph" */
/** @import Compiler from "../Compiler" */
/** @import Module from "../Module" */
/** @import { ChunkFilenameTemplate } from "../Chunk" */
/** @typedef {import("../Compilation").EntryData} EntryData */
/** @import { HtmlModuleBuildInfo } from "./HtmlModule" */
/** @import HtmlParser from "./HtmlParser" */
Expand Down
66 changes: 65 additions & 1 deletion lib/library/ModuleLibraryPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const Template = require("../Template");
const HarmonyExportExpressionDependency = require("../dependencies/HarmonyExportExpressionDependency");
const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency");
const HarmonyExportSpecifierDependency = require("../dependencies/HarmonyExportSpecifierDependency");
const ExternalStarReexportError = require("../errors/ExternalStarReexportError");
const JavascriptModulesPlugin = require("../javascript/JavascriptModulesPlugin");
const ConcatenatedModule = require("../optimize/ConcatenatedModule");
const { InlinedUsedName } = require("../optimize/InlineExports");
Expand Down Expand Up @@ -368,6 +369,7 @@ class ModuleLibraryPlugin extends AbstractLibraryPlugin {
}
}
}
this._checkStarReexportedExternals(module, compilation);
moduleGraph.addExtraReason(module, "used as library export");
let entries = libraryEntryModules.get(compilation);
if (entries === undefined) {
Expand All @@ -377,6 +379,61 @@ class ModuleLibraryPlugin extends AbstractLibraryPlugin {
entries.add(module);
}

/**
* Reports every external the entry star-reexports whose request names a property
* path, which `export *` has no way to reference.
* @param {Module} module the library entry module
* @param {Compilation} compilation the compilation
* @returns {void}
*/
_checkStarReexportedExternals(module, compilation) {
const { moduleGraph } = compilation;
/** @type {Set<Module>} */
const visitedReexportModules = new Set([module]);
/** @type {Module[]} */
const queue = [module];
/** @type {Set<ExternalModule>} */
const unsupportedExternals = new Set();

for (const current of queue) {
for (const connection of moduleGraph.getOutgoingConnections(current)) {
const dep = connection.dependency;

// Only handle star-reexport statement
if (
!(dep instanceof HarmonyExportImportedSpecifierDependency) ||
dep.name !== null
) {
continue;
}
const importedModule = connection.resolvedModule;

// Only a module providing unknown exports keeps its reexport rendered
if (
moduleGraph.getExportsInfo(importedModule)._otherExportsInfo
.provided !== null
) {
continue;
}
if (importedModule instanceof ExternalModule) {
const request = importedModule.getResolvedRequest();
if (Array.isArray(request) && request.length > 1) {
unsupportedExternals.add(importedModule);
}
} else if (!visitedReexportModules.has(importedModule)) {
visitedReexportModules.add(importedModule);
queue.push(importedModule);
}
}
}

for (const externalModule of unsupportedExternals) {
compilation.errors.push(
new ExternalStarReexportError(externalModule, module)
);
}
}

/**
* Returns preprocess as needed by overriding.
* @param {LibraryOptions} library normalized library option
Expand Down Expand Up @@ -449,7 +506,14 @@ class ModuleLibraryPlugin extends AbstractLibraryPlugin {
) {
// Handle export * from 'external'
if (importedModule instanceof ExternalModule) {
moduleRequests.add(importedModule.userRequest);
const request = importedModule.getResolvedRequest();
// A property path is reported by `_checkStarReexportedExternals`,
// and an absent one by the external's own build
if (typeof request === "string") {
moduleRequests.add(request);
} else if (Array.isArray(request) && request.length === 1) {
moduleRequests.add(request[0]);
}
} else if (!visitedReexportModules.has(importedModule)) {
visitedReexportModules.add(importedModule);
resolveDynamicStarReexport(importedModule, true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use strict";

module.exports = [
[
/Can't 'export \* from "property-path"'/,
/configured as \["nested-module","inner"\]/
]
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as library from "./library.mjs";

it("should reexport the external without a property path", () => {
expect(library.value).toBe(1);
expect(Object.keys(library)).toEqual(["value"]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "property-path";
export * from "plain-alias";
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use strict";

module.exports = {
/**
* @returns {string} emitted test bundle
*/
findBundle() {
return "main.mjs";
},
modules: {
"actual-module": { value: 1 },
"nested-module": { inner: { innerValue: 2 } }
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use strict";

/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
entry: {
main: "./index.js",
library: { import: "./library.js", library: { type: "module" } }
},
output: { module: true, filename: "[name].mjs" },
externalsType: "module",
externals: {
"./library.mjs": "./library.mjs",
"plain-alias": "actual-module",
"property-path": ["nested-module", "inner"]
},
optimization: { minimize: false }
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as library from "./library.mjs";

it("should reexport the configured external targets instead of their aliases", () => {
expect(library.value).toBe(1);
expect(library.arrayValue).toBe(2);
expect(library.recordValue).toBe(3);
expect(Object.keys(library)).toEqual(["arrayValue", "recordValue", "value"]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "alias";
export * from "alias-again";
export * from "array-alias";
export * from "record-alias";
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use strict";

module.exports = {
/**
* @param {number} index configuration index
* @returns {string} emitted test bundle
*/
findBundle(index) {
return `main${index}.mjs`;
},
modules: {
"actual-module": { value: 1 },
"actual-array-module": { arrayValue: 2 },
"actual-record-module": { recordValue: 3 }
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use strict";

/** @type {import("../../../../").Configuration[]} */
module.exports = [
{ type: "module", minimize: false },
{ type: "module", minimize: true },
{ type: "modern-module", minimize: false },
{ type: "modern-module", minimize: true }
].map(({ type, minimize }, index) => ({
mode: minimize ? "production" : "development",
entry: {
main: "./index.js",
library: { import: "./library.js", library: { type } }
},
output: { module: true, filename: `[name]${index}.mjs` },
externalsType: "module",
externals: {
"./library.mjs": `./library${index}.mjs`,
alias: "actual-module",
"alias-again": "actual-module",
"array-alias": ["actual-array-module"],
"record-alias": {
module: "actual-record-module",
commonjs: "unused-commonjs-module"
}
},
optimization: { minimize }
}));
6 changes: 6 additions & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9001,6 +9001,12 @@ declare class ExternalModule extends Module {
normalModuleFactory: NormalModuleFactory
): void;
canBeWrappedInConcatenation(): boolean;

/**
* The request this external resolves to for its own type, with the record form
* narrowed to its entry. An array is a module specifier plus a property path.
*/
getResolvedRequest(): undefined | string | string[];
static getCompilationHooks: (compilation: Compilation) => {
/**
* @since 5.106.0
Expand Down
Loading