Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tree shakable output for module library #18272

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/WebpackOptionsApply.js
Expand Up @@ -286,6 +286,11 @@ class WebpackOptionsApply extends OptionsApply {
"library type \"module\" is only allowed when 'experiments.outputModule' is enabled"
);
}
if (options.output.enabledLibraryTypes.includes("modern-module")) {
throw new Error(
"library type \"modern-module\" is only allowed when 'experiments.outputModule' is enabled"
);
}
if (options.externalsType === "module") {
throw new Error(
"'externalsType: \"module\"' is only allowed when 'experiments.outputModule' is enabled"
Expand Down
8 changes: 8 additions & 0 deletions lib/library/EnableLibraryPlugin.js
Expand Up @@ -238,6 +238,14 @@ class EnableLibraryPlugin {
}).apply(compiler);
break;
}
case "modern-module": {
enableExportProperty();
const ModernModuleLibraryPlugin = require("./ModernModuleLibraryPlugin");
new ModernModuleLibraryPlugin({
type
}).apply(compiler);
break;
}
default:
throw new Error(`Unsupported library type ${type}.
Plugins which provide custom library types must call EnableLibraryPlugin.setEnabled(compiler, type) to disable this error.`);
Expand Down
138 changes: 138 additions & 0 deletions lib/library/ModernModuleLibraryPlugin.js
@@ -0,0 +1,138 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/

"use strict";

const { ConcatSource } = require("webpack-sources");
const ConcatenatedModule = require("../optimize/ConcatenatedModule");
const AbstractLibraryPlugin = require("./AbstractLibraryPlugin");

/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../../declarations/WebpackOptions").LibraryOptions} LibraryOptions */
/** @typedef {import("../../declarations/WebpackOptions").LibraryType} LibraryType */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Module")} Module */
/** @typedef {import("../javascript/JavascriptModulesPlugin").StartupRenderContext} StartupRenderContext */
/** @template T @typedef {import("./AbstractLibraryPlugin").LibraryContext<T>} LibraryContext<T> */

/**
* @typedef {Object} ModernModuleLibraryPluginOptions
* @property {LibraryType} type
*/

/**
* @typedef {Object} ModernModuleLibraryPluginParsed
* @property {string} name
*/

/**
* @typedef {ModernModuleLibraryPluginParsed} T
* @extends {AbstractLibraryPlugin<ModernModuleLibraryPluginParsed>}
*/
class ModernModuleLibraryPlugin extends AbstractLibraryPlugin {
/**
* Apply the plugin
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
super.apply(compiler);

compiler.hooks.compilation.tap("ModernModuleLibraryPlugin", compilation => {
const { exportsDefinitions } =
ConcatenatedModule.getCompilationHooks(compilation);
exportsDefinitions.tap("ModernModuleLibraryPlugin", () => {
return true;
});
});
}

/**
* @param {ModernModuleLibraryPluginOptions} options the plugin options
*/
constructor(options) {
super({
pluginName: "ModernModuleLibraryPlugin",
type: options.type
});
}

/**
* @param {LibraryOptions} library normalized library option
* @returns {T | false} preprocess as needed by overriding
*/
parseOptions(library) {
const { name } = library;
if (name) {
throw new Error(
`Library name must be unset. ${AbstractLibraryPlugin.COMMON_LIBRARY_NAME_MESSAGE}`
);
}
return {
name: /** @type {string} */ (name)
};
}

/**
* @param {Source} source source
* @param {Module} module module
* @param {StartupRenderContext} renderContext render context
* @param {LibraryContext<T>} libraryContext context
* @returns {Source} source with library export
*/
renderStartup(
source,
module,
{ moduleGraph, chunk },
{ options, compilation }
) {
const result = new ConcatSource(source);
const exportsInfo = moduleGraph.getExportsInfo(module);
const exports = [];
const definitions = module.buildMeta.exportsFinalName;

for (const exportInfo of exportsInfo.orderedExports) {
let unprovidedReexport = false;
const reexport = exportInfo.findTarget(moduleGraph, _m => true);

if (reexport) {
const exportsInfo = moduleGraph.getExportsInfo(reexport.module);
for (const reexportInfo of exportsInfo.orderedExports) {
if (
!reexportInfo.provided &&
reexport.export.includes(reexportInfo.name)
) {
unprovidedReexport = true;
}
}
}

if (unprovidedReexport) continue;

const webpackExportsProperty = exportInfo.getUsedName(
exportInfo.name,
chunk.runtime
);
const finalName =
definitions[
/** @type {string} */
(webpackExportsProperty)
];
exports.push(
finalName === exportInfo.name
? finalName
: `${finalName} as ${exportInfo.name}`
);
}

if (exports.length > 0) {
result.add(`export { ${exports.join(", ")} };\n`);
}

return result;
}
}

module.exports = ModernModuleLibraryPlugin;
82 changes: 66 additions & 16 deletions lib/optimize/ConcatenatedModule.js
Expand Up @@ -7,6 +7,7 @@

