@plugjs/plug 0.0.15 → 0.0.17

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 (127) hide show
  1. package/dist/assert.cjs +18 -38
  2. package/dist/assert.cjs.map +1 -1
  3. package/dist/assert.d.ts +2 -16
  4. package/dist/assert.mjs +17 -32
  5. package/dist/assert.mjs.map +1 -1
  6. package/dist/async.cjs +2 -2
  7. package/dist/async.cjs.map +1 -1
  8. package/dist/async.d.ts +1 -1
  9. package/dist/async.mjs +2 -2
  10. package/dist/async.mjs.map +1 -1
  11. package/dist/build.cjs +20 -72
  12. package/dist/build.cjs.map +2 -2
  13. package/dist/build.d.ts +5 -3
  14. package/dist/build.mjs +22 -74
  15. package/dist/build.mjs.map +2 -2
  16. package/dist/failure.cjs +48 -0
  17. package/dist/failure.cjs.map +6 -0
  18. package/dist/failure.d.ts +13 -0
  19. package/dist/failure.mjs +22 -0
  20. package/dist/failure.mjs.map +6 -0
  21. package/dist/fork.cjs +21 -10
  22. package/dist/fork.cjs.map +1 -1
  23. package/dist/fork.d.ts +19 -1
  24. package/dist/fork.mjs +21 -11
  25. package/dist/fork.mjs.map +1 -1
  26. package/dist/helpers.cjs +8 -40
  27. package/dist/helpers.cjs.map +1 -1
  28. package/dist/helpers.d.ts +23 -3
  29. package/dist/helpers.mjs +8 -40
  30. package/dist/helpers.mjs.map +1 -1
  31. package/dist/index.cjs +16 -0
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.ts +3 -1
  34. package/dist/index.mjs +7 -0
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/log/logger.cjs +15 -12
  37. package/dist/log/logger.cjs.map +1 -1
  38. package/dist/log/logger.d.ts +2 -0
  39. package/dist/log/logger.mjs +15 -12
  40. package/dist/log/logger.mjs.map +1 -1
  41. package/dist/log/report.cjs +7 -4
  42. package/dist/log/report.cjs.map +1 -1
  43. package/dist/log/report.mjs +7 -4
  44. package/dist/log/report.mjs.map +1 -1
  45. package/dist/log.cjs +3 -0
  46. package/dist/log.cjs.map +1 -1
  47. package/dist/log.mjs +3 -0
  48. package/dist/log.mjs.map +1 -1
  49. package/dist/pipe.cjs +56 -14
  50. package/dist/pipe.cjs.map +1 -1
  51. package/dist/pipe.d.ts +42 -28
  52. package/dist/pipe.mjs +60 -13
  53. package/dist/pipe.mjs.map +1 -1
  54. package/dist/plugs/copy.cjs +1 -1
  55. package/dist/plugs/copy.cjs.map +1 -1
  56. package/dist/plugs/copy.mjs +1 -1
  57. package/dist/plugs/copy.mjs.map +1 -1
  58. package/dist/plugs/coverage.cjs +1 -2
  59. package/dist/plugs/coverage.cjs.map +1 -1
  60. package/dist/plugs/coverage.mjs +1 -2
  61. package/dist/plugs/coverage.mjs.map +1 -1
  62. package/dist/plugs/debug.cjs +6 -1
  63. package/dist/plugs/debug.cjs.map +1 -1
  64. package/dist/plugs/debug.d.ts +1 -1
  65. package/dist/plugs/debug.mjs +7 -2
  66. package/dist/plugs/debug.mjs.map +1 -1
  67. package/dist/plugs/esbuild.mjs.map +1 -1
  68. package/dist/plugs/eslint/runner.cjs +2 -1
  69. package/dist/plugs/eslint/runner.cjs.map +1 -1
  70. package/dist/plugs/eslint/runner.d.ts +1 -1
  71. package/dist/plugs/eslint/runner.mjs +3 -2
  72. package/dist/plugs/eslint/runner.mjs.map +1 -1
  73. package/dist/plugs/eslint.cjs +2 -2
  74. package/dist/plugs/eslint.cjs.map +1 -1
  75. package/dist/plugs/eslint.mjs +1 -1
  76. package/dist/plugs/exec.cjs +1 -1
  77. package/dist/plugs/exec.cjs.map +1 -1
  78. package/dist/plugs/exec.mjs +1 -1
  79. package/dist/plugs/exec.mjs.map +1 -1
  80. package/dist/plugs/mocha/runner.cjs +2 -2
  81. package/dist/plugs/mocha/runner.cjs.map +1 -1
  82. package/dist/plugs/mocha/runner.d.ts +1 -1
  83. package/dist/plugs/mocha/runner.mjs +2 -2
  84. package/dist/plugs/mocha/runner.mjs.map +1 -1
  85. package/dist/plugs/mocha.cjs +2 -2
  86. package/dist/plugs/mocha.cjs.map +1 -1
  87. package/dist/plugs/mocha.mjs +1 -1
  88. package/dist/plugs/rmf.mjs.map +1 -1
  89. package/dist/plugs/tsc/report.cjs.map +1 -1
  90. package/dist/plugs/tsc/report.mjs.map +1 -1
  91. package/dist/plugs/tsc/runner.cjs +3 -6
  92. package/dist/plugs/tsc/runner.cjs.map +2 -2
  93. package/dist/plugs/tsc/runner.mjs +4 -7
  94. package/dist/plugs/tsc/runner.mjs.map +2 -2
  95. package/dist/plugs/tsc.cjs +2 -2
  96. package/dist/plugs/tsc.cjs.map +1 -1
  97. package/dist/plugs/tsc.mjs +1 -1
  98. package/dist/types.d.ts +7 -8
  99. package/dist/utils/asyncfs.cjs.map +1 -1
  100. package/dist/utils/asyncfs.mjs.map +1 -1
  101. package/extra/cli.mjs +1 -1
  102. package/package.json +14 -13
  103. package/src/assert.ts +20 -34
  104. package/src/async.ts +3 -3
  105. package/src/failure.ts +31 -0
  106. package/src/fork.ts +43 -10
  107. package/src/helpers.ts +35 -56
  108. package/src/index.ts +4 -3
  109. package/src/log/logger.ts +22 -16
  110. package/src/log/report.ts +7 -3
  111. package/src/log.ts +4 -0
  112. package/src/pipe.ts +147 -44
  113. package/src/plugs/copy.ts +2 -2
  114. package/src/plugs/coverage/report.ts +1 -1
  115. package/src/plugs/coverage.ts +2 -3
  116. package/src/plugs/debug.ts +11 -4
  117. package/src/plugs/esbuild.ts +1 -1
  118. package/src/plugs/eslint/runner.ts +4 -3
  119. package/src/plugs/eslint.ts +1 -1
  120. package/src/plugs/exec.ts +1 -1
  121. package/src/plugs/mocha/runner.ts +3 -3
  122. package/src/plugs/mocha.ts +1 -1
  123. package/src/plugs/rmf.ts +1 -1
  124. package/src/plugs/tsc/runner.ts +5 -8
  125. package/src/plugs/tsc.ts +1 -1
  126. package/src/types.ts +8 -15
  127. package/src/utils/asyncfs.ts +1 -0
