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;