UNPKG

tsickle

Version:

Transpile TypeScript code to JavaScript with Closure annotations.

376 lines 18 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.makeDeclarationTransformerFactory = void 0; /** * @fileoverview Interactions with http://github.com/angular/clutz, which * is the inverse of Tsickle: Clutz converts Closure-annotated JS to d.ts files. * * It turns out when interacting with Clutz code, tsickle needs to be aware of * Clutz in a few ways. The functionality in this module is only needed in apps * that use both Clutz and Tsickle together. */ const ts = require("typescript"); const googmodule = require("./googmodule"); const path = require("./path"); /** * Constructs a ts.CustomTransformerFactory that postprocesses the .d.ts * that are generated by ordinary TypeScript compilations to add some * Clutz-specific logic. See generateClutzAliases. * * @param pathToModuleName Converts an absolute path to its dotted Closure name. */ function makeDeclarationTransformerFactory(typeChecker, pathToModuleName) { return (context) => { return { transformBundle() { // The TS API wants declaration transfomers to be able to handle Bundle, // but we don't support them within tsickle. throw new Error('did not expect to transform a bundle'); }, transformSourceFile(file) { const options = context.getCompilerOptions(); // Construct // import 'path/to/clutz_dts_file'; // for Clutz imports. // Humans write Clutz imports like // import 'goog:foo'; // or // import 'path/to/the/js_file'; // so to for that import to resolve, you need to first import the clutz // d.ts that defines that declared module. const imports = gatherNecessaryClutzImports(typeChecker, file); let importStmts; if (imports.length > 0) { importStmts = imports.map(fileName => { fileName = path.relative(options.rootDir, fileName); return ts.factory.createImportDeclaration( /* decorators */ undefined, /* modifiers */ undefined, /* importClause */ undefined, /* moduleSpecifier */ ts.factory.createStringLiteral(fileName)); }); } // Construct `declare global {}` in the Clutz namespace for symbols // Clutz might use. const globalBlock = generateClutzAliases(file, pathToModuleName(file.fileName), typeChecker, options); // Only need to transform file if we needed one of the above additions. if (!importStmts && !globalBlock) return file; return ts.factory.updateSourceFile(file, [ ...(importStmts !== null && importStmts !== void 0 ? importStmts : []), ...file.statements, ...(globalBlock ? [globalBlock] : []), ]); } }; }; } exports.makeDeclarationTransformerFactory = makeDeclarationTransformerFactory; /** Compares two strings and returns a number suitable for use in sort(). */ function stringCompare(a, b) { if (a < b) return -1; if (a > b) return 1; return 0; } /** * A tsickle produced declaration file might be consumed by Clutz * produced .d.ts files, which use symbol names based on Closure's internal * naming conventions, so we need to provide aliases for all the exported * symbols in the Clutz naming convention. */ function generateClutzAliases(sourceFile, moduleName, typeChecker, options) { var _a; const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile); const moduleExports = moduleSymbol && typeChecker.getExportsOfModule(moduleSymbol); if (!moduleExports) return undefined; // .d.ts files can be transformed, too, so we need to compare the original // node below. const origSourceFile = ts.getOriginalNode(sourceFile); // In order to write aliases, the exported symbols need to be available in the // the module scope. That is not always the case: // // export // 1) export const X; // works // // reexport // 2) export {X} from './foo'; // doesn't // // imported reexport // 3) import {X} from './foo'; // works // export {X} from './foo'; // // getExportsOfModule returns all three types, but we need to separate 2). // For now we 'fix' 2) by simply not emitting a clutz alias, since clutz // interop is used in minority of scenarios. // // TODO(radokirov): attempt to add appropriate imports for 2) so that // currently finding out local appears even harder than fixing exports. const localExports = moduleExports.filter(e => { // If there are no declarations, be conservative and don't emit the aliases. // I don't know how can this happen, we have no tests that excercise it. if (!e.declarations) return false; // Skip default exports, they are not currently supported. // default is a keyword in typescript, so the name of the export being // default means that it's a default export. if (e.name === 'default') return false; // Use the declaration location to determine separate cases above. for (const d of e.declarations) { // This is a special case for export *. Technically, it is outside the // three cases outlined, but at this point we have rewritten it to a // reexport or an imported reexport. However, it appears that the // rewriting also has made it behave different from explicit named export // in the sense that the declaration appears to point at the original // location not the reexport location. Since we can't figure out whether // there is a local import here, we err on the side of less emit. if (d.getSourceFile() !== origSourceFile) { return false; } // @internal marked APIs are not exported, so must not get aliases. // This uses an internal TS API, assuming that accessing this will be // more stable compared to implementing our own version. // tslint:disable-next-line:no-any const isInternalDeclaration = ts['isInternalDeclaration']; if (options.stripInternal && isInternalDeclaration(d, origSourceFile)) { return false; } if (!ts.isExportSpecifier(d)) { // we have a pure export (case 1) thus safe to emit clutz alias. return true; } // The declaration d is useless to separate reexport and import-reexport // because they both point to the reexporting file and not to the original // one. However, there is another ts API that can do a deeper resolution. const localSymbol = typeChecker.getExportSpecifierLocalTargetSymbol(d); // I don't know how can this happen, but err on the side of less emit. if (!localSymbol) return false; // `declarations` is undefined for builtin symbols, such as `unknown`. if (!localSymbol.declarations) return false; // In case of no import we ended up in a declaration in foo.ts, while in // case of having an import localD is still in the reexporing file. for (const localD of localSymbol.declarations) { if (localD.getSourceFile() !== origSourceFile) { return false; } } } return true; }); if (!localExports.length) return undefined; // TypeScript 2.8 and TypeScript 2.9 differ on the order in which the // module symbols come out, so sort here to make the tests stable. localExports.sort((a, b) => stringCompare(a.name, b.name)); const clutzModuleName = moduleName.replace(/\./g, '$'); // Clutz might refer to the name in two different forms (stemming from // goog.provide and goog.module respectively). // // 1) global in clutz: ಠ_ಠ.clutz.module$contents$path$to$module_Symbol... // 2) local in a module: ಠ_ಠ.clutz.module$exports$path$to$module.Symbol... // // See examples at: // https://github.com/angular/clutz/tree/master/src/test/java/com/google/javascript/clutz // Case (1) from above. const globalExports = []; // Case (2) from above. const nestedExports = []; for (const symbol of localExports) { let localName = symbol.name; const declaration = (_a = symbol.declarations) === null || _a === void 0 ? void 0 : _a.find(d => d.getSourceFile() === origSourceFile); if (declaration && ts.isExportSpecifier(declaration) && declaration.propertyName) { // If declared in an "export {X as Y};" export specifier, then X (stored // in propertyName) is the local name that resolves within the module, // whereas Y is only available on the exports, i.e. the name used to // address the symbol from outside the module. Use the localName for the // export then, but publish under the external name. localName = declaration.propertyName.text; } const mangledName = `module$contents$${clutzModuleName}_${symbol.name}`; // These ExportSpecifiers are the `foo as bar` bits as found in a larger // `export {foo as bar}` statement, which is constructed after this loop. globalExports.push(ts.factory.createExportSpecifier( /* isTypeOnly */ false, ts.factory.createIdentifier(localName), ts.factory.createIdentifier(mangledName))); nestedExports.push(ts.factory.createExportSpecifier( /* isTypeOnly */ false, localName === symbol.name ? undefined : localName, ts.factory.createIdentifier(symbol.name))); } // Create two export statements that will be used to contribute to the // ಠ_ಠ.clutz namespace. const globalDeclarations = [ // 1) For globalExports, // export {...}; ts.factory.createExportDeclaration( /* decorators */ undefined, /* modifiers */ undefined, /* isTypeOnly */ false, ts.factory.createNamedExports(globalExports)), // 2) For nestedExports // namespace module$exports$module$name$here { // export {...}; // } ts.factory.createModuleDeclaration( /* decorators */ undefined, /* modifiers */ [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(`module$exports$${clutzModuleName}`), ts.factory.createModuleBlock([ ts.factory.createExportDeclaration( /* decorators */ undefined, /* modifiers */ undefined, /* isTypeOnly */ false, ts.factory.createNamedExports(nestedExports)), ]), ts.NodeFlags.Namespace), ]; // Wrap a `declare global { namespace ಠ_ಠ.clutz { ... } }` around // the statements in globalDeclarations. return ts.factory.createModuleDeclaration( /* decorators */ undefined, /* modifiers */ [ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)], ts.factory.createIdentifier('global'), ts.factory.createModuleBlock([ ts.factory.createModuleDeclaration( /* decorators */ undefined, /* modifiers */ undefined, // Note: it's not exactly right to use a '.' within an identifier // like I am doing here, but I could not figure out how to construct // an AST that has a dotted name here -- the types require a // ModuleDeclaration, but nesting another ModuleDeclaration in here // always created a new {} block, despite trying the // 'NestedNamespace' flag. ts.factory.createIdentifier('ಠ_ಠ.clutz'), ts.factory.createModuleBlock(globalDeclarations), ts.NodeFlags.Namespace | ts.NodeFlags.NestedNamespace), ]), ts.NodeFlags.GlobalAugmentation); } /** * ambientModuleSymbolFromClutz returns the module's symbol if and only if it is * an ambient module that uses tsickle's support for namespace imports (using * goog: or declaring the namespace to import in a special * __clutz_actual_namespace field). */ function ambientModuleSymbolFromClutz(typeChecker, stmt) { if (!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) { return undefined; } if (!stmt.moduleSpecifier) { return undefined; // can be absent on 'export' statements. } const moduleSymbol = typeChecker.getSymbolAtLocation(stmt.moduleSpecifier); if ((moduleSymbol === null || moduleSymbol === void 0 ? void 0 : moduleSymbol.valueDeclaration) && ts.isSourceFile(moduleSymbol.valueDeclaration)) { return undefined; } const ignoredDiagnostics = []; const namespace = googmodule.namespaceForImportUrl(stmt, ignoredDiagnostics, stmt.moduleSpecifier.text, moduleSymbol); if (namespace === null) return undefined; return moduleSymbol; } /** * Given a QualifiedName (a reference like `foo.bar.baz`), checks if it's a * reference into a Clutz symbol, and if so return the underlying ts.Symbol. * * Note that this function works top-down over the AST: it starts from the * QualifiedName and looks at its constituent parts. It cannot work bottom-up, * because sometimes TS generates AST nodes that don't have a parent. */ function clutzSymbolFromQualifiedName(typeChecker, qname) { // Verify that the innermost reference is the look of disapproval. let inner = qname; while (ts.isQualifiedName(inner.left)) { inner = inner.left; } if (inner.left.text !== 'ಠ_ಠ' || inner.right.text !== 'clutz') { return undefined; // Not a look of disapproval. } const node = qname.right; let sym = typeChecker.getSymbolAtLocation(node); if (!sym) { // When the declarations transformer has synthesized a reference, for // example due to inference, the type checker will not return a symbol // underlying the node. Instead we reach through the TS internals for this. // tslint:disable-next-line:no-any circumventing private API sym = node['symbol']; } return sym; } /** * Given a ts.Node, checks if it's a `foo.bar.baz` reference into a Clutz * symbol, and if so return the underlying ts.Symbol. */ function clutzSymbolFromNode(typeChecker, node) { if (ts.isTypeReferenceNode(node) && ts.isQualifiedName(node.typeName)) { // Reference in type position. return clutzSymbolFromQualifiedName(typeChecker, node.typeName); } if (ts.isTypeQueryNode(node) && ts.isQualifiedName(node.exprName)) { // Reference in typeof position. return clutzSymbolFromQualifiedName(typeChecker, node.exprName); } return undefined; } /** * Given a ts.Symbol, returns the import path string to the source of that * symbol. * * This is a path to an underlying d.ts file that defines that symbol, without a * file extension. */ function importPathForSymbol(sym) { if (!sym.declarations || sym.declarations.length === 0) { // This can happen if an import or symbol somehow references a nonexistent // type, for example in a case where type checking failed or via 'any'. return undefined; } // A Clutz symbol may be multiply defined in the case where a single .js file // is a member of multiple libraries. Typically those will declaration-merge // due to having the same Clutz output (though also note that the output can // depend on which other files are included in the Clutz run!). We just need // any definition at all, so take the first. const clutzFileName = sym.declarations[0].getSourceFile().fileName; if (!clutzFileName.endsWith('.d.ts')) { throw new Error(`Expected d.ts file for ${sym} but found ${clutzFileName}`); } return clutzFileName.substring(0, clutzFileName.length - '.d.ts'.length); } /** * Given a ts.SourceFile, looks for imports/exports that resolve to goog: * namespaces and uses the "look of disapproval" namespace, and returns the * import paths of the underlying files that define them. */ function gatherNecessaryClutzImports(typeChecker, sf) { const imports = new Set(); for (const stmt of sf.statements) { // Recurse to find all non-imported accesses to symbols. ts.forEachChild(stmt, visit); // Then handle explicit import/export statements. const moduleSymbol = ambientModuleSymbolFromClutz(typeChecker, stmt); if (!moduleSymbol) continue; const importPath = importPathForSymbol(moduleSymbol); if (importPath) imports.add(importPath); } return Array.from(imports); /** * Recursively searches a node for references to members of the `ಠ_ಠ.clutz` * namespace, and adds any referenced source files to the `imports` set. * TODO(b/162295026): forbid this pattern and delete this logic. */ function visit(node) { const sym = clutzSymbolFromNode(typeChecker, node); if (sym) { const importPath = importPathForSymbol(sym); if (importPath) imports.add(importPath); // Note: no 'return' here, because we need to also visit children in // parameterized types like Foo<Bar>. } ts.forEachChild(node, visit); } } //# sourceMappingURL=clutz.js.map