Coder Social home page Coder Social logo

zaaack / foy Goto Github PK

View Code? Open in Web Editor NEW
258.0 7.0 14.0 1.01 MB

A simple, light-weight, type-friendly and modern task runner for general purpose.

Home Page: http://zaaack.github.io/foy

License: MIT License

TypeScript 86.36% HTML 2.64% JavaScript 2.40% CSS 8.60%
nodejs build-tool cli make task-runner promise async-await tasks runner jake

foy's Introduction

Foy

Build Status npm npm install size

A simple, light-weight and modern task runner for general purpose.

Contents

Features

  • Promise-based tasks and built-in utilities.
  • shelljs-like commands
  • Easy to learn, stop spending hours for build tools.
  • Small install size
    • foy: install size
    • gulp: install size
    • grunt: install size

GIF

Install

yarn add -D foy # or npm i -D foy

Or install globally with

yarn add -g foy # or npm i -g foy

Write a Foyfile

You need to add a Foyfile.js(or Foyfile.ts with 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:

// Foyfile.js
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() // This will throw an error if you passed some options that doesn't defined via `option()`
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() // This will throw an error if you passed some options that doesn't defined via `option()`
task<BuildOptions>('build', async ctx => { // ctx.options now has type BuildOptions instead of unknown
  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 }) // all tasks' options will be strict.

option('-aa') // strict via default
task('dev', async ctx => {

})
option('-bb') // strict via default
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') // Remove directory or file
  await fs.copy('/src', '/dist') // Copy folder or file
  let json = await fs.readJson('./xx.json')
  await ctx
    .env('NODE_ENV', 'production')
    .cd('./src')
    .exec('some command') // Execute an command
  let { stdout } = await ctx.exec('ls', { stdio: 'pipe' }) // Get the stdout, default is empty because it's redirected to current process via `stdio: 'inherit'`.
})

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'], // Run test and build before publish
  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, // run test parallelly
    force: true, // force rerun test whether it has been executed before or not.
  }, {
    name: 'build',
    async: true,
    force: true,
  },],
  async ctx => {
    await ctx.exec('npm version patch')
    await ctx.exec('npm publish')
  }
)

/* Sugar version */
task(
  'publish:patch',
  [ 'test'.async().force(),
    'build'.async().force() ],
  async ctx => {
    await ctx.exec('npm version patch')
    await ctx.exec('npm publish')
  }
)

/*
Priority for async tasks

Default is 0, higher values will be run earlier; so, in this next example, `build` will be run before `test`.
(Note: If you have multiple async dependencies with same priority, they will be executed in parallel.)
*/
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) // "{ forceRebuild: true, lazyOptions: 1 }"
  console.log(ctx.global.options) // options from command line "{ a: 1 }"
})


task('task2', [{
  name: 'task1',
  options: {
    forceRebuild: true,
  },
  // Some options that rely on ctx or asynchronization,
  // it will be merged to options.
  resolveOptions: async ctx => {
    return { lazyOptions: 1 }
  }
}])

// foy task2 -a 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 => { /* ... */ }) // client:start
  task('build', async ctx => { /* ... */ }) // client:build
  task('watch', async ctx => { /* ... */ }) // client:watch
  namespace('proj1', ns => { // nested namespace
    onerror(() => {
      logger.info('onerror', ns)
    })
    task('start', async ctx => { /* ... */ }) // client:proj1:start

  })
})

namespace('server', ns => {
  task('build', async ctx => { /* ... */ }) // server:build
  task('start', async ctx => { /* ... */ }) // server:start
  task('watch', async ctx => { /* ... */ }) // server:watch
})

task('start', ['client:start'.async(), 'server:start'.async()]) // start

