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));
};
};
}