@plugjs/plug 0.2.6 → 0.2.7

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 (56) hide show
  1. package/cli/plug.mjs +3 -15
  2. package/cli/ts-loader.mjs +1 -1
  3. package/cli/tsrun.mjs +1104 -36
  4. package/dist/asserts.cjs +4 -0
  5. package/dist/asserts.cjs.map +1 -1
  6. package/dist/asserts.mjs +4 -0
  7. package/dist/asserts.mjs.map +1 -1
  8. package/dist/fork.cjs +8 -5
  9. package/dist/fork.cjs.map +1 -1
  10. package/dist/fork.mjs +8 -5
  11. package/dist/fork.mjs.map +1 -1
  12. package/dist/helpers.cjs +20 -0
  13. package/dist/helpers.cjs.map +1 -1
  14. package/dist/helpers.d.ts +6 -0
  15. package/dist/helpers.mjs +20 -1
  16. package/dist/helpers.mjs.map +1 -1
  17. package/dist/logging/emit.cjs +4 -7
  18. package/dist/logging/emit.cjs.map +1 -1
  19. package/dist/logging/emit.mjs +4 -7
  20. package/dist/logging/emit.mjs.map +1 -1
  21. package/dist/logging/github.cjs +63 -0
  22. package/dist/logging/github.cjs.map +6 -0
  23. package/dist/logging/github.d.ts +13 -0
  24. package/dist/logging/github.mjs +38 -0
  25. package/dist/logging/github.mjs.map +6 -0
  26. package/dist/logging/options.cjs +20 -3
  27. package/dist/logging/options.cjs.map +1 -1
  28. package/dist/logging/options.d.ts +9 -2
  29. package/dist/logging/options.mjs +20 -3
  30. package/dist/logging/options.mjs.map +1 -1
  31. package/dist/logging/report.cjs +25 -3
  32. package/dist/logging/report.cjs.map +1 -1
  33. package/dist/logging/report.mjs +26 -4
  34. package/dist/logging/report.mjs.map +1 -1
  35. package/dist/logging.cjs +1 -0
  36. package/dist/logging.cjs.map +1 -1
  37. package/dist/logging.d.ts +1 -0
  38. package/dist/logging.mjs +1 -0
  39. package/dist/logging.mjs.map +1 -1
  40. package/dist/utils/exec.cjs +20 -13
  41. package/dist/utils/exec.cjs.map +2 -2
  42. package/dist/utils/exec.mjs +21 -14
  43. package/dist/utils/exec.mjs.map +1 -1
  44. package/extra/plug.mts +6 -3
  45. package/extra/tsrun.mts +91 -28
  46. package/extra/utils.ts +0 -18
  47. package/package.json +1 -1
  48. package/src/asserts.ts +7 -0
  49. package/src/fork.ts +10 -5
  50. package/src/helpers.ts +24 -1
  51. package/src/logging/emit.ts +2 -7
  52. package/src/logging/github.ts +71 -0
  53. package/src/logging/options.ts +29 -4
  54. package/src/logging/report.ts +40 -6
  55. package/src/logging.ts +1 -0
  56. package/src/utils/exec.ts +29 -18
package/extra/tsrun.mts CHANGED
@@ -2,8 +2,15 @@
2
2
  /* eslint-disable no-console */
3
3
 
4
4
  import _path from 'node:path'
5
+ import _repl from 'node:repl'
6
+ import _module from 'node:module'
5
7
 
6
- import { $blu, $gry, $rst, $und, $wht, main, version } from './utils.js'
8
+ import _yargs from 'yargs-parser'
9
+
10
+ import { $blu, $gry, $rst, $und, $wht, main } from './utils.js'
11
+
12
+ /** Version injected by esbuild */
13
+ declare const __version: string
7
14
 
8
15
  /** Our minimalistic help */
