@plugjs/plug 0.4.35 → 0.5.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 (66) hide show
  1. package/dist/build.cjs +12 -5
  2. package/dist/build.cjs.map +1 -1
  3. package/dist/build.d.ts +3 -1
  4. package/dist/build.mjs +12 -6
  5. package/dist/build.mjs.map +1 -1
  6. package/dist/cli.mjs +12 -7
  7. package/dist/cli.mjs.map +2 -2
  8. package/dist/fork.cjs +20 -13
  9. package/dist/fork.cjs.map +1 -1
  10. package/dist/fork.d.ts +1 -0
  11. package/dist/fork.mjs +20 -13
  12. package/dist/fork.mjs.map +1 -1
  13. package/dist/logging/emit.cjs +31 -2
  14. package/dist/logging/emit.cjs.map +2 -2
  15. package/dist/logging/emit.d.ts +14 -0
  16. package/dist/logging/emit.mjs +29 -2
  17. package/dist/logging/emit.mjs.map +2 -2
  18. package/dist/logging/github.cjs +4 -7
  19. package/dist/logging/github.cjs.map +1 -1
  20. package/dist/logging/github.mjs +4 -7
  21. package/dist/logging/github.mjs.map +1 -1
  22. package/dist/logging/logger.cjs +23 -37
  23. package/dist/logging/logger.cjs.map +1 -1
  24. package/dist/logging/logger.d.ts +7 -7
  25. package/dist/logging/logger.mjs +24 -38
  26. package/dist/logging/logger.mjs.map +1 -1
  27. package/dist/logging/options.cjs +30 -29
  28. package/dist/logging/options.cjs.map +1 -1
  29. package/dist/logging/options.d.ts +2 -11
  30. package/dist/logging/options.mjs +30 -29
  31. package/dist/logging/options.mjs.map +1 -1
  32. package/dist/logging.cjs +5 -3
  33. package/dist/logging.cjs.map +1 -1
  34. package/dist/logging.mjs +5 -3
  35. package/dist/logging.mjs.map +1 -1
  36. package/dist/plugs/exports.cjs +3 -5
  37. package/dist/plugs/exports.cjs.map +1 -1
  38. package/dist/plugs/exports.mjs +3 -5
  39. package/dist/plugs/exports.mjs.map +1 -1
  40. package/dist/utils/ansi.cjs +39 -0
  41. package/dist/utils/ansi.cjs.map +6 -0
  42. package/dist/utils/ansi.d.ts +4 -0
  43. package/dist/utils/ansi.mjs +13 -0
  44. package/dist/utils/ansi.mjs.map +6 -0
  45. package/dist/utils/exec.cjs +10 -1
  46. package/dist/utils/exec.cjs.map +1 -1
  47. package/dist/utils/exec.mjs +11 -2
  48. package/dist/utils/exec.mjs.map +1 -1
  49. package/dist/utils.cjs +2 -0
  50. package/dist/utils.cjs.map +1 -1
  51. package/dist/utils.d.ts +1 -0
  52. package/dist/utils.mjs +1 -0
  53. package/dist/utils.mjs.map +1 -1
  54. package/package.json +1 -1
  55. package/src/build.ts +14 -5
  56. package/src/cli.mts +11 -5
  57. package/src/fork.ts +26 -15
  58. package/src/logging/emit.ts +65 -2
  59. package/src/logging/github.ts +4 -11
  60. package/src/logging/logger.ts +25 -43
  61. package/src/logging/options.ts +29 -37
  62. package/src/logging.ts +5 -3
  63. package/src/plugs/exports.ts +3 -4
  64. package/src/utils/ansi.ts +10 -0
  65. package/src/utils/exec.ts +7 -2
  66. package/src/utils.ts +1 -0
package/src/cli.mts CHANGED
@@ -7,13 +7,16 @@ import _fs from 'node:fs'
7
7
  import { main, yargsParser } from '@plugjs/tsrun'
8
8
 
9
9
  import { BuildFailure } from './asserts'
10
+ import { runAsync } from './async'
10
11
  import { invokeTasks, isBuild } from './build'
11
12
  import { $blu, $gry, $p, $red, $t, $und, $wht } from './logging/colors'
12
13
  import { logLevels } from './logging/levels'
13
14
  import { logOptions } from './logging/options'
