• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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