UNPKG

tsickle

Version:

Transpile TypeScript code to JavaScript with Closure annotations.

1,002 lines 53.5 kB
"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