9
16
  function help(): never {
@@ -13,8 +20,10 @@ function help(): never {
13
20
 
14
21
  ${$blu}${$und}Options:${$rst}
15
22
 
16
- ${$wht}-h --help${$rst} Help! You're reading it now!
17
- ${$wht}-v --version${$rst} Version! This one: ${version()}!
23
+ ${$wht}-h --help ${$rst} Help! You're reading it now!
24
+ ${$wht}-v --version ${$rst} Version! This one: ${__version}!
25
+ ${$wht}-e --eval ${$rst} Evaluate the script
26
+ ${$wht}-p --print ${$rst} Evaluate the script and print the result
18
27
  ${$wht} --force-esm${$rst} Force transpilation of ".ts" files to EcmaScript modules
19
28
  ${$wht} --force-cjs${$rst} Force transpilation of ".ts" files to CommonJS modules
20
29
 
@@ -31,34 +40,88 @@ function help(): never {
31
40
 
32
41
  /** Process the command line */
33
42
  main((args: string[]): void => {
34
- let script: string | undefined
35
- let scriptArgs: string[] = []
43
+ let _script: string | undefined
44
+ let _scriptArgs: string[] = []
45
+ let _print: boolean = false
46
+ let _eval: boolean = false
47
+
48
+ /* Yargs-parse our arguments */
49
+ const parsed = _yargs(args, {
50
+ configuration: {
51
+ 'camel-case-expansion': false,
52
+ 'strip-aliased': true,
53
+ 'strip-dashed': true,
54
+ },
55
+
56
+ alias: {
57
+ 'version': [ 'v' ],
58
+ 'help': [ 'h' ],
59
+ 'eval': [ 'e' ],
60
+ 'print': [ 'p' ],
61
+ },
62
+
63
+ boolean: [ 'help', 'eval', 'print', 'force-esm', 'version', 'force-cjs' ],
64
+ })
36
65
 
37
66
  // Parse options, leaving script and scriptArgs with our code to run
38
- for (let i = 0; i < args.length; i++) {
39
- const arg = args[i]
40
-
41
- if ((arg === '-h') || (arg === '--help')) help()
42
- if ((arg === '-v') || (arg === '--version')) {
43
- console.log(`v${version()}`)
44
- process.exit(1)
45
- }
46
-
47
- if (arg!.startsWith('-')) {
48
- console.log(`${$wht}tsrun${$rst}: Uknown option "${$wht}${arg}${$rst}"`)
49
- process.exit(1)
67
+ for (const [ key, value ] of Object.entries(parsed)) {
68
+ switch (key) {
69
+ case '_': // extra arguments
70
+ [ _script, ..._scriptArgs ] = value
71
+ break
72
+ case 'help': // help screen
73
+ help()
74
+ break
75
+ case 'version': // version dump
76
+ console.log(`v${__version}`)
77
+ process.exit(0)
78
+ break
79
+ case 'eval': // eval script
80
+ _eval = value
81
+ break
82
+ case 'print': // eval script and print return value
83
+ _print = true
84
+ _eval = true
85
+ break
50
86
  }
51
-
52
- ([ script, ...scriptArgs ] = args.slice(i))
53
- break
54
87
  }
55
88
 
56
- // No script? Then help
57
- if (! script) help()
58
-
59
- // Resolve the _full_ path of the script, and tweak our process.argv
60
- // arguments, them simply import the script and let Node do its thing...
61
- script = _path.resolve(process.cwd(), script)
62
- process.argv = [ process.argv0, script, ...scriptArgs ]
63
- import(script)
89
+ // Start the repl or run the script?
90
+ if (! _script) {
91
+ // No script? Then repl
92
+ console.log(`Welcome to Node.js ${process.version} (tsrun v${__version}).`)
93
+ console.log('Type ".help" for more information.')
94
+ _repl.start()
95
+ } else if (_eval) {
96
+ // If we are evaluating a script, we need to use some node internals to do
97
+ // all the tricks to run this... We a fake script running the code to
98
+ // evaluate, instrumenting "globalThis" with all required vars and modules
99
+ const script = `
100
+ globalThis.module = module;
101
+ globalThis.require = require;
102
+ globalThis.exports = exports;
103
+ globalThis.__dirname = __dirname;
104
+ globalThis.__filename = __filename;
105
+
106
+ for (const module of require('repl').builtinModules) {
107
+ if (module.indexOf('/') >= 0) continue;
108
+ if (Object.hasOwn(globalThis, module)) continue;
109
+ Object.defineProperty(globalThis, module, { get: () => require(module) });
110
+ }
111
+
112
+ return require('node:vm').runInThisContext(${JSON.stringify(_script)}, '[eval]')
113
+ `
114
+
115
+ // Use the Node internal "Module._compile" to compile and run our script
116
+ const result = (new _module('[eval]') as any)._compile(script, '[eval]')
117
+
118
+ // If we need to print, then let's do it!
119
+ if (_print) console.log(result)
120
+ } else {
121
+ // Resolve the _full_ path of the script, and tweak our process.argv
122
+ // arguments, them simply import the script and let Node do its thing...
123
+ _script = _path.resolve(process.cwd(), _script)
124
+ process.argv = [ process.argv0, _script, ..._scriptArgs ]
125
+ import(_script)
126
+ }
64
127
  })
package/extra/utils.ts CHANGED
@@ -17,24 +17,6 @@ export const $wht = process.stdout.isTTY ? '\u001b[1;38;5;255m' : '' // full-bri
17
17
  export const $tsk = process.stdout.isTTY ? '\u001b[38;5;141m' : '' // the color for tasks (purple)
18
18
 
19
19
 
20
- /* ========================================================================== *
21
- * PACKAGE VERSION *
22
- * ========================================================================== */
23
-
24
- export function version(): string {
25
- const debug = _util.debuglog('plug:cli')
26
-
27
- try {
28
- const thisFile = _url.fileURLToPath(import.meta.url)
29
- const packageFile = _path.resolve(thisFile, '..', '..', 'package.json')
30
- const packageData = _fs.readFileSync(packageFile, 'utf-8')
31
- return JSON.parse(packageData).version || '(unknown)'
32
- } catch (error) {
33
- debug('Error parsing version:', error)
34
- return '(error)'
35
- }
36
- }
37
-
38
20
  /* ========================================================================== *
39
21
  * TS LOADER FORCE TYPE *
40
22
  * ========================================================================== */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plugjs/plug",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "type": "commonjs",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
package/src/asserts.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  * BUILD FAILURES *
3
3
  * ========================================================================== */
4
4
 
5
+ import { githubAnnotation } from './logging/github'
6
+
5
7
  /** A symbol marking {@link BuildFailure} instances */
6
8
  const buildFailure = Symbol.for('plugjs:buildFailure')
7
9
 
@@ -18,6 +20,11 @@ export class BuildFailure extends Error {
18
20
  constructor(message?: string | undefined, errors: any[] = []) {
19
21
  super(message || '')
20
22
 
23
+ // Annotate this build failure in GitHub
24
+ if (message) {
25
+ githubAnnotation({ type: 'error', title: 'Build Failure' }, message)
26
+ }
27
+
21
28
  /* Basic error setup: stack and errors */
22
29
  Error.captureStackTrace(this, BuildFailure)
23
30
  if (errors.length) this.errors = Object.freeze([ ...errors ])
package/src/fork.ts CHANGED
@@ -62,10 +62,10 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
62
62
 
63
63
  /* Get _this_ filename to spawn */
64
64
  const script = requireFilename(__fileurl)
65
- context.log.debug('About to fork plug from', $p(script))
65
+ context.log.debug('About to fork plug from', $p(this._scriptFile))
66
66
 
67
67
  /* Environment variables */
68
- const env = { ...process.env, ...logOptions.forkEnv(context.taskName) }
68
+ const env = { ...process.env, ...logOptions.forkEnv(context.taskName, 4) }
69
69
 
70
70
  /* Check our args (reversed) to see if the last specifies `coverageDir` */
71
71
  for (let i = this._arguments.length - 1; i >= 0; i --) {
@@ -80,10 +80,15 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
80
80
 
81
81
  /* Run our script in a _separate_ process */
82
82
  const child = fork(script, {
83
- stdio: [ 'ignore', 'inherit', 'inherit', 'ipc' ],
83
+ stdio: [ 'ignore', 'inherit', 'inherit', 'ipc', 'pipe' ],
84
84
  env,
85
85
  })
86
86
 
87
+ /* Pipe child logs directly to the writer */
88
+ if (child.stdio[4]) {
89
+ child.stdio[4].on('data', (data) => logOptions.output.write(data))
90
+ }
91
+
87
92
  context.log.info('Running', $p(script), $gry(`(pid=${child.pid})`))
88
93
 
89
94
  /* Return a promise from the child process events */
@@ -97,7 +102,7 @@ export abstract class ForkingPlug implements Plug<PlugResult> {
97
102
  })
98
103
 
99
104
  child.on('message', (message: ForkResult) => {
100
- context.log.debug('Message from child process', message)
105
+ context.log.debug('Message from child process with PID', child.pid, message)
101
106
  response = message
102
107
  })
103
108
 
@@ -176,7 +181,7 @@ if ((process.argv[1] === requireFilename(__fileurl)) && (process.send)) {
176
181
 
177
182
  /* First of all, our plug context */
178
183
  const context = new Context(buildFile, taskName)
179
- context.log.debug('Message from parent process', message)
184
+ context.log.debug('Message from parent process for PID', process.pid, message)
180
185
 
181
186
  /* Contextualize this run, and go! */
182
187
  const result = runAsync(context, taskName, async () => {
package/src/helpers.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdtempSync } from 'node:fs'
1
+ import { mkdtempSync, readFileSync } from 'node:fs'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
 
@@ -207,3 +207,26 @@ export function exec(
207
207
  const { params, options } = parseOptions(args)
208
208
  return execChild(cmd, params, options, requireContext())
209
209
  }
210
+
211
+ /**
212
+ * Read and parse a JSON file, throwing an error if not found.
213
+ *
214
+ * @params file The JSON file to parse
215
+ */
216
+ export function parseJson(file: string): any {
217
+ const jsonFile = requireContext().resolve(file)
218
+ let jsonText: string
219
+ try {
220
+ jsonText = readFileSync(jsonFile, 'utf-8')
221
+ } catch (error: any) {
222
+ if (error.code === 'ENOENT') log.fail(`File ${$p(jsonFile)} not found`)
223
+ if (error.code === 'EACCES') log.fail(`File ${$p(jsonFile)} can not be accessed`)
224
+ log.fail(`Error reading ${$p(jsonFile)}`, error)
225
+ }
226
+
227
+ try {
228
+ return JSON.parse(jsonText)
229
+ } catch (error) {
230
+ log.fail(`Error parsing ${$p(jsonFile)}`, error)
231
+ }
232
+ }
@@ -77,10 +77,7 @@ export const emitColor: LogEmitter = (options: LogEmitterOptions, args: any[]):
77
77
 
78
78
  /* Write each individual line out */
79
79
  for (const line of message.split('\n')) {
80
- _output.write(zapSpinner)
81
- _output.write(linePrefix)
82
- _output.write(line)
83
- _output.write('\n')
80
+ _output.write(`${zapSpinner}${linePrefix}${line}\n`)
84
81
  }
85
82
  }
86
83
 
@@ -120,8 +117,6 @@ export const emitPlain: LogEmitter = (options: LogEmitterOptions, args: any[]):
120
117
 
121
118
  /* Write each individual line out */
122
119
  for (const line of message.split('\n')) {
123
- _output.write(linePrefix)
124
- _output.write(line)
125
- _output.write('\n')
120
+ _output.write(`${linePrefix}${line}\n`)
126
121
  }
127
122
  }
@@ -0,0 +1,71 @@
1
+ import { EOL } from 'node:os'
2
+ import { formatWithOptions } from 'node:util'
3
+
4
+ import { logOptions } from './options'
5
+
6
+ import type { AbsolutePath } from '../paths'
7
+
8
+
9
+ /* Initial values, and subscribe to changes */
10
+ let _colors = logOptions.colors
11
+ let _output = logOptions.output
12
+ let _inspectOptions = logOptions.inspectOptions
13
+ let _githubAnnotations = logOptions.githubAnnotations
14
+ logOptions.on('changed', (options) => {
15
+ _colors = options.colors
16
+ _output = options.output
17
+ _githubAnnotations = options.githubAnnotations
18
+ _inspectOptions = { ...options.inspectOptions, breakLength: Infinity }
19
+ })
20
+
21
+
22
+ function escapeData(data: string): string {
23
+ return data
24
+ .replace(/%/g, '%25')
25
+ .replace(/\r/g, '%0D')
26
+ .replace(/\n/g, '%0A')
27
+ }
28
+
29
+ function escapeProp(prop: string | number): string {
30
+ return `${prop}`
31
+ .replace(/%/g, '%25')
32
+ .replace(/\r/g, '%0D')
33
+ .replace(/\n/g, '%0A')
34
+ .replace(/:/g, '%3A')
35
+ .replace(/,/g, '%2C')
36
+ }
37
+
38
+ export type GithubAnnotationType = 'warning' | 'error'
39
+
40
+ export interface GithubAnnotationOptions {
41
+ type: GithubAnnotationType
42
+ title?: string
43
+ file?: AbsolutePath
44
+ line?: number
45
+ endLine?: number
46
+ col?: number
47
+ endColumn?: number
48
+ }
49
+
50
+ export function githubAnnotation(type: GithubAnnotationType, message: string, ...args: any[]): void
51
+ export function githubAnnotation(options: GithubAnnotationOptions, message: string, ...args: any[]): void
52
+
53
+ export function githubAnnotation(options: GithubAnnotationType | GithubAnnotationOptions, ...args: any[]): void {
54
+ if (_colors || (! _githubAnnotations)) return
55
+
56
+ if (typeof options === 'string') options = { type: options }
57
+ const { type, ...parameters } = options
58
+
59
+ const attributes = Object.entries(parameters)
60
+ .filter(([ key, value ]) => !!(key && value))
61
+ .map(([ key, value ]) => `${key}=${escapeProp(value)}`)
62
+ .join(',')
63
+
64
+ const msg = escapeData(formatWithOptions(_inspectOptions, ...args))
65
+
66
+ if (attributes) {
67
+ _output.write(`::${type} ${attributes}::${msg}${EOL}`)
68
+ } else {
69
+ _output.write(`::${type}::${msg}${EOL}`)
70
+ }
71
+ }
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from 'node:events'
2
+ import { Socket } from 'node:net'
2
3
 
3
4
  import { getLevelNumber, NOTICE } from './levels'
4
5
 
@@ -28,6 +29,8 @@ export interface LogOptions {
28
29
  showSources: boolean,
29
30
  /** The task name to be used by default if a task is not contextualized. */
30
31
  defaultTaskName: string,
32
+ /** Whether GitHub annotations are enabled or not. */
33
+ githubAnnotations: boolean,
31
34
  /** The options used by NodeJS for object inspection. */
32
35
  readonly inspectOptions: InspectOptions,
33
36
 
@@ -41,8 +44,13 @@ export interface LogOptions {
41
44
  /** Remove an event listener for the specified event. */
42
45
  off(eventName: 'changed', listener: (logOptions: this) => void): this;
43
46
 
44
- /** Return a record of environment variables for forking. */
45
- forkEnv(taskName?: string): Record<string, string>
47
+ /**
48
+ * Return a record of environment variables for forking.
49
+ *
50
+ * @param taskName The default task name of the forked process
51
+ * @param logFd A file descriptor where logs should be sinked to
52
+ */
53
+ forkEnv(taskName?: string, logFd?: number): Record<string, string>
46
54
  }
47
55
 
48
56
  /* ========================================================================== *
@@ -58,6 +66,7 @@ class LogOptionsImpl extends EventEmitter implements LogOptions {
58
66
  private _lineLength = (<NodeJS.WriteStream> this._output).columns || 80
59
67
  private _lineLengthSet = false // has line length been set manually?
60
68
  private _showSources = true // by default, always show source snippets
69
+ private _githubAnnotations = false // ultimately set by the constructor
61
70
  private _inspectOptions: InspectOptions = {}
62
71
  private _defaultTaskName = ''
63
72
  private _taskLength = 0
@@ -78,27 +87,34 @@ class LogOptionsImpl extends EventEmitter implements LogOptions {
78
87
  // Other values don't change the value of `options.colors`
79
88
  }
80
89
 
90
+ /* If the `GITHUB_ACTIONS` is `true` then enable annotations */
91
+ this._githubAnnotations = process.env.GITHUB_ACTIONS === 'true'
92
+
81
93
  /*
82
94
  * The `__LOG_OPTIONS` variable is a JSON-serialized `LogOptions` object
83
95
  * and it's processed _last_ as it's normally only created by fork below
84
96
  * and consumed by the `Exec` plug (which has no other way of communicating)
85
97
  */
86
- Object.assign(this, JSON.parse(process.env.__LOG_OPTIONS || '{}'))
98
+ const { fd, ...options } = JSON.parse(process.env.__LOG_OPTIONS || '{}')
99
+ if (fd) this.output = new Socket({ fd }).unref()
100
+ Object.assign(this, options)
87
101
  }
88
102
 
89
103
  private _notifyListeners(): void {
90
104
  super.emit('changed', this)
91
105
  }
92
106
 
93
- forkEnv(taskName?: string): Record<string, string> {
107
+ forkEnv(taskName?: string, fd?: number): Record<string, string> {
94
108
  return {
95
109
  __LOG_OPTIONS: JSON.stringify({
96
110
  level: this._level,
97
111
  colors: this._colors,
98
112
  lineLength: this._lineLength,
99
113
  taskLength: this._taskLength,
114
+ githubAnnotations: this.githubAnnotations,
100
115
  defaultTaskName: taskName || this._defaultTaskName,
101
116
  spinner: false, // forked spinner is always false
117
+ fd, // file descriptor for logs
102
118
  }),
103
119
  }
104
120
  }
@@ -189,6 +205,15 @@ class LogOptionsImpl extends EventEmitter implements LogOptions {
189
205
  this._notifyListeners()
190
206
  }
191
207
 
208
+ get githubAnnotations(): boolean {
209
+ return this._githubAnnotations
210
+ }
211
+
212
+ set githubAnnotations(githubAnnotations: boolean) {
213
+ this._githubAnnotations = githubAnnotations
214
+ this._notifyListeners()
215
+ }
216
+
192
217
  get inspectOptions(): InspectOptions {
193
218
  return {
194
219
  colors: this._colors,
@@ -1,13 +1,22 @@
1
1
  import { BuildFailure } from '../asserts'
2
2
  import { readFile } from '../fs'
3
3
  import { $blu, $cyn, $gry, $red, $und, $wht, $ylw } from './colors'
4
- import { ERROR, NOTICE, WARN } from './levels'
4
+ import { ERROR, logLevels, NOTICE, WARN } from './levels'
5
5
  import { logOptions } from './options'
6
+ import { githubAnnotation } from './github'
6
7
 
7
8
  import type { AbsolutePath } from '../paths'
8
9
  import type { LogEmitter } from './emit'
9
10
  import type { LogLevels } from './levels'
10
11
 
12
+ let _showSources = logOptions.showSources
13
+ let _githubAnnotations = logOptions.githubAnnotations
14
+ logOptions.on('changed', (options) => {
15
+ _showSources = options.showSources
16
+ _githubAnnotations = options.githubAnnotations
17
+ })
18
+
19
+
11
20
  /* ========================================================================== */
12
21
 
13
22
  /** Levels used in a {@link Report} */
@@ -267,12 +276,12 @@ export class ReportImpl implements Report {
267
276
 
268
277
 
269
278
  done(showSources?: boolean | undefined): void {
270
- if (showSources == null) showSources = logOptions.showSources
279
+ if (showSources == null) showSources = _showSources
271
280
  if (! this.empty) this._emit(showSources)
272
281
  if (this.errors) throw BuildFailure.fail()
273
282
  }
274
283
 
275
- private _emit(showSources: boolean): this {
284
+ private _emit(showSources: boolean): void {
276
285
  /* Counters for all we need to print nicely */
277
286
  let fPad = 0
278
287
  let aPad = 0
@@ -281,7 +290,7 @@ export class ReportImpl implements Report {
281
290
  let cPad = 0
282
291
 
283
292
  /* Skip report all together if empty! */
284
- if ((this._annotations.size === 0) && (this._records.size === 0)) return this
293
+ if ((this._annotations.size === 0) && (this._records.size === 0)) return
285
294
 
286
295
  /* This is GIANT: sort and convert our data for easy reporting */
287
296
  const entries = [ ...this._annotations.keys(), ...this._records.keys() ]
@@ -431,7 +440,32 @@ export class ReportImpl implements Report {
431
440
  this._emitter(options, [ '' ])
432
441
  }
433
442
 
434
- /* Done! */
435
- return this
443
+ /* Annotate in GitHub */
444
+ if (_githubAnnotations) {
445
+ for (const entry of entries) {
446
+ const file = entry.file === nul ? undefined : entry.file
447
+
448
+ for (const report of entry.records) {
449
+ const type: 'error' | 'warning' | null =
450
+ report.level === logLevels.ERROR ? 'error' :
451
+ report.level === logLevels.WARN ? 'warning' :
452
+ null
453
+
454
+ if (! type) continue
455
+
456
+ const title = `${this._title} (task "${this._task}")`
457
+ const col = report.column || undefined
458
+ const line = report.line || undefined
459
+ const endColumn =
460
+ report.column ?
461
+ report.length >= Number.MAX_SAFE_INTEGER ? undefined :
462
+ ((report.column + report.length) || undefined) :
463
+ undefined
464
+ const message = report.messages.join('\n')
465
+
466
+ githubAnnotation({ type, title, file, col, line, endColumn }, message)
467
+ }
468
+ }
469
+ }
436
470
  }
437
471
  }
package/src/logging.ts CHANGED
@@ -3,6 +3,7 @@ import { getLogger, type Log } from './logging/logger'
3
3
  import { setupSpinner } from './logging/spinner'
4
4
 
5
5
  export * from './logging/colors'
6
+ export * from './logging/github'
6
7
  export * from './logging/levels'
7
8
  export * from './logging/logger'
8
9
  export * from './logging/options'
package/src/utils/exec.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path'
2
- import reaadline from 'node:readline'
2
+ import readline from 'node:readline'
3
3
  import { fork as forkProcess, spawn as spawnProcess } from 'node:child_process'
4
4
 
5
5
  import { assert, BuildFailure } from '../asserts'
@@ -62,7 +62,7 @@ export async function execChild(
62
62
 
63
63
  // Build our environment variables record
64
64
  const PATH = childPaths.join(path.delimiter)
65
- const logForkEnv = logOptions.forkEnv(context.taskName)
65
+ const logForkEnv = logOptions.forkEnv(context.taskName, 4)
66
66
  const childEnv: Record<string, string> = { ...process.env, ...env, ...logForkEnv, PATH }
67
67
 
68
68
  // Instrument coverage directory if needed
@@ -71,32 +71,43 @@ export async function execChild(
71
71
  // Prepare the options for calling `spawn`
72
72
  const childOptions: SpawnOptions = {
73
73
  ...extraOptions,
74
- stdio: [ 'ignore', 'pipe', 'pipe' ],
74
+ stdio: [ 'ignore', 'pipe', 'pipe', 'ipc', 'pipe' ],
75
75
  cwd: childCwd,
76
76
  env: childEnv,
77
77
  shell,
78
78
  }
79
79
 
80
- // Add the 'ipc' channel to stdio options if forking
81
- if (fork) childOptions.stdio = [ 'ignore', 'pipe', 'pipe', 'ipc' ]
82
-
83
80
  // Spawn our subprocess and monitor its stdout/stderr
84
- context.log.info('Executing', [ cmd, ...args ])
85
- context.log.info('Execution options', childOptions)
81
+ context.log.info(fork ? 'Forking' : 'Executing', [ cmd, ...args ])
82
+ context.log.debug('Child process options', childOptions)
83
+
86
84
  const child = fork ?
87
85
  forkProcess(cmd, args, childOptions) :
88
86
  spawnProcess(cmd, args, childOptions)
89
87
 
90
- // Standard output to "notice"
91
- if (child.stdout) {
92
- const out = reaadline.createInterface(child.stdout)
93
- out.on('line', (line) => line ? context.log.notice(line) : context.log.notice('\u00a0'))
94
- }
95
-
96
- // Standard error to "warning"
97
- if (child.stderr) {
98
- const err = reaadline.createInterface(child.stderr)
99
- err.on('line', (line) => line ? context.log.warn(line) : context.log.warn('\u00a0'))
88
+ try {
89
+ context.log.info('Child process PID', child.pid)
90
+
91
+ // Standard output to "notice"
92
+ if (child.stdout) {
93
+ const out = readline.createInterface(child.stdout)
94
+ out.on('line', (line) => context.log.notice(line || '\u00a0'))
95
+ }
96
+
97
+ // Standard error to "warning"
98
+ if (child.stderr) {
99
+ const err = readline.createInterface(child.stderr)
100
+ err.on('line', (line) => context.log.warn(line ||'\u00a0'))
101
+ }
102
+
103
+ // Log output bypass
104
+ if (child.stdio[4]) {
105
+ child.stdio[4].on('data', (data) => logOptions.output.write(data))
106
+ }
107
+ } catch (error) {
108
+ // If something happens before returning our promise, kill the child...
109
+ child.kill()
110
+ throw error
100
111
  }
101
112
 
102
113
  // Return our promise from the spawn events