sproutcore
Version:
SproutCore buildtools: Node.js buildtools for SproutCore, written in SproutCore
600 lines (535 loc) • 21.2 kB
JavaScript
/*jshint node:true*/
/*globals BT*/
BT.projectManager = SC.Object.create({
apps: null,
appsUrls: null,
fwclasses: null,
projectPath: null, // this is where the path to the project dir will be set by the loading process
init: function () {
this.apps = {};
this.appsUrls = {};
this.fwclasses = {};
this.watchers = {};
this.fileClasses = {};
this.themeClasses = {};
},
startServer: function () {
BT.Logger.info("Starting development server...");
if (BT.runBenchmarks) SC.Benchmark.end('sc_config_load');
//return;
var me = this,
http = require('http'),
url = require('url'),
path = require('path'),
ips = [],
processname = "SproutCore BuildTools",
hostname = (BT.serverConfig && BT.serverConfig.host) ? BT.serverConfig.host : 'localhost',
port = (BT.serverConfig && BT.serverConfig.port) ? BT.serverConfig.port : 4020,
proxyConf;
if (BT.serverConfig && BT.serverConfig.proxies) {
BT.Logger.info("Proxy config found");
if (SC.typeOf(BT.serverConfig.proxies === SC.T_ARRAY)) {
BT.Logger.debug("Proxy array found");
proxyConf = BT.serverConfig.proxies;
} else if (SC.typeOf(BT.serverConfig.proxies === SC.T_HASH)) {
BT.Logger.debug("Proxy hash found");
proxyConf = [BT.serverConfig.proxies];
}
else {
BT.Logger.error("Invalid proxy config...");
}
}
if (proxyConf) {
BT.Logger.info("Creating proxies");
this._proxies = [];
proxyConf.forEach(function (p) {
BT.Logger.debug("Feeding proxy: " + require('util').inspect(p));
this._proxies.push(BT.Proxy.create(p));
}, this);
//this._proxies = proxyConf.map(BT.Proxy.create);
}
if (BT.serverConfig && !BT.serverConfig.localOnly) {
var interfaces = require('os').networkInterfaces();
Object.keys(interfaces).forEach(function (k) {
interfaces[k].forEach(function (address) {
if (address.family === 'IPv4' && !address.internal) {
ips.push(address.address);
}
});
});
ips = ips.join(", ");
hostname = null;
}
this._activeServerConfig = { // useful for other bits in the manager
host: hostname || 'localhost',
port: port
};
var f = function () {
SC.RunLoop.begin();
me.onRequest.apply(me, arguments);
SC.RunLoop.end();
};
var server = http.createServer(f).on('error', function (err) {
if (err) {
if (err.code === "EOF") {
BT.Logger.error("Error while trying to attach the server. Is the port perhaps taken?");
process.exit(1);
}
else {
BT.Logger.error('Unknown error while trying to attach the server.');
process.exit(1);
}
}
}).listen(port, hostname, function () {
var serverUrl = url.format({ protocol: 'http', hostname: hostname || "*", port: port});
if (ips && ips.length > 0) {
serverUrl += ", ips: " + ips;
}
BT.Logger.debug("Server started in %@".fmt(BT.get('runningTime')));
BT.Logger.info("Server started on " + serverUrl);
if (BT.runBenchmarks) {
SC.Benchmark.end('BT_startup');
BT.Logger.log(SC.Benchmark.report());
}
if (process.mainModule && process.mainModule.filename) {
processname += "[" + path.basename(process.mainModule.filename) + "]";
}
process.title = processname;
// perhaps later
// if(BT.serverConfig && BT.serverConfig.REPL){
// BT.repl.start("scbt>>").context.server = me;
// }
if (BT.outputFiles) {
var outputPath = BT.outputFiles;
if (SC.typeOf(outputPath) !== SC.T_STRING) outputPath = 'debug_output';
outputPath = BT.mkdir(outputPath, outputPath[0] !== '/' ? BT.projectPath : '');
// write out all the files out in a outputPath folder
BT.Logger.info("Writing out files to '%@'".fmt(outputPath));
var pathlib = require('path');
var fslib = require('fs');
Object.keys(me.apps).forEach(function (appname) {
var appdebug = BT.mkdir(appname, outputPath);
var fws = me.apps[appname]._fws;
//var appfiles = me.apps[appname]._fws.getEach('files').flatten();
var appfiles = fws.getEach('scripts').concat(fws.getEach('stylesheets')).concat(fws.getEach('resources')).flatten();
// the line above needs to be able to also pick up the combined files...
appfiles.forEach(function (f) {
if (!f) return;
var relpath = f.get('relativePath');
BT.Logger.debug("Writing out file '%@'".fmt(relpath));
BT.mkdir(pathlib.dirname(relpath), appdebug);
fslib.writeFileSync(pathlib.join(appdebug, relpath), f.get('content'));
}, this);
// write out index
fslib.writeFileSync(pathlib.join(appdebug, "index.html"), me.apps[appname].getPath('indexHtmlFile.content'));
}, this);
BT.Logger.debug("done writing files...");
}
});
if (!(BT.serverConfig && BT.serverConfig.noSocket || BT.noSocket)) { // only disable when no socket is wanted
BT.socketManager = BT.SocketIO.create({ httpServer: server });
}
},
startBuild: function (opts) {
this.set('_buildOpts', opts);
if (BT.runBenchmarks) SC.Benchmark.start("build");
this.invokeNext('_startBuild'); // we need to wait a bit before everything is parsed...
},
_buildOpts: null,
_startBuild: function () {
// will save all apps, unless the app specifically indicates not wanting to be saved.
// The main issue with saving is that there will need to be a few ways in which you'd want
// to build, and in special cases even be able to provide your own build specification and / or procedure
// The build task will get all the apps and retrieve all the files from it
// for now only all apps
// essentially we take all files from all apps, including the index.html
// then we make sure every url is unique, which filters out any double files (in case of a central sproutcore or framework)
// then we save everything to disk
var apps = [];
var opts = this._buildOpts;
if (opts && opts.apps && opts.apps.length > 0) {
opts.apps.forEach(function (appname) {
var app = this.apps[appname];
if (!app) BT.Logger.warn("Could not find app '%@'".fmt(appname));
else {
apps.push(app);
}
}, this);
}
else {
apps = Object.keys(this.apps).map(function (appname) { return this.apps[appname]; }, this);
}
if (apps.length === 0) {
BT.Logger.warn("No apps found with this name");
process.exit(1);
}
//whatever happens, we should only start after everything is in, so we check the files of the apps
//and if not all files have a rawContent somehow, we are not going to build
var unloadedFile = apps.getEach('files').flatten().findProperty('rawContent', null);
if (unloadedFile) {
BT.Logger.warn("Postponing save, the apps are not ready yet: " + unloadedFile.get('path'));
this.invokeNext('_startBuild');
return;
}
BT.Logger.info("Building %@ app(s): %@".fmt(apps.get('length'), apps.getEach('name')));
this.appsLanguagesToBuild = [];
apps.forEach(function (app) {
app.get('languagesToBuild').forEach(function (language) {
this.appsLanguagesToBuild.push({ app: app, language: language });
}, this);
}, this);
this._startBuildAppLanguage();
},
_startBuildAppLanguage: function () {
BT.Logger.debug("_startBuildAppLanguage");
var appLanguage = this.buildAppLanguage = this.appsLanguagesToBuild.shift();
if (!appLanguage) return this._endBuild();
var app = this.appToBuild = appLanguage.app;
app.set('language', appLanguage.language);
this.invokeNext('_doBuildAppLanguage');
},
_doBuildAppLanguage: function () {
var app = this.appToBuild,
language = app.get('language'),
files = [],
allResources = [];
BT.Logger.debug("Start '%@' app build for '%@' language".fmt(app.get("name"), language));
app.get('files').flatten().forEach(function (f) {
allResources.push(f);
});
BT.Logger.trace("Did push allResources");
files.push(app.get('indexHtmlFile'));
BT.Logger.trace("Did add index.html");
app.get('buildFiles').flatten().forEach(function (f) {
var fLang = f.get('language');
if (fLang === 'any' || fLang === language) files.push(f);
});
BT.Logger.debug("Did add buildFiles");
BT.Logger.debug("Calculating file contents");
BT.Logger.debug("File count before filtering: " + files.get('length'));
var urls = [];
files = files.filter(function (f) {
var url = f.get('url');
if (!urls.contains(url)) {
if (f.get('content') === "") return false;
urls.push(url);
return true;
}
});
BT.Logger.debug("File count after filtering: " + files.get('length'));
//files = files.uniqueProperty('url'); // filter based on uniqueness... uniqueProperty doesn't exist...
// save files :)
BT.Logger.info("Saving '%@' files...".fmt(language));
var pathlib = require('path');
var fslib = require('fs');
BT.mkdir(app.get('relativeBuildPath'), BT.projectPath);
var basepath = pathlib.join(BT.projectPath, app.get('relativeBuildPath'));
files.forEach(function (file, i) {
var urlPath = BT.url2Path(file.get('url'));
var p = pathlib.join(basepath, urlPath);
if (fslib.existsSync(p) && !file.isAppCache) {
BT.Logger.info("File (%@ of %@) %@ already written to %@".fmt(i+1, files.length, file.get('url'), p));
return;
}
BT.mkdir(pathlib.dirname(urlPath), basepath);
BT.Logger.debug("About to save %@".fmt(file.get('url')));
fslib.writeFileSync(p, file.parseContent({ lastStageInBuild: true }));
if (file.get('has2x')) {
//replaceInBuild
//return this.parseContent({ x2: true });
urlPath = BT.url2Path(file.get('url2x'));
p = pathlib.join(basepath, urlPath);
fslib.writeFileSync(p, file.parseContent({ lastStageInBuild: true, x2: true }));
}
var resourceDeps = file.get('resourceDependencies');
if (resourceDeps && resourceDeps.get('length') > 0) {
resourceDeps.forEach(function (dep) {
var depPath = dep.get('path'),
depUrl = dep.get('url');
BT.mkdir(pathlib.dirname(depUrl), basepath);
if (fslib.statSync(depPath).isFile()) {
fslib.writeFileSync(pathlib.join(basepath, depUrl), fslib.readFileSync(depPath));
}
else {
BT.Logger.error("The file %@ doesn't exist".fmt(depPath));
}
}, this);
}
BT.Logger.info("File (%@ of %@) %@ written to %@".fmt(i+1, files.length, file.get('url'), p));
}, this);
this._startBuildAppLanguage();
},
_endBuild: function () {
BT.Logger.info("Build time %@".fmt(BT.get('runningTime')));
if (BT.runBenchmarks) {
SC.Benchmark.end("build");
BT.Logger.log(SC.Benchmark.report());
}
if (!this._buildOpts.hasREPL) process.exit(0);
},
targets: function () {
var appnames = Object.keys(this.apps);
// make sure that test runner is first
if (appnames.contains("sproutcore/tests")) {
appnames.sort(function (f, s) {
if (f === "sproutcore/tests") return -1;
else return 1;
});
}
var apps = appnames.map(function (a) { return this.apps[a]; }, this);
var targets = [];
apps.forEach(function (app) {
app._allFws.forEach(function (fw) {
if (fw.isApp) return; // don't add apps, they are already in...
var t = fw.get('targets');
if (targets.findProperty('name', t.name)) return; // don't do a thing if already exists
targets.push(t);
if (fw.isWrapperFramework) { //
t.parent = "";
// we can assume here all the deps have passed now
targets.forEach(function (tg) {
if (tg !== t && tg.name !== t.name && tg.name.indexOf(t.name) > -1 && !tg.parent) {
tg.parent = t.name;
}
});
}
});
targets.push(app.get('targets'));
});
return JSON.stringify(targets);
//return JSON.stringify(apps.getEach('targets').flatten());
}.property(),
onRequest: function (req, res) {
var urllib = require('url');
//var path = urllib.parse(req.url).pathname.slice(1);
var path = urllib.parse(req.url).pathname;
var hasServed = false;
var appsUrls = this.appsUrls;
var appurls = Object.keys(appsUrls);
var i;
BT.Logger.debug("http request for: " + path);
if (path === "/") { // root index
this.serverIndex(req, res);
hasServed = true;
}
if (path === "/debug_broadcaster") {
if (BT.socketManager && req.method == 'POST') {
var body = '';
req.on('data', function(data) { body += data; });
req.on('end', function() { BT.socketManager.broadcast(body); });
}
hasServed = true;
}
if (!hasServed) {
BT.Logger.trace(path + " is not an index, trying index file");
// now detection of app root
i = appurls.indexOf(path.slice(1));
if (i > -1) {
var index = appsUrls[appurls[i]].getPath('indexHtmlFile.content');
if (index) {
res.writeHead(200);
res.write(index);
res.end();
hasServed = true;
}
}
}
if (!hasServed) {
BT.Logger.trace(path + " is not an indexfile, trying targets.json");
if (path === "/sc/targets.json") { // specific to show all targets in this system
res.writeHead(200, {
'Content-Type': "application/json"
});
res.write(this.get('targets') || "");
res.end();
hasServed = true;
}
}
if (!hasServed) {
BT.Logger.trace(path + " is not targets.json, trying app file");
appurls.forEach(function (a) {
if (hasServed) return;
if (path.slice(1).indexOf(a) === 0) {
var c = appsUrls[a].fileFor(path);
if (c) {
res.writeHead(200, {
//'Content-Length': c.length,
'Content-Type': c.get('contentType')
});
var content = c.contentForPath(path);
if (!content || (typeof content !== 'string' && !(content instanceof Buffer))) {
BT.Logger.warn("file %@ doesn't have content?".fmt(c.get('path')));
BT.Logger.debug(require('util').inspect(content));
res.write("");
}
else res.write(content);
// res.write(c.get('content'));
res.end();
hasServed = true;
}
}
}, this);
}
if (!hasServed && this._proxies) {
BT.Logger.trace(path + " is not app file, trying proxy");
// try one by one
var hasProxied = false;
var numProxies = this._proxies.length;
this._proxies.forEach(function (proxy) {
BT.Logger.trace("trying path with proxy " + proxy.host + ":" + proxy.port);
if (!hasProxied) {
hasProxied = proxy.process(req, res);
BT.Logger.trace("proxy " + proxy.host + ":" + proxy.port + " returns " + hasProxied);
}
});
if (hasProxied) hasServed = true;
}
if (!hasServed) {
BT.Logger.trace("nothing matches with " + path + " so, returning 404");
res.writeHead(404);
res.write("File not found");
res.end();
}
},
serverIndex: function (req, res) { // automated list of apps
var ejs = require('ejs');
var pathlib = require('path');
var fslib = require('fs');
var template = fslib.readFileSync(pathlib.join(BT.btPath, "templates", "server_index.ejs"));
var ret = ejs.render(template.toString(), {
BT: BT,
apps: Object.keys(this.apps).map(function (appname) { return this.apps[appname]; }, this)
});
// we have to slow down the index page, because of the SC.Request test which uses the root to test
// timeout responses on. Problem is that the response is sufficiently fast to make the tests fail
// even when defining a timeout of 1ms in SC.Request.
// res.writeHead(200);
// res.write(ret.join("\n"));
// res.end();
setTimeout(function () {
res.writeHead(200);
res.write(ret);
res.end();
}, 15);
},
addApp: function (app) {
var appname = app.get('name');
if (!appname) throw new Error("An app should always carry a name!");
BT.Logger.info("Adding app '%@'".fmt(appname));
if (!this.apps[appname]) this.apps[appname] = app;
var url = app.get('url');
if (!this.appsUrls[url]) this.appsUrls[url] = app;
},
addFramework: function (fwclass) {
//if(!this.fwclasses) this.fwclasses = SC.Set.create();
var fwref = fwclass.prototype.ref;
BT.Logger.trace('registering framework: ' + fwref + " from " + fwclass.prototype.path);
this.fwclasses[fwref] = fwclass;
//BT.Logger.debug('keys in fwcache: ' + BT.util.inspect(Object.keys(this.fwclasses)));
},
getFrameworkClass: function (fwref) {
var pathlib = require('path');
BT.Logger.traceGroup('trying to find class for ' + fwref);
var ret = this.fwclasses[fwref];
if (!ret) { // try to actively locate the fw
BT.Logger.trace('class not found, actively locating...');
var relref = BT._resolveReference(fwref, "framework");
// essentially there are two locations for frameworks: globally (inside here)
// or inside the project dir. BT._resolveReference will return a relative url
// so we have to prepend it with the current projDir
var d = pathlib.join(BT.projectPath, relref, "sc_config");
//var d = pathlib.join(relref,"sc_config");
BT.Logger.trace("trying to runConfig: " + d);
var loadresult = sc_require(d);
BT.Logger.trace("loadresult: " + require('util').inspect(loadresult));
if (!SC.ok(loadresult) && loadresult.code === "ENOENT") {
BT.Logger.trace("not present in project, trying to load from global folder");
// try to load from here:
d = pathlib.join(BT.btPath, relref, "sc_config");
loadresult = sc_require(d);
if (loadresult.isError) {
throw loadresult;
}
}
else if (loadresult.isError) throw loadresult;
//BT.runConfig(d); // should auto-register
// try {
// sc_require(d);
// }
// catch(e){
// if(e.code === 'ENOENT'){ // code not found, try inside here...
// }
// }
ret = this.fwclasses[fwref]; // should now contain the fw
}
else {
BT.Logger.trace('class found...');
}
BT.Logger.traceGroupEnd();
return ret;
},
fileClasses: null,
registerFileClass: function (ext, klass) {
this.fileClasses[ext] = klass;
},
fileClassFor: function (ext) {
return this.fileClasses[ext];
},
extensions: function () {
return Object.keys(this.fileClasses);
}.property(),
scriptExtensions: function () {
var ret = [];
Object.keys(this.fileClasses).forEach(function (ext) {
if (this.fileClasses[ext].isScript) ret.push(ext);
});
return ret;
}.property(),
stylesheetExtensions: function () {
var ret = [];
Object.keys(this.fileClasses).forEach(function (ext) {
if (this.fileClasses[ext].isStylesheet) ret.push(ext);
});
return ret;
}.property(),
resourceExtensions: function () {
var ret = [];
Object.keys(this.fileClasses).forEach(function (ext) {
if (this.fileClasses[ext].isResource) ret.push(ext);
});
return ret;
}.property(),
templateExtensions: function () {
var ret = [];
Object.keys(this.fileClasses).forEach(function (ext) {
if (this.fileClasses[ext].isTemplate) ret.push(ext);
});
return ret;
}.property(),
addTheme: function (k) {
var ref = k.prototype.ref;
this.themeClasses[ref] = k;
},
getThemeClass: function (ref) {
var pathlib = require('path');
var ret = this.themeClasses[ref];
if (!ret) { // try to actively locate the fw
var relref = BT._resolveReference(ref, "theme");
// essentially there are two locations for themes: globally (inside here)
// or inside the project dir. BT._resolveReference will return a relative url
// so we have to prepend it with the current projDir
var d = pathlib.join(BT.projectPath, relref, "sc_config");
BT.Logger.info("trying to runConfig: " + d);
var loadresult = sc_require(d);
if (loadresult.isError && loadresult.code === "ENOENT") {
// try to load from here:
d = pathlib.join(BT.btPath, ref, "sc_config");
loadresult = sc_require(d);
if (loadresult.isError) throw loadresult;
}
else if (loadresult.isError) throw loadresult;
ret = this.themeClasses[ref]; // should now contain the fw
}
return ret;
}
});