Skip to content

Instantly share code, notes, and snippets.

@nyteshade
Last active January 27, 2019 07:04
Show Gist options
  • Save nyteshade/44f972b1e3353de8596cf5d0db5aad43 to your computer and use it in GitHub Desktop.
Save nyteshade/44f972b1e3353de8596cf5d0db5aad43 to your computer and use it in GitHub Desktop.
Quick easy project setup with command line options
#!/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)
})
}
}
@nyteshade
Copy link
Author

Got tired of setting up sample projects and creating babel and testing setup over and over again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment