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,
);
}