const eslintScope = require("eslint-scope");
const Referencer = require("eslint-scope/lib/referencer");
const { SyncBailHook } = require("tapable");
const {
CachedSource,
ConcatSource,
Expand Down Expand Up @@ -628,11 +629,20 @@ const addScopeSymbols = (s, nameSet, scopeSet1, scopeSet2) => {

const TYPES = new Set(["javascript"]);

/**
* @typedef {Object} ConcatenateModuleHooks
* @property {SyncBailHook<[Record<string, string>]>} exportsDefinitions
*/

/** @type {WeakMap<Compilation, ConcatenateModuleHooks>} */
const compilationHooksMap = new WeakMap();

class ConcatenatedModule extends Module {
/**
* @param {Module} rootModule the root module of the concatenation
* @param {Set<Module>} modules all modules in the concatenation (including the root module)
* @param {RuntimeSpec} runtime the runtime
* @param {Compilation} compilation the compilation
* @param {Object=} associatedObjectForCache object for caching
* @param {string | HashConstructor=} hashFunction hash function to use
* @returns {ConcatenatedModule} the module
Expand All @@ -641,6 +651,7 @@ class ConcatenatedModule extends Module {
rootModule,
modules,
runtime,
compilation,
associatedObjectForCache,
hashFunction = "md4"
) {
Expand All @@ -654,18 +665,35 @@ class ConcatenatedModule extends Module {
identifier,
rootModule,
modules,
runtime
runtime,
compilation
});
}

/**
* @param {Compilation} compilation the compilation
* @returns {ConcatenateModuleHooks} the attached hooks
*/
static getCompilationHooks(compilation) {
let hooks = compilationHooksMap.get(compilation);
if (hooks === undefined) {
hooks = {
exportsDefinitions: new SyncBailHook(["definitions"])
};
compilationHooksMap.set(compilation, hooks);
}
return hooks;
}

/**
* @param {Object} options options
* @param {string} options.identifier the identifier of the module
* @param {Module=} options.rootModule the root module of the concatenation
* @param {RuntimeSpec} options.runtime the selected runtime
* @param {Set<Module>=} options.modules all concatenated modules
* @param {Compilation} options.compilation the compilation
*/
constructor({ identifier, rootModule, modules, runtime }) {
constructor({ identifier, rootModule, modules, runtime, compilation }) {
super(JAVASCRIPT_MODULE_TYPE_ESM, null, rootModule && rootModule.layer);

// Info from Factory
Expand All @@ -677,6 +705,8 @@ class ConcatenatedModule extends Module {
this._modules = modules;
this._runtime = runtime;
this.factoryMeta = rootModule && rootModule.factoryMeta;
/** @type {Compilation | undefined} */
this.compilation = compilation;
}

/**
Expand Down Expand Up @@ -1427,6 +1457,8 @@ class ConcatenatedModule extends Module {
/** @type {BuildMeta} */
(rootInfo.module.buildMeta).strictHarmonyModule;
const exportsInfo = moduleGraph.getExportsInfo(rootInfo.module);
/** @type {Record<string, string>} */
const definitionsForHook = {};
for (const exportInfo of exportsInfo.orderedExports) {
const name = exportInfo.name;
if (exportInfo.provided === false) continue;
Expand All @@ -1451,6 +1483,7 @@ class ConcatenatedModule extends Module {
strictHarmonyModule,
true
);
definitionsForHook[used] = finalName;
return `/* ${
exportInfo.isReexport() ? "reexport" : "binding"
} */ ${finalName}`;
Expand All @@ -1466,21 +1499,20 @@ class ConcatenatedModule extends Module {
const result = new ConcatSource();

// add harmony compatibility flag (must be first because of possible circular dependencies)
let shouldAddHarmonyFlag = false;
if (
moduleGraph.getExportsInfo(this).otherExportsInfo.getUsed(runtime) !==
UsageState.Unused
) {
result.add(`// ESM COMPAT FLAG\n`);
result.add(
runtimeTemplate.defineEsModuleFlagStatement({
exportsArgument: this.exportsArgument,
runtimeRequirements
})
);
shouldAddHarmonyFlag = true;
}

// define exports
if (exportsMap.size > 0) {
const { exportsDefinitions } = ConcatenatedModule.getCompilationHooks(
this.compilation
);

runtimeRequirements.add(RuntimeGlobals.exports);
runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
const definitions = [];
Expand All @@ -1491,12 +1523,29 @@ class ConcatenatedModule extends Module {
)}`
);
}
result.add(`\n// EXPORTS\n`);
result.add(
`${RuntimeGlobals.definePropertyGetters}(${
this.exportsArgument
}, {${definitions.join(",")}\n});\n`
);
const shouldSkipRenderDefinitions =
exportsDefinitions.call(definitionsForHook);

if (shouldSkipRenderDefinitions) {
this.buildMeta.exportsFinalName = definitionsForHook;
} else {
if (shouldAddHarmonyFlag) {
result.add(`// ESM COMPAT FLAG\n`);
result.add(
runtimeTemplate.defineEsModuleFlagStatement({
exportsArgument: this.exportsArgument,
runtimeRequirements
})
);
}

result.add(`\n// EXPORTS\n`);
result.add(
`${RuntimeGlobals.definePropertyGetters}(${
this.exportsArgument
}, {${definitions.join(",")}\n});\n`
);
}
}

// list unused exports
Expand Down Expand Up @@ -1913,7 +1962,8 @@ ${defineGetters}`
identifier: undefined,
rootModule: undefined,
modules: undefined,
runtime: undefined
runtime: undefined,
compilation: undefined
});
obj.deserialize(context);
return obj;
Expand Down
2 changes: 2 additions & 0 deletions lib/optimize/ModuleConcatenationPlugin.js
Expand Up @@ -398,10 +398,12 @@ class ModuleConcatenationPlugin {
}

// Create a new ConcatenatedModule
ConcatenatedModule.getCompilationHooks(compilation);
let newModule = ConcatenatedModule.create(
rootModule,
modules,
concatConfiguration.runtime,
compilation,
compiler.root,
compilation.outputOptions.hashFunction
);
Expand Down