Skip to content

Commit 86ac355

Browse files
committed
fix: add default options for execa
1 parent 1a8d340 commit 86ac355

10 files changed

Lines changed: 141 additions & 52 deletions

File tree

Foyfile.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ task('demo2', async (ctx) => sleep(3000))
145145
task('demo3', ['demo2', 'demodemodemodemodemodemodemo1'], async (ctx) => sleep(3000))
146146

147147
task('demo', ['demodemodemodemodemodemodemo1', dep('demo2').async(), dep('demo3').async()])
148-
148+
task('$', async (ctx) => {
149+
await ctx.env('aa', '1').$`echo test`
150+
})
149151
task('error', async (ctx) => {
150152
throw new Error('aa')
151153
})

README.md

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,12 +378,64 @@ 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+
385+
```ts
386+
387+
task('build', async ctx => {
388+
// Using ctx.$ (same as standalone $)
389+
await ctx.$`tsc`
390+
await ctx.$`node ./test.js`
391+
392+
// Variable interpolation - values are automatically escaped
393+
const file = 'my file.txt' // note the space in filename
394+
await ctx.$`cat ${file}` // safely escapes to: cat 'my file.txt'
395+
396+
const count = 5
397+
await ctx.$`echo ${count}` // outputs: 5
398+
399+
// Using with options
400+
const { stdout } = await ctx.$`ls -la`
401+
console.log(stdout)
402+
403+
// Complex example with multiple variables
404+
const src = './src'
405+
const dest = './dist'
406+
await ctx.$`cp -r ${src} ${dest}`
407+
})
408+
```
409+
410+
**Key features of `ctx.$`:**
411+
- **Automatic escaping**: Variables are safely escaped for shell execution
412+
- **Template literal syntax**: More readable and familiar for shell commands
413+
- **Returns execa result**: Access `stdout`, `stderr`, `exitCode`, etc.
414+
- **Default options**: `stdio: 'inherit'` and `shell: true` are set by default
415+
416+
**Differences from `ctx.exec`:**
417+
418+
```ts
419+
// ctx.$ - template literal syntax, auto-escaping
420+
await ctx.$`echo ${userInput}` // safely escapes special characters
421+
422+
// ctx.exec - string syntax, no auto-escaping
423+
await ctx.exec(`echo ${userInput}`) // be careful with special characters!
424+
425+
// ctx.exec supports multiline commands
426+
await ctx.exec(`
427+
echo "step 1"
428+
echo "step 2"
429+
echo "step 3"
430+
`)
431+
```
432+
433+
#### Using `ctx.exec`
434+
381435
```ts
382436
import { logger, $ } from 'foy'
383437

384438
task('build', async ctx => {
385-
await $`tsc`
386-
await $`node ./test.js`
387439
await ctx.exec('tsc')
388440

389441
// run multiple commands synchronously

src/exec.ts

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,51 @@ import {
55
type Options,
66
type ResultPromise,
77
type Result,
8+
type ExecaScriptMethod,
89
execaNode,
10+
TemplateExpression,
11+
VerboseObject,
912
} from 'execa'
1013
import pathLib from 'path'
1114
import { logger, logger as _logger } from './logger'
1215
import { sleep, Is, DefaultLogFile } from './utils'
1316
import { fs, WatchDirOptions } from './fs'
1417
import { Stream, Writable } from 'stream'
1518
import { ChildProcess, ExecOptions, spawn } from 'child_process'
16-
import shellParser from 'shell-parser'
17-
import { stdin } from 'process'
18-
export { execa, _$ }
19-
export const $ = _$({
19+
import { createRequire } from 'module'
20+
21+
function _logCmd(cmd: string, env?: object) {
22+
let envStr = Object.keys(env || {})
23+
.map((k) => `${k}=${env?.[k] || ''}`)
24+
.join(' ')
25+
if (envStr) {
26+
envStr += ' '
27+
}
28+
logger.info(`$ ${envStr}${cmd}`)
29+
}
30+
31+
function joinTag(strings: TemplateStringsArray, ...values: TemplateExpression[]): string {
32+
return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '')
33+
}
34+
35+
const verbose = (verboseLine: string, verboseObject: VerboseObject) => {
36+
if (verboseObject.type === 'command') {
37+
_logCmd(verboseObject.escapedCommand, verboseObject.options.env)
38+
}
39+
}
40+
41+
const DefaultExecaOptions: Options = {
2042
stdio: 'inherit',
2143
shell: true,
22-
})
44+
verbose: verbose,
45+
extendEnv: true,
46+
}
2347

48+
export const $ = _$(DefaultExecaOptions)
2449
export function exec(cmd: string, options?: Options): ResultPromise<Options> {
50+
// _logCmd(cmd, options?.env)
2551
return execaCommand(cmd, {
26-
stdio: 'inherit',
27-
shell: true,
52+
...DefaultExecaOptions,
2853
...options,
2954
})
3055
}
@@ -33,6 +58,7 @@ export class ShellContext {
3358
private _cwdStack = [process.cwd()]
3459
private _env: { [k: string]: string | undefined } = {}
3560
logCommand = false
61+
execaOptions: Options | undefined
3662
sleep = sleep
3763
/**
3864
* get current word directory
@@ -71,7 +97,14 @@ export class ShellContext {
7197
return this
7298
}
7399

74-
$ = $
100+
get $() {
101+
return $({
102+
env: {
103+
...this._env,
104+
},
105+
verbose: this.logCommand ? verbose : undefined,
106+
})
107+
}
75108

76109
/**
77110
* NOTE!!!: New multiple commands are written as a single string with multiple lines,
@@ -91,19 +124,13 @@ export class ShellContext {
91124
* ```
92125
*/
93126
exec(command: string, options?: Options): ResultPromise<Options> {
94-
// async exec(commands: string[], options?: Options): Promise<Result[]>
95-
// async exec(
96-
// commands: string | string[],
97-
// options?: Options,
98-
// ): Promise<Result[]> | ResultPromise<Options>
99-
this._logCmd(command)
100127
let p = exec(command, {
101128
cwd: this.cwd,
102129
env: {
103-
...process.env,
104130
...this._env,
105131
},
106132
stdio: 'inherit',
133+
verbose: this.logCommand ? verbose : undefined,
107134
...options,
108135
})
109136
// tslint:disable-next-line:no-floating-promises
@@ -182,20 +209,6 @@ export class ShellContext {
182209
this._env = {}
183210
return this
184211
}
185-
private _logCmd(cmd: string | string[]) {
186-
if (this.logCommand) {
187-
let env = Object.keys(this._env)
188-
.map((k) => `${k}=${this._env[k] || ''}`)
189-
.join(' ')
190-
if (env) {
191-
env += ' '
192-
}
193-
cmd = Array.isArray(cmd) ? cmd : [cmd]
194-
cmd.forEach((cmd) => {
195-
this._logger.info(`$ ${env}${cmd}`)
196-
})
197-
}
198-
}
199212
}
200213

