tsickle
Version:
Transpile TypeScript code to JavaScript with Closure annotations.
803 lines • 41 kB
JavaScript
/**
* @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.generateExterns = exports.getGeneratedExterns = void 0;
/**
* @fileoverview Externs creates Closure Compiler #externs definitions from the
* ambient declarations in a TypeScript file.
*
* (Note that we cannot write the "@" form of the externs tag, even in comments,
* because the compiler greps for it in source files(!). So we write #externs
* instead.)
*
* For example, a
* declare interface Foo { bar: string; }
*
* Would generate a
* /.. #externs ./
* /.. @record ./
* var Foo = function() {};
* /.. @type {string} ./
* Foo.prototype.bar;
*
* The generated externs indicate to Closure Compiler that symbols are external
* to the optimization process, i.e. they are provided by outside code. That
* most importantly means they must not be renamed or removed.
*
* A major difficulty here is that TypeScript supports module-scoped external
* symbols; `.d.ts` files can contain `export`s and `import` other files.
* Closure Compiler does not have such a concept, so tsickle must emulate the
* behaviour. It does so by following this scheme:
*
* 1. non-module .d.ts produces global symbols
* 2. module .d.ts produce symbols namespaced to the module, by creating a
* mangled name matching the current file's path. tsickle expects outside
* code (e.g. build system integration or manually written code) to contain a
* goog.module/provide that references the mangled path.
* 3. declarations in `.ts` files produce types that can be separately emitted
* in e.g. an `externs.js`, using `getGeneratedExterns` below.
* 1. non-exported symbols produce global types, because that's what users
* expect and it matches TypeScripts emit, which just references `Foo` for
* a locally declared symbol `Foo` in a module. Arguably these should be
* wrapped in `declare global { ... }`.
* 2. exported symbols are scoped to the `.ts` file by prefixing them with a
* mangled name. Exported types are re-exported from the JavaScript
* `goog.module`, allowing downstream code to reference them. This has the
* same problem regarding ambient values as above, it is unclear where the
* value symbol would be defined, so for the time being this is
* unsupported.
*
* The effect of this is that:
* - symbols in a module (i.e. not globals) are generally scoped to the local
* module using a mangled name, preventing symbol collisions on the Closure
* side.
* - importing code can unconditionally refer to and import any symbol defined
* in a module `X` as `path.to.module.X`, regardless of whether the defining
* location is a `.d.ts` file or a `.ts` file, and regardless whether the
* symbol is ambient (assuming there's an appropriate shim).
* - if there is a shim present, tsickle avoids emitting the Closure namespace
* itself, expecting the shim to provide the namespace and initialize it to a
* symbol that provides the right value at runtime (i.e. the implementation of
* whatever third party library the .d.ts describes).
*/
const ts = require("typescript");
const annotator_host_1 = require("./annotator_host");
const enum_transformer_1 = require("./enum_transformer");
const googmodule_1 = require("./googmodule");
const jsdoc = require("./jsdoc");
const jsdoc_transformer_1 = require("./jsdoc_transformer");
const module_type_translator_1 = require("./module_type_translator");
const path = require("./path");
const transformer_util_1 = require("./transformer_util");
const type_translator_1 = require("./type_translator");
/**
* Symbols that are already declared as externs in Closure, that should
* be avoided by tsickle's "declare ..." => externs.js conversion.
*/
const PREDECLARED_CLOSURE_EXTERNS_LIST = [
'exports',
'global',
'module',
// ErrorConstructor is the interface of the Error object itself.
// tsickle detects that this is part of the TypeScript standard library
// and assumes it's part of the Closure standard library, but this
// assumption is wrong for ErrorConstructor. To properly handle this
// we'd somehow need to map methods defined on the ErrorConstructor
// interface into properties on Closure's Error object, but for now it's
// simpler to just treat it as already declared.
'ErrorConstructor',
'Symbol',
'WorkerGlobalScope',
];
/**
* The header to be used in generated externs. This is not included in the
* output of generateExterns() because generateExterns() works one file at a
* time, and typically you create one externs file from the entire compilation
* unit.
*
* Suppressions:
* - checkTypes: Closure's type system does not match TS'.
* - const: for clashes of const variable assignments. This is needed to not
* conflict with the hand-written closure externs.
* - duplicate: because externs might duplicate re-opened definitions from other
* JS files.
* - missingOverride: There's no benefit to having closure-compiler warn us that
* we're overriding methods. Producing such warnings, if any, should be
* the job of the TS type system.
*/
const EXTERNS_HEADER = `/**
* @${''}externs
* @suppress {checkTypes,const,duplicate,missingOverride}
*/
// NOTE: generated by tsickle, do not edit.
`;
/**
* Concatenate all generated externs definitions together into a string,
* including a file comment header.
*
* @param rootDir Project root. Emitted comments will reference paths relative
* to this root.
*/
function getGeneratedExterns(externs, rootDir) {
let allExterns = EXTERNS_HEADER;
for (const fileName of Object.keys(externs)) {
const srcPath = path.relative(rootDir, fileName);
allExterns += `// ${jsdoc.createGeneratedFromComment(srcPath)}\n`;
allExterns += externs[fileName];
}
return allExterns;
}
exports.getGeneratedExterns = getGeneratedExterns;
/**
* isInGlobalAugmentation returns true if declaration is the immediate child of a 'declare global'
* block.
*/
function isInGlobalAugmentation(declaration) {
// declare global { ... } creates a ModuleDeclaration containing a ModuleBlock containing the
// declaration, with the ModuleDeclaration having the GlobalAugmentation flag set.
if (!declaration.parent || !declaration.parent.parent)
return false;
return (declaration.parent.parent.flags & ts.NodeFlags.GlobalAugmentation) !== 0;
}
/**
* generateExterns generates extern definitions for all ambient declarations in the given source
* file. It returns a string representation of the Closure JavaScript, not including the initial
* comment with \@fileoverview and #externs (see above for that).
*/
function generateExterns(typeChecker, sourceFile, host, moduleResolutionHost, options) {
let output = '';
const diagnostics = [];
const isDts = (0, transformer_util_1.isDtsFileName)(sourceFile.fileName);
const isExternalModule = ts.isExternalModule(sourceFile);
const mtt = new module_type_translator_1.ModuleTypeTranslator(sourceFile, typeChecker, host, diagnostics, /*isForExterns*/ true);
// .d.ts files declare symbols. The code below translates these into a form understood by Closure
// Compiler, converting the type syntax, but also converting symbol names into a form accessible
// to Closure Compiler.
// Like regular .ts files, .d.ts can be either scripts or modules. Scripts declare symbols in the
// global namespace, which has the same semantics in Closure and TypeScript, so the code below
// emits those with the same name.
// Modules however declare symbols scoped to the module that can be exported. Closure has no
// concept of externs that are non-global, so tsickle needs to mangle the symbol names, both at
// their declaration and at their use site.
// This mangling happens by wrapping all declared symbols in a namespace based on the file name.
// This namespace is then essentially the exports object for the ambient module (externs in
// Closure terms). This namespace is called `moduleNamespace` below:
let moduleNamespace = '';
if (isExternalModule) {
moduleNamespace = (0, annotator_host_1.moduleNameAsIdentifier)(host, sourceFile.fileName);
}
// Symbols are generated starting in rootNamespace. For script .d.ts with global symbols, this is
// the empty string. For most module `.d.ts` files, this is the mangled namespace object. The
// remaining special case are `.d.ts` files containing an `export = something;` statement. In
// these, the effective exports object, i.e. the object containing the symbols that importing code
// receives, is different from the main module scope.
// tsickle handles the `export =` case by generating symbols in a different namespace (escaped
// with a `_`) below, and then assigning whatever is actually exported into the `moduleNamespace`
// below.
let rootNamespace = moduleNamespace;
// There can only be one export =, and if there is one, there cannot be any other exports.
const exportAssignment = sourceFile.statements.find(ts.isExportAssignment);
const hasExportEquals = exportAssignment && exportAssignment.isExportEquals;
if (hasExportEquals) {
// If so, move all generated symbols into a different sub-namespace, so that later on we can
// control what exactly goes on the actual exported namespace.
rootNamespace = rootNamespace + '_';
}
for (const stmt of sourceFile.statements) {
// Always collect alises for imported symbols.
importsVisitor(stmt);
if (!isDts && !(0, transformer_util_1.hasModifierFlag)(stmt, ts.ModifierFlags.Ambient)) {
continue;
}
visitor(stmt, []);
}
/**
* Convert a qualified name from a .d.ts file or declaration context into a mangled identifier.
* E.g. for a qualified name in `export = someName;` or `import = someName;`.
* If `someName` is `declare global { namespace someName {...} }`, tsickle must not qualify access
* to it with the mangled module namespace as it is emitted in the global namespace. Similarly, if
* the symbol is declared in a non-module context, it must not be mangled.
*/
function qualifiedNameToMangledIdentifier(name) {
const entityName = (0, transformer_util_1.getEntityNameText)(name);
let symbol = typeChecker.getSymbolAtLocation(name);
if (symbol) {
// If this is an aliased name (e.g. from an import), use the alias to refer to it.
if (symbol.flags & ts.SymbolFlags.Alias) {
symbol = typeChecker.getAliasedSymbol(symbol);
}
const alias = mtt.symbolsToAliasedNames.get(symbol);
if (alias)
return alias;
const isGlobalSymbol = symbol && symbol.declarations && symbol.declarations.some(d => {
if (isInGlobalAugmentation(d))
return true;
// If the declaration's source file is not a module, it must be global.
// If it is a module, the identifier must be local to this file, or handled above via the
// alias.
return !ts.isExternalModule(d.getSourceFile());
});
if (isGlobalSymbol)
return entityName;
}
return rootNamespace + '.' + entityName;
}
if (output && isExternalModule) {
// If tsickle generated any externs and this is an external module, prepend the namespace
// declaration for it.
output = `/** @const */\nvar ${rootNamespace} = {};\n` + output;
let exportedNamespace = rootNamespace;
if (exportAssignment && hasExportEquals) {
if (ts.isIdentifier(exportAssignment.expression) ||
ts.isQualifiedName(exportAssignment.expression)) {
// E.g. export = someName;
// If someName is "declare global { namespace someName {...} }", tsickle must not qualify
// access to it with module namespace as it is emitted in the global namespace.
exportedNamespace = qualifiedNameToMangledIdentifier(exportAssignment.expression);
}
else {
(0, transformer_util_1.reportDiagnostic)(diagnostics, exportAssignment.expression, `export = expression must be a qualified name, got ${ts.SyntaxKind[exportAssignment.expression.kind]}.`);
}
// Assign the actually exported namespace object (which lives somewhere under rootNamespace)
// into the module's namespace.
emit(`/**\n * export = ${exportAssignment.expression.getText()}\n * @const\n */\n`);
emit(`var ${moduleNamespace} = ${exportedNamespace};\n`);
}
if (isDts && host.provideExternalModuleDtsNamespace) {
// In a non-shimmed module, create a global namespace. This exists purely for backwards
// compatiblity, in the medium term all code using tsickle should always use `goog.module`s,
// so global names should not be neccessary.
for (const nsExport of sourceFile.statements.filter(ts.isNamespaceExportDeclaration)) {
const namespaceName = (0, transformer_util_1.getIdentifierText)(nsExport.name);
emit(`// export as namespace ${namespaceName}\n`);
writeVariableStatement(namespaceName, [], exportedNamespace);
}
}
}
return { output, diagnostics };
function emit(str) {
output += str;
}
/**
* isFirstDeclaration returns true if decl is the first declaration
* of its symbol. E.g. imagine
* interface Foo { x: number; }
* interface Foo { y: number; }
* we only want to emit the "\@record" for Foo on the first one.
*
* The exception are variable declarations, which - in externs - do not assign a value:
* /.. \@type {...} ./
* var someVariable;
* /.. \@type {...} ./
* someNamespace.someVariable;
* If a later declaration wants to add additional properties on someVariable, tsickle must still
* emit an assignment into the object, as it's otherwise absent.
*/
function isFirstValueDeclaration(decl) {
if (!decl.name)
return true;
const sym = typeChecker.getSymbolAtLocation(decl.name);
if (!sym.declarations || sym.declarations.length < 2)
return true;
const earlierDecls = sym.declarations.slice(0, sym.declarations.indexOf(decl));
// Either there are no earlier declarations, or all of them are variables (see above). tsickle
// emits a value for all other declaration kinds (function for functions, classes, interfaces,
// {} object for namespaces).
return earlierDecls.length === 0 || earlierDecls.every(ts.isVariableDeclaration);
}
/** Writes the actual variable statement of a Closure variable declaration. */
function writeVariableStatement(name, namespace, value) {
const qualifiedName = namespace.concat([name]).join('.');
if (namespace.length === 0)
emit(`var `);
emit(qualifiedName);
if (value)
emit(` = ${value}`);
emit(';\n');
}
/**
* Writes a Closure variable declaration, i.e. the variable statement with a leading JSDoc
* comment making it a declaration.
*/
function writeVariableDeclaration(decl, namespace) {
if (decl.name.kind === ts.SyntaxKind.Identifier) {
const name = (0, transformer_util_1.getIdentifierText)(decl.name);
if (PREDECLARED_CLOSURE_EXTERNS_LIST.indexOf(name) >= 0)
return;
emit(jsdoc.toString([{ tagName: 'type', type: mtt.typeToClosure(decl) }]));
emit('\n');
writeVariableStatement(name, namespace);
}
else {
errorUnimplementedKind(decl.name, 'externs for variable');
}
}
/**
* Emits a JSDoc declaration that merges the signatures of the given function declaration (for
* overloads), and returns the parameter names chosen.
*/
function emitFunctionType(decls, extraTags = []) {
const { tags, parameterNames } = mtt.getFunctionTypeJSDoc(decls, extraTags);
emit('\n');
emit(jsdoc.toString(tags));
return parameterNames;
}
function writeFunction(name, params, namespace) {
const paramsStr = params.join(', ');
if (namespace.length > 0) {
let fqn = namespace.join('.');
if (name.kind === ts.SyntaxKind.Identifier) {
fqn += '.'; // computed names include [ ] in their getText() representation.
}
fqn += name.getText();
emit(`${fqn} = function(${paramsStr}) {};\n`);
}
else {
if (name.kind !== ts.SyntaxKind.Identifier) {
(0, transformer_util_1.reportDiagnostic)(diagnostics, name, 'Non-namespaced computed name in externs');
}
emit(`function ${name.getText()}(${paramsStr}) {}\n`);
}
}
function writeEnum(decl, namespace) {
// E.g. /** @enum {number} */ var COUNTRY = {US: 1, CA: 1};
const name = (0, transformer_util_1.getIdentifierText)(decl.name);
let members = '';
const enumType = (0, enum_transformer_1.getEnumType)(typeChecker, decl);
// Closure enums members must have a value of the correct type, but the actual value does not
// matter in externs.
const initializer = enumType === 'string' ? `''` : 1;
for (const member of decl.members) {
let memberName;
switch (member.name.kind) {
case ts.SyntaxKind.Identifier:
memberName = (0, transformer_util_1.getIdentifierText)(member.name);
break;
case ts.SyntaxKind.StringLiteral:
const text = member.name.text;
if ((0, type_translator_1.isValidClosurePropertyName)(text))
memberName = text;
break;
default:
break;
}
if (!memberName) {
members += ` /* TODO: ${ts.SyntaxKind[member.name.kind]}: ${(0, jsdoc_transformer_1.escapeForComment)(member.name.getText())} */\n`;
continue;
}
members += ` ${memberName}: ${initializer},\n`;
}
emit(`\n/** @enum {${enumType}} */\n`);
writeVariableStatement(name, namespace, `{\n${members}}`);
}
function writeTypeAlias(decl, namespace) {
const typeStr = mtt.typeToClosure(decl, undefined);
emit(`\n/** @typedef {${typeStr}} */\n`);
writeVariableStatement((0, transformer_util_1.getIdentifierText)(decl.name), namespace);
}
function writeType(decl, namespace) {
const name = decl.name;
if (!name) {
(0, transformer_util_1.reportDiagnostic)(diagnostics, decl, 'anonymous type in externs');
return;
}
const typeName = namespace.concat([name.getText()]).join('.');
if (PREDECLARED_CLOSURE_EXTERNS_LIST.indexOf(typeName) >= 0)
return;
if (isFirstValueDeclaration(decl)) {
// Emit the 'function' that is actually the declaration of the interface
// itself. If it's a class, this function also must include the type
// annotations of the constructor.
let paramNames = [];
const jsdocTags = [];
let wroteJsDoc = false;
(0, jsdoc_transformer_1.maybeAddHeritageClauses)(jsdocTags, mtt, decl);
(0, jsdoc_transformer_1.maybeAddTemplateClause)(jsdocTags, decl);
if (decl.kind === ts.SyntaxKind.ClassDeclaration) {
// Translate class to a function to avoid redeclaration issues.
jsdocTags.push({ tagName: 'constructor' }, { tagName: 'struct' });
// Check for constructors in current and base classes
const ctors = getCtors(decl);
if (ctors.length) {
paramNames = emitFunctionType(ctors, jsdocTags);
wroteJsDoc = true;
}
}
else {
// Otherwise it's an interface; tag it as structurally typed.
jsdocTags.push({ tagName: 'record' }, { tagName: 'struct' });
}
if (!wroteJsDoc)
emit(jsdoc.toString(jsdocTags));
writeFunction(name, paramNames, namespace);
}
// Process everything except (MethodSignature|MethodDeclaration|Constructor)
const methods = new Map();
for (const member of decl.members) {
switch (member.kind) {
case ts.SyntaxKind.PropertySignature:
case ts.SyntaxKind.PropertyDeclaration:
const prop = member;
if (prop.name.kind === ts.SyntaxKind.Identifier) {
let type = mtt.typeToClosure(prop);
if (prop.questionToken && type === '?') {
// An optional 'any' type translates to '?|undefined' in Closure.
type = '?|undefined';
}
const isReadonly = (0, transformer_util_1.hasModifierFlag)(prop, ts.ModifierFlags.Readonly);
emit(jsdoc.toString([{ tagName: isReadonly ? 'const' : 'type', type }]));
if ((0, transformer_util_1.hasModifierFlag)(prop, ts.ModifierFlags.Static)) {
emit(`\n${typeName}.${prop.name.getText()};\n`);
}
else {
emit(`\n${typeName}.prototype.${prop.name.getText()};\n`);
}
continue;
}
// TODO: For now property names other than Identifiers are not handled; e.g.
// interface Foo { "123bar": number }
break;
case ts.SyntaxKind.MethodSignature:
case ts.SyntaxKind.MethodDeclaration:
const method = member;
const isStatic = (0, transformer_util_1.hasModifierFlag)(method, ts.ModifierFlags.Static);
const methodSignature = `${method.name.getText()}$$$${isStatic ? 'static' : 'instance'}`;
if (methods.has(methodSignature)) {
methods.get(methodSignature).push(method);
}
else {
methods.set(methodSignature, [method]);
}
continue;
case ts.SyntaxKind.Constructor:
continue; // Handled above.
default:
// Members can include things like index signatures, for e.g.
// interface Foo { [key: string]: number; }
// For now, just skip it.
break;
}
// If we get here, the member wasn't handled in the switch statement.
let memberName = namespace;
if (member.name) {
memberName = memberName.concat([member.name.getText()]);
}
emit(`\n/* TODO: ${ts.SyntaxKind[member.kind]}: ${memberName.join('.')} */\n`);
}
// Handle method declarations/signatures separately, since we need to deal with overloads.
for (const methodVariants of Array.from(methods.values())) {
const firstMethodVariant = methodVariants[0];
let parameterNames;
if (methodVariants.length > 1) {
parameterNames = emitFunctionType(methodVariants);
}
else {
parameterNames = emitFunctionType([firstMethodVariant]);
}
const methodNamespace = namespace.concat([name.getText()]);
// If the method is static, don't add the prototype.
if (!(0, transformer_util_1.hasModifierFlag)(firstMethodVariant, ts.ModifierFlags.Static)) {
methodNamespace.push('prototype');
}
writeFunction(firstMethodVariant.name, parameterNames, methodNamespace);
}
}
function writeExportDeclaration(exportDeclaration, namespace) {
if (!exportDeclaration.exportClause) {
emit(`\n// TODO(tsickle): export * declaration in ${debugLocationStr(exportDeclaration, namespace)}\n`);
return;
}
if (ts.isNamespaceExport(exportDeclaration.exportClause)) {
// TODO(#1135): Support generating externs using this syntax.
emit(`\n// TODO(tsickle): export * as declaration in ${debugLocationStr(exportDeclaration, namespace)}\n`);
return;
}
for (const exportSpecifier of exportDeclaration.exportClause.elements) {
// No need to do anything for properties exported under their original name.
if (!exportSpecifier.propertyName)
continue;
emit('/** @const */\n');
writeVariableStatement(exportSpecifier.name.text, namespace, namespace.join('.') + '.' + exportSpecifier.propertyName.text);
}
}
/**
* Returns the first non-zero argument constructor that can be found going
* down the inheritance chain. This should work as a class can only extends a
* single class and can only have one default constructor.
*/
function getCtors(decl) {
// Get ctors from current class
const currentCtors = decl.members.filter((m) => m.kind === ts.SyntaxKind.Constructor);
if (currentCtors.length) {
return currentCtors;
}
// Or look at base classes
if (decl.heritageClauses) {
const baseSymbols = decl.heritageClauses
.filter((h) => h.token === ts.SyntaxKind.ExtendsKeyword)
.flatMap((h) => h.types)
.filter((t) => t.expression.kind === ts.SyntaxKind.Identifier);
for (const base of baseSymbols) {
const sym = typeChecker.getSymbolAtLocation(base.expression);
if (!sym || !sym.declarations)
return [];
for (const d of sym.declarations) {
if (d.kind === ts.SyntaxKind.ClassDeclaration) {
return getCtors(d);
}
}
}
}
return [];
}
/**
* Adds aliases for the symbols imported in the given declaration, so that their types get
* printed as the fully qualified name, and not just as a reference to the local import alias.
*
* tsickle generates .js files that (at most) contain a `goog.provide`, but are not
* `goog.module`s. These files cannot express an aliased import. However Closure Compiler allows
* referencing types using fully qualified names in such files, so tsickle can resolve the
* imported module URI and produce `path.to.module.Symbol` as an alias, and use that when
* referencing the type.
*/
function addImportAliases(decl) {
var _a;
// Side effect import, like "import 'somepath';" declares no local aliases.
if (ts.isImportDeclaration(decl) && !decl.importClause)
return;
let moduleUri;
if (ts.isImportDeclaration(decl)) {
moduleUri = decl.moduleSpecifier;
}
else if (ts.isExternalModuleReference(decl.moduleReference)) {
// import foo = require('./bar');
moduleUri = decl.moduleReference.expression;
}
else {
// import foo = bar.baz.bam;
// handled at call site.
return;
}
// Only report diagnostics for .d.ts files. Diagnostics for .ts files have
// already been reported during JS emit.
const importDiagnostics = isDts ? diagnostics : [];
const moduleSymbol = typeChecker.getSymbolAtLocation(moduleUri);
if (!moduleSymbol) {
(0, transformer_util_1.reportDiagnostic)(importDiagnostics, moduleUri, `imported module has no symbol`);
return;
}
const googNamespace = (0, googmodule_1.namespaceForImportUrl)(moduleUri, importDiagnostics, moduleUri.text, moduleSymbol);
const isDefaultImport = ts.isImportDeclaration(decl) && !!((_a = decl.importClause) === null || _a === void 0 ? void 0 : _a.name);
if (googNamespace) {
mtt.registerImportAliases(googNamespace, isDefaultImport, moduleSymbol, () => googNamespace);
}
else {
mtt.registerImportAliases(null, isDefaultImport, moduleSymbol, getAliasPrefixForEsModule(moduleUri));
}
}
/**
* Returns a symbol alias prefix for export from an ECMAScript module (in
* contrast to a goog module/provide file). The prefix may then be used to
* reference the type in externs where import statements aren't allowed.
*/
function getAliasPrefixForEsModule(moduleUri) {
// Calls to resolveModuleName, moduleNameAsIdentifier and
// host.pathToModuleName can incur file system accesses, which are slow.
// Make sure they are only called once and if/when needed.
const fullUri = (0, googmodule_1.resolveModuleName)(host, sourceFile.fileName, moduleUri.text);
const ambientModulePrefix = (0, annotator_host_1.moduleNameAsIdentifier)(host, fullUri);
const defaultPrefix = host.pathToModuleName(sourceFile.fileName, (0, googmodule_1.resolveModuleName)(host, sourceFile.fileName, fullUri));
return (exportedSymbol) => {
// While type_translator does add the mangled prefix for ambient
// declarations, it only does so for non-aliased (i.e. not imported)
// symbols. That's correct for its use in regular modules, which will have
// a local symbol for the imported ambient symbol. However within an
// externs file, there are no imports, so we need to make sure the alias
// already contains the correct module name, which means the mangled
// module name in case of imports symbols. This only applies to
// non-Closure ('goog:') imports.
const isAmbientModuleDeclaration = exportedSymbol.declarations &&
exportedSymbol.declarations.some(d => (0, transformer_util_1.isAmbient)(d) || d.getSourceFile().isDeclarationFile);
return isAmbientModuleDeclaration ? ambientModulePrefix : defaultPrefix;
};
}
/**
* Produces a compiler error that references the Node's kind. This is useful for the "else"
* branch of code that is attempting to handle all possible input Node types, to ensure all cases
* covered.
*/
function errorUnimplementedKind(node, where) {
(0, transformer_util_1.reportDiagnostic)(diagnostics, node, `${ts.SyntaxKind[node.kind]} not implemented in ${where}`);
}
/**
* getNamespaceForLocalDeclaration returns the namespace that should be used for the given
* declaration, deciding whether to namespace the symbol to the file or whether to create a
* global name.
*
* The function covers these cases:
* 1) a declaration in a .d.ts
* 1a) where the .d.ts is an external module --> namespace
* 1b) where the .d.ts is not an external module --> global
* 2) a declaration in a .ts file (all are treated as modules)
* 2a) that is exported --> namespace
* 2b) that is unexported --> global
*
* For 1), all symbols in .d.ts should generally be namespaced to the file to avoid collisions.
* However .d.ts files that are not external modules do declare global names (1b).
*
* For 2), ambient declarations in .ts files must be namespaced, for the same collision reasons.
* The exception is 2b), where in TypeScript, an unexported local "declare const x: string;"
* creates a symbol that, when used locally, is emitted as just "x". That is, it behaves
* like a variable declared in a 'declare global' block. Closure Compiler would fail the build if
* there is no declaration for "x", so tsickle must generate a global external symbol, i.e.
* without the namespace wrapper.
*/
function getNamespaceForTopLevelDeclaration(declaration, namespace) {
// Only use rootNamespace for top level symbols, any other namespacing (global names, nested
// namespaces) is always kept.
if (namespace.length !== 0)
return namespace;
// All names in a module (external) .d.ts file can only be accessed locally, so they always get
// namespace prefixed.
if (isDts && isExternalModule)
return [rootNamespace];
// Same for exported declarations in regular .ts files.
if ((0, transformer_util_1.hasModifierFlag)(declaration, ts.ModifierFlags.Export))
return [rootNamespace];
// But local declarations in .ts files or .d.ts files (1b, 2b) are global, too.
return [];
}
/**
* Returns a string representation for the location: either the namespace, or, if empty, the
* current source file name. This is intended to be included in the emit for warnings, so that
* users can more easily find where a problematic definition is from.
*
* The code below does not use diagnostics to avoid breaking the build for harmless unhandled
* cases.
*/
function debugLocationStr(node, namespace) {
// Use a regex to grab the filename without a path, to make the output stable
// under bazel where sandboxes use different paths.
return namespace.join('.') || node.getSourceFile().fileName.replace(/.*[/\\]/, '');
}
function importsVisitor(node) {
switch (node.kind) {
case ts.SyntaxKind.ImportEqualsDeclaration:
const importEquals = node;
if (importEquals.moduleReference.kind ===
ts.SyntaxKind.ExternalModuleReference) {
addImportAliases(importEquals);
}
break;
case ts.SyntaxKind.ImportDeclaration:
addImportAliases(node);
break;
default:
// Ignore. This visitor is only concerned with imports.
break;
}
}
function visitor(node, namespace) {
if (node.parent === sourceFile) {
namespace = getNamespaceForTopLevelDeclaration(node, namespace);
}
switch (node.kind) {
case ts.SyntaxKind.ModuleDeclaration:
const decl = node;
switch (decl.name.kind) {
case ts.SyntaxKind.Identifier:
if (decl.flags & ts.NodeFlags.GlobalAugmentation) {
// E.g. "declare global { ... }". Reset to the outer namespace.
namespace = [];
}
else {
// E.g. "declare namespace foo {"
const name = (0, transformer_util_1.getIdentifierText)(decl.name);
if (isFirstValueDeclaration(decl)) {
emit('/** @const */\n');
writeVariableStatement(name, namespace, '{}');
}
namespace = namespace.concat(name);
}
if (decl.body)
visitor(decl.body, namespace);
break;
case ts.SyntaxKind.StringLiteral:
// E.g. "declare module 'foo' {" (note the quotes).
// We still want to emit externs for this module, but Closure doesn't provide a
// mechanism for module-scoped externs. Instead, we emit in a mangled namespace.
// The mangled namespace (after resolving files) matches the emit for an original module
// file, so effectively this augments any existing module.
const importName = decl.name.text;
const importedModuleName = (0, googmodule_1.resolveModuleName)({ moduleResolutionHost, options }, sourceFile.fileName, importName);
const mangled = (0, annotator_host_1.moduleNameAsIdentifier)(host, importedModuleName);
emit(`// Derived from: declare module "${importName}"\n`);
namespace = [mangled];
// Declare "mangled$name" if it's not declared already elsewhere.
if (isFirstValueDeclaration(decl)) {
emit('/** @const */\n');
writeVariableStatement(mangled, [], '{}');
}
// Declare the contents inside the "mangled$name".
if (decl.body)
visitor(decl.body, [mangled]);
break;
default:
errorUnimplementedKind(decl.name, 'externs generation of namespace');
break;
}
break;
case ts.SyntaxKind.ModuleBlock:
const block = node;
for (const stmt of block.statements) {
visitor(stmt, namespace);
}
break;
case ts.SyntaxKind.ImportEqualsDeclaration:
const importEquals = node;
if (importEquals.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) {
// Handled in `importsVisitor`.
break;
}
const localName = (0, transformer_util_1.getIdentifierText)(importEquals.name);
const qn = qualifiedNameToMangledIdentifier(importEquals.moduleReference);
// @const so that Closure Compiler understands this is an alias.
emit('/** @const */\n');
writeVariableStatement(localName, namespace, qn);
break;
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.InterfaceDeclaration:
writeType(node, namespace);
break;
case ts.SyntaxKind.FunctionDeclaration:
const fnDecl = node;
const name = fnDecl.name;
if (!name) {
(0, transformer_util_1.reportDiagnostic)(diagnostics, fnDecl, 'anonymous function in externs');
break;
}
// Gather up all overloads of this function.
const sym = typeChecker.getSymbolAtLocation(name);
const decls = sym.declarations.filter(ts.isFunctionDeclaration);
// Only emit the first declaration of each overloaded function.
if (fnDecl !== decls[0])
break;
const params = emitFunctionType(decls);
writeFunction(name, params, namespace);
break;
case ts.SyntaxKind.VariableStatement:
for (const decl of node.declarationList.declarations) {
writeVariableDeclaration(decl, namespace);
}
break;
case ts.SyntaxKind.EnumDeclaration:
writeEnum(node, namespace);
break;
case ts.SyntaxKind.TypeAliasDeclaration:
writeTypeAlias(node, namespace);
break;
case ts.SyntaxKind.ImportDeclaration:
// Handled in `importsVisitor`.
break;
case ts.SyntaxKind.NamespaceExportDeclaration:
case ts.SyntaxKind.ExportAssignment:
// Handled on the file level.
break;
case ts.SyntaxKind.ExportDeclaration:
const exportDeclaration = node;
writeExportDeclaration(exportDeclaration, namespace);
break;
default:
emit(`\n// TODO(tsickle): ${ts.SyntaxKind[node.kind]} in ${debugLocationStr(node, namespace)}\n`);
break;
}
}
}
exports.generateExterns = generateExterns;
//# sourceMappingURL=externs.js.map
;