UNPKG

@enact/cli

Version:

Full-featured build environment tool for Enact applications.

316 lines (292 loc) 10.1 kB
// @remove-file-on-eject const os = require('os'); const path = require('path'); const url = require('url'); const spawn = require('cross-spawn'); const fs = require('fs-extra'); const minimist = require('minimist'); const prompts = require('prompts'); const tar = require('tar'); let chalk; const TEMPLATE_DIR = path.join(process.env.APPDATA || os.homedir(), '.enact'); const INCLUDED = path.dirname(require.resolve('@enact/template-sandstone')); const DEFAULT_LINK = path.join(TEMPLATE_DIR, 'default'); function displayHelp() { let e = 'node ' + path.relative(process.cwd(), __filename); if (require.main !== module) e = 'enact template'; console.log(' Usage'); console.log(` ${e} <action> ...`); console.log(); console.log(' Actions'); console.log(` ${e} install [source] [name]`); console.log(chalk.dim(' Install a template from a local or remote source')); console.log(); console.log(' source Git URI, npm package or local directory'); console.log(' (default: cwd)'); console.log(' name Specific name for the template'); console.log(); console.log(` ${e} link [directory] [name]`); console.log(chalk.dim(' Symlink a directory into template management')); console.log(); console.log(' directory Local directory path to link'); console.log(' (default: cwd)'); console.log(' name Specific name for the template'); console.log(); console.log(` ${e} remove <name>`); console.log(chalk.dim(' Remove a template by name')); console.log(); console.log(' name Name of template to remove'); console.log(); console.log(` ${e} default [name]`); console.log(chalk.dim(' Choose a default template for "enact create"')); console.log(); console.log(' name Specific template to set default'); console.log(); console.log(` ${e} list`); console.log(chalk.dim(' List all templates installed/linked')); console.log(); console.log(' Options'); console.log(' -v, --version Display version information'); console.log(' -h, --help Display help information'); console.log(); process.exit(0); } function initTemplateArea() { if (!fs.existsSync(TEMPLATE_DIR)) { fs.mkdirSync(TEMPLATE_DIR); } else { // Remove any dead/unreadable link fs.readdirSync(TEMPLATE_DIR) .map(d => path.join(TEMPLATE_DIR, d)) .forEach(d => { try { fs.realpathSync(d); } catch (e) { fs.removeSync(d); } }); } const init = doLink(path.join(INCLUDED, 'template'), 'sandstone'); const sandstoneLink = path.join(TEMPLATE_DIR, 'sandstone'); return init.then(() => !fs.existsSync(DEFAULT_LINK) && doLink(sandstoneLink, 'default')); } function doInstall(target, name) { const github = target.match(/^([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})\/([-_.\w]+)((?:#|@)?[-_.\w]+)?$/); if (github) { // If target is GitHub shorthand, resolve to full HTTPS URI target = 'https://github.com/' + github[1] + '/' + github[2] + '.git' + (github[3] || ''); } let installation; if (/(?:git|ssh|https?|git@[-\w.]+):(\/\/)?(.*?)(\.git)(\/?|#[-\d\w._]+?)$/.test(target)) { installation = installFromGit(target, name); } else if (fs.existsSync(target)) { installation = installFromLocal(target, name); } else { installation = installFromNPM(target, name); } return installation.then(resolved => { // npm install if needed return new Promise((resolve, reject) => { const output = path.join(TEMPLATE_DIR, resolved); if (fs.existsSync(path.join(output, 'template')) && fs.existsSync(path.join(output, 'package.json'))) { const child = spawn('npm', ['--loglevel', 'error', 'install', '--production'], { stdio: 'inherit', cwd: output }); child.on('close', code => { if (code !== 0) { reject(new Error('Failed to npm install dynamic template. Ensure package.json is valid.')); } else { resolve(resolved); } }); } else { resolve(resolved); } }); }); } function normalizeName(name) { return name.replace(/(?:^enact-template-|^template-)/g, ''); } // Clone Git repository using specific branch if desired function installFromGit(target, name = normalizeName(path.basename(url.parse(target).pathname, '.git'))) { const git = target.match(/^(?:(^.*)#([\w\d-_.]+)?|(^.*))$/); const args = ['clone', git[1] || git[3], name, '-c', 'advice.detachedHead=false']; if (git[2]) args.splice(2, 0, '-b', git[2]); fs.removeSync(path.join(TEMPLATE_DIR, name)); return new Promise((resolve, reject) => { const child = spawn('git', args, {stdio: 'inherit', cwd: TEMPLATE_DIR}); child.on('close', code => { if (code !== 0) { reject(new Error(`Unable to clone git URI ${target}.`)); } else { resolve(name); } }); }); } // Copy directory files function installFromLocal(target, name = normalizeName(path.basename(target))) { const output = path.join(TEMPLATE_DIR, name); fs.removeSync(output); fs.ensureDirSync(output); return fs .copy(target, output) .then(() => name) .catch(err => { throw new Error(`Failed to copy template files from ${target}.\n${err.message}`); }); } // Download and extract NPM package function installFromNPM(target, name = normalizeName(path.basename(target).replace(/@.*$/g, ''))) { const tempDir = path.join(os.tmpdir(), 'enact'); fs.removeSync(tempDir); fs.ensureDirSync(tempDir); return new Promise((resolve, reject) => { const child = spawn('npm', ['--loglevel', 'error', 'pack', target], {stdio: 'ignore', cwd: tempDir}); child.on('close', code => { if (code !== 0) { reject(new Error('Invalid template target: ' + target)); } else { const tarball = fs.readdirSync(tempDir).filter(f => f.endsWith('.tgz'))[0]; if (tarball) { tar.x({file: path.join(tempDir, tarball), cwd: tempDir}, [], err => { if (err) { reject(new Error(`Tarball extraction failure.\n${err.message}`)); } else { resolve(); } }); } else { reject(new Error(`Failed to download npm package ${target} from registry.`)); } } }); }) .then(() => installFromLocal(path.join(tempDir, 'package'), name)) .then(() => { fs.removeSync(tempDir); return name; }); } function doLink(target, name = normalizeName(path.basename(path.resolve(target)))) { const directory = path.resolve(target); const prevCWD = process.cwd(); process.chdir(TEMPLATE_DIR); return fs .remove(name) .then(() => fs.symlink(directory, name, 'junction')) .then(() => { process.chdir(prevCWD); return {target, name}; }) .catch(err => { process.chdir(prevCWD); throw new Error(`Unable to setup symlink to ${directory}.\n${err.message}`); }); } function doRemove(name) { const output = path.join(TEMPLATE_DIR, name); const isDefault = fs.existsSync(DEFAULT_LINK) && fs.realpathSync(output) === fs.realpathSync(DEFAULT_LINK); if (!fs.existsSync(output)) return Promise.reject(new Error(`Unable to remove. Template "${name}" not found.`)); return fs .remove(output) .then(() => isDefault && fs.removeSync(DEFAULT_LINK)) .catch(err => { throw new Error(`Failed to delete template ${name}.\n${err.message}`); }); } function doDefault(name) { const all = fs.readdirSync(TEMPLATE_DIR).filter(t => t !== 'default'); let choice; if (name && all.includes(name)) { choice = Promise.resolve({template: name}); } else { const i = all.find(t => fs.realpathSync(path.join(TEMPLATE_DIR, t)) === fs.realpathSync(DEFAULT_LINK)); choice = prompts([ { name: 'template', type: 'list', choices: all, default: i, message: 'Which template would you like as the default?' } ]); } return choice.then(response => doLink(path.join(TEMPLATE_DIR, response.template), 'default')); } function doList() { const realDefault = fs.realpathSync(DEFAULT_LINK); const all = fs.readdirSync(TEMPLATE_DIR).filter(t => t !== 'default'); console.log(chalk.bold('Available Templates')); all.forEach(t => { let item = ' ' + t; const template = path.join(TEMPLATE_DIR, t); const realTemplate = fs.realpathSync(template); if (realTemplate === realDefault) { item += chalk.green(' (default)'); } if (fs.lstatSync(template).isSymbolicLink()) { item += chalk.dim(' -> ' + realTemplate); } console.log(item); }); } function api({action, target, name} = {}) { return initTemplateArea().then(() => { let actionPromise; if (['install', 'link', 'remove'].includes(action) && name === 'default') throw new Error('Template "default" name is reserved. ' + 'Use "enact template default" to modify it.'); switch (action) { case 'install': actionPromise = doInstall(target, name).then(resolved => { console.log(`Template "${resolved}" installed.`); }); break; case 'link': actionPromise = doLink(target, name).then(linked => { console.log(`Template "${linked.name}" linked from ${path.resolve(linked.target)}.`); }); break; case 'remove': actionPromise = doRemove(name).then(() => { console.log(`Template "${name}" removed.`); }); break; case 'default': actionPromise = doDefault(name).then(() => { console.log('Template successfully set as default.'); }); break; case 'list': doList(); break; default: actionPromise = Promise.reject(new Error(`Invalid template action: ${action}.`)); break; } return actionPromise; }); } function cli(args) { import('chalk').then(({default: _chalk}) => { chalk = _chalk; const opts = minimist(args, { boolean: ['help'], alias: {h: 'help'} }); if (opts.help) displayHelp(); const action = opts._[0]; const target = opts._[1] || process.cwd(); const name = ['install', 'link'].includes(action) ? opts._[2] : opts._[1]; if (!action) displayHelp(); api({action, name, target}).catch(err => { console.error('Template action failed.'); console.error(); console.error(chalk.red('ERROR: ') + err.message); process.exit(1); }); }); } module.exports = {api, cli};