1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | import * as babel from '@babel/types';
|
13 | import * as jsdoc from 'doctrine';
|
14 | import * as fsExtra from 'fs-extra';
|
15 | import * as minimatch from 'minimatch';
|
16 | import * as path from 'path';
|
17 | import * as analyzer from 'polymer-analyzer';
|
18 | import {Function as AnalyzerFunction} from 'polymer-analyzer/lib/javascript/function';
|
19 | import Uri from 'vscode-uri';
|
20 |
|
21 | import {closureParamToTypeScript, closureTypeToTypeScript} from './closure-types';
|
22 | import {isEsModuleDocument, resolveImportExportFeature} from './es-modules';
|
23 | import * as ts from './ts-ast';
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | export interface Config {
|
29 | |
30 |
|
31 |
|
32 |
|
33 |
|
34 | excludeFiles?: string[];
|
35 |
|
36 | |
37 |
|
38 |
|
39 |
|
40 | exclude?: string[];
|
41 |
|
42 | |
43 |
|
44 |
|
45 |
|
46 | excludeIdentifiers?: string[];
|
47 |
|
48 | |
49 |
|
50 |
|
51 |
|
52 | removeReferences?: string[];
|
53 |
|
54 | |
55 |
|
56 |
|
57 |
|
58 |
|
59 | addReferences?: {[filepath: string]: string[]};
|
60 |
|
61 | |
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 | renameTypes?: {[name: string]: string};
|
68 |
|
69 | |
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 | autoImport?: {[modulePath: string]: string[]};
|
77 |
|
78 | |
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 | googModules?: boolean;
|
85 |
|
86 | |
87 |
|
88 |
|
89 |
|
90 | hideWarnings?: boolean;
|
91 | }
|
92 |
|
93 | const defaultExclude = [
|
94 | 'index.html',
|
95 | 'test/**',
|
96 | 'demo/**',
|
97 | ];
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 | export async function generateDeclarations(
|
104 | rootDir: string, config: Config): Promise<Map<string, string>> {
|
105 |
|
106 |
|
107 | const isBowerProject =
|
108 | await fsExtra.pathExists(path.join(rootDir, 'bower_components')) === true;
|
109 | const a = new analyzer.Analyzer({
|
110 | urlLoader: new analyzer.FsUrlLoader(rootDir),
|
111 | urlResolver: new analyzer.PackageUrlResolver({
|
112 | packageDir: rootDir,
|
113 | componentDir: isBowerProject ? 'bower_components/' : 'node_modules/',
|
114 | }),
|
115 | moduleResolution: isBowerProject ? undefined : 'node',
|
116 | });
|
117 | const analysis = await a.analyzePackage();
|
118 | const outFiles = new Map<string, string>();
|
119 | for (const tsDoc of await analyzerToAst(analysis, config, rootDir)) {
|
120 | outFiles.set(tsDoc.path, tsDoc.serialize());
|
121 | }
|
122 | return outFiles;
|
123 | }
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 | async function analyzerToAst(
|
130 | analysis: analyzer.Analysis, config: Config, rootDir: string):
|
131 | Promise<ts.Document[]> {
|
132 | const excludeFiles = (config.excludeFiles || config.exclude || defaultExclude)
|
133 | .map((p) => new minimatch.Minimatch(p));
|
134 | const addReferences = config.addReferences || {};
|
135 | const removeReferencesResolved = new Set(
|
136 | (config.removeReferences || []).map((r) => path.resolve(rootDir, r)));
|
137 | const renameTypes = new Map(Object.entries(config.renameTypes || {}));
|
138 |
|
139 |
|
140 | const autoImportMap = new Map<string, string>();
|
141 | if (config.autoImport !== undefined) {
|
142 | for (const importPath in config.autoImport) {
|
143 | for (const identifier of config.autoImport[importPath]) {
|
144 | autoImportMap.set(identifier, importPath);
|
145 | }
|
146 | }
|
147 | }
|
148 |
|
149 | const analyzerDocs = [
|
150 | ...analysis.getFeatures({kind: 'html-document'}),
|
151 | ...analysis.getFeatures({kind: 'js-document'}),
|
152 | ];
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 | const declarationDocs = new Map<string, analyzer.Document[]>();
|
159 | for (const jsDoc of analyzerDocs) {
|
160 |
|
161 |
|
162 |
|
163 |
|
164 | if (jsDoc.isInline) {
|
165 | continue;
|
166 | }
|
167 | const sourcePath = analyzerUrlToRelativePath(jsDoc.url, rootDir);
|
168 | if (sourcePath === undefined) {
|
169 | console.warn(
|
170 | `Skipping source document without local file URL: ${jsDoc.url}`);
|
171 | continue;
|
172 | }
|
173 | if (excludeFiles.some((r) => r.match(sourcePath))) {
|
174 | continue;
|
175 | }
|
176 | const filename = makeDeclarationsFilename(sourcePath);
|
177 | let docs = declarationDocs.get(filename);
|
178 | if (!docs) {
|
179 | docs = [];
|
180 | declarationDocs.set(filename, docs);
|
181 | }
|
182 | docs.push(jsDoc);
|
183 | }
|
184 |
|
185 | const tsDocs = [];
|
186 | const warnings = [...analysis.getWarnings()];
|
187 | for (const [declarationsFilename, analyzerDocs] of declarationDocs) {
|
188 | const tsDoc = new ts.Document({
|
189 | path: declarationsFilename,
|
190 | header: makeHeader(
|
191 | analyzerDocs.map((d) => analyzerUrlToRelativePath(d.url, rootDir))
|
192 | .filter((url): url is string => url !== undefined)),
|
193 | tsLintDisables: [{
|
194 | ruleName: 'variable-name',
|
195 | why: `Describing an API that's defined elsewhere.`,
|
196 | }],
|
197 | });
|
198 | for (const analyzerDoc of analyzerDocs) {
|
199 | if (isEsModuleDocument(analyzerDoc)) {
|
200 | tsDoc.isEsModule = true;
|
201 | }
|
202 | }
|
203 |
|
204 | for (const analyzerDoc of analyzerDocs) {
|
205 | const generator = new TypeGenerator(
|
206 | tsDoc,
|
207 | analysis,
|
208 | analyzerDoc,
|
209 | rootDir,
|
210 | config.excludeIdentifiers || []);
|
211 | generator.handleDocument();
|
212 | warnings.push(...generator.warnings);
|
213 | }
|
214 |
|
215 | for (const ref of tsDoc.referencePaths) {
|
216 | const resolvedRef = path.resolve(rootDir, path.dirname(tsDoc.path), ref);
|
217 | if (removeReferencesResolved.has(resolvedRef)) {
|
218 | tsDoc.referencePaths.delete(ref);
|
219 | }
|
220 | }
|
221 | for (const ref of addReferences[tsDoc.path] || []) {
|
222 | tsDoc.referencePaths.add(path.relative(path.dirname(tsDoc.path), ref));
|
223 | }
|
224 | for (const node of tsDoc.traverse()) {
|
225 | if (node.kind === 'name') {
|
226 | const renamed = renameTypes.get(node.name);
|
227 | if (renamed !== undefined) {
|
228 | node.name = renamed;
|
229 | }
|
230 | }
|
231 | }
|
232 | addAutoImports(tsDoc, autoImportMap);
|
233 | tsDoc.simplify();
|
234 |
|
235 |
|
236 |
|
237 |
|
238 | tsDocs.push(tsDoc);
|
239 | }
|
240 |
|
241 | const filteredWarnings = warnings.filter((warning) => {
|
242 | if (config.hideWarnings && warning.severity !== analyzer.Severity.ERROR) {
|
243 | return false;
|
244 | }
|
245 | const sourcePath =
|
246 | analyzerUrlToRelativePath(warning.sourceRange.file, rootDir);
|
247 | return sourcePath !== undefined &&
|
248 | !excludeFiles.some((pattern) => pattern.match(sourcePath));
|
249 | });
|
250 | const warningPrinter =
|
251 | new analyzer.WarningPrinter(process.stderr, {maxCodeLines: 1});
|
252 | await warningPrinter.printWarnings(filteredWarnings);
|
253 | if (filteredWarnings.some(
|
254 | (warning) => warning.severity === analyzer.Severity.ERROR)) {
|
255 | throw new Error('Encountered error generating types.');
|
256 | }
|
257 |
|
258 | if (config.googModules) {
|
259 | return tsDocs.map((d) => transformToGoogStyle(d, rootDir));
|
260 | }
|
261 | return tsDocs;
|
262 | }
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 | function addAutoImports(tsDoc: ts.Document, autoImport: Map<string, string>) {
|
269 | const alreadyImported = getImportedIdentifiers(tsDoc);
|
270 |
|
271 | for (const node of tsDoc.traverse()) {
|
272 | if (node.kind === 'name') {
|
273 | let importSpecifier = autoImport.get(node.name);
|
274 | if (importSpecifier === undefined) {
|
275 | continue;
|
276 | }
|
277 | if (alreadyImported.has(node.name)) {
|
278 | continue;
|
279 | }
|
280 | if (importSpecifier.startsWith('.')) {
|
281 | if (makeDeclarationsFilename(importSpecifier) === tsDoc.path) {
|
282 |
|
283 | continue;
|
284 | }
|
285 | importSpecifier =
|
286 | path.relative(path.dirname(tsDoc.path), importSpecifier);
|
287 | if (!importSpecifier.startsWith('.')) {
|
288 | importSpecifier = './' + importSpecifier;
|
289 | }
|
290 | }
|
291 | tsDoc.members.push(new ts.Import({
|
292 | identifiers: [{identifier: node.name}],
|
293 | fromModuleSpecifier: importSpecifier,
|
294 | }));
|
295 | alreadyImported.add(node.name);
|
296 | }
|
297 | }
|
298 | }
|
299 |
|
300 | function getPackageName(rootDir: string) {
|
301 | let packageInfo: {name?: string};
|
302 | try {
|
303 | packageInfo = JSON.parse(
|
304 | fsExtra.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'));
|
305 | } catch {
|
306 | return undefined;
|
307 | }
|
308 | return packageInfo.name;
|
309 | }
|
310 |
|
311 | function googModuleForNameBasedImportSpecifier(spec: string) {
|
312 | const name =
|
313 |
|
314 | spec.replace(/(\.d\.ts|\.js)$/, '')
|
315 |
|
316 | .replace(/[-\.](\w)/g, (_, s) => s.toUpperCase())
|
317 |
|
318 | .replace(/^@/g, '')
|
319 |
|
320 | .replace(/\//g, '.');
|
321 |
|
322 |
|
323 | return `goog:${name}`;
|
324 | }
|
325 |
|
326 |
|
327 | function transformToGoogStyle(tsDoc: ts.Document, rootDir: string) {
|
328 | const packageName = getPackageName(rootDir);
|
329 | if (!tsDoc.isEsModule || !packageName) {
|
330 | return tsDoc;
|
331 | }
|
332 |
|
333 | for (const child of tsDoc.traverse()) {
|
334 | if (child.kind === 'import' || child.kind === 'export') {
|
335 | if (!child.fromModuleSpecifier) {
|
336 | continue;
|
337 | }
|
338 | let spec = child.fromModuleSpecifier;
|
339 | if (spec.startsWith('.')) {
|
340 | spec = path.join(
|
341 | packageName,
|
342 | path.relative(
|
343 | rootDir, path.join(rootDir, path.dirname(tsDoc.path), spec))
|
344 | .replace(/^\.\//, ''));
|
345 | }
|
346 | const elementName = spec.split('/')[1];
|
347 | let trailingComment: undefined|string = undefined;
|
348 | if (elementName && !/\./.test(elementName)) {
|
349 | trailingComment =
|
350 | ` // from //third_party/javascript/polymer/v2/${elementName}`;
|
351 | }
|
352 | const googSpecifier = googModuleForNameBasedImportSpecifier(spec);
|
353 | if (googSpecifier !== undefined) {
|
354 | child.fromModuleSpecifier = googSpecifier;
|
355 | child.trailingComment = trailingComment;
|
356 | }
|
357 | }
|
358 | }
|
359 |
|
360 | let googModuleName =
|
361 | googModuleForNameBasedImportSpecifier(path.join(packageName, tsDoc.path));
|
362 | if (googModuleName === undefined) {
|
363 | googModuleName = tsDoc.path;
|
364 | }
|
365 | return new ts.Document({
|
366 | path: tsDoc.path,
|
367 | header: tsDoc.header,
|
368 | referencePaths: tsDoc.referencePaths,
|
369 | tsLintDisables: tsDoc.tsLintDisables,
|
370 | isEsModule: false,
|
371 | members: [new ts.Namespace(
|
372 | {name: googModuleName, members: tsDoc.members, style: 'module'})]
|
373 | });
|
374 | }
|
375 |
|
376 |
|
377 |
|
378 |
|
379 | function getImportedIdentifiers(tsDoc: ts.Document): Set<string> {
|
380 | const identifiers = new Set<string>();
|
381 | for (const member of tsDoc.members) {
|
382 | if (member.kind === 'import') {
|
383 | for (const {identifier, alias} of member.identifiers) {
|
384 | if (identifier !== ts.AllIdentifiers) {
|
385 | identifiers.add(alias || identifier);
|
386 | }
|
387 | }
|
388 | }
|
389 | }
|
390 | return identifiers;
|
391 | }
|
392 |
|
393 |
|
394 |
|
395 |
|
396 |
|
397 |
|
398 | function analyzerUrlToRelativePath(
|
399 | analyzerUrl: string, rootDir: string): string|undefined {
|
400 | const parsed = Uri.parse(analyzerUrl);
|
401 | if (parsed.scheme !== 'file' || parsed.authority || !parsed.fsPath) {
|
402 | return undefined;
|
403 | }
|
404 | return path.relative(rootDir, parsed.fsPath);
|
405 | }
|
406 |
|
407 |
|
408 |
|
409 |
|
410 |
|
411 | function makeDeclarationsFilename(sourceUrl: string): string {
|
412 | const parsed = path.parse(sourceUrl);
|
413 | return path.join(parsed.dir, parsed.name) + '.d.ts';
|
414 | }
|
415 |
|
416 |
|
417 |
|
418 |
|
419 | function makeHeader(sourceUrls: string[]): string {
|
420 | return `DO NOT EDIT
|
421 |
|
422 | This file was automatically generated by
|
423 | https://github.com/Polymer/tools/tree/master/packages/gen-typescript-declarations
|
424 |
|
425 | To modify these typings, edit the source file(s):
|
426 | ${sourceUrls.map((url) => ' ' + url).join('\n')}`;
|
427 | }
|
428 |
|
429 | class TypeGenerator {
|
430 | public warnings: analyzer.Warning[] = [];
|
431 | private excludeIdentifiers: Set<String>;
|
432 |
|
433 | |
434 |
|
435 |
|
436 |
|
437 | private forceResolvable = new Set<string>();
|
438 |
|
439 | constructor(
|
440 | private root: ts.Document, private analysis: analyzer.Analysis,
|
441 | private analyzerDoc: analyzer.Document, private rootDir: string,
|
442 | excludeIdentifiers: string[]) {
|
443 | this.excludeIdentifiers = new Set(excludeIdentifiers);
|
444 | }
|
445 |
|
446 | private warn(feature: analyzer.Feature, message: string) {
|
447 | this.warnings.push(new analyzer.Warning({
|
448 | message,
|
449 | sourceRange: feature.sourceRange!,
|
450 | severity: analyzer.Severity.WARNING,
|
451 |
|
452 | code: 'GEN_TYPESCRIPT_DECLARATIONS_WARNING',
|
453 | parsedDocument: this.analyzerDoc.parsedDocument,
|
454 | }));
|
455 | }
|
456 |
|
457 | |
458 |
|
459 |
|
460 |
|
461 | handleDocument() {
|
462 | for (const feature of this.analyzerDoc.getFeatures()) {
|
463 | if ([...feature.identifiers].some(
|
464 | (id) => this.excludeIdentifiers.has(id))) {
|
465 | continue;
|
466 | }
|
467 | if (isPrivate(feature)) {
|
468 | continue;
|
469 | }
|
470 | if (feature.kinds.has('element')) {
|
471 | this.handleElement(feature as analyzer.Element);
|
472 | } else if (feature.kinds.has('behavior')) {
|
473 | this.handleBehavior(feature as analyzer.PolymerBehavior);
|
474 | } else if (feature.kinds.has('element-mixin')) {
|
475 | this.handleMixin(feature as analyzer.ElementMixin);
|
476 | } else if (feature.kinds.has('class')) {
|
477 | this.handleClass(feature as analyzer.Class);
|
478 | } else if (feature.kinds.has('function')) {
|
479 | this.handleFunction(feature as AnalyzerFunction);
|
480 | } else if (feature.kinds.has('namespace')) {
|
481 | this.handleNamespace(feature as analyzer.Namespace);
|
482 | } else if (feature.kinds.has('html-import')) {
|
483 |
|
484 |
|
485 |
|
486 |
|
487 |
|
488 |
|
489 | if (feature.sourceRange &&
|
490 | feature.sourceRange.file === this.analyzerDoc.url) {
|
491 | this.handleHtmlImport(feature as analyzer.Import);
|
492 | }
|
493 | } else if (feature.kinds.has('js-import')) {
|
494 | this.handleJsImport(feature as analyzer.JavascriptImport);
|
495 | } else if (feature.kinds.has('export')) {
|
496 | this.handleJsExport(feature as analyzer.Export);
|
497 | }
|
498 | }
|
499 | }
|
500 |
|
501 | |
502 |
|
503 |
|
504 | private handleElement(feature: analyzer.Element) {
|
505 |
|
506 |
|
507 | let constructable;
|
508 |
|
509 | let fullName;
|
510 | let shortName;
|
511 | let parent;
|
512 |
|
513 | if (feature.className) {
|
514 | constructable = true;
|
515 | let namespacePath;
|
516 | [namespacePath, shortName] = splitReference(feature.className);
|
517 | fullName = feature.className;
|
518 | parent = findOrCreateNamespace(this.root, namespacePath);
|
519 |
|
520 | } else if (feature.tagName) {
|
521 |
|
522 |
|
523 |
|
524 |
|
525 |
|
526 |
|
527 |
|
528 |
|
529 | constructable = false;
|
530 | shortName = kebabToCamel(feature.tagName) + 'Element';
|
531 | fullName = shortName;
|
532 | parent = this.root;
|
533 |
|
534 | } else {
|
535 | this.warn(feature, `Could not find element name.`);
|
536 | return;
|
537 | }
|
538 |
|
539 | const legacyPolymerInterfaces = [];
|
540 | if (isPolymerElement(feature)) {
|
541 | legacyPolymerInterfaces.push(...feature.behaviorAssignments.map(
|
542 | (behavior) => behavior.identifier));
|
543 |
|
544 | if (feature.isLegacyFactoryCall) {
|
545 | if (this.root.isEsModule) {
|
546 | legacyPolymerInterfaces.push('LegacyElementMixin');
|
547 | if (!getImportedIdentifiers(this.root).has('LegacyElementMixin')) {
|
548 | this.root.members.push(new ts.Import({
|
549 | identifiers: [{identifier: 'LegacyElementMixin'}],
|
550 | fromModuleSpecifier:
|
551 | '@polymer/polymer/lib/legacy/legacy-element-mixin.js',
|
552 | }));
|
553 | }
|
554 |
|
555 | } else {
|
556 | legacyPolymerInterfaces.push('Polymer.LegacyElementMixin');
|
557 | }
|
558 |
|
559 | legacyPolymerInterfaces.push('HTMLElement');
|
560 | }
|
561 | }
|
562 |
|
563 | if (constructable) {
|
564 | this.handleClass(feature);
|
565 |
|
566 | if (legacyPolymerInterfaces.length > 0) {
|
567 |
|
568 | parent.members.push(new ts.Interface({
|
569 | name: shortName,
|
570 | extends: legacyPolymerInterfaces,
|
571 | }));
|
572 | }
|
573 |
|
574 | } else {
|
575 | parent.members.push(new ts.Interface({
|
576 | name: shortName,
|
577 | description: feature.description || feature.summary,
|
578 | properties: this.handleProperties(feature.properties.values()),
|
579 |
|
580 |
|
581 |
|
582 | methods: this.handleMethods(feature.methods.values()),
|
583 | extends: [
|
584 | ...feature.mixins.map((mixin) => mixin.identifier),
|
585 | ...legacyPolymerInterfaces,
|
586 | ],
|
587 | }));
|
588 |
|
589 | if (isPolymerElement(feature) && feature.isLegacyFactoryCall &&
|
590 | this.root.isEsModule) {
|
591 | this.root.members.push(
|
592 | new ts.Export({identifiers: [{identifier: shortName}]}));
|
593 | }
|
594 | }
|
595 |
|
596 |
|
597 |
|
598 |
|
599 |
|
600 | if (feature.tagName) {
|
601 | const elementMap = findOrCreateInterface(
|
602 | this.root.isEsModule ? findOrCreateGlobalNamespace(this.root) :
|
603 | this.root,
|
604 | 'HTMLElementTagNameMap');
|
605 | elementMap.properties.push(new ts.Property({
|
606 | name: feature.tagName,
|
607 | type: new ts.NameType(fullName),
|
608 | }));
|
609 | }
|
610 | }
|
611 |
|
612 | |
613 |
|
614 |
|
615 |
|
616 | private handleBehavior(feature: analyzer.PolymerBehavior) {
|
617 | if (!feature.className) {
|
618 | this.warn(feature, `Could not find a name for behavior.`);
|
619 | return;
|
620 | }
|
621 |
|
622 | const [namespacePath, className] = splitReference(feature.className);
|
623 | const ns = findOrCreateNamespace(this.root, namespacePath);
|
624 |
|
625 |
|
626 |
|
627 |
|
628 | ns.members.push(new ts.Interface({
|
629 | name: className,
|
630 | description: feature.description || feature.summary,
|
631 | extends: feature.behaviorAssignments.map((b) => b.identifier),
|
632 | properties: this.handleProperties(feature.properties.values()),
|
633 | methods: this.handleMethods(feature.methods.values()),
|
634 | }));
|
635 |
|
636 |
|
637 |
|
638 |
|
639 |
|
640 | ns.members.push(new ts.ConstValue({
|
641 | name: className,
|
642 | type: new ts.NameType('object'),
|
643 | }));
|
644 | }
|
645 |
|
646 | |
647 |
|
648 |
|
649 | private handleMixin(feature: analyzer.ElementMixin) {
|
650 | const [namespacePath, mixinName] = splitReference(feature.name);
|
651 | const parentNamespace = findOrCreateNamespace(this.root, namespacePath);
|
652 | const transitiveMixins = [...this.transitiveMixins(feature)];
|
653 | const constructorName = mixinName + 'Constructor';
|
654 |
|
655 |
|
656 |
|
657 |
|
658 | parentNamespace.members.push(new ts.Function({
|
659 | name: mixinName,
|
660 | description: feature.description,
|
661 | templateTypes: ['T extends new (...args: any[]) => {}'],
|
662 | params: [
|
663 | new ts.ParamType({name: 'base', type: new ts.NameType('T')}),
|
664 | ],
|
665 | returns: new ts.IntersectionType([
|
666 | new ts.NameType('T'),
|
667 | new ts.NameType(constructorName),
|
668 | ...transitiveMixins.map(
|
669 | (mixin) => new ts.NameType(mixin.name + 'Constructor'))
|
670 | ]),
|
671 | }));
|
672 |
|
673 | if (this.root.isEsModule) {
|
674 |
|
675 |
|
676 |
|
677 | for (const mixin of transitiveMixins) {
|
678 | if (mixin.sourceRange === undefined) {
|
679 | continue;
|
680 | }
|
681 | const rootRelative =
|
682 | analyzerUrlToRelativePath(mixin.sourceRange.file, this.rootDir);
|
683 | if (rootRelative === undefined) {
|
684 | continue;
|
685 | }
|
686 | const fileRelative =
|
687 | path.relative(path.dirname(this.root.path), rootRelative);
|
688 | const fromModuleSpecifier =
|
689 | fileRelative.startsWith('.') ? fileRelative : './' + fileRelative;
|
690 | const identifiers = [{identifier: mixin.name + 'Constructor'}];
|
691 | if (!getImportedIdentifiers(this.root).has(mixin.name)) {
|
692 | identifiers.push({identifier: mixin.name});
|
693 | }
|
694 | this.root.members.push(new ts.Import({
|
695 | identifiers,
|
696 | fromModuleSpecifier,
|
697 | }));
|
698 | }
|
699 | }
|
700 |
|
701 |
|
702 |
|
703 |
|
704 | parentNamespace.members.push(new ts.Interface({
|
705 | name: constructorName,
|
706 | methods: [
|
707 | new ts.Method({
|
708 | name: 'new',
|
709 | params: [
|
710 | new ts.ParamType({
|
711 | name: 'args',
|
712 | type: new ts.ArrayType(ts.anyType),
|
713 | rest: true,
|
714 | }),
|
715 | ],
|
716 | returns: new ts.NameType(mixinName),
|
717 | }),
|
718 | ...this.handleMethods(feature.staticMethods.values()),
|
719 | ],
|
720 | }));
|
721 |
|
722 | if (this.root.isEsModule) {
|
723 |
|
724 |
|
725 | this.root.members.push(
|
726 | new ts.Export({identifiers: [{identifier: constructorName}]}));
|
727 | }
|
728 |
|
729 |
|
730 |
|
731 | parentNamespace.members.push(
|
732 | new ts.Interface({
|
733 | name: mixinName,
|
734 | properties: this.handleProperties(feature.properties.values()),
|
735 | methods: this.handleMethods(feature.methods.values()),
|
736 | extends: transitiveMixins.map((mixin) => mixin.name),
|
737 | }),
|
738 | );
|
739 | }
|
740 |
|
741 | |
742 |
|
743 |
|
744 |
|
745 |
|
746 | private transitiveMixins(
|
747 | parentMixin: analyzer.ElementMixin,
|
748 | result?: Set<analyzer.ElementMixin>): Set<analyzer.ElementMixin> {
|
749 | if (result === undefined) {
|
750 | result = new Set();
|
751 | }
|
752 | for (const childRef of parentMixin.mixins) {
|
753 | const childMixinSet = this.analysis.getFeatures(
|
754 | {id: childRef.identifier, kind: 'element-mixin'});
|
755 | if (childMixinSet.size !== 1) {
|
756 | this.warn(
|
757 | parentMixin,
|
758 | `Found ${childMixinSet.size} features for mixin ` +
|
759 | `${childRef.identifier}, expected 1.`);
|
760 | continue;
|
761 | }
|
762 | const childMixin = childMixinSet.values().next().value;
|
763 | result.add(childMixin);
|
764 | this.transitiveMixins(childMixin, result);
|
765 | }
|
766 | return result;
|
767 | }
|
768 |
|
769 | |
770 |
|
771 |
|
772 | private handleClass(feature: analyzer.Class) {
|
773 | if (!feature.className) {
|
774 | this.warn(feature, `Could not find a name for class.`);
|
775 | return;
|
776 | }
|
777 | const [namespacePath, name] = splitReference(feature.className);
|
778 | const m = new ts.Class({name});
|
779 | m.description = feature.description;
|
780 | m.properties = this.handleProperties(feature.properties.values());
|
781 | m.methods = [
|
782 | ...this.handleMethods(feature.staticMethods.values(), {isStatic: true}),
|
783 | ...this.handleMethods(feature.methods.values())
|
784 | ];
|
785 | m.constructorMethod =
|
786 | this.handleConstructorMethod(feature.constructorMethod);
|
787 | if (feature.superClass !== undefined) {
|
788 | m.extends = feature.superClass.identifier;
|
789 | }
|
790 | m.mixins = feature.mixins.map((mixin) => mixin.identifier);
|
791 | findOrCreateNamespace(this.root, namespacePath).members.push(m);
|
792 | }
|
793 |
|
794 | |
795 |
|
796 |
|
797 | private handleFunction(feature: AnalyzerFunction) {
|
798 | const [namespacePath, name] = splitReference(feature.name);
|
799 |
|
800 | const f = new ts.Function({
|
801 | name,
|
802 | description: feature.description || feature.summary,
|
803 | templateTypes: feature.templateTypes,
|
804 | returns: closureTypeToTypeScript(
|
805 | feature.return && feature.return.type, feature.templateTypes),
|
806 | returnsDescription: feature.return && feature.return.desc
|
807 | });
|
808 |
|
809 | for (const param of feature.params || []) {
|
810 |
|
811 |
|
812 | f.params.push(closureParamToTypeScript(
|
813 | param.name, param.type, feature.templateTypes));
|
814 | }
|
815 |
|
816 | findOrCreateNamespace(this.root, namespacePath).members.push(f);
|
817 | }
|
818 |
|
819 | |
820 |
|
821 |
|
822 |
|
823 | private handleProperties(analyzerProperties: Iterable<analyzer.Property>):
|
824 | ts.Property[] {
|
825 | const tsProperties = <ts.Property[]>[];
|
826 | for (const property of analyzerProperties) {
|
827 | if (property.inheritedFrom || property.privacy === 'private' ||
|
828 | this.excludeIdentifiers.has(property.name)) {
|
829 | continue;
|
830 | }
|
831 | const p = new ts.Property({
|
832 | name: property.name,
|
833 |
|
834 |
|
835 | type: closureTypeToTypeScript(property.type),
|
836 | readOnly: property.readOnly,
|
837 | });
|
838 | p.description = property.description || '';
|
839 | tsProperties.push(p);
|
840 | }
|
841 | return tsProperties;
|
842 | }
|
843 |
|
844 |
|
845 | |
846 |
|
847 |
|
848 |
|
849 | private handleMethods(analyzerMethods: Iterable<analyzer.Method>, opts?: {
|
850 | isStatic?: boolean
|
851 | }): ts.Method[] {
|
852 | const tsMethods = <ts.Method[]>[];
|
853 | for (const method of analyzerMethods) {
|
854 | if (method.inheritedFrom || method.privacy === 'private' ||
|
855 | this.excludeIdentifiers.has(method.name)) {
|
856 | continue;
|
857 | }
|
858 |
|
859 | tsMethods.push(this.handleMethod(method, opts));
|
860 | }
|
861 | return tsMethods;
|
862 | }
|
863 |
|
864 | |
865 |
|
866 |
|
867 | private handleMethod(method: analyzer.Method, opts?: {isStatic?: boolean}):
|
868 | ts.Method {
|
869 | const m = new ts.Method({
|
870 | name: method.name,
|
871 | returns: closureTypeToTypeScript(method.return && method.return.type),
|
872 | returnsDescription: method.return && method.return.desc,
|
873 | isStatic: opts && opts.isStatic,
|
874 | ignoreTypeCheck: this.documentationHasSuppressTypeCheck(method.jsdoc)
|
875 | });
|
876 | m.description = method.description || '';
|
877 |
|
878 | let requiredAhead = false;
|
879 | for (const param of reverseIter(method.params || [])) {
|
880 | const tsParam = closureParamToTypeScript(param.name, param.type);
|
881 | tsParam.description = param.description || '';
|
882 |
|
883 | if (param.defaultValue !== undefined) {
|
884 |
|
885 |
|
886 |
|
887 |
|
888 | if (!requiredAhead) {
|
889 | tsParam.optional = true;
|
890 | } else {
|
891 | tsParam.type = new ts.UnionType([tsParam.type, ts.undefinedType]);
|
892 | }
|
893 | } else if (!tsParam.optional) {
|
894 | requiredAhead = true;
|
895 | }
|
896 |
|
897 |
|
898 |
|
899 | tsParam.rest = tsParam.rest || !!param.rest;
|
900 | if (tsParam.rest && tsParam.type.kind !== 'array') {
|
901 |
|
902 |
|
903 | tsParam.type = new ts.ArrayType(tsParam.type);
|
904 | }
|
905 |
|
906 | m.params.unshift(tsParam);
|
907 | }
|
908 | return m;
|
909 | }
|
910 |
|
911 | private documentationHasSuppressTypeCheck(annotation: jsdoc.Annotation|
|
912 | undefined): boolean {
|
913 | if (!annotation) {
|
914 | return false;
|
915 | }
|
916 |
|
917 | const annotationValue = annotation.tags.find((e) => e.title === 'suppress');
|
918 | return annotationValue && annotationValue.description === '{checkTypes}' ||
|
919 | false;
|
920 | }
|
921 |
|
922 | private handleConstructorMethod(method?: analyzer.Method): ts.Method
|
923 | |undefined {
|
924 | if (!method) {
|
925 | return;
|
926 | }
|
927 | const m = this.handleMethod(method);
|
928 | m.returns = undefined;
|
929 | return m;
|
930 | }
|
931 |
|
932 | |
933 |
|
934 |
|
935 | private handleNamespace(feature: analyzer.Namespace) {
|
936 | const ns = findOrCreateNamespace(this.root, feature.name.split('.'));
|
937 | if (ns.kind === 'namespace') {
|
938 | ns.description = feature.description || feature.summary || '';
|
939 | }
|
940 | }
|
941 |
|
942 | |
943 |
|
944 |
|
945 | private handleJsImport(feature: analyzer.JavascriptImport) {
|
946 | const node = feature.astNode.node;
|
947 |
|
948 | if (babel.isImportDeclaration(node)) {
|
949 | const identifiers: ts.ImportSpecifier[] = [];
|
950 | for (const specifier of node.specifiers) {
|
951 | if (babel.isImportSpecifier(specifier)) {
|
952 |
|
953 | if (this.isResolvable(specifier.imported.name, feature)) {
|
954 | identifiers.push({
|
955 | identifier: specifier.imported.name,
|
956 | alias: specifier.local.name,
|
957 | });
|
958 | }
|
959 |
|
960 | } else if (babel.isImportDefaultSpecifier(specifier)) {
|
961 |
|
962 | if (this.isResolvable('default', feature)) {
|
963 | identifiers.push({
|
964 | identifier: 'default',
|
965 | alias: specifier.local.name,
|
966 | });
|
967 | }
|
968 |
|
969 | } else if (babel.isImportNamespaceSpecifier(specifier)) {
|
970 |
|
971 | identifiers.push({
|
972 | identifier: ts.AllIdentifiers,
|
973 | alias: specifier.local.name,
|
974 | });
|
975 | this.forceResolvable.add(specifier.local.name);
|
976 | }
|
977 | }
|
978 |
|
979 | if (identifiers.length > 0) {
|
980 | this.root.members.push(new ts.Import({
|
981 | identifiers: identifiers,
|
982 | fromModuleSpecifier: node.source && node.source.value,
|
983 | }));
|
984 | }
|
985 | } else if (
|
986 |
|
987 |
|
988 | !babel.isExportNamedDeclaration(node) &&
|
989 | !babel.isExportAllDeclaration(node)) {
|
990 | this.warn(feature, `Import with AST type ${node.type} not supported.`);
|
991 | }
|
992 | }
|
993 |
|
994 |
|
995 | |
996 |
|
997 |
|
998 | private handleJsExport(feature: analyzer.Export) {
|
999 | const node = feature.astNode.node;
|
1000 |
|
1001 | if (babel.isExportAllDeclaration(node)) {
|
1002 |
|
1003 | this.root.members.push(new ts.Export({
|
1004 | identifiers: ts.AllIdentifiers,
|
1005 | fromModuleSpecifier: node.source && node.source.value,
|
1006 | }));
|
1007 |
|
1008 | } else if (babel.isExportNamedDeclaration(node)) {
|
1009 | const identifiers = [];
|
1010 |
|
1011 | if (node.declaration) {
|
1012 |
|
1013 | for (const identifier of feature.identifiers) {
|
1014 | if (this.isResolvable(identifier, feature)) {
|
1015 | identifiers.push({identifier});
|
1016 | }
|
1017 | }
|
1018 |
|
1019 | } else {
|
1020 |
|
1021 | for (const specifier of node.specifiers) {
|
1022 | if (this.isResolvable(specifier.exported.name, feature) ||
|
1023 | this.isResolvable(specifier.local.name, feature)) {
|
1024 | identifiers.push({
|
1025 | identifier: specifier.local.name,
|
1026 | alias: specifier.exported.name,
|
1027 | });
|
1028 | }
|
1029 | }
|
1030 | }
|
1031 |
|
1032 | if (identifiers.length > 0) {
|
1033 | this.root.members.push(new ts.Export({
|
1034 | identifiers,
|
1035 | fromModuleSpecifier: node.source && node.source.value,
|
1036 | }));
|
1037 | }
|
1038 |
|
1039 | } else {
|
1040 | this.warn(
|
1041 | feature,
|
1042 | `Export feature with AST node type ${node.type} not supported.`);
|
1043 | }
|
1044 | }
|
1045 |
|
1046 | |
1047 |
|
1048 |
|
1049 |
|
1050 | private isResolvable(
|
1051 | identifier: string,
|
1052 | fromFeature: analyzer.JavascriptImport|analyzer.Export) {
|
1053 | if (this.forceResolvable.has(identifier)) {
|
1054 | return true;
|
1055 | }
|
1056 | if (this.excludeIdentifiers.has(identifier)) {
|
1057 | return false;
|
1058 | }
|
1059 | const resolved =
|
1060 | resolveImportExportFeature(fromFeature, identifier, this.analyzerDoc);
|
1061 | return resolved !== undefined && resolved.feature !== undefined &&
|
1062 | !isPrivate(resolved.feature) && !isBehaviorImpl(resolved);
|
1063 | }
|
1064 |
|
1065 | |
1066 |
|
1067 |
|
1068 |
|
1069 |
|
1070 | private handleHtmlImport(feature: analyzer.Import) {
|
1071 | let sourcePath = analyzerUrlToRelativePath(feature.url, this.rootDir);
|
1072 | if (sourcePath === undefined) {
|
1073 | this.warn(
|
1074 | feature,
|
1075 | `Skipping HTML import without local file URL: ${feature.url}`);
|
1076 | return;
|
1077 | }
|
1078 |
|
1079 |
|
1080 |
|
1081 |
|
1082 |
|
1083 |
|
1084 |
|
1085 |
|
1086 |
|
1087 |
|
1088 | sourcePath =
|
1089 | sourcePath.replace(/^(bower_components|node_modules)\//, '../');
|
1090 |
|
1091 |
|
1092 |
|
1093 |
|
1094 |
|
1095 |
|
1096 |
|
1097 |
|
1098 | sourcePath = sourcePath.replace(/^\.\.\/polymer\//, '../polymer/types/');
|
1099 |
|
1100 | this.root.referencePaths.add(path.relative(
|
1101 | path.dirname(this.root.path), makeDeclarationsFilename(sourcePath)));
|
1102 | }
|
1103 | }
|
1104 |
|
1105 |
|
1106 |
|
1107 |
|
1108 | function* reverseIter<T>(arr: T[]) {
|
1109 | for (let i = arr.length - 1; i >= 0; i--) {
|
1110 | yield arr[i];
|
1111 | }
|
1112 | }
|
1113 |
|
1114 |
|
1115 |
|
1116 |
|
1117 |
|
1118 | function findOrCreateGlobalNamespace(doc: ts.Document): ts.GlobalNamespace {
|
1119 | for (const member of doc.members) {
|
1120 | if (member.kind === 'globalNamespace') {
|
1121 | return member;
|
1122 | }
|
1123 | }
|
1124 | const globalNamespace = new ts.GlobalNamespace();
|
1125 | doc.members.push(globalNamespace);
|
1126 | return globalNamespace;
|
1127 | }
|
1128 |
|
1129 |
|
1130 |
|
1131 |
|
1132 |
|
1133 | function findOrCreateNamespace(
|
1134 | root: ts.Document|ts.Namespace|ts.GlobalNamespace,
|
1135 | path: string[]): ts.Document|ts.Namespace|ts.GlobalNamespace {
|
1136 | if (!path.length) {
|
1137 | return root;
|
1138 | }
|
1139 | let first: ts.Namespace|undefined;
|
1140 | for (const member of root.members) {
|
1141 | if (member.kind === 'namespace' && member.name === path[0]) {
|
1142 | first = member;
|
1143 | break;
|
1144 | }
|
1145 | }
|
1146 | if (!first) {
|
1147 | first = new ts.Namespace({name: path[0]});
|
1148 | root.members.push(first);
|
1149 | }
|
1150 | return findOrCreateNamespace(first, path.slice(1));
|
1151 | }
|
1152 |
|
1153 |
|
1154 |
|
1155 |
|
1156 |
|
1157 | function findOrCreateInterface(
|
1158 | root: ts.Document|ts.Namespace|ts.GlobalNamespace,
|
1159 | reference: string): ts.Interface {
|
1160 | const [namespacePath, name] = splitReference(reference);
|
1161 | const namespace_ = findOrCreateNamespace(root, namespacePath);
|
1162 | for (const member of namespace_.members) {
|
1163 | if (member.kind === 'interface' && member.name === name) {
|
1164 | return member;
|
1165 | }
|
1166 | }
|
1167 | const i = new ts.Interface({name});
|
1168 | namespace_.members.push(i);
|
1169 | return i;
|
1170 | }
|
1171 |
|
1172 |
|
1173 |
|
1174 |
|
1175 | function isPolymerElement(feature: analyzer.Feature):
|
1176 | feature is analyzer.PolymerElement {
|
1177 | return feature.kinds.has('polymer-element');
|
1178 | }
|
1179 |
|
1180 |
|
1181 |
|
1182 |
|
1183 |
|
1184 |
|
1185 |
|
1186 |
|
1187 |
|
1188 |
|
1189 |
|
1190 |
|
1191 |
|
1192 |
|
1193 |
|
1194 |
|
1195 |
|
1196 | function isBehaviorImpl(reference: analyzer.Reference<analyzer.Feature>) {
|
1197 | return reference.feature !== undefined &&
|
1198 | reference.feature.kinds.has('behavior') &&
|
1199 | (reference.feature as analyzer.PolymerBehavior).name !==
|
1200 | reference.identifier;
|
1201 | }
|
1202 |
|
1203 | interface MaybePrivate {
|
1204 | privacy?: 'public'|'private'|'protected';
|
1205 | }
|
1206 |
|
1207 |
|
1208 |
|
1209 |
|
1210 | function isPrivate(feature: analyzer.Feature&MaybePrivate): boolean {
|
1211 | return feature.privacy === 'private';
|
1212 | }
|
1213 |
|
1214 |
|
1215 |
|
1216 |
|
1217 | function kebabToCamel(s: string): string {
|
1218 | return s.replace(/(^|-)(.)/g, (_match, _p0, p1) => p1.toUpperCase());
|
1219 | }
|
1220 |
|
1221 |
|
1222 |
|
1223 |
|
1224 |
|
1225 | function splitReference(reference: string): [string[], string] {
|
1226 | const parts = reference.split('.');
|
1227 | const namespacePath = parts.slice(0, -1);
|
1228 | const name = parts[parts.length - 1];
|
1229 | return [namespacePath, name];
|
1230 | }
|