-
Notifications
You must be signed in to change notification settings - Fork 622
/
Copy pathCompilerState.ts
201 lines (167 loc) · 7.68 KB
/
CompilerState.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'path';
import * as ts from 'typescript';
import { JsonFile } from '@rushstack/node-core-library';
import { ExtractorConfig } from './ExtractorConfig';
import type { IExtractorInvokeOptions } from './Extractor';
import { Colorize } from '@rushstack/terminal';
/**
* Options for {@link CompilerState.create}
* @public
*/
export interface ICompilerStateCreateOptions {
/** {@inheritDoc IExtractorInvokeOptions.typescriptCompilerFolder} */
typescriptCompilerFolder?: string;
/**
* Additional .d.ts files to include in the analysis.
*/
additionalEntryPoints?: string[];
}
/**
* This class represents the TypeScript compiler state. This allows an optimization where multiple invocations
* of API Extractor can reuse the same TypeScript compiler analysis.
*
* @public
*/
export class CompilerState {
/**
* The TypeScript compiler's `Program` object, which represents a complete scope of analysis.
*/
public readonly program: unknown;
private constructor(properties: CompilerState) {
this.program = properties.program;
}
/**
* Create a compiler state for use with the specified `IExtractorInvokeOptions`.
*/
public static create(
extractorConfig: ExtractorConfig,
options?: ICompilerStateCreateOptions
): CompilerState {
let tsconfig: {} | undefined = extractorConfig.overrideTsconfig;
let configBasePath: string = extractorConfig.projectFolder;
if (!tsconfig) {
// If it wasn't overridden, then load it from disk
tsconfig = JsonFile.load(extractorConfig.tsconfigFilePath);
configBasePath = path.resolve(path.dirname(extractorConfig.tsconfigFilePath));
}
const commandLine: ts.ParsedCommandLine = ts.parseJsonConfigFileContent(tsconfig, ts.sys, configBasePath);
if (!commandLine.options.skipLibCheck && extractorConfig.skipLibCheck) {
commandLine.options.skipLibCheck = true;
console.log(
Colorize.cyan(
'API Extractor was invoked with skipLibCheck. This is not recommended and may cause ' +
'incorrect type analysis.'
)
);
}
const inputFilePaths: string[] = commandLine.fileNames.concat(extractorConfig.mainEntryPointFilePath);
if (options && options.additionalEntryPoints) {
inputFilePaths.push(...options.additionalEntryPoints);
}
// Append the entry points and remove any non-declaration files from the list
const analysisFilePaths: string[] = CompilerState._generateFilePathsForAnalysis(inputFilePaths);
const compilerHost: ts.CompilerHost = CompilerState._createCompilerHost(commandLine, options);
const program: ts.Program = ts.createProgram(analysisFilePaths, commandLine.options, compilerHost);
if (commandLine.errors.length > 0) {
const errorText: string = ts.flattenDiagnosticMessageText(commandLine.errors[0].messageText, '\n');
throw new Error(`Error parsing tsconfig.json content: ${errorText}`);
}
return new CompilerState({
program
});
}
/**
* Given a list of absolute file paths, return a list containing only the declaration
* files. Duplicates are also eliminated.
*
* @remarks
* The tsconfig.json settings specify the compiler's input (a set of *.ts source files,
* plus some *.d.ts declaration files used for legacy typings). However API Extractor
* analyzes the compiler's output (a set of *.d.ts entry point files, plus any legacy
* typings). This requires API Extractor to generate a special file list when it invokes
* the compiler.
*
* Duplicates are removed so that entry points can be appended without worrying whether they
* may already appear in the tsconfig.json file list.
*/
private static _generateFilePathsForAnalysis(inputFilePaths: string[]): string[] {
const analysisFilePaths: string[] = [];
const seenFiles: Set<string> = new Set<string>();
for (const inputFilePath of inputFilePaths) {
const inputFileToUpper: string = inputFilePath.toUpperCase();
if (!seenFiles.has(inputFileToUpper)) {
seenFiles.add(inputFileToUpper);
if (!path.isAbsolute(inputFilePath)) {
throw new Error('Input file is not an absolute path: ' + inputFilePath);
}
if (ExtractorConfig.hasDtsFileExtension(inputFilePath)) {
analysisFilePaths.push(inputFilePath);
}
}
}
return analysisFilePaths;
}
private static _createCompilerHost(
commandLine: ts.ParsedCommandLine,
options: IExtractorInvokeOptions | undefined
): ts.CompilerHost {
// Create a default CompilerHost that we will override
const compilerHost: ts.CompilerHost = ts.createCompilerHost(commandLine.options);
// Save a copy of the original members. Note that "compilerHost" cannot be the copy, because
// createCompilerHost() captures that instance in a closure that is used by the members.
const defaultCompilerHost: ts.CompilerHost = { ...compilerHost };
if (options && options.typescriptCompilerFolder) {
// Prevent a closure parameter
const typescriptCompilerLibFolder: string = path.join(options.typescriptCompilerFolder, 'lib');
compilerHost.getDefaultLibLocation = () => typescriptCompilerLibFolder;
}
// Used by compilerHost.fileExists()
// .d.ts file path --> whether the file exists
const dtsExistsCache: Map<string, boolean> = new Map<string, boolean>();
// Used by compilerHost.fileExists()
// Example: "c:/folder/file.part.ts"
const fileExtensionRegExp: RegExp = /^(.+)(\.[a-z0-9_]+)$/i;
compilerHost.fileExists = (fileName: string): boolean => {
// In certain deprecated setups, the compiler may write its output files (.js and .d.ts)
// in the same folder as the corresponding input file (.ts or .tsx). When following imports,
// API Extractor wants to analyze the .d.ts file; however recent versions of the compiler engine
// will instead choose the .ts file. To work around this, we hook fileExists() to hide the
// existence of those files.
// Is "fileName" a .d.ts file? The double extension ".d.ts" needs to be matched specially.
if (!ExtractorConfig.hasDtsFileExtension(fileName)) {
// It's not a .d.ts file. Is the file extension a potential source file?
const match: RegExpExecArray | null = fileExtensionRegExp.exec(fileName);
if (match) {
// Example: "c:/folder/file.part"
const pathWithoutExtension: string = match[1];
// Example: ".ts"
const fileExtension: string = match[2];
switch (fileExtension.toLocaleLowerCase()) {
case '.ts':
case '.tsx':
case '.js':
case '.jsx':
// Yes, this is a possible source file. Is there a corresponding .d.ts file in the same folder?
const dtsFileName: string = `${pathWithoutExtension}.d.ts`;
let dtsFileExists: boolean | undefined = dtsExistsCache.get(dtsFileName);
if (dtsFileExists === undefined) {
dtsFileExists = defaultCompilerHost.fileExists!(dtsFileName);
dtsExistsCache.set(dtsFileName, dtsFileExists);
}
if (dtsFileExists) {
// fileName is a potential source file and a corresponding .d.ts file exists.
// Thus, API Extractor should ignore this file (so the .d.ts file will get analyzed instead).
return false;
}
break;
}
}
}
// Fall through to the default implementation
return defaultCompilerHost.fileExists!(fileName);
};
return compilerHost;
}
}