Skip to content

Commit 9a89147

Browse files
Merge pull request microsoft#324 from Microsoft/watcherIardlyKnowEr
Support the '--watch' compiler flag.
2 parents 23117a9 + 5a8eff8 commit 9a89147

File tree

8 files changed

+186
-38
lines changed

8 files changed

+186
-38
lines changed

src/compiler/commandLineParser.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ module ts {
1111
"o": "out",
1212
"t": "target",
1313
"v": "version",
14+
"w": "watch",
1415
};
1516

16-
var options: CommandLineOption[] = [
17+
var optionDeclarations: CommandLineOption[] = [
1718
{ name: "charset", type: "string" },
1819
{ name: "codepage", type: "number" },
1920
{ name: "declaration", type: "boolean" },
@@ -32,14 +33,15 @@ module ts {
3233
{ name: "sourceMap", type: "boolean" },
3334
{ name: "sourceRoot", type: "string" },
3435
{ name: "target", type: { "es3": ScriptTarget.ES3, "es5": ScriptTarget.ES5 }, error: Diagnostics.Argument_for_target_option_must_be_es3_or_es5 },
35-
{ name: "version", type: "boolean" }
36+
{ name: "version", type: "boolean" },
37+
{ name: "watch", type: "boolean" },
3638
];
3739

3840
// Map command line switches to compiler options' property descriptors. Keys must be lower case spellings of command line switches.
3941
// The 'name' property specifies the property name in the CompilerOptions type. The 'type' property specifies the type of the option.
40-
var optionDeclarations: Map<CommandLineOption> = {};
41-
forEach(options, option => {
42-
optionDeclarations[option.name.toLowerCase()] = option;
42+
var optionMap: Map<CommandLineOption> = {};
43+
forEach(optionDeclarations, option => {
44+
optionMap[option.name.toLowerCase()] = option;
4345
});
4446

4547
export function parseCommandLine(commandLine: string[]): ParsedCommandLine {
@@ -73,8 +75,8 @@ module ts {
7375
s = shortOptionNames[s];
7476
}
7577

76-
if (hasProperty(optionDeclarations, s)) {
77-
var opt = optionDeclarations[s];
78+
if (hasProperty(optionMap, s)) {
79+
var opt = optionMap[s];
7880

7981
// Check to see if no argument was provided (e.g. "--locale" is the last command-line argument).
8082
if (!args[i] && opt.type !== "boolean") {

src/compiler/core.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,11 @@ module ts {
114114
}
115115

116116
export function isEmpty<T>(map: Map<T>) {
117-
for (var id in map) return false;
117+
for (var id in map) {
118+
if (hasProperty(map, id)) {
119+
return false;
120+
}
121+
}
118122
return true;
119123
}
120124

src/compiler/diagnosticInformationMap.generated.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,16 @@ module ts {
209209
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." },
210210
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." },
211211
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." },
212+
The_current_host_does_not_support_the_0_option: { code: 5001, category: DiagnosticCategory.Error, key: "The current host does not support the '{0}' option." },
212213
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." },
213214
Cannot_read_file_0_Colon_1: { code: 5012, category: DiagnosticCategory.Error, key: "Cannot read file '{0}': {1}" },
214215
Unsupported_file_encoding: { code: 5013, category: DiagnosticCategory.NoPrefix, key: "Unsupported file encoding." },
215216
Could_not_write_file_0_Colon_1: { code: 5033, category: DiagnosticCategory.Error, key: "Could not write file '{0}': {1}" },
216217
Option_mapRoot_cannot_be_specified_without_specifying_sourcemap_option: { code: 5038, category: DiagnosticCategory.Error, key: "Option mapRoot cannot be specified without specifying sourcemap option." },
217218
Option_sourceRoot_cannot_be_specified_without_specifying_sourcemap_option: { code: 5039, category: DiagnosticCategory.Error, key: "Option sourceRoot cannot be specified without specifying sourcemap option." },
218219
Version_0: { code: 6029, category: DiagnosticCategory.Message, key: "Version {0}" },
220+
File_change_detected_Compiling: { code: 6032, category: DiagnosticCategory.Message, key: "File change detected. Compiling..." },
221+
Compilation_complete_Watching_for_file_changes: { code: 6042, category: DiagnosticCategory.Message, key: "Compilation complete. Watching for file changes." },
219222
Variable_0_implicitly_has_an_1_type: { code: 7005, category: DiagnosticCategory.Error, key: "Variable '{0}' implicitly has an '{1}' type." },
220223
Parameter_0_implicitly_has_an_1_type: { code: 7006, category: DiagnosticCategory.Error, key: "Parameter '{0}' implicitly has an '{1}' type." },
221224
Member_0_implicitly_has_an_1_type: { code: 7008, category: DiagnosticCategory.Error, key: "Member '{0}' implicitly has an '{1}' type." },

src/compiler/diagnosticMessages.json

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,10 @@
830830
"category": "NoPrefix",
831831
"code": 4032
832832
},
833+
"The current host does not support the '{0}' option.": {
834+
"category": "Error",
835+
"code": 5001
836+
},
833837
"Cannot find the common subdirectory path for the input files.": {
834838
"category": "Error",
835839
"code": 5009
@@ -854,12 +858,18 @@
854858
"category": "Error",
855859
"code": 5039
856860
},
857-
858861
"Version {0}": {
859862
"category": "Message",
860863
"code": 6029
861-
},
862-
864+
},
865+
"File change detected. Compiling...": {
866+
"category": "Message",
867+
"code": 6032
868+
},
869+
"Compilation complete. Watching for file changes.": {
870+
"category": "Message",
871+
"code": 6042
872+
},
863873
"Variable '{0}' implicitly has an '{1}' type.": {
864874
"category": "Error",
865875
"code": 7005

src/compiler/parser.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3527,7 +3527,6 @@ module ts {
35273527
}
35283528

35293529
export function createProgram(rootNames: string[], options: CompilerOptions, host: CompilerHost): Program {
3530-
35313530
var program: Program;
35323531
var files: SourceFile[] = [];
35333532
var filesByName: Map<SourceFile> = {};
@@ -3536,7 +3535,9 @@ module ts {
35363535
var commonSourceDirectory: string;
35373536

35383537
forEach(rootNames, name => processRootFile(name, false));
3539-
if (!seenNoDefaultLib) processRootFile(host.getDefaultLibFilename(), true);
3538+
if (!seenNoDefaultLib) {
3539+
processRootFile(host.getDefaultLibFilename(), true);
3540+
}
35403541
verifyCompilerOptions();
35413542
errors.sort(compareDiagnostics);
35423543
program = {
@@ -3627,7 +3628,7 @@ module ts {
36273628

36283629
function processReferencedFiles(file: SourceFile, basePath: string) {
36293630
forEach(file.referencedFiles, ref => {
3630-
processSourceFile(normalizePath(combinePaths(basePath, ref.filename)), false, file, ref.pos, ref.end);
3631+
processSourceFile(normalizePath(combinePaths(basePath, ref.filename)), /* isDefaultLib */ false, file, ref.pos, ref.end);
36313632
});
36323633
}
36333634

@@ -3640,9 +3641,14 @@ module ts {
36403641
var searchPath = basePath;
36413642
while (true) {
36423643
var searchName = normalizePath(combinePaths(searchPath, moduleName));
3643-
if (findModuleSourceFile(searchName + ".ts", nameLiteral) || findModuleSourceFile(searchName + ".d.ts", nameLiteral)) break;
3644+
if (findModuleSourceFile(searchName + ".ts", nameLiteral) || findModuleSourceFile(searchName + ".d.ts", nameLiteral)) {
3645+
break;
3646+
}
3647+
36443648
var parentPath = getDirectoryPath(searchPath);
3645-
if (parentPath === searchPath) break;
3649+
if (parentPath === searchPath) {
3650+
break;
3651+
}
36463652
searchPath = parentPath;
36473653
}
36483654
}

src/compiler/sys.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ interface System {
55
newLine: string;
66
useCaseSensitiveFileNames: boolean;
77
write(s: string): void;
8-
writeErr(s: string): void;
98
readFile(fileName: string, encoding?: string): string;
109
writeFile(fileName: string, data: string): void;
10+
watchFile?(fileName: string, callback: (fileName: string) => void): FileWatcher;
1111
resolvePath(path: string): string;
1212
fileExists(path: string): boolean;
1313
directoryExists(path: string): boolean;
@@ -18,6 +18,10 @@ interface System {
1818
exit(exitCode?: number): void;
1919
}
2020

21+
interface FileWatcher {
22+
close(): void;
23+
}
24+
2125
declare var require: any;
2226
declare var module: any;
2327
declare var process: any;
@@ -187,6 +191,22 @@ var sys: System = (function () {
187191
},
188192
readFile: readFile,
189193
writeFile: writeFile,
194+
watchFile: (fileName, callback) => {
195+
// watchFile polls a file every 250ms, picking up file notifications.
196+
_fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged);
197+
198+
return {
199+
close() { _fs.unwatchFile(fileName, fileChanged); }
200+
};
201+
202+
function fileChanged(curr: any, prev: any) {
203+
if (+curr.mtime <= +prev.mtime) {
204+
return;
205+
}
206+
207+
callback(fileName);
208+
};
209+
},
190210
resolvePath: function (path: string): string {
191211
return _path.resolve(path);
192212
},

src/compiler/tc.ts

Lines changed: 123 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ module ts {
8181
function reportDiagnostic(error: Diagnostic) {
8282
if (error.file) {
8383
var loc = error.file.getLineAndCharacterFromPosition(error.start);
84-
sys.writeErr(error.file.filename + "(" + loc.line + "," + loc.character + "): " + error.messageText + sys.newLine);
84+
sys.write(error.file.filename + "(" + loc.line + "," + loc.character + "): " + error.messageText + sys.newLine);
8585
}
8686
else {
87-
sys.writeErr(error.messageText + sys.newLine);
87+
sys.write(error.messageText + sys.newLine);
8888
}
8989
}
9090

@@ -110,7 +110,7 @@ module ts {
110110
}
111111

112112
function reportStatisticalValue(name: string, value: string) {
113-
sys.writeErr(padRight(name + ":", 12) + padLeft(value.toString(), 10) + sys.newLine);
113+
sys.write(padRight(name + ":", 12) + padLeft(value.toString(), 10) + sys.newLine);
114114
}
115115

116116
function reportCountStatistic(name: string, count: number) {
@@ -179,33 +179,133 @@ module ts {
179179
};
180180
}
181181

182-
export function executeCommandLine(args: string[]): number {
183-
var cmds = parseCommandLine(args);
182+
export function executeCommandLine(args: string[]): void {
183+
var commandLine = parseCommandLine(args);
184184

185-
if (cmds.options.locale) {
186-
validateLocaleAndSetLanguage(cmds.options.locale, cmds.errors);
185+
if (commandLine.options.locale) {
186+
validateLocaleAndSetLanguage(commandLine.options.locale, commandLine.errors);
187187
}
188188

189-
if (cmds.filenames.length === 0 && !(cmds.options.help || cmds.options.version)) {
190-
cmds.errors.push(createCompilerDiagnostic(Diagnostics.No_input_files_specified));
191-
}
192-
193-
if (cmds.options.version) {
189+
if (commandLine.options.version) {
194190
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Version_0, version));
195-
return 0;
191+
sys.exit(0);
196192
}
197193

198-
if (cmds.filenames.length === 0 || cmds.options.help) {
194+
if (commandLine.options.help) {
199195
// TODO (drosen): Usage.
196+
sys.exit(0);
197+
}
198+
199+
if (commandLine.filenames.length === 0) {
200+
commandLine.errors.push(createCompilerDiagnostic(Diagnostics.No_input_files_specified));
201+
}
202+
203+
if (commandLine.errors.length) {
204+
reportDiagnostics(commandLine.errors);
205+
sys.exit(1);
206+
}
207+
208+
var defaultCompilerHost = createCompilerHost(commandLine.options);
209+
210+
if (commandLine.options.watch) {
211+
if (!sys.watchFile) {
212+
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"));
213+
sys.exit(1);
214+
}
215+
216+
watchProgram(commandLine, defaultCompilerHost);
217+
}
218+
else {
219+
sys.exit(compile(commandLine, defaultCompilerHost).errors.length > 0 ? 1 : 0);
220+
}
221+
}
222+
223+
/**
224+
* Compiles the program once, and then watches all given and referenced files for changes.
225+
* Upon detecting a file change, watchProgram will queue up file modification events for the next
226+
* 250ms and then perform a recompilation. The reasoning is that in some cases, an editor can
227+
* save all files at once, and we'd like to just perform a single recompilation.
228+
*/
229+
function watchProgram(commandLine: ParsedCommandLine, compilerHost: CompilerHost): void {
230+
var watchers: Map<FileWatcher> = {};
231+
var updatedFiles: Map<boolean> = {};
232+
233+
// Compile the program the first time and watch all given/referenced files.
234+
var program = compile(commandLine, compilerHost).program;
235+
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes));
236+
addWatchers(program);
237+
return;
238+
239+
function addWatchers(program: Program) {
240+
forEach(program.getSourceFiles(), f => {
241+
var filename = f.filename;
242+
watchers[filename] = sys.watchFile(filename, fileUpdated);
243+
});
200244
}
201245

202-
if (cmds.errors.length) {
203-
reportDiagnostics(cmds.errors);
204-
return 1;
246+
function removeWatchers(program: Program) {
247+
forEach(program.getSourceFiles(), f => {
248+
var filename = f.filename;
249+
if (hasProperty(watchers, filename)) {
250+
watchers[filename].close();
251+
}
252+
});
253+
254+
watchers = {};
255+
}
256+
257+
// Fired off whenever a file is changed.
258+
function fileUpdated(filename: string) {
259+
var firstNotification = isEmpty(updatedFiles);
260+
261+
updatedFiles[filename] = true;
262+
263+
// Only start this off when the first file change comes in,
264+
// so that we can batch up all further changes.
265+
if (firstNotification) {
266+
setTimeout(() => {
267+
var changedFiles = updatedFiles;
268+
updatedFiles = {};
269+
270+
recompile(changedFiles);
271+
}, 250);
272+
}
205273
}
206274

275+
function recompile(changedFiles: Map<boolean>) {
276+
reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Compiling));
277+
// Remove all the watchers, as we may not be watching every file
278+
// specified since the last compilation cycle.
279+
removeWatchers(program);
280+
281+
// Gets us syntactically correct files from the last compilation.
282+
var getUnmodifiedSourceFile = program.getSourceFile;
283+
284+
// We create a new compiler host for this compilation cycle.
285+
// This new host is effectively the same except that 'getSourceFile'
286+
// will try to reuse the SourceFiles from the last compilation cycle
287+
// so long as they were not modified.
288+
var newCompilerHost = clone(compilerHost);
289+
newCompilerHost.getSourceFile = (fileName, languageVersion, onError) => {
290+
if (!hasProperty(changedFiles, fileName)) {
291+
var sourceFile = getUnmodifiedSourceFile(fileName);
292+
if (sourceFile) {
293+
return sourceFile;
294+
}
295+
}
296+
297+
return compilerHost.getSourceFile(fileName, languageVersion, onError);
298+
};
299+
300+
program = compile(commandLine, newCompilerHost).program;
301+
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes));
302+
addWatchers(program);
303+
}
304+
}
305+
306+
function compile(commandLine: ParsedCommandLine, compilerHost: CompilerHost) {
207307
var parseStart = new Date().getTime();
208-
var program = createProgram(cmds.filenames, cmds.options, createCompilerHost(cmds.options));
308+
var program = createProgram(commandLine.filenames, commandLine.options, compilerHost);
209309
var bindStart = new Date().getTime();
210310
var errors = program.getDiagnostics();
211311
if (errors.length) {
@@ -224,7 +324,7 @@ module ts {
224324
}
225325

226326
reportDiagnostics(errors);
227-
if (cmds.options.diagnostics) {
327+
if (commandLine.options.diagnostics) {
228328
reportCountStatistic("Files", program.getSourceFiles().length);
229329
reportCountStatistic("Lines", countLines(program));
230330
reportCountStatistic("Nodes", checker ? checker.getNodeCount() : 0);
@@ -237,8 +337,10 @@ module ts {
237337
reportTimeStatistic("Emit time", reportStart - emitStart);
238338
reportTimeStatistic("Total time", reportStart - parseStart);
239339
}
240-
return errors.length ? 1 : 0;
340+
341+
return { program: program, errors: errors };
342+
241343
}
242344
}
243345

244-
sys.exit(ts.executeCommandLine(sys.args));
346+
ts.executeCommandLine(sys.args);

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,7 @@ module ts {
932932
sourceRoot?: string;
933933
target?: ScriptTarget;
934934
version?: boolean;
935+
watch?: boolean;
935936

936937
[option: string]: any;
937938
}

0 commit comments

Comments
 (0)