1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | import * as minimatch from 'minimatch';
|
13 | import * as path from 'path';
|
14 | import * as analyzer from 'polymer-analyzer';
|
15 | import {Function as AnalyzerFunction} from 'polymer-analyzer/lib/javascript/function';
|
16 |
|
17 | import {closureParamToTypeScript, closureTypeToTypeScript} from './closure-types';
|
18 | import * as ts from './ts-ast';
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | export interface Config {
|
24 | |
25 |
|
26 |
|
27 |
|
28 | exclude?: string[];
|
29 |
|
30 | |
31 |
|
32 |
|
33 |
|
34 | removeReferences?: string[];
|
35 |
|
36 | |
37 |
|
38 |
|
39 |
|
40 |
|
41 | addReferences?: {[filepath: string]: string[]};
|
42 | }
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | export async function generateDeclarations(
|
49 | rootDir: string, config: Config): Promise<Map<string, string>> {
|
50 | const a = new analyzer.Analyzer({
|
51 | urlLoader: new analyzer.FSUrlLoader(rootDir),
|
52 | urlResolver: new analyzer.PackageUrlResolver(),
|
53 | });
|
54 | const analysis = await a.analyzePackage();
|
55 | const outFiles = new Map<string, string>();
|
56 | for (const tsDoc of analyzerToAst(analysis, config, rootDir)) {
|
57 | outFiles.set(tsDoc.path, tsDoc.serialize())
|
58 | }
|
59 | return outFiles;
|
60 | }
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 | function analyzerToAst(
|
67 | analysis: analyzer.Analysis, config: Config, rootDir: string):
|
68 | ts.Document[] {
|
69 | const exclude = (config.exclude || ['test/**', 'demo/**'])
|
70 | .map((p) => new minimatch.Minimatch(p));
|
71 | const addReferences = config.addReferences || {};
|
72 | const removeReferencesResolved = new Set(
|
73 | (config.removeReferences || []).map((r) => path.resolve(rootDir, r)));
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 | const declarationDocs = new Map<string, analyzer.Document[]>();
|
82 | for (const jsDoc of analysis.getFeatures({kind: 'js-document'})) {
|
83 | if (exclude.some((r) => r.match(jsDoc.url))) {
|
84 | continue;
|
85 | }
|
86 | const filename = makeDeclarationsFilename(jsDoc.url);
|
87 | let docs = declarationDocs.get(filename);
|
88 | if (!docs) {
|
89 | docs = [];
|
90 | declarationDocs.set(filename, docs);
|
91 | }
|
92 | docs.push(jsDoc);
|
93 | }
|
94 |
|
95 | const tsDocs = [];
|
96 | for (const [declarationsFilename, analyzerDocs] of declarationDocs) {
|
97 | const tsDoc = new ts.Document({
|
98 | path: declarationsFilename,
|
99 | header: makeHeader(analyzerDocs.map((d) => d.url)),
|
100 | });
|
101 | for (const analyzerDoc of analyzerDocs) {
|
102 | handleDocument(analyzerDoc, tsDoc);
|
103 | }
|
104 | for (const ref of tsDoc.referencePaths) {
|
105 | const resolvedRef = path.resolve(rootDir, path.dirname(tsDoc.path), ref);
|
106 | if (removeReferencesResolved.has(resolvedRef)) {
|
107 | tsDoc.referencePaths.delete(ref);
|
108 | }
|
109 | }
|
110 | for (const ref of addReferences[tsDoc.path] || []) {
|
111 | tsDoc.referencePaths.add(path.relative(path.dirname(tsDoc.path), ref));
|
112 | }
|
113 | tsDoc.simplify();
|
114 |
|
115 |
|
116 |
|
117 | tsDocs.push(tsDoc);
|
118 | }
|
119 | return tsDocs;
|
120 | }
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 | function makeDeclarationsFilename(sourceUrl: string): string {
|
127 | const parsed = path.parse(sourceUrl);
|
128 | return path.join(parsed.dir, parsed.name) + '.d.ts';
|
129 | }
|
130 |
|
131 |
|
132 |
|
133 |
|
134 | function makeHeader(sourceUrls: string[]): string {
|
135 | return `DO NOT EDIT
|
136 |
|
137 | This file was automatically generated by
|
138 | https://github.com/Polymer/gen-typescript-declarations
|
139 |
|
140 | To modify these typings, edit the source file(s):
|
141 | ${sourceUrls.map((url) => ' ' + url).join('\n')}`;
|
142 | }
|
143 |
|
144 | interface MaybePrivate {
|
145 | privacy?: 'public'|'private'|'protected'
|
146 | }
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 | function handleDocument(doc: analyzer.Document, root: ts.Document) {
|
153 | for (const feature of doc.getFeatures()) {
|
154 | if ((feature as MaybePrivate).privacy === 'private') {
|
155 | continue;
|
156 | }
|
157 | if (feature.kinds.has('element')) {
|
158 | handleElement(feature as analyzer.Element, root);
|
159 | } else if (feature.kinds.has('behavior')) {
|
160 | handleBehavior(feature as analyzer.PolymerBehavior, root);
|
161 | } else if (feature.kinds.has('element-mixin')) {
|
162 | handleMixin(feature as analyzer.ElementMixin, root);
|
163 | } else if (feature.kinds.has('class')) {
|
164 | handleClass(feature as analyzer.Class, root);
|
165 | } else if (feature.kinds.has('function')) {
|
166 | handleFunction(feature as AnalyzerFunction, root);
|
167 | } else if (feature.kinds.has('namespace')) {
|
168 | handleNamespace(feature as analyzer.Namespace, root);
|
169 | } else if (feature.kinds.has('import')) {
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 | if (feature.sourceRange && feature.sourceRange.file === doc.url) {
|
177 | handleImport(feature as analyzer.Import, root);
|
178 | }
|
179 | }
|
180 | }
|
181 | }
|
182 |
|
183 |
|
184 |
|
185 |
|
186 | function handleElement(feature: analyzer.Element, root: ts.Document) {
|
187 |
|
188 |
|
189 | let constructable;
|
190 |
|
191 | let fullName;
|
192 | let shortName;
|
193 | let parent;
|
194 |
|
195 | if (feature.className) {
|
196 | constructable = true;
|
197 | let namespacePath;
|
198 | [namespacePath, shortName] = splitReference(feature.className);
|
199 | fullName = feature.className;
|
200 | parent = findOrCreateNamespace(root, namespacePath);
|
201 |
|
202 | } else if (feature.tagName) {
|
203 | constructable = false;
|
204 | shortName = kebabToCamel(feature.tagName);
|
205 | fullName = shortName;
|
206 |
|
207 | parent = root;
|
208 |
|
209 | } else {
|
210 | console.error('Could not find a name.');
|
211 | return;
|
212 | }
|
213 |
|
214 | if (constructable) {
|
215 |
|
216 | const c = new ts.Class({
|
217 | name: shortName,
|
218 | description: feature.description || feature.summary,
|
219 | extends: (feature.extends) ||
|
220 | (isPolymerElement(feature) ? 'Polymer.Element' : 'HTMLElement'),
|
221 | mixins: feature.mixins.map((mixin) => mixin.identifier),
|
222 | properties: handleProperties(feature.properties.values()),
|
223 | methods: handleMethods(feature.methods.values()),
|
224 | });
|
225 | parent.members.push(c);
|
226 |
|
227 | } else {
|
228 |
|
229 |
|
230 |
|
231 | const i = new ts.Interface({
|
232 | name: shortName,
|
233 | description: feature.description || feature.summary,
|
234 | properties: handleProperties(feature.properties.values()),
|
235 | methods: handleMethods(feature.methods.values()),
|
236 | });
|
237 |
|
238 | if (isPolymerElement(feature)) {
|
239 | i.extends.push('Polymer.Element');
|
240 | i.extends.push(...feature.behaviorAssignments.map(
|
241 | (behavior) => behavior.name));
|
242 | }
|
243 |
|
244 | parent.members.push(i);
|
245 | }
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 | if (feature.tagName) {
|
252 | const elementMap = findOrCreateInterface(root, 'HTMLElementTagNameMap');
|
253 | elementMap.properties.push(new ts.Property({
|
254 | name: feature.tagName,
|
255 | type: new ts.NameType(fullName),
|
256 | }));
|
257 | }
|
258 | }
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 | function handleBehavior(feature: analyzer.PolymerBehavior, root: ts.Document) {
|
265 | if (!feature.className) {
|
266 | console.error('Could not find a name for behavior.');
|
267 | return;
|
268 | }
|
269 | const [namespacePath, className] = splitReference(feature.className);
|
270 | const i = new ts.Interface({name: className});
|
271 | i.description = feature.description || feature.summary;
|
272 | i.properties = handleProperties(feature.properties.values());
|
273 | i.methods = handleMethods(feature.methods.values());
|
274 | findOrCreateNamespace(root, namespacePath).members.push(i);
|
275 | }
|
276 |
|
277 |
|
278 |
|
279 |
|
280 | function handleMixin(feature: analyzer.ElementMixin, root: ts.Document) {
|
281 | const [namespacePath, name] = splitReference(feature.name);
|
282 | const namespace_ = findOrCreateNamespace(root, namespacePath);
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 | const function_ = new ts.Mixin({name});
|
291 | function_.description = feature.description;
|
292 | function_.interfaces = [name, ...feature.mixins.map((m) => m.identifier)];
|
293 | namespace_.members.push(function_);
|
294 |
|
295 | const interface_ = new ts.Interface({name});
|
296 | interface_.properties = handleProperties(feature.properties.values());
|
297 | interface_.methods = handleMethods(feature.methods.values());
|
298 | namespace_.members.push(interface_);
|
299 | }
|
300 |
|
301 |
|
302 |
|
303 |
|
304 | function handleClass(feature: analyzer.Class, root: ts.Document) {
|
305 | if (!feature.className) {
|
306 | console.error('Could not find a name for class.');
|
307 | return;
|
308 | }
|
309 | const [namespacePath, name] = splitReference(feature.className);
|
310 | const m = new ts.Class({name});
|
311 | m.description = feature.description;
|
312 | m.properties = handleProperties(feature.properties.values());
|
313 | m.methods = handleMethods(feature.methods.values());
|
314 | findOrCreateNamespace(root, namespacePath).members.push(m);
|
315 | }
|
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 | function handleFunction(feature: AnalyzerFunction, root: ts.Document) {
|
322 | const [namespacePath, name] = splitReference(feature.name);
|
323 |
|
324 | const f = new ts.Function({
|
325 | name,
|
326 | description: feature.description || feature.summary,
|
327 | templateTypes: feature.templateTypes,
|
328 | returns: closureTypeToTypeScript(
|
329 | feature.return && feature.return.type, feature.templateTypes),
|
330 | returnsDescription: feature.return && feature.return.desc
|
331 | });
|
332 |
|
333 | for (const param of feature.params || []) {
|
334 |
|
335 |
|
336 | const {type, optional, rest} =
|
337 | closureParamToTypeScript(param.type, feature.templateTypes);
|
338 | f.params.push(new ts.Param({name: param.name, type, optional, rest}));
|
339 | }
|
340 |
|
341 | findOrCreateNamespace(root, namespacePath).members.push(f);
|
342 | }
|
343 |
|
344 |
|
345 |
|
346 |
|
347 |
|
348 | function handleProperties(analyzerProperties: Iterable<analyzer.Property>):
|
349 | ts.Property[] {
|
350 | const tsProperties = <ts.Property[]>[];
|
351 | for (const property of analyzerProperties) {
|
352 | if (property.inheritedFrom || property.privacy === 'private') {
|
353 | continue;
|
354 | }
|
355 | const p = new ts.Property({
|
356 | name: property.name,
|
357 |
|
358 |
|
359 | type: closureTypeToTypeScript(property.type),
|
360 | });
|
361 | p.description = property.description || '';
|
362 | tsProperties.push(p);
|
363 | }
|
364 | return tsProperties;
|
365 | }
|
366 |
|
367 |
|
368 |
|
369 |
|
370 |
|
371 |
|
372 | function handleMethods(analyzerMethods: Iterable<analyzer.Method>):
|
373 | ts.Method[] {
|
374 | const tsMethods = <ts.Method[]>[];
|
375 | for (const method of analyzerMethods) {
|
376 | if (method.inheritedFrom || method.privacy === 'private') {
|
377 | continue;
|
378 | }
|
379 | const m = new ts.Method({
|
380 | name: method.name,
|
381 | returns: closureTypeToTypeScript(method.return && method.return.type),
|
382 | returnsDescription: method.return && method.return.desc
|
383 | });
|
384 | m.description = method.description || '';
|
385 |
|
386 | let requiredAhead = false;
|
387 | for (const param of reverseIter(method.params || [])) {
|
388 | let {type, optional, rest} = closureParamToTypeScript(param.type);
|
389 |
|
390 | if (param.defaultValue !== undefined) {
|
391 |
|
392 |
|
393 |
|
394 |
|
395 | if (!requiredAhead) {
|
396 | optional = true;
|
397 | } else {
|
398 | type = new ts.UnionType([type, ts.undefinedType]);
|
399 | }
|
400 | } else if (!optional) {
|
401 | requiredAhead = true;
|
402 | }
|
403 |
|
404 |
|
405 |
|
406 | rest = rest || !!param.rest;
|
407 | if (rest && type.kind !== 'array') {
|
408 |
|
409 |
|
410 | type = new ts.ArrayType(type);
|
411 | }
|
412 |
|
413 | m.params.unshift(new ts.Param({
|
414 | name: param.name,
|
415 | description: param.description,
|
416 | type,
|
417 | optional,
|
418 | rest
|
419 | }));
|
420 | }
|
421 |
|
422 | tsMethods.push(m);
|
423 | }
|
424 | return tsMethods;
|
425 | }
|
426 |
|
427 |
|
428 |
|
429 |
|
430 | function* reverseIter<T>(arr: T[]) {
|
431 | for (let i = arr.length - 1; i >= 0; i--) {
|
432 | yield arr[i];
|
433 | }
|
434 | }
|
435 |
|
436 |
|
437 |
|
438 |
|
439 | function handleNamespace(feature: analyzer.Namespace, tsDoc: ts.Document) {
|
440 | const ns = findOrCreateNamespace(tsDoc, feature.name.split('.'));
|
441 | if (ns.kind === 'namespace') {
|
442 | ns.description = feature.description || feature.summary || '';
|
443 | }
|
444 | }
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 | function handleImport(feature: analyzer.Import, tsDoc: ts.Document) {
|
457 | if (!feature.url) {
|
458 | return;
|
459 | }
|
460 |
|
461 |
|
462 |
|
463 |
|
464 |
|
465 |
|
466 |
|
467 |
|
468 |
|
469 |
|
470 | const url = feature.url.replace(/^(bower_components|node_modules)\//, '../');
|
471 | tsDoc.referencePaths.add(
|
472 | path.relative(path.dirname(tsDoc.path), makeDeclarationsFilename(url)));
|
473 | }
|
474 |
|
475 |
|
476 |
|
477 |
|
478 |
|
479 | function findOrCreateNamespace(
|
480 | root: ts.Document|ts.Namespace, path: string[]): ts.Document|ts.Namespace {
|
481 | if (!path.length) {
|
482 | return root;
|
483 | }
|
484 | let first: ts.Namespace|undefined;
|
485 | for (const member of root.members) {
|
486 | if (member.kind === 'namespace' && member.name === path[0]) {
|
487 | first = member;
|
488 | break;
|
489 | }
|
490 | }
|
491 | if (!first) {
|
492 | first = new ts.Namespace({name: path[0]});
|
493 | root.members.push(first);
|
494 | }
|
495 | return findOrCreateNamespace(first, path.slice(1));
|
496 | }
|
497 |
|
498 |
|
499 |
|
500 |
|
501 |
|
502 | function findOrCreateInterface(
|
503 | root: ts.Document|ts.Namespace, reference: string): ts.Interface {
|
504 | const [namespacePath, name] = splitReference(reference);
|
505 | const namespace_ = findOrCreateNamespace(root, namespacePath);
|
506 | for (const member of namespace_.members) {
|
507 | if (member.kind === 'interface' && member.name === name) {
|
508 | return member;
|
509 | }
|
510 | }
|
511 | const i = new ts.Interface({name});
|
512 | namespace_.members.push(i);
|
513 | return i;
|
514 | }
|
515 |
|
516 |
|
517 |
|
518 |
|
519 | function isPolymerElement(feature: analyzer.Feature):
|
520 | feature is analyzer.PolymerElement {
|
521 | return feature.kinds.has('polymer-element');
|
522 | }
|
523 |
|
524 |
|
525 |
|
526 |
|
527 | function kebabToCamel(s: string): string {
|
528 | return s.replace(/(^|-)(.)/g, (_match, _p0, p1) => p1.toUpperCase());
|
529 | }
|
530 |
|
531 |
|
532 |
|
533 |
|
534 |
|
535 | function splitReference(reference: string): [string[], string] {
|
536 | const parts = reference.split('.');
|
537 | const namespacePath = parts.slice(0, -1);
|
538 | const name = parts[parts.length - 1];
|
539 | return [namespacePath, name];
|
540 | }
|