diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 8abd3c00d9e..7a3e27b8a02 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -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 = {}; - forEach(options, option => { - optionDeclarations[option.name.toLowerCase()] = option; + var optionMap: Map = {}; + 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") { diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 2aa6ae6e9a7..6b4db235dac 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -114,7 +114,11 @@ module ts { } export function isEmpty(map: Map) { - for (var id in map) return false; + for (var id in map) { + if (hasProperty(map, id)) { + return false; + } + } return true; } diff --git a/src/compiler/diagnosticInformationMap.generated.ts b/src/compiler/diagnosticInformationMap.generated.ts index 11342e81eeb..c1319edd442 100644 --- a/src/compiler/diagnosticInformationMap.generated.ts +++ b/src/compiler/diagnosticInformationMap.generated.ts @@ -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." }, diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 69b06cbcdf0..fac4ba6732f 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -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 diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 5174b9aa788..6902ebc4835 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -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 = {}; @@ -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; } } diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 51fe9214e71..564f22b9f08 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -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); }, diff --git a/src/compiler/tc.ts b/src/compiler/tc.ts index eb232f98654..e564002d03f 100644 --- a/src/compiler/tc.ts +++ b/src/compiler/tc.ts @@ -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 = {}; + var updatedFiles: Map = {}; + + // 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) { + 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); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 2a1e14d4f13..600fb5bdad4 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -932,6 +932,7 @@ module ts { sourceRoot?: string; target?: ScriptTarget; version?: boolean; + watch?: boolean; [option: string]: any; }