Socket
Socket
Sign inDemoInstall

@polymer/gen-typescript-declarations

Package Overview
Dependencies
126
Maintainers
7
Versions
25
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.3.0 to 1.4.0

lib/es-modules.d.ts

15

CHANGELOG.md

@@ -9,2 +9,17 @@ # Changelog

## [1.4.0] - 2018-07-25
- Support for ES module imports and exports.
- Warnings are now printed with file names, line numbers, and code snippets.
- Add `autoImport` config option to automatically add ES module imports when
particular identifiers are referenced.
- Automatically detect if a project uses NPM or Bower and configure module
resolution settings accordingly.
- Automatically import/export synthetic mixin constructor interfaces.
- Superclasses and mixins are now emitted for classes.
- Element super classes are now emitted.
- Legacy Polymer elements now extend `LegacyElementMixin` and `HTMLElement`
instead of `PolymerElement`.
- Mixin instance interfaces now extend the instance interfaces for the mixins
that they automatically apply.
## [1.3.0] - 2018-06-29

@@ -11,0 +26,0 @@ - Generate typings for class constructors.

4

lib/cli.js

@@ -25,4 +25,4 @@ "use strict";

const gen_ts_1 = require("./gen-ts");
const commandLineArgs = require('command-line-args');
const commandLineUsage = require('command-line-usage');
const commandLineArgs = require("command-line-args");
const commandLineUsage = require("command-line-usage");
const argDefs = [

@@ -29,0 +29,0 @@ {

@@ -257,3 +257,5 @@ "use strict";

// Cast because type is wrong: `FunctionType.result` is not an array.
node.result ? convert(node.result, templateTypes) : ts.anyType);
node.result ?
convert(node.result, templateTypes) :
ts.anyType);
}

@@ -267,3 +269,3 @@ }

}
let fieldType = field.value ? convert(field.value, templateTypes) : ts.anyType;
const fieldType = field.value ? convert(field.value, templateTypes) : ts.anyType;
// In Closure you can't declare a record field optional, instead you

@@ -270,0 +272,0 @@ // declare `foo: bar|undefined`. In TypeScript we can represent this as

@@ -53,2 +53,12 @@ /**

};
/**
* A map from an ES module path (relative to the analysis root directory) to
* an array of identifiers exported by that module. If any of those
* identifiers are encountered in a generated typings file, an import for that
* identifier from the specified module will be inserted into the typings
* file.
*/
autoImport?: {
[modulePath: string]: string[];
};
}

