tsickle
Version:
Transpile TypeScript code to JavaScript with Closure annotations.
1,002 lines • 53.5 kB
JavaScript
"use strict";
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.commonJsToGoogmoduleTransformer = exports.getAmbientModuleSymbol = exports.resolveModuleName = exports.namespaceForImportUrl = exports.getOriginalGoogModuleFromComment = exports.extractModuleMarker = void 0;
const ts = require("typescript");
const path = require("./path");
const transformer_util_1 = require("./transformer_util");
/**
* Returns true if node is a property access of `child` on the identifier
* `parent`.
*/
function isPropertyAccess(node, parent, child) {
if (!ts.isPropertyAccessExpression(node))
return false;
return ts.isIdentifier(node.expression) &&
node.expression.escapedText === parent && node.name.escapedText === child;
}
/** isUseStrict returns true if node is a "use strict"; statement. */
function isUseStrict(node) {
if (node.kind !== ts.SyntaxKind.ExpressionStatement)
return false;
const exprStmt = node;
const expr = exprStmt.expression;
if (expr.kind !== ts.SyntaxKind.StringLiteral)
return false;
const literal = expr;
return literal.text === 'use strict';
}
/**
* TypeScript inserts the following code to mark ES moduels in CommonJS:
* Object.defineProperty(exports, "__esModule", { value: true });
* This matches that code snippet.
*/
function isEsModuleProperty(stmt) {
// We're matching the explicit source text generated by the TS compiler.
// Object.defineProperty(exports, "__esModule", { value: true });
const expr = stmt.expression;
if (!ts.isCallExpression(expr))
return false;
if (!isPropertyAccess(expr.expression, 'Object', 'defineProperty')) {
return false;
}
if (expr.arguments.length !== 3)
return false;
const [exp, esM, val] = expr.arguments;
if (!ts.isIdentifier(exp) || exp.escapedText !== 'exports')
return false;
if (!ts.isStringLiteral(esM) || esM.text !== '__esModule')
return false;
if (!ts.isObjectLiteralExpression(val) || val.properties.length !== 1) {
return false;
}
const prop = val.properties[0];
if (!ts.isPropertyAssignment(prop))
return false;
const ident = prop.name;
if (!ident || !ts.isIdentifier(ident) || ident.text !== 'value')
return false;
return prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
}
/**
* Return `true`, if the statement looks like a `void 0` export initializer
* statement.
*
* TypeScript will assign `void 0` to **most** exported symbols at the
* start of the output file for reasons having to do with CommonJS spec
* compliance. (See https://github.com/microsoft/TypeScript/issues/38552)
* TS uses chained assignment for this up to some arbitrary limit
* (currently 50) so it doesn't have to have a separate statement for
* every assignment.
*
* ```js
* exports.p1 = exports.p2 = ... = exports.p50 = void 0;
* exports.p51 = exports.p52 = ... = exports.pN = void 0;
* ```
* However, closure-compiler will complain about these statements for
* several reasons.
*
* 1. These statements will come before any assignments directly to
* `exports` itself.
* 2. Multiple assignments to `exports.p` are not allowed.
* 3. Each assignment to `exports.p` must be a separate statement.
*
* We must drop these statements.
*
* Ideally we should make sure we don't drop any a statement that represents
* client code that was actually trying to export a value of `void 0`.
* Unfortunately, we've found that we cannot do that reliably without making
* significant changes to the way we handle goog modules.
*
* TypeScript won't generate such initializing assignments for some
* cases. These include the default export and symbols re-exported from
* other modules, but the exact conditions for determining when an
* initialization will appear and when it won't are undocumented.
*
* Also, we discovered that for at least the case of exports defined with
* destructuring, TypeScript won't generate code that we can reliably
* recognize here as being an export.
*
* ```
* // original TS code
* export const {X} = somethingWithAnXProperty;
* ```
*
* ```
* // Looks like this when we see it here.
* exports.X = void 0; // init
* // this `x` gets somehow turned into `exports.x` somewhere after our code
* // runs. We suspect it's done with the substitution API, but aren't sure.
* x = somethingWithAnXProperty.X;
* ```
*
* For now we've decided that the chances of a human actually intentionally
* exporting `void 0` is so low, that the danger of breaking that case is less
* than the danger of us trying something complicated here and still failing
* to catch an assignment we need to remove in some complex case we haven't
* yet discovered.
*/
function checkExportsVoid0Assignment(expr) {
// Verify this looks something like `exports.abc = exports.xyz = void 0;`.
if (!ts.isBinaryExpression(expr))
return false;
if (expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
return false;
// Verify the left side of the expression is an access on `exports`.
if (!ts.isPropertyAccessExpression(expr.left))
return false;
if (!ts.isIdentifier(expr.left.expression))
return false;
if (expr.left.expression.escapedText !== 'exports')
return false;
// If the right side is another `exports.abc = ...` check that to see if we
// eventually hit a `void 0`.
if (ts.isBinaryExpression(expr.right)) {
return checkExportsVoid0Assignment(expr.right);
}
// Verify the right side is exactly "void 0";
if (!ts.isVoidExpression(expr.right))
return false;
if (!ts.isNumericLiteral(expr.right.expression))
return false;
if (expr.right.expression.text !== '0')
return false;
return true;
}
/**
* Returns the string argument if call is of the form
* require('foo')
*/
function extractRequire(call) {
// Verify that the call is a call to require(...).
if (call.expression.kind !== ts.SyntaxKind.Identifier)
return null;
const ident = call.expression;
if (ident.escapedText !== 'require')
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 (arg.kind !== ts.SyntaxKind.StringLiteral)
return null;
return arg;
}
/**
* extractModuleMarker extracts the value of a well known marker symbol from the
* given module symbol. It returns undefined if the symbol wasn't found.
*/
function extractModuleMarker(symbol, name) {
const localSymbol = findLocalInDeclarations(symbol, name);
if (!localSymbol)
return undefined;
return literalTypeOfSymbol(localSymbol);
}
exports.extractModuleMarker = extractModuleMarker;
/**
* findLocalInDeclarations searches for a local name with the given name in all
* declarations of the given symbol. Note that not all declarations are
* containers that can have local symbols.
*/
function findLocalInDeclarations(symbol, name) {
if (!symbol.declarations) {
return undefined;
}
for (const decl of symbol.declarations) {
// This accesses a TypeScript internal API, "locals" of a container.
// This allows declaring special symbols in e.g. d.ts modules as locals
// that cannot be accessed from user code.
const internalDecl = decl;
const locals = internalDecl.locals;
if (!locals)
continue;
const sym = locals.get(ts.escapeLeadingUnderscores(name));
if (sym)
return sym;
}
return undefined;
}
/**
* literalTypeOfSymbol returns the literal type of symbol if it is
* declared in a variable declaration that has a literal type.
*/
function literalTypeOfSymbol(symbol) {
if (!symbol.declarations || symbol.declarations.length === 0) {
return undefined;
}
const varDecl = symbol.declarations[0];
if (!ts.isVariableDeclaration(varDecl))
return undefined;
if (!varDecl.type || !ts.isLiteralTypeNode(varDecl.type))
return undefined;
const literal = varDecl.type.literal;
if (ts.isLiteralExpression(literal))
return literal.text;
if (literal.kind === ts.SyntaxKind.TrueKeyword)
return true;
if (literal.kind === ts.SyntaxKind.FalseKeyword)
return false;
return undefined;
}
/**
* Returns the name of the goog.module, from which the given source file has
* been generated.
*/
function getOriginalGoogModuleFromComment(sf) {
const leadingComments = sf.getFullText().substring(sf.getFullStart(), sf.getLeadingTriviaWidth());
const match = /^\/\/ Original goog.module name: (.*)$/m.exec(leadingComments);
if (match) {
return match[1];
}
return null;
}
exports.getOriginalGoogModuleFromComment = getOriginalGoogModuleFromComment;
/**
* For a given import URL, extracts or finds the namespace to pass to
* `goog.require` in three special cases:
*
* 1) tsickle handles specially encoded URLs starting with `goog:`, e.g. for
* `import 'goog:foo.Bar';`, returns `foo.Bar`.
* 2) source files can contain a special comment, which contains the goog.module
* name.
* 3) ambient modules can contain a special marker symbol
* (`__clutz_actual_namespace`) that overrides the namespace to import.
*
* This is used to mark imports of Closure JavaScript sources and map them back
* to the correct goog.require namespace.
*
* If the given moduleSymbol is undefined, e.g. because tsickle runs with no
* type information available, (2) and (3) are disabled, but (1) works.
*
* If there's no special cased namespace, namespaceForImportUrl returns null.
*
* This is independent of tsickle's regular pathToModuleId conversion logic and
* happens before it.
*/
function namespaceForImportUrl(context, tsickleDiagnostics, tsImport, moduleSymbol) {
if (tsImport.match(/^goog:/))
return tsImport.substring('goog:'.length);
if (!moduleSymbol) {
return null; // No type information available, skip symbol resolution.
}
if (moduleSymbol.valueDeclaration &&
ts.isSourceFile(moduleSymbol.valueDeclaration)) {
return getOriginalGoogModuleFromComment(moduleSymbol.valueDeclaration);
}
const actualNamespaceSymbol = findLocalInDeclarations(moduleSymbol, '__clutz_actual_namespace');
if (!actualNamespaceSymbol)
return null;
const hasMultipleProvides = findLocalInDeclarations(moduleSymbol, '__clutz_multiple_provides');
if (hasMultipleProvides) {
// Report an error...
(0, transformer_util_1.reportDiagnostic)(tsickleDiagnostics, context, `referenced JavaScript module ${tsImport} provides multiple namespaces and cannot be imported by path.`);
// ... but continue producing an emit that effectively references the first
// provided symbol (to continue finding any additional errors).
}
const actualNamespace = literalTypeOfSymbol(actualNamespaceSymbol);
if (actualNamespace === undefined || typeof actualNamespace !== 'string') {
(0, transformer_util_1.reportDiagnostic)(tsickleDiagnostics, context, `referenced module's __clutz_actual_namespace not a variable with a string literal type`);
return null;
}
return actualNamespace;
}
exports.namespaceForImportUrl = namespaceForImportUrl;
/**
* Convert from implicit `import {} from 'pkg'` to a full resolved file name,
* including any `/index` suffix and also resolving path mappings. TypeScript
* and many module loaders support the shorthand, but `goog.module` does not, so
* tsickle needs to resolve the module name shorthand before generating
* `goog.module` names.
*/
function resolveModuleName({ options, moduleResolutionHost }, pathOfImportingFile, imported) {
// The strategy taken here is to use ts.resolveModuleName() to resolve the
// import to a specific path, which resolves any /index and path mappings.
const resolved = ts.resolveModuleName(imported, pathOfImportingFile, options, moduleResolutionHost);
if (!resolved || !resolved.resolvedModule)
return imported;
const resolvedModule = resolved.resolvedModule.resolvedFileName;
// Check if the resolution went into node_modules.
// Note that the ResolvedModule returned by resolveModuleName() has an
// attribute isExternalLibraryImport that is documented with
// "True if resolvedFileName comes from node_modules", but actually it is just
// true if the absolute path includes node_modules, and is always true when
// tsickle itself is under a directory named node_modules.
const relativeResolved = path.relative(options.rootDir || '', resolvedModule);
if (relativeResolved.indexOf('node_modules') !== -1) {
// Imports into node_modules resolve through package.json and must be
// specially handled by the loader anyway. Return the input.
return imported;
}
// Otherwise return the full resolved file name. This path will be turned into
// a module name using AnnotatorHost#pathToModuleName, which also takes care
// of baseUrl and rootDirs.
return resolved.resolvedModule.resolvedFileName;
}
exports.resolveModuleName = resolveModuleName;
/**
* importPathToGoogNamespace converts a TS/ES module './import/path' into a
* goog.module compatible namespace, handling regular imports and `goog:`
* namespace imports.
*/
function importPathToGoogNamespace(host, context, diagnostics, file, tsImport, moduleSymbol) {
let modName;
const nsImport = namespaceForImportUrl(context, diagnostics, tsImport, moduleSymbol);
if (nsImport != null) {
// This is a namespace import, of the form "goog:foo.bar".
// Fix it to just "foo.bar".
modName = nsImport;
}
else {
if (host.convertIndexImportShorthand) {
tsImport = resolveModuleName(host, file.fileName, tsImport);
}
modName = host.pathToModuleName(file.fileName, tsImport);
}
return (0, transformer_util_1.createSingleQuoteStringLiteral)(modName);
}
/**
* Replace "module.exports = ..." with just "exports = ...". Returns null if
* `expr` is not an exports assignment.
*/
function rewriteModuleExportsAssignment(expr) {
if (!ts.isBinaryExpression(expr.expression))
return null;
if (expr.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) {
return null;
}
if (!isPropertyAccess(expr.expression.left, 'module', 'exports'))
return null;
return ts.setOriginalNode(ts.setTextRange(ts.factory.createExpressionStatement(ts.factory.createAssignment(ts.factory.createIdentifier('exports'), expr.expression.right)), expr), expr);
}
/**
* Checks whether expr is of the form `exports.abc = identifier` and if so,
* returns the string abc, otherwise returns null.
*/
function isExportsAssignment(expr) {
// Verify this looks something like `exports.abc = ...`.
if (!ts.isBinaryExpression(expr))
return null;
if (expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
return null;
// Verify the left side of the expression is an access on `exports`.
if (!ts.isPropertyAccessExpression(expr.left))
return null;
if (!ts.isIdentifier(expr.left.expression))
return null;
if (expr.left.expression.escapedText !== 'exports')
return null;
// Check whether right side of assignment is an identifier.
if (!ts.isIdentifier(expr.right))
return null;
// Return the property name as string.
return expr.left.name.escapedText.toString();
}
/**
* Convert a series of comma-separated expressions
* x = foo, y(), z.bar();
* with statements
* x = foo; y(); z.bar();
* This is for handling in particular the case where
* exports.x = ..., exports.y = ...;
* which Closure rejects.
*
* @return An array of statements if it converted, or null otherwise.
*/
function rewriteCommaExpressions(expr) {
// There are two representation for comma expressions:
// 1) a tree of "binary expressions" whose contents are comma operators
const isBinaryCommaExpression = (expr) => ts.isBinaryExpression(expr) &&
expr.operatorToken.kind === ts.SyntaxKind.CommaToken;
// or,
// 2) a "comma list" expression, where the subexpressions are in one array
const isCommaList = (expr) => expr.kind === ts.SyntaxKind.CommaListExpression;
if (!isBinaryCommaExpression(expr) && !isCommaList(expr)) {
return null;
}
// Recursively visit comma-separated subexpressions, and collect them all as
// separate expression statements.
return visit(expr);
function visit(expr) {
if (isBinaryCommaExpression(expr)) {
return visit(expr.left).concat(visit(expr.right));
}
if (isCommaList(expr)) {
// TODO(blickly): Simplify using flatMap once node 11 available
return [].concat(...expr.elements.map(visit));
}
return [ts.setOriginalNode(ts.factory.createExpressionStatement(expr), expr)];
}
}
/**
* getAmbientModuleSymbol returns the module symbol for the module referenced
* by the given URL. It special cases ambient module URLs that cannot be
* resolved (e.g. because they exist on synthesized nodes) and looks those up
* separately.
*/
function getAmbientModuleSymbol(typeChecker, moduleUrl) {
let moduleSymbol = typeChecker.getSymbolAtLocation(moduleUrl);
if (!moduleSymbol) {
// Angular compiler creates import statements that do not retain the
// original AST (parse tree nodes). TypeChecker cannot resolve these
// import statements, thus moduleSymbols ends up undefined.
// The workaround is to resolve the module explicitly, using
// an @internal API. TypeChecker has resolveExternalModuleName, but
// that also relies on finding parse tree nodes.
// Given that the feature that needs this (resolve Closure names) is
// only relevant to ambient modules, we can fall back to the function
// specific to ambient modules.
const t = moduleUrl.text;
moduleSymbol =
// tslint:disable-next-line:no-any see above.
typeChecker.tryFindAmbientModuleWithoutAugmentations(t);
}
return moduleSymbol;
}
exports.getAmbientModuleSymbol = getAmbientModuleSymbol;
/**
* commonJsToGoogmoduleTransformer returns a transformer factory that converts
* TypeScript's CommonJS module emit to Closure Compiler compatible goog.module
* and goog.require statements.
*/
function commonJsToGoogmoduleTransformer(host, modulesManifest, typeChecker) {
return (context) => {
// 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) => {
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;
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;
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) => {
// 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['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();
/**
* 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, call, newIdent) {
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 = [];
const imp = importPathToGoogNamespace(host, importedUrl, ignoredDiagnostics, sf, importedUrl.text, moduleSymbol);
modulesManifest.addReferencedModule(sf.fileName, imp.text);
const existingImport = namespaceToModuleVarName.get(imp.text);
let initializer;
if (!existingImport) {
if (newIdent)
namespaceToModuleVarName.set(imp.text, newIdent);
initializer = (0, transformer_util_1.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 (0, transformer_util_1.createNotEmittedStatementWithComments)(sf, original);
}
const useConst = host.options.target !== ts.ScriptTarget.ES5;
if (newIdent) {
// Create a statement like one of:
// var foo = goog.require('bar');
// var foo = existingImport;
const varDecl = ts.factory.createVariableDeclaration(newIdent, /* exclamationToken */ undefined, /* type */ undefined, initializer);
const newStmt = ts.factory.createVariableStatement(
/* modifiers */ undefined, ts.factory.createVariableDeclarationList([varDecl],
// Use 'const' in ES6 mode so Closure properly forwards type
// aliases.
useConst ? ts.NodeFlags.Const : undefined));
return ts.setOriginalNode(ts.setTextRange(newStmt, original), original);
}
else if (!newIdent && !existingImport) {
// Create a statement like:
// goog.require('bar');
const newStmt = ts.factory.createExpressionStatement(initializer);
return ts.setOriginalNode(ts.setTextRange(newStmt, original), original);
}
return (0, transformer_util_1.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, call) {
// 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 = (0, transformer_util_1.createGoogLoadedModulesRegistration)(arg.text, ts.factory.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) {
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.factory.createExpressionStatement((0, transformer_util_1.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) {
// 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) {
// 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) {
return (p) => {
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.factory.createExpressionStatement(ts.factory.createAssignment(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('exports'), objDefArg2.text), realExportValue)), stmt), stmt);
return exportStmt;
}
// Set of all property names already seen on `exports`.
const exportsSeen = new Set();
/**
* visitTopLevelStatement implements the main CommonJS to goog.module
* conversion. It visits a SourceFile level statement and adds a
* (possibly) transformed representation of it into stmts. It adds at
* least one node per statement to stmts.
*
* 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(stmts, sf, node) {
// Handle each particular case by adding node to stmts, 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) {
stmts.push(rewrittenTsLib);
return;
}
}
switch (node.kind) {
case ts.SyntaxKind.ExpressionStatement: {
const exprStmt = node;
// Check for "use strict" and certain Object.defineProperty and skip
// it if necessary.
if (isUseStrict(exprStmt) || isEsModuleProperty(exprStmt)) {
stmts.push((0, transformer_util_1.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((0, transformer_util_1.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;
}
// Checks whether node is an assignment of the form
// exports.xyz = ...;
// If so, whether there is already a previous assignment
// to the same property. If so, remove all subsequent assignments
// to the property to avoid EXPORT_REPEATED_ERROR from JSCompiler.
const exportName = isExportsAssignment(exprStmt.expression);
if (exportName) {
if (exportsSeen.has(exportName)) {
stmts.push((0, transformer_util_1.createNotEmittedStatementWithComments)(sf, exprStmt));
return;
}
exportsSeen.add(exportName);
}
// 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) {
stmts.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;
if (isExportStar) {
// Extract the goog.require() from the call. (It will be verified
// as a goog.require() below.)
callExpr = expr.arguments[0];
newIdent = ts.factory.createIdentifier(nextModuleVar());
}
// Check whether the call is actually a require() and translate
// as appropriate.
const require = maybeCreateGoogRequire(exprStmt, callExpr, newIdent);
if (!require)
break;
stmts.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 = [newIdent];
if (expr.arguments.length > 1)
args.push(expr.arguments[1]);
stmts.push(ts.factory.createExpressionStatement(ts.factory.createCallExpression(expr.expression, undefined, args)));
}
return;
}
case ts.SyntaxKind.VariableStatement: {
// It's possibly of the form "var x = require(...);".
const varStmt = node;
// 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;
stmts.push(require);
return;
}
default:
break;
}
stmts.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 = [];
for (const stmt of sf.statements) {
visitTopLevelStatement(stmts, sf, stmt);
}
// Additional statements that will be prepended (goog.module call etc).
const headerStmts = [];
// Emit: goog.module('moduleName');
const googModule = ts.factory.createExpressionStatement((0, transformer_util_1.createGoogCall)('module', (0, transformer_util_1.createSingleQuoteStringLiteral)(moduleName)));
headerStmts.push(googModule);
maybeAddModuleId(host, typeChecker, sf, headerStmts);
// 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 tslibModu