-
-
Notifications
You must be signed in to change notification settings - Fork 600
/
Copy pathwatchProgram.ts
219 lines (193 loc) · 6.32 KB
/
watchProgram.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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import type { PluginContext } from 'rollup';
import typescript from 'typescript';
import type {
CustomTransformers,
Diagnostic,
EmitAndSemanticDiagnosticsBuilderProgram,
ParsedCommandLine,
Program,
WatchCompilerHostOfFilesAndCompilerOptions,
WatchStatusReporter,
WriteFileCallback
} from 'typescript';
import type { CustomTransformerFactories } from '../types';
import { buildDiagnosticReporter } from './diagnostics/emit';
import type { DiagnosticsHost } from './diagnostics/host';
import type { Resolver } from './moduleResolution';
import { mergeTransformers } from './customTransformers';
const { DiagnosticCategory } = typescript;
type BuilderProgram = EmitAndSemanticDiagnosticsBuilderProgram;
// @see https://github.com/microsoft/TypeScript/blob/master/src/compiler/diagnosticMessages.json
// eslint-disable-next-line no-shadow
enum DiagnosticCode {
FILE_CHANGE_DETECTED = 6032,
FOUND_1_ERROR_WATCHING_FOR_FILE_CHANGES = 6193,
FOUND_N_ERRORS_WATCHING_FOR_FILE_CHANGES = 6194
}
interface CreateProgramOptions {
/** Formatting host used to get some system functions and emit type errors. */
formatHost: DiagnosticsHost;
/** Parsed Typescript compiler options. */
parsedOptions: ParsedCommandLine;
/** Callback to save compiled files in memory. */
writeFile: WriteFileCallback;
/** Callback for the Typescript status reporter. */
status: WatchStatusReporter;
/** Function to resolve a module location */
resolveModule: Resolver;
/** Custom TypeScript transformers */
transformers?: CustomTransformerFactories | ((program: Program) => CustomTransformers);
}
type DeferredResolve = ((value: boolean | PromiseLike<boolean>) => void) | (() => void);
interface Deferred {
promise: Promise<boolean | void>;
resolve: DeferredResolve;
}
function createDeferred(timeout?: number): Deferred {
let promise: Promise<boolean | void>;
let resolve: DeferredResolve = () => {};
if (timeout) {
promise = Promise.race<boolean | void>([
new Promise((r) => setTimeout(r, timeout, true)),
new Promise((r) => (resolve = r))
]);
} else {
promise = new Promise((r) => (resolve = r));
}
return { promise, resolve };
}
/**
* Typescript watch program helper to sync Typescript watch status with Rollup hooks.
*/
export class WatchProgramHelper {
private _startDeferred: Deferred | null = null;
private _finishDeferred: Deferred | null = null;
watch(timeout = 1000) {
// Race watcher start promise against a timeout in case Typescript and Rollup change detection is not in sync.
this._startDeferred = createDeferred(timeout);
this._finishDeferred = createDeferred();
}
handleStatus(diagnostic: Diagnostic) {
// Fullfil deferred promises by Typescript diagnostic message codes.
if (diagnostic.category === DiagnosticCategory.Message) {
switch (diagnostic.code) {
case DiagnosticCode.FILE_CHANGE_DETECTED:
this.resolveStart();
break;
case DiagnosticCode.FOUND_1_ERROR_WATCHING_FOR_FILE_CHANGES:
case DiagnosticCode.FOUND_N_ERRORS_WATCHING_FOR_FILE_CHANGES:
this.resolveFinish();
break;
default:
}
}
}
resolveStart() {
if (this._startDeferred) {
this._startDeferred.resolve(false);
this._startDeferred = null;
}
}
resolveFinish() {
if (this._finishDeferred) {
this._finishDeferred.resolve(false);
this._finishDeferred = null;
}
}
async wait() {
if (this._startDeferred) {
const timeout = await this._startDeferred.promise;
// If there is no file change detected by Typescript skip deferred promises.
if (timeout) {
this._startDeferred = null;
this._finishDeferred = null;
}
await this._finishDeferred?.promise;
}
}
}
/**
* Create a language service host to use with the Typescript compiler & type checking APIs.
* Typescript hosts are used to represent the user's system,
* with an API for reading files, checking directories and case sensitivity etc.
* @see https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
*/
function createWatchHost(
ts: typeof typescript,
context: PluginContext,
{
formatHost,
parsedOptions,
writeFile,
status,
resolveModule,
transformers
}: CreateProgramOptions
): WatchCompilerHostOfFilesAndCompilerOptions<BuilderProgram> {
const createProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram;
const baseHost = ts.createWatchCompilerHost(
parsedOptions.fileNames,
parsedOptions.options,
ts.sys,
createProgram,
buildDiagnosticReporter(ts, context, formatHost),
status,
parsedOptions.projectReferences
);
let createdTransformers: CustomTransformers | undefined;
return {
...baseHost,
/** Override the created program so an in-memory emit is used */
afterProgramCreate(program) {
const origEmit = program.emit;
// eslint-disable-next-line no-param-reassign
program.emit = (
targetSourceFile,
_,
cancellationToken,
emitOnlyDtsFiles,
customTransformers
) => {
createdTransformers ??=
typeof transformers === 'function'
? transformers(program.getProgram())
: mergeTransformers(
program,
transformers,
customTransformers as CustomTransformerFactories
);
return origEmit(
targetSourceFile,
writeFile,
cancellationToken,
emitOnlyDtsFiles,
createdTransformers
);
};
return baseHost.afterProgramCreate!(program);
},
/** Add helper to deal with module resolution */
resolveModuleNames(
moduleNames,
containingFile,
_reusedNames,
redirectedReference,
_optionsOnlyWithNewerTsVersions,
containingSourceFile
) {
return moduleNames.map((moduleName, i) => {
const mode = containingSourceFile
? ts.getModeForResolutionAtIndex?.(containingSourceFile, i)
: undefined; // eslint-disable-line no-undefined
return resolveModule(moduleName, containingFile, redirectedReference, mode);
});
}
};
}
export default function createWatchProgram(
ts: typeof typescript,
context: PluginContext,
options: CreateProgramOptions
) {
return ts.createWatchProgram(createWatchHost(ts, context, options));
}