UNPKG

tarima

Version:
643 lines (500 loc) 16.1 kB
#!/usr/bin/env node 'use strict'; /* eslint-disable global-require */ /* eslint-disable no-nested-ternary */ const path = require('path'); const wargs = require('wargs'); // common helpers const die = process.exit.bind(process); const $ = require('./lib/utils'); // eslint-disable-line const DEFAULTS = { public: 'public', output: 'build', port: process.env.PORT || 3000, env: process.env.NODE_ENV || 'development', }; let _; try { _ = wargs(process.argv.slice(2), { boolean: 'sdqvVmdfhMUACI', default: DEFAULTS, alias: { M: 'esm', U: 'umd', A: 'amd', C: 'cjs', I: 'iife', S: 'no-serve', i: 'include', s: 'sources', b: 'bundle', a: 'alias', q: 'quiet', W: 'public', O: 'output', e: 'env', h: 'help', c: 'config', v: 'version', V: 'verbose', x: 'exclude', l: 'plugins', r: 'reloader', w: 'watching', d: 'debug', f: 'force', y: 'only', p: 'port', P: 'proxy', R: 'rename', G: 'globals', E: 'extensions', }, }); } catch (e) { $.errLog(`${e.message || e.toString()} (add --help for usage info)`); die(1); } // nice logs! const _level = _.flags.verbose ? 'verbose' : _.flags.debug ? 'debug' : 'info'; const logger = require('log-pose') .setLevel((_.flags.quiet && !_.flags.version && !_.flags.help) ? false : _level) .getLogger(12, process.stdout, process.stderr); if (_.flags.debug && _.flags.verbose) { require('debug').enable('*'); // eslint-disable-line require('log-pose').setLevel(false); // eslint-disable-line } // local debug const debug = require('debug')('tarima'); const thisPkg = require(path.join(__dirname, '../package.json')); _.flags.env = (_.flags.env !== true ? _.flags.env : '') || 'development'; // defaults process.name = 'tarima'; process.env.NODE_ENV = _.flags.env; const gitDir = path.join(__dirname, '../.git'); logger.printf('{% green %s v%s %} {% gray (node %s - %s%s) %}\n', thisPkg.name, thisPkg.version, process.version, $.isDir(gitDir) ? 'git:' : '', process.env.NODE_ENV); debug('v%s - node %s%s', thisPkg.version, process.version); if (_.flags.version) { die(); } const _bin = Object.keys(thisPkg.bin)[0]; if (_.flags.help) { logger.write(` Usage: ${_bin} [watch] ... Examples: ${_bin} app/assets js:es6 css:less ${_bin} src/**/*.js API_KEY=*secret* PORT=3000 ${_bin} watch lib -R "**/*:{basedir/1}/{fname}" -R "**/mock:{basedir/2}/api/{fname}" Options: -e, --env Customization per environment (e.g. -e production) -O, --output Destination for generated files -W, --public Public directory for serving assets -c, --config Use configuration file (e.g. -c ./config.js) You may also specify a suffix, e.g. -c DEV will map to ./tarima.DEV.{js,json} -l, --plugins Shorthand option for loading plugins (e.g. -l tarima-bower -l talavera) -i, --include Additional folder(s) to include on bundles (e.g. -i web_modules,app/modules) -p, --port Port used for live-server -h, --host Host to bind the live-server -P, --proxy Proxy requests (e.g. -P ROUTE:URL) -S, --no-serve Disable live-server during watch mode --serve Additional directories to mount (mmultiple) --no-browser Do not launch the default browser --browser Custom browser name to launch --middleware Add a middleware handler --entry-file Serve this one on missing files --spa Enable /abc to /#/abc translation for SPAs --wait Milliseconds to wait before reloading --cors Enable CORS for any origin -f, --force Force rendering/bundling of all given sources -a, --alias Enable custom aliasing for bundling (e.g. -a x:./src/y.js) -b, --bundle Scripts matching this will be bundled (e.g. -b "**/main/*.js") -s, --sources Save generated sourceMaps as .map files alongside outputted files -M, --esm Save bundles as ESM -U, --umd Save bundles as UMD wrapper -A, --amd Save bundles as AMD wrapper -C, --cjs Save bundles as CommonJS wrapper -I, --iife Save bundles as IIFE wrapper (default) -q, --quiet Minimize output logs -d, --debug Enable debug mode when transpiling -V, --verbose Enable verbose logs (use for trouble-shooting) -y, --only Filter out non-matching sources using src.indexOf("substr") -x, --exclude Filter out sources using globs (e.g. -x test/broken -x .coffee) Exclude patterns: - *foo -> !*foo - .bar -> !**/*.bar - x.y -> !**/x.y - foo -> !**/foo/** - foo/bar -> !**/foo/bar/** -r, --reloader Load module for reset stuff on changes (e.g. -r bin/server) -w, --watching Append additional directories for watch (e.g. -w _src -w bin) -E, --extensions Enable hidden extensions (e.g. -E .es6.js -E .post.css -E .js.hbs.pug) -G, --globals Shorthand for global variables (e.g. -G FOO=BAR -G AKI_PEY=xyz) -R, --rename Custom naming expressions (e.g. -R "**/*:{basedir/1}/{fname}") `); die(1); } function _debug(e) { return (_.flags.verbose && e.stack) || e.toString(); } const run = (opts, cb) => { const _runner = require('./lib'); debug('settings %s', JSON.stringify(opts, null, 2)); // delay once resolver loads process.nextTick(() => { try { _runner(opts, logger, cb); } catch (e) { $.errLog(_debug(e)); die(1); } }); }; const { spawn } = require('child_process'); // eslint-disable-line // empty dummy let mainPkg = {}; const cwd = process.cwd(); const pkg = path.join(cwd, 'package.json'); // load .env const env = require('dotenv').config(); // eslint-disable-line if (env.error && env.error.code !== 'ENOENT') { $.errLog(env.error); die(1); } delete env.error; if ($.isFile(pkg)) { debug('config %s', pkg); mainPkg = $.readJSON(pkg); } let isWatching = false; if (_._[0] === 'watch') { isWatching = true; _._.shift(); } const _src = _._; const defaultConfig = { cwd, watch: isWatching, serve: _.flags.serve, force: _.flags.force === true, bundle: $.toArray(_.flags.bundle), plugins: $.toArray(_.flags.plugins), watching: $.toArray(_.flags.watching), rename: $.toArray(_.flags.rename), from: _src, output: _.flags.output || DEFAULTS.output, public: _.flags.public || DEFAULTS.public, cacheFile: '.tarima', filter: [], notifications: { title: mainPkg.name || path.basename(cwd), okIcon: path.join(__dirname, 'ok.png'), errIcon: path.join(__dirname, 'err.png'), }, bundleOptions: { paths: $.toArray(_.flags.include), globals: _.data, extensions: _.params, }, flags: _.flags, reloader: _.flags.reloader, liveServer: { port: _.flags.port, host: _.flags.host, proxy: _.flags.proxy, spa: _.flags.spa, wait: _.flags.wait, cors: _.flags.cors, file: _.flags.entryFile, middleware: _.flags.middleware, open: _.flags.browser !== false, browser: _.flags.browser, }, }; // apply package settings try { const pkgInfo = mainPkg.tarima || {}; // normalize some inputs per-environment ['from', 'ignore', 'bundle', 'rename'].forEach(key => { if (pkgInfo[key] && !(typeof pkgInfo[key] === 'string' || Array.isArray(pkgInfo[key]))) { pkgInfo[key] = (pkgInfo[key].default || []).concat(pkgInfo[key][_.flags.env] || []); } }); if (defaultConfig.serve === false) { delete pkgInfo.serve; } $.merge(defaultConfig, pkgInfo); } catch (e) { $.errLog(`Configuration mismatch: ${_debug(e)}`); die(1); } // support for tarima.CONFIG.{js,json} let configFile = _.flags.config === true ? 'config' : _.flags.config; if (configFile && configFile.indexOf('.') === -1) { const fixedConfig = path.join(cwd, `tarima.${configFile}`); [`${fixedConfig}.js`, `${fixedConfig}.json`].forEach(file => { if ($.isFile(file)) { configFile = file; } }); } if (configFile) { if (!$.isFile(configFile)) { logger.info('\r{% fail Missing file: %s %}\n', configFile); die(1); } logger.info('{% log Loading settings from %s %}\n', path.relative(cwd, configFile)); debug('config %s', configFile); $.merge(defaultConfig, require(path.resolve(configFile))); } // normalize extensions $.merge(defaultConfig.bundleOptions.extensions, defaultConfig.extensions || {}); const rollupConfig = defaultConfig.bundleOptions.rollup = defaultConfig.bundleOptions.rollup || {}; // setup rollup format if (_.flags.es || _.flags.esm) { rollupConfig.format = 'esm'; } if (_.flags.umd) { rollupConfig.format = 'umd'; } if (_.flags.amd) { rollupConfig.format = 'amd'; } if (_.flags.cjs) { rollupConfig.format = 'cjs'; } if (_.flags.iife) { rollupConfig.format = 'iife'; } // enable dynamic aliasing based on environment! if (rollupConfig.aliases) { Object.assign(rollupConfig.aliases, rollupConfig.aliases.default); Object.keys(rollupConfig.aliases).forEach(key => { if (typeof rollupConfig.aliases[key] === 'object') { if (key === _.flags.env) { Object.assign(rollupConfig.aliases, rollupConfig.aliases[key]); } delete rollupConfig.aliases[key]; } }); } // additional aliases from given flags $.toArray(_.flags.alias).forEach(test => { const [from, to] = test.split(':'); rollupConfig.aliases = rollupConfig.aliases || {}; rollupConfig.aliases[from] = to; }); delete defaultConfig.extensions; function fixedValue(string) { if (/^-?\d+(\.\d+)?$/.test(string)) { return parseFloat(string); } const values = { true: true, false: false, }; if (typeof values[string] !== 'undefined') { return values[string]; } return string || null; } if (_.flags.only) { const test = $.toArray(_.flags.only); debug('--only %s', test.join(' ')); defaultConfig.filter.push(value => { value = path.relative(cwd, value); for (let i = 0; i < test.length; i += 1) { if (value.indexOf(test[i]) > -1) { return true; } } }); } if (_.flags.exclude) { const test = $.toArray(_.flags.exclude); debug('--exclude %s', test.join(' ')); test.forEach(skip => { if (skip.indexOf('*') > -1) { defaultConfig.filter.push(`!${skip}`); } else if (skip.substr(0, 1) === '.') { defaultConfig.filter.push(`!**/*${skip}`); } else if (skip.indexOf('.') > -1) { defaultConfig.filter.push(`!**/${skip}`); } else { defaultConfig.filter.push(`!**/${skip}/**`); } }); } // apply globals first const _globals = defaultConfig.globals || defaultConfig.env || {}; $.merge(env, _globals); $.merge(env, _globals[_.flags.env] || {}); // merge only ENV_VARS_IN_CAPS Object.keys(process.env).forEach(key => { if (/^[A-Z][A-Z\d_]*$/.test(key) && typeof env[key] === 'undefined') { env[key] = process.env[key]; } }); // package info defaultConfig.bundleOptions.locals = defaultConfig.bundleOptions.locals || {}; defaultConfig.bundleOptions.locals.env = env; defaultConfig.bundleOptions.locals.pkg = mainPkg; Object.keys(env).forEach(key => { defaultConfig.bundleOptions.globals[key] = env[key]; }); if (_.flags.globals) { const test = $.toArray(_.flags.globals); debug('--globals %s', test.join(' ')); test.forEach(value => { const parts = value.split('='); defaultConfig.bundleOptions.globals[parts[0]] = fixedValue(parts[1]); }); } if (_.flags.extensions) { const test = $.toArray(_.flags.extensions); debug('--extensions %s', test.join(' ')); test.forEach(exts => { const parts = exts.replace(/^\./, '').split('.').reverse(); defaultConfig.bundleOptions.extensions[parts.shift()] = parts; }); } defaultConfig.bundleOptions.compileDebug = _.flags.debug; defaultConfig.bundleOptions.verboseDebug = _.flags.verbose; if (_.flags.sources) { defaultConfig.bundleOptions.sourceMaps = true; defaultConfig.bundleOptions.sourceMapFiles = true; } const isDev = process.env.NODE_ENV === 'development' && isWatching; const cmd = _.raw || []; let child; function infoFiles(result) { if (isDev && result.output.length) { $.notify(`${result.output.length} file${result.output.length !== 1 ? 's' : ''}\n${result.output.slice(0, 3).join(', ')}`, defaultConfig.notifications.title, defaultConfig.notifications.okIcon); } if (!isWatching) { if (!result.output.length) { logger.printf('\r\r{% end Without changes %}\n'); } else { logger.printf('\r\r{% end %s file%s written %}\n', result.output.length, result.output.length !== 1 ? 's' : ''); } } } function exec(onError) { function restart() { // restart if (child) { child.kill('SIGINT'); } const _cmd = cmd .map(arg => (arg.indexOf(' ') === -1 ? arg : `"${arg}"`)).join(' '); logger.printf('\r\r{% gray $ %s %}\r\n', _cmd); debug('exec %s', _cmd); child = spawn(cmd[0], cmd.slice(1), { cwd: defaultConfig.cwd || defaultConfig.output, detached: true, }); child.stdout.pipe(process.stdout); const errors = []; child.stderr.on('data', data => { const line = data.toString().trim(); if (line) { errors.push(line); } }); child.on('close', exitCode => { let message = `${_cmd}\n— `; let icon = defaultConfig.notifications.okIcon; if (exitCode || errors.length) { icon = defaultConfig.notifications.errIcon; message += 'Error'; } else { message += 'Done'; } $.notify(message, defaultConfig.notifications.title, icon); debug('exec %s - %s', exitCode, _cmd); if (errors.length) { $.errLog(errors.join('\n')); onError({ msg: errors.join('\n') }); } if (exitCode && !isDev) { die(exitCode); } if (!isDev) { die(); } }); } return restart; } process.on('SIGINT', () => { logger.printf('\r\r'); if (child) { child.kill('SIGINT'); } die(); }); let _restart; process.nextTick(() => { if (!logger.isEnabled() && !(_.flags.debug && _.flags.verbose)) { process.stdout.write('\rProcessing sources...\r'); } const start = Date.now(); run(defaultConfig, function done(err, result) { if (err) { debug('failed %s', err); if (_.flags.quiet && err.filename) { $.errLog(`Failed source ${err.filename}`); } $.errLog(_debug(err)); if (!isWatching) { die(1); } return; } if (!logger.isEnabled() && !(_.flags.debug && _.flags.verbose)) { process.stdout.write(`\r${result.output.length} file${ result.output.length === 1 ? '' : 's' } ${result.output.length === 1 ? 'was' : 'were'} built in ${ (Date.now() - start) / 1000 }s\n`); } debug('done %s file%s added', result.output.length, result.output.length === 1 ? '' : 's'); infoFiles(result); if (cmd.length) { _restart = _restart || exec(this.emit.bind(null, 'error')); _restart(); return; } if (isDev) { logger.printf('\r\r{% log Waiting for changes... %} {% gray [press CTRL-C to quit] %}\n'); return; } if (!_.flags.reloader) { die(); } }); }); // clean exit process.on('exit', exitCode => { if (!isDev && !exitCode) { logger.write('\r\n'); } }); logger.info('{% log Output to: %} {% yellow %s %}\n', path.relative(cwd, defaultConfig.output) || '.'); logger.info('{% log Reading from %} {% yellow %s %} {% gray source%s %}\n', defaultConfig.from.length, defaultConfig.from.length === 1 ? '' : 's'); if (isWatching && defaultConfig.watching.length) { logger.info('{% log Watching from %} {% yellow %s %} {% gray source%s %}\n', defaultConfig.watching.length, defaultConfig.watching.length === 1 ? '' : 's'); }