201214
export async function shell(callback: (ctx: ShellContext) => Promise<any>) {

src/task-manager.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import { CliLoading } from './cli-loading'
22
import type { Task } from './task'
33
import { task } from './task'
44
import chalk from 'chalk'
5-
import { createRequire } from 'module'
6-
const require = createRequire(import.meta.url)
7-
const pkg = require('../package.json')
5+
// import { createRequire } from 'module'
6+
// const require = createRequire(import.meta.url)
7+
import pkg from '../package.json'
88
import { hashAny, defaults, Is, DefaultLogFile, formatDuration } from './utils'
99
import { Writable, Stream } from 'stream'
1010
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,9 +88,13 @@ 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)
97+
this.execaOptions = defaults(task.execaOptions, global.execa)
9398
this._logger = new Logger(global.logger)
9499
}
95100
/**
@@ -300,9 +305,7 @@ export class TaskManager {
300305
force: false,
301306
...props,
302307
}
303-
this._tasks.all =
304-
this._tasks.all ||
305-
(task('all', Object.keys(this._tasks)))
308+
this._tasks.all = this._tasks.all || task('all', Object.keys(this._tasks))
306309
this._tasks.default = this._tasks.default || this._tasks.all
307310

308311
if (!this._tasks[Is.str(name) ? name : name.name]) {
@@ -347,9 +350,9 @@ export class TaskManager {
347350
}
348351
}
349352

350-
const TMKey = `@foy${pkg.version}/taskManager`
351353
/** @internal */
352354
export function getGlobalTaskManager() {
353-
let taskManager: TaskManager = (global[TMKey] = global[TMKey] || new TaskManager())
355+
const TMKey = `@foy${pkg.version}/taskManager`
356+
let taskManager: TaskManager = (globalThis[TMKey] = globalThis[TMKey] || new TaskManager())
354357
return taskManager
355358
}

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

src/test/fixtures/Foyfile2.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ task('exec', async (ctx) => {
2626
let sleep = ctx.exec('sleep 1')
2727
logger.info(sleep.killed, typeof sleep.then, typeof sleep.kill)
2828
})
29+
task('$', async (ctx) => {
30+
let echo1 = await ctx.$`echo 1`
31+
logger.info(`echo1`, echo1.stdout)
32+
await ctx.$`sleep 1`
33+
})
2934
namespace('ns1', (ns) => {
3035
before((t) => {
3136
logger.log(`before ${ns}`, t.name)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
DependencyGraph for task [ee]:
22
Task: ee
3-
[info] $
3+
[info] $ echo 'aa\n' echo bb
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
DependencyGraph for task [fails]:
22
Task: fails
33
[info] Fail this task
4-
[info] $ node -e "console.log(\"start\"); process.exit(1)"
4+
[info] $ node -e '"console.log(\"start\");' 'process.exit(1)"'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
DependencyGraph for task [$]:
2+
[info] beforeAll $
3+
Task: $
4+
[info] $ echo 1
5+
[info] echo1 undefined
6+
[info] $ sleep 1
7+
[info] afterAll $

src/test/task.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ function test(cmd: string, expectedExitCode?: number) {
4747
async init() {
4848
let p = await exec(`tsx ./src/cli.ts --config ${fixturesDir}/${cmd}`, {
4949
stdio: void 0,
50+
verbose: 'none',
5051
env: {
51-
...process.env,
5252
DISABLE_V8_COMPILE_CACHE: '1',
5353
},
5454
}).catch((er) => er)
@@ -93,6 +93,7 @@ describe('task', function () {
9393
test(`Foyfile2.ts ns1:error`),
9494
test(`Foyfile2.ts ns1:ns2:t2`),
9595
test(`Foyfile2.ts exec`),
96+
test(`Foyfile2.ts $`),
9697
]
9798
before(
9899
async () => {

0 commit comments

Comments
 (0)