@plugjs/plug 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/asserts.cjs +9 -11
  2. package/dist/asserts.cjs.map +1 -1
  3. package/dist/asserts.d.ts +1 -2
  4. package/dist/asserts.mjs +8 -9
  5. package/dist/asserts.mjs.map +1 -1
  6. package/dist/build.cjs +29 -5
  7. package/dist/build.cjs.map +1 -1
  8. package/dist/build.d.ts +4 -0
  9. package/dist/build.mjs +27 -5
  10. package/dist/build.mjs.map +1 -1
  11. package/dist/cli.d.mts +12 -0
  12. package/dist/cli.mjs +266 -0
  13. package/dist/cli.mjs.map +6 -0
  14. package/dist/fork.cjs +35 -12
  15. package/dist/fork.cjs.map +1 -1
  16. package/dist/fork.d.ts +10 -0
  17. package/dist/fork.mjs +36 -13
  18. package/dist/fork.mjs.map +1 -1
  19. package/dist/helpers.cjs +30 -10
  20. package/dist/helpers.cjs.map +2 -2
  21. package/dist/helpers.d.ts +12 -0
  22. package/dist/helpers.mjs +35 -11
  23. package/dist/helpers.mjs.map +2 -2
  24. package/dist/index.cjs +5 -0
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.mjs +4 -1
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/logging/emit.cjs +4 -4
  30. package/dist/logging/emit.cjs.map +1 -1
  31. package/dist/logging/emit.mjs +4 -4
  32. package/dist/logging/emit.mjs.map +1 -1
  33. package/dist/logging/logger.cjs +43 -2
  34. package/dist/logging/logger.cjs.map +1 -1
  35. package/dist/logging/logger.d.ts +36 -3
  36. package/dist/logging/logger.mjs +43 -3
  37. package/dist/logging/logger.mjs.map +1 -1
  38. package/dist/logging/options.cjs +5 -2
  39. package/dist/logging/options.cjs.map +1 -1
  40. package/dist/logging/options.mjs +5 -2
  41. package/dist/logging/options.mjs.map +1 -1
  42. package/dist/logging.cjs +14 -3
  43. package/dist/logging.cjs.map +1 -1
  44. package/dist/logging.d.ts +2 -0
  45. package/dist/logging.mjs +13 -3
  46. package/dist/logging.mjs.map +1 -1
  47. package/dist/plugs/build.cjs +66 -0
  48. package/dist/plugs/build.cjs.map +6 -0
  49. package/dist/plugs/build.d.ts +13 -0
  50. package/dist/plugs/build.mjs +40 -0
  51. package/dist/plugs/build.mjs.map +6 -0
  52. package/dist/types.d.ts +2 -0
  53. package/dist/utils/exec.cjs +5 -12
  54. package/dist/utils/exec.cjs.map +2 -2
  55. package/dist/utils/exec.d.ts +0 -2
  56. package/dist/utils/exec.mjs +6 -13
  57. package/dist/utils/exec.mjs.map +1 -1
  58. package/package.json +7 -9
  59. package/src/asserts.ts +9 -11
  60. package/src/build.ts +33 -4
  61. package/{extra/plug.mts → src/cli.mts} +115 -141
  62. package/src/fork.ts +50 -13
  63. package/src/helpers.ts +53 -1
  64. package/src/index.ts +2 -1
  65. package/src/logging/emit.ts +4 -4
  66. package/src/logging/logger.ts +60 -7
  67. package/src/logging/options.ts +5 -1
  68. package/src/logging.ts +20 -5
  69. package/src/plugs/build.ts +58 -0
  70. package/src/types.ts +2 -0
  71. package/src/utils/exec.ts +6 -20
  72. package/cli/plug.mjs +0 -1385
  73. package/cli/ts-loader.mjs +0 -275
  74. package/cli/tsrun.mjs +0 -1204
  75. package/extra/ts-loader.mts +0 -546
  76. package/extra/tsrun.mts +0 -127
  77. package/extra/utils.ts +0 -150
@@ -2,52 +2,81 @@
2
2
  /* eslint-disable no-console */
3
3
 
4
4
  import _fs from 'node:fs'
5
- import _path from 'node:path'
6
5
 
7
- import _yargs from 'yargs-parser'
6
+ import { main, yargsParser } from '@plugjs/tsrun'
8
7
 
