UNPKG

@enact/cli

Version:

Full-featured build environment tool for Enact applications.

317 lines (285 loc) 10 kB
// @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 cp = require('child_process'); const os = require('os'); const path = require('path'); const fs = require('fs-extra'); const prompts = require('prompts'); const minimist = require('minimist'); const {packageRoot} = require('@enact/dev-utils'); const spawn = require('cross-spawn'); let chalk; const assets = [ {src: path.join(__dirname, '..', 'config'), dest: 'config'}, {src: path.join(__dirname, '..', 'config', 'jest'), dest: 'config/jest'}, {src: path.join(__dirname, '..', 'commands'), dest: 'scripts'} ]; const internal = [ '@babel/plugin-transform-modules-commonjs', 'babel-plugin-transform-rename-import', 'glob', 'global-modules', 'less-plugin-npm-import', 'semver', 'tar', 'validate-npm-package-name' ]; const enhanced = ['chalk', 'cross-spawn', 'filesize', 'fs-extra', 'minimist', 'strip-ansi']; const content = ['@babel/runtime', 'core-js', 'react', 'react-dom']; const bareDeps = {'cpy-cli': '^3.1.1', rimraf: '^3.0.2'}; const bareTasks = { serve: 'webpack-dev-server --hot --inline --env development --config config/webpack.config.js', pack: 'webpack --env development --config config/webpack.config.js && cpy public dist', 'pack-p': 'webpack --env production --config config/webpack.config.js && cpy public dist', watch: 'cpy public dist && webpack --env development --config config/webpack.config.js --watch', clean: 'rimraf build dist', lint: 'eslint --no-config-lookup --config enact --ignore-pattern config/* .', license: 'license-checker ', test: 'jest --config config/jest/jest.config.js', 'test-watch': 'jest --config config/jest/jest.config.js --watch' }; function displayHelp() { let e = 'node ' + path.relative(process.cwd(), __filename); if (require.main !== module) e = 'enact eject'; console.log(' Usage'); console.log(` ${e} [options]`); console.log(); console.log(' Options'); console.log(' -b, --bare Abandon Enact CLI command enhancements'); console.log(' and eject into a a barebones setup (using'); console.log(' webpack, eslint, karma, etc. directly)'); console.log(' -v, --version Display version information'); console.log(' -h, --help Display help information'); console.log(); process.exit(0); } function validateEject() { return prompts({ type: 'confirm', name: 'shouldEject', message: 'Are you sure you want to eject? This action is permanent.', default: false }).then(answer => { if (!answer.shouldEject) { console.log(chalk.cyan('Close one! Eject aborted.')); return {abort: true}; } else { checkGitStatus(); // Make shallow array of files paths const files = assets.reduce((list, dir) => { return list.concat( fs .readdirSync(dir.src) // set full relative path .map(file => ({ src: path.join(dir.src, file), dest: path.join(dir.dest, file) })) // omit dirs from file list .filter(file => fs.lstatSync(file.src).isFile()) ); }, []); files.forEach(verifyAbsent); return {files}; } }); } function checkGitStatus() { let status; try { const stdout = cp.execSync(`git status --porcelain`, {stdio: ['pipe', 'pipe', 'ignore']}); status = stdout.toString().trim(); } catch (e) { status = ''; } if (status) { throw new Error( chalk.red('This git repository has untracked files or uncommitted changes:') + '\n\n' + status .split('\n') .map(line => line.match(/ .*/g)[0].trim()) .join('\n') + '\n\n' + chalk.red('Remove untracked files, stash or commit any changes, and try again.') ); } } function verifyAbsent({dest}) { if (fs.existsSync(dest)) { throw new Error( `"${dest}" already exists in your app folder. We cannot ` + 'continue as you would lose all the changes in that file or directory. ' + 'Please move or delete it (maybe make a copy for backup) and run this ' + 'command again.' ); } } function copySanitizedFile({src, dest}) { let data = fs.readFileSync(src, {encoding: 'utf8'}); // Skip flagged files if (data.match(/\/\/ @remove-file-on-eject/)) { return false; } data = data // Remove dead code from .js files on eject .replace(/[\t ]*\/\/ @remove-on-eject-begin([\s\S]*?)\/\/ @remove-on-eject-end\n?/gm, '') // Remove dead code from .applescript files on eject .replace(/[\t ]*-- @remove-on-eject-begin([\s\S]*?)-- @remove-on-eject-end\n?/gm, '') .trim() + '\n'; console.log(` Adding ${chalk.cyan(dest)} to the project`); fs.writeFileSync(dest, data, {encoding: 'utf8'}); } function configurePackage(bare) { const own = require('../package.json'); const app = require(path.resolve('package.json')); const backup = JSON.stringify(app, null, 2) + os.EOL; const availScripts = fs.existsSync('./scripts') ? fs.readdirSync('./scripts').map(f => f.replace(/\.js$/, '')) : []; const enactCLI = new RegExp('enact (' + availScripts.join('|') + ')', 'g'); const eslintConfig = {extends: 'enact'}; const eslintIgnore = ['build/*', 'config/*', 'dist/*', 'node_modules/*', 'scripts/*']; const conflicts = []; app.dependencies = app.dependencies || []; app.devDependencies = app.devDependencies || []; // Merge the applicable dependencies Object.keys(own.dependencies).forEach(key => { if (!internal.includes(key)) { if (content.includes(key)) { console.log(` Adding ${chalk.cyan(key)} to dependencies`); app.dependencies[key] = app.dependencies[key] || own.dependencies[key]; } else if (!enhanced.includes(key) || !bare) { console.log(` Adding ${chalk.cyan(key)} to devDependencies`); app.devDependencies[key] = own.dependencies[key]; } } }); // Add any additional dependencies if (bare) { Object.keys(bareDeps).forEach(key => { console.log(` Adding ${chalk.cyan(key)} to devDependencies`); app.devDependencies[key] = bareDeps[key]; }); } console.log(); // Update NPM task scripts const type = chalk.cyan('npm script'); Object.keys(app.scripts).forEach(key => { if (bare && bareTasks[key]) { if (!conflicts.includes(type)) conflicts.push(type); const bin = bareTasks[key].match(/^(?:node\s+)*(\S*)/); const updated = (bin && bin[1]) || bareTasks[key]; console.log(` Updating npm task ${chalk.cyan(key)} to use ${chalk.cyan(updated)}`); app.scripts[key] = bareTasks[key]; } else if (!bare) { app.scripts[key] = app.scripts[key].replace(enactCLI, (match, name) => { console.log(` Updating npm task ${chalk.cyan(key)} to use ` + chalk.cyan(`scripts/${name}.js`)); return `node ./scripts/${name}.js`; }); } }); console.log(); // Update ESLint settings console.log(` Setting up ${chalk.cyan('ESlint')} config in package.json`); if (app.eslintConfig && JSON.stringify(app.eslintConfig) !== JSON.stringify(eslintConfig)) { conflicts.push(chalk.cyan('ESLint')); } app.eslintConfig = eslintConfig; app.eslintIgnore = app.eslintIgnore || []; app.eslintIgnore = app.eslintIgnore.concat(eslintIgnore.filter(l => !app.eslintIgnore.includes(l))); backupOld(['.eslintignore', 'eslint.config.js']); // Sort the package.json output ['dependencies', 'devDependencies'].forEach(obj => { const unsortedDependencies = app[obj]; delete app[obj]; app[obj] = {}; Object.keys(unsortedDependencies) .sort() .forEach(key => { app[obj][key] = unsortedDependencies[key]; }); }); fs.writeFileSync('package.json', JSON.stringify(app, null, 2) + os.EOL, {encoding: 'utf8'}); if (conflicts.length > 0) fs.writeFileSync('package.old.json', backup, {encoding: 'utf8'}); return conflicts; } function backupOld(files) { files.filter(fs.existsSync).forEach(f => { const backup = path.basename(f, path.extname(f)) + '.old' + path.extname(f); console.log(` Found existing ${chalk.cyan(f)}; backing up to ${chalk.cyan(backup)}`); fs.renameSync(f, backup); }); } function npmInstall() { return new Promise((resolve, reject) => { const proc = spawn('npm', ['--loglevel', 'error', 'install'], {stdio: 'inherit', cwd: process.cwd()}); proc.on('close', code => { if (code !== 0) { reject(new Error('npm install failed.')); } else { resolve(); } }); }); } function api({bare = false} = {}) { if (bare) { assets.pop(); } return validateEject().then(({abort = false, files = []}) => { if (!abort) { console.log('Ejecting...'); console.log(); console.log(chalk.cyan(`Copying files into ${process.cwd()}`)); assets.forEach(dir => !fs.existsSync(dir.dest) && fs.mkdirSync(dir.dest, {recursive: true})); files.forEach(copySanitizedFile); console.log(); console.log(chalk.cyan('Configuring package.json')); const con = configurePackage(bare); console.log(); console.log(chalk.cyan('Running npm install...')); return npmInstall().then(() => { if (con.length > 0) { let list = con[0]; if (con.length > 1) list = con.splice(1).join(', ') + ' and ' + list; console.log(); console.log( chalk.yellow( `NOTICE: Existing ${list} settings within the package.json ` + 'were overwritten. A backup of the original content has been ' + 'preserved to package.old.json.' ) ); } console.log(); console.log(chalk.green('Ejected successfully!')); console.log(); }); } }); } function cli(args) { const opts = minimist(args, { boolean: ['bare', 'help'], alias: {b: 'bare', h: 'help'} }); if (opts.help) displayHelp(); process.chdir(packageRoot().path); import('chalk').then(({default: _chalk}) => { chalk = _chalk; api({bare: opts.bare}).catch(err => { console.error(chalk.red('ERROR: ') + err.message); process.exit(1); }); }); } module.exports = {api, cli};