UNPKG

@polymer/gen-typescript-declarations

Version:

Generate TypeScript type declarations for Polymer components.

1,032 lines (1,030 loc) 44.2 kB
"use strict"; /** * @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