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