// foy start
// foy client:build

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')

  // copy file or directory
  await fs.copy('./fromPath', './toPath')

  // watch a directory
  await fs.watchDir('./src', (event, filename) => {
    logger.info(event, filename)
  })

  // make directory with parent directories
  await fs.mkdirp('./some/directory/with/parents/not/exists')

  // write file will auto create missing parent directories
  await fs.outputFile('./some/file/with/parents/not/exists', 'file data')

  // write json file will auto create missing parent directories
  await fs.outputJson('./some/file/with/parents/not/exists', {text: 'json data'})
  let file = await fs.readJson('./some/jsonFile')

  // iterate directory tree
  await fs.iter('./src', async (path, stat) => {
    if (stat.isDirectory()) {
      logger.info('directory:', path)
      // skip scan node_modules
      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')

  // run multiple commands synchronously
  await ctx.exec([
    'tsc --outDir ./lib',
    'tsc --module es6 --outDir ./es',
  ])

  // run multiple commands concurrently
  await Promise.all([
    ctx.exec('eslint'),
    ctx.exec('tsc'),
    ctx.exec('typedoc'),
  ])
  // restart process when file changes
  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') // auth detect long-running process when using ctx.exec
  })
  ctx.monitor('./src', async (p) => {
    // manually point out the process need to be killed when restart
    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 }) // disable loading animations

task('test', async cyx => { /* ... */ })
/*
$ foy test
DependencyGraph for task [test]:
─ test

Task: test
...
*/

Using lifecycle hooks

You can add lifecycle hooks via the before, after, and onerror functions.

import { before, after, onerror } from 'foy'
before(() => { // do something before all tasks tree start
  // ...
})
after(() => { // do something after all tasks tree finished
  // ...
})
onerror((err) => { // do something when error happens
  // ...
})

run task in task

task('task1', async ctx => { /* ... */ })
task('task2', async ctx => {
  // do things before task1

  // run task1 manually, so we can
  // do things before or after it
  await ctx.run('task1')

  // do things after task1
})

Watch and build

task('build', async ctx => { /* build your project */ })
task('run', async ctx => { /* start your project */ })

let p = null
task('watch', async ctx => {
  ctx.fs.watchDir('./src', async (evt, file) => {
    await ctx.run('build')
    p && !p.killed && p.kill()
    p = await ctx.run('run')
  })
})

Using with custom compiler

# Write Foyfile in ts, enabled by default
foy -r ts-node/register -c ./some/Foyfile.ts build

# Write Foyfile in coffee
foy -r coffeescript/register -c ./some/Foyfile.coffee build

zsh/bash auto completion (New!!!)

Add foy auto completion in zsh/bash:

# for bash
foy --completion-profile >> ~/.bashrc

# for zsh
foy --completion-profile >> ~/.zshrc

API documentation

https://zaaack.github.io/foy/api

License

MIT

foy's People

Contributors

arimgibson avatar noahtheduke avatar zaaack avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

foy's Issues

New task hierarchy feature causes duplicate console output

If the height of the hierarchy 'tree' is greater than the height of the terminal it is being run in, then the whole tree will be re-written to the termnial on every tick of the spinner.

I guess this may be a problem with the spinner library rather than the foy implementation

[Question] Having trouble using foy

Hi, I'm trying to use this library, but I'm having trouble with it.
even with the basic example that Foy init gives, it does not work at all, which is

const { task, desc, option, fs } = require('foy')

  task('build', async ctx => {
    await ctx.exec('echo "hello world"')
  })

and does not print at all. tried changing command to something simple, also does not work at all.
Am I missing something here?

for more information, I'm using node@20.

support only one dependency

should we support follow syntax?

task('foo', 'bar')

Now should write to:

task('foo', ['bar'])

and throw:

dependencies.map is not a function

FEATURE: Task progress for tasks run from within a parent task

Tracking in #6


Just wondering if you had an opinion on this, my previous PR (#4) added a flag to hide 'sub tasks' when called from within a parent context.

I get that the function of this message in the console is to indicate that a particular task has been called from the parent - I thought that perhaps a better way of displaying this would be for each subtask to have a spinner indicating the progress just as the top-level tasks do? The bottom most task/spinner would always be the top most task, with the children indented depending on the sub task level

I think this would be better for seeing progress of the subtasks and the hierarchy of tasks when there are multiple dependancies and subtasks.

Any thoughts? I've given a quick illustration below


Task: package.plugins - Depends on: package.docker.plugins
Task: package.docker.plugins - Depends on: package.docker.base
Task: package.docker.base - Depends on package.binaries
Task: package.binaries - Depends on build.core - Calls package.binaries.client, package.binaries.client
Task: build.core - Depends on build.core.pre - Calls build.core.client, build.core.main

Current Behaviour:

✔ build.core.pre
⠋ build.core Task: build.core.client
Task: build.core.main
✔ build.core
⠋ package.binaries Task: package.binaries.client
Task: package.binaries.main
✔ package.binaries
✔ package.docker.base
✔ package.docker.plugins
✔ package.plugins

Desired Behaviour:

    ✔ build.core
        ✔ build.core.pre
        ✔ build.core.client
        ✔ build.core.main
    ✔ package.binaries
        ✔ package.binaries.client
        ✔ package.binaries.main
    ✔ package.docker.plugins
        ✔ package.docker.base
✔ package.plugins

Out of date types

Hey hey!

Tried to use foy, but ran into issues with the typings. So I tried to build it locally, and found that it doesn't build at all for me. Looks like DefinitelyTyped has removed a bunch of package typings as they are now done in-house, and most of the typing packages you rely on aren't supported anymore: @types/node, @types/log-symbols, @types/log-update, @types/strip-ansi, @types/figures, @types/axios, @types/execa. And then it looks like you're using a bunch of outdated tools/libraries, which makes typings difficult (typedocs is on 0.19.1 now, for example).

Thanks!

difference with Just and support of i18n for messages?

I am using Just on a project but I need the task runner be able to provide non-english messages when running tasks. It seems Just doesn't support that.

I am looking at other task runners. What is the difference between foy and Just and does foy support i18n for messages?

Thanks.

Usage with Pure ESM Packages

Super good job on this tool; extremely easy to use and has made task running very easy.

I'm wondering you could add documentation/support for using pure ESM packages with Foy. It seems like when Foy runs ts-node, it doesn't take the ts-node settings from my tsconfig.json to be able to run ts-node in ESM.

This has resulted in some more modern packages being incompatible with Foy.

Happy to provide a minimal reproducible example as well if it's helpful; thanks!

Namespacify is cumbersome

namespacify requires writing the namespace and task logic twice: first in the namespacify call and then in the actual namespace and task calls. I might as well just write them out as strings, bypassing the main benefit of using a function like namespace. It would be nice if the namespacify work happened without intervention: you call namespace, and then inside you call a task, and it joins them together for you.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.