diff --git a/.gitmodules b/.gitmodules
index daeb1dd..220494d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -10,6 +10,3 @@
[submodule "modules/squill"]
path = modules/squill
url = https://github.com/gameclosure/squill
-[submodule "node_modules/jsio"]
- path = node_modules/jsio
- url = https://github.com/gameclosure/js.io
diff --git a/node_modules/jsio b/node_modules/jsio
deleted file mode 160000
index a4a90e7..0000000
--- a/node_modules/jsio
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit a4a90e715d1ab2798a5ea4e3ca82c3c70a71277a
diff --git a/package.json b/package.json
index 244ac92..f5a3394 100644
--- a/package.json
+++ b/package.json
@@ -14,10 +14,9 @@
"fs-extra": "^0.18.4",
"glob": "^5.0.2",
"graceful-fs": "^3.0.2",
- "image-size": "0.3.2",
- "jsio": "^2.2.0",
+ "image-size": "^0.3.2",
+ "jsio": "git+https://github.com/gameclosure/js.io#feat-noCompile",
"mime": "1.2.11",
- "mkdirp": "0.5.0",
"nib": "1.0.3",
"optimist": "^0.6.1",
"printf": "0.2.0",
@@ -26,7 +25,8 @@
"stylus": "0.48.1",
"uglify-js": "^2.4.17",
"vinyl": "^0.4.6",
- "vinyl-fs": "^1.0.0"
+ "vinyl-fs": "^1.0.0",
+ "resolve": "1.1.6"
},
"scripts": {
"preinstall": "sh scripts/preinstall.sh"
diff --git a/src/build/browser/browser-static/bootstrap.js b/src/build/browser/browser-static/bootstrap.js
index 2c55cf3..ff8caa5 100644
--- a/src/build/browser/browser-static/bootstrap.js
+++ b/src/build/browser/browser-static/bootstrap.js
@@ -4,36 +4,6 @@ function bootstrap(initialImport, target) {
var loc = w.location;
var q = loc.search + loc.hash;
- // check to see if we need chrome frame
- // if (target && (target=="desktop" || target=="facebook") && /MSIE/i.test(navigator.userAgent) && !d.createElement('canvas').getContext) {
- // var chromeframe_url = 'chromeframe.html' + (loc.search ? loc.search + "&" : "?") + "target="+ target;
- // bootstrap = function() {};
- // try {
- // var obj = new ActiveXObject('ChromeTab.ChromeFrame');
- // if (!obj) {
- // throw "bad object";
- // }
- // loc.replace(chromeframe_url);
- // } catch(e) {
- // w.onload = function() {
- // var e = d.createElement('script');
- // e.async = true;
- // e.src = "http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js";
- // e.onreadystatechange= function () {
- // if (this.readyState == 'loaded') {
- // CFInstall.check({
- // mode: "overlay",
- // oninstall: function() { loc.replace(chromeframe_url) },
- // url: "http://www.google.com/chromeframe/eula.html?user=true"
- // });
- // }
- // }
- // d.getElementsByTagName('head')[0].appendChild(e);
- // }
- // }
- // return;
- // }
-
// for tracking when the page started loading
w.__initialTime = +new Date();
@@ -75,18 +45,6 @@ function bootstrap(initialImport, target) {
var mobile = (/(iPod|iPhone|iPad)/i.test(ua) ? 'ios' : /BlackBerry/.test(ua) ? 'blackberry' : /Mobile Safari/.test(ua) ? 'android' : '');
var isKik = /Kik\/\d/.test(ua);
- // if (loc.search.match(/exportSettings=true/)) {
- // // just export localStorage
- // exportSettings();
- // } else if (mobile != 'blackberry' && !w.CONFIG.noRedirect) {
- // // redirect based on device
- // if (mobile && target != 'browser-mobile') {
- // return loc.replace('//' + loc.host + '/browser-mobile/' + loc.hash);
- // } else if (!mobile && target == 'browser-mobile') {
- // return loc.replace('//' + loc.host + '/browser-desktop/' + loc.hash);
- // }
- // }
-
// set the viewport
if (mobile == 'ios') {
// Using initial-scale on android makes everything blurry! I think only IOS
@@ -134,11 +92,37 @@ function bootstrap(initialImport, target) {
var loaded = false;
w._continueLoad = function() {
- if (!loaded) {
- loaded = true;
- var el = d.createElement('script');
- el.src = target + '.js';
- d.getElementsByTagName('head')[0].appendChild(el);
+ var loadTargetJS = function() {
+ if (!loaded) {
+ loaded = true;
+ // Include the game code
+ var el = d.createElement('script');
+ el.src = target + '.js';
+ d.getElementsByTagName('head')[0].appendChild(el);
+ }
+ };
+
+ var _continueLoadCallback;
+
+ w.addEventListener('message', function(event) {
+ if (event.data === 'partialLoadContinue') {
+ if (_continueLoadCallback) {
+ _continueLoadCallback();
+ _continueLoadCallback = undefined;
+ }
+ }
+ });
+ // Preload suggestions and then tell the parent bootstrapping is complete
+ jsio.__env.preloadModules(function() {
+ w.parent.postMessage('bootstrapping', '*');
+ });
+
+ var partialLoadKey = jsio.__env.getNamespace('partialLoad');
+ if (localStorage && localStorage.getItem(partialLoadKey)) {
+ localStorage.removeItem(partialLoadKey);
+ _continueLoadCallback = loadTargetJS;
+ } else {
+ loadTargetJS();
}
};
@@ -173,16 +157,6 @@ function bootstrap(initialImport, target) {
if (mobile && supportedOrientations) {
checkOrientation();
- // if (!orientationOk) {
- // var el = d.body.appendChild(d.createElement('div'));
- // el.innerHTML = 'please rotate your phone
\u21bb';
- // var width = d.body.offsetWidth;
- // el.style.cssText = 'opacity:0;z-index:9000;color:#FFF;background:rgba(40,40,40,0.8);border-radius:25px;text-align:center;padding:' + width / 10 + 'px;font-size:' + width / 20 + 'px;position:absolute;left:50%;width:' + width * 5 / 8 + 'px;margin-left:-' + width * 5 / 16 + 'px;margin-top:80px;pointer-events:none';
- // w.addEventListener('resize', function () {
- // checkOrientation();
- // el.style.display = orientationOk ? 'none': 'block';
- // });
- // }
}
var appCache = window.applicationCache;
@@ -190,55 +164,14 @@ function bootstrap(initialImport, target) {
appCache.addEventListener(evt, handleCacheEvent, false);
});
- // status 0 == UNCACHED
- // if (appCache.status) {
-
- // appCache.update(); // Attempt to update the user's cache.
- // }
-
function handleCacheEvent(evt) {
if (evt.type == 'updateready') {
- // var el = d.body.appendChild(d.createElement('div'));
- // el.style.cssText = 'opacity:0;position:absolute;z-index:9900000;top:-20px;margin:0px auto'
- // + 'height:20px;width:200px;'
- // + '-webkit-border-radius:0px 0px 5px 5px;'
- // + '-webkit-transition:all 0.7s ease-in-out;'
- // + '-webkit-transform:scale(' + w.devicePixelRatio + ');'
- // + '-webkit-transform-origin:50% 0%;'
- // + '-webkit-box-shadow:0px 2px 3px rgba(0, 0, 0, 0.4);'
- // + 'background:rgba(0,0,0,0.7);color:#FFF;'
- // + 'padding:10px 15px;'
- // + 'font-size: 15px;';
- // + 'text-align: center;';
- // + 'cursor:pointer;';
-
- // if (CONFIG.embeddedFonts && CONFIG.embeddedFonts.length) {
- // el.style.fontFamily = CONFIG.embeddedFonts[0];
- // }
-
- // el.innerText = 'game updated! tap here';
- // el.style.left = (d.body.offsetWidth - 200) / 2 + 'px';
-
- // el.setAttribute('noCapture', true); // prevent DevKit from stopping clicks on this event
- // el.addEventListener('click', reload, true);
- // el.addEventListener('touchstart', reload, true);
-
- // setTimeout(function () {
- // el.style.top='0px';
- // el.style.opacity='1';
- // }, 0);
-
- // setTimeout(function () {
- // el.style.top='-20px';
- // el.style.opacity='0';
- // }, 30000);
console.log("update ready");
// reload immediately if splash is still visible
var splash = d.getElementById('_GCSplash');
if (splash && splash.parentNode) {
try { appCache.swapCache(); } catch (e) {}
- //location.reload();
}
}
}
@@ -284,6 +217,6 @@ function bootstrap(initialImport, target) {
if (h > min) { increased = true; }
min = h;
// }
- }, 50);
+ }, 20);
}
}
diff --git a/src/build/browser/html.js b/src/build/browser/html.js
index 2a65040..1bcd630 100644
--- a/src/build/browser/html.js
+++ b/src/build/browser/html.js
@@ -1,12 +1,12 @@
var path = require('path');
var fs = require('fs');
-var stylus = require('stylus');
-var nib = require('nib');
var printf = require('printf');
var JSCompiler = require('../common/jsCompiler').JSCompiler;
var getBase64Image = require('./datauri').getBase64Image;
+var fileGenerator = require('../common/fileGenerator');
+
var TARGET_APPLE_TOUCH_ICON_SIZE = 152;
exports.IndexHTML = Class(function () {
@@ -76,14 +76,64 @@ exports.IndexHTML = Class(function () {
};
});
+var renderStylus = function(cssString, shouldCompress) {
+ // Only import if used, otherwise it takes forever
+ var stylus = require('stylus');
+ var nib = require('nib');
+
+ // process the stylus into css
+ var stylusRenderer = stylus(cssString)
+ .set('compress', shouldCompress)
+ .use(nib());
+ return stylusRenderer.render();
+};
+
exports.GameHTML = Class(function () {
- this.init = function () {
+ this.init = function (config) {
this._css = [];
this._js = [];
+
+ this._config = config;
+ this._binPath = path.join(config.outputPath, 'bin', 'html');
+ };
+
+ /**
+ * @param {string} css Path to some stylus file
+ */
+ // TODO: Actually should be "addStylus"
+ this.addCSS = function (name, css) {
+ var dest = path.join(this._binPath, 'css', name.replace(/\//g, '_'));
+ var shouldCompress = this._config.compress;
+
+ this._css.push(fileGenerator.dynamic(
+ css,
+ dest,
+ function(cb) {
+ cb(null, renderStylus(css, shouldCompress));
+ }
+ ));
};
- this.addCSS = function (css) { this._css.push(css); };
- this.addJS = function (js) { this._js.push(js); };
+ this.addCSSFile = function(cssPath) {
+ var dest = path.join(this._binPath, 'css', cssPath.replace(/\//g, '_'));
+ var shouldCompress = this._config.compress;
+
+ this._css.push(fileGenerator(
+ cssPath,
+ dest,
+ function(cb) {
+ fs.readFile(cssPath, 'utf8', function(err, cssSrc) {
+ if (err) { reject(err); return; }
+
+ cb(null, renderStylus(cssSrc, shouldCompress));
+ });
+ }
+ ));
+ };
+
+ this.addJS = function (js) {
+ this._js.push(js);
+ };
// return smallest icon size larger than targetSize or the largest icon if
// none are larger than targetSize
@@ -126,16 +176,16 @@ exports.GameHTML = Class(function () {
this.generate = function (api, app, config) {
var logger = api.logging.get('build-html');
- var css = this._css.join('\n');
+ var theCss = Promise.reduce(this._css, function(total, src) {
+ return total += src + '\n';
+ }, '');
+
var js = this._js.join(';');
- var stylusRenderer = stylus(css)
- .set('compress', config.compress)
- .use(nib());
var jsCompiler = new JSCompiler(api, app);
- var renderCSS = Promise.promisify(stylusRenderer.render, stylusRenderer);
var compileJS = Promise.promisify(jsCompiler.compress, jsCompiler);
+
return Promise.all([
- renderCSS(),
+ theCss,
config.compress
? compileJS('[bootstrap]', js, {showWarnings: false})
: js
diff --git a/src/build/browser/index.js b/src/build/browser/index.js
index 59b24d3..ff6f276 100644
--- a/src/build/browser/index.js
+++ b/src/build/browser/index.js
@@ -14,15 +14,6 @@
* along with the Game Closure SDK. If not, see .
*/
var path = require('path');
-var printf = require('printf');
-var fs = require('graceful-fs');
-var File = require('vinyl');
-var vfs = require('vinyl-fs');
-// var newer = require('gulp-newer');
-var slash = require('slash');
-var streamFromArray = require('stream-from-array');
-
-var readFile = Promise.promisify(fs.readFile);
var logger;
var INITIAL_IMPORT = 'devkit.browser.launchClient';
@@ -53,6 +44,19 @@ exports.configure = function (api, app, config, cb) {
};
exports.build = function (api, app, config, cb) {
+
+ var printf = require('printf');
+ var fs = require('graceful-fs');
+ var File = require('vinyl');
+ var vfs = require('vinyl-fs');
+ // var newer = require('gulp-newer');
+ var slash = require('slash');
+ var streamFromArray = require('stream-from-array');
+ var fileGenerator = require('../common/fileGenerator');
+ var glob = require('glob');
+
+ var readFile = Promise.promisify(fs.readFile);
+
logger = api.logging.get('build-browser');
var isMobile = (config.target !== 'browser-desktop');
@@ -60,24 +64,32 @@ exports.build = function (api, app, config, cb) {
var resources = require('../common/resources');
var CSSFontList = require('./fonts').CSSFontList;
var JSConfig = require('../common/jsConfig').JSConfig;
- var JSCompiler = require('../common/jsCompiler').JSCompiler;
-
- var sprite = require('../common/spriter')
- .sprite
- .bind(null, api, app, config);
+ var JSCompiler = require('../common/jsCompiler');
+
+ var sprite = null;
+ if (config.spriteImages && !config.isSimulated) {
+ sprite = require('../common/spriter')
+ .sprite
+ .bind(null, api, app, config);
+ } else {
+ sprite = require('../common/spritesheetMapGenerator')
+ .sprite
+ .bind(null, api, app, config);
+ }
var html = require('./html');
- var gameHTML = new html.GameHTML();
+ var gameHTML = new html.GameHTML(config);
var fontList = new CSSFontList();
var jsConfig = new JSConfig(api, app, config);
- var jsCompiler = new JSCompiler(api, app, config, jsConfig);
+ var jsCompiler = new JSCompiler.JSCompiler(api, app, config, jsConfig);
var compileJS = Promise.promisify(jsCompiler.compile, jsCompiler);
function getPreloadJS() {
- // get preload JS
if (/^native/.test(config.target)) {
- return Promise.resolve('jsio=function(){window._continueLoad()}');
+ var preloadSrc = '(window.jsio) ? (window._continueLoad()) : (jsio=function(){window._continueLoad()})';
+
+ return Promise.resolve(preloadSrc);
}
var isLiveEdit = (config.target === 'live-edit');
@@ -100,36 +112,38 @@ exports.build = function (api, app, config, cb) {
};
}
- return compileJS({
+ var compileOpts = {
initialImport: 'devkit.browser.bootstrap.launchBrowser',
appendImport: false,
preCompress: config.preCompressCallback
- });
+ };
+ return compileJS(compileOpts);
}
var baseDirectory = config.outputResourcePath;
resources.getDirectories(api, app, config)
.then(function (directories) {
- return [
- resources.getFiles(baseDirectory, directories),
+ var compileOpts = {
+ env: 'browser',
+ initialImport: [INITIAL_IMPORT].concat(config.imports).join(', '),
+ appendImport: false,
+ includeJsio: !config.excludeJsio,
+ debug: config.scheme === 'debug',
+ preCompress: config.preCompressCallback
+ };
+
+ return Promise.all([
+ config.isSimulated ? resources.getFiles(baseDirectory, directories) : [],
readFile(getLocalFilePath('../../clientapi/browser/cache-worker.js'), 'utf8'),
- getPreloadJS(),
- readFile(STATIC_BOOTSTRAP_CSS, 'utf8'),
+ config.isSimulated ? '' : getPreloadJS(),
readFile(STATIC_BOOTSTRAP_JS, 'utf8'),
isLiveEdit && readFile(STATIC_LIVE_EDIT_JS, 'utf8'),
config.spritesheets || config.spriteImages !== false && sprite(directories),
- compileJS({
- env: 'browser',
- initialImport: [INITIAL_IMPORT].concat(config.imports).join(', '),
- appendImport: false,
- includeJsio: !config.excludeJsio,
- debug: config.scheme === 'debug',
- preCompress: config.preCompressCallback
- })
- ];
+ config.isSimulated ? '' : compileJS(compileOpts)
+ ]);
})
- .spread(function (files, cacheWorkerJS, preloadJS, bootstrapCSS, bootstrapJS,
+ .spread(function (files, cacheWorkerJS, preloadJS, bootstrapJS,
liveEditJS, spriterResult, jsSrc) {
logger.log('Creating HTML and JavaScript...');
@@ -150,19 +164,23 @@ exports.build = function (api, app, config, cb) {
var tasks = [];
- // We need to generate a couple different files if this is going to be a
- gameHTML.addCSS(bootstrapCSS);
- gameHTML.addCSS(fontList.getCSS({
+ // ----- ----- GENERATE CSS ----- ----- //
+
+ gameHTML.addCSSFile(STATIC_BOOTSTRAP_CSS);
+
+ gameHTML.addCSS('fontList', fontList.getCSS({
embedFonts: config.browser.embedFonts,
formats: require('./fonts').getFormatsForTarget(config.target)
}));
if (config.browser.canvas.css) {
- gameHTML.addCSS('#timestep_onscreen_canvas{'
+ gameHTML.addCSS('canvas', '#timestep_onscreen_canvas{'
+ config.browser.canvas.css
+ '}');
}
+ // ----- ----- GENERATE JS ----- ----- //
+
gameHTML.addJS(jsConfig.toString());
gameHTML.addJS(bootstrapJS);
gameHTML.addJS(printf('bootstrap("%(initialImport)s", "%(target)s")', {
@@ -173,21 +191,59 @@ exports.build = function (api, app, config, cb) {
liveEditJS && gameHTML.addJS(liveEditJS);
+ // ----- ----- //
+
var hasWebAppManifest = !!config.browser.webAppManifest;
if (hasWebAppManifest) {
config.browser.headHTML.push('');
}
+ if (config.isSimulated) {
+ // Write the jsio.js and jsio_path.js files
+ var binPath = path.join(config.outputPath, 'bin');
+ tasks.push(
+ JSCompiler.writeJsioBin(binPath)
+ );
+
+ var pathAndCache = JSCompiler.getPathAndCache(app, config);
+ // Walk module dirs and add to the path cache (for fewer client side 404's)
+ pathAndCache.path.forEach(function(modulePath) {
+ // Note: We could probably just look for folders 1 level deep
+ var files = glob.sync(path.join(app.paths.root, modulePath, '**/*.js'));
+ files.forEach(function(filePath) {
+ var key = path.relative(path.join(app.paths.root, modulePath), filePath);
+ key = key.replace(/^\/|\.js$/g, ''); // replace leading slash and trailing .js
+ key = key.split('/', 1)[0]; // Get the top level
+
+ if (!pathAndCache.pathCache[key]) {
+ pathAndCache.pathCache[key] = path.join(modulePath, key);
+ }
+ });
+ });
+ // TODO: THE PATH MAP SHOULD PROBABLY COME IN ON THE API OBJECT
+ var _pathMap = {};
+ _pathMap[api.paths.devkit] = '/devkit';
+ tasks.push(
+ JSCompiler.writeJsioPath({
+ cwd: app.paths.root,
+ path: pathAndCache.path,
+ pathCache: pathAndCache.pathCache,
+ pathMap: _pathMap,
+ binPath: binPath
+ })
+ );
+
+ config.browser.headHTML.push('');
+ config.browser.headHTML.push('');
+ }
+
var hasIndexPage = !isMobile;
tasks.push(gameHTML.generate(api, app, config)
.then(function (html) {
- files.push(new File({
- base: baseDirectory,
- path: path.join(baseDirectory, hasIndexPage
+ var destPath = path.join(baseDirectory, hasIndexPage
? 'game.html'
- : 'index.html'),
- contents: new Buffer(html)
- }));
+ : 'index.html')
+ return fileGenerator.dynamic(html, destPath);
}));
if (hasIndexPage) {
@@ -212,15 +268,6 @@ exports.build = function (api, app, config, cb) {
.filter(addToInlineCache);
})
.then(function (files) {
- files.push(new File({
- base: baseDirectory,
- path: path.join(baseDirectory, config.target + '.js'),
- contents: new Buffer('NATIVE=false;'
- + 'CACHE=' + JSON.stringify(inlineCache) + ';\n'
- + jsSrc + ';'
- + 'jsio("import ' + INITIAL_IMPORT + '");')
- }));
-
files.forEach(function (file) {
if (file.history.length > 1) {
sourceMap[slash(file.relative)] = file.history[0];
@@ -311,22 +358,24 @@ exports.build = function (api, app, config, cb) {
files.push(f);
});
- return {
- files: files,
- spritesheets: spriterResult
- };
+ // https://github.com/petkaantonov/bluebird/issues/332
+ logger.log('Writing ' + files.length + ' files...');
+ return new Promise(function (resolve, reject) {
+ streamFromArray.obj(files)
+ // .pipe(newer(baseDirectory))
+ .pipe(vfs.dest(baseDirectory))
+ .on('end', resolve)
+ .on('error', reject);
+ });
+ })
+ .then(function() {
+ var src = 'NATIVE=false;' +
+ 'CACHE=' + JSON.stringify(inlineCache) + ';\n' +
+ jsSrc + ';' +
+ 'jsio("import ' + INITIAL_IMPORT + '");';
+ var destPath = path.join(baseDirectory, config.target + '.js');
+ return fileGenerator.dynamic(src, destPath);
});
})
- .tap(function (buildResult) {
- // https://github.com/petkaantonov/bluebird/issues/332
- logger.log('Writing files...');
- return new Promise(function (resolve, reject) {
- streamFromArray.obj(buildResult.files)
- // .pipe(newer(baseDirectory))
- .pipe(vfs.dest(baseDirectory))
- .on('end', resolve)
- .on('error', reject);
- });
- })
.nodeify(cb);
};
diff --git a/src/build/common/fileGenerator.js b/src/build/common/fileGenerator.js
new file mode 100644
index 0000000..e8943b3
--- /dev/null
+++ b/src/build/common/fileGenerator.js
@@ -0,0 +1,174 @@
+
+var fs = require('fs-extra');
+var path = require('path');
+
+var crypto = require('crypto');
+
+// Only rewrite files if needed
+
+var hashString = function(str) {
+ var md5sum = crypto.createHash('md5');
+ md5sum.update(str);
+ return md5sum.digest('hex');
+};
+
+var runGenerator = function(opts, cb) {
+ var doWrite = function(err, src) {
+ fs.mkdirp(path.dirname(opts.outputPath), function(err) {
+ if (err) { cb(err); return; }
+
+ fs.writeFile(opts.outputPath, src, function(err) {
+ if (err) { cb(err); return; }
+
+ // For dynamic calls the hash should be that of the input string, not the output file
+ if (opts.useInputHash) {
+ fs.writeFile(opts.outputHashPath, opts.inputHash, function(err) {
+ if (err) { cb(err); return; }
+ cb(null, src);
+ });
+ } else {
+ cb(null, src);
+ }
+ });
+
+ });
+ };
+
+ var useOldOutput = function() {
+ // Old one is still good, just read it
+ fs.readFile(opts.outputPath, 'utf8', function(err, src) {
+ if (err) { cb(err); return; }
+
+ cb(null, src);
+ });
+ }
+
+ var checkModifiedTimes = function() {
+ fs.stat(opts.sourcePath, function(err, srcStat) {
+ if (err) { cb(err); return; }
+
+ fs.stat(opts.outputPath, function(err, existingStat) {
+ if (err) { cb(err); return; }
+
+ if (existingStat.mtime > srcStat.mtime) {
+ useOldOutput();
+ return;
+ }
+
+ opts.generateFn(doWrite);
+ });
+ });
+ };
+
+ var checkInputHash = function() {
+ // Cheating: add to opts so we have it inside of doWrite
+ opts.useInputHash = true;
+ opts.outputHashPath = opts.outputPath + '.hash';
+ opts.inputHash = hashString(opts.sourceContents);
+
+ // Get the output hash
+ fs.exists(opts.outputHashPath, function(exists) {
+ if (exists) {
+
+ // Check the hashes
+ fs.readFile(opts.outputHashPath, 'utf-8', function(err, outputHash) {
+ if (err) { cb(err); return; }
+
+ if (opts.inputHash === outputHash) {
+ useOldOutput();
+ return;
+ }
+
+ opts.generateFn(doWrite);
+ });
+
+ } else {
+ opts.generateFn(doWrite);
+ }
+ });
+ };
+
+ // Check if the output exists
+ fs.exists(opts.outputPath, function(exists) {
+ if (exists) {
+ if (opts.sourcePath !== undefined) {
+ // It does, check the modified times
+ checkModifiedTimes();
+ } else if (opts.sourceContents !== undefined) {
+ checkInputHash();
+ } else {
+ throw new Error('unknown input');
+ }
+ } else {
+ opts.generateFn(doWrite);
+ }
+ });
+};
+
+
+/** take an input file (source), and generate the output with generateFn. finally
+run cb with cb(err, src). Will only run generateFn if the output file is older than
+the source file */
+module.exports = function(source, output, generateFn, cb) {
+ // If there is no callback, make it a promise
+ var def;
+ if (cb === undefined) {
+ def = Promise.defer();
+ cb = function(err, src) {
+ if (err) {
+ def.reject(err);
+ } else {
+ def.resolve(src);
+ }
+ };
+ }
+
+ runGenerator({
+ sourcePath: source,
+ outputPath: output,
+ generateFn: generateFn
+ }, cb);
+
+ return def ? def.promise : undefined;
+};
+
+module.exports.runGenerator = runGenerator;
+
+/** Use this when the source doesn't live on disk, but is generated dynamically.
+Will only run the generateFn if the sourceContents hash does not match the bin output hash */
+module.exports.dynamic = function(sourceContents, output, generateFn, cb) {
+ var def;
+ if (cb === undefined) {
+ def = Promise.defer();
+ cb = function(err, src) {
+ if (err) {
+ def.reject(err);
+ } else {
+ def.resolve(src);
+ }
+ };
+ }
+
+ runGenerator({
+ sourceContents: sourceContents,
+ outputPath: output,
+ generateFn: generateFn || function(cb) { cb(null, sourceContents); }
+ }, cb);
+
+ return def ? def.promise : undefined;
+};
+
+module.exports.sync = function(source, output, generateFn) {
+ if (fs.existsSync(output)) {
+ var srcStat = fs.statSync(source);
+ var existingStat = fs.statSync(output);
+ if (existingStat.mtime > srcStat.mtime) {
+ return false;
+ }
+ }
+ var src = generateFn();
+ fs.writeFileSync(output, src);
+
+ return src;
+};
+
diff --git a/src/build/common/jsCompiler.js b/src/build/common/jsCompiler.js
index 72ec67a..8fa89c2 100644
--- a/src/build/common/jsCompiler.js
+++ b/src/build/common/jsCompiler.js
@@ -1,16 +1,15 @@
var path = require('path');
-var fs = require('fs');
+var fs = require('fs-extra');
var crypto = require('crypto');
+var resolve = require('resolve');
var argv = require('optimist').argv;
-var mkdirp = require('mkdirp');
var EventEmitter = require('events').EventEmitter;
-var color = require('cli-color');
// clone to modify the path for this jsio but not any others
-var jsio = require('jsio').clone();
+var jsio = require('jsio').clone(); // ~10ms
-var uglify = require('uglify-js');
+var fileGenerator = require('./fileGenerator');
function deepCopy(obj) { return obj && JSON.parse(JSON.stringify(obj)); }
@@ -65,6 +64,7 @@ exports.JSCompiler = Class(function () {
var jsioOpts = {
cwd: opts.cwd || appPath,
+ outputPath: opts.outputPath,
environment: opts.env,
path: [require('jsio').__env.getPath(), '.', 'lib'].concat(this._path),
includeJsio: 'includeJsio' in opts ? opts.includeJsio : true,
@@ -76,7 +76,9 @@ exports.JSCompiler = Class(function () {
printOutput: opts.printJSIOCompileOutput,
gcManifest: path.join(appPath, 'manifest.json'),
gcDebug: opts.debug,
- preprocessors: ['cls', 'logger']
+ preprocessors: ['cls', 'logger'],
+
+ noCompile: opts.noCompile
};
if (opts.compress) {
@@ -124,20 +126,18 @@ exports.JSCompiler = Class(function () {
// start the compile by passing something equivalent to argv (first argument is
// ignored, but traditionally should be the name of the executable?)
- mkdirp(jsCachePath, function () {
+ // Compile the game code
+ fs.mkdirp(jsCachePath, function () {
compiler.start(['jsio_compile', jsioOpts.cwd || '.', importStatement], jsioOpts);
});
};
- /**
- * use the class opts to compress source code directly
- */
-
- this.strip = function (src, cb) {
- exports.strip(src, this.opts, cb);
- };
-
+ /**
+ * use the class opts to compress source code directly
+ */
this.compress = function (filename, src, opts, cb) {
+ // Import here because it takes a while, ~70ms
+ var color = require('cli-color');
var closureOpts = [
'--compilation_level', 'SIMPLE_OPTIMIZATIONS',
@@ -194,6 +194,8 @@ exports.JSCompiler = Class(function () {
}
try {
+ // Import here because it takes a while, ~70ms
+ var uglify = require('uglify-js');
var result = uglify.minify(src, {
fromString: true,
global_defs: defines
@@ -206,6 +208,126 @@ exports.JSCompiler = Class(function () {
};
});
+/**
+ * @param {String} binPath Where jsio.js should be written to
+ * @return {Promise} FileGenerator promise
+ */
+exports.writeJsioBin = function(binPath) {
+ var srcPath = require.resolve('jsio');
+ var destPath = path.join(binPath, 'jsio.js');
+ return fileGenerator(
+ srcPath,
+ destPath,
+ function(cb) {
+ var src = jsio.__jsio.__init__.toString(-1);
+ if (src.substring(0, 8) == 'function') {
+ src = 'jsio=(' + src + ')();';
+ }
+ cb(null, src);
+ }
+ );
+};
+
+
+function replaceSlashes(str) {
+ return str.replace(/\\+/g, '/').replace(/\/{2,}/g, '/');
+}
+
+/**
+ * @param {Object} app
+ * @param {Object} config
+ * @return {Object} object with path and pathCache variables
+ */
+exports.getPathAndCache = function(app, config) {
+ var devkitCorePath = path.join(app.paths.root, 'modules', 'devkit-core');
+ var jsioPath = path.dirname(resolve.sync('jsio', { basedir: devkitCorePath }));
+
+ var _path = [];
+ var _pathCache = {
+ jsio: jsioPath
+ };
+ var addClientPaths = function (clientPaths) {
+ for (var key in clientPaths) {
+ if (key !== '*') {
+ _pathCache[key] = clientPaths[key];
+ } else {
+ _path.push.apply(_path, clientPaths['*']);
+ }
+ }
+ };
+ if (config && config.clientPaths) {
+ addClientPaths(config.clientPaths);
+ }
+
+ if (app && app.clientPaths) {
+ addClientPaths(app.clientPaths);
+ }
+
+ return {
+ path: _path,
+ pathCache: _pathCache
+ };
+};
+
+/**
+ * @param {Object} opts
+ * @param {String} [opts.cwd]
+ * @param {String[]} [path] The array of wildcards
+ * @param {Object} [pathCache] A dictionary of exact paths
+ * @param {String} binPath Where the output should go
+ * @param {Object} [pathMap] Map pathCache results somewhere else
+ * @return {Promise} FileGenerator promise
+ */
+exports.writeJsioPath = function(opts) {
+ var cwd = opts.cwd || jsio.__env.getCwd();
+ var _path = opts.path || [];
+ var pathCache = opts.pathCache || {};
+ var pathMap = opts.pathMap || null;
+ var binPath = opts.binPath;
+
+ var util = jsio.__jsio.__util;
+
+ var jsioPath;
+ if (opts.cwd) {
+ var devkitCorePath = path.join(opts.cwd, 'modules', 'devkit-core');
+ jsioPath = path.dirname(resolve.sync('jsio', { basedir: devkitCorePath }));
+ } else {
+ jsioPath = jsio.__env.getPath();
+ }
+
+ _path = [jsioPath, '.', 'lib'].concat(_path);
+
+ var cache = {};
+ Object.keys(pathCache).forEach(function (key) {
+ var pathCacheValue = pathCache[key];
+
+ var resultPath;
+ if (path.isAbsolute(pathCacheValue)) {
+ // Check for a path mapping
+ for (var p in pathMap) {
+ if (pathCacheValue.indexOf(p) === 0) {
+ resultPath = pathCacheValue.replace(p, pathMap[p]);
+ break;
+ }
+ }
+ }
+
+ if (!resultPath) {
+ resultPath = util.relative(cwd, pathCacheValue);
+ }
+
+ cache[key] = replaceSlashes(resultPath) || './';
+ });
+
+ var contents = 'jsio.path.set('
+ + JSON.stringify(_path.map(function (value) {
+ return replaceSlashes(util.relative(cwd, value));
+ })) + ');jsio.path.cache=' + JSON.stringify(cache) + ';';
+
+ var destPath = path.join(binPath, 'jsio_path.js');
+ return fileGenerator.dynamic(contents, destPath);
+}
+
var DevKitJsioInterface = Class(EventEmitter, function () {
this.init = function (bridge) {
@@ -227,8 +349,32 @@ var DevKitJsioInterface = Class(EventEmitter, function () {
this.emit('error', e);
};
- this.onFinish = function (opts, src) {
- this.emit('code', src);
+ this.onFinish = function (opts, src, table) {
+ var binPath = path.join(opts.outputPath, 'bin');
+ var tasks = [];
+
+ if (opts.individualCompile) {
+ var keys = Object.keys(table);
+ logger.info('Writing individual compile files: ' + keys.length);
+
+ keys.forEach(function(key) {
+ var srcFname = path.join(opts.cwd, key);
+ var fname = path.join(binPath, key.replace(/\//g, '.'));
+ tasks.push(fileGenerator(
+ srcFname,
+ fname,
+ function(cb) {
+ cb(JSON.stringify(table[key]));
+ }
+ ));
+ });
+ }
+
+ Promise.all(tasks)
+ .bind(this)
+ .then(function() {
+ this.emit('code', src);
+ });
};
/**
diff --git a/src/build/common/spritesheetMapGenerator.js b/src/build/common/spritesheetMapGenerator.js
new file mode 100644
index 0000000..6288c7b
--- /dev/null
+++ b/src/build/common/spritesheetMapGenerator.js
@@ -0,0 +1,89 @@
+
+var path = require('path');
+var glob = Promise.promisify(require('glob'));
+var File = require('vinyl');
+var sizeOf = require('image-size');
+
+var spritePattern = /((?:.*)\/.*?)[-_ ](.*?)[-_ ](\d+)/;
+var allowedPattern = /\.png$|\.jpg$|\.jpeg$/;
+
+/** Don't actually sprite these directories, just build the map in the same way the
+ spriter would (so that the client knows what sprites are available) */
+exports.sprite = function (api, app, config, directories) {
+ var baseDirectory = config.outputResourcePath;
+ var relativeSpritesheetsDirectory = 'spritesheets';
+ var spritesheetsDirectory = path.join(baseDirectory,
+ relativeSpritesheetsDirectory);
+
+ var sheetMap = {};
+ var sourceMap = {};
+
+ return Promise.resolve(directories)
+ .map(exports.spriteDirectory.bind(exports, api, config))
+ .each(function (allFiles) {
+ allFiles.forEach(function(file) {
+ if (file.info) {
+ sheetMap[file.target] = file.info;
+ }
+
+ sourceMap[file.originalRelativePath] = true;
+ });
+ })
+ .then(function () {
+ // Needs to return: sourceMap{}, files[]
+ var obj = {
+ files: [
+ new File({
+ base: baseDirectory,
+ path: path.join(spritesheetsDirectory,
+ 'map.json'),
+ contents: new Buffer(JSON.stringify(sheetMap))
+ })
+ ],
+ sourceMap: sourceMap
+ };
+ return obj;
+ });
+};
+
+/** Walk the dir, get all the sprite files */
+exports.spriteDirectory = function (api, config, directory) {
+ var files = [];
+
+ var root = directory.src;
+ return glob('**/*', {cwd: root, nodir: true})
+ .map(function (filename) {
+
+ if (!allowedPattern.exec(filename)) {
+ return;
+ }
+
+ var srcPath = path.join(root, filename);
+ // var relativeRoot = root.substring(directory.src.length + 1, root.length);
+ var target = path.join(directory.target, filename);
+
+ var fileData = {
+ src: srcPath,
+ target: target,
+ originalRelativePath: filename,
+ info: exports.makeInfoFor(srcPath, target)
+ };
+
+ files.push(fileData);
+ })
+ .then(function() {
+ return files;
+ });
+};
+
+/** Make a spritesheets/map.json info object for this path */
+exports.makeInfoFor = function(path, target) {
+ var dimensions = sizeOf(path);
+
+ var info = {
+ w: dimensions.width,
+ h: dimensions.height
+ };
+
+ return info;
+};
diff --git a/src/build/native/env.js b/src/build/native/env.js
index ee940c6..fa16d03 100644
--- a/src/build/native/env.js
+++ b/src/build/native/env.js
@@ -1,5 +1,7 @@
// this file is included at the end of the embedded JS
+/* globals NATIVE, JSIO_ENV_CTOR: true */
+
// it's responsible for initializing the js.io environment
var util = {};
var formatRegExp = /%[sdj%]/g;
@@ -587,21 +589,39 @@ util._errnoException = function(err, syscall, original) {
return e;
};
-var JSIO_ENV_CTOR = function() {
- var SLICE = Array.prototype.slice,
- cwd = null;
-
- this.name = /android/.test(GLOBAL.userAgent) ? 'android' : 'ios';
+JSIO_ENV_CTOR = function() {
+ this.name = /android/i.test(GLOBAL.userAgent) ? 'android' : 'ios';
- this.global = GLOBAL;
- this.log = util.getLogger(NATIVE.console.log);
- this.getCwd = getCwd;
+ this.global = GLOBAL;
+ this.log = util.getLogger(NATIVE.console.log);
+ this.getCwd = getCwd;
- function getCwd() { return NATIVE.location.substring(0, NATIVE.location.lastIndexOf('/') + 1) + 'code/__cmd__/'; }
+ function getCwd() { return NATIVE.location; }
+ this.debugPath = function (path) { return path; };
this.getPath = function() { return './sdk/jsio/'; };
- this.eval = function(code, path) { return NATIVE.eval(code, path); };
+ this.eval = function(code, path) { return NATIVE.eval(code, this.debugPath(path)); };
this.fetch = function(filePath) { return false; }
+
+ this.getNamespace = function(key) { return CONFIG.shortName + ':' + key };
+ this.hasFetchFailed = function() { return false; };
+ this.setFetchFailed = function() {};
+ this.registerFoundModule = function() {};
+ this.preloadModules = function(cb) { cb(); };
+
+ var srcCache;
+ this.setCache = function(cache) { srcCache = cache; };
+
+ this.setCachedSrc = function(path, src, locked) {
+ if (srcCache[path] && srcCache[path].locked) {
+ console.warn('Cache is ignoring (already present and locked) src ' + path);
+ return;
+ }
+ srcCache[path] = { path: path, src: src, locked: locked };
+ };
+ this.getCachedSrc = function(path) {
+ return srcCache[path];
+ };
};
NATIVE.console.log(NATIVE.location);
diff --git a/src/clientapi/browser/launchClient.js b/src/clientapi/browser/launchClient.js
index 4995e70..ba25ed2 100644
--- a/src/clientapi/browser/launchClient.js
+++ b/src/clientapi/browser/launchClient.js
@@ -17,7 +17,7 @@
/* globals jsio, CONFIG, DEBUG */
// no dynamic source fetching
-jsio.__env.fetch = function (filename) { return false; };
+// jsio.__env.fetch = function (filename) { return false; };
import Promise;
GLOBAL.Promise = Promise;
@@ -133,7 +133,6 @@ function queueStart() {
}
function startApp () {
-
// setup timestep device API
import device;
import platforms.browser.initialize;
diff --git a/src/clientapi/index.js b/src/clientapi/index.js
index d1bc8c2..d489066 100644
--- a/src/clientapi/index.js
+++ b/src/clientapi/index.js
@@ -118,18 +118,25 @@ exports.ClientAPI = Class(lib.PubSub, function () {
import .UI;
this.ui = new UI();
-
- // this.track({
- // name: "campaignID",
- // category: "campaign",
- // subcategory: "id",
- // data: campaign
- // });
-
var map;
try {
if (GLOBAL.CACHE) {
map = JSON.parse(GLOBAL.CACHE['spritesheets/map.json']);
+
+ // Add some defaults
+ for (var key in map) {
+ var entry = map[key];
+ entry.marginLeft = entry.marginLeft !== undefined ? entry.marginLeft : 0;
+ entry.marginRight = entry.marginRight !== undefined ? entry.marginRight : 0;
+ entry.marginTop = entry.marginTop !== undefined ? entry.marginTop : 0;
+ entry.marginBottom = entry.marginBottom !== undefined ? entry.marginBottom : 0;
+
+ entry.x = entry.x !== undefined ? entry.x : 0;
+ entry.y = entry.y !== undefined ? entry.y : 0;
+ entry.scale = entry.scale !== undefined ? entry.scale : 0;
+
+ entry.sheet = entry.sheet !== undefined ? entry.sheet : key;
+ }
}
} catch (e) {
logger.warn("spritesheet map failed to parse", e);
diff --git a/src/clientapi/native/launchClient.js b/src/clientapi/native/launchClient.js
index 4437578..fd42409 100644
--- a/src/clientapi/native/launchClient.js
+++ b/src/clientapi/native/launchClient.js
@@ -14,7 +14,14 @@
* along with the Game Closure SDK. If not, see .
*/
-/* globals jsio, logging, logger */
+/* globals jsio, logging, logger, CONFIG, DEBUG */
+
+var env = jsio.__env;
+env.debugPath = function (path) {
+ var protocol = 'http:';
+ var domain = env.name == 'android' ? CONFIG.packageName : CONFIG.bundleID;
+ return protocol + '//' + domain + '/' + path.replace(/^[.\/\\]+/, '');
+};
GLOBAL.console = logging.get('console');
window.self = window;