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 * as weblog from 'webpack-log'
|
6 | import { toUnix } from './helpers'
|
7 | import { Checker } from './checker'
|
8 | import { CompilerInfo, LoaderConfig, TsConfig } from './interfaces'
|
9 | import { WatchModeSymbol } from './watch-mode'
|
10 | import { createHash } from 'crypto'
|
11 |
|
12 | import { Compiler } from 'webpack'
|
13 |
|
14 | import chalk from 'chalk'
|
15 |
|
16 | const log = weblog({ name: 'atl' })
|
17 |
|
18 | let pkg = require('../package.json')
|
19 | let mkdirp = require('mkdirp')
|
20 | let enhancedResolve = require('enhanced-resolve')
|
21 |
|
22 | export 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 |
|
39 | export interface Compiler {
|
40 | inputFileSystem: typeof fs
|
41 | _tsInstances: { [key: string]: Instance }
|
42 | options: {
|
43 | watch: boolean
|
44 | }
|
45 | }
|
46 |
|
47 | export 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 |
|
68 | export type QueryOptions = LoaderConfig & ts.CompilerOptions
|
69 |
|
70 | export function getRootCompiler(compiler) {
|
71 | if (compiler.parentCompilation) {
|
72 | return getRootCompiler(compiler.parentCompilation.compiler)
|
73 | } else {
|
74 | return compiler
|
75 | }
|
76 | }
|
77 |
|
78 | function resolveInstance(compiler, instanceName): Instance {
|
79 | if (!compiler._tsInstances) {
|
80 | compiler._tsInstances = {}
|
81 | }
|
82 | return compiler._tsInstances[instanceName]
|
83 | }
|
84 |
|
85 | const 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 |
|
89 | const 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 |
|
93 | let id = 0
|
94 | export 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 |
|
177 | function 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 |
|
186 | export 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 |
|
212 | function 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 |
|
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 |
|
245 | const resolver = enhancedResolve.create.sync()
|
246 |
|
247 | function 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 |
|
262 | function 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 |
|
300 | export interface Configs {
|
301 | configFilePath: string
|
302 | compilerConfig: TsConfig
|
303 | loaderConfig: LoaderConfig
|
304 | }
|
305 |
|
306 | function absolutize(fileName: string, context: string) {
|
307 | if (path.isAbsolute(fileName)) {
|
308 | return fileName
|
309 | } else {
|
310 | return path.join(context, fileName)
|
311 | }
|
312 | }
|
313 |
|
314 | export 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 |
|
367 | let EXTENSIONS = /\.tsx?$|\.jsx?$/
|
368 | export type Dict<T> = { [key: string]: T }
|
369 |
|
370 | const 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 |
|
381 | function 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 |
|
441 | enum WatchMode {
|
442 | Enabled,
|
443 | Disabled,
|
444 | Unknown
|
445 | }
|
446 |
|
447 | function 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 |
|
458 | function setupAfterCompile(compiler, instanceName, forkChecker = false) {
|
459 | compiler.hooks.afterCompile.tapAsync('at-loader', function(compilation, callback) {
|
460 |
|
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()
|
518 | return
|
519 | } else {
|
520 | return diag()
|
521 | }
|
522 | })
|
523 | .then(() => callback())
|
524 | .catch(callback)
|
525 | })
|
526 | }
|