/CommonJS/bin/flatten

http://github.com/cacaodev/cappuccino · #! · 368 lines · 286 code · 82 blank · 0 comment · 0 complexity · e525892134b62c88284ca574ebb25905 MD5 · raw file

  1. #!/usr/bin/env objj
  2. require("narwhal").ensureEngine("rhino");
  3. @import <Foundation/Foundation.j>
  4. @import "../lib/cappuccino/objj-analysis-tools.j"
  5. var FILE = require("file");
  6. var OS = require("os");
  7. var UTIL = require("narwhal/util");
  8. var CACHEMANIFEST = require("objective-j/cache-manifest");
  9. var stream = require("narwhal/term").stream;
  10. var parser = new (require("narwhal/args").Parser)();
  11. parser.usage("INPUT_PROJECT OUTPUT_PROJECT");
  12. parser.help("Combine a Cappuccino application into a single JavaScript file.");
  13. parser.option("-m", "--main", "main")
  14. .def("main.j")
  15. .set()
  16. .help("The relative path (from INPUT_PROJECT) to the main file (default: 'main.j')");
  17. parser.option("-F", "--framework", "frameworks")
  18. .push()
  19. .help("Add a frameworks directory, relative to INPUT_PROJECT (default: ['Frameworks'])");
  20. parser.option("-P", "--path", "paths")
  21. .push()
  22. .help("Add a path (relative to the application root) to inline.");
  23. parser.option("-f", "--force", "force")
  24. .def(false)
  25. .set(true)
  26. .help("Force overwriting OUTPUT_PROJECT if it exists");
  27. parser.option("--index", "index")
  28. .def("index.html")
  29. .set()
  30. .help("The root HTML file to modify (default: index.html)");
  31. parser.option("-s", "--split", "number", "split")
  32. .natural()
  33. .def(0)
  34. .help("Split into multiple files");
  35. parser.option("-c", "--compressor", "compressor")
  36. .def("shrinksafe")
  37. .set()
  38. .help("Select a compressor to use (closure-compiler, yuicompressor, shrinksafe), or \"none\" (default: shrinksafe)");
  39. parser.option("--manifest", "manifest")
  40. .set(true)
  41. .help("Generate HTML5 cache manifest.");
  42. parser.option("-v", "--verbose", "verbose")
  43. .def(false)
  44. .set(true)
  45. .help("Verbose logging");
  46. parser.helpful();
  47. function main(args)
  48. {
  49. var options = parser.parse(args);
  50. if (options.args.length < 2) {
  51. parser.printUsage(options);
  52. return;
  53. }
  54. var rootPath = FILE.path(options.args[0]).join("").absolute();
  55. var outputPath = FILE.path(options.args[1]).join("").absolute();
  56. if (outputPath.exists()) {
  57. if (options.force) {
  58. // FIXME: why doesn't this work?!
  59. //outputPath.rmtree();
  60. OS.system(["rm", "-rf", outputPath]);
  61. } else {
  62. stream.print("\0red(OUTPUT_PROJECT " + outputPath + " exists. Use -f to overwrite.\0)");
  63. OS.exit(1);
  64. }
  65. }
  66. options.frameworks.push("Frameworks");
  67. var mainPath = String(rootPath.join(options.main));
  68. var frameworks = options.frameworks.map(function(framework) { return rootPath.join(framework); });
  69. var environment = "Browser";
  70. stream.print("\0yellow("+Array(81).join("=")+"\0)");
  71. stream.print("Application root: \0green(" + rootPath + "\0)");
  72. stream.print("Output directory: \0green(" + outputPath + "\0)");
  73. stream.print("\0yellow("+Array(81).join("=")+"\0)");
  74. stream.print("Main file: \0green(" + mainPath + "\0)");
  75. stream.print("Frameworks: \0green(" + frameworks + "\0)");
  76. stream.print("Environment: \0green(" + environment + "\0)");
  77. var flattener = new ObjectiveJFlattener(rootPath);
  78. flattener.options = options;
  79. flattener.setIncludePaths(frameworks);
  80. flattener.setEnvironments([environment, "ObjJ"]);
  81. print("Loading application.");
  82. flattener.load(mainPath);
  83. print("Loading default theme.");
  84. flattener.require("objective-j").objj_eval("("+(function() {
  85. var defaultThemeName = [CPApplication defaultThemeName],
  86. bundle = nil;
  87. if (defaultThemeName === @"Aristo" || defaultThemeName === @"Aristo2")
  88. bundle = [CPBundle bundleForClass:[CPApplication class]];
  89. else
  90. bundle = [CPBundle mainBundle];
  91. var blend = [[CPThemeBlend alloc] initWithContentsOfURL:[bundle pathForResource:defaultThemeName + @".blend"]];
  92. [blend loadWithDelegate:nil];
  93. })+")")();
  94. var applicationJSs = flattener.buildApplicationJS();
  95. FILE.copyTree(rootPath, outputPath);
  96. applicationJSs.forEach(function(applicationJS, n) {
  97. var name = "Application"+(n||"")+".js";
  98. if (options.compressor === "none") {
  99. print("skipping compression: " + name);
  100. } else {
  101. print("compressing: " + name);
  102. applicationJS = require("minify/"+options.compressor).compress(applicationJS, { charset : "UTF-8", useServer : true });
  103. }
  104. outputPath.join(name).write(applicationJS, { charset : "UTF-8" });
  105. });
  106. rewriteMainHTML(outputPath.join(options.index));
  107. if (options.manifest) {
  108. CACHEMANIFEST.generateManifest(outputPath, {
  109. index : outputPath.join(options.index),
  110. exclude : Object.keys(flattener.filesToCache).map(function(path) { return outputPath.join(path).toString(); })
  111. });
  112. }
  113. }
  114. // ObjectiveJFlattener inherits from ObjectiveJRuntimeAnalyzer
  115. function ObjectiveJFlattener(rootPath) {
  116. ObjectiveJRuntimeAnalyzer.apply(this, arguments);
  117. this.filesToCache = {};
  118. this.fileCacheBuffer = [];
  119. this.functionsBuffer = [];
  120. }
  121. ObjectiveJFlattener.prototype = Object.create(ObjectiveJRuntimeAnalyzer.prototype);
  122. ObjectiveJFlattener.prototype.buildApplicationJS = function() {
  123. this.setupFileCache();
  124. this.serializeFunctions();
  125. this.serializeFileCache();
  126. var additions = FILE.read(FILE.join(FILE.dirname(module.path), "..", "..", "cappuccino", "lib", "cappuccino", "objj-flatten-additions.js"), { charset:"UTF-8" });
  127. var applicationJSs = [];
  128. if (this.options.split === 0) {
  129. var buffer = [];
  130. buffer.push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);");
  131. buffer.push(additions);
  132. buffer.push(this.fileCacheBuffer.join("\n"));
  133. buffer.push(this.functionsBuffer.join("\n"));
  134. buffer.push("ObjectiveJ.bootstrap();");
  135. applicationJSs.push(buffer.join("\n"));
  136. } else {
  137. var appFilesCount = this.options.split;
  138. var buffers = [];
  139. for (var i = 0; i <= appFilesCount; i++)
  140. buffers.push([]);
  141. var chunks = this.fileCacheBuffer.concat(this.functionsBuffer).sort(function(chunkA, chunkB) {
  142. return chunkA.length - chunkB.length;
  143. });
  144. // try to equally distribute the chunks. could be better but good enough for now.
  145. var n = 0;
  146. while (chunks.length) {
  147. buffers[(n++ % appFilesCount) + 1].push(chunks.pop());
  148. }
  149. buffers[0].push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);");
  150. buffers[0].push(additions);
  151. buffers[0].push("var appFilesCount = " + appFilesCount +";");
  152. buffers[0].push("for (var i = 1; i <= appFilesCount; i++) {");
  153. buffers[0].push(" var script = document.createElement(\"script\");");
  154. buffers[0].push(" script.src = \"Application\"+i+\".js\";");
  155. buffers[0].push(" script.charset = \"UTF-8\";");
  156. buffers[0].push(" script.onload = function() { if (--appFilesCount === 0) ObjectiveJ.bootstrap(); };");
  157. buffers[0].push(" document.getElementsByTagName(\"head\")[0].appendChild(script);");
  158. buffers[0].push("}");
  159. buffers.forEach(function(buffer) {
  160. applicationJSs.push(buffer.join("\n"));
  161. });
  162. }
  163. return applicationJSs;
  164. }
  165. ObjectiveJFlattener.prototype.serializeFunctions = function() {
  166. var inlineFunctions = true;//this.options.inlineFunctions;
  167. var outputFiles = {};
  168. var _cachedExecutableFunctions = {};
  169. this.require("objective-j").FileExecutable.allFileExecutables().forEach(function(executable) {
  170. var path = executable.path();
  171. if (inlineFunctions)
  172. {
  173. // stringify the function, replacing arguments
  174. var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK
  175. var relative = this.rootPath.relative(path).toString();
  176. this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");");
  177. }
  178. var bundle = this.context.global.CFBundle.bundleContainingURL(path);
  179. if (bundle && bundle.infoDictionary())
  180. {
  181. var executablePath = bundle.executablePath(),
  182. relativeToBundle = FILE.relative(FILE.join(bundle.path(), ""), path);
  183. if (executablePath)
  184. {
  185. if (inlineFunctions)
  186. {
  187. // remove the code since we're inlining the functions
  188. executable._code = "alert("+JSON.stringify(relativeToBundle)+");";
  189. }
  190. if (!outputFiles[executablePath])
  191. {
  192. outputFiles[executablePath] = [];
  193. outputFiles[executablePath].push("@STATIC;1.0;");
  194. }
  195. var fileContents = executable.toMarkedString();
  196. outputFiles[executablePath].push("p;" + relativeToBundle.length + ";" + relativeToBundle);
  197. outputFiles[executablePath].push("t;" + fileContents.length + ";" + fileContents);
  198. // stream.print("Adding \0green(" + this.rootPath.relative(path) + "\0) to \0cyan(" + this.rootPath.relative(executablePath) + "\0)");
  199. }
  200. }
  201. else
  202. CPLog.warn("No bundle (or info dictionary for) " + rootPath.relative(path));
  203. }, this);
  204. for (var executablePath in outputFiles)
  205. {
  206. var relative = this.rootPath.relative(executablePath).toString();
  207. var contents = outputFiles[executablePath].join("");
  208. this.filesToCache[relative] = contents;
  209. }
  210. }
  211. ObjectiveJFlattener.prototype.serializeFileCache = function() {
  212. for (var relative in this.filesToCache) {
  213. var contents = this.filesToCache[relative];
  214. print("caching: " + relative + " => " + (contents == null ? 404 : 200));
  215. if (contents == null)
  216. this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 404);");
  217. else
  218. this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 200, {}, "+JSON.stringify(contents)+");");
  219. }
  220. }
  221. ObjectiveJFlattener.prototype.setupFileCache = function() {
  222. var paths = {};
  223. UTIL.update(paths, this.requestedURLs);
  224. this.options.paths.forEach(function(relativePath) {
  225. paths[this.rootPath.join(relativePath)] = true;
  226. }, this);
  227. Object.keys(paths).forEach(function(absolute) {
  228. var relative = this.rootPath.relative(absolute).toString();
  229. if (relative.indexOf("..") === 0)
  230. {
  231. print("skipping (parent of app root): " + absolute);
  232. return;
  233. }
  234. if (FILE.isFile(absolute))
  235. {
  236. // if (this.options.maxCachedSize && FILE.size(absolute) > this.options.maxCachedSize)
  237. // {
  238. // print("skipping (larger than "+this.options.maxCachedSize+" bytes): " + absolute);
  239. // return;
  240. // }
  241. var contents = FILE.read(absolute, { charset : "UTF-8" });
  242. this.filesToCache[relative] = contents;
  243. } else {
  244. this.filesToCache[relative] = null;
  245. }
  246. }, this);
  247. }
  248. // "$1" is the matching indentation
  249. var scriptTagsBefore =
  250. '$1<script type = "text/javascript">\n'+
  251. '$1 OBJJ_AUTO_BOOTSTRAP = false;\n'+
  252. '$1</script>';
  253. var scriptTagsAfter =
  254. '$1<script type="text/javascript" src="Application.js" charset="UTF-8"></script>';
  255. // enable CPLog:
  256. // scriptTagsAfter = '$1<script type="text/javascript">\n$1 CPLogRegister(CPLogConsole);\n$1</script>\n' + scriptTagsAfter;
  257. function rewriteMainHTML(indexHTMLPath) {
  258. if (indexHTMLPath.isFile()) {
  259. var indexHTML = indexHTMLPath.read({ charset : "UTF-8" });
  260. // inline the Application.js if it's smallish
  261. var applicationJSPath = indexHTMLPath.dirname().join("Application.js");
  262. if (applicationJSPath.size() < 10*1024) {
  263. // escape any dollar signs by replacing them with two
  264. // then indent by splitting/joining on newlines
  265. scriptTagsAfter =
  266. '$1<script type="text/javascript">\n'+
  267. '$1 ' + applicationJSPath.read({ charset : "UTF-8" }).replace(/\$/g, "$$$$").split("\n").join("\n$1 ")+'\n'+
  268. '$1</script>';
  269. }
  270. // attempt to find Objective-J script tag and add ours
  271. var newIndexHTML = indexHTML.replace(/([ \t]+)<script[^>]+Objective-J\.js[^>]+>(?:\s*<\/script>)?/,
  272. scriptTagsBefore+'\n$&\n'+scriptTagsAfter);
  273. if (newIndexHTML !== indexHTML) {
  274. stream.print("\0green(Modified: "+indexHTMLPath+".\0)");
  275. indexHTMLPath.write(newIndexHTML, { charset : "UTF-8" });
  276. return;
  277. }
  278. } else {
  279. stream.print("\0yellow(Warning: "+indexHTMLPath+" does not exist. Specify an alternate index HTML file with the --index option.\0)");
  280. }
  281. stream.print("\0yellow(Warning: Unable to automatically modify "+indexHTMLPath + ".\0)");
  282. stream.print("\nAdd the following before the Objective-J script tag:");
  283. stream.print(scriptTagsBefore.replace(/\$1/g, " "));
  284. stream.print("\nAdd the following after the Objective-J script tag:");
  285. stream.print(scriptTagsAfter.replace(/\$1/g, " "));
  286. }