Skip to content

Commit 83b5cc1

Browse files
committed
fix: add default options for execa
1 parent 1a8d340 commit 83b5cc1

4 files changed

Lines changed: 92 additions & 19 deletions

File tree

README.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,12 +378,69 @@ task('build', async ctx => {
378378
A simple wrapper for sindresorhus's lovely module
379379
[execa](https://github.com/sindresorhus/execa)
380380

381+
#### Using `ctx.$` (template literal syntax)
382+
383+
`ctx.$` is a tagged template literal function that provides a shell-like syntax for executing commands. It's also available as a standalone export `$` from foy.
384+
381385
```ts
382-
import { logger, $ } from 'foy'
386+
import { $ } from 'foy'
383387

384388
task('build', async ctx => {
389+
// Basic usage - simple command
385390
await $`tsc`
386391
await $`node ./test.js`
392+
393+
// Using ctx.$ (same as standalone $)
394+
await ctx.$`tsc`
395+
await ctx.$`node ./test.js`
396+
397+
// Variable interpolation - values are automatically escaped
398+
const file = 'my file.txt' // note the space in filename
399+
await $`cat ${file}` // safely escapes to: cat 'my file.txt'
400+
401+
const count = 5
402+
await $`echo ${count}` // outputs: 5
403+
404+
// Using with options
405+
const { stdout } = await $`ls -la`
406+
console.log(stdout)
407+
408+
// Complex example with multiple variables
409+
const src = './src'
410+
const dest = './dist'
411+
await $`cp -r ${src} ${dest}`
412+
})
413+
```
414+
415+
**Key features of `ctx.$`:**
416+
- **Automatic escaping**: Variables are safely escaped for shell execution
417+
- **Template literal syntax**: More readable and familiar for shell commands
418+
- **Returns execa result**: Access `stdout`, `stderr`, `exitCode`, etc.
419+
- **Default options**: `stdio: 'inherit'` and `shell: true` are set by default
420+
421+
**Differences from `ctx.exec`:**
422+
423+
```ts
424+
// ctx.$ - template literal syntax, auto-escaping
425+
await $`echo ${userInput}` // safely escapes special characters
426+
427+
// ctx.exec - string syntax, no auto-escaping
428+
await ctx.exec(`echo ${userInput}`) // be careful with special characters!
429+
430+
// ctx.exec supports multiline commands
431+
await ctx.exec(`
432+
echo "step 1"
433+
echo "step 2"
434+
echo "step 3"
435+
`)
436+
```
437+
438+
#### Using `ctx.exec`
439+
440+
```ts
441+
import { logger, $ } from 'foy'
442+
443+
task('build', async ctx => {
387444
await ctx.exec('tsc')
388445

389446
// run multiple commands synchronously

src/exec.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type Options,
66
type ResultPromise,
77
type Result,
8+
type ExecaScriptMethod,
89
execaNode,
910
} from 'execa'
1011
import pathLib from 'path'
@@ -14,12 +15,19 @@ import { fs, WatchDirOptions } from './fs'
1415
import { Stream, Writable } from 'stream'
1516
import { ChildProcess, ExecOptions, spawn } from 'child_process'
1617
import shellParser from 'shell-parser'
17-
import { stdin } from 'process'
18-
export { execa, _$ }
19-
export const $ = _$({
20-
stdio: 'inherit',
21-
shell: true,
22-
})
18+
import { setGlobalOptions } from './task'
19+
import { getGlobalTaskManager } from './task-manager'
20+
export const $: ExecaScriptMethod<{
21+
stdio: 'inherit'
22+
shell: true
23+
}> = ((...args: any) => {
24+
const options = getGlobalTaskManager().globalOptions.execa
25+
return (_$ as any)({
26+
stdio: 'inherit',
27+
shell: true,
28+
...options,
29+
})(...args)
30+
}) as any
2331

2432
export function exec(cmd: string, options?: Options): ResultPromise<Options> {
2533
return execaCommand(cmd, {

src/task-manager.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { fs } from './fs'
1111
import { ShellContext } from './exec'
1212
import { logger, ILogInfo, ILoggerProps, LogLevels, Logger } from './logger'
1313
import figures from 'figures'
14-
14+
import { type Options as ExecaOptions } from 'execa'
1515
export interface GlobalOptions {
1616
/**
1717
* spinner
@@ -21,6 +21,7 @@ export interface GlobalOptions {
2121
/** @deprecated use spinner */
2222
loading?: boolean
2323
indent?: number
24+
execa?: ExecaOptions
2425
/**
2526
* Whether task options only allow defined options, default false
2627
* @default false
@@ -87,7 +88,10 @@ export class TaskContext<O = any> extends ShellContext {
8788
get error() {
8889
return this._logger.error
8990
}
90-
constructor(public task: Task<O>, public global: GlobalOptions) {
91+
constructor(
92+
public task: Task<O>,
93+
public global: GlobalOptions,
94+
) {
9195
super()
9296
this.logCommand = defaults(task.logger && task.logCommand, global.logCommand, true)
9397
this._logger = new Logger(global.logger)
@@ -300,9 +304,7 @@ export class TaskManager {
300304
force: false,
301305
...props,
302306
}
303-
this._tasks.all =
304-
this._tasks.all ||
305-
(task('all', Object.keys(this._tasks)))
307+
this._tasks.all = this._tasks.all || task('all', Object.keys(this._tasks))
306308
this._tasks.default = this._tasks.default || this._tasks.all
307309

308310
if (!this._tasks[Is.str(name) ? name : name.name]) {

src/task.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
import chalk from 'chalk'
21
import { ShellContext } from './exec'
32
import { hashAny, Is, defaults } from './utils'
43
import { fs } from './fs'
54
import { ILoggerProps, logger } from './logger'
65
import { CliLoading } from './cli-loading'
76
import { DepBuilder } from './dep-builder'
8-
import { GlobalOptions, RunTaskOptions, getGlobalTaskManager, TaskContext, ListenerNames } from './task-manager'
7+
import {
8+
GlobalOptions,
9+
RunTaskOptions,
10+
getGlobalTaskManager,
11+
TaskContext,
12+
ListenerNames,
13+
} from './task-manager'
914
import { deferRunCli } from './run-cli'
1015
interface OptionConfig {
11-
default?: any;
12-
type?: any[];
16+
default?: any
17+
type?: any[]
1318
}
1419
export type OptionDef = [string, string, OptionConfig | undefined]
1520

@@ -73,7 +78,7 @@ export function setGlobalOptions(options: GlobalOptions) {
7378
options.spinner ??= options.loading
7479
Object.assign(getGlobalTaskManager().globalOptions, options)
7580
}
76-
function appendCallback<Fn extends ((...args) => void | Promise<void>)>(name: ListenerNames, fn: Fn) {
81+
function appendCallback<Fn extends (...args) => void | Promise<void>>(name: ListenerNames, fn: Fn) {
7782
let tm = getGlobalTaskManager()
7883
tm.listeners[name].push({
7984
namespaces: tm.namespaces,
@@ -82,7 +87,8 @@ function appendCallback<Fn extends ((...args) => void | Promise<void>)>(name: Li
8287
}
8388
export const before = (fn: (t: Task) => void | Promise<void>) => appendCallback('before', fn)
8489
export const after = (fn: (t: Task) => void | Promise<void>) => appendCallback('after', fn)
85-
export const onerror = (fn: (err: Error, t: Task) => void | Promise<void>) => appendCallback('onerror', fn)
90+
export const onerror = (fn: (err: Error, t: Task) => void | Promise<void>) =>
91+
appendCallback('onerror', fn)
8692

8793
namespace TaskOptions {
8894
export let last = empty()
@@ -161,7 +167,7 @@ export function task<O>(
161167
strict: TaskOptions.last.strict,
162168
spinner: TaskOptions.last.spinner,
163169
rawArgs: [],
164-
dependencies: dependencies.map(d => {
170+
dependencies: dependencies.map((d) => {
165171
if (Is.str(d)) {
166172
return { name: d, options: {} } as Task
167173
} else if (d._isDepBuilder) {

0 commit comments

Comments
 (0)