package/src/helpers.ts CHANGED
@@ -2,37 +2,15 @@ import { assert } from './assert'
2
2
  import { requireContext } from './async'
3
3
  import { Files } from './files'
4
4
  import { $p, log } from './log'
5
- import { AbsolutePath, commonPath, getCurrentWorkingDirectory, resolveDirectory, resolveFile } from './paths'
6
- import { Context, Pipe, Plug, PlugFunction, PlugResult } from './pipe'
5
+ import { AbsolutePath, getCurrentWorkingDirectory, resolveDirectory, resolveFile } from './paths'
6
+ import { Pipe } from './pipe'
7
7
  import { rm } from './utils/asyncfs'
8
8
  import { ParseOptions, parseOptions } from './utils/options'
9
9
  import { walk, WalkOptions } from './utils/walk'
10
10
 
11
- class PipeImpl extends Pipe implements Pipe {
12
- constructor(private readonly _start: (context: Context) => Promise<Files>) {
13
- super()
14
- }
15
-
16
- plug(plug: Plug<Files>): Pipe
17
- plug(plug: PlugFunction<Files>): Pipe
18
- plug(plug: Plug<void | undefined>): Promise<undefined>
19
- plug(plug: PlugFunction<void | undefined>): Promise<undefined>
20
- plug(arg: Plug<PlugResult> | PlugFunction<PlugResult>): Pipe | Promise<undefined> {
21
- const plug = typeof arg === 'function' ? { pipe: arg } : arg
22
-
23
- const parent = this
24
- return new PipeImpl(async (context: Context): Promise<Files> => {
25
- const files = await parent._start(context)
26
- const files2 = await plug.pipe(files, context)
27
- assert(files2, 'Unable to extend pipe (part tres)')
28
- return files2
29
- })
30
- }
31
-
32
- async run(): Promise<Files> {
33
- return this._start(requireContext())
34
- }
35
- }
11
+ /* ========================================================================== *
12
+ * EXTERNAL HELPERS *
13
+ * ========================================================================== */
36
14
 
37
15
  /** The {@link FindOptions} interface defines the options for finding files. */
