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