@@ -55,0 +65,0 @@ /**

@@ -21,2 +21,4 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
const babel = require("@babel/types");
const fsExtra = require("fs-extra");
const minimatch = require("minimatch");

@@ -27,2 +29,3 @@ const path = require("path");

const closure_types_1 = require("./closure-types");
const es_modules_1 = require("./es-modules");
const ts = require("./ts-ast");

@@ -40,9 +43,16 @@ const defaultExclude = [

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 }),
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 analyzerToAst(analysis, config, rootDir)) {
for (const tsDoc of yield analyzerToAst(analysis, config, rootDir)) {
outFiles.set(tsDoc.path, tsDoc.serialize());

@@ -59,74 +69,140 @@ }

function analyzerToAst(analysis, config, rootDir) {
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 || {}));
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) {
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 warningPrinter = new analyzer.WarningPrinter(process.stderr, { maxCodeLines: 1 });
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)),
});
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();
yield warningPrinter.printWarnings(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);
}
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') {
continue;
}
const sourcePath = analyzerUrlToRelativePath(jsDoc.url, rootDir);
if (sourcePath === undefined) {
console.warn(`Skipping source document without local file URL: ${jsDoc.url}`);
const importPath = autoImport.get(node.name);
if (importPath === undefined) {
continue;
}
if (excludeFiles.some((r) => r.match(sourcePath))) {
if (alreadyImported.has(node.name)) {
continue;
}
const filename = makeDeclarationsFilename(sourcePath);
let docs = declarationDocs.get(filename);
if (!docs) {
docs = [];
declarationDocs.set(filename, docs);
if (makeDeclarationsFilename(importPath) === tsDoc.path) {
// Don't import from yourself.
continue;
}
docs.push(jsDoc);
const fileRelative = path.relative(path.dirname(tsDoc.path), importPath);
const fromModuleSpecifier = fileRelative.startsWith('.') ? fileRelative : './' + fileRelative;
tsDoc.members.push(new ts.Import({
identifiers: [{ identifier: node.name }],
fromModuleSpecifier,
}));
alreadyImported.add(node.name);
}
const tsDocs = [];
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)),
});
for (const analyzerDoc of analyzerDocs) {
handleDocument(analysis, analyzerDoc, tsDoc, rootDir, config.excludeIdentifiers || []);
}
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;
}
/**
* 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);
}
}
}
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);
}
return tsDocs;
return identifiers;
}

@@ -165,376 +241,573 @@ /**

}
/**
* Extend the given TypeScript declarations document with all of the relevant
* items in the given Polymer Analyzer document.
*/
function handleDocument(analysis, doc, root, rootDir, excludeIdentifiers) {
for (const feature of doc.getFeatures()) {
if (excludeIdentifiers.some((id) => feature.identifiers.has(id))) {
continue;
class TypeGenerator {
constructor(root, analysis, analyzerDoc, rootDir, excludeIdentifiers) {
this.root = root;
this.analysis = analysis;
this.analyzerDoc = analyzerDoc;
this.rootDir = rootDir;
this.excludeIdentifiers = excludeIdentifiers;
this.warnings = [];
}
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 (this.excludeIdentifiers.some((id) => feature.identifiers.has(id))) {
continue;
}
if (feature.privacy === 'private') {
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, this.analyzerDoc);
}
else if (feature.kinds.has('export')) {
this.handleJsExport(feature, this.analyzerDoc);
}
}
if (feature.privacy === 'private') {
continue;
}
/**
* 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);
}
if (feature.kinds.has('element')) {
handleElement(feature, root);
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 if (feature.kinds.has('behavior')) {
handleBehavior(feature, root);
else {
this.warn(feature, `Could not find element name.`);
return;
}
else if (feature.kinds.has('element-mixin')) {
handleMixin(feature, root, analysis);
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');
}
}
else if (feature.kinds.has('class')) {
handleClass(feature, root);
if (constructable) {
this.handleClass(feature);
if (legacyPolymerInterfaces.length > 0) {
// Augment the class interface.
parent.members.push(new ts.Interface({
name: shortName,
extends: legacyPolymerInterfaces,
}));
}
}
else if (feature.kinds.has('function')) {
handleFunction(feature, root);
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,
],
}));
}
else if (feature.kinds.has('namespace')) {
handleNamespace(feature, root);
// 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),
}));
}
else if (feature.kinds.has('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 === doc.url) {
handleImport(feature, root, rootDir);
}
/**
* 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,
}));
}
}
}
}
/**
* Add the given Element to the given TypeScript declarations document.
*/
function handleElement(feature, root) {
// 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(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 = root;
}
else {
console.error(`Could not find element name defined in ${feature.sourceRange}`);
return;
}
const behaviors = isPolymerElement(feature) ?
feature.behaviorAssignments.map((behavior) => behavior.identifier) :
[];
if (constructable) {
parent.members.push(new ts.Class({
name: shortName,
description: feature.description || feature.summary,
extends: (feature.extends) ||
(isPolymerElement(feature) ? 'Polymer.Element' : 'HTMLElement'),
mixins: feature.mixins.map((mixin) => mixin.identifier),
properties: handleProperties(feature.properties.values()),
// 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: [
...handleMethods(feature.staticMethods.values(), { isStatic: true }),
...handleMethods(feature.methods.values()),
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()),
],
constructorMethod: handleConstructorMethod(feature.constructorMethod)
}));
if (behaviors.length > 0) {
// We need to augment our class declaration with some behaviors. Behaviors
// are interfaces, so our class can't directly extend them, like we can do
// with mixin functions. However, the class declaration implicitly creates
// a corresponding interface with the same name, and we can augment that
// with the behavior interfaces using declaration merging.
parent.members.push(new ts.Interface({
name: shortName,
extends: behaviors,
}));
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),
}));
}
else {
// TODO How do we handle mixins when we are emitting an interface? We don't
// currently define interfaces for mixins, so we can't just add them to
// extends.
const i = new ts.Interface({
name: shortName,
/**
* 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,
properties: 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: handleMethods(feature.methods.values()),
templateTypes: feature.templateTypes,
returns: closure_types_1.closureTypeToTypeScript(feature.return && feature.return.type, feature.templateTypes),
returnsDescription: feature.return && feature.return.desc
});
if (isPolymerElement(feature)) {
i.extends.push('Polymer.Element');
i.extends.push(...behaviors);
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));
}
parent.members.push(i);
findOrCreateNamespace(this.root, namespacePath).members.push(f);
}
// 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(root, 'HTMLElementTagNameMap');
elementMap.properties.push(new ts.Property({
name: feature.tagName,
type: new ts.NameType(fullName),
}));
/**
* 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') {
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;
}
}
/**
* Add the given Polymer Behavior to the given TypeScript declarations
* document.
*/
function handleBehavior(feature, root) {
if (!feature.className) {
console.error(`Could not find a name for behavior defined in ${feature.sourceRange}`);
return;
/**
* 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') {
continue;
}
tsMethods.push(this.handleMethod(method, opts));
}
return tsMethods;
}
const [namespacePath, className] = splitReference(feature.className);
const ns = findOrCreateNamespace(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: handleProperties(feature.properties.values()),
methods: 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.
*/
function handleMixin(feature, root, analysis) {
const [namespacePath, mixinName] = splitReference(feature.name);
const parentNamespace = findOrCreateNamespace(root, namespacePath);
// 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(mixinName + 'Constructor'),
...Array.from(transitiveMixins(feature, analysis))
.map((mixin) => new ts.NameType(mixin + 'Constructor'))
]),
}));
// 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: mixinName + 'Constructor',
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),
}),
...handleMethods(feature.staticMethods.values()),
],
}));
// The interface for instances of this mixin. Has the same name as the
// function.
parentNamespace.members.push(new ts.Interface({
name: mixinName,
properties: handleProperties(feature.properties.values()),
methods: handleMethods(feature.methods.values()),
}));
}
;
/**
* 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.
*/
function transitiveMixins(parentMixin, analysis, result) {
if (result === undefined) {
result = new Set();
/**
* 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;
}
for (const childRef of parentMixin.mixins) {
result.add(childRef.identifier);
const childMixinSet = analysis.getFeatures({ id: childRef.identifier, kind: 'element-mixin' });
if (childMixinSet.size !== 1) {
console.error(`Found ${childMixinSet.size} features for mixin ` +
`${childRef.identifier}, expected 1.`);
continue;
documentationHasSuppressTypeCheck(annotation) {
if (!annotation) {
return false;
}
const childMixin = childMixinSet.values().next().value;
transitiveMixins(childMixin, analysis, result);
const annotationValue = annotation.tags.find((e) => e.title === 'suppress');
return annotationValue && annotationValue.description === '{checkTypes}' ||
false;
}
return result;
}
/**
* Add the given Class to the given TypeScript declarations document.
*/
function handleClass(feature, root) {
if (!feature.className) {
console.error(`Could not find a name for class defined in ${feature.sourceRange}`);
return;
handleConstructorMethod(method) {
if (!method) {
return;
}
const m = this.handleMethod(method);
m.returns = undefined;
return m;
}
const [namespacePath, name] = splitReference(feature.className);
const m = new ts.Class({ name });
m.description = feature.description;
m.properties = handleProperties(feature.properties.values());
m.methods = [
...handleMethods(feature.staticMethods.values(), { isStatic: true }),
...handleMethods(feature.methods.values())
];
m.constructorMethod = handleConstructorMethod(feature.constructorMethod);
findOrCreateNamespace(root, namespacePath).members.push(m);
}
/**
* Add the given Function to the given TypeScript declarations document.
*/
function handleFunction(feature, root) {
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));
/**
* 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 || '';
}
}
findOrCreateNamespace(root, namespacePath).members.push(f);
}
/**
* Convert the given Analyzer properties to their TypeScript declaration
* equivalent.
*/
function handleProperties(analyzerProperties) {
const tsProperties = [];
for (const property of analyzerProperties) {
if (property.inheritedFrom || property.privacy === 'private') {
continue;
/**
* Add a JavaScript import to the TypeScript declarations.
*/
handleJsImport(feature, doc) {
const node = feature.astNode.node;
const isResolvable = (identifier) => es_modules_1.resolveImportExportFeature(feature, identifier, doc) !== undefined;
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 (isResolvable(specifier.imported.name)) {
identifiers.push({
identifier: specifier.imported.name,
alias: specifier.local.name,
});
}
}
else if (babel.isImportDefaultSpecifier(specifier)) {
// E.g. import foo from './foo.js'
if (isResolvable('default')) {
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,
});
}
}
if (identifiers.length > 0) {
this.root.members.push(new ts.Import({
identifiers: identifiers,
fromModuleSpecifier: node.source && node.source.value,
}));
}
}
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);
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.`);
}
}
return tsProperties;
}
/**
* Convert the given Analyzer methods to their TypeScript declaration
* equivalent.
*/
function handleMethods(analyzerMethods, opts) {
const tsMethods = [];
for (const method of analyzerMethods) {
if (method.inheritedFrom || method.privacy === 'private') {
continue;
/**
* Add a JavaScript export to the TypeScript declarations.
*/
handleJsExport(feature, doc) {
const node = feature.astNode.node;
const isResolvable = (identifier) => es_modules_1.resolveImportExportFeature(feature, identifier, doc) !== undefined;
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,
}));
}
tsMethods.push(handleMethod(method, opts));
}
return tsMethods;
}
/**
* Convert the given Analyzer method to the equivalent TypeScript declaration
*/
function 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: 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 if (babel.isExportNamedDeclaration(node)) {
const identifiers = [];
if (node.declaration) {
// E.g. export class Foo {}
for (const identifier of feature.identifiers) {
if (isResolvable(identifier)) {
identifiers.push({ identifier });
}
}
}
else {
tsParam.type = new ts.UnionType([tsParam.type, ts.undefinedType]);
// E.g. export {Foo, Bar as Baz}
for (const specifier of node.specifiers) {
if (isResolvable(specifier.exported.name)) {
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 if (!tsParam.optional) {
requiredAhead = true;
else {
this.warn(feature, `Export feature with AST node type ${node.type} not supported.`);
}
// 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);
}
/**
* 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;
}
m.params.unshift(tsParam);
// 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)));
}
return m;
}
function documentationHasSuppressTypeCheck(annotation) {
if (!annotation) {
return false;
}
const annotationValue = annotation.tags.find(e => e.title === 'suppress');
return annotationValue && annotationValue.description === '{checkTypes}' ||
false;
}
function handleConstructorMethod(method) {
if (!method) {
return;
}
const m = handleMethod(method);
m.returns = undefined;
return m;
}
/**

@@ -549,42 +822,16 @@ * Iterate over an array backwards.

/**
* Add the given namespace to the given TypeScript declarations document.
* Find a document's global namespace declaration, or create one if it doesn't
* exist.
*/
function handleNamespace(feature, tsDoc) {
const ns = findOrCreateNamespace(tsDoc, feature.name.split('.'));
if (ns.kind === 'namespace') {
ns.description = feature.description || feature.summary || '';
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;
}
/**
* 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.
*/
function handleImport(feature, tsDoc, rootDir) {
let sourcePath = analyzerUrlToRelativePath(feature.url, rootDir);
if (sourcePath === undefined) {
console.warn(`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/');
tsDoc.referencePaths.add(path.relative(path.dirname(tsDoc.path), makeDeclarationsFilename(sourcePath)));
}
/**
* Traverse the given node to find the namespace AST node with the given path.

@@ -591,0 +838,0 @@ * If it could not be found, add one and return it.

@@ -13,3 +13,11 @@ /**

import { ParamType, Type } from './types';
export declare type Declaration = Namespace | Class | Interface | Function | ConstValue;
/** An AST node that can appear directly in a document or namespace. */
export declare type Declaration = GlobalNamespace | Namespace | Class | Interface | Function | ConstValue | Import | Export;
export declare class GlobalNamespace {
readonly kind: string;
members: Declaration[];
constructor(members?: Declaration[]);
traverse(): Iterable<Node>;
serialize(_depth?: number): string;
}
export declare class Namespace {

@@ -122,1 +130,48 @@ readonly kind: string;

}
/**
* The "*" token in an import or export.
*/
export declare const AllIdentifiers: unique symbol;
export declare type AllIdentifiers = typeof AllIdentifiers;
/**
* An identifier that is imported, possibly with a different name.
*/
export interface ImportSpecifier {
identifier: string | AllIdentifiers;
alias?: string;
}
/**
* A JavaScript module import.
*/
export declare class Import {
readonly kind: string;
identifiers: ImportSpecifier[];
fromModuleSpecifier: string;
constructor(data: {
identifiers: ImportSpecifier[];
fromModuleSpecifier: string;
});
traverse(): Iterable<Node>;
serialize(_depth?: number): string;
}
/**
* An identifier that is imported, possibly with a different name.
*/
export interface ExportSpecifier {
identifier: string;
alias?: string;
}
/**
* A JavaScript module export.
*/
export declare class Export {
readonly kind: string;
identifiers: ExportSpecifier[] | AllIdentifiers;
fromModuleSpecifier: string;
constructor(data: {
identifiers: ExportSpecifier[] | AllIdentifiers;
fromModuleSpecifier?: string;
});
traverse(): Iterable<Node>;
serialize(_depth?: number): string;
}

