UNPKG

11.8 kBPlain TextView Raw
1import * as fs from 'fs';
2import * as path from 'path';
3import * as _ from 'lodash';
4import * as ts from 'typescript';
5import { toUnix } from './helpers';
6import { Checker } from './checker';
7import { CompilerInfo, LoaderConfig, TsConfig } from './interfaces';
8import { WatchModeSymbol } from './watch-mode';
9
10let colors = require('colors/safe');
11let pkg = require('../package.json');
12let mkdirp = require('mkdirp');
13
14export interface Instance {
15 id: number;
16 babelImpl?: any;
17 compiledFiles: { [key: string]: boolean };
18 configFilePath: string;
19 compilerConfig: TsConfig;
20 loaderConfig: LoaderConfig;
21 checker: Checker;
22 cacheIdentifier: any;
23 context: string;
24}
25
26export interface Compiler {
27 inputFileSystem: typeof fs;
28 _tsInstances: { [key: string]: Instance };
29 options: {
30 watch: boolean
31 };
32}
33
34export interface Loader {
35 _compiler: Compiler;
36 cacheable: () => void;
37 query: string;
38 async: () => (err: Error, source?: string, map?: string) => void;
39 resourcePath: string;
40 resolve: () => void;
41 addDependency: (dep: string) => void;
42 clearDependencies: () => void;
43 emitFile: (fileName: string, text: string) => void;
44 emitWarning: (msg: string) => void;
45 emitError: (msg: string) => void;
46 options: {
47 ts?: LoaderConfig
48 };
49}
50
51export type QueryOptions = LoaderConfig & ts.CompilerOptions;
52
53export function getRootCompiler(compiler) {
54 if (compiler.parentCompilation) {
55 return getRootCompiler(compiler.parentCompilation.compiler);
56 } else {
57 return compiler;
58 }
59}
60
61function resolveInstance(compiler, instanceName) {
62 if (!compiler._tsInstances) {
63 compiler._tsInstances = {};
64 }
65 return compiler._tsInstances[instanceName];
66}
67
68const COMPILER_ERROR = colors.red(`\n\nTypescript compiler cannot be found, please add it to your package.json file:
69 npm install --save-dev typescript
70`);
71
72const BABEL_ERROR = colors.red(`\n\nBabel compiler cannot be found, please add it to your package.json file:
73 npm install --save-dev babel-core
74`);
75
76let id = 0;
77export function ensureInstance(
78 webpack: Loader,
79 query: QueryOptions,
80 options: LoaderConfig,
81 instanceName: string,
82 rootCompiler: any
83): Instance {
84 let exInstance = resolveInstance(rootCompiler, instanceName);
85 if (exInstance) {
86 return exInstance;
87 }
88
89 const watching = isWatching(rootCompiler);
90 const context = process.cwd();
91
92 let compilerInfo = setupTs(query.compiler);
93 let { tsImpl } = compilerInfo;
94
95 let { configFilePath, compilerConfig, loaderConfig } = readConfigFile(
96 context,
97 query,
98 options,
99 tsImpl
100 );
101
102 applyDefaults(
103 configFilePath,
104 compilerConfig,
105 loaderConfig,
106 context
107 );
108
109 if (!loaderConfig.silent) {
110 const sync = watching === WatchMode.Enabled ? ' (in a forked process)' : '';
111 console.log(`\n[${instanceName}] Using typescript@${compilerInfo.compilerVersion} from ${compilerInfo.compilerPath} and `
112 + `"tsconfig.json" from ${configFilePath}${sync}.\n`);
113 }
114
115 let babelImpl = setupBabel(loaderConfig, context);
116 let cacheIdentifier = setupCache(
117 loaderConfig,
118 tsImpl,
119 webpack,
120 babelImpl,
121 context
122 );
123 let compiler = (<any>webpack._compiler);
124
125 setupWatchRun(compiler, instanceName);
126 setupAfterCompile(compiler, instanceName);
127
128 const webpackOptions = _.pick(webpack._compiler.options, 'resolve');
129 const checker = new Checker(
130 compilerInfo,
131 loaderConfig,
132 compilerConfig,
133 webpackOptions,
134 context,
135 watching === WatchMode.Enabled
136 );
137
138 return rootCompiler._tsInstances[instanceName] = {
139 id: ++id,
140 babelImpl,
141 compiledFiles: {},
142 loaderConfig,
143 configFilePath,
144 compilerConfig,
145 checker,
146 cacheIdentifier,
147 context
148 };
149}
150
151function findTsImplPackage(inputPath: string) {
152 let pkgDir = path.dirname(inputPath);
153 if (fs.readdirSync(pkgDir).find((value) => value === 'package.json')) {
154 return path.join(pkgDir, 'package.json');
155 } else {
156 return findTsImplPackage(pkgDir);
157 }
158}
159
160export function setupTs(compiler: string): CompilerInfo {
161 let compilerPath = compiler || 'typescript';
162
163 let tsImpl: typeof ts;
164 let tsImplPath: string;
165 try {
166 tsImplPath = require.resolve(compilerPath);
167 tsImpl = require(tsImplPath);
168 } catch (e) {
169 console.error(e);
170 console.error(COMPILER_ERROR);
171 process.exit(1);
172 }
173
174 const pkgPath = findTsImplPackage(tsImplPath);
175 const compilerVersion = require(pkgPath).version;
176
177 let compilerInfo: CompilerInfo = {
178 compilerPath,
179 compilerVersion,
180 tsImpl,
181 };
182
183 return compilerInfo;
184}
185
186function setupCache(
187 loaderConfig: LoaderConfig,
188 tsImpl: typeof ts,
189 webpack: Loader,
190 babelImpl: any,
191 context: string
192) {
193 let cacheIdentifier = null;
194 if (loaderConfig.useCache) {
195 if (!loaderConfig.cacheDirectory) {
196 loaderConfig.cacheDirectory = path.join(context, '.awcache');
197 }
198
199 if (!fs.existsSync(loaderConfig.cacheDirectory)) {
200 mkdirp.sync(loaderConfig.cacheDirectory);
201 }
202
203 cacheIdentifier = {
204 'typescript': tsImpl.version,
205 'awesome-typescript-loader': pkg.version,
206 'awesome-typescript-loader-query': webpack.query,
207 'babel-core': babelImpl
208 ? babelImpl.version
209 : null
210 };
211 }
212}
213
214function setupBabel(loaderConfig: LoaderConfig, context: string): any {
215 let babelImpl: any;
216 if (loaderConfig.useBabel) {
217 try {
218 let babelPath = loaderConfig.babelCore || path.join(context, 'node_modules', 'babel-core');
219 babelImpl = require(babelPath);
220 } catch (e) {
221 console.error(BABEL_ERROR);
222 process.exit(1);
223 }
224 }
225
226 return babelImpl;
227}
228
229function applyDefaults(
230 configFilePath: string,
231 compilerConfig: TsConfig,
232 loaderConfig: LoaderConfig,
233 context: string
234) {
235 _.defaults(compilerConfig.options, {
236 sourceMap: true,
237 verbose: false,
238 skipDefaultLibCheck: true,
239 suppressOutputPathCheck: true
240 });
241
242 if (loaderConfig.transpileOnly) {
243 compilerConfig.options.isolatedModules = true;
244 }
245
246 _.defaults(compilerConfig.options, {
247 sourceRoot: compilerConfig.options.sourceMap ? context : undefined
248 });
249
250 _.defaults(loaderConfig, {
251 sourceMap: true,
252 verbose: false,
253 });
254
255 delete compilerConfig.options.outDir;
256 delete compilerConfig.options.outFile;
257 delete compilerConfig.options.out;
258 delete compilerConfig.options.noEmit;
259}
260
261export interface Configs {
262 configFilePath: string;
263 compilerConfig: TsConfig;
264 loaderConfig: LoaderConfig;
265}
266
267function absolutize(fileName: string, context: string) {
268 if (path.isAbsolute(fileName)) {
269 return fileName;
270 } else {
271 return path.join(context, fileName);
272 }
273}
274
275export function readConfigFile(
276 context: string,
277 query: QueryOptions,
278 options: LoaderConfig,
279 tsImpl: typeof ts
280): Configs {
281 let configFilePath: string;
282 if (query.configFileName && query.configFileName.match(/\.json$/)) {
283 configFilePath = absolutize(query.configFileName, context);
284 } else {
285 configFilePath = tsImpl.findConfigFile(context, tsImpl.sys.fileExists);
286 }
287
288 let existingOptions = tsImpl.convertCompilerOptionsFromJson(query, context, 'atl.query');
289
290 if (!configFilePath || query.configFileContent) {
291 return {
292 configFilePath: configFilePath || path.join(context, 'tsconfig.json'),
293 compilerConfig: tsImpl.parseJsonConfigFileContent(
294 query.configFileContent || {},
295 tsImpl.sys,
296 context,
297 _.extend({}, tsImpl.getDefaultCompilerOptions(), existingOptions.options) as ts.CompilerOptions,
298 context
299 ),
300 loaderConfig: query as LoaderConfig
301 };
302 }
303
304 let jsonConfigFile = tsImpl.readConfigFile(configFilePath, tsImpl.sys.readFile);
305 let compilerConfig = tsImpl.parseJsonConfigFileContent(
306 jsonConfigFile.config,
307 tsImpl.sys,
308 path.dirname(configFilePath),
309 existingOptions.options,
310 configFilePath
311 );
312
313 return {
314 configFilePath,
315 compilerConfig,
316 loaderConfig: _.defaults(
317 query,
318 jsonConfigFile.config.awesomeTypescriptLoaderOptions,
319 options
320 )
321 };
322}
323
324let EXTENSIONS = /\.tsx?$|\.jsx?$/;
325
326function setupWatchRun(compiler, instanceName: string) {
327 compiler.plugin('watch-run', function (watching, callback) {
328 const instance = resolveInstance(watching.compiler, instanceName);
329 const checker = instance.checker;
330 const watcher = watching.compiler.watchFileSystem.watcher
331 || watching.compiler.watchFileSystem.wfs.watcher;
332
333 const mtimes = watcher.mtimes || {};
334 const changedFiles = Object.keys(mtimes).map(toUnix);
335 const updates = changedFiles
336 .filter(file => EXTENSIONS.test(file))
337 .map(changedFile => {
338 if (fs.existsSync(changedFile)) {
339 checker.updateFile(changedFile, fs.readFileSync(changedFile).toString());
340 } else {
341 checker.removeFile(changedFile);
342 }
343 });
344
345 Promise.all(updates)
346 .then(() => callback())
347 .catch(callback);
348 });
349}
350
351enum WatchMode {
352 Enabled,
353 Disabled,
354 Unknown
355}
356
357function isWatching(compiler: any): WatchMode {
358 const value = compiler && compiler[WatchModeSymbol];
359 if (value === true) {
360 return WatchMode.Enabled;
361 } else if (value === false) {
362 return WatchMode.Disabled;
363 } else {
364 return WatchMode.Unknown;
365 }
366}
367
368function setupAfterCompile(compiler, instanceName, forkChecker = false) {
369 compiler.plugin('after-compile', function (compilation, callback) {
370 // Don't add errors for child compilations
371 if (compilation.compiler.isChild()) {
372 callback();
373 return;
374 }
375
376 const watchMode = isWatching(compilation.compiler);
377 const instance: Instance = resolveInstance(compilation.compiler, instanceName);
378 const silent = instance.loaderConfig.silent;
379 const asyncErrors = watchMode === WatchMode.Enabled && !silent;
380
381 let emitError = (msg) => {
382 if (compilation.bail) {
383 console.error('Error in bail mode:', msg);
384 process.exit(1);
385 }
386
387 if (asyncErrors) {
388 console.log(msg, '\n');
389 } else {
390 compilation.errors.push(new Error(msg));
391 }
392 };
393
394 instance.compiledFiles = {};
395 const files = instance.checker.getFiles()
396 .then(({files}) => {
397 Array.prototype.push.apply(compilation.fileDependencies, files.map(path.normalize));
398 });
399
400 const diag = instance.loaderConfig.transpileOnly
401 ? Promise.resolve()
402 : instance.checker.getDiagnostics()
403 .then(diags => {
404 diags.forEach(diag => emitError(diag.pretty));
405 });
406
407 files
408 .then(() => {
409 if (asyncErrors) {
410 // Don't wait for diags in watch mode
411 return;
412 } else {
413 return diag;
414 }
415 })
416 .then(() => callback())
417 .catch(callback);
418 });
419}