Saturday, October 6, 2012

TypeScript Interactive in Visual Studio (TypeScript REPL)

Last time I have been writing about JavaScript Interactive. After announcement of a new language called TypeScript last Monday (2012.10. 01) I have upgraded my solution to work also with TypeScript language. Everything works exactly the same as previously but instead of calling Generate method at the end of the file we need to call TypeScript’s counterpart GenerateFromTS. This method takes 3 parameters: two optional parameters already described (useClipboard , history – here TS file) and one new optional parameter jsFile. TypeScript language is compiled into JavaScript so parameter jsFile specifies the path where generated JavaScript code is stored. Let’s look at the sample usage of TypeScript Interactive:

image

New JS/TS Interactive implementation looks like this:

<#@ template hostspecific="true"#>
<#@ output extension=".txt"#>
<#@ assembly name="System.Windows.Forms" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Windows.Forms" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Threading" #>

<#+
public void Generate(bool useClipboard = false, string historyFile = null, Func<string, string> convertFileContent = null)
{
var fileContent = useClipboard ? GetClipboardText() : File.ReadAllText(Host.TemplateFile);
convertFileContent = convertFileContent ?? (s => s);

GenerationEnvironment.Clear();

var tempFile = Path.GetTempFileName();

if(string.IsNullOrEmpty(historyFile))
{
File.WriteAllText(tempFile, convertFileContent(fileContent));
RunProcess("node", tempFile, WriteLine, WriteLine);
File.Delete(tempFile);
}
else
{
var historyFilePath = Host.ResolvePath(historyFile);
if(!File.Exists(historyFilePath))
{
WriteLine("Specified history file path '{0}' does not exist.", historyFilePath);
}
else
{
var historyLines = File.ReadAllLines(historyFilePath);
int? printedOutputLinesCount = ExtractOutputLinesCount(historyLines);
long i = 0, max = printedOutputLinesCount ?? 0;
var tempFileLines =
(printedOutputLinesCount == null ? new [] {"//0"} : new string[0])
.Concat(historyLines)
.Concat(fileContent.Split(new string[] {Environment.NewLine},StringSplitOptions.None))
.Concat(new [] {new string('/',100)})
.ToArray();

var content = convertFileContent(string.Join(Environment.NewLine, tempFileLines));
if(content == null)
return;

File.WriteAllText(tempFile, content);
var result = RunProcess("node", tempFile, s => { if(++i >= max) WriteLine(s); }, WriteLine );
File.Delete(tempFile);

if(result == 0) // ok
{
tempFileLines[0] = @"//" + i;
File.WriteAllLines(historyFilePath, tempFileLines);
}
}
}
}

public void GenerateFromTS(string jsFile = null, bool useClipboard = false, string historyFile = null)
{
Generate(useClipboard, historyFile,
tsContent =>
{
var tsTempFile = Path.ChangeExtension(Path.GetTempFileName(), "ts");
var jsTempFile = Path.GetTempFileName();
File.WriteAllText(tsTempFile, tsContent);
var result = RunProcess(@"tsc", string.Format("--out \"{0}\" \"{1}\" ",jsTempFile, tsTempFile), WriteLine, WriteLine);
File.Delete(tsTempFile);

if(result != 0) // !ok
return null;

var jsContent = File.ReadAllText(jsTempFile);

if(!string.IsNullOrEmpty(jsFile))
{
var jsFilePath = Host.ResolvePath(jsFile);
if(!File.Exists(jsFilePath))
WriteLine("Specified JavaScript file path '{0}' does not exist.", jsFilePath);
else
File.WriteAllText(jsFilePath, jsContent);
}

File.Delete(jsTempFile);
return jsContent;
} );
}

private static int? ExtractOutputLinesCount(string[] allLines)
{
int count;
string firstLine = null;

return (allLines != null) && ((firstLine = allLines.FirstOrDefault()) != null) && int.TryParse(firstLine.Replace(@"//",""), out count) ?
(int?)count : null;
}

private static int RunProcess(string processName, string processArguments, Action<string> onOutputDataReceived = null, Action<string> onErrorDataReceived = null)
{
onOutputDataReceived = onOutputDataReceived ?? (s => {});
onErrorDataReceived = onErrorDataReceived ?? (s => {});

var processStartInfo = new ProcessStartInfo(processName, processArguments);

processStartInfo.RedirectStandardInput = true;
processStartInfo.RedirectStandardOutput = true;
processStartInfo.RedirectStandardError = true;
processStartInfo.CreateNoWindow = true;
processStartInfo.UseShellExecute = false;

var process = Process.Start(processStartInfo);

process.OutputDataReceived += (sender, args) => onOutputDataReceived(args.Data);
process.ErrorDataReceived += (sender, args) => { if(args.Data!=null) onErrorDataReceived(args.Data); };
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
return process.ExitCode;
}

private string GetClipboardText()
{
try
{
return Clipboard.GetDataObject().GetData(DataFormats.Text) as string;
}
catch
{
return "";
}
}
#>
 
There is one thing worth mentioning about executing TS Interactive using REPL session state stored inside history file. If we copy Greeter class definition to the clipboard and save the file twice we will get the error “Duplicate identifier ‘Greeter'”. It’s because the history file contains all successfully executed code snippets and this file as a whole needs to be correct. In TypeScript we cannot define two classes with the same name.

1 comment:

Andreas Fl├╝gge said...

Very cool! That was exactly what I was looking for. Great work!