Merge pull request #324 from Microsoft/watcherIardlyKnowEr

Support the '--watch' compiler flag.
This commit is contained in:
Daniel Rosenwasser 2014-08-02 17:52:02 -07:00
commit 9a89147587
8 changed files with 186 additions and 38 deletions

View file

@ -11,9 +11,10 @@ module ts {
"o": "out",
"t": "target",
"v": "version",
"w": "watch",
};
var options: CommandLineOption[] = [
var optionDeclarations: CommandLineOption[] = [
{ name: "charset", type: "string" },
{ name: "codepage", type: "number" },
{ name: "declaration", type: "boolean" },
@ -32,14 +33,15 @@ module ts {
{ name: "sourceMap", type: "boolean" },
{ name: "sourceRoot", type: "string" },
{ name: "target", type: { "es3": ScriptTarget.ES3, "es5": ScriptTarget.ES5 }, error: Diagnostics.Argument_for_target_option_must_be_es3_or_es5 },
{ name: "version", type: "boolean" }
{ name: "version", type: "boolean" },
{ name: "watch", type: "boolean" },
];
// Map command line switches to compiler options' property descriptors. Keys must be lower case spellings of command line switches.
// The 'name' property specifies the property name in the CompilerOptions type. The 'type' property specifies the type of the option.
var optionDeclarations: Map<CommandLineOption> = {};
forEach(options, option => {
optionDeclarations[option.name.toLowerCase()] = option;
var optionMap: Map<CommandLineOption> = {};
forEach(optionDeclarations, option => {
optionMap[option.name.toLowerCase()] = option;
});
export function parseCommandLine(commandLine: string[]): ParsedCommandLine {
@ -73,8 +75,8 @@ module ts {
s = shortOptionNames[s];
}
if (hasProperty(optionDeclarations, s)) {
var opt = optionDeclarations[s];
if (hasProperty(optionMap, s)) {
var opt = optionMap[s];
// Check to see if no argument was provided (e.g. "--locale" is the last command-line argument).
if (!args[i] && opt.type !== "boolean") {

View file

@ -114,7 +114,11 @@ module ts {
}
export function isEmpty<T>(map: Map<T>) {
for (var id in map) return false;
for (var id in map) {
if (hasProperty(map, id)) {
return false;
}
}
return true;
}

View file

@ -209,6 +209,7 @@ module ts {
Class_0_defines_instance_member_function_1_but_extended_class_2_defines_it_as_instance_member_property: { code: 4019, category: DiagnosticCategory.NoPrefix, key: "Class '{0}' defines instance member function '{1}', but extended class '{2}' defines it as instance member property." },
In_an_enum_with_multiple_declarations_only_one_declaration_can_omit_an_initializer_for_its_first_enum_element: { code: 4024, category: DiagnosticCategory.Error, key: "In an enum with multiple declarations, only one declaration can omit an initializer for its first enum element." },
Named_properties_0_of_types_1_and_2_are_not_identical: { code: 4032, category: DiagnosticCategory.NoPrefix, key: "Named properties '{0}' of types '{1}' and '{2}' are not identical." },
The_current_host_does_not_support_the_0_option: { code: 5001, category: DiagnosticCategory.Error, key: "The current host does not support the '{0}' option." },
Cannot_find_the_common_subdirectory_path_for_the_input_files: { code: 5009, category: DiagnosticCategory.Error, key: "Cannot find the common subdirectory path for the input files." },
Cannot_read_file_0_Colon_1: { code: 5012, category: DiagnosticCategory.Error, key: "Cannot read file '{0}': {1}" },
Unsupported_file_encoding: { code: 5013, category: DiagnosticCategory.NoPrefix, key: "Unsupported file encoding." },
@ -216,6 +217,8 @@ module ts {
Option_mapRoot_cannot_be_specified_without_specifying_sourcemap_option: { code: 5038, category: DiagnosticCategory.Error, key: "Option mapRoot cannot be specified without specifying sourcemap option." },
Option_sourceRoot_cannot_be_specified_without_specifying_sourcemap_option: { code: 5039, category: DiagnosticCategory.Error, key: "Option sourceRoot cannot be specified without specifying sourcemap option." },
Version_0: { code: 6029, category: DiagnosticCategory.Message, key: "Version {0}" },
File_change_detected_Compiling: { code: 6032, category: DiagnosticCategory.Message, key: "File change detected. Compiling..." },
Compilation_complete_Watching_for_file_changes: { code: 6042, category: DiagnosticCategory.Message, key: "Compilation complete. Watching for file changes." },
Variable_0_implicitly_has_an_1_type: { code: 7005, category: DiagnosticCategory.Error, key: "Variable '{0}' implicitly has an '{1}' type." },
Parameter_0_implicitly_has_an_1_type: { code: 7006, category: DiagnosticCategory.Error, key: "Parameter '{0}' implicitly has an '{1}' type." },
Member_0_implicitly_has_an_1_type: { code: 7008, category: DiagnosticCategory.Error, key: "Member '{0}' implicitly has an '{1}' type." },

View file

@ -830,6 +830,10 @@
"category": "NoPrefix",
"code": 4032
},
"The current host does not support the '{0}' option.": {
"category": "Error",
"code": 5001
},
"Cannot find the common subdirectory path for the input files.": {
"category": "Error",
"code": 5009
@ -854,12 +858,18 @@
"category": "Error",
"code": 5039
},
"Version {0}": {
"category": "Message",
"code": 6029
},
},
"File change detected. Compiling...": {
"category": "Message",
"code": 6032
},
"Compilation complete. Watching for file changes.": {
"category": "Message",
"code": 6042
},
"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
"code": 7005

View file

@ -3527,7 +3527,6 @@ module ts {
}
export function createProgram(rootNames: string[], options: CompilerOptions, host: CompilerHost): Program {
var program: Program;
var files: SourceFile[] = [];
var filesByName: Map<SourceFile> = {};
@ -3536,7 +3535,9 @@ module ts {
var commonSourceDirectory: string;
forEach(rootNames, name => processRootFile(name, false));
if (!seenNoDefaultLib) processRootFile(host.getDefaultLibFilename(), true);
if (!seenNoDefaultLib) {
processRootFile(host.getDefaultLibFilename(), true);
}
verifyCompilerOptions();
errors.sort(compareDiagnostics);
program = {
@ -3627,7 +3628,7 @@ module ts {
function processReferencedFiles(file: SourceFile, basePath: string) {
forEach(file.referencedFiles, ref => {
processSourceFile(normalizePath(combinePaths(basePath, ref.filename)), false, file, ref.pos, ref.end);
processSourceFile(normalizePath(combinePaths(basePath, ref.filename)), /* isDefaultLib */ false, file, ref.pos, ref.end);
});
}
@ -3640,9 +3641,14 @@ module ts {
var searchPath = basePath;
while (true) {
var searchName = normalizePath(combinePaths(searchPath, moduleName));
if (findModuleSourceFile(searchName + ".ts", nameLiteral) || findModuleSourceFile(searchName + ".d.ts", nameLiteral)) break;
if (findModuleSourceFile(searchName + ".ts", nameLiteral) || findModuleSourceFile(searchName + ".d.ts", nameLiteral)) {
break;
}
var parentPath = getDirectoryPath(searchPath);
if (parentPath === searchPath) break;
if (parentPath === searchPath) {
break;
}
searchPath = parentPath;
}
}

View file

@ -5,9 +5,9 @@ interface System {
newLine: string;
useCaseSensitiveFileNames: boolean;
write(s: string): void;
writeErr(s: string): void;
readFile(fileName: string, encoding?: string): string;
writeFile(fileName: string, data: string): void;
watchFile?(fileName: string, callback: (fileName: string) => void): FileWatcher;
resolvePath(path: string): string;
fileExists(path: string): boolean;
directoryExists(path: string): boolean;
@ -18,6 +18,10 @@ interface System {
exit(exitCode?: number): void;
}
interface FileWatcher {
close(): void;
}
declare var require: any;
declare var module: any;
declare var process: any;
@ -187,6 +191,22 @@ var sys: System = (function () {
},
readFile: readFile,
writeFile: writeFile,
watchFile: (fileName, callback) => {
// watchFile polls a file every 250ms, picking up file notifications.
_fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged);
return {
close() { _fs.unwatchFile(fileName, fileChanged); }
};
function fileChanged(curr: any, prev: any) {
if (+curr.mtime <= +prev.mtime) {
return;
}
callback(fileName);
};
},
resolvePath: function (path: string): string {
return _path.resolve(path);
},

View file

@ -81,10 +81,10 @@ module ts {
function reportDiagnostic(error: Diagnostic) {
if (error.file) {
var loc = error.file.getLineAndCharacterFromPosition(error.start);
sys.writeErr(error.file.filename + "(" + loc.line + "," + loc.character + "): " + error.messageText + sys.newLine);
sys.write(error.file.filename + "(" + loc.line + "," + loc.character + "): " + error.messageText + sys.newLine);
}
else {
sys.writeErr(error.messageText + sys.newLine);
sys.write(error.messageText + sys.newLine);
}
}
@ -110,7 +110,7 @@ module ts {
}
function reportStatisticalValue(name: string, value: string) {
sys.writeErr(padRight(name + ":", 12) + padLeft(value.toString(), 10) + sys.newLine);
sys.write(padRight(name + ":", 12) + padLeft(value.toString(), 10) + sys.newLine);
}
function reportCountStatistic(name: string, count: number) {
@ -179,33 +179,133 @@ module ts {
};
}
export function executeCommandLine(args: string[]): number {
var cmds = parseCommandLine(args);
export function executeCommandLine(args: string[]): void {
var commandLine = parseCommandLine(args);
if (cmds.options.locale) {
validateLocaleAndSetLanguage(cmds.options.locale, cmds.errors);
if (commandLine.options.locale) {
validateLocaleAndSetLanguage(commandLine.options.locale, commandLine.errors);
}
if (cmds.filenames.length === 0 && !(cmds.options.help || cmds.options.version)) {
cmds.errors.push(createCompilerDiagnostic(Diagnostics.No_input_files_specified));
}
if (cmds.options.version) {
if (commandLine.options.version) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Version_0, version));
return 0;
sys.exit(0);
}
if (cmds.filenames.length === 0 || cmds.options.help) {
if (commandLine.options.help) {
// TODO (drosen): Usage.
sys.exit(0);
}
if (cmds.errors.length) {
reportDiagnostics(cmds.errors);
return 1;
if (commandLine.filenames.length === 0) {
commandLine.errors.push(createCompilerDiagnostic(Diagnostics.No_input_files_specified));
}
if (commandLine.errors.length) {
reportDiagnostics(commandLine.errors);
sys.exit(1);
}
var defaultCompilerHost = createCompilerHost(commandLine.options);
if (commandLine.options.watch) {
if (!sys.watchFile) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"));
sys.exit(1);
}
watchProgram(commandLine, defaultCompilerHost);
}
else {
sys.exit(compile(commandLine, defaultCompilerHost).errors.length > 0 ? 1 : 0);
}
}
/**
* Compiles the program once, and then watches all given and referenced files for changes.
* Upon detecting a file change, watchProgram will queue up file modification events for the next
* 250ms and then perform a recompilation. The reasoning is that in some cases, an editor can
* save all files at once, and we'd like to just perform a single recompilation.
*/
function watchProgram(commandLine: ParsedCommandLine, compilerHost: CompilerHost): void {
var watchers: Map<FileWatcher> = {};
var updatedFiles: Map<boolean> = {};
// Compile the program the first time and watch all given/referenced files.
var program = compile(commandLine, compilerHost).program;
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes));
addWatchers(program);
return;
function addWatchers(program: Program) {
forEach(program.getSourceFiles(), f => {
var filename = f.filename;
watchers[filename] = sys.watchFile(filename, fileUpdated);
});
}
function removeWatchers(program: Program) {
forEach(program.getSourceFiles(), f => {
var filename = f.filename;
if (hasProperty(watchers, filename)) {
watchers[filename].close();
}
});
watchers = {};
}
// Fired off whenever a file is changed.
function fileUpdated(filename: string) {
var firstNotification = isEmpty(updatedFiles);
updatedFiles[filename] = true;
// Only start this off when the first file change comes in,
// so that we can batch up all further changes.
if (firstNotification) {
setTimeout(() => {
var changedFiles = updatedFiles;
updatedFiles = {};
recompile(changedFiles);
}, 250);
}
}
function recompile(changedFiles: Map<boolean>) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Compiling));
// Remove all the watchers, as we may not be watching every file
// specified since the last compilation cycle.
removeWatchers(program);
// Gets us syntactically correct files from the last compilation.
var getUnmodifiedSourceFile = program.getSourceFile;
// We create a new compiler host for this compilation cycle.
// This new host is effectively the same except that 'getSourceFile'
// will try to reuse the SourceFiles from the last compilation cycle
// so long as they were not modified.
var newCompilerHost = clone(compilerHost);
newCompilerHost.getSourceFile = (fileName, languageVersion, onError) => {
if (!hasProperty(changedFiles, fileName)) {
var sourceFile = getUnmodifiedSourceFile(fileName);
if (sourceFile) {
return sourceFile;
}
}
return compilerHost.getSourceFile(fileName, languageVersion, onError);
};
program = compile(commandLine, newCompilerHost).program;
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes));
addWatchers(program);
}
}
function compile(commandLine: ParsedCommandLine, compilerHost: CompilerHost) {
var parseStart = new Date().getTime();
var program = createProgram(cmds.filenames, cmds.options, createCompilerHost(cmds.options));
var program = createProgram(commandLine.filenames, commandLine.options, compilerHost);
var bindStart = new Date().getTime();
var errors = program.getDiagnostics();
if (errors.length) {
@ -224,7 +324,7 @@ module ts {
}
reportDiagnostics(errors);
if (cmds.options.diagnostics) {
if (commandLine.options.diagnostics) {
reportCountStatistic("Files", program.getSourceFiles().length);
reportCountStatistic("Lines", countLines(program));
reportCountStatistic("Nodes", checker ? checker.getNodeCount() : 0);
@ -237,8 +337,10 @@ module ts {
reportTimeStatistic("Emit time", reportStart - emitStart);
reportTimeStatistic("Total time", reportStart - parseStart);
}
return errors.length ? 1 : 0;
return { program: program, errors: errors };
}
}
sys.exit(ts.executeCommandLine(sys.args));
ts.executeCommandLine(sys.args);

View file

@ -932,6 +932,7 @@ module ts {
sourceRoot?: string;
target?: ScriptTarget;
version?: boolean;
watch?: boolean;
[option: string]: any;
}