UNPKG

9.83 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5"use strict";
6const ConstDependency = require("./dependencies/ConstDependency");
7const NullFactory = require("./NullFactory");
8const ParserHelpers = require("./ParserHelpers");
9
10const getQuery = request => {
11 const i = request.indexOf("?");
12 return i !== -1 ? request.substr(i) : "";
13};
14
15const collectDeclaration = (declarations, pattern) => {
16 const stack = [pattern];
17 while (stack.length > 0) {
18 const node = stack.pop();
19 switch (node.type) {
20 case "Identifier":
21 declarations.add(node.name);
22 break;
23 case "ArrayPattern":
24 for (const element of node.elements) {
25 if (element) {
26 stack.push(element);
27 }
28 }
29 break;
30 case "AssignmentPattern":
31 stack.push(node.left);
32 break;
33 case "ObjectPattern":
34 for (const property of node.properties) {
35 stack.push(property.value);
36 }
37 break;
38 case "RestElement":
39 stack.push(node.argument);
40 break;
41 }
42 }
43};
44
45const getHoistedDeclarations = (branch, includeFunctionDeclarations) => {
46 const declarations = new Set();
47 const stack = [branch];
48 while (stack.length > 0) {
49 const node = stack.pop();
50 // Some node could be `null` or `undefined`.
51 if (!node) continue;
52 switch (node.type) {
53 // Walk through control statements to look for hoisted declarations.
54 // Some branches are skipped since they do not allow declarations.
55 case "BlockStatement":
56 for (const stmt of node.body) {
57 stack.push(stmt);
58 }
59 break;
60 case "IfStatement":
61 stack.push(node.consequent);
62 stack.push(node.alternate);
63 break;
64 case "ForStatement":
65 stack.push(node.init);
66 stack.push(node.body);
67 break;
68 case "ForInStatement":
69 case "ForOfStatement":
70 stack.push(node.left);
71 stack.push(node.body);
72 break;
73 case "DoWhileStatement":
74 case "WhileStatement":
75 case "LabeledStatement":
76 stack.push(node.body);
77 break;
78 case "SwitchStatement":
79 for (const cs of node.cases) {
80 for (const consequent of cs.consequent) {
81 stack.push(consequent);
82 }
83 }
84 break;
85 case "TryStatement":
86 stack.push(node.block);
87 if (node.handler) {
88 stack.push(node.handler.body);
89 }
90 stack.push(node.finalizer);
91 break;
92 case "FunctionDeclaration":
93 if (includeFunctionDeclarations) {
94 collectDeclaration(declarations, node.id);
95 }
96 break;
97 case "VariableDeclaration":
98 if (node.kind === "var") {
99 for (const decl of node.declarations) {
100 collectDeclaration(declarations, decl.id);
101 }
102 }
103 break;
104 }
105 }
106 return Array.from(declarations);
107};
108
109class ConstPlugin {
110 apply(compiler) {
111 compiler.hooks.compilation.tap(
112 "ConstPlugin",
113 (compilation, { normalModuleFactory }) => {
114 compilation.dependencyFactories.set(ConstDependency, new NullFactory());
115 compilation.dependencyTemplates.set(
116 ConstDependency,
117 new ConstDependency.Template()
118 );
119
120 const handler = parser => {
121 parser.hooks.statementIf.tap("ConstPlugin", statement => {
122 const param = parser.evaluateExpression(statement.test);
123 const bool = param.asBool();
124 if (typeof bool === "boolean") {
125 if (statement.test.type !== "Literal") {
126 const dep = new ConstDependency(`${bool}`, param.range);
127 dep.loc = statement.loc;
128 parser.state.current.addDependency(dep);
129 }
130 const branchToRemove = bool
131 ? statement.alternate
132 : statement.consequent;
133 if (branchToRemove) {
134 // Before removing the dead branch, the hoisted declarations
135 // must be collected.
136 //
137 // Given the following code:
138 //
139 // if (true) f() else g()
140 // if (false) {
141 // function f() {}
142 // const g = function g() {}
143 // if (someTest) {
144 // let a = 1
145 // var x, {y, z} = obj
146 // }
147 // } else {
148 // …
149 // }
150 //
151 // the generated code is:
152 //
153 // if (true) f() else {}
154 // if (false) {
155 // var f, x, y, z; (in loose mode)
156 // var x, y, z; (in strict mode)
157 // } else {
158 // …
159 // }
160 //
161 // NOTE: When code runs in strict mode, `var` declarations
162 // are hoisted but `function` declarations don't.
163 //
164 let declarations;
165 if (parser.scope.isStrict) {
166 // If the code runs in strict mode, variable declarations
167 // using `var` must be hoisted.
168 declarations = getHoistedDeclarations(branchToRemove, false);
169 } else {
170 // Otherwise, collect all hoisted declaration.
171 declarations = getHoistedDeclarations(branchToRemove, true);
172 }
173 let replacement;
174 if (declarations.length > 0) {
175 replacement = `{ var ${declarations.join(", ")}; }`;
176 } else {
177 replacement = "{}";
178 }
179 const dep = new ConstDependency(
180 replacement,
181 branchToRemove.range
182 );
183 dep.loc = branchToRemove.loc;
184 parser.state.current.addDependency(dep);
185 }
186 return bool;
187 }
188 });
189 parser.hooks.expressionConditionalOperator.tap(
190 "ConstPlugin",
191 expression => {
192 const param = parser.evaluateExpression(expression.test);
193 const bool = param.asBool();
194 if (typeof bool === "boolean") {
195 if (expression.test.type !== "Literal") {
196 const dep = new ConstDependency(` ${bool}`, param.range);
197 dep.loc = expression.loc;
198 parser.state.current.addDependency(dep);
199 }
200 // Expressions do not hoist.
201 // It is safe to remove the dead branch.
202 //
203 // Given the following code:
204 //
205 // false ? someExpression() : otherExpression();
206 //
207 // the generated code is:
208 //
209 // false ? undefined : otherExpression();
210 //
211 const branchToRemove = bool
212 ? expression.alternate
213 : expression.consequent;
214 const dep = new ConstDependency(
215 "undefined",
216 branchToRemove.range
217 );
218 dep.loc = branchToRemove.loc;
219 parser.state.current.addDependency(dep);
220 return bool;
221 }
222 }
223 );
224 parser.hooks.expressionLogicalOperator.tap(
225 "ConstPlugin",
226 expression => {
227 if (
228 expression.operator === "&&" ||
229 expression.operator === "||"
230 ) {
231 const param = parser.evaluateExpression(expression.left);
232 const bool = param.asBool();
233 if (typeof bool === "boolean") {
234 // Expressions do not hoist.
235 // It is safe to remove the dead branch.
236 //
237 // ------------------------------------------
238 //
239 // Given the following code:
240 //
241 // falsyExpression() && someExpression();
242 //
243 // the generated code is:
244 //
245 // falsyExpression() && false;
246 //
247 // ------------------------------------------
248 //
249 // Given the following code:
250 //
251 // truthyExpression() && someExpression();
252 //
253 // the generated code is:
254 //
255 // true && someExpression();
256 //
257 // ------------------------------------------
258 //
259 // Given the following code:
260 //
261 // truthyExpression() || someExpression();
262 //
263 // the generated code is:
264 //
265 // truthyExpression() || false;
266 //
267 // ------------------------------------------
268 //
269 // Given the following code:
270 //
271 // falsyExpression() || someExpression();
272 //
273 // the generated code is:
274 //
275 // false && someExpression();
276 //
277 const keepRight =
278 (expression.operator === "&&" && bool) ||
279 (expression.operator === "||" && !bool);
280
281 if (param.isBoolean() || keepRight) {
282 // for case like
283 //
284 // return'development'===process.env.NODE_ENV&&'foo'
285 //
286 // we need a space before the bool to prevent result like
287 //
288 // returnfalse&&'foo'
289 //
290 const dep = new ConstDependency(` ${bool}`, param.range);
291 dep.loc = expression.loc;
292 parser.state.current.addDependency(dep);
293 } else {
294 parser.walkExpression(expression.left);
295 }
296 if (!keepRight) {
297 const dep = new ConstDependency(
298 "false",
299 expression.right.range
300 );
301 dep.loc = expression.loc;
302 parser.state.current.addDependency(dep);
303 }
304 return keepRight;
305 }
306 }
307 }
308 );
309 parser.hooks.evaluateIdentifier
310 .for("__resourceQuery")
311 .tap("ConstPlugin", expr => {
312 if (!parser.state.module) return;
313 return ParserHelpers.evaluateToString(
314 getQuery(parser.state.module.resource)
315 )(expr);
316 });
317 parser.hooks.expression
318 .for("__resourceQuery")
319 .tap("ConstPlugin", () => {
320 if (!parser.state.module) return;
321 parser.state.current.addVariable(
322 "__resourceQuery",
323 JSON.stringify(getQuery(parser.state.module.resource))
324 );
325 return true;
326 });
327 };
328
329 normalModuleFactory.hooks.parser
330 .for("javascript/auto")
331 .tap("ConstPlugin", handler);
332 normalModuleFactory.hooks.parser
333 .for("javascript/dynamic")
334 .tap("ConstPlugin", handler);
335 normalModuleFactory.hooks.parser
336 .for("javascript/esm")
337 .tap("ConstPlugin", handler);
338 }
339 );
340 }
341}
342
343module.exports = ConstPlugin;