using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Text.RegularExpressions; using System.Web; using System.Web.Script.Serialization; /// /// Simple JavaScript minimizer (using Google's Closure Compiler) implementation. /// public static class Program { //------------------------------------------------- /// /// Main entry point for the JavaScript minimizer. /// public static int Main(string[] args) { try { if (args.Length < 3) { throw new ArgumentException("Expected at least 3 arguments: outputFileName, versionRegex, file1, ..."); } Minimize(args[0], args[1], args.Skip(2)); return 0; } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Failed"); Console.Write(ex.GetType().FullName); Console.Write(": "); Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); return 1; } finally { Console.ResetColor(); } } //------------------------------------------------- /// /// Helper extension method to query dictionary for /// the specified key and cast the value (if found) /// to the specified type in a single statement. /// private static T GetValueOrDefault(this IDictionary dict, string key) { object value; if (dict.TryGetValue(key, out value)) { if (value == null || value is T) { return (T)value; } else { throw new InvalidCastException(string.Concat("Requested type \"", typeof(T).FullName, "\" is not compatible with actual type \"", value.GetType().FullName, "\"")); } } else { return default(T); } } //------------------------------------------------- /// /// Create a user-readable list of messages from a list /// of JSON error/warning objects. /// private static void GatherIssues(ArrayList issues, string prefix, ICollection messages) { if (issues != null) { foreach (var issue in issues) { var issueDict = (Dictionary)issue; messages.Add(string.Concat( prefix, " ", issueDict.GetValueOrDefault("type"), " (", issueDict.GetValueOrDefault("lineno"), ", ", issueDict.GetValueOrDefault("charno"), "): ", issueDict.GetValueOrDefault("error") ?? issueDict.GetValueOrDefault("warning"))); } } } //------------------------------------------------- /// /// Gather all the specified input files and call Google's /// Closure Compiler REST API with the combined script. /// private static void Minimize(string outputFileName, string versionRegex, IEnumerable files) { // gather all input files into one ".orig" file Console.ForegroundColor = ConsoleColor.DarkGray; Console.Write("Gathering input files: "); var source = files.Select(f => File.ReadAllText(f)).Aggregate((s1,s2) => string.Concat(s1, Environment.NewLine, s2)); source = source.Replace("\"use strict\";", string.Empty); var versionMatch = Regex.Match(source, versionRegex, RegexOptions.CultureInvariant | RegexOptions.Multiline); if (versionMatch.Success && versionMatch.Groups.Count > 1) { outputFileName = string.Format(CultureInfo.InvariantCulture, outputFileName, versionMatch.Groups[1].Value); } var outputFileNameDir = Path.GetDirectoryName(outputFileName); if (!string.IsNullOrEmpty(outputFileNameDir) && !Directory.Exists(outputFileNameDir)) // ensure target dir exists { Directory.CreateDirectory(outputFileNameDir); } var outputFileNameOrig = Path.ChangeExtension(outputFileName, string.Concat(".orig", Path.GetExtension(outputFileName))); if (File.Exists(outputFileNameOrig)) { File.SetAttributes(outputFileNameOrig, FileAttributes.Normal); } // ensure we can overwrite files marked as read-only File.WriteAllText(outputFileNameOrig, source); Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("OK"); // run closure compiler Console.ForegroundColor = ConsoleColor.DarkGray; Console.Write("Calling Closure Compiler: "); var webClient = new WebClient(); webClient.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); var request = string.Concat("compilation_level=SIMPLE_OPTIMIZATIONS&output_format=json&output_info=errors&output_info=warnings&output_info=compiled_code&js_code=", HttpUtility.UrlEncode(source)); var response = new JavaScriptSerializer().Deserialize>(webClient.UploadString("http://closure-compiler.appspot.com/compile", request)); Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("OK"); // check for compiler errors Console.ForegroundColor = ConsoleColor.DarkGray; Console.Write("Processing Compiler Response: "); var messages = new List(); GatherIssues(response.GetValueOrDefault("errors"), "Error", messages); if (messages.Count > 0) { throw new InvalidOperationException(string.Concat("Compiler reported errors", Environment.NewLine, string.Join(Environment.NewLine, messages.ToArray()))); } Console.ForegroundColor = ConsoleColor.Green; Console.Write("OK"); // check for compiler warnings GatherIssues(response.GetValueOrDefault("warnings"), "Warning", messages); if (messages.Count > 0) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine(string.Concat(" (", messages.Count, " warning", messages.Count == 1 ? ")" : "s)")); Console.WriteLine(string.Join(Environment.NewLine, messages.ToArray())); } else { Console.WriteLine(" (no errors or warnings)"); } // write output file Console.ForegroundColor = ConsoleColor.DarkGray; Console.Write("Writing Minimized File: "); if (File.Exists(outputFileName)) { File.SetAttributes(outputFileName, FileAttributes.Normal); } // ensure we can overwrite files marked as read-only File.WriteAllText(outputFileName, response.GetValueOrDefault("compiledCode")); Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("OK"); } }