@plugjs/plug 0.0.16 → 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 (119) 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 -41
  12. package/dist/build.cjs.map +2 -2
  13. package/dist/build.d.ts +1 -1
  14. package/dist/build.mjs +22 -43
  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 +2 -2
  27. package/dist/helpers.cjs.map +1 -1
  28. package/dist/helpers.mjs +2 -2
  29. package/dist/helpers.mjs.map +1 -1
  30. package/dist/index.cjs +4 -0
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.mjs +3 -0
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/log/logger.cjs +15 -12
  36. package/dist/log/logger.cjs.map +1 -1
  37. package/dist/log/logger.d.ts +2 -0
  38. package/dist/log/logger.mjs +15 -12
  39. package/dist/log/logger.mjs.map +1 -1
  40. package/dist/log/report.cjs +7 -4
  41. package/dist/log/report.cjs.map +1 -1
  42. package/dist/log/report.mjs +7 -4
  43. package/dist/log/report.mjs.map +1 -1
  44. package/dist/log.cjs +3 -0
  45. package/dist/log.cjs.map +1 -1
  46. package/dist/log.mjs +3 -0
  47. package/dist/log.mjs.map +1 -1
  48. package/dist/pipe.cjs +40 -34
  49. package/dist/pipe.cjs.map +1 -1
  50. package/dist/pipe.d.ts +10 -25
  51. package/dist/pipe.mjs +39 -33
  52. package/dist/pipe.mjs.map +1 -1
  53. package/dist/plugs/copy.mjs.map +1 -1
  54. package/dist/plugs/coverage.cjs +2 -3
  55. package/dist/plugs/coverage.cjs.map +1 -1
  56. package/dist/plugs/coverage.mjs +2 -3
  57. package/dist/plugs/coverage.mjs.map +1 -1
  58. package/dist/plugs/debug.mjs.map +1 -1
  59. package/dist/plugs/esbuild.mjs.map +1 -1
  60. package/dist/plugs/eslint/runner.cjs +2 -1
  61. package/dist/plugs/eslint/runner.cjs.map +1 -1
  62. package/dist/plugs/eslint/runner.d.ts +1 -1
  63. package/dist/plugs/eslint/runner.mjs +3 -2
  64. package/dist/plugs/eslint/runner.mjs.map +1 -1
  65. package/dist/plugs/eslint.cjs +2 -2
  66. package/dist/plugs/eslint.cjs.map +1 -1
  67. package/dist/plugs/eslint.mjs +1 -1
  68. package/dist/plugs/exec.cjs +1 -1
  69. package/dist/plugs/exec.cjs.map +1 -1
  70. package/dist/plugs/exec.mjs +1 -1
  71. package/dist/plugs/exec.mjs.map +1 -1
  72. package/dist/plugs/mocha/runner.cjs +2 -2
  73. package/dist/plugs/mocha/runner.cjs.map +1 -1
  74. package/dist/plugs/mocha/runner.d.ts +1 -1
  75. package/dist/plugs/mocha/runner.mjs +2 -2
  76. package/dist/plugs/mocha/runner.mjs.map +1 -1
  77. package/dist/plugs/mocha.cjs +2 -2
  78. package/dist/plugs/mocha.cjs.map +1 -1
  79. package/dist/plugs/mocha.mjs +1 -1
  80. package/dist/plugs/rmf.mjs.map +1 -1
  81. package/dist/plugs/tsc/report.cjs.map +1 -1
  82. package/dist/plugs/tsc/report.mjs.map +1 -1
  83. package/dist/plugs/tsc/runner.cjs +3 -6
  84. package/dist/plugs/tsc/runner.cjs.map +2 -2
  85. package/dist/plugs/tsc/runner.mjs +4 -7
  86. package/dist/plugs/tsc/runner.mjs.map +2 -2
  87. package/dist/plugs/tsc.cjs +2 -2
  88. package/dist/plugs/tsc.cjs.map +1 -1
  89. package/dist/plugs/tsc.mjs +1 -1
  90. package/dist/types.d.ts +2 -2
  91. package/dist/utils/asyncfs.cjs.map +1 -1
  92. package/dist/utils/asyncfs.mjs.map +1 -1
  93. package/extra/cli.mjs +1 -1
  94. package/package.json +14 -12
  95. package/src/assert.ts +20 -34
  96. package/src/async.ts +3 -3
  97. package/src/failure.ts +31 -0
  98. package/src/fork.ts +43 -10
  99. package/src/helpers.ts +2 -2
  100. package/src/index.ts +3 -2
  101. package/src/log/logger.ts +22 -16
  102. package/src/log/report.ts +7 -3
  103. package/src/log.ts +4 -0
  104. package/src/pipe.ts +90 -59
  105. package/src/plugs/copy.ts +1 -1
  106. package/src/plugs/coverage/report.ts +1 -1
  107. package/src/plugs/coverage.ts +2 -3
  108. package/src/plugs/debug.ts +1 -1
  109. package/src/plugs/esbuild.ts +1 -1
  110. package/src/plugs/eslint/runner.ts +4 -3
  111. package/src/plugs/eslint.ts +1 -1
  112. package/src/plugs/exec.ts +1 -1
  113. package/src/plugs/mocha/runner.ts +3 -3
  114. package/src/plugs/mocha.ts +1 -1
  115. package/src/plugs/rmf.ts +1 -1
  116. package/src/plugs/tsc/runner.ts +5 -8
  117. package/src/plugs/tsc.ts +1 -1
  118. package/src/types.ts +3 -3
  119. package/src/utils/asyncfs.ts +1 -0
