Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: convert log command #6324

Merged
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
23 changes: 23 additions & 0 deletions STYLEGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})),
})
```
41 changes: 27 additions & 14 deletions src/commands/logs/build.ts
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 })
})
}
40 changes: 20 additions & 20 deletions src/commands/logs/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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) {
Expand All @@ -23,51 +23,48 @@ function getLog(logData: { level: string; message: string }) {
logString += logData.level
break
}

return `${logString} ${logData.message}`
}

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

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`, { exit: true })
}

let selectedFunction
if (functionName) {
selectedFunction = functions.find((fn: any) => fn.n === functionName)
} else {
const { result } = await inquirer.prompt({
name: 'result',
type: 'list',
const result = await select({
Copy link
Contributor

Choose a reason for hiding this comment

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

I've managed to narrow down the failing tests to this select prompt but I have no idea why that would cause the timeouts. Digging further...

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah it's because we previously mocked the interaction for inquirer but we need to do the same here for the clack prompt

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
}

const { a: accountId, oid: functionId } = selectedFunction

const ws = getWebSocket('wss://socketeer.services.netlify.com/function/logs')

ws.on('open', () => {
ws.send(
JSON.stringify({
Expand All @@ -78,21 +75,24 @@ 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())) {
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', exit: true })
})
}
8 changes: 4 additions & 4 deletions src/commands/open/open-admin.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
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) => {
const { siteInfo } = command.netlify

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 })
}
8 changes: 4 additions & 4 deletions src/commands/open/open-site.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -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 })
}
5 changes: 3 additions & 2 deletions src/commands/open/open.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
10 changes: 8 additions & 2 deletions src/utils/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
36 changes: 21 additions & 15 deletions tests/integration/commands/logs/functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -72,6 +80,7 @@ describe('logs:function command', () => {
})

beforeEach(() => {
mockMessageLogCalls = 0
program = new BaseCommand('netlify')

createLogsFunctionCommand(program)
Expand Down Expand Up @@ -137,7 +146,6 @@ describe('logs:function command', () => {
on: spyOn,
send: spySend,
})
const spyLog = log as unknown as Mock<any, any>

const env = getEnvironmentVariables({ apiUrl })
Object.assign(process.env, env)
Expand All @@ -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 ({}) => {
Expand All @@ -169,8 +177,6 @@ describe('logs:function command', () => {
on: spyOn,
send: spySend,
})
const spyLog = log as unknown as Mock<any, any>

const env = getEnvironmentVariables({ apiUrl })
Object.assign(process.env, env)

Expand All @@ -189,6 +195,6 @@ describe('logs:function command', () => {
messageCallbackFunc(JSON.stringify(mockInfoData))
messageCallbackFunc(JSON.stringify(mockWarnData))

expect(spyLog).toHaveBeenCalledTimes(2)
expect(mockMessageLogCalls).toBe(2)
})
})
Loading