14
15
  import { getCurrentWorkingDirectory, resolveAbsolutePath, resolveDirectory, resolveFile } from './paths'
16
+ import { Context } from './pipe'
15
17
 
16
18
  import type { AbsolutePath } from './paths'
19
+ import type { Build } from './types'
17
20
 
18
21
  /* Log levels */
19
22
  const { TRACE, DEBUG, INFO, NOTICE, WARN, ERROR, OFF } = logLevels
@@ -258,11 +261,14 @@ main(import.meta.url, async (args: string[]): Promise<void> => {
258
261
  if (tasks.length === 0) tasks.push('default')
259
262
 
260
263
  // Import and check build file
261
- let maybeBuild = await import(buildFile)
262
- while (maybeBuild) {
263
- if (isBuild(maybeBuild)) break
264
- maybeBuild = maybeBuild.default
265
- }
264
+ const initialContext = new Context(buildFile, '')
265
+ const maybeBuild = await runAsync(initialContext, async (): Promise<Build | void> => {
266
+ let maybeBuild = await import(buildFile)
267
+ while (maybeBuild) {
268
+ if (isBuild(maybeBuild)) return maybeBuild
269
+ maybeBuild = maybeBuild.default
270
+ }
271
+ })
266
272
 
267
273
  // We _need_ a build
268
274
  if (! isBuild(maybeBuild)) {
package/src/fork.ts CHANGED
@@ -4,9 +4,11 @@ import { assert, BuildFailure } from './asserts'
4
4
  import { runAsync } from './async'
5
5
  import { Files } from './files'
6
6
  import { $gry, $p, $red, logOptions } from './logging'
7
+ import { emit, emitForked } from './logging/emit'
7
8
  import { requireFilename, resolveFile } from './paths'
8
9
  import { Context, install } from './pipe'
9
10
 
11
+ import type { ForkedLogMessage } from './logging/emit'
10
12
  import type { AbsolutePath } from './paths'
11
13
  import type { Plug, PlugName, PlugResult } from './pipe'
12
14
 
@@ -37,6 +39,7 @@ export interface ForkData {
37
39
  filesDir: AbsolutePath,
38
40
  /** All files to pipe */
39
41
  filesList: AbsolutePath[],
42
+ logIndent: number,
40
43
  }
41
44
 
42
45
  /** Fork result, from child to parent process */
@@ -69,6 +72,7 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
69
72
  buildFile: context.buildFile,
70
73
  filesDir: files.directory,
71
74
  filesList: [ ...files.absolutePaths() ],
75
+ logIndent: context.log.indent,
72
76
  }
73
77
 
74
78
  /* Get _this_ filename to spawn */
@@ -76,7 +80,7 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
76
80
  context.log.debug('About to fork plug from', $p(this._scriptFile))
77
81
 
78
82
  /* Environment variables */
79
- const env = { ...process.env, ...logOptions.forkEnv(context.taskName, 4) }
83
+ const env = { ...process.env, ...logOptions.forkEnv(context.taskName) }
80
84
 
81
85
  /* Check our args (reversed) to see if the last specifies `coverageDir` */
82
86
  for (let i = this._arguments.length - 1; i >= 0; i --) {
@@ -95,15 +99,11 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
95
99
 
96
100
  /* Run our script in a _separate_ process */
97
101
  const child = fork(script, {
98
- stdio: [ 'ignore', 'inherit', 'inherit', 'ipc', 'pipe' ],
102
+ stdio: [ 'ignore', 'inherit', 'inherit', 'ipc' ],
99
103
  env,
100
104
  })
101
105
 
102
- /* Pipe child logs directly to the writer */
103
- if (child.stdio[4]) {
104
- child.stdio[4].on('data', (data) => logOptions.output.write(data))
105
- }
106
-
106
+ /* Do some logging... */
107
107
  context.log.info('Running', $p(script), $gry(`(pid=${child.pid})`))
108
108
 
109
109
  /* Return a promise from the child process events */
@@ -116,9 +116,18 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
116
116
  return done || reject(BuildFailure.fail())
117
117
  })
118
118
 
119
- child.on('message', (message: ForkResult) => {
120
- context.log.debug('Message from forked plug process with PID', child.pid, message)
121
- response = message
119
+ child.on('message', (message: ForkResult | ForkedLogMessage) => {
120
+ if ('logLevel' in message) {
121
+ const { logLevel, taskName, lines } = message
122
+ lines.forEach((line) => {
123
+ // this is _a hack_, as we want to reuse the indent and prefix of
124
+ // the current log, but the task name from the forked plug!!!
125
+ (context.log as any)._emit(logLevel, [ line ], taskName)
126
+ })
127
+ } else {
128
+ context.log.debug('Message from forked plug process with PID', child.pid, message)
129
+ response = message
130
+ }
122
131
  })
123
132
 
124
133
  child.on('exit', (code, signal) => {
@@ -204,10 +213,15 @@ if ((process.argv[1] === requireFilename(__fileurl)) && (process.send)) {
204
213
  buildFile,
205
214
  filesDir,
206
215
  filesList,
216
+ logIndent,
207
217
  } = message
208
218
 
219
+ /* Before anything else, capture logs! */
220
+ emit.emitter = emitForked // replace the log emitter...
221
+
209
222
  /* First of all, our plug context */
210
223
  const context = new Context(buildFile, taskName)
224
+ context.log.indent = logIndent
211
225
  context.log.debug('Message from parent process for PID', process.pid, message)
212
226
 
213
227
  /* Contextualize this run, and go! */
@@ -260,11 +274,8 @@ if ((process.argv[1] === requireFilename(__fileurl)) && (process.send)) {
260
274
  console.error('\n\nError sending message back to parent process', error)
261
275
  process.exitCode = 1
262
276
  }).finally(() => {
263
- /* Flush and end our log output */
264
- logOptions.output.end(() => {
265
- process.disconnect()
266
- process.exit(process.exitCode)
267
- })
277
+ process.disconnect()
278
+ process.exit(process.exitCode)
268
279
  })
269
280
  })
270
281
  }
@@ -1,5 +1,6 @@
1
1
  import { formatWithOptions } from 'node:util'
2
2
 
3
+ import { fail } from '../asserts'
3
4
  import { $blu, $grn, $gry, $red, $t, $ylw } from './colors'
4
5
  import { DEBUG, INFO, NOTICE, TRACE, WARN } from './levels'
5
6
  import { logOptions } from './options'
@@ -14,13 +15,17 @@ let _output = logOptions.output
14
15
  let _indentSize = logOptions.indentSize
15
16
  let _taskLength = logOptions.taskLength
16
17
  let _lineLength = logOptions.lineLength
17
- let _inspectOptions = logOptions.inspectOptions
18
+ let _inspectOptions = { ...logOptions.inspectOptions }
18
19
  logOptions.on('changed', (options) => {
19
20
  _output = options.output
20
21
  _indentSize = options.indentSize
21
22
  _taskLength = options.taskLength
22
23
  _lineLength = options.lineLength
23
- _inspectOptions = options.inspectOptions
24
+ _inspectOptions = { ...options.inspectOptions } // proxy
25
+ _defaultEmitter =
26
+ options.format === 'fancy' ? emitFancy :
27
+ options.format === 'plain' ? emitPlain :
28
+ fail(`Invalid log format "${logOptions.format}"`)
24
29
  })
25
30
 
26
31
  /* ========================================================================== *
@@ -38,6 +43,12 @@ export interface LogEmitterOptions {
38
43
  /** Emit a line (or multiple lines) of text to the log */
39
44
  export type LogEmitter = (options: LogEmitterOptions, args: any[]) => void
40
45
 
46
+ /** A {@link LogEmitter} function configurable with a specific emitter */
47
+ export interface ConfigurableLogEmitter extends LogEmitter {
48
+ get emitter(): LogEmitter
49
+ set emitter(emitter: LogEmitter | undefined)
50
+ }
51
+
41
52
  /* ========================================================================== */
42
53
 
43
54
  /** Emit in full colors with spinner support and whatnot! */
@@ -120,3 +131,55 @@ export const emitPlain: LogEmitter = (options: LogEmitterOptions, args: any[]):
120
131
  _output.write(`${linePrefix}${line}\n`)
121
132
  }
