@enact/cli
Version:
Full-featured build environment tool for Enact applications.
297 lines (274 loc) • 10.3 kB
JavaScript
// @remove-file-on-eject
/**
* Portions of this source code file are from create-react-app, used under the
* following MIT license:
*
* Copyright (c) 2013-present, Facebook, Inc.
* https://github.com/facebook/create-react-app
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const os = require('os');
const path = require('path');
const spawn = require('cross-spawn');
const fs = require('fs-extra');
const minimist = require('minimist');
const validatePackageName = require('validate-npm-package-name');
let chalk;
const ENACT_DEV_NPM = '@enact/cli';
const INCLUDED = path.dirname(require.resolve('@enact/template-sandstone'));
const TEMPLATE_DIR = path.join(process.env.APPDATA || os.homedir(), '.enact');
const defaultGenerator = {
overwrite: false,
install: true,
type: 'app',
validate: ({template, name}) => {
const validation = validatePackageName(name);
if (!validation.validForNewPackages) {
throw new Error(
`Cannot create a project called ${chalk.bold(name)} because of npm naming restrictions:\n` +
validation.errors
.concat(validation.warnings)
.map(r => ' * ' + r)
.join('/n')
);
} else {
const meta = fs.readJsonSync(path.join(template, 'package.json'), {throws: false}) || {};
const deps = Object.keys(meta.dependencies || {}).concat(Object.keys(meta.devDependencies || {}));
deps.sort();
if (deps.includes(name)) {
throw new Error(
'Cannot create a project called ' +
chalk.bold(name) +
' because a dependency with the same name exists.\n' +
'Due to the way npm works, the following names are not allowed:\n\n' +
chalk.cyan(deps.map(d => '\t' + d).join('\n')) +
'\n\nPlease choose a different project name.'
);
}
}
},
prepare: ({directory, name, type}) => {
// After the workspace is ensured to exist, but before template is copied
const validFiles = [
// Generic project fragments and outside tool configs.
'.DS_Store',
'Thumbs.db',
'.git',
'.gitignore',
'.idea',
'README.md',
'LICENSE',
'web.iml',
'.hg',
'.hgignore',
'.hgcheck',
'.npmignore',
'mkdocs.yml',
'docs',
'.travis.yml',
'.gitlab-ci.yml',
'.gitattributes',
// Error fragments that can be ignored safely.
'npm-debug.log',
'npm-debug.log',
'yarn-error.log',
'yarn-debug.log'
];
const rel = path.relative(process.cwd(), directory);
if (!rel || rel === '.') {
console.log(`Creating a new Enact ${type}...`);
} else {
console.log(`Creating a new Enact ${type} in ${rel}...`);
}
console.log();
if (!fs.readdirSync(directory).every(f => validFiles.includes(f))) {
throw new Error(`The directory ${chalk.bold(name)} contains file(s) that could conflict. Aborting.`);
}
},
setup: ({directory, name}) => {
// Do stuff to setup the directory workspace after template is copied
// Update package.json name
const pkgJSON = path.join(directory, 'package.json');
const meta = fs.readJsonSync(pkgJSON, {encoding: 'UTF8', throws: false}) || {};
meta.name = name;
fs.writeJsonSync(pkgJSON, meta, {encoding: 'UTF8', spaces: '\t'});
// Update appinfo.json if it exists in the template
let appinfo = path.join(directory, 'appinfo.json');
if (!fs.existsSync(appinfo)) {
appinfo = path.join(directory, 'webos-meta', 'appinfo.json');
if (!fs.existsSync(appinfo)) {
appinfo = undefined;
}
}
if (appinfo) {
const aiMeta = fs.readJsonSync(appinfo, {encoding: 'UTF8', throws: false}) || {};
aiMeta.id = meta.name;
fs.writeJsonSync(appinfo, aiMeta, {encoding: 'UTF8', spaces: '\t'});
}
},
complete: ({directory, name}) => {
// After everything is complete, output message to user
console.log();
console.log('Success! Created ' + name + ' at ' + directory);
console.log();
console.log('Inside that directory, you can run several npm commands, including:');
console.log(chalk.cyan(' npm run serve'));
console.log(' Starts the development server.');
console.log(chalk.cyan(' npm run pack'));
console.log(' Bundles the app into static files in development mode.');
console.log(chalk.cyan(' npm run pack-p'));
console.log(' Bundles the app into static files in production mode.');
console.log(chalk.cyan(' npm run test'));
console.log(' Starts the test runner.');
console.log();
// @TODO
// console.log(chalk.cyan(' npm run eject'));
// console.log(' Removes this tool and copies build dependencies, configuration files');
// console.log(' and scripts into the app directory. If you do this, you can’t go back!');
// console.log();
console.log('We suggest that you begin by typing:');
if (path.resolve(process.cwd()) !== path.resolve(directory)) {
console.log(chalk.cyan(' cd ' + path.relative(process.cwd(), directory)));
}
console.log(' ' + chalk.cyan('npm run serve'));
console.log();
console.log('Have fun!');
}
};
function displayHelp() {
let e = 'node ' + path.relative(process.cwd(), __filename);
if (require.main !== module) e = 'enact create';
console.log(' Usage');
console.log(` ${e} [options] [<directory>]`);
console.log();
console.log(' Arguments');
console.log(' directory Optional destination directory');
console.log(' (default: cwd)');
console.log();
console.log(' Options');
console.log(' -t, --template Specific template to use');
console.log(' --local Include @enact/cli locally');
console.log(' --verbose Verbose output logging');
console.log(' -v, --version Display version information');
console.log(' -h, --help Display help information');
console.log();
process.exit(0);
}
function resolveTemplateGenerator(template) {
return new Promise((resolve, reject) => {
let templatePath = path.join(TEMPLATE_DIR, template);
if (!fs.existsSync(templatePath)) {
if (['default', 'sandstone'].includes(template)) {
templatePath = path.join(INCLUDED, 'template');
} else {
reject(new Error(`Template ${chalk.bold(template)} not found.`));
}
}
templatePath = fs.realpathSync(templatePath);
const subDir = path.join(templatePath, 'template');
if (fs.existsSync(subDir)) {
try {
const generator = require(templatePath) || {};
Object.keys(defaultGenerator).forEach(k => {
if (generator[k] === undefined) {
generator[k] = defaultGenerator[k];
}
});
resolve({generator, templatePath: subDir});
} catch (e) {
if (e.message === `Cannot find module '${templatePath}'`) {
resolve({generator: defaultGenerator, templatePath: subDir});
} else {
reject(new Error(`Failed to load ${chalk.bold(template)} template generator.\n${e}`));
}
}
} else {
resolve({generator: defaultGenerator, templatePath});
}
});
}
function copyTemplate(template, output, overwrite) {
const outputReadme = path.join(output, 'README.md');
const outputGitIgnore = path.join(output, '.gitignore');
let templateGitIgnore = fs.readdirSync(template).filter(f => ['.gitignore', 'gitignore'].includes(f))[0];
templateGitIgnore = templateGitIgnore && path.join(template, templateGitIgnore);
if (fs.existsSync(outputReadme) && fs.existsSync(path.join(template, 'README.md'))) {
console.log(chalk.yellow('Found an existing README.md file. Renaming to README.old.md to avoid overwriting.'));
console.log();
fs.moveSync(outputReadme, path.join(output, 'README.old.md'));
}
// Copy the files for the user
const filter = src => src !== templateGitIgnore;
return fs
.copy(template, output, {overwrite: overwrite, errorOnExist: !overwrite, filter: filter})
.then(() => {
// Handle gitignore after the fact to prevent npm from renaming it to .npmignore
// See: https://github.com/npm/npm/issues/1862
if (templateGitIgnore) {
if (fs.existsSync(outputGitIgnore)) {
// Append if there's already a `.gitignore` file there
const data = fs.readFileSync(templateGitIgnore, {encoding: 'UTF8'});
fs.appendFileSync(outputGitIgnore, data, {encoding: 'UTF8'});
} else {
fs.copySync(templateGitIgnore, outputGitIgnore);
}
}
})
.catch(err => {
throw new Error(`Failed to copy template files to ${output}\n${err.stack}`);
});
}
function npmInstall(directory, verbose, ...rest) {
const args = ['--loglevel', verbose ? 'verbose' : 'error', 'install', ...rest];
return new Promise((resolve, reject) => {
const proc = spawn('npm', args, {stdio: 'inherit', cwd: directory});
proc.on('close', code => {
if (code !== 0) {
reject(new Error('npm install failed.'));
} else {
resolve();
}
});
});
}
function api(opts = {}) {
return resolveTemplateGenerator(opts.template).then(({generator, templatePath}) => {
const params = Object.assign({}, opts, {opts, defaultGenerator, type: generator.type});
return new Promise(resolve => resolve(generator.validate && generator.validate(params)))
.then(() => fs.ensureDir(opts.directory))
.then(() => generator.prepare && generator.prepare(params))
.then(() => copyTemplate(templatePath, opts.directory, generator.overwrite))
.then(() => generator.setup && generator.setup(params))
.then(() => generator.install && npmInstall(opts.directory, opts.verbose))
.then(() => {
if (opts.local) {
console.log('Installing @enact/cli locally. This might take a couple minutes.');
return npmInstall(opts.directory, opts.verbose, '--save', '@babel/runtime', 'core-js').then(() =>
npmInstall(opts.directory, opts.verbose, '--save-dev', ENACT_DEV_NPM)
);
}
})
.then(() => generator.complete && generator.complete(params));
});
}
function cli(args) {
const opts = minimist(args, {
boolean: ['local', 'verbose', 'help'],
string: ['template'],
default: {template: 'default'},
alias: {t: 'template', h: 'help'}
});
if (opts.help) displayHelp();
opts.directory = path.resolve(typeof opts._[0] !== 'undefined' ? opts._[0] + '' : process.cwd());
opts.name = path.basename(opts.directory).replace(/ /g, '-').toLowerCase();
import('chalk').then(({default: _chalk}) => {
chalk = _chalk;
api(opts).catch(err => {
console.error(chalk.red('ERROR: ') + err.message);
process.exit(1);
});
});
}
module.exports = {api, cli};