Skip to content

Commit

Permalink
feat: root module is less prone to be wrapped in IIFE
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Apr 24, 2024
2 parents 77c4d0c + 551c372 commit c586c7b
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 139 deletions.
172 changes: 160 additions & 12 deletions lib/javascript/JavascriptModulesPlugin.js
Expand Up @@ -5,14 +5,16 @@

"use strict";

const eslintScope = require("eslint-scope");
const { SyncWaterfallHook, SyncHook, SyncBailHook } = require("tapable");
const vm = require("vm");
const {
ConcatSource,
OriginalSource,
PrefixSource,
RawSource,
CachedSource
CachedSource,
ReplaceSource
} = require("webpack-sources");
const Compilation = require("../Compilation");
const { tryRunOrWebpackError } = require("../HookWebpackError");
Expand All @@ -30,11 +32,13 @@ const { last, someInIterable } = require("../util/IterableHelpers");
const StringXor = require("../util/StringXor");
const { compareModulesByIdentifier } = require("../util/comparators");
const createHash = require("../util/createHash");
const { getPathInAst, getAllReferences } = require("../util/mergeScope");
const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
const { intersectRuntime } = require("../util/runtime");
const JavascriptGenerator = require("./JavascriptGenerator");
const JavascriptParser = require("./JavascriptParser");

