UNPKG

tsickle

Version:

Transpile TypeScript code to JavaScript with Closure annotations.

451 lines 21.5 kB
"use strict"; /** * @fileoverview * @suppress {untranspilableFeatures} ES2018 feature "RegExp named groups" * @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.createTsMigrationExportsShimTransformerFactory = void 0; const ts = require("typescript"); const transformer_util_1 = require("./transformer_util"); /** * Creates a transformer that eliminates goog.tsMigration*ExportsShim (tsmes) * statements and generates appropriate shim file content. If requested in the * TypeScript compiler options, it will also produce a `.d.ts` file. * * Files are stored in outputFileMap, the caller must make sure to emit them. * * This transformation will always report an error if * `generateTsMigrationExportsShim` is false. */ function createTsMigrationExportsShimTransformerFactory(typeChecker, host, manifest, tsickleDiagnostics, outputFileMap) { return (context) => { return (src) => { const srcFilename = host.rootDirsRelative(src.fileName); const srcModuleId = host.pathToModuleName('', src.fileName); const srcIds = new FileIdGroup(srcFilename, srcModuleId); const generator = new Generator(src, srcIds, typeChecker, host, manifest, tsickleDiagnostics); const tsmesFile = srcIds.google3PathWithoutExtension() + '.tsmes.js'; const dtsFile = srcIds.google3PathWithoutExtension() + '.tsmes.d.ts'; if (!generator.foundMigrationExportsShim()) { // If there is no export shims calls, we still need to generate empty // files, so that we always produce a predictable set of files. // TODO(martinprobst): the empty files might cause issues with code // that should be in mods or modules. outputFileMap.set(tsmesFile, ''); if (context.getCompilerOptions().declaration) { outputFileMap.set(dtsFile, ''); } return src; } const result = generator.generateExportShimJavaScript(); outputFileMap.set(tsmesFile, result); if (context.getCompilerOptions().declaration) { const dtsResult = generator.generateExportShimDeclarations(); outputFileMap.set(dtsFile, dtsResult); } return generator.transformSourceFile(); }; }; } exports.createTsMigrationExportsShimTransformerFactory = createTsMigrationExportsShimTransformerFactory; function stripSupportedExtensions(path) { return path.replace(SUPPORTED_EXTENSIONS, ''); } // .ts but not .d.ts const SUPPORTED_EXTENSIONS = /(?<!\.d)\.ts$/; /** A one-time-use object for running the tranformation. */ class Generator { constructor(src, srcIds, typeChecker, host, manifest, diagnostics) { this.src = src; this.srcIds = srcIds; this.typeChecker = typeChecker; this.host = host; this.manifest = manifest; this.diagnostics = diagnostics; // TODO(martinprobst): Generator is only partially initialized in its // constructor and the object is unusable in case no shim call is found, // with all subsequent methods checking whether it was initialized. // Instead, extractTsmesStatement should construct this generator object (or // return undefined if none), which would then encapsulate state that's // guaranteed to be initialized (no more |undefined). const moduleSymbol = this.typeChecker.getSymbolAtLocation(this.src); this.mainExports = moduleSymbol ? this.typeChecker.getExportsOfModule(moduleSymbol) : []; const outputFilename = this.srcIds.google3PathWithoutExtension() + '.tsmes.closure.js'; this.tsmesBreakdown = this.extractTsmesStatement(); if (this.tsmesBreakdown) { this.outputIds = new FileIdGroup(outputFilename, this.tsmesBreakdown.googModuleId.text); } } /** * Returns whether there were any migration exports shim calls in the source * file. */ foundMigrationExportsShim() { return !!this.tsmesBreakdown; } /** * Finds the top-level call to tsmes in the input, if any, * and returns the relevant info from within. * * If no such call exists, or the call is malformed, returns undefined. * Diagnostics about malformed calls will also be logged. */ extractTsmesStatement() { const startDiagnosticsCount = this.diagnostics.length; let tsmesCallStatement = undefined; let tsmesDlnCallStatement = undefined; for (const statement of this.src.statements) { const isTsmesCall = ts.isExpressionStatement(statement) && (0, transformer_util_1.isAnyTsmesCall)(statement.expression); const isTsmesDlnCall = ts.isExpressionStatement(statement) && (0, transformer_util_1.isTsmesDeclareLegacyNamespaceCall)(statement.expression); if (!isTsmesCall && !isTsmesDlnCall) { this.checkNonTopLevelTsmesCalls(statement); continue; } if (isTsmesCall) { if (tsmesCallStatement) { this.report(tsmesCallStatement, 'at most one call to any of goog.tsMigrationExportsShim, ' + 'goog.tsMigrationDefaultExportsShim, ' + 'goog.tsMigrationNamedExportsShim is allowed per file'); } else { tsmesCallStatement = statement; } } else if (isTsmesDlnCall) { if (tsmesDlnCallStatement) { this.report(tsmesDlnCallStatement, 'at most one call to ' + 'goog.tsMigrationExportsShimDeclareLegacyNamespace ' + 'is allowed per file'); } else { tsmesDlnCallStatement = statement; } } } if (!tsmesCallStatement) { if (tsmesDlnCallStatement) { this.report(tsmesDlnCallStatement, 'goog.tsMigrationExportsShimDeclareLegacyNamespace requires a ' + 'goog.tsMigration*ExportsShim call as well'); return undefined; } return undefined; } else if (!this.host.generateTsMigrationExportsShim) { this.report(tsmesCallStatement, 'calls to goog.tsMigration*ExportsShim are not enabled. Please set' + ' generate_ts_migration_exports_shim = True' + ' in the BUILD file to enable this feature.'); return undefined; } const tsmesCall = tsmesCallStatement.expression; if ((0, transformer_util_1.isGoogCallExpressionOf)(tsmesCall, 'tsMigrationExportsShim') && tsmesCall.arguments.length !== 2) { this.report(tsmesCall, 'goog.tsMigrationExportsShim requires 2 arguments'); return undefined; } if ((0, transformer_util_1.isTsmesShorthandCall)(tsmesCall) && tsmesCall.arguments.length !== 1) { this.report(tsmesCall, `goog.${(0, transformer_util_1.getGoogFunctionName)(tsmesCall)} requires exactly one argument`); return undefined; } if ((0, transformer_util_1.isGoogCallExpressionOf)(tsmesCall, 'tsMigrationDefaultExportsShim') && this.mainExports.length !== 1) { this.report(tsmesCall, 'can only call goog.tsMigrationDefaultExportsShim when there is' + ' exactly one export.'); return undefined; } const [moduleId, exportsExpr] = tsmesCall.arguments; if (!ts.isStringLiteral(moduleId)) { this.report(moduleId, `goog.${(0, transformer_util_1.getGoogFunctionName)(tsmesCall)} ID must be a string literal`); return undefined; } let googExports = undefined; const fnName = (0, transformer_util_1.getGoogFunctionName)(tsmesCall); switch (fnName) { case 'tsMigrationDefaultExportsShim': // Export the one and only export as an unnamed export. // vis. export = foo; googExports = this.mainExports[0].name; break; case 'tsMigrationNamedExportsShim': // Export all exports as named exports // vis. export.a = a; // export.b = b; googExports = new Map(); for (const mainExport of this.mainExports) { googExports.set(mainExport.name, mainExport.name); } break; case 'tsMigrationExportsShim': // Export the structure described by exportsExpr googExports = this.extractGoogExports(exportsExpr); break; default: throw new Error(`encountered unhandled goog.$fnName: ${fnName}`); } if (googExports === undefined) { if (startDiagnosticsCount >= this.diagnostics.length) { throw new Error('googExports should be defined unless some diagnostic is reported.'); } return undefined; } return { callStatement: tsmesCallStatement, googModuleId: moduleId, googExports, declareLegacyNamespaceStatement: tsmesDlnCallStatement, }; } /** * Given the exports from a tsmes call, return a simplified model of the * relevant values. * * If the exports are malformed, returns undefined. Diagnostics about * malformed exports are also logged. */ extractGoogExports(exportsExpr) { let googExports; const diagnosticCount = this.diagnostics.length; if (ts.isObjectLiteralExpression(exportsExpr)) { googExports = new Map(); for (const property of exportsExpr.properties) { if (ts.isShorthandPropertyAssignment(property)) { // {Bar} const symbol = this.typeChecker.getShorthandAssignmentValueSymbol(property); this.checkIsModuleExport(property.name, symbol); googExports.set(property.name.text, property.name.text); } else if (ts.isPropertyAssignment(property)) { // {Foo: Bar} const name = property.name; if (!ts.isIdentifier(name)) { this.report(name, 'export names must be simple keys'); continue; } const initializer = property.initializer; let identifier = null; if (ts.isAsExpression(initializer)) { identifier = this.maybeExtractTypeName(initializer); } else if (ts.isIdentifier(initializer)) { identifier = initializer; } else { this.report(initializer, 'export values must be plain identifiers'); continue; } if (identifier == null) { continue; } const symbol = this.typeChecker.getSymbolAtLocation(identifier); this.checkIsModuleExport(identifier, symbol); googExports.set(name.text, identifier.text); } else { this.report(property, `exports object must only contain (shorthand) properties`); } } } else if (ts.isIdentifier(exportsExpr)) { const symbol = this.typeChecker.getSymbolAtLocation(exportsExpr); this.checkIsModuleExport(exportsExpr, symbol); googExports = exportsExpr.text; } else if (ts.isAsExpression(exportsExpr)) { // {} as DefaultTypeExport const identifier = this.maybeExtractTypeName(exportsExpr); if (!identifier) { return undefined; } const symbol = this.typeChecker.getSymbolAtLocation(identifier); this.checkIsModuleExport(identifier, symbol); googExports = identifier.text; } else { this.report(exportsExpr, `exports object must be either an object literal ({A, B}) or the ` + `identifier of a module export (A)`); } return (diagnosticCount === this.diagnostics.length) ? googExports : undefined; } maybeExtractTypeName(cast) { if (!ts.isObjectLiteralExpression(cast.expression) || cast.expression.properties.length !== 0) { this.report(cast.expression, 'must be object literal with no keys'); return null; } const typeRef = cast.type; if (!ts.isTypeReferenceNode(typeRef)) { this.report(typeRef, 'must be a type reference'); return null; } const typeName = typeRef.typeName; if (typeRef.typeArguments || !ts.isIdentifier(typeName)) { this.report(typeRef, 'export types must be plain identifiers'); return null; } return typeName; } /** * Recurse through top-level statments looking for tsmes calls. * * tsmes is only allowed as a top-level statement, so if we find it deeper * down we report an error. */ checkNonTopLevelTsmesCalls(topLevelStatement) { const inner = (node) => { if ((0, transformer_util_1.isAnyTsmesCall)(node) || (0, transformer_util_1.isTsmesDeclareLegacyNamespaceCall)(node)) { const name = (0, transformer_util_1.getGoogFunctionName)(node); this.report(node, `goog.${name} is only allowed in top level statements`); } ts.forEachChild(node, inner); }; ts.forEachChild(topLevelStatement, inner); } /** * Generate the JS file that other JS files will goog.require to use the * shimmed export layout. * * NOTE: This code must be written to be compatible as-is with IE11. */ generateExportShimJavaScript() { if (!this.outputIds || !this.tsmesBreakdown) { throw new Error('tsmes call must be extracted first'); } let maybeDeclareLegacyNameCall = undefined; if (this.tsmesBreakdown.declareLegacyNamespaceStatement) { maybeDeclareLegacyNameCall = 'goog.module.declareLegacyNamespace();'; } // Note: We don't do a destructure here as that's not compatible with IE11. const mainModuleRequire = `var mainModule = goog.require('${this.srcIds.googModuleId}');`; let exportsAssignment; if (this.tsmesBreakdown.googExports instanceof Map) { // In the case that tsmes was passed named exports. const exports = Array.from(this.tsmesBreakdown.googExports) .map(([k, v]) => `exports.${k} = mainModule.${v};`); exportsAssignment = lines(...exports); } else { // In the case that tsmes was passed a default export. exportsAssignment = `exports = mainModule.${this.tsmesBreakdown.googExports};`; } this.manifest.addModule(this.outputIds.google3Path, this.outputIds.googModuleId); this.manifest.addReferencedModule(this.outputIds.google3Path, this.srcIds.googModuleId); const pintoModuleAnnotation = containsAtPintoModule(this.src) ? '@pintomodule found in original_file' : 'pintomodule absent in original_file'; return lines('/**', ' * @fileoverview generator:ts_migration_exports_shim.ts', ' * original_file:' + this.srcIds.google3Path, ` * ${pintoModuleAnnotation}`, ' */', `goog.module('${this.outputIds.googModuleId}');`, maybeDeclareLegacyNameCall, mainModuleRequire, exportsAssignment, ''); } /** * Generate the .d.ts file that approximates what clutz would generate for the * file produced by generateTsmesJs. * * Since no JS library holds the generated JS file, clutz will never run over * that code. This .d.ts is needed for downstream TS libraries to know about * the shimmed export types. */ generateExportShimDeclarations() { if (!this.outputIds || !this.tsmesBreakdown) { throw new Error('tsmes call must be extracted first'); } const generatedFromComment = '// Generated from ' + this.srcIds.google3Path; const dependencyFileImports = lines(`declare module 'ಠ_ಠ.clutz._dependencies' {`, ` import '${this.srcIds.esModuleImportPath()}';`, `}`); let clutzNamespaceDeclaration; let googColonModuleDeclaration; if (this.tsmesBreakdown.googExports instanceof Map) { // In the case that tsmes was passed named exports. const clutzNamespace = this.srcIds.clutzNamespace(); const clutzNamespaceReexports = Array.from(this.tsmesBreakdown.googExports) .map(([k, v]) => ` export import ${k} = ${clutzNamespace}.${v};`); clutzNamespaceDeclaration = lines(generatedFromComment, `declare namespace ${this.outputIds.clutzNamespace()} {`, ...clutzNamespaceReexports, `}`); googColonModuleDeclaration = lines(generatedFromComment, `declare module '${this.outputIds.clutzModuleId()}' {`, ` import x = ${this.outputIds.clutzNamespace()};`, ` export = x;`, `}`); } else { // In the case that tsmes was passed a default export. clutzNamespaceDeclaration = lines(generatedFromComment, `declare namespace ಠ_ಠ.clutz {`, ` export import ${this.outputIds.googModuleRewrittenId()} =`, ` ${this.srcIds.clutzNamespace()}.${this.tsmesBreakdown.googExports};`, `}`); googColonModuleDeclaration = lines(generatedFromComment, `declare module '${this.outputIds.clutzModuleId()}' {`, ` import x = ${this.outputIds.clutzNamespace()};`, ` export default x;`, `}`); } return lines('/**', ' * @fileoverview generator:ts_migration_exports_shim.ts', ' */', dependencyFileImports, clutzNamespaceDeclaration, googColonModuleDeclaration, ''); } /** * Strips the goog.tsMigrationNamedExportsShim (etc) calls from source file. */ transformSourceFile() { if (!this.outputIds || !this.tsmesBreakdown) { throw new Error('tsmes call must be extracted first'); } const outputStatements = [...this.src.statements]; const tsmesIndex = outputStatements.indexOf(this.tsmesBreakdown.callStatement); if (tsmesIndex < 0) { throw new Error('could not find tsmes call in file'); } // Just delete the tsmes call. outputStatements.splice(tsmesIndex, 1); if (this.tsmesBreakdown.declareLegacyNamespaceStatement) { const dlnIndex = outputStatements.indexOf(this.tsmesBreakdown.declareLegacyNamespaceStatement); if (dlnIndex < 0) { throw new Error('could not find the tsmes declareLegacyNamespace call in file'); } // Also delete the tsmes declareLegacyNamespace call. outputStatements.splice(dlnIndex, 1); } return ts.factory.updateSourceFile(this.src, ts.setTextRange(ts.factory.createNodeArray(outputStatements), this.src.statements)); } checkIsModuleExport(node, symbol) { if (!symbol) { this.report(node, `could not resolve symbol of exported property`); } else if (this.mainExports.indexOf(symbol) === -1) { this.report(node, `export must be an exported symbol of the module`); } else { return true; } return false; } report(node, messageText) { (0, transformer_util_1.reportDiagnostic)(this.diagnostics, node, messageText, undefined, ts.DiagnosticCategory.Error); } } function lines(...lines) { return lines.filter(line => line != null).join('\n'); } /** * The set of IDs associated with a single file. * * Each file can be identified in multiple ways, many of which are derivatives * of one another. */ class FileIdGroup { constructor(google3Path, googModuleId) { this.google3Path = google3Path; this.googModuleId = googModuleId; } google3PathWithoutExtension() { return stripSupportedExtensions(this.google3Path); } esModuleImportPath() { return 'google3/' + this.google3PathWithoutExtension(); } googModuleRewrittenId() { return 'module$exports$' + this.googModuleId.replace(/\./g, '$'); } clutzNamespace() { return 'ಠ_ಠ.clutz.' + this.googModuleRewrittenId(); } clutzModuleId() { return 'goog:' + this.googModuleId; } } function containsAtPintoModule(file) { const leadingTrivia = file.getFullText().substring(0, file.getLeadingTriviaWidth()); return /\s@pintomodule\s/.test(leadingTrivia); } //# sourceMappingURL=ts_migration_exports_shim.js.map