122
133
  }
134
+
135
+ /* ========================================================================== */
136
+
137
+ export interface ForkedLogMessage {
138
+ logLevel: LogLevel,
139
+ taskName: string,
140
+ lines: string[],
141
+ }
142
+
143
+ /** Emit to the parent process of a forked child, or to the default emitter */
144
+ export const emitForked: LogEmitter = (options: LogEmitterOptions, args: any[]): void => {
145
+ if (process.connected && process.send) {
146
+ const { taskName, level, prefix = '', indent = 0 } = options
147
+ const linePrefix = ''.padStart(indent * _indentSize) + prefix
148
+
149
+ /* Now for the normal logging of all our parameters */
150
+ const breakLength = _lineLength - _taskLength - linePrefix.length - 20
151
+ const message = formatWithOptions({ ..._inspectOptions, breakLength }, ...args)
152
+
153
+ /* Format each individual line */
154
+ const lines = message.split('\n').map((line) => `${linePrefix}${line}`)
155
+
156
+ /* Send the message to the parent process */
157
+ process.send({ logLevel: level, taskName, lines })
158
+ } else {
159
+ _defaultEmitter(options, args)
160
+ }
161
+ }
162
+
163
+ /* ========================================================================== */
164
+
165
+ /** The _default_ emitter (from `format`) */
166
+ let _defaultEmitter =
167
+ logOptions.format === 'fancy' ? emitFancy :
168
+ logOptions.format === 'plain' ? emitPlain :
169
+ fail(`Invalid log format "${logOptions.format}"`)
170
+
171
+ /** The _actual_ emitter (either default or configured) */
172
+ let _emitter = _defaultEmitter
173
+
174
+ /** Our `emit` wrapper function to export */
175
+ const wrapper: LogEmitter = function emit(options: LogEmitterOptions, args: any[]): void {
176
+ _defaultEmitter(options, args)
177
+ }
178
+
179
+ /** A _configurable_ {@link LogEmitter} where log should be emitted to */
180
+ export const emit = Object.defineProperty(wrapper, 'emitter', {
181
+ get: () => _emitter,
182
+ set: (emitter: LogEmitter | undefined) => {
183
+ _emitter = emitter || _defaultEmitter
184
+ },
185
+ }) as ConfigurableLogEmitter
@@ -1,20 +1,15 @@
1
1
  import { EOL } from 'node:os'
