/**
* @fileoverview Gruntfile for pcjs.org
* @author Jeff Parsons (@jeffpar)
* @version 1.0
* Created 2014-03-11
*
* Copyright © 2012-2015 Jeff Parsons
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at
* and .
*
* JSMachines is free software: you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* JSMachines is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with JSMachines.
* If not, see .
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
/*
* Overview
* ---
* Based on the specified version in package.json, we build a matching version
* folder in /versions for each of the machine simulations; each of those folders
* in turn receives compiled versions of the corresponding machine simulation scripts
* (c1p*.js and pc*.js), along with copies of both the shared and machine-specific
* CSS and XSL stylesheets that the scripts rely on.
*
* Usage
* ---
* grunt [task[:target] ...] [--rebuild]
*
* If no tasks are specified, the "default" task alias will be run. If --rebuild is present,
* grunt will run the "closureCompiler" task (with closureCompiler.options.checkModified set
* to false) followed by the "copy" task; otherwise, it will run the "closureCompiler" task
* (with closureCompiler.options.checkModified set to true) followed by the "newer:copy" task.
*
* The former insures that all files are recompiled and recopied, while the latter only recompiles
* and/or recopies those files whose "src" file(s) are newer than the corresponding "dest" file(s).
*
* The combination of "grunt-contrib-copy" and "grunt-newer" could have possibly been replaced
* by "grunt-rsync", except that the latter didn't seem to support "process" functions; however,
* it's also possible I didn't have it configured properly at the time.
*
* Utility Tasks
* ---
* grunt compile: runs "closureCompiler" to produce compiled c1p*.js and pc*.js scripts
* grunt nocompile: runs "concat" to produce uncompiled c1p*.js and pc*.js scripts
* grunt promote: runs "replace" to promote all machine XML files to the current version
*
* The "grunt nocompile" task is intended for debugging only; if you use it, be sure to run
* a final "grunt compile" (or "grunt --rebuild") and retest before pushing out a new release.
*
* The "manifester" Task
* ---
* I added the "manifester" task to process manifest.xml files, which I use to record the existence
* application archives. "manifester" walks all the manifests and verifies that they're still valid,
* and optionally fetches local copies of everything described.
*
* The "prepjs" Task
* ---
* I added the "prepjs" task to perform the same constant inlining that my old PHP script "inline.php"
* used to do as part of the old "jsmachines.net" build process. Unfortunately, Grunt appears to be
* a real memory hog, and so "prepjs" had to be disabled until I could resolve that issue (see my notes
* in "modules/grunts/prepjs/tasks/prepjs.js" for details).
*
* With the current version of the Closure Compiler, I don't see any measurable performance benefit
* to doing my own inlining, but I still wanted to port the code, partly in case I discover some benefit
* down the road (or if I think of some other pre-processing I'd like to incorporate), and partly for
* the experience of writing my own Grunt task -- an experience I didn't find all that pleasant, due to
* the weak documentation and unfortunate memory constraints.
*
* To integrate "prepjs" back into the build process, add it to the "default" task, and change the
* "closureCompiler" task to compile the inlined code in "/tmp" (the "/tmp" folder structure mirrors
* the "/versions" folder structure).
*/
var path = require("path");
module.exports = function(grunt) {
/*
* I could have added this to the top of the file instead:
*
* require("./package.json");
*
* but the following seems more "grunt-like". It's also more traditional to pass
* the package.json properties to grunt.initConfig() via a "pkg" property, like so:
*
* pkg: grunt.file.readJSON("package.json");
*
* and then use grunt template strings such as:
*
* "<%= pkg.name %>"
*
* which would be fine for most of my needs, but some of the information I need
* from the package.json is not in string form (eg, pcJSFiles, which is an array of
* file names). So I create a "pkg" variable first, which allows me to do both.
*/
/**
* @class
* @property {string} name
* @property {string} version
* @property {Array.} c1pJSFiles
* @property {Array.} pcJSFiles
* @property {Array.} closureCompilerExterns
*/
var pkg = grunt.file.readJSON("package.json");
var tmpC1Pjs = "./tmp/c1pjs/" + pkg.version + "/c1p.js";
var tmpPCjs = "./tmp/pcjs/" + pkg.version + "/pc.js";
grunt.initConfig({
pkg: pkg, // pass the "package.json" object to initConfig() as a property, too
manifester: {
options: {
},
"apps": {
src: "./apps/**/manifest.xml"
}
},
concat: {
options: {
// separator: ';'
},
"c1p.js": {
src: pkg.c1pJSFiles,
dest: "./versions/c1pjs/" + pkg.version + "/c1p.js",
options: {
process: function(src, filepath) {
return (path.basename(filepath) == "debugger.js"? "" : src);
}
}
},
"c1p-dbg.js": {
src: pkg.c1pJSFiles,
dest: "./versions/c1pjs/" + pkg.version + "/c1p-dbg.js"
},
"pc.js": {
src: pkg.pcJSFiles,
dest: "./versions/pcjs/" + pkg.version + "/pc.js",
options: {
process: function(src, filepath) {
return (path.basename(filepath) == "debugger.js"? "" : src);
}
}
},
"pc-dbg.js": {
src: pkg.pcJSFiles,
dest: "./versions/pcjs/" + pkg.version + "/pc-dbg.js"
},
"tmp-c1pjs": {
src: pkg.c1pJSFiles,
dest: tmpC1Pjs,
options: {
banner: '"use strict";\n\n',
process: function(src, filepath) {
return "// " + filepath + "\n\n" +
src.replace(/(^|\n)[ \t]*(['"])use strict\2;?\s*/g, '$1').replace(/[ \t]*if\s*\(typeof\s+(module|APP_PCJS)\s*!==\s*(['"])undefined\2\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, '').replace(/[ \t]*[A-Za-z_][A-Za-z0-9_\.]*\.assert\(.*\);[^\n]*(\n|$)/g, '');
}
}
},
"tmp-pcjs": {
src: pkg.pcJSFiles,
dest: tmpPCjs,
options: {
banner: '"use strict";\n\n',
process: function(src, filepath) {
return "// " + filepath + "\n\n" +
src.replace(/(^|\n)[ \t]*(['"])use strict\2;?\s*/g, '$1').replace(/[ \t]*if\s*\(typeof\s+(module|APP_PCJS)\s*!==\s*(['"])undefined\2\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, '').replace(/[ \t]*[A-Za-z_][A-Za-z0-9_\.]*\.assert\(.*\);[^\n]*(\n|$)/g, '');
}
}
}
},
prepjs: {
options: {
includeObjectConstants: false,
listConstants: true,
replaceConstants: true
},
"c1p.js": {
includeObjectConstants: true,
src: pkg.c1pJSFiles,
dest: tmpC1Pjs
},
"pc.js": {
src: pkg.pcJSFiles,
dest: tmpPCjs
}
},
closureCompiler: {
options: {
// [REQUIRED] Path to closure compiler
compilerFile: "./bin/compiler.jar",
// [OPTIONAL] set to true if you want to check if files were modified before starting compilation
checkModified: grunt.option("rebuild")? false : true,
// [OPTIONAL] Set Closure Compiler Directives here
compilerOpts: {
/*
* Keys will be used as directives for the compiler; values can be strings or arrays.
* If no value is required, use null.
*
* The directive 'externs' is treated as a special case, allowing a grunt file syntax (, *)
*/
compilation_level: "ADVANCED_OPTIMIZATIONS",
externs: pkg.closureCompilerExterns,
define: ["'SITEHOST=\"www.pcjs.org\"'", "'COMPILED=true'", "'DEBUG=false'"],
warning_level: "verbose",
// jscomp_off: ["checkTypes", "fileoverviewTags"],
// summary_detail_level: 3,
// formatting: "PRETTY_PRINT --debug",
output_wrapper: "\"(function(){%output%})();\""
},
// [OPTIONAL] Set exec method options
execOpts: {
/*
* Set maxBuffer if you got message "Error: maxBuffer exceeded."
* Node default: 200*1024
*/
// maxBuffer: 999999 * 1024
},
// [OPTIONAL] Java VM optimization options
//
// See: https://code.google.com/p/closure-compiler/wiki/FAQ#What_are_the_recommended_Java_VM_command-line_options?
//
// Setting one of these to 'true' is strongly recommended, and can reduce compile times by 50-80%
// depending on compilation size and hardware. On server-class hardware, such as with GitHub's Travis
// hook, TieredCompilation should be used; on standard developer hardware, d32 may be better.
//
// Set as appropriate for your environment. Default for both is 'false'; do not set both to 'true'.
d32: false, // will use 'java -client -d32 -jar compiler.jar'
TieredCompilation: false // will use 'java -server -XX:+TieredCompilation -jar compiler.jar'
},
"c1p.js": {
/*
* [OPTIONAL] Here you can add new or override previous option of the Closure Compiler Directives.
*
* IMPORTANT! The feature is provided as a temporary solution to [Grunt Issue #738](https://github.com/gruntjs/grunt/issues/738).
* If/when that issue is fixed, this feature may be removed.
*/
TEMPcompilerOpts: {
// create_source_map: "./tmp/c1pjs/" + pkg.version + "/c1p.map",
define: ["\"APPNAME='C1Pjs'\"", "\"APPVERSION='" + pkg.version + "'\"", "DEBUGGER=false",
"\"SITEHOST='www.pcjs.org'\"", "COMPILED=true", "DEBUG=false"],
// output_wrapper: "\"(function(){%output%})();//@ sourceMappingURL=/tmp/c1pjs/" + pkg.version + "/c1p.map\""
output_wrapper: "\"(function(){%output%})();\""
},
// src: pkg.c1pJSFiles,
src: tmpC1Pjs,
dest: "./versions/c1pjs/" + pkg.version + "/c1p.js"
},
"c1p-dbg.js": {
TEMPcompilerOpts: {
// create_source_map: "./tmp/c1pjs/" + pkg.version + "/c1p-dbg.map",
define: ["\"APPNAME='C1Pjs'\"", "\"APPVERSION='" + pkg.version + "'\"",
"\"SITEHOST='www.pcjs.org'\"", "COMPILED=true", "DEBUG=false"],
// output_wrapper: "\"(function(){%output%})();//@ sourceMappingURL=/tmp/c1pjs/" + pkg.version + "/c1p-dbg.map\""
output_wrapper: "\"(function(){%output%})();\""
},
// src: pkg.c1pJSFiles,
src: tmpC1Pjs,
dest: "./versions/c1pjs/" + pkg.version + "/c1p-dbg.js"
},
"pc.js": {
TEMPcompilerOpts: {
// create_source_map: "./tmp/pcjs/" + pkg.version + "/pc.map",
define: ["\"APPNAME='PCjs'\"", "\"APPVERSION='" + pkg.version + "'\"", "DEBUGGER=false",
"\"SITEHOST='www.pcjs.org'\"", "COMPILED=true", "DEBUG=false", "BACKTRACK=false", "I386=false"],
// output_wrapper: "\"(function(){%output%})();//@ sourceMappingURL=/tmp/pcjs/" + pkg.version + "/pc.map\""
output_wrapper: "\"(function(){%output%})();\""
},
// src: pkg.pcJSFiles,
src: tmpPCjs,
dest: "./versions/" + pkg.name + "/" + pkg.version + "/pc.js"
},
"pc-dbg.js": {
/*
* Technically, this is the one case we don't need to override the default 'define' settings, but maybe it's best to be explicit.
*/
TEMPcompilerOpts: {
// create_source_map: "./tmp/pcjs/" + pkg.version + "/pc-dbg.map",
define: ["\"APPNAME='PCjs'\"", "\"APPVERSION='" + pkg.version + "'\"",
"\"SITEHOST='www.pcjs.org'\"", "COMPILED=true", "DEBUG=false", "BACKTRACK=false", "I386=false"],
// output_wrapper: "\"(function(){%output%})();//@ sourceMappingURL=/tmp/pcjs/" + pkg.version + "/pc-dbg.map\""
output_wrapper: "\"(function(){%output%})();\""
},
// src: pkg.pcJSFiles,
src: tmpPCjs,
dest: "./versions/" + pkg.name + "/" + pkg.version + "/pc-dbg.js"
}
},
copy: {
"c1pxsl": {
files: [
{
cwd: "./modules/shared/templates/",
src: ["common.css", "common.xsl", "document.css", "document.xsl", "machine.xsl", "manifest.xsl", "outline.xsl"],
dest: "versions/c1pjs/<%= pkg.version %>/",
expand: true
}
],
options: {
process: function(content, srcPath) {
var s = content.replace(/()[^<]*(<\/xsl:variable>)/g, "$1" + pkg.version + "$2");
s = s.replace(/"[^"]*\/?(common.css|common.xsl|components.css|components.xsl|document.css|document.xsl)"/g, '"/versions/c1pjs/' + pkg.version + '/$1"');
s = s.replace(/[ \t]*\/\*[^\*][\s\S]*?\*\//g, "").replace(/[ \t]*[ \t]*\n?/g, "");
return s;
}
}
},
"pcxsl": {
files: [
{
cwd: "./modules/shared/templates/",
src: ["common.css", "common.xsl", "document.css", "document.xsl", "machine.xsl", "manifest.xsl", "outline.xsl"],
dest: "versions/pcjs/<%= pkg.version %>/",
expand: true
}
],
options: {
process: function(content, srcPath) {
var s = content.replace(/()[^<]*(<\/xsl:variable>)/g, "$1" + pkg.version + "$2");
s = s.replace(/"[^"]*\/?(common.css|common.xsl|components.css|components.xsl|document.css|document.xsl)"/g, '"/versions/pcjs/' + pkg.version + '/$1"');
s = s.replace(/[ \t]*\/\*[^\*][\s\S]*?\*\//g, "").replace(/[ \t]*[ \t]*\n?/g, "");
return s;
}
}
},
"c1pjs": {
files: [
{
cwd: "./modules/c1pjs/templates/",
src: ["components.*"],
dest: "versions/c1pjs/<%= pkg.version %>/",
expand: true
}
],
options: {
process: function(content, srcPath) {
var s = content.replace(/()[^<]*(<\/xsl:variable>)/g, "$1" + pkg.version + "$2");
s = s.replace(/[ \t]*\/\*[^\*][\s\S]*?\*\//g, "").replace(/[ \t]*[ \t]*\n?/g, "");
return s;
}
}
},
"pcjs": {
files: [
{
cwd: "./modules/pcjs/templates/",
src: ["components.*"],
dest: "versions/pcjs/<%= pkg.version %>/",
expand: true
}
],
options: {
process: function(content, srcPath) {
var s = content.replace(/()[^<]*(<\/xsl:variable>)/g, "$1" + pkg.version + "$2");
s = s.replace(/[ \t]*\/\*[^\*][\s\S]*?\*\//g, "").replace(/[ \t]*[ \t]*\n?/g, "");
return s;
}
}
},
"demos": {
files: [
{
cwd: "versions/pcjs/<%= pkg.version %>/",
src: ["pc.js", "pc-dbg.js", "components.css", "components.xsl"],
dest: "docs/pcjs/demos/",
expand: true
}
],
options: {
process: function(content, srcPath) {
return content.replace(/\/versions\/\{\$APPCLASS}\/\{\$APPVERSION}\//g, "");
}
}
}
},
run: {
"delete-indexes": {
options: {cwd: "."},
cmd: "./modules/htmlout/bin/delete_indexes.sh",
args: []
},
"zipify-demos": {
options: {cwd: "docs/pcjs/demos"},
cmd: "./zip.sh",
args: ["v" + pkg.version + ".zip"]
}
},
replace: {
"fix-source-maps": {
src: ["./tmp/c1pjs/" + pkg.version + "/c1p*.map", "./tmp/pcjs/" + pkg.version + "/pc*.map"],
overwrite: true,
replacements: [
{
from: /"sources":\s*\["\.\/tmp\//g,
to: '"sources":["/tmp/'
}
]
},
"promote-to-version": {
src: ["apps/**/*.xml", "devices/**/*.xml", "disks/**/*.xml", "pubs/**/*.xml"],
overwrite: true,
replacements: [
{
from: /href="\/versions\/([^\/]*)\/[0-9\.]*\/(machine|manifest|outline)\.xsl"/g,
to: 'href="/versions/$1/<%= pkg.version %>/$2.xsl"'
}
]
}
}
});
grunt.loadNpmTasks("grunt-closure-tools");
grunt.loadNpmTasks("grunt-contrib-concat");
grunt.loadNpmTasks("grunt-contrib-copy");
grunt.loadNpmTasks("grunt-newer");
grunt.loadNpmTasks("grunt-run");
grunt.loadNpmTasks("grunt-text-replace");
grunt.loadTasks("modules/grunts/manifester/tasks");
grunt.loadTasks("modules/grunts/prepjs/tasks");
grunt.registerTask("preCompiler", grunt.option("rebuild")? ["concat:tmp-c1pjs", "concat:tmp-pcjs"] : ["newer:concat:tmp-c1pjs", "newer:concat:tmp-pcjs"]);
grunt.registerTask("compile", ["preCompiler", "closureCompiler", "replace:fix-source-maps"]);
grunt.registerTask('nocompile', function(target) {
if (!target) {
grunt.task.run(["concat:c1p.js", "concat:c1p-dbg.js", "concat:pc.js", "concat:pc-dbg.js"]);
} else {
grunt.task.run("concat:" + target);
}
});
grunt.registerTask("promote", ["replace:promote-to-version"]);
grunt.registerTask("clean", ["run:delete-indexes"]);
grunt.registerTask("copyfiles", grunt.option("rebuild")? ["copy"] : ["newer:copy"]);
grunt.registerTask("default-osx", ["compile", "copyfiles", "run:zipify-demos"]);
grunt.registerTask("default", ["compile", "copyfiles"]);
};