forked from barbatus/meteor-typescript
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
288 lines (239 loc) · 8.47 KB
/
index.js
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
'use strict';
import assert from "assert";
import ts from "typescript";
import _ from "underscore";
import {
getDefaultCompilerOptions,
convertCompilerOptionsOrThrow,
validateTsConfig,
presetCompilerOptions,
} from "./options";
import CompileService, { createCSResult } from "./compile-service";
import ServiceHost from "./compile-service-host";
import sourceHost from "./files-source-host";
import { CompileCache, FileHashCache } from "./cache";
import logger from "./logger";
import { deepHash } from "./utils";
import { getExcludeRegExp } from "./ts-utils";
import { RefsChangeType, evalRefsChangeMap } from './refs';
let compileCache, fileHashCache;
export function setCacheDir(cacheDir) {
if (compileCache && compileCache.cacheDir === cacheDir) {
return;
}
compileCache = new CompileCache(cacheDir);
fileHashCache = new FileHashCache(cacheDir);
}
function getConvertedDefault(arch) {
return convertCompilerOptionsOrThrow(
getDefaultCompilerOptions(arch));
}
function isES6Target(target) {
return /es6/i.test(target) || /es2015/i.test(target);
}
function evalCompilerOptions(arch, opt) {
const defOpt = getDefaultCompilerOptions(arch);
const resOpt = opt || defOpt;
_.defaults(resOpt, defOpt);
// Add target to the lib since
// if target: "es6" and lib: ["es5"],
// it won't compile properly.
if (resOpt.target) {
resOpt.lib.push(resOpt.target);
}
resOpt.lib = _.union(resOpt.lib, defOpt.lib);
// Impose use strict for ES6 target.
if (opt && opt.noImplicitUseStrict !== undefined) {
if (isES6Target(resOpt.target)) {
resOpt.noImplicitUseStrict = false;
}
}
return resOpt;
}
function lazyInit() {
if (! compileCache) {
setCacheDir();
}
}
// A map of TypeScript Language Services
// per each Meteor architecture.
const serviceMap = {};
function getCompileService(arch) {
if (! arch) arch = "global";
if (serviceMap[arch]) return serviceMap[arch];
const serviceHost = new ServiceHost(fileHashCache);
const service = new CompileService(serviceHost);
serviceMap[arch] = service;
return service;
}
/**
* Class that represents an incremental TypeScript build (compilation).
* For the typical usage in a Meteor compiler plugin,
* see a TypeScript compiler that based on this NPM:
* https://github.com/barbatus/typescript-compiler/blob/master/typescript-compiler.js#L58
*
* @param filePaths Paths of the files to compile.
* @param getFileContent Method that takes a file path
* and returns that file's content. To be used to pass file contents
* from a Meteor compiler plugin to the TypeScript compiler.
* @param options Object with the options of the TypeSctipt build.
* Available options:
* - compilerOptions: TypeScript compiler options
* - arch: Meteor file architecture
* - useCache: whether to use cache
*/
export class TSBuild {
constructor(filePaths, getFileContent, options = {}) {
logger.debug("new build");
const compilerOptions = evalCompilerOptions(
options.arch, options.compilerOptions);
let resOptions = { ...options, compilerOptions };
resOptions = validateAndConvertOptions(resOptions);
resOptions.compilerOptions = presetCompilerOptions(
resOptions.compilerOptions);
this.options = resOptions;
lazyInit();
sourceHost.setSource(getFileContent);
const pset = logger.newProfiler("set files");
const compileService = getCompileService(resOptions.arch);
const serviceHost = compileService.getHost();
serviceHost.setFiles(filePaths, resOptions);
pset.end();
const prefs = logger.newProfiler("refs eval");
this.refsChangeMap = evalRefsChangeMap(filePaths,
(filePath) => serviceHost.isFileChanged(filePath),
(filePath) => {
const csResult = compileCache.getResult(filePath,
this.getFileOptions(filePath));
return csResult ? csResult.dependencies : null;
}, resOptions.evalDepth || 1);
prefs.end();
}
getFileOptions(filePath) {
// Omit arch to avoid re-compiling same files aimed for diff arch.
// Prepare file options which besides general ones
// should contain a module name.
const options = _.omit(this.options, "arch", "useCache", "evalDepth");
const module = options.compilerOptions.module;
const moduleName = module === "none" ? null :
ts.removeFileExtension(filePath);
return { options, moduleName };
}
emit(filePath) {
logger.debug("emit file %s", filePath);
const options = this.options;
const compileService = getCompileService(options.arch);
const serviceHost = compileService.getHost();
if (!serviceHost.hasFile(filePath)) {
throw new Error(`File ${filePath} not found`);
}
const csOptions = this.getFileOptions(filePath);
function compile() {
const pcomp = logger.newProfiler(`compile ${filePath}`);
const result = compileService.compile(filePath, csOptions.moduleName);
pcomp.end();
return result;
}
const useCache = options.useCache;
if (useCache === false) {
return compile();
}
const isTypingsChanged = serviceHost.isTypingsChanged();
const pget = logger.newProfiler("compileCache get");
const result = compileCache.get(filePath, csOptions, (cacheResult) => {
if (!cacheResult) {
logger.debug("cache miss: %s", filePath);
return compile();
}
const refsChange = this.refsChangeMap[filePath];
// Referenced files have changed, which may need recompilation in some cases.
// See https://github.com/Urigo/angular2-meteor/issues/102#issuecomment-191411701
if (refsChange === RefsChangeType.FILES) {
logger.debug("recompile: %s", filePath);
return compile();
}
// Diagnostics re-evaluation.
// First case: file is not changed but contains unresolved modules
// error from previous build (some node modules might have installed).
// Second case: dependency modules or typings have changed.
const csResult = createCSResult(filePath, cacheResult);
const tsDiag = csResult.diagnostics;
const unresolved = tsDiag.hasUnresolvedModules();
if (unresolved || refsChange !== RefsChangeType.NONE || isTypingsChanged) {
logger.debug("diagnostics re-evaluation: %s", filePath);
const pdiag = logger.newProfiler("diags update");
csResult.upDiagnostics(
compileService.getDiagnostics(filePath));
pdiag.end();
return csResult;
}
// Cached result is up to date, no action required.
logger.debug("file from cached: %s", filePath);
return null;
});
pget.end();
return result;
}
}
export function compile(fileContent, options = {}) {
if (typeof fileContent !== "string") {
throw new Error("fileContent should be a string");
}
let optPath = options.filePath;
if (!optPath) {
optPath = deepHash(fileContent, options);
const tsx = options.compilerOptions && options.compilerOptions.jsx;
optPath += tsx ? ".tsx" : ".ts";
}
const getFileContent = (filePath) => {
if (filePath === optPath) {
return fileContent;
}
};
const newBuild = new TSBuild([optPath], getFileContent, options);
return newBuild.emit(optPath);
};
const validOptions = {
"compilerOptions": "Object",
// Next three to be used mainly
// in the compile method above.
"filePath": "String",
"typings": "Array",
"arch": "String",
"useCache": "Boolean",
"evalDepth": "Number",
};
const validOptionsMsg =
"Valid options are compilerOptions, filePath, and typings.";
function checkType(option, optionName) {
if (!option) return true;
return option.constructor.name === validOptions[optionName];
}
export function validateAndConvertOptions(options) {
if (!options) return null;
// Validate top level options.
for (const option in options) {
if (options.hasOwnProperty(option)) {
if (validOptions[option] === undefined) {
throw new Error(`Unknown option: ${option}.\n${validOptionsMsg}`);
}
if (! checkType(options[option], option)) {
throw new Error(`${option} should be of type ${validOptions[option]}`);
}
}
}
const resOptions = _.clone(options);
// Validate and convert compilerOptions.
if (options.compilerOptions) {
resOptions.compilerOptions = convertCompilerOptionsOrThrow(
options.compilerOptions);
}
return resOptions;
}
export function getDefaultOptions(arch) {
return {
compilerOptions: getDefaultCompilerOptions(arch),
};
}
exports.validateTsConfig = validateTsConfig;
exports.getExcludeRegExp = getExcludeRegExp;