Package com.google.javascript.jscomp

Source Code of com.google.javascript.jscomp.ProcessCommonJSModules$ProcessCommonJsModulesCallback

/*
* Copyright 2011 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;

import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.AbstractPreOrderCallback;
import com.google.javascript.jscomp.Scope.Var;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;

import java.util.List;
import java.util.regex.Pattern;

/**
* Rewrites a CommonJS module http://wiki.commonjs.org/wiki/Modules/1.1.1
* into a form that can be safely concatenated.
* Does not add a function around the module body but instead adds suffixes
* to global variables to avoid conflicts.
* Calls to require are changed to reference the required module directly.
* goog.provide and goog.require are emitted for closure compiler automatic
* ordering.
*/
public class ProcessCommonJSModules implements CompilerPass {
  public static final String DEFAULT_FILENAME_PREFIX =
      "." + ES6ModuleLoader.MODULE_SLASH;

  private static final String MODULE_NAME_SEPARATOR = "\\$";
  private static final String MODULE_NAME_PREFIX = "module$";

  private static final String EXPORTS = "exports";

  private final Compiler compiler;
  private final ES6ModuleLoader loader;
  private final boolean reportDependencies;

  ProcessCommonJSModules(Compiler compiler, ES6ModuleLoader loader) {
    this(compiler, loader, true);
  }

  ProcessCommonJSModules(Compiler compiler, ES6ModuleLoader loader,
      boolean reportDependencies) {
    this.compiler = compiler;
    this.loader = loader;
    this.reportDependencies = reportDependencies;
  }

  @Override
  public void process(Node externs, Node root) {
    FindGoogProvideOrGoogModule finder = new FindGoogProvideOrGoogModule();
    NodeTraversal.traverse(compiler, root, finder);
    if (finder.found) {
      return;
    }
    NodeTraversal
        .traverse(compiler, root, new ProcessCommonJsModulesCallback());
  }

  String inputToModuleName(CompilerInput input) {
    return toModuleName(loader.getLoadAddress(input));
  }

  /**
   * Turns a filename into a JS identifier that is used for moduleNames in
   * rewritten code. Removes leading ./, replaces / with $, removes trailing .js
   * and replaces - with _. All moduleNames get a "module$" prefix.
   */
  public static String toModuleName(String filename) {
    return MODULE_NAME_PREFIX +
        filename.replaceAll("^\\." + Pattern.quote(ES6ModuleLoader.MODULE_SLASH), "")
            .replaceAll(Pattern.quote(ES6ModuleLoader.MODULE_SLASH), MODULE_NAME_SEPARATOR)
            .replaceAll(Pattern.quote("\\"), MODULE_NAME_SEPARATOR)
            .replaceAll("\\.js$", "")
            .replaceAll("-", "_")
            .replaceAll(":", "_")
            .replaceAll("\\.", "");
  }

  /**
   * Avoid processing if we find the appearance of goog.provide or goog.module.
   *
   * TODO(moz): Let ES6, CommonJS and goog.provide live happily together.
   */
  static class FindGoogProvideOrGoogModule extends AbstractPreOrderCallback {

    private boolean found;

    boolean isFound() {
      return found;
    }

