Skip to content

Commit

Permalink
Improved UMD wrapper support
Browse files Browse the repository at this point in the history
- Handle IIFE where expression if forced with !
- Check for UMD patterns using ternary operator
- More test cases
- Inline UMD Factories

Test cases and some UMD detection changes by Juho Teperi.
UMD factory inlining by Chad Killingsworth.

Closes #2597

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=170896115
  • Loading branch information
Deraen authored and dimvar committed Oct 4, 2017
1 parent e58a489 commit 3c0fed5
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 12 deletions.
132 changes: 120 additions & 12 deletions src/com/google/javascript/jscomp/ProcessCommonJSModules.java
Expand Up @@ -101,11 +101,15 @@ public void process(Node externs, Node root) {
finder.reportModuleErrors(); finder.reportModuleErrors();


if (!finder.umdPatterns.isEmpty()) { if (!finder.umdPatterns.isEmpty()) {
finder.replaceUmdPatterns(); boolean needsRetraverse = finder.replaceUmdPatterns();


// Removing the IIFE rewrites vars. We need to re-traverse if (!needsRetraverse) {
needsRetraverse = removeIIFEWrapper(root);
}

// Inlining functions rewrites vars. We need to re-traverse
// to get the new references. // to get the new references.
if (removeIIFEWrapper(root)) { if (needsRetraverse) {
finder = new FindImportsAndExports(); finder = new FindImportsAndExports();
NodeTraversal.traverseEs6(compiler, root, finder); NodeTraversal.traverseEs6(compiler, root, finder);
} }
Expand Down Expand Up @@ -186,6 +190,14 @@ private boolean removeIIFEWrapper(Node root) {
return false; return false;
} }


// Function expression can be forced with !, just skip !
// TODO(ChadKillingsworth):
// Expression could also be forced with: + - ~ void
// ! ~ void can be repeated any number of times
if (n != null && n.getFirstChild() != null && n.getFirstChild().isNot()) {
n = n.getFirstChild();
}

Node call = n.getFirstChild(); Node call = n.getFirstChild();
if (call == null || !call.isCall()) { if (call == null || !call.isCall()) {
return false; return false;
Expand Down Expand Up @@ -486,8 +498,11 @@ void initializeModule() {
Node rValue = NodeUtil.getRValueOfLValue(export.node); Node rValue = NodeUtil.getRValueOfLValue(export.node);
if (rValue == null || !rValue.isObjectLit()) { if (rValue == null || !rValue.isObjectLit()) {
directAssignments++; directAssignments++;
if (export.node.getParent().getParent().isExprResult() Node expr = export.node.getParent().getParent();
&& NodeUtil.isTopLevel(export.node.getParent().getParent().getParent())) { if (expr.isExprResult()
&& (NodeUtil.isTopLevel(expr.getParent())
|| (expr.getParent().isNormalBlock()
&& NodeUtil.isTopLevel(expr.getGrandparent())))) {
directAssignmentsAtTopLevel++; directAssignmentsAtTopLevel++;
} }
} }
Expand Down Expand Up @@ -569,7 +584,9 @@ private Node getOutermostIfAncestor(Node n) {
return null; return null;
} }


if (parent.isIf() && parent.getFirstChild() == n) { // When walking up ternary operations (hook), don't check if parent is the condition,
// because one ternary operation can be then/else branch of another.
if ((parent.isIf() && parent.getFirstChild() == n) || parent.isHook()) {
Node outerIf = getOutermostIfAncestor(parent); Node outerIf = getOutermostIfAncestor(parent);
if (outerIf != null) { if (outerIf != null) {
return outerIf; return outerIf;
Expand All @@ -582,15 +599,17 @@ private Node getOutermostIfAncestor(Node n) {
} }


/** Remove a Universal Module Definition and leave just the commonjs export statement */ /** Remove a Universal Module Definition and leave just the commonjs export statement */
void replaceUmdPatterns() { boolean replaceUmdPatterns() {
boolean needsRetraverse = false;
Node changeScope;
for (UmdPattern umdPattern : umdPatterns) { for (UmdPattern umdPattern : umdPatterns) {
Node parent = umdPattern.ifRoot.getParent(); Node parent = umdPattern.ifRoot.getParent();
Node newNode = umdPattern.activeBranch; Node newNode = umdPattern.activeBranch;


if (newNode == null) { if (newNode == null) {
parent.removeChild(umdPattern.ifRoot); parent.removeChild(umdPattern.ifRoot);
compiler.reportChangeToEnclosingScope(parent); compiler.reportChangeToEnclosingScope(parent);
return; continue;
} }


// Remove redundant block node. Not strictly necessary, but makes tests more legible. // Remove redundant block node. Not strictly necessary, but makes tests more legible.
Expand All @@ -602,12 +621,100 @@ void replaceUmdPatterns() {
umdPattern.ifRoot.detachChildren(); umdPattern.ifRoot.detachChildren();
} }
parent.replaceChild(umdPattern.ifRoot, newNode); parent.replaceChild(umdPattern.ifRoot, newNode);
// TODO(johnlenz): don't work on detached nodes changeScope = NodeUtil.getEnclosingChangeScopeRoot(newNode);
Node changeScope = NodeUtil.getEnclosingChangeScopeRoot(parent);
if (changeScope != null) { if (changeScope != null) {
compiler.reportChangeToEnclosingScope(parent); compiler.reportChangeToEnclosingScope(newNode);
}

Node block = parent;
if (block.isExprResult()) {
block = block.getParent();
}

// Detect UMD Factory Patterns and inline the functions
if (block.isNormalBlock() && block.getParent().isFunction()
&& block.getGrandparent().isCall()
&& parent.hasOneChild()) {
Node enclosingFnCall = block.getGrandparent();
Node fn = block.getParent();

Node enclosingScript = NodeUtil.getEnclosingScript(enclosingFnCall);
if (enclosingScript == null) {
continue;
}
CompilerInput ci = compiler.getInput(
NodeUtil.getEnclosingScript(enclosingFnCall).getInputId());
ModulePath modulePath = ci.getPath();
if (modulePath == null) {
continue;
}
needsRetraverse = true;
String factoryLabel = modulePath.toModuleName() + "_factory";

FunctionToBlockMutator mutator = new FunctionToBlockMutator(
compiler, compiler.getUniqueNameIdSupplier());
Node newStatements = mutator.mutate(
factoryLabel, fn, enclosingFnCall, null, false, false);

// Check to see if the returned block is of the form:
// {
// var jscomp$inline = function() {};
// jscomp$inline();
// }
//
// If so, inline again
if (newStatements.isNormalBlock()
&& newStatements.hasTwoChildren()
&& newStatements.getFirstChild().isVar()
&& newStatements.getFirstFirstChild().hasOneChild()
&& newStatements.getFirstFirstChild().getFirstChild().isFunction()
&& newStatements.getSecondChild().isExprResult()) {
Node inlinedFn = newStatements.getFirstFirstChild().getFirstChild();
String fnName = newStatements.getFirstFirstChild().getString();
Node expr = newStatements.getSecondChild().getFirstChild();
Node call = null;
if (expr.isAssign() && expr.getSecondChild().isCall()) {
call = expr.getSecondChild();
} else if (expr.isCall()) {
call = expr;
}

if (call != null) {
newStatements = mutator.mutate(
factoryLabel, inlinedFn, call, null, false, false);
if (expr.isAssign() && newStatements.hasOneChild()
&& newStatements.getFirstChild().isExprResult()) {
expr.replaceChild(
expr.getSecondChild(),
newStatements.getFirstFirstChild().detach());
newStatements = expr.getParent().detach();
}
}
}

Node callRoot = enclosingFnCall.getParent();
if (callRoot.isNot()) {
callRoot = callRoot.getParent();
}
if (callRoot.isExprResult()) {
callRoot = callRoot.getParent();

callRoot.detachChildren();
callRoot.addChildToFront(newStatements);
changeScope = NodeUtil.getEnclosingChangeScopeRoot(callRoot);
if (changeScope != null) {
compiler.reportChangeToEnclosingScope(callRoot);
}
} else {
parent.replaceChild(umdPattern.ifRoot, newNode);
changeScope = NodeUtil.getEnclosingChangeScopeRoot(newNode);
if (changeScope != null) {
compiler.reportChangeToEnclosingScope(newNode);
}
}
} }
} }
return needsRetraverse;
} }
} }


Expand Down Expand Up @@ -856,7 +963,8 @@ private void visitExport(NodeTraversal t, Node export) {
Node parent = root.getParent(); Node parent = root.getParent();
Node var = IR.var(updatedExport, rValue.detach()).useSourceInfoFrom(root.getParent()); Node var = IR.var(updatedExport, rValue.detach()).useSourceInfoFrom(root.getParent());
parent.getParent().replaceWith(var); parent.getParent().replaceWith(var);
} else if (root.getNext() != null && root.getNext().isName() && rValueVar.isGlobal()) { } else if (root.getNext() != null && root.getNext().isName()
&& rValueVar != null && rValueVar.isGlobal()) {
// This is a where a module export assignment is used in a complex expression. // This is a where a module export assignment is used in a complex expression.
// Before: `SOME_VALUE !== undefined && module.exports = SOME_VALUE` // Before: `SOME_VALUE !== undefined && module.exports = SOME_VALUE`
// After: `SOME_VALUE !== undefined && module$name` // After: `SOME_VALUE !== undefined && module$name`
Expand Down
85 changes: 85 additions & 0 deletions test/com/google/javascript/jscomp/ProcessCommonJSModulesTest.java
Expand Up @@ -640,6 +640,22 @@ public void testUMDRemoveIIFE() {
"goog.provide('module$test');", "goog.provide('module$test');",
"var module$test = {foo: 'bar'};")); "var module$test = {foo: 'bar'};"));


testModules(
"test.js",
LINE_JOINER.join(
"!function(){",
"var foobar = {foo: 'bar'};",
"if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"} else if (typeof define === 'function' && define.amd) {",
" define([], function() {return foobar;});",
"} else {",
" this.foobar = foobar;",
"}}()"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {foo: 'bar'};"));

testModules( testModules(
"test.js", "test.js",
LINE_JOINER.join( LINE_JOINER.join(
Expand Down Expand Up @@ -929,6 +945,75 @@ public void testIssue2593() {
"var fifth$$module$test=5;")); "var fifth$$module$test=5;"));
} }


public void testTernaryUMDWrapper() {
testModules(
"test.js",
LINE_JOINER.join(
"var foobar = {foo: 'bar'};",
"typeof module === 'object' && module.exports ?",
" module.exports = foobar :",
" typeof define === 'function' && define.amd ?",
" define([], function() {return foobar;}) :",
" this.foobar = foobar;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {foo: 'bar'};"));
}

public void testLeafletUMDWrapper() {
testModules(
"test.js",
LINE_JOINER.join(
"(function (global, factory) {",
" typeof exports === 'object' && typeof module !== 'undefined' ?",
" factory(exports) :",
" typeof define === 'function' && define.amd ?",
" define(['exports'], factory) :",
" (factory((global.L = {})));",
"}(this, (function (exports) {",
" 'use strict';",
" var webkit = userAgentContains('webkit');",
" function userAgentContains(str) {",
" return navigator.userAgent.toLowerCase().indexOf(str) >= 0;",
" }",
" exports.webkit = webkit",
"})));"),
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @const */ var module$test={};",
"{",
" var exports$jscomp$inline_3$$module$test=module$test;",
" var userAgentContains$jscomp$inline_5$$module$test=",
" function(str$jscomp$inline_6){",
" return navigator.userAgent.toLowerCase().indexOf(",
" str$jscomp$inline_6)>=0;",
" };",
" var webkit$jscomp$inline_4$$module$test=",
" userAgentContains$jscomp$inline_5$$module$test('webkit');",
" exports$jscomp$inline_3$$module$test.webkit=",
" webkit$jscomp$inline_4$$module$test;",
"}"));
}

public void testBowserUMDWrapper() {
testModules(
"test.js",
LINE_JOINER.join(
"!function (root, name, definition) {",
" if (typeof module != 'undefined' && module.exports)",
" module.exports = definition()",
" else if (typeof define == 'function' && define.amd)",
" define(name, definition)",
" else root[name] = definition()",
"}(this, 'foobar', function () {",
" return {foo: 'bar'};",
"});"),
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @const */ var module$test={};",
"module$test.foo = 'bar';"));
}

public void testDontSplitVarsInFor() { public void testDontSplitVarsInFor() {
testModules( testModules(
"test.js", "test.js",
Expand Down

0 comments on commit 3c0fed5

Please sign in to comment.