Skip to content

Commit

Permalink
simplify and improve webpack patch plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
tophf committed Jan 6, 2025
1 parent ee61b9d commit 7b3128f
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 133 deletions.
238 changes: 113 additions & 125 deletions tools/wp-raw-patch-plugin.js
Original file line number Diff line number Diff line change
@@ -1,56 +1,45 @@
'use strict';

/**
* Making exports directly invocable by converting `export const` to `export function`
* and removing (0,export)() in invocations as we don't rely on `this` in general,
* except cases where we expect it to be set explicitly e.g. `sender` for messages.
* Making exports directly invocable by patch webpack guts:
* 1. remove (0,export)() in invocations as we don't rely on `this` in general,
* except cases where we expect it to be set explicitly e.g. `sender` for messages.
* 2. set the exported functions directly on the exports object as values because
* a function declaration is hoisted and initialized before execution starts in the scope,
* 3. set the exported consts directly on the exports object as values at the end of the module.
*
* Aside from a negligible performance improvement, it restores sane debugging process when
* stepping line by line via F11 key to step inside the function. Previously, it would make 3
* stepping line by line via F11 key to step inside the function. Previously, it would make 3+
* nonsensical jumps which became excrutiating when doing it on a line with several exports e.g.
* mod.foo(mod.const1, mod.const2, mod.const3, mod.const4) with webpack's default implementation
* would force you to step 15 times instead of 1.
*
* A function declaration is hoisted and initialized before execution starts in the scope,
* so we can assign it immediately to the webpack exports map without making a getter.
* Their names won't be minified/mangled thanks to `keep_fnames` in terser's options.
*
* A literal `const` is inlined, otherwise the value is remembered on the first access.
* would force you to step 15 times (or more if those consts are reexported) instead of 1.
*/

const acorn = require('acorn');
const {SourceNode} = require('source-map-js');
const webpack = require('webpack');
const RG = webpack.RuntimeGlobals;
const ReplaceSource = require('webpack-sources/lib/ReplaceSource');
const ConcatenatedModule =
require('webpack/lib/optimize/ConcatenatedModule');
const HarmonyExportInitFragment =
require('webpack/lib/dependencies/HarmonyExportInitFragment');
const HarmonyExportSpecifierDependency =
require('webpack/lib/dependencies/HarmonyExportSpecifierDependency');
const DefinePropertyGettersRuntimeModule =
require('webpack/lib/runtime/DefinePropertyGettersRuntimeModule');
const HarmonyImportSpecifierDependency =
require('webpack/lib/dependencies/HarmonyImportSpecifierDependency');
const MakeNamespaceObjectRuntimeModule =
require('webpack/lib/runtime/MakeNamespaceObjectRuntimeModule');

