UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

336 lines (300 loc) 10.5 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2017 Christian Boulanger and others License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Christian Boulanger (info@bibliograph.org, @cboulanger) * Henner Kollmann (hkollmann) ************************************************************************ */ const fs = require("fs"); const path = require("upath"); const inquirer = require("inquirer"); /** * Create a new qooxdoo project. This will assemble the information needed to create the * new project by the following ways, in order of precedence: * 1. use parameters passed to the CLI command via the options * 2. if available, retrieve the info from the given environment * 3. ask the user the missing values interactively, offering default values where available * The variables needed are stored in the templates/template_vars.js file, together * with some metadata. * * Issues: automatic determination of qooxdoo path doesn't work yet. */ qx.Class.define("qx.tool.cli.commands.Create", { extend: qx.tool.cli.commands.Command, statics: { getYargsCommand() { return { command: "create <application namespace> [options]", describe: "create a new qooxdoo project", builder: { type: { alias: "t", describe: "Type of the application to create. Must be one of " + this.getSkeletonNames().join(", "), nargs: 1, requiresArg: true, type: "string" }, out: { alias: "o", describe: "Output directory for the application content." }, namespace: { alias: "s", describe: "Top-level namespace." }, name: { alias: "n", describe: "Name of application/library (defaults to namespace)." }, theme: { describe: "The name of the theme to be used.", default: "indigo" }, icontheme: { describe: "The name of the icon theme to be used.", default: "Tango" }, noninteractive: { alias: "I", describe: "Do not prompt for missing values" }, verbose: { alias: "v", describe: "Verbose logging" } } }; }, /** * Returns the names of the skeleton directories in the template folder * @returns {string[]} */ getSkeletonNames() { // need access to an non static method... let dir = path.join(qx.tool.utils.Utils.getTemplateDir(), "skeleton"); let res = fs.readdirSync(dir).filter(entry => { try { return fs.existsSync(`${dir}/${entry}/Manifest.tmpl.json`); } catch (e) { return false; } }); return res; } }, members: { /** * Creates a new qooxdoo application */ async process() { // init let argv = this.argv; let data = {}; let questions = []; let values = {}; // qooxdoo path data.qooxdoo_path = await this.getQxPath(); // use CLI options, if available // qooxdoo version try { data.qooxdoo_version = await qx.tool.config.Utils.getLibraryVersion( data.qooxdoo_path ); } catch (e) { qx.tool.compiler.Console.error(e.message); throw new qx.tool.utils.Utils.UserError( "Cannot find qooxdoo framework folder." ); } // get map of metdata on variables that need to be inserted in the templates data.template_dir = qx.tool.utils.Utils.getTemplateDir(); data.getLibraryVersion = qx.tool.config.Utils.getLibraryVersion.bind( qx.tool.config.Utils ); let template_vars; const template_vars_path = path.join( qx.tool.utils.Utils.getTemplateDir(), "template_vars" ); template_vars = require(template_vars_path)(argv, data); // prepare inquirer question data for (let var_name of Object.getOwnPropertyNames(template_vars)) { let v = template_vars[var_name]; let deflt = typeof v.default === "function" ? v.default() : v.default; // we have a final value that doesn't need to be asked for / confirmed. if (v.value !== undefined) { values[var_name] = typeof v.value === "function" ? v.value.call(values) : v.value; continue; } // do not ask for optional values in non-interactive mode if (argv.noninteractive) { if (v.optional || deflt) { values[var_name] = deflt; continue; } throw new qx.tool.utils.Utils.UserError( `Cannot skip required value for '${var_name}'.` ); } // ask user let message = `Please enter ${v.description} ${ v.optional ? "(optional)" : "" }:`; questions.push({ type: v.type || "input", choices: v.choices, name: var_name, message, default: v.default, validate: v.validate || function (answer, hash) { return true; } }); } // ask user for missing values let answers; try { answers = await inquirer.prompt(questions); } catch (e) { throw new qx.tool.utils.Utils.UserError(e.message); } // finalize values for (let var_name of Object.getOwnPropertyNames(template_vars)) { let value = values[var_name]; // combine preset and inquirer data if (answers[var_name] !== undefined) { value = answers[var_name]; } // handle special cases switch (var_name) { case "namespace": // match valid javascript object accessor TODO: allow unicode characters if (!value.match(/^([a-zA-Z_$][0-9a-zA-Z_$]*\.?)+$/)) { throw new qx.tool.utils.Utils.UserError( `Illegal characters in namespace "${value}."` ); } break; case "locales": value = JSON.stringify( value.split(/,/).map(locale => locale.trim()) ); break; // this sets 'authors' and 'authors_map' case "authors": { if (value === undefined) { values.author_map = "[]"; break; } let authors = value.split(/,/).map(a => a.trim()); values.author_map = JSON.stringify( authors.map(author => { let parts = author.split(/ /); let email = parts.pop(); return { name: parts.join(" "), email }; }), null, 2 ); value = authors.join("\n" + " ".repeat(12)); break; } } // update value values[var_name] = value; } // create application folder if it doesn't exist let appdir = path.normalize(values.out); if (!fs.existsSync(appdir)) { let parentDir = path.dirname(appdir); if (!fs.existsSync(parentDir)) { throw new qx.tool.utils.Utils.UserError( `Invalid directory ${appdir}` ); } try { fs.accessSync(parentDir, fs.constants.W_OK); } catch (e) { throw new qx.tool.utils.Utils.UserError( `Directory ${parentDir} is not writable.` ); } fs.mkdirSync(appdir); } // skeleton dir might come from options or was input interactively let app_type = argv.type || values.type; let skeleton_dir = path.join(data.template_dir, "skeleton", app_type); if (argv.type && !fs.existsSync(skeleton_dir)) { throw new qx.tool.utils.Utils.UserError( `Application type '${argv.type}' does not exist or has not been implemented yet.` ); } // copy template, replacing template vars let that = this; function traverseFileSystem(sourceDir, targetDir) { let files = fs.readdirSync(sourceDir); for (let part of files) { let sourceFile = path.join(sourceDir, part); let stats = fs.statSync(sourceFile); if (stats.isFile()) { let targetFile = path.join(targetDir, part.replace(/\.tmpl/, "")); if (sourceFile.includes(".tmpl")) { // template file let template = fs.readFileSync(sourceFile, "utf-8"); for (let var_name in values) { template = template.replace( new RegExp(`\\$\{${var_name}\}`, "g"), values[var_name] ); } if (argv.verbose) { qx.tool.compiler.Console.info( `>>> Creating ${targetFile} from template ${sourceFile}...` ); } // that.log(template); if (fs.existsSync(targetFile)) { throw new qx.tool.utils.Utils.UserError( `${targetFile} already exists.` ); } fs.writeFileSync(targetFile, template, "utf-8"); } else { // normal file if (argv.verbose) { qx.tool.compiler.Console.info( `>>> Copying ${sourceFile} to ${targetFile}...` ); } fs.copyFileSync(sourceFile, targetFile); } } else if (stats.isDirectory()) { let newTargetDir = targetDir; // replace "custon" with namespace, creating namespaced folders in the "class" dir, but not anywhere else let parts = part === "custom" ? values.namespace.split(/\./) : [part]; for (let part of parts) { newTargetDir = path.join(newTargetDir, part); fs.mkdirSync(newTargetDir); } traverseFileSystem(sourceFile, newTargetDir); } } } // go traverseFileSystem.bind(this)(skeleton_dir, appdir); } } });