clockwork-tools
Version:
CLI tools for developing Clockwork games
758 lines (714 loc) • 34.2 kB
JavaScript
(function () {
var fs = require('fs-extra');
var path = require('path');
var spawn = require('child_process').spawn;
var parseString = require('xml2js').parseString;
var ncp = require('ncp').ncp;
var archiver = require('archiver');
var request = require('request');
var prompt = require('prompt');
var geardoc = require('@clockwork/geardoc');
var exec = require('child_process').exec;
prompt.start();
prompt.message = "Some information is required";
var rootPath = "./";
var log = function (x) { }; //By default, dont log anything
var showError = function (x) { console.log(x); };//By default, output errors to the console
var getDataViaPrompt = function (data, callback) { return prompt.get(data, callback); };
var userArguments = process.argv.slice(2);
var bridges = {
web: require('clockwork-web-bridge'),
uwp: require('clockwork-uwp-bridge')
};
if (userArguments.length < 1) {
console.log("No action was specified, use 'clockwork ?' to see the available actions");
} else {
switch (userArguments[0]) {
case "init":
createProject(userArguments[1]);
break;
case "build":
buildProject(function (x) { console.log("The package has been successfully generated, you can find it at " + x) });
break;
case "list":
listPackages(userArguments[1]);
break;
case "add":
addPackage(getDataViaPrompt, userArguments[1], userArguments[2]);
break;
case "update":
updatePackage(userArguments[1]);
break;
case "register":
prompt.get({
properties: {
username: {
description: 'Enter your username',
required: true
},
email: {
description: 'Enter your email',
pattern: /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/,
required: true,
message: 'This must be a valid email',
},
password: {
description: 'Enter your password',
hidden: true,
required: true,
replace: '*'
}
}
}, function (err, result) {
register(result.username, result.email, result.password);
});
break;
case "publish":
prompt.get({
properties: {
sourceFile: {
description: 'Enter the location of the source file',
required: true
},
packageId: {
description: 'Enter the package name',
pattern: /^[a-zA-Z0-9]+$/,
message: 'Name must only contain alphanumeric characters',
required: true
},
packageVersion: {
description: 'Enter the package version',
pattern: /^[a-zA-Z0-9\.\-]+$/,
message: 'Name must only contain alphanumeric characters, dots and dashes',
required: true
}
}
}, function (err, result) {
tryPublish(result.sourceFile, result.packageId, result.packageVersion);
});
break;
case "bridge":
runBridge(userArguments[1]);
break;
case "help":
case "?":
help();
break;
default:
console.log("The specified action was not recognized");
break;
}
}
//Create a project following the template in the working directory with the specified name
function createProject(projectName) {
if (projectName === undefined) {
console.log("No name was specified for the project");
return;
}
function createFolder(path, folder) {
path = path + "/" + folder.name;
try {
fs.mkdirSync(rootPath + path);
} catch (e) {
}
if (folder.folders) {
folder.folders.forEach(createFolder.bind(null, path));
}
}
var newProject = require('./template/newProject.js');
newProject.folders.forEach(createFolder.bind(null, ""));
newProject.files.forEach(function (file) {
fs.readFile(__dirname + "/" + file.file, 'base64', function (err, data) {
if (err) {
return console.log(err);
}
if (file.template) {
var b = new Buffer(data, 'base64');
var content = b.toString().replace("#projectName#", projectName);
b = new Buffer(content);
data = b.toString('base64');
}
fs.writeFile(rootPath + file.name, data, 'base64', function (err) {
if (err) throw err;
});
});
});
}
//Looks for a project in the working directory and creates a Clockwork package
function buildProject(callback) {
var manifest = readManifest();
if (manifest != null) {
try {
var path = generatePackage(manifest).then(callback);
} catch (e) {
showError("Error:" + e);
}
} else {
console.log("The current directory does not contain a Clockwork project");
}
}
//Creates a Clockwork package using the given manifest
function generatePackage(manifest) {
var workingPath = path.resolve(rootPath);
try {
fs.mkdirSync(workingPath + '/ClockworkPackageTemp');
} catch (e) { }
copyFileSync(workingPath + '/manifest.json', workingPath + '/ClockworkPackageTemp/manifest.json');
return new Promise(function (res, rej) {
console.log("Copying project files...");
ncp(workingPath + '/' + manifest.scope, workingPath + '/ClockworkPackageTemp/' + manifest.scope, function (err) {
if (err) {
console.error(err);
rej();
} else {
res()
}
})
}).then(function () {
return preprocessPackage(workingPath + "/ClockworkPackageTemp");
}).then(function (x) {
return new Promise(function (res, rej) {
try {
fs.unlinkSync(workingPath + "/" + manifest.name + '.cw');
} catch (e) { }
var output = fs.createWriteStream(workingPath + "/" + manifest.name + '.cw');
var archive = archiver('zip');
output.on('close', function () {
console.log("Deleting temporary files...");
deleteFolderRecursive(workingPath + "/ClockworkPackageTemp/");
res();
});
archive.on('error', function (err) {
throw err;
});
archive.pipe(output);
console.log("Creating package..");
archive.glob("**", {
cwd: workingPath + "/ClockworkPackageTemp/"
})
// archive.bulk([
// { expand: true, cwd: workingPath + "/ClockworkPackageTemp/", src: ['**'], dest: '' }
// ]);
archive.finalize();
});
}).then(function (x) { return workingPath + "/" + manifest.name + ".cw" });
}
//Reads the manifest in the working directory
function readManifest(projectPath) {
try {
var manifest = JSON.parse(fs.readFileSync((projectPath || path.resolve(rootPath)) + "/manifest.json"));
return manifest;
} catch (e) {
return null;
}
}
//Writes the manifest in the working directory
function writeManifest(manifest, callback, projectPath) {
fs.writeFile((projectPath || path.resolve(rootPath)) + "/manifest.json", JSON.stringify(manifest), function (err) {
if (callback) {
callback(err)
};
});
}
//Run all the preprocessors on the package
function preprocessPackage(path) {
var manifest = readManifest();
//Convert xml spritesheets to json
return new Promise(function (resolvef, rejectf) {
console.log("Processing levels...");
var levels = manifest.levels.map(function (oldName, i) {
if (oldName.indexOf(".xml") != -1) {
var newName = oldName.split(".xml").join(".json");
manifest.levels[i] = newName;
return new Promise(function (resolve, reject) {
log("Reading " + path + "/" + manifest.scope + "/" + oldName);
fs.readFile(path + "/" + manifest.scope + "/" + oldName, function (err, data) {
if (err) {
return console.error(err);
} else {
parseString(data, function (err, result) {
if (err) {
showError("There are errors in " + oldName + ", check your XML.");
resolve();
} else {
log("Writing " + path + "/" + manifest.scope + "/" + newName);
fs.writeFile(path + "/" + manifest.scope + "/" + newName, JSON.stringify(XMLlevelsToJson(result, oldName)), function (err) {
if (err) {
return console.error(err);
}
resolve();
});
}
});
}
});
});
} else {
return new Promise(function (resolve, reject) { return resolve() });
}
});
var spritesheets = manifest.spritesheets.map(function (oldName, i) {
console.log("Processing spritesheets...");
//If it is an XML file, convert to JSON
if (oldName.indexOf(".xml") != -1) {
var newName = oldName.split(".xml").join(".json");
manifest.spritesheets[i] = newName;
return new Promise(function (resolve, reject) {
fs.readFile(path + "/" + manifest.scope + "/" + oldName, function (err, data) {
if (err) {
return console.error(err);
} else {
parseString(data, function (err, result) {
if (err) {
showError("There are errors in " + oldName + ", check your XML.");
resolve();
} else {
fs.writeFile(path + "/" + manifest.scope + "/" + newName, JSON.stringify(XMLspritesheetsToJson(result)), function (err) {
if (err) {
return console.error(err);
}
resolve();
});
}
});
}
});
});
} else if (oldName.indexOf(".js") == oldName.length - 3) {
//If it a .js file, stringify functions
return new Promise(function (resolve, reject) {
fs.readFile(path + "/" + manifest.scope + "/" + oldName, function (err, data) {
if (err) {
return console.error(err);
} else {
var spritesheetContent;
eval("spritesheetContent = " + data);
//Stringify functions
(function stringifyFunctions(o) {
for (x in o) {
if (typeof o[x] == "object") {
stringifyFunctions(o[x]);
}
if (typeof o[x] == "function") {
o[x] = o[x].toString();
}
}
})(spritesheetContent);
var newName = oldName.split(".js").join(".json");
fs.writeFile(path + "/" + manifest.scope + "/" + newName, JSON.stringify(spritesheetContent), function (err) {
if (err) {
return console.error(err);
}
resolve();
});
}
});
});
}
});
Promise.all(levels.concat(spritesheets)).then(function (x) {
console.log("Updating manifest...");
fs.writeFile(path + "/manifest.json", JSON.stringify(manifest), function (err) {
if (err) {
return console.error(err);
}
resolvef();
});
});
});
}
//Levels logic
function XMLlevelsToJson(result, oldName) {
try {
return result.levels.level.map(XMLlevelToJson);
} catch (e) {
showError(oldName + " does not contain a valid XML levels file")
}
}
function XMLlevelToJson(thislevel) {
var level = {};
level.id = thislevel.$.id;
if (thislevel.object) {
level.objects = thislevel.object.map(function (thisobject) {
var object = {};
//Set name
object.name = thisobject.$.name
//Set type
if (thisobject.type && thisobject.type.length > 0) {
//Composition
object.type = thisobject.type.map(function (x) { return x.$.id; });
} else {
//Inheritance
object.type = thisobject.$.type;
}
//Set spritesheet
object.sprite = thisobject.$.spritesheet ? thisobject.$.spritesheet : null;
//Set whether the object is static
object.isstatic = thisobject.$.static ? thisobject.$.static : null;
//Set x,y,z
object.x = +thisobject.$.x;
object.y = +thisobject.$.y;
object.z = thisobject.$.z ? +thisobject.$.z : null;
//Set vars
try {
object.vars = thisobject.$.vars ? JSON.parse(thisobject.$.vars) : {};
} catch (e) {
showError("This string should be a valid JSON object but it is not");
showError(thisobject.$.vars);
object.vars = {};
}
return object;
});
} else {
level.objects = [];
}
return level;
}
//Spritesheets logic
function XMLspritesheetsToJson(result) {
return result.spritesheets.spritesheet.map(XMLspritesheetToJson);
}
function Spritesheet() {
this.name = "";
this.img;
this.states = {}
this.layers = {}
this.frames = {};
}
function State() {
this.layers = [];
}
//Holds a layer: Body, arms...
function Layer() {
this.frames = [];
this.x;
this.y;
}
//Holds a single frame
function Frame(x, y, w, h, t) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.t = t;
}
function XMLspritesheetToJson(thisspritesheet) {
var newspritesheet = new Spritesheet();
newspritesheet.name = thisspritesheet.$.name;
if (thisspritesheet.$.src != undefined) {
newspritesheet.src = thisspritesheet.$.src;
}
if (thisspritesheet.$.positionBasedOptimizations != undefined) {
newspritesheet.positionBasedOptimizations = thisspritesheet.$.positionBasedOptimizations;
}
thisspritesheet.frames[0].frame.forEach(function (frame) {
var newframe = new Frame();
if (frame.$.code == undefined) {
if (frame.$.fullTexture) {
newframe.fullTexture = true;
} else {
newframe.x = +frame.$.x;
newframe.y = +frame.$.y;
newframe.w = +frame.$.w
newframe.h = +frame.$.h;
}
} else {
newframe.code = frame.$.code;
}
newframe.t = +frame.$.t;
newspritesheet.frames[frame.$.name] = newframe;
});
thisspritesheet.layers[0].layer.forEach(function (layer) {
var newlayer = new Layer();
newlayer.x = layer.$.x;
newlayer.y = layer.$.y;
newlayer.frames = layer.frame ? layer.frame.map(function (f) { return f.$.name; }) : [];
newspritesheet.layers[layer.$.name] = newlayer;
});
thisspritesheet.states[0].state.forEach(function (state) {
var newstate = new State();
newstate.layers = state.layer ? state.layer.map(function (l) { return l.$.name; }) : [];
if (state.$.flip) {
newstate.flip = state.$.flip;
}
newspritesheet.states[state.$.name] = newstate;
});
return newspritesheet;
}
//File system helpers
function copyFileSync(srcFile, destFile) {
var content = fs.readFileSync(srcFile);
fs.writeFileSync(destFile, content);
}
function deleteFolderRecursive(path) {
if (fs.existsSync(path)) {
fs.readdirSync(path).forEach(function (file, index) {
var curPath = path + "/" + file;
if (fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
}
};
///CWPM
function listPackages(packageId) {
if (packageId) {
request('https://clockworkdev.github.io/ClockworkPackages/packages/' + packageId + '/versions.json', function (error, response, body) {
if (!error && response.statusCode == 200) {
console.log("Versions of " + packageId + ":");
var packages = JSON.parse(body);
packages.forEach(function (p) {
console.log(" " + p.version + " published at " + p.date);
});
}
});
} else {
request('https://clockworkdev.github.io/ClockworkPackages/packages/packages.json', function (error, response, body) {
if (!error && response.statusCode == 200) {
console.log("Packages published:");
var packages = JSON.parse(body);
packages.forEach(function (p) {
console.log(" " + p.id + " by " + p.by);
});
}
});
}
}
function register(username, email, password) {
request.post({ url: 'http://cwpm.azurewebsites.net/api/developers', form: { name: username, email: email, password: password } }, function (err, httpResponse, body) {
if (JSON.parse(body).res == "OK") {
console.log("User registered successfully");
} else {
console.log("Error registering your account. Maybe that username is already taken?");
}
})
}
function tryPublish(sourceFile, packageId, packageVersion) {
fs.access('clockwork-packages.repo', fs.constants.F_OK, (err) => {
if (err) {
console.log("Please execute this command in the Clockwork Packages root folder (where the clockwork-packages.repo is)");
} else {
fs.copy(sourceFile, `./packages/${packageId}/${packageVersion}/components.js`, (err) => {
if (err) {
console.log(err);
} else {
console.log('Package saved');
fs.readFile(sourceFile, 'utf8', (err, data) => {
if (err) {
console.log(err);
} else {
fs.outputFile(`./doc/${packageId}/${packageVersion}/doc.html`, geardoc.generateDoc(data), (err) => {
console.log(err || 'Documentation saved');
});
const versionsFile = `packages/${packageId}/versions.json`;
fs.access(versionsFile, fs.constants.F_OK, (err) => {
var versions = err ? [] : JSON.parse(fs.readFileSync(versionsFile, { encoding: 'utf8' }));
versions = versions.filter(x=>x.version != packageVersion);
versions.push({ version: packageVersion, date: new Date() });
if (err) {
fs.appendFile(versionsFile, JSON.stringify(versions, null, 4), (e) => {
console.log(e || 'Version list updated');
});
} else {
fs.writeFile(versionsFile, JSON.stringify(versions, null, 4), (e) => {
console.log(e || 'Version list updated');
});
}
});
const packagesFile = `packages/packages.json`;
fs.access(packagesFile, fs.constants.F_OK, (err) => {
var packages = err ? [] : JSON.parse(fs.readFileSync(packagesFile, { encoding: 'utf8' }));
if (packages.filter(x => x.id == packageId).length == 0) {
exec('git config --global user.name', (err, stdout, stderr) => {
var name = stdout.replace('\n', '');
packages.push({ id: packageId, by: name });
if (err) {
fs.appendFile(packagesFile, JSON.stringify(packages, null, 4), (e) => {
console.log(e || 'Package list updated');
});
} else {
fs.writeFile(packagesFile, JSON.stringify(packages, null, 4), (e) => {
console.log(e || 'Package list updated');
});
}
});
}
});
}
});
}
});
}
});
}
function addPackage(getData, packageName, packageVersion) {
if (typeof packageName === 'undefined') {
console.log("You must specify a module");
return;
}
var manifest = readManifest();
if (manifest == null) {
console.log("There is no Clockwork project in the working folder");
return;
}
if (typeof manifest.dependencies[packageName] === 'undefined') {
continueAddPackage();
} else {
getData({
properties: {
confirm: {
description: 'This package is already a dependency, do you want to change the version? (Y/N)',
pattern: /Y|N/,
required: true
},
}
}, function (err, result) {
if (result.confirm == "Y") {
continueAddPackage();
}
});
}
function continueAddPackage() {
request('http://cwpm.azurewebsites.net/api/packages/' + packageName, function (error, response, body) {
if (!error && response.statusCode == 200) {
var packages = JSON.parse(body);
if (packages.length == 0) {
console.log("This package can't be found on the online repository.");
return;
}
if (typeof packageVersion === 'undefined') {
var lastVersion = packages.sort(function (a, b) { return new Date(b.date) - new Date(a.date) })[0].version;
manifest.dependencies[packageName] = lastVersion
writeManifest(manifest, function (err) {
if (err) {
console.log("An error happened while trying to update the manifest");
} else {
console.log("Version " + lastVersion + " of " + packageName + " added to the dependencies");
}
});
} else {
if (packages.filter(function (p) { return p.version == packageVersion; }).length > 0) {
manifest.dependencies[packageName] = packageVersion;
writeManifest(manifest, function (err) {
if (err) {
console.log("An error happened while trying to update the manifest");
} else {
console.log("Version " + packageVersion + " of " + packageName + " added to the dependencies");
}
});
} else {
console.log("This version can't be found. Please list all the published versions with 'clockwork list " + packageName + "'");
}
}
}
});
}
}
function updatePackage(packageName) {
var manifest = readManifest();
if (manifest == null) {
console.log("There is no Clockwork project in the working folder");
return;
}
if (typeof packageName === 'undefined') {
Promise.all(Object.keys(manifest.dependencies).map(function (x) { return updateDependencyPromise(x) })).then(function () {
writeManifest(manifest);
});
} else {
updateDependencyPromise(packageName).then(function () {
writeManifest(manifest);
});
}
function updateDependencyPromise(packageName) {
return new Promise(function (resolve, reject) {
request('http://cwpm.azurewebsites.net/api/packages/' + packageName, function (error, response, body) {
if (!error && response.statusCode == 200) {
var packages = JSON.parse(body);
if (packages.length == 0) {
console.log("No versions of the module " + packageName + " have been found in the online repository.");
} else {
var lastVersion = packages.sort(function (a, b) { return new Date(b.date) - new Date(a.date) })[0].version;
if (manifest.dependencies[packageName] != lastVersion) {
console.log(packageName + " updated from " + manifest.dependencies[packageName] + " to " + lastVersion);
manifest.dependencies[packageName] = lastVersion;
} else {
console.log(packageName + " is already up to date");
}
}
resolve();
}
});
});
}
}
function runBridge(name, callback) {
if (bridges[name]) {
buildProject(function (packageName) {
if (!fs.existsSync(rootPath + name)) {
fs.mkdirSync(rootPath + name);
}
bridges[name](packageName, rootPath + name);
if (callback) {
callback(true);
}
});
} else {
console.log("This bridge can't be found, the following bridges are available:");
for (var bridge in bridges) {
console.log(" " + bridge);
}
if (callback) {
callback(false);
}
}
}
//Help
function help() {
console.log("The following commands are allowed:");
console.log("\n > clockwork init <projectName>");
console.log(" Creates an empty Clockwork project in the working directory");
console.log("\n > clockwork build");
console.log(" Builds the Clockwork project in the working directory, generating a .cw file");
console.log("\n > clockwork list");
console.log(" Lists the Clockwork modules available in the online repository");
console.log("\n > clockwork list <moduleName>");
console.log(" Lists the versions of that module available in the online repository");
console.log("\n > clockwork add <moduleName>");
console.log(" Adds the last version of the specified module as a dependency of the current project");
console.log("\n > clockwork add <moduleName> <moduleVersion>");
console.log(" Adds the specified version of the specified module as a dependency of the current project");
console.log("\n > clockwork update <moduleName>");
console.log(" Updates the dependency to the specified package to the latest published version");
console.log("\n > clockwork update");
console.log(" Updates the dependencies to all the packages to the latest published versions");
// console.log("\n > clockwork register");
// console.log(" Registers a developer account, allowing you to publish Clockwork modules");
console.log("\n > clockwork publish");
console.log(" Publishes a module in the Clockwork online repository");
console.log("\n > clockwork bridge <bridgeName>");
console.log(" Uses the specified bridge to export the game");
}
module.exports = function (cwd, getData, logFunction, showErrorMessage) {
rootPath = cwd;
if (logFunction) {
log = logFunction;
}
if (showErrorMessage) {
showError = showErrorMessage;
}
return {
createProject: createProject,
buildProject: buildProject,
listPackages: listPackages,
addPackage: addPackage.bind(null, getData),
updatePackage: updatePackage,
register: register,
tryPublish: tryPublish.bind(null, getData),
runBridge: runBridge
}
};
})();