package/src/assert.ts CHANGED
@@ -2,46 +2,32 @@
2
2
  * BUILD FAILURES *
3
3
  * ========================================================================== */
4
4
 
5
- const buildError = Symbol.for('plugjs:buildError')
6
- const buildFailure = Symbol.for('plugjs:buildFailure')
5
+ import { BuildFailure } from './failure'
7
6
 
8
- /** Check if the specified argument is a {@link BuildError} */
9
- export function isBuildError(arg: any): arg is BuildError {
10
- return arg && arg[buildError] === buildError
11
- }
7
+ /** Await and assert that all specified promises were fulfilled */
8
+ export async function assertPromises<T>(promises: (T | Promise<T>)[], message: string): Promise<T[]> {
9
+ // Await for the settlement of all the promises
10
+ const settlements = await Promise.allSettled(promises)
12
11
 
13
- /** Check if the specified argument is a {@link BuildFailure} */
14
- export function isBuildFailure(arg: any): arg is BuildFailure {
15
- return arg && arg[buildFailure] === buildFailure
16
- }
12
+ // Separate the good from the bad...
13
+ const results: T[] = []
14
+ const failures = new Set<any>()
17
15
 
18
- /** An error produced in our build, with proper inspection for logging */
19
- export class BuildError extends Error {
20
- constructor(message: string) {
21
- super(message)
22
- Object.defineProperty(this, buildError, { value: buildError })
23
- }
24
- }
16
+ settlements.forEach((settlement) => {
17
+ if (settlement.status === 'fulfilled') {
18
+ results.push(settlement.value)
19
+ } else {
20
+ failures.add(settlement.reason)
21
+ }
22
+ })
23
+
24
+ // Check for errors and report/fail if anything happened
25
+ if (failures.size) throw new BuildFailure(message, [ ...failures ])
25
26
 
26
- /** A {@link BuildFailure} represents an error _already logged_ in our build. */
27
- export class BuildFailure extends Error {
28
- constructor() {
29
- super('Build Failure')
30
- Object.defineProperty(this, buildFailure, { value: buildFailure })
31
- }
27
+ return results
32
28
  }
33
29
 
34
30
  /** Asserts something as _truthy_ and fail the build if not */
35
31
  export function assert(assertion: any, message: string): asserts assertion {
36
- if (! assertion) throw new BuildError(message)
37
- }
38
-
39
- /** Throw a {@link BuildError} (an {@link Error} printed nicely) */
40
- export function fail(message: string): never {
41
- throw new BuildError(message)
42
- }
43
-
44
- /** Return a non-logged {@link BuildFailure} */
45
- export function failure(): BuildFailure {
46
- return new BuildFailure()
32
+ if (! assertion) throw new BuildFailure(message)
47
33
  }
package/src/async.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import type { Context } from './pipe'
2
+
1
3
  import { AsyncLocalStorage } from 'node:async_hooks'
2
- import { assert } from './assert'
3
- import { Context } from './pipe'
4
4
 
5
5
  /* ========================================================================== *
6
6
  * EXPORTED *
@@ -39,7 +39,7 @@ export function currentContext(): Context | undefined {
39
39
  */
40
40
  export function requireContext(): Context {
41
41
  const context = storage.getStore()
42
- assert(context, 'Unable to retrieve current context')
42
+ if (! context) throw new Error('Unable to retrieve current context')
43
43
  return context
44
44
  }
45
45
 
package/src/failure.ts ADDED
@@ -0,0 +1,31 @@
1
+ /** A symbol marking {@link BuildFailure} instances */
2
+ const buildFailure = Symbol.for('plugjs:buildFailure')
3
+
4
+ /** Check if the specified argument is a {@link BuildFailure} */
5
+ export function isBuildFailure(arg: any): arg is BuildFailure {
6
+ return arg && arg[buildFailure] === buildFailure
7
+ }
8
+
9
+ /** A {@link BuildFailure} represents an error _already logged_ in our build. */
10
+ export class BuildFailure extends Error {
11
+ readonly errors: readonly any[]
12
+ logged: boolean
13
+
14
+ /** Construct a {@link BuildFailure} that was already _logged_ (internal) */
15
+ constructor(options: { logged: true })
16
+ /** Construct a {@link BuildFailure} with a detail message */
17
+ constructor(message: string, errors?: any[])
18
+ // Constructor overload implementation
19
+ constructor(options: string | { logged: boolean } = 'Build Failure', errors: any[] = []) {
20
+ const { logged, message } =
21
+ typeof options === 'string' ?
22
+ { message: options, logged: false } :
23
+ { message: 'Build Failure', ...options }
24
+
25
+ super(message)
26
+ Error.captureStackTrace(this, BuildFailure)
27
+ Object.defineProperty(this, 'buildFailure', { value: buildFailure })
28
+ this.errors = errors.filter((e) => ! (isBuildFailure(e) && e.logged))
29
+ this.logged = logged
30
+ }
31
+ }
package/src/fork.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { fork } from 'node:child_process'
2
- import { assert, failure } from './assert'
2
+ import { assert } from './assert'
3
3
  import { runAsync } from './async'
4
+ import { BuildFailure } from './failure'
4
5
  import { Files } from './files'
5
6
  import { $gry, $p, LogOptions, logOptions } from './log'
6
7
  import { AbsolutePath, requireFilename, resolveFile } from './paths'
7
- import { Plug, PlugResult, Context } from './pipe'
8
+ import { Context, install, Plug, PlugName, PlugResult } from './pipe'
8
9
 
9
10
  /** Fork data, from parent to child process */
10
11
  export interface ForkData {
@@ -88,7 +89,7 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
88
89
 
89
90
  child.on('error', (error) => {
90
91
  context.log.error('Child process error', error)
91
- return done || reject(failure())
92
+ return done || reject(new BuildFailure({ logged: true }))
92
93
  })
93
94
 
94
95
  child.on('message', (message: ForkResult) => {
@@ -99,16 +100,16 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
99
100
  child.on('exit', (code, signal) => {
100
101
  if (signal) {
101
102
  context.log.error(`Child process exited with signal ${signal}`, $gry(`(pid=${child.pid})`))
102
- return done || reject(failure())
103
+ return done || reject(new BuildFailure({ logged: true }))
103
104
  } else if (code !== 0) {
104
105
  context.log.error(`Child process exited with code ${code}`, $gry(`(pid=${child.pid})`))
105
- return done || reject(failure())
106
+ return done || reject(new BuildFailure({ logged: true }))
106
107
  } else if (! result) {
107
108
  context.log.error('Child process exited with no result', $gry(`(pid=${child.pid})`))
108
- return done || reject(failure())
109
+ return done || reject(new BuildFailure({ logged: true }))
109
110
  } else if (result.failed) {
110
111
  // definitely logged on the child side
111
- return done || reject(failure())
112
+ return done || reject(new BuildFailure({ logged: true }))
112
113
  }
113
114
 
114
115
  /* We definitely have a successful result! */
@@ -122,16 +123,16 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
122
123
  const result = child.send(message, (error) => {
123
124
  if (error) {
124
125
  context.log.error('Error sending message to child process (callback failure)', error)
125
- reject(failure())
126
+ reject(new BuildFailure({ logged: true }))
126
127
  }
127
128
  })
128
129
  if (! result) {
129
130
  context.log.error('Error sending message to child process (send returned false)')
130
- reject(failure())
131
+ reject(new BuildFailure({ logged: true }))
131
132
  }
132
133
  } catch (error) {
133
134
  context.log.error('Error sending message to child process (exception caught)', error)
134
- reject(failure())
135
+ reject(new BuildFailure({ logged: true }))
135
136
  }
136
137
  }).finally(() => done = true)
137
138
  }
@@ -223,3 +224,35 @@ if ((process.argv[1] === requireFilename(__fileurl)) && (process.send)) {
223
224
  })
224
225
  })