2
2
  import { formatWithOptions } from 'node:util'
3
3
 
4
+ import { stripAnsi } from '../utils/ansi'
4
5
  import { logOptions } from './options'
5
6
 
6
7
  import type { AbsolutePath } from '../paths'
7
8
 
8
- /* Strip ANSI from strings */
9
- const ansiRegExp = new RegExp([
10
- '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
11
- '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
12
- ].join('|'), 'g')
13
-
14
9
  /* Initial values, and subscribe to changes */
15
10
  let _output = logOptions.output
16
- let _inspectOptions = logOptions.inspectOptions
17
11
  let _githubAnnotations = logOptions.githubAnnotations
12
+ let _inspectOptions = { ...logOptions.inspectOptions, breakLength: Infinity }
18
13
  logOptions.on('changed', (options) => {
19
14
  _output = options.output
20
15
  _githubAnnotations = options.githubAnnotations
@@ -23,16 +18,14 @@ logOptions.on('changed', (options) => {
23
18
 
24
19
 
25
20
  function escapeData(data: string): string {
26
- return data
27
- .replace(ansiRegExp, '')
21
+ return stripAnsi(data)
28
22
  .replace(/%/g, '%25')
29
23
  .replace(/\r/g, '%0D')
30
24
  .replace(/\n/g, '%0A')
31
25
  }
32
26
 
33
27
  function escapeProp(prop: string | number): string {
34
- return `${prop}`
35
- .replace(ansiRegExp, '')
28
+ return stripAnsi(`${prop}`)
36
29
  .replace(/%/g, '%25')
37
30
  .replace(/\r/g, '%0D')
38
31
  .replace(/\n/g, '%0A')
@@ -1,8 +1,10 @@
1
1
  import { formatWithOptions } from 'node:util'
2
2
 
3
3
  import { BuildFailure } from '../asserts'
4
+ import { currentContext } from '../async'
5
+ import { stripAnsi } from '../utils/ansi'
4
6
  import { $gry } from './colors'
5
- import { emitFancy, emitPlain } from './emit'
7
+ import { emit } from './emit'
6
8
  import { DEBUG, ERROR, INFO, NOTICE, TRACE, WARN } from './levels'
7
9
  import { logOptions } from './options'
8
10
  import { ReportImpl } from './report'
@@ -16,11 +18,7 @@ import type { Report } from './report'
16
18
 
17
19
  /* Initial value of log colors, and subscribe to changes */
18
20
  let _level = logOptions.level
19
- let _format = logOptions.format
20
- let _defaultTaskName = logOptions.defaultTaskName
21
- logOptions.on('changed', ({ defaultTaskName, format, level }) => {
22
- _defaultTaskName = defaultTaskName
23
- _format = format
21
+ logOptions.on('changed', ({ level }) => {
24
22
  _level = level
25
23
  })
26
24
 
@@ -50,6 +48,8 @@ export interface Log {
50
48
  export interface Logger extends Log {
51
49
  /** The current level for logging. */
52
50
  level: LogLevel,
51
+ /** The current indent level for logging. */
52
+ indent: number,
53
53
 
54
54
  /** Enter a sub-level of logging, increasing indent */
55
55
  enter(): void
@@ -64,36 +64,31 @@ export interface Logger extends Log {
64
64
  }
65
65
 
66
66
  /** Return a {@link Logger} associated with the specified task name. */
67
- export function getLogger(task: string = _defaultTaskName): Logger {
68
- let logger = _loggers.get(task)
69
- if (! logger) {
70
- const emitter = _format === 'fancy' ? emitFancy : emitPlain
71
- logger = new LoggerImpl(task, emitter)
72
- _loggers.set(task, logger)
73
- }
74
- return logger
67
+ export function getLogger(task?: string, indent?: number): Logger {
68
+ const context = currentContext()
69
+ const taskName = task === undefined ? (context?.taskName || '') : task
70
+ const indentLevel = indent === undefined ? (context?.log.indent || 0) : 0
71
+ return new LoggerImpl(taskName, emit, indentLevel)
75
72
  }
76
73
 
77
74
  /* ========================================================================== */
78
75
 
79
- /** Cache of loggers by task-name. */
80
- const _loggers = new Map<string, Logger>()
81
76
  /** Weak set of already logged build failures */
82
77
  const _loggedFailures = new WeakSet<BuildFailure>()
83
78
 
84
79
  /** Default implementation of the {@link Logger} interface. */
85
80
  class LoggerImpl implements Logger {
86
81
  private readonly _stack: { level: LogLevel, message: string, indent: number }[] = []
87
- private _level = _level
88
- private _indent = 0
82
+ public level = _level
89
83
 
90
84
  constructor(
91
85
  private readonly _task: string,
92
86
  private readonly _emitter: LogEmitter,
87
+ public indent: number,
93
88
  ) {}
94
89
 
95
- private _emit(level: LogLevel, args: [ any, ...any ]): void {
96
- if (this._level > level) return
90
+ private _emit(level: LogLevel, args: [ any, ...any ], taskName = this._task): void {
91
+ if (this.level > level) return
97
92
 
98
93
  // The `BuildFailure` is a bit special case
99
94
  const params = args.filter((arg) => {
@@ -123,7 +118,7 @@ class LoggerImpl implements Logger {
123
118
  if (params.length === 0) return
124
119
 
125
120
  // Prepare our options for logging
126
- const options = { level, taskName: this._task, indent: this._indent }
121
+ const options = { level, taskName, indent: this.indent }
127
122
 
128
123
  // Dump any existing stack entry
129
124
  if (this._stack.length) {
@@ -137,14 +132,6 @@ class LoggerImpl implements Logger {
137
132
  this._emitter(options, params)
138
133
  }
139
134
 
140
- get level(): LogLevel {
141
- return this._level
142
- }
143
-
144
- set level(level: LogLevel) {
145
- this._level = level
146
- }
147
-
148
135
  trace(...args: [ any, ...any ]): void {
149
136
  this._emit(TRACE, args)
150
137
  }
@@ -179,19 +166,19 @@ class LoggerImpl implements Logger {
179
166
  enter(...args: [] | [ level: LogLevel, message: string ]): void {
180
167
  if (args.length) {
181
168
  const [ level, message ] = args
182
- this._stack.push({ level, message, indent: this._indent })
169
+ this._stack.push({ level, message, indent: this.indent })
183
170
  }
184
171
 
185
- this._indent ++
172
+ this.indent ++
186
173
  }
187
174
 
188
175
  leave(): void
189
176
  leave(level: LogLevel, message: string): void
190
177
  leave(...args: [] | [ level: LogLevel, message: string ]): void {
191
178
  this._stack.pop()
192
- this._indent --
179
+ this.indent --
193
180
 
194
- if (this._indent < 0) this._indent = 0
181
+ if (this.indent < 0) this.indent = 0
195
182
 
196
183
  if (args.length) {
197
184
  const [ level, message ] = args
@@ -209,8 +196,8 @@ class LoggerImpl implements Logger {
209
196
  }
210
197
 
211
198
  let { indent = 0, prefix = '' } = options
212
- prefix = this._indent ? $gry('| ') + prefix : prefix
213
- indent += this._indent
199
+ prefix = this.indent ? $gry('| ') + prefix : prefix
200
+ indent += this.indent
214
201
  this._emitter({ ...options, indent, prefix }, args)
215
202
  }
216
203
  return new ReportImpl(title, this._task, emitter)
@@ -219,12 +206,7 @@ class LoggerImpl implements Logger {
219
206
 
220
207
  /* ========================================================================== */
221
208
 
222
- /** Pattern to match ANSI expressions */
223
- const ansiPattern = '[\\u001b\\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]'
224
- /** Regular expression matching ANSI */
225
- const ansiRegExp = new RegExp(ansiPattern, 'g')
226
-
227
- /** A test logger, writing to a buffer always _without_ colors */
209
+ /** A test logger, writing to a buffer always _without_ colors/indent */
228
210
  export class TestLogger extends LoggerImpl {
229
211
  private _lines: string[] = []
230
212
 
@@ -236,10 +218,10 @@ export class TestLogger extends LoggerImpl {
236
218
  /* Now for the normal logging of all our parameters */
237
219
  formatWithOptions({ colors: false, breakLength: 120 }, ...args)
238
220
  .split('\n').forEach((line) => {
239
- const stripped = line.replaceAll(ansiRegExp, '')
221
+ const stripped = stripAnsi(line)
240
222
  this._lines.push(`${linePrefix}${stripped}`)
241
223
  })
242
- })
224
+ }, 0)
243
225
  }
244
226
 
245
227
  /** Return the _current_ buffer for this instance */
@@ -1,5 +1,4 @@
1
1
  import { EventEmitter } from 'node:events'
2
- import { Socket } from 'node:net'
3
2
 
4
3
  import { getSingleton } from '../utils/singleton'
5
4
  import { getLevelNumber, NOTICE } from './levels'
@@ -30,16 +29,12 @@ export interface LogOptions {
30
29
  indentSize: number,
31
30
  /** Whether to show sources in reports or not. */
32
31
  showSources: boolean,
33
- /** The task name to be used by default if a task is not contextualized. */
34
- defaultTaskName: string,
35
32
  /** Whether GitHub annotations are enabled or not. */
36
33
  githubAnnotations: boolean,
34
+
37
35
  /** The options used by NodeJS for object inspection. */
38
36
  readonly inspectOptions: InspectOptions,
39
37
 
40
- /** Set an inspect option in {@link LogOptions.inspectOptions}). */
41
- setInspectOption<K extends keyof InspectOptions>(key: K, value: InspectOptions[K]): void
42
-
43
38
  /** Add an event listener for the specified event. */
44
39
  on(eventName: 'changed', listener: (logOptions: this) => void): this;
45
40
  /** Add an event listener for the specified event triggering only once. */
@@ -51,9 +46,8 @@ export interface LogOptions {
51
46
  * Return a record of environment variables for forking.
52
47
  *
53
48
  * @param taskName The default task name of the forked process
54
- * @param logFd A file descriptor where logs should be sinked to
55
49
  */
56
- forkEnv(taskName?: string, logFd?: number): Record<string, string>
50
+ forkEnv(taskName?: string): Record<string, string>
57
51
  }
58
52
 
59
53
  /* ========================================================================== *
@@ -72,7 +66,6 @@ class LogOptionsImpl extends EventEmitter implements LogOptions {
72
66
  private _showSources = true // by default, always show source snippets
73
67
  private _githubAnnotations = false // ultimately set by the constructor
74
68
  private _inspectOptions: InspectOptions = {}
75
- private _defaultTaskName = ''
76
69
  private _taskLength = 0
77
70
  private _indentSize = 2
78
71
 
@@ -104,12 +97,7 @@ class LogOptionsImpl extends EventEmitter implements LogOptions {
104
97
  * and it's processed _last_ as it's normally only created by fork below
105
98
  * and consumed by the `Exec` plug (which has no other way of communicating)
106
99
  */
107
- const { fd, ...options } = JSON.parse(process.env.__LOG_OPTIONS || '{}')
108
- if (fd) {
109
- const output = new Socket({ fd, readable: false, writable: true }).unref()
110
- process.on('beforeExit', () => output.end())
111
- this._output = output
112
- }
100
+ const options = JSON.parse(process.env.__LOG_OPTIONS || '{}')
113
101
  Object.assign(this, options)
114
102
  }
115
103
 
@@ -117,17 +105,19 @@ class LogOptionsImpl extends EventEmitter implements LogOptions {
117
105
  super.emit('changed', this)
118
106
  }
119
107
 
120
- forkEnv(taskName?: string, fd?: number): Record<string, string> {
108
+ forkEnv(taskName?: string): Record<string, string> {
109
+ void taskName
121
110
  return {
122
111
  __LOG_OPTIONS: JSON.stringify({
123
112
  level: this._level,
124
113
  colors: this._colors,
114
+ format: this._format,
125
115
  lineLength: this._lineLength,
126
116
  taskLength: this._taskLength,
117
+ showSources: this._showSources,
127
118
  githubAnnotations: this.githubAnnotations,
128
- defaultTaskName: taskName || this._defaultTaskName,
119
+ indentSize: this.indentSize,
129
120
  spinner: false, // forked spinner is always false
130
- fd, // file descriptor for logs
131
121
  }),
132
122
  }
133
123
  }
@@ -159,6 +149,7 @@ class LogOptionsImpl extends EventEmitter implements LogOptions {
159
149
  set colors(color: boolean) {
160
150
  this._colors = color
161
151
  this._colorsSet = true
152
+ this._inspectOptions.colors = color
162
153
  this._notifyListeners()
163
154
  }
164
155
 
@@ -218,15 +209,6 @@ class LogOptionsImpl extends EventEmitter implements LogOptions {
218
209
  this._notifyListeners()
219
210
  }
220
211
 
221
- get defaultTaskName(): string {
222
- return this._defaultTaskName
223
- }
224
-
225
- set defaultTaskName(defaultTaskName: string) {
226
- this._defaultTaskName = defaultTaskName
227
- this._notifyListeners()
228
- }
229
-
230
212
  get githubAnnotations(): boolean {
231
213
  return this._githubAnnotations
232
214
  }
@@ -237,16 +219,26 @@ class LogOptionsImpl extends EventEmitter implements LogOptions {
237
219
  }
238
220
 
239
221
  get inspectOptions(): InspectOptions {
240
- return {
241
- colors: this._colors,
242
- breakLength: this._lineLength,
243
- ...this._inspectOptions,
244
- }
245
- }
246
-
247
- setInspectOption<K extends keyof InspectOptions>(key: K, value: InspectOptions[K]): void {
248
- this._inspectOptions[key] = value
249
- this._notifyListeners()
222
+ return new Proxy(this._inspectOptions, {
223
+ get: (target, prop): any => {
224
+ if (prop === 'colors') return this.colors
225
+ if (prop === 'breakLength') return this._lineLength
226
+ return (target as any)[prop]
227
+ },
228
+ set: (target, prop, value): boolean => {
229
+ if (prop === 'colors') {
230
+ this.colors = !! value
231
+ } else if (prop === 'breakLength') {
232
+ const length = parseInt(value)
233
+ if (isNaN(length)) return false
234
+ this.lineLength = length
235
+ } else {
236
+ (target as any)[prop] = value
237
+ }
238
+ this._notifyListeners()
239
+ return true
240
+ },
241
+ })
250
242
  }
251
243
  }
252
244
 
package/src/logging.ts CHANGED
@@ -2,6 +2,7 @@ import { currentContext } from './async'
2
2
  import { $gry, $wht } from './logging/colors'
3
3
  import { getLogger } from './logging/logger'
4
4
  import { setupSpinner } from './logging/spinner'
5
+ import { stripAnsi } from './utils/ansi'
5
6
 
6
7
  import type { Log, Logger } from './logging/logger'
7
8
 
@@ -75,13 +76,14 @@ export const log: LogFunction = ((): LogFunction => {
75
76
 
76
77
  /** Print a nice _banner_ message on the log */
77
78
  export function banner(message: string): void {
78
- const padMessage = message.length > 60 ? message.length : 60
79
- const padLines = padMessage + 2
79
+ const length = stripAnsi(message).length
80
+ const padLines = length > 60 ? length + 2 : 62
81
+ const padBlank = length > 60 ? 0 : 60 - length
80
82
 
81
83
  log.notice([
82
84
  '',
83
85
  $gry(`\u2554${''.padStart(padLines, '\u2550')}\u2557`),
84
- `${$gry('\u2551')} ${$wht(message.padEnd(padMessage, ' '))} ${$gry('\u2551')}`,
86
+ `${$gry('\u2551')} ${$wht(message)}${''.padEnd(padBlank, ' ')} ${$gry('\u2551')}`,
85
87
  $gry(`\u255A${''.padStart(padLines, '\u2550')}\u255D`),
86
88
  '',
87
89
  ].join('\n'))
@@ -1,6 +1,7 @@
1
1
  import { EOL } from 'node:os'
2
2
  import { sep } from 'node:path'
3
3
 
4
+ import { assert } from '../asserts'
4
5
  import { Files } from '../files'
5
6
  import { readFile, writeFile } from '../fs'
6
7
  import { $p } from '../logging'
@@ -76,7 +77,7 @@ install('exports', class Exports implements Plug<Files> {
76
77
  packageData.type === 'commonjs' ? 'commonjs' :
77
78
  packageData.type == null ? 'commonjs' :
78
79
  undefined
79
- if (! type) context.log.fail(`Unknown module type "${packageData.type}" in ${$p(incomingFile)}`)
80
+ assert(type, `Unknown module type "${packageData.type}" in ${$p(incomingFile)}`)
80
81
 
81
82
  context.log.debug(`Package file ${$p(incomingFile)} declares module type "${type}"`)
82
83
 
@@ -84,9 +85,7 @@ install('exports', class Exports implements Plug<Files> {
84
85
  const esmExtension = this._esmExtension || (type === 'module' ? '.js' : '.mjs')
85
86
 
86
87
  // reject when commonjs and ecmascript modules have the same extension
87
- if (cjsExtension === esmExtension) {
88
- context.log.fail(`CommonJS and EcmaScript modules both resolve to same extension "${cjsExtension}"`)
89
- }
88
+ assert(cjsExtension !== esmExtension, `CommonJS and EcmaScript modules both resolve to same extension "${cjsExtension}"`)
90
89
 
91
90
  const exports: ExportsDeclaration = {}
92
91
  function addExport(
@@ -0,0 +1,10 @@
1
+ /** A {@link RegExp} matching ANSI escapes */
2
+ export const ansiRegExp = new RegExp([
3
+ '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
4
+ '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
5
+ ].join('|'), 'g')
6
+
7
+ /** Strip ANSI characters (colors) from a string */
8
+ export function stripAnsi(string: string): string {
9
+ return string && string.replace(ansiRegExp, '')
10
+ }
package/src/utils/exec.ts CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path'
3
3
  import readline from 'node:readline'
4
4
 
5
5
  import { assert, BuildFailure } from '../asserts'
6
- import { $p } from '../logging'
6
+ import { $p, logOptions } from '../logging'
7
7
  import { getCurrentWorkingDirectory, resolveDirectory } from '../paths'
8
8
 
9
9
  import type { SpawnOptions } from 'node:child_process'
@@ -56,7 +56,12 @@ export async function execChild(
56
56
 
57
57
  // Build our environment variables record
58
58
  const PATH = childPaths.join(path.delimiter)
59
- const childEnv: Record<string, string> = { ...process.env, ...env, PATH }
59
+ const childEnv: Record<string, string> = {
60
+ ...process.env, // environment from current running process
61
+ ...env, // environment configured from "execChild" arguments
62
+ ...logOptions.forkEnv(), // forked log options for child plugjs
63
+ PATH, // path with all ".../node_modules/.bin" directories
64
+ }
60
65
 
61
66
  // Instrument coverage directory if needed
62
67
  if (coverageDir) childEnv.NODE_V8_COVERAGE = context.resolve(coverageDir)
package/src/utils.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './utils/ansi'
1
2
  export * from './utils/diff'
2
3
  export * from './utils/exec'
3
4
  export * from './utils/jsonc'