Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ Component,Origin,Licence,Copyright
@types/debug,dev,MIT,Copyright (c) Microsoft Corporation
@types/tiny-async-pool,dev,MIT,Copyright (c) Microsoft Corporation
@types/deep-extend,dev,MIT,Copyright Microsoft Corporation
@types/inquirer,dev,MIT,Copyright Microsoft Corporation
@types/jest,dev,MIT,Copyright Microsoft Corporation
@types/js-yaml,dev,MIT,Bart van der Schoor
@types/node,dev,MIT,Copyright Microsoft Corporation
Expand All @@ -45,8 +44,6 @@ form-data,import,MIT,Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.co
fuzzy,import,MIT,Copyright (c) 2012 Matt York
get-value,import,MIT,"Copyright (c) 2014-present, Jon Schlinkert."
glob,import,ISC,Copyright (c) Isaac Z. Schlueter and Contributors
inquirer,import,MIT,Copyright (c) 2012 Simon Boudrias
inquirer-checkbox-plus-prompt,import,MIT,Copyright (c) 2018 Mohammad Anas Fares
is-docker,import,MIT,Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
jest,dev,MIT,"Copyright (c) Facebook, Inc. and its affiliates."
js-yaml,import,MIT,Copyright (C) 2011-2015 by Vitaly Puzrin
Expand Down Expand Up @@ -88,6 +85,8 @@ jszip,import,MIT,Copyright (c) 2009-2016 Stuart Knightley and other contributors
@google-cloud/run,import,Apache-2.0,Copyright (c) 2023 Google LLC and other contributors
google-auth-library,import,Apache-2.0,Copyright (c) 2023 Google LLC and other contributors
@google-cloud/logging,import,Apache-2.0,Copyright (c) 2023 Google LLC and other contributors
@inquirer/core,import,MIT,Copyright (c) 2025 Simon Boudrias
@inquirer/prompts,import,MIT,Copyright (c) 2025 Simon Boudrias
jest-diff,import,MIT,"Copyright (c) Meta Platforms, Inc. and other contributors"
tsx,dev,MIT,Copyright (c) Hiroki Osame <hiroki.osame@gmail.com>
typescript-eslint,dev,MIT,Copyright (c) 2019 typescript-eslint and other contributors
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,12 @@
"knip": {
"ignoreDependencies": [
"@datadog/datadog-ci-plugin-*",
"@inquirer/prompts",
"@inquirer/core",
"@microsoft/eslint-formatter-sarif",
"dd-trace",
"glob",
"tsdown",
"syncpack"
]
},
Expand Down
4 changes: 2 additions & 2 deletions packages/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@
},
"dependencies": {
"@antfu/install-pkg": "1.1.0",
"@inquirer/core": "11.1.8",
"@inquirer/prompts": "8.4.1",
"@types/datadog-metrics": "0.6.1",
"async-retry": "1.3.3",
"chalk": "3.0.0",
Expand All @@ -127,7 +129,6 @@
"fast-xml-parser": "5.5.9",
"form-data": "4.0.4",
"glob": "13.0.6",
"inquirer": "8.2.7",
"is-docker": "4.0.0",
"jest-diff": "30.2.0",
"js-yaml": "4.1.1",
Expand All @@ -145,7 +146,6 @@
"@types/async-retry": "1.4.8",
"@types/debug": "4.1.12",
"@types/deep-extend": "0.4.31",
"@types/inquirer": "8.2.6",
"@types/js-yaml": "4.0.9",
"@types/semver": "7.7.1",
"@types/tiny-async-pool": "2.0.3"
Expand Down
31 changes: 20 additions & 11 deletions packages/base/src/helpers/__tests__/prompt.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
jest.mock('inquirer')
import {prompt} from 'inquirer'
jest.mock('../inquirer', () => ({
loadPrompts: jest.fn(),
}))

import {loadPrompts} from '../inquirer'
import {confirmationQuestion, requestConfirmation, requestFilePath} from '../prompt'

describe('prompt', () => {
const mockConfirm = jest.fn()
const mockInput = jest.fn()

beforeEach(() => {
jest.resetAllMocks()
;(loadPrompts as jest.Mock).mockResolvedValue({
confirm: mockConfirm,
input: mockInput,
})
})

describe('confirmationQuestion', () => {
test('returns question with provided message', async () => {
const message = 'Do you want to continue?'
const question = confirmationQuestion(message)
expect(await question.message).toBe(message)
expect(question.message).toBe(message)
})
})

describe('requestConfirmation', () => {
test('returns boolean when users responds to confirmation question', async () => {
;(prompt as any).mockImplementation(() =>
Promise.resolve({
confirmation: true,
})
)
mockConfirm.mockResolvedValue(true)

const confirmation = await requestConfirmation('Do you want to continue?')
expect(confirmation).toBe(true)
})

test('throws error when something unexpected happens while prompting', async () => {
;(prompt as any).mockImplementation(() => Promise.reject(new Error('Unexpected error')))
mockConfirm.mockRejectedValue(new Error('Unexpected error'))
let error
try {
await requestConfirmation('Do you wanna continue?')
Expand All @@ -42,14 +51,14 @@ describe('prompt', () => {
const mockFilePath = '/Users/username/project/test.ts'

test('returns the selected file path', async () => {
;(prompt as any).mockImplementation(() => Promise.resolve({filePath: mockFilePath}))
mockInput.mockResolvedValue(mockFilePath)

const selectedPath = await requestFilePath()
expect(mockFilePath).toBe(selectedPath)
})

test('throws error when something unexpected happens while prompting', async () => {
;(prompt as any).mockImplementation(() => Promise.reject(new Error('Unexpected error')))
mockInput.mockRejectedValue(new Error('Unexpected error'))
let error
try {
await requestFilePath()
Expand Down
110 changes: 110 additions & 0 deletions packages/base/src/helpers/inquirer.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any way we can avoid handwriting the types here? this feels like itll quickly become a problem to update in the future

Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
type PromptValidation<Value> = (value: Value) => boolean | string | Promise<boolean | string>

type Choice<Value> =
| Value
| {
checked?: boolean
description?: string
disabled?: boolean | string
name: string
short?: string
value: Value
}

export type CheckboxConfig<Value = string> = {
choices: readonly Choice<Value>[]
default?: readonly Value[]
message: string
pageSize?: number
validate?: PromptValidation<readonly Value[]>
}

export type ConfirmConfig = {
default?: boolean
message: string
}

export type InputConfig = {
default?: string
message: string
validate?: PromptValidation<string>
}

export type PasswordConfig = {
default?: string
mask?: boolean | string
message: string
validate?: PromptValidation<string>
}

export type SelectConfig<Value = string> = {
choices: readonly Choice<Value>[]
default?: Value
message: string
}

export type InquirerPrompts = {
checkbox: <Value = string>(config: CheckboxConfig<Value>) => Promise<Value[]>
confirm: (config: ConfirmConfig) => Promise<boolean>
input: (config: InputConfig) => Promise<string>
password: (config: PasswordConfig) => Promise<string>
select: <Value = string>(config: SelectConfig<Value>) => Promise<Value>
}

type InquirerPromptStatus = 'idle' | 'loading' | 'done'

type KeypressEvent = {
ctrl: boolean
name: string
shift: boolean
}

type Readline = {
clearLine: (dir: number) => void
line: string
write: (input: string) => void
}

type InquirerTheme = {
style: {
answer: (text: string) => string
error: (text: string) => string
highlight: (text: string) => string
message: (text: string, status: InquirerPromptStatus) => string
}
}

type PaginationConfig<Item> = {
active: number
items: readonly Item[]
loop?: boolean
pageSize: number
renderItem: (options: {index: number; isActive: boolean; item: Item}) => string
}

type PromptView<Value, Config> = (config: Config, done: (value: Value) => void) => string | [string, string | undefined]

type InquirerCore = {
createPrompt: <Value, Config>(view: PromptView<Value, Config>) => (config: Config) => Promise<Value>
isDownKey: (key: KeypressEvent) => boolean
isEnterKey: (key: KeypressEvent) => boolean
isSpaceKey: (key: KeypressEvent) => boolean
isUpKey: (key: KeypressEvent) => boolean
makeTheme: <Theme>(defaultTheme: Theme, customTheme?: unknown) => Theme & InquirerTheme
useEffect: (effect: (readline: Readline) => void | (() => void), dependencies?: readonly unknown[]) => void
useKeypress: (handler: (key: KeypressEvent, readline: Readline) => void | Promise<void>) => void
useMemo: <Value>(value: () => Value, dependencies: readonly unknown[]) => Value
usePagination: <Item>(config: PaginationConfig<Item>) => string
usePrefix: (options: {status: InquirerPromptStatus; theme?: unknown}) => string
useState: <Value>(value: Value) => [Value, (value: Value) => void]
}

// eslint-disable-next-line @typescript-eslint/no-implied-eval -- TypeScript rewrites plain `import()` to `require()` in our CommonJS emit.
const importInquirerModule = new Function('specifier', 'return import(specifier)') as (
specifier: string
) => Promise<unknown>

// Preserve a real runtime dynamic import so Node can load the ESM-only prompt package from CommonJS output.
export const loadPrompts = () => importInquirerModule('@inquirer/prompts') as Promise<InquirerPrompts>

export const loadCore = () => importInquirerModule('@inquirer/core') as Promise<InquirerCore>
Comment on lines +102 to +110

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed because we emit commonjs and the new inquirer package is esm only? if so, I'd love to discuss with the guild what it would take to migrate our package from commonjs to esm since it seems like that's the future of the ecosystem

23 changes: 9 additions & 14 deletions packages/base/src/helpers/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,20 @@
* @file Functions used to prompt the user for input.
*/

import inquirer from 'inquirer'
import type {ConfirmConfig, InputConfig} from './inquirer'

export const confirmationQuestion = (
message: string,
defaultValue = true
): inquirer.ConfirmQuestion<{confirmation: boolean}> => ({
import {loadPrompts} from './inquirer'

export const confirmationQuestion = (message: string, defaultValue = true): ConfirmConfig => ({
message,
name: 'confirmation',
type: 'confirm',
default: defaultValue,
})

export const requestConfirmation = async (message: string, defaultValue = true) => {
try {
const confirmationAnswer = await inquirer.prompt(confirmationQuestion(message, defaultValue))
const {confirm} = await loadPrompts()

return confirmationAnswer.confirmation
return await confirm(confirmationQuestion(message, defaultValue))
} catch (err) {
if (err instanceof Error) {
throw Error(`Couldn't receive confirmation. ${err.message}`)
Expand All @@ -29,14 +26,12 @@ export const requestConfirmation = async (message: string, defaultValue = true)

export const requestFilePath = async () => {
try {
const question: inquirer.InputQuestion<{filePath: string}> = {
const question: InputConfig = {
message: 'Please enter a file path, or press Enter to finish:',
name: 'filePath',
type: 'input',
}
const filePathAnswer = await inquirer.prompt([question])
const {input} = await loadPrompts()

return filePathAnswer.filePath
return await input(question)
} catch (err) {
if (err instanceof Error) {
throw Error(`Couldn't receive file path. ${err.message}`)
Expand Down
2 changes: 0 additions & 2 deletions packages/plugin-cloud-run/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@
"@google-cloud/run": "3.0.0",
"chalk": "3.0.0",
"google-auth-library": "10.2.1",
"inquirer": "8.2.7",
"inquirer-checkbox-plus-prompt": "1.4.2",
"ora": "5.4.1",
"upath": "2.0.1"
}
Expand Down
Loading
Loading