225
226
  }
227
+
228
+ /**
229
+ * Install a _forking_ {@link Plug} in the {@link Pipe}, in other words
230
+ * execute the plug in a separate process.
231
+ *
232
+ * As a contract, if the _last non-null_ parameter of the constructor is an
233
+ * object and contains the key `coverageDir`, the process will be forked with
234
+ * the approptiately resolved `NODE_V8_COVERAGE` environment variable.
235
+ *
236
+ * Also, forking plugs require some special attention:
237
+ *
238
+ * * plug functions are not supported, only classes implementing the
239
+ * {@link Plug} interface can be used with this.
240
+ *
241
+ * * the class itself _MUST_ be exported as the _default_ export for the
242
+ * `scriptFile` specified below. This is to simplify interoperability between
243
+ * CommonJS and ESM modules as we use dynamic `import(...)` statements.
244
+ */
245
+ export function installForking<Name extends PlugName>(
246
+ plugName: Name,
247
+ scriptFile: AbsolutePath,
248
+ ): void {
249
+ /** Extend out our ForkingPlug below */
250
+ const ctor = class extends ForkingPlug {
251
+ constructor(...args: any[]) {
252
+ super(scriptFile, args)
253
+ }
254
+ }
255
+
256
+ /** Install the plug in */
257
+ install(plugName, ctor as any)
258
+ }
package/src/helpers.ts CHANGED
@@ -31,7 +31,7 @@ export function find(...args: ParseOptions<FindOptions>): Pipe {
31
31
  const { params: globs, options } = parseOptions(args, {})
32
32
 
33
33
  const context = requireContext()
34
- return new Pipe(context, async (): Promise<Files> => {
34
+ return new Pipe(context, Promise.resolve().then(async () => {
35
35
  const directory = options.directory ?
36
36
  context.resolve(options.directory) :
37
37
  getCurrentWorkingDirectory()
@@ -42,7 +42,7 @@ export function find(...args: ParseOptions<FindOptions>): Pipe {
42
42
  }
43
43
 
44
44
  return builder.build()
45
- })
45
+ }))
46
46
  }
