PageRenderTime 21ms CodeModel.GetById 16ms app.highlight 1ms RepoModel.GetById 1ms app.codeStats 1ms

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