1 | import * as fs from 'fs';
|
2 | import * as path from 'path';
|
3 | import * as _ from 'lodash';
|
4 | import * as ts from 'typescript';
|
5 | import { toUnix } from './helpers';
|
6 | import { Checker } from './checker';
|
7 | import { CompilerInfo, LoaderConfig, TsConfig } from './interfaces';
|
8 | import { WatchModeSymbol } from './watch-mode';
|
9 |
|
10 | let colors = require('colors/safe');
|
11 | let pkg = require('../package.json');
|
12 | let mkdirp = require('mkdirp');
|
13 |
|
14 | export 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 |
|
26 | export interface Compiler {
|
27 | inputFileSystem: typeof fs;
|
28 | _tsInstances: { [key: string]: Instance };
|
29 | options: {
|
30 | watch: boolean
|
31 | };
|
32 | }
|
33 |
|
34 | export 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 |
|
51 | export type QueryOptions = LoaderConfig & ts.CompilerOptions;
|
52 |
|
53 | export function getRootCompiler(compiler) {
|
54 | if (compiler.parentCompilation) {
|
55 | return getRootCompiler(compiler.parentCompilation.compiler);
|
56 | } else {
|
57 | return compiler;
|
58 | }
|
59 | }
|
60 |
|
61 | function resolveInstance(compiler, instanceName) {
|
62 | if (!compiler._tsInstances) {
|
63 | compiler._tsInstances = {};
|
64 | }
|
65 | return compiler._tsInstances[instanceName];
|
66 | }
|
67 |
|
68 | const 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 |
|
72 | const 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 |
|
76 | let id = 0;
|
77 | export 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 |
|
151 | function 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 |
|
160 | export 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 |
|
186 | function 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 |
|
214 | function 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 |
|
229 | function 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 |
|
261 | export interface Configs {
|
262 | configFilePath: string;
|
263 | compilerConfig: TsConfig;
|
264 | loaderConfig: LoaderConfig;
|
265 | }
|
266 |
|
267 | function absolutize(fileName: string, context: string) {
|
268 | if (path.isAbsolute(fileName)) {
|
269 | return fileName;
|
270 | } else {
|
271 | return path.join(context, fileName);
|
272 | }
|
273 | }
|
274 |
|
275 | export 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 |
|
324 | let EXTENSIONS = /\.tsx?$|\.jsx?$/;
|
325 |
|
326 | function 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 |
|
351 | enum WatchMode {
|
352 | Enabled,
|
353 | Disabled,
|
354 | Unknown
|
355 | }
|
356 |
|
357 | function 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 |
|
368 | function setupAfterCompile(compiler, instanceName, forkChecker = false) {
|
369 | compiler.plugin('after-compile', function (compilation, callback) {
|
370 |
|
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 |
|
411 | return;
|
412 | } else {
|
413 | return diag;
|
414 | }
|
415 | })
|
416 | .then(() => callback())
|
417 | .catch(callback);
|
418 | });
|
419 | }
|