38
16
  export interface FindOptions extends WalkOptions {
@@ -40,16 +18,20 @@ export interface FindOptions extends WalkOptions {
40
18
  directory?: string
41
19
  }
42
20
 
43
-
21
+ /** Find files in the current directory using the specified _glob_. */
44
22
  export function find(glob: string): Pipe
23
+ /** Find files in the current directory using the specified _globs_. */
45
24
  export function find(glob: string, ...globs: string[]): Pipe
25
+ /** Find files using the specified _glob_ and {@link FindOptions | options}. */
46
26
  export function find(glob: string, options: FindOptions): Pipe
27
+ /** Find files using the specified _globs_ and {@link FindOptions | options}. */
47
28
  export function find(glob: string, ...extra: [...globs: string[], options: FindOptions]): Pipe
48
-
29
+ /* Overload */
49
30
  export function find(...args: ParseOptions<FindOptions>): Pipe {
50
31
  const { params: globs, options } = parseOptions(args, {})
51
32
 
52
- return new PipeImpl(async (context: Context): Promise<Files> => {
33
+ const context = requireContext()
34
+ return new Pipe(context, Promise.resolve().then(async () => {
53
35
  const directory = options.directory ?
54
36
  context.resolve(options.directory) :
55
37
  getCurrentWorkingDirectory()
@@ -60,30 +42,7 @@ export function find(...args: ParseOptions<FindOptions>): Pipe {
60
42
  }
61
43
 
62
44
  return builder.build()
63
- })
64
- }
65
-
66
- export function merge(...pipes: Pipe[]): Pipe {
67
- return new PipeImpl(async (): Promise<Files> => {
68
- if (pipes.length === 0) return Files.builder(getCurrentWorkingDirectory()).build()
69
-
70
- const results: Files[] = []
71
-
72
- for (const pipe of pipes) {
73
- const result = await pipe.run()
74
- assert(result, 'Pipe did not return a Files result')
75
- results.push(result)
76
- }
77
-
78
- const [ first, ...others ] = results
79
-
80
- const firstDir = first.directory
81
- const otherDirs = others.map((f) => f.directory)
82
-
83
- const directory = commonPath(firstDir, ...otherDirs)
84
-
85
- return Files.builder(directory).merge(first, ...others).build()
86
- })
45
+ }))
87
46
  }
88
47
 
89
48
  /**
@@ -108,13 +67,33 @@ export async function rmrf(directory: string): Promise<void> {
108
67
  await rm(dir, { recursive: true })
109
68
  }
110
69
 
111
- /** Return an absolute path of the file if it exist on disk */
70
+ /**
71
+ * Resolve a (set of) path(s) into an {@link AbsolutePath}.
72
+ *
73
+ * If the path (or first component thereof) starts with `@...`, then the
74
+ * resolved path will be relative to the directory containing the build file
75
+ * where the current task was defined, otherwise it will be relative to the
76
+ * current working directory.
77
+ */
78
+ export function resolve(...paths: [ string, ...string[] ]): AbsolutePath {
79
+ return requireContext().resolve(...paths)
80
+ }
81
+
82
+ /**
83
+ * Return an absolute path of the file if it exist on disk.
84
+ *
85
+ * See the comments on {@link resolve} to understand how paths are resolved.
86
+ */
112
87
  export function isFile(...paths: [ string, ...string[] ]): AbsolutePath | undefined {
113
88
  const path = requireContext().resolve(...paths)
114
89
  return resolveFile(path)
115
90
  }
116
91
 
117
- /** Return an absolute path of the file if it exist on disk */
92
+ /**
93
+ * Return an absolute path of the directory if it exist on disk.
94
+ *
95
+ * See the comments on {@link resolve} to understand how paths are resolved.
96
+ */
118
97
  export function isDirectory(...paths: [ string, ...string[] ]): AbsolutePath | undefined {
119
98
  const path = requireContext().resolve(...paths)
120
99
  return resolveDirectory(path)
package/src/index.ts CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  export type { AbsolutePath } from './paths'
4
4
  export type { Files, FilesBuilder } from './files'
5
- export type { Pipe } from './pipe'
5
+
6
+ export { BuildFailure } from './failure'
7
+ export { Pipe } from './pipe'
6
8
 
7
9
  // Our minimal exports
8
10
  export * from './assert'
9
11
  export * from './build'
12
+ export * from './fork'
10
13
  export * from './helpers'
11
14
  export * from './log'
12
-
13
- // PlugJS types
14
15
  export * from './plugs'
15
16
  export * from './types'
package/src/log/logger.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isBuildError, isBuildFailure } from '../assert'
1
+ import { BuildFailure, isBuildFailure } from '../failure'
2
2
  import { emitColor, emitPlain, LogEmitter } from './emit'
3
3
  import { DEBUG, ERROR, INFO, LogLevel, NOTICE, TRACE, WARN } from './levels'
4
4
  import { logOptions } from './options'
@@ -34,6 +34,8 @@ export interface Log {
34
34
  warn(...args: [ any, ...any ]): this
35
35
  /** Log an `ERROR` message */
36
36
  error(...args: [ any, ...any ]): this
37
+ /** Log an `ERROR` message and fail the build */
38
+ fail(...args: [ any, ...any ]): never
37
39
  }
38
40
 
39
41
  /** A {@link Logger} extends the basic {@link Log} adding some state. */
@@ -83,9 +85,18 @@ class LoggerImpl implements Logger {
83
85
  private _emit(level: LogLevel, args: [ any, ...any ]): this {
84
86
  if (this._level > level) return this
85
87
 
86
- // Filter out build failures (they are not to be logged)
87
- const params = args.filter((arg) => ! isBuildFailure(arg))
88
- if (params.length === 0) return this // no logging only build failures
88
+ // Filter out build failures that were already logged
89
+ const params = args.filter((arg) => {
90
+ if (isBuildFailure(arg)) {
91
+ if (arg.logged) return false // already logged? skip!
92
+ return arg.logged = true // set logged to true and log!
93
+ } else {
94
+ return true
95
+ }
96
+ })
97
+
98
+ // If there's nothing left to log, then we're done
99
+ if (params.length === 0) return this
89
100
 
90
101
  // Prepare our options for logging
91
102
  const options = { level, taskName: this._task, indent: this._indent }
@@ -98,18 +109,8 @@ class LoggerImpl implements Logger {
98
109
  this._stack.splice(0)
99
110
  }
100
111
 
101
- // Print all `BuildError`s _before_ any other entry
102
- const remaining = params.filter((arg) => {
103
- if (isBuildError(arg)) {
104
- this._emitter({ ...options, level: ERROR }, [ arg.message ])
105
- return false
106
- } else {
107
- return true
108
- }
109
- })
110
-
111
- // If we have any leftovers, dump them out, too!
112
- if (remaining) this._emitter(options, remaining)
112
+ // Emit our log lines and return
113
+ this._emitter(options, params)
113
114
  return this
114
115
  }
115
116
 
@@ -145,6 +146,11 @@ class LoggerImpl implements Logger {
145
146
  return this._emit(ERROR, args)
146
147
  }
147
148
 
149
+ fail(...args: [ any, ...any ]): never {
150
+ this._emit(ERROR, args)
151
+ throw new BuildFailure({ logged: true })
152
+ }
153
+
148
154
  enter(): this
149
155
  enter(level: LogLevel, message: string): this
150
156
  enter(...args: [] | [ level: LogLevel, message: string ]): this {
package/src/log/report.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { fail, failure } from '../assert'
1
+ import { BuildFailure } from '../failure'
2
2
  import { AbsolutePath } from '../paths'
3
3
  import { readFile } from '../utils/asyncfs'
4
4
  import { $blu, $cyn, $gry, $red, $und, $wht, $ylw } from './colors'
@@ -191,7 +191,11 @@ export class ReportImpl implements Report {
191
191
  [ ...record.message.map((msg) => msg.split('\n')).flat(1) ] :
192
192
  record.message.split('\n')
193
193
  messages = messages.filter((message) => !! message)
194
- if (! messages.length) fail('No message for report record')
194
+ if (! messages.length) {
195
+ const options = { taskName: this._task, level: ERROR }
196
+ this._emitter(options, [ 'No message for report record' ])
197
+ throw new BuildFailure({ logged: true })
198
+ }
195
199
 
196
200
  const level = record.level
197
201
  const file = record.file
@@ -263,7 +267,7 @@ export class ReportImpl implements Report {
263
267
  done(showSources?: boolean | undefined): void {
264
268
  if (showSources == null) showSources = logOptions.showSources
265
269
  if (! this.empty) this._emit(showSources)
266
- if (this.errors) throw failure()
270
+ if (this.errors) throw new BuildFailure({ logged: true })
267
271
  }
268
272
 
269
273
  private _emit(showSources: boolean): this {
package/src/log.ts CHANGED
@@ -58,6 +58,10 @@ export const log: LogFunction = ((): LogFunction => {
58
58
  logger().error(...args)
59
59
  return wrapper
60
60
  },
61
+
62
+ fail(...args: [ any, ...any ]): never {
63
+ throw logger().fail(...args) // fail() returns never but ?!?!?!?!?
64
+ },
61
65
  }
62
66
 
63
67
  /* Create a function that will default logging to "NOTICE" */
package/src/pipe.ts CHANGED
@@ -1,9 +1,17 @@
1
- import type { Files } from './files'
2
- import { getLogger, Logger } from './log'
3
- import { AbsolutePath, getAbsoluteParent, getCurrentWorkingDirectory, resolveAbsolutePath } from './paths'
1
+ import {
2
+ AbsolutePath,
3
+ commonPath,
4
+ getAbsoluteParent,
5
+ getCurrentWorkingDirectory,
6
+ resolveAbsolutePath,
7
+ } from './paths'
4
8
 
5
- import { ForkingPlug } from './fork'
6
9
  import { sep } from 'path'
10
+ import { assert, assertPromises } from './assert'
11
+ import { requireContext } from './async'
12
+ import { Files } from './files'
13
+ import { getLogger, Logger } from './log'
14
+ import { Result } from './types'
7
15
 
8
16
  /* ========================================================================== *
9
17
  * PLUGS *
@@ -50,6 +58,14 @@ export class Context {
50
58
  this.log = getLogger(taskName)
51
59
  }
52
60
 
61
+ /**
62
+ * Resolve a (set of) path(s) in this {@link Context}.
63
+ *
64
+ * If the path (or first component thereof) starts with `@...`, then the
65
+ * resolved path will be relative to the directory containing the build file
66
+ * where the current task was defined, otherwise it will be relative to the
67
+ * current working directory.
68
+ */
53
69
  resolve(path: string, ...paths: string[]): AbsolutePath {
54
70
  // Paths starting with "@" are relative to the build file directory
55
71
  if (path && path.startsWith('@')) {
@@ -70,6 +86,25 @@ export class Context {
70
86
  * PIPES *
71
87
  * ========================================================================== */
72
88
 
89
+ /**
90
+ * In pipe chains, we want to keep track of the _leaf_ `Files` promises (that
91
+ * is, when a derived pipe is created calling `plug` we want to track only the
92
+ * new, derived, promise).
93
+ *
94
+ * We key these _leaf_ promises by _context_ (with a WeakMap), and those will
95
+ * be awaited at the end of the task.
96
+ */
97
+ const contextPromises = new WeakMap<Context, Set<Promise<Result>>>()
98
+
99
+ export function getContextPromises(context: Context): Set<Promise<Result>> {
100
+ let promises = contextPromises.get(context)
101
+ if (! promises) {
102
+ promises = new Set<Promise<Files>>()
103
+ contextPromises.set(context, promises)
104
+ }
105
+ return promises
106
+ }
107
+
73
108
  /**
74
109
  * A class that will be extended by {@link Pipe} where {@link install} will
75
110
  * add prototype properties from installed {@link Plug}s
@@ -79,16 +114,116 @@ abstract class PipeProto {
79
114
  }
80
115
 
81
116
  /**
82
- * The {@link Pipe} abstract defines processing pipeline where multiple
117
+ * The {@link Pipe} class defines processing pipeline where multiple
83
118
  * {@link Plug}s can transform lists of {@link Files}.
84
119
  */
85
- export abstract class Pipe extends PipeProto {
86
- abstract plug(plug: Plug<Files>): Pipe
87
- abstract plug(plug: PlugFunction<Files>): Pipe
88
- abstract plug(plug: Plug<void | undefined>): Promise<undefined>
89
- abstract plug(plug: PlugFunction<void | undefined>): Promise<undefined>
120
+ export class Pipe extends PipeProto implements Promise<Files> {
121
+ readonly [Symbol.toStringTag] = 'Pipe'
122
+
123
+ constructor(
124
+ private readonly _context: Context,
125
+ private readonly _promise: Promise<Result>,
126
+ ) {
127
+ super()
128
+
129
+ // New "Pipe", remember the promise!
130
+ getContextPromises(_context).add(_promise)
131
+ }
132
+
133
+ /* ------------------------------------------------------------------------ *
134
+ * Promise implementation *
135
+ * ------------------------------------------------------------------------ *
136
+ * From a _types_ point of view, the `Pipe` implements a `Promise<Files>` *
137
+ * (because only when plugging the correct `Plug` the correct value are *
138
+ * returned). *
139
+ * *
140
+ * Whether to return (as a type) another `Pipe` or a `Promise<undefined>` *
141
+ * is determined by the type of the `plug` parameter below. *
142
+ * *
143
+ * That said, in practice, a `Pipe` implements `Promise<Files | undefined>` *
144
+ * because the result of the plug is _eventually_ computed asynchronously *
145
+ * while `plug` returns immediately.
146
+ * *
147
+ * So, all those "as whatever" below are kind-of-legit... *
148
+ * ------------------------------------------------------------------------ */
149
+
150
+ then<R1 = Files, R2 = never>(
151
+ onfulfilled?: ((value: Files) => R1 | PromiseLike<R1>) | null | undefined,
152
+ onrejected?: ((reason: any) => R2 | PromiseLike<R2>) | null | undefined,
153
+ ): Promise<R1 | R2> {
154
+ // We are delegating the handling of this promise to the caller
155
+ getContextPromises(this._context).delete(this._promise)
156
+ return this._promise.then(onfulfilled as (value: Result) => R1 | PromiseLike<R1>, onrejected)
157
+ }
158
+
159
+ catch<R = never>(
160
+ onrejected?: ((reason: any) => R | PromiseLike<R>) | null | undefined,
161
+ ): Promise<Files | R> {
162
+ // We are delegating the handling of this promise to the caller
163
+ getContextPromises(this._context).delete(this._promise)
164
+ return this._promise.catch(onrejected) as Promise<Files | R>
165
+ }
166
+
167
+ finally(onfinally?: (() => void) | null | undefined): Promise<Files> {
168
+ // We are delegating the handling of this promise to the caller
169
+ getContextPromises(this._context).delete(this._promise)
170
+ return this._promise.finally(onfinally) as Promise<Files>
171
+ }
172
+
173
+ /* ------------------------------------------------------------------------ *
174
+ * Pipe implementation *
175
+ * ------------------------------------------------------------------------ */
176
+
177
+ plug(plug: Plug<Files>): Pipe
178
+ plug(plug: PlugFunction<Files>): Pipe
179
+ plug(plug: Plug<void | undefined>): Promise<undefined>
180
+ plug(plug: PlugFunction<void | undefined>): Promise<undefined>
181
+ plug(arg: Plug<PlugResult> | PlugFunction<PlugResult>): Pipe | Promise<undefined> {
182
+ const plug = typeof arg === 'function' ? { pipe: arg } : arg
183
+
184
+ // We are creating a new "leaf" Pipe, we can forget our promise
185
+ getContextPromises(this._context).delete(this._promise)
186
+
187
+ // Create and return the new Pipe
188
+ return new Pipe(this._context, this._promise.then(async (result) => {
189
+ assert(result, 'Unable to extend pipe')
190
+ const result2 = await plug.pipe(result, this._context)
191
+ return result2 || undefined
192
+ }))
193
+ }
194
+
195
+ /**
196
+ * Merge the results of several {@link Pipe}s into a single one.
197
+ *
198
+ * Merging is performed _in parallel_. When serial execution is to be desired,
199
+ * we can merge the awaited _result_ of the {@link Pipe}.
200
+ *
201
+ * For example:
202
+ *
203
+ * ```
204
+ * const pipe: Pipe = merge([
205
+ * await this.anotherTask1(),
206
+ * await this.anotherTask2(),
207
+ * ])
208
+ * ```
209
+ */
210
+ static merge(pipes: (Pipe | Files | Promise<Files>)[]): Pipe {
211
+ const context = requireContext()
212
+ return new Pipe(context, Promise.resolve().then(async () => {
213
+ // No pipes? Just send off an empty pipe...
214
+ if (pipes.length === 0) return Files.builder(getCurrentWorkingDirectory()).build()
215
+
216
+ // Await for all pipes / files / files promises
217
+ const results = await assertPromises<Files>(pipes, 'Error in pipe while merging')
90
218
 
91
- abstract run(): Promise<Files>
219
+ // Find the common directory between all the Files instances
220
+ const [ firstDir, ...otherDirs ] = results.map((f) => f.directory)
221
+ const directory = commonPath(firstDir, ...otherDirs)
222
+
223
+ // Build our new files instance merging all the results
224
+ return Files.builder(directory).merge(...results).build()
225
+ }))
226
+ }
92
227
  }
93
228
 
94
229
  /* ========================================================================== *
@@ -96,7 +231,7 @@ export abstract class Pipe extends PipeProto {
96
231
  * ========================================================================== */
97
232
 
98
233
  /** The names which can be installed as direct plugs. */
99
- export type PlugName = string & Exclude<keyof Pipe, 'plug' | 'run'>
234
+ export type PlugName = string & Exclude<keyof Pipe, 'plug' | keyof Promise<any>>
100
235
 
101
236
  /** The parameters of the plug extension with the given name */
102
237
  export type PipeParameters<Name extends PlugName> = PipeOverloads<Name>['args']
@@ -210,35 +345,3 @@ export function install<
210
345
  /* Inject the create function in the Pipe's prototype */
211
346
  Object.defineProperty(PipeProto.prototype, name, { value: plug })
212
347
  }
213
-
214
- /**
215
- * Install a _forking_ {@link Plug} in the {@link Pipe}, in other words
216
- * execute the plug in a separate process.
217
- *
218
- * As a contract, if the _last non-null_ parameter of the constructor is an
219
- * object and contains the key `coverageDir`, the process will be forked with
220
- * the approptiately resolved `NODE_V8_COVERAGE` environment variable.
221
- *
222
- * Also, forking plugs require some special attention:
223
- *
224
- * * plug functions are not supported, only classes implementing the
225
- * {@link Plug} interface can be used with this.
226
- *
227
- * * the class itself _MUST_ be exported as the _default_ export for the
228
- * `scriptFile` specified below. This is to simplify interoperability between
229
- * CommonJS and ESM modules as we use dynamic `import(...)` statements.
230
- */
231
- export function installForking<Name extends PlugName>(
232
- plugName: Name,
233
- scriptFile: AbsolutePath,
234
- ): void {
235
- /** Extend out our ForkingPlug below */
236
- const ctor = class extends ForkingPlug {
237
- constructor(...args: any[]) {
238
- super(scriptFile, args)
239
- }
240
- } as unknown as PlugConstructor<Name>
241
-
242
- /** Install the plug in */
243
- install(plugName, ctor)
244
- }
package/src/plugs/copy.ts CHANGED
@@ -2,7 +2,7 @@ import { assert } from '../assert'
2
2
  import { Files } from '../files'
3
3
  import { $p } from '../log'
4
4
  import { assertAbsolutePath, getAbsoluteParent, resolveAbsolutePath } from '../paths'
5
- import { install, PipeParameters, Plug, Context } from '../pipe'
5
+ import { Context, install, PipeParameters, Plug } from '../pipe'
6
6
  import { chmod, copyFile, fsConstants, mkdir } from '../utils/asyncfs'
7
7
 
8
8
  /** Options for copying files */
@@ -95,7 +95,7 @@ install('copy', class Copy implements Plug<Files> {
95
95
  }
96
96
 
97
97
  /* Record this file */
98
- builder.add(relative)
98
+ builder.add(target)
99
99
  }
100
100
 
101
101
  const result = builder.build()
@@ -10,7 +10,7 @@ import {
10
10
  isTSTypeReference,
11
11
  isTypeScript,
12
12
  Node,
13
- VISITOR_KEYS
13
+ VISITOR_KEYS,
14
14
  } from '@babel/types'
15
15
 
16
16
  import { parse } from '@babel/parser'
@@ -1,10 +1,9 @@
1
1
  import { html, initFunction } from '@plugjs/cov8-html'
2
2
  import { sep } from 'node:path'
3
- import { fail } from '../assert'
4
3
  import { Files } from '../files'
5
4
  import { $gry, $ms, $p, $red, $ylw, ERROR, NOTICE, WARN } from '../log'
6
5
  import { AbsolutePath, resolveAbsolutePath } from '../paths'
7
- import { install, PipeParameters, Plug, Context } from '../pipe'
6
+ import { Context, install, PipeParameters, Plug } from '../pipe'
8
7
  import { walk } from '../utils/walk'
9
8
  import { createAnalyser, SourceMapBias } from './coverage/analysis'
10
9
  import { coverageReport, CoverageResult } from './coverage/report'
@@ -79,7 +78,7 @@ install('coverage', class Coverage implements Plug<Files | undefined> {
79
78
  }
80
79
 
81
80
  if (coverageFiles.length === 0) {
82
- fail(`No coverage files found in ${$p(coverageDir)}`)
81
+ throw context.log.fail(`No coverage files found in ${$p(coverageDir)}`)
83
82
  }
84
83
 
85
84
  const sourceFiles = [ ...files.absolutePaths() ]
@@ -1,11 +1,11 @@
1
1
  import { Files } from '../files'
2
- import { $gry, $p, $und } from '../log'
3
- import { install, Plug, Context } from '../pipe'
2
+ import { $gry, $p, $und, $ylw } from '../log'
3
+ import { Context, install, PipeParameters, Plug } from '../pipe'
4
4
 
5
5
  declare module '../pipe' {
6
6
  export interface Pipe {
7
7
  /** Log some info about the current {@link Files} being passed around. */
8
- debug(): Pipe
8
+ debug(title?: string): Pipe
9
9
  }
10
10
  }
11
11
 
@@ -15,8 +15,15 @@ declare module '../pipe' {
15
15
 
16
16
  /** Writes some info about the current {@link Files} being passed around. */
17
17
  install('debug', class Debug implements Plug<Files> {
18
+ private readonly _title: string
19
+
20
+ constructor(...args: PipeParameters<'debug'>) {
21
+ const [ title = 'Debugging' ] = args
22
+ this._title = title
23
+ }
24
+
18
25
  async pipe(files: Files, context: Context): Promise<Files> {
19
- context.log.notice('Debugging', files.length, 'files')
26
+ context.log.notice(this._title, `${$gry('(')}${$ylw(files.length)} ${$gry('files)')}`)
20
27
  context.log.notice('- base dir:', $p(context.resolve('@')))
21
28
  context.log.notice('- build file dir:', $p(context.resolve('.')))
22
29
  context.log.notice('- files dir:', $p(files.directory))
@@ -4,7 +4,7 @@ import { assert } from '../assert'
4
4
  import { Files, FilesBuilder } from '../files'
5
5
  import { $p, ERROR, Logger, ReportLevel, ReportRecord, WARN } from '../log'
6
6
  import { AbsolutePath, getAbsoluteParent, resolveAbsolutePath } from '../paths'
7
- import { install, PipeParameters, Plug, Context } from '../pipe'
7
+ import { Context, install, PipeParameters, Plug } from '../pipe'
8
8
  import { readFile } from '../utils/asyncfs'
9
9
 
10
10
  export type ESBuildOptions = Omit<BuildOptions, 'absWorkingDir' | 'entryPoints' | 'watch'>
@@ -1,9 +1,10 @@
1
1
  import { ESLint as RealESLint } from 'eslint'
2
- import { assert, failure } from '../../assert'
2
+ import { assert } from '../../assert'
3
+ import { BuildFailure } from '../../failure'
3
4
  import { Files } from '../../files'
4
5
  import { $p, ERROR, NOTICE, WARN } from '../../log'
5
6
  import { getCurrentWorkingDirectory, resolveAbsolutePath, resolveDirectory, resolveFile } from '../../paths'
6
- import { PipeParameters, Plug, Context } from '../../pipe'
7
+ import { Context, PipeParameters, Plug } from '../../pipe'
7
8
  import { readFile } from '../../utils/asyncfs'
8
9
  import { ESLintOptions } from '../eslint'
9
10
 
@@ -59,7 +60,7 @@ export default class ESLint implements Plug<void> {
59
60
 
60
61
  /* In case of failures from promises, fail! */
61
62
  const { results, failures } = summary
62
- if (failures) throw failure() // already logged above
63
+ if (failures) throw new BuildFailure({ logged: true })
63
64
 
64
65
  /* Create our report */
65
66
  const report = context.log.report('ESLint Report')
@@ -1,5 +1,5 @@
1
+ import { installForking } from '../fork'
1
2
  import { requireResolve } from '../paths'
2
- import { installForking } from '../pipe'
3
3
 
4
4
  export interface ESLintOptions {
5
5
  /** ESLint's own _current working directory_, where config files are. */
package/src/plugs/exec.ts CHANGED
@@ -3,12 +3,12 @@ import reaadline from 'node:readline'
3
3
 
4
4
  import { spawn, SpawnOptions } from 'node:child_process'
5
5
  import { assert } from '../assert'
6
+ import { requireContext } from '../async'
6
7
  import { Files } from '../files'
7
8
  import { $p, logOptions } from '../log'
8
9
  import { AbsolutePath, getCurrentWorkingDirectory, resolveDirectory } from '../paths'
9
10
  import { Context, install, PipeParameters, Plug } from '../pipe'
10
11
  import { parseOptions } from '../utils/options'
11
- import { requireContext } from '../async'
12
12
 
13
13
  /** Options for executing scripts */
14
14
  export interface ExecOptions {
@@ -1,9 +1,9 @@
1
1
  import RealMocha from 'mocha' // Mocha types pollute the global scope!
2
2
 
3
- import { failure } from '../../assert'
3
+ import { BuildFailure } from '../../failure'
4
4
  import { Files } from '../../files'
5
5
  import { $wht, NOTICE } from '../../log'
6
- import { PipeParameters, Plug, Context } from '../../pipe'
6
+ import { Context, PipeParameters, Plug } from '../../pipe'
7
7
  import { MochaOptions } from '../mocha'
8
8
  import { logSymbol, PlugReporter } from './reporter'
9
9
 
@@ -40,7 +40,7 @@ export default class Mocha implements Plug<void> {
40
40
  return new Promise((resolve, reject) => {
41
41
  try {
42
42
  mocha.run((failures) => {
43
- if (failures) reject(failure())
43
+ if (failures) reject(new BuildFailure({ logged: true }))
44
44
  resolve(undefined)
45
45
  })
46
46
  } catch (error) {
@@ -1,5 +1,5 @@
1
+ import { installForking } from '../fork'
1
2
  import { requireResolve } from '../paths'
2
- import { installForking } from '../pipe'
3
3
 
4
4
  /** Options to construct our {@link Mocha} plug. */
5
5
  export interface MochaOptions {
package/src/plugs/rmf.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Files } from '../files'
2
2
  import { $gry, $p } from '../log'
3
- import { install, PipeParameters, Plug, Context } from '../pipe'
3
+ import { Context, install, PipeParameters, Plug } from '../pipe'
4
4
  import { rm } from '../utils/asyncfs'
5
5
 
6
6
  declare module '../pipe' {