/** @typedef {import("eslint-scope").Variable} Variable */
/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../ChunkGraph")} ChunkGraph */
Expand Down Expand Up @@ -818,13 +822,19 @@ class JavascriptModulesPlugin {
const lastInlinedModule = last(inlinedModules);
const startupSource = new ConcatSource();
startupSource.add(`var ${RuntimeGlobals.exports} = {};\n`);
const renamedInlinedModules = this.renamedRootModule(
allModules,
renderContext,
inlinedModules,
chunkRenderContext,
hooks
);

for (const m of inlinedModules) {
const renderedModule = this.renderModule(
m,
chunkRenderContext,
hooks,
false
);
const renderedModule =
renamedInlinedModules.get(m) ||
this.renderModule(m, chunkRenderContext, hooks, false);

if (renderedModule) {
const innerStrict = !allStrict && m.buildInfo.strict;
const runtimeRequirements = chunkGraph.getModuleRuntimeRequirements(
Expand All @@ -840,11 +850,9 @@ class JavascriptModulesPlugin {
? // TODO check globals and top-level declarations of other entries and chunk modules
// to make a better decision
"it need to be isolated against other entry modules."
: chunkModules
? "it need to be isolated against other modules in the chunk."
: exports && !webpackExports
? `it uses a non-standard name for the exports (${m.exportsArgument}).`
: hooks.embedInRuntimeBailout.call(m, renderContext);
: exports && !webpackExports
? `it uses a non-standard name for the exports (${m.exportsArgument}).`
: hooks.embedInRuntimeBailout.call(m, renderContext);
let footer;
if (iife !== undefined) {
startupSource.add(
Expand Down Expand Up @@ -1383,6 +1391,146 @@ class JavascriptModulesPlugin {
"JavascriptModulesPlugin.getCompilationHooks().renderRequire"
);
}

/**
* @param {Module[]} allModules allModules
* @param {MainRenderContext} renderContext renderContext
* @param {Set<Module>} inlinedModules inlinedModules
* @param {ChunkRenderContext} chunkRenderContext chunkRenderContext
* @param {CompilationHooks} hooks hooks
* @returns {Map<Module, Source>} renamed inlined modules
*/
renamedRootModule(
allModules,
renderContext,
inlinedModules,
chunkRenderContext,
hooks
) {
const { runtimeTemplate } = renderContext;

/** @type {Map<Module, { source: Source, ast: any, variables: Set<Variable> }>} */
const inlinedModulesToInfo = new Map();
/** @type {Set<string>} */
const nonInlinedModuleThroughIdentifiers = new Set();
/** @type {Map<Module, Source>} */
const renamedInlinedModules = new Map();
/** @type {Set<{ m: Module, variable: Variable}>} */
const usedIdentifiers = new Set();

for (const m of allModules) {
const isInlinedModule = inlinedModules && inlinedModules.has(m);
const moduleSource = this.renderModule(
m,
chunkRenderContext,
hooks,
isInlinedModule ? false : true
);

if (!moduleSource) continue;
const code = /** @type {string} */ (moduleSource.source());
const ast = JavascriptParser._parse(code, {
sourceType: "auto"
});

const scopeManager = eslintScope.analyze(ast, {
ecmaVersion: 6,
sourceType: "module",
optimistic: true,
ignoreEval: true
});

const globalScope = scopeManager.acquire(ast);
if (inlinedModules && inlinedModules.has(m)) {
const moduleScope = globalScope.childScopes[0];
const variables = new Set();
for (const variable of moduleScope.variables) {
variables.add(variable);
}
inlinedModulesToInfo.set(m, { source: moduleSource, ast, variables });
} else {
for (const ref of globalScope.through) {
nonInlinedModuleThroughIdentifiers.add(ref.identifier.name);
}
}
}

for (const [module, { variables }] of inlinedModulesToInfo) {
for (const variable of variables) {
if (nonInlinedModuleThroughIdentifiers.has(variable.name)) {
usedIdentifiers.add({ m: module, variable });
}
}
}

const usedName = new Set();
for (const { variable, m } of usedIdentifiers) {
const references = getAllReferences(variable);
const { ast, source: _source } = inlinedModulesToInfo.get(m);
const source = new ReplaceSource(_source);
const allIdentifiers = new Set(
references.map(r => r.identifier).concat(variable.identifiers)
);

const newName = this.findNewName(
variable.name,
usedName,
m.readableIdentifier(runtimeTemplate.requestShortener)
);

for (const identifier of allIdentifiers) {
const r = identifier.range;
const path = getPathInAst(ast, identifier);
if (path && path.length > 1) {
const maybeProperty =
path[1].type === "AssignmentPattern" && path[1].left === path[0]
? path[2]
: path[1];
if (maybeProperty.type === "Property" && maybeProperty.shorthand) {
source.insert(r[1], `: ${newName}`);
continue;
}
}
source.replace(r[0], r[1] - 1, newName);
}

renamedInlinedModules.set(m, source);
}

return renamedInlinedModules;
}

/**
* @param {string} oldName oldName
* @param {Set<string>} usedName usedName
* @param {string} extraInfo extraInfo
* @returns {string} extraInfo
*/
findNewName(oldName, usedName, extraInfo) {
let name = oldName;

// Remove uncool stuff
extraInfo = extraInfo.replace(
/\.+\/|(\/index)?\.([a-zA-Z0-9]{1,4})($|\s|\?)|\s*\+\s*\d+\s*modules/g,
""
);
const splittedInfo = extraInfo.split("/");
while (splittedInfo.length) {
name = splittedInfo.pop() + (name ? "_" + name : "");
const nameIdent = Template.toIdentifier(name);
if (!usedName.has(nameIdent)) {
return nameIdent;
}
}

let i = 0;
let nameWithNumber = Template.toIdentifier(`${oldName}_${i}`);
while (usedName.has(nameWithNumber)) {
i++;
nameWithNumber = Template.toIdentifier(`${oldName}_${i}`);
}
return nameWithNumber;
}
}

module.exports = JavascriptModulesPlugin;
Expand Down
67 changes: 1 addition & 66 deletions lib/optimize/ConcatenatedModule.js
Expand Up @@ -26,6 +26,7 @@ const { concatComparators } = require("../util/comparators");
const createHash = require("../util/createHash");
const { makePathsRelative } = require("../util/identifier");
const makeSerializable = require("../util/makeSerializable");
const { getAllReferences, getPathInAst } = require("../util/mergeScope");
const propertyAccess = require("../util/propertyAccess");
const { propertyName } = require("../util/propertyName");
const {
Expand Down Expand Up @@ -625,72 +626,6 @@ const addScopeSymbols = (s, nameSet, scopeSet1, scopeSet2) => {
}
};

/**
* @param {Variable} variable variable
* @returns {Reference[]} references
*/
const getAllReferences = variable => {
let set = variable.references;
// Look for inner scope variables too (like in class Foo { t() { Foo } })
const identifiers = new Set(variable.identifiers);
for (const scope of variable.scope.childScopes) {
for (const innerVar of scope.variables) {
if (innerVar.identifiers.some(id => identifiers.has(id))) {
set = set.concat(innerVar.references);
break;
}
}
}
return set;
};

/**
* @param {TODO} ast ast
* @param {TODO} node node
* @returns {TODO} result
*/
const getPathInAst = (ast, node) => {
if (ast === node) {
return [];
}

const nr = node.range;

const enterNode = n => {
if (!n) return undefined;
const r = n.range;
if (r) {
if (r[0] <= nr[0] && r[1] >= nr[1]) {
const path = getPathInAst(n, node);
if (path) {
path.push(n);
return path;
}
}
}
return undefined;
};

if (Array.isArray(ast)) {
for (let i = 0; i < ast.length; i++) {
const enterResult = enterNode(ast[i]);
if (enterResult !== undefined) return enterResult;
}
} else if (ast && typeof ast === "object") {
const keys = Object.keys(ast);
for (let i = 0; i < keys.length; i++) {
const value = ast[keys[i]];
if (Array.isArray(value)) {
const pathResult = getPathInAst(value, node);
if (pathResult !== undefined) return pathResult;
} else if (value && typeof value === "object") {
const enterResult = enterNode(value);
if (enterResult !== undefined) return enterResult;
}
}
}
};

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

class ConcatenatedModule extends Module {
Expand Down
79 changes: 79 additions & 0 deletions lib/util/mergeScope.js
@@ -0,0 +1,79 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/

"use strict";

/** @typedef {import("eslint-scope").Reference} Reference */
/** @typedef {import("eslint-scope").Variable} Variable */
/** @typedef {import("../javascript/JavascriptParser").AnyNode} AnyNode */
/** @typedef {import("../javascript/JavascriptParser").Program} Program */

/**
* @param {Variable} variable variable
* @returns {Reference[]} references
*/
const getAllReferences = variable => {
let set = variable.references;
// Look for inner scope variables too (like in class Foo { t() { Foo } })
const identifiers = new Set(variable.identifiers);
for (const scope of variable.scope.childScopes) {
for (const innerVar of scope.variables) {
if (innerVar.identifiers.some(id => identifiers.has(id))) {
set = set.concat(innerVar.references);
break;
}
}
}
return set;
};

/**
* @param {Program | Program[]} ast ast
* @param {AnyNode} node node
* @returns {undefined | AnyNode[]} result
*/
const getPathInAst = (ast, node) => {
if (ast === node) {
return [];
}

const nr = node.range;

const enterNode = n => {
if (!n) return undefined;
const r = n.range;
if (r) {
if (r[0] <= nr[0] && r[1] >= nr[1]) {
const path = getPathInAst(n, node);
if (path) {
path.push(n);
return path;
}
}
}
return undefined;
};

if (Array.isArray(ast)) {
for (let i = 0; i < ast.length; i++) {
const enterResult = enterNode(ast[i]);
if (enterResult !== undefined) return enterResult;
}
} else if (ast && typeof ast === "object") {
const keys = Object.keys(ast);
for (let i = 0; i < keys.length; i++) {
const value = ast[keys[i]];
if (Array.isArray(value)) {
const pathResult = getPathInAst(value, node);
if (pathResult !== undefined) return pathResult;
} else if (value && typeof value === "object") {
const enterResult = enterNode(value);
if (enterResult !== undefined) return enterResult;
}
}
}
};

module.exports = { getAllReferences, getPathInAst };

0 comments on commit c586c7b

Please sign in to comment.