@@ -15,2 +15,23 @@ "use strict";

const types_1 = require("./types");
class GlobalNamespace {
constructor(members) {
this.kind = 'globalNamespace';
this.members = members || [];
}
*traverse() {
for (const m of this.members) {
yield* m.traverse();
}
yield this;
}
serialize(_depth = 0) {
let out = `declare global {\n`;
for (const member of this.members) {
out += '\n' + member.serialize(1);
}
out += `}\n`;
return out;
}
}
exports.GlobalNamespace = GlobalNamespace;
class Namespace {

@@ -287,6 +308,88 @@ constructor(data) {

serialize(depth = 0) {
return `${formatting_1.indent(depth)}const ${this.name}: ${this.type.serialize()};\n`;
return formatting_1.indent(depth) + (depth === 0 ? 'declare ' : '') +
`const ${this.name}: ${this.type.serialize()};\n`;
}
}
exports.ConstValue = ConstValue;
/**
* The "*" token in an import or export.
*/
exports.AllIdentifiers = Symbol('*');
/**
* A JavaScript module import.
*/
class Import {
constructor(data) {
this.kind = 'import';
this.identifiers = data.identifiers;
this.fromModuleSpecifier = data.fromModuleSpecifier;
}
*traverse() {
yield this;
}
serialize(_depth = 0) {
if (this.identifiers.some((i) => i.identifier === exports.AllIdentifiers)) {
// Namespace imports have a different form. You can also have a default
// import, but no named imports.
const parts = [];
for (const identifier of this.identifiers) {
if (identifier.identifier === 'default') {
parts.push(identifier.alias);
}
else if (identifier.identifier === exports.AllIdentifiers) {
parts.push(`* as ${identifier.alias}`);
}
}
return `import ${parts.join(', ')} ` +
`from '${this.fromModuleSpecifier}';\n`;
}
else {
const parts = [];
for (const { identifier, alias } of this.identifiers) {
if (identifier === exports.AllIdentifiers) {
// Can't happen, see above.
continue;
}
parts.push(identifier +
(alias !== undefined && alias !== identifier ? ` as ${alias}` :
''));
}
return `import {${parts.join(', ')}} ` +
`from '${this.fromModuleSpecifier}';\n`;
}
}
}
exports.Import = Import;
/**
* A JavaScript module export.
*/
class Export {
constructor(data) {
this.kind = 'export';
this.identifiers = data.identifiers;
this.fromModuleSpecifier = data.fromModuleSpecifier || '';
}
*traverse() {
yield this;
}
serialize(_depth = 0) {
let out = 'export ';
if (this.identifiers === exports.AllIdentifiers) {
out += '*';
}
else {
const specifiers = this.identifiers.map(({ identifier, alias }) => {
return identifier +
(alias !== undefined && alias !== identifier ? ` as ${alias}` : '');
});
out += `{${specifiers.join(', ')}}`;
}
if (this.fromModuleSpecifier !== '') {
out += ` from '${this.fromModuleSpecifier}'`;
}
out += ';\n';
return out;
}
}
exports.Export = Export;
//# sourceMappingURL=declarations.js.map

@@ -19,2 +19,3 @@ /**

header: string;
isEsModule: boolean;
constructor(data: {

@@ -25,2 +26,3 @@ path: string;

header?: string;
isEsModule?: boolean;
});

@@ -27,0 +29,0 @@ /**

@@ -14,2 +14,3 @@ "use strict";

const formatting_1 = require("./formatting");
const index_1 = require("./index");
class Document {

@@ -22,2 +23,3 @@ constructor(data) {

this.header = data.header || '';
this.isEsModule = data.isEsModule || false;
}

@@ -56,2 +58,8 @@ /**

out += this.members.map((m) => m.serialize()).join('\n');
// If these are typings for an ES module, we want to be sure that TypeScript
// will treat them as one too, which requires at least one import or export.
if (this.isEsModule === true &&
!this.members.some((m) => m.kind === 'import' || m.kind === 'export')) {
out += '\n' + (new index_1.Export({ identifiers: [] })).serialize();
}
return out;

@@ -58,0 +66,0 @@ }

@@ -19,3 +19,2 @@ "use strict";

}
;
*traverse() {

@@ -22,0 +21,0 @@ yield this;

{
"name": "@polymer/gen-typescript-declarations",
"version": "1.3.0",
"version": "1.4.0",
"description": "Generate TypeScript type declarations for Polymer components.",
"homepage": "https://github.com/Polymer/tools/tree/master/packages/gen-typescript-declarations",
"repository": "github:Polymer/tools",
"bugs": "https://github.com/Polymer/tools/issues",
"license": "BSD-3-Clause",
"author": "The Polymer Project Authors",
"main": "lib/gen-ts.js",

@@ -15,5 +20,10 @@ "types": "lib/gen-ts.d.ts",

"dependencies": {
"@babel/types": "^7.0.0-beta.42",
"@types/command-line-args": "^5.0.0",
"@types/command-line-usage": "^5.0.1",
"@types/doctrine": "0.0.3",
"@types/fs-extra": "^5.0.0",
"@types/glob": "^5.0.35",
"@types/parse5": "^2.2.34",
"babylon": "^7.0.0-beta.42",
"command-line-args": "^5.0.1",

@@ -30,12 +40,7 @@ "command-line-usage": "^5.0.2",

"devDependencies": {
"@types/chai": "^4.0.4",
"@types/mocha": "^5.2.3",
"bower": "^1.8.2",
"chai": "^4.1.2",
"clang-format": "^1.1.0",
"mocha": "^5.0.0",
"rimraf": "^2.6.2",
"source-map-support": "^0.5.0",
"tsc-then": "^1.0.1",
"typescript": "^2.7.2"
"tsc-then": "^1.0.1"
},

@@ -45,21 +50,13 @@ "scripts": {

"format": "find src -name '*.ts' -not -path 'src/test/fixtures/*' -not -path 'src/test/goldens/*' | xargs clang-format --style=file -i",
"build": "rm -f node_modules/parse5/lib/index.d.ts && npm run clean && tsc",
"lint": "tslint --project . --format stylish",
"build": "npm run clean && tsc",
"build:watch": "tsc --watch",
"prepack": "npm run build",
"prepublishOnly": "npm run test",
"test": "npm run test:setup && npm run build && mocha",
"test": "npm run test:unit",
"test:unit": "npm run lint && npm run test:setup && npm run build && mocha",
"test:watch": "tsc-then -- mocha --color",
"test:setup": "scripts/setup-fixtures.sh",
"test:make-goldens": "npm run build && scripts/make-goldens.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Polymer/gen-typescript-declarations.git"
},
"author": "The Polymer Project Authors",
"license": "BSD-3-Clause",
"bugs": {
"url": "https://github.com/Polymer/gen-typescript-declarations/issues"
},
"homepage": "https://github.com/Polymer/gen-typescript-declarations#readme"
"test:setup": "bash scripts/setup-fixtures.sh",
"update-goldens": "npm run build && scripts/update-goldens.js"
}
}
# gen-typescript-declarations
[![Build Status](https://travis-ci.org/Polymer/gen-typescript-declarations.svg?branch=master)](https://travis-ci.org/Polymer/gen-typescript-declarations)
[![NPM version](https://img.shields.io/npm/v/@polymer/gen-typescript-declarations.svg)](https://www.npmjs.com/package/@polymer/gen-typescript-declarations)

@@ -13,13 +12,8 @@

Typings for Polymer 2 are planned for regular release starting with version
2.4, and are available in the
[`types/`](https://github.com/Polymer/polymer/tree/master/types) directory on
the `master` branch now.
Once Polymer 2.4 is released, to use the typings, install Polymer normally from
Bower, and add a [triple-slash
To use the typings, install Polymer normally from Bower (versions 2.4 and
above), and add a [triple-slash
directive](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html)
anywhere in your TypeScript project for the typings you require. Each HTML
import from Polymer has a corresponding typings file. For example, if you
depend on `polymer-element.html`:
import from Polymer has a corresponding typings file. For example, if you depend
on `polymer-element.html`:

@@ -26,0 +20,0 @@ ```ts

@@ -18,4 +18,4 @@ /**

const commandLineArgs = require('command-line-args') as any;
const commandLineUsage = require('command-line-usage') as any;
import commandLineArgs = require('command-line-args');
import commandLineUsage = require('command-line-usage');

@@ -59,3 +59,3 @@ const argDefs = [

interface args {
interface Args {
help?: boolean;

@@ -70,3 +70,3 @@ version?: boolean;

async function run(argv: string[]) {
const args = commandLineArgs(argDefs, {argv}) as args;
const args = commandLineArgs(argDefs, {argv}) as Args;

@@ -73,0 +73,0 @@ if (args.help) {

@@ -219,3 +219,3 @@ /**

case 'void':
return false
return false;
}

@@ -287,3 +287,5 @@ return true;

// Cast because type is wrong: `FunctionType.result` is not an array.
node.result ? convert(node.result as any, templateTypes) : ts.anyType);
node.result ?
convert(node.result as {} as doctrine.Type, templateTypes) :
ts.anyType);
}

@@ -299,3 +301,3 @@ }

}
let fieldType =
const fieldType =
field.value ? convert(field.value, templateTypes) : ts.anyType;

@@ -302,0 +304,0 @@

@@ -12,3 +12,5 @@ /**

import * as babel from '@babel/types';
import * as jsdoc from 'doctrine';
import * as fsExtra from 'fs-extra';
import * as minimatch from 'minimatch';

@@ -21,2 +23,3 @@ import * as path from 'path';

import {closureParamToTypeScript, closureTypeToTypeScript} from './closure-types';
import {isEsModuleDocument, resolveImportExportFeature} from './es-modules';
import * as ts from './ts-ast';

@@ -67,2 +70,11 @@

renameTypes?: {[name: string]: string};
/**
* A map from an ES module path (relative to the analysis root directory) to
* an array of identifiers exported by that module. If any of those
* identifiers are encountered in a generated typings file, an import for that
* identifier from the specified module will be inserted into the typings
* file.
*/
autoImport?: {[modulePath: string]: string[]};
}

