UNPKG

19.4 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
5 * This code may only be used under the BSD style license found at
6 * http://polymer.github.io/LICENSE.txt The complete set of authors may be found
7 * at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
8 * be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
9 * Google as part of the polymer project is also subject to an additional IP
10 * rights grant found at http://polymer.github.io/PATENTS.txt
11 */
12var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
13 return new (P || (P = Promise))(function (resolve, reject) {
14 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
15 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
16 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
17 step((generator = generator.apply(thisArg, _arguments || [])).next());
18 });
19};
20Object.defineProperty(exports, "__esModule", { value: true });
21const minimatch = require("minimatch");
22const path = require("path");
23const analyzer = require("polymer-analyzer");
24const closure_types_1 = require("./closure-types");
25const ts = require("./ts-ast");
26/**
27 * Analyze all files in the given directory using Polymer Analyzer, and return
28 * TypeScript declaration document strings in a map keyed by relative path.
29 */
30function generateDeclarations(rootDir, config) {
31 return __awaiter(this, void 0, void 0, function* () {
32 const a = new analyzer.Analyzer({
33 urlLoader: new analyzer.FSUrlLoader(rootDir),
34 urlResolver: new analyzer.PackageUrlResolver(),
35 });
36 const analysis = yield a.analyzePackage();
37 const outFiles = new Map();
38 for (const tsDoc of analyzerToAst(analysis, config, rootDir)) {
39 outFiles.set(tsDoc.path, tsDoc.serialize());
40 }
41 return outFiles;
42 });
43}
44exports.generateDeclarations = generateDeclarations;
45/**
46 * Make TypeScript declaration documents from the given Polymer Analyzer
47 * result.
48 */
49function analyzerToAst(analysis, config, rootDir) {
50 const exclude = (config.exclude || ['test/**', 'demo/**'])
51 .map((p) => new minimatch.Minimatch(p));
52 const addReferences = config.addReferences || {};
53 const removeReferencesResolved = new Set((config.removeReferences || []).map((r) => path.resolve(rootDir, r)));
54 // Analyzer can produce multiple JS documents with the same URL (e.g. an
55 // HTML file with multiple inline scripts). We also might have multiple
56 // files with the same basename (e.g. `foo.html` with an inline script,
57 // and `foo.js`). We want to produce one declarations file for each
58 // basename, so we first group Analyzer documents by their declarations
59 // filename.
60 const declarationDocs = new Map();
61 for (const jsDoc of analysis.getFeatures({ kind: 'js-document' })) {
62 if (exclude.some((r) => r.match(jsDoc.url))) {
63 continue;
64 }
65 const filename = makeDeclarationsFilename(jsDoc.url);
66 let docs = declarationDocs.get(filename);
67 if (!docs) {
68 docs = [];
69 declarationDocs.set(filename, docs);
70 }
71 docs.push(jsDoc);
72 }
73 const tsDocs = [];
74 for (const [declarationsFilename, analyzerDocs] of declarationDocs) {
75 const tsDoc = new ts.Document({
76 path: declarationsFilename,
77 header: makeHeader(analyzerDocs.map((d) => d.url)),
78 });
79 for (const analyzerDoc of analyzerDocs) {
80 handleDocument(analyzerDoc, tsDoc);
81 }
82 for (const ref of tsDoc.referencePaths) {
83 const resolvedRef = path.resolve(rootDir, path.dirname(tsDoc.path), ref);
84 if (removeReferencesResolved.has(resolvedRef)) {
85 tsDoc.referencePaths.delete(ref);
86 }
87 }
88 for (const ref of addReferences[tsDoc.path] || []) {
89 tsDoc.referencePaths.add(path.relative(path.dirname(tsDoc.path), ref));
90 }
91 tsDoc.simplify();
92 // Include even documents with no members. They might be dependencies of
93 // other files via the HTML import graph, and it's simpler to have empty
94 // files than to try and prune the references (especially across packages).
95 tsDocs.push(tsDoc);
96 }
97 return tsDocs;
98}
99/**
100 * Create a TypeScript declarations filename for the given source document URL.
101 * Simply replaces the file extension with `d.ts`.
102 */
103function makeDeclarationsFilename(sourceUrl) {
104 const parsed = path.parse(sourceUrl);
105 return path.join(parsed.dir, parsed.name) + '.d.ts';
106}
107/**
108 * Generate the header comment to show at the top of a declarations document.
109 */
110function makeHeader(sourceUrls) {
111 return `DO NOT EDIT
112
113This file was automatically generated by
114 https://github.com/Polymer/gen-typescript-declarations
115
116To modify these typings, edit the source file(s):
117${sourceUrls.map((url) => ' ' + url).join('\n')}`;
118}
119/**
120 * Extend the given TypeScript declarations document with all of the relevant
121 * items in the given Polymer Analyzer document.
122 */
123function handleDocument(doc, root) {
124 for (const feature of doc.getFeatures()) {
125 if (feature.privacy === 'private') {
126 continue;
127 }
128 if (feature.kinds.has('element')) {
129 handleElement(feature, root);
130 }
131 else if (feature.kinds.has('behavior')) {
132 handleBehavior(feature, root);
133 }
134 else if (feature.kinds.has('element-mixin')) {
135 handleMixin(feature, root);
136 }
137 else if (feature.kinds.has('class')) {
138 handleClass(feature, root);
139 }
140 else if (feature.kinds.has('function')) {
141 handleFunction(feature, root);
142 }
143 else if (feature.kinds.has('namespace')) {
144 handleNamespace(feature, root);
145 }
146 else if (feature.kinds.has('import')) {
147 // Sometimes an Analyzer document includes an import feature that is
148 // inbound (things that depend on me) instead of outbound (things I
149 // depend on). For example, if an HTML file has a <script> tag for a JS
150 // file, then the JS file's Analyzer document will include that <script>
151 // tag as an import feature. We only care about outbound dependencies,
152 // hence this check.
153 if (feature.sourceRange && feature.sourceRange.file === doc.url) {
154 handleImport(feature, root);
155 }
156 }
157 }
158}
159/**
160 * Add the given Element to the given TypeScript declarations document.
161 */
162function handleElement(feature, root) {
163 // Whether this element has a constructor that is assigned and can be called.
164 // If it does we'll emit a class, otherwise an interface.
165 let constructable;
166 let fullName; // Fully qualified reference, e.g. `Polymer.DomModule`.
167 let shortName; // Just the last part of the name, e.g. `DomModule`.
168 let parent; // Where in the namespace tree does this live.
169 if (feature.className) {
170 constructable = true;
171 let namespacePath;
172 [namespacePath, shortName] = splitReference(feature.className);
173 fullName = feature.className;
174 parent = findOrCreateNamespace(root, namespacePath);
175 }
176 else if (feature.tagName) {
177 constructable = false;
178 shortName = kebabToCamel(feature.tagName);
179 fullName = shortName;
180 // We're going to pollute the global scope with an interface.
181 parent = root;
182 }
183 else {
184 console.error('Could not find a name.');
185 return;
186 }
187 if (constructable) {
188 // TODO How do we handle behaviors with classes?
189 const c = new ts.Class({
190 name: shortName,
191 description: feature.description || feature.summary,
192 extends: (feature.extends) ||
193 (isPolymerElement(feature) ? 'Polymer.Element' : 'HTMLElement'),
194 mixins: feature.mixins.map((mixin) => mixin.identifier),
195 properties: handleProperties(feature.properties.values()),
196 methods: handleMethods(feature.methods.values()),
197 });
198 parent.members.push(c);
199 }
200 else {
201 // TODO How do we handle mixins when we are emitting an interface? We don't
202 // currently define interfaces for mixins, so we can't just add them to
203 // extends.
204 const i = new ts.Interface({
205 name: shortName,
206 description: feature.description || feature.summary,
207 properties: handleProperties(feature.properties.values()),
208 methods: handleMethods(feature.methods.values()),
209 });
210 if (isPolymerElement(feature)) {
211 i.extends.push('Polymer.Element');
212 i.extends.push(...feature.behaviorAssignments.map((behavior) => behavior.name));
213 }
214 parent.members.push(i);
215 }
216 // The `HTMLElementTagNameMap` global interface maps custom element tag names
217 // to their definitions, so that TypeScript knows that e.g.
218 // `dom.createElement('my-foo')` returns a `MyFoo`. Augment the map with this
219 // custom element.
220 if (feature.tagName) {
221 const elementMap = findOrCreateInterface(root, 'HTMLElementTagNameMap');
222 elementMap.properties.push(new ts.Property({
223 name: feature.tagName,
224 type: new ts.NameType(fullName),
225 }));
226 }
227}
228/**
229 * Add the given Polymer Behavior to the given TypeScript declarations
230 * document.
231 */
232function handleBehavior(feature, root) {
233 if (!feature.className) {
234 console.error('Could not find a name for behavior.');
235 return;
236 }
237 const [namespacePath, className] = splitReference(feature.className);
238 const i = new ts.Interface({ name: className });
239 i.description = feature.description || feature.summary;
240 i.properties = handleProperties(feature.properties.values());
241 i.methods = handleMethods(feature.methods.values());
242 findOrCreateNamespace(root, namespacePath).members.push(i);
243}
244/**
245 * Add the given Mixin to the given TypeScript declarations document.
246 */
247function handleMixin(feature, root) {
248 const [namespacePath, name] = splitReference(feature.name);
249 const namespace_ = findOrCreateNamespace(root, namespacePath);
250 // We represent mixins in two parts: a mixin function that is called to
251 // augment a given class with this mixin, and an interface with the
252 // properties and methods that are added by this mixin. We can use the same
253 // name for both parts because one is in value space, and the other is in
254 // type space.
255 const function_ = new ts.Mixin({ name });
256 function_.description = feature.description;
257 function_.interfaces = [name, ...feature.mixins.map((m) => m.identifier)];
258 namespace_.members.push(function_);
259 const interface_ = new ts.Interface({ name });
260 interface_.properties = handleProperties(feature.properties.values());
261 interface_.methods = handleMethods(feature.methods.values());
262 namespace_.members.push(interface_);
263}
264/**
265 * Add the given Class to the given TypeScript declarations document.
266 */
267function handleClass(feature, root) {
268 if (!feature.className) {
269 console.error('Could not find a name for class.');
270 return;
271 }
272 const [namespacePath, name] = splitReference(feature.className);
273 const m = new ts.Class({ name });
274 m.description = feature.description;
275 m.properties = handleProperties(feature.properties.values());
276 m.methods = handleMethods(feature.methods.values());
277 findOrCreateNamespace(root, namespacePath).members.push(m);
278}
279/**
280 * Add the given Function to the given TypeScript declarations document.
281 */
282function handleFunction(feature, root) {
283 const [namespacePath, name] = splitReference(feature.name);
284 const f = new ts.Function({
285 name,
286 description: feature.description || feature.summary,
287 templateTypes: feature.templateTypes,
288 returns: closure_types_1.closureTypeToTypeScript(feature.return && feature.return.type, feature.templateTypes),
289 returnsDescription: feature.return && feature.return.desc
290 });
291 for (const param of feature.params || []) {
292 // TODO Handle parameter default values. Requires support from Analyzer
293 // which only handles this for class method parameters currently.
294 const { type, optional, rest } = closure_types_1.closureParamToTypeScript(param.type, feature.templateTypes);
295 f.params.push(new ts.Param({ name: param.name, type, optional, rest }));
296 }
297 findOrCreateNamespace(root, namespacePath).members.push(f);
298}
299/**
300 * Convert the given Analyzer properties to their TypeScript declaration
301 * equivalent.
302 */
303function handleProperties(analyzerProperties) {
304 const tsProperties = [];
305 for (const property of analyzerProperties) {
306 if (property.inheritedFrom || property.privacy === 'private') {
307 continue;
308 }
309 const p = new ts.Property({
310 name: property.name,
311 // TODO If this is a Polymer property with no default value, then the
312 // type should really be `<type>|undefined`.
313 type: closure_types_1.closureTypeToTypeScript(property.type),
314 });
315 p.description = property.description || '';
316 tsProperties.push(p);
317 }
318 return tsProperties;
319}
320/**
321 * Convert the given Analyzer methods to their TypeScript declaration
322 * equivalent.
323 */
324function handleMethods(analyzerMethods) {
325 const tsMethods = [];
326 for (const method of analyzerMethods) {
327 if (method.inheritedFrom || method.privacy === 'private') {
328 continue;
329 }
330 const m = new ts.Method({
331 name: method.name,
332 returns: closure_types_1.closureTypeToTypeScript(method.return && method.return.type),
333 returnsDescription: method.return && method.return.desc
334 });
335 m.description = method.description || '';
336 let requiredAhead = false;
337 for (const param of reverseIter(method.params || [])) {
338 let { type, optional, rest } = closure_types_1.closureParamToTypeScript(param.type);
339 if (param.defaultValue !== undefined) {
340 // Parameters with default values generally behave like optional
341 // parameters. However, unlike optional parameters, they may be
342 // followed by a required parameter, in which case the default value is
343 // set by explicitly passing undefined.
344 if (!requiredAhead) {
345 optional = true;
346 }
347 else {
348 type = new ts.UnionType([type, ts.undefinedType]);
349 }
350 }
351 else if (!optional) {
352 requiredAhead = true;
353 }
354 // Analyzer might know this is a rest parameter even if there was no
355 // JSDoc type annotation (or if it was wrong).
356 rest = rest || !!param.rest;
357 if (rest && type.kind !== 'array') {
358 // Closure rest parameter types are written without the Array syntax,
359 // but in TypeScript they must be explicitly arrays.
360 type = new ts.ArrayType(type);
361 }
362 m.params.unshift(new ts.Param({
363 name: param.name,
364 description: param.description,
365 type,
366 optional,
367 rest
368 }));
369 }
370 tsMethods.push(m);
371 }
372 return tsMethods;
373}
374/**
375 * Iterate over an array backwards.
376 */
377function* reverseIter(arr) {
378 for (let i = arr.length - 1; i >= 0; i--) {
379 yield arr[i];
380 }
381}
382/**
383 * Add the given namespace to the given TypeScript declarations document.
384 */
385function handleNamespace(feature, tsDoc) {
386 const ns = findOrCreateNamespace(tsDoc, feature.name.split('.'));
387 if (ns.kind === 'namespace') {
388 ns.description = feature.description || feature.summary || '';
389 }
390}
391/**
392 * Add an HTML import to a TypeScript declarations file. For a given HTML
393 * import, we assume there is a corresponding declarations file that was
394 * generated by this same process.
395 *
396 * TODO If the import was to an external package, we currently don't know if
397 * the typings file actually exists. Also, if we end up placing type
398 * declarations in a types/ subdirectory, we will need to update these paths to
399 * match.
400 */
401function handleImport(feature, tsDoc) {
402 if (!feature.url) {
403 return;
404 }
405 // When we analyze a package's Git repo, our dependencies are installed to
406 // "<repo>/bower_components". However, when this package is itself installed
407 // as a dependency, our own dependencies will instead be siblings, one
408 // directory up the tree.
409 //
410 // Analyzer (since 2.5.0) will set an import feature's URL to the resolved
411 // dependency path as discovered on disk. An import for "../foo/foo.html"
412 // will be resolved to "bower_components/foo/foo.html". Transform the URL
413 // back to the style that will work when this package is installed as a
414 // dependency.
415 const url = feature.url.replace(/^(bower_components|node_modules)\//, '../');
416 tsDoc.referencePaths.add(path.relative(path.dirname(tsDoc.path), makeDeclarationsFilename(url)));
417}
418/**
419 * Traverse the given node to find the namespace AST node with the given path.
420 * If it could not be found, add one and return it.
421 */
422function findOrCreateNamespace(root, path) {
423 if (!path.length) {
424 return root;
425 }
426 let first;
427 for (const member of root.members) {
428 if (member.kind === 'namespace' && member.name === path[0]) {
429 first = member;
430 break;
431 }
432 }
433 if (!first) {
434 first = new ts.Namespace({ name: path[0] });
435 root.members.push(first);
436 }
437 return findOrCreateNamespace(first, path.slice(1));
438}
439/**
440 * Traverse the given node to find the interface AST node with the given path.
441 * If it could not be found, add one and return it.
442 */
443function findOrCreateInterface(root, reference) {
444 const [namespacePath, name] = splitReference(reference);
445 const namespace_ = findOrCreateNamespace(root, namespacePath);
446 for (const member of namespace_.members) {
447 if (member.kind === 'interface' && member.name === name) {
448 return member;
449 }
450 }
451 const i = new ts.Interface({ name });
452 namespace_.members.push(i);
453 return i;
454}
455/**
456 * Type guard that checks if a Polymer Analyzer feature is a PolymerElement.
457 */
458function isPolymerElement(feature) {
459 return feature.kinds.has('polymer-element');
460}
461/**
462 * Convert kebab-case to CamelCase.
463 */
464function kebabToCamel(s) {
465 return s.replace(/(^|-)(.)/g, (_match, _p0, p1) => p1.toUpperCase());
466}
467/**
468 * Split a reference into an array of namespace path parts, and a name part
469 * (e.g. `"Foo.Bar.Baz"` => `[ ["Foo", "Bar"], "Baz" ]`).
470 */
471function splitReference(reference) {
472 const parts = reference.split('.');
473 const namespacePath = parts.slice(0, -1);
474 const name = parts[parts.length - 1];
475 return [namespacePath, name];
476}
477//# sourceMappingURL=gen-ts.js.map
\No newline at end of file