node-typescript-compiler
Version:
Exposes typescript compiler (tsc) as a node.js module
167 lines (140 loc) • 5.57 kB
JavaScript
import { EOL } from 'node:os'
import path from 'node:path'
import { spawn } from 'cross-spawn'
import tildify from 'tildify'
import _ from 'lodash'
const { flatten, map, split, isArray } = _ // 2022/03 lodash is still commonjs
import { EXECUTABLE, find_tsc } from './find-tsc.mjs'
import { LIB } from './consts.mjs'
import { create_logger_state, display_banner_if_1st_output } from './logger.mjs'
///////////////////////////////////////////////////////
const RADIX = EXECUTABLE
///////////////////////////////////////////////////////
export async function compile(tscOptions, files, options) {
tscOptions = tscOptions || {}
files = files || []
options = options || {}
options.verbose = Boolean(options.verbose)
let logger_state = create_logger_state(options.banner)
if (options.verbose) logger_state = display_banner_if_1st_output(logger_state)
return new Promise((resolve, reject) => {
let stdout = ''
let stderr = ''
let already_failed = false
function on_failure(reason, err) {
if (already_failed && !options.verbose)
return
stdout = stdout.trim()
stderr = stderr.trim()
const reason_from_output = (() => {
const src = stderr || stdout
const first_line = src.split(EOL)[0]
if (first_line && first_line.toLowerCase().includes('error'))
return first_line
return null
})()
err = err || new Error(`${reason_from_output || reason}`)
err.stdout = stdout
err.stderr = stderr
err.reason = reason
if (options.verbose) {
logger_state = display_banner_if_1st_output(logger_state)
console.error(`[${LIB}] ✖ Failure during tsc invocation: ${reason}`)
console.error(err)
}
logger_state = display_banner_if_1st_output(logger_state)
err.message = `[${LIB}@dir:${path.parse(process.cwd()).base}] ${err.message}`
reject(err)
already_failed = true
}
try {
const tsc_options_as_array = flatten(map(tscOptions, (value, key) => {
if (value === false)
return []
if (value === true)
return [ `--${key}` ]
if (isArray(value))
value = value.join(',')
return [ `--${key}`, value ]
}))
const spawn_params = tsc_options_as_array.concat(files)
// not returning due to complex "callback style" async code
find_tsc(function _display_banner_if_1st_output() {
logger_state = display_banner_if_1st_output(logger_state)
})
.then(tsc_executable_absolute_path => {
if (options.verbose) console.log(`[${LIB}] ✔ found a typescript compiler at this location: "${tsc_executable_absolute_path}" aka. "${tildify(tsc_executable_absolute_path)}"`)
if (options.verbose) console.log(`[${LIB}] ► now spawning the compilation command: "${[tsc_executable_absolute_path, ...spawn_params].join(' ')}"...\n`)
const spawn_options = {
env: process.env,
}
const spawn_instance = spawn(tsc_executable_absolute_path, spawn_params, spawn_options)
// listen to events
spawn_instance.on('error', err => {
on_failure('got event "err"', err)
})
spawn_instance.on('disconnect', () => {
logger_state = display_banner_if_1st_output(logger_state)
console.log(`[${LIB}] Spawn: got event "disconnect"`)
})
spawn_instance.on('exit', (code, signal) => {
// when receiving "exit", io streams may still be open
// we do nothing, another event "close" will follow.
if (code !== 0) {
logger_state = display_banner_if_1st_output(logger_state)
console.log(`[${LIB}] Spawn: got event "exit" with error code "${code}" & signal "${signal}"!`)
}
})
spawn_instance.on('close', (code, signal) => {
if (code === 0)
resolve(stdout)
else
on_failure(`got event "close" with error code "${code}" & signal "${signal}"`)
})
// for debug purpose only
spawn_instance.stdin.on('data', data => {
logger_state = display_banner_if_1st_output(logger_state)
console.log(`[${LIB}] got stdin event "data": "${data}"`)
})
// mandatory for correct error detection
spawn_instance.stdin.on('error', err => {
on_failure('got stdin event "error"', err)
})
spawn_instance.stdout.on('data', data => {
logger_state = display_banner_if_1st_output(logger_state)
split(data, EOL).forEach(line => {
if (!line.length) return // convenience for more compact output
if (line[0] === '/')
line = tildify(line) // convenience for readability if using --listFiles
if (line.slice(-35) === 'Starting incremental compilation...')
console.log('\n************************************')
console.log(RADIX + '> ' + line)
})
stdout += data
})
// mandatory for correct error detection
spawn_instance.stdout.on('error', err => {
on_failure('got stdout event "error"', err)
})
spawn_instance.stderr.on('data', data => {
logger_state = display_banner_if_1st_output(logger_state)
split(data, EOL).forEach(line => console.log(RADIX + '! ' + line))
stderr += data
})
// mandatory for correct error detection
spawn_instance.stderr.on('error', err => {
on_failure('got stderr event "error"', err)
})
})
.catch(err => on_failure('final catch', err)) // ugly but due to complex "callback style" async code
}
catch (err) {
on_failure(`unexpected global catch`, err)
}
})
.then(stdout => {
if (options.verbose) console.log(`[${LIB}] ✔ executed successfully.`)
return stdout
})
}
///////////////////////////////////////////////////////