47
47
 
48
48
  /**
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
+
6
+ export { BuildFailure } from './failure'
5
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
@@ -7,11 +7,11 @@ import {
7
7
  } from './paths'
8
8
 
9
9
  import { sep } from 'path'
10
- import { assert } from './assert'
10
+ import { assert, assertPromises } from './assert'
11
11
  import { requireContext } from './async'
12
12
  import { Files } from './files'
13
- import { ForkingPlug } from './fork'
14
13
  import { getLogger, Logger } from './log'
14
+ import { Result } from './types'
15
15
 
16
16
  /* ========================================================================== *
17
17
  * PLUGS *
@@ -86,6 +86,25 @@ export class Context {
86
86
  * PIPES *
87
87
  * ========================================================================== */
88
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
+
89
108
  /**
90
109
  * A class that will be extended by {@link Pipe} where {@link install} will
91
110
  * add prototype properties from installed {@link Plug}s
@@ -98,14 +117,63 @@ abstract class PipeProto {
98
117
  * The {@link Pipe} class defines processing pipeline where multiple
99
118
  * {@link Plug}s can transform lists of {@link Files}.
100
119
  */
101
- export class Pipe extends PipeProto {
120
+ export class Pipe extends PipeProto implements Promise<Files> {
121
+ readonly [Symbol.toStringTag] = 'Pipe'
122
+
102
123
  constructor(
103
124
  private readonly _context: Context,
104
- private readonly _run: () => Promise<Files>,
125
+ private readonly _promise: Promise<Result>,
105
126
  ) {
106
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>
107
171
  }
108
172
 
173
+ /* ------------------------------------------------------------------------ *
174
+ * Pipe implementation *
175
+ * ------------------------------------------------------------------------ */
176
+
109
177
  plug(plug: Plug<Files>): Pipe
110
178
  plug(plug: PlugFunction<Files>): Pipe
111
179
  plug(plug: Plug<void | undefined>): Promise<undefined>
@@ -113,18 +181,15 @@ export class Pipe extends PipeProto {
113
181
  plug(arg: Plug<PlugResult> | PlugFunction<PlugResult>): Pipe | Promise<undefined> {
114
182
  const plug = typeof arg === 'function' ? { pipe: arg } : arg
115
183
 
116
- const parent = this
117
- const context = this._context
118
- return new Pipe(context, async (): Promise<Files> => {
119
- const previous = await parent.run()
120
- const current = await plug.pipe(previous, context)
121
- assert(current, 'Unable to extend pipe')
122
- return current
123
- })
124
- }
184
+ // We are creating a new "leaf" Pipe, we can forget our promise
185
+ getContextPromises(this._context).delete(this._promise)
125
186
 
126
- run(): Promise<Files> {
127
- return this._run()
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
+ }))
128
193
  }