@@ -82,10 +94,18 @@

rootDir: string, config: Config): Promise<Map<string, string>> {
// Note that many Bower projects also have a node_modules/, but the reverse is
// unlikely.
const isBowerProject =
await fsExtra.pathExists(path.join(rootDir, 'bower_components')) === true;
const a = new analyzer.Analyzer({
urlLoader: new analyzer.FsUrlLoader(rootDir),
urlResolver: new analyzer.PackageUrlResolver({packageDir: rootDir}),
urlResolver: new analyzer.PackageUrlResolver({
packageDir: rootDir,
componentDir: isBowerProject ? 'bower_components/' : 'node_modules/',
}),
moduleResolution: isBowerProject ? undefined : 'node',
});
const analysis = await a.analyzePackage();
const outFiles = new Map<string, string>();
for (const tsDoc of analyzerToAst(analysis, config, rootDir)) {
outFiles.set(tsDoc.path, tsDoc.serialize())
for (const tsDoc of await analyzerToAst(analysis, config, rootDir)) {
outFiles.set(tsDoc.path, tsDoc.serialize());
}

@@ -99,5 +119,5 @@ return outFiles;

*/
function analyzerToAst(
async function analyzerToAst(
analysis: analyzer.Analysis, config: Config, rootDir: string):
ts.Document[] {
Promise<ts.Document[]> {
const excludeFiles = (config.excludeFiles || config.exclude || defaultExclude)

@@ -110,2 +130,12 @@ .map((p) => new minimatch.Minimatch(p));

// Map from identifier to the module path that exports it.
const autoImportMap = new Map<string, string>();
if (config.autoImport !== undefined) {
for (const importPath in config.autoImport) {
for (const identifier of config.autoImport[importPath]) {
autoImportMap.set(identifier, importPath);
}
}
}
const analyzerDocs = [

@@ -148,2 +178,5 @@ ...analysis.getFeatures({kind: 'html-document'}),

const tsDocs = [];
const warningPrinter =
new analyzer.WarningPrinter(process.stderr, {maxCodeLines: 1});
for (const [declarationsFilename, analyzerDocs] of declarationDocs) {

@@ -157,9 +190,18 @@ const tsDoc = new ts.Document({

for (const analyzerDoc of analyzerDocs) {
handleDocument(
if (isEsModuleDocument(analyzerDoc)) {
tsDoc.isEsModule = true;
}
}
for (const analyzerDoc of analyzerDocs) {
const generator = new TypeGenerator(
tsDoc,
analysis,
analyzerDoc,
tsDoc,
rootDir,
config.excludeIdentifiers || []);
generator.handleDocument();
await warningPrinter.printWarnings(generator.warnings);
}
for (const ref of tsDoc.referencePaths) {

@@ -182,2 +224,3 @@ const resolvedRef = path.resolve(rootDir, path.dirname(tsDoc.path), ref);

}
addAutoImports(tsDoc, autoImportMap);
tsDoc.simplify();

@@ -193,2 +236,52 @@ // Include even documents with no members. They might be dependencies of

/**
* Insert imports into the typings for any referenced identifiers listed in the
* autoImport configuration, unless they are already imported.
*/
function addAutoImports(tsDoc: ts.Document, autoImport: Map<string, string>) {
const alreadyImported = getImportedIdentifiers(tsDoc);
for (const node of tsDoc.traverse()) {
if (node.kind !== 'name') {
continue;
}
const importPath = autoImport.get(node.name);
if (importPath === undefined) {
continue;
}
if (alreadyImported.has(node.name)) {
continue;
}
if (makeDeclarationsFilename(importPath) === tsDoc.path) {
// Don't import from yourself.
continue;
}
const fileRelative = path.relative(path.dirname(tsDoc.path), importPath);
const fromModuleSpecifier =
fileRelative.startsWith('.') ? fileRelative : './' + fileRelative;
tsDoc.members.push(new ts.Import({
identifiers: [{identifier: node.name}],
fromModuleSpecifier,
}));
alreadyImported.add(node.name);
}
}
/**
* Return all local identifiers imported by the given typings.
*/
function getImportedIdentifiers(tsDoc: ts.Document): Set<string> {
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

@@ -230,431 +323,652 @@ * path (e.g. "file:/foo/bar"). Return just the file path, relative to our

interface MaybePrivate {
privacy?: 'public'|'private'|'protected'
privacy?: 'public'|'private'|'protected';
}
/**
* Extend the given TypeScript declarations document with all of the relevant
* items in the given Polymer Analyzer document.
*/
function handleDocument(
analysis: analyzer.Analysis,
doc: analyzer.Document,
root: ts.Document,
rootDir: string,
excludeIdentifiers: string[]) {
for (const feature of doc.getFeatures()) {
if (excludeIdentifiers.some((id) => feature.identifiers.has(id))) {
continue;
class TypeGenerator {
public warnings: analyzer.Warning[] = [];
constructor(
private root: ts.Document,
private analysis: analyzer.Analysis,
private analyzerDoc: analyzer.Document,
private rootDir: string,
private excludeIdentifiers: string[]) {
}
private warn(feature: analyzer.Feature, message: string) {
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 (this.excludeIdentifiers.some((id) => feature.identifiers.has(id))) {
continue;
}
if ((feature as MaybePrivate).privacy === 'private') {
continue;
}
if (feature.kinds.has('element')) {
this.handleElement(feature as analyzer.Element);
} else if (feature.kinds.has('behavior')) {
this.handleBehavior(feature as analyzer.PolymerBehavior);
} else if (feature.kinds.has('element-mixin')) {
this.handleMixin(feature as analyzer.ElementMixin);
} else if (feature.kinds.has('class')) {
this.handleClass(feature as analyzer.Class);
} else if (feature.kinds.has('function')) {
this.handleFunction(feature as AnalyzerFunction);
} else if (feature.kinds.has('namespace')) {
this.handleNamespace(feature as analyzer.Namespace);
} 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 as analyzer.Import);
}
} else if (feature.kinds.has('js-import')) {
this.handleJsImport(
feature as analyzer.JavascriptImport, this.analyzerDoc);
} else if (feature.kinds.has('export')) {
this.handleJsExport(feature as analyzer.Export, this.analyzerDoc);
}
}
if ((feature as MaybePrivate).privacy === 'private') {
continue;
}
/**
* Add the given Element to the given TypeScript declarations document.
*/
private handleElement(feature: analyzer.Element) {
// 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;
}
if (feature.kinds.has('element')) {
handleElement(feature as analyzer.Element, root);
} else if (feature.kinds.has('behavior')) {
handleBehavior(feature as analyzer.PolymerBehavior, root);
} else if (feature.kinds.has('element-mixin')) {
handleMixin(feature as analyzer.ElementMixin, root, analysis);
} else if (feature.kinds.has('class')) {
handleClass(feature as analyzer.Class, root);
} else if (feature.kinds.has('function')) {
handleFunction(feature as AnalyzerFunction, root);
} else if (feature.kinds.has('namespace')) {
handleNamespace(feature as analyzer.Namespace, root);
} else if (feature.kinds.has('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 === doc.url) {
handleImport(feature as analyzer.Import, root, rootDir);
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,
],
}));
}
// 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 Element to the given TypeScript declarations document.
*/
function handleElement(feature: analyzer.Element, root: ts.Document) {
// 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;
/**
* Add the given Polymer Behavior to the given TypeScript declarations
* document.
*/
private handleBehavior(feature: analyzer.PolymerBehavior) {
if (!feature.className) {
this.warn(feature, `Could not find a name for behavior.`);
return;
}
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.
const [namespacePath, className] = splitReference(feature.className);
const ns = findOrCreateNamespace(this.root, namespacePath);
if (feature.className) {
constructable = true;
let namespacePath;
[namespacePath, shortName] = splitReference(feature.className);
fullName = feature.className;
parent = findOrCreateNamespace(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()),
}));
} 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 = root;
} else {
console.error(
`Could not find element name defined in ${feature.sourceRange}`);
return;
// 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'),
}));
}
const behaviors = isPolymerElement(feature) ?
feature.behaviorAssignments.map((behavior) => behavior.identifier) :
[];
/**
* Add the given Mixin to the given TypeScript declarations document.
*/
private handleMixin(feature: analyzer.ElementMixin) {
const [namespacePath, mixinName] = splitReference(feature.name);
const parentNamespace = findOrCreateNamespace(this.root, namespacePath);
const transitiveMixins = [...this.transitiveMixins(feature)];
const constructorName = mixinName + 'Constructor';
if (constructable) {
parent.members.push(new ts.Class({
name: shortName,
description: feature.description || feature.summary,
extends: (feature.extends) ||
(isPolymerElement(feature) ? 'Polymer.Element' : 'HTMLElement'),
mixins: feature.mixins.map((mixin) => mixin.identifier),
properties: handleProperties(feature.properties.values()),
// 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: [
...handleMethods(feature.staticMethods.values(), {isStatic: true}),
...handleMethods(feature.methods.values()),
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()),
],
constructorMethod: handleConstructorMethod(feature.constructorMethod)
}));
if (behaviors.length > 0) {
// We need to augment our class declaration with some behaviors. Behaviors
// are interfaces, so our class can't directly extend them, like we can do
// with mixin functions. However, the class declaration implicitly creates
// a corresponding interface with the same name, and we can augment that
// with the behavior interfaces using declaration merging.
parent.members.push(new ts.Interface({
name: shortName,
extends: behaviors,
}));
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}]}));
}
} else {
// TODO How do we handle mixins when we are emitting an interface? We don't
// currently define interfaces for mixins, so we can't just add them to
// extends.
const i = new ts.Interface({
name: shortName,
// 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.
*/
private transitiveMixins(
parentMixin: analyzer.ElementMixin,
result?: Set<analyzer.ElementMixin>): Set<analyzer.ElementMixin> {
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.
*/
private handleClass(feature: analyzer.Class) {
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.
*/
private handleFunction(feature: AnalyzerFunction) {
const [namespacePath, name] = splitReference(feature.name);
const f = new ts.Function({
name,
description: feature.description || feature.summary,
properties: 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: handleMethods(feature.methods.values()),
templateTypes: feature.templateTypes,
returns: closureTypeToTypeScript(
feature.return && feature.return.type, feature.templateTypes),
returnsDescription: feature.return && feature.return.desc
});
if (isPolymerElement(feature)) {
i.extends.push('Polymer.Element');
i.extends.push(...behaviors);
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(closureParamToTypeScript(
param.name, param.type, feature.templateTypes));
}
parent.members.push(i);
findOrCreateNamespace(this.root, namespacePath).members.push(f);
}
// 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(root, 'HTMLElementTagNameMap');
elementMap.properties.push(new ts.Property({
name: feature.tagName,
type: new ts.NameType(fullName),
}));
/**
* Convert the given Analyzer properties to their TypeScript declaration
* equivalent.
*/
private handleProperties(analyzerProperties: Iterable<analyzer.Property>):
ts.Property[] {
const tsProperties = <ts.Property[]>[];
for (const property of analyzerProperties) {
if (property.inheritedFrom || property.privacy === 'private') {
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: closureTypeToTypeScript(property.type),
readOnly: property.readOnly,
});
p.description = property.description || '';
tsProperties.push(p);
}
return tsProperties;
}
}
/**
* Add the given Polymer Behavior to the given TypeScript declarations
* document.
*/
function handleBehavior(feature: analyzer.PolymerBehavior, root: ts.Document) {
if (!feature.className) {
console.error(
`Could not find a name for behavior defined in ${feature.sourceRange}`);
return;
/**
* Convert the given Analyzer methods to their TypeScript declaration
* equivalent.
*/
private handleMethods(analyzerMethods: Iterable<analyzer.Method>, opts?: {
isStatic?: boolean
}): ts.Method[] {
const tsMethods = <ts.Method[]>[];
for (const method of analyzerMethods) {
if (method.inheritedFrom || method.privacy === 'private') {
continue;
}
tsMethods.push(this.handleMethod(method, opts));
}
return tsMethods;
}
const [namespacePath, className] = splitReference(feature.className);
const ns = findOrCreateNamespace(root, namespacePath);
/**
* Convert the given Analyzer method to the equivalent TypeScript declaration
*/
private handleMethod(method: analyzer.Method, opts?: {isStatic?: boolean}):
ts.Method {
const m = new ts.Method({
name: method.name,
returns: 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 || '';
// 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: handleProperties(feature.properties.values()),
methods: handleMethods(feature.methods.values()),
}));
let requiredAhead = false;
for (const param of reverseIter(method.params || [])) {
const tsParam = closureParamToTypeScript(param.name, param.type);
tsParam.description = param.description || '';
// 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'),
}));
}
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;
}
/**
* Add the given Mixin to the given TypeScript declarations document.
*/
function handleMixin(
feature: analyzer.ElementMixin,
root: ts.Document,
analysis: analyzer.Analysis) {
const [namespacePath, mixinName] = splitReference(feature.name);
const parentNamespace = findOrCreateNamespace(root, namespacePath);
// 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);
}
// 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(mixinName + 'Constructor'),
...Array.from(transitiveMixins(feature, analysis))
.map((mixin) => new ts.NameType(mixin + 'Constructor'))
]),
}));
m.params.unshift(tsParam);
}
return m;
}
// 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: mixinName + 'Constructor',
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),
}),
...handleMethods(feature.staticMethods.values()),
],
}));
private documentationHasSuppressTypeCheck(annotation: jsdoc.Annotation|
undefined): boolean {
if (!annotation) {
return false;
}
// The interface for instances of this mixin. Has the same name as the
// function.
parentNamespace.members.push(
new ts.Interface({
name: mixinName,
properties: handleProperties(feature.properties.values()),
methods: handleMethods(feature.methods.values()),
}),
);
};
const annotationValue = annotation.tags.find((e) => e.title === 'suppress');
return annotationValue && annotationValue.description === '{checkTypes}' ||
false;
}
/**
* 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.
*/
function transitiveMixins(
parentMixin: analyzer.ElementMixin,
analysis: analyzer.Analysis,
result?: Set<string>): Set<string> {
if (result === undefined) {
result = new Set();
}
for (const childRef of parentMixin.mixins) {
result.add(childRef.identifier);
const childMixinSet =
analysis.getFeatures({id: childRef.identifier, kind: 'element-mixin'});
if (childMixinSet.size !== 1) {
console.error(
`Found ${childMixinSet.size} features for mixin ` +
`${childRef.identifier}, expected 1.`);
continue;
private handleConstructorMethod(method?: analyzer.Method): ts.Method
|undefined {
if (!method) {
return;
}
const childMixin = childMixinSet.values().next().value;
transitiveMixins(childMixin, analysis, result);
const m = this.handleMethod(method);
m.returns = undefined;
return m;
}
return result;
}
/**
* Add the given Class to the given TypeScript declarations document.
*/
function handleClass(feature: analyzer.Class, root: ts.Document) {
if (!feature.className) {
console.error(
`Could not find a name for class defined in ${feature.sourceRange}`);
return;
/**
* Add the given namespace to the given TypeScript declarations document.
*/
private handleNamespace(feature: analyzer.Namespace) {
const ns = findOrCreateNamespace(this.root, feature.name.split('.'));
if (ns.kind === 'namespace') {
ns.description = feature.description || feature.summary || '';
}
}
const [namespacePath, name] = splitReference(feature.className);
const m = new ts.Class({name});
m.description = feature.description;
m.properties = handleProperties(feature.properties.values());
m.methods = [
...handleMethods(feature.staticMethods.values(), {isStatic: true}),
...handleMethods(feature.methods.values())
];
m.constructorMethod = handleConstructorMethod(feature.constructorMethod);
findOrCreateNamespace(root, namespacePath).members.push(m);
}
/**
* Add a JavaScript import to the TypeScript declarations.
*/
private handleJsImport(
feature: analyzer.JavascriptImport,
doc: analyzer.Document) {
const node = feature.astNode.node;
/**
* Add the given Function to the given TypeScript declarations document.
*/
function handleFunction(feature: AnalyzerFunction, root: ts.Document) {
const [namespacePath, name] = splitReference(feature.name);
const isResolvable = (identifier: string) =>
resolveImportExportFeature(feature, identifier, doc) !== undefined;
const f = new ts.Function({
name,
description: feature.description || feature.summary,
templateTypes: feature.templateTypes,
returns: closureTypeToTypeScript(
feature.return && feature.return.type, feature.templateTypes),
returnsDescription: feature.return && feature.return.desc
});
if (babel.isImportDeclaration(node)) {
const identifiers: ts.ImportSpecifier[] = [];
for (const specifier of node.specifiers) {
if (babel.isImportSpecifier(specifier)) {
// E.g. import {Foo, Bar as Baz} from './foo.js'
if (isResolvable(specifier.imported.name)) {
identifiers.push({
identifier: specifier.imported.name,
alias: specifier.local.name,
});
}
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(closureParamToTypeScript(
param.name, param.type, feature.templateTypes));
}
} else if (babel.isImportDefaultSpecifier(specifier)) {
// E.g. import foo from './foo.js'
if (isResolvable('default')) {
identifiers.push({
identifier: 'default',
alias: specifier.local.name,
});
}
findOrCreateNamespace(root, namespacePath).members.push(f);
}
} else if (babel.isImportNamespaceSpecifier(specifier)) {
// E.g. import * as foo from './foo.js'
identifiers.push({
identifier: ts.AllIdentifiers,
alias: specifier.local.name,
});
}
}
/**
* Convert the given Analyzer properties to their TypeScript declaration
* equivalent.
*/
function handleProperties(analyzerProperties: Iterable<analyzer.Property>):
ts.Property[] {
const tsProperties = <ts.Property[]>[];
for (const property of analyzerProperties) {
if (property.inheritedFrom || property.privacy === 'private') {
continue;
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.`);
}
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: 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.
*/
function handleMethods(
analyzerMethods: Iterable<analyzer.Method>,
opts?: {isStatic?: boolean}): ts.Method[] {
const tsMethods = <ts.Method[]>[];
for (const method of analyzerMethods) {
if (method.inheritedFrom || method.privacy === 'private') {
continue;
}
/**
* Add a JavaScript export to the TypeScript declarations.
*/
private handleJsExport(feature: analyzer.Export, doc: analyzer.Document) {
const node = feature.astNode.node;
tsMethods.push(handleMethod(method, opts));
}
return tsMethods;
}
const isResolvable = (identifier: string) =>
resolveImportExportFeature(feature, identifier, doc) !== undefined;
/**
* Convert the given Analyzer method to the equivalent TypeScript declaration
*/
function handleMethod(
method: analyzer.Method, opts?: {isStatic?: boolean}): ts.Method {
const m = new ts.Method({
name: method.name,
returns: closureTypeToTypeScript(method.return && method.return.type),
returnsDescription: method.return && method.return.desc,
isStatic: opts && opts.isStatic,
ignoreTypeCheck: documentationHasSuppressTypeCheck(method.jsdoc)
});
m.description = method.description || '';
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,
}));
let requiredAhead = false;
for (const param of reverseIter(method.params || [])) {
const tsParam = closureParamToTypeScript(param.name, param.type);
tsParam.description = param.description || '';
} else if (babel.isExportNamedDeclaration(node)) {
const identifiers = [];
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;
if (node.declaration) {
// E.g. export class Foo {}
for (const identifier of feature.identifiers) {
if (isResolvable(identifier)) {
identifiers.push({identifier});
}
}
} else {
tsParam.type = new ts.UnionType([tsParam.type, ts.undefinedType]);
// E.g. export {Foo, Bar as Baz}
for (const specifier of node.specifiers) {
if (isResolvable(specifier.exported.name)) {
identifiers.push({
identifier: specifier.local.name,
alias: specifier.exported.name,
});
}
}
}
} 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);
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.`);
}
m.params.unshift(tsParam);
}
return m;
}
function documentationHasSuppressTypeCheck(annotation: jsdoc.Annotation|
undefined): boolean {
if (!annotation) {
return false;
}
/**
* 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.
*/
private handleHtmlImport(feature: analyzer.Import) {
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)\//, '../');
const annotationValue = annotation.tags.find(e => e.title === 'suppress');
return annotationValue && annotationValue.description === '{checkTypes}' ||
false;
}
// 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/');
function handleConstructorMethod(method?: analyzer.Method): ts.Method|
undefined {
if (!method) {
return;
this.root.referencePaths.add(path.relative(
path.dirname(this.root.path), makeDeclarationsFilename(sourcePath)));
}
const m = handleMethod(method);
m.returns = undefined;
return m;
}

@@ -672,48 +986,17 @@

/**
* Add the given namespace to the given TypeScript declarations document.
* Find a document's global namespace declaration, or create one if it doesn't
* exist.
*/
function handleNamespace(feature: analyzer.Namespace, tsDoc: ts.Document) {
const ns = findOrCreateNamespace(tsDoc, feature.name.split('.'));
if (ns.kind === 'namespace') {
ns.description = feature.description || feature.summary || '';
function findOrCreateGlobalNamespace(doc: ts.Document): ts.GlobalNamespace {
for (const member of doc.members) {
if (member.kind === 'globalNamespace') {
return member;
}
}
const globalNamespace = new ts.GlobalNamespace();
doc.members.push(globalNamespace);
return globalNamespace;
}
/**
* 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.
*/
function handleImport(
feature: analyzer.Import, tsDoc: ts.Document, rootDir: string) {
let sourcePath = analyzerUrlToRelativePath(feature.url, rootDir);
if (sourcePath === undefined) {
console.warn(`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/');
tsDoc.referencePaths.add(path.relative(
path.dirname(tsDoc.path), makeDeclarationsFilename(sourcePath)));
}
/**
* Traverse the given node to find the namespace AST node with the given path.

@@ -723,3 +1006,4 @@ * If it could not be found, add one and return it.

function findOrCreateNamespace(
root: ts.Document|ts.Namespace, path: string[]): ts.Document|ts.Namespace {
root: ts.Document|ts.Namespace|ts.GlobalNamespace,
path: string[]): ts.Document|ts.Namespace|ts.GlobalNamespace {
if (!path.length) {

@@ -747,3 +1031,4 @@ return root;

function findOrCreateInterface(
root: ts.Document|ts.Namespace, reference: string): ts.Interface {
root: ts.Document|ts.Namespace|ts.GlobalNamespace,
reference: string): ts.Interface {
const [namespacePath, name] = splitReference(reference);

@@ -750,0 +1035,0 @@ const namespace_ = findOrCreateNamespace(root, namespacePath);

@@ -16,5 +16,31 @@ /**

// An AST node that can appear directly in a document or namespace.
export type Declaration = Namespace|Class|Interface|Function|ConstValue;
/** An AST node that can appear directly in a document or namespace. */
export type Declaration =
GlobalNamespace|Namespace|Class|Interface|Function|ConstValue|Import|Export;
export class GlobalNamespace {
readonly kind = 'globalNamespace';
members: Declaration[];
constructor(members?: Declaration[]) {
this.members = members || [];
}
* traverse(): Iterable<Node> {
for (const m of this.members) {
yield* m.traverse();
}
yield this;
}
serialize(_depth: number = 0): string {
let out = `declare global {\n`;
for (const member of this.members) {
out += '\n' + member.serialize(1);
}
out += `}\n`;
return out;
}
}
export class Namespace {

@@ -44,8 +70,8 @@ readonly kind = 'namespace';

serialize(depth: number = 0): string {
let out = ''
let out = '';
if (this.description) {
out += formatComment(this.description, depth);
}
const i = indent(depth)
out += i
const i = indent(depth);
out += i;
if (depth === 0) {

@@ -123,3 +149,3 @@ out += 'declare ';

out += `\n${i2}${this.extends || 'Object'}`;
out += ')'.repeat(this.mixins.length)
out += ')'.repeat(this.mixins.length);

@@ -238,3 +264,3 @@ } else if (this.extends) {

serialize(depth: number = 0): string {
let out = ''
let out = '';
const i = indent(depth);

@@ -282,7 +308,7 @@

out += this.params.map((p) => p.serialize()).join(', ');
out += `)`
out += `)`;
if (this.returns) {
out += `: ${this.returns.serialize()}`;
}
out += `;\n`
out += `;\n`;
return out;

@@ -378,4 +404,119 @@ }

serialize(depth: number = 0): string {
return `${indent(depth)}const ${this.name}: ${this.type.serialize()};\n`;
return indent(depth) + (depth === 0 ? 'declare ' : '') +
`const ${this.name}: ${this.type.serialize()};\n`;
}
}
/**
* The "*" token in an import or export.
*/
export const AllIdentifiers = Symbol('*');
export type AllIdentifiers = typeof AllIdentifiers;
/**
* An identifier that is imported, possibly with a different name.
*/
export interface ImportSpecifier {
identifier: string|AllIdentifiers;
alias?: string;
}
/**
* A JavaScript module import.
*/
export class Import {
readonly kind = 'import';
identifiers: ImportSpecifier[];
fromModuleSpecifier: string;
constructor(data: {
identifiers: ImportSpecifier[]; fromModuleSpecifier: string
}) {
this.identifiers = data.identifiers;
this.fromModuleSpecifier = data.fromModuleSpecifier;
}
* traverse(): Iterable<Node> {
yield this;
}
serialize(_depth: number = 0): string {
if (this.identifiers.some((i) => i.identifier === AllIdentifiers)) {
// Namespace imports have a different form. You can also have a default
// import, but no named imports.
const parts = [];
for (const identifier of this.identifiers) {
if (identifier.identifier === 'default') {
parts.push(identifier.alias);
} else if (identifier.identifier === AllIdentifiers) {
parts.push(`* as ${identifier.alias}`);
}
}
return `import ${parts.join(', ')} ` +
`from '${this.fromModuleSpecifier}';\n`;
}
else {
const parts = [];
for (const {identifier, alias} of this.identifiers) {
if (identifier === AllIdentifiers) {
// Can't happen, see above.
continue;
}
parts.push(
identifier +
(alias !== undefined && alias !== identifier ? ` as ${alias}` :
''));
}
return `import {${parts.join(', ')}} ` +
`from '${this.fromModuleSpecifier}';\n`;
}
}
}
/**
* An identifier that is imported, possibly with a different name.
*/
export interface ExportSpecifier {
identifier: string;
alias?: string;
}
/**
* A JavaScript module export.
*/
export class Export {
readonly kind = 'export';
identifiers: ExportSpecifier[]|AllIdentifiers;
fromModuleSpecifier: string;
constructor(data: {
identifiers: ExportSpecifier[]|AllIdentifiers,
fromModuleSpecifier?: string
}) {
this.identifiers = data.identifiers;
this.fromModuleSpecifier = data.fromModuleSpecifier || '';
}
* traverse(): Iterable<Node> {
yield this;
}
serialize(_depth: number = 0): string {
let out = 'export ';
if (this.identifiers === AllIdentifiers) {
out += '*';
} else {
const specifiers = this.identifiers.map(({identifier, alias}) => {
return identifier +
(alias !== undefined && alias !== identifier ? ` as ${alias}` : '');
});
out += `{${specifiers.join(', ')}}`;
}
if (this.fromModuleSpecifier !== '') {
out += ` from '${this.fromModuleSpecifier}'`;
}
out += ';\n';
return out;
}
}

@@ -14,3 +14,3 @@ /**

import {formatComment} from './formatting';
import {Node} from './index';
import {Export, Node} from './index';

@@ -23,2 +23,3 @@ export class Document {

header: string;
isEsModule: boolean;

@@ -29,3 +30,4 @@ constructor(data: {

referencePaths?: Iterable<string>,
header?: string
header?: string,
isEsModule?: boolean,
}) {

@@ -36,2 +38,3 @@ this.path = data.path;

this.header = data.header || '';
this.isEsModule = data.isEsModule || false;
}

@@ -73,4 +76,10 @@

out += this.members.map((m) => m.serialize()).join('\n');
// If these are typings for an ES module, we want to be sure that TypeScript
// will treat them as one too, which requires at least one import or export.
if (this.isEsModule === true &&
!this.members.some((m) => m.kind === 'import' || m.kind === 'export')) {
out += '\n' + (new Export({identifiers: []})).serialize();
}
return out;
}
}

@@ -31,3 +31,3 @@ /**

// embedded a JavaScript style block comment.
comment = comment.replace(/\*\//g, '*\\/')
comment = comment.replace(/\*\//g, '*\\/');

@@ -37,3 +37,3 @@ // Indent the comment one space so that it doesn't touch the `*` we add next,

// one space, then they would have an unneccessary space after the `*`.
comment = comment.replace(/^(.)/gm, ' $1')
comment = comment.replace(/^(.)/gm, ' $1');

@@ -40,0 +40,0 @@ // Indent to the given level and add the `*` character.

@@ -25,3 +25,3 @@ /**

this.name = name;
};
}

@@ -318,3 +318,4 @@ * traverse(): Iterable<Node> {

serialize(): string {
return `{[key: ${this.keyType.serialize()}]: ${this.valueType.serialize()}}`
return `{[key: ${this.keyType.serialize()}]: ${
this.valueType.serialize()}}`;
}

@@ -321,0 +322,0 @@ }

{
"extends": "../../tsconfig-base.json",
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"lib": [
"es2017"
],
"declaration": true,
"sourceMap": true,
"pretty": true,
"outDir": "./lib"
"outDir": "./lib",
"baseUrl": ".",
"paths": {
"parse5": [
"node_modules/@types/parse5",
"*"
]
}
},

@@ -21,0 +13,0 @@ "include": [

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc