Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

prevent adding/removing files crashes - fixes #358 #364

Merged
merged 3 commits into from
Nov 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
256 changes: 156 additions & 100 deletions src/after-compile.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -20,110 +19,162 @@ 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;
callback();
};
}

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
Expand All @@ -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);
}
});
}
Expand Down
8 changes: 4 additions & 4 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}

Expand Down
16 changes: 9 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends {}>(obj: T, property: string) {
Expand Down Expand Up @@ -42,7 +42,7 @@ export function formatErrors(
} else {
error = makeError({ rawMessage: messageText });
}
return <interfaces.WebpackError> objectAssign(error, merge);
return <interfaces.WebpackError>objectAssign(error, merge);
});
}

Expand All @@ -69,14 +69,16 @@ export function makeError({ rawMessage, message, location, file }: MakeError): i
loaderSource: 'ts-loader'
};

return <interfaces.WebpackError> objectAssign(error, { location, file });
return <interfaces.WebpackError>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;
}
4 changes: 1 addition & 3 deletions test/comparison-tests/aliasResolution/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
{
"compilerOptions": {

},
"files": [
]
}
}
4 changes: 1 addition & 3 deletions test/comparison-tests/babel-es6resolveParent/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,5 @@
"target": "es6",
"moduleResolution": "node",
"jsx": "react"
},
"files": [
]
}
}
3 changes: 1 addition & 2 deletions test/comparison-tests/babel-issue81/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@
"target": "es6",
"sourceMap": true,
"experimentalDecorators": true
},
"files": []
}
}
Loading