UNPKG

12.9 kBPlain TextView Raw
1import * as fs from 'fs'
2import * as path from 'path'
3import * as _ from 'lodash'
4import * as ts from 'typescript'
5import * as weblog from 'webpack-log'
6import { toUnix } from './helpers'
7import { Checker } from './checker'
8import { CompilerInfo, LoaderConfig, TsConfig } from './interfaces'
9import { WatchModeSymbol } from './watch-mode'
10import { createHash } from 'crypto'
11
12import { Compiler } from 'webpack'
13
14import chalk from 'chalk'
15
16const log = weblog({ name: 'atl' })
17
18let pkg = require('../package.json')
19let mkdirp = require('mkdirp')
20let enhancedResolve = require('enhanced-resolve')
21
22export interface Instance {
23 id: number
24 babelImpl?: any
25 compiledFiles: { [key: string]: boolean }
26 compiledDeclarations: { name: string; text: string }[]
27 configFilePath: string
28 compilerConfig: TsConfig
29 loaderConfig: LoaderConfig
30 checker: Checker
31 cacheIdentifier: string
32 context: string
33
34 times: Dict<number>
35 watchedFiles?: Set<string>
36 startTime?: number
37}
38
39export interface Compiler {
40 inputFileSystem: typeof fs
41 _tsInstances: { [key: string]: Instance }
42 options: {
43 watch: boolean
44 }
45}
46
47export interface Loader {
48 _compiler: Compiler
49 _module: {
50 meta: any
51 }
52 cacheable: () => void
53 query: string
54 async: () => (err: Error, source?: string, map?: string) => void
55 resourcePath: string
56 resolve: () => void
57 addDependency: (dep: string) => void
58 clearDependencies: () => void
59 emitFile: (fileName: string, text: string) => void
60 emitWarning: (msg: Error) => void
61 emitError: (msg: string) => void
62 context: string
63 options: {
64 ts?: LoaderConfig
65 }
66}
67
68export type QueryOptions = LoaderConfig & ts.CompilerOptions
69
70export function getRootCompiler(compiler) {
71 if (compiler.parentCompilation) {
72 return getRootCompiler(compiler.parentCompilation.compiler)
73 } else {
74 return compiler
75 }
76}
77
78function resolveInstance(compiler, instanceName): Instance {
79 if (!compiler._tsInstances) {
80 compiler._tsInstances = {}
81 }
82 return compiler._tsInstances[instanceName]
83}
84
85const COMPILER_ERROR = chalk.red(`\n\nTypescript compiler cannot be found, please add it to your package.json file:
86 npm install --save-dev typescript
87`)
88
89const BABEL_ERROR = chalk.red(`\n\nBabel compiler cannot be found, please add it to your package.json file:
90 npm install --save-dev babel-core
91`)
92
93let id = 0
94export function ensureInstance(
95 webpack: Loader,
96 query: QueryOptions,
97 options: LoaderConfig,
98 instanceName: string,
99 rootCompiler: any
100): Instance {
101 let exInstance = resolveInstance(rootCompiler, instanceName)
102 if (exInstance) {
103 return exInstance
104 }
105
106 const watching = isWatching(rootCompiler)
107 const context = options.context || process.cwd()
108
109 let compilerInfo = setupTs(query.compiler)
110 let { tsImpl } = compilerInfo
111
112 let { configFilePath, compilerConfig, loaderConfig } = readConfigFile(
113 context,
114 query,
115 options,
116 tsImpl
117 )
118
119 applyDefaults(configFilePath, compilerConfig, loaderConfig, context)
120
121 if (!loaderConfig.silent) {
122 const tscVersion = compilerInfo.compilerVersion
123 const tscPath = compilerInfo.compilerPath
124 log.info(`Using typescript@${chalk.bold(tscVersion)} from ${chalk.bold(tscPath)}`)
125
126 const sync = watching === WatchMode.Enabled ? ' (in a forked process)' : ''
127 log.info(`Using ${chalk.bold('tsconfig.json')} from ${chalk.bold(configFilePath)}${sync}`)
128 }
129
130 let babelImpl = setupBabel(loaderConfig, context)
131 let cacheIdentifier = setupCache(
132 compilerConfig,
133 loaderConfig,
134 tsImpl,
135 webpack,
136 babelImpl,
137 context
138 )
139 let compiler = <any>webpack._compiler
140
141 if (!rootCompiler.hooks) {
142 throw new Error(
143 "It looks like you're using an old webpack version without hooks support. " +
144 "If you're using awesome-script-loader with React storybooks consider " +
145 'upgrading @storybook/react to at least version 4.0.0-alpha.3'
146 )
147 }
148
149 setupWatchRun(compiler, instanceName)
150 setupAfterCompile(compiler, instanceName)
151
152 const webpackOptions = _.pick(webpack._compiler.options, 'resolve')
153 const checker = new Checker(
154 compilerInfo,
155 loaderConfig,
156 compilerConfig,
157 webpackOptions,
158 context,
159 watching === WatchMode.Enabled
160 )
161
162 return (rootCompiler._tsInstances[instanceName] = {
163 id: ++id,
164 babelImpl,
165 compiledFiles: {},
166 compiledDeclarations: [],
167 loaderConfig,
168 configFilePath,
169 compilerConfig,
170 checker,
171 cacheIdentifier,
172 context,
173 times: {}
174 })
175}
176
177function findTsImplPackage(inputPath: string) {
178 let pkgDir = path.dirname(inputPath)
179 if (fs.readdirSync(pkgDir).find(value => value === 'package.json')) {
180 return path.join(pkgDir, 'package.json')
181 } else {
182 return findTsImplPackage(pkgDir)
183 }
184}
185
186export function setupTs(compiler: string): CompilerInfo {
187 let compilerPath = compiler || 'typescript'
188
189 let tsImpl: typeof ts
190 let tsImplPath: string
191 try {
192 tsImplPath = require.resolve(compilerPath)
193 tsImpl = require(tsImplPath)
194 } catch (e) {
195 console.error(e)
196 console.error(COMPILER_ERROR)
197 process.exit(1)
198 }
199
200 const pkgPath = findTsImplPackage(tsImplPath)
201 const compilerVersion = require(pkgPath).version
202
203 let compilerInfo: CompilerInfo = {
204 compilerPath,
205 compilerVersion,
206 tsImpl
207 }
208
209 return compilerInfo
210}
211
212function setupCache(
213 compilerConfig: TsConfig,
214 loaderConfig: LoaderConfig,
215 tsImpl: typeof ts,
216 webpack: Loader,
217 babelImpl: any,
218 context: string
219): string {
220 if (loaderConfig.useCache) {
221 if (!loaderConfig.cacheDirectory) {
222 loaderConfig.cacheDirectory = path.join(context, '.awcache')
223 }
224
225 if (!fs.existsSync(loaderConfig.cacheDirectory)) {
226 mkdirp.sync(loaderConfig.cacheDirectory)
227 }
228
229 let hash = createHash('sha512') as any
230 let contents = JSON.stringify({
231 typescript: tsImpl.version,
232 'awesome-typescript-loader': pkg.version,
233 'babel-core': babelImpl ? babelImpl.version : null,
234 babelPkg: pkg.babel,
235 // TODO: babelrc.json/babelrc.js
236 compilerConfig,
237 env: process.env.BABEL_ENV || process.env.NODE_ENV || 'development'
238 })
239
240 hash.end(contents)
241 return hash.read().toString('hex')
242 }
243}
244
245const resolver = enhancedResolve.create.sync()
246
247function setupBabel(loaderConfig: LoaderConfig, context: string): any {
248 let babelImpl: any
249 if (loaderConfig.useBabel) {
250 try {
251 let babelPath = loaderConfig.babelCore || resolver(context, 'babel-core')
252 babelImpl = require(babelPath)
253 } catch (e) {
254 console.error(BABEL_ERROR, e)
255 process.exit(1)
256 }
257 }
258
259 return babelImpl
260}
261
262function applyDefaults(
263 configFilePath: string,
264 compilerConfig: TsConfig,
265 loaderConfig: LoaderConfig,
266 context: string
267) {
268 const def: any = {
269 sourceMap: true,
270 verbose: false,
271 skipDefaultLibCheck: true,
272 suppressOutputPathCheck: true
273 }
274
275 if (compilerConfig.options.outDir && compilerConfig.options.declaration) {
276 def.declarationDir = compilerConfig.options.outDir
277 }
278
279 _.defaults(compilerConfig.options, def)
280
281 if (loaderConfig.transpileOnly) {
282 compilerConfig.options.isolatedModules = true
283 }
284
285 _.defaults(compilerConfig.options, {
286 sourceRoot: compilerConfig.options.sourceMap ? context : undefined
287 })
288
289 _.defaults(loaderConfig, {
290 sourceMap: true,
291 verbose: false
292 })
293
294 delete compilerConfig.options.outDir
295 delete compilerConfig.options.outFile
296 delete compilerConfig.options.out
297 delete compilerConfig.options.noEmit
298}
299
300export interface Configs {
301 configFilePath: string
302 compilerConfig: TsConfig
303 loaderConfig: LoaderConfig
304}
305
306function absolutize(fileName: string, context: string) {
307 if (path.isAbsolute(fileName)) {
308 return fileName
309 } else {
310 return path.join(context, fileName)
311 }
312}
313
314export function readConfigFile(
315 context: string,
316 query: QueryOptions,
317 options: LoaderConfig,
318 tsImpl: typeof ts
319): Configs {
320 let configFilePath: string
321 if (query.configFileName && query.configFileName.match(/\.json$/)) {
322 configFilePath = toUnix(absolutize(query.configFileName, context))
323 } else {
324 configFilePath = tsImpl.findConfigFile(context, tsImpl.sys.fileExists)
325 }
326
327 let existingOptions = tsImpl.convertCompilerOptionsFromJson(query, context, 'atl.query')
328
329 if (!configFilePath || query.configFileContent) {
330 return {
331 configFilePath: configFilePath || toUnix(path.join(context, 'tsconfig.json')),
332 compilerConfig: tsImpl.parseJsonConfigFileContent(
333 query.configFileContent || {},
334 tsImpl.sys,
335 context,
336 _.extend(
337 {},
338 tsImpl.getDefaultCompilerOptions(),
339 existingOptions.options
340 ) as ts.CompilerOptions,
341 context
342 ),
343 loaderConfig: query as LoaderConfig
344 }
345 }
346
347 let jsonConfigFile = tsImpl.readConfigFile(configFilePath, tsImpl.sys.readFile)
348 let compilerConfig = tsImpl.parseJsonConfigFileContent(
349 jsonConfigFile.config,
350 tsImpl.sys,
351 path.dirname(configFilePath),
352 existingOptions.options,
353 configFilePath
354 )
355
356 return {
357 configFilePath,
358 compilerConfig,
359 loaderConfig: _.defaults(
360 query,
361 jsonConfigFile.config.awesomeTypescriptLoaderOptions,
362 options
363 )
364 }
365}
366
367let EXTENSIONS = /\.tsx?$|\.jsx?$/
368export type Dict<T> = { [key: string]: T }
369
370const filterMtimes = (mtimes: any) => {
371 const res = {}
372 Object.keys(mtimes).forEach(fileName => {
373 if (!!EXTENSIONS.test(fileName)) {
374 res[fileName] = mtimes[fileName]
375 }
376 })
377
378 return res
379}
380
381function setupWatchRun(compiler, instanceName: string) {
382 compiler.hooks.watchRun.tapAsync('at-loader', function(compiler, callback) {
383 const instance = resolveInstance(compiler, instanceName)
384 const checker = instance.checker
385 const watcher = compiler.watchFileSystem.watcher || compiler.watchFileSystem.wfs.watcher
386
387 const startTime = instance.startTime || compiler.startTime
388 const times = filterMtimes(watcher.getTimes())
389 const lastCompiled = instance.compiledFiles
390
391 instance.compiledFiles = {}
392 instance.compiledDeclarations = []
393 instance.startTime = startTime
394
395 const set = new Set(Object.keys(times).map(toUnix))
396 if (instance.watchedFiles || lastCompiled) {
397 const removedFiles = []
398 const checkFiles = (instance.watchedFiles || Object.keys(lastCompiled)) as any
399 checkFiles.forEach(file => {
400 if (!set.has(file)) {
401 removedFiles.push(file)
402 }
403 })
404
405 removedFiles.forEach(file => {
406 checker.removeFile(file)
407 })
408 }
409
410 instance.watchedFiles = set
411
412 const instanceTimes = instance.times
413 instance.times = { ...times } as any
414
415 const changedFiles = Object.keys(times).filter(fileName => {
416 const updated = times[fileName] > (instanceTimes[fileName] || startTime)
417 return updated
418 })
419
420 const updates = changedFiles.map(fileName => {
421 const unixFileName = toUnix(fileName)
422 if (fs.existsSync(unixFileName)) {
423 return checker.updateFile(
424 unixFileName,
425 fs.readFileSync(unixFileName).toString(),
426 true
427 )
428 } else {
429 return checker.removeFile(unixFileName)
430 }
431 })
432
433 Promise.all(updates)
434 .then(() => {
435 callback()
436 })
437 .catch(callback)
438 })
439}
440
441enum WatchMode {
442 Enabled,
443 Disabled,
444 Unknown
445}
446
447function isWatching(compiler: any): WatchMode {
448 const value = compiler && compiler[WatchModeSymbol]
449 if (value === true) {
450 return WatchMode.Enabled
451 } else if (value === false) {
452 return WatchMode.Disabled
453 } else {
454 return WatchMode.Unknown
455 }
456}
457
458function setupAfterCompile(compiler, instanceName, forkChecker = false) {
459 compiler.hooks.afterCompile.tapAsync('at-loader', function(compilation, callback) {
460 // Don"t add errors for child compilations
461 if (compilation.compiler.isChild()) {
462 callback()
463 return
464 }
465
466 const watchMode = isWatching(compilation.compiler)
467 const instance: Instance = resolveInstance(compilation.compiler, instanceName)
468 const silent = instance.loaderConfig.silent
469 const asyncErrors = watchMode === WatchMode.Enabled && !silent
470
471 let emitError = msg => {
472 if (asyncErrors) {
473 console.log(msg, '\n')
474 } else {
475 if (!instance.loaderConfig.errorsAsWarnings) {
476 compilation.errors.push(new Error(msg))
477 } else {
478 compilation.warnings.push(new Error(msg))
479 }
480 }
481 }
482
483 const files = instance.checker.getFiles().then(({ files }) => {
484 Array.prototype.push.apply(compilation.fileDependencies, files.map(path.normalize))
485 })
486
487 instance.compiledDeclarations.forEach(declaration => {
488 const assetPath = path.relative(compilation.compiler.outputPath, declaration.name)
489 compilation.assets[assetPath] = {
490 source: () => declaration.text,
491 size: () => declaration.text.length
492 }
493 })
494
495 const timeStart = +new Date()
496 const diag = () =>
497 instance.loaderConfig.transpileOnly
498 ? Promise.resolve()
499 : instance.checker.getDiagnostics().then(diags => {
500 if (!silent) {
501 if (diags.length) {
502 log.error(
503 chalk.red(`Checking finished with ${diags.length} errors`)
504 )
505 } else {
506 let totalTime = (+new Date() - timeStart).toString()
507 log.info(`Time: ${chalk.bold(totalTime)}ms`)
508 }
509 }
510
511 diags.forEach(diag => emitError(diag.pretty))
512 })
513
514 files
515 .then(() => {
516 if (asyncErrors) {
517 diag() // Don"t wait for diags in watch mode
518 return
519 } else {
520 return diag()
521 }
522 })
523 .then(() => callback())
524 .catch(callback)
525 })
526}