Skip to content

Commit

Permalink
feat: add preparse hook (#1005)
Browse files Browse the repository at this point in the history
* feat: add preparse hook

* fix: update this.argv after preparse hook
  • Loading branch information
mdonnalley committed Mar 22, 2024
1 parent f2b7f51 commit 80745c4
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 2 deletions.
10 changes: 9 additions & 1 deletion src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,15 @@ export abstract class Command {
flags: aggregateFlags<F, B>(options.flags, options.baseFlags, options.enableJsonFlag),
}

const results = await Parser.parse<F, B, A>(argv, opts)
const hookResult = await this.config.runHook('preparse', {argv: [...argv], options: opts})

// Since config.runHook will only run the hook for the root plugin, hookResult.successes will always have a length of 0 or 1
// But to be extra safe, we find the result that matches the root plugin.
const argvToParse = hookResult.successes?.length
? hookResult.successes.find((s) => s.plugin.root === Cache.getInstance().get('rootPlugin')?.root)?.result ?? argv
: argv
this.argv = [...argvToParse]
const results = await Parser.parse<F, B, A>(argvToParse, opts)
this.warnIfFlagDeprecated(results.flags ?? {})

return results
Expand Down
5 changes: 4 additions & 1 deletion src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const debug = Debug()

const _pjson = Cache.getInstance().get('@oclif/core')
const BASE = `${_pjson.name}@${_pjson.version}`
const ROOT_ONLY_HOOKS = new Set<keyof Hooks>(['preparse'])

function channelFromVersion(version: string) {
const m = version.match(/[^-]+(?:-([^.]+))?/)
Expand Down Expand Up @@ -530,7 +531,9 @@ export class Config implements IConfig {
failures: [],
successes: [],
} as Hook.Result<Hooks[T]['return']>
const promises = [...this.plugins.values()].map(async (p) => {

const plugins = ROOT_ONLY_HOOKS.has(event) ? [this.rootPlugin] : [...this.plugins.values()]
const promises = plugins.map(async (p) => {
const debug = require('debug')([this.bin, p.name, 'hooks', event].join(':'))
const context: Hook.Context = {
config: this,
Expand Down
8 changes: 8 additions & 0 deletions src/interfaces/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Command} from '../command'
import {Config} from './config'
import {Input, OutputFlags} from './parser'
import {Plugin} from './plugin'

interface HookMeta {
Expand Down Expand Up @@ -47,6 +48,13 @@ export interface Hooks {
}
return: void
}
preparse: {
options: {
argv: string[]
options: Input<OutputFlags<any>, OutputFlags<any>, OutputFlags<any>>
}
return: string[]
}
prerun: {
options: {Command: Command.Class; argv: string[]}
return: void
Expand Down
14 changes: 14 additions & 0 deletions test/parser/fixtures/preparse-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "preparse-plugin",
"private": true,
"files": [],
"engines": {
"node": ">=18.0.0"
},
"oclif": {
"commands": "./lib/commands",
"hooks": {
"preparse": "./lib/hooks/preparse.js"
}
}
}
64 changes: 64 additions & 0 deletions test/parser/fixtures/preparse-plugin/src/commands/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {Command, Flags, Interfaces} from '../../../../../../src'
import {BooleanFlag} from '../../../../../../src/interfaces'

type GroupAliasOption = {
flag: string
option?: string
}

function groupAliasFlag<T = boolean>(
options: Partial<BooleanFlag<T> & {groupAlias: GroupAliasOption[]}> = {},
): BooleanFlag<T> {
return {
parse: async (b, _) => b,
...options,
allowNo: Boolean(options.allowNo),
type: 'boolean',
} as BooleanFlag<T>
}

export default class Test extends Command {
static args = {}

static flags = {
burger: Flags.string({
char: 'b',
default: async () => 'double',
}),
combo: groupAliasFlag({
char: 'c',
groupAlias: [
{flag: 'burger'},
{flag: 'fries'},
{
flag: 'shake',
option: 'strawberry',
},
],
}),
shake: Flags.option({
options: ['chocolate', 'vanilla', 'strawberry'],
char: 's',
})(),
fries: Flags.boolean({
allowNo: true,
char: 'f',
}),
'flags-dir': Flags.directory(),
sauce: Flags.string({
multiple: true,
default: ['ketchup'],
}),
}

async run(): Promise<{
args: Interfaces.InferredArgs<typeof Test.args>
flags: Interfaces.InferredFlags<typeof Test.flags>
}> {
const {args, flags} = await this.parse(Test)
return {
args,
flags,
}
}
}
81 changes: 81 additions & 0 deletions test/parser/fixtures/preparse-plugin/src/hooks/preparse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {readFile, readdir} from 'node:fs/promises'
import {join, parse} from 'node:path'

import {Hook} from '../../../../../../src'

const hook: Hook<'preparse'> = async function ({argv, options}) {
const flagsToIgnore = new Set(
Object.entries(options.flags ?? {})
.filter(
([_, flagOptions]) =>
// don't ignore if flag can take multiple values
(flagOptions.type === 'option' && flagOptions.multiple !== true) || flagOptions.type === 'boolean',
)
.filter(
([flagName, flagOptions]) =>
// ignore if short char flag is present
argv.includes(`-${flagOptions.char}`) ||
// ignore if long flag is present
argv.includes(`--${flagName}`) ||
// ignore if --no- flag is present
(flagOptions.type === 'boolean' && flagOptions.allowNo && argv.includes(`--no-${flagName}`)),
)
.map(([flagName]) => flagName),
)

const groupAliasFlags = Object.fromEntries(
Object.entries(options.flags ?? {}).filter(
([_, flagOptions]) =>
// @ts-expect-error because the type isn't aware of the custom flag we made
flagOptions.groupAlias,
),
)

for (const [flagName, flagOptions] of Object.entries(groupAliasFlags)) {
const groupAliasFlagPresent = argv.includes(`--${flagName}`) || argv.includes(`-${flagOptions.char}`)

if (groupAliasFlagPresent) {
// @ts-expect-error because the type isn't aware of the custom flag we made
for (const groupAliasOption of flagOptions.groupAlias) {
if (flagsToIgnore.has(groupAliasOption.flag)) continue
argv.push(`--${groupAliasOption.flag}`)
if (groupAliasOption.option) argv.push(groupAliasOption.option)
if (typeof options.flags?.[groupAliasOption.flag].default === 'function') {
// eslint-disable-next-line no-await-in-loop
argv.push(await options.flags?.[groupAliasOption.flag].default())
continue
}

if (options.flags?.[groupAliasOption.flag].default) {
argv.push(options.flags?.[groupAliasOption.flag].default)
}
}
}
}

if (argv.includes('--flags-dir')) {
const flagsDir = argv[argv.indexOf('--flags-dir') + 1]
const filesInDir = await readdir(flagsDir)
const flagsToInsert = await Promise.all(
filesInDir
// ignore files that were provided as flags
.filter((f) => !flagsToIgnore.has(f))
.map(async (file) => {
const contents = await readFile(join(flagsDir, file), 'utf8')
const values = contents?.split('\n')
return [parse(file).name, values]
}),
)

for (const [flag, values] of flagsToInsert) {
for (const value of values) {
argv.push(`--${flag}`)
if (value) argv.push(value)
}
}
}

return argv
}

export default hook
7 changes: 7 additions & 0 deletions test/parser/fixtures/preparse-plugin/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"outDir": "./lib",
"rootDirs": ["./src"]
},
"include": ["./src/**/*"]
}
Loading

0 comments on commit 80745c4

Please sign in to comment.