summaryrefslogtreecommitdiff
path: root/tools/ts_library_builder/ast_util.ts
diff options
context:
space:
mode:
Diffstat (limited to 'tools/ts_library_builder/ast_util.ts')
-rw-r--r--tools/ts_library_builder/ast_util.ts331
1 files changed, 331 insertions, 0 deletions
diff --git a/tools/ts_library_builder/ast_util.ts b/tools/ts_library_builder/ast_util.ts
new file mode 100644
index 000000000..c13195b08
--- /dev/null
+++ b/tools/ts_library_builder/ast_util.ts
@@ -0,0 +1,331 @@
+import { relative } from "path";
+import { readFileSync } from "fs";
+import { EOL } from "os";
+import {
+ ExportDeclaration,
+ ImportDeclaration,
+ InterfaceDeclaration,
+ JSDoc,
+ Project,
+ PropertySignature,
+ SourceFile,
+ StatementedNode,
+ ts,
+ TypeGuards,
+ VariableStatement,
+ VariableDeclarationKind
+} from "ts-simple-ast";
+
+/** Add a property to an interface */
+export function addInterfaceProperty(
+ interfaceDeclaration: InterfaceDeclaration,
+ name: string,
+ type: string,
+ jsdocs?: JSDoc[]
+): PropertySignature {
+ return interfaceDeclaration.addProperty({
+ name,
+ type,
+ docs: jsdocs && jsdocs.map(jsdoc => jsdoc.getText())
+ });
+}
+
+/** Add `@url` comment to node. */
+export function addSourceComment(
+ node: StatementedNode,
+ sourceFile: SourceFile,
+ rootPath: string
+): void {
+ node.insertStatements(
+ 0,
+ `// @url ${relative(rootPath, sourceFile.getFilePath())}\n\n`
+ );
+}
+
+/** Add a declaration of a variable to a node */
+export function addVariableDeclaration(
+ node: StatementedNode,
+ name: string,
+ type: string,
+ jsdocs?: JSDoc[]
+): VariableStatement {
+ return node.addVariableStatement({
+ declarationKind: VariableDeclarationKind.Const,
+ declarations: [{ name, type }],
+ docs: jsdocs && jsdocs.map(jsdoc => jsdoc.getText())
+ });
+}
+
+/** Check diagnostics, and if any exist, exit the process */
+export function checkDiagnostics(project: Project, onlyFor?: string[]) {
+ const program = project.getProgram();
+ const diagnostics = [
+ ...program.getGlobalDiagnostics(),
+ ...program.getSyntacticDiagnostics(),
+ ...program.getSemanticDiagnostics(),
+ ...program.getDeclarationDiagnostics()
+ ]
+ .filter(diagnostic => {
+ const sourceFile = diagnostic.getSourceFile();
+ return onlyFor && sourceFile
+ ? onlyFor.includes(sourceFile.getFilePath())
+ : true;
+ })
+ .map(diagnostic => diagnostic.compilerObject);
+
+ if (diagnostics.length) {
+ console.log(
+ ts.formatDiagnosticsWithColorAndContext(diagnostics, formatDiagnosticHost)
+ );
+ process.exit(1);
+ }
+}
+
+export interface FlattenNamespaceOptions {
+ customSources?: { [sourceFilePath: string]: string };
+ debug?: boolean;
+ rootPath: string;
+ sourceFile: SourceFile;
+}
+
+/** Take a namespace and flatten all exports. */
+export function flattenNamespace({
+ customSources,
+ debug,
+ rootPath,
+ sourceFile
+}: FlattenNamespaceOptions): string {
+ const sourceFiles = new Set<SourceFile>();
+ let output = "";
+ const exportedSymbols = getExportedSymbols(sourceFile);
+
+ function flattenDeclarations(
+ declaration: ImportDeclaration | ExportDeclaration
+ ) {
+ const declarationSourceFile = declaration.getModuleSpecifierSourceFile();
+ if (declarationSourceFile) {
+ processSourceFile(declarationSourceFile);
+ declaration.remove();
+ }
+ }
+
+ function rectifyNodes(currentSourceFile: SourceFile) {
+ currentSourceFile.forEachChild(node => {
+ if (TypeGuards.isAmbientableNode(node)) {
+ node.setHasDeclareKeyword(false);
+ }
+ if (TypeGuards.isExportableNode(node)) {
+ const nodeSymbol = node.getSymbol();
+ if (
+ nodeSymbol &&
+ !exportedSymbols.has(nodeSymbol.getFullyQualifiedName())
+ ) {
+ node.setIsExported(false);
+ }
+ }
+ });
+ }
+
+ function processSourceFile(currentSourceFile: SourceFile) {
+ if (sourceFiles.has(currentSourceFile)) {
+ return;
+ }
+ sourceFiles.add(currentSourceFile);
+
+ const currentSourceFilePath = currentSourceFile.getFilePath();
+ if (customSources && currentSourceFilePath in customSources) {
+ output += customSources[currentSourceFilePath];
+ return;
+ }
+
+ currentSourceFile.getImportDeclarations().forEach(flattenDeclarations);
+ currentSourceFile.getExportDeclarations().forEach(flattenDeclarations);
+
+ rectifyNodes(currentSourceFile);
+
+ output +=
+ (debug ? getSourceComment(currentSourceFile, rootPath) : "") +
+ currentSourceFile.print();
+ }
+
+ sourceFile.getExportDeclarations().forEach(exportDeclaration => {
+ processSourceFile(exportDeclaration.getModuleSpecifierSourceFileOrThrow());
+ exportDeclaration.remove();
+ });
+
+ rectifyNodes(sourceFile);
+
+ return (
+ output +
+ (debug ? getSourceComment(sourceFile, rootPath) : "") +
+ sourceFile.print()
+ );
+}
+
+/** Used when formatting diagnostics */
+const formatDiagnosticHost: ts.FormatDiagnosticsHost = {
+ getCurrentDirectory() {
+ return process.cwd();
+ },
+ getCanonicalFileName(path: string) {
+ return path;
+ },
+ getNewLine() {
+ return EOL;
+ }
+};
+
+/** Return a set of fully qualified symbol names for the files exports */
+function getExportedSymbols(sourceFile: SourceFile): Set<string> {
+ const exportedSymbols = new Set<string>();
+ const exportDeclarations = sourceFile.getExportDeclarations();
+ for (const exportDeclaration of exportDeclarations) {
+ const exportSpecifiers = exportDeclaration.getNamedExports();
+ for (const exportSpecifier of exportSpecifiers) {
+ const aliasedSymbol = exportSpecifier
+ .getSymbolOrThrow()
+ .getAliasedSymbol();
+ if (aliasedSymbol) {
+ exportedSymbols.add(aliasedSymbol.getFullyQualifiedName());
+ }
+ }
+ }
+ return exportedSymbols;
+}
+
+/** Returns a string which indicates the source file as the source */
+export function getSourceComment(
+ sourceFile: SourceFile,
+ rootPath: string
+): string {
+ return `\n// @url ${relative(rootPath, sourceFile.getFilePath())}\n\n`;
+}
+
+/**
+ * Load and write to a virtual file system all the default libs needed to
+ * resolve types on project.
+ */
+export function loadDtsFiles(project: Project) {
+ loadFiles(
+ project,
+ [
+ "lib.es2015.collection.d.ts",
+ "lib.es2015.core.d.ts",
+ "lib.es2015.d.ts",
+ "lib.es2015.generator.d.ts",
+ "lib.es2015.iterable.d.ts",
+ "lib.es2015.promise.d.ts",
+ "lib.es2015.proxy.d.ts",
+ "lib.es2015.reflect.d.ts",
+ "lib.es2015.symbol.d.ts",
+ "lib.es2015.symbol.wellknown.d.ts",
+ "lib.es2016.array.include.d.ts",
+ "lib.es2016.d.ts",
+ "lib.es2017.d.ts",
+ "lib.es2017.intl.d.ts",
+ "lib.es2017.object.d.ts",
+ "lib.es2017.sharedmemory.d.ts",
+ "lib.es2017.string.d.ts",
+ "lib.es2017.typedarrays.d.ts",
+ "lib.es2018.d.ts",
+ "lib.es2018.intl.d.ts",
+ "lib.es2018.promise.d.ts",
+ "lib.es2018.regexp.d.ts",
+ "lib.es5.d.ts",
+ "lib.esnext.d.ts",
+ "lib.esnext.array.d.ts",
+ "lib.esnext.asynciterable.d.ts",
+ "lib.esnext.intl.d.ts",
+ "lib.esnext.symbol.d.ts"
+ ].map(fileName => `node_modules/typescript/lib/${fileName}`)
+ );
+}
+
+/** Load a set of files into a file system host. */
+export function loadFiles(project: Project, filePaths: string[]) {
+ const fileSystem = project.getFileSystem();
+ for (const filePath of filePaths) {
+ const fileText = readFileSync(filePath, {
+ encoding: "utf8"
+ });
+ fileSystem.writeFileSync(filePath, fileText);
+ }
+}
+
+export interface NamespaceSourceFileOptions {
+ debug?: boolean;
+ namespace?: string;
+ rootPath: string;
+ sourceFileMap: Map<SourceFile, string>;
+}
+
+/**
+ * Take a source file (`.d.ts`) and convert it to a namespace, resolving any
+ * imports as their own namespaces.
+ */
+export function namespaceSourceFile(
+ sourceFile: SourceFile,
+ { debug, namespace, rootPath, sourceFileMap }: NamespaceSourceFileOptions
+): string {
+ if (sourceFileMap.has(sourceFile)) {
+ return "";
+ }
+ if (!namespace) {
+ namespace = sourceFile.getBaseNameWithoutExtension();
+ }
+ sourceFileMap.set(sourceFile, namespace);
+
+ sourceFile.forEachChild(node => {
+ if (TypeGuards.isAmbientableNode(node)) {
+ node.setHasDeclareKeyword(false);
+ }
+ });
+
+ const globalNamespace = sourceFile.getNamespace("global");
+ const globalNamespaceText = globalNamespace && globalNamespace.print();
+ if (globalNamespace) {
+ globalNamespace.remove();
+ }
+
+ const output = sourceFile
+ .getImportDeclarations()
+ .map(declaration => {
+ if (
+ declaration.getNamedImports().length ||
+ !declaration.getNamespaceImport()
+ ) {
+ throw new Error(
+ "Unsupported import clause.\n" +
+ ` In: "${declaration.getSourceFile().getFilePath()}"\n` +
+ ` Text: "${declaration.getText()}"`
+ );
+ }
+ const text = namespaceSourceFile(
+ declaration.getModuleSpecifierSourceFileOrThrow(),
+ {
+ debug,
+ namespace: declaration.getNamespaceImportOrThrow().getText(),
+ rootPath,
+ sourceFileMap
+ }
+ );
+ declaration.remove();
+ return text;
+ })
+ .join("\n");
+ sourceFile
+ .getExportDeclarations()
+ .forEach(declaration => declaration.remove());
+
+ return `${output}
+ ${globalNamespaceText || ""}
+ namespace ${namespace} {
+ ${debug ? getSourceComment(sourceFile, rootPath) : ""}
+ ${sourceFile.getText()}
+ }`;
+}
+
+/** Mirrors TypeScript's handling of paths */
+export function normalizeSlashes(path: string): string {
+ return path.replace(/\\/g, "/");
+}