Skip to content

Instantly share code, notes, and snippets.

@josefaidt
Last active February 28, 2023 14:52
Show Gist options
  • Save josefaidt/9f707c48f7b8e03a246c24ec8aab729d to your computer and use it in GitHub Desktop.
Save josefaidt/9f707c48f7b8e03a246c24ec8aab729d to your computer and use it in GitHub Desktop.
#!/usr/bin/env node
const { existsSync: exists, promises: fs } = require('fs');
const { spawn } = require('child_process');
const path = require('path');
const c = require('chalk');
const dotenv = require('dotenv');
const nodemon = require('nodemon');
// set default node_env
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const { log } = console;
const packageDir = path.dirname(path.resolve('package.json'));
async function loadEnv(options = { set: false }) {
// load using dotenv - follow CRA loading pattern
const env = {};
const envFile = path.resolve(packageDir, '.env');
if (exists(envFile)) {
Object.assign(env, dotenv.parse(await fs.readFile(envFile)));
}
const envNodeFile = path.resolve(`${envFile}.${process.env.NODE_ENV.toLowerCase()}`);
if (exists(envNodeFile)) {
Object.assign(env, dotenv.parse(await fs.readFile(envNodeFile)));
}
const envLocalFile = path.resolve(`${envNodeFile}.local`);
if (exists(envLocalFile)) {
Object.assign(env, dotenv.parse(await fs.readFile(envLocalFile)));
}
if (options && options.set) {
for (const e in env) {
process.env[e] = env[e];
}
}
return env;
}
async function dev(entry, options) {
// dev command
nodemon({
script: entry,
execMap: {
js: 'babel-node --presets=@babel/preset-env',
},
env: {
NODE_ENV: process.env.NODE_ENV || 'development',
PORT: 3000,
...(await loadEnv()),
},
})
.on('start', function() {
log(c.blue('Started Dev Server'));
})
.on('quit', function() {
log(c.yellow('Quitting'));
process.exit();
})
.on('restart', function(files) {
// console.log(`Dev Server restarted due to ${files.length} files`);
log(c.yellow(`Dev Server restarted due to ${files.length} files`));
});
}
// build command
async function build(entry, options) {
// set process env, default to production
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
await loadEnv({ set: true });
const outDir = path.join(packageDir, options.output || 'out');
if (exists(outDir)) {
try {
await fs.rmdir(outDir, { recursive: true });
} catch (error) {
throw new Error(`Unable to delete output directory: ${error}`);
}
}
const cmd = spawn(
'babel',
[
'.',
'--out-dir',
'out',
'--source-maps',
'--ignore',
'node_modules,dist,out,config',
'--copy-files',
'--presets=@babel/preset-env',
'--plugins=@babel/plugin-transform-runtime',
],
{
cwd: packageDir,
}
);
cmd.stdout.on('data', data => {
log(c.green(`${data}`));
});
cmd.stderr.on('data', data => {
log(c.red(`${data}`));
});
cmd.on('close', code => {
if (code === 0) {
log(c.green('Packer exited with code 0'));
} else {
log(c.yellow(`Packer exited with code ${code}`));
}
});
}
async function start(entry, options) {
await loadEnv({ set: true });
const cmd = spawn('node', ['out/index.js'], {
cwd: packageDir,
});
cmd.stdout.on('data', data => {
log(`${data}`);
});
cmd.stderr.on('data', data => {
log(c.red(`${data}`));
});
cmd.on('close', code => {
if (code === 0) {
log(c.green('Packer exited with code 0'));
} else {
log(c.yellow(`Packer exited with code ${code}`));
}
});
}
async function init() {
const packageFilePath = path.resolve('package.json');
const packageFile = JSON.parse(await fs.readFile(packageFilePath, 'utf8'));
return {
entry:
(packageFile.main && path.resolve(packageFile.main)) || (packageFile.entry && path.resolve(packageFile.entry)),
name: packageFile.name,
};
}
const commands = [dev, build, start];
async function main(command, args) {
// start!
// clear();
log(c.blue(`Packer CLI ${JSON.parse(await fs.readFile(path.join(__dirname, 'package.json'))).version}`));
// get project details
let { entry, name } = await init();
// argument regex (--arg, -a)
const regex = /^-{1,2}(?<arg>[A-z]+)+$/i;
// parse argHash (--yay true => { yay: true })
const argHash = args.reduce((argHash, arg, index) => {
if (regex.test(arg)) {
// verify next input is not also an argument without an input
const name = arg.match(/^-{1,2}(?<arg>[A-z]+)+$/i).groups.arg;
if (regex.test(args[index + 1])) throw new Error(`Did not receive input for ${name}`);
argHash[name] = args[index + 1];
} else if (/\.(js|mjs|cjs)$/i.test(arg)) {
// test if argument is actually a file path
// then overwrite entry
entry = path.resolve(arg);
}
return argHash;
}, {});
if (!entry) throw new Error('Must define an entry point as package.main or passed to command');
// execute command
await commands.find(cmd => cmd.name === command)(entry, argHash);
}
const [nodePath, packerPath, command, ...args] = process.argv;
main(command, args);
@josefaidt
Copy link
Author

josefaidt commented May 29, 2020

{
  "name": "builder-cli",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "bin": {
    "builder": "./index.js"
  },
  "dependencies": {
    "@babel/core": "^7.10.0",
    "@babel/preset-env": "^7.10.0",
    "@babel/plugin-transform-runtime": "^7.10.1",
    "@babel/node": "^7.10.1",
    "@babel/runtime": "^7.9.2",
    "chalk": "^4.0.0",
    "clear": "^0.1.0",
    "dotenv": "^8.2.0"
  },
  "babel": {
    "presets": [
      "@babel/preset-env"
    ],
    "plugins": [
      "@babel/plugin-transform-runtime"
    ]
  }

@josefaidt
Copy link
Author

# Build CLI

Simplified, shareable command line tool to assist developers in quickly authoring REST Node services using ECMAscript Modules (ESM). Built on top of:

- [nodemon](https://www.npmjs.com/package/nodemon)
- [Babel](https://babeljs.io/)

## Installation and Usage

1. `yarn add build-cli`
2. Create symlink to new package by running `yarn install`
3. Set up npm scripts

   ```json
   {
     "main": "index.js",
     "scripts": {
       "start": "node index.js",
       "pm2": "packer start",
       "dev": "packer dev",
       "build": "packer build"
     }
   }

Commands

Packer-CLI offers only three commands to use.

  • start: loads environment, runs node using the file in package.json's main key, or by an entry point
    • example: build start index.js
  • dev: executes nodemon, loads environment. Only watches JavaScript and JSON files
  • build: runs JavaScript files through Babel, copies other files (e.g. JSON, XLSX)

Environment Variables

Build-CLI has enabled the use of environment variable files (e.g. .env) using a loading pattern inspired by create-react-app. By default ENV files are loaded using the following pattern in order of availability, meaning files loaded last will overwrite what is loaded before:

.env
.env.[environment]
.env.[environment].local

In practice this may look like:

.env
.env.development
.env.development.local

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