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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/webpack5-module-minifier-plugin",
"comment": "Add an opt-in `inlineShortLicenses` option to inline complete license comments when they are smaller than the companion-file reference banner, and preserve license banners for chunks containing only inlined modules.",
"type": "minor"
}
],
"packageName": "@rushstack/webpack5-module-minifier-plugin"
}
3 changes: 2 additions & 1 deletion common/reviews/api/webpack5-module-minifier-plugin.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const CHUNK_MODULE_REGEX: RegExp;
export const CHUNK_MODULE_TOKEN: '__WEBPACK_CHUNK_MODULE__';

// @public
export function generateLicenseFileForAsset(compilation: Compilation, asset: IAssetInfo): string;
export function generateLicenseFileForAsset(compilation: Compilation, asset: IAssetInfo, inlineShortLicenses?: boolean): string;

// @public
export interface IAssetInfo {
Expand Down Expand Up @@ -75,6 +75,7 @@ export interface IModuleMinifierPluginHooks {

// @public
export interface IModuleMinifierPluginOptions {
inlineShortLicenses?: boolean;
minifier: IModuleMinifier;
sourceMap?: boolean;
}
Expand Down
20 changes: 20 additions & 0 deletions webpack/webpack5-module-minifier-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ optimization: {
}
```

## License comments

By default, the plugin extracts license comments into a companion `.LICENSE.txt`
asset and adds a reference banner to the JavaScript chunk.

Set `inlineShortLicenses: true` in the `ModuleMinifierPlugin` options to inline
the complete license text when it is smaller than that banner. The comparison
uses UTF-8 byte lengths after deduplicating all license comments in the chunk.
Equal-size and larger notices remain in companion assets.

```js
new ModuleMinifierPlugin({
minifier: new LocalMinifier(),
inlineShortLicenses: true
});
```

The decision is made before emitting the companion asset. Inlined comments
participate in source-map generation, module-position metadata, and content hashes.

## Links

- [CHANGELOG.md](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,19 @@ function getAllComments(modules: Iterable<Module>): Set<string> {
}

/**
* Generates a companion asset containing all extracted comments. If it is non-empty, returns a banner comment directing users to said companion asset.
* Generates a companion asset containing all extracted comments and returns a banner directing users to it,
* or returns the comments themselves when short-license inlining is enabled and saves bytes.
*
* @param compilation - The webpack compilation
* @param asset - The asset to process
* @param inlineShortLicenses - Inline the complete license text when it is smaller than the reference banner
* @public
*/
export function generateLicenseFileForAsset(compilation: Compilation, asset: IAssetInfo): string {
export function generateLicenseFileForAsset(
compilation: Compilation,
asset: IAssetInfo,
inlineShortLicenses: boolean = false
): string {
// Extracted comments from the modules.
const modules: Iterable<Module> = compilation.chunkGraph.getChunkModulesIterable(asset.chunk);
const comments: Set<string> = getAllComments(modules);
Expand All @@ -50,14 +56,17 @@ export function generateLicenseFileForAsset(compilation: Compilation, asset: IAs
let banner: string = '';

if (comments.size) {
// There are license comments in this chunk, so generate the companion file and inject a banner
const licenseSource: sources.ConcatSource = new compilation.compiler.webpack.sources.ConcatSource();
comments.forEach((comment) => {
licenseSource.add(comment);
});
const licenseFileName: string = `${assetName}.LICENSE.txt`;
compilation.emitAsset(licenseFileName, licenseSource);
banner = `/*! For license information please see ${path.basename(licenseFileName)} */\n`;
if (inlineShortLicenses && licenseSource.size() < Buffer.byteLength(banner, 'utf8')) {
banner = licenseSource.source().toString();
} else {
compilation.emitAsset(licenseFileName, licenseSource);
}
}

return banner;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const PLUGIN_NAME: 'ModuleMinifierPlugin' = 'ModuleMinifierPlugin';

// Monotonically increasing identifier to be incremented any time the code generation logic changes
// Will be applied to the webpack hash.
const CODE_GENERATION_REVISION: number = 1;
const CODE_GENERATION_REVISION: number = 2;
// Match behavior of terser's "some" option
// https://github.com/terser/terser/blob/d3d924fa9e4c57bbe286b811c6068bcc7026e902/lib/output.js#L175
const LICENSE_COMMENT_REGEX: RegExp = /@preserve|@lic|@cc_on|^\**!/i;
Expand Down Expand Up @@ -93,7 +93,8 @@ function hashCodeFragment(code: string): string {
*/
function defaultRehydrateAssets(
dehydratedAssets: IDehydratedAssets,
compilation: Compilation
compilation: Compilation,
inlineShortLicenses: boolean
): IDehydratedAssets {
const { assets, modules } = dehydratedAssets;

Expand All @@ -106,7 +107,8 @@ function defaultRehydrateAssets(

// Now assets/modules contain fully minified code. Rehydrate.
for (const [assetName, info] of assets) {
const banner: string = info.type === 'javascript' ? generateLicenseFileForAsset(compilation, info) : '';
const banner: string =
info.type === 'javascript' ? generateLicenseFileForAsset(compilation, info, inlineShortLicenses) : '';

const replacementSource: sources.Source = rehydrateAsset(compilation, info, modules, banner, true);
metadataByAssetFileName.set(assetName, {
Expand Down Expand Up @@ -188,7 +190,7 @@ export class ModuleMinifierPlugin implements WebpackPluginInstance {
postProcessCodeFragment: new SyncWaterfallHook(['code', 'context'])
};

const { minifier, sourceMap } = options;
const { minifier, sourceMap, inlineShortLicenses = false } = options;

this.#optionsForHash = {
...options,
Expand All @@ -198,7 +200,11 @@ export class ModuleMinifierPlugin implements WebpackPluginInstance {

this.#enhancers = [];

this.hooks.rehydrateAssets.tap(PLUGIN_NAME, defaultRehydrateAssets);
this.hooks.rehydrateAssets.tap(
PLUGIN_NAME,
(dehydratedAssets: IDehydratedAssets, compilation: Compilation): IDehydratedAssets =>
defaultRehydrateAssets(dehydratedAssets, compilation, inlineShortLicenses)
);
this.minifier = minifier;

this.#sourceMap = sourceMap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ export interface IModuleMinifierPluginOptions {
* Set to `false` for faster builds at the expense of debuggability.
*/
sourceMap?: boolean;

/**
* Inline a chunk's deduplicated license comments instead of emitting a companion `.LICENSE.txt` asset
* when the complete license text is smaller than the reference banner, measured in UTF-8 bytes.
* Equal-size and larger notices remain external.
*
* @defaultValue `false`
*/
inlineShortLicenses?: boolean;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ export function rehydrateAsset(
} = compilation.compiler;

const assetCode: string = assetSource.source().toString();
const { CachedSource, ConcatSource, ReplaceSource } = sources;

const tokenIndex: number = assetCode.indexOf(CHUNK_MODULE_TOKEN);
if (tokenIndex < 0) {
// This is not a JS asset.
return assetSource;
// Chunks containing only inlined modules still need their license banner.
return banner ? new CachedSource(new ConcatSource(banner, assetSource)) : assetSource;
}

const { CachedSource, ConcatSource, ReplaceSource } = sources;

CHUNK_MODULE_REGEX.lastIndex = -1;
let lastStart: number = 0;

Expand Down
Loading
Loading