/** * @license * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. * This code may only be used under the BSD style license found at * http://polymer.github.io/LICENSE.txt The complete set of authors may be found * at http://polymer.github.io/AUTHORS.txt The complete set of contributors may * be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by * Google as part of the polymer project is also subject to an additional IP * rights grant found at http://polymer.github.io/PATENTS.txt */ import * as minimatch from 'minimatch'; import * as path from 'path'; import * as analyzer from 'polymer-analyzer'; import {Function as AnalyzerFunction} from 'polymer-analyzer/lib/javascript/function'; import {closureParamToTypeScript, closureTypeToTypeScript} from './closure-types'; import * as ts from './ts-ast'; /** * Configuration for declaration generation. */ export interface Config { /** * Skip source files whose paths match any of these glob patterns. If * undefined, defaults to excluding directories ending in "test" or "demo". */ exclude?: string[]; /** * Remove any triple-slash references to these files, specified as paths * relative to the analysis root directory. */ removeReferences?: string[]; /** * Additional files to insert as triple-slash reference statements. Given the * map `a: b[]`, a will get an additional reference statement for each file * path in b. All paths are relative to the analysis root directory. */ addReferences?: {[filepath: string]: string[]}; } /** * Analyze all files in the given directory using Polymer Analyzer, and return * TypeScript declaration document strings in a map keyed by relative path. */ export async function generateDeclarations( rootDir: string, config: Config): Promise> { const a = new analyzer.Analyzer({ urlLoader: new analyzer.FSUrlLoader(rootDir), urlResolver: new analyzer.PackageUrlResolver(), }); const analysis = await a.analyzePackage(); const outFiles = new Map(); for (const tsDoc of analyzerToAst(analysis, config, rootDir)) { outFiles.set(tsDoc.path, tsDoc.serialize()) } return outFiles; } /** * Make TypeScript declaration documents from the given Polymer Analyzer * result. */ function analyzerToAst( analysis: analyzer.Analysis, config: Config, rootDir: string): ts.Document[] { const exclude = (config.exclude || ['test/**', 'demo/**']) .map((p) => new minimatch.Minimatch(p)); const addReferences = config.addReferences || {}; const removeReferencesResolved = new Set( (config.removeReferences || []).map((r) => path.resolve(rootDir, r))); // Analyzer can produce multiple JS documents with the same URL (e.g. an // HTML file with multiple inline scripts). We also might have multiple // files with the same basename (e.g. `foo.html` with an inline script, // and `foo.js`). We want to produce one declarations file for each // basename, so we first group Analyzer documents by their declarations // filename. const declarationDocs = new Map(); for (const jsDoc of analysis.getFeatures({kind: 'js-document'})) { if (exclude.some((r) => r.match(jsDoc.url))) { continue; } const filename = makeDeclarationsFilename(jsDoc.url); let docs = declarationDocs.get(filename); if (!docs) { docs = []; declarationDocs.set(filename, docs); } docs.push(jsDoc); } const tsDocs = []; for (const [declarationsFilename, analyzerDocs] of declarationDocs) { const tsDoc = new ts.Document({ path: declarationsFilename, header: makeHeader(analyzerDocs.map((d) => d.url)), }); for (const analyzerDoc of analyzerDocs) { handleDocument(analyzerDoc, tsDoc); } 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)); } 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; } /** * Create a TypeScript declarations filename for the given source document URL. * Simply replaces the file extension with `d.ts`. */ function makeDeclarationsFilename(sourceUrl: string): string { const parsed = path.parse(sourceUrl); return path.join(parsed.dir, parsed.name) + '.d.ts'; } /** * Generate the header comment to show at the top of a declarations document. */ function makeHeader(sourceUrls: string[]): string { return `DO NOT EDIT This file was automatically generated by https://github.com/Polymer/gen-typescript-declarations To modify these typings, edit the source file(s): ${sourceUrls.map((url) => ' ' + url).join('\n')}`; } interface MaybePrivate { privacy?: 'public'|'private'|'protected' } /** * Extend the given TypeScript declarations document with all of the relevant * items in the given Polymer Analyzer document. */ function handleDocument(doc: analyzer.Document, root: ts.Document) { for (const feature of doc.getFeatures()) { if ((feature as MaybePrivate).privacy === 'private') { continue; } 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); } 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