mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-25 21:17:03 +00:00
359 lines
13 KiB
Plaintext
Executable File
359 lines
13 KiB
Plaintext
Executable File
#!/usr/bin/env objj
|
|
|
|
require("narwhal").ensureEngine("rhino");
|
|
|
|
@import <Foundation/Foundation.j>
|
|
|
|
@import "../lib/cappuccino/objj-analysis-tools.j"
|
|
|
|
var FILE = require("file");
|
|
var OS = require("os");
|
|
var UTIL = require("util");
|
|
|
|
var CACHEMANIFEST = require("objective-j/cache-manifest");
|
|
|
|
var stream = require("term").stream;
|
|
var parser = new (require("args").Parser)();
|
|
|
|
parser.usage("INPUT_PROJECT OUTPUT_PROJECT");
|
|
parser.help("Combine a Cappuccino application into a single JavaScript file.");
|
|
|
|
parser.option("-m", "--main", "main")
|
|
.def("main.j")
|
|
.set()
|
|
.help("The relative path (from INPUT_PROJECT) to the main file (default: 'main.j')");
|
|
|
|
parser.option("-F", "--framework", "frameworks")
|
|
.push()
|
|
.help("Add a frameworks directory, relative to INPUT_PROJECT (default: ['Frameworks'])");
|
|
|
|
parser.option("-P", "--path", "paths")
|
|
.push()
|
|
.help("Add a path (relative to the application root) to inline.");
|
|
|
|
parser.option("-f", "--force", "force")
|
|
.def(false)
|
|
.set(true)
|
|
.help("Force overwriting OUTPUT_PROJECT if it exists");
|
|
|
|
parser.option("--index", "index")
|
|
.def("index.html")
|
|
.set()
|
|
.help("The root HTML file to modify (default: index.html)");
|
|
|
|
parser.option("-s", "--split", "number", "split")
|
|
.natural()
|
|
.def(0)
|
|
.help("Split into multiple files");
|
|
|
|
parser.option("-c", "--compressor", "compressor")
|
|
.def("shrinksafe")
|
|
.set()
|
|
.help("Select a compressor to use (closure-compiler, yuicompressor, shrinksafe), or \"none\" (default: shrinksafe)");
|
|
|
|
parser.option("--manifest", "manifest")
|
|
.set(true)
|
|
.help("Generate HTML5 cache manifest.");
|
|
|
|
parser.option("-v", "--verbose", "verbose")
|
|
.def(false)
|
|
.set(true)
|
|
.help("Verbose logging");
|
|
|
|
parser.helpful();
|
|
|
|
function main(args)
|
|
{
|
|
var options = parser.parse(args);
|
|
|
|
if (options.args.length < 2) {
|
|
parser.printUsage(options);
|
|
return;
|
|
}
|
|
|
|
var rootPath = FILE.path(options.args[0]).join("").absolute();
|
|
var outputPath = FILE.path(options.args[1]).join("").absolute();
|
|
|
|
if (outputPath.exists()) {
|
|
if (options.force) {
|
|
// FIXME: why doesn't this work?!
|
|
//outputPath.rmtree();
|
|
OS.system(["rm", "-rf", outputPath]);
|
|
} else {
|
|
stream.print("\0red(OUTPUT_PROJECT " + outputPath + " exists. Use -f to overwrite.\0)");
|
|
OS.exit(1);
|
|
}
|
|
}
|
|
|
|
options.frameworks.push("Frameworks");
|
|
|
|
var mainPath = String(rootPath.join(options.main));
|
|
var frameworks = options.frameworks.map(function(framework) { return rootPath.join(framework); });
|
|
var environment = "Browser";
|
|
|
|
stream.print("\0yellow("+Array(81).join("=")+"\0)");
|
|
stream.print("Application root: \0green(" + rootPath + "\0)");
|
|
stream.print("Output directory: \0green(" + outputPath + "\0)");
|
|
|
|
stream.print("\0yellow("+Array(81).join("=")+"\0)");
|
|
stream.print("Main file: \0green(" + mainPath + "\0)");
|
|
stream.print("Frameworks: \0green(" + frameworks + "\0)");
|
|
stream.print("Environment: \0green(" + environment + "\0)");
|
|
|
|
var flattener = new ObjectiveJFlattener(rootPath);
|
|
|
|
flattener.options = options;
|
|
|
|
flattener.setIncludePaths(frameworks);
|
|
flattener.setEnvironments([environment, "ObjJ"]);
|
|
|
|
print("Loading application.");
|
|
flattener.load(mainPath);
|
|
|
|
print("Loading default theme.");
|
|
flattener.require("objective-j").objj_eval("("+(function() {
|
|
var blend = [[CPThemeBlend alloc] initWithContentsOfURL:[[CPBundle bundleForClass:[CPApplication class]] pathForResource:[CPApplication defaultThemeName] + ".blend"]];
|
|
[blend loadWithDelegate:nil];
|
|
})+")")();
|
|
|
|
var applicationJSs = flattener.buildApplicationJS();
|
|
|
|
FILE.copyTree(rootPath, outputPath);
|
|
|
|
applicationJSs.forEach(function(applicationJS, n) {
|
|
var name = "Application"+(n||"")+".js";
|
|
if (options.compressor === "none") {
|
|
print("skipping compression: " + name);
|
|
} else {
|
|
print("compressing: " + name);
|
|
applicationJS = require("minify/"+options.compressor).compress(applicationJS, { charset : "UTF-8", useServer : true });
|
|
}
|
|
outputPath.join(name).write(applicationJS, { charset : "UTF-8" });
|
|
});
|
|
|
|
rewriteMainHTML(outputPath.join(options.index));
|
|
|
|
if (options.manifest) {
|
|
CACHEMANIFEST.generateManifest(outputPath, {
|
|
index : outputPath.join(options.index),
|
|
exclude : Object.keys(flattener.filesToCache).map(function(path) { return outputPath.join(path).toString(); })
|
|
});
|
|
}
|
|
}
|
|
|
|
// ObjectiveJFlattener inherits from ObjectiveJRuntimeAnalyzer
|
|
function ObjectiveJFlattener(rootPath) {
|
|
ObjectiveJRuntimeAnalyzer.apply(this, arguments);
|
|
|
|
this.filesToCache = {};
|
|
this.fileCacheBuffer = [];
|
|
this.functionsBuffer = [];
|
|
}
|
|
|
|
ObjectiveJFlattener.prototype = Object.create(ObjectiveJRuntimeAnalyzer.prototype);
|
|
|
|
ObjectiveJFlattener.prototype.buildApplicationJS = function() {
|
|
|
|
this.setupFileCache();
|
|
this.serializeFunctions();
|
|
this.serializeFileCache();
|
|
|
|
var additions = FILE.read(FILE.join(FILE.dirname(module.path), "..", "..", "cappuccino", "lib", "cappuccino", "objj-flatten-additions.js"), { charset:"UTF-8" });
|
|
|
|
var applicationJSs = [];
|
|
|
|
if (this.options.split === 0) {
|
|
var buffer = [];
|
|
buffer.push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);");
|
|
buffer.push(additions);
|
|
buffer.push(this.fileCacheBuffer.join("\n"));
|
|
buffer.push(this.functionsBuffer.join("\n"));
|
|
buffer.push("ObjectiveJ.bootstrap();");
|
|
applicationJSs.push(buffer.join("\n"));
|
|
} else {
|
|
var appFilesCount = this.options.split;
|
|
|
|
var buffers = [];
|
|
for (var i = 0; i <= appFilesCount; i++)
|
|
buffers.push([]);
|
|
|
|
var chunks = this.fileCacheBuffer.concat(this.functionsBuffer).sort(function(chunkA, chunkB) {
|
|
return chunkA.length - chunkB.length;
|
|
});
|
|
|
|
// try to equally distribute the chunks. could be better but good enough for now.
|
|
var n = 0;
|
|
while (chunks.length) {
|
|
buffers[(n++ % appFilesCount) + 1].push(chunks.pop());
|
|
}
|
|
|
|
buffers[0].push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);");
|
|
buffers[0].push(additions);
|
|
|
|
buffers[0].push("var appFilesCount = " + appFilesCount +";");
|
|
buffers[0].push("for (var i = 1; i <= appFilesCount; i++) {");
|
|
buffers[0].push(" var script = document.createElement(\"script\");");
|
|
buffers[0].push(" script.src = \"Application\"+i+\".js\";");
|
|
buffers[0].push(" script.charset = \"UTF-8\";");
|
|
buffers[0].push(" script.onload = function() { if (--appFilesCount === 0) ObjectiveJ.bootstrap(); };");
|
|
buffers[0].push(" document.getElementsByTagName(\"head\")[0].appendChild(script);");
|
|
buffers[0].push("}");
|
|
|
|
buffers.forEach(function(buffer) {
|
|
applicationJSs.push(buffer.join("\n"));
|
|
});
|
|
}
|
|
|
|
return applicationJSs;
|
|
}
|
|
|
|
ObjectiveJFlattener.prototype.serializeFunctions = function() {
|
|
var inlineFunctions = true;//this.options.inlineFunctions;
|
|
|
|
var outputFiles = {};
|
|
|
|
var _cachedExecutableFunctions = {};
|
|
|
|
this.require("objective-j").FileExecutable.allFileExecutables().forEach(function(executable) {
|
|
var path = executable.path();
|
|
|
|
if (inlineFunctions)
|
|
{
|
|
// stringify the function, replacing arguments
|
|
var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK
|
|
|
|
var relative = this.rootPath.relative(path).toString();
|
|
this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");");
|
|
}
|
|
|
|
var bundle = this.context.global.CFBundle.bundleContainingURL(path);
|
|
if (bundle && bundle.infoDictionary())
|
|
{
|
|
var executablePath = bundle.executablePath(),
|
|
relativeToBundle = FILE.relative(FILE.join(bundle.path(), ""), path);
|
|
|
|
if (executablePath)
|
|
{
|
|
if (inlineFunctions)
|
|
{
|
|
// remove the code since we're inlining the functions
|
|
executable._code = "alert("+JSON.stringify(relativeToBundle)+");";
|
|
}
|
|
|
|
if (!outputFiles[executablePath])
|
|
{
|
|
outputFiles[executablePath] = [];
|
|
outputFiles[executablePath].push("@STATIC;1.0;");
|
|
}
|
|
|
|
var fileContents = executable.toMarkedString();
|
|
|
|
outputFiles[executablePath].push("p;" + relativeToBundle.length + ";" + relativeToBundle);
|
|
outputFiles[executablePath].push("t;" + fileContents.length + ";" + fileContents);
|
|
|
|
// stream.print("Adding \0green(" + this.rootPath.relative(path) + "\0) to \0cyan(" + this.rootPath.relative(executablePath) + "\0)");
|
|
}
|
|
}
|
|
else
|
|
CPLog.warn("No bundle (or info dictionary for) " + rootPath.relative(path));
|
|
}, this);
|
|
|
|
for (var executablePath in outputFiles)
|
|
{
|
|
var relative = this.rootPath.relative(executablePath).toString();
|
|
var contents = outputFiles[executablePath].join("");
|
|
this.filesToCache[relative] = contents;
|
|
}
|
|
}
|
|
|
|
ObjectiveJFlattener.prototype.serializeFileCache = function() {
|
|
for (var relative in this.filesToCache) {
|
|
var contents = this.filesToCache[relative];
|
|
print("caching: " + relative + " => " + (contents == null ? 404 : 200));
|
|
if (contents == null)
|
|
this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 404);");
|
|
else
|
|
this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 200, {}, "+JSON.stringify(contents)+");");
|
|
}
|
|
}
|
|
|
|
ObjectiveJFlattener.prototype.setupFileCache = function() {
|
|
var paths = {};
|
|
|
|
UTIL.update(paths, this.requestedURLs);
|
|
|
|
this.options.paths.forEach(function(relativePath) {
|
|
paths[this.rootPath.join(relativePath)] = true;
|
|
}, this);
|
|
|
|
Object.keys(paths).forEach(function(absolute) {
|
|
var relative = this.rootPath.relative(absolute).toString();
|
|
if (relative.indexOf("..") === 0)
|
|
{
|
|
print("skipping (parent of app root): " + absolute);
|
|
return;
|
|
}
|
|
|
|
if (FILE.isFile(absolute))
|
|
{
|
|
// if (this.options.maxCachedSize && FILE.size(absolute) > this.options.maxCachedSize)
|
|
// {
|
|
// print("skipping (larger than "+this.options.maxCachedSize+" bytes): " + absolute);
|
|
// return;
|
|
// }
|
|
|
|
var contents = FILE.read(absolute, { charset : "UTF-8" });
|
|
this.filesToCache[relative] = contents;
|
|
} else {
|
|
this.filesToCache[relative] = null;
|
|
}
|
|
}, this);
|
|
}
|
|
|
|
// "$1" is the matching indentation
|
|
var scriptTagsBefore =
|
|
'$1<script type = "text/javascript">\n'+
|
|
'$1 OBJJ_AUTO_BOOTSTRAP = false;\n'+
|
|
'$1</script>';
|
|
|
|
var scriptTagsAfter =
|
|
'$1<script type="text/javascript" src="Application.js" charset="UTF-8"></script>';
|
|
|
|
// enable CPLog:
|
|
// scriptTagsAfter = '$1<script type="text/javascript">\n$1 CPLogRegister(CPLogConsole);\n$1</script>\n' + scriptTagsAfter;
|
|
|
|
function rewriteMainHTML(indexHTMLPath) {
|
|
if (indexHTMLPath.isFile()) {
|
|
var indexHTML = indexHTMLPath.read({ charset : "UTF-8" });
|
|
|
|
// inline the Application.js if it's smallish
|
|
var applicationJSPath = indexHTMLPath.dirname().join("Application.js");
|
|
if (applicationJSPath.size() < 10*1024) {
|
|
// escape any dollar signs by replacing them with two
|
|
// then indent by splitting/joining on newlines
|
|
scriptTagsAfter =
|
|
'$1<script type="text/javascript">\n'+
|
|
'$1 ' + applicationJSPath.read({ charset : "UTF-8" }).replace(/\$/g, "$$$$").split("\n").join("\n$1 ")+'\n'+
|
|
'$1</script>';
|
|
}
|
|
|
|
// attempt to find Objective-J script tag and add ours
|
|
var newIndexHTML = indexHTML.replace(/([ \t]+)<script[^>]+Objective-J\.js[^>]+>(?:\s*<\/script>)?/,
|
|
scriptTagsBefore+'\n$&\n'+scriptTagsAfter);
|
|
|
|
if (newIndexHTML !== indexHTML) {
|
|
stream.print("\0green(Modified: "+indexHTMLPath+".\0)");
|
|
indexHTMLPath.write(newIndexHTML, { charset : "UTF-8" });
|
|
return;
|
|
}
|
|
} else {
|
|
stream.print("\0yellow(Warning: "+indexHTMLPath+" does not exist. Specify an alternate index HTML file with the --index option.\0)");
|
|
}
|
|
|
|
stream.print("\0yellow(Warning: Unable to automatically modify "+indexHTMLPath + ".\0)");
|
|
stream.print("\nAdd the following before the Objective-J script tag:");
|
|
stream.print(scriptTagsBefore.replace(/\$1/g, " "));
|
|
stream.print("\nAdd the following after the Objective-J script tag:");
|
|
stream.print(scriptTagsAfter.replace(/\$1/g, " "));
|
|
}
|