const rxExportArrow = /^export const (\$?\w*) = \([^)]*?\) =>/m;
/** Patching __.ABCD and (0,export)() in invocations */
const re = /\b__\.([$_A-Z][$_A-Z\d]*)\b|\(0,(\w+\.\$?\w*)\)(?=\()/g;
/** Patching __.ABCD */
const rxVar = /\b__\.([$_A-Z][$_A-Z\d]*)\b/g;
/** Patching (0,module.export) */
const rxCall = /^\(0,([$\w]+\.[$\w]+)\)$/;
const STAGE = (/**@type {typeof import('webpack/types').Compilation}*/webpack.Compilation)
.PROCESS_ASSETS_STAGE_OPTIMIZE_COMPATIBILITY;
const STATIC = '/*static:';
const NAME = __filename.slice(__dirname.length);
const NAME = __filename.slice(__dirname.length + 1).replace(/\.\w+$/, '');
const SYM = Symbol(NAME);
const CONST = 'const';
const FUNC = 'func';
const VAL = 'val';
const PATCH_EXPORTS_SRC = 'for(var key in definition) {';
const PATCH_EXPORTS = `$&
let v = definition[key];
if (typeof v == "object") {
if (v[0]) exports[key] = v[1];
else Object.defineProperty(exports, v[0] = key, {
configurable: true,
enumerable: true,
get: () => Object.defineProperty(exports, v[0], {value: v = v[1]()}) && v,
});
continue;
}`;
let exportHooked;
const toOneLine = str => str.replace(/[\r\n]\s*/g, '');

class RawEnvPlugin {
constructor(vars, raws = {}) {
Expand All @@ -73,8 +62,8 @@ class RawEnvPlugin {
const assetSource = assets[assetName];
const str = assetSource.source();
let replacer;
for (let m, val; (m = re.exec(str));) {
if ((val = m[2]) || (val = map[m[1]]) != null) {
for (let m, val; (m = rxVar.exec(str));) {
if ((val = map[m[1]]) != null) {
replacer ??= new ReplaceSource(assetSource);
replacer.replace(m.index, m.index + m[0].length - 1, val);
}
Expand All @@ -86,82 +75,30 @@ class RawEnvPlugin {
params.normalModuleFactory.hooks.parser.for('javascript/' + type)
.tap('staticExport', arrowToFuncParser);
}
exportHooked ??= hookFunc(compilation.runtimeTemplate, 'returningFunction', exportHook);
});
}
}

function arrowToFuncLoader(text) {
if (!rxExportArrow.test(text)) {
return text;
}
const source = new SourceNode(null, null, text);
const comments = [];
const ast = acorn.parse(text, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
ranges: true,
onComment: comments,
});
ast.comments = comments;
let iSrc = 0;
for (const top of ast.body) {
const td = top.declaration;
if (!td || td.kind !== CONST || top.type !== 'ExportNamedDeclaration') {
continue;
}
for (const dvar of td.declarations) {
const init = dvar.init;
if (init.type !== 'ArrowFunctionExpression') {
continue;
}
const args = init.params;
/** @type {Expression} */
const body = init.body;
const expr = body.type !== 'BlockStatement';
if (iSrc < td.start) source.add(text.slice(iSrc, td.start));
if (init.async) source.add('async ');
source.add('function ');
source.add(dvar.id.name);
source.add('(');
if (args[0]) source.add(text.slice(args[0].start, args.at(-1).end));
source.add(')');
if (expr) source.add('{return(');
source.add(text.slice(body.start, body.end));
if (expr) source.add(')}');
iSrc = td.end;
}
}
if (iSrc) {
source.add(text.slice(iSrc));
text = source.toStringWithSourceMap();
this.callback(null, text.code, text.map.toJSON());
} else {
this.callback(null, text, null, {webpackAST: ast});
}
}

/** @param {import('webpack/types').JavascriptParser} parser */
function arrowToFuncParser(parser) {
/** @type {WeakMap<object, Map<string,string>>} */
const topConsts = new WeakMap();
parser.hooks.program.tap({name: arrowToFuncLoader.name}, /**@param {Program} ast*/ast => {
parser.hooks.program.tap(NAME, /**@param {Program} ast*/ast => {
let tc;
for (let top of ast.body) {
if (top.type === 'ExportNamedDeclaration')
if (top.type === 'ExportNamedDeclaration' || top.type === 'ExportDefaultDeclaration')
top = top.declaration;
if (top?.kind !== CONST)
if (!top || top.kind !== CONST && top.type !== 'FunctionDeclaration')
continue;
for (const td of top.declarations) {
for (const td of top.declarations || [top]) {
if (!(tc ??= topConsts.get(parser.scope)))
topConsts.set(parser.scope, tc = new Map());
tc.set(td.id.name, !td.init.regex && td.init.value || '');
topConsts.set(parser.scope, tc = new Set());
tc.add(td.id.name);
}
}
});
parser.hooks.exportSpecifier.intercept({
name: [arrowToFuncLoader.name],
name: [NAME],
register(tap) {
if (tap.name === 'HarmonyExportDependencyParserPlugin') {
const {fn} = tap;
Expand All @@ -170,15 +107,14 @@ function arrowToFuncParser(parser) {
const res = fn.call(this, ...args);
const dep = parser.state.current.dependencies.at(-1);
if (dep?.name === exportedName) {
let tc;
const decl = exp.declaration;
if (decl?.type === 'FunctionDeclaration') {
dep[SYM] = FUNC;
} else if (
(decl?.kind === CONST || exp.specifiers) &&
(tc = topConsts.get(parser.scope)?.get(name)) != null
topConsts.get(parser.scope)?.has(name)
) {
dep[SYM] = !tc ? CONST : VAL + ' ' + JSON.stringify(tc).replaceAll('*/', '\n');
dep[SYM] = CONST;
}
return res;
}
Expand All @@ -189,40 +125,92 @@ function arrowToFuncParser(parser) {
});
}

function exportHook(...args) {
let res = Reflect.apply(...args);
let i = res.indexOf(STATIC);
if (i >= 0) {
const info = res.slice(i += STATIC.length, res.indexOf('*/', i));
const type = info.match(/\w+/)[0];
const val = type === CONST ? '0,' + res
: type === FUNC ? '1,' + res.slice(i + info.length + 2, -1)
: '2,' + info.slice(type.length + 1).replaceAll('\b', '*/');
res = `${STATIC}${type}*/ [${val}]`;
hookFunc(HarmonyExportSpecifierDependency.Template, 'apply', (fn, me, args) => {
const [dep, /*source*/, {initFragments: frags, concatenationScope}] = args;
const old = frags.length;
const res = Reflect.apply(fn, me, args);
if (dep[SYM]) {
if (old < frags.length) {
const boundVal = '/* binding */ ' + dep.id;
frags.at(-1).exportMap.forEach((val, key, map) => {
if (val === boundVal) map.set(key, `/*${dep[SYM]}*/${dep.id}`);
});
} else {
(concatenationScope._currentModule.module[SYM] ??= {})[dep.id] = dep[SYM];
}
}
return res;
}
});

hookFunc(HarmonyImportSpecifierDependency.Template, '_getCodeForIds', (fn, me, args) => {
const res = Reflect.apply(fn, me, args);
return res.replace(/^\(0,([$\w]+\.[$\w]+)\)$/, '$1');
});

hookFunc(HarmonyExportInitFragment, 'getContent', (fn, me, args) => {
const [a, b] = flattenExports(Reflect.apply(fn, me, args));
me.endContent = b;
return a;
});

hookFunc(ConcatenatedModule, 'codeGeneration', (fn, me, args) => {
const res = Reflect.apply(fn, me, args);
for (const src of res.sources.values())
for (let i = 0, child, exp, arr = src._source._children; i < arr.length; i++) {
child = arr[i];
if (!exp && child === '\n// EXPORTS\n') {
exp = {};
for (const mod of me.modules)
Object.assign(exp, mod[SYM]);
exp = flattenExports(arr[i + 1], exp);
arr.splice(i + 1, 1);
arr.push(exp[1]);
arr[i] = exp[0];
continue;
}
for (const r of child._replacements || []) {
if (rxCall.test(r.content)) {
r.content = RegExp.$1;
}
}
}
return res;
});

MakeNamespaceObjectRuntimeModule.prototype.generate = () => toOneLine(`\
${RG.makeNamespaceObject} = ${exports =>
Object.defineProperties(exports, {
[Symbol.toStringTag]: {value: 'Module'},
__esModule: {value: true},
})
}`);

function hookFunc(obj, name, hook) {
if (typeof obj === 'function') obj = obj.prototype;
obj[name] = new Proxy(obj[name], {apply: hook});
}

hookFunc(HarmonyExportSpecifierDependency.Template, 'apply', (fn, me, args) => {
const [dep, /*source*/, {initFragments: frags}] = args;
const old = frags.length;
const res = Reflect.apply(fn, me, args);
if (dep[SYM] && old < frags.length) {
const boundVal = '/* binding */ ' + dep.id;
frags.at(-1).exportMap.forEach((val, key, map) => {
if (val === boundVal) map.set(key, `${STATIC}${dep[SYM]}*/${dep.id}`);
function flattenExports(str, ids) {
let flat1 = '';
let flat2 = '';
str = str.replaceAll('/* harmony export */ ', '').replace(
/\s*"?([$\w]+)"?: \(\) => \((?:\/\*\s*(?:(c)onst|(f)unc|\w+)\s*\*\/\s*)?([$\w]+)\),?\s*/g,
(match, id, isConst, isFunc, dest) => {
if (
(isFunc ??= ids?.[id] === FUNC) ||
(isConst ??= (ids?.[id] === CONST || dest === '__WEBPACK_DEFAULT_EXPORT__'))
) {
match = `${RG.exports}.${id} = ${dest};\n`;
if (isFunc) flat1 += match; else flat2 += match;
match = '';
}
return match;
});
}
return res;
});

hookFunc(DefinePropertyGettersRuntimeModule, 'generate', (...args) =>
Reflect.apply(...args).replace(PATCH_EXPORTS_SRC, PATCH_EXPORTS));
if (str.replace(/\s+/g, '') !== `${RG.definePropertyGetters}(${RG.exports},{});`)
flat2 += str;
return [flat1, flat2];
}

module.exports = arrowToFuncLoader;
module.exports.RawEnvPlugin = RawEnvPlugin;
module.exports = {
RawEnvPlugin,
};
10 changes: 2 additions & 8 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,9 @@ const getBaseConfig = hasCodeMirror => ({
}])(), {
test: /\.(png|svg|jpe?g|gif|ttf)$/i,
type: 'asset/resource',
}, {
test: new RegExp(SRC_ESC + /.*\.m?js(\?.*)?$/.source),
use: [
{loader: './tools/wp-raw-patch-plugin'},
!MV3 && {loader: 'babel-loader', options: {root: ROOT}},
].filter(Boolean),
resolve: {fullySpecified: false},
}, !MV3 && {
test: new RegExp(String.raw`^(?!${SRC_ESC}).*\.m?js(\?.*)?$`),
test: /\.m?js(\?.*)?$/,
exclude: [CM_PACKAGE_PATH], // speedup: excluding known ES5 or ES6 libraries
loader: 'babel-loader',
options: {root: ROOT},
resolve: {fullySpecified: false},
Expand Down

0 comments on commit 7b3128f

Please sign in to comment.