Last active
January 27, 2019 07:04
-
-
Save nyteshade/44f972b1e3353de8596cf5d0db5aad43 to your computer and use it in GitHub Desktop.
Quick easy project setup with command line options
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| let spawn = require('child_process').spawn | |
| let path = require('path') | |
| let fs = require('fs') | |
| // --------------------------------------------------------------------------- | |
| // BEGIN: Setup data | |
| // --------------------------------------------------------------------------- | |
| let packageConfig = { | |
| scripts: { | |
| build: "babel src --out-dir dist", | |
| test: "jest" | |
| }, | |
| devDependencies: { | |
| "@babel/cli": "^7.0.0", | |
| "@babel/core": "^7.0.0", | |
| "@babel/plugin-proposal-class-properties": "^7.0.0", | |
| "@babel/plugin-proposal-optional-chaining": "^7.0.0", | |
| "@babel/plugin-proposal-decorators": "^7.0.0", | |
| "@babel/preset-env": "^7.0.0", | |
| "jest": "^23.5.0", | |
| }, | |
| } | |
| let babelConfig = { | |
| presets: ["@babel/preset-env"], | |
| plugins: [ | |
| "@babel/plugin-proposal-optional-chaining", | |
| "@babel/plugin-proposal-decorators", | |
| "@babel/plugin-proposal-class-properties", | |
| ] | |
| } | |
| let usage = ` | |
| Usage: | |
| $ SetupProject [options] | |
| Options: | |
| --init, -n Initialize npm first (using defaults) | |
| --interactive, -i Perform an interactive intiailization | |
| --react, -r Include @babel/preset-react && react | |
| --graphql, -g Include GraphQL && ne-schemata, graphql-yoga | |
| --flow, -f Include Flow for type annotations | |
| --tags-n-types, -t Include ne-tag-fns and ne-types | |
| --help, -h Stop and show this help text | |
| --force, -F Overwrites existing config files [CAREFUL!] | |
| --install, -I Run \`npm install\` after finished | |
| ` | |
| let woofOpts = { | |
| flags: { | |
| force: { alias: 'F', type: 'boolean' }, | |
| flow: { alias: 'f', type: 'boolean' }, | |
| 'tags-n-types': { alias: 't', type: 'boolean' }, | |
| graphql: { alias: 'g', type: 'boolean', }, | |
| help: { alias: 'h', type: 'boolean', }, | |
| init: { alias: 'n', type: 'boolean', }, | |
| install: { alias: 'I', type: 'boolean' }, | |
| interactive: { alias: 'i', type: 'boolean', }, | |
| react: { alias: 'r', type: 'boolean', }, | |
| } | |
| } | |
| // --------------------------------------------------------------------------- | |
| // FINISH: Setup data | |
| // --------------------------------------------------------------------------- | |
| // --------------------------------------------------------------------------- | |
| // BEGIN: Handle requirements (to keep this all a single file affair) | |
| // --------------------------------------------------------------------------- | |
| let { combine, deepmerge } = (() => { | |
| function isMergeableObject(value) { | |
| return isNonNullObject(value) && | |
| !isSpecial(value) | |
| } | |
| function isNonNullObject(value) { | |
| return !!value && typeof value === 'object' | |
| } | |
| function isSpecial(value) { | |
| var stringValue = Object.prototype.toString.call(value) | |
| return stringValue === '[object RegExp]' || | |
| stringValue === '[object Date]' || | |
| isReactElement(value) | |
| } | |
| // see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25 | |
| var canUseSymbol = typeof Symbol === 'function' && Symbol.for | |
| var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7 | |
| function isReactElement(value) { | |
| return value.$$typeof === REACT_ELEMENT_TYPE | |
| } | |
| function emptyTarget(val) { | |
| return Array.isArray(val) ? [] : {} | |
| } | |
| function cloneUnlessOtherwiseSpecified(value, options) { | |
| return (options.clone !== false && options.isMergeableObject(value)) ? | |
| deepmerge(emptyTarget(value), value, options) : | |
| value | |
| } | |
| function defaultArrayMerge(target, source, options) { | |
| return target.concat(source).map(function(element) { | |
| return cloneUnlessOtherwiseSpecified(element, options) | |
| }) | |
| } | |
| function mergeObject(target, source, options) { | |
| var destination = {} | |
| if (options.isMergeableObject(target)) { | |
| Object.keys(target).forEach(function(key) { | |
| destination[key] = cloneUnlessOtherwiseSpecified(target[key], options) | |
| }) | |
| } | |
| Object.keys(source).forEach(function(key) { | |
| if (!options.isMergeableObject(source[key]) || !target[key]) { | |
| destination[key] = cloneUnlessOtherwiseSpecified(source[key], options) | |
| } else { | |
| destination[key] = deepmerge(target[key], source[key], options) | |
| } | |
| }) | |
| return destination | |
| } | |
| function deepmerge(target, source, options) { | |
| options = options || {} | |
| options.arrayMerge = options.arrayMerge || defaultArrayMerge | |
| options.isMergeableObject = options.isMergeableObject || isMergeableObject | |
| var sourceIsArray = Array.isArray(source) | |
| var targetIsArray = Array.isArray(target) | |
| var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray | |
| if (!sourceAndTargetTypesMatch) { | |
| return cloneUnlessOtherwiseSpecified(source, options) | |
| } else if (sourceIsArray) { | |
| return options.arrayMerge(target, source, options) | |
| } else { | |
| return mergeObject(target, source, options) | |
| } | |
| } | |
| deepmerge.all = function deepmergeAll(array, options) { | |
| if (!Array.isArray(array)) { | |
| throw new Error('first argument should be an array') | |
| } | |
| return array.reduce(function(prev, next) { | |
| return deepmerge(prev, next, options) | |
| }, {}) | |
| } | |
| function combine(options, ...objects) { | |
| let dest = {} | |
| for (let object of objects) { | |
| dest = deepmerge(dest, object) | |
| } | |
| return dest | |
| } | |
| return { deepmerge, combine } | |
| })() | |
| let Woof = (() => { | |
| function dedent(str) { | |
| let foundCharacter = false; | |
| let maxSize = 0; | |
| let parts = str.split(''); | |
| // finding the trailing spaces and turn tabs into spaces | |
| parts = parts.map((c) => { | |
| if (c == '\n' && !foundCharacter) return ''; // ignore new lines | |
| if (c === ' ' && !foundCharacter) { | |
| maxSize += 1; | |
| return ' '; | |
| } | |
| if (c == '\t' && !foundCharacter) { | |
| maxSize += 4; | |
| return ' '; | |
| } | |
| foundCharacter = true; | |
| if (c == '\t') { | |
| return ' '; | |
| } | |
| return c; | |
| }); | |
| let regex = new RegExp(`^${new Array(maxSize).join(' ')}`); | |
| return parts.join('').split('\n').map((line) => { | |
| return line.replace(regex, ''); | |
| }).join('\n'); | |
| } | |
| function flatten(flags, allowShorthand, allowExtendedShorthand) { | |
| let map = {}; | |
| Object.keys(flags).forEach((k) => { | |
| const { alias, type, validate } = flags[k]; | |
| let value = { name: k }; | |
| if (type) value.type = type; | |
| if (validate) value.validate = validate; | |
| // include the name as value that can be invoked | |
| // also include the shorthands if allowed | |
| map[k] = value; | |
| if (allowShorthand) map[`-${k}`] = value; | |
| if (allowExtendedShorthand) map[`--${k}`] = value; | |
| // If the alias is a string we don't need to map anything | |
| if (typeof alias === 'string') { | |
| if (allowShorthand) map[`-${alias}`] = value; | |
| if (allowExtendedShorthand) map[`--${alias}`] = value; | |
| map[alias] = value; // the default alias is included | |
| } | |
| // If the alias is an array, we should map over the values and set them | |
| if (Array.isArray(alias)) { | |
| alias.forEach((a) => { | |
| if (allowShorthand) map[`-${a}`] = value; | |
| if (allowExtendedShorthand) map[`--${a}`] = value; | |
| // the default alias is included | |
| map[a] = value; | |
| }); | |
| } | |
| }); | |
| return map; | |
| } | |
| return function Woof(helpMessage, options = {}) { | |
| delete require.cache[__filename]; | |
| const parentDir = path.dirname('.'); | |
| let program = {}; | |
| let { version, args = process.argv.slice(2), flags = {}, commands = {} } = options; | |
| // sets the defaults | |
| Object.keys(flags).forEach((flag) => { | |
| if (flags[flag].default) { | |
| program[flag] = flags[flag].default; | |
| } | |
| }); | |
| // make the flag and command objects a hashmap for easy access | |
| let flagMap = flatten(flags, true, true); | |
| let commandMap = flatten(commands, true); | |
| let versionMap = { 'version': true, '--version': true }; | |
| let helpMap = { 'help': true, '--help': true }; | |
| // loop through the args, either command line or given | |
| let arg = ''; | |
| while ((arg = args.shift()) !== undefined) { | |
| if (arg.indexOf('=') > -1) { | |
| let parsed = arg.split('='); | |
| // add the values to the original array | |
| args.push(parsed[0]); | |
| args.push(parsed[1]); | |
| // continue from the next true argument | |
| continue; | |
| } | |
| // The user has requested either of these values and further arguments should not be parsed | |
| if (program['error'] || program['version'] || program['help']) return; | |
| if (helpMap[arg]) { | |
| program['help'] = true; | |
| // Use the predefined help message to render a help screen | |
| process.stdout.write(`\n${dedent(helpMessage)}\n`); | |
| } | |
| if (versionMap[arg]) { | |
| program['version'] = true; | |
| // If the version is provided, just use that | |
| if (version) { | |
| process.stdout.write(`v${version}\n`); | |
| } else { | |
| // try to get the version from the current applications package.json | |
| try { | |
| process.stdout.write(`v${require(`${parentDir}/package.json`).version}\n`); | |
| } catch(ex) { | |
| process.stdout.write('v?\n'); | |
| } | |
| } | |
| } | |
| // check the flag maps see if it exists | |
| // since we are working off a stack, the next value is always 0, so we can shift the next value to get it | |
| if(flagMap[arg]) { | |
| let { type, name } = flagMap[arg]; | |
| switch(type) { | |
| case 'string': | |
| program[name] = args.shift(); | |
| break; | |
| case 'integer': | |
| program[name] = parseInt(args.shift()); | |
| break; | |
| case 'boolean': | |
| default: | |
| program[name] = true; | |
| break; | |
| } | |
| // they have passed in the validate argument and want to validate the value that we have collected | |
| if(typeof flagMap[arg]['validate'] == 'function') { | |
| let isValid = flagMap[arg]['validate'](program[name]); | |
| // isValid is boolean and is falsey that will trigger the default throw logic | |
| if(typeof isValid == 'boolean' && !isValid) { | |
| program['error'] = `the value passed into ${arg} is not acceptable: ${program[name]}`; | |
| } | |
| // isValid is a string that was passed by the validate function | |
| if(typeof isValid == 'string') { | |
| program['error'] = new Error(isValid); | |
| } | |
| } | |
| } | |
| if(commandMap[arg]) program[commandMap[arg].name] = true; | |
| } | |
| return program; | |
| }; | |
| })() | |
| // --------------------------------------------------------------------------- | |
| // FINISH: Handle requirements | |
| // --------------------------------------------------------------------------- | |
| let woof = Woof(usage, woofOpts) | |
| if (woof.help) { | |
| console.log(usage) | |
| process.exit() | |
| } | |
| if (woof.graphql) { | |
| packageConfig = combine(undefined, packageConfig, { | |
| dependencies: { | |
| "graphql": "^14.0.0", | |
| "graphql-yoga": "^1.16.2", | |
| "ne-schemata": "^1.10.5", | |
| } | |
| }) | |
| } | |
| if (woof['tags-n-types']) { | |
| packageConfig = combine(undefined, packageConfig, { | |
| dependencies: { | |
| "ne-types": "^1.0.5", | |
| "ne-tag-fns": "^0.6.8", | |
| } | |
| }) | |
| } | |
| if (woof.flow) { | |
| packageConfig = combine(undefined, packageConfig, { | |
| devDependencies: { | |
| "flow-bin": "^0.91.0", | |
| "@babel/preset-flow": "^7.0.0" | |
| } | |
| }) | |
| babelConfig = combine(undefined, babelConfig, { | |
| presets: ["@babel/preset-flow"] | |
| }) | |
| } | |
| if (woof.react) { | |
| packageConfig = combine(undefined, packageConfig, { | |
| dependencies: { | |
| "react": "^16.4.2", | |
| }, | |
| devDependencies: { | |
| "@babel/preset-react": "^7.0.0", | |
| } | |
| }) | |
| babelConfig = combine(undefined, babelConfig, { | |
| presets: ['@babel/preset-react'] | |
| }) | |
| } | |
| // If init was requested, invoke it first. | |
| if (woof.init) { | |
| let args = ['init'] | |
| if (!woof.interactive) { | |
| args.push('-y') | |
| } | |
| if (woof.force) { | |
| args.push('--force') | |
| } | |
| let proc = spawn('npm', args, {stdio: 'inherit'}) | |
| proc.on('close', (code) => { | |
| if (code === 0) { | |
| proceed(woof, packageConfig, babelConfig) | |
| } | |
| else { | |
| process.exit(code) | |
| } | |
| }) | |
| } | |
| else { | |
| proceed(woof, packageConfig, babelConfig) | |
| } | |
| function proceed(woof, packageConfig, babelConfig) { | |
| // Break if there is no package.json | |
| if (!fs.existsSync('./package.json')) { | |
| console.log('There is no package.json file present!') | |
| process.exit(1) | |
| } | |
| // Modify the package.json contents | |
| let package = JSON.parse(fs.readFileSync('./package.json').toString()) | |
| package = combine(undefined, package, packageConfig) | |
| // Write the modified contents back to disk and format | |
| fs.writeFileSync( | |
| './package.json', | |
| JSON.stringify(package, undefined, 2) | |
| ) | |
| // Write out the babel 7 configuration file | |
| // TODO: Modify instead of create | |
| if (woof.force || !fs.existsSync('./babel.config.js')) { | |
| fs.writeFileSync( | |
| './babel.config.js', | |
| `module.exports = ${JSON.stringify(babelConfig, undefined, 2)}` | |
| ) | |
| } | |
| else { | |
| console.warn(` | |
| \x1b[33mWARNING\x1b[0m | |
| The following contents were not written out to a new \`babel.config.js\` | |
| file as there is already file with that name. Please use \`-F\` or | |
| \`--force\` to overwrite the files. | |
| module.exports = ${JSON.stringify(babelConfig, undefined, 2)} | |
| `) | |
| } | |
| let dirs = ['dist', 'src', 'test'] | |
| for (let dir of dirs) { | |
| let dirPath = path.join('.', dir) | |
| if (!fs.existsSync(dirPath)) { | |
| fs.mkdirSync(dirPath) | |
| } | |
| } | |
| console.log(` | |
| Your project has been modified. You may now wish to run \`npm install\` | |
| to fetch the changed dependencies your project has. | |
| `) | |
| if (woof.install) { | |
| console.log('Running `npm install` as requested.\n') | |
| spawn('npm', ['install'], {stdio: 'inherit'}).on('close', (code) => { | |
| process.exit(code) | |
| }) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Got tired of setting up sample projects and creating babel and testing setup over and over again.