From 2f4df00d050f3ac07cf38b9ff670ab656748aba6 Mon Sep 17 00:00:00 2001 From: khen <30577427+khendrikse@users.noreply.github.com> Date: Wed, 17 Jan 2024 13:03:54 +0100 Subject: [PATCH 1/4] chore: convert logs:functions --- src/commands/logs/functions.ts | 42 ++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/commands/logs/functions.ts b/src/commands/logs/functions.ts index dc84063bee2..aa4c5ac15ac 100644 --- a/src/commands/logs/functions.ts +++ b/src/commands/logs/functions.ts @@ -1,7 +1,9 @@ +import process from 'process' + import { OptionValues } from 'commander' -import inquirer from 'inquirer' -import { chalk, log } from '../../utils/command-helpers.js' +import { chalk } from '../../utils/command-helpers.js' +import { NetlifyLog, intro, outro, select } from '../../utils/styles/index.js' import { getWebSocket } from '../../utils/websockets/index.js' import type BaseCommand from '../base-command.js' @@ -28,12 +30,13 @@ function getLog(logData: { level: string; message: string }) { } export const logsFunction = async (functionName: string | undefined, options: OptionValues, command: BaseCommand) => { + intro('logs:function') const client = command.netlify.api const { site } = command.netlify const { id: siteId } = site if (options.level && !options.level.every((level: string) => LOG_LEVELS_LIST.includes(level))) { - log(`Invalid log level. Choices are:${CLI_LOG_LEVEL_CHOICES_STRING}`) + NetlifyLog.warn(`Invalid log level. Choices are:${CLI_LOG_LEVEL_CHOICES_STRING}`) } const levelsToPrint = options.level || LOG_LEVELS_LIST @@ -41,26 +44,26 @@ export const logsFunction = async (functionName: string | undefined, options: Op const { functions = [] } = await client.searchSiteFunctions({ siteId }) if (functions.length === 0) { - log(`No functions found for the site`) - return + NetlifyLog.error(`No functions found for the site`) } let selectedFunction if (functionName) { - selectedFunction = functions.find((fn: any) => fn.n === functionName) + selectedFunction = functions.find((fn: { n: string }) => fn.n === functionName) } else { - const { result } = await inquirer.prompt({ - name: 'result', - type: 'list', + const result = await select({ message: 'Select a function', - choices: functions.map((fn: any) => fn.n), + maxItems: 7, + options: functions.map((fn: { n: string }) => ({ + value: fn.n, + })), }) - selectedFunction = functions.find((fn: any) => fn.n === result) + selectedFunction = functions.find((fn: { n: string }) => fn.n === result) } if (!selectedFunction) { - log(`Could not find function ${functionName}`) + NetlifyLog.error(`Could not find function ${functionName}`) return } @@ -84,15 +87,20 @@ export const logsFunction = async (functionName: string | undefined, options: Op if (!levelsToPrint.includes(logData.level.toLowerCase())) { return } - log(getLog(logData)) + NetlifyLog.message(getLog(logData)) }) ws.on('close', () => { - log('Connection closed') + NetlifyLog.info('Connection closed') + }) + + ws.on('error', (err: unknown) => { + NetlifyLog.error('Connection error', { exit: false }) + NetlifyLog.error(err) }) - ws.on('error', (err: any) => { - log('Connection error') - log(err) + process.on('SIGINT', () => { + outro({ message: 'Closing connection' }) + process.exit() }) } From 7fdf01fe3f66ce83f18c79b371fcce81b97eb267 Mon Sep 17 00:00:00 2001 From: khen <30577427+khendrikse@users.noreply.github.com> Date: Wed, 17 Jan 2024 16:42:00 +0100 Subject: [PATCH 2/4] chore: convert logs:build --- STYLEGUIDE.md | 23 +++++++++++++++++++ src/commands/logs/build.ts | 41 ++++++++++++++++++++++------------ src/commands/logs/functions.ts | 3 +-- src/utils/styles/index.ts | 10 +++++++-- 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 4bcb4ec80d8..ff25240cc5c 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -104,3 +104,26 @@ export const basicCommand = async () => { outro() } ``` + +## Asking for user input + +There are several helpers available for asking the user for input. They are all available in the +`'./utils/styles/index.js'` file. + +### select + +The `select` method allows you to ask the user to select an option from a list of options. You can pass a list of +options to the `select` method and it will return the selected option. + +```js +import { select } from '../../utils/styles/index.js' + +const result = await select({ + message: 'Select an option', + maxItems: 7, + options: list.map((thing) => ({ + label: thing.name, + value: thing.id, + })), +}) +``` diff --git a/src/commands/logs/build.ts b/src/commands/logs/build.ts index 502d1b97d92..a698ae8e759 100644 --- a/src/commands/logs/build.ts +++ b/src/commands/logs/build.ts @@ -1,11 +1,20 @@ +import process from 'process' + import type { OptionValues } from 'commander' -import inquirer from 'inquirer' -import { log, chalk } from '../../utils/command-helpers.js' +import { chalk } from '../../utils/command-helpers.js' +import { NetlifyLog, intro, outro, select } from '../../utils/styles/index.js' import { getWebSocket } from '../../utils/websockets/index.js' import type BaseCommand from '../base-command.js' -export function getName({ deploy, userId }: { deploy: any; userId: string }) { +type Deploy = { + id: string + user_id?: string + context?: string + review_id: string +} + +export function getName({ deploy, userId }: { deploy: Deploy; userId: string }) { let normalisedName = '' const isUserDeploy = deploy.user_id === userId @@ -16,7 +25,7 @@ export function getName({ deploy, userId }: { deploy: any; userId: string }) { case 'deploy-preview': { // Deploys via the CLI can have the `deploy-preview` context // but no review id because they don't come from a PR. - // + const id = deploy.review_id normalisedName = id ? `Deploy Preview #${id}` : 'Deploy Preview' break @@ -33,6 +42,7 @@ export function getName({ deploy, userId }: { deploy: any; userId: string }) { } export const logsBuild = async (options: OptionValues, command: BaseCommand) => { + intro('logs:deploy') await command.authenticate() const client = command.netlify.api const { site } = command.netlify @@ -42,23 +52,22 @@ export const logsBuild = async (options: OptionValues, command: BaseCommand) => const deploys = await client.listSiteDeploys({ siteId, state: 'building' }) if (deploys.length === 0) { - log('No active builds') - return + NetlifyLog.info('No active builds') + outro({ exit: true }) } let [deploy] = deploys if (deploys.length > 1) { - const { result } = await inquirer.prompt({ - name: 'result', - type: 'list', + const result = await select({ message: `Select a deploy\n\n${chalk.yellow('*')} indicates a deploy created by you`, - choices: deploys.map((dep: any) => ({ - name: getName({ deploy: dep, userId }), + maxItems: 7, + options: deploys.map((dep: Deploy) => ({ + label: getName({ deploy: dep, userId }), value: dep.id, })), }) - deploy = deploys.find((dep: any) => dep.id === result) + deploy = deploys.find((dep: Deploy) => dep.id === result) } const { id } = deploy @@ -71,7 +80,7 @@ export const logsBuild = async (options: OptionValues, command: BaseCommand) => ws.on('message', (data: string) => { const { message, section, type } = JSON.parse(data) - log(message) + NetlifyLog.message(message, { noSpacing: true }) if (type === 'report' && section === 'building') { // end of build @@ -80,6 +89,10 @@ export const logsBuild = async (options: OptionValues, command: BaseCommand) => }) ws.on('close', () => { - log('---') + outro({ message: 'Closing connection', exit: true }) + }) + + process.on('SIGINT', () => { + outro({ message: 'Closing connection', exit: true }) }) } diff --git a/src/commands/logs/functions.ts b/src/commands/logs/functions.ts index aa4c5ac15ac..8af873cefbf 100644 --- a/src/commands/logs/functions.ts +++ b/src/commands/logs/functions.ts @@ -100,7 +100,6 @@ export const logsFunction = async (functionName: string | undefined, options: Op }) process.on('SIGINT', () => { - outro({ message: 'Closing connection' }) - process.exit() + outro({ message: 'Closing connection', exit: true }) }) } diff --git a/src/utils/styles/index.ts b/src/utils/styles/index.ts index d7456453bf7..695fafbf37a 100644 --- a/src/utils/styles/index.ts +++ b/src/utils/styles/index.ts @@ -528,15 +528,21 @@ export type LogMessageOptions = { symbol?: string error?: boolean writeStream?: NodeJS.WriteStream + noSpacing?: boolean } export const NetlifyLog = { message: ( message = '', - { error = false, symbol = chalk.gray(symbols.BAR), writeStream = process.stdout }: LogMessageOptions = {}, + { + error = false, + noSpacing, + symbol = chalk.gray(symbols.BAR), + writeStream = process.stdout, + }: LogMessageOptions = {}, ) => { if (jsonOnly()) return - const parts = [`${chalk.gray(symbols.BAR)}`] + const parts = noSpacing ? [] : [`${chalk.gray(symbols.BAR)}`] if (message) { const [firstLine, ...lines] = message.split('\n') parts.push( From 2ed32c6b0f1279b563f42814b2b07ca35d9a66cc Mon Sep 17 00:00:00 2001 From: Karin Hendrikse <30577427+khendrikse@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:07:25 +0100 Subject: [PATCH 3/4] chore: convert open command (#6325) --- src/commands/open/open-admin.ts | 8 ++++---- src/commands/open/open-site.ts | 8 ++++---- src/commands/open/open.ts | 5 +++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/commands/open/open-admin.ts b/src/commands/open/open-admin.ts index 68690eb4cc4..3d681af36b8 100644 --- a/src/commands/open/open-admin.ts +++ b/src/commands/open/open-admin.ts @@ -1,7 +1,7 @@ import { OptionValues } from 'commander' -import { exit, log } from '../../utils/command-helpers.js' import openBrowser from '../../utils/open-browser.js' +import { NetlifyLog, outro } from '../../utils/styles/index.js' import BaseCommand from '../base-command.js' export const openAdmin = async (options: OptionValues, command: BaseCommand) => { @@ -9,9 +9,9 @@ export const openAdmin = async (options: OptionValues, command: BaseCommand) => await command.authenticate() - log(`Opening "${siteInfo.name}" site admin UI:`) - log(`> ${siteInfo.admin_url}`) + NetlifyLog.info(`Opening "${siteInfo.name}" site admin UI:`) + NetlifyLog.info(`> ${siteInfo.admin_url}`) await openBrowser({ url: siteInfo.admin_url }) - exit() + outro({ exit: true }) } diff --git a/src/commands/open/open-site.ts b/src/commands/open/open-site.ts index 46969c38831..4a982eeabf1 100644 --- a/src/commands/open/open-site.ts +++ b/src/commands/open/open-site.ts @@ -1,7 +1,7 @@ import { OptionValues } from 'commander' -import { exit, log } from '../../utils/command-helpers.js' import openBrowser from '../../utils/open-browser.js' +import { NetlifyLog, outro } from '../../utils/styles/index.js' import BaseCommand from '../base-command.js' export const openSite = async (options: OptionValues, command: BaseCommand) => { @@ -10,9 +10,9 @@ export const openSite = async (options: OptionValues, command: BaseCommand) => { await command.authenticate() const url = siteInfo.ssl_url || siteInfo.url - log(`Opening "${siteInfo.name}" site url:`) - log(`> ${url}`) + NetlifyLog.info(`Opening "${siteInfo.name}" site url:`) + NetlifyLog.info(`> ${url}`) await openBrowser({ url }) - exit() + outro({ exit: true }) } diff --git a/src/commands/open/open.ts b/src/commands/open/open.ts index 3d9d40a7391..9f6a5f194f0 100644 --- a/src/commands/open/open.ts +++ b/src/commands/open/open.ts @@ -1,14 +1,15 @@ import { OptionValues } from 'commander' -import { log } from '../../utils/command-helpers.js' +import { NetlifyLog, intro } from '../../utils/styles/index.js' import BaseCommand from '../base-command.js' import { openAdmin } from './open-admin.js' import { openSite } from './open-site.js' export const open = async (options: OptionValues, command: BaseCommand) => { + intro('open') if (!options.site || !options.admin) { - log(command.helpInformation()) + NetlifyLog.info(command.helpInformation()) } if (options.site) { From 78f160b063610f615c974bcc5cde320cf80fa575 Mon Sep 17 00:00:00 2001 From: Lewis Thorley Date: Thu, 18 Jan 2024 14:28:46 +0000 Subject: [PATCH 4/4] chore: fix log tests and mocks --- src/commands/logs/functions.ts | 17 +++------ .../commands/logs/functions.test.ts | 36 +++++++++++-------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/commands/logs/functions.ts b/src/commands/logs/functions.ts index 8af873cefbf..e6abc0f290c 100644 --- a/src/commands/logs/functions.ts +++ b/src/commands/logs/functions.ts @@ -1,14 +1,12 @@ -import process from 'process' - import { OptionValues } from 'commander' +import inquirer from 'inquirer' -import { chalk } from '../../utils/command-helpers.js' -import { NetlifyLog, intro, outro, select } from '../../utils/styles/index.js' +import { chalk, log } from '../../utils/command-helpers.js' import { getWebSocket } from '../../utils/websockets/index.js' import type BaseCommand from '../base-command.js' import { CLI_LOG_LEVEL_CHOICES_STRING, LOG_LEVELS, LOG_LEVELS_LIST } from './log-levels.js' - +import { NetlifyLog, intro, outro, select } from '../../utils/styles/index.js' function getLog(logData: { level: string; message: string }) { let logString = '' switch (logData.level) { @@ -25,7 +23,6 @@ function getLog(logData: { level: string; message: string }) { logString += logData.level break } - return `${logString} ${logData.message}` } @@ -44,12 +41,12 @@ export const logsFunction = async (functionName: string | undefined, options: Op const { functions = [] } = await client.searchSiteFunctions({ siteId }) if (functions.length === 0) { - NetlifyLog.error(`No functions found for the site`) + NetlifyLog.error(`No functions found for the site`, { exit: true }) } let selectedFunction if (functionName) { - selectedFunction = functions.find((fn: { n: string }) => fn.n === functionName) + selectedFunction = functions.find((fn: any) => fn.n === functionName) } else { const result = await select({ message: 'Select a function', @@ -66,11 +63,8 @@ export const logsFunction = async (functionName: string | undefined, options: Op NetlifyLog.error(`Could not find function ${functionName}`) return } - const { a: accountId, oid: functionId } = selectedFunction - const ws = getWebSocket('wss://socketeer.services.netlify.com/function/logs') - ws.on('open', () => { ws.send( JSON.stringify({ @@ -81,7 +75,6 @@ export const logsFunction = async (functionName: string | undefined, options: Op }), ) }) - ws.on('message', (data: string) => { const logData = JSON.parse(data) if (!levelsToPrint.includes(logData.level.toLowerCase())) { diff --git a/tests/integration/commands/logs/functions.test.ts b/tests/integration/commands/logs/functions.test.ts index 278dba7423f..54811bf48f8 100644 --- a/tests/integration/commands/logs/functions.test.ts +++ b/tests/integration/commands/logs/functions.test.ts @@ -3,29 +3,37 @@ import { Mock, afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createLogsFunctionCommand } from '../../../../src/commands/logs/index.js' import { LOG_LEVELS } from '../../../../src/commands/logs/log-levels.js' -import { log } from '../../../../src/utils/command-helpers.js' import { getWebSocket } from '../../../../src/utils/websockets/index.js' import { startMockApi } from '../../utils/mock-api-vitest.ts' import { getEnvironmentVariables } from '../../utils/mock-api.js' +let mockMessageLogCalls = 0 vi.mock('../../../../src/utils/websockets/index.js', () => ({ getWebSocket: vi.fn(), })) -vi.mock('../../../../src/utils/command-helpers.js', async () => { - const actual = await vi.importActual('../../../../src/utils/command-helpers.js') +vi.mock('../../../../src/utils/styles/index.js', async () => { + const actual = await vi.importActual('../../../../src/utils/styles/index.js') + const actualNetlifyLog = actual.NetlifyLog return { ...actual, - log: vi.fn(), + NetlifyLog: { + // @ts-expect-error - spread types may only be created from object types + ...actualNetlifyLog, + // We were unable to use spys here and referencing top level variables with vitest will not work + // if we want the mockMessageLog variable to be vi.fn so we have settled for this + message: () => mockMessageLogCalls++, + }, } }) -vi.mock('inquirer', () => ({ - default: { - prompt: vi.fn().mockResolvedValue({ result: 'cool-function' }), - registerPrompt: vi.fn(), - }, -})) +vi.mock('@clack/core', async () => { + const actual = await vi.importActual('@clack/core') + return { + ...actual, + SelectPrompt: vi.fn().mockReturnValue({ prompt: vi.fn().mockResolvedValue('cool-function') }), + } +}) const siteInfo = { admin_url: 'https://app.netlify.com/sites/site-name/overview', @@ -72,6 +80,7 @@ describe('logs:function command', () => { }) beforeEach(() => { + mockMessageLogCalls = 0 program = new BaseCommand('netlify') createLogsFunctionCommand(program) @@ -137,7 +146,6 @@ describe('logs:function command', () => { on: spyOn, send: spySend, }) - const spyLog = log as unknown as Mock const env = getEnvironmentVariables({ apiUrl }) Object.assign(process.env, env) @@ -157,7 +165,7 @@ describe('logs:function command', () => { messageCallbackFunc(JSON.stringify(mockInfoData)) messageCallbackFunc(JSON.stringify(mockWarnData)) - expect(spyLog).toHaveBeenCalledTimes(1) + expect(mockMessageLogCalls).toBe(1) }) test('should print all the log levels', async ({}) => { @@ -169,8 +177,6 @@ describe('logs:function command', () => { on: spyOn, send: spySend, }) - const spyLog = log as unknown as Mock - const env = getEnvironmentVariables({ apiUrl }) Object.assign(process.env, env) @@ -189,6 +195,6 @@ describe('logs:function command', () => { messageCallbackFunc(JSON.stringify(mockInfoData)) messageCallbackFunc(JSON.stringify(mockWarnData)) - expect(spyLog).toHaveBeenCalledTimes(2) + expect(mockMessageLogCalls).toBe(2) }) })