    @Override
    public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
      // Shallow traversal, since we don't need to inspect within function declarations.
      if (parent == null || !parent.isFunction()
          || n == parent.getFirstChild()) {
        if (n.isExprResult()) {
          Node maybeGetProp = n.getFirstChild().getFirstChild();
          if (maybeGetProp != null
              && (maybeGetProp.matchesQualifiedName("goog.provide")
                  || maybeGetProp.matchesQualifiedName("goog.module"))) {
            found = true;
            return false;
          }
        }
        return true;
      }
      return false;
    }
  }

  /**
   * Visits require, every "script" and special module.exports assignments.
   */
  private class ProcessCommonJsModulesCallback extends
      AbstractPostOrderCallback {

    private int scriptNodeCount = 0;
    private List<Node> moduleExportRefs = Lists.newArrayList();
    private List<Node> exportRefs = Lists.newArrayList();

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (n.isCall() && n.getChildCount() == 2 &&
          n.getFirstChild().matchesQualifiedName("require") &&
          n.getChildAtIndex(1).isString()) {
        visitRequireCall(t, n, parent);
      }

      if (n.isScript()) {
        scriptNodeCount++;
        visitScript(t, n);
      }

      if (n.isGetProp() &&
          "module.exports".equals(n.getQualifiedName())) {
        moduleExportRefs.add(n);
      }

      if (n.isName() && EXPORTS.equals(n.getString())) {
        Var v = t.getScope().getVar(n.getString());
        if (v == null || v.isGlobal()) {
          exportRefs.add(n);
        }
      }
    }

    /**
     * Visit require calls. Emit corresponding goog.require and rewrite require
     * to be a direct reference to name of require module.
     */
    private void visitRequireCall(NodeTraversal t, Node require, Node parent) {
      String requireName = require.getChildAtIndex(1).getString();
      String loadAddress = loader.locate(requireName, t.getInput());
      try {
        loader.load(loadAddress);
      } catch (ES6ModuleLoader.LoadFailedException e) {
        t.makeError(require, ES6ModuleLoader.LOAD_ERROR, requireName);
      }

      String moduleName = toModuleName(loadAddress);
      Node moduleRef = IR.name(moduleName).srcref(require);
      parent.replaceChild(require, moduleRef);
      Node script = getCurrentScriptNode(parent);
      if (reportDependencies) {
        t.getInput().addRequire(moduleName);
      }
      // Rewrite require("name").
      script.addChildToFront(IR.exprResult(
          IR.call(IR.getprop(IR.name("goog"), IR.string("require")),
              IR.string(moduleName))).copyInformationFromForTree(require));
      compiler.reportCodeChange();
    }

    /**
     * Emit goog.provide and add suffix to all global vars to avoid conflicts
     * with other modules.
     */
    private void visitScript(NodeTraversal t, Node script) {
      Preconditions.checkArgument(scriptNodeCount == 1,
          "ProcessCommonJSModules supports only one invocation per " +
          "CompilerInput / script node");

      String moduleName = inputToModuleName(t.getInput());

      // Rename vars to not conflict in global scope.
      NodeTraversal.traverse(compiler, script, new SuffixVarsCallback(
          moduleName));

      // Replace all refs to module.exports and exports
      processExports(script, moduleName);
      moduleExportRefs.clear();
      exportRefs.clear();

      // Add goog.provide calls.
      if (reportDependencies) {
        CompilerInput ci = t.getInput();
        ci.addProvide(moduleName);
      }
      script.addChildToFront(IR.exprResult(
          IR.call(IR.getprop(IR.name("goog"), IR.string("provide")),
              IR.string(moduleName))).copyInformationFromForTree(script));

      compiler.reportCodeChange();
    }


    /**
     * Process all references to module.exports and exports.
     *
     * In CommonJS systems, module.exports and exports point to
     * the same object, unless one of them is re-assigned.
     *
     * We handle 2 special forms:
     * 1) Exactly 1 top-level assign to module.exports.
     *    module.exports = ...;
     * 2) Direct reads of exports and module.exports.
     *    This includes assignments to properties of exports,
     *    because these only read the slot itself.
     *    module.exports.prop = ...; // 1 or more times.
     *
     * We do this so that these forms type-check better.
     *
     * All other forms are handled by a more general algorithm.
     */
    private void processExports(Node script, String moduleName) {
      if (hasOneTopLevelModuleExportAssign()) {
        // One top-level assign: transform to
        // /** @const */ var moduleName = rhs
        Node ref = moduleExportRefs.get(0);
        Node newName = IR.name(moduleName);
        newName.putProp(Node.ORIGINALNAME_PROP, ref.getQualifiedName());

        Node newVar = IR.var(newName)
            .copyInformationFromForTree(ref.getParent());
        Node rhsValue = ref.getNext().detachFromParent();
        newVar.getFirstChild().addChildToFront(rhsValue);
        newVar.setJSDocInfo(NodeUtil.createConstantJsDoc());

        // If the rValue is an object literal, check each property to see if
        // it's an alias, and if it is, copy the annotation over.
        // This is a common idiom to export a set of constructors.
        if (rhsValue.isObjectLit()) {
          Scope globalScope = new SyntacticScopeCreator(compiler)
              .createScope(script, null);
          for (Node key = rhsValue.getFirstChild();
               key != null; key = key.getNext()) {
            if (key.getJSDocInfo() == null
                && key.getFirstChild().isName()) {
              Var aliasedVar =
                  globalScope.getVar(key.getFirstChild().getString());
              JSDocInfo info =
                  aliasedVar == null ? null : aliasedVar.getJSDocInfo();
              if (info != null &&
                  info.getVisibility() != JSDocInfo.Visibility.PRIVATE) {
                key.setJSDocInfo(info);
              }
            }
          }
        }

        Node assign = ref.getParent();
        Node exprResult = assign.getParent();
        script.replaceChild(exprResult, newVar);
        return;
      }

      if (!hasExportLValues()) {
        // Transform to:
        //
        // /** @const */ var moduleName = {};
        // moduleName.prop0 = 0; // etc.
        //
        // We consider the 0-ref case a special case of this.
        Node newVar = injectExportsObject(script, moduleName);
        newVar.setJSDocInfo(NodeUtil.createConstantJsDoc());

        for (Node ref : Iterables.concat(moduleExportRefs, exportRefs)) {
          Node newRef = IR.name(moduleName).copyInformationFrom(ref);
          newRef.putProp(Node.ORIGINALNAME_PROP, ref.getQualifiedName());
          ref.getParent().replaceChild(ref, newRef);
        }
        return;
      }

      // The general case:
      // At the beginning, add the stanza:
      // var moduleName = {}; var moduleName$$exports = moduleName;
      // Transform module.exports to moduleName
      // Transform exports to moduleName$$exports
      Node exportsNode = injectExportsObject(script, moduleName);

      for (Node ref : moduleExportRefs) {
        Node newRef = IR.name(moduleName).copyInformationFrom(ref);
        ref.getParent().replaceChild(ref, newRef);
      }

      if (!exportRefs.isEmpty()) {
        String aliasName = "exports$$" + moduleName;
        Node aliasNode = IR.var(IR.name(aliasName), IR.name(moduleName))
            .copyInformationFromForTree(script);
        script.addChildAfter(aliasNode, exportsNode);

        for (Node ref : exportRefs) {
          ref.putProp(Node.ORIGINALNAME_PROP, ref.getString());
          ref.setString(aliasName);
        }
      }
    }

    /**
     * Creates an exports object for this module.
     * var moduleName = {};
     */
    private Node injectExportsObject(Node script, String moduleName) {
      Node varNode = IR.var(IR.name(moduleName), IR.objectlit())
          .copyInformationFromForTree(script);
      script.addChildToFront(varNode);
      return varNode;
    }

    /**
     * Recognize export pattern [1] (see above).
     */
    private boolean hasOneTopLevelModuleExportAssign() {
      return moduleExportRefs.size() == 1 &&
          exportRefs.isEmpty() &&
          isTopLevelAssignLhs(moduleExportRefs.get(0));
    }

    private boolean isTopLevelAssignLhs(Node n) {
      Node parent = n.getParent();
      return parent.isAssign() && n == parent.getFirstChild() &&
          parent.getParent().isExprResult() &&
          parent.getParent().getParent().isScript();
    }

    /**
     * Recognize the opposite of export pattern [2] (see above).
     */
    private boolean hasExportLValues() {
      for (Node ref : Iterables.concat(moduleExportRefs, exportRefs)) {
        if (NodeUtil.isLValue(ref)) {
          return true;
        }
      }
      return false;
    }

    /**
     * Returns next script node in parents.
     */
    private Node getCurrentScriptNode(Node n) {
      while (true) {
        if (n.isScript()) {
          return n;
        }
        n = n.getParent();
      }
    }
  }

  /**
   * Traverses a node tree and appends a suffix to all global variable names.
   */
  private class SuffixVarsCallback extends AbstractPostOrderCallback {
    private final String suffix;

    SuffixVarsCallback(String suffix) {
      this.suffix = suffix;
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      JSDocInfo info = n.getJSDocInfo();
      if (info != null) {
        for (Node typeNode : info.getTypeNodes()) {
          fixTypeNode(t, typeNode);
        }
      }

      if (n.isName()) {
        String name = n.getString();
        if (suffix.equals(name)) {
          return;
        }

        // refs to 'exports' are handled separately.
        if (EXPORTS.equals(name)) {
          return;
        }

        // closure_test_suite looks for test*() functions
        if (compiler.getOptions().exportTestFunctions && name.startsWith("test")) {
          return;
        }

        Scope.Var var = t.getScope().getVar(name);
        if (var != null && var.isGlobal()) {
          n.setString(name + "$$" + suffix);
          n.putProp(Node.ORIGINALNAME_PROP, name);
        }
      }
    }

    /**
     * Replace type name references.
     */
    private void fixTypeNode(NodeTraversal t, Node typeNode) {
      if (typeNode.isString()) {
        String name = typeNode.getString();
        if (ES6ModuleLoader.isRelativeIdentifier(name)) {
          int lastSlash = name.lastIndexOf("/");
          int endIndex = name.indexOf('.', lastSlash);
          String localTypeName = null;
          if (endIndex == -1) {
            endIndex = name.length();
          } else {
            localTypeName = name.substring(endIndex);
          }

          String moduleName = name.substring(0, endIndex);
          String loadAddress = loader.locate(moduleName, t.getInput());
          if (loadAddress == null) {
            t.makeError(typeNode, ES6ModuleLoader.LOAD_ERROR, moduleName);
            return;
          }

          String globalModuleName = toModuleName(loadAddress);
          typeNode.setString(
              localTypeName == null ?
              globalModuleName :
              globalModuleName + localTypeName);
        } else {
          int endIndex = name.indexOf('.');
          if (endIndex == -1) {
            endIndex = name.length();
          }
          String baseName = name.substring(0, endIndex);
          Scope.Var var = t.getScope().getVar(baseName);
          if (var != null && var.isGlobal()) {
            typeNode.setString(baseName + "$$" + suffix + name.substring(endIndex));
            typeNode.putProp(Node.ORIGINALNAME_PROP, name);
          }
        }
      }

      for (Node child = typeNode.getFirstChild(); child != null;
           child = child.getNext()) {
        fixTypeNode(t, child);
      }
    }
  }
}
TOP

Related Classes of com.google.javascript.jscomp.ProcessCommonJSModules$ProcessCommonJsModulesCallback

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.