mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-25 13:07:02 +00:00
A Cappuccino project has always needed to have a flat file structure to be able to compile from source in the web browser. A new dictionary can be added to the project's Info.plist with the key 'CPFileTranslationDictionary'. It should contain all the path translations in the project to allow the Browser to find out where in the file structure the source files are. Check in the AppKit framework for an example. Also, a include list can be added in the Info.plist for the key 'CPCompileIncludeFileArray'. The file in the list will be included by a '#include' statement for each file that is compiled in this project A last new feature is to add macros in the 'OBJJ_COMPILER_FLAGS' global variable. It is usually declared in the index.html file. It is done with the format '-Dmacroname[=macrodefinition]'. An updated Info.plist file in the AppKit framework will allow the AppKit to compile from the source in the web browser. The following things is needed to get this to work: 1. Replace the AppKit folder in your project's Frameworks folder with a copy of the AppKit folder from the Cappuccino project. 2. Add a copy of the built Aristo2.blend folder to the AppKit's Resource folder. 3. Add "-DPLATFORM_DOM", "-DPLATFORM_BROWSER" and optionally "-DDEBUG" to the global variable 'OBJJ_COMPILER_FLAGS' in the project's index.html file. The app load time will increase (about 3 seconds on a 2018 MacBook Pro) as the AppKit framework is compiled from source. The Foundation framework is not yet tested but it might be working by adding info about the file structure in the Info.plist file.
369 lines
13 KiB
Plaintext
Executable File
369 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("narwhal/util");
|
|
|
|
var CACHEMANIFEST = require("objective-j/cache-manifest");
|
|
|
|
var stream = require("narwhal/term").stream;
|
|
var parser = new (require("narwhal/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 defaultThemeName = [CPApplication defaultThemeName],
|
|
bundle = nil;
|
|
|
|
if (defaultThemeName === @"Aristo" || defaultThemeName === @"Aristo2")
|
|
bundle = [CPBundle bundleForClass:[CPApplication class]];
|
|
else
|
|
bundle = [CPBundle mainBundle];
|
|
|
|
var blend = [[CPThemeBlend alloc] initWithContentsOfURL:[bundle pathForResource: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.StaticResource._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, " "));
|
|
}
|