129
194
 
130
195
  /**
@@ -137,8 +202,6 @@ export class Pipe extends PipeProto {
137
202
  *
138
203
  * ```
139
204
  * const pipe: Pipe = merge([
140
- * // other tasks return `Pipe & Promise<Files>` so we can
141
- * // direcrly await their result without invoking `run()`
142
205
  * await this.anotherTask1(),
143
206
  * await this.anotherTask2(),
144
207
  * ])
@@ -146,20 +209,20 @@ export class Pipe extends PipeProto {
146
209
  */
147
210
  static merge(pipes: (Pipe | Files | Promise<Files>)[]): Pipe {
148
211
  const context = requireContext()
149
- return new Pipe(context, async (): Promise<Files> => {
212
+ return new Pipe(context, Promise.resolve().then(async () => {
213
+ // No pipes? Just send off an empty pipe...
150
214
  if (pipes.length === 0) return Files.builder(getCurrentWorkingDirectory()).build()
151
215
 
152
- const [ first, ...other ] = await Promise.all(pipes.map((pipe) => {
153
- return 'run' in pipe ? pipe.run() : pipe
154
- }))
155
-
156
- const firstDir = first.directory
157
- const otherDirs = other.map((f) => f.directory)
216
+ // Await for all pipes / files / files promises
217
+ const results = await assertPromises<Files>(pipes, 'Error in pipe while merging')
158
218
 
219
+ // Find the common directory between all the Files instances
220
+ const [ firstDir, ...otherDirs ] = results.map((f) => f.directory)
159
221
  const directory = commonPath(firstDir, ...otherDirs)
160
222
 
161
- return Files.builder(directory).merge(first, ...other).build()
162
- })
223
+ // Build our new files instance merging all the results
224
+ return Files.builder(directory).merge(...results).build()
225
+ }))
163
226
  }
164
227
  }
165
228
 
@@ -168,7 +231,7 @@ export class Pipe extends PipeProto {
168
231
  * ========================================================================== */
169
232
 
170
233
  /** The names which can be installed as direct plugs. */
171
- export type PlugName = string & Exclude<keyof Pipe, 'plug' | 'run'>
234
+ export type PlugName = string & Exclude<keyof Pipe, 'plug' | keyof Promise<any>>
172
235
 
173
236
  /** The parameters of the plug extension with the given name */
174
237
  export type PipeParameters<Name extends PlugName> = PipeOverloads<Name>['args']
@@ -282,35 +345,3 @@ export function install<
282
345
  /* Inject the create function in the Pipe's prototype */
283
346
  Object.defineProperty(PipeProto.prototype, name, { value: plug })
284
347
  }
285
-
286
- /**
287
- * Install a _forking_ {@link Plug} in the {@link Pipe}, in other words
288
- * execute the plug in a separate process.
289
- *
290
- * As a contract, if the _last non-null_ parameter of the constructor is an
291
- * object and contains the key `coverageDir`, the process will be forked with
292
- * the approptiately resolved `NODE_V8_COVERAGE` environment variable.
293
- *
294
- * Also, forking plugs require some special attention:
295
- *
296
- * * plug functions are not supported, only classes implementing the
297
- * {@link Plug} interface can be used with this.
298
- *
299
- * * the class itself _MUST_ be exported as the _default_ export for the
300
- * `scriptFile` specified below. This is to simplify interoperability between
301
- * CommonJS and ESM modules as we use dynamic `import(...)` statements.
302
- */
303
- export function installForking<Name extends PlugName>(
304
- plugName: Name,
305
- scriptFile: AbsolutePath,
306
- ): void {
307
- /** Extend out our ForkingPlug below */
308
- const ctor = class extends ForkingPlug {
309
- constructor(...args: any[]) {
310
- super(scriptFile, args)
311
- }
312
- } as unknown as PlugConstructor<Name>
313
-
314
- /** Install the plug in */
315
- install(plugName, ctor)
316
- }
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 */
@@ -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,6 +1,6 @@
1
1
  import { Files } from '../files'
2
2
  import { $gry, $p, $und, $ylw } from '../log'
3
- import { install, Plug, Context, PipeParameters } from '../pipe'
3
+ import { Context, install, PipeParameters, Plug } from '../pipe'
4
4
 
5
5
  declare module '../pipe' {
6
6
  export interface Pipe {
@@ -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')