Skip to content

Commit

Permalink
Refactor code-style
Browse files Browse the repository at this point in the history
*   Make some errors better
*   Make errors on invalid input better
*   Use `node:test`
*   Remove unneeded (dev-)dependencies
*   Add more tests
  • Loading branch information
wooorm committed Aug 21, 2023
1 parent 26f3ea8 commit 5546bb5
Show file tree
Hide file tree
Showing 19 changed files with 1,341 additions and 1,221 deletions.
150 changes: 80 additions & 70 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,61 @@
/**
* @typedef {import('unified-engine').Options} EngineOptions
* @typedef {import('unified-engine').Context} EngineContext
* @typedef {import('unified-engine').Callback} EngineCallback
* @typedef {import('./options.js').Options} Options
* @typedef {import('unified-engine').Context} EngineContext
*
* @typedef {import('./parse-argv.js').Options} Options
* @typedef {import('./parse-argv.js').State} State
*/

import assert from 'node:assert/strict'
import process from 'node:process'
import stream from 'node:stream'
import {fileURLToPath} from 'node:url'
import chalk from 'chalk'
import chokidar from 'chokidar'
import {engine} from 'unified-engine'
import {options} from './options.js'
import {parseArgv} from './parse-argv.js'

// Fake TTY stream.
const ttyStream = Object.assign(new stream.Readable(), {isTTY: true})

// Exit, lazily, with the correct exit status code.
let exitStatus = 0

process.on('exit', onexit)
const ttyStream = new stream.Readable()
// @ts-expect-error: TS doesn’t understand but that’s how Node streams work.
ttyStream.isTTY = true

// Handle uncaught errors, such as from unexpected async behaviour.
process.on('uncaughtException', fail)

/**
* Start the CLI.
*
* @param {Options} cliConfig
* @param {Options} options
* Configuration (required).
* @returns {undefined}
* Nothing.
*/
export function args(cliConfig) {
/** @type {EngineOptions & {help: boolean, helpMessage: string, watch: boolean, version: boolean}} */
let config
/** @type {chokidar.FSWatcher|undefined} */
export function args(options) {
/** @type {State} */
let state
/** @type {chokidar.FSWatcher | undefined} */
let watcher
/** @type {URL|boolean|string|undefined} */
/** @type {URL | boolean | string | undefined} */
let output

try {
// @ts-expect-error: Close enough.
config = options(process.argv.slice(2), cliConfig)
state = parseArgv(process.argv.slice(2), options)
} catch (error) {
const exception = /** @type {Error} */ (error)
return fail(exception, true)
return fail(exception)
}

if (config.help) {
if (state.args.help) {
process.stdout.write(
[
'Usage: ' + cliConfig.name + ' [options] [path | glob ...]',
'Usage: ' + options.name + ' [options] [path | glob ...]',
'',
' ' + cliConfig.description,
' ' + options.description,
'',
'Options:',
'',
config.helpMessage,
state.args.helpMessage,
''
].join('\n'),
noop
Expand All @@ -62,21 +64,20 @@ export function args(cliConfig) {
return
}

if (config.version) {
process.stdout.write(cliConfig.version + '\n', noop)

if (state.args.version) {
process.stdout.write(options.version + '\n', noop)
return
}

// Modify `config` for watching.
if (config.watch) {
output = config.output
// Modify `state` for watching.
if (state.args.watch) {
output = state.engine.output

// Do not read from stdin(4).
config.streamIn = ttyStream
state.engine.streamIn = ttyStream

// Do not write to stdout(4).
config.out = false
state.engine.out = false

process.stderr.write(
chalk.bold('Watching...') + ' (press CTRL+C to exit)\n',
Expand All @@ -85,8 +86,7 @@ export function args(cliConfig) {

// Prevent infinite loop if set to regeneration.
if (output === true) {
config.output = false

state.engine.output = false
process.stderr.write(
chalk.yellow('Note') + ': Ignoring `--output` until exit.\n',
noop
Expand All @@ -95,7 +95,7 @@ export function args(cliConfig) {
}

// Initial run.
engine(config, done)
engine(state.engine, done)

/**
* Handle complete run.
Expand All @@ -107,9 +107,9 @@ export function args(cliConfig) {
clean()
fail(error)
} else {
exitStatus = code || 0
if (typeof code === 'number') process.exitCode = code

if (config.watch && !watcher && context) {
if (state.args.watch && !watcher && context) {
subscribe(context)
}
}
Expand All @@ -118,6 +118,7 @@ export function args(cliConfig) {
// Clean the watcher.
function clean() {
if (watcher) {
process.removeListener('SIGINT', onsigint)
watcher.close()
watcher = undefined
}
Expand All @@ -127,58 +128,67 @@ export function args(cliConfig) {
* Subscribe a chokidar watcher to all processed files.
*
* @param {EngineContext} context
* Context.
* @returns {undefined}
* Nothing.
*/
function subscribe(context) {
/** @type {Array<string>} */
const urls = context.fileSet.origins
/* c8 ignore next 4 - this works, but it’s only used in `watch`, where we have one config for */
const cwd =
typeof state.engine.cwd === 'object'
? fileURLToPath(state.engine.cwd)
: state.engine.cwd

assert(typeof cwd === 'string', '`cwd` is defined')

watcher = chokidar
// @ts-expect-error: `fileSet` is available.
.watch(context.fileSet.origins, {cwd: config.cwd, ignoreInitial: true})
.watch(urls, {cwd, ignoreInitial: true})
.on('error', done)
.on('change', (filePath) => {
config.files = [filePath]
engine(config, done)
.on('change', function (filePath) {
state.engine.files = [filePath]
engine(state.engine, done)
})

process.on('SIGINT', onsigint)
}

/**
* Handle a SIGINT.
*/
function onsigint() {
// Hide the `^C` in terminal.
process.stderr.write('\n', noop)
/**
* Handle a SIGINT.
*/
function onsigint() {
// Hide the `^C` in terminal.
process.stderr.write('\n', noop)

clean()
clean()

// Do another process if `output` specified regeneration.
if (output === true) {
config.output = output
config.watch = false
engine(config, done)
}
// Do another process if `output` specified regeneration.
if (output === true) {
state.engine.output = output
state.args.watch = false
engine(state.engine, done)
}
}
}

/**
* Print an error, optionally with stack.
* Print an error to `stderr`, optionally with stack.
*
* @param {Error} error
* @param {boolean} [pretty=false]
* Error to print.
* @returns {undefined}
* Nothing.
*/
function fail(error, pretty) {
// Old versions of Node
/* c8 ignore next 1 */
const message = String((pretty ? error : error.stack) || error)

exitStatus = 1

process.stderr.write(message.trim() + '\n', noop)
}

function onexit() {
/* eslint-disable unicorn/no-process-exit */
process.exit(exitStatus)
/* eslint-enable unicorn/no-process-exit */
function fail(error) {
process.exitCode = 1
process.stderr.write(String(error.stack || error).trimEnd() + '\n', noop)
}

/**
* Do nothing.
*
* @returns {undefined}
* Nothing.
*/
function noop() {}

0 comments on commit 5546bb5

Please sign in to comment.