webpack
Version:
Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.
289 lines (262 loc) • 8.76 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
;
const Template = require("../Template");
/** @typedef {import("estree").Node} Node */
/** @typedef {import("../javascript/JavascriptModulesPlugin").Scope} Scope */
/** @typedef {import("../javascript/JavascriptModulesPlugin").Reference} Reference */
/** @typedef {import("../javascript/JavascriptModulesPlugin").Variable} Variable */
/** @typedef {import("../javascript/JavascriptParser").Range} Range */
/** @typedef {Set<string>} UsedNames */
const DEFAULT_EXPORT = "__WEBPACK_DEFAULT_EXPORT__";
const NAMESPACE_OBJECT_EXPORT = "__WEBPACK_NAMESPACE_OBJECT__";
/**
* Gets all references.
* @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 } })
// identifiers is tiny (usually 1 declaration) — indexOf beats a Set here
const identifiers = variable.identifiers;
for (const scope of variable.scope.childScopes) {
for (const innerVar of scope.variables) {
if (innerVar.identifiers.some((id) => identifiers.includes(id))) {
// copy-on-write to keep the common no-match case allocation-free
if (set === variable.references) set = [...set];
for (const ref of innerVar.references) set.push(ref);
break;
}
}
}
return set;
};
/**
* Returns result.
* @param {Node | Node[]} ast ast
* @param {Node} node node
* @returns {undefined | Node[]} result
*/
const getPathInAst = (ast, node) => {
if (ast === node) {
return [];
}
const nr = /** @type {Range} */ (node.range);
/**
* Returns result.
* @param {Node} n node
* @returns {Node[] | undefined} result
*/
const enterNode = (n) => {
if (!n) return;
const r = n.range;
if (r && r[0] <= nr[0] && r[1] >= nr[1]) {
const path = getPathInAst(n, node);
if (path) {
path.push(n);
return path;
}
}
};
if (Array.isArray(ast)) {
// sibling ranges are ordered and disjoint; binary search the container
let lo = 0;
let hi = ast.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
const item = ast[mid];
const r = item && item.range;
if (!r) {
// holes or range-less nodes: scan the remaining window linearly
for (let i = lo; i <= hi; i++) {
const enterResult = enterNode(ast[i]);
if (enterResult !== undefined) return enterResult;
}
return;
}
if (r[0] > nr[0]) {
hi = mid - 1;
} else if (nr[0] >= r[1]) {
lo = mid + 1;
} else {
return r[1] >= nr[1] ? enterNode(item) : undefined;
}
}
} else if (ast && typeof ast === "object") {
const keys =
/** @type {(keyof Node)[]} */
(Object.keys(ast));
for (let i = 0; i < keys.length; i++) {
// We are making the faster check in `enterNode` using `n.range`
const value =
ast[
/** @type {Exclude<keyof Node, "range" | "loc" | "leadingComments" | "trailingComments">} */
(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;
}
}
}
};
/** @type {Map<string, string[]>} */
const splittedInfoCache = new Map();
/**
* Returns path segments of the cleaned extra info.
* @param {string} extraInfo extra info
* @returns {string[]} cleaned path segments
*/
const getSplittedInfo = (extraInfo) => {
let splittedInfo = splittedInfoCache.get(extraInfo);
if (splittedInfo === undefined) {
// bound the cache — extraInfo repeats for every renamed binding of a
// module, but distinct values grow with project size
if (splittedInfoCache.size >= 4096) splittedInfoCache.clear();
// Remove uncool stuff
splittedInfo = extraInfo
.replace(
/\.+\/|(?:\/index)?\.[a-zA-Z0-9]{1,4}(?:$|\s|\?)|\s*\+\s*\d+\s*modules/g,
""
)
.split("/");
splittedInfoCache.set(extraInfo, splittedInfo);
}
return splittedInfo;
};
/**
* Returns found new name.
* @param {string} oldName old name
* @param {UsedNames} usedNamed1 used named 1
* @param {UsedNames} usedNamed2 used named 2
* @param {string} extraInfo extra info
* @returns {string} found new name
*/
function findNewName(oldName, usedNamed1, usedNamed2, extraInfo) {
let name = oldName;
if (name === DEFAULT_EXPORT) {
name = "";
}
if (name === NAMESPACE_OBJECT_EXPORT) {
name = "namespaceObject";
}
const splittedInfo = getSplittedInfo(extraInfo);
for (let i = splittedInfo.length - 1; i >= 0; i--) {
name = splittedInfo[i] + (name ? `_${name}` : "");
const nameIdent = Template.toIdentifier(name);
if (
!usedNamed1.has(nameIdent) &&
(!usedNamed2 || !usedNamed2.has(nameIdent))
) {
return nameIdent;
}
}
// `_${i}` is identifier-safe, so escaping the base once is equivalent to
// escaping every candidate — avoids two regexes per collision
const nameIdent = Template.toIdentifier(name);
let i = 0;
let nameWithNumber = `${nameIdent}_${i}`;
while (
usedNamed1.has(nameWithNumber) ||
// eslint-disable-next-line no-unmodified-loop-condition
(usedNamed2 && usedNamed2.has(nameWithNumber))
) {
i++;
nameWithNumber = `${nameIdent}_${i}`;
}
return nameWithNumber;
}
/** @typedef {Set<Scope>} ScopeSet */
/**
* Adds scope symbols.
* @param {Scope | null} s scope
* @param {UsedNames} nameSet name set
* @param {ScopeSet} scopeSet1 scope set 1
* @param {ScopeSet} scopeSet2 scope set 2
*/
const addScopeSymbols = (s, nameSet, scopeSet1, scopeSet2) => {
let scope = s;
while (scope) {
if (scopeSet1.has(scope)) break;
if (scopeSet2.has(scope)) break;
scopeSet1.add(scope);
for (const variable of scope.variables) {
nameSet.add(variable.name);
}
scope = scope.upper;
}
};
const RESERVED_NAMES = new Set(
[
// internal names (should always be renamed)
DEFAULT_EXPORT,
NAMESPACE_OBJECT_EXPORT,
// keywords
"abstract,arguments,async,await,boolean,break,byte,case,catch,char,class,const,continue",
"debugger,default,delete,do,double,else,enum,eval,export,extends,false,final,finally,float",
"for,function,goto,if,implements,import,in,instanceof,int,interface,let,long,native,new,null",
"package,private,protected,public,return,short,static,super,switch,synchronized,this,throw",
"throws,transient,true,try,typeof,var,void,volatile,while,with,yield",
// commonjs/amd
"module,__dirname,__filename,exports,require,define",
// js globals
"Array,Date,eval,function,hasOwnProperty,Infinity,isFinite,isNaN,isPrototypeOf,length,Math",
"NaN,name,Number,Object,prototype,String,Symbol,toString,undefined,valueOf",
// browser globals
"alert,all,anchor,anchors,area,assign,blur,button,checkbox,clearInterval,clearTimeout",
"clientInformation,close,closed,confirm,constructor,crypto,decodeURI,decodeURIComponent",
"defaultStatus,document,element,elements,embed,embeds,encodeURI,encodeURIComponent,escape",
"event,fileUpload,focus,form,forms,frame,innerHeight,innerWidth,layer,layers,link,location",
"mimeTypes,navigate,navigator,frames,frameRate,hidden,history,image,images,offscreenBuffering",
"open,opener,option,outerHeight,outerWidth,packages,pageXOffset,pageYOffset,parent,parseFloat",
"parseInt,password,pkcs11,plugin,prompt,propertyIsEnum,radio,reset,screenX,screenY,scroll",
"secure,select,self,setInterval,setTimeout,status,submit,taint,text,textarea,top,unescape",
"untaint,window",
// window events
"onblur,onclick,onerror,onfocus,onkeydown,onkeypress,onkeyup,onmouseover,onload,onmouseup,onmousedown,onsubmit"
]
.join(",")
.split(",")
);
/** @typedef {{ usedNames: UsedNames, alreadyCheckedScopes: ScopeSet }} ScopeInfo */
/** @typedef {Map<string, Map<string, ScopeInfo>>} UsedNamesInScopeInfo */
/**
* Gets used names in scope info.
* @param {UsedNamesInScopeInfo} usedNamesInScopeInfo used names in scope info
* @param {string} module module identifier
* @param {string} id export id
* @returns {ScopeInfo} info
*/
const getUsedNamesInScopeInfo = (usedNamesInScopeInfo, module, id) => {
// nested maps avoid building a `${module}-${id}` key string per lookup
let byId = usedNamesInScopeInfo.get(module);
if (byId === undefined) {
byId = new Map();
usedNamesInScopeInfo.set(module, byId);
}
let info = byId.get(id);
if (info === undefined) {
info = {
usedNames: new Set(),
alreadyCheckedScopes: new Set()
};
byId.set(id, info);
}
return info;
};
module.exports = {
DEFAULT_EXPORT,
NAMESPACE_OBJECT_EXPORT,
RESERVED_NAMES,
addScopeSymbols,
findNewName,
getAllReferences,
getPathInAst,
getUsedNamesInScopeInfo
};