@polymer/gen-typescript-declarations
Version:
Generate TypeScript type declarations for Polymer components.
1,032 lines (1,030 loc) • 44.2 kB
JavaScript
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const babel = require("@babel/types");
const fsExtra = require("fs-extra");
const minimatch = require("minimatch");
const path = require("path");
const analyzer = require("polymer-analyzer");
const vscode_uri_1 = require("vscode-uri");
const closure_types_1 = require("./closure-types");
const es_modules_1 = require("./es-modules");
const ts = require("./ts-ast");
const defaultExclude = [
'index.html',
'test/**',
'demo/**',
];
/**
* Analyze all files in the given directory using Polymer Analyzer, and return
* TypeScript declaration document strings in a map keyed by relative path.
*/
function generateDeclarations(rootDir, config) {
return __awaiter(this, void 0, void 0, function* () {
// Note that many Bower projects also have a node_modules/, but the reverse is
// unlikely.
const isBowerProject = (yield fsExtra.pathExists(path.join(rootDir, 'bower_components'))) === true;
const a = new analyzer.Analyzer({
urlLoader: new analyzer.FsUrlLoader(rootDir),
urlResolver: new analyzer.PackageUrlResolver({
packageDir: rootDir,
componentDir: isBowerProject ? 'bower_components/' : 'node_modules/',
}),
moduleResolution: isBowerProject ? undefined : 'node',
});
const analysis = yield a.analyzePackage();
const outFiles = new Map();
for (const tsDoc of yield analyzerToAst(analysis, config, rootDir)) {
outFiles.set(tsDoc.path, tsDoc.serialize());
}
return outFiles;
});
}
exports.generateDeclarations = generateDeclarations;
/**
* Make TypeScript declaration documents from the given Polymer Analyzer
* result.
*/
function analyzerToAst(analysis, config, rootDir) {
return __awaiter(this, void 0, void 0, function* () {
const excludeFiles = (config.excludeFiles || config.exclude || defaultExclude)
.map((p) => new minimatch.Minimatch(p));
const addReferences = config.addReferences || {};
const removeReferencesResolved = new Set((config.removeReferences || []).map((r) => path.resolve(rootDir, r)));
const renameTypes = new Map(Object.entries(config.renameTypes || {}));
// Map from identifier to the module path that exports it.
const autoImportMap = new Map();
if (config.autoImport !== undefined) {
for (const importPath in config.autoImport) {
for (const identifier of config.autoImport[importPath]) {
autoImportMap.set(identifier, importPath);
}
}
}
const analyzerDocs = [
...analysis.getFeatures({ kind: 'html-document' }),
...analysis.getFeatures({ kind: 'js-document' }),
];
// We want to produce one declarations file for each file basename. There
// might be both `foo.html` and `foo.js`, and we want their declarations to be
// combined into a signal `foo.d.ts`. So we first group Analyzer documents by
// their declarations filename.
const declarationDocs = new Map();
for (const jsDoc of analyzerDocs) {
// For every HTML or JS file, Analyzer is going to give us 1) the top-level
// document, and 2) N inline documents for any nested content (e.g. script
// tags in HTML). The top-level document will give us all the nested
// features we need, so skip any inline ones.
if (jsDoc.isInline) {
continue;
}
const sourcePath = analyzerUrlToRelativePath(jsDoc.url, rootDir);
if (sourcePath === undefined) {
console.warn(`Skipping source document without local file URL: ${jsDoc.url}`);
continue;
}
if (excludeFiles.some((r) => r.match(sourcePath))) {
continue;
}
const filename = makeDeclarationsFilename(sourcePath);
let docs = declarationDocs.get(filename);
if (!docs) {
docs = [];
declarationDocs.set(filename, docs);
}
docs.push(jsDoc);
}
const tsDocs = [];
const warnings = [...analysis.getWarnings()];
for (const [declarationsFilename, analyzerDocs] of declarationDocs) {
const tsDoc = new ts.Document({
path: declarationsFilename,
header: makeHeader(analyzerDocs.map((d) => analyzerUrlToRelativePath(d.url, rootDir))
.filter((url) => url !== undefined)),
tsLintDisables: [{
ruleName: 'variable-name',
why: `Describing an API that's defined elsewhere.`,
}],
});
for (const analyzerDoc of analyzerDocs) {
if (es_modules_1.isEsModuleDocument(analyzerDoc)) {
tsDoc.isEsModule = true;
}
}
for (const analyzerDoc of analyzerDocs) {
const generator = new TypeGenerator(tsDoc, analysis, analyzerDoc, rootDir, config.excludeIdentifiers || []);
generator.handleDocument();
warnings.push(...generator.warnings);
}
for (const ref of tsDoc.referencePaths) {
const resolvedRef = path.resolve(rootDir, path.dirname(tsDoc.path), ref);
if (removeReferencesResolved.has(resolvedRef)) {
tsDoc.referencePaths.delete(ref);
}
}
for (const ref of addReferences[tsDoc.path] || []) {
tsDoc.referencePaths.add(path.relative(path.dirname(tsDoc.path), ref));
}
for (const node of tsDoc.traverse()) {
if (node.kind === 'name') {
const renamed = renameTypes.get(node.name);
if (renamed !== undefined) {
node.name = renamed;
}
}
}
addAutoImports(tsDoc, autoImportMap);
tsDoc.simplify();
// Include even documents with no members. They might be dependencies of
// other files via the HTML import graph, and it's simpler to have empty
// files than to try and prune the references (especially across packages).
tsDocs.push(tsDoc);
}
const filteredWarnings = warnings.filter((warning) => {
if (config.hideWarnings && warning.severity !== analyzer.Severity.ERROR) {
return false;
}
const sourcePath = analyzerUrlToRelativePath(warning.sourceRange.file, rootDir);
return sourcePath !== undefined &&
!excludeFiles.some((pattern) => pattern.match(sourcePath));
});
const warningPrinter = new analyzer.WarningPrinter(process.stderr, { maxCodeLines: 1 });
yield warningPrinter.printWarnings(filteredWarnings);
if (filteredWarnings.some((warning) => warning.severity === analyzer.Severity.ERROR)) {
throw new Error('Encountered error generating types.');
}
if (config.googModules) {
return tsDocs.map((d) => transformToGoogStyle(d, rootDir));
}
return tsDocs;
});
}
/**
* Insert imports into the typings for any referenced identifiers listed in the
* autoImport configuration, unless they are already imported.
*/
function addAutoImports(tsDoc, autoImport) {
const alreadyImported = getImportedIdentifiers(tsDoc);
for (const node of tsDoc.traverse()) {
if (node.kind === 'name') {
let importSpecifier = autoImport.get(node.name);
if (importSpecifier === undefined) {
continue;
}
if (alreadyImported.has(node.name)) {
continue;
}
if (importSpecifier.startsWith('.')) {
if (makeDeclarationsFilename(importSpecifier) === tsDoc.path) {
// Don't import from yourself.
continue;
}
importSpecifier =
path.relative(path.dirname(tsDoc.path), importSpecifier);
if (!importSpecifier.startsWith('.')) {
importSpecifier = './' + importSpecifier;
}
}
tsDoc.members.push(new ts.Import({
identifiers: [{ identifier: node.name }],
fromModuleSpecifier: importSpecifier,
}));
alreadyImported.add(node.name);
}
}
}
function getPackageName(rootDir) {
let packageInfo;
try {
packageInfo = JSON.parse(fsExtra.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'));
}
catch (_a) {
return undefined;
}
return packageInfo.name;
}
function googModuleForNameBasedImportSpecifier(spec) {
const name =
// remove trailing .d.ts and .js
spec.replace(/(\.d\.ts|\.js)$/, '')
// foo-bar.dom becomes fooBarDom
.replace(/[-\.](\w)/g, (_, s) => s.toUpperCase())
// remove leading @
.replace(/^@/g, '')
// slash separated paths becomes dot separated namespace
.replace(/\//g, '.');
// add goog: at the beginning
return `goog:${name}`;
}
/* Note: this function modifies tsDoc. */
function transformToGoogStyle(tsDoc, rootDir) {
const packageName = getPackageName(rootDir);
if (!tsDoc.isEsModule || !packageName) {
return tsDoc;
}
for (const child of tsDoc.traverse()) {
if (child.kind === 'import' || child.kind === 'export') {
if (!child.fromModuleSpecifier) {
continue;
}
let spec = child.fromModuleSpecifier;
if (spec.startsWith('.')) {
spec = path.join(packageName, path.relative(rootDir, path.join(rootDir, path.dirname(tsDoc.path), spec))
.replace(/^\.\//, ''));
}
const elementName = spec.split('/')[1];
let trailingComment = undefined;
if (elementName && !/\./.test(elementName)) {
trailingComment =
` // from //third_party/javascript/polymer/v2/${elementName}`;
}
const googSpecifier = googModuleForNameBasedImportSpecifier(spec);
if (googSpecifier !== undefined) {
child.fromModuleSpecifier = googSpecifier;
child.trailingComment = trailingComment;
}
}
}
let googModuleName = googModuleForNameBasedImportSpecifier(path.join(packageName, tsDoc.path));
if (googModuleName === undefined) {
googModuleName = tsDoc.path;
}
return new ts.Document({
path: tsDoc.path,
header: tsDoc.header,
referencePaths: tsDoc.referencePaths,
tsLintDisables: tsDoc.tsLintDisables,
isEsModule: false,
members: [new ts.Namespace({ name: googModuleName, members: tsDoc.members, style: 'module' })]
});
}
/**
* Return all local identifiers imported by the given typings.
*/
function getImportedIdentifiers(tsDoc) {
const identifiers = new Set();
for (const member of tsDoc.members) {
if (member.kind === 'import') {
for (const { identifier, alias } of member.identifiers) {
if (identifier !== ts.AllIdentifiers) {
identifiers.add(alias || identifier);
}
}
}
}
return identifiers;
}
/**
* Analyzer always returns fully specified URLs with a protocol and an absolute
* path (e.g. "file:/foo/bar"). Return just the file path, relative to our
* project root.
*/
function analyzerUrlToRelativePath(analyzerUrl, rootDir) {
const parsed = vscode_uri_1.default.parse(analyzerUrl);
if (parsed.scheme !== 'file' || parsed.authority || !parsed.fsPath) {
return undefined;
}
return path.relative(rootDir, parsed.fsPath);
}
/**
* Create a TypeScript declarations filename for the given source document URL.
* Simply replaces the file extension with `d.ts`.
*/
function makeDeclarationsFilename(sourceUrl) {
const parsed = path.parse(sourceUrl);
return path.join(parsed.dir, parsed.name) + '.d.ts';
}
/**
* Generate the header comment to show at the top of a declarations document.
*/
function makeHeader(sourceUrls) {
return `DO NOT EDIT
This file was automatically generated by
https://github.com/Polymer/tools/tree/master/packages/gen-typescript-declarations
To modify these typings, edit the source file(s):
${sourceUrls.map((url) => ' ' + url).join('\n')}`;
}
class TypeGenerator {
constructor(root, analysis, analyzerDoc, rootDir, excludeIdentifiers) {
this.root = root;
this.analysis = analysis;
this.analyzerDoc = analyzerDoc;
this.rootDir = rootDir;
this.warnings = [];
/**
* Identifiers in this set will always be considered resolvable, e.g.
* for when determining what identifiers should be exported.
*/
this.forceResolvable = new Set();
this.excludeIdentifiers = new Set(excludeIdentifiers);
}
warn(feature, message) {
this.warnings.push(new analyzer.Warning({
message,
sourceRange: feature.sourceRange,
severity: analyzer.Severity.WARNING,
// We don't really need specific codes.
code: 'GEN_TYPESCRIPT_DECLARATIONS_WARNING',
parsedDocument: this.analyzerDoc.parsedDocument,
}));
}
/**
* Extend the given TypeScript declarations document with all of the relevant
* items in the given Polymer Analyzer document.
*/
handleDocument() {
for (const feature of this.analyzerDoc.getFeatures()) {
if ([...feature.identifiers].some((id) => this.excludeIdentifiers.has(id))) {
continue;
}
if (isPrivate(feature)) {
continue;
}
if (feature.kinds.has('element')) {
this.handleElement(feature);
}
else if (feature.kinds.has('behavior')) {
this.handleBehavior(feature);
}
else if (feature.kinds.has('element-mixin')) {
this.handleMixin(feature);
}
else if (feature.kinds.has('class')) {
this.handleClass(feature);
}
else if (feature.kinds.has('function')) {
this.handleFunction(feature);
}
else if (feature.kinds.has('namespace')) {
this.handleNamespace(feature);
}
else if (feature.kinds.has('html-import')) {
// Sometimes an Analyzer document includes an import feature that is
// inbound (things that depend on me) instead of outbound (things I
// depend on). For example, if an HTML file has a <script> tag for a JS
// file, then the JS file's Analyzer document will include that <script>
// tag as an import feature. We only care about outbound dependencies,
// hence this check.
if (feature.sourceRange &&
feature.sourceRange.file === this.analyzerDoc.url) {
this.handleHtmlImport(feature);
}
}
else if (feature.kinds.has('js-import')) {
this.handleJsImport(feature);
}
else if (feature.kinds.has('export')) {
this.handleJsExport(feature);
}
}
}
/**
* Add the given Element to the given TypeScript declarations document.
*/
handleElement(feature) {
// Whether this element has a constructor that is assigned and can be
// called. If it does we'll emit a class, otherwise an interface.
let constructable;
let fullName; // Fully qualified reference, e.g. `Polymer.DomModule`.
let shortName; // Just the last part of the name, e.g. `DomModule`.
let parent; // Where in the namespace tree does this live.
if (feature.className) {
constructable = true;
let namespacePath;
[namespacePath, shortName] = splitReference(feature.className);
fullName = feature.className;
parent = findOrCreateNamespace(this.root, namespacePath);
}
else if (feature.tagName) {
// No `className` means this is an element defined by a call to the
// Polymer function without a LHS assignment. We'll follow the convention
// of the Closure Polymer Pass, and emit a global namespace interface
// called `FooBarElement` (given a `tagName` of `foo-bar`). More context
// here:
//
// https://github.com/google/closure-compiler/wiki/Polymer-Pass#element-type-names-for-1xhybrid-call-syntax
// https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/PolymerClassDefinition.java#L128
constructable = false;
shortName = kebabToCamel(feature.tagName) + 'Element';
fullName = shortName;
parent = this.root;
}
else {
this.warn(feature, `Could not find element name.`);
return;
}
const legacyPolymerInterfaces = [];
if (isPolymerElement(feature)) {
legacyPolymerInterfaces.push(...feature.behaviorAssignments.map((behavior) => behavior.identifier));
if (feature.isLegacyFactoryCall) {
if (this.root.isEsModule) {
legacyPolymerInterfaces.push('LegacyElementMixin');
if (!getImportedIdentifiers(this.root).has('LegacyElementMixin')) {
this.root.members.push(new ts.Import({
identifiers: [{ identifier: 'LegacyElementMixin' }],
fromModuleSpecifier: '@polymer/polymer/lib/legacy/legacy-element-mixin.js',
}));
}
}
else {
legacyPolymerInterfaces.push('Polymer.LegacyElementMixin');
}
legacyPolymerInterfaces.push('HTMLElement');
}
}
if (constructable) {
this.handleClass(feature);
if (legacyPolymerInterfaces.length > 0) {
// Augment the class interface.
parent.members.push(new ts.Interface({
name: shortName,
extends: legacyPolymerInterfaces,
}));
}
}
else {
parent.members.push(new ts.Interface({
name: shortName,
description: feature.description || feature.summary,
properties: this.handleProperties(feature.properties.values()),
// Don't worry about about static methods when we're not
// constructable. Since there's no handle to the constructor, they
// could never be called.
methods: this.handleMethods(feature.methods.values()),
extends: [
...feature.mixins.map((mixin) => mixin.identifier),
...legacyPolymerInterfaces,
],
}));
if (isPolymerElement(feature) && feature.isLegacyFactoryCall &&
this.root.isEsModule) {
this.root.members.push(new ts.Export({ identifiers: [{ identifier: shortName }] }));
}
}
// The `HTMLElementTagNameMap` global interface maps custom element tag
// names to their definitions, so that TypeScript knows that e.g.
// `dom.createElement('my-foo')` returns a `MyFoo`. Augment the map with
// this custom element.
if (feature.tagName) {
const elementMap = findOrCreateInterface(this.root.isEsModule ? findOrCreateGlobalNamespace(this.root) :
this.root, 'HTMLElementTagNameMap');
elementMap.properties.push(new ts.Property({
name: feature.tagName,
type: new ts.NameType(fullName),
}));
}
}
/**
* Add the given Polymer Behavior to the given TypeScript declarations
* document.
*/
handleBehavior(feature) {
if (!feature.className) {
this.warn(feature, `Could not find a name for behavior.`);
return;
}
const [namespacePath, className] = splitReference(feature.className);
const ns = findOrCreateNamespace(this.root, namespacePath);
// An interface with the properties and methods that this behavior adds to
// an element. Note that behaviors are not classes, they are just data
// objects which the Polymer library uses to augment element classes.
ns.members.push(new ts.Interface({
name: className,
description: feature.description || feature.summary,
extends: feature.behaviorAssignments.map((b) => b.identifier),
properties: this.handleProperties(feature.properties.values()),
methods: this.handleMethods(feature.methods.values()),
}));
// The value that contains the actual definition of the behavior for
// Polymer. It's not important to know the shape of this object, so the
// `object` type is good enough. The main use of this is to make statements
// like `Polymer.mixinBehaviors([Polymer.SomeBehavior], ...)` compile.
ns.members.push(new ts.ConstValue({
name: className,
type: new ts.NameType('object'),
}));
}
/**
* Add the given Mixin to the given TypeScript declarations document.
*/
handleMixin(feature) {
const [namespacePath, mixinName] = splitReference(feature.name);
const parentNamespace = findOrCreateNamespace(this.root, namespacePath);
const transitiveMixins = [...this.transitiveMixins(feature)];
const constructorName = mixinName + 'Constructor';
// The mixin function. It takes a constructor, and returns an intersection
// of 1) the given constructor, 2) the constructor for this mixin, 3) the
// constructors for any other mixins that this mixin also applies.
parentNamespace.members.push(new ts.Function({
name: mixinName,
description: feature.description,
templateTypes: ['T extends new (...args: any[]) => {}'],
params: [
new ts.ParamType({ name: 'base', type: new ts.NameType('T') }),
],
returns: new ts.IntersectionType([
new ts.NameType('T'),
new ts.NameType(constructorName),
...transitiveMixins.map((mixin) => new ts.NameType(mixin.name + 'Constructor'))
]),
}));
if (this.root.isEsModule) {
// We need to import all of the synthetic constructor interfaces that our
// own signature references. We can assume they're exported from the same
// module that the mixin is defined in.
for (const mixin of transitiveMixins) {
if (mixin.sourceRange === undefined) {
continue;
}
const rootRelative = analyzerUrlToRelativePath(mixin.sourceRange.file, this.rootDir);
if (rootRelative === undefined) {
continue;
}
const fileRelative = path.relative(path.dirname(this.root.path), rootRelative);
const fromModuleSpecifier = fileRelative.startsWith('.') ? fileRelative : './' + fileRelative;
const identifiers = [{ identifier: mixin.name + 'Constructor' }];
if (!getImportedIdentifiers(this.root).has(mixin.name)) {
identifiers.push({ identifier: mixin.name });
}
this.root.members.push(new ts.Import({
identifiers,
fromModuleSpecifier,
}));
}
}
// The interface for a constructor of this mixin. Returns the instance
// interface (see below) when instantiated, and may also have methods of its
// own (static methods from the mixin class).
parentNamespace.members.push(new ts.Interface({
name: constructorName,
methods: [
new ts.Method({
name: 'new',
params: [
new ts.ParamType({
name: 'args',
type: new ts.ArrayType(ts.anyType),
rest: true,
}),
],
returns: new ts.NameType(mixinName),
}),
...this.handleMethods(feature.staticMethods.values()),
],
}));
if (this.root.isEsModule) {
// If any other mixin applies us, it will need to import our synthetic
// constructor interface.
this.root.members.push(new ts.Export({ identifiers: [{ identifier: constructorName }] }));
}
// The interface for instances of this mixin. Has the same name as the
// function.
parentNamespace.members.push(new ts.Interface({
name: mixinName,
properties: this.handleProperties(feature.properties.values()),
methods: this.handleMethods(feature.methods.values()),
extends: transitiveMixins.map((mixin) => mixin.name),
}));
}
/**
* Mixins can automatically apply other mixins, indicated by the @appliesMixin
* annotation. However, since those mixins may themselves apply other mixins,
* to know the full set of them we need to traverse down the tree.
*/
transitiveMixins(parentMixin, result) {
if (result === undefined) {
result = new Set();
}
for (const childRef of parentMixin.mixins) {
const childMixinSet = this.analysis.getFeatures({ id: childRef.identifier, kind: 'element-mixin' });
if (childMixinSet.size !== 1) {
this.warn(parentMixin, `Found ${childMixinSet.size} features for mixin ` +
`${childRef.identifier}, expected 1.`);
continue;
}
const childMixin = childMixinSet.values().next().value;
result.add(childMixin);
this.transitiveMixins(childMixin, result);
}
return result;
}
/**
* Add the given Class to the given TypeScript declarations document.
*/
handleClass(feature) {
if (!feature.className) {
this.warn(feature, `Could not find a name for class.`);
return;
}
const [namespacePath, name] = splitReference(feature.className);
const m = new ts.Class({ name });
m.description = feature.description;
m.properties = this.handleProperties(feature.properties.values());
m.methods = [
...this.handleMethods(feature.staticMethods.values(), { isStatic: true }),
...this.handleMethods(feature.methods.values())
];
m.constructorMethod =
this.handleConstructorMethod(feature.constructorMethod);
if (feature.superClass !== undefined) {
m.extends = feature.superClass.identifier;
}
m.mixins = feature.mixins.map((mixin) => mixin.identifier);
findOrCreateNamespace(this.root, namespacePath).members.push(m);
}
/**
* Add the given Function to the given TypeScript declarations document.
*/
handleFunction(feature) {
const [namespacePath, name] = splitReference(feature.name);
const f = new ts.Function({
name,
description: feature.description || feature.summary,
templateTypes: feature.templateTypes,
returns: closure_types_1.closureTypeToTypeScript(feature.return && feature.return.type, feature.templateTypes),
returnsDescription: feature.return && feature.return.desc
});
for (const param of feature.params || []) {
// TODO Handle parameter default values. Requires support from Analyzer
// which only handles this for class method parameters currently.
f.params.push(closure_types_1.closureParamToTypeScript(param.name, param.type, feature.templateTypes));
}
findOrCreateNamespace(this.root, namespacePath).members.push(f);
}
/**
* Convert the given Analyzer properties to their TypeScript declaration
* equivalent.
*/
handleProperties(analyzerProperties) {
const tsProperties = [];
for (const property of analyzerProperties) {
if (property.inheritedFrom || property.privacy === 'private' ||
this.excludeIdentifiers.has(property.name)) {
continue;
}
const p = new ts.Property({
name: property.name,
// TODO If this is a Polymer property with no default value, then the
// type should really be `<type>|undefined`.
type: closure_types_1.closureTypeToTypeScript(property.type),
readOnly: property.readOnly,
});
p.description = property.description || '';
tsProperties.push(p);
}
return tsProperties;
}
/**
* Convert the given Analyzer methods to their TypeScript declaration
* equivalent.
*/
handleMethods(analyzerMethods, opts) {
const tsMethods = [];
for (const method of analyzerMethods) {
if (method.inheritedFrom || method.privacy === 'private' ||
this.excludeIdentifiers.has(method.name)) {
continue;
}
tsMethods.push(this.handleMethod(method, opts));
}
return tsMethods;
}
/**
* Convert the given Analyzer method to the equivalent TypeScript declaration
*/
handleMethod(method, opts) {
const m = new ts.Method({
name: method.name,
returns: closure_types_1.closureTypeToTypeScript(method.return && method.return.type),
returnsDescription: method.return && method.return.desc,
isStatic: opts && opts.isStatic,
ignoreTypeCheck: this.documentationHasSuppressTypeCheck(method.jsdoc)
});
m.description = method.description || '';
let requiredAhead = false;
for (const param of reverseIter(method.params || [])) {
const tsParam = closure_types_1.closureParamToTypeScript(param.name, param.type);
tsParam.description = param.description || '';
if (param.defaultValue !== undefined) {
// Parameters with default values generally behave like optional
// parameters. However, unlike optional parameters, they may be
// followed by a required parameter, in which case the default value is
// set by explicitly passing undefined.
if (!requiredAhead) {
tsParam.optional = true;
}
else {
tsParam.type = new ts.UnionType([tsParam.type, ts.undefinedType]);
}
}
else if (!tsParam.optional) {
requiredAhead = true;
}
// Analyzer might know this is a rest parameter even if there was no
// JSDoc type annotation (or if it was wrong).
tsParam.rest = tsParam.rest || !!param.rest;
if (tsParam.rest && tsParam.type.kind !== 'array') {
// Closure rest parameter types are written without the Array syntax,
// but in TypeScript they must be explicitly arrays.
tsParam.type = new ts.ArrayType(tsParam.type);
}
m.params.unshift(tsParam);
}
return m;
}
documentationHasSuppressTypeCheck(annotation) {
if (!annotation) {
return false;
}
const annotationValue = annotation.tags.find((e) => e.title === 'suppress');
return annotationValue && annotationValue.description === '{checkTypes}' ||
false;
}
handleConstructorMethod(method) {
if (!method) {
return;
}
const m = this.handleMethod(method);
m.returns = undefined;
return m;
}
/**
* Add the given namespace to the given TypeScript declarations document.
*/
handleNamespace(feature) {
const ns = findOrCreateNamespace(this.root, feature.name.split('.'));
if (ns.kind === 'namespace') {
ns.description = feature.description || feature.summary || '';
}
}
/**
* Add a JavaScript import to the TypeScript declarations.
*/
handleJsImport(feature) {
const node = feature.astNode.node;
if (babel.isImportDeclaration(node)) {
const identifiers = [];
for (const specifier of node.specifiers) {
if (babel.isImportSpecifier(specifier)) {
// E.g. import {Foo, Bar as Baz} from './foo.js'
if (this.isResolvable(specifier.imported.name, feature)) {
identifiers.push({
identifier: specifier.imported.name,
alias: specifier.local.name,
});
}
}
else if (babel.isImportDefaultSpecifier(specifier)) {
// E.g. import foo from './foo.js'
if (this.isResolvable('default', feature)) {
identifiers.push({
identifier: 'default',
alias: specifier.local.name,
});
}
}
else if (babel.isImportNamespaceSpecifier(specifier)) {
// E.g. import * as foo from './foo.js'
identifiers.push({
identifier: ts.AllIdentifiers,
alias: specifier.local.name,
});
this.forceResolvable.add(specifier.local.name);
}
}
if (identifiers.length > 0) {
this.root.members.push(new ts.Import({
identifiers: identifiers,
fromModuleSpecifier: node.source && node.source.value,
}));
}
}
else if (
// Exports are handled as exports below. Analyzer also considers them
// imports when they export from another module.
!babel.isExportNamedDeclaration(node) &&
!babel.isExportAllDeclaration(node)) {
this.warn(feature, `Import with AST type ${node.type} not supported.`);
}
}
/**
* Add a JavaScript export to the TypeScript declarations.
*/
handleJsExport(feature) {
const node = feature.astNode.node;
if (babel.isExportAllDeclaration(node)) {
// E.g. export * from './foo.js'
this.root.members.push(new ts.Export({
identifiers: ts.AllIdentifiers,
fromModuleSpecifier: node.source && node.source.value,
}));
}
else if (babel.isExportNamedDeclaration(node)) {
const identifiers = [];
if (node.declaration) {
// E.g. export class Foo {}
for (const identifier of feature.identifiers) {
if (this.isResolvable(identifier, feature)) {
identifiers.push({ identifier });
}
}
}
else {
// E.g. export {Foo, Bar as Baz}
for (const specifier of node.specifiers) {
if (this.isResolvable(specifier.exported.name, feature) ||
this.isResolvable(specifier.local.name, feature)) {
identifiers.push({
identifier: specifier.local.name,
alias: specifier.exported.name,
});
}
}
}
if (identifiers.length > 0) {
this.root.members.push(new ts.Export({
identifiers,
fromModuleSpecifier: node.source && node.source.value,
}));
}
}
else {
this.warn(feature, `Export feature with AST node type ${node.type} not supported.`);
}
}
/**
* True if the given identifier can be resolved to a feature that will be
* exported as a TypeScript type.
*/
isResolvable(identifier, fromFeature) {
if (this.forceResolvable.has(identifier)) {
return true;
}
if (this.excludeIdentifiers.has(identifier)) {
return false;
}
const resolved = es_modules_1.resolveImportExportFeature(fromFeature, identifier, this.analyzerDoc);
return resolved !== undefined && resolved.feature !== undefined &&
!isPrivate(resolved.feature) && !isBehaviorImpl(resolved);
}
/**
* Add an HTML import to a TypeScript declarations file. For a given HTML
* import, we assume there is a corresponding declarations file that was
* generated by this same process.
*/
handleHtmlImport(feature) {
let sourcePath = analyzerUrlToRelativePath(feature.url, this.rootDir);
if (sourcePath === undefined) {
this.warn(feature, `Skipping HTML import without local file URL: ${feature.url}`);
return;
}
// When we analyze a package's Git repo, our dependencies are installed to
// "<repo>/bower_components". However, when this package is itself installed
// as a dependency, our own dependencies will instead be siblings, one
// directory up the tree.
//
// Analyzer (since 2.5.0) will set an import feature's URL to the resolved
// dependency path as discovered on disk. An import for "../foo/foo.html"
// will be resolved to "bower_components/foo/foo.html". Transform the URL
// back to the style that will work when this package is installed as a
// dependency.
sourcePath =
sourcePath.replace(/^(bower_components|node_modules)\//, '../');
// Polymer is a special case where types are output to the "types/"
// subdirectory instead of as sibling files, in order to avoid cluttering
// the repo. It would be more pure to store this fact in the Polymer
// gen-tsd.json config file and discover it when generating types for repos
// that depend on it, but that's probably more complicated than we need,
// assuming no other repos deviate from emitting their type declarations as
// sibling files.
sourcePath = sourcePath.replace(/^\.\.\/polymer\//, '../polymer/types/');
this.root.referencePaths.add(path.relative(path.dirname(this.root.path), makeDeclarationsFilename(sourcePath)));
}
}
/**
* Iterate over an array backwards.
*/
function* reverseIter(arr) {
for (let i = arr.length - 1; i >= 0; i--) {
yield arr[i];
}
}
/**
* Find a document's global namespace declaration, or create one if it doesn't
* exist.
*/
function findOrCreateGlobalNamespace(doc) {
for (const member of doc.members) {
if (member.kind === 'globalNamespace') {
return member;
}
}
const globalNamespace = new ts.GlobalNamespace();
doc.members.push(globalNamespace);
return globalNamespace;
}
/**
* Traverse the given node to find the namespace AST node with the given path.
* If it could not be found, add one and return it.
*/
function findOrCreateNamespace(root, path) {
if (!path.length) {
return root;
}
let first;
for (const member of root.members) {
if (member.kind === 'namespace' && member.name === path[0]) {
first = member;
break;
}
}
if (!first) {
first = new ts.Namespace({ name: path[0] });
root.members.push(first);
}
return findOrCreateNamespace(first, path.slice(1));
}
/**
* Traverse the given node to find the interface AST node with the given path.
* If it could not be found, add one and return it.
*/
function findOrCreateInterface(root, reference) {
const [namespacePath, name] = splitReference(reference);
const namespace_ = findOrCreateNamespace(root, namespacePath);
for (const member of namespace_.members) {
if (member.kind === 'interface' && member.name === name) {
return member;
}
}
const i = new ts.Interface({ name });
namespace_.members.push(i);
return i;
}
/**
* Type guard that checks if a Polymer Analyzer feature is a PolymerElement.
*/
function isPolymerElement(feature) {
return feature.kinds.has('polymer-element');
}
/**
* Return whether a reference looks like it is a FooBehaviorImpl style behavior
* object, which we want to ignore.
*
* Polymer behavior libraries are often written like:
*
* /** @polymerBehavior FooBehavior *\/
* export const FooBehaviorImpl = {};
*
* /** @polymerBehavior *\/
* export const FooBehavior = [FooBehaviorImpl, OtherBehavior];
*
* In this case, Analyzer merges FooBehaviorImpl into FooBehavior and does not
* emit a behavior feature for FooBehaviorImpl. However, there is still an
* export feature for FooBehaviorImpl, so we exclude it here.
*/
function isBehaviorImpl(reference) {
return reference.feature !== undefined &&
reference.feature.kinds.has('behavior') &&
reference.feature.name !==
reference.identifier;
}
/**
* Return whether the given Analyzer feature has "private" visibility.
*/
function isPrivate(feature) {
return feature.privacy === 'private';
}
/**
* Convert kebab-case to CamelCase.
*/
function kebabToCamel(s) {
return s.replace(/(^|-)(.)/g, (_match, _p0, p1) => p1.toUpperCase());
}
/**
* Split a reference into an array of namespace path parts, and a name part
* (e.g. `"Foo.Bar.Baz"` => `[ ["Foo", "Bar"], "Baz" ]`).
*/
function splitReference(reference) {
const parts = reference.split('.');
const namespacePath = parts.slice(0, -1);
const name = parts[parts.length - 1];
return [namespacePath, name];
}
//# sourceMappingURL=gen-ts.js.map
;