9
- import { $blu, $gry, $rst, $tsk, $und, $wht, isDirectory, isFile, main } from './utils.js'
8
+ import { BuildFailure } from './asserts'
9
+ import { invokeTasks, isBuild } from './build'
10
+ import { $blu, $gry, $p, $red, $t, $und, $wht } from './logging/colors'
11
+ import { getCurrentWorkingDirectory, resolveDirectory, resolveFile, resolveAbsolutePath } from './paths'
10
12
 
11
- import type { BuildFailure } from '../src/asserts.js'
12
- import type { Build } from '../src/index.js'
13
+ import type { AbsolutePath } from './paths'
13
14
 
14
- /** Version injected by esbuild */
15
- declare const __version: string
15
+ /* Extra colors */
16
+ const $bnd = (s: string): string => $blu($und(s))
17
+ const $gnd = (s: string): string => $gry($und(s))
18
+ const $wnd = (s: string): string => $wht($und(s))
19
+
20
+ /** Version injected by esbuild, defaulted in case of dynamic transpilation */
21
+ const version = typeof __version === 'string' ? __version : '0.0.0-dev'
22
+ declare const __version: string | undefined
16
23
 
17
24
  /* ========================================================================== *
18
- * ========================================================================== *
19
- * BUILD INSPECTION *
20
- * ========================================================================== *
25
+ * HELP SCREEN *
21
26
  * ========================================================================== */
22
27
 
23
- /** Symbol indicating that an object is a Build */
24
- const buildMarker = Symbol.for('plugjs:isBuild')
28
+ /** Show help screen */
29
+ function help(): void {
30
+ console.log(`${$bnd('Usage:')}
25
31
 
26
- /** Symbol indicating that an object is a Build Failure */
27
- const buildFailure = Symbol.for('plugjs:buildFailure')
32
+ ${$wht('plugjs')} ${$gry('[')}--options${$gry('] [... ')}prop=val${$gry(' ...] [... ')}task${$gry(' ...]')}
28
33
 
29
- /** Check if the specified build is actually a {@link Build} */
30
- function isBuild(build: any): build is Build<Record<string, any>> & {
31
- [buildMarker]: (tasks: string[], props?: Record<string, string | undefined>) => Promise<void>
32
- } {
33
- return build && typeof build[buildMarker] === 'function'
34
- }
34
+ ${$bnd('Options:')}
35
35
 
36
- /** Check if the specified argument is a {@link BuildFailure} */
37
- function isBuildFailure(arg: any): arg is BuildFailure {
38
- return arg && arg[buildFailure] === buildFailure
39
- }
36
+ ${$wht(`-f --file ${$gnd('file')}`)} Specify the build file to use (default ${$wnd('./build.ts')})
37
+ ${$wht(`-w --watch ${$gnd('dir')}`)} Watch for changes on the specified directory and run
38
+ ${$wht('-v --verbose')} Increase logging verbosity
39
+ ${$wht('-q --quiet')} Decrease logging verbosity
40
+ ${$wht('-c --colors')} Force colorful output (use ${$wnd('--no-colors')} to force plain text)
41
+ ${$wht('-l --list')} Only list the tasks defined by the build, nothing more!
42
+ ${$wht('-h --help')} Help! You're reading it now!
43
+ ${$wht(' --version')} Version! This one: ${version}!
44
+
45
+ ${$bnd('Properties:')}
46
+
47
+ Any argument in the format ${$wnd('key=value')} will be interpeted as a property to
48
+ be injected in the build process (e.g. ${$wnd('mode=production')}).
49
+
50
+ ${$bnd('Tasks:')}
40
51
 
52
+ Any other argument will be treated as a task name. If no task names are
53
+ specified, the ${$t('default')} task will be executed.
41
54
 
55
+ ${$bnd('Watch Mode:')}
56
+
57
+ The ${$wnd('--watch')} option can be specified multiple times, and each single
58
+ directory specified will be watched for changes. Note that Plug's own
59
+ watch mode is incredibly basic, for more complex scenarios use something
60
+ more advanced like nodemon ${$gry('(')}${$gnd('https://www.npmjs.com/package/nodemon')}${$gry(')')}.
61
+
62
+ ${$bnd('TypeScript module format:')}
63
+
64
+ Normally our TypeScript loader will transpile ${$wnd('.ts')} files to the type
65
+ specified in ${$wnd('package.json')}, either ${$wnd('commonjs')} (the default) or ${$wnd('module')}.
66
+
67
+ To force a specific module format use one of the following flags:
68
+
69
+ ${$wht('--force-esm')} Force transpilation of ${$wnd('.ts')} files to EcmaScript modules
70
+ ${$wht('--force-cjs')} Force transpilation of ${$wnd('.ts')} files to CommonJS modules
71
+ `)
72
+ }
42
73
  /* ========================================================================== *
43
- * ========================================================================== *
44
74
  * PARSE COMMAND LINE ARGUMENTS *
45
- * ========================================================================== *
46
75
  * ========================================================================== */
