1 | # Foy
|
2 |
|
3 | [![Build Status](https://travis-ci.org/zaaack/foy.svg?branch=master)](https://travis-ci.org/zaaack/foy) [![npm](https://img.shields.io/npm/v/foy.svg)](https://www.npmjs.com/package/foy) [![npm](https://img.shields.io/npm/dm/foy.svg)](https://www.npmjs.com/package/foy) [![install size](https://packagephobia.now.sh/badge?p=foy)](https://packagephobia.now.sh/result?p=foy)
|
4 |
|
5 | A simple, light-weight and modern task runner for general purpose.
|
6 |
|
7 | ## Contents
|
8 |
|
9 | - [Foy](#foy)
|
10 | - [Contents](#contents)
|
11 | - [Features](#features)
|
12 | - [Install](#install)
|
13 | - [Write a Foyfile](#write-a-foyfile)
|
14 | - [Using with built-in promised-based API](#using-with-built-in-promised-based-api)
|
15 | - [Using with other packages](#using-with-other-packages)
|
16 | - [Using dependencies](#using-dependencies)
|
17 | - [Using namespaces](#using-namespaces)
|
18 | - [Useful utils](#useful-utils)
|
19 | - [fs](#fs)
|
20 | - [logger](#logger)
|
21 | - [exec command](#exec-command)
|
22 | - [Using in CI servers](#using-in-ci-servers)
|
23 | - [Using lifecycle hooks](#using-lifecycle-hooks)
|
24 | - [run task in task](#run-task-in-task)
|
25 | - [Watch and build](#watch-and-build)
|
26 | - [Using with custom compiler](#using-with-custom-compiler)
|
27 | - [API documentation](#api-documentation)
|
28 | - [License](#license)
|
29 |
|
30 | ## Features
|
31 |
|
32 | - Promise-based tasks and built-in utilities.
|
33 | - <a href="https://github.com/shelljs/shelljs" target="_blank">shelljs</a>-like commands
|
34 | - Easy to learn, stop spending hours for build tools.
|
35 | - Small install size
|
36 | - foy: [![install size](https://packagephobia.now.sh/badge?p=foy)](https://packagephobia.now.sh/result?p=foy)
|
37 | - gulp: [![install size](https://packagephobia.now.sh/badge?p=gulp)](https://packagephobia.now.sh/result?p=gulp)
|
38 | - grunt: [![install size](https://packagephobia.now.sh/badge?p=grunt)](https://packagephobia.now.sh/result?p=grunt)
|
39 |
|
40 | ![GIF](https://github.com/zaaack/foy/blob/master/docs/capture.gif?raw=true)
|
41 |
|
42 | ## Install
|
43 |
|
44 | ```sh
|
45 | yarn add -D foy # or npm i -D foy
|
46 | ```
|
47 |
|
48 | Or install globally with
|
49 |
|
50 | ```sh
|
51 | yarn add -g foy # or npm i -g foy
|
52 | ```
|
53 |
|
54 | ## Write a Foyfile
|
55 |
|
56 | You need to add a Foyfile.js(or Foyfile.ts with [ts-node](https://github.com/TypeStrong/ts-node) installed) to your project root.
|
57 |
|
58 | Also, you can simply generate a Foyfile.js via:
|
59 |
|
60 | ```sh
|
61 | foy --init
|
62 | ```
|
63 |
|
64 | which will create a simple `Foyfile.js` in the current folder:
|
65 |
|
66 | ```js
|
67 | // Foyfile.js
|
68 | const { task } = require('foy')
|
69 |
|
70 | task('build', async ctx => {
|
71 | await ctx.exec('tsc')
|
72 | })
|
73 | ```
|
74 |
|
75 | You can also generate a `Foyfile.ts` via
|
76 |
|
77 | ```sh
|
78 | foy --init ts
|
79 | ```
|
80 |
|
81 | Then we can run `foy build` to execute the `build` task.
|
82 |
|
83 | ```sh
|
84 | foy build
|
85 | ```
|
86 |
|
87 | You can also add some options and a description to your tasks:
|
88 |
|
89 | ```ts
|
90 | import { task, desc, option, strict } from 'foy'
|
91 |
|
92 | desc('Build ts files with tsc')
|
93 | option('-w, --watch', 'watch file changes')
|
94 | strict() // This will throw an error if you passed some options that doesn't defined via `option()`
|
95 | task('build', async ctx => {
|
96 | await ctx.exec(`tsc ${ctx.options.watch ? '-w' : ''}`)
|
97 | })
|
98 | ```
|
99 |
|
100 | ```sh
|
101 | foy build -w
|
102 | ```
|
103 |
|
104 | Warning! If you want to set flags like strict for all tasks, please use `setGlobalOptions`:
|
105 |
|
106 | ```ts
|
107 | import { setGlobalOptions } from 'foy'
|
108 |
|
109 | setGlobalOptions({ strict: true }) // all tasks' options will be strict.
|
110 |
|
111 | option('-aa') // strict via default
|
112 | task('dev', async ctx => {
|
113 |
|
114 | })
|
115 | option('-bb') // strict via default
|
116 | task('build', async ctx => {
|
117 |
|
118 | })
|
119 |
|
120 | ```
|
121 |
|
122 | ## Using with built-in promised-based API
|
123 |
|
124 | ```ts
|
125 | import { fs, task } from 'foy'
|
126 |
|
127 | task('some task', async ctx => {
|
128 | await fs.rmrf('/some/dir/or/file') // Remove directory or file
|
129 | await fs.copy('/src', '/dist') // Copy folder or file
|
130 | let json = await fs.readJson('./xx.json')
|
131 | await ctx
|
132 | .env('NODE_ENV', 'production')
|
133 | .cd('./src')
|
134 | .exec('some command') // Execute an command
|
135 | let { stdout } = await ctx.exec('ls', { stdio: 'pipe' }) // Get the stdout, default is empty because it's redirected to current process via `stdio: 'inherit'`.
|
136 | })
|
137 | ```
|
138 |
|
139 | ## Using with other packages
|
140 |
|
141 | ```ts
|
142 | import { task, logger } from 'foy'
|
143 | import * as axios from 'axios'
|
144 |
|
145 | task('build', async ctx => {
|
146 | let res = await axios.get('https://your.server/data.json')
|
147 | logger.info(res.data)
|
148 | })
|
149 | ```
|
150 |
|
151 | ## Using dependencies
|
152 |
|
153 |
|
154 | ```ts
|
155 |
|
156 | import { task } from 'foy'
|
157 | import * as axios from 'axios'
|
158 |
|
159 | task('test', async ctx => {
|
160 | await ctx.exec('mocha')
|
161 | })
|
162 |
|
163 | task('build', async ctx => {
|
164 | let res = await axios.get('https://your.server/data.json')
|
165 | console.log(res.data)
|
166 | await ctx.exec('build my awesome project')
|
167 | })
|
168 | task(
|
169 | 'publish:patch',
|
170 | ['test', 'build'], // Run test and build before publish
|
171 | async ctx => {
|
172 | await ctx.exec('npm version patch')
|
173 | await ctx.exec('npm publish')
|
174 | }
|
175 | )
|
176 | ```
|
177 |
|
178 | Dependencies run serially by default but you can specify when a task should be run concurrently.
|
179 |
|
180 | Example: Passing running options to dependencies:
|
181 |
|
182 | ```ts
|
183 | task(
|
184 | 'publish:patch',
|
185 | [{
|
186 | name: 'test',
|
187 | async: true, // run test parallelly
|
188 | force: true, // force rerun test whether it has been executed before or not.
|
189 | }, {
|
190 | name: 'build',
|
191 | async: true,
|
192 | force: true,
|
193 | },],
|
194 | async ctx => {
|
195 | await ctx.exec('npm version patch')
|
196 | await ctx.exec('npm publish')
|
197 | }
|
198 | )
|
199 |
|
200 | /* Sugar version */
|
201 | task(
|
202 | 'publish:patch',
|
203 | [ 'test'.async().force(),
|
204 | 'build'.async().force() ],
|
205 | async ctx => {
|
206 | await ctx.exec('npm version patch')
|
207 | await ctx.exec('npm publish')
|
208 | }
|
209 | )
|
210 |
|
211 | /*
|
212 | Priority for async tasks
|
213 |
|
214 | Default is 0, higher values will be run earlier; so, in this next example, `build` will be run before `test`.
|
215 | (Note: If you have multiple async dependencies with same priority, they will be executed in parallel.)
|
216 | */
|
217 | task(
|
218 | 'publish:patch',
|
219 | [ 'test'.async(0).force(),
|
220 | 'build'.async(1).force() ],
|
221 | async ctx => {
|
222 | await ctx.exec('npm version patch')
|
223 | await ctx.exec('npm publish')
|
224 | }
|
225 | )
|
226 | ```
|
227 |
|
228 | You can also pass options to dependencies:
|
229 |
|
230 | ```ts
|
231 | task('task1', async ctx => {
|
232 | console.log(ctx.options) // "{ forceRebuild: true, lazyOptions: 1 }"
|
233 | console.log(ctx.global.options) // options from command line "{ a: 1 }"
|
234 | })
|
235 |
|
236 |
|
237 | task('task2', [{
|
238 | name: 'task1',
|
239 | options: {
|
240 | forceRebuild: true,
|
241 | },
|
242 | // Some options that rely on ctx or asynchronization,
|
243 | // it will be merged to options.
|
244 | resolveOptions: async ctx => {
|
245 | return { lazyOptions: 1 }
|
246 | }
|
247 | }])
|
248 |
|
249 | // foy task2 -a 1
|
250 | ```
|
251 |
|
252 | ## Using namespaces
|
253 |
|
254 | To avoid name collisions, Foy provides namespaces to group tasks via the `namespace` function:
|
255 |
|
256 | ```ts
|
257 | import { task, namespace } from 'foy'
|
258 |
|
259 | namespace('client', ns => {
|
260 | before(() => {
|
261 | logger.info('before')
|
262 | })
|
263 | after(() => {
|
264 | logger.info('after')
|
265 | })
|
266 | onerror(() => {
|
267 | logger.info('onerror')
|
268 | })
|
269 | task('start', async ctx => { /* ... */ }) // client:start
|
270 | task('build', async ctx => { /* ... */ }) // client:build
|
271 | task('watch', async ctx => { /* ... */ }) // client:watch
|
272 | namespace('proj1', ns => { // nested namespace
|
273 | onerror(() => {
|
274 | logger.info('onerror', ns)
|
275 | })
|
276 | task('start', async ctx => { /* ... */ }) // client:proj1:start
|
277 |
|
278 | })
|
279 | })
|
280 |
|
281 | namespace('server', ns => {
|
282 | task('build', async ctx => { /* ... */ }) // server:build
|
283 | task('start', async ctx => { /* ... */ }) // server:start
|
284 | task('watch', async ctx => { /* ... */ }) // server:watch
|
285 | })
|
286 |
|
287 | task('start', ['client:start'.async(), 'server:start'.async()]) // start
|
288 |
|
289 | // foy start
|
290 | // foy client:build
|
291 | ```
|
292 |
|
293 |
|
294 | ## Useful utils
|
295 |
|
296 | ### fs
|
297 |
|
298 | 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.
|
299 |
|
300 | ```ts
|
301 | import { fs } from 'foy'
|
302 |
|
303 |
|
304 | task('build', async ctx => {
|
305 | let f = await fs.readFileSync('./assets/someFile')
|
306 |
|
307 | // copy file or directory
|
308 | await fs.copy('./fromPath', './toPath')
|
309 |
|
310 | // watch a directory
|
311 | await fs.watchDir('./src', (event, filename) => {
|
312 | logger.info(event, filename)
|
313 | })
|
314 |
|
315 | // make directory with parent directories
|
316 | await fs.mkdirp('./some/directory/with/parents/not/exists')
|
317 |
|
318 | // write file will auto create missing parent directories
|
319 | await fs.outputFile('./some/file/with/parents/not/exists', 'file data')
|
320 |
|
321 | // write json file will auto create missing parent directories
|
322 | await fs.outputJson('./some/file/with/parents/not/exists', {text: 'json data'})
|
323 | let file = await fs.readJson('./some/jsonFile')
|
324 |
|
325 | // iterate directory tree
|
326 | await fs.iter('./src', async (path, stat) => {
|
327 | if (stat.isDirectory()) {
|
328 | logger.info('directory:', path)
|
329 | // skip scan node_modules
|
330 | if (path.endsWith('node_modules')) {
|
331 | return true
|
332 | }
|
333 | } else if (stat.isFile()) {
|
334 | logger.warn('file:', path)
|
335 | }
|
336 | })
|
337 | })
|
338 | ```
|
339 |
|
340 | ### logger
|
341 |
|
342 | Foy includes a light-weight built-in logger
|
343 |
|
344 | ```ts
|
345 | import { logger } from 'foy'
|
346 |
|
347 | task('build', async ctx => {
|
348 |
|
349 | logger.debug('debug', { aa: 1})
|
350 | logger.info('info')
|
351 | logger.warn('warn')
|
352 | logger.error('error')
|
353 |
|
354 | })
|
355 |
|
356 | ```
|
357 |
|
358 | ### exec command
|
359 |
|
360 | A simple wrapper for sindresorhus's lovely module
|
361 | [execa](https://github.com/sindresorhus/execa)
|
362 |
|
363 | ```ts
|
364 | import { logger } from 'foy'
|
365 |
|
366 | task('build', async ctx => {
|
367 | await ctx.exec('tsc')
|
368 |
|
369 | // run multiple commands synchronously
|
370 | await ctx.exec([
|
371 | 'tsc --outDir ./lib',
|
372 | 'tsc --module es6 --outDir ./es',
|
373 | ])
|
374 |
|
375 | // run multiple commands concurrently
|
376 | await Promise.all([
|
377 | ctx.exec('eslint'),
|
378 | ctx.exec('tsc'),
|
379 | ctx.exec('typedoc'),
|
380 | ])
|
381 | // restart process when file changes
|
382 | ctx.monitor('./src', 'node ./dist')
|
383 | ctx.monitor('./src', ['rm -rf dist', 'tsc', 'node dist'])
|
384 | ctx.monitor('./src', async () => {
|
385 | await ctx.run('build:server')
|
386 | await ctx.exec('node ./dist') // auth detect long-running process when using ctx.exec
|
387 | })
|
388 | ctx.monitor('./src', async (p) => {
|
389 | // manually point out the process need to be killed when restart
|
390 | p.current = require('child_process').exec('node dist')
|
391 | })
|
392 | })
|
393 |
|
394 |
|
395 | ```
|
396 |
|
397 | ## Using in CI servers
|
398 |
|
399 | If you use Foy in CI servers, you won't want the [loading spinners](https://github.com/sindresorhus/cli-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:
|
400 |
|
401 | ```ts
|
402 | import { task, setGlobalOptions } from 'foy'
|
403 |
|
404 | setGlobalOptions({ loading: false }) // disable loading animations
|
405 |
|
406 | task('test', async cyx => { /* ... */ })
|
407 | /*
|
408 | $ foy test
|
409 | DependencyGraph for task [test]:
|
410 | ─ test
|
411 |
|
412 | Task: test
|
413 | ...
|
414 | */
|
415 | ```
|
416 |
|
417 | ## Using lifecycle hooks
|
418 |
|
419 | You can add lifecycle hooks via the `before`, `after`, and `onerror` functions.
|
420 |
|
421 | ```ts
|
422 | import { before, after, onerror } from 'foy'
|
423 | before(() => { // do something before all tasks tree start
|
424 | // ...
|
425 | })
|
426 | after(() => { // do something after all tasks tree finished
|
427 | // ...
|
428 | })
|
429 | onerror((err) => { // do something when error happens
|
430 | // ...
|
431 | })
|
432 | ```
|
433 |
|
434 | ## run task in task
|
435 |
|
436 | ```ts
|
437 |
|
438 | task('task1', async ctx => { /* ... */ })
|
439 | task('task2', async ctx => {
|
440 | // do things before task1
|
441 |
|
442 | // run task1 manually, so we can
|
443 | // do things before or after it
|
444 | await ctx.run('task1')
|
445 |
|
446 | // do things after task1
|
447 | })
|
448 |
|
449 | ```
|
450 |
|
451 | ## Watch and build
|
452 |
|
453 | ```ts
|
454 |
|
455 | task('build', async ctx => { /* build your project */ })
|
456 | task('run', async ctx => { /* start your project */ })
|
457 |
|
458 | let p = null
|
459 | task('watch', async ctx => {
|
460 | ctx.fs.watchDir('./src', async (evt, file) => {
|
461 | await ctx.run('build')
|
462 | p && !p.killed && p.kill()
|
463 | p = await ctx.run('run')
|
464 | })
|
465 | })
|
466 | ```
|
467 |
|
468 | ## Using with custom compiler
|
469 |
|
470 | ```sh
|
471 |
|
472 | # Write Foyfile in ts, enabled by default
|
473 | foy -r ts-node/register -c ./some/Foyfile.ts build
|
474 |
|
475 | # Write Foyfile in coffee
|
476 | foy -r coffeescript/register -c ./some/Foyfile.coffee build
|
477 |
|
478 | ```
|
479 |
|
480 | ## API documentation
|
481 |
|
482 | <https://zaaack.github.io/foy/api>
|
483 |
|
484 | ## License
|
485 |
|
486 | MIT
|