export function commonJsToGoogmoduleTransformer()

in src/googmodule.ts [480:1106]


export function commonJsToGoogmoduleTransformer(
    host: GoogModuleProcessorHost, modulesManifest: ModulesManifest,
    typeChecker: ts.TypeChecker): (context: ts.TransformationContext) =>
    ts.Transformer<ts.SourceFile> {
  return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
    // TS' CommonJS processing uses onSubstituteNode to, at the very end of
    // processing, substitute `modulename.someProperty` property accesses and
    // replace them with just `modulename` in two special cases. See below for
    // the cases & motivation.
    const previousOnSubstituteNode = context.onSubstituteNode;
    context.enableSubstitution(ts.SyntaxKind.PropertyAccessExpression);
    context.onSubstituteNode = (hint, node: ts.Node): ts.Node => {
      node = previousOnSubstituteNode(hint, node);
      // Check if this is a property.access.
      if (!ts.isPropertyAccessExpression(node)) return node;
      if (!ts.isIdentifier(node.expression)) return node;
      // Find the import declaration node.expression (the LHS) comes from.
      // This may be the original ImportDeclaration, if the identifier was
      // transformed from it.
      const orig = ts.getOriginalNode(node.expression);
      let importExportDecl: ts.ImportDeclaration|ts.ExportDeclaration;
      if (ts.isImportDeclaration(orig) || ts.isExportDeclaration(orig)) {
        importExportDecl = orig;
      } else {
        // Alternatively, we can try to find the declaration of the symbol. This
        // only works for user-written .default accesses, the generated ones do
        // not have a symbol associated as they are only produced in the
        // CommonJS transformation, after type checking.
        const sym = typeChecker.getSymbolAtLocation(node.expression);
        if (!sym) return node;
        const decls = sym.getDeclarations();
        if (!decls || !decls.length) return node;
        const decl = decls[0];
        if (decl.parent && decl.parent.parent &&
            ts.isImportDeclaration(decl.parent.parent)) {
          importExportDecl = decl.parent.parent;
        } else {
          return node;
        }
      }
      // export declaration with no URL.
      if (!importExportDecl.moduleSpecifier) return node;

      // If the import declaration's URL is a "goog:..." style namespace, then
      // all ".default" accesses on it should be replaced with the symbol
      // itself. This allows referring to the module-level export of a
      // "goog.module" or "goog.provide" as if it was an ES6 default export.
      const isDefaultAccess = node.name.text === 'default';
      const moduleSpecifier =
          importExportDecl.moduleSpecifier as ts.StringLiteral;
      if (isDefaultAccess && moduleSpecifier.text.startsWith('goog:')) {
        // Substitute "foo.default" with just "foo".
        return node.expression;
      }
      // Alternatively, modules may export a well known symbol
      // '__clutz_strip_property'.
      const moduleSymbol = getAmbientModuleSymbol(typeChecker, moduleSpecifier);
      if (!moduleSymbol) return node;
      const stripDefaultNameSymbol =
          findLocalInDeclarations(moduleSymbol, '__clutz_strip_property');
      if (!stripDefaultNameSymbol) return node;
      const stripName = literalTypeOfSymbol(stripDefaultNameSymbol);
      // In this case, emit `modulename` instead of `modulename.property` if and
      // only if the accessed name matches the declared name.
      if (stripName === node.name.text) return node.expression;
      return node;
    };

    return (sf: ts.SourceFile): ts.SourceFile => {
      // In TS2.9, transformers can receive Bundle objects, which this code
      // cannot handle (given that a bundle by definition cannot be a
      // goog.module()). The cast through any is necessary to remain compatible
      // with earlier TS versions.
      // tslint:disable-next-line:no-any
      if ((sf as any)['kind'] !== ts.SyntaxKind.SourceFile) return sf;

      // JS scripts (as opposed to modules), must not be rewritten to
      // goog.modules.
      if (host.isJsTranspilation && !isModule(sf)) {
        return sf;
      }

      let moduleVarCounter = 1;
      /**
       * Creates a new unique variable name for holding an imported module. This
       * is used to split places where TS wants to codegen code like:
       *   someExpression(require(...));
       * which we then rewrite into
       *   var x = require(...); someExpression(x);
       */
      function nextModuleVar() {
        return `tsickle_module_${moduleVarCounter++}_`;
      }

      /**
       * Maps goog.require namespaces to the variable name they are assigned
       * into. E.g.: var $varName = goog.require('$namespace'));
       */
      const namespaceToModuleVarName = new Map<string, ts.Identifier>();

      /**
       * maybeCreateGoogRequire returns a `goog.require()` call for the given
       * CommonJS `require` call. Returns null if `call` is not a CommonJS
       * require.
       *
       * @param newIdent The identifier to assign the result of the goog.require
       *     to, or undefined if no assignment is needed.
       */
      function maybeCreateGoogRequire(
          original: ts.Statement, call: ts.CallExpression,
          newIdent: ts.Identifier|undefined): ts.Statement|null {
        const importedUrl = extractRequire(call);
        if (!importedUrl) return null;
        const moduleSymbol = getAmbientModuleSymbol(typeChecker, importedUrl);
        // if importPathToGoogNamespace reports an error, it has already been
        // reported when originally transforming the file to JS (e.g. to produce
        // the goog.requireType call). Side-effect imports generate no
        // requireType, but given they do not import a symbol, there is also no
        // ambiguity what symbol to import, so not reporting an error for
        // side-effect imports is working as intended.
        const ignoredDiagnostics: ts.Diagnostic[] = [];
        const imp = importPathToGoogNamespace(
            host, importedUrl, ignoredDiagnostics, sf, importedUrl.text,
            moduleSymbol);
        modulesManifest.addReferencedModule(sf.fileName, imp.text);
        const existingImport: ts.Identifier|undefined =
            namespaceToModuleVarName.get(imp.text);
        let initializer: ts.Expression;
        if (!existingImport) {
          if (newIdent) namespaceToModuleVarName.set(imp.text, newIdent);
          initializer = createGoogCall('require', imp);
        } else {
          initializer = existingImport;
        }

        // In JS modules it's recommended that users get a handle on the
        // goog namespace via:
        //
        //    import * as goog from 'google3/javascript/closure/goog.js';
        //
        // In a goog.module we just want to access the global `goog` value,
        // so we skip emitting that import as a goog.require.
        // We check the goog module name so that we also catch relative imports.
        if (newIdent && newIdent.escapedText === 'goog' &&
            imp.text === 'google3.javascript.closure.goog') {
          return createNotEmittedStatementWithComments(sf, original);
        }

        if (newIdent) {
          // Create a statement like one of:
          //   var foo = goog.require('bar');
          //   var foo = existingImport;
          const varDecl = ts.createVariableDeclaration(
              newIdent, /* type */ undefined, initializer);
          const newStmt = ts.createVariableStatement(
              /* modifiers */ undefined,
              ts.createVariableDeclarationList(
                  [varDecl],
                  // Use 'const' in ES6 mode so Closure properly forwards type
                  // aliases.
                  host.es5Mode ? undefined : ts.NodeFlags.Const));
          return ts.setOriginalNode(
              ts.setTextRange(newStmt, original), original);
        } else if (!newIdent && !existingImport) {
          // Create a statement like:
          //   goog.require('bar');
          const newStmt = ts.createExpressionStatement(initializer);
          return ts.setOriginalNode(
              ts.setTextRange(newStmt, original), original);
        }
        return createNotEmittedStatementWithComments(sf, original);
      }

      /**
       * Rewrite goog.declareModuleId to something that works in a goog.module.
       *
       * goog.declareModuleId exposes a JS module as a goog.module. After we
       * convert the JS module to a goog.module, what we really want is to
       * expose the current goog.module at two different module ids. This isn't
       * possible with the public APIs, but we can make it work at runtime
       * by writing a record to goog.loadedModules_.
       *
       * This only works at runtime, and would fail if compiled by closure
       * compiler, but that's ok because we only transpile JS in development
       * mode.
       */
      function maybeRewriteDeclareModuleId(
          original: ts.Statement, call: ts.CallExpression): ts.Statement|null {
        // Verify that the call is a call to goog.declareModuleId(...).
        if (!ts.isPropertyAccessExpression(call.expression)) {
          return null;
        }
        const propAccess = call.expression;
        if (propAccess.name.escapedText !== 'declareModuleId') {
          return null;
        }
        if (!ts.isIdentifier(propAccess.expression) ||
            propAccess.expression.escapedText !== 'goog') {
          return null;
        }

        // Verify the call takes a single string argument and grab it.
        if (call.arguments.length !== 1) {
          return null;
        }
        const arg = call.arguments[0];
        if (!ts.isStringLiteral(arg)) {
          return null;
        }
        const newStmt = createGoogLoadedModulesRegistration(
            arg.text, ts.createIdentifier('exports'));
        return ts.setOriginalNode(ts.setTextRange(newStmt, original), original);
      }

      /**
       * maybeRewriteRequireTslib rewrites a require('tslib') calls to
       * goog.require('tslib'). It returns the input statement untouched if it
       * does not match.
       */
      function maybeRewriteRequireTslib(stmt: ts.Statement): ts.Statement|null {
        if (!ts.isExpressionStatement(stmt)) return null;
        if (!ts.isCallExpression(stmt.expression)) return null;
        const callExpr = stmt.expression;
        if (!ts.isIdentifier(callExpr.expression) ||
            callExpr.expression.text !== 'require') {
          return null;
        }
        if (callExpr.arguments.length !== 1) return stmt;
        const arg = callExpr.arguments[0];
        if (!ts.isStringLiteral(arg) || arg.text !== 'tslib') return null;
        return ts.setOriginalNode(
            ts.setTextRange(
                ts.createStatement(createGoogCall('require', arg)), stmt),
            stmt);
      }

      /**
       * Rewrites code generated by `export * as ns from 'ns'` to something
       * like:
       *
       * ```
       * const tsickle_module_n_ = goog.require('ns');
       * exports.ns = tsickle_module_n_;
       * ```
       *
       * Separating the `goog.require` and `exports.ns` assignment is required
       * by Closure to correctly infer the type of the exported namespace.
       */
      function maybeRewriteExportStarAsNs(stmt: ts.Statement): ts.Statement[]|
          null {
        // Ensure this looks something like `exports.ns = require('ns);`.
        if (!ts.isExpressionStatement(stmt)) return null;
        if (!ts.isBinaryExpression(stmt.expression)) return null;
        if (stmt.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) {
          return null;
        }

        // Ensure the left side of the expression is an access on `exports`.
        if (!ts.isPropertyAccessExpression(stmt.expression.left)) return null;
        if (!ts.isIdentifier(stmt.expression.left.expression)) return null;
        if (stmt.expression.left.expression.escapedText !== 'exports') {
          return null;
        }

        // Grab the call to `require`, and exit early if not calling `require`.
        if (!ts.isCallExpression(stmt.expression.right)) return null;
        const ident = ts.factory.createIdentifier(nextModuleVar());
        const require =
            maybeCreateGoogRequire(stmt, stmt.expression.right, ident);
        if (!require) return null;

        const exportedName = stmt.expression.left.name;
        const exportStmt = ts.setOriginalNode(
            ts.setTextRange(
                ts.factory.createExpressionStatement(
                    ts.factory.createAssignment(
                        ts.factory.createPropertyAccessExpression(
                            ts.factory.createIdentifier('exports'),
                            exportedName),
                        ident)),
                stmt),
            stmt);
        ts.addSyntheticLeadingComment(
            exportStmt, ts.SyntaxKind.MultiLineCommentTrivia, '* @const ',
            /* trailing newline */ true);

        return [require, exportStmt];
      }

      /**
       * When re-exporting an export from another module TypeScript will wrap it
       * with an `Object.defineProperty` and getter function to emulate a live
       * binding, per the ESM spec. goog.module doesn't allow for mutable
       * exports and Closure Compiler doesn't allow `Object.defineProperty` to
       * be used with `exports`, so we rewrite the live binding to look like a
       * plain `exports` assignment. For example, this statement:
       *
       * ```
       * Object.defineProperty(exports, "a", {
       *   enumerable: true, get: function () { return a_1.a; }
       * });
       * ```
       *
       * will be transformed into:
       *
       * ```
       * exports.a = a_1.a;
       * ```
       */
      function rewriteObjectDefinePropertyOnExports(
          stmt: ts.ExpressionStatement): ts.Statement|null {
        // Verify this node is a function call.
        if (!ts.isCallExpression(stmt.expression)) return null;

        // Verify the node being called looks like `a.b`.
        const callExpr = stmt.expression;
        if (!ts.isPropertyAccessExpression(callExpr.expression)) return null;

        // Verify that the `a.b`-ish thing is actully `Object.defineProperty`.
        const propAccess = callExpr.expression;
        if (!ts.isIdentifier(propAccess.expression)) return null;
        if (propAccess.expression.text !== 'Object') return null;
        if (propAccess.name.text !== 'defineProperty') return null;

        // Grab each argument to `Object.defineProperty`, and verify that there
        // are exactly three arguments. The first argument should be the global
        // `exports` object, the second is the exported name as a string
        // literal, and the third is a configuration object.
        if (callExpr.arguments.length !== 3) return null;
        const [objDefArg1, objDefArg2, objDefArg3] = callExpr.arguments;
        if (!ts.isIdentifier(objDefArg1)) return null;
        if (objDefArg1.text !== 'exports') return null;
        if (!ts.isStringLiteral(objDefArg2)) return null;
        if (!ts.isObjectLiteralExpression(objDefArg3)) return null;

        // Returns a "finder" function to location an object property.
        function findPropNamed(name: string) {
          return (p: ts.ObjectLiteralElementLike) => {
            return ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) &&
                p.name.text === name;
          };
        }

        // Verify that the export is marked as enumerable. If it isn't then this
        // was not generated by TypeScript.
        const enumerableConfig =
            objDefArg3.properties.find(findPropNamed('enumerable'));
        if (!enumerableConfig) return null;
        if (!ts.isPropertyAssignment(enumerableConfig)) return null;
        if (enumerableConfig.initializer.kind !== ts.SyntaxKind.TrueKeyword) {
          return null;
        }

        // Verify that the export has a getter function.
        const getConfig = objDefArg3.properties.find(findPropNamed('get'));
        if (!getConfig) return null;
        if (!ts.isPropertyAssignment(getConfig)) return null;
        if (!ts.isFunctionExpression(getConfig.initializer)) return null;

        // Verify that the getter function has exactly one statement that is a
        // return statement. The node being returned is the real exported value.
        const getterFunc = getConfig.initializer;
        if (getterFunc.body.statements.length !== 1) return null;
        const getterReturn = getterFunc.body.statements[0];
        if (!ts.isReturnStatement(getterReturn)) return null;
        const realExportValue = getterReturn.expression;
        if (!realExportValue) return null;

        // Create a new export statement using the exported name found as the
        // second argument to `Object.defineProperty` with the value of the
        // node returned by the getter function.
        const exportStmt = ts.setOriginalNode(
            ts.setTextRange(
                ts.createExpressionStatement(ts.createAssignment(
                    ts.createPropertyAccess(
                        ts.createIdentifier('exports'), objDefArg2.text),
                    realExportValue)),
                stmt),
            stmt);

        return exportStmt;
      }

      /**
       * visitTopLevelStatement implements the main CommonJS to goog.module
       * conversion. It visits a SourceFile level statement and adds a
       * (possibly) transformed representation of it into statements. It adds at
       * least one node per statement to statements.
       *
       * visitTopLevelStatement:
       * - converts require() calls to goog.require() calls, with or w/o var
       * assignment
       * - removes "use strict"; and "Object.defineProperty(__esModule)"
       * statements
       * - converts module.exports assignments to just exports assignments
       * - splits __exportStar() calls into require and export (this needs two
       * statements)
       * - makes sure to only import each namespace exactly once, and use
       * variables later on
       */
      function visitTopLevelStatement(
          statements: ts.Statement[], sf: ts.SourceFile,
          node: ts.Statement): void {
        // Handle each particular case by adding node to statements, then
        // return. For unhandled cases, break to jump to the default handling
        // below.

        // In JS transpilation mode, always rewrite `require('tslib')` to
        // goog.require('tslib'), ignoring normal module resolution.
        if (host.isJsTranspilation) {
          const rewrittenTsLib = maybeRewriteRequireTslib(node);
          if (rewrittenTsLib) {
            statements.push(rewrittenTsLib);
            return;
          }
        }

        switch (node.kind) {
          case ts.SyntaxKind.ExpressionStatement: {
            const exprStmt = node as ts.ExpressionStatement;
            // Check for "use strict" and certain Object.defineProperty and skip
            // it if necessary.
            if (isUseStrict(exprStmt) || isEsModuleProperty(exprStmt)) {
              stmts.push(createNotEmittedStatementWithComments(sf, exprStmt));
              return;
            }

            // If we have not already seen the defaulted export assignment
            // initializing all exports to `void 0`, skip the statement and mark
            // that we have have now seen it.
            if (checkExportsVoid0Assignment(exprStmt.expression)) {
              stmts.push(createNotEmittedStatementWithComments(sf, exprStmt));
              return;
            }

            // Check for:
            //   module.exports = ...;
            const modExports = rewriteModuleExportsAssignment(exprStmt);
            if (modExports) {
              stmts.push(modExports);
              return;
            }
            // Check for use of the comma operator.
            // This occurs in code like
            //   exports.a = ..., exports.b = ...;
            // which we want to change into multiple statements.
            const commaExpanded = rewriteCommaExpressions(exprStmt.expression);
            if (commaExpanded) {
              stmts.push(...commaExpanded);
              return;
            }
            // Check for:
            //   exports.ns = require('...');
            // which is generated by the `export * as ns from` syntax.
            const exportStarAsNs = maybeRewriteExportStarAsNs(exprStmt);
            if (exportStarAsNs) {
              stmts.push(...exportStarAsNs);
              return;
            }

            // Checks for:
            //   Object.defineProperty(exports, 'a', {
            //     enumerable: true, get: { return ...; }
            //   })
            // which is a live binding generated when re-exporting from another
            // module.
            const exportFromObjDefProp =
                rewriteObjectDefinePropertyOnExports(exprStmt);
            if (exportFromObjDefProp) {
              stmts.push(exportFromObjDefProp);
              return;
            }

            // The rest of this block handles only some function call forms:
            //   goog.declareModuleId(...);
            //   require('foo');
            //   __exportStar(require('foo'), ...);
            const expr = exprStmt.expression;
            if (!ts.isCallExpression(expr)) break;
            let callExpr = expr;

            // Check for declareModuleId.
            const declaredModuleId =
                maybeRewriteDeclareModuleId(exprStmt, callExpr);
            if (declaredModuleId) {
              statements.push(declaredModuleId);
              return;
            }

            // Check for __exportStar, the commonjs version of 'export *'.
            // export * creates either a pure top-level '__export(require(...))'
            // or the imported version, 'tslib.__exportStar(require(...))'. The
            // imported version is only substituted later on though, so appears
            // as a plain "__exportStar" on the top level here.
            const isExportStar = ts.isIdentifier(expr.expression) &&
                (expr.expression.text === '__exportStar' ||
                 expr.expression.text === '__export');
            let newIdent: ts.Identifier|undefined;
            if (isExportStar) {
              // Extract the goog.require() from the call. (It will be verified
              // as a goog.require() below.)
              callExpr = expr.arguments[0] as ts.CallExpression;
              newIdent = ts.createIdentifier(nextModuleVar());
            }

            // Check whether the call is actually a require() and translate
            // as appropriate.
            const require =
                maybeCreateGoogRequire(exprStmt, callExpr, newIdent);
            if (!require) break;
            statements.push(require);

            // If this was an export star, split it up into the import (created
            // by the maybe call above), and the export operation. This avoids a
            // Closure complaint about non-top-level requires.
            if (isExportStar) {
              const args: ts.Expression[] = [newIdent!];
              if (expr.arguments.length > 1) args.push(expr.arguments[1]);
              statements.push(ts.createExpressionStatement(
                  ts.createCall(expr.expression, undefined, args)));
            }
            return;
          }
          case ts.SyntaxKind.VariableStatement: {
            // It's possibly of the form "var x = require(...);".
            const varStmt = node as ts.VariableStatement;
            // Verify it's a single decl (and not "var x = ..., y = ...;").
            if (varStmt.declarationList.declarations.length !== 1) break;
            const decl = varStmt.declarationList.declarations[0];

            // Grab the variable name (avoiding things like destructuring
            // binds).
            if (decl.name.kind !== ts.SyntaxKind.Identifier) break;
            if (!decl.initializer || !ts.isCallExpression(decl.initializer)) {
              break;
            }
            const require =
                maybeCreateGoogRequire(varStmt, decl.initializer, decl.name);
            if (!require) break;
            statements.push(require);
            return;
          }
          default:
            break;
        }
        statements.push(node);
      }

      const moduleName = host.pathToModuleName('', sf.fileName);
      // Register the namespace this file provides.
      modulesManifest.addModule(sf.fileName, moduleName);

      // Convert each top level statement to goog.module.
      const stmts: ts.Statement[] = [];
      for (const stmt of sf.statements) {
        visitTopLevelStatement(stmts, sf, stmt);
      }

      // Additional statements that will be prepended (goog.module call etc).
      const headerStmts: ts.Statement[] = [];

      // Emit: goog.module('moduleName');
      const googModule = ts.createStatement(
          createGoogCall('module', createSingleQuoteStringLiteral(moduleName)));
      headerStmts.push(googModule);

      // Allow code to use `module.id` to discover its module URL, e.g. to
      // resolve a template URL against. Uses 'var', as this code is inserted in
      // ES6 and ES5 modes. The following pattern ensures closure doesn't throw
      // an error in advanced optimizations mode. var module = module || {id:
      // 'path/to/module.ts'};
      const moduleId = host.fileNameToModuleId(sf.fileName);
      const moduleVarInitializer = ts.createBinary(
          ts.createIdentifier('module'), ts.SyntaxKind.BarBarToken,
          ts.createObjectLiteral([ts.createPropertyAssignment(
              'id', createSingleQuoteStringLiteral(moduleId))]));
      const modAssign = ts.createVariableStatement(
          /* modifiers */ undefined,
          ts.createVariableDeclarationList([ts.createVariableDeclaration(
              'module', /* type */ undefined, moduleVarInitializer)]));
      headerStmts.push(modAssign);

      // Add `goog.require('tslib');` if not JS transpilation, and it hasn't
      // already been required. Rationale: TS gets compiled to Development mode
      // (ES5) and Closure mode (~ES6) sources. Tooling generates module
      // manifests from the Closure version. These manifests are used both with
      // the Closure version and the Development mode version. 'tslib' is
      // sometimes required by the development version but not the Closure
      // version. Inserting the import below unconditionally makes sure that the
      // module manifests are identical between Closure and Development mode,
      // avoiding breakages caused by missing module dependencies.
      if (!host.isJsTranspilation) {
        // Get a copy of the already resolved module names before calling
        // resolveModuleName on 'tslib'. Otherwise, resolveModuleName will
        // add 'tslib' to namespaceToModuleVarName and prevent checking whether
        // 'tslib' has already been required.
        const resolvedModuleNames = [...namespaceToModuleVarName.keys()];

        const tslibModuleName = host.pathToModuleName(
            sf.fileName, resolveModuleName(host, sf.fileName, 'tslib'));

        // Only add the extra require if it hasn't already been required
        if (resolvedModuleNames.indexOf(tslibModuleName) === -1) {
          const tslibImport = ts.createExpressionStatement(createGoogCall(
              'require', createSingleQuoteStringLiteral(tslibModuleName)));

          // Place the goog.require('tslib') statement right after the
          // goog.module statements
          headerStmts.push(tslibImport);
        }
      }
      // Insert goog.module() etc after any leading comments in the source file.
      // The comments have been converted to NotEmittedStatements by
      // transformer_util, which this depends on.
      const insertionIdx =
          stmts.findIndex(s => s.kind !== ts.SyntaxKind.NotEmittedStatement);
      if (insertionIdx === -1) {
        stmts.push(...headerStmts);
      } else {
        stmts.splice(insertionIdx, 0, ...headerStmts);
      }

      return ts.updateSourceFileNode(
          sf, ts.setTextRange(ts.createNodeArray(stmts), sf.statements));
    };
  };
}