47
76
 
48
77
  /* Parsed and normalised command line options */
49
78
  interface CommandLineOptions {
50
- buildFile: string,
79
+ buildFile: AbsolutePath,
51
80
  watchDirs: string[],
52
81
  tasks: string[],
53
82
  props: Record<string, string>
@@ -57,7 +86,7 @@ interface CommandLineOptions {
57
86
  /** Parse `perocess.argv` and return our normalised command line options */
58
87
  export function parseCommandLine(args: string[]): CommandLineOptions {
59
88
  /* Yargs-parse our arguments */
60
- const parsed = _yargs(args, {
89
+ const parsed = yargsParser(args, {
61
90
  configuration: {
62
91
  'camel-case-expansion': false,
63
92
  'strip-aliased': true,
@@ -91,7 +120,6 @@ export function parseCommandLine(args: string[]): CommandLineOptions {
91
120
  let colors: boolean | undefined = undefined
92
121
  let file: string | undefined = undefined
93
122
  let listOnly = false
94
- let help = false
95
123
 
96
124
  /* Switcharoo on arguments */
97
125
  for (const [ key, value ] of Object.entries(parsed)) {
@@ -123,69 +151,19 @@ export function parseCommandLine(args: string[]): CommandLineOptions {
123
151
  listOnly = !! value
124
152
  break
125
153
  case 'help':
126
- help = !! value
154
+ help()
155
+ process.exit(0)
127
156
  break
128
157
  case 'version':
129
- console.log(`v${__version}`)
158
+ console.log(`PlugJS ${$gry('ver.')} ${$wnd(version)}`)
130
159
  process.exit(0)
131
160
  break
132
161
  default:
133
- console.log(`Unsupported option "${key}" (try "--help")`)
162
+ console.log(`Unsupported option ${$wnd(key)} (try ${$wnd('--help')})`)
134
163
  process.exit(1)
135
164
  }
136
165
  }
137
166
 
138
- /* ======================================================================== *
139
- * HELP OR NOT *
140
- * ======================================================================== */
141
-
142
- /* If help, end here! */
143
- if (help) {
144
- console.log(`${$blu}${$und}Usage:${$rst}
145
-
146
- ${$wht}plugjs${$rst} ${$gry}[${$rst}--options${$gry}] [...${$rst}prop=val${$gry}] [...${$rst}tasks${$gry}]${$rst}
147
-
148
- ${$blu}${$und}Options:${$rst}
149
-
150
- ${$wht}-f --file ${$gry}${$und}file${$rst} Specify the build file to use (default "./build.[ts/js/...]")
151
- ${$wht}-w --watch ${$gry}${$und}dir${$rst} Watch for changes on the specified directory and run build
152
- ${$wht}-v --verbose${$rst} Increase logging verbosity
153
- ${$wht}-q --quiet${$rst} Decrease logging verbosity
154
- ${$wht}-c --colors${$rst} Force colorful output (use "--no-colors" to force plain text)
155
- ${$wht}-l --list${$rst} Only list the tasks defined by the build, nothing more!
156
- ${$wht}-h --help${$rst} Help! You're reading it now!
157
- ${$wht} --version${$rst} Version! This one: ${__version}!
158
-
159
- ${$blu}${$und}Properties:${$rst}
160
-
161
- Any argument in the format "key=value" will be interpeted as a property to
162
- be injected in the build process (e.g. "mode=production").
163
-
164
- ${$blu}${$und}Tasks:${$rst}
165
-
166
- Any other argument will be treated as a task name. If no task names are
167
- specified, the "default" task will be executed.
168
-
169
- ${$blu}${$und}Watch Mode:${$rst}
170
-
171
- The "-w" option can be specified multiple times, and each single directory
172
- specified will be watched for changes. Please note that Plug's own watch
173
- mode is incredibly basic, for more complex scenarios use something more
174
- advanced like nodemon (https://www.npmjs.com/package/nodemon).
175
-
176
- ${$blu}${$und}TypeScript module format:${$rst}
177
-
178
- Normally our TypeScript loader will transpile ".ts" files to the "type"
179
- specified in "package.json", either "commonjs" (the default) or "module".
180
-
181
- To force a specific module format we can use one of the following flags:
182
-
183
- ${$wht}--force-esm ${$rst} Force transpilation of ".ts" files to EcmaScript modules
184
- ${$wht}--force-cjs ${$rst} Force transpilation of ".ts" files to CommonJS modules
185
- `)
186
- process.exit(0)
187
- }
188
-
189
167
  /* ======================================================================== *
190
168
  * LOG OPTIONS AS ENVIRONMENT VARIABLES *
191
169
  * ======================================================================== */
@@ -208,13 +186,14 @@ export function parseCommandLine(args: string[]): CommandLineOptions {
208
186
  * ======================================================================== */
209
187
 
210
188
  /* Find our build file */
189
+ const cwd = getCurrentWorkingDirectory()
211
190
  const exts = [ 'ts', 'mts', 'mjs', 'js', 'mjs', 'cjs' ]
212
191
 
213
- let buildFile: string | undefined = undefined
192
+ let buildFile: AbsolutePath | undefined = undefined
214
193
 
215
194
  if (file) {
216
- const absolute = _path.resolve(file)
217
- if (! isFile(absolute)) {
195
+ const absolute = resolveFile(cwd, file)
196
+ if (! absolute) {
218
197
  console.log(`Specified build file "${file}" was not found`)
219
198
  process.exit(1)
220
199
  } else {
@@ -222,8 +201,8 @@ export function parseCommandLine(args: string[]): CommandLineOptions {
222
201
  }
223
202
  } else {
224
203
  for (const ext of exts) {
225
- const absolute = _path.resolve(`build.${ext}`)
226
- if (! isFile(absolute)) continue
204
+ const absolute = resolveFile(cwd, `build.${ext}`)
205
+ if (! absolute) continue
227
206
  buildFile = absolute
228
207
  break
229
208
  }
@@ -231,7 +210,7 @@ export function parseCommandLine(args: string[]): CommandLineOptions {
231
210
 
232
211
  /* Final check */
233
212
  if (! buildFile) {
234
- console.log(`Unable to find build file "./build.[${exts.join('|')}]`)
213
+ console.log(`${$red('Unable to find build file')} ${$wht(`./build.[${exts.join('|')}]`)}`)
235
214
  process.exit(1)
236
215
  }
237
216
 
@@ -240,9 +219,10 @@ export function parseCommandLine(args: string[]): CommandLineOptions {
240
219
  * ======================================================================== */
241
220
 
242
221
  watchDirs.forEach((watchDir) => {
243
- const absolute = _path.resolve(watchDir)
244
- if (! isDirectory(absolute)) {
245
- console.log(`Specified watch directory "${watchDir}" was not found`)
222
+ const absolute = resolveDirectory(cwd, watchDir)
223
+ if (! absolute) {
224
+ const path = resolveAbsolutePath(cwd, watchDir)
225
+ console.log(`Specified watch directory "${$p(path)}" was not found`)
246
226
  process.exit(1)
247
227
  } else {
248
228
  watchDir = absolute
@@ -257,21 +237,10 @@ export function parseCommandLine(args: string[]): CommandLineOptions {
257
237
  }
258
238
 
259
239
  /* ========================================================================== *
260
- * ========================================================================== *
261
240
  * MAIN ENTRY POINT *
262
- * ========================================================================== *
263
241
  * ========================================================================== */
264
242
 
265
- /* ========================================================================== *
266
- * ========================================================================== *
267
- * PROCESS SETUP *
268
- * ========================================================================== *
269
- * ========================================================================== */
270
-
271
- /* eslint-disable no-console */
272
-
273
- /* We have everyhing we need to start our asynchronous main! */
274
- main(async (args: string[]): Promise<void> => {
243
+ main(import.meta.url, async (args: string[]): Promise<void> => {
275
244
  // Parse and destructure command line
276
245
  const {
277
246
  buildFile,
@@ -293,13 +262,13 @@ main(async (args: string[]): Promise<void> => {
293
262
 
294
263
  // We _need_ a build
295
264
  if (! isBuild(maybeBuild)) {
296
- console.log('Build file did not export a proper build')
265
+ console.log($red('Build file did not export a proper build'))
297
266
  console.log()
298
267
  console.log('- If using CommonJS export your build as "module.exports"')
299
- console.log(' e.g.: module.exports = build({ ... })')
268
+ console.log(` e.g.: ${$wht('module.exports = build({ ... })')}`)
300
269
  console.log()
301
270
  console.log('- If using ESM modules export your build as "default"')
302
- console.log(' e.g.: export default build({ ... })')
271
+ console.log(` e.g.: ${$wht('export default build({ ... })')}`)
303
272
  console.log()
304
273
  process.exit(1)
305
274
  }
@@ -315,20 +284,18 @@ main(async (args: string[]): Promise<void> => {
315
284
  (typeof value === 'string' ? propNames : taskNames).push(key)
316
285
  }
317
286
 
318
- const buildFileName = _path.relative(process.cwd(), buildFile)
319
-
320
- console.log(`\n${$gry}Outline of ${$wht}${buildFileName}${$rst}`)
287
+ console.log(`\n${$gry('Outline of')} ${$p(buildFile)}`)
321
288
 
322
289
  console.log('\nKnown tasks:\n')
323
290
  for (const taskName of taskNames.sort()) {
324
- console.log(` ${$gry}\u25a0${$tsk} ${taskName}${$rst}`)
291
+ console.log(` ${$gry('\u25a0')} ${$t(taskName)}`)
325
292
  }
326
293
 
327
294
  console.log('\nKnown properties:\n')
328
295
  for (const propName of propNames.sort()) {
329
296
  const value = build[propName] ?
330
- ` ${$gry}(default "${$rst}${$und}${build[propName]}${$gry})` : ''
331
- console.log(` ${$gry}\u25a1${$blu} ${propName}${value}${$rst}`)
297
+ ` ${$gry('(default')} ${$und(build[propName])}${$gry(')')}` : ''
298
+ console.log(` ${$gry('\u25a1')} ${$blu(propName)}${value}`)
332
299
  }
333
300
 
334
301
  console.log()
@@ -337,40 +304,47 @@ main(async (args: string[]): Promise<void> => {
337
304
 
338
305
  // Watch directories
339
306
  if (watchDirs.length) {
340
- let timeout: NodeJS.Timeout | undefined = undefined
341
-
342
- const runme = (): void => {
343
- build[buildMarker](tasks, props)
344
- .then(() => {
345
- console.log(`\n${$gry}Watching for files change...${$rst}\n`)
346
- }, (error) => {
347
- if (isBuildFailure(error)) {
348
- console.log(`\n${$gry}Watching for files change...${$rst}\n`)
349
- } else {
350
- console.log(error)
351
- watchers.forEach((watcher) => watcher.close())
352
- }
353
- })
354
- .finally(() => {
355
- timeout = undefined
356
- })
357
- }
358
-
359
- const watchers = watchDirs.map((watchDir) => {
360
- return _fs.watch(watchDir, { recursive: true }, () => {
361
- if (! timeout) timeout = setTimeout(runme, 250)
307
+ return new Promise((_, reject) => {
308
+ // filesystems change trigger a new run after 250 ms a change is detected,
309
+ // in order to give time to editors to save a bunch of files open and
310
+ // modified at the same time...
311
+ let timeout: NodeJS.Timeout | undefined = undefined
312
+
313
+ // our runner executed by the timeout
314
+ const runme = (): void => {
315
+ invokeTasks(build, tasks, props)
316
+ .then(() => {
317
+ console.log(`\n${$gry('Watching for files change...')}\n`)
318
+ }, (error) => {
319
+ if (error instanceof BuildFailure) {
320
+ console.log(`\n${$gry('Watching for files change...')}\n`)
321
+ } else {
322
+ watchers.forEach((watcher) => watcher.close())
323
+ reject(error)
324
+ }
325
+ })
326
+ .finally(() => {
327
+ timeout = undefined
328
+ })
329
+ }
330
+
331
+ // watch all directories and trigger a run after 250 milliseconds
332
+ const watchers = watchDirs.map((watchDir) => {
333
+ return _fs.watch(watchDir, { recursive: true }, () => {
334
+ if (! timeout) timeout = setTimeout(runme, 250)
335
+ })
362
336
  })
363
- })
364
337
 
365
- runme()
366
- return
338
+ // start a build immediately on first run
339
+ runme()
340
+ })
367
341
  }
368
342
 
369
343
  // Normal build (no list, no watchers)
370
344
  try {
371
- await build[buildMarker](tasks, props)
345
+ await invokeTasks(build, tasks, props)
372
346
  } catch (error) {
373
- if (! isBuildFailure(error)) console.log(error)
374
- process.exit(1)
347
+ if (!(error instanceof BuildFailure)) console.log(error)
348
+ process.exitCode = 1
375
349
  }
376
350
  })
package/src/fork.ts CHANGED
@@ -3,13 +3,24 @@ import { fork } from 'node:child_process'
3
3
  import { assert, BuildFailure } from './asserts'
4
4
  import { runAsync } from './async'
5
5
  import { Files } from './files'
6
- import { $gry, $p, logOptions } from './logging'
6
+ import { $gry, $p, $red, logOptions } from './logging'
7
7
  import { requireFilename, resolveFile } from './paths'
8
8
  import { Context, install } from './pipe'
9
9
 
10
10
  import type { AbsolutePath } from './paths'
11
11
  import type { Plug, PlugName, PlugResult } from './pipe'
12
12
 
13
+ /**
14
+ * Options accepted by {@link ForkingPlug}'s instrumenting how the process
15
+ * will be spawned (environment variables to be passed to the child process).
16
+ */
17
+ export interface ForkOptions {
18
+ /** The directory where coverage data will be saved */
19
+ coverageDir?: string,
20
+ /** Force the specified module type when dynamically transpiling TypeScript */
21
+ forceModule?: 'commonjs' | 'module'
22
+ }
23
+
13
24
  /** Fork data, from parent to child process */
14
25
  export interface ForkData {
15
26
  /** Script name for the Plug to execute */
@@ -75,6 +86,10 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
75
86
  const dir = env.NODE_V8_COVERAGE = context.resolve(this._arguments[i].coverageDir)
76
87
  context.log.debug('Forked process will produce coverage in', $p(dir))
77
88
  }
89
+ if (typeof this._arguments[i].forceModule === 'string') {
90
+ const force = env.__TS_LOADER_FORCE_TYPE = this._arguments[i].forceModule
91
+ context.log.debug('Forked process will force module type as', $p(force))
92
+ }
78
93
  }
79
94
  }
80
95
 
@@ -97,24 +112,24 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
97
112
  let response: ForkResult | undefined = undefined
98
113
 
99
114
  child.on('error', (error) => {
100
- context.log.error('Child process error', error)
115
+ context.log.error('Forked plug process error', error)
101
116
  return done || reject(BuildFailure.fail())
102
117
  })
103
118
 
104
119
  child.on('message', (message: ForkResult) => {
105
- context.log.debug('Message from child process with PID', child.pid, message)
120
+ context.log.debug('Message from forked plug process with PID', child.pid, message)
106
121
  response = message
107
122
  })
108
123
 
109
124
  child.on('exit', (code, signal) => {
110
125
  if (signal) {
111
- context.log.error(`Child process exited with signal ${signal}`, $gry(`(pid=${child.pid})`))
126
+ context.log.error(`Forked plug process exited with signal ${signal}`, $gry(`(pid=${child.pid})`))
112
127
  return done || reject(BuildFailure.fail())
113
128
  } else if (code !== 0) {
114
- context.log.error(`Child process exited with code ${code}`, $gry(`(pid=${child.pid})`))
129
+ context.log.error(`Forked plug process exited with code ${code}`, $gry(`(pid=${child.pid})`))
115
130
  return done || reject(BuildFailure.fail())
116
131
  } else if (! response) {
117
- context.log.error('Child process exited with no result', $gry(`(pid=${child.pid})`))
132
+ context.log.error('Forked plug process exited with no result', $gry(`(pid=${child.pid})`))
118
133
  return done || reject(BuildFailure.fail())
119
134
  } else if (response.failed) {
120
135
  // definitely logged on the child side
@@ -131,16 +146,16 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
131
146
  try {
132
147
  const result = child.send(request, (error) => {
133
148
  if (error) {
134
- context.log.error('Error sending message to child process (callback failure)', error)
149
+ context.log.error('Error sending message to forked plug process (callback failure)', error)
135
150
  reject(BuildFailure.fail())
136
151
  }
137
152
  })
138
153
  if (! result) {
139
- context.log.error('Error sending message to child process (send returned false)')
154
+ context.log.error('Error sending message to forked plug process (send returned false)')
140
155
  reject(BuildFailure.fail())
141
156
  }
142
157
  } catch (error) {
143
- context.log.error('Error sending message to child process (exception caught)', error)
158
+ context.log.error('Error sending message to forked plug process (exception caught)', error)
144
159
  reject(BuildFailure.fail())
145
160
  }
146
161
  }).finally(() => done = true)
@@ -158,12 +173,22 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
158
173
  * for the message and respond once the plug returns _something_!
159
174
  */
160
175
  if ((process.argv[1] === requireFilename(__fileurl)) && (process.send)) {
176
+ /* Unhandled exceptions and graceful termination */
177
+ process.on('uncaughtException', (error, origin) => {
178
+ // eslint-disable-next-line no-console
179
+ console.error(
180
+ $red('\n= UNCAUGHT EXCEPTION ========================================='),
181
+ `\nError (${origin}):`, error,
182
+ `\nNode.js ${process.version} (pid=${process.pid})\n`)
183
+ process.nextTick(() => process.exit(3))
184
+ })
185
+
161
186
  /* If we haven't processed our message in 5 seconds, fail _badly_ */
162
187
  const timeout = setTimeout(() => {
163
188
  // eslint-disable-next-line no-console
164
189
  console.error('Fork not initialized in 5 seconds')
165
190
  process.exit(2)
166
- }, 5000)
191
+ }, 5000).unref()
167
192
 
168
193
  /* Await our message to initialize and run the plug */
169
194
  process.on('message', (message: ForkData) => {
@@ -227,10 +252,22 @@ if ((process.argv[1] === requireFilename(__fileurl)) && (process.send)) {
227
252
  /* The promise generated by `process.send()` simply triggers process exit */
228
253
  promise.then(() => {
229
254
  context.log.debug('Forked plug exiting')
230
- process.exit(0)
255
+ process.disconnect()
256
+ process.exitCode = 0
231
257
  }, (error) => {
232
- context.log.error('Error sending message back to parent process', error)
233
- process.exit(1)
258
+ // eslint-disable-next-line no-console
259
+ console.log('\n\nError sending message back to parent process', error)
260
+ process.exitCode = 1
261
+ }).finally(() => {
262
+ /* Flush and end our log output */
263
+ logOptions.output.end()
264
+
265
+ /* Set a timeout _forcefully_ killing the process in 5 seconds */
266
+ setTimeout(() => {
267
+ // eslint-disable-next-line no-console
268
+ console.log('\n\nProcess %d for %s did not exit in 5 seconds', process.pid, exportName)
269
+ process.exit(2)
270
+ }, 5000).unref()
234
271
  })
235
272
  })
236
273
  }
package/src/helpers.ts CHANGED
@@ -7,14 +7,23 @@ import { requireContext } from './async'
7
7
  import { Files } from './files'
8
8
  import { rm } from './fs'
9
9
  import { $p, log } from './logging'
10
- import { commonPath, getCurrentWorkingDirectory, resolveDirectory, resolveFile } from './paths'
10
+ import {
11
+ commonPath,
12
+ getAbsoluteParent,
13
+ getCurrentWorkingDirectory,
14
+ resolveDirectory,
15
+ resolveFile,
16
+ } from './paths'
11
17
  import { PipeImpl } from './pipe'
18
+ import { RunBuild } from './plugs/build'
12
19
  import { execChild } from './utils/exec'
13
20
  import { parseOptions } from './utils/options'
14
21
  import { walk } from './utils/walk'
15
22
 
23
+ import type { ForkOptions } from './fork'
16
24
  import type { Pipe } from './index'
17
25
  import type { AbsolutePath } from './paths'
26
+ import type { Context } from './pipe'
18
27
  import type { ExecChildOptions } from './utils/exec'
19
28
  import type { ParseOptions } from './utils/options'
20
29
  import type { WalkOptions } from './utils/walk'
@@ -29,6 +38,11 @@ export interface FindOptions extends WalkOptions {
29
38
  directory?: string
30
39
  }
31
40
 
41
+ /** Return the current execution {@link Context} */
42
+ export function context(): Context {
43
+ return requireContext()
44
+ }
45
+
32
46
  /** Find files in the current directory using the specified _glob_. */
33
47
  export function find(glob: string): Pipe
34
48
  /** Find files in the current directory using the specified _globs_. */
@@ -56,6 +70,44 @@ export function find(...args: ParseOptions<FindOptions>): Pipe {
56
70
  }))
57
71
  }
58
72
 
73
+ export type InvokeBuildOptions = ForkOptions & Record<string, string>
74
+ export type InvokeBuildTasks = string | [ string, ...string[] ]
75
+
76
+ export function invokeBuild(buildFile: string): Promise<void>
77
+ export function invokeBuild(buildFile: string, task: string): Promise<void>
78
+ export function invokeBuild(buildFile: string, task: string, options: InvokeBuildOptions): Promise<void>
79
+ export function invokeBuild(buildFile: string, tasks: [ string, ...string[] ]): Promise<void>
80
+ export function invokeBuild(buildFile: string, tasks: [ string, ...string[] ], options: InvokeBuildOptions): Promise<void>
81
+ export function invokeBuild(buildFile: string, options: InvokeBuildOptions): Promise<void>
82
+ export async function invokeBuild(
83
+ buildFile: string,
84
+ tasksOrOptions?: string | [ string, ...string[] ] | InvokeBuildOptions,
85
+ maybeOptions?: InvokeBuildOptions,
86
+ ): Promise<void> {
87
+ const [ tasks, options = {} ] =
88
+ typeof tasksOrOptions === 'string' ?
89
+ [ [ tasksOrOptions ], maybeOptions ] :
90
+ Array.isArray(tasksOrOptions) ?
91
+ [ tasksOrOptions, maybeOptions ] :
92
+ typeof tasksOrOptions === 'object' ?
93
+ [ [ 'default' ], tasksOrOptions ] :
94
+ [ [ 'default' ], {} ]
95
+
96
+ if (tasks.length === 0) tasks.push('default')
97
+
98
+ const { coverageDir, forceModule, ...props } = options
99
+ const forkOptions = { coverageDir, forceModule }
100
+
101
+ const context = requireContext()
102
+ const file = context.resolve(buildFile)
103
+ const dir = getAbsoluteParent(file)
104
+ const files = Files.builder(dir).add(file).build()
105
+
106
+ return new RunBuild(tasks, props, forkOptions)
107
+ .pipe(files, context)
108
+ .then(() => void 0)
109
+ }
110
+
59
111
  /**
60
112
  * Recursively remove the specified directory _**(use with care)**_.
61
113
  */
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export interface Pipe extends Promise<Files> {
28
28
 
29
29
  // Submodule exports (our package.json exports)
30
30
  export * as asserts from './asserts'
31
+ export * as async from './async'
31
32
  export * as files from './files'
32
33
  export * as fork from './fork'
33
34
  export * as fs from './fs'
@@ -37,7 +38,7 @@ export * as pipe from './pipe'
37
38
  export * as utils from './utils'
38
39
 
39
40
  // Individual utilities
40
- export { log, $ms, $p, $t, $blu, $cyn, $grn, $gry, $mgt, $red, $und, $wht, $ylw } from './logging'
41
+ export { banner, log, $ms, $p, $t, $blu, $cyn, $grn, $gry, $mgt, $red, $und, $wht, $ylw } from './logging'
41
42
  export { assert, fail, BuildFailure } from './asserts'
42
43
 
43
44
  // Our minimal exports
@@ -42,8 +42,8 @@ export type LogEmitter = (options: LogEmitterOptions, args: any[]) => void
42
42
 
43
43
  /** Emit in full colors! */
44
44
  export const emitColor: LogEmitter = (options: LogEmitterOptions, args: any[]): void => {
45
- const { taskName, level, prefix, indent } = options
46
- const logPrefix = prefix ? prefix : indent ? ''.padStart(indent * _indentSize) : ''
45
+ const { taskName, level, prefix = '', indent = 0 } = options
46
+ const logPrefix = ''.padStart(indent * _indentSize) + prefix
47
47
 
48
48
  /* Prefixes, to prepend at the beginning of each line */
49
49
  const prefixes: string[] = []
@@ -85,8 +85,8 @@ export const emitColor: LogEmitter = (options: LogEmitterOptions, args: any[]):
85
85
 
86
86
  /** Emit in plain text! (no colors) */
87
87
  export const emitPlain: LogEmitter = (options: LogEmitterOptions, args: any[]): void => {
88
- const { taskName, level, prefix, indent } = options
89
- const logPrefix = prefix ? prefix : indent ? ''.padStart(indent * _indentSize) : ''
88
+ const { taskName, level, prefix = '', indent = 0 } = options
89
+ const logPrefix = ''.padStart(indent * _indentSize) + prefix
90
90
 
91
91
  const prefixes: string[] = []
92
92