function generateClutzAliases()

in src/clutz.ts [96:281]


function generateClutzAliases(
    sourceFile: ts.SourceFile, moduleName: string, typeChecker: ts.TypeChecker,
    options: ts.CompilerOptions): ts.Statement|undefined {
  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 as any)['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: ts.ExportSpecifier[] = [];
  // Case (2) from above.
  const nestedExports: ts.ExportSpecifier[] = [];
  for (const symbol of localExports) {
    let localName = symbol.name;
    const declaration =
        symbol.declarations?.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.createExportSpecifier(
        /* isTypeOnly */ false, ts.createIdentifier(localName),
        ts.createIdentifier(mangledName)));
    nestedExports.push(ts.createExportSpecifier(
        /* isTypeOnly */ false,
        localName === symbol.name ? undefined : localName,
        ts.createIdentifier(symbol.name)));
  }

  // Create two export statements that will be used to contribute to the
  // ಠ_ಠ.clutz namespace.
  const globalDeclarations: ts.Statement[] = [
    // 1) For globalExports,
    //      export {...};
    ts.createExportDeclaration(
        /* decorators */ undefined,
        /* modifiers */ undefined,
        ts.createNamedExports(globalExports),
        ),
    // 2) For nestedExports
    //      namespace module$exports$module$name$here {
    //        export {...};
    //      }
    ts.createModuleDeclaration(
        /* decorators */ undefined,
        /* modifiers */[ts.createModifier(ts.SyntaxKind.ExportKeyword)],
        ts.createIdentifier(`module$exports$${clutzModuleName}`),
        ts.createModuleBlock([
          ts.createExportDeclaration(
              /* decorators */ undefined,
              /* modifiers */ undefined,
              ts.createNamedExports(nestedExports),
              ),
        ]),
        ts.NodeFlags.Namespace,
        ),
  ];


  // Wrap a `declare global { namespace ಠ_ಠ.clutz { ... } }` around
  // the statements in globalDeclarations.
  return ts.createModuleDeclaration(
      /* decorators */ undefined,
      /* modifiers */[ts.createModifier(ts.SyntaxKind.DeclareKeyword)],
      ts.createIdentifier('global'),
      ts.createModuleBlock([
        ts.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.createIdentifier('ಠ_ಠ.clutz'),
            ts.createModuleBlock(globalDeclarations),
            ts.NodeFlags.Namespace | ts.NodeFlags.NestedNamespace,
            ),
      ]),
      ts.NodeFlags.GlobalAugmentation,
  );
}