diff --git a/CHANGELOG.md b/CHANGELOG.md index 409927b44..08b0ea398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v1.1.1 - NOT RELEASED YET + +- [Crash when adding/removing files in watch-mode](https://github.com/TypeStrong/ts-loader/pull/364) [#358] - thanks @jbbr for the suggested fix + ## v1.1.0 - [Added support for vuejs via `appendTsSuffixTo` option](https://github.com/TypeStrong/ts-loader/pull/354) [#270] - thanks @HerringtonDarkholme diff --git a/src/after-compile.ts b/src/after-compile.ts index cd472c4b8..27f47b0a1 100644 --- a/src/after-compile.ts +++ b/src/after-compile.ts @@ -1,13 +1,12 @@ import interfaces = require('./interfaces'); import path = require('path'); +import typescript = require('typescript'); import utils = require('./utils'); function makeAfterCompile( instance: interfaces.TSInstance, configFilePath: string ) { - const { compiler, languageService } = instance; - let getCompilerOptionDiagnostics = true; let checkAllFilesForErrors = true; @@ -20,103 +19,18 @@ function makeAfterCompile( removeTSLoaderErrors(compilation.errors); - // handle compiler option errors after the first compile - if (getCompilerOptionDiagnostics) { - getCompilerOptionDiagnostics = false; - utils.registerWebpackErrors( - compilation.errors, - utils.formatErrors(languageService.getCompilerOptionsDiagnostics(), - instance.loaderOptions, - compiler, - { file: configFilePath || 'tsconfig.json' })); - } + provideCompilerOptionDiagnosticErrorsToWebpack(getCompilerOptionDiagnostics, compilation, instance, configFilePath); + getCompilerOptionDiagnostics = false; - // build map of all modules based on normalized filename - // this is used for quick-lookup when trying to find modules - // based on filepath - const modules: { [modulePath: string]: interfaces.WebpackModule[] } = {}; - compilation.modules.forEach(module => { - if (module.resource) { - const modulePath = path.normalize(module.resource); - if (utils.hasOwnProperty(modules, modulePath)) { - const existingModules = modules[modulePath]; - if (existingModules.indexOf(module) === -1) { - existingModules.push(module); - } - } else { - modules[modulePath] = [module]; - } - } - }); + const modules = determineModules(compilation); - // gather all errors from TypeScript and output them to webpack - let filesWithErrors: interfaces.TSFiles = {}; - // calculate array of files to check - let filesToCheckForErrors: interfaces.TSFiles = null; - if (checkAllFilesForErrors) { - // check all files on initial run - filesToCheckForErrors = instance.files; - checkAllFilesForErrors = false; - } else { - filesToCheckForErrors = {}; - // check all modified files, and all dependants - Object.keys(instance.modifiedFiles).forEach(modifiedFileName => { - collectAllDependants(instance, modifiedFileName).forEach(fName => { - filesToCheckForErrors[fName] = instance.files[fName]; - }); - }); - } - // re-check files with errors from previous build - if (instance.filesWithErrors) { - Object.keys(instance.filesWithErrors).forEach(fileWithErrorName => - filesToCheckForErrors[fileWithErrorName] = instance.filesWithErrors[fileWithErrorName] - ); - } + const filesToCheckForErrors = determineFilesToCheckForErrors(checkAllFilesForErrors, instance); + checkAllFilesForErrors = false; - Object.keys(filesToCheckForErrors) - .filter(filePath => !!filePath.match(/(\.d)?\.ts(x?)$/)) - .forEach(filePath => { - const errors = languageService.getSyntacticDiagnostics(filePath).concat(languageService.getSemanticDiagnostics(filePath)); - if (errors.length > 0) { - if (null === filesWithErrors) { - filesWithErrors = {}; - } - filesWithErrors[filePath] = instance.files[filePath]; - } + const filesWithErrors: interfaces.TSFiles = {}; + provideErrorsToWebpack(filesToCheckForErrors, filesWithErrors, compilation, modules, instance); - // if we have access to a webpack module, use that - if (utils.hasOwnProperty(modules, filePath)) { - const associatedModules = modules[filePath]; - - associatedModules.forEach(module => { - // remove any existing errors - removeTSLoaderErrors(module.errors); - - // append errors - const formattedErrors = utils.formatErrors(errors, instance.loaderOptions, compiler, { module }); - utils.registerWebpackErrors(module.errors, formattedErrors); - utils.registerWebpackErrors(compilation.errors, formattedErrors); - }); - } else { - // otherwise it's a more generic error - utils.registerWebpackErrors(compilation.errors, utils.formatErrors(errors, instance.loaderOptions, compiler, { file: filePath })); - } - }); - - // gather all declaration files from TypeScript and output them to webpack - Object.keys(filesToCheckForErrors) - .filter(filePath => !!filePath.match(/\.ts(x?)$/)) - .forEach(filePath => { - const output = languageService.getEmitOutput(filePath); - const declarationFile = output.outputFiles.filter(fp => !!fp.name.match(/\.d.ts$/)).pop(); - if (declarationFile) { - const assetPath = path.relative(compilation.compiler.context, declarationFile.name); - compilation.assets[assetPath] = { - source: () => declarationFile.text, - size: () => declarationFile.text.length, - }; - } - }); + provideDeclarationFilesToWebpack(filesToCheckForErrors, instance.languageService, compilation); instance.filesWithErrors = filesWithErrors; instance.modifiedFiles = null; @@ -124,6 +38,143 @@ function makeAfterCompile( }; } +interface Modules { + [modulePath: string]: interfaces.WebpackModule[]; +} + +/** + * handle compiler option errors after the first compile + */ +function provideCompilerOptionDiagnosticErrorsToWebpack( + getCompilerOptionDiagnostics: boolean, + compilation: interfaces.WebpackCompilation, + instance: interfaces.TSInstance, + configFilePath: string +) { + const { languageService, loaderOptions, compiler } = instance; + if (getCompilerOptionDiagnostics) { + utils.registerWebpackErrors( + compilation.errors, + utils.formatErrors( + languageService.getCompilerOptionsDiagnostics(), + loaderOptions, compiler, + { file: configFilePath || 'tsconfig.json' })); + } +} + +/** + * build map of all modules based on normalized filename + * this is used for quick-lookup when trying to find modules + * based on filepath + */ +function determineModules( + compilation: interfaces.WebpackCompilation +) { + const modules: Modules = {}; + compilation.modules.forEach(module => { + if (module.resource) { + const modulePath = path.normalize(module.resource); + if (utils.hasOwnProperty(modules, modulePath)) { + const existingModules = modules[modulePath]; + if (existingModules.indexOf(module) === -1) { + existingModules.push(module); + } + } else { + modules[modulePath] = [module]; + } + } + }); + return modules; +} + +function determineFilesToCheckForErrors( + checkAllFilesForErrors: boolean, + instance: interfaces.TSInstance +) { + const { files, modifiedFiles, filesWithErrors } = instance + // calculate array of files to check + let filesToCheckForErrors: interfaces.TSFiles = {}; + if (checkAllFilesForErrors) { + // check all files on initial run + filesToCheckForErrors = files; + } else if (modifiedFiles) { + // check all modified files, and all dependants + Object.keys(modifiedFiles).forEach(modifiedFileName => { + collectAllDependants(instance.reverseDependencyGraph, modifiedFileName) + .forEach(fileName => { + filesToCheckForErrors[fileName] = files[fileName]; + }); + }); + } + + // re-check files with errors from previous build + if (filesWithErrors) { + Object.keys(filesWithErrors).forEach(fileWithErrorName => + filesToCheckForErrors[fileWithErrorName] = filesWithErrors[fileWithErrorName] + ); + } + return filesToCheckForErrors; +} + +function provideErrorsToWebpack( + filesToCheckForErrors: interfaces.TSFiles, + filesWithErrors: interfaces.TSFiles, + compilation: interfaces.WebpackCompilation, + modules: Modules, + instance: interfaces.TSInstance +) { + const { compiler, languageService, files, loaderOptions } = instance; + Object.keys(filesToCheckForErrors) + .filter(filePath => !!filePath.match(/(\.d)?\.ts(x?)$/)) + .forEach(filePath => { + const errors = languageService.getSyntacticDiagnostics(filePath).concat(languageService.getSemanticDiagnostics(filePath)); + if (errors.length > 0) { + filesWithErrors[filePath] = files[filePath]; + } + + // if we have access to a webpack module, use that + if (utils.hasOwnProperty(modules, filePath)) { + const associatedModules = modules[filePath]; + + associatedModules.forEach(module => { + // remove any existing errors + removeTSLoaderErrors(module.errors); + + // append errors + const formattedErrors = utils.formatErrors(errors, loaderOptions, compiler, { module }); + utils.registerWebpackErrors(module.errors, formattedErrors); + utils.registerWebpackErrors(compilation.errors, formattedErrors); + }); + } else { + // otherwise it's a more generic error + utils.registerWebpackErrors(compilation.errors, utils.formatErrors(errors, loaderOptions, compiler, { file: filePath })); + } + }); +} + +/** + * gather all declaration files from TypeScript and output them to webpack + */ +function provideDeclarationFilesToWebpack( + filesToCheckForErrors: interfaces.TSFiles, + languageService: typescript.LanguageService, + compilation: interfaces.WebpackCompilation +) { + Object.keys(filesToCheckForErrors) + .filter(filePath => !!filePath.match(/\.ts(x?)$/)) + .forEach(filePath => { + const output = languageService.getEmitOutput(filePath); + const declarationFile = output.outputFiles.filter(fp => !!fp.name.match(/\.d.ts$/)).pop(); + if (declarationFile) { + const assetPath = path.relative(compilation.compiler.context, declarationFile.name); + compilation.assets[assetPath] = { + source: () => declarationFile.text, + size: () => declarationFile.text.length, + }; + } + }); +} + /** * handle all other errors. The basic approach here to get accurate error * reporting is to start with a "blank slate" each compilation and gather @@ -145,14 +196,19 @@ function removeTSLoaderErrors(errors: interfaces.WebpackError[]) { /** * Recursively collect all possible dependants of passed file */ -function collectAllDependants(instance: interfaces.TSInstance, fileName: string, collected: any = {}): string[] { - let result = {}; +function collectAllDependants( + reverseDependencyGraph: interfaces.ReverseDependencyGraph, + fileName: string, + collected: {[file:string]: boolean} = {} +): string[] { + const result = {}; result[fileName] = true; collected[fileName] = true; - if (instance.reverseDependencyGraph[fileName]) { - Object.keys(instance.reverseDependencyGraph[fileName]).forEach(dependantFileName => { + if (reverseDependencyGraph[fileName]) { + Object.keys(reverseDependencyGraph[fileName]).forEach(dependantFileName => { if (!collected[dependantFileName]) { - collectAllDependants(instance, dependantFileName, collected).forEach(fName => result[fName] = true); + collectAllDependants(reverseDependencyGraph, dependantFileName, collected) + .forEach(fName => result[fName] = true); } }); } diff --git a/src/interfaces.ts b/src/interfaces.ts index 0ba989d1c..cac74a3b1 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -134,12 +134,12 @@ export interface TSInstances { } interface DependencyGraph { - [index: string]: string[]; + [file: string]: string[]; } -interface ReverseDependencyGraph { - [index: string]: { - [index: string]: boolean +export interface ReverseDependencyGraph { + [file: string]: { + [file: string]: boolean }; } diff --git a/src/utils.ts b/src/utils.ts index 8c7ba517e..a54266d18 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,7 +6,7 @@ import constants = require('./constants'); import interfaces = require('./interfaces'); export function registerWebpackErrors(existingErrors: interfaces.WebpackError[], errorsToPush: interfaces.WebpackError[]) { - Array.prototype.splice.apply(existingErrors, (<(number | interfaces.WebpackError)[]> [0, 0]).concat(errorsToPush)); + Array.prototype.splice.apply(existingErrors, (<(number | interfaces.WebpackError)[]>[0, 0]).concat(errorsToPush)); } export function hasOwnProperty(obj: T, property: string) { @@ -42,7 +42,7 @@ export function formatErrors( } else { error = makeError({ rawMessage: messageText }); } - return objectAssign(error, merge); + return objectAssign(error, merge); }); } @@ -69,14 +69,16 @@ export function makeError({ rawMessage, message, location, file }: MakeError): i loaderSource: 'ts-loader' }; - return objectAssign(error, { location, file }); + return objectAssign(error, { location, file }); } export function appendTsSuffixIfMatch(patterns: RegExp[], path: string): string { - for (let regexp of patterns) { - if (regexp.test(path)) { - return path + '.ts'; - } + if (patterns.length > 0) { + for (let regexp of patterns) { + if (regexp.test(path)) { + return path + '.ts'; + } + } } return path; } diff --git a/test/comparison-tests/aliasResolution/tsconfig.json b/test/comparison-tests/aliasResolution/tsconfig.json index b7e84945a..94d996e96 100644 --- a/test/comparison-tests/aliasResolution/tsconfig.json +++ b/test/comparison-tests/aliasResolution/tsconfig.json @@ -1,7 +1,5 @@ { "compilerOptions": { - }, - "files": [ - ] + } } \ No newline at end of file diff --git a/test/comparison-tests/babel-es6resolveParent/tsconfig.json b/test/comparison-tests/babel-es6resolveParent/tsconfig.json index 1471fc877..094529053 100644 --- a/test/comparison-tests/babel-es6resolveParent/tsconfig.json +++ b/test/comparison-tests/babel-es6resolveParent/tsconfig.json @@ -3,7 +3,5 @@ "target": "es6", "moduleResolution": "node", "jsx": "react" - }, - "files": [ - ] + } } \ No newline at end of file diff --git a/test/comparison-tests/babel-issue81/tsconfig.json b/test/comparison-tests/babel-issue81/tsconfig.json index 15262ce6e..c82ccfa51 100644 --- a/test/comparison-tests/babel-issue81/tsconfig.json +++ b/test/comparison-tests/babel-issue81/tsconfig.json @@ -3,6 +3,5 @@ "target": "es6", "sourceMap": true, "experimentalDecorators": true - }, - "files": [] + } } \ No newline at end of file diff --git a/test/comparison-tests/babel-issue92/tsconfig.json b/test/comparison-tests/babel-issue92/tsconfig.json index e3ff4fd85..49ba2ff12 100644 --- a/test/comparison-tests/babel-issue92/tsconfig.json +++ b/test/comparison-tests/babel-issue92/tsconfig.json @@ -1,7 +1,5 @@ { "compilerOptions": { "target": "es6" - }, - "files": [ - ] + } } \ No newline at end of file diff --git a/test/comparison-tests/declarationOutput/tsconfig.json b/test/comparison-tests/declarationOutput/tsconfig.json index 3dd31f25b..6c9058ec4 100644 --- a/test/comparison-tests/declarationOutput/tsconfig.json +++ b/test/comparison-tests/declarationOutput/tsconfig.json @@ -1,7 +1,5 @@ { "compilerOptions": { "declaration": true - }, - "files": [ - ] + } } diff --git a/test/comparison-tests/html-webpack-plugin/tsconfig.json b/test/comparison-tests/html-webpack-plugin/tsconfig.json index 01e163ec5..0efa0a638 100644 --- a/test/comparison-tests/html-webpack-plugin/tsconfig.json +++ b/test/comparison-tests/html-webpack-plugin/tsconfig.json @@ -1,6 +1,4 @@ { "compilerOptions": { - }, - "files": [ - ] + } } \ No newline at end of file diff --git a/test/comparison-tests/instance/_FLAKY_ b/test/comparison-tests/instance/_FLAKY_ new file mode 100644 index 000000000..e69de29bb diff --git a/test/comparison-tests/instance/tsconfig.json b/test/comparison-tests/instance/tsconfig.json index fc16344c1..427787ccb 100644 --- a/test/comparison-tests/instance/tsconfig.json +++ b/test/comparison-tests/instance/tsconfig.json @@ -1,5 +1,7 @@ { "compilerOptions": { }, - "files": [] + "files": [ + "a.ts" + ] } \ No newline at end of file diff --git a/test/comparison-tests/issue71/tsconfig.json b/test/comparison-tests/issue71/tsconfig.json index ca2bf6cb6..20a52967e 100644 --- a/test/comparison-tests/issue71/tsconfig.json +++ b/test/comparison-tests/issue71/tsconfig.json @@ -1,7 +1,5 @@ { "compilerOptions": { "noEmitOnError": true - }, - "files": [ - ] + } } \ No newline at end of file diff --git a/test/comparison-tests/large/tsconfig.json b/test/comparison-tests/large/tsconfig.json index 9ec54a63c..94d996e96 100644 --- a/test/comparison-tests/large/tsconfig.json +++ b/test/comparison-tests/large/tsconfig.json @@ -1,8 +1,5 @@ { "compilerOptions": { - }, - "files": [ - - ] + } } \ No newline at end of file diff --git a/test/comparison-tests/noErrorsPlugin/tsconfig.json b/test/comparison-tests/noErrorsPlugin/tsconfig.json index b7e84945a..94d996e96 100644 --- a/test/comparison-tests/noErrorsPlugin/tsconfig.json +++ b/test/comparison-tests/noErrorsPlugin/tsconfig.json @@ -1,7 +1,5 @@ { "compilerOptions": { - }, - "files": [ - ] + } } \ No newline at end of file diff --git a/test/comparison-tests/nodeResolution/tsconfig.json b/test/comparison-tests/nodeResolution/tsconfig.json index b7e84945a..94d996e96 100644 --- a/test/comparison-tests/nodeResolution/tsconfig.json +++ b/test/comparison-tests/nodeResolution/tsconfig.json @@ -1,7 +1,5 @@ { "compilerOptions": { - }, - "files": [ - ] + } } \ No newline at end of file diff --git a/test/comparison-tests/vue/tsconfig.json b/test/comparison-tests/vue/tsconfig.json index 0508d5f17..b71270932 100644 --- a/test/comparison-tests/vue/tsconfig.json +++ b/test/comparison-tests/vue/tsconfig.json @@ -1,5 +1,4 @@ { "compilerOptions": { - }, - "files": [] + } }