Foy
A simple, light-weight and modern task runner for general purpose.
Contents
Features
- Promise-based tasks and built-in utilities.
<a href="https://github.com/shelljs/shelljs" target="_blank">
shelljs </a>
-like commands- Easy to learn, stop spending hours for build tools.
- Small install size
Install
yarn add -D foy
Or install globally with
yarn add -g foy
Write a Foyfile
You need to add a Foyfile.js(or Foyfile.ts with tsx or @swc-node/register or ts-node installed) to your project root.
Also, you can simply generate a Foyfile.js via:
foy --init
which will create a simple Foyfile.js
in the current folder:
const { task } = require('foy')
task('build', async ctx => {
await ctx.exec('tsc')
})
You can also generate a Foyfile.ts
via
foy --init ts
Then we can run foy build
to execute the build
task.
foy build
You can also add some options and a description to your tasks:
import { task, desc, option, strict } from 'foy'
desc('Build ts files with tsc')
option('-w, --watch', 'watch file changes')
strict()
task('build', async ctx => {
await ctx.exec(`tsc ${ctx.options.watch ? '-w' : ''}`)
})
And, if using TypeScript, add types to your options through the task
generic:
import { task, desc, option, strict } from 'foy'
type BuildOptions = {
watch: boolean
}
desc('Build ts files with tsc')
option('-w, --watch', 'watch file changes')
strict()
task<BuildOptions>('build', async ctx => {
await ctx.exec(`tsc ${ctx.options.watch ? '-w' : ''}`)
})
foy build -w
Warning! If you want to set flags like strict for all tasks, please use setGlobalOptions
:
import { setGlobalOptions } from 'foy'
setGlobalOptions({ strict: true })
option('-aa')
task('dev', async ctx => {
})
option('-bb')
task('build', async ctx => {
})
Using with built-in promised-based API
import { fs, task } from 'foy'
task('some task', async ctx => {
await fs.rmrf('/some/dir/or/file')
await fs.copy('/src', '/dist')
let json = await fs.readJson('./xx.json')
await ctx
.env('NODE_ENV', 'production')
.env('NODE_ENV=production')
.cd('./src')
.exec('some command')
let { stdout } = await ctx.exec('ls', { stdio: 'pipe' })
})
Using with other packages
import { task, logger } from 'foy'
import * as axios from 'axios'
task('build', async ctx => {
let res = await axios.get('https://your.server/data.json')
logger.info(res.data)
})
Using dependencies
import { task } from 'foy'
import * as axios from 'axios'
task('test', async ctx => {
await ctx.exec('mocha')
})
task('build', async ctx => {
let res = await axios.get('https://your.server/data.json')
console.log(res.data)
await ctx.exec('build my awesome project')
})
task(
'publish:patch',
['test', 'build'],
async ctx => {
await ctx.exec('npm version patch')
await ctx.exec('npm publish')
}
)
Dependencies run serially by default but you can specify when a task should be run concurrently.
Example: Passing running options to dependencies:
task(
'publish:patch',
[{
name: 'test',
async: true,
force: true,
}, {
name: 'build',
async: true,
force: true,
},],
async ctx => {
await ctx.exec('npm version patch')
await ctx.exec('npm publish')
}
)
task(
'publish:patch',
[ 'test'.async().force(),
'build'.async().force() ],
async ctx => {
await ctx.exec('npm version patch')
await ctx.exec('npm publish')
}
)
task(
'publish:patch',
[ 'test'.async(0).force(),
'build'.async(1).force() ],
async ctx => {
await ctx.exec('npm version patch')
await ctx.exec('npm publish')
}
)
You can also pass options to dependencies:
task('task1', async ctx => {
console.log(ctx.options)
console.log(ctx.global.options)
})
task('task2', [{
name: 'task1',
options: {
forceRebuild: true,
},
resolveOptions: async ctx => {
return { lazyOptions: 1 }
}
}])
Using namespaces
To avoid name collisions, Foy provides namespaces to group tasks via the namespace
function:
import { task, namespace } from 'foy'
namespace('client', ns => {
before(() => {
logger.info('before')
})
after(() => {
logger.info('after')
})
onerror(() => {
logger.info('onerror')
})
task('start', async ctx => { })
task('build', async ctx => { })
task('watch', async ctx => { })
namespace('proj1', ns => {
onerror(() => {
logger.info('onerror', ns)
})
task('start', async ctx => { })
})
})
namespace('server', ns => {
task('build', async ctx => { })
task('start', async ctx => { })
task('watch', async ctx => { })
})
task('start', ['client:start'.async(), 'server:start'.async()])
Useful utils
fs
Foy wraps the NodeJS's fs
(file system) module with a promise-based API, so you can easily use async/await patterns, if you prefer. Foy also implements some useful utility functions for build scripts not present in NodeJS's built-in modules.
import { fs } from 'foy'
task('build', async ctx => {
let f = await fs.readFileSync('./assets/someFile')
await fs.copy('./fromPath', './toPath')
await fs.watchDir('./src', (event, filename) => {
logger.info(event, filename)
})
await fs.mkdirp('./some/directory/with/parents/not/exists')
await fs.outputFile('./some/file/with/parents/not/exists', 'file data')
await fs.outputJson('./some/file/with/parents/not/exists', {text: 'json data'})
let file = await fs.readJson('./some/jsonFile')
await fs.iter('./src', async (path, stat) => {
if (stat.isDirectory()) {
logger.info('directory:', path)
if (path.endsWith('node_modules')) {
return true
}
} else if (stat.isFile()) {
logger.warn('file:', path)
}
})
})
logger
Foy includes a light-weight built-in logger
import { logger } from 'foy'
task('build', async ctx => {
logger.debug('debug', { aa: 1})
logger.info('info')
logger.warn('warn')
logger.error('error')
})
exec command
A simple wrapper for sindresorhus's lovely module
execa
import { logger } from 'foy'
task('build', async ctx => {
await ctx.exec('tsc')
await ctx.exec([
'tsc --outDir ./lib',
'tsc --module es6 --outDir ./es',
])
await Promise.all([
ctx.exec('eslint'),
ctx.exec('tsc'),
ctx.exec('typedoc'),
])
ctx.monitor('./src', 'node ./dist')
ctx.monitor('./src', ['rm -rf dist', 'tsc', 'node dist'])
ctx.monitor('./src', async () => {
await ctx.run('build:server')
await ctx.exec('node ./dist')
})
ctx.monitor('./src', async (p) => {
p.current = require('child_process').exec('node dist')
})
})
Using in CI servers
If you use Foy in CI servers, you won't want the loading spinners as most CI servers will log stdout and stderr in discreet frames not meant for continuous streaming animations. Luckily, Foy has already considered this! You can simply disable the loading animation like this:
import { task, setGlobalOptions } from 'foy'
setGlobalOptions({ loading: false })
task('test', async cyx => { })
Using lifecycle hooks
You can add lifecycle hooks via the before
, after
, and onerror
functions.
import { before, after, onerror } from 'foy'
before(() => {
})
after(() => {
})
onerror((err) => {
})
run task in task
task('task1', async ctx => { })
task('task2', async ctx => {
await ctx.run('task1')
})
Watch and build
task('build', async ctx => { })
let p = null
task('watch', async ctx => {
ctx.monitor('./src', async ()=> {
ctx.exec('node ./src/server.ts')
})
})
Using with custom compiler
foy -r ts-node/register -c ./some/Foyfile.ts build
foy -r coffeescript/register -c ./some/Foyfile.coffee build
zsh/bash auto completion (New!!!)
Add foy auto completion in zsh/bash:
foy --completion-profile >> ~/.bashrc
foy --completion-profile >> ~/.zshrc
API documentation
https://zaaack.github.io/foy/api
License
MIT