1/// <reference types="node"/> 2 3import * as ts from "../lib/typescript"; 4import * as path from "path"; 5 6function endsWith(s: string, suffix: string) { 7 return s.lastIndexOf(suffix, s.length - suffix.length) !== -1; 8} 9 10function isStringEnum(declaration: ts.EnumDeclaration) { 11 return declaration.members.length && declaration.members.every(m => !!m.initializer && m.initializer.kind === ts.SyntaxKind.StringLiteral); 12} 13 14class DeclarationsWalker { 15 private visitedTypes: ts.Type[] = []; 16 private text = ""; 17 private removedTypes: ts.Type[] = []; 18 19 private constructor(private typeChecker: ts.TypeChecker, private protocolFile: ts.SourceFile) { 20 } 21 22 static getExtraDeclarations(typeChecker: ts.TypeChecker, protocolFile: ts.SourceFile): string { 23 const walker = new DeclarationsWalker(typeChecker, protocolFile); 24 walker.visitTypeNodes(protocolFile); 25 let text = walker.text 26 ? `declare namespace ts.server.protocol {\n${walker.text}}` 27 : ""; 28 if (walker.removedTypes) { 29 text += "\ndeclare namespace ts {\n"; 30 text += " // these types are empty stubs for types from services and should not be used directly\n"; 31 for (const type of walker.removedTypes) { 32 text += ` export type ${type.symbol.name} = never;\n`; 33 } 34 text += "}"; 35 } 36 return text; 37 } 38 39 private processType(type: ts.Type): void { 40 if (this.visitedTypes.indexOf(type) >= 0) { 41 return; 42 } 43 this.visitedTypes.push(type); 44 const s = type.aliasSymbol || type.getSymbol(); 45 if (!s) { 46 return; 47 } 48 if (s.name === "Array" || s.name === "ReadOnlyArray") { 49 // we should process type argument instead 50 return this.processType((<any>type).typeArguments[0]); 51 } 52 else { 53 const declarations = s.getDeclarations(); 54 if (declarations) { 55 for (const decl of declarations) { 56 const sourceFile = decl.getSourceFile(); 57 if (sourceFile === this.protocolFile || /lib(\..+)?\.d.ts/.test(path.basename(sourceFile.fileName))) { 58 return; 59 } 60 if (decl.kind === ts.SyntaxKind.EnumDeclaration && !isStringEnum(decl as ts.EnumDeclaration)) { 61 this.removedTypes.push(type); 62 return; 63 } 64 else { 65 // splice declaration in final d.ts file 66 const text = decl.getFullText(); 67 this.text += `${text}\n`; 68 // recursively pull all dependencies into result dts file 69 70 this.visitTypeNodes(decl); 71 } 72 } 73 } 74 } 75 } 76 77 private visitTypeNodes(node: ts.Node) { 78 if (node.parent) { 79 switch (node.parent.kind) { 80 case ts.SyntaxKind.VariableDeclaration: 81 case ts.SyntaxKind.MethodDeclaration: 82 case ts.SyntaxKind.MethodSignature: 83 case ts.SyntaxKind.PropertyDeclaration: 84 case ts.SyntaxKind.PropertySignature: 85 case ts.SyntaxKind.Parameter: 86 case ts.SyntaxKind.IndexSignature: 87 if (((<ts.VariableDeclaration | ts.MethodDeclaration | ts.PropertyDeclaration | ts.ParameterDeclaration | ts.PropertySignature | ts.MethodSignature | ts.IndexSignatureDeclaration>node.parent).type) === node) { 88 this.processTypeOfNode(node); 89 } 90 break; 91 case ts.SyntaxKind.InterfaceDeclaration: 92 const heritageClauses = (<ts.InterfaceDeclaration>node.parent).heritageClauses; 93 if (heritageClauses) { 94 if (heritageClauses[0].token !== ts.SyntaxKind.ExtendsKeyword) { 95 throw new Error(`Unexpected kind of heritage clause: ${ts.SyntaxKind[heritageClauses[0].kind]}`); 96 } 97 for (const type of heritageClauses[0].types) { 98 this.processTypeOfNode(type); 99 } 100 } 101 break; 102 } 103 } 104 ts.forEachChild(node, n => this.visitTypeNodes(n)); 105 } 106 107 private processTypeOfNode(node: ts.Node): void { 108 if (node.kind === ts.SyntaxKind.UnionType) { 109 for (const t of (<ts.UnionTypeNode>node).types) { 110 this.processTypeOfNode(t); 111 } 112 } 113 else { 114 const type = this.typeChecker.getTypeAtLocation(node); 115 if (type && !(type.flags & (ts.TypeFlags.TypeParameter))) { 116 this.processType(type); 117 } 118 } 119 } 120} 121 122function writeProtocolFile(outputFile: string, protocolTs: string, typeScriptServicesDts: string) { 123 const options = { target: ts.ScriptTarget.ES5, declaration: true, noResolve: false, types: <string[]>[], stripInternal: true }; 124 125 /** 126 * 1st pass - generate a program from protocol.ts and typescriptservices.d.ts and emit core version of protocol.d.ts with all internal members stripped 127 * @return text of protocol.d.t.s 128 */ 129 function getInitialDtsFileForProtocol() { 130 const program = ts.createProgram([protocolTs, typeScriptServicesDts, path.join(typeScriptServicesDts, "../lib.es5.d.ts")], options); 131 132 let protocolDts: string | undefined; 133 const emitResult = program.emit(program.getSourceFile(protocolTs), (file, content) => { 134 if (endsWith(file, ".d.ts")) { 135 protocolDts = content; 136 } 137 }); 138 139 if (protocolDts === undefined) { 140 const diagHost: ts.FormatDiagnosticsHost = { 141 getCanonicalFileName(f) { return f; }, 142 getCurrentDirectory() { return "."; }, 143 getNewLine() { return "\r\n"; } 144 }; 145 const diags = emitResult.diagnostics.map(d => ts.formatDiagnostic(d, diagHost)).join("\r\n"); 146 throw new Error(`Declaration file for protocol.ts is not generated:\r\n${diags}`); 147 } 148 return protocolDts; 149 } 150 151 const protocolFileName = "protocol.d.ts"; 152 /** 153 * Second pass - generate a program from protocol.d.ts and typescriptservices.d.ts, then augment core protocol.d.ts with extra types from typescriptservices.d.ts 154 */ 155 function getProgramWithProtocolText(protocolDts: string, includeTypeScriptServices: boolean) { 156 const host = ts.createCompilerHost(options); 157 const originalGetSourceFile = host.getSourceFile; 158 host.getSourceFile = (fileName) => { 159 if (fileName === protocolFileName) { 160 return ts.createSourceFile(fileName, protocolDts, options.target); 161 } 162 return originalGetSourceFile.apply(host, [fileName]); 163 }; 164 const rootFiles = includeTypeScriptServices ? [protocolFileName, typeScriptServicesDts] : [protocolFileName]; 165 return ts.createProgram(rootFiles, options, host); 166 } 167 168 let protocolDts = getInitialDtsFileForProtocol(); 169 const program = getProgramWithProtocolText(protocolDts, /*includeTypeScriptServices*/ true); 170 171 const protocolFile = program.getSourceFile("protocol.d.ts")!; 172 const extraDeclarations = DeclarationsWalker.getExtraDeclarations(program.getTypeChecker(), protocolFile); 173 if (extraDeclarations) { 174 protocolDts += extraDeclarations; 175 } 176 protocolDts += "\nimport protocol = ts.server.protocol;"; 177 protocolDts += "\nexport = protocol;"; 178 protocolDts += "\nexport as namespace protocol;"; 179 180 // do sanity check and try to compile generated text as standalone program 181 const sanityCheckProgram = getProgramWithProtocolText(protocolDts, /*includeTypeScriptServices*/ false); 182 const diagnostics = [...sanityCheckProgram.getSyntacticDiagnostics(), ...sanityCheckProgram.getSemanticDiagnostics(), ...sanityCheckProgram.getGlobalDiagnostics()]; 183 184 ts.sys.writeFile(outputFile, protocolDts); 185 186 if (diagnostics.length) { 187 const flattenedDiagnostics = diagnostics.map(d => `${ts.flattenDiagnosticMessageText(d.messageText, "\n")} at ${d.file ? d.file.fileName : "<unknown>"} line ${d.start}`).join("\n"); 188 throw new Error(`Unexpected errors during sanity check: ${flattenedDiagnostics}`); 189 } 190} 191 192if (process.argv.length < 5) { 193 console.log(`Expected 3 arguments: path to 'protocol.ts', path to 'typescriptservices.d.ts' and path to output file`); 194 process.exit(1); 195} 196 197const protocolTs = process.argv[2]; 198const typeScriptServicesDts = process.argv[3]; 199const outputFile = process.argv[4]; 200writeProtocolFile(outputFile, protocolTs, typeScriptServicesDts); 201